+
-
- {/* Required for the spin animation if not in tailwind config */}
-
);
}
diff --git a/sites/hacklytics2027/components/pixel/PixelGarden.tsx b/sites/hacklytics2027/components/pixel/PixelGarden.tsx
index 604f00be..2740191c 100644
--- a/sites/hacklytics2027/components/pixel/PixelGarden.tsx
+++ b/sites/hacklytics2027/components/pixel/PixelGarden.tsx
@@ -1,6 +1,6 @@
"use client";
-import React, { useEffect, useMemo, useRef } from "react";
+import React, { useMemo } from "react";
import { PixelImage } from "./PixelSprite";
import { spriteToDataUri } from "./spriteUri";
import { usePauseOffscreen } from "./useInView";
@@ -12,11 +12,9 @@ import {
SPORE,
SPROUT,
TULIP,
- VINE
-
-
+ VINE,
} from "./sprites";
-import type {PaletteName, SpriteMap} from "./sprites";
+import type { PaletteName, SpriteMap } from "./sprites";
/* Deterministic PRNG — same layout on server and client, no hydration mismatch. */
function mulberry32(seed: number) {
@@ -30,26 +28,29 @@ function mulberry32(seed: number) {
}
const FLORA: SpriteMap[] = [DAISY, TULIP, BLOOM, SPROUT, MUSHROOM];
-const TINTS: PaletteName[] = ["pink", "cyan", "lime", "purple"];
+/* Magenta + lime live in the sprites. Cyan shows up as water/petal tints, not UI chrome. */
+const TINTS: PaletteName[] = ["pink", "lime", "pink", "cyan", "lime", "pink"];
type Planted = {
map: SpriteMap;
palette: PaletteName;
left: number;
+ top?: number;
scale: number;
delay: number;
duration: number;
flip: boolean;
+ hideOnMobile?: boolean;
};
-function plant(seed: number, count: number, scaleRange: [number, number]): Planted[] {
+function plantBed(seed: number, count: number, scaleRange: [number, number]): Planted[] {
const rnd = mulberry32(seed);
return Array.from({ length: count }, () => {
const t = rnd();
return {
map: FLORA[Math.floor(rnd() * FLORA.length)],
palette: TINTS[Math.floor(rnd() * TINTS.length)],
- left: rnd() * 100,
+ left: rnd() * 96,
scale: Math.round(scaleRange[0] + t * (scaleRange[1] - scaleRange[0])),
delay: -rnd() * 6,
duration: 4 + rnd() * 5,
@@ -58,6 +59,177 @@ function plant(seed: number, count: number, scaleRange: [number, number]): Plant
});
}
+function plantField(
+ seed: number,
+ count: number,
+ leftRange: [number, number],
+ topRange: [number, number],
+ scaleRange: [number, number],
+): Planted[] {
+ const rnd = mulberry32(seed);
+ return Array.from({ length: count }, () => {
+ const t = rnd();
+ const scale = Math.round(scaleRange[0] + t * (scaleRange[1] - scaleRange[0]));
+ return {
+ map: FLORA[Math.floor(rnd() * FLORA.length)],
+ palette: TINTS[Math.floor(rnd() * TINTS.length)],
+ left: leftRange[0] + rnd() * (leftRange[1] - leftRange[0]),
+ top: topRange[0] + rnd() * (topRange[1] - topRange[0]),
+ scale,
+ delay: -rnd() * 8,
+ duration: 4 + rnd() * 6,
+ flip: rnd() > 0.5,
+ hideOnMobile: scale >= 8,
+ };
+ });
+}
+
+/** Tileable sine-wave vine, painted with petal + leaf pixels so pink palette reads magenta. */
+function sineVineMap(
+ period: number,
+ amplitude: number,
+ thickness: number,
+ leafEvery = 8,
+): SpriteMap {
+ const h = amplitude * 2 + thickness + 5;
+ const rows: string[][] = Array.from({ length: h }, () =>
+ Array.from({ length: period }, () => "."),
+ );
+ const mid = amplitude + 2;
+ for (let x = 0; x < period; x++) {
+ const y = Math.round(mid + Math.sin((x / period) * Math.PI * 2) * amplitude);
+ for (let t = 0; t < thickness; t++) {
+ const yy = y + t;
+ if (yy >= 0 && yy < h) rows[yy][x] = t === 0 ? "P" : "p";
+ }
+ if (leafEvery && x % leafEvery === 3) {
+ const dir = x % (leafEvery * 2) === 3 ? -1 : 1;
+ const ly = y + (dir === -1 ? -1 : thickness);
+ if (ly >= 0 && ly < h) rows[ly][x] = "G";
+ }
+ }
+ return rows.map((r) => r.join(""));
+}
+
+function sineVineMapVertical(period: number, amplitude: number, thickness: number): SpriteMap {
+ const w = amplitude * 2 + thickness + 5;
+ const rows: string[][] = Array.from({ length: period }, () =>
+ Array.from({ length: w }, () => "."),
+ );
+ const mid = amplitude + 2;
+ for (let y = 0; y < period; y++) {
+ const x = Math.round(mid + Math.sin((y / period) * Math.PI * 2) * amplitude);
+ for (let t = 0; t < thickness; t++) {
+ const xx = x + t;
+ if (xx >= 0 && xx < w) rows[y][xx] = t === 0 ? "P" : "p";
+ }
+ }
+ return rows.map((r) => r.join(""));
+}
+
+function PixelSineVine({
+ top,
+ palette = "pink",
+ period = 56,
+ amplitude = 5,
+ thickness = 2,
+ scale = 4,
+ opacity = 0.92,
+}: {
+ top: string;
+ palette?: PaletteName;
+ period?: number;
+ amplitude?: number;
+ thickness?: number;
+ scale?: number;
+ opacity?: number;
+}) {
+ const sprite = useMemo(
+ () => spriteToDataUri(sineVineMap(period, amplitude, thickness), palette),
+ [period, amplitude, thickness, palette],
+ );
+
+ return (
+
+ );
+}
+
+function PixelSineVineVertical({
+ side = "right",
+ palette = "pink",
+ period = 40,
+ amplitude = 4,
+ thickness = 2,
+ scale = 4,
+ opacity = 0.85,
+}: {
+ side?: "left" | "right";
+ palette?: PaletteName;
+ period?: number;
+ amplitude?: number;
+ thickness?: number;
+ scale?: number;
+ opacity?: number;
+}) {
+ const sprite = useMemo(
+ () => spriteToDataUri(sineVineMapVertical(period, amplitude, thickness), palette),
+ [period, amplitude, thickness, palette],
+ );
+
+ return (
+
+ );
+}
+
+function FloraLayer({ plants, className = "" }: { plants: Planted[]; className?: string }) {
+ return (
+
+ {plants.map((f, i) => (
+
+ ))}
+
+ );
+}
+
/* ─── Ground: tiled grass + soil, with flora growing out of it ──────────── */
export function PixelGround({
@@ -72,7 +244,7 @@ export function PixelGround({
className?: string;
}) {
const ground = useMemo(() => spriteToDataUri(GROUND, "lime"), []);
- const flora = useMemo(() => plant(seed, count, [3, 5]), [seed, count]);
+ const flora = useMemo(() => plantBed(seed, count, [3, 6]), [seed, count]);
const ref = usePauseOffscreen
();
return (
@@ -82,8 +254,7 @@ export function PixelGround({
className={`relative w-full select-none ${className}`}
aria-hidden="true"
>
- {/* flora stands on top of the soil line */}
-
+
{flora.map((f, i) => (
- {/* flip lives on the sprite: the wrapper's transform is the sway animation */}
@@ -152,9 +321,9 @@ export function PixelVine({
);
}
-/* ─── Drifting spores ──────────────────────────────────────────────────── */
+/* ─── Drifting spores / yellow embers ──────────────────────────────────── */
-function Spores({ count = 10, seed = 3 }: { count?: number; seed?: number }) {
+function Spores({ count = 18, seed = 3 }: { count?: number; seed?: number }) {
const spores = useMemo(() => {
const rnd = mulberry32(seed);
return Array.from({ length: count }, () => ({
@@ -180,84 +349,60 @@ function Spores({ count = 10, seed = 3 }: { count?: number; seed?: number }) {
animationDuration: `${s.duration}s`,
}}
>
-
+
))}
>
);
}
-/* ─── Fixed background garden with two parallax layers ──────────────────── */
+/* ─── Full-fold Digital Bloom: vines + dense flora, scoped to the hero ─── */
export default function PixelGarden() {
- const backRef = useRef
(null);
- const midRef = useRef(null);
-
- const back = useMemo(() => plant(11, 6, [2, 3]), []);
- const mid = useMemo(() => plant(23, 5, [4, 6]), []);
-
- useEffect(() => {
- if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
-
- let frame = 0;
- const onScroll = () => {
- if (frame) return;
- frame = requestAnimationFrame(() => {
- const y = window.scrollY;
- if (backRef.current) backRef.current.style.transform = `translate3d(0, ${y * -0.04}px, 0)`;
- if (midRef.current) midRef.current.style.transform = `translate3d(0, ${y * -0.09}px, 0)`;
- frame = 0;
- });
- };
-
- window.addEventListener("scroll", onScroll, { passive: true });
- return () => {
- window.removeEventListener("scroll", onScroll);
- if (frame) cancelAnimationFrame(frame);
- };
- }, []);
+ const far = useMemo(() => plantField(11, 32, [1, 97], [2, 80], [2, 3]), []);
+ const midLeft = useMemo(() => plantField(89, 14, [2, 44], [18, 68], [3, 5]), []);
+ const mid = useMemo(() => plantField(23, 26, [36, 96], [6, 74], [3, 5]), []);
+ const near = useMemo(() => plantField(47, 16, [48, 94], [16, 70], [5, 8]), []);
+ const vineBloom = useMemo(() => plantField(71, 16, [4, 96], [12, 64], [3, 5]), []);
+ const ref = usePauseOffscreen();
return (
-
- {/* far layer — small, dim */}
-
- {back.map((f, i) => (
-
- ))}
-
+
+
+
+
+
- {/* near layer — bigger, glowing */}
-
- {mid.map((f, i) => (
-
- ))}
-
+
+
+
+
+
+
+
);
}
diff --git a/sites/hacklytics2027/components/pixel/sprites.ts b/sites/hacklytics2027/components/pixel/sprites.ts
index 84dd38b9..5bafaf7f 100644
--- a/sites/hacklytics2027/components/pixel/sprites.ts
+++ b/sites/hacklytics2027/components/pixel/sprites.ts
@@ -104,6 +104,21 @@ export const MUSHROOM: SpriteMap = [
"..ddddd..",
];
+/** Tiny pixel globe — DS@GT mark, not hero art. Cyan water + green land. */
+export const GLOBE: SpriteMap = [
+ "....ppp....",
+ "..ppGGGpp..",
+ ".pGGppGGpp.",
+ ".pGpppGGGp.",
+ "pGppppGGGGp",
+ "pGppGGppGGp",
+ "pGGpppppGGp",
+ ".pGGppGGpp.",
+ ".ppGGGGppp.",
+ "..ppGGGpp..",
+ "....ppp....",
+];
+
/** Vertically tileable vine segment. */
export const VINE: SpriteMap = [
"...g...",