JA
games/satisfying-bounce / src / Scene / DominoScene.php · commit 8bdd6e4
<?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);        }    }}