my 9 year old asked why we have summer and winter. can you draw it so she gets it?
Great question — and a fun one to answer with a picture instead of a lecture. I built you a little interactive picture-book: your daughter can grab the Earth, drag it around the Sun, watch the seasons flip, try the flashlight trick, and even "break" the seasons by straightening the Earth's tilt.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Why do we have summer & winter?</title>
<link href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@600;700;800&family=Nunito:wght@600;700;800;900&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<style>
:root{
--ink:#3A3350; --paper:#FFF6E8; --gold:#FFC145; --coral:#FF8A5C;
--mint:#7CC66B; --ice:#7EC8E3; --sky:#262254; --bg:#161233;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%}
body{background:var(--bg);font-family:'Nunito',system-ui,sans-serif;color:var(--ink);overflow:hidden}
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
[hidden]{display:none!important}
#app{display:grid;grid-template-columns:minmax(330px,392px) 1fr;gap:16px;padding:16px;height:100dvh}
/* ---------- the book ---------- */
#book{position:relative;display:flex;flex-direction:column;min-height:0;border-radius:22px;background:var(--paper);
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2'/%3E%3CfeColorMatrix values='0 0 0 0 0.35 0 0 0 0 0.28 0 0 0 0 0.2 0 0 0 0.05 0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
box-shadow:0 12px 32px rgba(0,0,0,.45), inset 0 0 0 2px #EAD9BC;}
.bhead{padding:16px 18px 8px}
.badge{display:inline-flex;align-items:center;gap:6px;font-size:.72rem;font-weight:800;background:var(--gold);border:2px solid var(--ink);border-radius:999px;padding:3px 10px;box-shadow:0 2px 0 var(--ink)}
.badge svg{width:13px;height:13px}
.titlerow{display:flex;align-items:flex-start;gap:10px;margin-top:10px}
h1{font-family:'Baloo 2',cursive;font-size:1.6rem;line-height:1.1;font-weight:800}
h1 em{font-style:normal}
.c-summer{color:#F06A2E}.c-winter{color:#2E8AB0}
.sub{margin-top:6px;font-size:.85rem;font-weight:700;color:#7A6F8F}
#snd{margin-left:auto;flex:none;width:38px;height:38px;border-radius:50%;background:#fff;border:2px solid var(--ink);box-shadow:0 2px 0 var(--ink);display:grid;place-items:center}
#snd svg{width:17px;height:17px}
#snd:active{transform:translateY(2px);box-shadow:none}
.tabs{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;padding:6px 18px 12px}
.tab{display:flex;align-items:center;justify-content:center;gap:6px;padding:8px 4px;border-radius:14px;border:2px solid var(--ink);background:#fff;font-weight:800;font-size:.78rem;box-shadow:0 3px 0 var(--ink)}
.tab svg{width:15px;height:15px}
.tab.on{background:var(--gold)}
.tab:active{transform:translateY(2px);box-shadow:0 1px 0 var(--ink)}
.pages{flex:1;min-height:0;overflow-y:auto;padding:4px 18px 14px;border-top:2px dashed #E4D2B4}
.pages::-webkit-scrollbar{width:9px}
.pages::-webkit-scrollbar-thumb{background:#D9C6A4;border-radius:8px;border:2px solid var(--paper)}
.page{animation:pagein .35s ease}
@keyframes pagein{from{opacity:0;transform:translateY(6px)}}
.page h3{font-family:'Baloo 2',cursive;font-size:1.05rem;margin:12px 0 6px}
.lead{font-size:.9rem;line-height:1.5;margin-bottom:8px;font-weight:600}
.lead b{font-weight:900}
.today{font-size:.8rem;font-weight:800;color:#7A6F8F;margin:2px 0 8px}
.legend{list-style:none;margin:4px 0 10px;display:grid;gap:6px}
.legend li{display:flex;gap:8px;align-items:flex-start;font-size:.8rem;font-weight:700;color:#5C5370}
.legend svg{width:15px;height:15px;flex:none;margin-top:1px;color:#8A5A3A}
.live{background:#fff;border:2px solid var(--ink);border-radius:16px;box-shadow:0 3px 0 var(--ink);padding:12px 14px;margin:10px 0}
.lab{font-size:.68rem;font-weight:800;letter-spacing:.6px;text-transform:uppercase;color:#9A8FAF;display:block}
.live-row{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:2px}
.live-row b{font-family:'Baloo 2',cursive;font-size:1.05rem}
.seasonWord{font-family:'Baloo 2',cursive;font-weight:800;font-size:1.85rem;line-height:1;display:inline-block;transform:rotate(-2deg);margin:2px 0 10px}
.seasonWord.pop{animation:pop .4s}
@keyframes pop{0%{transform:rotate(-2deg) scale(.6)}60%{transform:rotate(-2deg) scale(1.15)}100%{transform:rotate(-2deg) scale(1)}}
.thermo{display:flex;align-items:center;gap:8px;margin:2px 0 10px}
.thermo svg{width:16px;height:16px;flex:none}
.th-track{display:flex;gap:3px;flex:1}
.seg{height:12px;flex:1;border-radius:4px;background:#EFE3CC}
.seg.on{background:var(--c);box-shadow:inset 0 -2px 0 rgba(0,0,0,.12)}
.th-word{font-size:.75rem;font-weight:900;min-width:92px;text-align:right}
.day{display:flex;align-items:center;gap:8px}
.day svg{width:15px;height:15px}
.dtrack{flex:1;height:12px;border-radius:6px;background:#2E2950;overflow:hidden}
.dfill{height:100%;background:var(--gold);border-radius:6px;transition:width .2s}
.day b{font-size:.8rem;font-weight:900;min-width:64px;text-align:right}
.penguin{display:flex;gap:9px;align-items:flex-start;background:#E8F4EA;border:2px solid #3F8F3E;border-radius:14px;padding:9px 12px;margin:10px 0;font-size:.82rem;font-weight:700}
.penguin svg{width:16px;height:16px;flex:none;color:#3F8F3E;margin-top:1px}
details.myth{background:#FFF3D6;border:2px solid var(--gold);border-radius:14px;padding:10px 12px;margin:10px 0}
details.myth summary{cursor:pointer;font-weight:800;font-size:.84rem;display:flex;gap:8px;align-items:center;list-style:none}
details.myth summary::-webkit-details-marker{display:none}
details.myth summary svg{width:16px;height:16px;flex:none}
details.myth[open] summary{margin-bottom:6px}
details.myth p{font-size:.82rem;line-height:1.5;font-weight:600}
.try{background:#FDE7DC;border:2px solid #E86A4A;border-radius:14px;padding:9px 12px;font-size:.82rem;font-weight:700;margin:10px 0;line-height:1.45}
.btnrow{display:flex;gap:8px;margin:10px 0}
.mini{display:inline-flex;align-items:center;gap:6px;padding:8px 12px;border-radius:12px;border:2px solid var(--ink);font-weight:800;font-size:.8rem;box-shadow:0 3px 0 var(--ink);background:#fff}
.mini svg{width:14px;height:14px}
.mini:active{transform:translateY(2px);box-shadow:0 1px 0 var(--ink)}
.mini.gold{background:var(--gold)}.mini.ice{background:#CDEBF6}
.dots{display:flex;gap:5px;margin:8px 0 10px}
.dot{width:14px;height:14px;border-radius:50%;background:#EFE3CC;border:2px solid var(--ink)}
.dot.on{background:var(--coral)}
.flive-row{display:flex;justify-content:space-between;align-items:baseline;font-size:.85rem;font-weight:800;margin:4px 0}
.flive-row b{font-family:'Baloo 2',cursive}
.bfoot{padding:10px 18px 14px;border-top:2px dashed #E4D2B4}
.bfoot p{font-size:.75rem;font-weight:700;color:#7A6F8F;line-height:1.45}
/* ---------- the stage ---------- */
#stage{position:relative;border-radius:22px;overflow:hidden;background:var(--sky);box-shadow:0 12px 32px rgba(0,0,0,.45), inset 0 0 0 2px #171334}
#scene{position:absolute;inset:0;width:100%;height:100%;touch-action:none;display:block}
#hint{position:absolute;top:14px;left:50%;transform:translateX(-50%);background:var(--paper);border:2px solid var(--ink);border-radius:999px;padding:6px 14px;font-weight:800;font-size:.8rem;box-shadow:0 3px 0 var(--ink);transition:opacity .4s;pointer-events:none;white-space:nowrap;z-index:3}
#hint.off{opacity:0}
#years{position:absolute;top:14px;left:14px;display:flex;align-items:center;gap:6px;background:var(--paper);border:2px solid var(--ink);border-radius:999px;padding:6px 12px;font-weight:800;font-size:.78rem;box-shadow:0 3px 0 var(--ink);z-index:3}
#years svg{width:14px;height:14px;color:#D69512}
#years.bump{animation:bump .5s}
@keyframes bump{30%{transform:scale(1.18) rotate(-3deg)}}
#play{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);width:54px;height:54px;border-radius:50%;background:var(--gold);border:3px solid var(--ink);box-shadow:0 4px 0 var(--ink);display:grid;place-items:center;z-index:3}
#play svg{width:22px;height:22px}
#play:active{transform:translateX(-50%) translateY(3px);box-shadow:0 1px 0 var(--ink)}
#party{position:absolute;inset:0;display:grid;place-items:center;background:rgba(22,18,51,.45);backdrop-filter:blur(2px);z-index:4}
.pcard{background:var(--paper);border:3px solid var(--ink);border-radius:20px;box-shadow:0 8px 0 var(--ink);padding:22px 24px;max-width:330px;text-align:center;transform:rotate(-2deg);animation:pop .45s}
.pcard>svg{width:34px;height:34px;color:#D69512}
.pcard h2{font-family:'Baloo 2',cursive;font-size:1.4rem;margin:6px 0}
.pcard p{font-size:.85rem;font-weight:700;line-height:1.5;margin-bottom:12px}
#toast{position:fixed;left:50%;bottom:22px;transform:translateX(-50%);display:flex;align-items:center;gap:8px;background:var(--paper);border:2px solid var(--ink);border-radius:999px;padding:8px 16px;font-weight:800;font-size:.85rem;box-shadow:0 3px 0 var(--ink);z-index:50}
#toast svg{width:15px;height:15px;color:#D69512}
@media (max-width:920px){
body{overflow:auto}
#app{grid-template-columns:1fr;grid-template-rows:54dvh auto;height:auto;gap:12px;padding:12px}
#stage{order:1;min-height:340px}
#book{order:2}
}
</style>
</head>
<body>
<div id="app">
<aside id="book">
<header class="bhead">
<span class="badge"><i data-lucide="sparkles"></i>A picture-book explanation</span>
<div class="titlerow">
<h1>Why do we have <em class="c-summer">summer</em> & <em class="c-winter">winter</em>?</h1>
<button id="snd" aria-label="Toggle sound">
<i data-lucide="volume-2" class="ic-on"></i><i data-lucide="volume-x" class="ic-off" style="display:none"></i>
</button>
</div>
<p class="sub">The story of a tilted Earth — best read out loud, together.</p>
</header>
<nav class="tabs">
<button class="tab on" data-mode="orbit"><i data-lucide="orbit"></i>The year</button>
<button class="tab" data-mode="flash"><i data-lucide="flashlight"></i>Flashlight</button>
<button class="tab" data-mode="notilt"><i data-lucide="person-standing"></i>No tilt!</button>
</nav>
<div class="pages">
<!-- PAGE 1 -->
<section class="page" id="page-orbit">
<h3>1 · A year around the Sun</h3>
<p class="lead">Earth is a giant spinning top that <b>leans over by 23.5°</b> — and as it travels around the Sun, the lean always points the <b>same way</b>.</p>
<p class="lead">When our side (the North — that's the little house) leans <b>toward</b> the Sun, sunlight hits us straight-on and strong: <b class="c-summer">summer!</b> When we lean <b>away</b>, sunlight arrives slanted and weak: <b class="c-winter">winter!</b></p>
<p class="today">We're starting with <b id="todayM">—</b>, today's real month.</p>
<ul class="legend">
<li><i data-lucide="slash"></i>The stick through Earth is its tilt — always 23.5°, always pointing the same way.</li>
<li><i data-lucide="home"></i>The house is the North (our home). The penguin is the South.</li>
<li><i data-lucide="sun"></i>Gold rays are sunlight. Watch which side of Earth they hit!</li>
</ul>
<div class="live">
<div class="live-row"><span class="lab">Month</span><b data-live="month">—</b></div>
<span class="lab">Season in the North</span>
<span class="seasonWord" data-live="season">—</span>
<div class="thermo">
<i data-lucide="snowflake"></i>
<div class="th-track">
<i class="seg" style="--c:#7EC8E3"></i><i class="seg" style="--c:#8AD4D8"></i><i class="seg" style="--c:#A8E0B0"></i><i class="seg" style="--c:#C8E88A"></i><i class="seg" style="--c:#F2D96B"></i><i class="seg" style="--c:#F7B45C"></i><i class="seg" style="--c:#F79A52"></i><i class="seg" style="--c:#FF7A4C"></i>
</div>
<i data-lucide="flame"></i>
<span class="th-word" data-live="temp">—</span>
</div>
<div class="day">
<i data-lucide="sun"></i>
<div class="dtrack"><div class="dfill"></div></div>
<b data-live="day">—</b>
</div>
</div>
<div class="penguin"><i data-lucide="bird"></i><p data-live="peng">—</p></div>
<details class="myth" id="myth">
<summary><i data-lucide="lightbulb"></i>Wait — is winter cold because we're farther from the Sun?</summary>
<p><b>Nope!</b> Earth's orbit is almost a perfect circle. Earth is actually <b>closest</b> to the Sun in early January — right in the middle of northern winter! The distance changes only a little (147 vs 152 million km), far too little to make seasons. <em>The tilt does all the work.</em></p>
</details>
</section>
<!-- PAGE 2 -->
<section class="page" id="page-flash" hidden>
<h3>2 · The flashlight trick</h3>
<p class="lead">Here's the secret behind summer and winter — it's all in how the light lands. Point a flashlight <b>straight</b> at a wall: small, bright spot. Now <b>tilt</b> it: the same light, spread out — bigger and fainter.</p>
<p class="lead">In summer, sunlight hits the North more straight-on. In winter it arrives at a slant, like a tilted flashlight. <b>Slanted light is weaker light.</b></p>
<div class="btnrow">
<button class="mini gold" id="snapSummer"><i data-lucide="sun"></i>Summer sun</button>
<button class="mini ice" id="snapWinter"><i data-lucide="snowflake"></i>Winter sun</button>
</div>
<div class="live">
<div class="flive-row"><span class="lab">Flashlight tilt</span><b data-live="ftilt">—</b></div>
<div class="dots">
<i class="dot"></i><i class="dot"></i><i class="dot"></i><i class="dot"></i><i class="dot"></i>
</div>
<div class="flive-row"><span class="lab">Light on the wall</span><b data-live="fspot">—</b></div>
</div>
<p class="try">Real-life mission: tonight, dim the lights, grab a real flashlight, and try it on a wall, the ceiling… or a sleepy cat. Count the rays in the picture — always seven. Tilting never adds or removes light; it only spreads it.</p>
</section>
<!-- PAGE 3 -->
<section class="page" id="page-notilt" hidden>
<h3>3 · What if Earth stood up straight?</h3>
<p class="lead">Earth's tilt is only <b>23.5°</b> — but it's the whole show. If Earth stood up <b>perfectly straight</b>, every place would get the same slant of sunlight all year long.</p>
<p class="lead">No summer. No winter. Just the same day, 365 times. Watch the thermometer refuse to move!</p>
<div class="live">
<div class="live-row"><span class="lab">Month</span><b data-live="month">—</b></div>
<span class="lab">Season in the North</span>
<span class="seasonWord" data-live="season">—</span>
<div class="thermo">
<i data-lucide="snowflake"></i>
<div class="th-track">
<i class="seg" style="--c:#7EC8E3"></i><i class="seg" style="--c:#8AD4D8"></i><i class="seg" style="--c:#A8E0B0"></i><i class="seg" style="--c:#C8E88A"></i><i class="seg" style="--c:#F2D96B"></i><i class="seg" style="--c:#F7B45C"></i><i class="seg" style="--c:#F79A52"></i><i class="seg" style="--c:#FF7A4C"></i>
</div>
<i data-lucide="flame"></i>
<span class="th-word" data-live="temp">—</span>
</div>
<div class="day">
<i data-lucide="sun"></i>
<div class="dtrack"><div class="dfill"></div></div>
<b data-live="day">—</b>
</div>
</div>
<p class="try">The tilt is tiny — but it's the entire reason summer and winter exist. Straighten it, and the year goes flat.</p>
</section>
</div>
<footer class="bfoot">
<p>For a curious 9-year-old. Grown-ups: afterwards, ask <em>"So… is summer when we're closer to the Sun?"</em> and enjoy the show.</p>
</footer>
</aside>
<main id="stage">
<canvas id="scene" role="img" aria-label="Interactive drawing of the Earth orbiting the Sun"></canvas>
<div id="hint">Drag the Earth around the Sun</div>
<div id="years"><i data-lucide="star"></i>Trips around the Sun: <b id="yearN">0</b></div>
<button id="play" aria-label="Play one year">
<i data-lucide="play" class="ic-play"></i><i data-lucide="pause" class="ic-pause" style="display:none"></i>
</button>
<div id="party" hidden>
<div class="pcard">
<i data-lucide="party-popper"></i>
<h2>YOU ORBITED THE SUN!</h2>
<p>One full trip around the Sun is one whole year — 365 days, four seasons. Earth just did it, and you steered.</p>
<button class="mini gold" id="partyOk">Keep going!</button>
</div>
</div>
</main>
</div>
<div id="toast" hidden><i data-lucide="sparkles"></i><span id="toastTxt"></span></div>
<script>
'use strict';
/* ================= helpers & state ================= */
const TAU=Math.PI*2, RA=Math.PI/180, INK='#3A3350';
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
const easeIO=t=>t<.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2;
const wrapPI=a=>{a=(a+Math.PI)%TAU; if(a<0)a+=TAU; return a-Math.PI;};
const MONTHS=['January','February','March','April','May','June','July','August','September','October','November','December'];
const M3=['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'];
const SEASON_OF=['winter','winter','spring','spring','spring','summer','summer','summer','autumn','autumn','autumn','winter'];
const SEASON_TXT={winter:'#2E8AB0',spring:'#3F8F3E',summer:'#F06A2E',autumn:'#C97F22',flat:'#C48A00'};
const SEASON_IDX={winter:0,spring:1,summer:2,autumn:3};
const S={
mode:'orbit', tiltTarget:23.5, tiltCur:23.5,
theta:0, playing:false, dragging:false, dragPending:false, hov:false,
hasDragged:false, flashTouched:false, flashDrag:false,
tween:null, fT:null, flashA:20*RA,
years:0, nextYearAt:0, lastMi:-1, mythOpen:false, muted:false,
catWake:0, time:0, earth:{x:0,y:0,r:24}, monthHits:[],
};
const nowM=new Date().getMonth();
S.theta=nowM*TAU/12; S.nextYearAt=S.theta+TAU; S.lastMi=nowM;
/* ================= DOM ================= */
const $=s=>document.querySelector(s), $$=s=>document.querySelectorAll(s);
const cv=$('#scene'), ctx=cv.getContext('2d'), stage=$('#stage');
const hint=$('#hint'), yearsEl=$('#years'), yearN=$('#yearN'), playBtn=$('#play');
const partyEl=$('#party'), toastEl=$('#toast'), toastTxt=$('#toastTxt');
const pageOrbit=$('#page-orbit'), pageFlash=$('#page-flash'), pageNotilt=$('#page-notilt');
try{ window.lucide && lucide.createIcons(); }catch(e){}
/* ================= audio ================= */
let AC=null;
function initAudio(){ if(!AC){ try{AC=new (window.AudioContext||window.webkitAudioContext)();}catch(e){} } if(AC&&AC.state==='suspended')AC.resume(); }
function note(f,t=0,d=.09,v=.1,type='triangle'){
if(!AC||S.muted)return;
const o=AC.createOscillator(),g=AC.createGain(),n=AC.currentTime+t;
o.type=type;o.frequency.value=f;
g.gain.setValueAtTime(v,n); g.gain.exponentialRampToValueAtTime(.0001,n+d);
o.connect(g).connect(AC.destination); o.start(n); o.stop(n+d+.02);
}
const yearArp=()=>[523,659,784,1046].forEach((f,i)=>note(f,i*.09,.22,.11));
/* ================= canvas size & stars ================= */
let W=0,H=0,DPR=1,stars=[];
function resize(){
const r=stage.getBoundingClientRect();
DPR=Math.min(window.devicePixelRatio||1,2);
W=r.width; H=r.height;
cv.width=Math.round(W*DPR); cv.height=Math.round(H*DPR);
stars=[];
const n=Math.round(W*H/9000)+40;
for(let i=0;i<n;i++) stars.push({x:Math.random()*W,y:Math.random()*H,r:.5+Math.random()*1.3,p:Math.random()*TAU,s:.6+Math.random()*1.6,sp:Math.random()<.08});
}
new ResizeObserver(resize).observe(stage);
resize();
/* ================= geometry ================= */
function geo(){
const cx=W/2, cy=H/2;
const A=Math.max(60,Math.min(W*.40, W/2-88));
const B=Math.max(42,Math.min(A*.72, H/2-72));
const sr=clamp(Math.min(W,H)*.075,24,46);
const er=clamp(Math.min(W,H)*.052,16,30);
return {cx,cy,A,B,sr,er};
}
function fgeo(){
return {wallTop:H*.10, wallBot:H*.36, wallY:H*.23, ox:W*.5, oy:H*.72, wl:W*.06, wr:W*.94};
}
/* ================= drawing helpers ================= */
function rr(c,x,y,w,h,r){r=Math.min(r,w/2,h/2);c.beginPath();c.moveTo(x+r,y);c.arcTo(x+w,y,x+w,y+h,r);c.arcTo(x+w,y+h,x,y+h,r);c.arcTo(x,y+h,x,y,r);c.arcTo(x,y,x+w,y,r);c.closePath();}
function ink(w=2.5){ctx.strokeStyle=INK;ctx.lineWidth=w;ctx.lineJoin='round';ctx.lineCap='round';}
function dot(x,y,r){ctx.beginPath();ctx.arc(x,y,r,0,TAU);ctx.fill();}
function blob(x,y,rx,ry,rot){ctx.beginPath();ctx.ellipse(x,y,rx,ry,rot,0,TAU);ctx.fill();}
function pill(x,y,txt,fill='#FFF6E8'){
ctx.font='800 11px "Baloo 2"'; const w=ctx.measureText(txt).width+16;
ctx.fillStyle=fill; rr(ctx,x-w/2,y-11,w,22,11); ctx.fill(); ink(1.8); ctx.stroke();
ctx.fillStyle=INK; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText(txt,x,y+.5);
}
/* ================= sky ================= */
const shoot={t:4,x:0,y:0,vx:0,vy:0,life:0};
function drawSky(dt){
ctx.fillStyle='#262254'; ctx.fillRect(0,0,W,H);
const vg=ctx.createRadialGradient(W/2,H*.42,Math.min(W,H)*.3,W/2,H/2,Math.max(W,H)*.72);
vg.addColorStop(0,'rgba(0,0,0,0)'); vg.addColorStop(1,'rgba(10,6,30,.5)');
ctx.fillStyle=vg; ctx.fillRect(0,0,W,H);
ctx.fillStyle='#FFF6E8';
for(const st of stars){
ctx.globalAlpha=.32+.5*(.5+.5*Math.sin(S.time*st.s+st.p));
if(st.sp){ ctx.save();ctx.translate(st.x,st.y);ctx.beginPath();ctx.moveTo(0,-st.r*3);ctx.lineTo(st.r,0);ctx.lineTo(0,st.r*3);ctx.lineTo(-st.r,0);ctx.closePath();ctx.fill();ctx.restore(); }
else dot(st.x,st.y,st.r);
}
ctx.globalAlpha=1;
shoot.t-=dt;
if(shoot.t<0&&shoot.life<=0){ shoot.life=.7; shoot.x=W*(.3+Math.random()*.5); shoot.y=H*(.05+Math.random()*.2); shoot.vx=-(160+Math.random()*120); shoot.vy=90+Math.random()*60; shoot.t=6+Math.random()*7; }
if(shoot.life>0){
shoot.life-=dt; shoot.x+=shoot.vx*dt; shoot.y+=shoot.vy*dt;
ctx.strokeStyle=`rgba(255,246,232,${Math.max(0,shoot.life)})`; ctx.lineWidth=2; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(shoot.x,shoot.y); ctx.lineTo(shoot.x-shoot.vx*.13,shoot.y-shoot.vy*.13); ctx.stroke();
}
}
/* ================= sun ================= */
function drawSun(x,y,r){
const g=ctx.createRadialGradient(x,y,r*.55,x,y,r*2.4);
g.addColorStop(0,'rgba(255,193,69,.5)'); g.addColorStop(1,'rgba(255,193,69,0)');
ctx.fillStyle=g; ctx.beginPath(); ctx.arc(x,y,r*2.4,0,TAU); ctx.fill();
ctx.save(); ctx.translate(x,y); ctx.rotate(S.time*.07);
for(let i=0;i<12;i++){
const long=i%2===0, len=long?r*.52:r*.32, w=long?.17:.12, a=i*TAU/12;
ctx.fillStyle=long?'#FFD97A':'#F5B93E';
ctx.beginPath();
ctx.moveTo(Math.cos(a-w)*r*.92,Math.sin(a-w)*r*.92);
ctx.lineTo(Math.cos(a)*(r+len),Math.sin(a)*(r+len));
ctx.lineTo(Math.cos(a+w)*r*.92,Math.sin(a+w)*r*.92);
ctx.closePath(); ctx.fill();
}
ctx.restore();
ctx.fillStyle='#FFC145'; ctx.beginPath(); ctx.arc(x,y,r,0,TAU); ctx.fill(); ink(2.6); ctx.stroke();
const flat=S.tiltCur<8;
ctx.strokeStyle=INK; ctx.lineWidth=2.4; ctx.lineCap='round';
if(!flat){ // happy closed eyes
ctx.beginPath(); ctx.arc(x-r*.34,y-r*.16,r*.13,Math.PI*1.12,Math.PI*1.88); ctx.stroke();
ctx.beginPath(); ctx.arc(x+r*.34,y-r*.16,r*.13,Math.PI*1.12,Math.PI*1.88); ctx.stroke();
ctx.beginPath(); ctx.arc(x,y+r*.08,r*.30,Math.PI*.18,Math.PI*.82); ctx.stroke(); // smile
}else{ // surprised: no tilt, no seasons!
ctx.fillStyle=INK; dot(x-r*.32,y-r*.18,r*.07); dot(x+r*.32,y-r*.18,r*.07);
ctx.beginPath(); ctx.arc(x,y+r*.22,r*.11,0,TAU); ctx.stroke();
}
ctx.fillStyle='rgba(255,122,76,.45)'; dot(x-r*.52,y+r*.10,r*.11); dot(x+r*.52,y+r*.10,r*.11);
}
/* ================= sunlight rays ================= */
function drawRays(sx,sy,sr,ex,ey,er){
const dx=ex-sx,dy=ey-sy,L=Math.hypot(dx,dy)||1,ux=dx/L,uy=dy/L,px=-uy,py=ux;
ctx.save(); ctx.strokeStyle='rgba(255,217,122,.65)'; ctx.lineWidth=2; ctx.lineCap='round';
ctx.setLineDash([7,8]); ctx.lineDashOffset=-S.time*28;
for(const o of [-1.5,-.5,.5,1.5]){
const ox=px*o*er*.42, oy=py*o*er*.42;
ctx.beginPath();
ctx.moveTo(sx+ux*(sr+4)+ox, sy+uy*(sr+4)+oy);
ctx.lineTo(ex-ux*(er+5)+ox, ey-uy*(er+5)+oy);
ctx.stroke();
}
ctx.restore();
}
/* ================= earth & friends ================= */
function drawHouse(x,y,s,season){
ctx.fillStyle=['#F4F7FB','#7CC66B','#57A94E','#D89A4E'][season];
ctx.beginPath(); ctx.ellipse(x,y+6*s,15*s,4.5*s,0,0,TAU); ctx.fill();
if(season===1){ ctx.fillStyle='#FF8AB0'; dot(x-8*s,y+5*s,1.6*s); ctx.fillStyle='#FFD97A'; dot(x+6*s,y+7*s,1.4*s); dot(x-2*s,y+8*s,1.3*s); }
if(season===0){ // tiny snowman
ctx.fillStyle='#FFFFFF'; ink(1.4);
ctx.beginPath(); ctx.arc(x-11*s,y+2*s,3.1*s,0,TAU); ctx.fill(); ctx.stroke();
ctx.beginPath(); ctx.arc(x-11*s,y-2.4*s,2*s,0,TAU); ctx.fill(); ctx.stroke();
}
const tx=x+12*s; // tree
ctx.fillStyle='#8A5A3A'; ctx.fillRect(tx-1.2*s,y-6*s,2.4*s,8*s);
ctx.fillStyle=['#F4F7FB','#7CC66B','#3F8F3E','#E8A34C'][season];
ctx.beginPath(); ctx.arc(tx,y-9*s,4.5*s,0,TAU); ctx.fill(); ink(1.6); ctx.stroke();
ctx.fillStyle='#FFF4E0'; rr(ctx,x-7*s,y-8*s,14*s,13*s,2*s); ctx.fill(); ink(2); ctx.stroke();
ctx.fillStyle='#B06A4A'; ctx.fillRect(x+3.5*s,y-15*s,3*s,6*s); ctx.strokeRect(x+3.5*s,y-15*s,3*s,6*s);
ctx.fillStyle='#E86A4A'; ctx.beginPath(); ctx.moveTo(x-9.5*s,y-8*s); ctx.lineTo(x+9.5*s,y-8*s); ctx.lineTo(x,y-16.5*s); ctx.closePath(); ctx.fill(); ink(2); ctx.stroke();
ctx.fillStyle='#8A5A3A'; rr(ctx,x-2*s,y-3*s,4.2*s,8*s,1.5*s); ctx.fill();
ctx.fillStyle=season===0?'#FFD97A':'#9CD3E8'; ctx.fillRect(x+2.6*s,y-6.5*s,3.4*s,3.4*s);
ctx.strokeStyle=INK; ctx.lineWidth=1.3; ctx.strokeRect(x+2.6*s,y-6.5*s,3.4*s,3.4*s);
if(season===0){ const k=(S.time*.5)%1; ctx.fillStyle=`rgba(255,246,232,${.7*(1-k)})`; dot(x+5*s,y-17*s-k*9*s,1.6*s+k); }
}
function drawPenguin(x,y,s,ns){
ctx.fillStyle=ns>.25?'#F4F7FB':'#6B7A99';
ctx.beginPath(); ctx.ellipse(x,y+5*s,11*s,3.5*s,0,0,TAU); ctx.fill();
ctx.fillStyle='#2E2A44'; ctx.beginPath(); ctx.ellipse(x,y-2*s,6*s,8.5*s,0,0,TAU); ctx.fill(); ink(1.8); ctx.stroke();
ctx.fillStyle='#FFF6E8'; ctx.beginPath(); ctx.ellipse(x,y-.5*s,3.6*s,5.5*s,0,0,TAU); ctx.fill();
ctx.fillStyle='#F5A623'; ctx.beginPath(); ctx.moveTo(x-1.5*s,y-6.2*s); ctx.lineTo(x+1.5*s,y-6.2*s); ctx.lineTo(x,y-3.6*s); ctx.closePath(); ctx.fill();
ctx.fillStyle='#FFF'; dot(x-2*s,y-7.2*s,1.1*s); dot(x+2*s,y-7.2*s,1.1*s);
ctx.fillStyle=INK; dot(x-2*s,y-7.2*s,.5*s); dot(x+2*s,y-7.2*s,.5*s);
ctx.fillStyle='#F5A623';
ctx.beginPath(); ctx.ellipse(x-2.4*s,y+6.4*s,1.8*s,.9*s,0,0,TAU); ctx.fill();
ctx.beginPath(); ctx.ellipse(x+2.4*s,y+6.4*s,1.8*s,.9*s,0,0,TAU); ctx.fill();
}
function drawEarth(ex,ey,r,th,ns,cx,cy){
const dx=cx-ex,dy=cy-ey,L=Math.hypot(dx,dy)||1,ux=dx/L,uy=dy/L;
const aa=S.tiltCur*RA, ax=Math.sin(aa), ay=-Math.cos(aa);
if((S.hov||S.dragging)&&!S.dragging){ ctx.setLineDash([4,5]); ctx.strokeStyle='rgba(255,193,69,.85)'; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(ex,ey,r*1.55,0,TAU); ctx.stroke(); ctx.setLineDash([]); }
if(S.dragging){ ctx.setLineDash([4,5]); ctx.strokeStyle='rgba(255,193,69,1)'; ctx.lineWidth=2.4; ctx.beginPath(); ctx.arc(ex,ey,r*1.6,0,TAU); ctx.stroke(); ctx.setLineDash([]); }
ctx.save();
ctx.beginPath(); ctx.arc(ex,ey,r,0,TAU); ctx.clip();
ctx.fillStyle='#4FA3C7'; ctx.fillRect(ex-r,ey-r,2*r,2*r);
ctx.fillStyle='rgba(38,80,120,.35)'; ctx.beginPath(); ctx.ellipse(ex+r*.3,ey+r*.35,r*.7,r*.5,.5,0,TAU); ctx.fill();
ctx.save(); ctx.translate(ex,ey); ctx.rotate(aa);
ctx.fillStyle='#6DB35F'; blob(-.38*r,-.34*r,.40*r,.27*r,.6);
ctx.fillStyle='#7CC66B'; blob(.42*r,-.02*r,.24*r,.32*r,-.5); blob(-.05*r,.44*r,.34*r,.18*r,.2);
ctx.fillStyle='#6DB35F'; dot(.10*r,-.58*r,.13*r);
ctx.fillStyle='#F4F7FB';
ctx.beginPath(); ctx.ellipse(0,-r*.94,r*.5,r*.26,0,0,TAU); ctx.fill();
ctx.beginPath(); ctx.ellipse(0,r*.94,r*.5,r*.26,0,0,TAU); ctx.fill();
ctx.setLineDash([4,6]); ctx.strokeStyle='rgba(255,246,232,.6)'; ctx.lineWidth=1.5;
ctx.beginPath(); ctx.ellipse(0,0,r,r*.32,0,0,TAU); ctx.stroke(); ctx.setLineDash([]);
ctx.restore();
ctx.fillStyle='rgba(23,19,54,.62)'; ctx.beginPath(); ctx.arc(ex-ux*r*.62,ey-uy*r*.62,r*1.04,0,TAU); ctx.fill();
ctx.fillStyle='rgba(255,214,130,.10)'; ctx.beginPath(); ctx.arc(ex+ux*r*.45,ey+uy*r*.45,r*1.05,0,TAU); ctx.fill();
ctx.restore();
ink(2.4); ctx.beginPath(); ctx.arc(ex,ey,r,0,TAU); ctx.stroke();
// axis (the tilt)
ctx.strokeStyle='#FFF6E8'; ctx.lineWidth=2.4; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(ex-ax*r*1.42,ey-ay*r*1.42); ctx.lineTo(ex+ax*r*1.42,ey+ay*r*1.42); ctx.stroke();
ctx.fillStyle='#FFC145'; dot(ex+ax*r*1.42,ey+ay*r*1.42,3.4); ink(1.6); ctx.stroke();
ctx.font='800 10px Nunito'; ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.lineWidth=3; ctx.strokeStyle='rgba(23,19,54,.85)';
const nx=ex+ax*(r*1.42+11), ny=ey+ay*(r*1.42+11), sx2=ex-ax*(r*1.42+11), sy2=ey-ay*(r*1.42+11);
ctx.strokeText('N',nx,ny); ctx.fillStyle='#FFF6E8'; ctx.fillText('N',nx,ny);
ctx.strokeText('S',sx2,sy2); ctx.fillText('S',sx2,sy2);
if(S.tiltCur>1.5){
ctx.font='800 11px "Baloo 2"';
const tx2=ex+ax*(r*1.42+22)-ay*14, ty2=ey+ay*(r*1.42+22)+ax*14;
ctx.strokeText(Math.round(S.tiltCur)+'°',tx2,ty2); ctx.fillStyle='#FFD97A'; ctx.fillText(Math.round(S.tiltCur)+'°',tx2,ty2);
}
const mi=Math.floor(th/(TAU/12))%12;
const s=r/26;
const hx=ex+ax*r*.66, hy=ey+ay*r*.66+r*.30;
const px2=ex-ax*r*.55, py2=ey-ay*r*.55+r*.24;
drawHouse(hx,hy,s,SEASON_IDX[SEASON_OF[mi]]);
drawPenguin(px2,py2,s,ns);
ctx.font='800 9px Nunito'; ctx.textAlign='center'; ctx.fillStyle='rgba(255,246,232,.85)';
ctx.lineWidth=2.6; ctx.strokeStyle='rgba(23,19,54,.7)';
ctx.strokeText('the North',hx,hy+13*s+5); ctx.fillText('the North',hx,hy+13*s+5);
ctx.strokeText('the South',px2,py2+11*s+5); ctx.fillText('the South',px2,py2+11*s+5);
// midnight sun / polar night
const pol=clamp((Math.abs(ns)-.82)/.18,0,1);
if(pol>0){
ctx.globalAlpha=pol;
pill(ex,ey-r*1.78, ns>0?'the Sun never sets here!':'polar night here');
pill(px2,py2+r*1.5, ns>0?'polar night for penguins':'midnight sun for penguins');
ctx.globalAlpha=1;
}
if(!S.hasDragged){
const bob=Math.sin(S.time*3)*4, side=ex>W-180?-1:1;
const bx=ex+side*r*2.1, by=ey-r*1.5+bob;
ctx.font='800 11px "Baloo 2"';
const t='Drag me around the Sun!', w=ctx.measureText(t).width+18;
const x0=side>0?bx:bx-w;
ctx.fillStyle='#FFF6E8'; rr(ctx,x0,by-12,w,24,12); ctx.fill(); ink(2); ctx.stroke();
ctx.beginPath(); ctx.moveTo(x0+(side>0?6:w-6),by+11); ctx.lineTo(ex,ey-r*.9); ctx.lineTo(x0+(side>0?18:w-18),by+11); ctx.closePath(); ctx.fillStyle='#FFF6E8'; ctx.fill();
ctx.fillStyle=INK; ctx.textAlign='center'; ctx.fillText(t,x0+w/2,by+.5);
}
}
/* ================= myth distance tags ================= */
function drawMythTags(cx,cy,A,B){
ctx.font='700 9px Nunito'; ctx.textAlign='left'; ctx.textBaseline='middle';
const box=(x,y,l1,l2,ax,ay)=>{
ctx.strokeStyle='rgba(255,193,69,.8)'; ctx.lineWidth=1.6;
ctx.beginPath(); ctx.moveTo(ax,ay); ctx.lineTo(x+(l1.length>20?70:60),y+14); ctx.stroke();
ctx.fillStyle='#FFF6E8'; rr(ctx,x,y,150,30,8); ctx.fill(); ink(1.6); ctx.stroke();
ctx.fillStyle=INK; ctx.fillText(l1,x+8,y+10); ctx.fillStyle='#7A6F8F'; ctx.fillText(l2,x+8,y+21);
};
box(cx+A-150, cy+30, 'January — CLOSEST', '147 million km', cx+A, cy);
box(cx-A-6, cy-64, 'July — farthest', '152 million km', cx-A, cy);
}
/* ================= orbit scene ================= */
function drawOrbit(dt){
const {cx,cy,A,B,sr,er}=geo();
const th=((S.theta%TAU)+TAU)%TAU;
const ex=cx+A*Math.cos(th), ey=cy-B*Math.sin(th);
S.earth={x:ex,y:ey,r:er};
const ns=-Math.cos(th);
ctx.save(); ctx.setLineDash([5,9]); ctx.lineWidth=2; ctx.strokeStyle='rgba(255,246,232,.28)';
ctx.beginPath(); ctx.ellipse(cx,cy,A,B,0,0,TAU); ctx.stroke(); ctx.setLineDash([]);
if(th>.02){ ctx.strokeStyle='rgba(255,193,69,.9)'; ctx.lineWidth=4.5; ctx.lineCap='round';
ctx.beginPath(); ctx.ellipse(cx,cy,A,B,0,0,-th,true); ctx.stroke(); }
ctx.restore();
ctx.font='800 10px Nunito'; ctx.textAlign='center'; ctx.textBaseline='middle';
S.monthHits=[];
const curMi=Math.floor(th/(TAU/12))%12;
for(let m=0;m<12;m++){
const a=m*TAU/12, lx=cx+A*1.14*Math.cos(a), ly=cy-B*1.14*Math.sin(a);
S.monthHits.push({x:lx,y:ly,m});
if(m===curMi){ ctx.fillStyle='#FFC145'; rr(ctx,lx-21,ly-10,42,20,10); ctx.fill(); ink(1.6); ctx.stroke(); ctx.fillStyle=INK; }
else ctx.fillStyle='rgba(255,246,232,.5)';
ctx.fillText(M3[m],lx,ly+.5);
}
if(S.mythOpen) drawMythTags(cx,cy,A,B);
drawSun(cx,cy,sr);
drawRays(cx,cy,sr,ex,ey,er);
drawEarth(ex,ey,er,th,ns,cx,cy);
}
/* ================= flashlight scene ================= */
function spotWord(a){
const d=Math.abs(a)/RA;
if(d<8) return 'Straight on — small & BRIGHT';
if(d<26) return 'A little slant — still quite bright';
if(d<46) return 'Slanted — bigger & dimmer';
return 'Very slanted — same light, spread thin';
}
function drawTorch(x,y,a){
ctx.save(); ctx.translate(x,y); ctx.rotate(Math.atan2(-Math.cos(a),Math.sin(a)));
ctx.fillStyle='#F2B24C'; rr(ctx,-76,-11,66,22,10); ctx.fill(); ink(2.4); ctx.stroke();
ctx.strokeStyle='rgba(58,51,80,.35)'; ctx.lineWidth=2;
for(let i=0;i<3;i++){ ctx.beginPath(); ctx.moveTo(-60+i*9,-8); ctx.lineTo(-60+i*9,8); ctx.stroke(); }
ctx.fillStyle='#E89A3C'; ctx.beginPath(); ctx.moveTo(-12,-13); ctx.lineTo(2,-17); ctx.lineTo(2,17); ctx.lineTo(-12,13); ctx.closePath(); ctx.fill(); ink(2.4); ctx.stroke();
ctx.fillStyle='#FFF6E8'; ctx.beginPath(); ctx.ellipse(2,0,4.5,15,0,0,TAU); ctx.fill(); ink(2.2); ctx.stroke();
ctx.fillStyle='#FFE9A8'; ctx.beginPath(); ctx.ellipse(3,0,2.2,9,0,0,TAU); ctx.fill();
ctx.fillStyle='#FF8A5C'; dot(-32,-13,4); ink(1.8); ctx.stroke();
ctx.restore();
}
function drawCat(x,y,awake){
ctx.fillStyle='#E86A4A'; rr(ctx,x-30,y-8,60,18,9); ctx.fill(); ink(2.2); ctx.stroke();
ctx.strokeStyle='#F5E6C8'; ctx.lineWidth=5; ctx.lineCap='round';
ctx.beginPath(); ctx.arc(x+8,y-6,11,-.6,2.2); ctx.stroke();
ctx.fillStyle='#F5E6C8'; ctx.beginPath(); ctx.ellipse(x+2,y-8,14,9,0,0,TAU); ctx.fill(); ink(2.2); ctx.stroke();
ctx.beginPath(); ctx.arc(x-10,y-14,7.5,0,TAU); ctx.fill(); ctx.stroke();
ctx.beginPath(); ctx.moveTo(x-16,y-18); ctx.lineTo(x-14,y-24); ctx.lineTo(x-10,y-19); ctx.closePath(); ctx.fill(); ctx.stroke();
ctx.beginPath(); ctx.moveTo(x-8,y-19); ctx.lineTo(x-5,y-24); ctx.lineTo(x-2,y-18); ctx.closePath(); ctx.fill(); ctx.stroke();
if(awake){ ctx.fillStyle=INK; dot(x-13,y-14,1.4); dot(x-7,y-14,1.4); }
else{ ctx.strokeStyle=INK; ctx.lineWidth=1.6;
ctx.beginPath(); ctx.arc(x-13,y-14,2,Math.PI*1.1,Math.PI*1.9); ctx.stroke();
ctx.beginPath(); ctx.arc(x-7,y-14,2,Math.PI*1.1,Math.PI*1.9); ctx.stroke(); }
ctx.font='800 11px "Baloo 2"'; ctx.textAlign='center'; ctx.textBaseline='middle';
if(!awake){
const k=(S.time*.5)%1; ctx.globalAlpha=1-k; ctx.fillStyle='rgba(255,246,232,.9)'; ctx.fillText('z',x+16,y-22-k*10);
const k2=(S.time*.5+.5)%1; ctx.globalAlpha=1-k2; ctx.fillText('Z',x+23,y-26-k2*12);
ctx.globalAlpha=1;
} else pill(x+2,y-36,'Meow!');
}
function drawFlash(dt){
ctx.fillStyle='#221E48'; ctx.fillRect(0,0,W,H);
const vg=ctx.createRadialGradient(W/2,H/2,Math.min(W,H)*.3,W/2,H/2,Math.max(W,H)*.7);
vg.addColorStop(0,'rgba(0,0,0,0)'); vg.addColorStop(1,'rgba(8,5,24,.5)');
ctx.fillStyle=vg; ctx.fillRect(0,0,W,H);
const g=fgeo(), a=S.flashA;
const dirx=Math.sin(a), diry=-Math.cos(a), perpx=Math.cos(a), perpy=Math.sin(a), aw=30;
// floor + rug
ctx.fillStyle='#1C1838'; ctx.fillRect(0,H*.86,W,H*.14);
ctx.fillStyle='#3F8F3E'; ctx.beginPath(); ctx.ellipse(W*.30,H*.92,W*.14,H*.045,0,0,TAU); ctx.fill(); ink(2); ctx.stroke();
ctx.strokeStyle='rgba(255,246,232,.5)'; ctx.lineWidth=2;
ctx.beginPath(); ctx.ellipse(W*.30,H*.92,W*.09,H*.028,0,0,TAU); ctx.stroke();
// wall
ctx.fillStyle='#37315F'; rr(ctx,g.wl,g.wallTop,g.wr-g.wl,g.wallBot-g.wallTop,14); ctx.fill(); ink(2.5); ctx.stroke();
ctx.fillStyle='#2E2950'; rr(ctx,g.wl,g.wallTop,g.wr-g.wl,10,8); ctx.fill();
ctx.fillStyle='#FFF6E8'; rr(ctx,g.wl-6,g.wallBot-6,g.wr-g.wl+12,10,5); ctx.fill(); ink(2); ctx.stroke();
// window with moon
const wxx=g.wl+30, wxy=g.wallY-6;
ctx.fillStyle='#1B1840'; rr(ctx,wxx,wxy-28,76,52,8); ctx.fill(); ink(2.2); ctx.stroke();
ctx.fillStyle='#FFC145'; dot(wxx+52,wxy-8,9);
ctx.fillStyle='#1B1840'; dot(wxx+57,wxy-12,8);
ctx.fillStyle='#FFF6E8'; dot(wxx+18,wxy-16,1.6); dot(wxx+28,wxy-2,1.3); dot(wxx+12,wxy+4,1.2);
ctx.fillStyle='#FFF6E8'; rr(ctx,wxx-6,wxy+22,88,7,3); ctx.fill(); ink(1.8); ctx.stroke();
// rays & spot
const oris=[],hits=[];
for(let i=-3;i<=3;i++){
const s=i*(aw/3), o={x:g.ox+perpx*s,y:g.oy+perpy*s};
const t=(g.wallY-o.y)/diry;
oris.push(o); hits.push({x:o.x+t*dirx,y:g.wallY});
}
const xs=hits.map(h=>clamp(h.x,g.wl+8,g.wr-8));
const minX=Math.min(...xs), maxX=Math.max(...xs);
const spotX=(minX+maxX)/2, core=(maxX-minX)/2, bright=Math.cos(a);
ctx.save(); rr(ctx,g.wl,g.wallTop,g.wr-g.wl,g.wallBot-g.wallTop,14); ctx.clip();
const gx=ctx.createRadialGradient(spotX,g.wallY,4,spotX,g.wallY,core+16);
gx.addColorStop(0,`rgba(255,232,160,${.30+.55*bright})`); gx.addColorStop(1,'rgba(255,232,160,0)');
ctx.fillStyle=gx; ctx.beginPath(); ctx.ellipse(spotX,g.wallY,core+16,26,0,0,TAU); ctx.fill();
ctx.fillStyle=`rgba(255,244,214,${.12+.35*bright})`;
ctx.beginPath(); ctx.ellipse(spotX,g.wallY,Math.max(10,core*.55),11,0,0,TAU); ctx.fill();
ctx.restore();
if(Math.abs(a)>.1){
ctx.setLineDash([4,5]); ctx.strokeStyle='rgba(255,246,232,.5)'; ctx.lineWidth=1.6;
ctx.beginPath(); ctx.ellipse(g.ox,g.wallY,aw,15,0,0,TAU); ctx.stroke(); ctx.setLineDash([]);
ctx.font='700 9px Nunito'; ctx.fillStyle='rgba(255,246,232,.6)'; ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText('straight-on size',g.ox,g.wallY+30);
}
// shelf + cat
const catX=W*.80;
ctx.fillStyle='#FFF6E8'; rr(ctx,catX-34,g.wallY+4,68,7,3.5); ctx.fill(); ink(2); ctx.stroke();
const awake=S.catWake>0;
drawCat(catX,g.wallY+14,awake);
if(Math.abs(spotX-catX)<60) S.catWake=2.2; else S.catWake-=dt;
// rays
ctx.lineWidth=2.2; ctx.lineCap='round';
for(let i=0;i<7;i++){
ctx.strokeStyle=`rgba(255,217,122,${.28+.45*bright})`;
ctx.beginPath(); ctx.moveTo(oris[i].x,oris[i].y);
ctx.lineTo(clamp(hits[i].x,g.wl+6,g.wr-6),g.wallY); ctx.stroke();
}
ctx.fillStyle=`rgba(255,214,130,${.07+.10*bright})`;
ctx.beginPath();
ctx.moveTo(oris[0].x,oris[0].y); ctx.lineTo(oris[6].x,oris[6].y);
ctx.lineTo(clamp(hits[6].x,g.wl,g.wr),g.wallY); ctx.lineTo(clamp(hits[0].x,g.wl,g.wr),g.wallY);
ctx.closePath(); ctx.fill();
drawTorch(g.ox,g.oy,a);
pill(g.ox,g.oy+44,`tilt ${Math.round(Math.abs(a)/RA)}°`);
pill(clamp(spotX,70,W-70),g.wallTop-14,spotWord(a));
ctx.font='700 12px Nunito'; ctx.fillStyle='rgba(255,246,232,.75)'; ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText('Always the same 7 rays — a slant just spreads them out.',W/2,H-16);
S.flashLive={a,bright,word:spotWord(a)};
}
/* ================= confetti ================= */
let confetti=[];
const CCOL=['#FFC145','#FF8A5C','#7CC66B','#7EC8E3','#FFF6E8','#FF8AB0'];
function burst(x,y,n){
for(let i=0;i<n;i++) confetti.push({
x,y,vx:(Math.random()-.5)*7,vy:-Math.random()*8-3,
rot:Math.random()*TAU,vr:(Math.random()-.5)*.3,
s:4+Math.random()*5,c:CCOL[i%CCOL.length],life:1,dec:.008+Math.random()*.01
});
}
function drawConfetti(dt){
confetti=confetti.filter(p=>p.life>0);
for(const p of confetti){
p.vy+=.16; p.x+=p.vx; p.y+=p.vy; p.rot+=p.vr; p.life-=p.dec;
ctx.save(); ctx.globalAlpha=Math.max(0,p.life); ctx.translate(p.x,p.y); ctx.rotate(p.rot);
ctx.fillStyle=p.c; ctx.fillRect(-p.s/2,-p.s/4,p.s,p.s/2); ctx.restore();
}
ctx.globalAlpha=1;
}
/* ================= live readouts ================= */
const L={season:'',mi:-1,peng:''};
function setAll(key,val){ $$(`[data-live=${key}]`).forEach(el=>{ if(el.textContent!==val) el.textContent=val; }); }
function fmtH(hours){ const tm=Math.round(hours*12); return Math.floor(tm/12)+'h '+String((tm%12)*5).padStart(2,'0')+'m'; }
function seasonWordPop(){
$$('[data-live=season]').forEach(el=>{ el.classList.remove('pop'); void el.offsetWidth; el.classList.add('pop'); });
}
function refreshLive(){
if(S.mode==='flash'){
const f=S.flashLive||{a:S.flashA,bright:Math.cos(S.flashA),word:spotWord(S.flashA)};
setAll('ftilt',Math.round(Math.abs(f.a)/RA)+'° · '+Math.round(f.bright*100)+'% strong');
setAll('fspot',f.word);
const on=Math.round(f.bright*5);
$$('#page-flash .dot').forEach((d,i)=>d.classList.toggle('on',i<on));
return;
}
const th=((S.theta%TAU)+TAU)%TAU, mi=Math.floor(th/(TAU/12))%12, ns=-Math.cos(th);
const flat=S.mode==='notilt'||S.tiltCur<8;
const season=flat?'flat':SEASON_OF[mi];
const hours=flat?12:12+4.2*ns;
setAll('month',MONTHS[mi]);
setAll('day',fmtH(hours));
if(season!==L.season){
L.season=season;
const word=flat?'NO SEASONS!':season.toUpperCase();
$$('[data-live=season]').forEach(el=>{ el.textContent=word; el.style.color=SEASON_TXT[season]; });
seasonWordPop();
}
const tw=flat?'Same all year':ns<-.6?'Freezing!':ns<-.2?'Chilly':ns<.2?'Mild':ns<.6?'Warm!':'Hot, hot, hot!';
setAll('temp',tw);
const on=flat?4:clamp(Math.round((ns+1)/2*8),0,8);
$$('.th-track').forEach(tr=>tr.querySelectorAll('.seg').forEach((sg,i)=>sg.classList.toggle('on',i<on)));
const pct=flat?50:clamp((hours-6)/12*100,4,96);
$$('.dfill').forEach(d=>d.style.width=pct+'%');
let peng;
if(flat) peng='Penguin report: with no tilt the South is as mild as the North. He is confused.';
else if(season==='summer') peng='Penguin report: it is winter in the South right now. Opposite seasons!';
else if(season==='winter') peng='Penguin report: summer in the South! He is having a lovely time.';
else peng='Penguin report: opposite-ish seasons down there, half a world away.';
if(peng!==L.peng){ L.peng=peng; setAll('peng',peng); }
}
/* ================= year celebration ================= */
let partyT,toastT;
function toast(msg){ toastTxt.textContent=msg; toastEl.hidden=false; clearTimeout(toastT); toastT=setTimeout(()=>toastEl.hidden=true,2600); }
function onYear(){
S.years++; yearN.textContent=S.years;
yearsEl.classList.remove('bump'); void yearsEl.offsetWidth; yearsEl.classList.add('bump');
burst(S.earth.x,S.earth.y,S.years===1?110:46);
yearArp();
if(S.years===1){ partyEl.hidden=false; clearTimeout(partyT); partyT=setTimeout(()=>partyEl.hidden=true,5500); }
else toast('Year '+S.years+' around the Sun — nice steering!');
}
$('#partyOk').addEventListener('click',()=>partyEl.hidden=true);
/* ================= input ================= */
function ptPos(e){ const r=cv.getBoundingClientRect(); return {x:e.clientX-r.left,y:e.clientY-r.top}; }
function ptrTheta(p){ const g=geo(); return Math.atan2(-(p.y-g.cy),p.x-g.cx); }
function hitMonth(p){ for(const h of S.monthHits){ if(Math.hypot(p.x-h.x,p.y-h.y)<16) return h.m; } return -1; }
function aimFlash(p){ const g=fgeo(); S.fT=null; S.flashA=clamp(Math.atan2(p.x-g.ox,g.oy-p.y),-68*RA,68*RA); }
function tweenTheta(to){ S.tween={from:S.theta,to:S.theta+wrapPI(to-S.theta),t0:S.time,dur:.65}; }
let lastPtr=0,downP=null,downHitM=-1,moved=false;
cv.addEventListener('pointerdown',e=>{
initAudio(); e.preventDefault();
const p=ptPos(e); downP=p; moved=false;
try{cv.setPointerCapture(e.pointerId);}catch(_){}
if(S.mode==='flash'){ S.flashDrag=true; S.flashTouched=true; aimFlash(p); cv.style.cursor='grabbing'; }
else{ downHitM=hitMonth(p); lastPtr=ptrTheta(p); S.dragPending=true; }
});
cv.addEventListener('pointermove',e=>{
const p=ptPos(e);
if(S.mode==='flash'){
if(S.flashDrag) aimFlash(p);
else{ const g=fgeo(); cv.style.cursor=Math.hypot(p.x-g.ox,p.y-g.oy)<58?'grab':'default'; }
return;
}
if(S.dragPending||S.dragging){
if(!moved&&downP&&Math.hypot(p.x-downP.x,p.y-downP.y)>5){
moved=true; S.dragging=true; S.dragPending=false;
S.playing=false; updatePlay(); S.tween=null; S.hasDragged=true;
cv.style.cursor='grabbing';
}
if(S.dragging){ const t=ptrTheta(p); S.theta+=wrapPI(t-lastPtr); lastPtr=t; }
}else{
const g=geo(),th=((S.theta%TAU)+TAU)%TAU;
const ex=g.cx+g.A*Math.cos(th),ey=g.cy-g.B*Math.sin(th);
S.hov=Math.hypot(p.x-ex,p.y-ey)<g.er*1.7;
cv.style.cursor=S.hov?'grab':'default';
}
});
function endPtr(){
if(S.mode==='flash'){ S.flashDrag=false; cv.style.cursor='default'; return; }
if(!moved&&downHitM>=0) tweenTheta(downHitM*TAU/12);
S.dragging=false; S.dragPending=false; downHitM=-1; cv.style.cursor='default';
}
cv.addEventListener('pointerup',endPtr);
cv.addEventListener('pointercancel',endPtr);
cv.addEventListener('pointerleave',()=>{ S.hov=false; });
addEventListener('keydown',e=>{
if(e.code==='Space'&&S.mode!=='flash'){ e.preventDefault(); S.playing=!S.playing; updatePlay(); }
else if(e.key==='ArrowRight'||e.key==='ArrowLeft'){
if(S.mode==='flash'){ S.flashA=clamp(S.flashA+(e.key==='ArrowRight'?1:-1)*3*RA,-68*RA,68*RA); }
else{ const dir=e.key==='ArrowRight'?1:-1, cur=((S.theta%TAU)+TAU)%TAU;
tweenTheta((Math.round(cur/(TAU/12))+dir)*TAU/12); }
}
});
/* ================= UI wiring ================= */
function setIcon(sel,on){ $$(sel).forEach(el=>el.style.display=on?'':'none'); }
function updatePlay(){ setIcon('.ic-play',!S.playing); setIcon('.ic-pause',S.playing); }
playBtn.addEventListener('click',()=>{ initAudio(); S.playing=!S.playing; updatePlay(); note(520,0,.06,.06); });
$('#snd').addEventListener('click',()=>{ initAudio(); S.muted=!S.muted; setIcon('.ic-on',!S.muted); setIcon('.ic-off',S.muted); if(!S.muted)note(660,0,.08,.08); });
$('#myth').addEventListener('toggle',e=>S.mythOpen=e.target.open);
$('#snapSummer').addEventListener('click',()=>{ initAudio(); S.fT={from:S.flashA,to:20*RA,t0:S.time,dur:.7}; note(600,0,.06,.06); });
$('#snapWinter').addEventListener('click',()=>{ initAudio(); S.fT={from:S.flashA,to:58*RA,t0:S.time,dur:.7}; note(380,0,.06,.06); });
function setMode(m){
if(S.mode===m)return;
S.mode=m; S.tween=null; S.playing=(m==='notilt'); updatePlay();
S.tiltTarget=(m==='notilt')?0:23.5;
$$('.tab').forEach(b=>b.classList.toggle('on',b.dataset.mode===m));
pageOrbit.hidden=m!=='orbit'; pageFlash.hidden=m!=='flash'; pageNotilt.hidden=m!=='notilt';
playBtn.hidden=(m==='flash'); yearsEl.hidden=(m==='flash');
partyEl.hidden=true;
hint.textContent= m==='flash'?'Drag the flashlight side to side'
: m==='notilt'?'Drag the Earth through a year with no tilt'
: 'Drag the Earth around the Sun';
hint.classList.remove('off');
note(500,0,.05,.05);
}
$$('.tab').forEach(b=>b.addEventListener('click',()=>{ initAudio(); setMode(b.dataset.mode); }));
/* ================= main loop ================= */
function checkMonthsYears(){
const th=((S.theta%TAU)+TAU)%TAU, mi=Math.floor(th/(TAU/12))%12;
if(mi!==S.lastMi){ S.lastMi=mi; if(S.dragging||S.playing||S.tween) note(470+mi*22,0,.05,.04); }
while(S.theta>=S.nextYearAt){ S.nextYearAt+=TAU; onYear(); }
}
let last=0,started=false;
function loop(ts){
const dt=Math.min(.05,(ts-last)/1000||.016); last=ts; S.time+=dt;
S.tiltCur+=(S.tiltTarget-S.tiltCur)*Math.min(1,dt*3);
if(S.playing&&S.mode!=='flash') S.theta+=dt*TAU/22;
if(S.tween){ const k=clamp((S.time-S.tween.t0)/S.tween.dur,0,1);
S.theta=S.tween.from+(S.tween.to-S.tween.from)*easeIO(k); if(k>=1)S.tween=null; }
if(S.fT){ const k=clamp((S.time-S.fT.t0)/S.fT.dur,0,1);
S.flashA=S.fT.from+(S.fT.to-S.fT.from)*easeIO(k); if(k>=1)S.fT=null; }
checkMonthsYears();
ctx.setTransform(DPR,0,0,DPR,0,0);
if(S.mode==='flash') drawFlash(dt); else { drawSky(dt); drawOrbit(dt); }
drawConfetti(dt);
refreshLive();
if(S.mode==='flash'&&S.flashTouched) hint.classList.add('off');
if(S.mode!=='flash'&&S.hasDragged) hint.classList.add('off');
requestAnimationFrame(loop);
}
function start(){ if(started)return; started=true; last=performance.now(); requestAnimationFrame(loop); }
if(document.fonts&&document.fonts.ready) document.fonts.ready.then(start);
setTimeout(start,900);
$('#todayM').textContent=MONTHS[nowM];
updatePlay();
</script>
</body>
</html>
Save it as an .html file, open it in any browser, and sit next to her — it's built to be explored together, not just watched.
How to use it with her
- Tab 1 — The year. She can grab the Earth and drag it around the Sun (or press play). The little house is the North, the penguin is the South, and the stick through Earth is the 23.5° tilt — always leaning the same way. The book page updates live: month, season, a warmth meter, and day length. Completing a full lap triggers a "you orbited the Sun!" celebration.
- Tab 2 — The flashlight trick. This is the actual why: drag the flashlight and watch seven rays of light spread out and dim on the wall. Slanted light = weaker light. There's a sleeping cat on the shelf if she aims well.
- Tab 3 — No tilt! Earth stands up straight and the seasons die. Drag through a whole year while the thermometer refuses to move — that's what proves the tilt is the cause.
It also busts the classic mix-up (it's not distance — Earth is actually closest to the Sun in January) and shows that seasons are opposite in the two hemispheres. Afterward, the best follow-up is a real flashlight on the wall tonight.
The catch: The Earth is closest to the sun in early January, in the northern winter: the seasons come from its tilt.
Run details
- Model
- z-ai/glm-5.3-flashx-legal
- Route
- OpenRouter API, one user message
- Served by
- Z.AI
- Effort
- max, OpenRouter's default for this model
- Limits
- 60 min · 131,072 output tokens (the model's limit)
- Date
- 2 Nov 2026
- Time
- 11 min 44 s
- Words
- 3,087
- Tokens
- 34 in, 78,289 out (61,235 of them thinking)
- List price
- $0.098 at today's price ($0.37 in / $1.25 out per M)
- Paid
- $0.098
Exact prompt
my 9 year old asked why we have summer and winter. can you draw it so she gets it?