Fixed matrix transforms, fixed transitions, added ASS timing system, fixed stroke shape changing from closed to open segments, fixed clip matrix transform from parent

This commit is contained in:
DataHoarder 2022-01-07 11:51:07 +01:00
parent 0b5e7f8635
commit 75507b2b9e
34 changed files with 382 additions and 142 deletions

View file

@ -5,7 +5,6 @@ namespace swf2ass;
class DrawPath {
public StyleRecord $style;
public bool $is_closed;
public Shape $commands;
@ -13,15 +12,14 @@ class DrawPath {
$p = new DrawPath();
$p->style = $style;
$p->commands = $shape;
$p->is_closed = true;
return $p;
}
public static function stroke(LineStyleRecord $style, Shape $shape, bool $is_closed): DrawPath {
public static function stroke(LineStyleRecord $style, Shape $shape): DrawPath {
//TODO: stroke to fill conversion
$p = new DrawPath();
$p->style = $style;
$p->commands = $shape;
$p->is_closed = $is_closed;
return $p;
}
}

View file

@ -7,6 +7,19 @@ class LineStyleRecord implements StyleRecord {
public int $width;
public Color $color;
const LINE_JOIN_MITER = 0;
const LINE_JOIN_MITER_CLIP = 1;
const LINE_JOIN_ROUND = 2;
const LINE_JOIN_BEVEL = 3;
const LINE_CAP_BUTT = 0;
const LINE_CAP_SQUARE = 1;
const LINE_CAP_ROUND = 2;
public int $start_cap = self::LINE_CAP_BUTT;
public int $end_cap = self::LINE_CAP_BUTT;
public int $line_join = self::LINE_JOIN_MITER;
public function __construct(int $width, Color $color) {
$this->width = $width;
$this->color = $color;

View file

@ -13,9 +13,9 @@ class MatrixTransform {
public function __construct(?Vector2 $scale, ?Vector2 $rotateSkew, ?Vector2 $translation) {
$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]
[$scale !== null ? $scale->x : 1 /* a */, /* c */ $rotateSkew !== null ? $rotateSkew->y : 0, 0],
[$rotateSkew !== null ? $rotateSkew->x : 0 /* b */, /* d */ $scale !== null ? $scale->y : 1, 0],
[$translation !== null ? $translation->x : 0, $translation !== null ? $translation->y : 0, 1]
]);
}
@ -32,9 +32,9 @@ class MatrixTransform {
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]
[$this->get_a()->toFixed(), $this->get_b()->toFixed(), 0],
[$this->get_c()->toFixed(), $this->get_d()->toFixed(), 0],
[$this->get_tx()->toFixed(), $this->get_ty()->toFixed(), 1]
];
}else{
return [
@ -72,7 +72,7 @@ class MatrixTransform {
return new MatrixTransform(null, new Vector2(0, tan($angle)), null);
}
public function combine(MatrixTransform $other): MatrixTransform {
public function multiply(MatrixTransform $other): MatrixTransform {
$result = clone $this;
$result->matrix = $this->matrix->multiply($other->matrix);
return $result;
@ -83,11 +83,11 @@ class MatrixTransform {
}
public function get_b() : RealNumber{
return $this->matrix->get(0, 1);
return $this->matrix->get(1, 0);
}
public function get_c() : RealNumber{
return $this->matrix->get(1, 0);
return $this->matrix->get(0, 1);
}
public function get_d() : RealNumber{
@ -95,11 +95,11 @@ class MatrixTransform {
}
public function get_tx() : RealNumber{
return $this->matrix->get(2, 0);
return $this->matrix->get(0, 2);
}
public function get_ty() : RealNumber{
return $this->matrix->get(2, 1);
return $this->matrix->get(1, 2);
}
public function getMatrix() : RealMatrix{
@ -112,11 +112,11 @@ class MatrixTransform {
public function applyToVector(Vector2 $vector, bool $applyTranslation = true): Vector2 {
if($applyTranslation){
$result = $this->matrix->multiply(MatrixFactory::createFromColumnVector([$vector->x, $vector->y, 1]));
$result = (new RealMatrix([[$vector->x, $vector->y, 1]]))->multiply($this->matrix);
}else{
$result = $this->matrix->submatrix(0, 0, 1, 1)->multiply(MatrixFactory::createFromColumnVector([$vector->x, $vector->y]));
$result = (new RealMatrix([[$vector->x, $vector->y]]))->multiply($this->matrix->submatrix(0, 0, 1, 1));
}
return new Vector2($result->get(0, 0)->toFloat(), $result->get(1, 0)->toFloat());
return new Vector2($result->get(0, 0)->toFloat(), $result->get(0, 1)->toFloat());
}
public function applyToShape(Shape $shape, bool $applyTranslation = true): Shape {
@ -129,7 +129,7 @@ class MatrixTransform {
}
public function equals(MatrixTransform $other): bool {
return $this->matrix->isEqual($other->matrix);
return $this->matrix->isEqual($other->matrix, new RealNumber(0.0001));
}
static function fromSWFArray(array $element): MatrixTransform {

View file

@ -29,6 +29,7 @@ class MorphShapeDefinition implements ObjectDefinition {
public function getShapeList(?float $ratio): DrawPathList {
//TODO: cache shapes by ratio
//TOD: refactor this to use color transforms (and if able) matrix transforms
if($ratio === null or abs($ratio) < Constants::EPSILON){
return $this->startShapeList;
}
@ -81,7 +82,7 @@ class MorphShapeDefinition implements ObjectDefinition {
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);
$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);
}else{
var_dump($c1->style);
var_dump($c2->style);

View file

@ -28,10 +28,10 @@ class PendingPath {
}
}
if ($merged === null) {
$this->segments[] = $new_segment;
} else {
if ($merged !== null) {
$this->merge_path($merged, $directed);
} else {
$this->segments[] = $new_segment;
}
}
}

View file

@ -26,6 +26,18 @@ class Shape {
$this->edges[] = $record;
}
public function start(): Vector2 {
return reset($this->edges)->getStart();
}
public function end(): Vector2 {
return end($this->edges)->getEnd();
}
public function is_closed(): bool {
return $this->start()->equals($this->end());
}
/**
* Calculates the intersection between two Shape.
* Shapes part of the clips need to be flat (or they will be flattened)
@ -46,10 +58,9 @@ class Shape {
$self = $this->flatten();
$other = $other->flatten();
var_dump((new Shape($self->getRecords()))->getArea());
var_dump((new \swf2ass\ass\drawTag(new Shape($self->getRecords()), 1))->encode(new \swf2ass\ass\ASSLine(), 1));
var_dump((new \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\ASSLine(), 1));
fgets(STDIN);
var_dump((new \swf2ass\ass\drawTag(new Shape($other->getRecords()), 1))->encode(new \swf2ass\ass\ASSEventTime(1, 1, 1)));
return [$this]; //TODO: fix this breakage, some clips being overlapping shapes????
}
}
@ -97,10 +108,10 @@ class Shape {
$pos = $point;
}
if($shape->getArea() > Constants::EPSILON){ //TODO
//if($shape->getArea() > Constants::EPSILON){ //TODO
$shape->addRecord(new LineRecord($start, $pos)); //Close shape
$shapes[] = $shape;
}
//}
}
return $shapes;

View file

@ -200,22 +200,23 @@ class ShapeConverter {
$style = $this->styles->getLineStyle($styleId - 1);
//wrap around all segments, even if closed. ASS does NOT like them otherwise. so we draw everything backwards to have border around the line, not just on one side
$newSegments = new PendingPath();
foreach ($path->segments as $segment) {
$segmentStyle = $style;
//Close non-closed segments by double drawing backwards
if (!$segment->is_closed()) {
$other = clone $segment;
$other->flip();
$segment->merge($other);
$other = clone $segment;
$other->flip();
$segment->merge($other);
//Reduce width of line style to account for double border
$segmentStyle = clone $style;
$segmentStyle->width /= 2;
}
$this->commands->commands[] = DrawPath::stroke($segmentStyle, $segment->getShape(), $segment->is_closed());
$newSegments->merge_path($segment, false);
}
if(count($newSegments->segments) > 0){
//Reduce width of line style to account for double border
$fixedStyle = clone $style;
$fixedStyle->width /= 2;
$this->commands->commands[] = DrawPath::stroke($fixedStyle, $newSegments->getShape());
}
//TODO: leave this as-is and create a fill in renderer
}
$this->strokes->map = [];
}

View file

@ -22,7 +22,7 @@ class Vector2 {
public function equals(Vector2 $b, $epsilon = Constants::EPSILON): bool {
return abs($b->x - $this->x) <= $epsilon and abs($b->y - $this->y) <= $epsilon;
return ($b->x === $this->x or abs($b->x - $this->x) <= $epsilon) and ($b->y === $this->y or abs($b->y - $this->y) <= $epsilon);
}
public function distance(Vector2 $b): float {

View file

@ -70,7 +70,7 @@ class ViewFrame {
$matrixTransform = $parentMatrix;
if($this->matrixTransform !== null){
$matrixTransform = $parentMatrix !== null ? $parentMatrix->combine($this->matrixTransform) : $this->matrixTransform;
$matrixTransform = $parentMatrix !== null ? $this->matrixTransform->multiply($parentMatrix) : $this->matrixTransform;
}
$colorTransform = $parentColor;
@ -84,16 +84,18 @@ class ViewFrame {
$clipPath = null;
if ($this->clipDepthMap !== null) {
$colorIdentity = ColorTransform::identity();
$matrixIdentity = MatrixTransform::identity();
$matrixIdentity = $parentMatrix;
foreach ($this->clipDepthMap as $clipDepth => $clipFrame) {
//TODO: detect rectangle clips?
//TODO: clip clips?
foreach ($clipFrame->render($clipDepth, $depthChain, $colorIdentity, $matrixIdentity)->getObjects() as $clipObject) {
$clipShape = new ClipPath();
foreach ($clipObject->drawPathList->commands as $p) {
$s = $p->commands->flatten();
if($s->getArea() > Constants::EPSILON){
$clipShape->addShape($s);
if($p->style instanceof FillStyleRecord){ //Only clip with fills
$s = $p->commands->flatten();
if($s->getArea() > Constants::EPSILON){
$clipShape->addShape($s);
}
}
}

View file

@ -119,19 +119,22 @@ class ViewLayout {
$frame = new ViewFrame($this->getObjectId(), null);
/** @var ClippingViewLayout[] $clipMap */
$clipMap = [];
/** @var ViewFrame[] $clipFrame */
$clipFrame = [];
ksort($this->depthMap);
foreach ($this->depthMap as $depth => $child) {
if ($child instanceof ClippingViewLayout) {
$clipMap[$depth] = $child;
$clipFrame[$depth] = $child->nextFrame($actionList);
} else {
/** @var ViewFrame[] $clips */
$clips = []; //TODO: make something else?
foreach ($clipMap as $clipDepth => $clip) {
//$targetDepth = $clip->getClipDepth() + $clipDepth;
if ($clip->getClipDepth() > $depth and $clipDepth < $depth) {
$clips[$clipDepth] = $clip->nextFrame($actionList);
$clips[$clipDepth] = $clipFrame[$clipDepth];
}
}

55
src/ass/ASSEventTime.php Normal file
View file

@ -0,0 +1,55 @@
<?php
namespace swf2ass\ass;
class ASSEventTime {
/**
* Frame duration in milliseconds
* @var float
*/
public float $frame_duration;
public ASSTime $start;
public int $start_frame;
public ASSTime $end;
public int $end_frame;
public int $duration;
public function __construct(int $startFrame, int $duration, float $frameDurationMilliseconds, int $decimalPrecision = 2){
$this->frame_duration = $frameDurationMilliseconds;
$this->start_frame = $startFrame;
$this->start = new ASSTime($this->start_frame * $frameDurationMilliseconds, $decimalPrecision, true);
$this->end_frame = $startFrame + $duration;
$this->end = new ASSTime($this->end_frame * $frameDurationMilliseconds, $decimalPrecision, false);
$this->duration = $duration;
}
public function getMillisecondsFromStartOffset(int $frameOffset) : int {
if(($this->start_frame + $frameOffset) > $this->end_frame){
throw new \Exception("Out of bounds: {$this->start_frame} + $frameOffset > {$this->end_frame}");
}
return ($this->frame_duration * $frameOffset) + $this->start->adjusted_ms_error;
}
public function getMillisecondsFromEndOffset(int $frameOffset) : int {
if($frameOffset > $this->duration){
throw new \Exception("Out of bounds: $frameOffset > {$this->duration}");
}
return $this->getMillisecondsFromStartOffset($this->duration - $frameOffset);
}
public function slice(int $frameOffset, int $frameDuration = 1) : ASSEventTime{
if(($this->start_frame + $frameOffset + $frameDuration) > $this->end_frame){
throw new \Exception("Out of bounds: {$this->start_frame} + $frameOffset + $frameDuration > {$this->end_frame}");
}
return new ASSEventTime($this->start_frame + $frameOffset, $frameDuration, $this->start->adjusted_ms_precision);
}
}

View file

@ -7,8 +7,6 @@ use swf2ass\RenderedFrame;
use swf2ass\RenderedObject;
class ASSLine {
const HOURS_MS = 1000 * 3600;
const MINUTES_MS = 1000 * 60;
/** @var int[] */
public array $layer;
@ -26,7 +24,7 @@ class ASSLine {
public bool $isComment = false;
/** @var ASSTag[] */
public array $tags;
public array $tags = [];
private ?string $cachedEncode = null;
@ -107,28 +105,6 @@ class ASSLine {
return $lines;
}
/**
* NOTE: libass parses milliseconds in a way that anything different than 2 decimal precisions will make it fail.
* TODO: use \t for exact timed line entries
*
* @param int $ms
* @param int $msPrecision
* @return string
*/
public static function encodeTime(int $ms, int $msPrecision = 2): string {
if ($ms < 0) {
throw new \LogicException("ms less than 0: $ms");
}
$hours = intdiv($ms, self::HOURS_MS);
$ms -= $hours * self::HOURS_MS;
$minutes = intdiv($ms, self::MINUTES_MS);
$ms -= $minutes * self::MINUTES_MS;
$msPadding = 3 + $msPrecision;
return sprintf("%01d:%02d:%0{$msPadding}.{$msPrecision}F", $hours, $minutes, $ms / 1000);
}
public function dropCache(){
$this->cachedEncode = null;
}
@ -151,9 +127,22 @@ class ASSLine {
if($frameDurationMs === 1000 and $msPrecision === 2 and $this->cachedEncode !== null){
return $this->cachedEncode;
}
$line = ($this->isComment ? "Comment" : "Dialogue") . ": " . $this->getPackedLayer() . "," . self::encodeTime($this->start * $frameDurationMs, $msPrecision) . "," . self::encodeTime(($this->end + 1) * $frameDurationMs, $msPrecision) . "," . $this->style . "," . $this->name . "," . $this->marginLeft . "," . $this->marginRight . "," . $this->marginVertical . "," . $this->effect . ",";
$assEventTime = new ASSEventTime($this->start, $this->end - $this->start + 1, $frameDurationMs, $msPrecision);
$line = ($this->isComment ? "Comment" : "Dialogue") . ": " . $this->getPackedLayer() . "," . $assEventTime->start->encode() . "," . $assEventTime->end->encode() . "," . $this->style . "," . $this->name . "," . $this->marginLeft . "," . $this->marginRight . "," . $this->marginVertical . "," . $this->effect . ",";
if($assEventTime->start->adjusted_ms_error !== 0 or $assEventTime->end->adjusted_ms_error !== 0){
//Maybe use fade?
$frameStartTime = $assEventTime->getMillisecondsFromStartOffset(0);
$frameEndTime = $assEventTime->getMillisecondsFromEndOffset(0);
//TODO: maybe needs to be -1?
$line .= "{\\fade(255,0,255,{$frameStartTime},{$frameStartTime},{$frameEndTime},{$frameEndTime})\\err({$assEventTime->start->milliseconds}~{$assEventTime->start->adjusted_ms_error},{$assEventTime->end->milliseconds}~{$assEventTime->end->adjusted_ms_error})}";
}
foreach ($this->tags as $tag){
$line .= "{" . $tag->encode($this, $frameDurationMs) . "}";
$line .= "{" . $tag->encode($assEventTime) . "}";
}
if($frameDurationMs === 1000){
$this->cachedEncode = $line;

View file

@ -14,25 +14,29 @@ class ASSRenderer {
/** @var ASSLine[] */
private array $runningBuffer = [];
private array $settings;
private static array $settings = [];
public function getSetting($name, $default = null){
return $this->settings[$name] ?? $default;
public static function getSetting($name, $default = null){
return self::$settings[$name] ?? $default;
}
public function __construct(float $frameRate, Rectangle $viewPort, array $settings = []) {
$this->settings = $settings;
public static function setSettings(array $settings){
self::$settings = $settings;
}
public function __construct(float $frameRate, Rectangle $viewPort) {
$display = $viewPort->toPixel();
$width = $display->getWidth() * $this->getSetting("videoScaleMultiplier", 1);
$height = $display->getHeight() * $this->getSetting("videoScaleMultiplier", 1);
$width = $display->getWidth() * self::getSetting("videoScaleMultiplier", 1);
$height = $display->getHeight() * self::getSetting("videoScaleMultiplier", 1);
$ar = $width / $height;
if(($frameRate * 2) <= 60){
/*if(($frameRate * 2) <= 60){
$frameRate *= 2;
}
}*/
$frameRate *= self::getSetting("videoRateMultiplier", 1);
$timerPrecision = sprintf("%.4F", (100 / $this->getSetting("timerSpeed", 100)) * 100);
$timerPrecision = sprintf("%.4F", (100 / self::getSetting("timerSpeed", 100)) * 100);
$this->header = <<<ASSHEADER
[Script Info]
@ -48,7 +52,7 @@ PlayResY: {$height}
Timer: {$timerPrecision}
[Aegisub Project Garbage]
Last Style Storage: Default
Last Style Storage: f
Video File: ?dummy:{$frameRate}:10000:{$width}:{$height}:160:160:160:c
Video AR Value: {$ar}
@ -75,11 +79,12 @@ ASSHEADER;
$runningBuffer = [];
$scale = MatrixTransform::scale(new Vector2($this->getSetting("videoScaleMultiplier", 1), $this->getSetting("videoScaleMultiplier", 1)));
$scale = MatrixTransform::scale(new Vector2(self::getSetting("videoScaleMultiplier", 1), self::getSetting("videoScaleMultiplier", 1)));
$animated = 0;
foreach ($objects as $object) {
$object = clone $object;
$object->matrixTransform = $scale->combine($object->matrixTransform);
$object->matrixTransform = $scale->multiply($object->matrixTransform); //TODO order?
$depth = $object->getDepth();
@ -99,6 +104,7 @@ ASSHEADER;
$tag = $tag->transition($information, $object);
if($tag !== null){
$transitionedTags[] = $tag;
$tag->dropCache();
}else{
$canTransition = false;
break;
@ -106,11 +112,12 @@ ASSHEADER;
}
if($canTransition and count($transitionedTags) > 0){
$animated += count($transitionedTags);
$runningBuffer = array_merge($runningBuffer, $transitionedTags);
}else{
$this->runningBuffer = array_merge($this->runningBuffer, $tagsToTransition);
foreach (ASSLine::fromRenderObject($information, $object, $this->getSetting("bakeTransforms", false)) as $line) {
foreach (ASSLine::fromRenderObject($information, $object, self::getSetting("bakeTransforms", false)) as $line) {
$line->style = "f";
$line->dropCache();
$runningBuffer[] = $line;
@ -118,11 +125,13 @@ ASSHEADER;
}
}
echo "[ASS] Total " . count($objects) . " objects, " . count($this->runningBuffer) . " flush, " . count($runningBuffer) ." buffer, $animated animated tags.\n";
//Flush non dupes
foreach ($this->runningBuffer as $line) {
$line->name .= " f:{$line->start}>{$line->end}~".($line->end - $line->start + 1);
$line->dropCache();
yield $line->encode($information->getFrameDurationMilliSeconds() * ($this->getSetting("timerSpeed", 100) / 100), $this->getSetting("timePrecision"));
yield $line->encode($information->getFrameDurationMilliSeconds() * (self::getSetting("timerSpeed", 100) / 100), self::getSetting("timePrecision"));
}
$this->runningBuffer = $runningBuffer;
@ -132,7 +141,7 @@ ASSHEADER;
foreach ($this->runningBuffer as $line) {
$line->name .= " f:{$line->start}>{$line->end}~".($line->end - $line->start + 1);
$line->dropCache();
yield $line->encode($information->getFrameDurationMilliSeconds() * ($this->getSetting("timerSpeed", 100) / 100), $this->getSetting("timePrecision"));
yield $line->encode($information->getFrameDurationMilliSeconds() * (self::getSetting("timerSpeed", 100) / 100), self::getSetting("timePrecision"));
}
$this->runningBuffer = [];
}

View file

@ -9,5 +9,5 @@ interface ASSTag {
public function equals(ASSTag $tag): bool;
public function encode(ASSLine $line, float $frameDurationMs): string;
public function encode(ASSEventTime $event): string;
}

56
src/ass/ASSTime.php Normal file
View file

@ -0,0 +1,56 @@
<?php
namespace swf2ass\ass;
class ASSTime {
const HOURS_MS = 1000 * 3600;
const MINUTES_MS = 1000 * 60;
public int $milliseconds_total;
public int $milliseconds_total_adjusted;
public int $hours;
public int $minutes;
public int $seconds;
public int $milliseconds;
public int $adjusted_ms;
public int $adjusted_ms_precision;
public int $adjusted_ms_error;
public function __construct(int $ms, int $decimalPrecision = 2, bool $roundDown = true){
if ($ms < 0) {
throw new \LogicException("ms less than 0: $ms");
}
$this->milliseconds_total = $ms;
$this->adjusted_ms_precision = $decimalPrecision;
$this->hours = intdiv($ms, self::HOURS_MS);
$ms -= $this->hours * self::HOURS_MS;
$this->minutes = intdiv($ms, self::MINUTES_MS);
$ms -= $this->minutes * self::MINUTES_MS;
$this->seconds = intdiv($ms, 1000);
$this->milliseconds = $ms - $this->seconds * 1000;
$ms_adjustement = (10 * (3 - $this->adjusted_ms_precision));
$this->adjusted_ms = intdiv($this->milliseconds, $ms_adjustement);
$this->adjusted_ms_error = $this->milliseconds - $this->adjusted_ms * $ms_adjustement;
if(!$roundDown and $this->adjusted_ms_error > 0){
$this->adjusted_ms++;
$this->adjusted_ms_error -= $ms_adjustement;
}
$this->milliseconds_total_adjusted = $this->milliseconds_total + $this->adjusted_ms_error;
}
/**
* NOTE: libass parses milliseconds in a way that anything different than 2 decimal precisions will make it fail.
* @return string
*/
public function encode() : string{
return sprintf("%01d:%02d:%02d.%0{$this->adjusted_ms_precision}d", $this->hours, $this->minutes, $this->seconds, $this->adjusted_ms);
}
}

View file

@ -18,7 +18,7 @@ class blurEdgesGaussianTag implements ASSStyleTag {
return self::fromStyleRecord($record);
}
public function encode(ASSLine $line, float $frameDurationMs): string {
public function encode(ASSEventTime $event): string {
return "\\blur" . round($this->strength, 4);
}

View file

@ -17,7 +17,7 @@ class blurEdgesTag implements ASSStyleTag {
return self::fromStyleRecord($record);
}
public function encode(ASSLine $line, float $frameDurationMs): string {
public function encode(ASSEventTime $event): string {
return "\\be{$this->strength}";
}

View file

@ -19,7 +19,7 @@ class borderTag implements ASSStyleTag {
return self::fromStyleRecord($record);
}
public function encode(ASSLine $line, float $frameDurationMs): string {
public function encode(ASSEventTime $event): string {
if($this->size->x === $this->size->y){
return sprintf("\\bord%.02F", $this->size->x);
}else{

View file

@ -5,7 +5,7 @@ namespace swf2ass\ass;
class clipTag extends clippingTag {
public function encode(ASSLine $line, float $frameDurationMs): string {
public function encode(ASSEventTime $event): string {
$scaleMultiplier = 2 ** ($this->scale - 1);
return $this->isNull ? "" : "\\clip({$this->scale}," . implode(" ", $this->getCommands($scaleMultiplier, $this->scale >= 5 ? 0 : 2)) . ")";
}

View file

@ -7,6 +7,7 @@ use MathPHP\LinearAlgebra\MatrixFactory;
use swf2ass\ClipPath;
use swf2ass\ColorTransform;
use swf2ass\DrawPath;
use swf2ass\LineStyleRecord;
use swf2ass\MatrixTransform;
use swf2ass\Shape;
use swf2ass\StyleRecord;
@ -24,7 +25,7 @@ class containerTag implements ASSColorTag, ASSPositioningTag, ASSStyleTag, ASSPa
public function transitionColor(ASSLine $line, ColorTransform $transform): ?containerTag {
$container = clone $this;
$index = $line->end - $line->start - 1;
$index = $line->end - $line->start;
if(!isset($container->transitions[$index])){
$container->transitions[$index] = [];
}
@ -53,7 +54,7 @@ class containerTag implements ASSColorTag, ASSPositioningTag, ASSStyleTag, ASSPa
$container = clone $this;
$index = $line->end - $line->start - 1;
$index = $line->end - $line->start;
if(!isset($container->transitions[$index])){
$container->transitions[$index] = [];
}
@ -80,7 +81,7 @@ class containerTag implements ASSColorTag, ASSPositioningTag, ASSStyleTag, ASSPa
public function transitionStyleRecord(ASSLine $line, StyleRecord $record): ?containerTag {
$container = clone $this;
$index = $line->end - $line->start - 1;
$index = $line->end - $line->start;
if(!isset($container->transitions[$index])){
$container->transitions[$index] = [];
}
@ -115,6 +116,10 @@ class containerTag implements ASSColorTag, ASSPositioningTag, ASSStyleTag, ASSPa
$container->try_append(new clipTag($clip));
if($path->style instanceof LineStyleRecord){ //Convert to fill
}
$container->try_append(borderTag::fromStyleRecord($path->style));
$container->try_append(shadowTag::fromStyleRecord($path->style));
$container->try_append(lineColorTag::fromStyleRecord($path->style)->applyColorTransform($colorTransform));
@ -169,30 +174,40 @@ class containerTag implements ASSColorTag, ASSPositioningTag, ASSStyleTag, ASSPa
return false;
}
public function encode(ASSLine $line, float $frameDurationMs): string {
public function encode(ASSEventTime $event): string {
$ret = "";
foreach ($this->tags as $tag) {
if(!($tag instanceof drawingTag)){
$ret .= $tag->encode($line, $frameDurationMs);
$ret .= $tag->encode($event);
}
}
foreach ($this->transitions as $index => $transitions){
if(count($transitions) === 0){
continue;
}
//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) - 1) . "," . (floor($frameDurationMs * ($index + 1)) - 1) . ",";
//$ret .= "\\t(" . floor($frameDurationMs * ($index + 1)) . "," . floor($frameDurationMs * ($index + 1)) . ",";
if(ASSRenderer::getSetting("smoothTransitions", false)){
$startTime = $event->getMillisecondsFromStartOffset($index - 1) + 1;
$endTime = $event->getMillisecondsFromStartOffset($index + 1) - 1;
}else{
$startTime = $event->getMillisecondsFromStartOffset($index) - 1;
$endTime = $event->getMillisecondsFromStartOffset($index);
}
$ret .= "\\t({$startTime},{$endTime},";
foreach ($transitions as $tag){
$ret .= $tag->encode($line, $frameDurationMs);
$ret .= $tag->encode($event);
}
$ret .= ")";
}
foreach ($this->tags as $tag) {
if($tag instanceof drawingTag){
$ret .= $tag->encode($line, $frameDurationMs);
$ret .= $tag->encode($event);
}
}
return $ret;
@ -201,7 +216,7 @@ class containerTag implements ASSColorTag, ASSPositioningTag, ASSStyleTag, ASSPa
public function transitionShape(ASSLine $line, Shape $shape): ?ASSPathTag {
$container = clone $this;
$index = $line->end - $line->start - 1;
$index = $line->end - $line->start;
if(!isset($container->transitions[$index])){
$container->transitions[$index] = [];
}
@ -223,7 +238,7 @@ class containerTag implements ASSColorTag, ASSPositioningTag, ASSStyleTag, ASSPa
public function transitionClipPath(ASSLine $line, ?ClipPath $clip): ?ASSClipPathTag {
$container = clone $this;
$index = $line->end - $line->start - 1;
$index = $line->end - $line->start;
if(!isset($container->transitions[$index])){
$container->transitions[$index] = [];
}

View file

@ -18,7 +18,7 @@ class drawTag extends drawingTag implements ASSPathTag {
return $this->shape->equals($shape) ? $this : null;
}
public function encode(ASSLine $line, float $frameDurationMs): string {
public function encode(ASSEventTime $event): string {
$scaleMultiplier = 2 ** ($this->scale - 1);
return "\\p".$this->scale."}" . implode(" ", $this->getCommands($scaleMultiplier, $this->scale >= 5 ? 0 : 2)) . "{\\p0";
}

View file

@ -6,13 +6,11 @@ use swf2ass\Constants;
use swf2ass\CubicCurveRecord;
use swf2ass\CubicSplineCurveRecord;
use swf2ass\LineRecord;
use swf2ass\LineStyleRecord;
use swf2ass\MatrixTransform;
use swf2ass\MoveRecord;
use swf2ass\QuadraticCurveRecord;
use swf2ass\Record;
use swf2ass\Shape;
use swf2ass\StyleRecord;
abstract class drawingTag implements ASSTag {
const PRECISION = 2;
@ -87,6 +85,11 @@ abstract class drawingTag implements ASSTag {
$lastEdge = $edge; //TODO
}
/*if(!$this->shape->is_closed()){
$coords = $this->shape->start()->multiply($scale / Constants::TWIP_SIZE);
$commands[] = "n " . round($coords->x, $precision) . " " . round($coords->y, $precision);
}*/
return $commands;
}

View file

@ -25,7 +25,7 @@ class fillColorTag extends colorTag {
return new fillColorTag(null, null);
}
public function encode(ASSLine $line, float $frameDurationMs): string {
public function encode(ASSEventTime $event): string {
return $this->color === null ? "\\1a&HFF&" : ("\\1c&H" . strtoupper(Utils::padHex(dechex($this->color->b))) . strtoupper(Utils::padHex(dechex($this->color->g))) . strtoupper(Utils::padHex(dechex($this->color->r))) . "&\\1a&H" . strtoupper(Utils::padHex(dechex(255 - $this->color->alpha))) . "&");
}
}

View file

@ -4,7 +4,7 @@ namespace swf2ass\ass;
class insideClipTag extends clippingTag {
public function encode(ASSLine $line, float $frameDurationMs): string {
public function encode(ASSEventTime $event): string {
$scaleMultiplier = 2 ** ($this->scale - 1);
return $this->isNull ? "" : "\\iclip({$this->scale}," . implode(" ", $this->getCommands($scaleMultiplier, $this->scale >= 5 ? 0 : 2)) . ")";
}

View file

@ -15,7 +15,7 @@ class lineColorTag extends colorTag {
return new lineColorTag(null, null);
}
public function encode(ASSLine $line, float $frameDurationMs): string {
public function encode(ASSEventTime $event): string {
return $this->color === null ? "\\3a&HFF&" : ("\\3c&H" . strtoupper(Utils::padHex(dechex($this->color->b))) . strtoupper(Utils::padHex(dechex($this->color->g))) . strtoupper(Utils::padHex(dechex($this->color->r))) . "&\\3a&H" . strtoupper(Utils::padHex(dechex(255 - $this->color->alpha))) . "&");
}
}

View file

@ -22,35 +22,84 @@ class matrixTransformTag implements ASSPositioningTag {
}
public function transitionMatrixTransform(ASSLine $line, MatrixTransform $transform): ?matrixTransformTag {
$new = self::fromMatrixTransform($transform);
return $this->equals($new) ? $this : null; //TODO: check this
return self::fromMatrixTransform($transform);
}
public function encode(ASSLine $line, float $frameDurationMs): string {
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 encode(ASSEventTime $event): string {
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($event) . $this->rotation->encode($event) . $this->shear->encode($event);
}
public function equals(ASSTag $tag): bool {
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 {
$isZero = function (RealNumber $v) : bool{
return $v->isZero(new RealNumber("0.00001")); ///TODO
};
private static function getByTransform_NumericallyStable(MatrixTransform $transform) : ?matrixTransformTag{
$scale_x = $scale_y = $frx = $fry = $frz = $fax = $fay = new RealNumber(0);
//Numerically stable implementation by MrSmile
$a = $transform->get_a();
$b = $transform->get_b();
$c = $transform->get_c();
$d = $transform->get_d();
if(($isZero($a) and $isZero($b)) or ($isZero($c) and $isZero($d))){
throw new \Exception("Invalid transform");
$ac2 = $a->power(2)->add($c->power(2));
$bd2 = $b->power(2)->add($d->power(2));
$det = $a->multiply($d)->subtract($b->multiply($c));
$dot = $a->multiply($b)->add($c->multiply($d));
$scale_x = $scale_y = $frx = $fry = $frz = $fax = $fay = new RealNumber(0);
if($ac2->greater($bd2)){
if($ac2->greater(0)){
$frz = new RealNumber(atan2($c->toFloat(), $a->toFloat()) * (180 / M_PI));
$scale_x = $ac2->sqrt();
$scale_y = $det->absolute()->divide($ac2->sqrt());
$fax = $dot->divide($ac2);
if($det->lesser(0)){
$frz = $frz->negate();
$frx = new RealNumber(180);
}
}
}else{
if($bd2->greater(0)){
$frz = new RealNumber(atan2($b->negate()->toFloat(), $d->toFloat()) * (180 / M_PI));
$scale_x = $det->absolute()->divide($bd2->sqrt());
$scale_y = $bd2->sqrt();
$fay = $dot->divide($bd2);
if($det->lesser(0)){
$frz = $frz->negate();
$fry = new RealNumber(180);
}
}
}
$frz = $frz->negate();
$fscx = $scale_x->absolute()->multiply(100);
$fscy = $scale_y->absolute()->multiply(100);
return new matrixTransformTag($transform, new Vector2($fscx->toFloat(), $fscy->toFloat()), $frx->toFloat(), $fry->toFloat(), $frz->toFloat(), $fax->toFloat(), $fay->toFloat());
}
private static function getByTransform_NumericallyUnstable(MatrixTransform $transform) : ?matrixTransformTag{
//Numerically unstable implementation by Oneric
$isZero = function (RealNumber $v) : bool{
return $v->isZero(new RealNumber("0.000001")); ///TODO
};
$a = $transform->get_a();
$b = $transform->get_b();
$c = $transform->get_c();
$d = $transform->get_d();
$scale_x = $scale_y = $frx = $fry = $frz = $fax = $fay = new RealNumber(0);
if(
!(
($isZero($a) and !$isZero($b))
@ -115,15 +164,22 @@ class matrixTransformTag implements ASSPositioningTag {
$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);
$frz->frz = $frz->add(180);
}
}else {
echo $transform . "\n";
throw new \Exception("Invalid transform state. This should not happen");
}
$frz = $frz->negate();
$fscx = $scale_x->absolute()->multiply(100);
$fscy = $scale_y->absolute()->multiply(100);
return new matrixTransformTag($transform, new Vector2($fscx->toFloat(), $fscy->toFloat()), $frx->toFloat(), $fry->toFloat(), $frz->toFloat(), $fax->toFloat(), $fay->toFloat());
}
public static function fromMatrixTransform(MatrixTransform $transform): ?matrixTransformTag {
return self::getByTransform_NumericallyStable($transform);
}
}

View file

@ -15,7 +15,7 @@ class originTag implements ASSTag {
$this->origin = $origin;
}
public function encode(ASSLine $line, float $frameDurationMs): string {
public function encode(ASSEventTime $event): string {
return "\\org(" . round($this->origin->x, 5) ."," . round($this->origin->y, 5) .")";
}

View file

@ -66,19 +66,18 @@ class positionTag implements ASSPositioningTag {
}
}
public function encode(ASSLine $line, float $frameDurationMs): string {
$frame = $line->end - $line->start;
public function encode(ASSEventTime $event): string {
$hasMoved = $this->start !== $this->end;
$shift = $this->end - $this->start;
if($hasMoved){
if($shift > 1){
$start = ceil(($this->start - 1) * $frameDurationMs) + 1;
$end = floor($this->end * $frameDurationMs) - 1;
if($shift > 1 or ASSRenderer::getSetting("smoothTransitions", false)){
$start = $event->getMillisecondsFromStartOffset($this->start - 1);
$end = $event->getMillisecondsFromStartOffset($this->end);
}else{
$start = floor($this->start * $frameDurationMs) - 1;
$end = floor(($this->end - 1) * $frameDurationMs) - 1;
$start = $event->getMillisecondsFromStartOffset($this->start) - 1;
$end = $event->getMillisecondsFromStartOffset($this->start);
}
return "\\move(" . $this->from->x ."," . $this->from->y ."," . $this->to->x ."," . $this->to->y .",".$start.",".$end.")";
}

View file

@ -21,7 +21,7 @@ class rotationTag implements ASSPositioningTag {
return self::fromMatrixTransform($transform);
}
public function encode(ASSLine $line, float $frameDurationMs): string {
public function encode(ASSEventTime $event): string {
return sprintf("\\frx%.2F\\fry%.2F\\frz%.2F", $this->x, $this->y, $this->z);
}

View file

@ -19,7 +19,7 @@ class scaleTag implements ASSPositioningTag {
return self::fromMatrixTransform($transform);
}
public function encode(ASSLine $line, float $frameDurationMs): string {
public function encode(ASSEventTime $event): string {
return sprintf("\\fscx%.5F\\fscy%.5F", $this->scale->x, $this->scale->y);
}

View file

@ -18,7 +18,7 @@ class shadowTag implements ASSStyleTag {
return self::fromStyleRecord($record);
}
public function encode(ASSLine $line, float $frameDurationMs): string {
public function encode(ASSEventTime $event): string {
return "\\shad{$this->depth}";
}

View file

@ -19,7 +19,7 @@ class shearingTag implements ASSPositioningTag {
return self::fromMatrixTransform($transform);
}
public function encode(ASSLine $line, float $frameDurationMs): string {
public function encode(ASSEventTime $event): string {
return sprintf("\\fax%.5F\\fay%.5F", $this->shear->x, $this->shear->y);
}

View file

@ -48,7 +48,7 @@ class RealMatrix extends ObjectMatrix{
return $this->m === $this->n;
}
public function isEqual(Matrix $B): bool
public function isEqual(Matrix $B, ?RealNumber $epsilon = null): bool
{
if (!$this->isEqualSizeAndType($B)) {
return false;
@ -60,7 +60,7 @@ class RealMatrix extends ObjectMatrix{
for ($i = 0; $i < $m; $i++) {
for ($j = 0; $j < $n; $j++) {
/** @var $B RealNumber[][] */
if (!$B[$i][$j]->equalWithEpsilon($this->A[$i][$j])) {
if (!$B[$i][$j]->equalWithEpsilon($this->A[$i][$j], $epsilon)) {
return false;
}
}

View file

@ -4,9 +4,13 @@ require_once __DIR__ . "/vendor/autoload.php";
$settings = [
"videoScaleMultiplier" => 1, //TODO: not finished, leave at 1
//TODO: make this actually interpolate smooth transitions in a baked way
"videoRateMultiplier" => 1, //Sets the "framerate" multiplier for output video file, not lines. Helps with smoothing transitions if enabled.
"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,
"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
];
$swfContent = file_get_contents($argv[1]);
@ -59,7 +63,31 @@ $knownFlashSignatures = [
new RemovalEntry(null /*3*/, [0, 145]),
new RemovalEntry(null /*69*/, [0, 146]),
]
]
],
"229d7569ebf3b3b04fb03aa86162ab646d96be0353e8f4be32b1f6bf4e96af4e" => [
"name" => "cirno's_arithmetic_school.swf",
"remove" => [
//remove lyrics button/mask
new RemovalEntry(null /*30*/, [0, 259]),
new RemovalEntry(null /*31*/, [0, 264]),
]
],
"1203faf6504edf4845e72b957ce6b5c5d92597ecc0328aa36eacaef32eb7125a" => [
"name" => "cirno's_arithmetic_school_2011.swf",
"remove" => [
//remove lyrics button/mask
new RemovalEntry(null /*30*/, [0, 229]),
new RemovalEntry(null /*31*/, [0, 234]),
]
],
"3bdc7f8bbfb648e825f79dbe6095288871b1780b1d37f8cd2ad4c50cae40b5ca" => [
"name" => "IOSYS_CirnoENG.swf",
"remove" => [
//remove lyrics button/mask
new RemovalEntry(null /*33*/, [0, 1721]),
new RemovalEntry(null /*34*/, [0, 1752]),
]
],
];
if(isset($knownFlashSignatures[$signature])){
@ -91,7 +119,8 @@ $testVectors = [];
if ($swf->header["signature"]) {
$processor = new \swf2ass\SWFProcessor($swf);
$assRenderer = new \swf2ass\ass\ASSRenderer($processor->getFrameRate(), $processor->getViewPort(), $settings);
\swf2ass\ass\ASSRenderer::setSettings($settings);
$assRenderer = new \swf2ass\ass\ASSRenderer($processor->getFrameRate(), $processor->getViewPort());
$keyFrameInterval = 10 * $processor->getFrameRate(); //kf every 10 seconds TODO: make this dynamic, per-shape
$lastFrame = null;