Implemented \clip intersection, and more

This commit is contained in:
DataHoarder 2022-01-04 01:15:31 +01:00
parent 263df3e895
commit b7030433d8
35 changed files with 927 additions and 214 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

@ -43,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);
}

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);
}

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,10 +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->coord->equals($other->coord);
return $other instanceof $this and $this->start->equals($other->start) and $this->to->equals($other->to);
}
}

View file

@ -85,92 +85,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 +104,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,10 +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->coord->equals($other->coord);
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

@ -45,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);
}

View file

@ -4,6 +4,7 @@ namespace swf2ass;
interface Record {
public function getStart(): Vector2;
public function getEnd(): Vector2;
public function reverse(): 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,23 +3,159 @@
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)){
if(count($this->edges) !== count($other->edges) and $this->isFlat === $other->isFlat){
return false;
}
foreach ($this->edges as $i => $record){

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

@ -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(){

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

@ -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,332 @@ 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;
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,25 @@ 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
]);
$keyFrameInterval = 10 * $processor->getFrameRate(); //kf every 10 seconds TODO: make this dynamic, per-shape
@ -37,14 +49,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");