悄悄话流星十一改:划走的星消失、点星光波碎星与星尘、合成音效与开关、全部落完收尾 (v26100175)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 07:58:33 +00:00
parent 534ccc3ca6
commit a68f39040b
3 changed files with 147 additions and 5 deletions

View File

@@ -7,7 +7,7 @@
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>兜爸心选 · 孕育陪伴</title>
<!-- V:26100174 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
<!-- V:26100175 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
<link rel="stylesheet" href="mama-box.css">
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<!-- 共享助手:授权/跳商城/位置上报(分享卡片同步,见 docs/h5-auth-standard.md 第四节) -->
@@ -1780,10 +1780,12 @@
<svg viewBox="0 0 24 24" width="20" height="20"><path d="M15 5l-7 7 7 7" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="whp-tag" id="whpTag"></span>
<button class="whv-snd" id="whvSnd" type="button" aria-label="关闭声音"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9.5h3.5L12 5.5v13l-4.5-4H4z" fill="currentColor" stroke="none"/><path class="on" d="M15.5 9a4 4 0 010 6M18 6.5a7.5 7.5 0 010 11"/><path class="off" d="M16 9.5l5 5M21 9.5l-5 5"/></svg></button>
<button class="whv-all" id="whvAll" type="button">全部</button>
</header>
<div class="wsk-title" id="wskTitle"></div>
<p class="wsk-tip" id="wskTip"><i></i>点一下小星星,听听她们说了什么</p>
<div class="wsk-done" id="wskDone"><p>星星都落下来啦<br><span>她们的话,你都听到了</span></p><button type="button" id="wskBack">让星星回来</button></div>
<div class="wsc" id="wsc" aria-live="polite"></div>
<h2 class="sr-only" id="whpTitle">同孕周悄悄话</h2>
<div class="whd" id="whDrawer" aria-hidden="true">

118
js/app.js
View File

@@ -3,7 +3,7 @@
(function () {
'use strict';
/* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */
var VER = '26100174';
var VER = '26100175';
const $ = (s, c) => (c || document).querySelector(s);
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));
@@ -3376,7 +3376,7 @@
const x = 36 + ((c + .5 + ((h % 100) / 100 - .5) * .7) / cols) * 58;
const y = 21 + ((r + .5 + (((h >> 8) % 100) / 100 - .5) * .6) / rows) * 38;
const sz = it.mine ? 26 : Math.round(15 + Math.min(it.hugs || 0, 30) / 30 * 11);
return '<button type="button" class="wsk-st' + (it.mine ? ' mine' : '') + (seen.has(it.id) ? ' seen' : '') + (i === fresh && !seen.size ? ' hint' : '') +
return '<button type="button" class="wsk-st' + (it.mine ? ' mine' : '') + (seen.has(it.id) ? ' used' : '') + (i === fresh && !seen.size ? ' hint' : '') +
'" data-go="' + (i + 1) + '" style="left:' + x.toFixed(1) + '%;top:' + y.toFixed(1) + '%;--s:' + sz + 'px;--tw:' + (2 + (h % 30) / 10).toFixed(1) + 's" aria-label="' + esc(it.text) + '"><i></i></button>';
}).join('');
}
@@ -3407,10 +3407,106 @@
cancelAnimationFrame(m.raf);
m.el.remove();
m.btn.classList.remove('flare', 'gone');
m.btn.classList.add('seen');
m.btn.classList.add('used');
mts.delete(m.i);
if (done) paintCard(m.it, m.i);
checkDone();
}
/* 十一改(用户:「被点掉的流星划过之后,就不要再出现在原来的位置;点完之后给一些动效、声音」):
· 划走的星从夜空里消失(.used),全部落完 → 「星星都落下来啦」+「让星星回来」;
· 点下去:一圈光波 + 一把碎星迸开;飞行中头后撒星尘;
· 声音用 WebAudio 现合成,不加载音频文件:点星 = 一声随机五声音阶的「叮」(连点几颗像在弹琴),
唰出去 = 一道由低到高的风声;右上角喇叭可关,记在本地。 */
function checkDone() {
const all = $$('.wsk-st', page);
$('#wskDone').classList.toggle('show', all.length > 0 && all.every(b => b.classList.contains('used')) && !mts.size);
}
const SND = 'mmbox.whsnd.v1';
let actx = null, sndOn = true;
try { sndOn = localStorage.getItem(SND) !== '0'; } catch (e) {}
const sndBtn = $('#whvSnd');
const paintSnd = () => { sndBtn.classList.toggle('off', !sndOn); sndBtn.setAttribute('aria-label', sndOn ? '关闭声音' : '打开声音'); };
paintSnd();
sndBtn.addEventListener('click', () => {
sndOn = !sndOn;
try { localStorage.setItem(SND, sndOn ? '1' : '0'); } catch (e) {}
paintSnd();
haptic();
if (sndOn) chime();
});
function ac() {
if (!sndOn) return null;
try {
actx = actx || new (window.AudioContext || window.webkitAudioContext)();
if (actx.state === 'suspended') actx.resume();
} catch (e) { return null; }
return actx;
}
const NOTES = [1046.5, 1174.7, 1318.5, 1568, 1760, 2093];
function chime() {
const c = ac();
if (!c) return;
const t = c.currentTime, f = NOTES[Math.floor(Math.random() * NOTES.length)];
const g = c.createGain();
g.gain.setValueAtTime(0, t);
g.gain.linearRampToValueAtTime(.16, t + .012);
g.gain.exponentialRampToValueAtTime(.001, t + 1.8);
g.connect(c.destination);
[[f, 'sine', 1], [f * 2, 'sine', .28], [f * 3.01, 'triangle', .08]].forEach(([fr, ty, v]) => {
const o = c.createOscillator(), og = c.createGain();
o.type = ty; o.frequency.value = fr; og.gain.value = v;
o.connect(og); og.connect(g); o.start(t); o.stop(t + 1.9);
});
}
function whoosh() {
const c = ac();
if (!c) return;
const t = c.currentTime, d = .9;
const buf = c.createBuffer(1, Math.floor(c.sampleRate * d), c.sampleRate), ch = buf.getChannelData(0);
for (let k = 0; k < ch.length; k++) ch[k] = Math.random() * 2 - 1;
const src = c.createBufferSource(), bp = c.createBiquadFilter(), g = c.createGain();
src.buffer = buf;
bp.type = 'bandpass'; bp.Q.value = 1.4;
bp.frequency.setValueAtTime(500, t);
bp.frequency.exponentialRampToValueAtTime(3800, t + d * .75);
g.gain.setValueAtTime(0, t);
g.gain.linearRampToValueAtTime(.1, t + d * .5);
g.gain.exponentialRampToValueAtTime(.001, t + d);
src.connect(bp); bp.connect(g); g.connect(c.destination);
src.start(t); src.stop(t + d);
}
function fx(cls, x, y, ms, set) {
const e = document.createElement('i');
e.className = cls;
e.style.left = x.toFixed(1) + 'px';
e.style.top = y.toFixed(1) + 'px';
if (set) set(e);
page.insertBefore(e, card);
setTimeout(() => e.remove(), ms);
}
function tapFx(x, y, gold) {
if (RM) return;
fx('wfx-ring' + (gold ? ' gold' : ''), x, y, 800);
for (let k = 0; k < 10; k++) {
const a = Math.PI * 2 * k / 10 + Math.random() * .5, r = 26 + Math.random() * 30;
fx('wfx-sp' + (gold ? ' gold' : ''), x, y, 900, e => {
e.style.setProperty('--x', (Math.cos(a) * r).toFixed(0) + 'px');
e.style.setProperty('--y', (Math.sin(a) * r).toFixed(0) + 'px');
e.style.setProperty('--z', (5 + Math.random() * 7).toFixed(0) + 'px');
e.style.animationDelay = (Math.random() * 80).toFixed(0) + 'ms';
});
}
}
$('#wskBack').addEventListener('click', () => {
haptic();
seen.clear();
$('#wskDone').classList.remove('show');
paintSky(list());
$('#wskBright').classList.remove('back');
void $('#wskBright').offsetWidth;
$('#wskBright').classList.add('back');
chime();
});
const stopAll = () => [...mts.values()].forEach(m => stopMeteor(m));
/* 第一次进来:一闪一闪的提示,点过一次星星就永久收起 */
const TIP = 'mmbox.whtip.v1';
@@ -3432,7 +3528,7 @@
const btn = $('.wsk-st[data-go="' + i + '"]', page);
if (!btn) { paintCard(it, i); return; }
btn.classList.add('cur');
if (RM) { btn.classList.add('seen'); paintCard(it, i); return; }
if (RM) { btn.classList.add('used'); paintCard(it, i); checkDone(); return; }
const pr = page.getBoundingClientRect(), br = btn.getBoundingClientRect();
const x0 = br.left + br.width / 2 - pr.left, y0 = br.top + br.height / 2 - pr.top;
/* 瞄准左下(屏幕左边外、六成高处):越靠左起步角度越陡,保证在屏幕里拖得够长 */
@@ -3457,6 +3553,11 @@
/* 节奏:星闪 420ms → 以约 90px/s 起步并缓缓提速,整句拖出来后再留 0.7 秒给眼睛 → 加速度猛增,1.3 秒内唰出屏幕 */
const T0 = 420, v0 = .09, TR = (body + 20) / v0 + 700, A = .0018, TE = 1300;
btn.classList.add('flare');
btn.classList.remove('used');
$('#wskDone').classList.remove('show');
chime();
tapFx(x0, y0, it.mine);
let swish = false, dustT = 0;
const t0 = performance.now();
const frame = now => {
const t = now - t0;
@@ -3465,6 +3566,7 @@
const tc = t - T0, dt = Math.max(0, tc - TR), u = Math.min(1, dt / TE);
const s = v0 * tc + v0 * tc * tc / 16000 + .5 * A * dt * dt;
const v = v0 * (1 + tc / 8000) + A * dt;
if (dt > 0 && !swish) { swish = true; whoosh(); }
const hx = x0 + dx * s, hy = y0 + dy * s, at = 'translate(' + hx.toFixed(1) + 'px,' + hy.toFixed(1) + 'px) rotate(' + rot.toFixed(1) + 'deg)';
const st = Math.min(1.6, Math.max(0, (v - .25) / .5));
head.style.transform = at + ' scaleX(' + (1 + st * 1.6).toFixed(2) + ')';
@@ -3479,6 +3581,13 @@
c.style.opacity = (Math.min(1, d / 26) * fade).toFixed(2);
c.style.transform = 'translate(' + (x0 + dx * d).toFixed(1) + 'px,' + (y0 + dy * d).toFixed(1) + 'px) translate(-50%,-50%) rotate(' + rot.toFixed(1) + 'deg)';
});
if (t - dustT > 70 && u < .7 && hx > -20 && hy < pr.height + 20) {
dustT = t;
fx('wfx-dust' + (it.mine ? ' gold' : ''), hx + (Math.random() - .5) * 10, hy + (Math.random() - .5) * 10, 1000, e => {
e.style.setProperty('--z', (3 + Math.random() * 5).toFixed(0) + 'px');
e.style.setProperty('--y', (8 + Math.random() * 14).toFixed(0) + 'px');
});
}
if (u >= 1) { stopMeteor(m, true); return; }
m.raf = requestAnimationFrame(frame);
};
@@ -3525,6 +3634,7 @@
card.classList.remove('show');
paintPage();
tip(true);
checkDone();
page.classList.add('show');
page.setAttribute('aria-hidden', 'false');
drawer(false);

View File

@@ -4000,3 +4000,33 @@ a.skip:focus{left:16px;top:16px}
.wmt-l::before{background:linear-gradient(90deg,#fff,rgba(255,255,255,.75) 25%,rgba(245,238,255,.4) 60%,rgba(255,255,255,.08) 92%,transparent)}
.wmt span{text-shadow:0 0 1px rgba(60,30,110,.95),0 0 3px rgba(60,30,110,.7),0 0 12px rgba(255,255,255,.85),0 0 24px rgba(255,255,255,.5)}
.wmt.mine span{text-shadow:0 0 1px rgba(110,60,20,.9),0 0 3px rgba(110,60,20,.6),0 0 12px rgba(255,236,200,.85),0 0 24px rgba(255,210,150,.5)}
/* 十一改:划走的星消失;点星光波 + 碎星;飞行星尘;声音开关;全部落完 */
.wsk-st.used{opacity:0;visibility:hidden;pointer-events:none;transition:opacity .6s,visibility .6s}
.wsk-st.flare,.wsk-st.flare.used{visibility:visible;opacity:1}
.wsk-st.flare.gone{opacity:0}
.wsk-bright.back .wsk-st{animation:wskBack .9s cubic-bezier(.2,.8,.2,1) both}
@keyframes wskBack{from{opacity:0;transform:translateY(-30px) scale(.3)}to{opacity:1;transform:none}}
.wfx-ring,.wfx-sp,.wfx-dust{position:absolute;z-index:2;pointer-events:none}
.wfx-ring{width:16px;height:16px;margin:-8px 0 0 -8px;border-radius:50%;border:2px solid rgba(255,255,255,.9);
box-shadow:0 0 12px rgba(255,255,255,.8),inset 0 0 8px rgba(255,255,255,.6);animation:wfxRing .8s cubic-bezier(.2,.8,.2,1) forwards}
.wfx-ring.gold{border-color:rgba(255,220,160,.95);box-shadow:0 0 12px rgba(255,200,120,.8),inset 0 0 8px rgba(255,200,120,.6)}
@keyframes wfxRing{0%{transform:scale(.4);opacity:1}100%{transform:scale(6);opacity:0;border-width:.5px}}
.wfx-sp{width:var(--z);height:var(--z);margin:calc(var(--z) / -2) 0 0 calc(var(--z) / -2);background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cdefs%3E%3CradialGradient id='g'%3E%3Cstop offset='0' stop-color='%23FFF4E6' stop-opacity='.85'/%3E%3Cstop offset='1' stop-color='%23FFF4E6' stop-opacity='0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Ccircle cx='10' cy='10' r='10' fill='url(%23g)' opacity='.45'/%3E%3Cpath d='M10 .5C10.9 7.4 12.6 9.1 19.5 10 12.6 10.9 10.9 12.6 10 19.5 9.1 12.6 7.4 10.9.5 10 7.4 9.1 9.1 7.4 10 .5Z' fill='%23FFFFFF'/%3E%3C/svg%3E") center/contain no-repeat;
filter:drop-shadow(0 0 3px #fff);animation:wfxSp .85s cubic-bezier(.15,.7,.3,1) both}
.wfx-sp.gold,.wfx-dust.gold{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cdefs%3E%3CradialGradient id='g'%3E%3Cstop offset='0' stop-color='%23FFC27A' stop-opacity='.85'/%3E%3Cstop offset='1' stop-color='%23FFC27A' stop-opacity='0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Ccircle cx='10' cy='10' r='10' fill='url(%23g)' opacity='.45'/%3E%3Cpath d='M10 .5C10.9 7.4 12.6 9.1 19.5 10 12.6 10.9 10.9 12.6 10 19.5 9.1 12.6 7.4 10.9.5 10 7.4 9.1 9.1 7.4 10 .5Z' fill='%23FFE2A8'/%3E%3C/svg%3E")}
@keyframes wfxSp{0%{transform:translate(0,0) scale(.3) rotate(0);opacity:1}70%{opacity:1}100%{transform:translate(var(--x),var(--y)) scale(1) rotate(90deg);opacity:0}}
.wfx-dust{width:var(--z);height:var(--z);margin:calc(var(--z) / -2) 0 0 calc(var(--z) / -2);background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cdefs%3E%3CradialGradient id='g'%3E%3Cstop offset='0' stop-color='%23FFF4E6' stop-opacity='.85'/%3E%3Cstop offset='1' stop-color='%23FFF4E6' stop-opacity='0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Ccircle cx='10' cy='10' r='10' fill='url(%23g)' opacity='.45'/%3E%3Cpath d='M10 .5C10.9 7.4 12.6 9.1 19.5 10 12.6 10.9 10.9 12.6 10 19.5 9.1 12.6 7.4 10.9.5 10 7.4 9.1 9.1 7.4 10 .5Z' fill='%23FFFFFF'/%3E%3C/svg%3E") center/contain no-repeat;animation:wfxDust 1s ease-out forwards}
@keyframes wfxDust{0%{opacity:.95;transform:scale(1)}100%{opacity:0;transform:translateY(var(--y)) scale(.3) rotate(60deg)}}
.whv-snd{display:grid;place-items:center;width:32px;height:32px;margin-left:auto;border-radius:50%;background:rgba(255,255,255,.12);color:#fff;backdrop-filter:blur(6px)}
.whv-snd .off,.whv-snd.off .on{display:none}
.whv-snd.off .off{display:inline}
.whv-snd.off{color:rgba(255,255,255,.55)}
.whv-snd+.whv-all{margin-left:8px}
.wsk-done{position:absolute;left:0;right:0;top:34%;z-index:3;text-align:center;opacity:0;visibility:hidden;transform:translateY(10px);transition:opacity .6s,transform .6s,visibility .6s}
.wsk-done.show{opacity:1;visibility:visible;transform:none}
.wsk-done p{margin:0;font-size:19px;font-weight:300;letter-spacing:2px;line-height:1.7;color:#FFF3E2;text-shadow:0 0 12px rgba(255,220,180,.6)}
.wsk-done p span{font-size:13px;color:rgba(255,255,255,.6)}
.wsk-done button{margin-top:16px;height:34px;padding:0 18px;border-radius:99px;font-size:13px;font-weight:600;color:#fff;
background:rgba(255,255,255,.12);box-shadow:inset 0 0 0 1px rgba(255,255,255,.3),0 0 18px rgba(255,230,200,.25)}
@media (prefers-reduced-motion:reduce){.wsk-bright.back .wsk-st{animation:none}}