Siteβ―Builder
Editing:
cookie.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 β Crinkly Paper Edition</title> <style> :root{ --bg1:#0f1222; --bg2:#1c2038; --gold1:#e6b66a; --gold2:#c48a3a; --gold3:#8a5f2a; --paper:#ffffff; --paperEdge:#e8e8e8; --paperShade:#d7d7d7; --ink:#111; --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, 680px); 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);} p.sub{margin:0 0 10px; opacity:.7; font-size:14px;} /* SVG */ #scene{ width:min(92vw, 560px); height:auto; display:block; margin-inline:auto; cursor:pointer; touch-action:manipulation; } /* Buttons */ .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;} /* Idle wobble */ .idle #cookie { animation: idleWobble 1.8s ease-in-out infinite; transform-origin: 200px 170px; } @keyframes idleWobble{ 0%,100%{transform:rotate(0) translateY(0)} 50%{transform:rotate(.7deg) translateY(-1.2px)} } /* Cookie halves + crack */ #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 wrap (animation container for paper + text) */ #paperWrap{ opacity:0; transform: translate(0px, 6px) scale(.96) rotate(-1deg); transform-box: view-box; transform-origin:center; transition: opacity .35s ease .15s, transform .7s cubic-bezier(.18,.86,.22,1) .15s; pointer-events:none; /* purely visual */ } /* In cracked stateβ¦ */ .cracked #cookie .left{ transform: translateX(-28px) rotate(-16deg); } .cracked #cookie .right{ transform: translateX(28px) rotate(16deg); } .cracked #crackLine{ opacity:.65; stroke-dashoffset:0; } .cracked #paperWrap{ opacity:1; transform: translate(0px, -74px) scale(1) rotate(0deg); } /* 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;} } /* Paper + text styling */ #fortuneText{ font-size:14px; fill:var(--ink); font-weight:700; letter-spacing:.15px; } #fortuneOutput{ margin-top:8px; min-height:1.4em; color:#dfe3ff; font-weight:500; letter-spacing:.2px; } .cursor::after{ content:"β"; animation: blink 1s step-end infinite; } @keyframes blink{ 50%{opacity:0;} } /* Reduced motion */ @media (prefers-reduced-motion: reduce){ .idle #cookie{ animation:none; } #cookie .half, #paperWrap{ 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> <!-- Cookie shading --> <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> <!-- Generic shadow --> <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> <!-- Paper drop shadow (tighter) --> <filter id="paperShadow" x="-50%" y="-120%" width="200%" height="260%"> <feDropShadow dx="0" dy="2" stdDeviation="2.2" flood-opacity=".25" flood-color="#000"/> <feDropShadow dx="0" dy="7" stdDeviation="8" flood-opacity=".12" flood-color="#000"/> </filter> <!-- Paper crinkle: subtle displacement using fractal noise --> <filter id="paperCrinkle" x="-20%" y="-40%" width="140%" height="180%"> <feTurbulence type="fractalNoise" baseFrequency="0.03 0.12" numOctaves="3" seed="7" result="turb"/> <feDisplacementMap in="SourceGraphic" in2="turb" scale="3.5" xChannelSelector="R" yChannelSelector="G"/> </filter> <!-- Paper grain overlay: very light speckle --> <filter id="paperGrain" x="-10%" y="-10%" width="120%" height="120%"> <feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="1" seed="3" result="noise"/> <feColorMatrix type="matrix" values=" 0 0 0 0 0.08 0 0 0 0 0.08 0 0 0 0 0.08 0 0 0 0.25 0" /> <feBlend in="SourceGraphic" in2="noise" mode="multiply"/> </filter> <!-- Faint top crease gradient --> <linearGradient id="foldShading" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" stop-color="#000" stop-opacity=".10"/> <stop offset=".5" stop-color="#000" stop-opacity="0"/> <stop offset="1" stop-color="#fff" stop-opacity=".06"/> </linearGradient> </defs> <!-- Ground shadow --> <ellipse cx="200" cy="230" rx="95" ry="20" fill="rgba(0,0,0,.35)" /> <!-- Cookie --> <g id="cookie" filter="url(#shadow)"> <g class="half left"> <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" /> <path d="M175,150 C160,150 145,164 147,178" fill="none" stroke="#ffd89b" stroke-width="2" opacity=".35"/> </g> <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> <path id="crackLine" d="M200 142 L200 195" stroke="var(--gold3)" stroke-width="2.2" stroke-linecap="round"/> </g> <!-- PAPER: a long skinny slip that pops above the cookie --> <g id="paperWrap" filter="url(#paperShadow)"> <!-- Important: paperBase gets the crinkle; text stays crisp --> <g id="paperBase" filter="url(#paperCrinkle)" transform="translate(200 165)"> <!-- Long skinny rounded rect --> <rect x="-130" y="-18" rx="5" ry="5" width="260" height="36" fill="var(--paper)" stroke="var(--paperEdge)" /> <!-- Soft fold/relief shading overlay --> <rect x="-130" y="-18" rx="5" ry="5" width="260" height="36" fill="url(#foldShading)" opacity=".6"/> <!-- Very subtle deckled edge: wavy strokes along long edges --> <path d="M -126 -17 Q -95 -15 -64 -16 Q -32 -18 0 -16 Q 32 -14 64 -15 Q 95 -17 126 -16" stroke="var(--paperShade)" stroke-opacity=".25" stroke-width="1" fill="none"/> <path d="M -126 17 Q -95 15 -64 16 Q -32 18 0 16 Q 32 14 64 15 Q 95 17 126 16" stroke="var(--paperShade)" stroke-opacity=".22" stroke-width="1" fill="none"/> <!-- Grain overlay (ultra light) --> <rect x="-130" y="-18" width="260" height="36" fill="#fff" opacity=".15" filter="url(#paperGrain)"/> </g> <!-- Fortune text (crisp, not displaced) --> <g transform="translate(200 165)"> <text id="fortuneText" text-anchor="middle" dominant-baseline="middle"></text> </g> </g> <!-- Crumbs --> <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'; /* Editable fortunes */ 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'; 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; } /* Types text into the paper, keeping lines wrapped inside 240px width */ function renderWrappedText(textStr, maxWidth = 240, lineHeight = 18){ while (fortuneText.firstChild) fortuneText.removeChild(fortuneText.firstChild); 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 || '700'; meas.style.fontFamily = window.getComputedStyle(fortuneText).fontFamily; 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; 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; } }else{ const test = current + " " + w; meas.textContent = test; if (meas.getComputedTextLength() <= targetWidth){ current = test; }else{ lines.push(current); current = w; 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); }); meas.remove(); } function typeToPaper(fullText){ if (prefersReducedMotion()){ renderWrappedText(fullText); fortuneOutput.textContent = fullText; return Promise.resolve(); } return new Promise(resolve=>{ let i = 0; const step = () => { i++; const current = fullText.slice(0, i); renderWrappedText(current); fortuneOutput.textContent = current; if (i < fullText.length){ const delay = 14 + Math.random()*18; // human-ish typing 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'); // tiny haptic feel app.animate([{transform:'scale(1)'},{transform:'scale(1.008)'},{transform:'scale(1)'}], {duration:220, easing:'ease-out'}); popCrumbs(); // pick & type fortune after the paper fades/pops in const text = pickFortune(); await new Promise(r=> setTimeout(r, 340)); // sync with paper pop await typeToPaper(text); againBtn.classList.remove('hidden'); copyBtn.classList.remove('hidden'); state = 'done'; } function reset(){ if (state === 'idle') return; state = 'idle'; while (fortuneText.firstChild) fortuneText.removeChild(fortuneText.firstChild); fortuneOutput.textContent = ""; againBtn.classList.add('hidden'); copyBtn.classList.add('hidden'); app.classList.remove('cracked'); void app.offsetWidth; // reflow 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); } } // Events scene.addEventListener('click', ()=> (state==='idle'? crack(): reset())); crackBtn.addEventListener('click', ()=> (state==='idle'? crack(): reset())); againBtn.addEventListener('click', reset); copyBtn.addEventListener('click', copyFortune); 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