<?phpnamespace App\Scene;use App\Audio\Scale;use App\Render\Gfx;use JumPHP\Renderer\Renderer;/** * An ASMR pond: slow drops fall into the water, each one makes soft rings that spread, cross and fade, and a round drop sound of a * minor pentatonic scale (the further to the right, the higher). A low pad breathes underneath. Nothing hurries: the calm is the point. */final class RainPondScene extends VideoScene{ public const SECONDS = 24.0; private const POND = [135.0, 290.0, 130.0, 118.0]; // centre x, y, radii /** @var list<array{x: float, y: float, age: float, hue: float, big: float}> */ private array $ripples = []; /** @var list<array{x: float, y: float, from: float, hit: float, hue: float}> */ private array $drops = []; private float $next = 0.5; private int $count = 0; protected function melodyNotes(): string { return 'A3'; } protected function headingText(): string { return 'RAIN ON A QUIET POND'; } protected function hookText(): string { return 'BREATHE IN. LISTEN.'; } protected function captionText(): string { return $this->t('DROPS {n}', ['n' => $this->count]); } public function dropCount(): int { return $this->count; } protected function build(): void { $this->world->reset($this->seed); $this->ripples = []; $this->drops = []; $this->next = 0.5; $this->count = 0; $this->pad([Scale::freq(Scale::MINOR_PENTA, 0, 45), Scale::freq(Scale::MINOR_PENTA, 3, 45)], 6.0, 0.06); } protected function tick(float $dt): void { $rand = $this->world->random; if ($this->time >= $this->next) { $this->next = $this->time + $rand->between(0.45, 1.05); [$cx, $cy, $rx, $ry] = self::POND; $x = $cx + $rand->between(-0.62, 0.62) * $rx; $y = $cy + $rand->between(-0.55, 0.55) * $ry; $fall = sqrt(2 * ($y - 120) / 1100); $this->drops[] = ['x' => $x, 'y' => $y, 'from' => $this->time, 'hit' => $this->time + $fall, 'hue' => 0.5 + 0.2 * ($x - $cx) / $rx]; } foreach ($this->drops as $i => $d) { if ($this->time >= $d['hit']) { unset($this->drops[$i]); $this->count++; $this->ripples[] = ['x' => $d['x'], 'y' => $d['y'], 'age' => 0.0, 'hue' => $d['hue'], 'big' => 0.8 + $rand->between(0, 0.5)]; $deg = 3 + (int)floor(($d['x'] - (self::POND[0] - self::POND[2])) / (2 * self::POND[2]) * 8); $this->play(Scale::freq(Scale::MINOR_PENTA, $deg, 57), 'drop', 0.9, 0.22 + $rand->between(0, 0.06), 0.1); $this->fx->burst($d['x'], $d['y'], Gfx::hsv($d['hue'], 0.4), 4, 28); $this->pulse = max($this->pulse, 0.25); $this->tint = Gfx::hsv($d['hue'], 0.5); } } $this->drops = array_values($this->drops); foreach ($this->ripples as $i => &$rp) { $rp['age'] += $dt; } unset($rp); $this->ripples = array_values(array_filter($this->ripples, fn (array $rp) => $rp['age'] < 3.2)); if (count($this->ripples) > 12) { array_shift($this->ripples); } } protected function drawWorld(Renderer $r): void { [$cx, $cy, $rx, $ry] = self::POND; Gfx::blob($r, $cx, $cy, $rx + 6, $ry + 5, 0.0, [34, 40, 86]); Gfx::blob($r, $cx, $cy, $rx, $ry, 0.0, [12, 28, 60]); Gfx::blob($r, $cx, $cy + 6, $rx * 0.8, $ry * 0.7, 0.0, [15, 36, 72]); foreach ($this->ripples as $rp) { foreach ([0.0, 0.32, 0.64] as $n => $delay) { $a = $rp['age'] - $delay; if ($a <= 0) { continue; } $rad = 6 + 44 * $a * $rp['big'] * (1 - 0.16 * $a); $k = max(0.0, 1 - $a / 2.6); $rgb = Gfx::mix([12, 28, 60], Gfx::hsv($rp['hue'], 0.45, 1.0), $k * (1 - $n * 0.25)); Gfx::arc($r, $rp['x'], $rp['y'], $rad, 0, 2 * M_PI, 2.2 - $n * 0.4, $rgb, 0.6); } } foreach ($this->drops as $d) { // a drop on its way down $u = ($this->time - $d['from']) / max(0.01, $d['hit'] - $d['from']); $y = 120 + ($d['y'] - 120) * $u * $u; Gfx::blob($r, $d['x'], $y, 2.3, 4.2, M_PI / 2, Gfx::hsv($d['hue'], 0.3, 1.0)); } }}