Skip to content

Commit

Permalink
🎨 Add Mm4Job
Browse files Browse the repository at this point in the history
  • Loading branch information
matyo91 committed Oct 20, 2024
1 parent beb53f1 commit 7253ded
Show file tree
Hide file tree
Showing 9 changed files with 379 additions and 6 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ Inspired by
- https://github.com/CodingTrain/Wave-Function-Collapse
- https://github.com/FeatheredSnek/phpwfc

```
bin/console app:wave-function-collapse
```

## License

Live Flow is released under the MIT License.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"doctrine/doctrine-bundle": "^2.12",
"doctrine/doctrine-migrations-bundle": "^3.3",
"doctrine/orm": "^3.2",
"imagine/imagine": "^1.3",
"php-etl/pipeline": "^0.6.1",
"phpdocumentor/reflection-docblock": "^5.4",
"phpstan/phpdoc-parser": "^1.29",
Expand Down
64 changes: 63 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

114 changes: 114 additions & 0 deletions src/Command/WaveFunctionCollapseCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

declare(strict_types=1);

namespace App\Command;

use App\EnumType\WaveFunctionCollapse\DataSetEnumType;
use App\Job\WaveFunctionCollapse\CollapseJob;
use App\Job\WaveFunctionCollapse\ImgJob;
use App\Job\WaveFunctionCollapse\Mp4Job;
use App\Model\WaveFunctionCollapse\Board;
use Flow\Flow\Flow;
use Flow\Flow\YFlow;
use Flow\Ip;
use Imagine\Gd\Imagine;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use ValueError;

use function sprintf;

#[AsCommand(
name: 'app:wave-function-collapse',
description: 'Add a short description for your command',
)]
class WaveFunctionCollapseCommand extends Command
{
public function __construct(
#[Autowire('%kernel.project_dir%/assets')]
private string $assetsDir,
#[Autowire('%kernel.cache_dir%/wave_function_collapse')]
private string $cacheDir,
?string $name = null
) {
parent::__construct($name);
}

protected function configure(): void
{
$this
->addOption('width', null, InputOption::VALUE_OPTIONAL, 'Width of the grid', 5)
->addOption('height', null, InputOption::VALUE_OPTIONAL, 'Height of the grid', 5)
->addOption('dataset', null, InputOption::VALUE_OPTIONAL, 'Dataset to use', DataSetEnumType::CIRCUIT_CODING_TRAIN->value)
;
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);

$imagine = new Imagine();
$width = (int) $input->getOption('width');
$height = (int) $input->getOption('height');
$dataSetValue = $input->getOption('dataset');

try {
$dataSet = DataSetEnumType::from($dataSetValue);
} catch (ValueError $e) {
$io->error('Invalid dataset specified. Available options are: ' . implode(', ', array_column(DataSetEnumType::cases(), 'value')));

return Command::FAILURE;
}

$io->writeln(sprintf('Grid size: %dx%d', $width, $height));
$io->writeln(sprintf('Dataset: %s', $dataSet->value));

$board = new Board($width, $height);
$board->reset($dataSet);

$flow = Flow::do(function () use ($io, $imagine, $board, $dataSet) {
yield new YFlow(function ($collapseLoop) use ($imagine, $board, $dataSet) {
return function ($data) use ($collapseLoop, $imagine, $board, $dataSet) {
[$grid, $images] = $data;

$images[] = (new ImgJob(
$imagine,
$this->assetsDir,
$board->tiles,
$board->width,
$board->height,
$dataSet,
256
))($grid);
$nextGrid = (new CollapseJob($board->tiles, $board->width, $board->height))($grid);

if ($nextGrid === null) {
return [$grid, $images];
}

return $collapseLoop([$nextGrid, $images]);
};
});
yield function ($data) {
[$grid, $images] = $data;

return (new Mp4Job($this->cacheDir))($images);
};
yield static function ($path) use ($io) {
$io->success(sprintf('Movie is generated: %s', $path));
};
});

$flow(new Ip([$board->grid, []]));

$flow->await();

return Command::SUCCESS;
}
}
6 changes: 3 additions & 3 deletions src/Job/WaveFunctionCollapse/CollapseJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
use function in_array;

/**
* @implements JobInterface<mixed, mixed>
* @implements JobInterface<Cell[], Cell[]|null>
*/
class CollapseJob implements JobInterface
{
Expand Down Expand Up @@ -51,10 +51,10 @@ public function __invoke($grid): mixed

$cell = $gridCopy[array_rand($gridCopy)];
$cell->setCollapsed(true);
$pick = $cell->getOptions()[array_rand($cell->getOptions())];
if ($pick === null) {
if (empty($cell->getOptions())) {
return null;
}
$pick = $cell->getOptions()[array_rand($cell->getOptions())];
$cell->setOptions([$pick]);

$nextGrid = [];
Expand Down
67 changes: 67 additions & 0 deletions src/Job/WaveFunctionCollapse/ImgJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace App\Job\WaveFunctionCollapse;

use App\EnumType\WaveFunctionCollapse\DataSetEnumType;
use App\Model\WaveFunctionCollapse\Cell;
use App\Model\WaveFunctionCollapse\Tile;
use Flow\JobInterface;
use Imagine\Image\Box;
use Imagine\Image\ImageInterface;
use Imagine\Image\ImagineInterface;
use Imagine\Image\Point;

use function sprintf;

/**
* @implements JobInterface<Cell[], ImageInterface>
*/
class ImgJob implements JobInterface
{
public function __construct(
private ImagineInterface $imagine,
private string $assetsDir,
/** @var Tile[] */
public array $tiles,
public int $width,
public int $height,
public DataSetEnumType $dataSet,
public int $tileSize = 32,
) {}

public function __invoke($grid): ImageInterface
{
$image = $this->imagine->create(new Box($this->width * $this->tileSize, $this->height * $this->tileSize));

for ($j = 0; $j < $this->height; $j++) {
for ($i = 0; $i < $this->width; $i++) {
$index = $i + $j * $this->height;
$cell = $grid[$index];
if ($cell->isCollapsed()) {
$tile = $this->tiles[$cell->options[0]];
$tileImagePath = sprintf(
'%s/images/wave-function-collapse/%s/%d.png',
$this->assetsDir,
$this->dataSet->value,
$tile->index
);
$tileImage = $this->imagine->open($tileImagePath);

// Resize the tile image if it's not the correct size
if ($tileImage->getSize()->getWidth() !== $this->tileSize || $tileImage->getSize()->getHeight() !== $this->tileSize) {
$tileImage->resize(new Box($this->tileSize, $this->tileSize));
}

// Rotate the tile image based on its direction
$rotatedTileImage = $tileImage->rotate($tile->direction * 90);

$image->paste($rotatedTileImage, new Point($i * $this->tileSize, $j * $this->tileSize));
}
}
}

return $image;
}
}
62 changes: 62 additions & 0 deletions src/Job/WaveFunctionCollapse/Mp4Job.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace App\Job\WaveFunctionCollapse;

use Flow\JobInterface;
use Imagine\Image\ImageInterface;
use RuntimeException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Process\Process;

use function sprintf;

/**
* @implements JobInterface<ImageInterface[], string>
*/
class Mp4Job implements JobInterface
{
public function __construct(
private string $cacheDir,
) {}

public function __invoke($images): string
{
$filesystem = new Filesystem();
$filesystem->mkdir($this->cacheDir);
$generationId = 'wave_function_collapse_' . uniqid();

// make frames
foreach ($images as $i => $image) {
$imagePath = sprintf('%s/%s_%03d.png', $this->cacheDir, $generationId, $i);
$image->save($imagePath);
}

// animate
$outputFile = sprintf('%s/%s.mp4', $this->cacheDir, $generationId);
$cmd = sprintf(
'ffmpeg -framerate 10 -i %s/%s_%%03d.png -vf fps=30 -c:v libx264 -pix_fmt yuv420p %s',
$this->cacheDir,
$generationId,
$outputFile
);

$process = Process::fromShellCommandline($cmd);
$process->run();

if (!$process->isSuccessful()) {
throw new RuntimeException($process->getErrorOutput());
}

// cleanup
$finder = new Finder();
$finder->files()->in($this->cacheDir)->name(sprintf('%s_*.png', $generationId));
foreach ($finder as $file) {
$filesystem->remove($file->getRealPath());
}

return $outputFile;
}
}
3 changes: 2 additions & 1 deletion tools/phpstan/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
"symfonycasts/sass-bundle": "^0.7.0",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0",
"php-etl/pipeline": "^0.6.1"
"php-etl/pipeline": "^0.6.1",
"imagine/imagine": "^1.3"
},
"require-dev": {
"amphp/amp": "^3.0",
Expand Down
Loading

0 comments on commit 7253ded

Please sign in to comment.