*/ private array $steps = []; private int $k = 0; private float $phase = 0.0; /** the world is multiplied by this since the start (it is rebased): the true growth is GROW ** hops */ private int $hops = 0; private float $objSize = 0.0; private float $objX = 0.0; private float $objY = 0.0; private float $objAngle = 0.0; private float $squashT = 0.0; private float $screenSize = self::BASE; protected function melodyNotes(): string { return 'C4'; } protected function headingText(): string { return 'IT GROWS WITH EVERY STEP'; } protected function hookText(): string { return 'WATCH IT GROW'; } protected function captionText(): string { return $this->t('STEPS {n} SIZE x{z}', ['n' => $this->hops, 'z' => $this->growthLabel()]); } private function growthLabel(): string { $z = self::GROW ** $this->hops; return $z < 1000 ? (string)(int)round($z) : (string)(int)round($z / 1000) . 'K'; } protected function defaultCaptions(): array { $c = parent::defaultCaptions(); $c[] = ['text' => $this->t('BUT ITS SIZE ON SCREEN NEVER CHANGES'), 'from' => $this->seconds * 0.38, 'to' => $this->seconds * 0.38 + 3.4, 'at' => 396, 'scale' => 2, 'color' => '#9fe8ff', 'style' => 'pop']; return $c; } /** Size of the shape now on screen (always about BASE) */ public function screenSize(): float { return $this->screenSize; } public function stepCount(): int { return count($this->steps); } public function hops(): int { return $this->hops; } /** the largest coordinate of the world (it stays small: the world is rebased) */ public function extent(): float { $m = 0.0; foreach ($this->steps as $s) { $m = max($m, abs($s['x']), abs($s['y']), $s['w']); } return $m; } protected function build(): void { $this->steps = []; $this->k = 0; $this->hops = 0; $this->phase = 0.0; $this->squashT = 0.0; $this->world->reset($this->seed); $x = 0.0; $y = 0.0; $w = 40.0; for ($i = 0; $i < self::AHEAD; $i++) { $this->steps[$i] = ['x' => $x, 'y' => $y, 'w' => $w, 'h' => $w * 0.55]; $x += $w; $y += $w * 0.55; $w *= self::GROW; } $this->objSize = 40.0 * 0.46; $this->place(); } /** where the shape is, and the camera, for the current hop phase */ private function place(): void { $k = $this->k; $u = min(1.0, $this->phase / self::HOP); $e = ZoomView::ease($u); $a = $this->steps[$k]; $b = $this->steps[$k + 1]; $size0 = $a['w'] * 0.46; $size1 = $b['w'] * 0.46; $this->objSize = $size0 * ($size1 / $size0) ** $e; $ax = $a['x'] + $a['w'] / 2; $bx = $b['x'] + $b['w'] / 2; $ay = $a['y'] - $size0 / 2; $by = $b['y'] - $size1 / 2; $this->objX = $ax + ($bx - $ax) * $e; $arc = $a['h'] * 1.6 + $size0 * 0.6; $this->objY = $ay + ($by - $ay) * $e - $arc * sin(M_PI * $u); $this->objAngle = $u * 2 * M_PI; $this->view->scale = self::BASE / $this->objSize; $this->view->cx = $ax + ($bx - $ax) * $e; $this->view->cy = $ay + ($by - $ay) * $e + $this->objSize * 0.2; $this->view->sx = 96; $this->view->sy = 200; $this->screenSize = $this->view->len($this->objSize); } protected function tick(float $dt): void { $this->phase += $dt; $this->squashT = max(0.0, $this->squashT - $dt * 6); while ($this->phase >= self::HOP) { $this->phase -= self::HOP; $this->land(); } $this->place(); } private function land(): void { $this->k++; $this->hops++; $this->bounces++; $this->squashT = 1.0; $step = $this->steps[$this->k]; $rgb = Gfx::hsv($this->hops * 0.067, 0.55); $this->burst($this->view->x($step['x'] + $step['w'] / 2), $this->view->y($step['y']), $rgb, 8, 70); $this->play(Scale::freq(Scale::LYDIAN, 14 - $this->hops % 14, 60), 'marimba', 0.7, 0.3, 0.0); // new steps ahead $last = max(array_keys($this->steps)); while ($last < $this->k + self::AHEAD) { $p = $this->steps[$last]; $this->steps[++$last] = ['x' => $p['x'] + $p['w'], 'y' => $p['y'] + $p['h'], 'w' => $p['w'] * self::GROW, 'h' => $p['w'] * self::GROW * 0.55]; } foreach (array_keys($this->steps) as $i) { if ($i < $this->k - 9) { unset($this->steps[$i]); } } // the world is rebased: every length divided, so the numbers never grow $f = $this->steps[$this->k]['w'] > 400 ? 40.0 / $this->steps[$this->k]['w'] : 1.0; if ($f !== 1.0) { foreach ($this->steps as $i => $s) { $this->steps[$i] = ['x' => $s['x'] * $f, 'y' => $s['y'] * $f, 'w' => $s['w'] * $f, 'h' => $s['h'] * $f]; } } } protected function drawWorld(Renderer $r): void { $w = $r->getWidth(); $h = $r->getHeight(); foreach ($this->steps as $i => $s) { $x0 = $this->view->x($s['x']); $x1 = $this->view->x($s['x'] + $s['w']); $y0 = $this->view->y($s['y']); if ($x1 < -2 || $x0 > $w || $y0 > $h + 2) { continue; } $hue = ($i + $this->hopsBase()) * 0.067; $face = Gfx::hsv($hue, 0.5, 0.42); $top = Gfx::hsv($hue, 0.45, 0.95); $r->fillRect($x0, $y0, $x1 - $x0, $h - $y0 + 4, Gfx::color($face)); $r->fillRect($x0, $y0, $x1 - $x0, max(1.2, $this->view->len($s['h']) * 0.16), Gfx::color($top)); } $this->drawShape($r); } /** hues are tied to the absolute step number: the ones dropped behind and the rebase never change a colour */ private function hopsBase(): int { return $this->hops - $this->k; } private function drawShape(Renderer $r): void { $x = $this->view->x($this->objX); $y = $this->view->y($this->objY); $half = self::BASE / 2; $shape = self::SHAPES[$this->hops % count(self::SHAPES)]; $rgb = Gfx::hsv(($this->hops + $this->phase / self::HOP) * 0.067 + 0.5, 0.45, 1.0); Gfx::glow($r, $x, $y, $half, $rgb, 0.8); $sq = $this->squashT; $u = min(1.0, $this->phase / self::HOP); $ang = $u < 1.0 ? $this->objAngle : 0.0; switch ($shape) { case 'square': Gfx::box($r, $x, $y + $half * 0.25 * $sq, self::BASE * (1 + 0.18 * $sq), self::BASE * (1 - 0.2 * $sq), $ang, $rgb); break; case 'star': Gfx::star($r, $x, $y, $half * 1.25, $half * 0.55, $ang, $rgb); break; case 'ball': Gfx::blob($r, $x, $y + $half * 0.2 * $sq, $half * (1 + 0.2 * $sq), $half * (1 - 0.2 * $sq), 0.0, $rgb); break; case 'ring': Gfx::arc($r, $x, $y, $half * 0.8, 0, 2 * M_PI, self::BASE * 0.24, $rgb); break; default: Gfx::box($r, $x, $y, self::BASE * 0.5, self::BASE * 1.1, $ang * 0.5, $rgb); } Gfx::disc($r, $x - $half * 0.3, $y - $half * 0.35, $half * 0.2, [255, 255, 255], 170); } }