Fix colors, add multi layer system WiP, fix single frame move precision, reduced drawing commands

This commit is contained in:
DataHoarder 2022-01-01 10:54:27 +01:00
parent bc592291bb
commit ec04e81253
18 changed files with 206 additions and 94 deletions

View file

@ -107,7 +107,7 @@ class SWFio {
public function collectSB($num) {
$val = $this->collectBits($num);
if ($val >= (1 << ($num - 1))) { // If high bit is set
if ($num > 0 and ($val & (1 << ($num - 1))) > 0) { // If high bit is set
$val -= 1 << $num; // Negate
}
return $val;

View file

@ -23,9 +23,9 @@ namespace swf;
class SWFrec {
private $io; // SWF for basic I/O
private SWFio $io; // SWF for basic I/O
public function __construct($io) {
public function __construct(SWFio $io) {
$this->io = $io;
}

View file

@ -24,11 +24,11 @@ namespace swf;
class SWFtag {
private $io; // SWFio for basic I/O
private $rec; // SWFrec for simple and complex records
private $swfVersion; // Version of this SWF file
private SWFio $io; // SWFio for basic I/O
private SWFrec $rec; // SWFrec for simple and complex records
private int $swfVersion; // Version of this SWF file
public function __construct($io, $rec, $swfVersion) {
public function __construct(SWFio $io, SWFrec $rec, int $swfVersion) {
$this->io = $io;
$this->rec = $rec;
$this->swfVersion = $swfVersion;

10
src/Circle.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace swf2ass;
class Circle extends Oval {
public function __construct(Vector2 $center, float $radius) {
parent::__construct($center, new Vector2($radius, $radius));
}
}

View file

@ -13,7 +13,7 @@ class ColorTransform {
}
public static function identity(): ColorTransform {
return new ColorTransform(new Color(256, 256, 256, 256), new Color(0, 0, 0));
return new ColorTransform(new Color(256, 256, 256, 256), new Color(0, 0, 0, 0));
}
public function applyToStyleRecord(StyleRecord $record): StyleRecord {

View file

@ -3,5 +3,8 @@
namespace swf2ass;
interface ComplexShape {
public function draw();
/**
* @return Record[]
*/
public function draw() : array;
}

View file

@ -28,6 +28,26 @@ class CubicCurveRecord implements Record {
}
public static function fromQuadraticRecord(QuadraticCurveRecord $q): CubicCurveRecord {
return new CubicCurveRecord($q->start->add($q->control->multiply(2))->divide(3), $q->anchor->add($q->control->multiply(2))->divide(3), $q->anchor, $q->start);
return new CubicCurveRecord(
$q->start->add($q->control->multiply(2))->divide(3),
$q->anchor->add($q->control->multiply(2))->divide(3),
$q->anchor,
$q->start
);
}
/**
* Finds if Cubic curve is a perfect fit of a Quadratic curve (aka, it was upconverted)
*
* @return ?QuadraticCurveRecord
*/
public function toSingleQuadraticRecord() : ?QuadraticCurveRecord{
$control1 = $this->control1->multiply(3)->sub($this->start)->divide(2);
$control2 = $this->control2->multiply(3)->sub($this->anchor)->divide(2);
if($control1->equals($control2)){
return new QuadraticCurveRecord($control1, $this->anchor, $this->start);
}
return null;
}
}

View file

@ -9,24 +9,55 @@ use MathPHP\LinearAlgebra\NumericMatrix;
class MatrixTransform {
private NumericMatrix $matrix;
private Vector2 $translation;
public function __construct(?Vector2 $scale, ?Vector2 $rotateSkew, ?Vector2 $translation) {
$this->translation = $translation ?? new Vector2(0, 0);
$this->matrix = MatrixFactory::createNumeric([
[$scale !== null ? $scale->x : 1, $rotateSkew !== null ? $rotateSkew->y : 0],
[$rotateSkew !== null ? $rotateSkew->x : 0, $scale !== null ? $scale->y : 1]
[$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]
]);
}
public static function identity(): MatrixTransform {
return new MatrixTransform(new Vector2(1, 1), new Vector2(0, 0), new Vector2(0, 0));
public static function scale(Vector2 $scale) : MatrixTransform{
return new MatrixTransform($scale, null, null);
}
public static function rotate(float $angle) : MatrixTransform{
$cos = cos($angle);
$sin = sin($angle);
return new MatrixTransform(new Vector2($cos, $cos), new Vector2(-$sin, $sin), null);
}
public static function translate(Vector2 $translation) : MatrixTransform{
return new MatrixTransform(null, null, $translation);
}
public static function identity(): MatrixTransform {
return new MatrixTransform(null, null, null);
}
//TODO: skewX, skewY
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 = $this->matrix->multiply($other->matrix);
$result->translation = $this->translation->add($other->translation); //TODO check
$result->matrix = $other->matrix->multiply($this->matrix);
return $result;
}
@ -47,11 +78,11 @@ class MatrixTransform {
}
public function get_e(){
return $this->translation->x;
return $this->matrix->get(2, 0);
}
public function get_f(){
return $this->translation->y;
return $this->matrix->get(2, 1);
}
/**
@ -83,9 +114,11 @@ class MatrixTransform {
$result->scale = new Vector2($this->get_a(), $this->get_d());
$result->skew = new Vector2($this->get_c(), $this->get_b());
$result->translation = $this->translation;
$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());
@ -143,12 +176,16 @@ class MatrixTransform {
}
public function getTranslation(): Vector2 {
return $this->translation;
return $this->applyToVector(new Vector2(0, 0));
}
public function applyToVector(Vector2 $vector, bool $applyTranslation = true): Vector2 {
$result = MatrixFactory::createNumeric([[$vector->x, $vector->y]])->multiply($this->matrix);
return $applyTranslation ? (new Vector2($result->get(0, 0), $result->get(0, 1)))->add($this->translation) : new Vector2($result->get(0, 0), $result->get(0, 1));
if($applyTranslation){
$result = MatrixFactory::createFromRowVector([$vector->x, $vector->y, 1])->multiply($this->matrix);
}else{
$result = MatrixFactory::createFromRowVector([$vector->x, $vector->y])->multiply($this->matrix->submatrix(0, 0, 1, 1));
}
return new Vector2($result->get(0, 0), $result->get(0, 1));
}
public function applyToShape(Shape $shape, bool $applyTranslation = true): Shape {
@ -161,7 +198,7 @@ class MatrixTransform {
}
public function equals(MatrixTransform $other): bool {
return $this->translation->equals($other->translation) and $this->matrix->isEqual($other->matrix);
return $this->matrix->isEqual($other->matrix);
}
static function fromArray(array $element): MatrixTransform {

36
src/Oval.php Normal file
View file

@ -0,0 +1,36 @@
<?php
namespace swf2ass;
class Oval implements ComplexShape {
protected const c = 0.55228474983; // (4/3) * (sqrt(2) - 1)
//protected const c = 0.551915024494; // https://spencermortensen.com/articles/bezier-circle/
public Vector2 $center;
public Vector2 $radius;
public function __construct(Vector2 $center, Vector2 $radius) {
$this->center = $center;
$this->radius = $radius;
}
protected function getQuarter(Vector2 $size) : CubicCurveRecord{
return new CubicCurveRecord(
new Vector2($this->center->x - $size->x, $this->center->y - self::c * $size->y),
new Vector2($this->center->x - self::c * $size->x, $this->center->y - $size->y),
new Vector2($this->center->x, $this->center->y - $size->y),
new Vector2($this->center->x - $size->x, $this->center->y)
);
}
public function draw(): array {
return [
$this->getQuarter(new Vector2(-$this->radius->x, $this->radius->y)),
$this->getQuarter($this->radius),
$this->getQuarter(new Vector2($this->radius->x, -$this->radius->y)),
$this->getQuarter(new Vector2(-$this->radius->x, -$this->radius->y)),
];
}
}

View file

@ -54,24 +54,4 @@ class Rectangle implements ComplexShape {
public static function fromArray(array $element): Rectangle {
return new Rectangle(new Vector2($element["xmin"], $element["ymin"]), new Vector2($element["xmax"], $element["ymax"]));
}
public static function fromData($bitdata, &$offset): Rectangle {
$nbits = Utils::binary2dec(substr($bitdata, $offset, 5));
var_dump($nbits);
$offset += 5;
$xMin = Utils::binary2dec(substr($bitdata, $offset, $nbits));
$offset += $nbits;
$xMax = Utils::binary2dec(substr($bitdata, $offset, $nbits));
$offset += $nbits;
$yMin = Utils::binary2dec(substr($bitdata, $offset, $nbits));
$offset += $nbits;
$yMax = Utils::binary2dec(substr($bitdata, $offset, $nbits));
$offset += $nbits;
return new Rectangle(new Vector2($xMin, $yMin), new Vector2($xMax, $yMax));
}
}

View file

@ -56,6 +56,7 @@ class SWFTreeProcessor {
$framesCount = $node["frameCount"];
$spriteTree = new SWFTreeProcessor($objectID, $node["tags"], $this->objects);
$actions = new ActionList();
/** @var ViewFrame[] $frames */
$frames = [];
while (($frame = $spriteTree->nextFrame($actions)) !== null) {
@ -106,6 +107,8 @@ class SWFTreeProcessor {
$this->layout->remove($depth);
break;
}
//TODO: ratio, which also seems to exists for Sprites
$transform = isset($node["matrix"]) ? MatrixTransform::fromArray($node["matrix"]) : null;
@ -117,16 +120,14 @@ class SWFTreeProcessor {
$currentObject = $this->layout->get($depth);
if ($replace and $currentObject !== null) {
if ($currentObject->getObjectId() === $object->getObjectId()) {
if ($transform !== null) {
$currentObject->setMatrixTransform($transform);
}
if ($colorTransform !== null) {
$currentObject->setColorTransform($colorTransform);
}
break;
if ($replace and $currentObject !== null and $currentObject->getObjectId() === $object->getObjectId()) {
if ($transform !== null) {
$currentObject->setMatrixTransform($transform);
}
if ($colorTransform !== null) {
$currentObject->setColorTransform($colorTransform);
}
break;
}
$view = $clipDepth !== null ? new ClippingViewLayout($clipDepth, $objectID, $object, $this->layout) : new ViewLayout($objectID, $object, $this->layout);

View file

@ -26,7 +26,7 @@ class SpriteDefinition implements MultiFrameObjectDefinition {
public function getShapeList(): DrawPathList {
$list = new DrawPathList();
foreach ($this->frames[$this->frameCounter]->render(0, [], ColorTransform::identity(), MatrixTransform::identity())->getObjects() as $object) {
foreach ($this->frames[$this->frameCounter]->render(0, [], null, null)->getObjects() as $object) {
$list = $list->merge($object->drawPathList);
}
return $list;

View file

@ -14,35 +14,38 @@ class StyleList {
$this->lineStyles = $lineStyles;
}
public static function parseFillStyleRecord(array $node): FillStyleRecord {
switch ($node["type"]) {
case 0x00: // Solid fill
return new FillStyleRecord(Color::fromArray($node["color"]));
break;
case 0x10: // Linear gradient fill
return new FillStyleRecord(LinearGradient::fromArray($node["gradient"], MatrixTransform::fromArray($node["matrix"])));
break;
case 0x12: // Radial gradient fill
return new FillStyleRecord(RadialGradient::fromArray($node["gradient"], MatrixTransform::fromArray($node["matrix"])));
break;
case 0x13: // Focal gradient fill
return new FillStyleRecord(FocalGradient::fromArray($node["focalGradient"], MatrixTransform::fromArray($node["matrix"])));
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, 20));
break;
default:
var_dump($node);
throw new \Exception("Unknown style " . $node["type"]);
}
}
public static function fromArray(array $element): StyleList {
$fillStyles = [];
$lineStyles = [];
foreach ($element["fillStyles"] as $node) {
switch ($node["type"]) {
case 0x00: // Solid fill
$fillStyles[] = new FillStyleRecord(Color::fromArray($node["color"]));
break;
case 0x10: // Linear gradient fill
$fillStyles[] = new FillStyleRecord(LinearGradient::fromArray($node["gradient"], MatrixTransform::fromArray($node["matrix"])));
break;
case 0x12: // Radial gradient fill
$fillStyles[] = new FillStyleRecord(RadialGradient::fromArray($node["gradient"], MatrixTransform::fromArray($node["matrix"])));
break;
case 0x13: // Focal gradient fill
$fillStyles[] = new FillStyleRecord(FocalGradient::fromArray($node["focalGradient"], MatrixTransform::fromArray($node["matrix"])));
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);
$fillStyles[] = new FillStyleRecord(new Color(0, 0, 0, 20));
break;
default:
var_dump($node);
throw new \Exception("Unknown style " . $node["type"]);
}
$fillStyles[] = self::parseFillStyleRecord($node);
}
foreach ($element["lineStyles"] as $node) {
$color = isset($node["color"]) ? Color::fromArray($node["color"]) : null;
@ -50,6 +53,12 @@ class StyleList {
//TODO: fill flag
if ($color === null) {
var_dump($node);
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)?
$lineStyles[] = new LineStyleRecord(max(Constants::TWIP_SIZE, $node["width"]), $color);

View file

@ -104,11 +104,25 @@ class ASSLine {
$this->cachedEncode = null;
}
public function getPackedLayer() : int{
//Segment depth into specific layers, leaving 2^16 for first, at least 2^8 for second (and 2^8 for third), if no third it'll use whole for second TODO: handle higher depths gracefully
//It is known layers CAN overlap, TODO: check if limiting range might make sense?
//TODO: change this to a truly dynamic mode. might need 2-pass to check for hole overlap
$layer = $this->layer[0] << 16;
if(isset($this->layer[2])){
$layer |= $this->layer[1] & 0xFF;
$layer |= $this->layer[2] & 0xFF;
}else{
$layer |= $this->layer[1] ?? 0;
}
return $layer;
}
public function encode($frameDurationMs): string {
if($frameDurationMs === 1000 and $this->cachedEncode !== null){
return $this->cachedEncode;
}
$line = ($this->isComment ? "Comment" : "Dialogue") . ": " . $this->layer[0] . "," . 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) . "," . self::encodeTime(($this->end + 1) * $frameDurationMs) . "," . $this->style . "," . $this->name . "," . $this->marginLeft . "," . $this->marginRight . "," . $this->marginVertical . "," . $this->effect . ",";
foreach ($this->tags as $tag){
$line .= "{" . $tag->encode($this, $frameDurationMs) . "}";
}

View file

@ -44,11 +44,12 @@ class containerTag implements ASSColorTag, ASSPositioningTag, ASSStyleTag {
public function transitionMatrixTransform(ASSLine $line, MatrixTransform $transform): ?containerTag {
if($this->bakeTransforms !== null){
if(!$transform->getMatrix()->isEqual($this->bakeTransforms->getMatrix())){ //Do not allow matrix changes but moves
if(!$transform->getMatrix()->submatrix(0, 0, 1, 1)->isEqual($this->bakeTransforms->getMatrix()->submatrix(0, 0, 1, 1))){ //Do not allow matrix changes but moves
return null;
}
}
$container = clone $this;
$index = $line->end - $line->start - 1;
@ -126,12 +127,11 @@ class containerTag implements ASSColorTag, ASSPositioningTag, ASSStyleTag {
if($bakeTransforms){
$container->bakeTransforms = $matrixTransform;
$container->try_append(positionTag::fromMatrixTransform($matrixTransform));
$drawTag = new drawTag($path->commands);
if(!$matrixTransform->getMatrix()->isEqual(MatrixFactory::identity(2))){
if(!$matrixTransform->getMatrix()->isEqual(MatrixFactory::identity(3))){
$drawTag = $drawTag->applyMatrixTransform($matrixTransform, false);
}
$pos = $matrixTransform->getTranslation()->toPixel();
$container->try_append(new positionTag($pos, $pos, 1, 1));
$container->try_append($drawTag);
}else{
$container->try_append(positionTag::fromMatrixTransform($matrixTransform));
@ -186,7 +186,7 @@ class containerTag implements ASSColorTag, ASSPositioningTag, ASSStyleTag {
//TODO: clone $line?
//TODO: animations with smoothing really don't play well. maybe allow them when only one animation "direction" exists, or smooth them manually?
//Or just don't animate MatrixTransform / do it in a single tick
$ret .= "\\t(" . floor($frameDurationMs * $index) . "," . (floor($frameDurationMs * ($index + 1))) . ",";
$ret .= "\\t(" . (floor($frameDurationMs * $index) - 1) . "," . (floor($frameDurationMs * ($index + 1)) - 1) . ",";
//$ret .= "\\t(" . floor($frameDurationMs * ($index + 1)) . "," . floor($frameDurationMs * ($index + 1)) . ",";
foreach ($transitions as $tag){
$ret .= $tag->encode($line, $frameDurationMs);

View file

@ -37,20 +37,21 @@ abstract class drawingTag implements ASSTag {
foreach ($this->shape->edges as $edge) {
if ($edge instanceof MoveRecord) {
$coords = $edge->coord->multiply($scale / Constants::TWIP_SIZE);
$commands[] = "m " . round($coords->x, $precision) . " " . round($coords->y, $precision);
$commands[] = ($lastEdge instanceof $edge ? " " : "m ") . round($coords->x, $precision) . " " . round($coords->y, $precision);
} else if ($edge instanceof LineRecord) {
$coords = $edge->coord->multiply($scale / Constants::TWIP_SIZE);
$commands[] = "l " . round($coords->x, $precision) . " " . round($coords->y, $precision);
$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);
//TODO: check "q" command
}
if ($edge instanceof CubicCurveRecord) {
$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[] = "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
@ -63,6 +64,7 @@ abstract class drawingTag implements ASSTag {
}
$commands[] = "s " . implode(" ", $controlPoints) . " " . round($anchor->x, $precision) . " " . round($anchor->y, $precision);
$commands[] = "c";
}
} else {

View file

@ -22,7 +22,7 @@ class positionTag implements ASSPositioningTag {
}
public function transitionMatrixTransform(ASSLine $line, MatrixTransform $transform): ?positionTag {
$translation = $transform->getTranslation()->toPixel();
$translation = $transform->applyToVector(new Vector2(0, 0), true)->toPixel();
$frame = $line->end - $line->start;
@ -68,12 +68,12 @@ class positionTag implements ASSPositioningTag {
public function encode(ASSLine $line, float $frameDurationMs): string {
$frame = $line->end - $line->start;
$hasMoved = $this->start < $frame;
$hasMoved = $this->start !== $this->end;
$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->end) * $frameDurationMs).",".floor($this->end * $frameDurationMs).")";
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).")";
}
return "\\pos(".$this->from->x ."," . $this->from->y.")";
}
@ -83,7 +83,7 @@ class positionTag implements ASSPositioningTag {
}
public static function fromMatrixTransform(MatrixTransform $transform): ?positionTag {
$translation = $transform->getTranslation()->toPixel();
$translation = $transform->applyToVector(new Vector2(0, 0), true)->toPixel();
return new positionTag($translation, $translation, 1, 1);
}
}

View file

@ -13,7 +13,7 @@ if ($swf->header["signature"]) {
"bakeTransforms" => true //TODO: fix ASS matrix transform rendering and remove this
]);
$keyFrameInterval = 10 * $processor->getFrameRate(); //kf every 10 seconds
$keyFrameInterval = 10 * $processor->getFrameRate(); //kf every 10 seconds TODO: make this dynamic, per-shape
$frameOffset = 0;
$lastFrame = null;
while(($frame = $processor->nextFrameOutput()) !== null){