Morph shapes, proper ASS transforms, known signatures
This commit is contained in:
parent
f1196e854f
commit
0b5e7f8635
|
@ -15,6 +15,8 @@
|
|||
"ext-zlib": "*",
|
||||
"ext-gmp": "*",
|
||||
"markrogoyski/math-php": "2.*",
|
||||
"kudm761/martinez-rueda-php": "^0.1.2"
|
||||
"kudm761/martinez-rueda-php": "^0.1.2",
|
||||
"ext-json": "*",
|
||||
"ext-decimal": "*"
|
||||
}
|
||||
}
|
||||
|
|
12
deps/swf-4real/src/SWFio.php
vendored
12
deps/swf-4real/src/SWFio.php
vendored
|
@ -26,6 +26,8 @@ namespace swf;
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Basic IO
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
use swf2ass\math\RealNumber;
|
||||
|
||||
class SWFio {
|
||||
public $b; // Byte array (file contents)
|
||||
public $bytePos; // Byte position
|
||||
|
@ -83,19 +85,19 @@ class SWFio {
|
|||
}
|
||||
|
||||
// Bit values
|
||||
public function collectFB($num) { //XXX NOT SURE - NEEDS FIX
|
||||
public function collectFB($num) : RealNumber { //XXX NOT SURE - NEEDS FIX
|
||||
$ret = $this->collectBits($num);
|
||||
if (($ret & (1 << ($num - 1))) == 0) {
|
||||
// Positive
|
||||
$hi = ($ret >> 16) & 0xffff;
|
||||
$lo = $ret & 0xffff;
|
||||
$ret = $hi + $lo / 65536.0;
|
||||
$ret = (new RealNumber($lo))->divide(65536)->add($hi);
|
||||
} else {
|
||||
// Negative
|
||||
$ret = (1 << $num) - $ret;
|
||||
$hi = ($ret >> 16) & 0xffff;
|
||||
$lo = $ret & 0xffff;
|
||||
$ret = -($hi + $lo / 65536.0);
|
||||
$ret = (new RealNumber($lo))->divide(65536)->add($hi)->negate();
|
||||
}
|
||||
// echo sprintf("collectFB, num is %d, will return [0x%04x]\n", $num, $ret);
|
||||
return $ret;
|
||||
|
@ -114,7 +116,7 @@ class SWFio {
|
|||
}
|
||||
|
||||
// Fixed point numbers
|
||||
public function collectFixed8() {
|
||||
public function collectFixed8() { //TODO: change to RealNumber
|
||||
$lo0 = $lo = $this->collectUI8();
|
||||
$hi0 = $hi = $this->collectUI8();
|
||||
if ($hi < 128) {
|
||||
|
@ -132,7 +134,7 @@ class SWFio {
|
|||
public function collectFixed() {
|
||||
$lo = $this->collectUI16();
|
||||
$hi = $this->collectUI16();
|
||||
$ret = $hi + $lo / 65536.0;
|
||||
$ret = (new RealNumber($lo))->divide(65536)->add($hi);;
|
||||
// echo sprintf("collectFixed hi=[0x%X], lo=[0x%X], return [%s]\n", $hi, $lo, $ret);
|
||||
return $ret;
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ class BitmapDefinition implements ObjectDefinition {
|
|||
return $this->id;
|
||||
}
|
||||
|
||||
public function getShapeList(): DrawPathList {
|
||||
public function getShapeList(?float $ratio): DrawPathList {
|
||||
return $this->drawPathList;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,9 +15,9 @@ class DrawPathList {
|
|||
return new DrawPathList(array_merge($this->commands, $b->commands));
|
||||
}
|
||||
|
||||
public static function fromArray(array $element, StyleList $currentStyles): DrawPathList {
|
||||
public static function fromArray(array $element, StyleList $currentStyles, ?array $otherElement = null): DrawPathList {
|
||||
|
||||
$converter = new ShapeConverter($element, $currentStyles);
|
||||
$converter = new ShapeConverter($element, $currentStyles, $otherElement);
|
||||
|
||||
return $converter->commands;
|
||||
}
|
||||
|
|
|
@ -4,20 +4,47 @@ namespace swf2ass;
|
|||
|
||||
|
||||
use MathPHP\LinearAlgebra\MatrixFactory;
|
||||
use MathPHP\LinearAlgebra\NumericMatrix;
|
||||
use swf2ass\math\RealNumber;
|
||||
use swf2ass\math\RealMatrix;
|
||||
|
||||
class MatrixTransform {
|
||||
|
||||
private NumericMatrix $matrix;
|
||||
private RealMatrix $matrix;
|
||||
|
||||
public function __construct(?Vector2 $scale, ?Vector2 $rotateSkew, ?Vector2 $translation) {
|
||||
$this->matrix = MatrixFactory::createNumeric([
|
||||
[$scale !== null ? $scale->x : 1, $rotateSkew !== null ? $rotateSkew->y : 0, 0],
|
||||
[$rotateSkew !== null ? $rotateSkew->x : 0, $scale !== null ? $scale->y : 1, 0],
|
||||
[$translation !== null ? $translation->x : 0, $translation !== null ? $translation->y : 0, 1]
|
||||
$this->matrix = new RealMatrix([
|
||||
[$scale !== null ? $scale->x : 1 /* a */, /* b */ $rotateSkew !== null ? $rotateSkew->x : 0, $translation !== null ? $translation->x : 0],
|
||||
[$rotateSkew !== null ? $rotateSkew->y : 0 /* c */, /* d */ $scale !== null ? $scale->y : 1, $translation !== null ? $translation->y : 0],
|
||||
[0, 0, 1]
|
||||
]);
|
||||
}
|
||||
|
||||
public static function fromMatrix(RealMatrix $matrix) : MatrixTransform{
|
||||
$o = new MatrixTransform(null, null, null);
|
||||
$o->matrix = $matrix;
|
||||
return $o;
|
||||
}
|
||||
|
||||
public static function fromArray(array $array) : MatrixTransform{
|
||||
return new MatrixTransform(new Vector2($array[0][0], $array[1][1]), new Vector2($array[1][0], $array[0][1]), new Vector2($array[0][2], $array[1][2]));
|
||||
}
|
||||
|
||||
public function toArray(bool $translation = true) : array{
|
||||
if($translation){
|
||||
return [
|
||||
[$this->get_a()->toFixed(), $this->get_b()->toFixed(), $this->get_tx()->toFixed()],
|
||||
[$this->get_c()->toFixed(), $this->get_d()->toFixed(), $this->get_ty()->toFixed()],
|
||||
[0, 0, 1]
|
||||
];
|
||||
}else{
|
||||
return [
|
||||
[$this->get_a()->toFixed(), $this->get_b()->toFixed(), 0],
|
||||
[$this->get_c()->toFixed(), $this->get_d()->toFixed(), 0],
|
||||
[0, 0, 1]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public static function scale(Vector2 $scale) : MatrixTransform{
|
||||
return new MatrixTransform($scale, null, null);
|
||||
}
|
||||
|
@ -47,35 +74,35 @@ class MatrixTransform {
|
|||
|
||||
public function combine(MatrixTransform $other): MatrixTransform {
|
||||
$result = clone $this;
|
||||
$result->matrix = $other->matrix->multiply($this->matrix);
|
||||
$result->matrix = $this->matrix->multiply($other->matrix);
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function get_a(){
|
||||
public function get_a() : RealNumber{
|
||||
return $this->matrix->get(0, 0);
|
||||
}
|
||||
|
||||
public function get_b(){
|
||||
public function get_b() : RealNumber{
|
||||
return $this->matrix->get(0, 1);
|
||||
}
|
||||
|
||||
public function get_c(){
|
||||
public function get_c() : RealNumber{
|
||||
return $this->matrix->get(1, 0);
|
||||
}
|
||||
|
||||
public function get_d(){
|
||||
public function get_d() : RealNumber{
|
||||
return $this->matrix->get(1, 1);
|
||||
}
|
||||
|
||||
public function get_e(){
|
||||
public function get_tx() : RealNumber{
|
||||
return $this->matrix->get(2, 0);
|
||||
}
|
||||
|
||||
public function get_f(){
|
||||
public function get_ty() : RealNumber{
|
||||
return $this->matrix->get(2, 1);
|
||||
}
|
||||
|
||||
public function getMatrix() : NumericMatrix{
|
||||
public function getMatrix() : RealMatrix{
|
||||
return $this->matrix;
|
||||
}
|
||||
|
||||
|
@ -85,11 +112,11 @@ class MatrixTransform {
|
|||
|
||||
public function applyToVector(Vector2 $vector, bool $applyTranslation = true): Vector2 {
|
||||
if($applyTranslation){
|
||||
$result = MatrixFactory::createFromRowVector([$vector->x, $vector->y, 1])->multiply($this->matrix);
|
||||
$result = $this->matrix->multiply(MatrixFactory::createFromColumnVector([$vector->x, $vector->y, 1]));
|
||||
}else{
|
||||
$result = MatrixFactory::createFromRowVector([$vector->x, $vector->y])->multiply($this->matrix->submatrix(0, 0, 1, 1));
|
||||
$result = $this->matrix->submatrix(0, 0, 1, 1)->multiply(MatrixFactory::createFromColumnVector([$vector->x, $vector->y]));
|
||||
}
|
||||
return new Vector2($result->get(0, 0), $result->get(0, 1));
|
||||
return new Vector2($result->get(0, 0)->toFloat(), $result->get(1, 0)->toFloat());
|
||||
}
|
||||
|
||||
public function applyToShape(Shape $shape, bool $applyTranslation = true): Shape {
|
||||
|
@ -105,7 +132,7 @@ class MatrixTransform {
|
|||
return $this->matrix->isEqual($other->matrix);
|
||||
}
|
||||
|
||||
static function fromArray(array $element): MatrixTransform {
|
||||
static function fromSWFArray(array $element): MatrixTransform {
|
||||
return new MatrixTransform(
|
||||
isset($element["scaleX"]) ? new Vector2($element["scaleX"], $element["scaleY"]) : null,
|
||||
isset($element["rotateSkew0"]) ? new Vector2($element["rotateSkew1"], $element["rotateSkew0"]) : null,
|
||||
|
|
119
src/MorphShapeDefinition.php
Normal file
119
src/MorphShapeDefinition.php
Normal file
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
namespace swf2ass;
|
||||
|
||||
class MorphShapeDefinition implements ObjectDefinition {
|
||||
public int $id;
|
||||
public Rectangle $startBounds;
|
||||
public Rectangle $endBounds;
|
||||
public DrawPathList $startShapeList;
|
||||
public DrawPathList $endShapeList;
|
||||
|
||||
public float $ratio = 0;
|
||||
|
||||
|
||||
public function __construct(int $id, Rectangle $startBounds, Rectangle $endBounds, DrawPathList $startShapeList, DrawPathList $endShapeList) {
|
||||
$this->id = $id;
|
||||
$this->startBounds = $startBounds;
|
||||
$this->endBounds = $endBounds;
|
||||
$this->startShapeList = $startShapeList;
|
||||
$this->endShapeList = $endShapeList;
|
||||
if(count($this->startShapeList->commands) !== count($this->endShapeList->commands)){
|
||||
throw new \Exception("Morph command count is different: start " . count($this->startShapeList->commands) . " != end " . count($this->endShapeList->commands));
|
||||
}
|
||||
}
|
||||
|
||||
public function getObjectId(): int {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getShapeList(?float $ratio): DrawPathList {
|
||||
//TODO: cache shapes by ratio
|
||||
if($ratio === null or abs($ratio) < Constants::EPSILON){
|
||||
return $this->startShapeList;
|
||||
}
|
||||
if(abs($ratio - 1.0) < Constants::EPSILON){
|
||||
return $this->endShapeList;
|
||||
}
|
||||
|
||||
$drawPathList = new DrawPathList();
|
||||
foreach ($this->startShapeList->commands as $i => $c1){
|
||||
$c2 = $this->endShapeList->commands[$i];
|
||||
|
||||
$records1 = $c1->commands->getRecords();
|
||||
$records2 = $c2->commands->getRecords();
|
||||
|
||||
$shape = new Shape();
|
||||
foreach ($records1 as $j => $r1){
|
||||
$r2 = $records2[$j];
|
||||
|
||||
//Convert line records that morph into/from curves
|
||||
if ($r1 instanceof LineRecord and $r2 instanceof QuadraticCurveRecord){
|
||||
$r1 = QuadraticCurveRecord::fromLineRecord($r1);
|
||||
}else if ($r2 instanceof LineRecord and $r1 instanceof QuadraticCurveRecord){
|
||||
$r2 = QuadraticCurveRecord::fromLineRecord($r2);
|
||||
}
|
||||
|
||||
if($r1 instanceof LineRecord and $r2 instanceof LineRecord){
|
||||
$shape->addRecord(new LineRecord(self::lerpVector2($r1->to, $r2->to, $ratio), self::lerpVector2($r1->start, $r2->start, $ratio)));
|
||||
}else if($r1 instanceof QuadraticCurveRecord and $r2 instanceof QuadraticCurveRecord){
|
||||
$shape->addRecord(new QuadraticCurveRecord(self::lerpVector2($r1->control, $r2->control, $ratio), self::lerpVector2($r1->anchor, $r2->anchor, $ratio), self::lerpVector2($r1->start, $r2->start, $ratio)));
|
||||
}else if($r1 instanceof MoveRecord and $r2 instanceof MoveRecord){
|
||||
$shape->addRecord(new MoveRecord(self::lerpVector2($r1->to, $r2->to, $ratio), self::lerpVector2($r1->start, $r2->start, $ratio)));
|
||||
}else{
|
||||
var_dump($r1);
|
||||
var_dump($r2);
|
||||
throw new \Exception();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//TODO: morph styles
|
||||
if($c1->style instanceof FillStyleRecord and $c2->style instanceof FillStyleRecord){
|
||||
if($c1->style->fill instanceof Color){
|
||||
$drawPathList->commands[] = DrawPath::fill(new FillStyleRecord(self::lerpColor($c1->style->fill, $c2->style->fill, $ratio)), $shape);
|
||||
}else if($c1->style->fill instanceof Gradient){
|
||||
//TODO: proper gradients
|
||||
$drawPathList->commands[] = DrawPath::fill(new FillStyleRecord(self::lerpColor($c1->style->fill->getItems()[0]->color, $c2->style->fill->getItems()[0]->color, $ratio)), $shape);
|
||||
}else{
|
||||
var_dump($c1->style);
|
||||
var_dump($c2->style);
|
||||
throw new \Exception();
|
||||
}
|
||||
}else if($c1->style instanceof LineStyleRecord and $c2->style instanceof LineStyleRecord){
|
||||
$drawPathList->commands[] = DrawPath::stroke(new LineStyleRecord(self::lerpInteger($c1->style->width, $c2->style->width, $ratio), self::lerpColor($c1->style->color, $c2->style->color, $ratio)), $shape, $c1->is_closed);
|
||||
}else{
|
||||
var_dump($c1->style);
|
||||
var_dump($c2->style);
|
||||
throw new \Exception();
|
||||
}
|
||||
}
|
||||
|
||||
return $drawPathList;
|
||||
}
|
||||
|
||||
private static function lerpInteger(int $start, int $end, float $ratio): int {
|
||||
return $start + ($end - $start) * $ratio;
|
||||
}
|
||||
|
||||
private static function lerpFloat(float $start, float $end, float $ratio): float {
|
||||
return $start + ($end - $start) * $ratio;
|
||||
}
|
||||
|
||||
private static function lerpVector2(Vector2 $start, Vector2 $end, float $ratio): Vector2 {
|
||||
return $start->add($end->sub($start)->multiply($ratio));
|
||||
}
|
||||
|
||||
private static function lerpColor(Color $start, Color $end, float $ratio): Color {
|
||||
return new Color(self::lerpInteger($start->r, $end->r, $ratio), self::lerpInteger($start->g, $end->g, $ratio), self::lerpInteger($start->b, $end->b, $ratio), self::lerpInteger($start->alpha, $end->alpha, $ratio));
|
||||
}
|
||||
|
||||
static function fromArray(array $element): MorphShapeDefinition {
|
||||
$styles = MorphStyleList::fromArray($element);
|
||||
|
||||
$start = DrawPathList::fromArray($element["startEdges"], $styles->getStartStyleList());
|
||||
$end = DrawPathList::fromArray($element["endEdges"], $styles->getEndStyleList(), $element["startEdges"]);
|
||||
|
||||
return new MorphShapeDefinition($element["characterId"], Rectangle::fromArray($element["startBounds"]), Rectangle::fromArray($element["endBounds"]), $start, $end);
|
||||
}
|
||||
}
|
152
src/MorphStyleList.php
Normal file
152
src/MorphStyleList.php
Normal file
|
@ -0,0 +1,152 @@
|
|||
<?php
|
||||
|
||||
namespace swf2ass;
|
||||
|
||||
|
||||
class MorphStyleList {
|
||||
/** @var FillStyleRecord[] */
|
||||
public array $startFillStyles;
|
||||
/** @var FillStyleRecord[] */
|
||||
public array $endFillStyles;
|
||||
/** @var LineStyleRecord[] */
|
||||
public array $startLineStyles;
|
||||
/** @var LineStyleRecord[] */
|
||||
public array $endLineStyles;
|
||||
|
||||
/**
|
||||
* @param FillStyleRecord[] $startFillStyles
|
||||
* @param FillStyleRecord[] $endFillStyles
|
||||
* @param LineStyleRecord[] $startLineStyles
|
||||
* @param LineStyleRecord[] $endLineStyles
|
||||
*/
|
||||
public function __construct(array $startFillStyles = [], array $endFillStyles = [], array $startLineStyles = [], array $endLineStyles = []) {
|
||||
$this->startFillStyles = $startFillStyles;
|
||||
$this->endFillStyles = $endFillStyles;
|
||||
$this->startLineStyles = $startLineStyles;
|
||||
$this->endLineStyles = $endLineStyles;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $node
|
||||
* @return FillStyleRecord[]
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function parseFillStyleRecord(array $node): array {
|
||||
switch ($node["fillStyleType"]) {
|
||||
case 0x00: // Solid fill
|
||||
return [new FillStyleRecord(Color::fromArray($node["startColor"])), new FillStyleRecord(Color::fromArray($node["endColor"]))];
|
||||
break;
|
||||
case 0x10: // Linear gradient fill
|
||||
//TODO
|
||||
return [new FillStyleRecord(new Color(0, 0, 0, 20)), new FillStyleRecord(new Color(0, 0, 255, 20))];
|
||||
return [new FillStyleRecord(LinearGradient::fromArray($node["gradient"], MatrixTransform::fromSWFArray($node["startGradientMatrix"]))), new FillStyleRecord(LinearGradient::fromArray($node["gradient"], MatrixTransform::fromSWFArray($node["endGradientMatrix"])))];
|
||||
break;
|
||||
case 0x12: // Radial gradient fill
|
||||
//TODO
|
||||
return [new FillStyleRecord(new Color(0, 0, 0, 20)), new FillStyleRecord(new Color(0, 0, 255, 20))];
|
||||
return [new FillStyleRecord(RadialGradient::fromArray($node["gradient"], MatrixTransform::fromSWFArray($node["startGradientMatrix"]))), new FillStyleRecord(RadialGradient::fromArray($node["gradient"], MatrixTransform::fromSWFArray($node["endGradientMatrix"])))];
|
||||
break;
|
||||
case 0x13: // Focal gradient fill
|
||||
//TODO
|
||||
return [new FillStyleRecord(new Color(0, 0, 0, 20)), new FillStyleRecord(new Color(0, 0, 255, 20))];
|
||||
return [new FillStyleRecord(FocalGradient::fromArray($node["focalGradient"], MatrixTransform::fromSWFArray($node["startGradientMatrix"]))), new FillStyleRecord(FocalGradient::fromArray($node["focalGradient"], MatrixTransform::fromSWFArray($node["endGradientMatrix"])))];
|
||||
break;
|
||||
case 0x40: // Repeating bitmap fill
|
||||
case 0x41: // Clipped bitmap fill
|
||||
case 0x42: // Non-smoothed repeating bitmap
|
||||
case 0x43: // Non-smoothed clipped bitmap
|
||||
var_dump($node);
|
||||
return $node["bitmapId"] === 65535 ? [new FillStyleRecord(new Color(0, 0, 0, 0)), new FillStyleRecord(new Color(0, 0, 0, 0))] : [new FillStyleRecord(new Color(0, 0, 0, 20)), new FillStyleRecord(new Color(255, 0, 0, 20))];
|
||||
break;
|
||||
default:
|
||||
var_dump($node);
|
||||
throw new \Exception("Unknown style " . $node["type"]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getStartStyleList() : StyleList{
|
||||
return new StyleList($this->startFillStyles, $this->startLineStyles);
|
||||
}
|
||||
|
||||
public function getEndStyleList() : StyleList{
|
||||
return new StyleList($this->endFillStyles, $this->endLineStyles);
|
||||
}
|
||||
|
||||
public static function fromArray(array $element): MorphStyleList {
|
||||
$startFillStyles = [];
|
||||
$endFillStyles = [];
|
||||
$startLineStyles = [];
|
||||
$endLineStyles = [];
|
||||
foreach ($element["morphFillStyles"] as $node) {
|
||||
$record = self::parseFillStyleRecord($node);
|
||||
$startFillStyles[] = $record[0];
|
||||
$endFillStyles[] = $record[1];
|
||||
}
|
||||
foreach ($element["morphLineStyles"] as $node) {
|
||||
$startColor = isset($node["startColor"]) ? Color::fromArray($node["startColor"]) : null;
|
||||
$endColor = isset($node["endColor"]) ? Color::fromArray($node["endColor"]) : null;
|
||||
|
||||
//TODO: fill flag
|
||||
if ($startColor === null) {
|
||||
var_dump($node);
|
||||
//TODO
|
||||
/*
|
||||
if(isset($node["fillType"])){
|
||||
$color = self::parseFillStyleRecord($node["fillType"])->fill;
|
||||
if($color instanceof Gradient){
|
||||
$color = $color->getItems()[0]->color;
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
if ($endColor === null) {
|
||||
var_dump($node);
|
||||
//TODO
|
||||
/*
|
||||
if(isset($node["fillType"])){
|
||||
$color = self::parseFillStyleRecord($node["fillType"])->fill;
|
||||
if($color instanceof Gradient){
|
||||
$color = $color->getItems()[0]->color;
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
//TODO: any reason for max(Constants::TWIP_SIZE)?
|
||||
$startLineStyles[] = new LineStyleRecord(max(Constants::TWIP_SIZE, $node["startWidth"]), $startColor);
|
||||
$endLineStyles[] = new LineStyleRecord(max(Constants::TWIP_SIZE, $node["endWidth"]), $startColor);
|
||||
}
|
||||
return new MorphStyleList($startFillStyles, $endFillStyles, $startLineStyles, $endLineStyles);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $i
|
||||
* @return FillStyleRecord|null
|
||||
*/
|
||||
public function getStartFillStyle($i): ?FillStyleRecord {
|
||||
return $this->startFillStyles[$i] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $i
|
||||
* @return FillStyleRecord|null
|
||||
*/
|
||||
public function getEndFillStyle($i): ?FillStyleRecord {
|
||||
return $this->endFillStyles[$i] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $i
|
||||
* @return LineStyleRecord|null
|
||||
*/
|
||||
public function getStartLineStyle($i): ?LineStyleRecord {
|
||||
return $this->startLineStyles[$i] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $i
|
||||
* @return LineStyleRecord|null
|
||||
*/
|
||||
public function getEndLineStyle($i): ?LineStyleRecord {
|
||||
return $this->endLineStyles[$i] ?? null;
|
||||
}
|
||||
}
|
|
@ -6,5 +6,5 @@ interface ObjectDefinition {
|
|||
|
||||
public function getObjectId(): int;
|
||||
|
||||
public function getShapeList(): DrawPathList;
|
||||
public function getShapeList(float $ratio): DrawPathList;
|
||||
}
|
|
@ -2,9 +2,6 @@
|
|||
|
||||
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
|
||||
|
@ -64,6 +61,15 @@ class QuadraticCurveRecord implements Record {
|
|||
return new QuadraticCurveRecord($control, $anchor, $cursor);
|
||||
}
|
||||
|
||||
public static function fromLineRecord(LineRecord $l): QuadraticCurveRecord {
|
||||
$delta = $l->to->sub($l->start)->divide(2);
|
||||
return new QuadraticCurveRecord(
|
||||
$l->start->add($delta),
|
||||
$l->start->add($delta->multiply(2)),
|
||||
$l->start
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LineRecord[]
|
||||
*/
|
||||
|
|
|
@ -21,10 +21,6 @@ class RenderedObject {
|
|||
$this->clip = $clip;
|
||||
}
|
||||
|
||||
public function getShape() : Shape{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
|
|
|
@ -42,6 +42,11 @@ class SWFTreeProcessor {
|
|||
}
|
||||
|
||||
switch ($node["tagType"]) {
|
||||
case "DefineMorphShape":
|
||||
case "DefineMorphShape2":
|
||||
$shape = MorphShapeDefinition::fromArray($node);
|
||||
$this->objects->add($shape);
|
||||
break;
|
||||
case "DefineShape":
|
||||
case "DefineShape2":
|
||||
case "DefineShape3":
|
||||
|
@ -75,12 +80,14 @@ class SWFTreeProcessor {
|
|||
break;
|
||||
case "DefineBitsLossless":
|
||||
case "DefineBitsLossless2":
|
||||
break; //TODO
|
||||
$bitmap = BitmapDefinition::fromArray($node);
|
||||
|
||||
$this->objects->add($bitmap);
|
||||
break;
|
||||
case "DefineBitsJPEG2":
|
||||
case "DefineBitsJPEG3":
|
||||
break; //TODO
|
||||
$bitmap = JPEGBitmapDefinition::fromArray($node);
|
||||
$this->objects->add($bitmap);
|
||||
break;
|
||||
|
@ -107,11 +114,11 @@ class SWFTreeProcessor {
|
|||
$this->layout->remove($depth);
|
||||
break;
|
||||
}
|
||||
//TODO: ratio, which also seems to exists for Sprites
|
||||
|
||||
$ratio = isset($node["ratio"]) ? $node["ratio"] / 65535 : null;
|
||||
|
||||
|
||||
|
||||
$transform = isset($node["matrix"]) ? MatrixTransform::fromArray($node["matrix"]) : null;
|
||||
$transform = isset($node["matrix"]) ? MatrixTransform::fromSWFArray($node["matrix"]) : null;
|
||||
|
||||
|
||||
|
||||
|
@ -127,12 +134,16 @@ class SWFTreeProcessor {
|
|||
if ($colorTransform !== null) {
|
||||
$currentObject->setColorTransform($colorTransform);
|
||||
}
|
||||
if($ratio !== null){
|
||||
$currentObject->setRatio($ratio);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$view = $clipDepth !== null ? new ClippingViewLayout($clipDepth, $objectID, $object, $this->layout) : new ViewLayout($objectID, $object, $this->layout);
|
||||
$view = $clipDepth !== null ? new ClippingViewLayout($clipDepth, $objectID, $object, $this->layout, $ratio) : new ViewLayout($objectID, $object, $this->layout, $ratio);
|
||||
$view->setMatrixTransform($transform);
|
||||
$view->setColorTransform($colorTransform);
|
||||
$view->setRatio($ratio);
|
||||
|
||||
if ($replace) {
|
||||
$this->layout->replace($depth, $view);
|
||||
|
|
|
@ -49,7 +49,7 @@ class Shape {
|
|||
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);
|
||||
fgets(STDIN);
|
||||
return [$this]; //TODO: fix this breakage, some clips being overlapping shapes????
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ class ShapeConverter {
|
|||
public DrawPathList $commands;
|
||||
|
||||
|
||||
public function __construct(array $element, StyleList $currentStyles) {
|
||||
public function __construct(array $element, StyleList $currentStyles, ?array $otherElement = null) {
|
||||
$this->styles = $currentStyles;
|
||||
$this->position = new Vector2(0, 0);
|
||||
$this->fills = new PendingPathMap();
|
||||
|
@ -28,7 +28,47 @@ class ShapeConverter {
|
|||
|
||||
$finished = false;
|
||||
|
||||
foreach ($element as $node) {
|
||||
reset($element);
|
||||
if($otherElement !== null){
|
||||
reset($otherElement);
|
||||
}
|
||||
|
||||
do{
|
||||
$otherNode = current($otherElement ?? []);
|
||||
if($otherNode === false){
|
||||
$otherNode = null;
|
||||
}
|
||||
$node = current($element);
|
||||
if($node === false){
|
||||
break;
|
||||
}
|
||||
|
||||
$advanceNode = true;
|
||||
if($otherNode !== null){
|
||||
if($otherNode["type"] === "StyleChangeRecord"){
|
||||
if($node["type"] === "StyleChangeRecord"){
|
||||
//Inject style record entries
|
||||
foreach (["stateNewStyles", "stateLineStyle", "stateFillStyle0", "stateFillStyle1", "fillStyles", "lineStyles", "fillStyle0", "fillStyle1", "lineStyle"] as $k){
|
||||
if(isset($otherNode[$k])){
|
||||
$node[$k] = $otherNode[$k];
|
||||
}
|
||||
}
|
||||
}else{
|
||||
//Inject style record
|
||||
$node = $otherNode;
|
||||
$node["stateMoveTo"] = 0;
|
||||
unset($node["moveDeltaX"]);
|
||||
unset($node["moveDeltaY"]);
|
||||
$advanceNode = false;
|
||||
}
|
||||
}
|
||||
next($otherElement);
|
||||
}
|
||||
|
||||
if($advanceNode){
|
||||
next($element);
|
||||
}
|
||||
|
||||
if($finished){
|
||||
var_dump($node);
|
||||
throw new \Exception("More paths after end");
|
||||
|
@ -97,7 +137,7 @@ class ShapeConverter {
|
|||
} else if ($node["type"] === "EndShapeRecord") {
|
||||
$finished = true;
|
||||
}
|
||||
}
|
||||
}while(true);
|
||||
|
||||
$this->flush_layer();
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ class ShapeDefinition implements ObjectDefinition {
|
|||
return $this->id;
|
||||
}
|
||||
|
||||
public function getShapeList(): DrawPathList {
|
||||
public function getShapeList(?float $ratio): DrawPathList {
|
||||
return $this->shapeList;
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ class SpriteDefinition implements MultiFrameObjectDefinition {
|
|||
return $this->id;
|
||||
}
|
||||
|
||||
public function getShapeList(): DrawPathList {
|
||||
public function getShapeList(?float $ratio): DrawPathList {
|
||||
$list = new DrawPathList();
|
||||
foreach ($this->frames[$this->frameCounter]->render(0, [], null, null)->getObjects() as $object) {
|
||||
$list = $list->merge($object->drawPathList);
|
||||
|
|
|
@ -9,7 +9,11 @@ class StyleList {
|
|||
/** @var LineStyleRecord[] */
|
||||
public array $lineStyles;
|
||||
|
||||
public function __construct($fillStyles = [], $lineStyles = []) {
|
||||
/**
|
||||
* @param FillStyleRecord[] $fillStyles
|
||||
* @param LineStyleRecord[] $lineStyles
|
||||
*/
|
||||
public function __construct(array $fillStyles = [], array $lineStyles = []) {
|
||||
$this->fillStyles = $fillStyles;
|
||||
$this->lineStyles = $lineStyles;
|
||||
}
|
||||
|
@ -20,13 +24,13 @@ class StyleList {
|
|||
return new FillStyleRecord(Color::fromArray($node["color"]));
|
||||
break;
|
||||
case 0x10: // Linear gradient fill
|
||||
return new FillStyleRecord(LinearGradient::fromArray($node["gradient"], MatrixTransform::fromArray($node["matrix"])));
|
||||
return new FillStyleRecord(LinearGradient::fromArray($node["gradient"], MatrixTransform::fromSWFArray($node["matrix"])));
|
||||
break;
|
||||
case 0x12: // Radial gradient fill
|
||||
return new FillStyleRecord(RadialGradient::fromArray($node["gradient"], MatrixTransform::fromArray($node["matrix"])));
|
||||
return new FillStyleRecord(RadialGradient::fromArray($node["gradient"], MatrixTransform::fromSWFArray($node["matrix"])));
|
||||
break;
|
||||
case 0x13: // Focal gradient fill
|
||||
return new FillStyleRecord(FocalGradient::fromArray($node["focalGradient"], MatrixTransform::fromArray($node["matrix"])));
|
||||
return new FillStyleRecord(FocalGradient::fromArray($node["focalGradient"], MatrixTransform::fromSWFArray($node["matrix"])));
|
||||
break;
|
||||
case 0x40: // Repeating bitmap fill
|
||||
case 0x41: // Clipped bitmap fill
|
||||
|
|
|
@ -84,4 +84,8 @@ class Vector2 {
|
|||
public function toPixel($twipSize = Constants::TWIP_SIZE): Vector2 {
|
||||
return $this->divide($twipSize);
|
||||
}
|
||||
|
||||
public function __toString() {
|
||||
return "Vector2({$this->x}, {$this->y})";
|
||||
}
|
||||
}
|
|
@ -16,6 +16,8 @@ class ViewLayout {
|
|||
|
||||
private int $objectId;
|
||||
|
||||
private ?float $ratio = null;
|
||||
|
||||
public function __construct(int $objectId, ?ObjectDefinition $object, ?ViewLayout $parent = null) {
|
||||
$this->objectId = $objectId;
|
||||
if ($object !== null and $objectId !== $object->getObjectId()) {
|
||||
|
@ -98,12 +100,20 @@ class ViewLayout {
|
|||
$this->colorTransform = $transform;
|
||||
}
|
||||
|
||||
public function getRatio() : ?float{
|
||||
return $this->ratio;
|
||||
}
|
||||
|
||||
public function setRatio(?float $ratio){
|
||||
$this->ratio = $ratio;
|
||||
}
|
||||
|
||||
public function nextFrame(ActionList $actionList): ViewFrame {
|
||||
if ($this->object !== null) {
|
||||
if ($this->object instanceof MultiFrameObjectDefinition) {
|
||||
$frame = $this->object->nextFrame();
|
||||
} else {
|
||||
$frame = new ViewFrame($this->getObjectId(), $this->object->getShapeList());
|
||||
$frame = new ViewFrame($this->getObjectId(), $this->object->getShapeList($this->ratio));
|
||||
}
|
||||
} else {
|
||||
$frame = new ViewFrame($this->getObjectId(), null);
|
||||
|
|
|
@ -3,16 +3,18 @@
|
|||
namespace swf2ass\ass;
|
||||
|
||||
use swf2ass\FrameInformation;
|
||||
use swf2ass\MatrixTransform;
|
||||
use swf2ass\Rectangle;
|
||||
use swf2ass\RenderedFrame;
|
||||
use swf2ass\RenderedObject;
|
||||
use swf2ass\Vector2;
|
||||
|
||||
class ASSRenderer {
|
||||
private ?string $header;
|
||||
|
||||
/** @var ASSLine[] */
|
||||
private array $runningBuffer = [];
|
||||
private array $settings = [];
|
||||
private array $settings;
|
||||
|
||||
|
||||
public function getSetting($name, $default = null){
|
||||
|
@ -20,17 +22,17 @@ class ASSRenderer {
|
|||
}
|
||||
|
||||
public function __construct(float $frameRate, Rectangle $viewPort, array $settings = []) {
|
||||
$display = $viewPort->toPixel();
|
||||
$width = $display->getWidth();
|
||||
$height = $display->getHeight();
|
||||
$ar = $width / $height;
|
||||
$this->settings = $settings;
|
||||
$display = $viewPort->toPixel();
|
||||
$width = $display->getWidth() * $this->getSetting("videoScaleMultiplier", 1);
|
||||
$height = $display->getHeight() * $this->getSetting("videoScaleMultiplier", 1);
|
||||
$ar = $width / $height;
|
||||
|
||||
if(($frameRate * 2) <= 60){
|
||||
$frameRate *= 2;
|
||||
}
|
||||
|
||||
$timerPrecision = str_replace(".", ",", sprintf("%.4F", (100 / $this->getSetting("timerSpeed", 100)) * 100));
|
||||
$timerPrecision = sprintf("%.4F", (100 / $this->getSetting("timerSpeed", 100)) * 100);
|
||||
|
||||
$this->header = <<<ASSHEADER
|
||||
[Script Info]
|
||||
|
@ -73,7 +75,12 @@ ASSHEADER;
|
|||
|
||||
$runningBuffer = [];
|
||||
|
||||
$scale = MatrixTransform::scale(new Vector2($this->getSetting("videoScaleMultiplier", 1), $this->getSetting("videoScaleMultiplier", 1)));
|
||||
|
||||
foreach ($objects as $object) {
|
||||
$object = clone $object;
|
||||
$object->matrixTransform = $scale->combine($object->matrixTransform);
|
||||
|
||||
$depth = $object->getDepth();
|
||||
|
||||
/** @var ASSLine $tagsToTransition */
|
||||
|
|
|
@ -5,13 +5,13 @@ namespace swf2ass\ass;
|
|||
use swf2ass\Constants;
|
||||
use swf2ass\LineStyleRecord;
|
||||
use swf2ass\StyleRecord;
|
||||
use swf2ass\Vector2;
|
||||
|
||||
class borderTag implements ASSStyleTag {
|
||||
|
||||
/** @var int|float */
|
||||
private $size;
|
||||
private Vector2 $size;
|
||||
|
||||
public function __construct($size = 0) {
|
||||
public function __construct(Vector2 $size) {
|
||||
$this->size = $size;
|
||||
}
|
||||
|
||||
|
@ -20,17 +20,21 @@ class borderTag implements ASSStyleTag {
|
|||
}
|
||||
|
||||
public function encode(ASSLine $line, float $frameDurationMs): string {
|
||||
return "\\bord" . round($this->size, 2);
|
||||
if($this->size->x === $this->size->y){
|
||||
return sprintf("\\bord%.02F", $this->size->x);
|
||||
}else{
|
||||
return sprintf("\\xbord%.02F\\ybord%.02F", $this->size->x, $this->size->y);
|
||||
}
|
||||
}
|
||||
|
||||
public static function fromStyleRecord(StyleRecord $record): ?borderTag {
|
||||
if ($record instanceof LineStyleRecord) {
|
||||
return new borderTag($record->width / Constants::TWIP_SIZE);
|
||||
return new borderTag(new Vector2($record->width / Constants::TWIP_SIZE, $record->width / Constants::TWIP_SIZE));
|
||||
}
|
||||
return new borderTag();
|
||||
return new borderTag(new Vector2(0, 0));
|
||||
}
|
||||
|
||||
public function equals(ASSTag $tag): bool {
|
||||
return $tag instanceof $this and abs($this->size - $tag->size) <= Constants::EPSILON;
|
||||
return $tag instanceof $this and $this->size->equals($tag->size);
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
namespace swf2ass\ass;
|
||||
|
||||
use swf2ass\Constants;
|
||||
use swf2ass\math\RealNumber;
|
||||
use swf2ass\MatrixTransform;
|
||||
use swf2ass\Vector2;
|
||||
|
||||
|
@ -11,8 +12,10 @@ class matrixTransformTag implements ASSPositioningTag {
|
|||
private scaleTag $scale;
|
||||
private rotationTag $rotation;
|
||||
private shearingTag $shear;
|
||||
private MatrixTransform $transform;
|
||||
|
||||
public function __construct(Vector2 $scale, $rotationX, $rotationY, $rotationZ, $shearX, $shearY) {
|
||||
public function __construct(MatrixTransform $transform, Vector2 $scale, $rotationX, $rotationY, $rotationZ, $shearX, $shearY) {
|
||||
$this->transform = $transform;
|
||||
$this->scale = new scaleTag($scale);
|
||||
$this->rotation = new rotationTag($rotationX, $rotationY, $rotationZ);
|
||||
$this->shear = new shearingTag(new Vector2($shearX, $shearY));
|
||||
|
@ -24,257 +27,20 @@ class matrixTransformTag implements ASSPositioningTag {
|
|||
}
|
||||
|
||||
public function encode(ASSLine $line, float $frameDurationMs): string {
|
||||
return $this->scale->encode($line, $frameDurationMs) . $this->rotation->encode($line, $frameDurationMs) . $this->shear->encode($line, $frameDurationMs);
|
||||
return sprintf("\\matrix(%.05F,%.05F,%.05F,%.05F,%.02F,%.02F)", $this->transform->get_a()->toFloat(), $this->transform->get_b()->toFloat(), $this->transform->get_c()->toFloat(), $this->transform->get_d()->toFloat(), $this->transform->get_tx()->divide(Constants::TWIP_SIZE)->toFloat(), $this->transform->get_ty()->divide(Constants::TWIP_SIZE)->toFloat())
|
||||
. $this->scale->encode($line, $frameDurationMs) . $this->rotation->encode($line, $frameDurationMs) . $this->shear->encode($line, $frameDurationMs);
|
||||
}
|
||||
|
||||
public function equals(ASSTag $tag): bool {
|
||||
return $tag instanceof $this and $this->scale->equals($tag->scale) and $this->rotation->equals($tag->rotation) and $this->shear->equals($tag->shear);
|
||||
return $tag instanceof $this and $this->transform->equals($tag->transform) and $this->scale->equals($tag->scale) and $this->rotation->equals($tag->rotation) and $this->shear->equals($tag->shear);
|
||||
}
|
||||
|
||||
public static function fromMatrixTransform(MatrixTransform $transform): ?matrixTransformTag {
|
||||
/*
|
||||
!! Everything here is untested and best check for errors in the calculations !!
|
||||
|
||||
Choose \fay, \fax, \fscx and \fscy to create a (almost) general 2D-transform-matrix
|
||||
(Rotations are sometimes also used)
|
||||
|
||||
(ASS Transform order: combined faxy, scale, frz, frx, fry)
|
||||
|
||||
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;
|
||||
$isZero = function (RealNumber $v) : bool{
|
||||
return $v->isZero(new RealNumber("0.00001")); ///TODO
|
||||
};
|
||||
|
||||
$sign = function (float $a) : int {
|
||||
if($a < 0){
|
||||
return -1;
|
||||
}else{
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
$scale_x = $scale_y = $frx = $fry = $frz = $fax = $fay = new RealNumber(0);
|
||||
|
||||
$a = $transform->get_a();
|
||||
$b = $transform->get_b();
|
||||
|
@ -285,113 +51,79 @@ class matrixTransformTag implements ASSPositioningTag {
|
|||
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)))
|
||||
!(
|
||||
($isZero($a) and !$isZero($b))
|
||||
or
|
||||
($isZero($d) and !$isZero($c))
|
||||
)
|
||||
){
|
||||
//Trivial case
|
||||
$scale_x = $a;
|
||||
$scale_y = $d;
|
||||
$fax = $b / $scale_x;
|
||||
$fay = $c / $scale_y;
|
||||
|
||||
$sgn_x = $sign($scale_x);
|
||||
$sgn_y = $sign($scale_y);
|
||||
$fax = $isZero($a) ? new RealNumber(0) : $b->divide($a);
|
||||
$fay = $isZero($d) ? new RealNumber(0) : $c->divide($d);
|
||||
|
||||
if($sgn_x === -1){
|
||||
$fry = 180;
|
||||
if($scale_x->lesser(0)){
|
||||
$fry = new RealNumber(180);
|
||||
}
|
||||
if($sgn_y === -1){
|
||||
$frx = 180;
|
||||
if($scale_y->lesser(0)){
|
||||
$frx = new RealNumber(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)))
|
||||
}elseif (
|
||||
!(
|
||||
($isZero($b) and !$isZero($a))
|
||||
or
|
||||
($isZero($c) and !$isZero($d))
|
||||
)
|
||||
){
|
||||
//Rowswap
|
||||
|
||||
$frz = new RealNumber(90);
|
||||
$scale_x = $c;
|
||||
$scale_y = -$b;
|
||||
$fax = $d / $scale_x;
|
||||
$fay = $a / -$scale_y;
|
||||
$scale_y = $b->negate();
|
||||
|
||||
$scale_x *= -1;
|
||||
$fax = $isZero($c) ? new RealNumber(0) : $d->divide($c);
|
||||
$fay = $isZero($b) ? new RealNumber(0) : $a->divide($b);
|
||||
|
||||
$sgn_x = $sign($scale_x);
|
||||
$sgn_y = $sign($scale_y);
|
||||
|
||||
if($sgn_y === -1){
|
||||
$fry = 180;
|
||||
if($scale_x->lesser(0)){
|
||||
$frx = new RealNumber(180);
|
||||
}
|
||||
if($sgn_x === -1){
|
||||
$frx = 180;
|
||||
if($scale_y->lesser(0)){
|
||||
$fry = new RealNumber(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))
|
||||
}elseif (
|
||||
$isZero($a) and $isZero($c) and !$isZero($b) 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))
|
||||
//Zero Col Left
|
||||
$scale_y = new RealNumber(0);
|
||||
$fax = new RealNumber(0);
|
||||
$fay = new RealNumber(0);
|
||||
$scale_x = $b->power(2)->add($d->power(2))->sqrt();
|
||||
$frz = new RealNumber(atan($b->negate()->toFloat() / $d->toFloat()) * (180 / M_PI));
|
||||
if($a->lesser(0)){ // atan always yields positive cos
|
||||
$frz = $frz->add(180);
|
||||
}
|
||||
}elseif (
|
||||
!$isZero($a) and !$isZero($c) and $isZero($b) and $isZero($d)
|
||||
){
|
||||
//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");
|
||||
//Zero Col Right
|
||||
$scale_y = new RealNumber(0);
|
||||
$fax = new RealNumber(0);
|
||||
$fay = new RealNumber(0);
|
||||
$scale_x = $a->power(2)->add($c->power(2))->sqrt();
|
||||
$frz = new RealNumber(atan($c->toFloat() / $a->toFloat()) * (180 / M_PI));
|
||||
if($a->lesser(0)){ // atan always yields positive cos
|
||||
$style->frz = $frz->add(180);
|
||||
}
|
||||
}else {
|
||||
throw new \Exception("Invalid transform state. This should not happen");
|
||||
}
|
||||
*/
|
||||
|
||||
$fscx = abs($scale_x) * 100;
|
||||
$fscy = abs($scale_y) * 100;
|
||||
$fscx = $scale_x->absolute()->multiply(100);
|
||||
$fscy = $scale_y->absolute()->multiply(100);
|
||||
|
||||
return new matrixTransformTag(new Vector2($fscx, $fscy), $frx, $fry, $frz, $fax, $fay);
|
||||
return new matrixTransformTag($transform, new Vector2($fscx->toFloat(), $fscy->toFloat()), $frx->toFloat(), $fry->toFloat(), $frz->toFloat(), $fax->toFloat(), $fay->toFloat());
|
||||
}
|
||||
}
|
83
src/math/RealMatrix.php
Normal file
83
src/math/RealMatrix.php
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
namespace swf2ass\math;
|
||||
|
||||
use MathPHP\LinearAlgebra\Matrix;
|
||||
use MathPHP\LinearAlgebra\ObjectMatrix;
|
||||
|
||||
class RealMatrix extends ObjectMatrix{
|
||||
|
||||
public function __construct($value){
|
||||
|
||||
if(!$value instanceof RealMatrix){
|
||||
|
||||
if($value instanceof Matrix){
|
||||
$value = $value->getMatrix();
|
||||
}
|
||||
foreach ($value as $i => $r){
|
||||
foreach ($r as $j => $v){
|
||||
if(!($v instanceof RealNumber)){
|
||||
$value[$i][$j] = new RealNumber($v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parent::__construct($value);
|
||||
}else{
|
||||
parent::__construct($value->A);
|
||||
}
|
||||
}
|
||||
|
||||
public function get(int $i, int $j) : RealNumber {
|
||||
return parent::get($i, $j);
|
||||
}
|
||||
|
||||
public function multiply($B): RealMatrix {
|
||||
return new RealMatrix(parent::multiply(new RealMatrix($B)));
|
||||
}
|
||||
|
||||
public function transpose(): RealMatrix {
|
||||
return new RealMatrix(parent::transpose());
|
||||
}
|
||||
|
||||
public function submatrix(int $m₁, int $n₁, int $m₂, int $n₂): RealMatrix {
|
||||
return new RealMatrix(parent::submatrix($m₁, $n₁, $m₂, $n₂));
|
||||
}
|
||||
|
||||
public function isSquare(): bool {
|
||||
return $this->m === $this->n;
|
||||
}
|
||||
|
||||
public function isEqual(Matrix $B): bool
|
||||
{
|
||||
if (!$this->isEqualSizeAndType($B)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$m = $this->m;
|
||||
$n = $this->n;
|
||||
// All elements are the same
|
||||
for ($i = 0; $i < $m; $i++) {
|
||||
for ($j = 0; $j < $n; $j++) {
|
||||
/** @var $B RealNumber[][] */
|
||||
if (!$B[$i][$j]->equalWithEpsilon($this->A[$i][$j])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function __toString(): string {
|
||||
return \trim(\array_reduce(\array_map(
|
||||
function ($mᵢ) {
|
||||
return '[' . \implode(', ', array_map(function (RealNumber $n){return $n->toFixed(7);}, $mᵢ)) . ']';
|
||||
},
|
||||
$this->A
|
||||
), function ($A, $mᵢ) {
|
||||
return $A . \PHP_EOL . $mᵢ;
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
213
src/math/RealNumber.php
Normal file
213
src/math/RealNumber.php
Normal file
|
@ -0,0 +1,213 @@
|
|||
<?php
|
||||
|
||||
namespace swf2ass\math;
|
||||
|
||||
use Decimal\Decimal;
|
||||
use MathPHP\Number\ObjectArithmetic;
|
||||
|
||||
class RealNumber implements ObjectArithmetic {
|
||||
|
||||
private static ?RealNumber $pi = null;
|
||||
private static ?RealNumber $epsilon = null;
|
||||
private Decimal $decimal;
|
||||
const DECIMAL_PRECISION = Decimal::DEFAULT_PRECISION;
|
||||
/**
|
||||
* @param RealNumber|Decimal|int|float|string $value
|
||||
*/
|
||||
public function __construct($value){
|
||||
if($value instanceof Decimal){
|
||||
$this->decimal = $value;
|
||||
}else if($value instanceof RealNumber){
|
||||
$this->decimal = $value->decimal;
|
||||
}else if(is_float($value)){
|
||||
$this->decimal = new Decimal(sprintf("%.0" . PHP_FLOAT_DIG . "F", $value));
|
||||
}else if(is_string($value) or is_integer($value)){
|
||||
$this->decimal = new Decimal($value, self::DECIMAL_PRECISION + 1);
|
||||
}else{
|
||||
$this->decimal = new Decimal($value, self::DECIMAL_PRECISION + 1);
|
||||
}
|
||||
}
|
||||
|
||||
public static function pi() : RealNumber{
|
||||
return self::$pi ?? (self::$pi = new RealNumber("3.1415926535897932384626433832"));
|
||||
}
|
||||
|
||||
public static function setDefaultEpsilon(RealNumber $number){
|
||||
self::$epsilon = $number;
|
||||
}
|
||||
|
||||
public static function epsilon() :RealNumber{
|
||||
return self::$epsilon ?? (self::$epsilon = new RealNumber("0.0000000000001"));
|
||||
}
|
||||
|
||||
public function greater($object_or_scalar) : bool{
|
||||
return $this->compareTo($object_or_scalar) === 1;
|
||||
}
|
||||
|
||||
public function equalWithEpsilon($object_or_scalar, ?RealNumber $epsilon = null) : bool{
|
||||
if($epsilon === null){
|
||||
$epsilon = self::epsilon();
|
||||
}
|
||||
return $this->subtract($object_or_scalar)->absolute()->lesserEqual($epsilon);
|
||||
}
|
||||
|
||||
public function isZero(?RealNumber $epsilon = null) : bool{
|
||||
return $this->equalWithEpsilon(0, $epsilon);//$this->decimal->isZero();
|
||||
}
|
||||
|
||||
public function equal($object_or_scalar) : bool{
|
||||
return $this->compareTo($object_or_scalar) === 0;
|
||||
}
|
||||
|
||||
public function lesser($object_or_scalar) : bool{
|
||||
return $this->compareTo($object_or_scalar) === -1;
|
||||
}
|
||||
|
||||
public function greaterEqual($object_or_scalar) : bool{
|
||||
return $this->compareTo($object_or_scalar) >= 0;
|
||||
}
|
||||
|
||||
public function lesserEqual($object_or_scalar) : bool{
|
||||
return $this->compareTo($object_or_scalar) <= 0;
|
||||
}
|
||||
|
||||
public function compareTo($object_or_scalar) : int{
|
||||
if($object_or_scalar instanceof RealNumber){
|
||||
return $this->decimal->compareTo($object_or_scalar->decimal);
|
||||
}else if($object_or_scalar instanceof Decimal){
|
||||
return $this->decimal->compareTo($object_or_scalar);
|
||||
}else{
|
||||
return $this->decimal->compareTo((new RealNumber($object_or_scalar))->decimal);
|
||||
}
|
||||
}
|
||||
|
||||
public function negate() : RealNumber{
|
||||
return new RealNumber($this->decimal->negate());
|
||||
}
|
||||
|
||||
public function cosine(int $terms = 12) : RealNumber{
|
||||
if($this->decimal->isZero()){
|
||||
return new RealNumber(1);
|
||||
}
|
||||
|
||||
$div = $this->divide(self::pi())->toInteger();
|
||||
$x = $this->subtract(self::pi()->multiply($div));
|
||||
$sign = 1;
|
||||
if($div % 2 !== 0){
|
||||
$sign = -1;
|
||||
}
|
||||
|
||||
$result = new RealNumber(1);
|
||||
$inter = new RealNumber(1);
|
||||
$num = $x->power(2);
|
||||
for($i = 1; $i <= $terms; ++$i){
|
||||
$comp = 2 * $i;
|
||||
$den = $comp * ($comp - 1);
|
||||
$inter = $inter->multiply($num->divide($den));
|
||||
if($i % 2 === 0){
|
||||
$result = $result->add($inter);
|
||||
}else{
|
||||
$result = $result->subtract($inter);
|
||||
}
|
||||
}
|
||||
|
||||
return $result->multiply($sign);
|
||||
}
|
||||
|
||||
public function sine(int $terms = 12) : RealNumber{
|
||||
if($this->decimal->isZero()){
|
||||
return new RealNumber(0);
|
||||
}
|
||||
return self::pi()->divide(2)->subtract($this)->cosine($terms);
|
||||
}
|
||||
|
||||
public function getDecimal() : Decimal{
|
||||
return $this->decimal;
|
||||
}
|
||||
|
||||
public function toFloat() : float{
|
||||
return $this->decimal->toFloat();
|
||||
}
|
||||
|
||||
public function toFixed($places = self::DECIMAL_PRECISION) : string{
|
||||
return $this->decimal->trim()->toFixed($places);
|
||||
}
|
||||
|
||||
public function toInteger() : int{
|
||||
return $this->decimal->toInt();
|
||||
}
|
||||
|
||||
public function __toString(){
|
||||
return $this->toFixed();
|
||||
}
|
||||
|
||||
public function __debugInfo(){
|
||||
return [
|
||||
"precision" => $this->getDecimal()->precision(),
|
||||
"value" => $this->toFixed()
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public function add($object_or_scalar): RealNumber {
|
||||
if($object_or_scalar instanceof RealNumber){
|
||||
return new RealNumber($this->decimal->add($object_or_scalar->decimal));
|
||||
}else if($object_or_scalar instanceof Decimal){
|
||||
return new RealNumber($this->decimal->add($object_or_scalar));
|
||||
}else{
|
||||
return new RealNumber($this->decimal->add((new RealNumber($object_or_scalar))->decimal));
|
||||
}
|
||||
}
|
||||
|
||||
public function subtract($object_or_scalar): RealNumber {
|
||||
if($object_or_scalar instanceof RealNumber){
|
||||
return new RealNumber($this->decimal->sub($object_or_scalar->decimal));
|
||||
}else if($object_or_scalar instanceof Decimal){
|
||||
return new RealNumber($this->decimal->sub($object_or_scalar));
|
||||
}else{
|
||||
return new RealNumber($this->decimal->sub((new RealNumber($object_or_scalar))->decimal));
|
||||
}
|
||||
}
|
||||
|
||||
public function absolute(): RealNumber {
|
||||
return new RealNumber($this->decimal->abs());
|
||||
}
|
||||
|
||||
public function sqrt(): RealNumber {
|
||||
return new RealNumber($this->decimal->sqrt());
|
||||
}
|
||||
|
||||
public function power($object_or_scalar): RealNumber {
|
||||
if($object_or_scalar instanceof RealNumber){
|
||||
return new RealNumber($this->decimal->pow($object_or_scalar->decimal));
|
||||
}else if($object_or_scalar instanceof Decimal){
|
||||
return new RealNumber($this->decimal->pow($object_or_scalar));
|
||||
}else{
|
||||
return new RealNumber($this->decimal->pow((new RealNumber($object_or_scalar))->decimal));
|
||||
}
|
||||
}
|
||||
|
||||
public function multiply($object_or_scalar): RealNumber {
|
||||
if($object_or_scalar instanceof RealNumber){
|
||||
return new RealNumber($this->decimal->mul($object_or_scalar->decimal));
|
||||
}else if($object_or_scalar instanceof Decimal){
|
||||
return new RealNumber($this->decimal->mul($object_or_scalar));
|
||||
}else{
|
||||
return new RealNumber($this->decimal->mul((new RealNumber($object_or_scalar))->decimal));
|
||||
}
|
||||
}
|
||||
|
||||
public function divide($object_or_scalar): RealNumber {
|
||||
if($object_or_scalar instanceof RealNumber){
|
||||
return new RealNumber($this->decimal->div($object_or_scalar->decimal));
|
||||
}else if($object_or_scalar instanceof Decimal){
|
||||
return new RealNumber($this->decimal->div($object_or_scalar));
|
||||
}else{
|
||||
return new RealNumber($this->decimal->div((new RealNumber($object_or_scalar))->decimal));
|
||||
}
|
||||
}
|
||||
|
||||
public static function createZeroValue(): RealNumber {
|
||||
return new RealNumber(0);
|
||||
}
|
||||
}
|
157
swf2ass.php
157
swf2ass.php
|
@ -2,33 +2,98 @@
|
|||
|
||||
require_once __DIR__ . "/vendor/autoload.php";
|
||||
|
||||
$swf = new \swf\SWF(file_get_contents($argv[1]));
|
||||
$settings = [
|
||||
"videoScaleMultiplier" => 1, //TODO: not finished, leave at 1
|
||||
"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,
|
||||
];
|
||||
|
||||
$swfContent = file_get_contents($argv[1]);
|
||||
$signature = hash("sha256", $swfContent, false);
|
||||
|
||||
|
||||
$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();
|
||||
$swf = new \swf\SWF($swfContent);
|
||||
$swfContent = null;
|
||||
unset($swfContent);
|
||||
|
||||
$fp = fopen($argv[2], "w+");
|
||||
|
||||
$fromFrame = isset($argv[3]) ? (int) $argv[3] : null;
|
||||
$frameEnd = isset($argv[4]) ? (int) $argv[4] : null;
|
||||
if($fromFrame !== null and $frameEnd === null){
|
||||
$frameEnd = $fromFrame;
|
||||
}
|
||||
|
||||
class RemovalEntry{
|
||||
//TODO: accept names as well?
|
||||
public ?int $objectId;
|
||||
public ?array $depth;
|
||||
|
||||
/**
|
||||
* @param int $objectId
|
||||
* @param int[] $depth
|
||||
*/
|
||||
public function __construct(?int $objectId, ?array $depth){
|
||||
$this->objectId = $objectId;
|
||||
$this->depth = $depth;
|
||||
}
|
||||
|
||||
public function equals(\swf2ass\RenderedObject $object): bool {
|
||||
return ($this->objectId === null or $object->objectId === $this->objectId) and ($this->depth === null or (count($object->depth) >= count($this->depth) and array_slice($object->depth, 0, count($this->depth)) === $this->depth));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$frameOffset = 0;
|
||||
|
||||
$objectRemovalEntries = [];
|
||||
|
||||
//TODO: make this a JSON file elsewhere
|
||||
$knownFlashSignatures = [
|
||||
"52e75b7d6831293ebf4e5b28574a60f5bce10b1eae8afa4e69ab213a98b0b008" => [
|
||||
"name" => "IJSW.swf",
|
||||
"remove" => [
|
||||
//removes playback menus
|
||||
new RemovalEntry(null /*2*/, [0, 31]),
|
||||
new RemovalEntry(null /*3*/, [0, 145]),
|
||||
new RemovalEntry(null /*69*/, [0, 146]),
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
if(isset($knownFlashSignatures[$signature])){
|
||||
$e = $knownFlashSignatures[$signature];
|
||||
echo "Found known signature for " . $e["name"] .", adding rules\n";
|
||||
$objectRemovalEntries = $e["remove"];
|
||||
if(isset($e["frameOffset"])){
|
||||
$frameOffset = $e["frameOffset"];
|
||||
}
|
||||
if(isset($e["frameEnd"])){
|
||||
$frameEnd = $e["frameEnd"];
|
||||
}
|
||||
}
|
||||
|
||||
//Function to decide whether to display object or not
|
||||
function filterObject(\swf2ass\RenderedObject $object) : bool{
|
||||
global $objectRemovalEntries;
|
||||
/** @var RemovalEntry[] $objectRemovalEntries */
|
||||
foreach ($objectRemovalEntries as $entry){
|
||||
if($entry->equals($object)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
$testVectors = [];
|
||||
|
||||
if ($swf->header["signature"]) {
|
||||
$processor = new \swf2ass\SWFProcessor($swf);
|
||||
$assRenderer = new \swf2ass\ass\ASSRenderer($processor->getFrameRate(), $processor->getViewPort(), [
|
||||
"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
|
||||
]);
|
||||
$assRenderer = new \swf2ass\ass\ASSRenderer($processor->getFrameRate(), $processor->getViewPort(), $settings);
|
||||
|
||||
$keyFrameInterval = 10 * $processor->getFrameRate(); //kf every 10 seconds TODO: make this dynamic, per-shape
|
||||
$frameOffset = 0;
|
||||
$lastFrame = null;
|
||||
while(($frame = $processor->nextFrameOutput()) !== null){
|
||||
$audio = $processor->getAudio();
|
||||
|
@ -44,11 +109,24 @@ if ($swf->header["signature"]) {
|
|||
|
||||
$rendered = $frame->getFrame()->render(0, [], null, null);
|
||||
|
||||
if($frame->getFrameNumber() === 0){
|
||||
foreach ($rendered->getObjects() as $ob){
|
||||
echo "frame 0: object {$ob->objectId} depth: " . implode(",", $ob->depth) . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
$filteredRendered = new \swf2ass\RenderedFrame();
|
||||
|
||||
$drawCalls = 0;
|
||||
$drawItems = 0;
|
||||
$filteredObjects = 0;
|
||||
$clipCalls = 0;
|
||||
$clipItems = 0;
|
||||
foreach ($rendered->getObjects() as $object){
|
||||
if(filterObject($object)){
|
||||
++$filteredObjects;
|
||||
continue;
|
||||
}
|
||||
if($object->clip !== null){
|
||||
++$clipCalls;
|
||||
$clipItems += count($object->clip->getShape()->getRecords());
|
||||
|
@ -57,10 +135,48 @@ if ($swf->header["signature"]) {
|
|||
++$drawCalls;
|
||||
$drawItems += count($path->commands->getRecords());
|
||||
}
|
||||
$filteredRendered->add($object);
|
||||
}
|
||||
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;
|
||||
echo "=== frame ".$frame->getFrameNumber()."/".$processor->getExpectedFrameCount()." ~ $frameOffset : Depth count: " . count($frame->getFrame()->getDepthMap()) . " :: Object count: " . count($filteredRendered->getObjects()) ." :: Paths: $drawCalls draw calls, $drawItems items :: Filtered: $filteredObjects :: Clips: $clipCalls draw calls, $clipItems items" . PHP_EOL;
|
||||
|
||||
foreach ($assRenderer->renderFrame($frame, $rendered) as $line){
|
||||
|
||||
if($fromFrame !== null){
|
||||
if($frame->getFrameNumber() < $fromFrame){
|
||||
continue;
|
||||
}else{
|
||||
foreach ($rendered->getObjects() as $object){
|
||||
$count = 0;
|
||||
|
||||
foreach ($object->drawPathList->commands as $i => $path){
|
||||
foreach ($path->commands->getRecords() as $j => $record){
|
||||
$v = [
|
||||
"objectId" => $object->objectId,
|
||||
"depth" => implode(".", $object->getDepth()) . "[$i][$j]",
|
||||
"transform" => $object->matrixTransform->toArray(false)
|
||||
];
|
||||
|
||||
if($record instanceof \swf2ass\MoveRecord or $record instanceof \swf2ass\LineRecord){
|
||||
$v["vector"] = $record->start->toArray();
|
||||
$testVectors[] = $v;
|
||||
$v["vector"] = $record->to->toArray();
|
||||
$testVectors[] = $v;
|
||||
}else if($record instanceof \swf2ass\QuadraticCurveRecord){
|
||||
$v["vector"] = $record->start->toArray();
|
||||
$testVectors[] = $v;
|
||||
$v["vector"] = $record->control->toArray();
|
||||
$testVectors[] = $v;
|
||||
$v["vector"] = $record->anchor->toArray();
|
||||
$testVectors[] = $v;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($assRenderer->renderFrame($frame, $filteredRendered) as $line){
|
||||
fwrite($fp, $line . "\n");
|
||||
}
|
||||
|
||||
|
@ -70,12 +186,19 @@ if ($swf->header["signature"]) {
|
|||
}
|
||||
}
|
||||
|
||||
if($frameEnd !== null and $frame->getFrameNumber() >= $frameEnd){
|
||||
break;
|
||||
}
|
||||
}
|
||||
foreach ($assRenderer->flush($lastFrame) as $line){
|
||||
fwrite($fp, $line . "\n");
|
||||
}
|
||||
}
|
||||
|
||||
if(count($testVectors) > 0){
|
||||
file_put_contents($argv[2] . ".test.json", json_encode($testVectors, JSON_PRETTY_PRINT));
|
||||
}
|
||||
|
||||
fclose($fp);
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue