Files
mama-box/demo/countdown/index.html

143 lines
9.2 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<title>宝宝把倒计时改了 · 创意预览</title>
<!-- 2026-10-03 创意预览(不进正式首页):「宝宝把倒计时改了」—— 设计为每天一次的小彩蛋。
头图拆层同 demo/occupy(胶囊里的「67」单独用 HTML 渲染,才能做翻牌)。
时间线:1s 后一只小胖手从羊水囊那边伸过来 → 手指按住「67」往下拨,个位 7 滚成 6(66)→
手停住、冒个问号(发现不对)→ 偷偷往上拨回 67 → 手飞快缩回去 → 胶囊旁冒一句「我有点着急。」带一滴汗。
正式上线时按北京时间「每天首次打开首页」播一次(localStorage 记日期);本预览页每次打开都播,并给「再看一次」。
?rec=1:由 __set(t) 逐帧驱动(生成动图用)。 -->
<style>
html,body{margin:0;background:#F8F3EF;-webkit-tap-highlight-color:transparent;font-family:-apple-system,"PingFang SC","Noto Sans SC",sans-serif}
.page{position:relative;max-width:480px;margin:0 auto;overflow:hidden}
.hero-wrap{position:relative;width:100%;overflow:hidden}
.stage{position:absolute;left:0;top:0;width:375px;height:270px;transform-origin:0 0;overflow:hidden}
.stage>*{position:absolute}
.bg{left:0;top:0;width:375px;height:270px}
.pill{left:14px;top:52px;width:159.69px;height:42px}
.week{left:14px;top:86px;width:110.9px;height:70px}
.prog{left:14px;top:150px;width:192px;height:36px}
/* 「67」:十位固定,个位是一条竖排数字带,translateY 滚动 */
.num{left:120.6px;top:63px;height:20px;transform-origin:50% 60%;display:flex;font:900 13.5px/20px -apple-system,"PingFang SC","Noto Sans SC",sans-serif;
color:#FFF3C4;text-shadow:0 1px 3px rgba(120,30,10,.28)}
.num .win{display:inline-block;height:20px;overflow:hidden;position:relative;width:.62em}
.num .reel{display:flex;flex-direction:column;will-change:transform}
.num .reel i{font-style:normal;height:20px;text-align:center}
.num.hl{color:#fff}
.hand{left:0;top:0;width:150px;height:46px;transform-origin:9px 14px;will-change:transform;
filter:drop-shadow(0 0 .8px #fff) drop-shadow(0 0 .6px #fff) drop-shadow(0 3px 3px rgba(120,40,20,.28))}
.q{left:0;top:0;font:900 16px/1 -apple-system,"PingFang SC",sans-serif;color:#fff;text-shadow:0 1px 3px rgba(120,30,10,.35);opacity:0}
.bubble{left:152px;top:90px;padding:7px 12px 8px 12px;border-radius:4px 14px 14px 14px;background:#fff;white-space:nowrap;
font-size:13px;font-weight:800;color:#D9583A;letter-spacing:.3px;box-shadow:0 6px 16px rgba(150,60,30,.22);
transform-origin:0 0;opacity:0;display:flex;align-items:center;gap:5px}
.bubble::after{content:"";position:absolute;left:8px;top:-5px;border:6px solid transparent;border-bottom-color:#fff;border-top:0}
.bubble svg{width:12px;height:14px}
.below{display:block;width:100%}
.replay{position:absolute;left:50%;transform:translateX(-50%);z-index:5;border:0;padding:7px 16px;border-radius:999px;
font:600 12.5px -apple-system,"PingFang SC",sans-serif;color:#C2563B;background:rgba(255,255,255,.85);
-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-shadow:0 6px 16px rgba(180,80,50,.16),inset 0 0 0 1px #fff;
opacity:0;pointer-events:none;transition:opacity .3s}
.replay.on{opacity:1;pointer-events:auto}
</style>
</head>
<body>
<div class="page">
<div class="hero-wrap" id="wrap">
<div class="stage" id="stage">
<img class="bg" src="../_hero/bg.jpg" alt="">
<img class="pill" src="../_hero/pill.png" alt="">
<div class="num" id="num"><span>6</span><span class="win"><span class="reel" id="reel"><i>8</i><i>7</i><i>6</i></span></span></div>
<img class="week" src="../_hero/week.png" alt="">
<img class="prog" src="../_hero/prog.png" alt="">
<!-- 小胖手(手指朝左):前臂从右边伸进来,食指伸直,其余手指蜷着 -->
<svg class="hand" id="hand" viewBox="0 0 150 46" aria-hidden="true">
<path d="M150 14 L58 17 Q50 18 50 23 Q50 28 58 29 L150 33 Z" fill="#FFD6BF"/>
<path d="M86 18.5 q2 4.5 0 9 M96 18.2 q2 4.8 0 9.6" stroke="#F4BCA0" stroke-width="1.8" fill="none" stroke-linecap="round"/>
<ellipse cx="44" cy="25" rx="16" ry="13" fill="#FFDCC8"/>
<ellipse cx="38" cy="34" rx="7" ry="5" fill="#FFD2BA"/>
<ellipse cx="47" cy="36" rx="6.5" ry="4.6" fill="#FFD2BA"/>
<path d="M32 31 q-3 3 1 6" stroke="#F4BCA0" stroke-width="1.6" fill="none" stroke-linecap="round"/>
<path d="M33 19 L10 15" stroke="#FFDCC8" stroke-width="9" stroke-linecap="round"/>
<ellipse cx="9.5" cy="14.5" rx="2.6" ry="2.2" fill="#FFF1E8"/>
<path d="M44 13 q6 -4 10 2" stroke="#FFDCC8" stroke-width="7" fill="none" stroke-linecap="round"/>
</svg>
<div class="q" id="q">?</div>
<div class="bubble" id="bubble">
<span>我有点着急。</span>
<svg viewBox="0 0 12 14" aria-hidden="true"><path d="M6 1 C6 1 1.5 7 1.5 9.3 A4.5 4.5 0 0 0 10.5 9.3 C10.5 7 6 1 6 1z" fill="#8FD0F2"/><ellipse cx="4.6" cy="9.4" rx="1.1" ry="1.6" fill="#fff" opacity=".8"/></svg>
</div>
</div>
<button class="replay" id="replay">再看一次</button>
</div>
<img class="below" src="../_hero/below.jpg" alt="">
</div>
<script>
(function () {
const REC = /[?&]rec=1/.test(location.search);
const $ = id => document.getElementById(id);
const wrap = $('wrap'), stage = $('stage'), hand = $('hand'), reel = $('reel'), num = $('num'), q = $('q'), bubble = $('bubble'), replay = $('replay');
function fit() {
const w = Math.min(wrap.parentNode.clientWidth, 480), s = w / 375;
stage.style.transform = 'scale(' + s + ')'; wrap.style.height = 250 * s + 'px';
replay.style.top = 214 * s + 'px';
}
fit(); addEventListener('resize', fit);
const cl = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
const seg = (t, a, b) => cl((t - a) / (b - a));
const lerp = (a, b, u) => a + (b - a) * u;
const eio = u => u < .5 ? 2 * u * u : 1 - Math.pow(-2 * u + 2, 2) / 2;
const eo = u => 1 - Math.pow(1 - u, 3);
const pop = u => u <= 0 ? 0 : 1 + Math.exp(-6 * u) * Math.sin(14 * u) * .35 - Math.exp(-14 * u);
const T = 8.4;
// 指尖坐标(头图坐标系)。手的 SVG 指尖在 (9,14)
const TIP_HOME = [236, 74], TIP_UP = [135, 61], TIP_DN = [135, 71];
function frame(t) {
let tip = TIP_HOME.slice(), ang = 0, digit = 1; // reel 位置:0=8,1=7,2=6
// 1.0–1.25 从羊水囊边探出一点,停一下;1.25–1.9 伸到「67」上方
if (t < 1.0) tip = [400, 74];
else if (t < 1.25) tip = [lerp(400, 250, eo(seg(t, 1.0, 1.25))), 74];
else if (t < 1.4) tip = [250, 74];
else if (t < 1.95) { const u = eio(seg(t, 1.4, 1.95)); tip = [lerp(250, TIP_UP[0], u), lerp(74, TIP_UP[1], u) - Math.sin(u * Math.PI) * 6]; }
// 1.95–2.35 往下拨:7 → 6
else if (t < 2.35) { const u = eio(seg(t, 1.95, 2.35)); tip = [TIP_UP[0], lerp(TIP_UP[1], TIP_DN[1], u)]; digit = 1 + u; ang = -8 * u; }
// 2.35–3.3 停住,看一眼……发现不对(问号、手抖两下)
else if (t < 3.3) { tip = TIP_DN.slice(); digit = 2; ang = -8; const w = seg(t, 2.8, 3.25); tip[0] += Math.sin(w * Math.PI * 6) * 1.6 * (1 - w); }
// 3.3–3.55 手指绕到下面,3.55–3.85 偷偷往上拨回 7
else if (t < 3.55) { const u = eio(seg(t, 3.3, 3.55)); tip = [lerp(TIP_DN[0], TIP_DN[0] + 2, u), lerp(TIP_DN[1], TIP_DN[1] + 3, u)]; digit = 2; ang = lerp(-8, 6, u); }
else if (t < 3.85) { const u = eio(seg(t, 3.55, 3.85)); tip = [TIP_DN[0] + 2, lerp(TIP_DN[1] + 3, TIP_UP[1], u)]; digit = 2 - u; ang = lerp(6, 10, u); }
// 3.85–4.25 飞快缩回去
else if (t < 4.3) { const u = seg(t, 3.9, 4.25); tip = [lerp(TIP_UP[0], 420, u * u), lerp(TIP_UP[1], 70, u)]; ang = lerp(10, 0, u); }
else tip = [420, 70];
hand.style.transform = 'translate(' + (tip[0] - 9).toFixed(1) + 'px,' + (tip[1] - 14).toFixed(1) + 'px) rotate(' + ang.toFixed(1) + 'deg)';
reel.style.transform = 'translateY(' + (-digit * 20).toFixed(2) + 'px)';
num.classList.toggle('hl', t > 1.95 && t < 3.85);
const hk = seg(t, 1.85, 2.05) * (1 - seg(t, 3.9, 4.15)); // 被拨的时候数字微微放大,好让人看见
num.style.transform = 'scale(' + (1 + .22 * hk).toFixed(3) + ')';
// 问号
const qi = seg(t, 2.55, 2.75), qo = seg(t, 3.25, 3.4);
q.style.opacity = Math.min(qi, 1 - qo);
q.style.transform = 'translate(' + (TIP_DN[0] + 16) + 'px,' + (38 - qi * 4) + 'px) scale(' + (.6 + .4 * pop(qi)).toFixed(3) + ')';
// 气泡 4.35 → 7.4
const bIn = seg(t, 4.35, 4.85), bOut = seg(t, 7.2, 7.45);
bubble.style.opacity = Math.min(cl(bIn * 3), 1 - bOut);
bubble.style.transform = 'scale(' + (bIn > 0 ? Math.max(0, pop(bIn * 1.5)) * (1 - bOut * .2) : 0).toFixed(3) + ')';
replay.classList.toggle('on', !REC && t > T - .4);
}
if (REC) { window.__set = frame; window.__T = T; frame(0); window.__ready = true; return; }
let start = performance.now();
(function loop(now) { frame(Math.min((now - start) / 1000, T)); requestAnimationFrame(loop); })(start);
replay.addEventListener('click', () => { start = performance.now() - 300; });
})();
</script>
</body>
</html>