<?phpnamespace App\Scene;use App\Audio\Scale;use App\Render\Gfx;use App\Render\ZoomView;use JumPHP\Renderer\Renderer;/** * Dominoes on a spiral seen from above: a wave of falling tiles runs from the outside in, each one ringing the next, higher note, and turns * from white to its colour; then they all stand up again in the opposite order, softly. 12 seconds for the whole cycle: the film loops with * no seam. */final class DominoScene extends VideoScene{ public const SECONDS = 24.0; protected const LOOPS = true; public const PERIOD = 12.0; public const COUNT = 56; private const DT = 0.1; private const FALL = 0.45; private const RISE_AT = 6.0; /** @var list<array{0: float, 1: float, 2: float}> x, y and the direction of the path of each tile */ private array $path = []; private float $prev = 0.0; private int $fallen = 0; protected function melodyNotes(): string { return 'C4'; } protected function headingText(): string { return 'THE PERFECT CHAIN REACTION'; } protected function hookText(): string { return 'WAIT FOR THE LAST ONE'; } protected function captionText(): string { return $this->t('FALLEN {n} / {m}', ['n' => $this->fallen, 'm' => self::COUNT]); } protected function build(): void { $this->prev = 0.0; $this->fallen = 0; $this->path = []; // a spiral from the outside in, the tiles a constant distance apart along it $theta = 0.0; $rad = 128.0; for ($i = 0; $i < self::COUNT; $i++) { $this->path[] = [135 + cos($theta) * $rad, 270 + sin($theta) * $rad, $theta + M_PI / 2]; $dTheta = 17.0 / $rad; // tiles 17 px apart along the path, 57 px between two turns $theta += $dTheta; $rad = max(14.0, $rad - 57.0 * $dTheta / (2 * M_PI)); } } public function fallenCount(): int { return $this->fallen; } private function angleAt(int $i, float $tt): float { $f = $i * self::DT; $up = self::RISE_AT + (self::COUNT - 1 - $i) * self::DT; if ($tt < $f) { return 0.0; } if ($tt < $f + self::FALL) { return M_PI / 2 * ZoomView::ease(($tt - $f) / self::FALL); } if ($tt < $up) { return M_PI / 2; } if ($tt < $up + self::FALL) { return M_PI / 2 * (1 - ZoomView::ease(($tt - $up) / self::FALL)); } return 0.0; } protected function tick(float $dt): void { $tt = fmod($this->time, self::PERIOD); $prev = $this->prev; if ($tt < $prev) { $prev = -0.001; // a new cycle } $this->fallen = 0; for ($i = 0; $i < self::COUNT; $i++) { if ($this->angleAt($i, $tt) > 1.5) { $this->fallen++; } $f = $i * self::DT; $up = self::RISE_AT + (self::COUNT - 1 - $i) * self::DT; [$x, $y] = $this->path[$i]; if ($f > $prev && $f <= $tt) { $this->play(Scale::freq(Scale::MAJOR_PENTA, (int)floor($i * 0.33), 55), 'marimba', 0.5, 0.26, 0.0); $this->fx->burst($x, $y, Gfx::hsv($i / self::COUNT), 2, 40); $this->pulse = max($this->pulse, 0.5); $this->tint = Gfx::hsv($i / self::COUNT); } if ($up > $prev && $up <= $tt) { $this->play(Scale::freq(Scale::MAJOR_PENTA, (int)floor((self::COUNT - $i) * 0.33), 67), 'sine', 0.45, 0.13, 0.0); } } $this->prev = $tt; } protected function drawWorld(Renderer $r): void { $tt = fmod($this->time, self::PERIOD); foreach ($this->path as $i => [$x, $y, $dir]) { $phi = $this->angleAt($i, $tt); $s = sin($phi); $k = $phi / (M_PI / 2); $rgb = Gfx::mix([236, 238, 255], Gfx::hsv($i / self::COUNT * 0.9 + 0.02, 0.62, 1.0), min(1.0, $k * 1.4)); $cx = $x + cos($dir) * 7 * $s; $cy = $y + sin($dir) * 7 * $s; $len = 4.2 + 12.5 * $s; // a soft shadow where it lies, then the tile with a darker edge if ($s > 0.05) { Gfx::box($r, $cx + 1.5, $cy + 2, $len + 1, 17, $dir, Gfx::mix(Gfx::BG_BOTTOM, [0, 0, 0], 0.5)); } Gfx::box($r, $cx, $cy, $len, 16, $dir, Gfx::mix($rgb, [20, 20, 50], 0.35 * (1 - $k))); Gfx::box($r, $cx, $cy, max(0.5, $len - 1.6), 14.4, $dir, $rgb); } }}