创意预览页:宝宝占领首页(demo/occupy)、宝宝把倒计时改了(demo/countdown),头图分层素材 demo/_hero

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 15:30:11 +00:00
parent b017cccfae
commit c692d25576
7 changed files with 353 additions and 0 deletions

142
demo/countdown/index.html Normal file
View File

@@ -0,0 +1,142 @@
<!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>