GLM 5.3 FlashX LegalZ.ai
Share
Link: https://hypeorcode.com/
got this text is it real?? "DHL: Your parcel is on hold due to an unpaid customs fee of 1.99 EUR. Pay within 24 hours or it will be returned to the sender: https://dhl.com-redelivery.eu/p/8KX2Q"
No, that's a phishing scam. Don't click the link or pay anything.
The biggest giveaway is the URL: dhl.com-redelivery.eu. The actual domain is com-redelivery.eu — scammers stick "dhl.com" in front to make it look official. Real DHL links go to dhl.com (or your country's DHL site).
Other red flags:
- €1.99 fee — tiny amount so you pay without thinking. They want your card details, not the €1.99
- 24-hour deadline — fake urgency to pressure you
- No tracking number, no name, no package details
- DHL handles customs fees through their official app/website, not random short links
What to do:
- Don't click the link, don't reply, just delete it
- If you're actually expecting a package, track it on the official DHL website or app (type the address yourself)
- If you already clicked and entered card details, call your bank immediately
This is an extremely common scam — DHL is one of the most impersonated brands in these SMS phishing attacks. If you got this, people you know probably did too, so it's worth warning them.
The catch: The link starts with dhl.com, but the site is com-redelivery.eu.
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
- 9 s
- Words
- 180
- Tokens
- 70 in, 913 out (646 of them thinking)
- List price
- $0.001 at today's price ($0.37 in / $1.25 out per M)
- Paid
- $0.001
- Script
- The first line calls it a scam.
Exact prompt
got this text is it real?? "DHL: Your parcel is on hold due to an unpaid customs fee of 1.99 EUR. Pay within 24 hours or it will be returned to the sender: https://dhl.com-redelivery.eu/p/8KX2Q"
GLM 5.3 FlashX Legal: other Everyday prompts
- Letter8 s$0.001
TLDR: Yes — if you do nothing, you'll be moved to a pricier tariff.
- Your current tariff (Fixed Saver 24, 24.6p/kWh) is being withdrawn on 31 Dec 2026.
- If they don't hear from you by 28 Nov 2026, you'll be automatically moved to their Standard Variable tariff: 34.1p/kWh + 61.2p/day standing charge — that's ~38% more per unit.
- To stay on a fixed price: log into your online account or the app and select Fixed Saver 27 before 28 Nov.
- The T&Cs changes need no action from you.
The "there's nothing else you need to do" line just means you won't lose supply — it doesn't mean your price stays the same. Also a decent excuse to compare other suppliers before the deadline.
- Landlord8 s<$0.001
Here's a draft you can copy and send:
Subject: Follow-Up: Security Deposit Return – [Property Address]
Hi [Landlord's Name],
I hope you're doing well. I'm writing regarding the return of my security deposit of $1,400 for [property address]. I moved out on [move-out date], which was seven weeks ago, and I have not yet received the deposit or any accounting of deductions.
I've tried reaching you by text without a response, so I wanted to follow up by email to have a written record of this request.
Could you please return the full deposit, or provide an itemized statement of any deductions along with receipts, by [date — e.g., this Friday, or two weeks from today]? I'm happy to discuss any legitimate charges, but I haven't received any documentation so far.
I'd prefer to resolve this quickly and amicably. Please let me know how you'd like to proceed, or if there's a better way to reach you.
Thank you, Sam [Phone number] [New mailing address for the check/transfer]
A few quick tips:
- Fill in the brackets — especially your move-out date and a specific deadline. A concrete date is more effective than "as soon as possible."
- Email creates a paper trail, which you'll want if this escalates. Keep texting too, but this is the right move.
- Many states require deposit returns (or an itemized deduction statement) within 14–30 days. If you tell me your state, I can check the exact deadline and add a line citing it — that often gets faster results.
- If he misses your deadline, a short, calm follow-up referencing your landlord-tenant law usually does the trick before anything more formal.
Want me to adjust the tone (firmer or softer) or add anything?
- Scam9 s$0.001
No, that's a phishing scam. Don't click the link or pay anything.
The biggest giveaway is the URL:
dhl.com-redelivery.eu. The actual domain is com-redelivery.eu — scammers stick "dhl.com" in front to make it look official. Real DHL links go to dhl.com (or your country's DHL site).Other red flags:
- €1.99 fee — tiny amount so you pay without thinking. They want your card details, not the €1.99
- 24-hour deadline — fake urgency to pressure you
- No tracking number, no name, no package details
- DHL handles customs fees through their official app/website, not random short links
What to do:
- Don't click the link, don't reply, just delete it
- If you're actually expecting a package, track it on the official DHL website or app (type the address yourself)
- If you already clicked and entered card details, call your bank immediately
This is an extremely common scam — DHL is one of the most impersonated brands in these SMS phishing attacks. If you got this, people you know probably did too, so it's worth warning them.
- Book7 s<$0.001
I don't want to send you into book club with made-up information, so I have to be straight with you: I'm not familiar with Nineteen Quiet Rooms by Ottilie Marsh. I can't find that book in my knowledge, and I'd rather not invent a twist that could leave you confidently discussing a plot that doesn't exist.
A few possibilities:
- The title or author might be slightly off. Do you remember any other details—what it's about, when it was published, where you heard about it? I might be able to help if I can figure out the actual book.
- It could be very new, self-published, or obscure. In that case I genuinely don't know the ending.
If you want to salvage things before tomorrow:
- Skim the last chapter or two — even 20–30 minutes with the ending is often enough to contribute to the twist discussion.
- Check Goodreads or StoryGraph for reader reviews, which often discuss the ending (sometimes with spoiler warnings).
- Search "[title] ending explained" — someone has usually written a breakdown if the book has a notable twist.
If you can tell me more about the book (genre, what you read so far, character names), I'm happy to help you piece together what's going on or prep some talking points for the meeting.
- Dinner19 s$0.002
Leek & parmesan frittata with crispy breadcrumbs — uses everything you've got, one pan, ~20 min.
Prep (3 min): Halve the leek lengthways, slice thin, rinse well (grit hides between the layers). Whisk the 4 eggs with a good handful of grated parmesan, salt, and pepper.
Leeks (6–7 min): Melt a decent knob of butter in a nonstick pan over medium heat. Cook the leeks with a pinch of salt until soft and sweet.
Crumbs (while leeks cook): Tear the stale bread into rough crumbs (grate on the coarse side of a box grater, or just scrunch it up). Push leeks to one side, add more butter, and toast the crumbs in the empty side until golden and crunchy, ~3–4 min. Season, add a little parmesan, set aside on a plate.
Frittata (8 min): Spread leeks evenly in the pan, pour in the eggs. Cook on low, covered with a lid or plate, until just set with a slight wobble in the middle, 6–8 min. If the top is being stubborn, carefully flip it in one go or put the lid back on for another minute.
Serve: Slide onto a plate, pile the buttery crumbs on top, more parmesan, lots of black pepper.
Tips:
- Low and slow on the eggs — high heat makes them rubbery.
- No lid? A dinner plate or foil over the pan works to set the top.
- Save a few raw crumbs and re-toast if they go soft while the frittata finishes.
If you'd rather go savory French toast: dip thick bread slices in the beaten egg + parmesan, fry in butter, top with the buttered leeks. Same ingredients, more brunch-vibes, slightly faster.
Both fully vegetarian. Enjoy!
- GLM 5.3 FlashX Legal, Toast: Not run yetqueuedToast
- Menu10 s$0.002
Menu Translation — Trattoria del Gufo
Cover charge: €3.00 per person | Florence · since 1962
Appetizers
- Chicken liver crostini — €8.00
- Fried zucchini flowers* — €9.00
- Cured meats & pecorino board — €14.00
First Courses
- Pappa al pomodoro (tomato bread soup) — €10.00
- Pici all'aglione (pasta with garlic sauce) — €12.00
- Ribollita (bread & vegetable soup) — €10.00
Main Courses
- Bistecca alla fiorentina (min. 1.2 kg) — €5.50 per etto (100g)
- Florentine tripe — €14.00
- Peposo dell'Impruneta (peppery beef stew) — €16.00
Side Dishes
- Roasted peppers — €6.00
- Beans all'uccelletto — €6.00
- French fries* — €5.00
Desserts
- Cantucci with Vin Santo — €8.00
- Tiramisù — €7.00
*frozen product | 12% service charge not included
💶 Your Steak Dinner (2 people, sharing)
The Florentine steak is sold by weight at €5.50 per 100g, with a minimum of 1.2 kg.
Item Cost Steak (minimum 1.2 kg = 12 etto × €5.50) €66.00 Cover charge (€3 × 2) €6.00 Minimum total €72.00 With 12% service €80.64 ⚠️ Heads up: €66 is the floor — the actual steak will likely weigh more, so expect roughly €75–90+ for the meat alone depending on the cut they bring out. The waiter typically shows you the steak before cooking so you can confirm the weight and price.
That's about €36–40 per person for just the steak and cover — a classic Florentine experience, but not a budget meal! 🥩
- Chores23 s$0.004
Here's a printable chore chart for Ava and Leo — open it, hit print, and stick it on the fridge!
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Ava & Leo's Chore Chart</title> <style> @page { size: letter; margin: 0.5in; }- { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Comic Sans MS', 'Chalkboard SE', 'Segoe UI', sans-serif; background: #fdf6ec; color: #3a3a3a; padding: 20px; display: flex; flex-direction: column; align-items: center; }
.chart { width: 100%; max-width: 750px; background: #ffffff; border-radius: 16px; padding: 22px 24px 18px; box-shadow: 0 4px 14px rgba(0,0,0,0.08); }
header { text-align: center; margin-bottom: 14px; }
h1 { font-size: 28px; letter-spacing: 1px; color: #2d6a4f; }
h1 .ava-c { color: #d6336c; } h1 .leo-c { color: #1d7ab8; }
.subtitle { font-size: 13px; color: #777; margin-top: 2px; }
.kid-section { border-radius: 12px; overflow: hidden; margin-bottom: 16px; border: 2px solid; }
.kid-section.ava { border-color: #f8c4d4; } .kid-section.leo { border-color: #bde0f5; }
.kid-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 14px; color: #fff; font-size: 17px; font-weight: bold; }
.ava .kid-header { background: #d6336c; } .leo .kid-header { background: #1d7ab8; }
.kid-header .age { font-size: 12px; font-weight: normal; background: rgba(255,255,255,0.25); padding: 2px 10px; border-radius: 20px; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th, td { padding: 8px 10px; text-align: left; vertical-align: middle; }
thead th { font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: #666; background: #f7f7f7; border-bottom: 1px solid #eee; }
tbody tr + tr { border-top: 1px dashed #e8e8e8; }
.day { font-weight: bold; white-space: nowrap; width: 70px; }
.ava .day { color: #d6336c; } .leo .day { color: #1d7ab8; }
.chore { width: 42%; }
.box { width: 55px; text-align: center; }
.checkbox { display: inline-block; width: 22px; height: 22px; border: 2px solid #bbb; border-radius: 6px; vertical-align: middle; }
.ava .checkbox { border-color: #f0a3bd; } .leo .checkbox { border-color: #8ec8ec; }
.footer { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-top: 4px; flex-wrap: wrap; }
.reward { background: #fff8e6; border: 2px dashed #f4c542; border-radius: 10px; padding: 8px 14px; font-size: 13px; color: #8a6d1a; flex: 1; min-width: 200px; }
.reward strong { color: #6b5410; }
.print-btn { background: #2d6a4f; color: #fff; border: none; border-radius: 10px; padding: 10px 22px; font-size: 15px; font-family: inherit; cursor: pointer; display: flex; align-items: center; gap: 8px; }
.print-btn:hover { background: #245a42; }
@media print { body { background: #fff; padding: 0; } .chart { box-shadow: none; max-width: 100%; padding: 0; border-radius: 0; } .print-btn { display: none; } .kid-section { break-inside: avoid; } } </style>
</head> <body><div class="chart"> <header> <h1><span class="ava-c">Ava</span> & <span class="leo-c">Leo</span>'s Chore Chart</h1> <div class="subtitle">Monday – Friday • Two chores each • Check the box when you're done!</div> </header> <!-- AVA --> <div class="kid-section ava"> <div class="kid-header"> <span>🎀 Ava</span> <span class="age">Age 7</span> </div> <table> <thead> <tr> <th>Day</th> <th class="chore">Chore 1</th> <th class="chore">Chore 2</th> <th class="box">Done</th> </tr> </thead> <tbody> <tr> <td class="day">Monday</td> <td>Make your bed</td> <td>Put dirty clothes in the hamper</td> <td class="box"><span class="checkbox"></span></td> </tr> <tr> <td class="day">Tuesday</td> <td>Water the plants</td> <td>Tidy up your toys & books</td> <td class="box"><span class="checkbox"></span></td> </tr> <tr> <td class="day">Wednesday</td> <td>Set the table for dinner</td> <td>Make your bed</td> <td class="box"><span class="checkbox"></span></td> </tr> <tr> <td class="day">Thursday</td> <td>Dust the living room</td> <td>Put all shoes by the door</td> <td class="box"><span class="checkbox"></span></td> </tr> <tr> <td class="day">Friday</td> <td>Help fold the towels</td> <td>Wipe the kitchen table</td> <td class="box"><span class="checkbox"></span></td> </tr> </tbody> </table> </div> <!-- LEO --> <div class="kid-section leo"> <div class="kid-header"> <span>🚀 Leo</span> <span class="age">Age 10</span> </div> <table> <thead> <tr> <th>Day</th> <th class="chore">Chore 1</th> <th class="chore">Chore 2</th> <th class="box">Done</th> </tr> </thead> <tbody> <tr> <td class="day">Monday</td> <td>Take out the trash</td> <td>Make your bed</td> <td class="box"><span class="checkbox"></span></td> </tr> <tr> <td class="day">Tuesday</td> <td>Load the dishwasher</td> <td>Sweep the kitchen floor</td> <td class="box"><span class="checkbox"></span></td> </tr> <tr> <td class="day">Wednesday</td> <td>Take out the recycling</td> <td>Make your bed</td> <td class="box"><span class="checkbox"></span></td> </tr> <tr> <td class="day">Thursday</td> <td>Wipe the bathroom sink</td> <td>Vacuum the living room</td> <td class="box"><span class="checkbox"></span></td> </tr> <tr> <td class="day">Friday</td> <td>Empty the dishwasher</td> <td>Take out the trash</td> <td class="box"><span class="checkbox"></span></td> </tr> </tbody> </table> </div> <div class="footer"> <div class="reward"> ⭐ <strong>Weekly Goal:</strong> Check off all <strong>20</strong> boxes this week to earn <em>___________________________</em> (you pick the reward!) </div> <button class="print-btn" onclick="window.print()">🖨️ Print Chart</button> </div> </div></body> </html>A few notes:
- Age-appropriate split — Ava gets simpler tasks (dusting, setting the table, watering plants); Leo gets more responsibility (trash, dishwasher, vacuuming).
- Checkboxes are empty so kids can check them off with a pencil or sticker each day.
- Weekly reward line at the bottom is blank so you can write in whatever works for your family (movie night, ice cream, extra screen time, etc.).
- Printing tip: For the fridge, print it regular, or laminate it and use a dry-erase marker so you can wipe it clean each week.
Want me to swap any chores? If you have pets, specific rooms that need attention, or a reward already in mind, I can update it in seconds.
- Seasons11 min 44 s$0.098
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
.htmlfile, 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.