MiniMax M3.1 Flash: Four HTML Coding Benchmarks on Free OpenCode

Four original HTML files, 16 passing task-board cases, and a game-breaking NaN physics defect verified in Chromium.
Co-Founder & Lead Programmer of AcceleratedLogic AI
MiniMax M3.1 Flash: release, pricing and capabilities
Publisher benchmarks versus our measurements
How we ran the four prompts
opencode/minimax-m3.1-flash-free from the installed OpenCode catalog and used OpenCode 2.0.32 with opencode run --model opencode/minimax-m3.1-flash-free --format json. Each task used a fresh default build-agent session. No output cap, sampling value or reasoning variant was manually set. The first successful crane response became the crane submission; the remaining three tasks each supplied one response. We did not ask for repairs or substitute another model.Results at a glance
| Task | Recorded step time | Delivery | Browser finding |
|---|---|---|---|
| Glass sphere and crane | 62.9 s | Complete HTML | SVG renders; local references resolve |
| Procedural globe | 277.9 s | Complete HTML | WebGL2, slider, drag, zoom/reset and CDN-error checks pass |
| Wizard game | 412.0 s | Complete HTML | Black scene; player height and vertical velocity are NaN |
| Task board | 32.6 s | Complete HTML | 16/16 selected functional browser checks pass |
Test 1: Glass sphere and origami crane
Actual MiniMax M3.1 Flash glass-crane HTML rendered in Chromium
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Origami Crane in Glass — SVG Illustration</title>
<style>
:root{
--bg-0:#0b0d14;
--bg-1:#141a28;
--bg-2:#1d2536;
--glass-1:#eaf4ff;
--glass-2:#9fc7e8;
--accent:#f6d28a;
--accent-2:#e79a5b;
--ink:#0d1220;
}
*{box-sizing:border-box;}
html,body{height:100%;}
body{
margin:0;
min-height:100%;
background:
radial-gradient(120% 90% at 18% 8%, #232c40 0%, rgba(35,44,64,0) 55%),
radial-gradient(110% 80% at 82% 92%, #1b2233 0%, rgba(27,34,51,0) 60%),
linear-gradient(165deg, var(--bg-0) 0%, var(--bg-1) 45%, var(--bg-2) 100%);
color:#dfe7f3;
font-family:"Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif;
display:flex;
align-items:center;
justify-content:center;
padding:clamp(12px,3.5vw,48px);
overflow-x:hidden;
}
.stage{
position:relative;
width:min(96vw, 860px);
margin-inline:auto;
display:flex;
flex-direction:column;
gap:clamp(10px,1.8vw,22px);
align-items:center;
}
.frame{
position:relative;
width:100%;
aspect-ratio:1 / 1;
max-height:82vh;
border-radius:clamp(14px,2.4vw,28px);
overflow:hidden;
background:
radial-gradient(80% 70% at 30% 22%, #2a3purple 0%, rgba(0,0,0,0) 0%),
linear-gradient(180deg,#10141f,#090c13);
box-shadow:
0 30px 70px -20px rgba(0,0,0,.75),
0 2px 0 rgba(255,255,255,.06) inset,
0 0 0 1px rgba(255,255,255,.07);
isolation:isolate;
}
.frame::after{
content:"";
position:absolute;inset:0;
background:
repeating-linear-gradient(0deg, rgba(255,255,255,.022) 0 1px, rgba(0,0,0,0) 1px 3px),
radial-gradient(70% 60% at 50% 45%, rgba(120,170,230,.10) 0%, rgba(0,0,0,0) 70%);
mix-blend-mode:screen;
pointer-events:none;
}
svg.scene{
display:block;
width:100%;
height:100%;
}
.caption{
width:100%;
display:flex;
flex-wrap:wrap;
align-items:baseline;
justify-content:space-between;
gap:10px 18px;
padding-inline:6px;
}
.caption h1{
margin:0;
font-size:clamp(15px,2.4vw,22px);
font-weight:600;
letter-spacing:.14em;
text-transform:uppercase;
color:#eef4ff;
}
.caption h1 span{color:var(--accent);}
.caption p{
margin:0;
font-size:clamp(11px,1.5vw,13.5px);
letter-spacing:.06em;
color:#93a3bd;
max-width:52ch;
}
.badge{
font-size:10.5px;
letter-spacing:.18em;
text-transform:uppercase;
color:#8fa3c2;
border:1px solid rgba(255,255,255,.14);
border-radius:999px;
padding:5px 12px;
background:rgba(255,255,255,.035);
white-space:nowrap;
}
@media (max-width:560px){
.caption{justify-content:center;text-align:center;}
.frame{max-height:none;}
}
@media (prefers-reduced-motion: reduce){
*{animation:none !important; transition:none !important;}
.sway, .float, .shimmer{transform:none !important;}
}
@keyframes floaty{
0%,100%{transform:translateY(0);}
50%{transform:translateY(-10px);}
}
@keyframes sway{
0%,100%{transform:rotate(-2.2deg);}
50%{transform:rotate(2.2deg);}
}
@keyframes shimmer{
0%{transform:translateX(-30%) skewX(-16deg);}
100%{transform:translateX(130%) skewX(-16deg);}
}
.float{animation:floaty 7s ease-in-out infinite;}
.sway{animation:sway 9s ease-in-out infinite; transform-origin:50% 78%;}
.shimmer{animation:shimmer 6.5s ease-in-out infinite;}
.shimmer2{animation:shimmer 8.5s ease-in-out infinite reverse;}
@media (prefers-reduced-motion: reduce){
.float,.sway,.shimmer,.shimmer2{animation:none;}
}
</style>
</head>
<body>
<main class="stage">
<div class="frame">
<svg class="scene" viewBox="0 0 800 800" xmlns="http://www.w3.org/2000/svg"
role="img" aria-labelledby="svgTitle svgDesc" preserveAspectRatio="xMidYMid meet">
<title id="svgTitle">A photorealistic glass sphere containing a folded origami crane</title>
<desc id="svgDesc">A hand-folded white origami crane with sharply creased paper facets, warm ochre highlights and visible fold lines, suspended inside a transparent glass sphere. The sphere shows layered reflections, a bright specular highlight, refracted light streaks, a soft contact shadow and a caustic glow on the dark studio floor.</desc>
<defs>
<!-- ======== STUDIO BACKDROP ======== -->
<radialGradient id="bgGlow" cx="42%" cy="26%" r="78%">
<stop offset="0%" stop-color="#33405c"/>
<stop offset="42%" stop-color="#1c2436"/>
<stop offset="100%" stop-color="#080b12"/>
</radialGradient>
<linearGradient id="floorGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0c111c"/>
<stop offset="100%" stop-color="#05070c"/>
</linearGradient>
<radialGradient id="poolLight" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#7fa6d8" stop-opacity=".30"/>
<stop offset="60%" stop-color="#4d6c99" stop-opacity=".10"/>
<stop offset="100%" stop-color="#000" stop-opacity="0"/>
</radialGradient>
<!-- ======== GLASS ======== -->
<radialGradient id="glassFill" cx="35%" cy="24%" r="78%">
<stop offset="0%" stop-color="#ffffff" stop-opacity=".40"/>
<stop offset="28%" stop-color="#cfe6ff" stop-opacity=".16"/>
<stop offset="62%" stop-color="#6d9bc4" stop-opacity=".09"/>
<stop offset="86%" stop-color="#26405c" stop-opacity=".16"/>
<stop offset="100%" stop-color="#0d1c2c" stop-opacity=".34"/>
</radialGradient>
<radialGradient id="glassEdge" cx="50%" cy="50%" r="50%">
<stop offset="72%" stop-color="#ffffff" stop-opacity="0"/>
<stop offset="90%" stop-color="#bcdcf8" stop-opacity=".30"/>
<stop offset="97%" stop-color="#eaf7ff" stop-opacity=".62"/>
<stop offset="100%" stop-color="#ffffff" stop-opacity=".18"/>
</radialGradient>
<linearGradient id="rimLight" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#ffffff" stop-opacity=".85"/>
<stop offset="35%" stop-color="#a9d4f5" stop-opacity=".22"/>
<stop offset="70%" stop-color="#7fb0dc" stop-opacity=".05"/>
<stop offset="100%" stop-color="#ffffff" stop-opacity=".55"/>
</linearGradient>
<radialGradient id="specBlob" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#ffffff" stop-opacity=".96"/>
<stop offset="45%" stop-color="#eaf6ff" stop-opacity=".42"/>
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
</radialGradient>
<linearGradient id="sheenGrad" x1="0" y1="0" x2="1" y2="0.6">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0"/>
<stop offset="40%" stop-color="#ffffff" stop-opacity=".22"/>
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="caustic" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#bfe2ff" stop-opacity="0"/>
<stop offset="30%" stop-color="#d9efff" stop-opacity=".55"/>
<stop offset="55%" stop-color="#8fc4ef" stop-opacity=".30"/>
<stop offset="80%" stop-color="#ffe6b8" stop-opacity=".22"/>
<stop offset="100%" stop-color="#bfe2ff" stop-opacity="0"/>
</linearGradient>
<radialGradient id="causticGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#ffe7bb" stop-opacity=".42"/>
<stop offset="45%" stop-color="#a8d5ff" stop-opacity=".22"/>
<stop offset="100%" stop-color="#000" stop-opacity="0"/>
</radialGradient>
<!-- ======== PAPER ======== -->
<linearGradient id="paperTop" x1="0.1" y1="0" x2="0.9" y2="1">
<stop offset="0%" stop-color="#ffffff"/>
<stop offset="45%" stop-color="#f3f0ea"/>
<stop offset="100%" stop-color="#d9d5cc"/>
</linearGradient>
<linearGradient id="paperMid" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#f6f3ec"/>
<stop offset="55%" stop-color="#e6e1d6"/>
<stop offset="100%" stop-color="#c9c3b6"/>
</linearGradient>
<linearGradient id="paperShade" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#d8d2c5"/>
<stop offset="60%" stop-color="#bdb6a7"/>
<stop offset="100%" stop-color="#9d968a"/>
</linearGradient>
<linearGradient id="paperDeep" x1="0" y1="0" x2="0.7" y2="1">
<stop offset="0%" stop-color="#b0a898"/>
<stop offset="100%" stop-color="#7e786d"/>
</linearGradient>
<linearGradient id="paperWarm" x1="0.1" y1="0" x2="0.9" y2="1">
<stop offset="0%" stop-color="#fffaf0"/>
<stop offset="40%" stop-color="#f6e6cd"/>
<stop offset="100%" stop-color="#e0c9a4"/>
</linearGradient>
<linearGradient id="paperGold" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#ffeccb"/>
<stop offset="55%" stop-color="#f2d09b"/>
<stop offset="100%" stop-color="#cf9f63"/>
</linearGradient>
<linearGradient id="wingUpper" x1="0" y1="0" x2="0.8" y2="1">
<stop offset="0%" stop-color="#ffffff"/>
<stop offset="48%" stop-color="#efe9dd"/>
<stop offset="100%" stop-color="#cdc6b6"/>
</linearGradient>
<linearGradient id="wingUnder" x1="0" y1="0" x2="1" y2="0.8">
<stop offset="0%" stop-color="#cfc8ba"/>
<stop offset="60%" stop-color="#a49d8f"/>
<stop offset="100%" stop-color="#7d766a"/>
</linearGradient>
<linearGradient id="beakGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#fbe3c1"/>
<stop offset="100%" stop-color="#c98f52"/>
</linearGradient>
<!-- ======== FILTERS ======== -->
<filter id="softShadow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="10"/>
</filter>
<filter id="tinyBlur" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3"/>
</filter>
<filter id="paperShadow" x="-40%" y="-40%" width="180%" height="180%">
<feDropShadow dx="6" dy="10" stdDeviation="9" flood-color="#0a0f1a" flood-opacity=".45"/>
</filter>
<filter id="innerGlow" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur stdDeviation="6" result="b"/>
<feComposite in="b" in2="SourceAlpha" operator="out" result="o"/>
<feFlood flood-color="#dff0ff" flood-opacity=".55" result="f"/>
<feComposite in="f" in2="o" operator="in" result="i"/>
<feMerge><feMergeNode in="SourceGraphic"/><feMergeNode in="i"/></feMerge>
</filter>
<filter id="grain">
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="3" seed="7" result="n"/>
<feColorMatrix in="n" type="saturate" values="0" result="d"/>
<feComponentTransfer in="d" result="t">
<feFuncA type="linear" slope=".055"/>
</feComponentTransfer>
<feComposite in="t" in2="SourceGraphic" operator="atop"/>
</filter>
<!-- ======== CLIP ======== -->
<clipPath id="sphereClip">
<circle cx="400" cy="386" r="258"/>
</clipPath>
<clipPath id="wingClipL">
<path d="M372 300 L196 176 L246 292 L360 344 Z"/>
</clipPath>
<!-- ======== MASKS ======== -->
<mask id="innerMask">
<circle cx="400" cy="386" r="258" fill="#fff"/>
</mask>
</defs>
<!-- ================= BACKDROP ================= -->
<rect width="800" height="800" fill="url(#bgGlow)"/>
<g filter="url(#grain)" opacity=".5">
<rect width="800" height="800" fill="#808080" opacity=".35"/>
</g>
<ellipse cx="400" cy="700" rx="330" ry="120" fill="url(#poolLight)"/>
<rect x="0" y="600" width="800" height="200" fill="url(#floorGrad)" opacity=".55"/>
<ellipse cx="400" cy="690" rx="205" ry="52" fill="#05070c" opacity=".72" filter="url(#softShadow)"/>
<ellipse cx="400" cy="676" rx="150" ry="30" fill="url(#causticGlow)" filter="url(#tinyBlur)"/>
<path d="M250 678 L560 672 L520 706 L290 712 Z" fill="url(#caustic)" opacity=".55" filter="url(#tinyBlur)"/>
<!-- ================= GLASS BACK HALF ================= -->
<circle cx="400" cy="386" r="258" fill="url(#glassFill)"/>
<!-- ================= CRANE (inside sphere) ================= -->
<g clip-path="url(#sphereClip)">
<!-- ambient interior light -->
<ellipse cx="400" cy="386" rx="250" ry="240" fill="#9dc6ee" opacity=".07"/>
<g class="float">
<!-- soft cast shadow of crane onto sphere interior -->
<g opacity=".28" filter="url(#softShadow)" transform="translate(16,18)">
<path d="M372 300 L196 176 L246 292 L360 344 Z" fill="#0a1422"/>
<path d="M372 300 L604 176 L554 292 L440 344 Z" fill="#0a1422"/>
<path d="M330 250 L520 250 L556 420 L296 420 Z" fill="#0a1422"/>
</g>
<g class="sway">
<!-- ============ BODY (folded diamond) ============ -->
<g filter="url(#paperShadow)">
<!-- left body facet -->
<path d="M330 252 L400 232 L400 392 L296 420 Z" fill="url(#paperMid)"/>
<!-- right body facet -->
<path d="M400 232 L470 252 L556 420 L400 392 Z" fill="url(#paperShade)"/>
<!-- front chest facet (bright) -->
<path d="M400 232 L470 252 L400 392 L296 420 Z" fill="url(#paperMid)" opacity=".0"/>
<path d="M330 252 L400 232 L400 392 L296 420 Z" fill="url(#paperTop)" opacity=".55"/>
<!-- lower belly fold -->
<path d="M296 420 L400 392 L556 420 L400 452 Z" fill="url(#paperDeep)"/>
<path d="M400 392 L556 420 L400 452 Z" fill="#6f6a60" opacity=".35"/>
<!-- central spine crease -->
<path d="M400 232 L400 392" stroke="#8c8578" stroke-width="1.4" opacity=".7" fill="none"/>
<path d="M400 392 L400 452" stroke="#6b6559" stroke-width="1.2" opacity=".6" fill="none"/>
<!-- belly ridge highlight -->
<path d="M300 418 L400 390 L554 418" stroke="#fffdf7" stroke-width="1.6" opacity=".55" fill="none"/>
</g>
<!-- ============ TAIL (left sweep) ============ -->
<g filter="url(#paperShadow)">
<path d="M368 262 L214 214 L268 302 L352 330 Z" fill="url(#paperWarm)"/>
<path d="M368 262 L214 214 L300 268 Z" fill="#fffaf0" opacity=".85"/>
<path d="M368 262 L300 268 L268 302 L352 330 Z" fill="url(#paperGold)" opacity=".55"/>
<path d="M368 262 L214 214 L268 302" stroke="#a2937a" stroke-width="1.2" fill="none" opacity=".65"/>
<path d="M214 214 L300 268 L352 330" stroke="#ffffff" stroke-width="1" fill="none" opacity=".45"/>
<path d="M300 268 L352 330 L368 262 Z" fill="#c79a5f" opacity=".35"/>
</g>
<!-- ============ WINGS ============ -->
<!-- RIGHT WING -->
<g filter="url(#paperShadow)">
<path d="M392 292 L610 160 L560 288 L436 344 Z" fill="url(#wingUpper)"/>
<!-- upper bright facet -->
<path d="M392 292 L610 160 L470 268 Z" fill="#ffffff" opacity=".92"/>
<!-- mid facet -->
<path d="M392 292 L470 268 L560 288 Z" fill="url(#paperMid)"/>
<!-- trailing facet -->
<path d="M392 292 L560 288 L436 344 Z" fill="url(#paperShade)"/>
<!-- underside sliver -->
<path d="M436 344 L560 288 L612 168 L604 214 Z" fill="url(#wingUnder)" opacity=".85"/>
<!-- crease strokes -->
<g stroke="#94897a" stroke-width="1.25" fill="none" opacity=".62" stroke-linecap="round">
<path d="M392 292 L610 160"/>
<path d="M392 292 L470 268 L560 288"/>
<path d="M392 292 L436 344"/>
<path d="M470 268 L492 214"/>
</g>
<!-- highlight strokes -->
<g stroke="#ffffff" stroke-width="1.1" fill="none" opacity=".7" stroke-linecap="round">
<path d="M404 284 L598 168"/>
<path d="M408 296 L466 274"/>
</g>
<!-- wing-tip edge -->
<path d="M610 160 L604 214" stroke="#fff6e6" stroke-width="2" opacity=".55" fill="none"/>
</g>
<!-- LEFT WING (behind, darker, for depth) -->
<g filter="url(#paperShadow)" opacity=".97">
<path d="M368 296 L192 166 L244 292 L356 344 Z" fill="url(#paperMid)"/>
<path d="M368 296 L192 166 L292 246 Z" fill="#f4f1ea"/>
<path d="M368 296 L292 246 L244 292 Z" fill="url(#paperShade)"/>
<path d="M368 296 L244 292 L356 344 Z" fill="url(#paperDeep)"/>
<path d="M356 344 L244 292 L198 176 L196 220 Z" fill="url(#wingUnder)" opacity=".7"/>
<g stroke="#8b8071" stroke-width="1.2" fill="none" opacity=".58" stroke-linecap="round">
<path d="M368 296 L192 166"/>
<path d="M368 296 L292 246 L244 292"/>
<path d="M368 296 L356 344"/>
<path d="M292 246 L262 196"/>
</g>
<path d="M198 168 L292 246" stroke="#ffffff" stroke-width="1" opacity=".5" fill="none"/>
</g>
<!-- ============ HEAD & NECK ============ -->
<g filter="url(#paperShadow)">
<!-- neck (reverse fold, thin spike going up-left) -->
<path d="M398 246 L344 118 L322 152 L372 268 Z" fill="url(#paperWarm)"/>
<path d="M398 246 L344 118 L356 210 Z" fill="#fffaf1" opacity=".9"/>
<path d="M344 118 L322 152 L372 268 Z" fill="url(#paperGold)" opacity=".45"/>
<!-- head -->
<path d="M344 118 L300 96 L318 132 L352 142 Z" fill="url(#paperTop)"/>
<!-- beak -->
<path d="M300 96 L262 108 L312 124 Z" fill="url(#beakGrad)"/>
<path d="M262 108 L312 124 L300 96 Z" fill="#b57a41" opacity=".45"/>
<!-- eye -->
<circle cx="330" cy="116" r="3.1" fill="#2c2a26"/>
<circle cx="331" cy="114.6" r="1" fill="#ffffff" opacity=".9"/>
<!-- head creases -->
<g stroke="#9a8f7d" stroke-width="1.1" fill="none" opacity=".6" stroke-linecap="round">
<path d="M300 96 L352 142"/>
<path d="M344 118 L318 132"/>
<path d="M398 246 L356 210"/>
</g>
<path d="M304 100 L318 130" stroke="#fff" stroke-width="1" opacity=".55" fill="none"/>
</g>
</g>
</g>
<!-- interior refraction distortion overlay -->
<circle cx="400" cy="386" r="258" fill="#bcd9f5" opacity=".05"/>
<!-- refracted caustic band across crane -->
<g opacity=".30" filter="url(#tinyBlur)">
<path d="M180 240 L620 470 L600 500 L160 270 Z" fill="url(#caustic)"/>
</g>
</g>
<!-- ================= GLASS FRONT: reflections ================= -->
<g pointer-events="none">
<!-- outer rim light -->
<circle cx="400" cy="386" r="258" fill="url(#glassEdge)"/>
<circle cx="400" cy="386" r="258" fill="none" stroke="url(#rimLight)" stroke-width="3.2"/>
<circle cx="400" cy="386" r="252" fill="none" stroke="#ffffff" stroke-width="1" opacity=".18"/>
<!-- refraction: inverted faint arc seen through glass edge -->
<g opacity=".22" filter="url(#tinyBlur)">
<path d="M196 250 A 258 258 0 0 1 604 250" fill="none" stroke="#dceeff" stroke-width="6"/>
</g>
<!-- big soft sheen top-left -->
<ellipse cx="288" cy="230" rx="150" ry="96" fill="url(#sheenGrad)" opacity=".55"
transform="rotate(-32 288 230)" filter="url(#tinyBlur)"/>
<path class="shimmer" d="M120 120 L210 120 L520 690 L430 690 Z" fill="#ffffff" opacity=".07"/>
<!-- crisp specular highlight -->
<ellipse cx="278" cy="212" rx="60" ry="36" fill="url(#specBlob)" transform="rotate(-34 278 212)"/>
<ellipse cx="252" cy="196" rx="22" ry="13" fill="#ffffff" opacity=".72" transform="rotate(-34 252 196)" filter="url(#tinyBlur)"/>
<!-- secondary small highlight -->
<ellipse cx="596" cy="546" rx="34" ry="15" fill="#ffffff" opacity=".22" transform="rotate(-40 596 546)" filter="url(#tinyBlur)"/>
<circle cx="556" cy="196" r="9" fill="#ffffff" opacity=".38" filter="url(#tinyBlur)"/>
<!-- top glint -->
<ellipse cx="404" cy="132" rx="46" ry="10" fill="#e8f6ff" opacity=".28" filter="url(#tinyBlur)"/>
<!-- window reflection (studio softbox) -->
<g opacity=".14" filter="url(#tinyBlur)" fill="#ffffff">
<rect x="150" y="176" width="44" height="120" rx="8" transform="rotate(-24 172 236)"/>
<rect x="204" y="150" width="16" height="96" rx="6" transform="rotate(-24 212 198)"/>
</g>
<!-- fresnel inner bright ring -->
<circle cx="400" cy="386" r="244" fill="none" stroke="#eaf6ff" stroke-width="1.4" opacity=".2"/>
</g>
<!-- ================= FLOOR REFLECTION OF SPHERE ================= -->
<g opacity=".16" transform="translate(0,1246) scale(1,-1)" mask="url(#innerMask)">
<ellipse cx="400" cy="386" rx="258" ry="258" fill="url(#glassFill)" opacity=".5"/>
</g>
<ellipse cx="400" cy="672" rx="210" ry="34" fill="url(#caustic)" opacity=".42" filter="url(#tinyBlur)"/>
<ellipse cx="400" cy="668" rx="96" ry="16" fill="#ffe3ad" opacity=".16" filter="url(#tinyBlur)"/>
<!-- ================= FOREGROUND DUST / VIGNETTE ================= -->
<g opacity=".5">
<circle cx="150" cy="160" r="1.6" fill="#dbe9ff" opacity=".6"/>
<circle cx="668" cy="300" r="1.2" fill="#dbe9ff" opacity=".45"/>
<circle cx="118" cy="520" r="1.8" fill="#dbe9ff" opacity=".4"/>
<circle cx="700" cy="596" r="1.4" fill="#dbe9ff" opacity=".5"/>
<circle cx="512" cy="118" r="1" fill="#ffffff" opacity=".5"/>
<circle cx="262" cy="700" r="1.3" fill="#dbe9ff" opacity=".35"/>
</g>
<rect width="800" height="800" fill="none" stroke="#000000" stroke-opacity=".0"/>
</svg>
</div>
<div class="caption">
<h1>Paper <span>&</span> Glass</h1>
<p>A single folded crane suspended in a hand-blown sphere — twenty-four facets, one continuous crease path, and light doing the rest.</p>
<span class="badge">SVG · no dependencies</span>
</div>
</main>
</body>
</html>
Test 2: Interactive procedural globe
Actual MiniMax procedural globe after browser interaction checks
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="color-scheme" content="dark">
<title>Procedural Globe — AcceleratedLogic</title>
<style>
:root{
--bg:#04060f; --ink:#e9f0fb; --muted:#93a3bd;
--line:rgba(150,180,230,.18); --glass:rgba(13,19,36,.66);
--accent:#67e8f9; --accent2:#a78bfa; --danger:#ff8f8f;
}
*{box-sizing:border-box}
[hidden]{display:none !important}
html,body{height:100%}
body{
margin:0; overflow:hidden; color:var(--ink);
font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
background:
radial-gradient(1100px 780px at 16% 10%, rgba(30,64,120,.55), transparent 62%),
radial-gradient(900px 720px at 86% 84%, rgba(78,30,112,.45), transparent 58%),
radial-gradient(620px 620px at 52% 48%, rgba(16,30,62,.65), transparent 72%),
var(--bg);
-webkit-font-smoothing:antialiased;
}
#stage{position:fixed; inset:0; outline:none}
#stage::after{content:""; position:absolute; inset:0; pointer-events:none;
background:radial-gradient(circle at 50% 50%, transparent 42%, rgba(0,0,0,.6) 100%)}
#stage:focus-visible{box-shadow:inset 0 0 0 2px var(--accent)}
canvas{display:block; width:100%; height:100%; touch-action:none; cursor:grab}
canvas.dragging{cursor:grabbing}
.glass{
background:var(--glass); border:1px solid var(--line); border-radius:16px;
backdrop-filter:blur(14px) saturate(140%); -webkit-backdrop-filter:blur(14px) saturate(140%);
box-shadow:0 18px 48px rgba(0,0,0,.45);
}
.hud{
position:fixed; top:clamp(12px,3vw,26px); left:clamp(12px,3vw,26px); z-index:5;
padding:14px 18px; max-width:min(78vw,340px);
}
.hud h1{margin:0 0 4px; font-size:15px; letter-spacing:.14em; text-transform:uppercase;
background:linear-gradient(92deg,var(--accent),var(--accent2)); -webkit-background-clip:text;
background-clip:text; color:transparent}
.hud p{margin:0; font-size:12.5px; line-height:1.5; color:var(--muted)}
.badge{display:inline-block; margin-top:8px; padding:3px 9px; border-radius:999px; font-size:11px;
border:1px solid var(--line); color:var(--accent); background:rgba(103,232,249,.08)}
.panel{
position:fixed; z-index:5; right:clamp(12px,3vw,26px); bottom:clamp(12px,3vw,26px);
padding:14px 16px; width:min(330px,calc(100vw - 24px)); display:grid; gap:12px;
}
.row{display:flex; gap:8px; align-items:center}
.row .spacer{flex:1}
button{
font:inherit; color:var(--ink); background:rgba(255,255,255,.05); cursor:pointer;
border:1px solid var(--line); border-radius:11px; padding:9px 13px; min-width:44px; min-height:42px;
transition:background .18s ease, transform .12s ease, border-color .18s ease;
}
button:hover{background:rgba(103,232,249,.14); border-color:rgba(103,232,249,.45)}
button:active{transform:translateY(1px)}
button:focus-visible{outline:2px solid var(--accent); outline-offset:2px}
button.primary{background:linear-gradient(180deg, rgba(103,232,249,.22), rgba(167,139,250,.18));
border-color:rgba(103,232,249,.45); font-weight:600}
button .glyph{display:block; font-size:17px; line-height:1}
.zoomwrap{display:flex; gap:6px; align-items:center}
.zoomwrap span{font-size:11px; color:var(--muted); letter-spacing:.06em; text-transform:uppercase}
.sliderwrap{display:grid; grid-template-columns:1fr auto; gap:6px 10px; align-items:center}
.sliderwrap label{font-size:12px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted)}
.sliderwrap output{font-variant-numeric:tabular-nums; font-size:12.5px; color:var(--accent)}
input[type=range]{grid-column:1 / -1; width:100%; height:22px; margin:0; background:transparent;
-webkit-appearance:none; appearance:none; cursor:pointer}
input[type=range]::-webkit-slider-runnable-track{height:6px; border-radius:999px;
background:linear-gradient(90deg, var(--accent), var(--accent2)); opacity:.85}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none; width:18px; height:18px; margin-top:-6px;
border-radius:50%; background:#eaf6ff; border:2px solid #0a1020; box-shadow:0 0 0 1px var(--accent)}
input[type=range]::-moz-range-track{height:6px; border-radius:999px;
background:linear-gradient(90deg, var(--accent), var(--accent2))}
input[type=range]::-moz-range-thumb{width:16px; height:16px; border-radius:50%; background:#eaf6ff;
border:2px solid #0a1020; box-shadow:0 0 0 1px var(--accent)}
input[type=range]:focus-visible{outline:2px solid var(--accent); outline-offset:4px; border-radius:8px}
.hint{margin:0; font-size:11.5px; color:var(--muted); line-height:1.5}
.overlay{position:fixed; inset:0; z-index:20; display:grid; place-items:center; padding:24px;
background:radial-gradient(circle at 50% 50%, rgba(8,14,30,.72), rgba(2,4,10,.93))}
.card{max-width:420px; text-align:center; padding:28px 26px; display:grid; gap:10px; justify-items:center}
.card h2{margin:0; font-size:16px; letter-spacing:.04em}
.card p{margin:0; font-size:13px; color:var(--muted); line-height:1.6}
.card .err{border-color:rgba(255,143,143,.4)}
.card .err h2{color:var(--danger)}
.spinner{width:46px; height:46px; border-radius:50%; border:3px solid rgba(103,232,249,.18);
border-top-color:var(--accent); animation:spin 1s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.bar{width:190px; height:3px; border-radius:999px; background:rgba(255,255,255,.08); overflow:hidden}
.bar i{display:block; height:100%; width:40%; border-radius:999px;
background:linear-gradient(90deg,transparent,var(--accent),transparent); animation:sweep 1.6s ease-in-out infinite}
@keyframes sweep{0%{transform:translateX(-120%)}100%{transform:translateX(320%)}}
.sr-only{position:absolute; width:1px; height:1px; margin:-1px; padding:0; overflow:hidden;
clip:rect(0 0 0 0); white-space:nowrap; border:0}
@media (max-width:640px){
.panel{left:clamp(12px,3vw,26px); right:clamp(12px,3vw,26px); width:auto}
.hud{max-width:none; right:clamp(12px,3vw,26px)}
}
@media (prefers-reduced-motion: reduce){
.spinner,.bar i{animation:none}
.spinner{border-top-color:var(--accent); opacity:.7}
button{transition:none}
}
</style>
</head>
<body>
<div id="stage" tabindex="0" role="group"
aria-label="Interactive three dimensional globe. Drag with a pointer or use the arrow keys to rotate. Use the zoom buttons to move closer or further away.">
<canvas id="gl" aria-hidden="true"></canvas>
</div>
<header class="hud glass">
<h1>Procedural Globe</h1>
<p>Continents are synthesised from layered noise in the browser — no map data, no images, no tiles.</p>
<span class="badge" id="badge">Three.js r170 · WebGL</span>
</header>
<section class="panel glass" aria-label="Globe controls">
<div class="row">
<div class="zoomwrap">
<button id="zoomOut" aria-label="Zoom out" title="Zoom out"><span class="glyph" aria-hidden="true">−</span></button>
<button id="zoomIn" aria-label="Zoom in" title="Zoom in"><span class="glyph" aria-hidden="true">+</span></button>
<span id="zoomVal" aria-hidden="true">100%</span>
</div>
<span class="spacer"></span>
<button id="reset" class="primary" aria-label="Reset the view to its default orientation and distance">Reset</button>
</div>
<div class="sliderwrap">
<label for="speed">Rotation speed</label>
<output id="spinVal" for="speed">20%</output>
<input id="speed" type="range" min="0" max="100" step="1" value="20"
aria-describedby="speedHelp">
</div>
<p class="hint" id="speedHelp">Drag to spin · scroll to zoom · arrow keys, plus and minus work when the globe is focused.</p>
</section>
<p class="sr-only" id="live" role="status" aria-live="polite"></p>
<div class="overlay" id="overlay" role="status" aria-live="polite">
<div class="card glass">
<div class="spinner" aria-hidden="true"></div>
<h2 id="ovTitle">Loading the 3D engine…</h2>
<p id="ovMsg">Fetching Three.js r170 from the jsDelivr CDN.</p>
<div class="bar" aria-hidden="true"><i></i></div>
</div>
</div>
<div class="overlay" id="errorBox" role="alert" hidden>
<div class="card glass err">
<h2>The globe could not start</h2>
<p id="errMsg"></p>
<button id="retry" class="primary">Try again</button>
</div>
</div>
<script>
/* Watchdog: if the ES module never runs (blocked CDN, old browser, parse error) fail loudly. */
window.__globeOk = false;
window.addEventListener('error', function (e) {
if (!window.__globeOk && e && e.target && e.target.tagName === 'SCRIPT') showGlobeError('script');
});
function showGlobeError(reason) {
var box = document.getElementById('errorBox');
if (!box || !box.hasAttribute('hidden')) return;
document.getElementById('errMsg').textContent =
'The Three.js module could not be loaded from https://cdn.jsdelivr.net. This usually means the network request was blocked or the CDN was unreachable. Check your connection and retry.';
document.getElementById('overlay').setAttribute('hidden', '');
box.removeAttribute('hidden');
}
setTimeout(function () { if (!window.__globeOk) showGlobeError('timeout'); }, 12000);
document.getElementById('retry').addEventListener('click', function () { location.reload(); });
</script>
<script type="module">
const $ = (id) => document.getElementById(id);
const overlay = $('overlay'), ovTitle = $('ovTitle'), ovMsg = $('ovMsg');
const errorBox = $('errorBox'), errMsg = $('errMsg'), live = $('live');
const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
function fail(msg) {
errMsg.textContent = msg;
errorBox.removeAttribute('hidden');
overlay.setAttribute('hidden', '');
window.__globeOk = true; // stop the watchdog from overwriting a specific message
}
/* ------------------------------------------------------------------ noise */
const hash = (x, y, z) => {
let n = (Math.imul(x, 374761393) ^ Math.imul(y, 668265263) ^ Math.imul(z, 1274126177)) | 0;
n = Math.imul(n ^ (n >>> 13), 1274126177);
return ((n ^ (n >>> 16)) >>> 0) / 4294967295;
};
const fade = (t) => t * t * (3 - 2 * t);
function vnoise(x, y, z) {
const xi = Math.floor(x), yi = Math.floor(y), zi = Math.floor(z);
const u = fade(x - xi), v = fade(y - yi), w = fade(z - zi);
const c000 = hash(xi, yi, zi), c100 = hash(xi + 1, yi, zi);
const c010 = hash(xi, yi + 1, zi), c110 = hash(xi + 1, yi + 1, zi);
const c001 = hash(xi, yi, zi + 1), c101 = hash(xi + 1, yi, zi + 1);
const c011 = hash(xi, yi + 1, zi + 1), c111 = hash(xi + 1, yi + 1, zi + 1);
const x00 = c000 + (c100 - c000) * u, x10 = c010 + (c110 - c010) * u;
const x01 = c001 + (c101 - c001) * u, x11 = c011 + (c111 - c011) * u;
const y0 = x00 + (x10 - x00) * v, y1 = x01 + (x11 - x01) * v;
return y0 + (y1 - y0) * w;
}
function fbm(x, y, z, oct) {
let amp = 0.5, freq = 1, sum = 0, norm = 0;
for (let i = 0; i < oct; i++) {
sum += amp * vnoise(x * freq, y * freq, z * freq);
norm += amp; freq *= 2.03; amp *= 0.5;
}
return sum / norm;
}
let seed = 20240607;
const rand = () => { seed = (seed * 1664525 + 1013904223) >>> 0; return seed / 4294967296; };
/* --------------------------------------------------- procedural maps */
const W = 768, H = 384;
function buildMaps() {
const colC = document.createElement('canvas'), hgtC = document.createElement('canvas');
const rghC = document.createElement('canvas'), litC = document.createElement('canvas');
[colC, hgtC, rghC, litC].forEach((c) => { c.width = W; c.height = H; });
const cctx = colC.getContext('2d'), hctx = hgtC.getContext('2d');
const rctx = rghC.getContext('2d'), lctx = litC.getContext('2d');
const cImg = cctx.createImageData(W, H), hImg = hctx.createImageData(W, H);
const rImg = rctx.createImageData(W, H), lImg = lctx.createImageData(W, H);
const cd = cImg.data, hd = hImg.data, rd = rImg.data, ld = lImg.data;
const mask = new Uint8Array(W * H);
const SEA = 0.53;
const stops = [
[0.00, 226, 208, 158], [0.10, 128, 152, 92], [0.30, 70, 116, 66],
[0.52, 106, 112, 74], [0.72, 124, 98, 72], [0.88, 152, 148, 142], [1.00, 238, 243, 248]
];
for (let y = 0; y < H; y++) {
const lat = (0.5 - y / (H - 1)) * Math.PI;
const cl = Math.cos(lat), sl = Math.sin(lat), latN = Math.abs(lat) / (Math.PI / 2);
for (let x = 0; x < W; x++) {
const lon = (x / W) * Math.PI * 2;
const px = cl * Math.cos(lon), py = sl, pz = cl * Math.sin(lon);
const qx = fbm(px * 1.15 + 11.3, py * 1.15 + 2.7, pz * 1.15 + 5.1, 2);
const qy = fbm(px * 1.15 + 3.7, py * 1.15 + 9.2, pz * 1.15 + 1.3, 2);
const qz = fbm(px * 1.15 + 7.4, py * 1.15 + 5.6, pz * 1.15 + 8.8, 2);
const r = fbm(px * 1.85 + qx * 3.1, py * 1.85 + qy * 3.1, pz * 1.85 + qz * 3.1, 5);
const h = r + 0.035 * Math.cos(lat * 2) - 0.02 * latN;
const i = y * W + x, o = i * 4;
const land = h > SEA;
mask[i] = land ? 1 : 0;
let cr, cg, cb, rough;
if (land) {
let e = clamp((h - SEA) / (1 - SEA), 0, 1);
e = Math.pow(e, 1.25);
let s = 0;
while (s < stops.length - 2 && e > stops[s + 1][0]) s++;
const a = stops[s], b = stops[s + 1];
const t = clamp((e - a[0]) / (b[0] - a[0]), 0, 1);
cr = a[1] + (b[1] - a[1]) * t; cg = a[2] + (b[2] - a[2]) * t; cb = a[3] + (b[3] - a[3]) * t;
const desert = Math.exp(-Math.pow((latN - 0.33) / 0.12, 2)) * (0.6 + 0.4 * vnoise(px * 9, py * 9, pz * 9));
const dry = clamp(desert * 1.4 - e * 1.2, 0, 1);
cr += (223 - cr) * dry; cg += (200 - cg) * dry; cb += (152 - cb) * dry;
const ice = clamp((latN - 0.74) / 0.16, 0, 1) * (0.35 + 0.65 * clamp(e * 2.2, 0, 1));
cr += (245 - cr) * ice; cg += (250 - cg) * ice; cb += (255 - cb) * ice;
const hv = 118 + e * 132;
hd[o] = hd[o + 1] = hd[o + 2] = hv; hd[o + 3] = 255;
rough = 205 + e * 40;
} else {
const depth = clamp((SEA - h) / 0.24, 0, 1);
const sw = (vnoise(px * 14, py * 14, pz * 14) - 0.5) * 16;
cr = 20 -16 * depth + sw; cg = 62 - 46 * depth + sw; cb = 108 - 70 * depth + sw * 1.4;
hd[o] = hd[o + 1] = hd[o + 2] = 46; hd[o + 3] = 255;
rough = 48 -22 * depth;
}
cd[o] = clamp(cr, 0, 255); cd[o + 1] = clamp(cg, 0, 255); cd[o + 2] = clamp(cb, 0, 255); cd[o + 3] = 255;
rd[o] = rd[o + 1] = rd[o + 2] = clamp(rough, 0, 255); rd[o + 3] = 255;
ld[o] = ld[o + 1] = ld[o + 2] = 0; ld[o + 3] = 255;
}
}
/* night-side city lights: bright near coastlines, clustered by a high frequency field */
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
const i = y * W + x;
if (!mask[i]) continue;
let coastal = 0;
for (let k = 0; k < 8; k++) {
const nx = x + [3, -3, 0, 0, 3, -3, 3, -3][k], ny = y + [0, 0, 3, -3, 3, 3, -3, -3][k];
if (nx < 0 || ny < 0 || nx >= W || ny >= H || !mask[ny * W + nx]) { coastal = 1; break; }
}
const lon = (x / W) * Math.PI * 2, lat = (0.5 - y / (H - 1)) * Math.PI;
const px = Math.cos(lat) * Math.cos(lon), py = Math.sin(lat), pz = Math.cos(lat) * Math.sin(lon);
const dens = vnoise(px * 46, py * 46, pz * 46);
const p = coastal ? 0.10 : 0.004;
if (rand() < p * dens * dens * 3) {
const o = i * 4;
ld[o] = 255; ld[o + 1] = 236; ld[o + 2] = 196;
const nb = [[1, 0], [-1, 0], [0, 1], [0, -1]];
for (const [dx, dy] of nb) {
const nx = x + dx, ny = y + dy;
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
const q = (ny * W + nx) * 4;
if (ld[q] === 0) { ld[q] = 74; ld[q + 1] = 66; ld[q + 2] = 48; }
}
}
}
}
cctx.putImageData(cImg, 0, 0); hctx.putImageData(hImg, 0, 0);
rctx.putImageData(rImg, 0, 0); lctx.putImageData(lImg, 0, 0);
return { colC, hgtC, rghC, litC };
}
/* ------------------------------------------------------------ bootstrap */
(async function init() {
let THREE;
try {
THREE = await import('https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js');
} catch (err) {
fail('Three.js r170 could not be downloaded from https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js (' + (err && err.message ? err.message : 'network error') + ').');
return;
}
const canvas = $('gl');
let renderer;
try {
renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true, powerPreference: 'high-performance' });
} catch (err) {
fail('WebGL could not be initialised. Your browser or GPU driver may block WebGL, or hardware acceleration may be disabled. ' + (err && err.message ? err.message : ''));
return;
}
if (!renderer.capabilities.isWebGL2 && !renderer.getContext()) {
fail('No usable WebGL context was created.');
return;
}
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.08;
renderer.outputColorSpace = THREE.SRGBColorSpace;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 400);
camera.position.set(0, 0, 3);
/* soft environment light from a tiny generated gradient */
const envC = document.createElement('canvas'); envC.width = 32; envC.height = 16;
const ectx = envC.getContext('2d');
const grd = ectx.createLinearGradient(0, 0, 0, 16);
grd.addColorStop(0, '#6f8fd0'); grd.addColorStop(0.5, '#16233f'); grd.addColorStop(1, '#05080f');
ectx.fillStyle = grd; ectx.fillRect(0, 0, 32, 16);
const envTex = new THREE.CanvasTexture(envC);
envTex.mapping = THREE.EquirectangularReflectionMapping;
envTex.colorSpace = THREE.SRGBColorSpace;
const pmrem = new THREE.PMREMGenerator(renderer);
scene.environment = pmrem.fromEquirectangular(envTex).texture;
envTex.dispose(); pmrem.dispose();
/* ------------------------------------------------------------- globe */
const globe = new THREE.Group();
scene.add(globe);
ovTitle.textContent = 'Synthesising continents…';
ovMsg.textContent = 'Layering noise fields into colour, elevation, roughness and city-light maps.';
await new Promise((r) => requestAnimationFrame(() => setTimeout(r, 40)));
const maps = buildMaps();
const aniso = Math.min(8, renderer.capabilities.getMaxAnisotropy());
const texMap = new THREE.CanvasTexture(maps.colC); texMap.colorSpace = THREE.SRGBColorSpace; texMap.anisotropy = aniso;
const texBump = new THREE.CanvasTexture(maps.hgtC); texBump.anisotropy = aniso;
const texRough = new THREE.CanvasTexture(maps.rghC); texRough.anisotropy = aniso;
const texLight = new THREE.CanvasTexture(maps.litC); texLight.colorSpace = THREE.SRGBColorSpace; texLight.anisotropy = aniso;
const planetMat = new THREE.MeshStandardMaterial({
map: texMap, bumpMap: texBump, bumpScale: 0.035,
roughnessMap: texRough, metalness: 0.0,
emissiveMap: texLight, emissive: new THREE.Color(0xfff0c8), emissiveIntensity: 1.5,
envMapIntensity: 0.45
});
const sunDirView = new THREE.Vector3(0, 0, 1);
planetMat.onBeforeCompile = (shader) => {
shader.uniforms.uSunDir = { value: sunDirView };
shader.fragmentShader = 'uniform vec3 uSunDir;\n' + shader.fragmentShader.replace(
'#include <emissivemap_fragment>',
`#include <emissivemap_fragment>
float night = smoothstep(0.12, -0.28, dot(normalize(normal), normalize(uSunDir)));
totalEmissiveRadiance *= night;`
);
};
const planet = new THREE.Mesh(new THREE.SphereGeometry(1, 128, 96), planetMat);
planet.rotation.z = 0.41;
globe.add(planet);
/* ---------------------------------------------------------- lighting */
const sun = new THREE.DirectionalLight(0xfff3dc, 2.6);
sun.position.set(4.2, 2.1, 3.6); scene.add(sun);
const fill = new THREE.DirectionalLight(0x6f8cff, 0.5); fill.position.set(-4, -1.5, -3); scene.add(fill);
scene.add(new THREE.HemisphereLight(0x2f4f86, 0x060a14, 0.55));
/* -------------------------------------------------- atmosphere shells */
const atmoVert = `
varying vec3 vWorld;
void main(){
vWorld = (modelMatrix * vec4(position,1.0)).xyz;
gl_Position = projectionMatrix * viewMatrix * vec4(vWorld,1.0);
}`;
const atmoFrag = `
uniform vec3 uInner; uniform vec3 uOuter; uniform float uR; uniform float uK; uniform float uGain;
varying vec3 vWorld;
void main(){
vec3 ro = cameraPosition;
vec3 rd = normalize(vWorld - ro);
float tca = dot(-ro, rd);
float b = length(ro + rd * tca); // closest approach of the ray to the planet centre
float d = b - uR;
float inner = exp(-max(-d, 0.0) / uK);
float outer = exp(-max(d, 0.0) / (uK * 2.6));
float i = max(inner * 0.85, outer);
vec3 col = mix(uInner, uOuter, clamp(outer, 0.0, 1.0));
gl_FragColor = vec4(col * i * uGain, i * uGain);
}`;
const mkShell = (radius, side, inner, outer, k, gain) => {
const m = new THREE.ShaderMaterial({
uniforms: {
uInner: { value: new THREE.Color(inner) }, uOuter: { value: new THREE.Color(outer) },
uR: { value: 1.0 }, uK: { value: k }, uGain: { value: gain }
},
vertexShader: atmoVert, fragmentShader: atmoFrag,
transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, side
});
const mesh = new THREE.Mesh(new THREE.SphereGeometry(radius, 96, 64), m);
mesh.renderOrder = 2;
globe.add(mesh);
return mesh;
};
mkShell(1.045, THREE.FrontSide, 0x9fd8ff, 0x3f7dff, 0.030, 0.85);
mkShell(1.34, THREE.BackSide, 0x7fb6ff, 0x2a52ff, 0.055, 1.15);
/* ------------------------------------------------- orbital light ring */
const TILT = 0.34;
const COUNT = 1500;
const aRadius = new Float32Array(COUNT), aAngle = new Float32Array(COUNT), aSpeed = new Float32Array(COUNT);
const aSize = new Float32Array(COUNT), aHeight = new Float32Array(COUNT), aPhase = new Float32Array(COUNT);
for (let i = 0; i < COUNT; i++) {
aRadius[i] = 1.16 + Math.pow(rand(), 0.8) * 0.62;
aAngle[i] = rand() * Math.PI * 2;
aSpeed[i] = (0.10 + rand() * 0.42) * (rand() < 0.1 ? -0.8 : 1);
aSize[i] = 1.1 + Math.pow(rand(), 2.6) * 3.4;
aHeight[i] = (rand() - 0.5) * 0.11;
aPhase[i] = rand() * Math.PI * 2;
}
const pGeo = new THREE.BufferGeometry();
pGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(COUNT * 3), 3));
pGeo.setAttribute('aRadius', new THREE.BufferAttribute(aRadius, 1));
pGeo.setAttribute('aAngle', new THREE.BufferAttribute(aAngle, 1));
pGeo.setAttribute('aSpeed', new THREE.BufferAttribute(aSpeed, 1));
pGeo.setAttribute('aSize', new THREE.BufferAttribute(aSize, 1));
pGeo.setAttribute('aHeight', new THREE.BufferAttribute(aHeight, 1));
pGeo.setAttribute('aPhase', new THREE.BufferAttribute(aPhase, 1));
const pMat = new THREE.ShaderMaterial({
uniforms: { uTime: { value: 0 }, uPR: { value: Math.min(window.devicePixelRatio || 1, 2) }, uTilt: { value: TILT } },
vertexShader: `
uniform float uTime; uniform float uPR; uniform float uTilt;
attribute float aRadius, aAngle, aSpeed, aSize, aHeight, aPhase;
varying float vA; varying float vT;
void main(){
float ang = aAngle + uTime * aSpeed;
vec3 p = vec3(cos(ang) * aRadius, aHeight, sin(ang) * aRadius);
float c = cos(uTilt), s = sin(uTilt);
p = vec3(p.x, p.y * c - p.z * s, p.y * s + p.z * c);
vec4 mv = modelViewMatrix * vec4(p, 1.0);
gl_Position = projectionMatrix * mv;
float d = max(-mv.z, 0.001);
gl_PointSize = aSize * uPR * (170.0 / d);
vA = (0.45 + 0.55 * sin(aPhase + uTime * 1.7)) * smoothstep(8.0, 2.0, d);
vT = aSpeed;
}`,
fragmentShader: `
varying float vA; varying float vT;
void main(){
vec2 q = gl_PointCoord - 0.5;
float a = smoothstep(0.5, 0.02, length(q));
a = a * a;
vec3 col = mix(vec3(0.40, 0.78, 1.0), vec3(1.0, 0.93, 0.78), clamp(abs(vT) * 1.4, 0.0, 1.0));
gl_FragColor = vec4(col * a * vA * 1.7, a * vA);
}`,
transparent: true, blending: THREE.AdditiveBlending, depthWrite: false
});
const particles = new THREE.Points(pGeo, pMat);
particles.renderOrder = 3;
globe.add(particles);
const ringPts = [];
for (let i = 0; i <= 256; i++) {
const a = (i / 256) * Math.PI * 2;
ringPts.push(new THREE.Vector3(Math.cos(a) * 1.46, 0, Math.sin(a) * 1.46));
}
const ring = new THREE.LineLoop(
new THREE.BufferGeometry().setFromPoints(ringPts),
new THREE.LineBasicMaterial({ color: 0x67e8f9, transparent: true, opacity: 0.22, blending: THREE.AdditiveBlending, depthWrite: false })
);
ring.rotation.x = TILT;
globe.add(ring);
/* ------------------------------------------------------------- stars */
const sPos = new Float32Array(2200 * 3), sCol = new Float32Array(2200 * 3), sSize = new Float32Array(2200);
const tint = new THREE.Color();
for (let i = 0; i < 2200; i++) {
const u = rand() * 2 - 1, th = rand() * Math.PI * 2, r = 60 + rand() * 60;
const s = Math.sqrt(1 - u * u);
sPos[i * 3] = r * s * Math.cos(th); sPos[i * 3 + 1] = r * u; sPos[i * 3 + 2] = r * s * Math.sin(th);
tint.setHSL(0.55 + rand() * 0.12, 0.35, 0.72 + rand() * 0.28);
sCol[i * 3] = tint.r; sCol[i * 3 + 1] = tint.g; sCol[i * 3 + 2] = tint.b;
sSize[i] = 0.4 + Math.pow(rand(), 3) * 2.6;
}
const sGeo = new THREE.BufferGeometry();
sGeo.setAttribute('position', new THREE.BufferAttribute(sPos, 3));
sGeo.setAttribute('color', new THREE.BufferAttribute(sCol, 3));
sGeo.setAttribute('aSize', new THREE.BufferAttribute(sSize, 1));
const stars = new THREE.Points(sGeo, new THREE.ShaderMaterial({
uniforms: { uPR: { value: Math.min(window.devicePixelRatio || 1, 2) } },
vertexShader: `attribute float aSize; uniform float uPR; varying vec3 vC;
void main(){ vC = color; vec4 mv = modelViewMatrix * vec4(position,1.0);
gl_Position = projectionMatrix * mv; gl_PointSize = aSize * uPR; }`,
fragmentShader: `varying vec3 vC;
void main(){ float a = smoothstep(0.5, 0.0, length(gl_PointCoord - 0.5));
gl_FragColor = vec4(vC, a * 0.85); }`,
vertexColors: true, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending
}));
scene.add(stars);
/* -------------------------------------------------------- interaction */
const stage = $('stage'), slider = $('speed'), spinVal = $('spinVal'), zoomVal = $('zoomVal');
const reduceQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
const BASE_Z = 3.05, MIN_Z = 1.42, MAX_Z = 6.2;
let zoomTarget = BASE_Z, zoomNow = BASE_Z, speed = 0.09, motionOn = !reduceQuery.matches;
if (reduceQuery.matches) { slider.value = '0'; speed = 0; motionOn = false; }
function paintSlider() {
spinVal.textContent = slider.value + '%';
speed = (Number(slider.value) / 100) * 0.5;
if (speed > 0) motionOn = true;
}
paintSlider();
slider.addEventListener('input', paintSlider);
function setZoom(z, announce) {
zoomTarget = clamp(z, MIN_Z, MAX_Z);
const pct = Math.round((BASE_Z / zoomTarget) * 100);
zoomVal.textContent = pct + '%';
if (announce) live.textContent = 'Zoom ' + pct + ' percent';
}
$('zoomIn').addEventListener('click', () => setZoom(zoomTarget * 0.82, true));
$('zoomOut').addEventListener('click', () => setZoom(zoomTarget * 1.22, true));
$('reset').addEventListener('click', () => {
globe.rotation.set(0, globe.rotation.y % (Math.PI * 2), 0);
setZoom(BASE_Z, false); live.textContent = 'View reset';
});
let dragging = false, lastX = 0, lastY = 0, velX = 0, velY = 0;
canvas.addEventListener('pointerdown', (e) => {
dragging = true; lastX = e.clientX; lastY = e.clientY;
canvas.setPointerCapture(e.pointerId); canvas.classList.add('dragging');
stage.focus({ preventScroll: true });
});
canvas.addEventListener('pointermove', (e) => {
if (!dragging) return;
const dx = e.clientX - lastX, dy = e.clientY - lastY;
lastX = e.clientX; lastY = e.clientY;
velX = dx * 0.005; velY = dy * 0.005;
globe.rotation.y += velX;
globe.rotation.x = clamp(globe.rotation.x + velY, -1.25, 1.25);
});
const endDrag = () => { dragging = false; canvas.classList.remove('dragging'); };
canvas.addEventListener('pointerup', endDrag);
canvas.addEventListener('pointercancel', endDrag);
canvas.addEventListener('lostpointercapture', endDrag);
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
setZoom(zoomTarget * (e.deltaY > 0 ? 1.09 : 0.92), false);
}, { passive: false });
stage.addEventListener('keydown', (e) => {
const step = e.shiftKey ? 0.16 : 0.08;
switch (e.key) {
case 'ArrowLeft': globe.rotation.y -= step; break;
case 'ArrowRight': globe.rotation.y += step; break;
case 'ArrowUp': globe.rotation.x = clamp(globe.rotation.x - step, -1.25, 1.25); break;
case 'ArrowDown': globe.rotation.x = clamp(globe.rotation.x + step, -1.25, 1.25); break;
case '+': case '=': setZoom(zoomTarget * 0.82, true); break;
case '-': case '_': setZoom(zoomTarget * 1.22, true); break;
case '0': $('reset').click(); break;
default: return;
}
e.preventDefault();
});
const badge = $('badge');
function syncMotionBadge() {
badge.textContent = reduceQuery.matches && !motionOn
? 'Reduced motion · auto-spin off' : 'Three.js r170 · WebGL';
}
reduceQuery.addEventListener?.('change', (e) => {
if (e.matches) { slider.value = '0'; motionOn = false; paintSlider(); }
syncMotionBadge();
});
syncMotionBadge();
/* ----------------------------------------------------------- resize */
function resize() {
const w = stage.clientWidth || window.innerWidth, h = stage.clientHeight || window.innerHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h, false);
const pr = Math.min(window.devicePixelRatio || 1, 2);
renderer.setPixelRatio(pr);
pMat.uniforms.uPR.value = pr;
stars.material.uniforms.uPR.value = pr;
}
resize();
window.addEventListener('resize', resize);
if (window.ResizeObserver) new ResizeObserver(resize).observe(stage);
/* ------------------------------------------------------------- loop */
const clock = new THREE.Clock();
let elapsed = 0;
function frame() {
requestAnimationFrame(frame);
const dt = Math.min(clock.getDelta(), 0.05);
if (motionOn) {
elapsed += dt;
globe.rotation.y += speed * dt;
pMat.uniforms.uTime.value = elapsed;
}
if (!dragging) {
velX *= 0.93; velY *= 0.93;
if (Math.abs(velX) > 0.0002) globe.rotation.y += velX;
if (Math.abs(velY) > 0.0002) globe.rotation.x = clamp(globe.rotation.x + velY, -1.25, 1.25);
}
zoomNow += (zoomTarget - zoomNow) * Math.min(1, dt * 7);
camera.position.z = zoomNow;
camera.position.y = 0;
camera.lookAt(0, 0, 0);
sunDirView.copy(sun.position).sub(sun.target.position).normalize().transformDirection(camera.matrixWorldInverse);
stars.rotation.y += motionOn ? dt * 0.004 : 0;
renderer.render(scene, camera);
}
frame();
window.__globeOk = true;
overlay.setAttribute('hidden', '');
live.textContent = '3D globe ready. Drag to rotate, plus and minus buttons to zoom.';
})();
</script>
</body>
</html>
Test 3: Wizard game — delivered, but broken
NaN, with the game not paused.vel: new THREE.Vector3() but never initializes the separate player.vy field. The simulation executes player.vy -= GRAVITY * dt and adds that value to player.pos.y. Subtracting from undefined produces NaN, which then propagates into position and camera-follow calculations. The restart function resets the vector but also omits this separate vertical field. Resetting the visible score therefore does not establish playable physics.
Original wizard-game output showing its HUD over a black scene
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Arcane Skies — Crystal Run</title>
<style>
:root{
--ink:#2f2358; --panel:rgba(255,255,255,.74); --edge:rgba(255,255,255,.9);
--pink:#ff8fb1; --gold:#ffd166; --violet:#a98bff;
}
*{box-sizing:border-box}
html,body{margin:0;height:100%;overflow:hidden;background:#101433;color:var(--ink);
font-family:"Trebuchet MS","Segoe UI",system-ui,-apple-system,sans-serif;
-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;touch-action:none}
#stage{position:fixed;inset:0}
#stage canvas{display:block}
#hud{position:fixed;inset:0;pointer-events:none;display:flex;flex-direction:column;justify-content:space-between;padding:14px;gap:10px}
.hrow{display:flex;justify-content:space-between;align-items:flex-start;gap:10px}
.hrow.bottom{justify-content:center}
.panel{background:var(--panel);border:2px solid var(--edge);border-radius:16px;padding:9px 14px;
box-shadow:0 8px 20px rgba(25,15,60,.28);backdrop-filter:blur(7px);-webkit-backdrop-filter:blur(7px)}
.cap{font-size:10px;letter-spacing:.16em;text-transform:uppercase;font-weight:700;opacity:.6}
#hearts{display:flex;gap:3px;margin-top:3px;font-size:22px;line-height:1.1}
.heart{color:#ff5f88;text-shadow:0 2px 4px rgba(255,90,140,.35);transition:opacity .25s,filter .25s,transform .2s}
.heart.off{color:#cfc9e0;opacity:.5;filter:grayscale(1);text-shadow:none}
.stats{display:flex;gap:16px;align-items:center}
.stat b{font-size:19px;line-height:1.15;display:block}
#hint{font-size:13px;line-height:1.55;text-align:center;max-width:680px}
#hint b{color:#5b3fc0}
kbd{background:#fff;border:1px solid #bcb4d8;border-bottom-width:3px;border-radius:6px;padding:1px 6px;
font-family:inherit;font-size:11.5px;font-weight:700;margin:0 1px}
#toast{position:fixed;left:50%;top:15%;transform:translate(-50%,0);pointer-events:none;
font-weight:700;font-size:clamp(15px,3.2vw,23px);color:#fff;white-space:nowrap;opacity:0;
text-shadow:0 3px 10px rgba(30,10,60,.85);transition:opacity .3s}
#toast.show{opacity:1}
#flash{position:fixed;inset:0;pointer-events:none;opacity:0;z-index:10;
background:radial-gradient(circle at 50% 50%,rgba(255,60,90,0) 38%,rgba(255,40,80,.7) 100%);
transition:opacity .4s}
#overlay{position:fixed;inset:0;display:none;place-items:center;z-index:20;padding:20px;
background:radial-gradient(circle at 50% 38%,rgba(70,45,130,.42),rgba(8,6,28,.82));backdrop-filter:blur(4px)}
#overlay.show{display:grid}
.card{text-align:center;max-width:min(92vw,470px);padding:26px 30px;border-radius:24px;
background:linear-gradient(160deg,#fff8fc,#eaf1ff);border:3px solid #fff;
box-shadow:0 24px 60px rgba(5,0,30,.6)}
.card h1{margin:0 0 8px;font-size:clamp(26px,6vw,40px);letter-spacing:.01em}
.card p{margin:6px 0 18px;opacity:.78;line-height:1.5;font-size:15px}
button{pointer-events:auto;font:inherit;font-weight:700;font-size:17px;color:#3a2a08;
background:linear-gradient(180deg,#ffe49a,#ffc46b);border:none;border-bottom:4px solid #d9973f;
border-radius:14px;padding:12px 28px;cursor:pointer;box-shadow:0 10px 20px rgba(0,0,0,.28)}
button:hover{filter:brightness(1.06)} button:active{transform:translateY(2px);border-bottom-width:2px}
#jump{position:fixed;right:16px;bottom:16px;z-index:15;pointer-events:auto;width:62px;height:62px;
border-radius:50%;font-size:24px;line-height:1;padding:0;display:none}
#fatal{position:fixed;inset:0;display:none;place-items:center;z-index:50;padding:22px;background:#120f30}
#fatal.show{display:grid}
@media (max-width:720px){
#hud{padding:9px}
.panel{padding:7px 10px;border-radius:12px}
#hearts{font-size:18px}
.stat b{font-size:16px}
.stats{gap:11px}
#hint{font-size:11px;line-height:1.5}
#jump{display:block}
}
@media (max-height:520px){ #hint{display:none} }
</style>
</head>
<body>
<div id="stage"></div>
<div id="hud">
<div class="hrow">
<div class="panel">
<div class="cap">Vitality</div>
<div id="hearts"></div>
</div>
<div class="panel">
<div class="stats">
<div class="stat"><div class="cap">Crystals</div><b id="crystals">0/0</b></div>
<div class="stat"><div class="cap">Score</div><b id="score">0</b></div>
<div class="stat"><div class="cap">Sky</div><b id="phase">Day</b></div>
</div>
</div>
</div>
<div id="toast"></div>
<div class="hrow bottom">
<div class="panel" id="hint">
<b>Move</b> <kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> ·
<b>Jump</b> <kbd>Space</kbd> ·
<b>Look</b> drag mouse · <b>Zoom</b> scroll ·
<b>Pause</b> <kbd>P</kbd> · <b>Restart</b> <kbd>R</kbd>
<br>Collect every crystal on the floating islands — dodge the elemental turrets!
</div>
</div>
</div>
<div id="flash"></div>
<button id="jump" aria-label="Jump">▲</button>
<div id="overlay">
<div class="card">
<h1 id="ovTitle">Paused</h1>
<p id="ovText"></p>
<button id="restart">Resume</button>
</div>
</div>
<div id="fatal">
<div class="card">
<h1>Unable to start</h1>
<p id="fatalMsg"></p>
</div>
</div>
<script type="module">
/* ============================ boot & failure handling ============================ */
const CDN = 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
const $ = id => document.getElementById(id);
const clamp = (v,a,b) => v<a?a:(v>b?b:v);
const lerp = (a,b,t) => a+(b-a)*t;
const angLerp = (a,b,t) => { let d = ((b-a+Math.PI)%(Math.PI*2)+Math.PI*2)%(Math.PI*2)-Math.PI; return a+d*t; };
function fatal(msg){ $('fatalMsg').textContent = msg; $('fatal').classList.add('show'); }
let THREE;
try {
THREE = await Promise.race([
import(CDN),
new Promise((_,rej)=>setTimeout(()=>rej(new Error('cdn timeout')), 15000))
]);
} catch (err) {
fatal('Three.js r170 could not be downloaded from cdn.jsdelivr.net. Check your internet connection, proxy or firewall, then reload this page.');
throw err;
}
let renderer;
try {
renderer = new THREE.WebGLRenderer({ antialias:true, powerPreference:'high-performance' });
} catch (err) {
fatal('This browser could not create a WebGL 2 context. Enable hardware acceleration or open the page in Chrome, Edge, Firefox or Safari 15+.');
throw err;
}
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.12;
renderer.outputColorSpace = THREE.SRGBColorSpace;
$('stage').appendChild(renderer.domElement);
/* ============================ scene, camera, lights ============================ */
const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0xdff1ff, 95, 330);
const camera = new THREE.PerspectiveCamera(58, innerWidth/innerHeight, 0.1, 700);
const hemi = new THREE.HemisphereLight(0xe2f3ff, 0x7d6aa4, 0.9); scene.add(hemi);
const sun = new THREE.DirectionalLight(0xfff3d6, 1.5);
sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);
Object.assign(sun.shadow.camera, { left:-72, right:72, top:72, bottom:-72, near:1, far:300 });
sun.shadow.camera.updateProjectionMatrix();
sun.shadow.bias = -0.0007;
scene.add(sun); scene.add(sun.target);
/* ============================ textures & helpers ============================ */
const glowTex = (() => {
const c = document.createElement('canvas'); c.width = c.height = 64;
const g = c.getContext('2d');
const grd = g.createRadialGradient(32,32,0,32,32,32);
grd.addColorStop(0,'rgba(255,255,255,1)'); grd.addColorStop(.28,'rgba(255,255,255,.55)');
grd.addColorStop(1,'rgba(255,255,255,0)');
g.fillStyle = grd; g.fillRect(0,0,64,64);
const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace; return t;
})();
const std = (color, extra) => new THREE.MeshStandardMaterial(
Object.assign({ color, flatShading:true, roughness:.78, metalness:.05 }, extra || {}));
const stdColor = new THREE.Color();
/* sky dome + stars */
const skyU = { top:{ value:new THREE.Color(0x6fc6f5) }, bot:{ value:new THREE.Color(0xdff1ff) } };
const skyDome = new THREE.Mesh(new THREE.SphereGeometry(340, 26, 18), new THREE.ShaderMaterial({
side:THREE.BackSide, depthWrite:false, uniforms:skyU,
vertexShader:'varying vec3 vP;void main(){vP=position;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}',
fragmentShader:'uniform vec3 top;uniform vec3 bot;varying vec3 vP;void main(){float h=normalize(vP).y*0.5+0.5;gl_FragColor=vec4(mix(bot,top,pow(h,0.85)),1.0);}'
}));
scene.add(skyDome);
const starGeo = new THREE.BufferGeometry();
const starPos = new Float32Array(600*3);
for (let i=0;i<600;i++){
const v = new THREE.Vector3().randomDirection().multiplyScalar(300);
if (v.y < 0) v.y = -v.y;
starPos[i*3]=v.x; starPos[i*3+1]=v.y; starPos[i*3+2]=v.z;
}
starGeo.setAttribute('position', new THREE.BufferAttribute(starPos,3));
const starMat = new THREE.PointsMaterial({ color:0xffffff, size:2.4, sizeAttenuation:false, transparent:true, opacity:0, depthWrite:false, map:glowTex });
const stars = new THREE.Points(starGeo, starMat); scene.add(stars);
const moon = new THREE.Mesh(new THREE.SphereGeometry(7,14,10), new THREE.MeshBasicMaterial({ color:0xfff6e0, transparent:true, opacity:0 }));
scene.add(moon);
/* clouds + distant islets */
const clouds = [];
const cloudMat = new THREE.MeshStandardMaterial({ color:0xffffff, flatShading:true, transparent:true, opacity:.92, roughness:1 });
for (let i=0;i<16;i++){
const g = new THREE.Group();
const n = 3 + (Math.random()*3|0);
for (let j=0;j<n;j++){
const m = new THREE.Mesh(new THREE.IcosahedronGeometry(2.2+Math.random()*2.4,0), cloudMat);
m.position.set((Math.random()-.5)*6, (Math.random()-.5)*1.4, (Math.random()-.5)*4.5);
m.scale.y = .55; g.add(m);
}
const a = Math.random()*Math.PI*2, r = 55 + Math.random()*95;
g.position.set(Math.cos(a)*r, 16+Math.random()*26, Math.sin(a)*r - 28);
g.userData = { a, r, s:(Math.random()*.35+.22)*(Math.random()<.5?-1:1) };
scene.add(g); clouds.push(g);
}
for (let i=0;i<12;i++){
const a = Math.random()*Math.PI*2, r = 80+Math.random()*110;
const m = new THREE.Mesh(new THREE.ConeGeometry(4+Math.random()*6, 9+Math.random()*10, 7), std(0xb9a6e8));
m.position.set(Math.cos(a)*r, -14+Math.random()*34, Math.sin(a)*r-28);
m.rotation.x = Math.PI; m.rotation.y = Math.random()*3;
scene.add(m);
}
/* ============================ particle system ============================ */
const FX = {
max:900, i:0,
init(scene){
this.pos=new Float32Array(this.max*3); this.col=new Float32Array(this.max*3);
this.siz=new Float32Array(this.max); this.alp=new Float32Array(this.max);
this.vel=new Float32Array(this.max*3); this.life=new Float32Array(this.max); this.mlife=new Float32Array(this.max);
this.geo = new THREE.BufferGeometry();
this.geo.setAttribute('position', new THREE.BufferAttribute(this.pos,3));
this.geo.setAttribute('pcolor', new THREE.BufferAttribute(this.col,3));
this.geo.setAttribute('psize', new THREE.BufferAttribute(this.siz,1));
this.geo.setAttribute('palpha', new THREE.BufferAttribute(this.alp,1));
this.mat = new THREE.ShaderMaterial({
transparent:true, depthWrite:false, blending:THREE.AdditiveBlending,
vertexShader:'attribute vec3 pcolor;attribute float psize;attribute float palpha;varying vec3 vC;varying float vA;void main(){vC=pcolor;vA=palpha;vec4 mv=modelViewMatrix*vec4(position,1.0);gl_PointSize=psize*(330.0/max(0.001,-mv.z));gl_Position=projectionMatrix*mv;}',
fragmentShader:'varying vec3 vC;varying float vA;void main(){vec2 d=gl_PointCoord-vec2(0.5);float r=length(d);if(r>0.5)discard;float a=(1.0-smoothstep(0.04,0.5,r))*vA;gl_FragColor=vec4(vC*a,a);}'
});
this.points = new THREE.Points(this.geo, this.mat);
this.points.frustumCulled = false;
scene.add(this.points);
},
emit(p, color, count, speed, life, size){
stdColor.set(color);
for (let k=0;k<count;k++){
const i = this.i; this.i = (this.i+1) % this.max;
this.pos[i*3]=p.x; this.pos[i*3+1]=p.y; this.pos[i*3+2]=p.z;
const th = Math.random()*Math.PI*2, ph = Math.acos(2*Math.random()-1), sp = speed*(0.35+Math.random()*0.9);
this.vel[i*3]=Math.sin(ph)*Math.cos(th)*sp;
this.vel[i*3+1]=Math.abs(Math.cos(ph))*sp*.95 + 1.2;
this.vel[i*3+2]=Math.sin(ph)*Math.sin(th)*sp;
this.col[i*3]=stdColor.r; this.col[i*3+1]=stdColor.g; this.col[i*3+2]=stdColor.b;
this.siz[i]=size*(0.6+Math.random()*0.85);
this.life[i]=this.mlife[i]=life*(0.7+Math.random()*0.6);
this.alp[i]=1;
}
},
update(dt){
for (let i=0;i<this.max;i++){
if (this.life[i] <= 0){ if (this.alp[i] !== 0){ this.alp[i]=0; this.siz[i]=0; } continue; }
this.life[i]-=dt;
this.alp[i]=Math.max(0,this.life[i]/this.mlife[i]);
this.vel[i*3+1]-=7*dt;
this.pos[i*3]+=this.vel[i*3]*dt;
this.pos[i*3+1]+=this.vel[i*3+1]*dt;
this.pos[i*3+2]+=this.vel[i*3+2]*dt;
}
this.geo.attributes.position.needsUpdate=true;
this.geo.attributes.pcolor.needsUpdate=true;
this.geo.attributes.psize.needsUpdate=true;
this.geo.attributes.palpha.needsUpdate=true;
}
};
FX.init(scene);
/* ============================ world layout ============================ */
const platforms = [], crystals = [], turrets = [], projectiles = [];
const CRYSTAL_COLORS = [0x7ff0ff, 0xffa8e6, 0xfff07f, 0xa8ffb8, 0xc4a8ff, 0xffc08a];
const ELEMENTS = { fire:{c:0xff7a3d,name:'Fire'}, ice:{c:0x74e6ff,name:'Ice'}, storm:{c:0xc58bff,name:'Storm'} };
const ISLANDS = [
{ x:0, y:0, z:0, r:10, grass:0x9be8a8, rock:0xc9b6ef, seg:10,
items:[{t:'crystal',ox:-5.5,oz:3.5},{t:'crystal',ox:5.5,oz:-3}] },
{ x:26, y:2.5, z:-6, r:6.5, grass:0xffe2a8, rock:0xf0b7c9, seg:9,
items:[{t:'crystal',ox:1,oz:-1.5},{t:'turret',ox:-2.6,oz:2.4,el:'fire'}] },
{ x:44, y:5, z:-22, r:6, grass:0xa8e6ff, rock:0xb9c9ef, seg:9,
items:[{t:'crystal',ox:-1,oz:1.5},{t:'turret',ox:2.4,oz:-1.6,el:'ice'}] },
{ x:34, y:8, z:-46, r:7, grass:0xffc6e0, rock:0xe0b6ef, seg:10,
items:[{t:'crystal',ox:2,oz:-2},{t:'turret',ox:-2.6,oz:2,el:'storm'}] },
{ x:8, y:6.5, z:-56, r:6.5, grass:0xc6f5d0, rock:0xa9d9ef, seg:9,
items:[{t:'crystal',ox:0,oz:1.6},{t:'turret',ox:-2.5,oz:-1.6,el:'fire'}] },
{ x:-16, y:3, z:-44, r:6, grass:0xffeeb0, rock:0xe8c0a8, seg:9,
items:[{t:'crystal',ox:1.6,oz:1}] },
{ x:-28, y:0.5, z:-20, r:7, grass:0xd8c6ff, rock:0xa8e0d4, seg:9,
items:[{t:'crystal',ox:0,oz:1},{t:'turret',ox:-3,oz:1.4,el:'ice'}] }
];
function addCrystal(x,y,z,color){
const outer = new THREE.Group(); outer.position.set(x,y,z);
const inner = new THREE.Group(); outer.add(inner);
const core = new THREE.Mesh(new THREE.OctahedronGeometry(.5,0),
new THREE.MeshStandardMaterial({ color, emissive:color, emissiveIntensity:1.2, flatShading:true, transparent:true, opacity:.93, roughness:.2, metalness:.25 }));
inner.add(core);
const halo = new THREE.Sprite(new THREE.SpriteMaterial({ map:glowTex, color, transparent:true, blending:THREE.AdditiveBlending, depthWrite:false, opacity:.8 }));
halo.scale.set(3.4,3.4,1); inner.add(halo);
scene.add(outer);
crystals.push({ outer, inner, core, halo, pos:new THREE.Vector3(x,y,z), taken:false, ph:Math.random()*6.28 });
}
function addTurret(x,y,z,key){
const e = ELEMENTS[key];
const g = new THREE.Group(); g.position.set(x,y,z);
const base = new THREE.Mesh(new THREE.CylinderGeometry(.9,1.15,.5,8), std(0x9a8fd0)); base.position.y=.25; base.castShadow=true; g.add(base);
const post = new THREE.Mesh(new THREE.CylinderGeometry(.26,.34,1,8), std(0x6f6496)); post.position.y=.9; post.castShadow=true; g.add(post);
const head = new THREE.Group(); head.position.y=1.55; g.add(head);
const body = new THREE.Mesh(new THREE.BoxGeometry(.95,.8,.85), std(0xded8f7)); body.castShadow=true; head.add(body);
const barrel = new THREE.Mesh(new THREE.CylinderGeometry(.15,.19,1.05,8), std(0x5a5182));
barrel.rotation.x = Math.PI/2; barrel.position.set(0,0,.72); head.add(barrel);
const core = new THREE.Mesh(new THREE.OctahedronGeometry(.28),
new THREE.MeshStandardMaterial({ color:e.c, emissive:e.c, emissiveIntensity:2, flatShading:true }));
core.position.set(0,.12,.05); head.add(core);
const halo = new THREE.Sprite(new THREE.SpriteMaterial({ map:glowTex, color:e.c, transparent:true, blending:THREE.AdditiveBlending, depthWrite:false, opacity:.6 }));
halo.scale.set(1.8,1.8,1); halo.position.copy(core.position); head.add(halo);
scene.add(g);
turrets.push({ group:g, head, core, halo, pos:new THREE.Vector3(x,y+1.55,z), color:e.c, name:e.name, cooldown:1+Math.random()*2, charge:0, angle:0 });
}
function buildIsland(d){
const g = new THREE.Group(); g.position.set(d.x,0,d.z);
const seg = d.seg, r = d.r, y = d.y, bodyH = 3.6;
const body = new THREE.Mesh(new THREE.CylinderGeometry(r*.94, r*.5, bodyH, seg), std(d.rock));
body.position.y = y-bodyH/2; body.castShadow=true; body.receiveShadow=true; g.add(body);
const spike = new THREE.Mesh(new THREE.ConeGeometry(r*.5, 6, seg), std(d.rock));
spike.position.y = y-bodyH-2.8; spike.rotation.x = Math.PI; spike.castShadow=true; g.add(spike);
const cap = new THREE.Mesh(new THREE.CylinderGeometry(r, r*.93, .5, seg), std(d.grass));
cap.position.y = y-.25; cap.receiveShadow=true; cap.castShadow=true; g.add(cap);
const rim = new THREE.Mesh(new THREE.TorusGeometry(r*.99, .13, 5, seg*2), std(d.grass,{roughness:.6}));
rim.rotation.x = Math.PI/2; rim.position.y = y-.08; g.add(rim);
const busy = d.items.map(i => ({x:i.ox, z:i.oz}));
const clear = (tx,tz,rad) => !busy.some(s => Math.hypot(s.x-tx, s.z-tz) < rad);
const trees = Math.max(2, Math.round(r/2.4));
for (let i=0;i<trees;i++){
const a = Math.random()*Math.PI*2, rad = r*(.35+Math.random()*.55);
const tx = Math.cos(a)*rad, tz = Math.sin(a)*rad;
if (!clear(tx,tz,2.8)) continue;
const h = 1.7+Math.random()*1.5;
const trunk = new THREE.Mesh(new THREE.CylinderGeometry(.13,.18,h*.5,6), std(0xb98a63));
trunk.position.set(tx, y+h*.25, tz); trunk.castShadow=true; g.add(trunk);
const leaves = new THREE.Mesh(new THREE.ConeGeometry(h*.42, h, 7), std(i%2 ? 0x7fd8a0 : 0xa4ecae));
leaves.position.set(tx, y+h*.75, tz); leaves.castShadow=true; g.add(leaves);
}
for (let i=0;i<4;i++){
const a = Math.random()*Math.PI*2, rad = r*(.2+Math.random()*.6);
const tx = Math.cos(a)*rad, tz = Math.sin(a)*rad;
if (!clear(tx,tz,2.1)) continue;
const rock = new THREE.Mesh(new THREE.IcosahedronGeometry(.3+Math.random()*.32,0), std(d.rock));
rock.position.set(tx, y+.2, tz); rock.castShadow=true; g.add(rock);
}
scene.add(g);
platforms.push({ x:d.x, z:d.z, r, y, kind:'island' });
for (const it of d.items){
if (it.t === 'crystal') addCrystal(d.x+it.ox, y+1.6, d.z+it.oz, CRYSTAL_COLORS[crystals.length % CRYSTAL_COLORS.length]);
else addTurret(d.x+it.ox, y, d.z+it.oz, it.el);
}
}
ISLANDS.forEach(buildIsland);
/* stepping stones linking every island so each crystal is reachable */
const stoneSpots = [];
for (let i=0;i<ISLANDS.length;i++){
const a = ISLANDS[i], b = ISLANDS[(i+1) % ISLANDS.length];
const dx = b.x-a.x, dz = b.z-a.z, len = Math.hypot(dx,dz);
const t0 = a.r/len, t1 = 1 - b.r/len, gap = len - a.r - b.r;
const n = clamp(Math.round(gap/4.5), 2, 6);
const mid = Math.floor(n/2);
for (let s=0;s<n;s++){
const t = t0 + (s+1)/(n+1)*(t1-t0);
const px = a.x+dx*t, pz = a.z+dz*t;
const py = a.y + (b.y-a.y)*t + Math.sin(t*Math.PI)*1.2 - .35;
const g = new THREE.Group(); g.position.set(px,py,pz);
const slab = new THREE.Mesh(new THREE.CylinderGeometry(2, 1.5, .7, 6), std(0xf3e6ff));
slab.position.y = -.35; slab.castShadow=true; slab.receiveShadow=true; g.add(slab);
const under = new THREE.Mesh(new THREE.ConeGeometry(1.5, 2.6, 6), std(0xc0a8e8));
under.position.y = -2; under.rotation.x = Math.PI; under.castShadow=true; g.add(under);
scene.add(g);
platforms.push({ x:px, z:pz, r:1.85, y:py, kind:'stone' });
if (s === mid && (i === 2 || i === 5)) stoneSpots.push({ x:px, y:py, z:pz });
}
}
stoneSpots.forEach(s => addCrystal(s.x, s.y+1.6, s.z, CRYSTAL_COLORS[crystals.length % CRYSTAL_COLORS.length]));
const TOTAL_CRYSTALS = crystals.length;
/* ============================ the wizard ============================ */
function buildWizard(){
const g = new THREE.Group();
const robeM = std(0x8f6fe8), skinM = std(0xffdcc0), hatM = std(0x6f4fd0), trimM = std(0xffd166);
const robe = new THREE.Mesh(new THREE.CylinderGeometry(.24,.66,1.35,9), robeM); robe.position.y=.68; robe.castShadow=true; g.add(robe);
const sash = new THREE.Mesh(new THREE.TorusGeometry(.42,.07,6,12), trimM); sash.rotation.x=Math.PI/2; sash.position.y=1.0; g.add(sash);
const torso = new THREE.Mesh(new THREE.SphereGeometry(.33,9,7), robeM); torso.position.y=1.5; torso.castShadow=true; g.add(torso);
const head = new THREE.Mesh(new THREE.SphereGeometry(.27,11,9), skinM); head.position.y=1.95; head.castShadow=true; g.add(head);
const beard = new THREE.Mesh(new THREE.ConeGeometry(.26,.5,7), std(0xfff6e5)); beard.position.set(0,1.73,.13); beard.rotation.x=.28; g.add(beard);
const brim = new THREE.Mesh(new THREE.CylinderGeometry(.66,.66,.07,12), hatM); brim.position.y=2.14; brim.castShadow=true; g.add(brim);
const hat = new THREE.Mesh(new THREE.ConeGeometry(.44,1.3,10), hatM); hat.position.set(.1,2.72,-.02); hat.rotation.z=-.2; hat.castShadow=true; g.add(hat);
const tip = new THREE.Mesh(new THREE.SphereGeometry(.1,7,6), trimM); tip.position.set(.23,3.35,0); g.add(tip);
for (const s of [-1,1]){
const e = new THREE.Mesh(new THREE.SphereGeometry(.05,6,5), std(0x2b2150)); e.position.set(s*.1,1.99,.245); g.add(e);
}
const arms = [];
for (const s of [-1,1]){
const a = new THREE.Mesh(new THREE.CylinderGeometry(.09,.11,.72,6), robeM);
a.position.set(s*.38,1.3,.06); a.rotation.z = s*.25; a.castShadow=true; g.add(a); arms.push(a);
}
const legs = [];
for (const s of [-1,1]){
const l = new THREE.Mesh(new THREE.CylinderGeometry(.11,.09,.32,6), std(0x5b3fb0)); l.position.set(s*.16,.16,0); g.add(l); legs.push(l);
}
const staff = new THREE.Group(); staff.position.set(.62,0,.05); staff.rotation.z=-.12;
const pole = new THREE.Mesh(new THREE.CylinderGeometry(.045,.05,2.1,6), std(0x8a6a4a)); pole.position.y=1.05; pole.castShadow=true; staff.add(pole);
const orb = new THREE.Mesh(new THREE.IcosahedronGeometry(.17,0),
new THREE.MeshStandardMaterial({ color:0x8ef7ff, emissive:0x8ef7ff, emissiveIntensity:1.6, flatShading:true }));
orb.position.y=2.2; staff.add(orb);
const halo = new THREE.Sprite(new THREE.SpriteMaterial({ map:glowTex, color:0x8ef7ff, transparent:true, blending:THREE.AdditiveBlending, depthWrite:false, opacity:.85 }));
halo.scale.set(1.5,1.5,1); halo.position.y=2.2; staff.add(halo);
g.add(staff);
g.userData = { legs, arms, orb, halo, robe };
return g;
}
const wizard = buildWizard();
scene.add(wizard);
/* ============================ audio (user-gesture only) ============================ */
const SFX = {
ctx:null, master:null,
init(){
if (!this.ctx){
const AC = window.AudioContext || window.webkitAudioContext;
if (!AC) return;
this.ctx = new AC();
this.master = this.ctx.createGain();
this.master.gain.value = .45;
this.master.connect(this.ctx.destination);
}
if (this.ctx.state === 'suspended') this.ctx.resume();
},
tone(f, dur, type='sine', vol=.25, slide=0){
if (!this.ctx) return;
const t = this.ctx.currentTime, o = this.ctx.createOscillator(), g = this.ctx.createGain();
o.type = type; o.frequency.setValueAtTime(f, t);
if (slide) o.frequency.exponentialRampToValueAtTime(Math.max(40, f+slide), t+dur);
g.gain.setValueAtTime(.0001, t);
g.gain.exponentialRampToValueAtTime(vol, t+.012);
g.gain.exponentialRampToValueAtTime(.0001, t+dur);
o.connect(g); g.connect(this.master); o.start(t); o.stop(t+dur+.02);
},
jump(){ this.tone(360,.18,'triangle',.22,340); },
collect(){ [660,880,1180].forEach((f,i)=>setTimeout(()=>this.tone(f,.22,'sine',.2), i*70)); },
hurt(){ this.tone(210,.32,'sawtooth',.22,-130); this.tone(105,.42,'square',.1,-40); },
shoot(){ this.tone(760,.11,'square',.06,-470); },
win(){ [523,659,784,1046,1318].forEach((f,i)=>setTimeout(()=>this.tone(f,.35,'triangle',.22), i*120)); },
lose(){ [440,330,220,150].forEach((f,i)=>setTimeout(()=>this.tone(f,.42,'sawtooth',.18), i*150)); }
};
const kickAudio = () => { SFX.init(); window.removeEventListener('pointerdown', kickAudio); window.removeEventListener('keydown', kickAudio); };
window.addEventListener('pointerdown', kickAudio);
window.addEventListener('keydown', kickAudio);
/* ============================ game state ============================ */
const MAXHP = 5;
const state = { health:MAXHP, score:0, got:0, clock:12, elapsed:0, paused:false, over:false, won:false, invuln:0, phase:'' };
const player = { pos:new THREE.Vector3(0,0,0), vel:new THREE.Vector3(), facing:0, grounded:false, coyote:0 };
let lastSafe = { x:0, z:0, y:0, r:10 };
const focus = new THREE.Vector3(0,2,0);
let camYaw = 2.8, camPitch = .34, camDist = 15, jumpQueued = false, walkPhase = 0;
const keys = Object.create(null);
const setText = (el, v) => { if (el.textContent !== v) el.textContent = v; };
let toastTimer = 0;
function toast(msg){ const t = $('toast'); t.textContent = msg; t.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(()=>t.classList.remove('show'), 1500); }
function flash(){ const f = $('flash'); f.style.opacity = 1; setTimeout(()=>{ f.style.opacity = 0; }, 90); }
function showOverlay(title, text, btn){ $('ovTitle').textContent = title; $('ovText').textContent = text; $('restart').textContent = btn; $('overlay').classList.add('show'); }
function hideOverlay(){ $('overlay').classList.remove('show'); }
let lastHP = -1, lastGot = -1, lastScore = -1;
function updateHUD(){
if (state.health !== lastHP){ lastHP = state.health; const h = $('hearts'); h.innerHTML = ''; for (let i=0;i<MAXHP;i++){ const s = document.createElement('span'); s.className = 'heart' + (i < state.health ? '' : ' off'); s.textContent = '♥'; h.appendChild(s); } }
setText($('crystals'), state.got + '/' + TOTAL_CRYSTALS);
setText($('score'), String(state.score));
setText($('phase'), state.phase);
}
/* ============================ input ============================ */
addEventListener('keydown', e => {
keys[e.code] = true;
if (e.code === 'Space'){ jumpQueued = true; e.preventDefault(); }
if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) e.preventDefault();
if (e.code === 'KeyP') togglePause();
if (e.code === 'KeyR') resetGame();
});
addEventListener('keyup', e => { keys[e.code] = false; });
const canvas = renderer.domElement;
let dragging = false, lastX = 0, lastY = 0;
canvas.addEventListener('pointerdown', e => { dragging = true; lastX = e.clientX; lastY = e.clientY; canvas.setPointerCapture(e.pointerId); });
canvas.addEventListener('pointermove', e => {
if (!dragging) return;
const dx = e.clientX - lastX, dy = e.clientY - lastY;
lastX = e.clientX; lastY = e.clientY;
camYaw -= dx * .006;
camPitch = clamp(camPitch + dy * .005, .12, 1.25);
});
canvas.addEventListener('pointerup', () => { dragging = false; });
canvas.addEventListener('pointercancel', () => { dragging = false; });
canvas.addEventListener('wheel', e => { camDist = clamp(camDist + e.deltaY * .02, 7, 30); e.preventDefault(); }, { passive:false });
$('jump').addEventListener('pointerdown', e => { e.preventDefault(); jumpQueued = true; });
$('restart').addEventListener('click', () => { SFX.init(); if (state.paused) setPaused(false); else resetGame(); });
addEventListener('resize', () => {
camera.aspect = innerWidth/innerHeight; camera.updateProjectionMatrix();
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setSize(innerWidth, innerHeight);
});
/* pause when focus is lost */
function setPaused(p){
if (state.over || state.won) return;
state.paused = p;
if (p){ showOverlay('Paused', 'The islands wait for you. Take a breath.', 'Resume'); }
else { hideOverlay(); }
}
function togglePause(){ setPaused(!state.paused); }
addEventListener('blur', () => setPaused(true));
document.addEventListener('visibilitychange', () => { if (document.hidden) setPaused(true); });
/* ============================ gameplay ============================ */
function collectCrystal(c){
c.taken = true; c.outer.visible = false;
state.got++; state.score += 150;
FX.emit(c.pos, c.core.material.color.getHex(), 34, 6, .85, .7);
SFX.collect(); toast('Crystal ' + state.got + ' of ' + TOTAL_CRYSTALS + '!');
updateHUD();
if (state.got >= TOTAL_CRYSTALS) win();
}
function win(){
state.won = true; state.score += state.health * 100;
SFX.win();
const p = new THREE.Vector3(player.pos.x, player.pos.y+2, player.pos.z);
for (let i=0;i<6;i++) FX.emit(p, [0xff7ab8,0x7ff0ff,0xfff07f,0xa8ffb8,0xc4a8ff,0xffc08a][i], 40, 9, 1.4, .8);
showOverlay('Victory!', 'All ' + TOTAL_CRYSTALS + ' crystals claimed in ' + Math.round(state.elapsed) + 's with ' + state.health + ' vitality left. Final score: ' + state.score, 'Play again');
updateHUD();
}
function gameOver(){
state.over = true; SFX.lose();
showOverlay('Defeated', 'Your vitality faded on the islands. The crystals still glow — try again!', 'Try again');
}
function hurt(src){
if (state.invuln > 0 || state.over || state.won) return;
state.health--; state.invuln = 1.5; SFX.hurt(); flash();
const d = new THREE.Vector3(player.pos.x - src.x, 0, player.pos.z - src.z);
if (d.lengthSq() < .001) d.set(1,0,0);
d.normalize();
player.vel.x += d.x * 8; player.vel.z += d.z * 8; player.vy = Math.max(player.vy, 5.5);
FX.emit(new THREE.Vector3(player.pos.x, player.pos.y+1.2, player.pos.z), 0xff5d7a, 24, 7, .75, .6);
toast('Ouch! −1 vitality');
if (state.health <= 0){ state.health = 0; gameOver(); }
updateHUD();
}
function fireTurret(t){
const pc = new THREE.Vector3(player.pos.x, player.pos.y + 1.25, player.pos.z);
const muzzle = t.head.getWorldPosition(new THREE.Vector3());
const dir = pc.clone().sub(muzzle).normalize();
const mesh = new THREE.Mesh(new THREE.IcosahedronGeometry(.26,0), new THREE.MeshBasicMaterial({ color:t.color }));
mesh.position.copy(muzzle).add(dir.clone().multiplyScalar(.9));
scene.add(mesh);
projectiles.push({ mesh, vel:dir.multiplyScalar(18), life:4 });
FX.emit(mesh.position, t.color, 10, 4, .5, .55);
SFX.shoot();
}
function resetGame(){
state.health = MAXHP; state.score = 0; state.got = 0; state.clock = 12; state.elapsed = 0;
state.over = false; state.won = false; state.invuln = 1; state.paused = false;
crystals.forEach(c => { c.taken = false; c.outer.visible = true; });
turrets.forEach(t => { t.cooldown = 1.2 + Math.random()*2; t.charge = 0; });
projectiles.forEach(p => scene.remove(p.mesh)); projectiles.length = 0;
player.pos.set(0, .05, 0); player.vel.set(0,0,0); player.facing = 0;
lastSafe = { x:0, z:0, y:0, r:10 };
focus.set(0,2,0); camera.position.set(0,7,-14);
hideOverlay(); updateHUD(); toast('Collect every crystal!');
}
/* ============================ simulation ============================ */
const GRAVITY = 26, SPEED = 9.2, JUMP = 13;
const tmp = new THREE.Vector3();
function update(dt){
state.clock += dt;
if (!state.over && !state.won) state.elapsed += dt;
state.invuln = Math.max(0, state.invuln - dt);
/* --- movement (camera relative, smoothed) --- */
let ix = 0, iz = 0;
if (keys['KeyW'] || keys['ArrowUp']) iz += 1;
if (keys['KeyS'] || keys['ArrowDown']) iz -= 1;
if (keys['KeyA'] || keys['ArrowLeft']) ix -= 1;
if (keys['KeyD'] || keys['ArrowRight']) ix += 1;
const l = Math.hypot(ix,iz) || 1; ix /= l; iz /= l;
const fwd = { x:Math.sin(camYaw), z:Math.cos(camYaw) };
const rgt = { x:-fwd.z, z:fwd.x };
const tx = (fwd.x*iz + rgt.x*ix) * SPEED;
const tz = (fwd.z*iz + rgt.z*ix) * SPEED;
const accel = 1 - Math.pow(.0001, dt);
player.vel.x += (tx - player.vel.x) * accel;
player.vel.z += (tz - player.vel.z) * accel;
if (jumpQueued){
jumpQueued = false;
if (player.grounded || player.coyote > 0){
player.vy = JUMP; player.grounded = false; player.coyote = 0;
FX.emit(new THREE.Vector3(player.pos.x, player.pos.y+.1, player.pos.z), 0xffffff, 12, 3.4, .45, .45);
SFX.jump();
}
}
/* --- integrate & collide with platforms --- */
const prevY = player.pos.y;
player.vy -= GRAVITY * dt;
player.pos.x += player.vel.x * dt;
player.pos.z += player.vel.z * dt;
player.pos.y += player.vy * dt;
player.grounded = false;
for (const p of platforms){
const dx = player.pos.x - p.x, dz = player.pos.z - p.z;
if (dx*dx + dz*dz > (p.r - .1) * (p.r - .1)) continue;
if (player.vy <= 0 && prevY >= p.y - .02 && player.pos.y <= p.y){
player.pos.y = p.y; player.vy = 0; player.grounded = true;
lastSafe = { x:p.x, z:p.z, y:p.y, r:p.r };
}
}
if (player.grounded) player.coyote = .12; else player.coyote = Math.max(0, player.coyote - dt);
/* --- fall recovery --- */
if (player.pos.y < -35){
player.pos.set(lastSafe.x, lastSafe.y + .05, lastSafe.z);
player.vel.set(0,0,0); player.vy = 0; state.invuln = 1;
FX.emit(player.pos, 0x9be8a8, 20, 5, .7, .6);
toast('Back on solid ground');
}
/* --- wizard animation --- */
const spd = Math.hypot(player.vel.x, player.vel.z);
if (spd > .4) player.facing = angLerp(player.facing, Math.atan2(player.vel.x, player.vel.z), 1 - Math.pow(.0005, dt));
wizard.position.copy(player.pos);
wizard.rotation.y = player.facing;
const u = wizard.userData;
walkPhase += spd * dt * 2.4;
const sw = Math.sin(walkPhase) * Math.min(1, spd/7);
u.legs[0].rotation.x = sw*.85; u.legs[1].rotation.x = -sw*.85;
u.arms[0].rotation.x = -sw*.6; u.arms[1].rotation.x = sw*.6;
u.robe.position.y = .68 + Math.abs(Math.sin(walkPhase))*.05;
u.orb.rotation.y += dt*2.2; u.orb.rotation.x += dt*1.4;
u.halo.scale.setScalar(1.35 + Math.sin(state.clock*4)*.12);
wizard.visible = !(state.invuln > 0 && Math.floor(state.clock*14) % 2 === 0 && state.health< MAXHP);
/* --- crystals --- */
for (const c of crystals){
if (c.taken) continue;
c.inner.rotation.y += dt*1.1;
c.inner.position.y = Math.sin(state.clock*1.6 + c.ph)*.22;
c.core.material.emissiveIntensity = 1.1 + Math.sin(state.clock*3 + c.ph)*.35;
const dx = c.pos.x - player.pos.x, dz = c.pos.z - player.pos.z, dy = c.pos.y - (player.pos.y + 1.2);
if (dx*dx + dz*dz < 2.4*2.4 && Math.abs(dy) < 2.6) collectCrystal(c);
}
/* --- turrets --- */
const pc = new THREE.Vector3(player.pos.x, player.pos.y + 1.25, player.pos.z);
for (const t of turrets){
const dx = pc.x - t.pos.x, dz = pc.z - t.pos.z, dy = pc.y - t.pos.y;
const dist = Math.hypot(dx, dy, dz);
t.cooldown -= dt;
if (dist < 19 && !state.over && !state.won){
t.angle = angLerp(t.angle, Math.atan2(dx, dz), 1 - Math.pow(.02, dt));
t.head.rotation.y = t.angle;
if (t.cooldown <= 0){
t.charge += dt/.8;
if (t.charge >= 1){ t.charge = 0; t.cooldown = 1.7 + Math.random()*1.4; fireTurret(t); }
}
} else {
t.charge = Math.max(0, t.charge - dt);
t.head.rotation.y += dt*.4;
t.angle = t.head.rotation.y;
}
t.core.material.emissiveIntensity = .7 + t.charge*3;
t.halo.scale.setScalar(1.5 + t.charge*2);
t.halo.material.opacity = .45 + t.charge*.5;
}
/* --- projectiles --- */
for (let i = projectiles.length-1; i >= 0; i--){
const p = projectiles[i];
p.life -= dt;
p.mesh.position.addScaledVector(p.vel, dt);
p.mesh.rotation.x += dt*6; p.mesh.rotation.y += dt*5;
if (Math.random() < .55) FX.emit(p.mesh.position, p.mesh.material.color.getHex(), 1, .7, .4, .4);
if (p.mesh.position.distanceTo(pc) < .95){ hurt(p.mesh.position); scene.remove(p.mesh); projectiles.splice(i,1); continue; }
if (p.life <= 0){ scene.remove(p.mesh); projectiles.splice(i,1); }
}
FX.update(dt);
/* --- camera follow + orbit --- */
focus.lerp(tmp.set(player.pos.x, player.pos.y + 2.0, player.pos.z), 1 - Math.pow(.0012, dt));
const off = tmp.set(-Math.sin(camYaw)*Math.cos(camPitch), Math.sin(camPitch), -Math.cos(camYaw)*Math.cos(camPitch)).multiplyScalar(camDist);
camera.position.lerp(tmp.set(focus.x+off.x, focus.y+off.y, focus.z+off.z), 1 - Math.pow(.0015, dt));
if (camera.position.y < focus.y - 3) camera.position.y = focus.y - 3;
camera.lookAt(focus);
skyDome.position.copy(focus); stars.position.copy(focus); moon.position.copy(focus);
/* --- day / night cycle --- */
const DAY = 80, p = (state.clock % DAY) / DAY, a = p*Math.PI*2;
const elev = Math.sin(a);
const daylight = clamp((elev + .12)/.42, 0, 1);
const dusk = clamp(1 - Math.abs(elev)/.4, 0, 1) * .9;
const dayTop = 0x63c4f7, dayBot = 0xe4f4ff, duskTop = 0x7a5fb8, duskBot = 0xffb59a;
const nightTop = 0x080c28, nightBot = 0x2b2c62;
skyU.top.value.setHex(nightTop).lerp(tmpColor.setHex(dayTop), daylight).lerp(tmpColor.setHex(duskTop), dusk*.45);
skyU.bot.value.setHex(nightBot).lerp(tmpColor.setHex(dayBot), daylight).lerp(tmpColor.setHex(duskBot), dusk*.5);
scene.fog.color.copy(skyU.bot.value);
sun.intensity = .12 + daylight*1.5;
sun.color.setHex(0xbcd0ff).lerp(tmpColor.setHex(0xfff3d6), daylight).lerp(tmpColor.setHex(0xffb46a), dusk*.7);
sun.position.set(focus.x + Math.cos(a)*85, Math.max(14, elev*85), focus.z + Math.sin(a)*85);
sun.target.position.copy(focus);
hemi.intensity = .3 + daylight*.65;
hemi.color.setHex(0x6d7fbf).lerp(tmpColor.setHex(0xe2f3ff), daylight);
starMat.opacity = Math.pow(1 - daylight, 1.6);
moon.position.set(focus.x - Math.cos(a)*85, Math.max(14, -elev*85), focus.z - Math.sin(a)*85);
moon.material.opacity = Math.pow(1 - daylight, 2);
state.phase = daylight > .62 ? 'Day' : (dusk > .35 ? 'Dusk' : 'Night');
/* --- ambient motion --- */
for (const c of clouds){
c.userData.a += c.userData.s * dt * .012;
c.position.x = Math.cos(c.userData.a)*c.userData.r;
c.position.z = Math.sin(c.userData.a)*c.userData.r - 28;
}
updateHUD();
}
const tmpColor = new THREE.Color();
/* ============================ loop ============================ */
updateHUD();
let last = performance.now();
function loop(now){
requestAnimationFrame(loop);
const dt = Math.min(.05, (now - last)/1000);
last = now;
if (!state.paused) update(dt);
renderer.render(scene, camera);
}
requestAnimationFrame(loop);
</script>
</body>
</html>
Test 4: Accessible task board
Original task board after deleting every task and verifying an empty reload
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Task Board</title>
<style>
:root {
--bg: #0f172a;
--bg-soft: #131c33;
--panel: #1a2440;
--panel-2: #202c4d;
--line: #2c3a5f;
--text: #e7ecf7;
--muted: #97a3bf;
--accent: #5b8cff;
--accent-2: #7c5bff;
--todo: #f0b429;
--doing: #35c2a0;
--done: #8b95b0;
--danger: #ff6b6b;
--focus: #ffd166;
--radius: 14px;
--shadow: 0 10px 30px rgba(3, 8, 22, 0.45);
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
background:
radial-gradient(1100px 600px at 12% -10%, #1d2a4d 0%, transparent 60%),
radial-gradient(900px 500px at 95% 0%, #2a2150 0%, transparent 55%),
var(--bg);
color: var(--text);
font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
}
.wrap {
max-width: 1180px;
margin: 0 auto;
padding: 28px 20px 56px;
}
header.app {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
margin-bottom: 22px;
}
h1 {
margin: 0 0 4px;
font-size: clamp(1.5rem, 3.2vw, 2rem);
letter-spacing: -0.02em;
}
.sub {
margin: 0;
color: var(--muted);
font-size: 0.92rem;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
label.lbl {
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
font-weight: 600;
}
input[type="text"] {
background: var(--panel);
border: 1px solid var(--line);
color: var(--text);
border-radius: 10px;
padding: 10px 12px;
font: inherit;
min-width: 240px;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
input[type="text"]::placeholder { color: #6d789a; }
input[type="text"]:hover { border-color: #3d4f7d; }
button {
font: inherit;
color: var(--text);
background: var(--panel-2);
border: 1px solid var(--line);
border-radius: 10px;
padding: 10px 14px;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease, transform 0.08s ease;
}
button:hover { background: #27345a; border-color: #3d4f7d; }
button:active { transform: translateY(1px); }
button.primary {
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);
border-color: transparent;
font-weight: 650;
box-shadow: 0 6px 18px rgba(91, 140, 255, 0.32);
}
button.primary:hover { filter: brightness(1.08); background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%); }
button.ghost { background: transparent; }
button.danger:hover {
background: rgba(255, 107, 107, 0.14);
border-color: rgba(255, 107, 107, 0.55);
color: #ffb3b3;
}
:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 2px;
border-radius: 8px;
}
.form-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: flex-end;
background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0));
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 16px 18px;
margin-bottom: 20px;
box-shadow: var(--shadow);
}
.form-row .field { flex: 1 1 320px; }
.form-row input[type="text"] { width: 100%; min-width: 0; }
.hint {
color: var(--muted);
font-size: 0.8rem;
margin: 6px 0 0;
}
.board {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
align-items: start;
}
.column {
background: var(--bg-soft);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 14px;
min-height: 220px;
box-shadow: var(--shadow);
}
.col-head {
display: flex;
align-items: center;
gap: 10px;
padding: 2px 4px 12px;
border-bottom: 1px solid var(--line);
margin-bottom: 12px;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex: 0 0 auto;
}
.col-title {
margin: 0;
font-size: 0.95rem;
font-weight: 700;
letter-spacing: 0.02em;
}
.count {
margin-left: auto;
background: var(--panel-2);
border: 1px solid var(--line);
color: var(--muted);
border-radius: 999px;
padding: 2px 10px;
font-size: 0.78rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.cards { display: flex; flex-direction: column; gap: 10px; }
.card {
background: var(--panel);
border: 1px solid var(--line);
border-left: 4px solid var(--accent);
border-radius: 12px;
padding: 12px 12px 10px;
box-shadow: 0 4px 14px rgba(3, 8, 22, 0.35);
animation: pop 0.18s ease-out;
}
.card[data-status="doing"] { border-left-color: var(--doing); }
.card[data-status="done"] { border-left-color: var(--done); }
.card[data-status="todo"] { border-left-color: var(--todo); }
@keyframes pop {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: none; }
}
.card-title {
margin: 0 0 10px;
font-size: 0.98rem;
font-weight: 600;
overflow-wrap: anywhere;
word-break: break-word;
}
.card-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.card-actions button {
padding: 6px 10px;
font-size: 0.82rem;
border-radius: 8px;
}
.empty {
border: 1px dashed var(--line);
border-radius: 12px;
padding: 22px 14px;
text-align: center;
color: var(--muted);
font-size: 0.88rem;
background: rgba(255, 255, 255, 0.015);
}
.empty strong { color: var(--text); display: block; margin-bottom: 4px; font-size: 0.95rem; }
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
.status-line {
margin: 18px 0 0;
color: var(--muted);
font-size: 0.85rem;
min-height: 1.3em;
}
.board-empty {
grid-column: 1 / -1;
background: var(--bg-soft);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 40px 20px;
text-align: center;
color: var(--muted);
}
.board-empty h2 { margin: 0 0 6px; color: var(--text); font-size: 1.05rem; }
.notice {
margin: 0 0 16px;
padding: 10px 14px;
border-radius: 10px;
background: rgba(255, 209, 102, 0.12);
border: 1px solid rgba(255, 209, 102, 0.45);
color: #ffe1a3;
font-size: 0.86rem;
}
.notice[hidden] { display: none; }
@media (max-width: 860px) {
.board { grid-template-columns: 1fr; }
.column { min-height: 0; }
}
@media (max-width: 560px) {
.wrap { padding: 20px 14px 40px; }
input[type="text"] { min-width: 0; width: 100%; }
.toolbar { width: 100%; }
.toolbar .field { flex: 1 1 100%; }
}
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; transition: none !important; }
}
</style>
</head>
<body>
<div class="wrap">
<header class="app">
<div>
<h1>Task Board</h1>
<p class="sub">A small, keyboard-friendly board. Tasks move forward through To do, Doing, and Done.</p>
</div>
<div class="toolbar">
<div class="field">
<label class="lbl" for="task-search">Search tasks</label>
<input type="text" id="task-search" name="task-search" placeholder="Filter by title…" autocomplete="off" aria-describedby="search-hint" />
</div>
<button type="button" id="reset-demo" class="ghost danger">Reset demo</button>
</div>
</header>
<p class="hint" id="search-hint">Search matches task titles, ignoring case.</p>
<p class="notice" id="storage-notice" hidden></p>
<form class="form-row" id="task-form" novalidate>
<div class="field">
<label class="lbl" for="task-title">Task title</label>
<input type="text" id="task-title" name="task-title" placeholder="e.g. Draft release notes" maxlength="120" autocomplete="off" aria-describedby="title-hint" required />
<p class="hint" id="title-hint">New tasks are added to the To do column.</p>
</div>
<button type="submit" id="add-task" class="primary">Add task</button>
</form>
<main>
<h2 class="sr-only">Board columns</h2>
<div class="board" id="board">
<section class="column" aria-labelledby="col-todo">
<div class="col-head">
<span class="dot" style="background: var(--todo)" aria-hidden="true"></span>
<h3 class="col-title" id="col-todo">To do</h3>
<span class="count" id="count-todo" aria-hidden="true">0</span>
</div>
<div class="cards" id="list-todo"></div>
<p class="empty" id="empty-todo" hidden></p>
</section>
<section class="column" aria-labelledby="col-doing">
<div class="col-head">
<span class="dot" style="background: var(--doing)" aria-hidden="true"></span>
<h3 class="col-title" id="col-doing">Doing</h3>
<span class="count" id="count-doing" aria-hidden="true">0</span>
</div>
<div class="cards" id="list-doing"></div>
<p class="empty" id="empty-doing" hidden></p>
</section>
<section class="column" aria-labelledby="col-done">
<div class="col-head">
<span class="dot" style="background: var(--done)" aria-hidden="true"></span>
<h3 class="col-title" id="col-done">Done</h3>
<span class="count" id="count-done" aria-hidden="true">0</span>
</div>
<div class="cards" id="list-done"></div>
<p class="empty" id="empty-done" hidden></p>
</section>
</div>
</main>
<p class="status-line" id="live-status" role="status" aria-live="polite"></p>
</div>
<script>
(function () {
"use strict";
var STORAGE_KEY = "task-board.v1";
var STATUSES = ["todo", "doing", "done"];
var STATUS_LABEL = { todo: "To do", doing: "Doing", done: "Done" };
var SEARCH_LABEL = { todo: "no tasks in To do", doing: "no tasks in Doing", done: "no tasks in Done" };
var el = {
form: document.getElementById("task-form"),
title: document.getElementById("task-title"),
add: document.getElementById("add-task"),
search: document.getElementById("task-search"),
reset: document.getElementById("reset-demo"),
board: document.getElementById("board"),
live: document.getElementById("live-status"),
notice: document.getElementById("storage-notice"),
lists: {
todo: document.getElementById("list-todo"),
doing: document.getElementById("list-doing"),
done: document.getElementById("list-done")
},
counts: {
todo: document.getElementById("count-todo"),
doing: document.getElementById("count-doing"),
done: document.getElementById("count-done")
},
empties: {
todo: document.getElementById("empty-todo"),
doing: document.getElementById("empty-doing"),
done: document.getElementById("empty-done")
}
};
var storageOk = true;
var memoryStore = null; // fallback: { key: string, value: string }
var tasks = [];
var seq = 0;
function uid() {
seq += 1;
return "t-" + Date.now().toString(36) + "-" + seq.toString(36);
}
function makeTask(title, status) {
return { id: uid(), title: title, status: status, createdAt: Date.now() };
}
function seedTasks() {
seq = 0;
return [
makeTask("Plan launch", "todo"),
makeTask("Build prototype", "doing"),
makeTask("Write brief", "done")
];
}
/* ---------- storage (localStorage with in-memory fallback) ---------- */
function rawGet(key) {
try {
if (memoryStore && memoryStore.key === key) return memoryStore.value;
return window.localStorage.getItem(key);
} catch (err) {
storageOk = false;
return memoryStore ? memoryStore.value : null;
}
}
function rawSet(key, value) {
try {
window.localStorage.setItem(key, value);
memoryStore = null;
return true;
} catch (err) {
storageOk = false;
memoryStore = { key: key, value: value };
return false;
}
}
function rawRemove(key) {
try {
window.localStorage.removeItem(key);
} catch (err) {
storageOk = false;
}
memoryStore = null;
}
function showNotice(text) {
el.notice.textContent = text;
el.notice.hidden = !text;
}
function storageModeMessage() {
if (storageOk) return "";
return "Local storage is unavailable or blocked, so changes are kept in memory for this session only.";
}
function loadTasks() {
var raw = rawGet(STORAGE_KEY);
if (raw === null || raw === undefined) {
tasks = seedTasks();
persist();
return;
}
var parsed;
try {
parsed = JSON.parse(raw);
} catch (err) {
showNotice("Saved tasks could not be read and were reset to the demo data.");
tasks = seedTasks();
persist();
return;
}
if (!parsed || !Array.isArray(parsed.tasks)) {
showNotice("Saved tasks were in an unknown format and were reset to the demo data.");
tasks = seedTasks();
persist();
return;
}
var clean = [];
var seen = Object.create(null);
for (var i = 0; i < parsed.tasks.length; i++) {
var t = parsed.tasks[i];
if (!t || typeof t !== "object") continue;
if (typeof t.title !== "string" || !t.title.trim()) continue;
if (STATUSES.indexOf(t.status) === -1) continue;
var id = typeof t.id === "string" && t.id && !seen[t.id] ? t.id : uid();
seen[id] = true;
clean.push({
id: id,
title: t.title.slice(0, 200),
status: t.status,
createdAt: typeof t.createdAt === "number" ? t.createdAt : Date.now()
});
}
tasks = clean;
}
function persist() {
var ok = rawSet(STORAGE_KEY, JSON.stringify({ version: 1, tasks: tasks }));
if (!ok) {
showNotice(storageModeMessage());
} else {
showNotice("");
}
}
/* ---------- rendering (textContent only, never innerHTML) ---------- */
function matches(task, query) {
if (!query) return true;
return task.title.toLowerCase().indexOf(query) !== -1;
}
function announce(message) {
el.live.textContent = "";
window.setTimeout(function () { el.live.textContent = message; }, 30);
}
function makeCard(task) {
var article = document.createElement("article");
article.className = "card";
article.setAttribute("data-task-id", task.id);
article.setAttribute("data-status", task.status);
article.setAttribute("aria-labelledby", "title-" + task.id);
var h = document.createElement("h4");
h.className = "card-title";
h.id = "title-" + task.id;
h.textContent = task.title;
article.appendChild(h);
var actions = document.createElement("div");
actions.className = "card-actions";
var move = document.createElement("button");
move.type = "button";
move.className = "move";
var isDone = task.status === "done";
move.disabled = isDone;
move.setAttribute("data-action", "move");
move.setAttribute("data-task-id", task.id);
if (isDone) {
move.textContent = "Completed";
move.setAttribute("aria-label", "Task \"" + task.title + "\" is in Done and cannot move forward");
} else {
var next = STATUSES[STATUSES.indexOf(task.status) + 1];
move.textContent = "Move forward";
move.setAttribute("aria-label", "Move task \"" + task.title + "\" forward to " + STATUS_LABEL[next]);
}
actions.appendChild(move);
var del = document.createElement("button");
del.type = "button";
del.className = "danger delete";
del.setAttribute("data-action", "delete");
del.setAttribute("data-task-id", task.id);
del.textContent = "Delete";
del.setAttribute("aria-label", "Delete task \"" + task.title + "\"");
actions.appendChild(del);
article.appendChild(actions);
return article;
}
function render() {
var query = el.search.value.trim().toLowerCase();
var visible = tasks.filter(function (t) { return matches(t, query); });
STATUSES.forEach(function (status) {
var list = el.lists[status];
var count = 0;
var frag = document.createDocumentFragment();
for (var i = 0; i < visible.length; i++) {
if (visible[i].status === status) {
frag.appendChild(makeCard(visible[i]));
count += 1;
}
}
list.textContent = "";
list.appendChild(frag);
el.counts[status].textContent = String(count);
var empty = el.empties[status];
if (count === 0) {
empty.textContent = "";
var strong = document.createElement("strong");
strong.textContent = "Nothing here yet";
var span = document.createElement("span");
span.textContent = query
? "No tasks match your search in " + STATUS_LABEL[status] + "."
: "Add a task to start " + STATUS_LABEL[status] + ".";
empty.appendChild(strong);
empty.appendChild(span);
empty.hidden = false;
} else {
empty.hidden = true;
}
});
}
/* ---------- actions ---------- */
function findTask(id) {
for (var i = 0; i < tasks.length; i++) {
if (tasks[i].id === id) return tasks[i];
}
return null;
}
function addTask() {
var value = el.title.value.trim();
if (!value) {
el.title.focus();
announce("Please enter a task title before adding a task.");
return;
}
tasks.push(makeTask(value.slice(0, 120), "todo"));
el.title.value = "";
persist();
render();
announce("Task \"" + value + "\" added to To do. " + tasks.length + " task" + (tasks.length === 1 ? "" : "s") + " in total.");
}
function moveTask(id) {
var task = findTask(id);
if (!task) return;
var index = STATUSES.indexOf(task.status);
if (index === -1 || index >= STATUSES.length - 1) {
announce("Task \"" + task.title + "\" is already in Done and cannot move further.");
return;
}
var next = STATUSES[index + 1];
task.status = next;
persist();
render();
announce("Task \"" + task.title + "\" moved to " + STATUS_LABEL[next] + ".");
}
function deleteTask(id) {
var task = findTask(id);
if (!task) return;
var ok = window.confirm("Delete the task \"" + task.title + "\"?");
if (!ok) {
announce("Delete cancelled.");
return;
}
tasks = tasks.filter(function (t) { return t.id !== id; });
persist();
render();
announce("Task \"" + task.title + "\" deleted.");
}
el.form.addEventListener("submit", function (event) {
event.preventDefault();
addTask();
});
el.search.addEventListener("input", function () {
render();
var q = el.search.value.trim();
announce(q ? "Filtering tasks by \"" + q + "\"." : "Search cleared, showing all tasks.");
});
el.board.addEventListener("click", function (event) {
var target = event.target;
if (!target || !target.getAttribute) return;
var action = target.getAttribute("data-action");
if (!action) return;
var id = target.getAttribute("data-task-id");
if (action === "move") moveTask(id);
else if (action === "delete") deleteTask(id);
});
el.reset.addEventListener("click", function () {
var ok = window.confirm("Reset the demo? This removes all current tasks and restores the three sample tasks.");
if (!ok) {
announce("Reset cancelled.");
return;
}
tasks = seedTasks();
el.search.value = "";
persist();
render();
announce("Demo reset. Three sample tasks restored: Plan launch, Build prototype, Write brief.");
});
/* ---------- boot ---------- */
loadTasks();
render();
var mode = storageModeMessage();
if (mode) showNotice(mode);
announce("Task board ready with " + tasks.length + " task" + (tasks.length === 1 ? "" : "s") + ".");
})();
</script>
</body>
</html>
What the checks establish
scripts/check-minimax-m31-benchmark.mjs reproduces the SHA-256 and document/source checks, launches isolated Chromium contexts, records runtime errors and screenshots, exercises the controls, inspects the game’s running physics state and writes the result bundle. Generated code runs in the browser, not inside the Node checker. The dedicated test origin has no app data, and external requests are blocked except the exact pinned Three.js module, served from an unmodified cached copy whose hash is recorded.Takeaway
Read Next
Recursive Flint: All Four LogicMark Benchmarks, Scores and Failures
We test Recursive Flint on both LogicMark 1.0.0 suites in Tool-assisted and Quick modes, documenting objective scores, protocol failures, timing and API usage.
Ling 3.1 Flash: Free HTML Benchmark and Availability Results
OpenRouter chat produced a detailed glass-crane SVG, while globe requests hit provider rate limits and the game retry returned no visible HTML.
DeepSeek V4.1 Flash Review: Three Hands-On Coding Benchmarks
A hands-on DeepSeek V4.1 Flash review covering its 552B MoE architecture, compressed KV cache, official agent benchmarks, and three interactive creative-coding tests.