<?phpnamespace App\Scene;use App\Audio\Scale;use App\Render\Gfx;use JumPHP\Physics2d\Ball;use JumPHP\Physics2d\Wall;use JumPHP\Renderer\Renderer;/** * Three round cats bounce in a box and meow at every bounce: the meows are synthesized (a glide in pitch through moving vowel formants, no * recording), each cat has its own pitch from a pentatonic scale so they sing together, and a purr rumbles underneath. The cat that * hits opens its mouth and squeezes its eyes. */final class CatBounceScene extends VideoScene{ public const SECONDS = 24.0; public const CATS = 4; /** seconds between two cats showing up, and how long the pop-in takes */ public const SPAWN_EVERY = 0.9; private const POP = 0.45; protected float $punch = 0.015; private const BOX = [26.0, 150.0, 244.0, 410.0]; private const PITCH = [392.0, 523.25, 659.25, 783.99]; private float $nextPurr = 0.0; private int $meows = 0; private float $builtAt = 0.0; private int $spawned = 0; protected function melodyNotes(): string { return 'G4'; } protected function headingText(): string { return 'CATS BOUNCE. CATS MEOW.'; } protected function hookText(): string { return 'TURN THE SOUND ON'; } protected function captionText(): string { return $this->t('MEOWS {n}', ['n' => $this->meows]); } public function meows(): int { return $this->meows; } protected function build(): void { $this->world->reset($this->seed + $this->round * 7919); $this->world->gravity = 380; $this->world->ballsCollide = true; $this->world->maxSpeed = 700; $this->meows = 0; $this->nextPurr = 0.0; [$x0, $y0, $x1, $y1] = self::BOX; foreach ([[$x0, $y0, $x1, $y0], [$x1, $y0, $x1, $y1], [$x1, $y1, $x0, $y1], [$x0, $y1, $x0, $y0]] as [$a, $b, $c, $d]) { $this->world->addWall(new Wall($a, $b, $c, $d, '#3b3f6e')); } $this->builtAt = $this->time; $this->spawned = 0; $this->spawnCat(); } /** How many cats are in the box (they show up one by one). */ public function visibleCats(): int { return $this->spawned; } private function spawnCat(): void { $i = $this->spawned++; $rand = $this->world->random; $hues = [0.08, 0.58, 0.9, 0.3]; $radius = [20.0, 17.5, 15.0, 13.5]; $ball = new Ball(70 + ($i % 3) * 65 + ($i >= 3 ? 20 : 0), 200 + $rand->between(0, 40), $rand->between(-120, 120), $rand->between(-60, 0), $radius[$i], self::hue($hues[$i]), 40); $ball->data['cat'] = $i; $ball->data['born'] = $this->time; $this->world->addBall($ball); if ($i > 0) { // a soft hello (the first one is the film's very first frame) if ($this->play(self::PITCH[$i], 'meow', 0.55 + 0.1 * min($i, 2), 0.2, 0.1)) { $this->meows++; } } $this->burst($ball->x, $ball->y, $ball->color, 6, 50); } protected function tick(float $dt): void { while ($this->spawned < self::CATS && $this->time >= $this->builtAt + $this->spawned * self::SPAWN_EVERY) { $this->spawnCat(); } if ($this->time >= $this->nextPurr) { $this->nextPurr = $this->time + 4.6; $this->recorder->note($this->time, 52.0, 5.2, 'purr', 0.16, false); } foreach ($this->world->balls() as $ball) { $ball->data['open'] = max(0.0, (float)($ball->data['open'] ?? 0) - $dt * 3.2); } } protected function onHit(array $hit): void { /** @var Ball $ball */ $ball = $hit['ball']; if (($hit['type'] !== 'wall' && $hit['type'] !== 'ball') || $hit['speed'] < 70) { return; } $cat = (int)($ball->data['cat'] ?? 0); $ball->data['open'] = 1.0; $this->squash($ball, (float)$hit['x'], (float)$hit['y']); $this->burst((float)$hit['x'], (float)$hit['y'], $ball->color, 4, 60); $bump = $hit['type'] === 'ball'; $freq = self::PITCH[$cat] * [1.0, 1.122, 1.26, 0.89][(int)($this->world->random->between(0, 3.99))] * ($bump ? 1.25 : 1.0); if ($this->play($freq, 'meow', $bump ? 0.34 : 0.55 + 0.1 * $cat, $bump ? 0.2 : 0.3, 0.2)) { $this->meows++; } } protected function drawWorld(Renderer $r): void { [$x0, $y0, $x1, $y1] = self::BOX; Gfx::bar($r, $x0, $y0, $x1, $y0, 3, [70, 76, 130]); Gfx::bar($r, $x1, $y0, $x1, $y1, 3, [70, 76, 130]); Gfx::bar($r, $x1, $y1, $x0, $y1, 3, [70, 76, 130]); Gfx::bar($r, $x0, $y1, $x0, $y0, 3, [70, 76, 130]); foreach ($this->world->balls() as $ball) { $this->drawCat($r, $ball); } } private function drawCat(Renderer $r, Ball $ball): void { $fur = Gfx::rgb($ball->color); $x = $ball->x; $y = $ball->y; $age = ($this->time - (float)($ball->data['born'] ?? -9)) / self::POP; $pop = $age >= 1.0 ? 1.0 : max(0.0, 1 + 2.7 * ($age - 1) ** 3 + 1.7 * ($age - 1) ** 2); // ease out back: overshoots, settles $rad = $ball->radius * $pop; if ($rad < 0.5) { return; } $open = (float)($ball->data['open'] ?? 0); $sq = (float)($ball->data['sq'] ?? 0); $dark = Gfx::mix($fur, [30, 20, 50], 0.45); Gfx::glow($r, $x, $y, $rad, $fur, 0.7); $tilt = max(-0.4, min(0.4, $ball->vx / 700)); foreach ([-1, 1] as $s) { // ears $ax = $x + $s * $rad * 0.95; $ay = $y - $rad * 0.55; Gfx::poly($r, [[$ax - $s * $rad * 0.55 + $tilt * 3, $ay + $rad * 0.2], [$ax + $s * $rad * 0.1 + $tilt * 6, $ay - $rad * 0.85], [$ax + $s * $rad * 0.05, $ay + $rad * 0.45]], $fur); Gfx::poly($r, [[$ax - $s * $rad * 0.3 + $tilt * 3, $ay + $rad * 0.2], [$ax + $s * $rad * 0.02 + $tilt * 5, $ay - $rad * 0.5], [$ax, $ay + $rad * 0.35]], [255, 170, 190]); } Gfx::blob($r, $x, $y + $rad * 0.12 * $sq, $rad * (1 + 0.12 * $sq), $rad * (1 - 0.12 * $sq), 0.0, $fur); foreach ([-1, 1] as $s) { // eyes: round, or squeezed shut when it meows $ex = $x + $s * $rad * 0.42; $ey = $y - $rad * 0.12; if ($open > 0.35) { Gfx::bar($r, $ex - $rad * 0.2, $ey + $rad * 0.05, $ex, $ey - $rad * 0.12, max(0.8, $rad * 0.1), $dark); Gfx::bar($r, $ex, $ey - $rad * 0.12, $ex + $rad * 0.2, $ey + $rad * 0.05, max(0.8, $rad * 0.1), $dark); } else { Gfx::blob($r, $ex, $ey, $rad * 0.2, $rad * 0.26, 0.0, [250, 245, 200]); Gfx::blob($r, $ex, $ey, $rad * 0.07, $rad * 0.22, 0.0, [25, 20, 40]); } } Gfx::poly($r, [[$x - $rad * 0.1, $y + $rad * 0.12], [$x + $rad * 0.1, $y + $rad * 0.12], [$x, $y + $rad * 0.24]], [255, 140, 170]); if ($open > 0.1) { // the mouth Gfx::blob($r, $x, $y + $rad * 0.5, $rad * 0.2 * (0.6 + $open), $rad * 0.26 * $open, 0.0, [90, 25, 50]); } foreach ([-1, 1] as $s) { // whiskers for ($k = -1; $k <= 1; $k++) { Gfx::bar($r, $x + $s * $rad * 0.45, $y + $rad * (0.25 + $k * 0.08), $x + $s * $rad * 1.15, $y + $rad * (0.25 + $k * 0.3), 0.5, [245, 245, 255]); } } }}