Compare commits

...

4 commits

37 changed files with 1463 additions and 235 deletions

View file

@ -14,6 +14,7 @@
"ext-imagick": "*",
"ext-zlib": "*",
"ext-gmp": "*",
"markrogoyski/math-php": "2.*"
"markrogoyski/math-php": "2.*",
"kudm761/martinez-rueda-php": "^0.1.2"
}
}

53
composer.lock generated
View file

@ -4,8 +4,59 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c0f6ba8f3e0787ea4c8bdccdb45dd2ce",
"content-hash": "f628e429199d537ddecb7543bc12ad6e",
"packages": [
{
"name": "kudm761/martinez-rueda-php",
"version": "0.1.2",
"source": {
"type": "git",
"url": "https://github.com/BardoQi/polygon_utils.git",
"reference": "b55ba0520eedf9dda48e2874539df86b4abc94e4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/BardoQi/polygon_utils/zipball/b55ba0520eedf9dda48e2874539df86b4abc94e4",
"reference": "b55ba0520eedf9dda48e2874539df86b4abc94e4",
"shasum": ""
},
"require": {
"php": ">=7.0"
},
"require-dev": {
"phpunit/phpunit": "5.5.*"
},
"type": "library",
"autoload": {
"psr-4": {
"MartinezRueda\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"authors": [
{
"name": "Dmitry Kubitsky",
"email": "kudm761@gmail.com",
"role": "Developer"
}
],
"description": "Martinez-Rueda algorithm for polygon boolean operations",
"keywords": [
"geography",
"martinez php",
"martinez polygon algorithm",
"polygon boolean operations",
"polygon clipping",
"polygon difference",
"polygon intersection",
"polygon union",
"polygon xor"
],
"support": {
"source": "https://github.com/BardoQi/polygon_utils/tree/0.1.2Release"
},
"time": "2020-06-11T07:53:13+00:00"
},
{
"name": "markrogoyski/math-php",
"version": "v2.5.0",

View file

@ -19,10 +19,10 @@ function outputFrame($frame, $endFrame, $frameDurationMs) {
new \swf2ass\ass\shadowTag(0),
\swf2ass\ass\fillColorTag::fromStyleRecord($path->style)
];
if($shape->edges[0] instanceof \swf2ass\MoveRecord){
$shape = (new \swf2ass\MatrixTransform(null, null, $shape->edges[0]->coord->multiply(-1)))->applyToShape($shape);
if($shape->getRecords()[0] instanceof \swf2ass\MoveRecord){
$shape = (new \swf2ass\MatrixTransform(null, null, $shape->getRecords()[0]->to->multiply(-1)))->applyToShape($shape);
}
$line->tags[] = new \swf2ass\ass\positionTag($shape->edges[0]->getStart()->divide(-\swf2ass\Constants::TWIP_SIZE), $shape->edges[0]->getStart()->divide(-\swf2ass\Constants::TWIP_SIZE), $endFrame, $endFrame);
$line->tags[] = new \swf2ass\ass\positionTag($shape->getRecords()[0]->getStart()->divide(-\swf2ass\Constants::TWIP_SIZE), $shape->getRecords()[0]->getStart()->divide(-\swf2ass\Constants::TWIP_SIZE), $endFrame, $endFrame);
$line->tags[] = new \swf2ass\ass\drawTag($shape, 1);
echo $line->encode($frameDurationMs) . PHP_EOL;
}
@ -171,13 +171,13 @@ ASSHEADER;
$pframeRender = (new \swf2ass\BitmapConverter($quantizedPframe))->render(false);
$pframePathCount = array_reduce($pframeRender->commands, function (int $item, \swf2ass\DrawPath $path){
return $item + count($path->commands->edges);
return $item + count($path->commands->getRecords());
}, 0);
if ($frameBuffer === null or $pframePathCount > 0) {
$iframeRender = (new \swf2ass\BitmapConverter($quantizedIframe))->render(true);
$iframePathCount = array_reduce($iframeRender->commands, function (int $item, \swf2ass\DrawPath $path){
return $item + count($path->commands->edges);
return $item + count($path->commands->getRecords());
}, 0);
}

View file

@ -29,7 +29,7 @@ class BitmapConverter {
if (count($currentPolygon) === 0) {
$currentPolygon[] = $n->start;
}
$currentPolygon[] = $n->coord;
$currentPolygon[] = $n->to;
}
}
if (count($currentPolygon) > 0) {
@ -73,7 +73,7 @@ class BitmapConverter {
$newPath[] = new MoveRecord($e->start, $lastCursor);
}
$newPath[] = $e;
$lastCursor = $e->coord;
$lastCursor = $e->to;
}
return $newPath;
@ -94,10 +94,10 @@ class BitmapConverter {
$cornerMap[$p->start->x . "_" . $p->start->y] = [];
}
$cornerMap[$p->start->x . "_" . $p->start->y][$i] = $p;
if (!isset($cornerMap[$p->coord->x . "_" . $p->coord->y])) {
$cornerMap[$p->coord->x . "_" . $p->coord->y] = [];
if (!isset($cornerMap[$p->to->x . "_" . $p->to->y])) {
$cornerMap[$p->to->x . "_" . $p->to->y] = [];
}
$cornerMap[$p->coord->x . "_" . $p->coord->y][$i] = $p;
$cornerMap[$p->to->x . "_" . $p->to->y][$i] = $p;
}
$index = 0;
@ -106,10 +106,10 @@ class BitmapConverter {
if ($currentPath !== null) {
$dupes = 0;
foreach ($cornerMap[$currentPath->start->x . "_" . $currentPath->start->y] as $i => $p) {
if ((($currentPath->start->equals($p->start) and $currentPath->coord->equals($p->coord)) or ($currentPath->start->equals($p->coord) and $currentPath->coord->equals($p->start)))) {
if ((($currentPath->start->equals($p->start) and $currentPath->to->equals($p->to)) or ($currentPath->start->equals($p->to) and $currentPath->to->equals($p->start)))) {
unset($path[$i]);
unset($cornerMap[$k1 = $p->start->x . "_" . $p->start->y][$i]);
unset($cornerMap[$k2 = $p->coord->x . "_" . $p->coord->y][$i]);
unset($cornerMap[$k2 = $p->to->x . "_" . $p->to->y][$i]);
if ($currentPath !== $p) {
++$dupes;
}
@ -135,10 +135,10 @@ class BitmapConverter {
$cornerMap[$p->start->x . "_" . $p->start->y] = [];
}
$cornerMap[$p->start->x . "_" . $p->start->y][$i] = $p;
if (!isset($cornerMap[$p->coord->x . "_" . $p->coord->y])) {
$cornerMap[$p->coord->x . "_" . $p->coord->y] = [];
if (!isset($cornerMap[$p->to->x . "_" . $p->to->y])) {
$cornerMap[$p->to->x . "_" . $p->to->y] = [];
}
$cornerMap[$p->coord->x . "_" . $p->coord->y][$i] = $p;
$cornerMap[$p->to->x . "_" . $p->to->y][$i] = $p;
}
$sortedPath = [];
@ -149,21 +149,21 @@ class BitmapConverter {
$currentPath = $newPath[$index++] ?? null;
if ($currentPath !== null) {
$startPath = $currentPath;
$currentVector = $currentPath->coord->sub($currentPath->start);
$currentVector = $currentPath->to->sub($currentPath->start);
$sortedPath[] = $startPath;
unset($newPath[$index - 1]);
unset($cornerMap[$startPath->start->x . "_" . $startPath->start->y][$index - 1]);
unset($cornerMap[$startPath->coord->x . "_" . $startPath->coord->y][$index - 1]);
unset($cornerMap[$startPath->to->x . "_" . $startPath->to->y][$index - 1]);
while (($nextPath = self::findNextCorner($currentPath->coord, $newPath, $cornerMap)) !== null) {
$nextVector = $nextPath->coord->sub($nextPath->start);
while (($nextPath = self::findNextCorner($currentPath->to, $newPath, $cornerMap)) !== null) {
$nextVector = $nextPath->to->sub($nextPath->start);
if ($nextVector->equals($currentVector)) { //Enlongate
$currentPath->coord = $nextPath->coord;
$currentPath->to = $nextPath->to;
continue;
}
$sortedPath[] = $nextPath;
if ($nextPath->coord->equals($nextPath->start)) { //Reached the end!
if ($nextPath->to->equals($nextPath->start)) { //Reached the end!
$startPath = null;
break;
}
@ -187,10 +187,10 @@ class BitmapConverter {
foreach ($cornerMap[$corner->x . "_" . $corner->y] as $i => $p) {
unset($path[$i]);
unset($cornerMap[$p->start->x . "_" . $p->start->y][$i]);
unset($cornerMap[$p->coord->x . "_" . $p->coord->y][$i]);
unset($cornerMap[$p->to->x . "_" . $p->to->y][$i]);
if ($p->start->equals($corner)) {
return $p;
} else if ($p->coord->equals($corner)) {
} else if ($p->to->equals($corner)) {
return $p->reverse();
}
}
@ -204,8 +204,8 @@ class BitmapConverter {
/*if(count($path) > 0){
$first = reset($path);
if($first instanceof MoveRecord or $first instanceof LineRecord){
$bb->topLeft = $first->coord;
$bb->bottomRight = $first->coord;
$bb->topLeft = $first->to;
$bb->bottomRight = $first->to;
}
}*/
foreach ($path as $i => $e) {
@ -214,11 +214,11 @@ class BitmapConverter {
continue;
}
if (!$bb->inBounds($e->coord)) {
$bb->topLeft->x = min($e->coord->x, $bb->topLeft->x);
$bb->topLeft->y = min($e->coord->y, $bb->topLeft->y);
$bb->bottomRight->x = max($e->coord->x, $bb->bottomRight->x);
$bb->bottomRight->y = max($e->coord->y, $bb->bottomRight->y);
if (!$bb->inBounds($e->to)) {
$bb->topLeft->x = min($e->to->x, $bb->topLeft->x);
$bb->topLeft->y = min($e->to->y, $bb->topLeft->y);
$bb->bottomRight->x = max($e->to->x, $bb->bottomRight->x);
$bb->bottomRight->y = max($e->to->y, $bb->bottomRight->y);
}
if (!$bb->inBounds($e->start)) {
$bb->topLeft->x = min($e->start->x, $bb->topLeft->x);
@ -282,9 +282,9 @@ class BitmapConverter {
foreach ($path as $edge) {
if ($edge instanceof MoveRecord) {
$edges[] = new MoveRecord($edge->coord->multiply(Constants::TWIP_SIZE), $edge->start->multiply(Constants::TWIP_SIZE));
$edges[] = new MoveRecord($edge->to->multiply(Constants::TWIP_SIZE), $edge->start->multiply(Constants::TWIP_SIZE));
} else if ($edge instanceof LineRecord) {
$edges[] = new LineRecord($edge->coord->multiply(Constants::TWIP_SIZE), $edge->start->multiply(Constants::TWIP_SIZE));
$edges[] = new LineRecord($edge->to->multiply(Constants::TWIP_SIZE), $edge->start->multiply(Constants::TWIP_SIZE));
} else {
var_dump($edge);
throw new \Exception();

60
src/ClipPath.php Normal file
View file

@ -0,0 +1,60 @@
<?php
namespace swf2ass;
class ClipPath {
/** @var Shape[] */
public array $shapes;
/**
* @param Shape[] $shapes
*/
public function __construct(array $shapes = []){
$this->shapes = $shapes;
}
public function getShape() : Shape{
$shape = new Shape();
foreach ($this->shapes as $s){
$shape = $shape->merge($s);
}
return $shape;
}
public function addShape(Shape $shape){
$this->shapes[] = $shape;
}
/**
* Calculates the intersection between two ClipPath.
* Shapes part of the clips need to be flat (or they will be flattened)
*
* @param ClipPath $other
* @return ClipPath
*/
public function intersect(ClipPath $other) : ClipPath{
$shapes = $this->shapes;
foreach ($other->shapes as $o) {
$n = [];
foreach ($shapes as $shape){
$n = array_merge($n, $shape->intersect($o));
}
$shapes = $n;
}
return new ClipPath($shapes);
}
public function applyMatrixTransform(MatrixTransform $transform, bool $applyTranslation = true) : ClipPath{
$shapes = [];
foreach ($this->shapes as $shape){
$shapes[] = $transform->applyToShape($shape, $applyTranslation);
}
return new ClipPath($shapes);
}
}

View file

@ -4,5 +4,5 @@ namespace swf2ass;
abstract class Constants {
const TWIP_SIZE = 20;
const EPSILON = 0.000001; //TODO: maybe change to PHP_FLOAT_EPSILON
const EPSILON = PHP_FLOAT_EPSILON;
}

View file

@ -2,7 +2,31 @@
namespace swf2ass;
/*
* Contains adapted code from http://antigrain.com/research/adaptive_bezier/index.html
* Anti-Grain Geometry (AGG) - Version 2.5
* A high quality rendering engine for C++
* Copyright (C) 2002-2006 Maxim Shemanarev
* Contact: mcseem@antigrain.com
* mcseemagg@yahoo.com
* http://antigrain.com
*
* AGG is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* AGG is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
class CubicCurveRecord implements Record {
private const RECURSION_LIMIT = 32;
private const CURVE_COLLINEARITY_EPSILON = PHP_FLOAT_EPSILON; //1E-30?
private const CURVE_ANGLE_TOLERANCE_EPSILON = 0.01;
public Vector2 $start;
public Vector2 $control1;
public Vector2 $control2;
@ -19,6 +43,10 @@ class CubicCurveRecord implements Record {
return $this->start;
}
public function getEnd(): Vector2 {
return $this->anchor;
}
public function reverse(): CubicCurveRecord {
return new CubicCurveRecord($this->control2, $this->control1, $this->start, $this->anchor);
}
@ -36,6 +64,264 @@ class CubicCurveRecord implements Record {
);
}
/**
* @return LineRecord[]
*/
public function toLineRecords(float $scale = 1.0) : array{
$points = [];
$distance_tolerance_square = 0.5 / $scale;
$distance_tolerance_square *= $distance_tolerance_square;
self::recursive_bezier($points, 0.0, 0.0, $distance_tolerance_square, $this->start, $this->control1, $this->control2, $this->anchor, 0);
$points[] = $this->anchor;
$result = [];
$current = $this->start;
foreach ($points as $point){
$result[] = new LineRecord($point, $current);
$current = $point;
}
return $result;
}
/**
* @param Vector2[] $points
* @param float $angle_tolerance
* @param float $distance_tolerance_square
* @param Vector2 $v1
* @param Vector2 $v2
* @param Vector2 $v3
* @param int $level
*/
private static function recursive_bezier(array &$points, float $cusp_limit, float $angle_tolerance, float $distance_tolerance_square, Vector2 $v1, Vector2 $v2, Vector2 $v3, Vector2 $v4, int $level){
if($level > self::RECURSION_LIMIT){
return;
}
// Calculate all the mid-points of the line segments
//----------------------
$x12 = ( $v1->x + $v2->x) / 2;
$y12 = ( $v1->y + $v2->y) / 2;
$x23 = ( $v2->x + $v3->x) / 2;
$y23 = ( $v2->y + $v3->y) / 2;
$x34 = ( $v3->x + $v4->x) / 2;
$y34 = ( $v3->y + $v4->y) / 2;
$x123 = ( $x12 + $x23) / 2;
$y123 = ( $y12 + $y23) / 2;
$x234 = ( $x23 + $x34) / 2;
$y234 = ( $y23 + $y34) / 2;
$x1234 = ( $x123 + $x234) / 2;
$y1234 = ( $y123 + $y234) / 2;
// Try to approximate the full cubic curve by a single straight line
//------------------
$dx = $v4->x- $v1->x;
$dy = $v4->y- $v1->y;
$d2 = abs((( $v2->x - $v4->x) * $dy - ( $v2->y - $v4->y) * $dx));
$d3 = abs((( $v3->x - $v4->x) * $dy - ( $v3->y - $v4->y) * $dx));
$da1 = $da2 = $k = null;
switch(((int)( $d2 > self::CURVE_COLLINEARITY_EPSILON) << 1) +
(int)( $d3 > self::CURVE_COLLINEARITY_EPSILON))
{
case 0:
// All collinear OR p1==p4
//----------------------
$k = $dx* $dx + $dy* $dy;
if($k == 0)
{
$d2 = $v1->distanceSquare($v2);
$d3 = $v4->distanceSquare($v3);
}
else
{
$k = 1 / $k;
$da1 = $v2->x - $v1->x;
$da2 = $v2->y - $v1->y;
$d2 = $k * ( $da1* $dx + $da2* $dy);
$da1 = $v3->x - $v1->x;
$da2 = $v3->y - $v1->y;
$d3 = $k * ( $da1* $dx + $da2* $dy);
if( $d2 > 0 && $d2 < 1 && $d3 > 0 && $d3 < 1)
{
// Simple collinear case, 1---2---3---4
// We can leave just two endpoints
return;
}
if( $d2 <= 0){
$d2 = $v2->distanceSquare($v1);
}
else if( $d2 >= 1) {
$d2 = $v2->distanceSquare($v4);
}
else {
$d2 = $v2->distanceSquare($v1->add(new Vector2($d2 * $dx, $d2 * $dy)));
}
if( $d3 <= 0) {
$d3 = $v3->distanceSquare($v1);
}
else if( $d3 >= 1) {
$d3 = $v3->distanceSquare($v4);
}
else {
$d3 = $v3->distanceSquare($v1->add(new Vector2($d2 * $dx, $d2 * $dy)));
}
}
if( $d2 > $d3)
{
if( $d2 < $distance_tolerance_square)
{
$points[] = $v2;
return;
}
}
else
{
if( $d3 < $distance_tolerance_square)
{
$points[] = $v3;
return;
}
}
break;
case 1:
// p1,p2,p4 are collinear, p3 is significant
//----------------------
if( $d3 * $d3 <= $distance_tolerance_square * ( $dx* $dx + $dy* $dy))
{
if($angle_tolerance < self::CURVE_ANGLE_TOLERANCE_EPSILON)
{
$points[] = new Vector2($x23, $y23);
return;
}
// Angle Condition
//----------------------
$da1 = abs(atan2( $v4->y - $v3->y, $v4->x - $v3->x) - atan2( $v3->y - $v2->y, $v3->x - $v2->x));
if( $da1 >= M_PI) {
$da1 = 2 * M_PI - $da1;
}
if( $da1 < $angle_tolerance)
{
$points[] = $v2;
$points[] = $v3;
return;
}
if($cusp_limit != 0.0)
{
if( $da1 > $cusp_limit)
{
$points[] = $v3;
return;
}
}
}
break;
case 2:
// p1,p3,p4 are collinear, p2 is significant
//----------------------
if( $d2 * $d2 <= $distance_tolerance_square * ( $dx* $dx + $dy* $dy))
{
if($angle_tolerance < self::CURVE_ANGLE_TOLERANCE_EPSILON)
{
$points[] = new Vector2($x23, $y23);
return;
}
// Angle Condition
//----------------------
$da1 = abs(atan2( $v3->y - $v2->y, $v3->x - $v2->x) - atan2( $v2->y - $v1->y, $v2->x - $v1->x));
if( $da1 >= M_PI) {
$da1 = 2 * M_PI - $da1;
}
if( $da1 < $angle_tolerance)
{
$points[] = $v2;
$points[] = $v3;
return;
}
if($cusp_limit != 0.0)
{
if( $da1 > $cusp_limit)
{
$points[] = $v2;
return;
}
}
}
break;
case 3:
// Regular case
//-----------------
if(( $d2 + $d3)*( $d2 + $d3) <= $distance_tolerance_square * ( $dx* $dx + $dy* $dy))
{
// If the curvature doesn't exceed the distance_tolerance value
// we tend to finish subdivisions.
//----------------------
if($angle_tolerance < self::CURVE_ANGLE_TOLERANCE_EPSILON)
{
$points[] = new Vector2($x23, $y23);
return;
}
// Angle & Cusp Condition
//----------------------
$k = atan2( $v3->y - $v2->y, $v3->x - $v2->x);
$da1 = abs($k - atan2( $v2->y - $v1->y, $v2->x - $v1->x));
$da2 = abs(atan2( $v4->y - $v3->y, $v4->x - $v3->x) - $k);
if( $da1 >= M_PI) {
$da1 = 2 * M_PI - $da1;
}
if( $da2 >= M_PI) {
$da2 = 2 * M_PI - $da2;
}
if( $da1 + $da2 < $angle_tolerance)
{
// Finally we can stop the recursion
//----------------------
$points[] = new Vector2($x23, $y23);
return;
}
if($cusp_limit != 0.0)
{
if( $da1 > $cusp_limit)
{
$points[] = $v2;
return;
}
if( $da2 > $cusp_limit)
{
$points[] = $v3;
return;
}
}
}
break;
}
// Continue subdivision
//----------------------
self::recursive_bezier( $points, $cusp_limit, $angle_tolerance, $distance_tolerance_square, $v1, new Vector2($x12, $y12), new Vector2($x123, $y123), new Vector2($x1234, $y1234),$level + 1);
self::recursive_bezier( $points, $cusp_limit, $angle_tolerance, $distance_tolerance_square, new Vector2($x1234, $y1234), new Vector2($x234, $y234), new Vector2($x34, $y34), $v4,$level + 1);
}
/**
* Finds if Cubic curve is a perfect fit of a Quadratic curve (aka, it was upconverted)
*
@ -50,4 +336,8 @@ class CubicCurveRecord implements Record {
return null;
}
public function equals(Record $other): bool {
return $other instanceof $this and $this->start->equals($other->start) and $this->control1->equals($other->control1) and $this->control2->equals($other->control2) and $this->anchor->equals($other->anchor);
}
}

View file

@ -23,6 +23,10 @@ class CubicSplineCurveRecord implements Record {
return $this->start;
}
public function getEnd(): Vector2 {
return $this->anchor;
}
public function reverse(): CubicSplineCurveRecord {
return new CubicSplineCurveRecord(array_reverse($this->control), $this->start, $this->anchor);
}
@ -36,6 +40,21 @@ class CubicSplineCurveRecord implements Record {
return new CubicSplineCurveRecord($control, $transform->applyToVector($this->anchor, $applyTranslation), $transform->applyToVector($this->start, $applyTranslation));
}
public function equals(Record $other): bool {
if($other instanceof $this and count($this->control) === count($other->control)){
foreach ($this->control as $i => $c) {
if(!$c->equals($other[$i])){
return false;
}
}
return $this->start->equals($other->start) and $this->anchor->equals($other->anchor);
}
return false;
}
public function append(Record $record): ?CubicSplineCurveRecord {
if ($record instanceof CubicCurveRecord) {

View file

@ -4,10 +4,10 @@ namespace swf2ass;
class LineRecord implements Record {
public Vector2 $start;
public Vector2 $coord;
public Vector2 $to;
public function __construct(Vector2 $coord, Vector2 $start) {
$this->coord = $coord;
public function __construct(Vector2 $to, Vector2 $start) {
$this->to = $to;
$this->start = $start;
}
@ -15,8 +15,49 @@ class LineRecord implements Record {
return $this->start;
}
public function getEnd(): Vector2 {
return $this->to;
}
public function reverse(): LineRecord {
return new LineRecord($this->start, $this->coord);
return new LineRecord($this->start, $this->to);
}
private function delta() : Vector2 {
return $this->to->sub($this->start);
}
private static function Fake2DCross(Vector2 $a, Vector2 $b){
return $a->x * $b->y - $a->y * $b->x;
}
/**
* @param LineRecord $other
* @return Vector2|LineRecord|null
*/
public function intersect(LineRecord $other) : ?object {
$p = $this->start;
$q = $other->start;
$r = $this->delta();
$s = $other->delta();
$denom = self::Fake2DCross($r, $s);
if(abs($denom) < Constants::EPSILON){
//parallel
return new LineRecord($this->to->add($this->to->sub($other->to)), $p->add($p->sub($q)));
}
$tNumer = self::Fake2DCross($q->sub($p), $s);
$uNumer = self::Fake2DCross($q->sub($p), $r);
$t = $tNumer / $denom;
$u = $uNumer / $denom;
if($t < 0 or $t > 1 or $u < 0 or $u > 1){
//No intersection
return null;
}
return $p->add($r->multiply($t));
}
public static function fromArray(array $element, Vector2 $cursor): LineRecord {
@ -24,6 +65,10 @@ class LineRecord implements Record {
}
public function applyMatrixTransform(MatrixTransform $transform, bool $applyTranslation = true): LineRecord {
return new LineRecord($transform->applyToVector($this->coord, $applyTranslation), $transform->applyToVector($this->start, $applyTranslation));
return new LineRecord($transform->applyToVector($this->to, $applyTranslation), $transform->applyToVector($this->start, $applyTranslation));
}
public function equals(Record $other): bool {
return $other instanceof $this and $this->start->equals($other->start) and $this->to->equals($other->to);
}
}

View file

@ -22,6 +22,7 @@ class MatrixTransform {
return new MatrixTransform($scale, null, null);
}
//TODO: check sin sign location
public static function rotate(float $angle) : MatrixTransform{
$cos = cos($angle);
$sin = sin($angle);
@ -36,26 +37,15 @@ class MatrixTransform {
return new MatrixTransform(null, null, null);
}
//TODO: skewX, skewY
public static function skewX(float $angle) : MatrixTransform{
return new MatrixTransform(null, new Vector2(tan($angle), 0), null);
}
public static function skewY(float $angle) : MatrixTransform{
return new MatrixTransform(null, new Vector2(0, tan($angle)), null);
}
public function combine(MatrixTransform $other): MatrixTransform {
/*
return new MatrixTransform(
new Vector2(
$this->get_a() * $other->get_a() + $this->get_c() * $other->get_b(),
$this->get_b() * $other->get_c() + $this->get_d() * $other->get_d(),
),
new Vector2(
$this->get_b() * $other->get_a() + $this->get_d() * $other->get_b(),
$this->get_a() * $other->get_c() + $this->get_c() * $other->get_d(),
),
new Vector2(
$this->get_a() * $other->get_e() + $this->get_c() * $other->get_f() + $this->get_e(),
$this->get_b() * $other->get_e() + $this->get_d() * $other->get_f() + $this->get_f()
)
);
*/
$result = clone $this;
$result->matrix = $other->matrix->multiply($this->matrix);
return $result;
@ -85,92 +75,6 @@ class MatrixTransform {
return $this->matrix->get(2, 1);
}
/**
* Do special decomposition that maps all values into scale(X,Y), rotation(), skew(X,0), translation()
* @return DecomposedMatrixTransform
* @throws \MathPHP\Exception\IncorrectTypeException
* @throws \MathPHP\Exception\MathException
* @throws \MathPHP\Exception\MatrixException
*/
public function decompose() : DecomposedMatrixTransform{
$result = new DecomposedMatrixTransform();
/*$rotationX = atan2($this->get_b(), $this->get_a());
$rotationY = atan2(-$this->get_c(), $this->get_d());
$result->rotation = $rotationX;
$result->skew = new Vector2($rotationY - $rotationX, 0);
$result->scale = new Vector2(sqrt($this->get_a() * $this->get_a() + $this->get_b() * $this->get_b()), $this->get_c() * $this->get_c() + $this->get_d() * $this->get_d());
$result->translation = $this->translation;
return $result;*/
$result->rotation = atan($this->get_c() / $this->get_d());
$scaleX = sqrt($this->get_a() * $this->get_a() + $this->get_b() * $this->get_b());
$scaleY = sqrt($this->get_c() * $this->get_c() + $this->get_d() * $this->get_d());
$result->scale = new Vector2($this->get_a(), $this->get_d());
$result->skew = new Vector2($this->get_c(), $this->get_b());
$result->translation = new Vector2($this->get_e(), $this->get_f());
return $result;
//TODO: all down here has to be shifted
$scaleX = sqrt($this->get_a() * $this->get_a() + $this->get_b() * $this->get_b());
$scaleY = sqrt($this->get_c() * $this->get_c() + $this->get_d() * $this->get_d());
$px = $this->applyToVector(new Vector2(0, 1), false);
$py = $this->applyToVector(new Vector2(1, 0), false);
$rad = -atan2($px->y, $px->x) - M_PI_2; //TODO: maybe -
$skewX = atan2($px->y, $px->x) - M_PI_2;
$skewY = atan2($py->y, $py->x);
var_dump($scaleX);
var_dump($scaleY);
$q1 = MatrixFactory::createNumeric([
[cos($rad), sin($rad)],
[-sin($rad), cos($rad)]
]);
$a = MatrixFactory::createNumeric([
[1, $this->get_b()],
[$this->get_c(), 1]
]);
$QR = $a->qrDecomposition();
[$q2, $r] = [$QR->Q, $QR->R];
$I = MatrixFactory::diagonal(array_map(function ($i){ //Calculate sign
return $i < 0 ? -1 : ($i === 0 ? 0 : 1);
}, $r->diagonal()->getDiagonalElements()));
$r = $I->multiply($r);
/*$r = $r->multiply(MatrixFactory::createNumeric([
[$this->get_a(), 0],
[0, $this->get_c()]
]));*/
$q2 = $q2->multiply($I);
$q = $q1->multiply($q2);
$result->rotation = -atan2($q->get(1, 0), $q->get(0, 0));
$result->skew = new Vector2($r->get(0, 1), 0);
$result->scale = new Vector2($r->get(0, 0), $r->get(1, 1));
$result->translation = $this->translation;
var_dump($r);
var_dump($result);
exit();
return $result;
}
public function getMatrix() : NumericMatrix{
return $this->matrix;
}
@ -190,8 +94,8 @@ class MatrixTransform {
public function applyToShape(Shape $shape, bool $applyTranslation = true): Shape {
$newShape = new Shape();
foreach ($shape->edges as $edge) {
$newShape->edges[] = $edge->applyMatrixTransform($this, $applyTranslation);
foreach ($shape->getRecords() as $edge) {
$newShape->addRecord($edge->applyMatrixTransform($this, $applyTranslation));
}
return $newShape;

View file

@ -4,10 +4,10 @@ namespace swf2ass;
class MoveRecord implements Record {
public Vector2 $start;
public Vector2 $coord;
public Vector2 $to;
public function __construct(Vector2 $coord, Vector2 $start) {
$this->coord = $coord;
public function __construct(Vector2 $to, Vector2 $start) {
$this->to = $to;
$this->start = $start;
}
@ -15,8 +15,12 @@ class MoveRecord implements Record {
return $this->start;
}
public function getEnd(): Vector2 {
return $this->to;
}
public function reverse(): MoveRecord {
return new MoveRecord($this->start, $this->coord);
return new MoveRecord($this->start, $this->to);
}
public static function fromArray(array $element, Vector2 $cursor): MoveRecord {
@ -24,6 +28,10 @@ class MoveRecord implements Record {
}
public function applyMatrixTransform(MatrixTransform $transform, bool $applyTranslation = true): MoveRecord {
return new MoveRecord($transform->applyToVector($this->coord, $applyTranslation), $transform->applyToVector($this->start, $applyTranslation));
return new MoveRecord($transform->applyToVector($this->to, $applyTranslation), $transform->applyToVector($this->start, $applyTranslation));
}
public function equals(Record $other): bool {
return $other instanceof $this and $this->start->equals($other->start) and $this->to->equals($other->to);
}
}

View file

@ -71,15 +71,11 @@ class PathSegment {
}
$shape = new Shape();
$first = reset($this->points);
$pos = new Vector2(0, 0);
$shape->edges[] = new MoveRecord($first->pos, $pos);
$pos = $first->pos;
$pos = reset($this->points)->pos;
while (($point = next($this->points)) !== false) {
if (!$point->is_bezier_control) {
$shape->edges[] = new LineRecord($point->pos, $pos);
$shape->addRecord(new LineRecord($point->pos, $pos));
$pos = $point->pos;
} else {
$end = next($this->points);
@ -87,7 +83,7 @@ class PathSegment {
throw new \Exception("Bezier without endpoint");
}
$shape->edges[] = new QuadraticCurveRecord($point->pos, $end->pos, $pos);
$shape->addRecord(new QuadraticCurveRecord($point->pos, $end->pos, $pos));
$pos = $end->pos;
}
}

View file

@ -39,7 +39,7 @@ class PendingPath {
public function getShape(): Shape {
$shape = new Shape();
foreach ($this->segments as $segment) {
$shape->edges = array_merge($shape->edges, $segment->getShape()->edges);
$shape = $shape->merge($segment->getShape());
}
return $shape;

View file

@ -2,7 +2,35 @@
namespace swf2ass;
use MathPHP\LinearAlgebra\Matrix;
use MathPHP\LinearAlgebra\MatrixFactory;
/*
* Contains adapted code from http://antigrain.com/research/adaptive_bezier/index.html
* Anti-Grain Geometry (AGG) - Version 2.5
* A high quality rendering engine for C++
* Copyright (C) 2002-2006 Maxim Shemanarev
* Contact: mcseem@antigrain.com
* mcseemagg@yahoo.com
* http://antigrain.com
*
* AGG is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* AGG is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
class QuadraticCurveRecord implements Record {
private const RECURSION_LIMIT = 32;
private const CURVE_COLLINEARITY_EPSILON = PHP_FLOAT_EPSILON; //1E-30?
private const CURVE_ANGLE_TOLERANCE_EPSILON = 0.01;
public Vector2 $start;
public Vector2 $control;
public Vector2 $anchor;
@ -17,6 +45,10 @@ class QuadraticCurveRecord implements Record {
return $this->start;
}
public function getEnd(): Vector2 {
return $this->anchor;
}
public function reverse(): QuadraticCurveRecord {
return new QuadraticCurveRecord($this->control, $this->start, $this->anchor);
}
@ -31,4 +63,127 @@ class QuadraticCurveRecord implements Record {
return new QuadraticCurveRecord($control, $anchor, $cursor);
}
/**
* @return LineRecord[]
*/
public function toLineRecords(float $scale = 1.0) : array{
$points = [];
$distance_tolerance_square = 0.5 / $scale;
$distance_tolerance_square *= $distance_tolerance_square;
self::recursive_bezier($points, 0.0, $distance_tolerance_square, $this->start, $this->control, $this->anchor, 0);
$points[] = $this->anchor;
$result = [];
$current = $this->start;
foreach ($points as $point){
$result[] = new LineRecord($point, $current);
$current = $point;
}
return $result;
}
/**
* @param Vector2[] $points
* @param float $angle_tolerance
* @param float $distance_tolerance_square
* @param Vector2 $v1
* @param Vector2 $v2
* @param Vector2 $v3
* @param int $level
*/
private static function recursive_bezier(array &$points, float $angle_tolerance, float $distance_tolerance_square, Vector2 $v1, Vector2 $v2, Vector2 $v3, int $level){
if($level > self::RECURSION_LIMIT){
return;
}
// Calculate all the mid-points of the line segments
//----------------------
$x12 = ( $v1->x + $v2->x) / 2;
$y12 = ( $v1->y + $v2->y) / 2;
$x23 = ( $v2->x + $v3->x) / 2;
$y23 = ( $v2->y + $v3->y) / 2;
$x123 = ( $x12 + $x23) / 2;
$y123 = ( $y12 + $y23) / 2;
$dx = $v3->x- $v1->x;
$dy = $v3->y- $v1->y;
$d = abs((( $v2->x - $v3->x) * $dy - ( $v2->y - $v3->y) * $dx));
if($d > self::CURVE_COLLINEARITY_EPSILON)
{
// Regular case
//-----------------
if($d * $d <= $distance_tolerance_square * ( $dx* $dx + $dy* $dy))
{
// If the curvature doesn't exceed the distance_tolerance value
// we tend to finish subdivisions.
//----------------------
if($angle_tolerance < self::CURVE_ANGLE_TOLERANCE_EPSILON)
{
$points[] = new Vector2($x123, $y123);
return;
}
// Angle & Cusp Condition
//----------------------
$da = abs(atan2( $v3->y - $v2->y, $v3->x - $v2->x) - atan2( $v2->y - $v1->y, $v2->x - $v1->x));
if($da >= M_PI){
$da = 2*M_PI - $da;
}
if($da < $angle_tolerance)
{
// Finally we can stop the recursion
//----------------------
$points[] = new Vector2($x123, $y123);
return;
}
}
}
else
{
// Collinear case
//------------------
$da = $dx*$dx + $dy*$dy;
if($da == 0)
{
$d = $v1->distanceSquare($v2);
}
else
{
$d = (($v2->x - $v1->x)*$dx + ($v2->y - $v1->y)*$dy) / $da;
if($d > 0 && $d < 1)
{
// Simple collinear case, 1---2---3
// We can leave just two endpoints
return;
}
if($d <= 0){
$d = $v2->distanceSquare($v1);
}else if($d >= 1){
$d = $v2->distanceSquare($v3);
}else{
$d = $v2->distanceSquare($v1->add(new Vector2($d * $dx, $d * $dy)));
}
}
if($d < $distance_tolerance_square)
{
$points[] = $v2;
return;
}
}
// Continue subdivision
//----------------------
self::recursive_bezier( $points, $angle_tolerance, $distance_tolerance_square, $v1, new Vector2($x12, $y12), new Vector2($x123, $y123), $level + 1);
self::recursive_bezier( $points, $angle_tolerance, $distance_tolerance_square, new Vector2($x123, $y123), new Vector2($x23, $y23), $v3, $level + 1);
}
public function equals(Record $other): bool {
return $other instanceof $this and $this->start->equals($other->start) and $this->control->equals($other->control) and $this->anchor->equals($other->anchor);
}
}

View file

@ -4,8 +4,11 @@ namespace swf2ass;
interface Record {
public function getStart(): Vector2;
public function getEnd(): Vector2;
public function reverse(): Record;
public function equals(Record $other): bool;
public function applyMatrixTransform(MatrixTransform $transform, bool $applyTranslation = true): Record;
}

View file

@ -8,11 +8,11 @@ class RenderedObject {
public array $depth;
public int $objectId;
public DrawPathList $drawPathList;
public ?Shape $clip;
public ?ClipPath $clip;
public ColorTransform $colorTransform;
public MatrixTransform $matrixTransform;
public function __construct(array $depth, int $objectId, DrawPathList $drawPathList, ColorTransform $colorTransform, MatrixTransform $matrixTransform, ?Shape $clip = null) {
public function __construct(array $depth, int $objectId, DrawPathList $drawPathList, ColorTransform $colorTransform, MatrixTransform $matrixTransform, ?ClipPath $clip = null) {
$this->depth = $depth;
$this->objectId = $objectId;
$this->drawPathList = $drawPathList;
@ -21,6 +21,10 @@ class RenderedObject {
$this->clip = $clip;
}
public function getShape() : Shape{
}
/**
* @return int[]
*/

View file

@ -20,6 +20,8 @@ class SWFProcessor extends SWFTreeProcessor {
private SWF $swf;
private int $expectedFrameCount;
public function __construct(SWF $swf) {
$this->swf = $swf;
parent::__construct(0, null);
@ -29,6 +31,11 @@ class SWFProcessor extends SWFTreeProcessor {
$this->viewPort = Rectangle::fromArray($this->swf->header["frameSize"]);
$this->frameRate = $this->swf->header["frameRate"];
$this->expectedFrameCount = $this->swf->header["frameCount"];
}
public function getExpectedFrameCount() : int {
return $this->expectedFrameCount;
}
protected function current(): ?array {

View file

@ -3,18 +3,166 @@
namespace swf2ass;
use MartinezRueda\Algorithm;
use MartinezRueda\Polygon;
class Shape {
/** @var Record[] */
public array $edges;
private array $edges = [];
private bool $isFlat = true;
/**
* @param Record[] $edges
*/
public function __construct(array $edges = []) {
$this->edges = $edges;
array_map([$this, "addRecord"], $edges);
}
public function addRecord(Record $record){
if(!($record instanceof MoveRecord) and !($record instanceof LineRecord)){
$this->isFlat = false;
}
$this->edges[] = $record;
}
/**
* Calculates the intersection between two Shape.
* Shapes part of the clips need to be flat (or they will be flattened)
* Each shape must not have move records
*
* If shapes are concave it can return multiple Shapes
*
* @param Shape $other
* @return Shape[]
*/
public function intersect(Shape $other) : array{
try{
return self::fromPolygon((new Algorithm())->getIntersection($this->toPolygon(), $other->toPolygon()));
}catch (\Exception $e){
var_dump($this);
var_dump($other);
echo $e;
$self = $this->flatten();
$other = $other->flatten();
var_dump((new Shape($self->getRecords()))->getArea());
var_dump((new \swf2ass\ass\drawTag(new Shape($self->getRecords()), 1))->encode(new \swf2ass\ass\ASSLine(), 1));
var_dump((new Shape($other->getRecords()))->getArea());
var_dump((new \swf2ass\ass\drawTag(new Shape($other->getRecords()), 1))->encode(new \swf2ass\ass\ASSLine(), 1));
//fgets(STDIN);
return [$this]; //TODO: fix this breakage, some clips being overlapping shapes????
}
}
/**
* @return Vector2[]
* @throws \Exception
*/
private function toPoints() : array{
$self = $this->flatten();
$points = [];
foreach ($self->getRecords() as $record){
if($record instanceof LineRecord){
$points[] = $record->start;
}else{
var_dump($record);
throw new \Exception("Found record of type " . get_class($record));
}
}
return $points;
}
private function toPolygon() : Polygon{
return new Polygon([array_map(function (Vector2 $point){return $point->toArray();}, $this->toPoints())]);
}
private static function fromPolygon(Polygon $p) : array{
$result = $p->toArray();
if(count($result) === 0){ //Nothing!
return [];
}
$shapes = [];
foreach ($result as $contour){
$shape = new Shape();
$start = $pos = new Vector2(...reset($contour));
while (($p = next($contour)) !== false){
$point = new Vector2(...$p);
$shape->addRecord(new LineRecord($point, $pos));
$pos = $point;
}
if($shape->getArea() > Constants::EPSILON){ //TODO
$shape->addRecord(new LineRecord($start, $pos)); //Close shape
$shapes[] = $shape;
}
}
return $shapes;
}
/**
* @return Record[]
*/
public function getRecords() : array{
return $this->edges;
}
public function getArea() : float{
$area = 0;
$points = $this->toPoints();
foreach ($points as $i => $p1){
$p2 = $points[($i + 1) % count($points)];
$area += $p1->x * $p2->y - $p1->y * $p2->x;
}
return $area / 2;
}
public function merge(Shape $shape): Shape {
return new Shape(array_merge($this->edges, $shape->edges));
$newShape = new Shape([]);
$newShape->edges = array_merge($this->edges, $shape->edges);
$newShape->isFlat = $shape->isFlat === $this->isFlat ? $this->isFlat : false;
return $newShape;
}
public function flatten() : Shape{
if($this->isFlat){
return $this;
}
$newShape = new Shape();
//TODO: b-spline
foreach ($this->edges as $edge){
if($edge instanceof QuadraticCurveRecord){
array_map([$newShape, "addRecord"], $edge->toLineRecords());
}else if($edge instanceof CubicCurveRecord){
array_map([$newShape, "addRecord"], $edge->toLineRecords());
}else if($edge instanceof LineRecord){
$newShape->addRecord($edge);
}else if($edge instanceof MoveRecord){
$newShape->addRecord($edge);
}else{
throw new \Exception("unimplemented");
}
}
return $newShape;
}
public function equals(Shape $other) : bool{
if(count($this->edges) !== count($other->edges) and $this->isFlat === $other->isFlat){
return false;
}
foreach ($this->edges as $i => $record){
if(!$record->equals($other->edges[$i])){
return false;
}
}
return true;
}
}

View file

@ -36,7 +36,7 @@ class ShapeConverter {
if ($node["type"] === "StyleChangeRecord") {
if (isset($node["moveDeltaX"])) {
$move = MoveRecord::fromArray($node, $this->position);
$this->position = $move->coord;
$this->position = $move->to;
$this->flush_paths();
}
@ -84,9 +84,9 @@ class ShapeConverter {
} else if ($node["type"] === "StraightEdgeRecord") {
$line = LineRecord::fromArray($node, $this->position);
$this->visit_point($line->coord, false);
$this->visit_point($line->to, false);
$this->position = $line->coord;
$this->position = $line->to;
} else if ($node["type"] === "CurvedEdgeRecord") {
$curve = QuadraticCurveRecord::fromArray($node, $this->position);

View file

@ -13,6 +13,13 @@ class Vector2 {
$this->y = $y;
}
/**
* @return numeric[]
*/
public function toArray() : array{
return [$this->x, $this->y];
}
public function equals(Vector2 $b, $epsilon = Constants::EPSILON): bool {
return abs($b->x - $this->x) <= $epsilon and abs($b->y - $this->y) <= $epsilon;
@ -22,6 +29,10 @@ class Vector2 {
return sqrt(pow($this->x - $b->x, 2) + pow($this->y - $b->y, 2));
}
public function distanceSquare(Vector2 $b): float {
return pow($this->x - $b->x, 2) + pow($this->y - $b->y, 2);
}
public function invert(): Vector2 {
return new Vector2($this->y, $this->x);
}

View file

@ -81,33 +81,40 @@ class ViewFrame {
$renderedFrame = new RenderedFrame();
$clipShape = null;
$clipPath = null;
if ($this->clipDepthMap !== null) {
$colorIdentity = ColorTransform::identity();
$matrixIdentity = MatrixTransform::identity();
$clipShape = new Shape();
foreach ($this->clipDepthMap as $clipDepth => $clipFrame) {
//TODO: detect rectangle clips? for \iclip
//TODO: add \clip for bounds
//TODO: detect rectangle clips?
//TODO: clip clips?
foreach ($clipFrame->render($clipDepth, $depthChain, $colorIdentity, $matrixIdentity)->getObjects() as $clipObject) {
foreach ($clipObject->drawPathList->commands as $clipPath) {
//TODO: is transform here needed?
$clipShape = $clipShape->merge($clipObject->matrixTransform->applyToShape($clipPath->commands));
$clipShape = new ClipPath();
foreach ($clipObject->drawPathList->commands as $p) {
$s = $p->commands->flatten();
if($s->getArea() > Constants::EPSILON){
$clipShape->addShape($s);
}
}
if(count($clipShape->shapes) > 0){
$clipShape = $clipShape->applyMatrixTransform($clipObject->matrixTransform);
$clipPath = $clipPath === null ? $clipShape : $clipShape->intersect($clipPath);
}
}
}
}
if ($this->drawPathList !== null) {
$renderedFrame->add(new RenderedObject($depthChain, $this->getObjectId(), $this->drawPathList, $colorTransform ?? ColorTransform::identity(), $matrixTransform ?? MatrixTransform::identity(), $clipShape));
$renderedFrame->add(new RenderedObject($depthChain, $this->getObjectId(), $this->drawPathList, $colorTransform ?? ColorTransform::identity(), $matrixTransform ?? MatrixTransform::identity(), $clipPath));
} else {
foreach ($this->depthMap as $depth => $frame) {
$objects = $frame->render($depth, $depthChain, $colorTransform, $matrixTransform)->getObjects();
foreach ($objects as $object) {
if ($object->clip !== null and $clipShape !== null) {
$object->clip = $object->clip->merge($clipShape);
} else if ($clipShape !== null) {
$object->clip = $clipShape;
if ($object->clip !== null and $clipPath !== null) {
$object->clip = $object->clip->intersect($clipPath);
} else if ($clipPath !== null) {
$object->clip = $clipPath;
}
$renderedFrame->add($object);

View file

@ -0,0 +1,9 @@
<?php
namespace swf2ass\ass;
use swf2ass\ClipPath;
interface ASSClipPathTag extends ASSTag {
public function transitionClipPath(ASSLine $line, ?ClipPath $clip): ?ASSClipPathTag;
}

View file

@ -12,6 +12,7 @@ class ASSLine {
/** @var int[] */
public array $layer;
public int $shapeIndex;
public int $objectId;
public int $start;
public int $end;
@ -38,7 +39,15 @@ class ASSLine {
$line->end = $information->getFrameNumber();
$line->tags = [];
//TODO: clip?
if($object->getDepth() === $this->layer and $object->objectId === $this->objectId) {
$command = $object->drawPathList->commands[$line->shapeIndex] ?? null;
if($command === null){
return null;
}
foreach ($this->tags as $tag){
if($tag instanceof ASSPositioningTag){
$tag = $tag->transitionMatrixTransform($line, $object->matrixTransform);
@ -53,6 +62,20 @@ class ASSLine {
}
}
if($tag instanceof ASSPathTag){
$tag = $tag->transitionShape($line, $command->commands);
if($tag === null){
return null;
}
}
if($tag instanceof ASSClipPathTag){
$tag = $tag->transitionClipPath($line, $object->clip);
if($tag === null){
return null;
}
}
$line->tags[] = $tag;
}
}
@ -68,12 +91,14 @@ class ASSLine {
*/
public static function fromRenderObject(FrameInformation $information, RenderedObject $object, bool $bakeTransforms = false): array {
$lines = [];
foreach ($object->drawPathList->commands as $drawPath) {
foreach ($object->drawPathList->commands as $i => $drawPath) {
$line = new ASSLine();
$line->layer = $object->getDepth();
$line->shapeIndex = $i;
$line->objectId = $object->objectId;
$line->start = $information->getFrameNumber();
$line->end = $information->getFrameNumber();
//TODO: do gradient splitting here
$line->tags[] = containerTag::fromPathEntry($drawPath, $object->clip, $object->colorTransform, $object->matrixTransform, $bakeTransforms);
$line->name = "o:{$object->objectId} d:" . implode(".", array_slice($object->depth, 1));
$lines[] = $line;
@ -82,7 +107,15 @@ class ASSLine {
return $lines;
}
public static function encodeTime(int $ms, $msPrecision = 2): string {
/**
* NOTE: libass parses milliseconds in a way that anything different than 2 decimal precisions will make it fail.
* TODO: use \t for exact timed line entries
*
* @param int $ms
* @param int $msPrecision
* @return string
*/
public static function encodeTime(int $ms, int $msPrecision = 2): string {
if ($ms < 0) {
throw new \LogicException("ms less than 0: $ms");
}
@ -92,12 +125,8 @@ class ASSLine {
$minutes = intdiv($ms, self::MINUTES_MS);
$ms -= $minutes * self::MINUTES_MS;
$s = explode(".", strval(round($ms / 1000, $msPrecision)));
if (!isset($s[1])) {
$s[1] = 0;
}
return ((string)$hours) . ":" . str_pad((string)$minutes, 2, "0", STR_PAD_LEFT) . ':' . str_pad($s[0], 2, "0", STR_PAD_LEFT) . "." . str_pad($s[1], $msPrecision, "0", STR_PAD_RIGHT);
$msPadding = 3 + $msPrecision;
return sprintf("%01d:%02d:%0{$msPadding}.{$msPrecision}F", $hours, $minutes, $ms / 1000);
}
public function dropCache(){
@ -118,11 +147,11 @@ class ASSLine {
return $layer;
}
public function encode($frameDurationMs): string {
if($frameDurationMs === 1000 and $this->cachedEncode !== null){
public function encode($frameDurationMs, int $msPrecision = 2): string {
if($frameDurationMs === 1000 and $msPrecision === 2 and $this->cachedEncode !== null){
return $this->cachedEncode;
}
$line = ($this->isComment ? "Comment" : "Dialogue") . ": " . $this->getPackedLayer() . "," . self::encodeTime($this->start * $frameDurationMs) . "," . self::encodeTime(($this->end + 1) * $frameDurationMs) . "," . $this->style . "," . $this->name . "," . $this->marginLeft . "," . $this->marginRight . "," . $this->marginVertical . "," . $this->effect . ",";
$line = ($this->isComment ? "Comment" : "Dialogue") . ": " . $this->getPackedLayer() . "," . self::encodeTime($this->start * $frameDurationMs, $msPrecision) . "," . self::encodeTime(($this->end + 1) * $frameDurationMs, $msPrecision) . "," . $this->style . "," . $this->name . "," . $this->marginLeft . "," . $this->marginRight . "," . $this->marginVertical . "," . $this->effect . ",";
foreach ($this->tags as $tag){
$line .= "{" . $tag->encode($this, $frameDurationMs) . "}";
}
@ -132,7 +161,7 @@ class ASSLine {
return $line;
}
public function equalish(ASSLine $line){
public function equalish(ASSLine $line): bool {
return $this->layer === $line->layer and $this->objectId === $line->objectId and count($this->tags) === count($line->tags) and $this->encode(1000) === $line->encode(1000);
}
}

9
src/ass/ASSPathTag.php Normal file
View file

@ -0,0 +1,9 @@
<?php
namespace swf2ass\ass;
use swf2ass\Shape;
interface ASSPathTag extends ASSTag {
public function transitionShape(ASSLine $line, Shape $shape): ?ASSPathTag;
}

View file

@ -30,16 +30,20 @@ class ASSRenderer {
$frameRate *= 2;
}
$timerPrecision = str_replace(".", ",", sprintf("%.4F", (100 / $this->getSetting("timerSpeed", 100)) * 100));
$this->header = <<<ASSHEADER
[Script Info]
; Script generated by swf2ass ASSRenderer
; https://git.gammaspectra.live/WeebDataHoarder/swf2ass
ScriptType: v4.00+
; TODO: maybe set WrapStyle: 2
WrapStyle: 0
ScaledBorderAndShadow: yes
YCbCr Matrix: PC.709
PlayResX: {$width}
PlayResY: {$height}
Timer: {$timerPrecision}
[Aegisub Project Garbage]
Last Style Storage: Default
@ -111,7 +115,7 @@ ASSHEADER;
foreach ($this->runningBuffer as $line) {
$line->name .= " f:{$line->start}>{$line->end}~".($line->end - $line->start + 1);
$line->dropCache();
yield $line->encode($information->getFrameDurationMilliSeconds());
yield $line->encode($information->getFrameDurationMilliSeconds() * ($this->getSetting("timerSpeed", 100) / 100), $this->getSetting("timePrecision"));
}
$this->runningBuffer = $runningBuffer;
@ -121,7 +125,7 @@ ASSHEADER;
foreach ($this->runningBuffer as $line) {
$line->name .= " f:{$line->start}>{$line->end}~".($line->end - $line->start + 1);
$line->dropCache();
yield $line->encode($information->getFrameDurationMilliSeconds());
yield $line->encode($information->getFrameDurationMilliSeconds() * ($this->getSetting("timerSpeed", 100) / 100), $this->getSetting("timePrecision"));
}
$this->runningBuffer = [];
}

View file

@ -2,9 +2,11 @@
namespace swf2ass\ass;
class clipTag extends drawingTag {
class clipTag extends clippingTag {
public function encode(ASSLine $line, float $frameDurationMs): string {
return "\\clip(" . implode(" ", $this->getCommands()) . ")";
$scaleMultiplier = 2 ** ($this->scale - 1);
return $this->isNull ? "" : "\\clip({$this->scale}," . implode(" ", $this->getCommands($scaleMultiplier, $this->scale >= 5 ? 0 : 2)) . ")";
}
}

41
src/ass/clippingTag.php Normal file
View file

@ -0,0 +1,41 @@
<?php
namespace swf2ass\ass;
use swf2ass\ClipPath;
use swf2ass\Constants;
use swf2ass\LineRecord;
use swf2ass\Shape;
use swf2ass\Vector2;
abstract class clippingTag extends drawingTag implements ASSClipPathTag {
protected int $scale;
protected bool $isNull;
public function __construct(?ClipPath $clip, $scale = 6) {
if($clip === null){
$this->isNull = true;
$shape = new Shape([]);
}else{
$this->isNull = false;
$shape = $clip->getShape();
if(count($shape->getRecords()) === 0){ //Full clip
$shape = new Shape([new LineRecord(new Vector2(0, Constants::TWIP_SIZE), new Vector2(0, 0))]);
}
}
parent::__construct($shape);
$this->scale = $scale;
}
public function transitionClipPath(ASSLine $line, ?ClipPath $clip): ?clippingTag{
if($clip === null){
return $this->isNull ? $this : null;
}
return $this->shape->equals($clip->getShape()) ? $this : null;
}
public function equals(ASSTag $tag): bool {
return $tag instanceof $this and $this->isNull === $tag->isNull and $this->shape->equals($tag->shape);
}
}

View file

@ -4,6 +4,7 @@ namespace swf2ass\ass;
use MathPHP\LinearAlgebra\MatrixFactory;
use swf2ass\ClipPath;
use swf2ass\ColorTransform;
use swf2ass\DrawPath;
use swf2ass\MatrixTransform;
@ -11,7 +12,7 @@ use swf2ass\Shape;
use swf2ass\StyleRecord;
use swf2ass\Vector2;
class containerTag implements ASSColorTag, ASSPositioningTag, ASSStyleTag {
class containerTag implements ASSColorTag, ASSPositioningTag, ASSStyleTag, ASSPathTag, ASSClipPathTag {
/** @var ASSTag[] */
private array $tags = [];
@ -109,21 +110,17 @@ class containerTag implements ASSColorTag, ASSPositioningTag, ASSStyleTag {
}
}
public static function fromPathEntry(DrawPath $path, ?Shape $clip, ?ColorTransform $colorTransform, ?MatrixTransform $matrixTransform, bool $bakeTransforms = false): containerTag {
public static function fromPathEntry(DrawPath $path, ?ClipPath $clip, ?ColorTransform $colorTransform, ?MatrixTransform $matrixTransform, bool $bakeTransforms = false): containerTag {
$container = new containerTag();
$container->try_append(new clipTag($clip));
$container->try_append(borderTag::fromStyleRecord($path->style));
$container->try_append(shadowTag::fromStyleRecord($path->style));
$container->try_append(lineColorTag::fromStyleRecord($path->style)->applyColorTransform($colorTransform));
$container->try_append(fillColorTag::fromStyleRecord($path->style)->applyColorTransform($colorTransform));
$matrixTransform = $matrixTransform ?? MatrixTransform::identity();
if ($clip !== null) {
//TODO: matrix transform?
//$container->try_append(new clipTag($clip));
}
if($bakeTransforms){
$container->bakeTransforms = $matrixTransform;
@ -200,4 +197,48 @@ class containerTag implements ASSColorTag, ASSPositioningTag, ASSStyleTag {
}
return $ret;
}
public function transitionShape(ASSLine $line, Shape $shape): ?ASSPathTag {
$container = clone $this;
$index = $line->end - $line->start - 1;
if(!isset($container->transitions[$index])){
$container->transitions[$index] = [];
}
foreach ($container->tags as $tag) {
if ($tag instanceof ASSPathTag) {
$newTag = $tag->transitionShape($line, $shape);
if ($newTag === null) {
return null;
}
if(!$newTag->equals($tag)){
$container->transitions[$index][] = $newTag;
}
}
}
return $container;
}
public function transitionClipPath(ASSLine $line, ?ClipPath $clip): ?ASSClipPathTag {
$container = clone $this;
$index = $line->end - $line->start - 1;
if(!isset($container->transitions[$index])){
$container->transitions[$index] = [];
}
foreach ($container->tags as $tag) {
if ($tag instanceof ASSClipPathTag) {
$newTag = $tag->transitionClipPath($line, $clip);
if ($newTag === null) {
return null;
}
if(!$newTag->equals($tag)){
$container->transitions[$index][] = $newTag;
}
}
}
return $container;
}
}

View file

@ -6,7 +6,7 @@ use swf2ass\Constants;
use swf2ass\MoveRecord;
use swf2ass\Shape;
class drawTag extends drawingTag {
class drawTag extends drawingTag implements ASSPathTag {
protected int $scale;
public function __construct(Shape $shape, $scale = 6) {
@ -14,8 +14,12 @@ class drawTag extends drawingTag {
$this->scale = $scale;
}
public function transitionShape(ASSLine $line, Shape $shape): ?drawTag{
return $this->shape->equals($shape) ? $this : null;
}
public function encode(ASSLine $line, float $frameDurationMs): string {
$scaleMultiplier = 2 ** ($this->scale - 1);
return "\\p".$this->scale."}" . implode(" ", $this->getCommands($scaleMultiplier, 0)) . "{\\p0";
return "\\p".$this->scale."}" . implode(" ", $this->getCommands($scaleMultiplier, $this->scale >= 5 ? 0 : 2)) . "{\\p0";
}
}

View file

@ -34,13 +34,25 @@ abstract class drawingTag implements ASSTag {
$commands = [];
/** @var ?Record $lastEdge */
$lastEdge = null;
foreach ($this->shape->edges as $edge) {
foreach ($this->shape->getRecords() as $edge) {
if($lastEdge === null){
if(!($edge instanceof MoveRecord)){
$coords = $edge->getStart()->multiply($scale / Constants::TWIP_SIZE);
$commands[] = "m " . round($coords->x, $precision) . " " . round($coords->y, $precision);
}
}else if(!$lastEdge->getEnd()->equals($edge->getStart()) and !($edge instanceof MoveRecord)){
$coords = $edge->getStart()->multiply($scale / Constants::TWIP_SIZE);
$commands[] = "m " . round($coords->x, $precision) . " " . round($coords->y, $precision);
$lastEdge = null;
}
if ($edge instanceof MoveRecord) {
$coords = $edge->coord->multiply($scale / Constants::TWIP_SIZE);
$commands[] = ($lastEdge instanceof $edge ? " " : "m ") . round($coords->x, $precision) . " " . round($coords->y, $precision);
$coords = $edge->to->multiply($scale / Constants::TWIP_SIZE);
$commands[] = "m " . round($coords->x, $precision) . " " . round($coords->y, $precision);
} else if ($edge instanceof LineRecord) {
$coords = $edge->coord->multiply($scale / Constants::TWIP_SIZE);
$commands[] = ($lastEdge instanceof $edge ? " " : "l ") . round($coords->x, $precision) . " " . round($coords->y, $precision);
$coords = $edge->to->multiply($scale / Constants::TWIP_SIZE);
$commands[] = ($lastEdge instanceof $edge ? "" : "l ") . round($coords->x, $precision) . " " . round($coords->y, $precision);
} else if ($edge instanceof QuadraticCurveRecord or $edge instanceof CubicCurveRecord or $edge instanceof CubicSplineCurveRecord) {
if ($edge instanceof QuadraticCurveRecord) {
$edge = CubicCurveRecord::fromQuadraticRecord($edge);
@ -51,7 +63,7 @@ abstract class drawingTag implements ASSTag {
$control1 = $edge->control1->multiply($scale / Constants::TWIP_SIZE);
$control2 = $edge->control2->multiply($scale / Constants::TWIP_SIZE);
$anchor = $edge->anchor->multiply($scale / Constants::TWIP_SIZE);
$commands[] = ($lastEdge instanceof $edge ? " " : "b ") . round($control1->x, $precision) . " " . round($control1->y, $precision) . " " . round($control2->x, $precision) . " " . round($control2->y, $precision) . " " . round($anchor->x, $precision) . " " . round($anchor->y, $precision);
$commands[] = ($lastEdge instanceof $edge ? "" : "b ") . round($control1->x, $precision) . " " . round($control1->y, $precision) . " " . round($control2->x, $precision) . " " . round($control2->y, $precision) . " " . round($anchor->x, $precision) . " " . round($anchor->y, $precision);
}
//TODO
@ -79,7 +91,7 @@ abstract class drawingTag implements ASSTag {
}
public function equals(ASSTag $tag): bool {
return $tag instanceof $this and $this->getCommands() === $tag->getCommands();
return $tag instanceof $this and $this->shape->equals($tag->shape);
}
}

View file

@ -2,9 +2,10 @@
namespace swf2ass\ass;
class insideClipTag extends drawingTag {
class insideClipTag extends clippingTag {
public function encode(ASSLine $line, float $frameDurationMs): string {
return "\\iclip(" . implode(" ", $this->getCommands()) . ")";
$scaleMultiplier = 2 ** ($this->scale - 1);
return $this->isNull ? "" : "\\iclip({$this->scale}," . implode(" ", $this->getCommands($scaleMultiplier, $this->scale >= 5 ? 0 : 2)) . ")";
}
}

View file

@ -2,7 +2,7 @@
namespace swf2ass\ass;
use MathPHP\LinearAlgebra\MatrixFactory;
use swf2ass\Constants;
use swf2ass\MatrixTransform;
use swf2ass\Vector2;
@ -32,20 +32,366 @@ class matrixTransformTag implements ASSPositioningTag {
}
public static function fromMatrixTransform(MatrixTransform $transform): ?matrixTransformTag {
// Several bugs in libass and xy-VSFilter / VSFilter do not allow having working skewX and skewY at the same time
// See https://mechaweaponsvidya.wordpress.com/2014/08/
// However values can be mapped back into just one of \fax or \fay, and scale/rotation
/*
!! Everything here is untested and best check for errors in the calculations !!
$qr = $transform->decompose();
Choose \fay, \fax, \fscx and \fscy to create a (almost) general 2D-transform-matrix
(Rotations are sometimes also used)
$frz = (180 / M_PI) * -$qr->rotation;
$fax = $qr->skew->x;
$fay = $qr->skew->y;
$fsc = $qr->scale->multiply(100);
(ASS Transform order: combined faxy, scale, frz, frx, fry)
$frx = $fsc->y < 0 ? 180 : 0;
$fry = $fsc->x < 0 ? 180 : 0;
//$frz = 0;
return new matrixTransformTag($fsc->abs(), $frx, $fry, $frz, $fax, $fay);
If shearOrigin == scaleOrigin == rotationOrigin == (0,0)^T
(which iirc is true for drawings with \org(0,0)?),
and there are no run splits, and if assuming \frz0 for now,
ASS will effectively use the following transform matrix
| scale_x 0 | | 1 fax |
| | * | |
| 0 scale_y | | fay 1 |
| scale_x scale_x fax |
= | |
| scale_y fay scale_y |
Let's assume that's true.
Then given an arbitrary transform matrix M,
with known coefficients a, b, c, d \in \R
| a b |
M = | |
| c d |
we get:
I: scale_x = a
II: scale_y = d
III: scale_x fax = b
IV: scale_y fay = c
If (a = b = 0) or (c = d = 0), then scale_x = 0
or scale_y = 0 and all other values can be set to zero
as the event vanishes anyway.
If (a = 0 and b != 0) or (c != 0 and d = 0), then
we can't solve this as is directly. Ignore this
case for now, we'll later describe how to adapt the solution for this.
If scale_x = a != 0 != d = scale_y, it follows that
III => fax = b / scale_x
IV => fay = c / scale_y
But even in this case, there's one issue:
scale_x, scale_y may be negative,
but ASS only allows positive scale values.
To remedy this we'll use 3D rotations to emulate negative scale.
It is easy to see, that by themselves a
rotation around x by π, is equivalent to scale_y = -1 and
a rotation around y by π is equivalent to scale_x = -1.
If we use \fscx and \fscy to scale by the respective absolute values
and immediately follow this up by a -1-scale if needed the result
will be equivalent to a scale by the proper value.
In general this isn't so straightforward as ASS always applies rotations
in the order of z, x, y, but we chose rot_z = 0, resulting in it being a
dismissable identity operation.
If scale_x is negative, pass the absolute value to \fscx and add \fry180.
If scale_y is negative, pass the absolute value to \fscy and add \frx180.
ASS-3D-rotation matrices:
| 1 0 0 |
| |
Rx = | 0 cos(rot_x) sin(rot_x) |
| |
| 0 sin(rot_x) -cos(rot_x) |
| cos(rot_y) 0 sin(rot_y) |
| |
Ry = | 0 1 0 |
| |
| sin(rot_y) 0 -cos(rot_y) |
| cos(rot_z) -sin(rot_z) 0 |
| |
Rz = | sin(rot_z) cos(rot_z) 0 | (for choice of rot_z=0 identity matrix)
| |
| 0 0 1 |
(from: https://sourceforge.net/p/guliverkli2/code/HEAD/tree/src/subtitles/RTS.cpp#l148)
Using sgn_x, sgn_y \in {-1, 1} as the sign of the respective scales
and sc_x, sc_y \in { x >= 0 | x \in \R } as the absolute scale values,
all in all we get the following tags
\fry(-360*(sgn_x-1)) \frx(-360/(sgn_y-1)) \fscx(sc_x) \fscy(sc_y) \fax(fax) \fay(fay) \org(0,0)
TODO: ^ this is wrong, use just > If scale_x is negative, pass the absolute value to \fscx and add \fry180. ||| > If scale_y is negative, pass the absolute value to \fscy and add \frx180.
which results in the ASS transform matrix:
| sgn_x 0 0 | | 1 0 0 | | sc_x sc_x fax 0 |
| | | | | |
| 0 1 0 | * | 0 sgn_y 0 | * | sc_y fay sc_y 0 |
| | | | | |
| 0 0 -sgn_x | | 0 0 -sgn_y | | 0 0 1 |
| sgn_x 0 0 | | sc_x sc_x fax 0 |
| | | |
= | 0 sgn_y 0 | * | sc_y fay sc_y 0 |
| | | |
| 0 0 sgn_x*sgn_y | | 0 0 1 |
| sgn_x*sc_x sgn_x*sc_x fax 0 |
| |
= | sgn_y*sc_y fay sgn_y*sc_y 0 |
| |
| 0 0 sgn_x*sgn_y |
using sgn_x*sc_x = scale_x and sgn_y*sc_y = scale_y
| scale_x scale_x fax 0 |
| |
= | scale_y fay scale_y 0 |
| |
| 0 0 sgn_x*sgn_y |
| a b 0 |
| |
= | c d 0 |
| |
| 0 0 sgn_x*sgn_y |
Apart from flipping the sign of the z-coordinates when sgn_x != sgn_y
this is exactly the matrix we wanted.
Since 2D-glyphs and -drawings are initially placed at z=0,
this additional sign-flip is of no concern.
---
Now coming back to the case of (a = 0 and b != 0) or (c != 0 and d = 0):
To be able to solve this we also need to apply a z-Rotation:
1) If in each column there's at least one non-zero entry or one row is all zero
(together with case condition the latter means only b xor c is non-zero):
Use \frz90, being a row swap with a sign flip:
| 0 -1 |
\frz90 = | |
| 1 0 |
So our transform-matrix equation becomes:
| -scale_y fay -scale_y | | a b |
| | = | |
| scale_x scale_x fax | | c d |
Which can be solved as before for signed scales.
The additon of a z-Rotation means our "scale sign"-roations
no longer immediately follow the actual scale.
To compensate, I'm guessing swapping the sign of scale_x
and using \frx for sgn_x and \fry for sgn_y probably works.
(Again: untested)
2) Otherwise:
(If one column is zero and the other one has only non-zero entries)
2.1) a = 0 = c and b 0 d
| scale_x*cos(rz) - scale_y*fay*sin(rz) scale_x*fax*cos(rz) - scale_y*sin(rz) | | 0 b |
| | = | |
| scale_x*sin(rz) + scale_y*fay*cos(rz) scale_x*fax*sin(tz) + scale_y*cos(rz) | | 0 d |
I: scale_x*cos(rz) - scale_y*fay*sin(rz) = 0
II: scale_x*sin(rz) + scale_y*fay*cos(rz) = 0
III: scale_x*fax*cos(rz) - scale_y*sin(rz) = b
IV: scale_x*fax*sin(tz) + scale_y*cos(rz) = d
+ II² scale_x^2 = - scale_y^2 * fay^2 scale_x = 0 and (scale_y = 0 or fay = 0)
III' : -scale_y*sin(rz) = b
IV' : scale_y*cos(rz) = d
since b 0 d scale_y 0 fay = 0
scale_y = sqrt(b^2 + d^2)
rz = sin⁻¹(-b/scale_y)
Note: we can without loss of generality choose the positive solution for scale_y,
this will just change rz by π ( cos(rz+π) = -cos(rz) and sin(rz+π) = -sin(rz) )
Since scale_y > 0 and scale_x = 0 we do not need \frx and \fry to simulate a negative scale.
2.2) b = 0 = d and a 0 c
| scale_x*cos(rz) - scale_y*fay*sin(rz) scale_x*fax*cos(rz) - scale_y*sin(rz) | | a 0 |
| | = | |
| scale_x*sin(rz) + scale_y*fay*cos(rz) scale_x*fax*sin(tz) + scale_y*cos(rz) | | c 0 |
I: scale_x*fax*cos(rz) - scale_y*sin(rz) = 0
II: scale_x*fax*sin(tz) + scale_y*cos(rz) = 0
III: scale_x*cos(rz) - scale_y*fay*sin(rz) = a
IV: scale_x*sin(rz) + scale_y*fay*cos(rz) = b
+ II² scale_y^2 = - scale_x^2 * fax^2 scale_y = 0 and (scale_x = 0 or fax = 0)
III' : scale_x*cos(rz) = a
IV' : scale_x*sin(rz) = c
since a 0 c scale_x 0 fax = 0
scale_x = sqrt(a^2 + c^2)
rz = sin⁻¹(c/scale_x)
Note: we can without loss of generality choose the positive solution for scale_x,
since this will just change rz by π ( cos(rz+π) = -cos(rz) and sin(rz+π) = -sin(rz) )
Since scale_x > 0 and scale_y = 0 we do not need \frx and \fry to simulate a negative scale.
In both 2.1) and 2.2) we get one dimension scaled to zero, and if I didin't miss something
no contradictions in the equations. Meaning the event actually vanishes and is visually identical
to choosing all parameters zero. If interpolations are involved having the mathematically accurate
transform matrix might still be good though.
*/
$isZero = function ($v){
return abs($v) < Constants::EPSILON;
};
$sign = function (float $a) : int {
if($a < 0){
return -1;
}else{
return 1;
}
};
$a = $transform->get_a();
$b = $transform->get_b();
$c = $transform->get_c();
$d = $transform->get_d();
if(($isZero($a) and $isZero($b)) or ($isZero($c) and $isZero($d))){
throw new \Exception("Invalid transform");
}
$scale_x = $scale_y = $frx = $fry = $frz = $fax = $fay = 0;
//TODO: WiP, fix rotations on negative scales
$i = sqrt($a * $a + $c * $c);
$j = sqrt($b * $b + $d * $d);
if($i >= $j){
$n = $i;
$frz = (180 / M_PI) * atan2($c, $a);
$scale_x = $n;
$scale_y = ($a * $d - $b * $c) / $n;
$fax = ($a * $b + $c * $d) / ($n * $n);
$fay = 0;
if($scale_y < 0){
$frx = 180;
}
}else{
$n = $j;
$frz = (180 / M_PI) * atan2(-$b, $d);
$scale_x = ($a * $d - $b * $c) / $n;
$scale_y = $n;
$fax = 0;
$fay = ($a * $b + $c * $d) / ($n * $n);
if($scale_x < 0){
$fry = 180;
}
}
/*
if(
!$isZero($a) and !$isZero($d)
//!(($isZero($a) and !$isZero($b)) or (!$isZero($c) and $isZero($d)))
){
$scale_x = $a;
$scale_y = $d;
$fax = $b / $scale_x;
$fay = $c / $scale_y;
$sgn_x = $sign($scale_x);
$sgn_y = $sign($scale_y);
if($sgn_x === -1){
$fry = 180;
}
if($sgn_y === -1){
$frx = 180;
}
$frx = -90 * ($sign($scale_y) - 1);
$fry = -90 * ($sign($scale_x) - 1);
}else if (
(($isZero($a) or $isZero($c)) and ($isZero($c) or $isZero($d)))
or
(($isZero($a) and $isZero($b)) !== ($isZero($c) and $isZero($d)))
){
$scale_x = $c;
$scale_y = -$b;
$fax = $d / $scale_x;
$fay = $a / -$scale_y;
$scale_x *= -1;
$sgn_x = $sign($scale_x);
$sgn_y = $sign($scale_y);
if($sgn_y === -1){
$fry = 180;
}
if($sgn_x === -1){
$frx = 180;
}
$frx = -90 * ($sign($scale_x) - 1);
$fry = -90 * ($sign($scale_y) - 1);
$frz = 90;
}elseif(
($isZero($a) and $isZero($b))
and
(!$isZero($c) and !$isZero($d))
){
//This cases will all be "zero" but giving parameters allows interpolations
$scale_y = sqrt($b * $b + $d * $d);
$frz = (180 / M_PI) * asin(-$b / $scale_y);
}elseif(
($isZero($b) and $isZero($d))
and
(!$isZero($a) and !$isZero($c))
){
//This cases will all be "zero" but giving parameters allows interpolations
$scale_x = sqrt($a * $a + $c * $c);
$frz = (180 / M_PI) * asin($c / $scale_x);
}else{
echo $transform . "\n";
throw new \Exception("Invalid transform state");
}
*/
$fscx = abs($scale_x) * 100;
$fscy = abs($scale_y) * 100;
return new matrixTransformTag(new Vector2($fscx, $fscy), $frx, $fry, $frz, $fax, $fay);
}
}

View file

@ -73,7 +73,14 @@ class positionTag implements ASSPositioningTag {
$shift = $this->end - $this->start;
if($hasMoved){
return "\\move(" . $this->from->x ."," . $this->from->y ."," . $this->to->x ."," . $this->to->y .",".(ceil(($shift > 1 ? ($this->start - 1) : $this->start) * $frameDurationMs) - 1).",".(floor(($shift > 1 ? $this->end : $this->end - 1) * $frameDurationMs) - 1).")";
if($shift > 1){
$start = ceil(($this->start - 1) * $frameDurationMs) + 1;
$end = floor($this->end * $frameDurationMs) - 1;
}else{
$start = floor($this->start * $frameDurationMs) - 1;
$end = floor(($this->end - 1) * $frameDurationMs) - 1;
}
return "\\move(" . $this->from->x ."," . $this->from->y ."," . $this->to->x ."," . $this->to->y .",".$start.",".$end.")";
}
return "\\pos(".$this->from->x ."," . $this->from->y.")";
}

View file

@ -22,7 +22,7 @@ class rotationTag implements ASSPositioningTag {
}
public function encode(ASSLine $line, float $frameDurationMs): string {
return "\\frx" . round($this->x, 5) ."\\fry" . round($this->y, 5) ."\\frz" . round($this->z, 5);
return sprintf("\\frx%.2F\\fry%.2F\\frz%.2F", $this->x, $this->y, $this->z);
}
public function equals(ASSTag $tag): bool {

View file

@ -20,7 +20,7 @@ class scaleTag implements ASSPositioningTag {
}
public function encode(ASSLine $line, float $frameDurationMs): string {
return "\\fscx" . round($this->scale->x, 5) ."\\fscy" . round($this->scale->y, 5);
return sprintf("\\fscx%.5F\\fscy%.5F", $this->scale->x, $this->scale->y);
}
public function equals(ASSTag $tag): bool {

View file

@ -20,7 +20,7 @@ class shearingTag implements ASSPositioningTag {
}
public function encode(ASSLine $line, float $frameDurationMs): string {
return "\\fax" . round($this->shear->x, 5) ."\\fay" . round($this->shear->y, 5) ."";
return sprintf("\\fax%.5F\\fay%.5F", $this->shear->x, $this->shear->y);
}
public function equals(ASSTag $tag): bool {

View file

@ -4,13 +4,27 @@ require_once __DIR__ . "/vendor/autoload.php";
$swf = new \swf\SWF(file_get_contents($argv[1]));
$m = new \swf2ass\MatrixTransform(new \swf2ass\Vector2(-1, 1), new \swf2ass\Vector2(0.17115783691406, 0.16761779785156), new \swf2ass\Vector2(0, 0));
//$m = new \swf2ass\MatrixTransform(new \swf2ass\Vector2(-0.69596862792969, 0.71046447753906), new \swf2ass\Vector2(0.17115783691406, 0.16761779785156), new \swf2ass\Vector2(1374, 2174));
$t = \swf2ass\ass\matrixTransformTag::fromMatrixTransform($m);
echo $m . "\n";
var_dump($t);
var_dump($t->encode(new \swf2ass\ass\ASSLine(), 1));
//exit();
$fp = fopen($argv[2], "w+");
if ($swf->header["signature"]) {
$processor = new \swf2ass\SWFProcessor($swf);
$assRenderer = new \swf2ass\ass\ASSRenderer($processor->getFrameRate(), $processor->getViewPort(), [
"bakeTransforms" => true //TODO: fix ASS matrix transform rendering and remove this
"bakeTransforms" => false, //TODO: fix ASS matrix transform rendering and remove this
"timerSpeed" => 100, //NOTE: libass does not implement "Timer:", which is used by this setting. Leave at 100 by default
"timePrecision" => 2, //NOTE: libass does not implement anything different from 2. Leave at 2 by default
]);
$keyFrameInterval = 10 * $processor->getFrameRate(); //kf every 10 seconds TODO: make this dynamic, per-shape
@ -37,14 +51,14 @@ if ($swf->header["signature"]) {
foreach ($rendered->getObjects() as $object){
if($object->clip !== null){
++$clipCalls;
$clipItems += count($object->clip->edges);
$clipItems += count($object->clip->getShape()->getRecords());
}
foreach ($object->drawPathList->commands as $path){
++$drawCalls;
$drawItems += count($path->commands->edges);
$drawItems += count($path->commands->getRecords());
}
}
echo "=== frame ".$frame->getFrameNumber()." ~ $frameOffset : Depth count: " . count($frame->getFrame()->getDepthMap()) . " :: Object count: " . count($rendered->getObjects()) ." :: Paths: $drawCalls draw calls, $drawItems items :: Clips: $clipCalls draw calls, $clipItems items" . PHP_EOL;
echo "=== frame ".$frame->getFrameNumber()."/".$processor->getExpectedFrameCount()." ~ $frameOffset : Depth count: " . count($frame->getFrame()->getDepthMap()) . " :: Object count: " . count($rendered->getObjects()) ." :: Paths: $drawCalls draw calls, $drawItems items :: Clips: $clipCalls draw calls, $clipItems items" . PHP_EOL;
foreach ($assRenderer->renderFrame($frame, $rendered) as $line){
fwrite($fp, $line . "\n");