Siteβ―Builder
Editing:
cookie1.html
writable 0666
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Fortune Cookie β SVG + JS</title> <style> :root{ --bg1:#0f1222; --bg2:#1c2038; --gold1:#e6b66a; --gold2:#c48a3a; --gold3:#8a5f2a; --paper:#ffffff; --paperEdge:#e9e9e9; --ink:#222; --accent:#a7b6ff; } html,body{height:100%;} body{ margin:0; display:grid; place-items:center; background: radial-gradient(1200px 700px at 50% 35%, var(--bg2), var(--bg1)); font-family: system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji","Segoe UI Emoji", "Segoe UI Symbol"; color:#eef0ff; } .wrap{ width:min(92vw, 640px); padding:20px 20px 28px; border-radius:16px; background: rgba(255,255,255,0.03); box-shadow: 0 20px 60px rgba(0,0,0,.45), inset 0 1px 0 rgba(255,255,255,.06); backdrop-filter: blur(6px); text-align:center; } h1{ margin:6px 0 2px; font-size:clamp(20px, 1.8vw + 14px, 32px); letter-spacing:.2px; } p.sub{ margin:0 0 10px; opacity:.7; font-size:14px; } /* SVG scales responsively */ #scene{ width:min(92vw, 520px); height:auto; display:block; margin-inline:auto; cursor:pointer; touch-action:manipulation; } /* Button styles */ .controls{margin-top:10px; display:flex; gap:10px; justify-content:center; flex-wrap:wrap;} button{ appearance:none; border:0; border-radius:12px; padding:12px 16px; font-weight:600; letter-spacing:.2px; background:linear-gradient(180deg, #fafcff, #e5ebff); color:#0b1133; box-shadow: 0 8px 18px rgba(10,18,60,.25); transition: transform .1s ease, box-shadow .2s ease, opacity .2s ease; cursor:pointer; } button:hover{transform: translateY(-1px); box-shadow:0 12px 28px rgba(10,18,60,.35);} button:active{transform: translateY(0);} .ghost{ background:transparent; color:#eaf; border:1px solid rgba(255,255,255,.15); box-shadow:none; } .hidden{display:none !important;} /* Pre-crack subtle idle wiggle */ .idle #cookie { animation: idleWobble 1.8s ease-in-out infinite; transform-origin: 200px 170px; } @keyframes idleWobble{ 0%,100%{ transform: rotate(0deg) translateY(0); } 50%{ transform: rotate(.7deg) translateY(-1.2px); } } /* Crack animation states */ #cookie .half{ transition: transform 520ms cubic-bezier(.2,.7,.2,1); transform-box: fill-box; transform-origin: center; will-change: transform; } #crackLine{ opacity:0; stroke-dasharray:6 6; stroke-dashoffset:60; transition: opacity .25s ease .05s, stroke-dashoffset .6s ease; } #paper{ opacity:0; transform: translate(0px, 10px) scale(.96); transform-box: view-box; transform-origin:center; transition: opacity .35s ease .25s, transform .6s cubic-bezier(.18,.86,.22,1) .25s; } /* Active (cracked) */ .cracked #cookie .left{ transform: translateX(-28px) rotate(-16deg); } .cracked #cookie .right{ transform: translateX(28px) rotate(16deg); } .cracked #crackLine{ opacity: .65; stroke-dashoffset: 0; } .cracked #paper{ opacity:1; transform: translate(0px, -44px) scale(1); } /* Crumbs pop */ #crumbs circle{ transform-box: fill-box; transform-origin: center; } @keyframes pop{ from{ transform: translate(0,0) scale(1); opacity:1;} to{ transform: translate(var(--dx), var(--dy)) scale(.6); opacity:0;} } /* Fortune text on slip */ #fortuneText{ font-size:14px; fill:var(--ink); font-weight:600; letter-spacing:.2px; } .cursor::after{ content:"β"; animation: blink 1s step-end infinite; } @keyframes blink{ 50%{opacity:0;} } /* Output below (screen-reader friendly live region for the same text) */ #fortuneOutput{ margin-top:8px; min-height:1.4em; color:#dfe3ff; font-weight:500; letter-spacing:.2px; } /* Reduced motion: skip some flourish */ @media (prefers-reduced-motion: reduce){ .idle #cookie{ animation:none; } #cookie .half, #paper{ transition:none; } #crackLine{ transition:none; } } </style> </head> <body> <div class="wrap idle" id="app"> <h1>Crack the Fortune Cookie</h1> <p class="sub">Click the cookie (or press Space/Enter)</p> <!-- SCENE --> <svg id="scene" viewBox="0 0 400 300" role="button" aria-label="Crack the fortune cookie" tabindex="0"> <defs> <linearGradient id="cookieL" x1="0" y1="0" x2="1" y2="1"> <stop offset="0" stop-color="#f1c983"/> <stop offset="1" stop-color="#c58a3b"/> </linearGradient> <linearGradient id="cookieR" x1="1" y1="0" x2="0" y2="1"> <stop offset="0" stop-color="#f6d598"/> <stop offset="1" stop-color="#d2984d"/> </linearGradient> <filter id="shadow" x="-30%" y="-30%" width="160%" height="160%"> <feDropShadow dx="0" dy="7" stdDeviation="10" flood-opacity=".4" flood-color="#000"/> </filter> </defs> <!-- Ground shadow --> <ellipse cx="200" cy="230" rx="95" ry="20" fill="rgba(0,0,0,.35)" /> <!-- Cookie group --> <g id="cookie" filter="url(#shadow)"> <!-- Left half --> <g class="half left"> <!-- a stylized fortune cookie half --> <path d="M200,150 C 176,121 130,135 136,174 C 144,222 195,220 200,190 C 204,173 205,163 200,150 Z" fill="url(#cookieL)" stroke="var(--gold3)" stroke-width="1.8" /> <!-- subtle highlight --> <path d="M175,150 C160,150 145,164 147,178" fill="none" stroke="#ffd89b" stroke-width="2" opacity=".35"/> </g> <!-- Right half --> <g class="half right"> <path d="M200,150 C 224,121 270,135 264,174 C 256,222 205,220 200,190 C 196,173 195,163 200,150 Z" fill="url(#cookieR)" stroke="var(--gold3)" stroke-width="1.8" /> <path d="M225,150 C240,150 255,164 253,178" fill="none" stroke="#ffe1ad" stroke-width="2" opacity=".35"/> </g> <!-- Crack line (revealed on crack) --> <path id="crackLine" d="M200 142 L200 195" stroke="var(--gold3)" stroke-width="2.2" stroke-linecap="round"/> </g> <!-- Paper slip (comes out between halves) --> <g id="paper" transform="translate(200 165)"> <rect x="-100" y="-16" rx="5" ry="5" width="200" height="32" fill="var(--paper)" stroke="var(--paperEdge)" /> <text id="fortuneText" text-anchor="middle" dominant-baseline="middle"></text> </g> <!-- Crumbs (spawned dynamically) --> <g id="crumbs"></g> </svg> <div id="fortuneOutput" aria-live="polite"></div> <div class="controls"> <button id="crackBtn">Crack it</button> <button id="againBtn" class="ghost hidden">New Cookie</button> <button id="copyBtn" class="ghost hidden" title="Copy fortune to clipboard">Copy Fortune</button> </div> </div> <script> (function(){ const app = document.getElementById('app'); const scene = document.getElementById('scene'); const crackBtn = document.getElementById('crackBtn'); const againBtn = document.getElementById('againBtn'); const copyBtn = document.getElementById('copyBtn'); const fortuneOutput = document.getElementById('fortuneOutput'); const fortuneText = document.getElementById('fortuneText'); const crumbs = document.getElementById('crumbs'); const svgNS = 'http://www.w3.org/2000/svg'; const fortunes = [ "Your courage opens new doors.", "A tiny step today becomes a giant leap.", "Expect a pleasant surprise soon.", "Someone believes in youβbelieve back.", "Luck favors the wellβprepared mind.", "Your kindness will return multiplied.", "Adventure aheadβpack curiosity.", "Focus beats talent when talent is unfocused.", "A solved problem was once a brave start.", "You are exactly where growth begins.", "Fortune sides with those who smile.", "The best time was yesterday. The next best is now.", "A conversation changes everything.", "Your idea will spark anotherβs light.", "Momentum loves consistency.", "Make the thing you wish existed.", "Stay curious; wonder is compounding.", "Small rituals. Big results.", "Your future self is applauding.", "The winds shift in your favor." ]; let state = 'idle'; // 'idle' | 'cracking' | 'done' let lastFortune = ""; function pickFortune(){ let f; do { f = fortunes[Math.floor(Math.random()*fortunes.length)]; } while (f === lastFortune && fortunes.length > 1); lastFortune = f; return f; } function prefersReducedMotion(){ return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; } function renderWrappedText(textStr, maxWidth = 180, lineHeight = 18){ // Clear existing tspans while (fortuneText.firstChild) fortuneText.removeChild(fortuneText.firstChild); // Measurement helper const meas = document.createElementNS(svgNS, 'text'); meas.setAttribute('x', -1000); meas.setAttribute('y', -1000); meas.setAttribute('fill', 'transparent'); meas.style.fontSize = window.getComputedStyle(fortuneText).fontSize || '14px'; meas.style.fontWeight = window.getComputedStyle(fortuneText).fontWeight || '600'; meas.style.fontFamily = window.getComputedStyle(fortuneText).fontFamily; // Important: start anchor for width measurement meas.setAttribute('text-anchor', 'start'); scene.appendChild(meas); const words = textStr.split(/\s+/); const lines = []; let current = ""; const padding = 16; const targetWidth = maxWidth - padding; for (let i=0; i<words.length; i++){ const w = words[i]; if (!current){ current = w; // If single word is too long, force-break by characters meas.textContent = w; if (meas.getComputedTextLength() > targetWidth){ let chunk = ""; for (const ch of w){ meas.textContent = chunk + ch; if (meas.getComputedTextLength() > targetWidth){ lines.push(chunk); current = ch; chunk = ch; }else{ chunk += ch; } } } }else{ const test = current + " " + w; meas.textContent = test; if (meas.getComputedTextLength() <= targetWidth){ current = test; }else{ lines.push(current); current = w; // guard again for very long word meas.textContent = w; if (meas.getComputedTextLength() > targetWidth){ let chunk = ""; for (const ch of w){ meas.textContent = chunk + ch; if (meas.getComputedTextLength() > targetWidth){ lines.push(chunk); chunk = ch; }else{ chunk += ch; } } current = chunk; } } } } if (current) lines.push(current); const yStart = -((lines.length - 1) * lineHeight)/2; lines.forEach((line, idx)=>{ const tspan = document.createElementNS(svgNS, 'tspan'); tspan.setAttribute('x', 0); tspan.setAttribute('y', (yStart + idx*lineHeight).toFixed(2)); tspan.textContent = line; fortuneText.appendChild(tspan); }); // Cleanup measurer meas.remove(); } function typeToPaper(fullText){ if (prefersReducedMotion()){ renderWrappedText(fullText); fortuneOutput.textContent = fullText; return Promise.resolve(); } return new Promise(resolve=>{ let i = 0; let current = ""; const step = () => { i++; current = fullText.slice(0, i); renderWrappedText(current); fortuneOutput.textContent = current; if (i < fullText.length){ // Slightly irregular type cadence for a more βhumanβ feel const delay = 14 + Math.random()*18; setTimeout(step, delay); }else{ resolve(); } }; step(); }); } function popCrumbs(){ crumbs.innerHTML = ""; const n = 12; for (let i=0; i<n; i++){ const c = document.createElementNS(svgNS, 'circle'); c.setAttribute('cx', 200); c.setAttribute('cy', 170); c.setAttribute('r', (Math.random()*1.8 + .8).toFixed(2)); c.setAttribute('fill', i%3 ? '#d7a56a' : '#c8924f'); const dx = (Math.random()*120 - 60).toFixed(1) + 'px'; const dy = (Math.random()*80 - 40).toFixed(1) + 'px'; c.style.setProperty('--dx', dx); c.style.setProperty('--dy', dy); c.style.animation = `pop ${420 + Math.random()*320|0}ms cubic-bezier(.21,.61,.35,1) forwards`; crumbs.appendChild(c); } setTimeout(()=>{ crumbs.innerHTML = ""; }, 850); } async function crack(){ if (state !== 'idle') return; state = 'cracking'; app.classList.remove('idle'); app.classList.add('cracked'); // Micro-haptic via tiny scale nudge (visual only) app.animate([{transform:'scale(1)'},{transform:'scale(1.008)'}, {transform:'scale(1)'}], {duration:220, easing:'ease-out'}); popCrumbs(); // Choose a fortune and type it as the slip comes out const text = pickFortune(); await new Promise(r=> setTimeout(r, 340)); // sync with paper slide await typeToPaper(text); // Show controls againBtn.classList.remove('hidden'); copyBtn.classList.remove('hidden'); state = 'done'; } function reset(){ if (state === 'idle') return; state = 'idle'; // Clear text while (fortuneText.firstChild) fortuneText.removeChild(fortuneText.firstChild); fortuneOutput.textContent = ""; againBtn.classList.add('hidden'); copyBtn.classList.add('hidden'); // Reset classes app.classList.remove('cracked'); void app.offsetWidth; // reflow to reset transitions app.classList.add('idle'); } async function copyFortune(){ const txt = fortuneOutput.textContent.trim(); try{ await navigator.clipboard.writeText(txt); copyBtn.textContent = "Copied β"; setTimeout(()=> copyBtn.textContent = "Copy Fortune", 1100); }catch{ copyBtn.textContent = "Select & Copy β"; setTimeout(()=> copyBtn.textContent = "Copy Fortune", 1400); } } // Event wiring scene.addEventListener('click', ()=> (state==='idle'? crack(): reset())); crackBtn.addEventListener('click', ()=> (state==='idle'? crack(): reset())); againBtn.addEventListener('click', reset); copyBtn.addEventListener('click', copyFortune); // Keyboard access scene.addEventListener('keydown', (e)=>{ if (e.code === 'Space' || e.code === 'Enter'){ e.preventDefault(); (state==='idle'? crack(): reset()); } }); })(); </script> </body> </html>
Save changes
Create folder
writable 0777
Create
Cancel