Compare commits
4 commits
ec04e81253
...
f1196e854f
Author | SHA1 | Date | |
---|---|---|---|
DataHoarder | f1196e854f | ||
DataHoarder | f3255b57ac | ||
DataHoarder | b7030433d8 | ||
DataHoarder | 263df3e895 |
|
@ -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
53
composer.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
60
src/ClipPath.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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[]
|
||||
*/
|
||||
|
|
|
@ -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 {
|
||||
|
|
154
src/Shape.php
154
src/Shape.php
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
9
src/ass/ASSClipPathTag.php
Normal file
9
src/ass/ASSClipPathTag.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace swf2ass\ass;
|
||||
|
||||
use swf2ass\ClipPath;
|
||||
|
||||
interface ASSClipPathTag extends ASSTag {
|
||||
public function transitionClipPath(ASSLine $line, ?ClipPath $clip): ?ASSClipPathTag;
|
||||
}
|
|
@ -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
9
src/ass/ASSPathTag.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace swf2ass\ass;
|
||||
|
||||
use swf2ass\Shape;
|
||||
|
||||
interface ASSPathTag extends ASSTag {
|
||||
public function transitionShape(ASSLine $line, Shape $shape): ?ASSPathTag;
|
||||
}
|
|
@ -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 = [];
|
||||
}
|
||||
|
|
|
@ -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
41
src/ass/clippingTag.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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)) . ")";
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
I² + 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
|
||||
|
||||
I² + 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);
|
||||
}
|
||||
}
|
|
@ -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.")";
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
22
swf2ass.php
22
swf2ass.php
|
@ -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");
|
||||
|
|
Loading…
Reference in a new issue