Compare commits

...

2 commits

Author SHA1 Message Date
DataHoarder a56b30609e WiP: gradients and others 2022-05-21 15:15:28 +02:00
DataHoarder 4dc069e6f5 Action list fixes 2022-01-08 20:29:26 +01:00
31 changed files with 720 additions and 149 deletions

55
audio.php Normal file
View file

@ -0,0 +1,55 @@
<?php
use swf2ass\ass\ASSEventTime;
require_once __DIR__ . "/vendor/autoload.php";
$width = 512;
$height = 512;
$ar = $width / $height;
$frameRate = 30;
$frameDurationMs = (1 / $frameRate) * 1000;
echo <<<ASSHEADER
[Script Info]
; Script generated by swf2ass
; https://git.gammaspectra.live/WeebDataHoarder/swf2ass
ScriptType: v4.00+
WrapStyle: 0
ScaledBorderAndShadow: yes
YCbCr Matrix: PC.709
PlayResX: {$width}
PlayResY: {$height}
[Aegisub Project Garbage]
Last Style Storage: f
Video File: ?dummy:{$frameRate}:10000:{$width}:{$height}:160:160:160:c
Video AR Value: {$ar}
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: f,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
ASSHEADER;
$frameDuration = 1;
for($frame = 0; $frame < 6523; $frame += $frameDuration){
$line = new \swf2ass\ass\ASSLine();
$line->layer = [$frame];
$line->objectId = 0;
$line->start = $frame;
$line->end = $frame + $frameDuration - 1;
$line->name = "{$line->start}->{$line->end}";
$line->style = "f";
$assEventTime = new ASSEventTime($line->start, $line->end - $line->start + 1, $frameDurationMs);
$l = trim(file_get_contents("/tmp/tmp6_pqjbve/" . str_pad("$frame.ass", 10, "0", STR_PAD_LEFT)));
$l = str_replace("pos(0,0)", "pos(0,416)", $l);
$l = $line->encode($frameDurationMs) . "{\\blur0.5}" . $l;
echo $l . "\n";
}

12
composer.lock generated
View file

@ -51,16 +51,16 @@
},
{
"name": "markrogoyski/math-php",
"version": "v2.5.0",
"version": "v2.6.0",
"source": {
"type": "git",
"url": "https://github.com/markrogoyski/math-php.git",
"reference": "ca71ca97dc136e7bb9e9e1fe05782f343a5692d4"
"reference": "85d7d7fe205a6df2b20f956720e25341f3b1462a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/markrogoyski/math-php/zipball/ca71ca97dc136e7bb9e9e1fe05782f343a5692d4",
"reference": "ca71ca97dc136e7bb9e9e1fe05782f343a5692d4",
"url": "https://api.github.com/repos/markrogoyski/math-php/zipball/85d7d7fe205a6df2b20f956720e25341f3b1462a",
"reference": "85d7d7fe205a6df2b20f956720e25341f3b1462a",
"shasum": ""
},
"require": {
@ -120,9 +120,9 @@
],
"support": {
"issues": "https://github.com/markrogoyski/math-php/issues",
"source": "https://github.com/markrogoyski/math-php/tree/v2.5.0"
"source": "https://github.com/markrogoyski/math-php/tree/v2.6.0"
},
"time": "2021-11-22T05:14:07+00:00"
"time": "2022-04-10T05:15:37+00:00"
}
],
"packages-dev": [],

View file

@ -9,6 +9,7 @@ namespace MartinezRueda;
*/
class Contour
{
/** @var Point[] */
public $points = [];
protected $holes = [];

View file

@ -7,6 +7,7 @@ namespace MartinezRueda;
*/
class Polygon
{
/** @var Contour[] */
public $contours = [];
/**

View file

@ -15,21 +15,21 @@ function outputFrame($frame, $endFrame, $frameDurationMs) {
$shape = $path->commands;
$line->tags = [
new \swf2ass\ass\borderTag(0),
new \swf2ass\ass\borderTag(new \swf2ass\Vector2(0, 0)),
new \swf2ass\ass\shadowTag(0),
\swf2ass\ass\fillColorTag::fromStyleRecord($path->style)
];
if($shape->getRecords()[0] instanceof \swf2ass\MoveRecord){
/*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->getRecords()[0]->getStart()->divide(-\swf2ass\Constants::TWIP_SIZE), $shape->getRecords()[0]->getStart()->divide(-\swf2ass\Constants::TWIP_SIZE), $endFrame, $endFrame);
}*/
$line->tags[] = new \swf2ass\ass\positionTag(new \swf2ass\Vector2(0, 0), new \swf2ass\Vector2(0, 0), 1, 1);
$line->tags[] = new \swf2ass\ass\drawTag($shape, 1);
echo $line->encode($frameDurationMs) . PHP_EOL;
}
}
$fps = 30000 / 1001;//30;
$colorN = 64;
$fps = 30;//30000 / 1001;//30;
$colorN = 2;
$pframeColorDistance = 16;
if (is_dir($argv[1])) {
@ -43,7 +43,7 @@ sort($frames);
$frameBuffer = null;
$quantizedFrameBuffer = $frameBuffer;
$dynamicPalette = true;
$dynamicPalette = false;
$palette = [];
for ($i = 0; $i < $colorN; ++$i) {
$palette[] = new \swf2ass\Color((int)round($i * (255 / ($colorN - 1))), (int)round($i * (255 / ($colorN - 1))), (int)round($i * (255 / ($colorN - 1))));
@ -51,7 +51,7 @@ for ($i = 0; $i < $colorN; ++$i) {
$frameNumber = 1;
$endFrame = 100;
$endFrame = 6522;
$lastFrameNumber = null;
$currentFrames = [];

View file

@ -3,7 +3,7 @@
namespace swf2ass;
class Circle extends Oval {
class Circle extends Ellipse {
public function __construct(Vector2 $center, float $radius) {
parent::__construct($center, new Vector2($radius, $radius));
}

View file

@ -3,31 +3,25 @@
namespace swf2ass;
use MartinezRueda\Algorithm;
use MartinezRueda\Polygon;
class ClipPath {
/** @var Shape[] */
public array $shapes;
private ComplexPolygon $clip;
//private Shape $clip;
/**
* @param Shape[] $shapes
*/
public function __construct(array $shapes = []){
$this->shapes = $shapes;
public function __construct(?Shape $shape = null){
//$this->clip = $shape ?? new Shape();
$this->clip = ComplexPolygon::fromShape($shape ?? new Shape());
}
public function getShape() : Shape{
$shape = new Shape();
foreach ($this->shapes as $s){
$shape = $shape->merge($s);
}
return $shape;
//return $this->clip;
return $this->clip->toShape();
}
public function addShape(Shape $shape){
$this->shapes[] = $shape;
//$this->clip = $this->clip->merge($shape);
$this->clip = ComplexPolygon::fromShape($this->clip->toShape()->merge($shape));
}
@ -39,64 +33,24 @@ class ClipPath {
* @return ClipPath
*/
public function intersect(ClipPath $other) : ClipPath{
//return new ClipPath($this->clip->merge($other->clip));
try{
return self::fromPolygon((new Algorithm())->getIntersection($this->toPolygon(), $other->toPolygon()));
$clipPath = new ClipPath();
$clipPath->clip = $this->clip->intersect($other->clip);
return $clipPath;
}catch (\Exception $e){
var_dump($this);
var_dump($other);
echo $e;
$self = $this->getShape()->flatten();
$other = $other->getShape()->flatten();
var_dump((new Shape($self->getRecords()))->getArea());
$self = $this->getShape();
$other = $other->getShape();
var_dump((new \swf2ass\ass\drawTag(new Shape($self->getRecords()), 1))->encode(new \swf2ass\ass\ASSEventTime(1, 1, 1)));
var_dump((new Shape($other->getRecords()))->getArea());
var_dump((new \swf2ass\ass\drawTag(new Shape($other->getRecords()), 1))->encode(new \swf2ass\ass\ASSEventTime(1, 1, 1)));
//fgets(STDIN);
return $this; //TODO: fix this breakage, some clips being overlapping shapes????
}
}
private function toPolygon() : Polygon{
$contours = [];
foreach ($this->shapes as $shape){
$contours[] = array_map(function (Vector2 $point){return $point->toArray();}, $shape->toPoints());
}
return new Polygon($contours);
}
private static function fromPolygon(Polygon $p) : ClipPath{
$result = $p->toArray();
$clipPath = new ClipPath();
if(count($result) === 0){ //Nothing!
return $clipPath;
}
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
$clipPath->addShape($shape);
//}
}
return $clipPath;
}
public function applyMatrixTransform(MatrixTransform $transform, bool $applyTranslation = true) : ClipPath{
$shapes = [];
foreach ($this->shapes as $shape){
$shapes[] = $transform->applyToShape($shape, $applyTranslation);
}
return new ClipPath($shapes);
return new ClipPath($transform->applyToShape($this->getShape(), $applyTranslation));
}
}

View file

@ -27,7 +27,29 @@ class Color {
return new Color($element["red"], $element["green"], $element["blue"], $element["alpha"] ?? 255);
}
public function toString($noAlpha = false){
public function __toString() : string{
return $this->toString(false);
}
public function toString(bool $noAlpha = false) : string{
return $noAlpha ? "rgb({$this->r},{$this->g},{$this->b})" : "rgba({$this->r},{$this->g},{$this->b},{$this->alpha})";
}
public function toLinearRGB() : Color{
return new Color(
pow($this->r / 255, 2.2) * 255,
pow($this->g / 255, 2.2) * 255,
pow($this->b / 255, 2.2) * 255,
pow($this->alpha / 255, 2.2) * 255,
);
}
public function tosRGB() : Color{
return new Color(
pow($this->r / 255, 0.4545) * 255,
pow($this->g / 255, 0.4545) * 255,
pow($this->b / 255, 0.4545) * 255,
pow($this->alpha / 255, 0.4545) * 255,
);
}
}

161
src/ComplexPolygon.php Normal file
View file

@ -0,0 +1,161 @@
<?php
namespace swf2ass;
use MartinezRueda\Algorithm;
use MartinezRueda\Contour;
class ComplexPolygon {
protected \MartinezRueda\Polygon $polygon;
protected \MartinezRueda\Polygon $holes;
/**
* @param Polygon[] $poly
*/
public function __construct(array $poly){
/** @var Polygon[] $polygons */
$polygons = [];
/** @var Polygon[] $holes */
$holes = [];
while (count($poly) > 0){
$polygon = reset($poly);
$found = false;
if($polygon->getArea() < Constants::EPSILON){
var_dump("WARNING: zero area shape: " . (new \swf2ass\ass\drawTag($polygon->toShape(), 1))->encode(new \swf2ass\ass\ASSEventTime(1, 1, 1)));
$found = true; //Remove zero-area shapes
}
if($found){
array_shift($poly);
continue;
}
foreach ($polygons as $p){
if($p->contains($polygon)){
$found = true;
$holes[] = $polygon;
break;
}
}
if($found){
array_shift($poly);
continue;
}
foreach ($poly as $p){
if($p->contains($polygon)){
$found = true;
$holes[] = $polygon;
break;
}
}
if(!$found){
$polygons[] = $polygon;
}
array_shift($poly);
}
$this->polygon = new \MartinezRueda\Polygon(array_map(function (Polygon $polygon){
return array_map(function (Vector2 $point){
return $point->toArray();
}, $polygon->toPoints());
}, $polygons));
$this->holes = new \MartinezRueda\Polygon(array_map(function (Polygon $polygon){
return array_map(function (Vector2 $point){
return $point->toArray();
}, $polygon->toPoints());
}, $holes));
}
public function toShape() : Shape{
$shape = new Shape();
foreach ($this->polygon->contours as $contour){
$shape = $shape->merge(Polygon::fromAlgorithmContour($contour)->toShape());
}
foreach ($this->holes->contours as $contour){
$shape = $shape->merge(Polygon::fromAlgorithmContour($contour)->toShape());
}
return $shape;
}
/**
* @param ComplexPolygon $polygon
* @return ComplexPolygon
*/
public function intersect(ComplexPolygon $polygon) : ComplexPolygon{
// H = H1 OR H2 OR ... Hn
// C = P NAND H
// Ca AND Cb = Pa AND Pb NAND (Ha OR Hb)
// (Ha OR Hb)
$holes = (new Algorithm())->getUnion($this->holes, $polygon->holes);
// Pa AND Pb
$poly = new ComplexPolygon(array_map(function (Contour $contour){
return Polygon::fromAlgorithmContour($contour);
}, (new Algorithm())->getIntersection($this->polygon, $polygon->polygon)->contours));
// Can get holes from above, merge them
$holes = (new Algorithm())->getUnion($holes, $poly->holes);
// P NAND H
//TODO: maybe only do this at the end? check if correct as well
return new ComplexPolygon(array_map(function (Contour $contour){
return Polygon::fromAlgorithmContour($contour);
}, (new Algorithm())->getDifference($poly->polygon, $holes)->contours));
}
/**
* @param LineRecord $line
* @param bool $includeTouching
* @return LineRecord[]|Vector2[]
*/
public function calculateLineIntersections(LineRecord $line, bool $includeTouching = false) : array{
$intersections = $this->calculateLineIntersections($line, $includeTouching);
foreach ($this->holes as $p){
$intersections = array_merge($intersections, $p->calculateLineIntersections($line, $includeTouching));
}
return $intersections;
}
public static function fromShape(Shape $shape) : ComplexPolygon {
$shape = $shape->flatten();
$poly = [];
/** @var LineRecord[] $edges */
$edges = [];
$lastEdge = null;
foreach ($shape->getRecords() as $record){
if($lastEdge !== null and !$lastEdge->getEnd()->equals($record->getStart())){
$poly[] = new Polygon($edges);
$edges = [];
}
if($record instanceof LineRecord){
$edges[] = $record;
}else{
var_dump($record);
throw new \Exception("Found record of type " . get_class($record));
}
$lastEdge = $record;
}
if(count($edges) > 0){
$poly[] = new Polygon($edges);
}
return new ComplexPolygon($poly);
}
}

View file

@ -3,7 +3,7 @@
namespace swf2ass;
class Oval implements ComplexShape {
class Ellipse implements ComplexShape {
protected const c = 0.55228474983; // (4/3) * (sqrt(2) - 1)
//protected const c = 0.551915024494; // https://spencermortensen.com/articles/bezier-circle/
@ -28,9 +28,9 @@ class Oval implements ComplexShape {
public function draw(): array {
return [
$this->getQuarter(new Vector2(-$this->radius->x, $this->radius->y)),
$this->getQuarter($this->radius),
$this->getQuarter($this->radius)->reverse(), //Reverse so paths connect
$this->getQuarter(new Vector2($this->radius->x, -$this->radius->y)),
$this->getQuarter(new Vector2(-$this->radius->x, -$this->radius->y)),
$this->getQuarter(new Vector2(-$this->radius->x, -$this->radius->y))->reverse(),
];
}
}

View file

@ -7,7 +7,10 @@ class FillStyleRecord implements StyleRecord {
/** @var Gradient|Color */
public $fill;
public function __construct($fill) {
public ?LineStyleRecord $border = null;
public function __construct($fill, ?LineStyleRecord $border = null) {
$this->fill = $fill;
$this->border = $border;
}
}

View file

@ -3,12 +3,31 @@
namespace swf2ass;
interface Gradient {
const AUTO_SLICES = -1;
const SPREAD_PAD = 0;
const SPREAD_REFLECT = 1;
const SPREAD_REPEAT = 2;
const SPREAD_RESERVED = 3;
//TODO
const INTERPOLATE_NORMAL_RGB = 0;
const INTERPOLATE_LINEAR_RGB = 1;
const INTERPOLATE_RESERVED1 = 2;
const INTERPOLATE_RESERVED2 = 3;
public function getSpreadMode() : int;
public function getInterpolationMode() : int;
/**
* @return GradientItem[]
*/
public function getItems(): array;
public function getInterpolatedDrawPaths(int $overlap = 0, int $slices = self::AUTO_SLICES): DrawPathList;
public function getMatrixTransform(): MatrixTransform;
public function applyColorTransform(ColorTransform $transform): Gradient;

15
src/GradientSlice.php Normal file
View file

@ -0,0 +1,15 @@
<?php
namespace swf2ass;
class GradientSlice {
public Color $color;
public float $startRatio;
public float $endRatio;
public function __construct(Color $color, float $startRatio, float $endRatio){
$this->color = $color;
$this->startRatio = $startRatio;
$this->endRatio = $endRatio;
}
}

View file

@ -3,23 +3,61 @@
namespace swf2ass;
class LinearGradient implements Gradient {
/** @var GradientItem[] */
public array $colors;
public MatrixTransform $transform;
public int $spreadMode;
public int $interpolationMode;
/**
* @param GradientItem[] $colors
* @param MatrixTransform $transform
*/
public function __construct(array $colors, MatrixTransform $transform) {
public function __construct(array $colors, MatrixTransform $transform, int $spreadMode, int $interpolationMode) {
$this->colors = $colors;
$this->transform = $transform;
$this->spreadMode = $spreadMode;
$this->interpolationMode = $interpolationMode;
}
public function getItems(): array {
return $this->colors;
}
public function getSpreadMode() : int{
return $this->spreadMode;
}
public function getInterpolationMode() : int{
return $this->interpolationMode;
}
public function getInterpolatedDrawPaths(int $overlap = 0, int $slices = self::AUTO_SLICES): DrawPathList{
//items is max size 8 to 15 depending on SWF version
$min = -16384;
$max = 16384;
$diff = $max - $min;
//TODO spreadMode
$paths = new DrawPathList();
foreach (Utils::lerpGradient($this, $slices) as $item){
$paths->commands[] = DrawPath::fill(
new FillStyleRecord($item->color),
$this->getMatrixTransform()->applyToShape(new Shape((new Rectangle(
new Vector2($min + ($item->startRatio / 255) * $diff - $overlap / 2, $min),
new Vector2($min + ($item->endRatio / 255) * $diff + $overlap / 2, $max)
))->draw())));
}
return $paths;
}
public function getMatrixTransform(): MatrixTransform {
return $this->transform;
@ -33,7 +71,7 @@ class LinearGradient implements Gradient {
//TODO: interpolationMode, spreadMode
return new LinearGradient($colors, $transform);
return new LinearGradient($colors, $transform, $element["spreadMode"], $element["interpolationMode"]);
}
public function applyColorTransform(ColorTransform $transform): Gradient{

View file

@ -123,11 +123,11 @@ class MorphShapeDefinition implements ObjectDefinition {
//No need to convert types!
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)));
$shape->addRecord(new LineRecord(Utils::lerpVector2($r1->to, $r2->to, $ratio), Utils::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)));
$shape->addRecord(new QuadraticCurveRecord(Utils::lerpVector2($r1->control, $r2->control, $ratio), Utils::lerpVector2($r1->anchor, $r2->anchor, $ratio), Utils::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)));
$shape->addRecord(new MoveRecord(Utils::lerpVector2($r1->to, $r2->to, $ratio), Utils::lerpVector2($r1->start, $r2->start, $ratio)));
}else{
var_dump($records1);
var_dump($records2);
@ -138,17 +138,17 @@ class MorphShapeDefinition implements ObjectDefinition {
//TODO: morph styles properly
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);
$drawPathList->commands[] = DrawPath::fill(new FillStyleRecord(Utils::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);
$drawPathList->commands[] = DrawPath::fill(new FillStyleRecord(Utils::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);
$drawPathList->commands[] = DrawPath::stroke(new LineStyleRecord(Utils::lerpInteger($c1->style->width, $c2->style->width, $ratio), Utils::lerpColor($c1->style->color, $c2->style->color, $ratio)), $shape);
}else{
var_dump($c1->style);
var_dump($c2->style);
@ -159,22 +159,6 @@ class MorphShapeDefinition implements ObjectDefinition {
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);

118
src/Polygon.php Normal file
View file

@ -0,0 +1,118 @@
<?php
namespace swf2ass;
use MartinezRueda\Algorithm;
class Polygon {
/** @var LineRecord[] */
protected array $edges;
private ?float $area = null;
public function __construct(array $edges){
$this->edges = $edges;
}
/**
* @return LineRecord[]
*/
public function getEdges() : array{
return $this->edges;
}
/**
* @param LineRecord $line
* @param bool $includeTouching
* @return LineRecord[]|Vector2[]
*/
public function calculateLineIntersections(LineRecord $line, bool $includeTouching = false) : array{
$intersections = [];
foreach ($this->edges as $edge){
if(($intersection = $line->intersect($edge)) !== null){
if($intersection instanceof LineRecord and !$includeTouching){
continue;
}
$intersections[] = $intersection;
}
}
return $intersections;
}
public function toAlgorithmPolygon() : \MartinezRueda\Polygon{
return new \MartinezRueda\Polygon([array_map(function (Vector2 $point){
return $point->toArray();
}, $this->toPoints())]);
}
/**
* @param \MartinezRueda\Contour $contour
* @return Polygon
*/
public static function fromAlgorithmContour(\MartinezRueda\Contour $contour) : Polygon {
$records = [];
$i = reset($contour->points);
$start = $pos = new Vector2($i->x, $i->y);
while (($p = next($contour->points)) !== false){
$point = new Vector2($p->x, $p->y);
$records[] = new LineRecord($point, $pos);
$pos = $point;
}
if(!$start->equals($pos)){
$records[] = new LineRecord($start, $pos); //Close shape
}
return new Polygon($records);
}
/**
* @return Vector2[]
*/
public function toPoints() : array{
return array_map(function (LineRecord $record){
return $record->getStart();
}, $this->edges);
}
public function isPointInside(Vector2 $point, bool $includeTouching = false) : bool{
$segment = new LineRecord($point, new Vector2(PHP_INT_MAX, PHP_INT_MAX)); //TODO: maybe pick better falues? this might wrap
return count($this->calculateLineIntersections($segment, $includeTouching)) % 2 === 0; //even-odd rule
}
public function contains(Polygon $polygon): bool {
foreach ($polygon->edges as $edge){
if(!$this->isPointInside($edge->getStart())){
return false;
}
}
foreach ($this->edges as $edge){ //Reverse check
if($polygon->isPointInside($edge->getStart())){
return false;
}
}
return true;
}
public function toShape() : Shape{
return new Shape($this->getEdges());
}
public function getArea() : float{
if($this->area !== null){
return $this->area;
}
$this->area = 0;
foreach ($this->edges as $i => $e1){
$e2 = $this->edges[($i + 1) % count($this->edges)];
$this->area += $e1->getStart()->x * $e2->getStart()->y - $e1->getStart()->y * $e2->getStart()->x;
}
return $this->area = abs($this->area / 2);
}
}

View file

@ -8,19 +8,57 @@ class RadialGradient implements Gradient {
public MatrixTransform $transform;
public int $spreadMode;
public int $interpolationMode;
/**
* @param GradientItem[] $colors
* @param MatrixTransform $transform
*/
public function __construct(array $colors, MatrixTransform $transform) {
public function __construct(array $colors, MatrixTransform $transform, int $spreadMode, int $interpolationMode) {
$this->colors = $colors;
$this->transform = $transform;
$this->spreadMode = $spreadMode;
$this->interpolationMode = $interpolationMode;
}
public function getItems(): array {
return $this->colors;
}
public function getSpreadMode() : int{
return $this->spreadMode;
}
public function getInterpolationMode() : int{
return $this->interpolationMode;
}
public function getInterpolatedDrawPaths(int $overlap = 0, int $slices = self::AUTO_SLICES): DrawPathList{
//items is max size 8 to 15 depending on SWF version
$min = -16384;
$max = 16384;
$diff = $max - $min;
//TODO spreadMode
$paths = new DrawPathList();
foreach (Utils::lerpGradient($this, $slices) as $item){
$shape = new Shape();
//Create concentric circles to cut out a shape
$shape = $shape->merge(new Shape((new Circle(new Vector2(0, 0), (($item->endRatio / 255) * $diff) / 2 + $overlap / 4))->draw()));
$shape = $shape->merge(new Shape((new Circle(new Vector2(0, 0), (($item->startRatio / 255) * $diff) / 2 - $overlap / 4))->draw()));
$paths->commands[] = DrawPath::fill(
new FillStyleRecord($item->color),
$this->getMatrixTransform()->applyToShape($shape));
}
return $paths;
}
public function getMatrixTransform(): MatrixTransform {
return $this->transform;
}
@ -33,7 +71,7 @@ class RadialGradient implements Gradient {
//TODO: interpolationMode, spreadMode
return new RadialGradient($colors, $transform);
return new RadialGradient($colors, $transform, $element["spreadMode"], $element["interpolationMode"]);
}
public function applyColorTransform(ColorTransform $transform): Gradient{

View file

@ -69,14 +69,23 @@ class SWFProcessor extends SWFTreeProcessor {
return $node["tagType"];
case "SoundStreamHead":
case "SoundStreamHead2":
if($this->loops > 0){
break;
}
$this->audio = AudioStream::fromSoundStreamHeadTag($node);
return $node["tagType"];
case "DefineSound":
if($this->loops > 0){
break;
}
$this->audio = new AudioStream(0, 0, 0, 0);
$this->audio->setStartFrame($this->getFrame());
//TODO $this->audio = (object)["node" => $node, "start" => $this->getFrame(), "content" => []];
return $node["tagType"];
case "SoundStreamBlock":
if($this->loops > 0){
break;
}
if($this->audio !== null){
if($this->audio->getStartFrame() === null){
$this->audio->setStartFrame($this->getFrame());
@ -96,7 +105,7 @@ class SWFProcessor extends SWFTreeProcessor {
return null;
}
if(!$this->isPlaying() and ($this->audio === null or $this->audio->getStartFrame() === null)){ //Force play till finding audio
if(!$this->isPlaying() and ($this->audio === null or $this->audio->getStartFrame() === null) or $this->getFrame() === 1){ //Force play till finding audio, or first frame is 0
$this->playing = true;
}

View file

@ -59,6 +59,9 @@ class SWFTreeProcessor {
switch ($node["tagType"]) {
case "DefineMorphShape":
case "DefineMorphShape2":
if($this->loops > 0){
break;
}
$shape = MorphShapeDefinition::fromArray($node);
$this->objects->add($shape);
break;
@ -67,11 +70,17 @@ class SWFTreeProcessor {
case "DefineShape3":
case "DefineShape4":
case "DefineShape5":
if($this->loops > 0){
break;
}
$shape = ShapeDefinition::fromArray($node);
$this->objects->add($shape);
break;
case "DefineSprite":
if($this->loops > 0){
break;
}
$objectID = $node["spriteId"];
$framesCount = $node["frameCount"];
@ -81,6 +90,9 @@ class SWFTreeProcessor {
break;
case "DefineBitsLossless":
case "DefineBitsLossless2":
if($this->loops > 0){
break;
}
break; //TODO
$bitmap = BitmapDefinition::fromArray($node);
@ -88,6 +100,9 @@ class SWFTreeProcessor {
break;
case "DefineBitsJPEG2":
case "DefineBitsJPEG3":
if($this->loops > 0){
break;
}
break; //TODO
$bitmap = JPEGBitmapDefinition::fromArray($node);
$this->objects->add($bitmap);
@ -200,6 +215,10 @@ class SWFTreeProcessor {
return $this->playing;
}
public function getLoops() : int{
return $this->loops;
}
public function nextFrame(): ?ViewFrame {
$actions = new ActionList();
if(!$this->playing){
@ -216,6 +235,7 @@ class SWFTreeProcessor {
}
if ($nodeName === null) { //Loop again
$this->loops++;
$this->frame = 0;
$this->index = 0;
$this->layout = new ViewLayout($this->layout->getObjectId(), null);

View file

@ -38,27 +38,6 @@ class Shape {
return $this->start()->equals($this->end());
}
/**
* @return Vector2[]
* @throws \Exception
*/
public 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;
}
/**
* @return Record[]
*/
@ -66,16 +45,6 @@ class Shape {
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 {
$newShape = new Shape([]);
$newShape->edges = array_merge($this->edges, $shape->edges);

View file

@ -30,7 +30,8 @@ class SpriteDefinition implements MultiFrameObjectDefinition {
}
public function nextFrame(): ViewFrame {
return $this->currentFrame = $this->swf->nextFrame();
//TODO: figure out why this can return null. missing shapes?
return $this->currentFrame = $this->swf->nextFrame() ?? new ViewFrame($this->getObjectId(), new DrawPathList());
}
public function getSafeObject() : SpriteDefinition{

View file

@ -17,4 +17,89 @@ abstract class Utils {
static function binary2dec($bin) {
return gmp_intval(gmp_init($bin, 2));
}
public static function lerpInteger(int $start, int $end, float $ratio): int {
return $start + ($end - $start) * $ratio;
}
public static function lerpFloat(float $start, float $end, float $ratio): float {
return $start + ($end - $start) * $ratio;
}
public static function lerpVector2(Vector2 $start, Vector2 $end, float $ratio): Vector2 {
return $start->add($end->sub($start)->multiply($ratio));
}
public 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));
}
/**
* @param Gradient $gradient
* @param int $slices
* @return \Iterator<GradientSlice>|GradientSlice[]
*/
public static function lerpGradient(Gradient $gradient, int $slices = Gradient::AUTO_SLICES): \Iterator{
$items = $gradient->getItems();
//TODO: spread modes
$first = reset($items);
$last = end($items);
if($first->ratio !== 0){
$first = clone $first;
$first->ratio = 0;
array_unshift($items, $first);
}
if($last->ratio !== 255){
$last = clone $last;
$last->ratio = 255;
array_push($items, $last);
}
$prevItem = null;
foreach ($items as $item){
if($prevItem !== null){
if($gradient->getInterpolationMode() === Gradient::INTERPOLATE_LINEAR_RGB){
$prevColor = $prevItem->color->toLinearRGB();
$currentColor = $item->color->toLinearRGB();
}else{
$prevColor = $prevItem->color;
$currentColor = $item->color;
}
$maxColorDistance = max(abs($prevColor->r - $currentColor->r), abs($prevColor->g - $currentColor->g), abs($prevColor->b - $currentColor->b), abs($prevColor->alpha - $currentColor->alpha));
$prevPosition = $prevItem->ratio;
$currentPosition = $item->ratio;
$distance = abs($prevPosition - $currentPosition);
if($maxColorDistance < Constants::EPSILON){
$partitions = 1;
}else if($slices === Gradient::AUTO_SLICES){
$partitions = min(255 / (count($items) + 1), max(1, ceil($maxColorDistance)));
}else{
$partitions = ($distance / 255) * $slices;
}
$partitions = max(1, ceil($partitions));
$fromPos = $prevPosition;
for($i = 1; $i <= $partitions; ++$i){
$ratio = $i / $partitions;
$color = Utils::lerpColor($prevColor, $currentColor, $ratio);
if($gradient->getInterpolationMode() === Gradient::INTERPOLATE_LINEAR_RGB){
$color = $color->tosRGB();
}
$toPos = Utils::lerpFloat($prevPosition, $currentPosition, $ratio);
yield new GradientSlice($color, $fromPos, $toPos);
$fromPos = $toPos;
}
}
$prevItem = $item;
}
}
}

View file

@ -92,7 +92,7 @@ class ViewFrame {
}
}
if (count($clipShape->shapes) > 0) {
if (count($clipShape->getShape()->getRecords()) > 0) {
$clipShape = $clipShape->applyMatrixTransform($clipObject->matrixTransform);
$clipPath = $clipPath === null ? $clipShape : $clipShape->intersect($clipPath);
}

View file

@ -2,7 +2,12 @@
namespace swf2ass\ass;
use swf2ass\ClipPath;
use swf2ass\DrawPath;
use swf2ass\FillStyleRecord;
use swf2ass\FrameInformation;
use swf2ass\Gradient;
use swf2ass\MatrixTransform;
use swf2ass\RenderedFrame;
use swf2ass\RenderedObject;
@ -96,7 +101,6 @@ class ASSLine {
$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;

View file

@ -2,7 +2,13 @@
namespace swf2ass\ass;
use swf2ass\ClipPath;
use swf2ass\Constants;
use swf2ass\DrawPath;
use swf2ass\DrawPathList;
use swf2ass\FillStyleRecord;
use swf2ass\FrameInformation;
use swf2ass\Gradient;
use swf2ass\MatrixTransform;
use swf2ass\Rectangle;
use swf2ass\RenderedFrame;
@ -29,7 +35,7 @@ class ASSRenderer {
$display = $viewPort->toPixel();
$width = $display->getWidth() * self::getSetting("videoScaleMultiplier", 1);
$height = $display->getHeight() * self::getSetting("videoScaleMultiplier", 1);
$ar = $width / $height;
$ar = sprintf("%.6F", $width / $height);
/*if(($frameRate * 2) <= 60){
$frameRate *= 2;
@ -42,6 +48,7 @@ class ASSRenderer {
[Script Info]
; Script generated by swf2ass ASSRenderer
; https://git.gammaspectra.live/WeebDataHoarder/swf2ass
Title: swf2ass
ScriptType: v4.00+
; TODO: maybe set WrapStyle: 2
WrapStyle: 0
@ -55,10 +62,12 @@ Timer: {$timerPrecision}
Last Style Storage: f
Video File: ?dummy:{$frameRate}:10000:{$width}:{$height}:160:160:160:c
Video AR Value: {$ar}
Active Line: 0
Video Zoom Percent: 2.000000
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: f,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1
Style: f,Arial,20,&H00000000,&H00000000,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
@ -66,6 +75,35 @@ ASSHEADER;
}
/**
* @param RenderedObject $object
* @return RenderedObject
*/
public static function bakeGradients(RenderedObject $object) : RenderedObject{
$baked = null;
$drawPathList = new DrawPathList();
foreach ($object->drawPathList->commands as $command){
if($command->style instanceof FillStyleRecord and $command->style->fill instanceof Gradient){
$baked = $baked ?? new RenderedObject($object->depth, $object->objectId, $drawPathList, $object->colorTransform, $object->matrixTransform, $object->clip);
$gradientClip = new ClipPath($command->commands);
//Convert gradients to many tags
foreach ($command->style->fill->getInterpolatedDrawPaths(0, self::getSetting("gradientSlices", Gradient::AUTO_SLICES))->commands as $gradientPath){
//echo (new drawTag($gradientPath->commands, 1))->encode(new ASSEventTime(1, 1, 1)) . "\n";
$newPath = DrawPath::fill($gradientPath->style, $gradientClip->intersect(new ClipPath($gradientPath->commands))->getShape());
//echo (new drawTag($newPath->commands, 1))->encode(new ASSEventTime(1, 1, 1)) . "\n\n";
if(count($newPath->commands->getRecords()) === 0){
continue;
}
$drawPathList->commands[] = $newPath;
}
}else{
$drawPathList->commands[] = $command;
}
}
return $baked ?? $object;
}
public function renderFrame(FrameInformation $information, RenderedFrame $frame): \Generator {
if ($this->header !== null) {
foreach (explode("\n", $this->header) as $line) {
@ -83,7 +121,7 @@ ASSHEADER;
$animated = 0;
foreach ($objects as $object) {
$object = clone $object;
$object = clone self::bakeGradients($object);
$object->matrixTransform = $scale->multiply($object->matrixTransform); //TODO order?
$depth = $object->getDepth();

View file

@ -3,6 +3,7 @@
namespace swf2ass\ass;
use swf2ass\Constants;
use swf2ass\FillStyleRecord;
use swf2ass\LineStyleRecord;
use swf2ass\StyleRecord;
use swf2ass\Vector2;
@ -30,6 +31,8 @@ class borderTag implements ASSStyleTag {
public static function fromStyleRecord(StyleRecord $record): ?borderTag {
if ($record instanceof LineStyleRecord) {
return new borderTag(new Vector2($record->width / Constants::TWIP_SIZE, $record->width / Constants::TWIP_SIZE));
}else if ($record instanceof FillStyleRecord and $record->border !== null) {
return new borderTag(new Vector2($record->border->width / Constants::TWIP_SIZE, $record->border->width / Constants::TWIP_SIZE));
}
return new borderTag(new Vector2(0, 0));
}

View file

@ -7,6 +7,8 @@ use MathPHP\LinearAlgebra\MatrixFactory;
use swf2ass\ClipPath;
use swf2ass\ColorTransform;
use swf2ass\DrawPath;
use swf2ass\FillStyleRecord;
use swf2ass\Gradient;
use swf2ass\LineStyleRecord;
use swf2ass\MatrixTransform;
use swf2ass\Shape;

View file

@ -17,6 +17,8 @@ class fillColorTag extends colorTag {
$color = $record->fill;
} else if ($record->fill instanceof Gradient) { //TODO: split this elsewhere
$color = $record->fill->getItems()[0]->color;
var_dump($record->fill);
throw new \Exception("Invalid Gradient Fill record");
} else {
throw new \Exception("Invalid Fill record");
}

View file

@ -3,6 +3,7 @@
namespace swf2ass\ass;
use swf2ass\Color;
use swf2ass\FillStyleRecord;
use swf2ass\LineStyleRecord;
use swf2ass\StyleRecord;
use swf2ass\Utils;
@ -11,6 +12,8 @@ class lineColorTag extends colorTag {
public static function fromStyleRecord(StyleRecord $record): ?lineColorTag {
if ($record instanceof LineStyleRecord) {
return new lineColorTag($record->color, $record->color);
}else if ($record instanceof FillStyleRecord and $record->border !== null){
return new lineColorTag($record->border->color, $record->border->color);
}
return new lineColorTag(null, null);
}

View file

@ -11,6 +11,7 @@ $settings = [
"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,
"smoothTransitions" => false, //All transitions will happen smoothly from start till finish instead of happening instantly on frame thresholds. NOTE: this can break objects that did not get selected to be animated, or matrix transforms
"gradientSlices" => /*24,*/\swf2ass\Gradient::AUTO_SLICES,
];
$swfContent = file_get_contents($argv[1]);
@ -126,7 +127,7 @@ if ($swf->header["signature"]) {
$lastFrame = null;
while(($frame = $processor->nextFrameOutput()) !== null){
$lastFrame = $frame;
if(!$processor->isPlaying()){
if(!$processor->isPlaying() or $processor->getLoops() > 0){
break;
}
$audio = $processor->getAudio();
@ -222,6 +223,7 @@ if ($swf->header["signature"]) {
break;
}
}
foreach ($assRenderer->flush($lastFrame) as $line){
fwrite($fp, $line . "\n");
}

24
swf2json.php Normal file
View file

@ -0,0 +1,24 @@
<?php
require_once __DIR__ . "/vendor/autoload.php";
$swfContent = file_get_contents($argv[1]);
$swf = new \swf\SWF($swfContent);
$swfContent = null;
unset($swfContent);
if ($swf->header["signature"]) {
$json = [
"header" => $swf->header,
"tags" => []
];
foreach ($swf->tags as $tag) {
$json["tags"][] = $swf->parseTag($tag);
}
echo json_encode($json, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_PRETTY_PRINT);
}