Spaces:
Running
Running
Case Zero - initial public release (fully local: Qwen2.5-1.5B via llama.cpp + Supertonic, custom pixel-noir SPA via gradio.Server)
414dc55 | // Pixel engine — crisp nearest-neighbor canvas rendering. Ported from prototype/js/pixel.jsx. | |
| import { useEffect, useRef, useState } from 'preact/hooks' | |
| import type { JSX } from 'preact' | |
| import { type Pal, drawMap } from './draw' | |
| export type ScenePainter = ( | |
| ctx: CanvasRenderingContext2D, | |
| w: number, | |
| h: number, | |
| t: number, | |
| ) => void | |
| interface PixelCanvasProps { | |
| frames?: string[][] | |
| map?: string[] | |
| pal?: Pal | |
| px?: number | |
| fps?: number | |
| className?: string | |
| style?: JSX.CSSProperties | |
| playing?: boolean | |
| onClick?: (e: MouseEvent) => void | |
| } | |
| /** A sprite, optionally animated across frames. */ | |
| export function PixelCanvas({ | |
| frames, | |
| map, | |
| pal, | |
| px = 4, | |
| fps = 4, | |
| className, | |
| style, | |
| playing = true, | |
| onClick, | |
| }: PixelCanvasProps) { | |
| const ref = useRef<HTMLCanvasElement>(null) | |
| const allFrames = frames || [map as string[]] | |
| const cols = allFrames[0][0].length | |
| const rows = allFrames[0].length | |
| const [f, setF] = useState(0) | |
| useEffect(() => { | |
| if (!playing || allFrames.length < 2) return | |
| const id = setInterval(() => setF((v) => (v + 1) % allFrames.length), 1000 / fps) | |
| return () => clearInterval(id) | |
| }, [playing, allFrames.length, fps]) | |
| useEffect(() => { | |
| const cv = ref.current | |
| if (!cv) return | |
| const ctx = cv.getContext('2d')! | |
| ctx.imageSmoothingEnabled = false | |
| ctx.clearRect(0, 0, cv.width, cv.height) | |
| drawMap(ctx, allFrames[f], pal, px) | |
| }, [f, px, pal, allFrames]) | |
| return ( | |
| <canvas | |
| ref={ref} | |
| width={cols * px} | |
| height={rows * px} | |
| onClick={onClick} | |
| class={className} | |
| style={{ imageRendering: 'pixelated', display: 'block', ...style }} | |
| /> | |
| ) | |
| } | |
| interface SceneCanvasProps { | |
| paint: ScenePainter | |
| w?: number | |
| h?: number | |
| className?: string | |
| style?: JSX.CSSProperties | |
| deps?: unknown[] | |
| anim?: boolean | |
| full?: boolean | |
| rain?: boolean | |
| } | |
| /** Procedural painter at low internal res. Static scenes paint once to an offscreen | |
| * buffer; anim scenes blit the buffer + a cheap rain overlay so we never re-dither | |
| * the whole canvas every frame. `full` forces a true per-frame repaint. */ | |
| export function SceneCanvas({ | |
| paint, | |
| w = 240, | |
| h = 135, | |
| className, | |
| style, | |
| deps = [], | |
| anim = false, | |
| full = false, | |
| rain = true, | |
| }: SceneCanvasProps) { | |
| const ref = useRef<HTMLCanvasElement>(null) | |
| const bufRef = useRef<HTMLCanvasElement | null>(null) | |
| const tRef = useRef(0) | |
| useEffect(() => { | |
| const cv = ref.current | |
| if (!cv) return | |
| const ctx = cv.getContext('2d')! | |
| ctx.imageSmoothingEnabled = false | |
| const buf = document.createElement('canvas') | |
| buf.width = w | |
| buf.height = h | |
| const bctx = buf.getContext('2d')! | |
| bctx.imageSmoothingEnabled = false | |
| paint(bctx, w, h, 0) | |
| bufRef.current = buf | |
| let raf = 0 | |
| // always paint a first frame synchronously (rAF is paused in background iframes) | |
| ctx.clearRect(0, 0, w, h) | |
| ctx.drawImage(buf, 0, 0) | |
| if (full) { | |
| let last = 0 | |
| const loop = (ts: number) => { | |
| if (ts - last > 110) { | |
| last = ts | |
| tRef.current += 1 | |
| ctx.clearRect(0, 0, w, h) | |
| paint(ctx, w, h, tRef.current) | |
| } | |
| raf = requestAnimationFrame(loop) | |
| } | |
| raf = requestAnimationFrame(loop) | |
| } else if (anim && rain) { | |
| let last = 0 | |
| const loop = (ts: number) => { | |
| if (ts - last > 70) { | |
| last = ts | |
| tRef.current += 1 | |
| ctx.clearRect(0, 0, w, h) | |
| ctx.drawImage(buf, 0, 0) | |
| ctx.fillStyle = 'rgba(176,196,206,0.26)' | |
| const t = tRef.current | |
| for (let i = 0; i < 36; i++) { | |
| const x = (i * 41 + t * 5) % w | |
| const y = (i * 57 + t * 9) % h | |
| ctx.fillRect(Math.floor(x), Math.floor(y), 1, 3) | |
| } | |
| } | |
| raf = requestAnimationFrame(loop) | |
| } | |
| raf = requestAnimationFrame(loop) | |
| } else { | |
| ctx.clearRect(0, 0, w, h) | |
| ctx.drawImage(buf, 0, 0) | |
| } | |
| return () => cancelAnimationFrame(raf) | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, deps) | |
| return ( | |
| <canvas | |
| ref={ref} | |
| width={w} | |
| height={h} | |
| class={className} | |
| style={{ imageRendering: 'pixelated', display: 'block', width: '100%', height: '100%', ...style }} | |
| /> | |
| ) | |
| } | |
| /** Full-screen pixel rain on a canvas. */ | |
| export function RainFX({ density = 90 }: { density?: number }) { | |
| const ref = useRef<HTMLCanvasElement>(null) | |
| useEffect(() => { | |
| const cv = ref.current | |
| if (!cv) return | |
| const ctx = cv.getContext('2d')! | |
| let W = 0 | |
| let H = 0 | |
| let drops: { x: number; y: number; v: number; len: number }[] = [] | |
| let raf = 0 | |
| const resize = () => { | |
| W = cv.width = Math.ceil(window.innerWidth / 3) | |
| H = cv.height = Math.ceil(window.innerHeight / 3) | |
| cv.style.width = window.innerWidth + 'px' | |
| cv.style.height = window.innerHeight + 'px' | |
| drops = Array.from({ length: density }, () => ({ | |
| x: Math.random() * W, | |
| y: Math.random() * H, | |
| v: 2 + Math.random() * 3, | |
| len: 3 + Math.random() * 5, | |
| })) | |
| } | |
| resize() | |
| window.addEventListener('resize', resize) | |
| let last = 0 | |
| const loop = (ts: number) => { | |
| if (ts - last > 33) { | |
| last = ts | |
| ctx.clearRect(0, 0, W, H) | |
| ctx.fillStyle = 'rgba(180,200,210,0.30)' | |
| for (const d of drops) { | |
| ctx.fillRect(Math.floor(d.x), Math.floor(d.y), 1, Math.floor(d.len)) | |
| d.y += d.v | |
| d.x += 0.4 | |
| if (d.y > H) { | |
| d.y = -d.len | |
| d.x = Math.random() * W | |
| } | |
| } | |
| } | |
| raf = requestAnimationFrame(loop) | |
| } | |
| raf = requestAnimationFrame(loop) | |
| return () => { | |
| cancelAnimationFrame(raf) | |
| window.removeEventListener('resize', resize) | |
| } | |
| }, [density]) | |
| return <canvas ref={ref} class="fx-rain" style={{ position: 'fixed', inset: 0, imageRendering: 'pixelated' }} /> | |
| } | |
| /** True if the user asked the OS to reduce motion. */ | |
| export function prefersReducedMotion(): boolean { | |
| return typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches | |
| } | |
| /** Typewriter hook: returns [visibleText, done]. Honors prefers-reduced-motion (instant). */ | |
| export function useTypewriter(text: string, speed = 28, on = true): [string, boolean] { | |
| if (prefersReducedMotion()) on = false | |
| const [out, setOut] = useState(on ? '' : text) | |
| const [done, setDone] = useState(!on) | |
| useEffect(() => { | |
| if (!on) { | |
| setOut(text) | |
| setDone(true) | |
| return | |
| } | |
| setOut('') | |
| setDone(false) | |
| if (!text) { | |
| setDone(true) | |
| return | |
| } | |
| let i = 0 | |
| const id = setInterval(() => { | |
| i++ | |
| setOut(text.slice(0, i)) | |
| if (i >= text.length) { | |
| clearInterval(id) | |
| setDone(true) | |
| } | |
| }, speed) | |
| return () => clearInterval(id) | |
| }, [text, speed, on]) | |
| return [out, done] | |
| } | |