demo/bump v3.1:右侧卡片去掉数胎动,改成「这周宝宝学会了」+ 宝宝悄悄话

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 16:41:57 +00:00
parent 83611d82c5
commit ec97586afc

View File

@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"> <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<title>宝宝在肚子里动 · 创意预览</title> <title>宝宝在肚子里动 · 创意预览</title>
<!-- 2026-10-03 创意预览(不进正式首页):「整块头图是妈妈的肚皮」v3。 <!-- 2026-10-03 创意预览(不进正式首页):「整块头图是妈妈的肚皮」v3。
头图纹理改为 JS 现场合成(canvas):肚皮底 belly_bg.jpg + LOGO + 倒计时 + 周数 + 进度条 + 右侧「今日胎动」玻璃卡。 头图纹理改为 JS 现场合成(canvas):肚皮底 belly_bg.jpg + LOGO + 倒计时 + 周数 + 进度条 + 右侧「这周的宝宝」玻璃卡。
右侧原来是胎儿插画,现在放胎动卡:宝宝每动一下,次数 +1,下面一行写「刚刚 · 小拳头顶了一下」; 右侧原来是胎儿插画,现在放「这周宝宝学会了 · 睁眼看光」+ 宝宝的一句悄悄话,顶出来/被摁回去时换话;
卡片也画在纹理里,所以被顶到时会和字一起鼓起来变形。 卡片也画在纹理里,所以被顶到时会和字一起鼓起来变形。
WebGL 把纹理当一层肉肉的肚皮: WebGL 把纹理当一层肉肉的肚皮:
· 宝宝时不时用不同部位往外顶:小脚丫(尖一点的小帐篷,顶上隐约几个脚趾)、小拳头(小而圆)、 · 宝宝时不时用不同部位往外顶:小脚丫(尖一点的小帐篷,顶上隐约几个脚趾)、小拳头(小而圆)、
@@ -145,14 +145,14 @@ void main(){
const uS = U('S'), uF = U('F'), uK = U('K'), uG = U('G'), uP = U('P'), uJ = U('J'), uT = U('Tm'); const uS = U('S'), uF = U('F'), uK = U('K'), uG = U('G'), uP = U('P'), uJ = U('J'), uT = U('Tm');
gl.uniform2f(uS, W, H); gl.uniform2f(uS, W, H);
/* ---------- 纹理:canvas 现场合成,胎动卡的数字和文案会变 ---------- */ /* ---------- 纹理:canvas 现场合成,宝宝卡的悄悄话会变 ---------- */
const tc = document.createElement('canvas'); tc.width = W * SC; tc.height = H * SC; const tc = document.createElement('canvas'); tc.width = W * SC; tc.height = H * SC;
const cx = tc.getContext('2d'); const cx = tc.getContext('2d');
const SRC = { bg: 'belly_bg.jpg', top: 'top.png', pill: '../_hero/pill.png', week: '../_hero/week.png', prog: '../_hero/prog.png' }; const SRC = { bg: 'belly_bg.jpg', top: 'top.png', pill: '../_hero/pill.png', week: '../_hero/week.png', prog: '../_hero/prog.png' };
const IMG = {}; let loaded = 0; const IMG = {}; let loaded = 0;
const tex = gl.createTexture(); let ready = false; const tex = gl.createTexture(); let ready = false;
const FONT = '-apple-system,"PingFang SC","Noto Sans SC",sans-serif'; const FONT = '-apple-system,"PingFang SC","Noto Sans SC",sans-serif';
const card = { n: 12, line: '宝宝醒着,正在活动', ago: '' }; const card = { line: '我认得妈妈的声音' };
function rr(x, y, w, h, r) { cx.beginPath(); cx.moveTo(x + r, y); cx.arcTo(x + w, y, x + w, y + h, r); cx.arcTo(x + w, y + h, x, y + h, r); cx.arcTo(x, y + h, x, y, r); cx.arcTo(x, y, x + w, y, r); cx.closePath(); } function rr(x, y, w, h, r) { cx.beginPath(); cx.moveTo(x + r, y); cx.arcTo(x + w, y, x + w, y + h, r); cx.arcTo(x + w, y + h, x, y + h, r); cx.arcTo(x, y + h, x, y, r); cx.arcTo(x, y, x + w, y, r); cx.closePath(); }
function compose() { function compose() {
cx.setTransform(SC, 0, 0, SC, 0, 0); cx.setTransform(SC, 0, 0, SC, 0, 0);
@@ -173,33 +173,32 @@ void main(){
cx.shadowColor = 'transparent'; cx.shadowColor = 'transparent';
cx.drawImage(IMG.week, 14, 86, 110.9, 70); cx.drawImage(IMG.week, 14, 86, 110.9, 70);
cx.drawImage(IMG.prog, 14, 150, 192, 36); cx.drawImage(IMG.prog, 14, 150, 192, 36);
// 「今日胎动」玻璃卡 // 「这周宝宝学会了」玻璃卡:30周的发育小变化 + 宝宝的一句悄悄话(被顶/被摁时换话)
const X = 232, Y = 62, CW = 128, CH = 118; const X = 228, Y = 60, CW = 134, CH = 122;
cx.save(); cx.save();
cx.shadowColor = 'rgba(150,55,30,.16)'; cx.shadowBlur = 18; cx.shadowOffsetY = 8; cx.shadowColor = 'rgba(150,55,30,.16)'; cx.shadowBlur = 18; cx.shadowOffsetY = 8;
rr(X, Y, CW, CH, 20); cx.fillStyle = 'rgba(255,255,255,.17)'; cx.fill(); rr(X, Y, CW, CH, 20); cx.fillStyle = 'rgba(255,255,255,.17)'; cx.fill();
cx.restore(); cx.restore();
const hl = cx.createLinearGradient(X, Y, X, Y + CH); hl.addColorStop(0, 'rgba(255,255,255,.22)'); hl.addColorStop(1, 'rgba(255,255,255,.04)'); const hl = cx.createLinearGradient(X, Y, X, Y + CH); hl.addColorStop(0, 'rgba(255,255,255,.24)'); hl.addColorStop(1, 'rgba(255,255,255,.05)');
rr(X, Y, CW, CH, 20); cx.fillStyle = hl; cx.fill(); rr(X, Y, CW, CH, 20); cx.fillStyle = hl; cx.fill();
cx.lineWidth = 1; cx.strokeStyle = 'rgba(255,255,255,.55)'; rr(X + .5, Y + .5, CW - 1, CH - 1, 19.5); cx.stroke(); cx.lineWidth = 1; cx.strokeStyle = 'rgba(255,255,255,.55)'; rr(X + .5, Y + .5, CW - 1, CH - 1, 19.5); cx.stroke();
cx.shadowColor = 'rgba(120,30,10,.25)'; cx.shadowBlur = 4; cx.shadowOffsetY = 1; cx.shadowColor = 'rgba(120,30,10,.25)'; cx.shadowBlur = 4; cx.shadowOffsetY = 1;
cx.fillStyle = '#FFF3C4'; cx.beginPath(); cx.arc(X + 17, Y + 20, 3.2, 0, 7); cx.fill(); cx.fillStyle = 'rgba(255,255,255,.9)'; cx.font = '600 11px ' + FONT; cx.fillText('这周宝宝学会了', X + 14, Y + 19);
cx.fillStyle = 'rgba(255,255,255,.95)'; cx.font = '700 12px ' + FONT; cx.fillText('今日胎动', X + 26, Y + 20.5); cx.font = '900 25px ' + FONT; cx.fillStyle = '#fff'; cx.fillText('睁眼看光', X + 13, Y + 45);
cx.font = '900 40px ' + FONT; cx.fillStyle = '#fff'; cx.font = '600 10.5px ' + FONT; cx.fillStyle = 'rgba(255,255,255,.9)'; cx.fillText('晒太阳时,我能感觉到亮', X + 14, Y + 68);
const ns = String(card.n); cx.fillText(ns, X + 14, Y + 55);
const nw = cx.measureText(ns).width;
cx.font = '700 13px ' + FONT; cx.fillText('次', X + 18 + nw, Y + 62);
cx.shadowColor = 'transparent'; cx.shadowColor = 'transparent';
cx.fillStyle = 'rgba(255,255,255,.28)'; cx.fillRect(X + 14, Y + 82, CW - 28, 1); cx.fillStyle = 'rgba(255,255,255,.28)'; cx.fillRect(X + 14, Y + 81, CW - 28, 1);
// 悄悄话:小气泡尾巴 + 一句话
cx.fillStyle = 'rgba(255,255,255,.92)'; cx.beginPath(); cx.arc(X + 18, Y + 101, 2.6, 0, 7); cx.fill();
cx.shadowColor = 'rgba(120,30,10,.22)'; cx.shadowBlur = 3; cx.shadowOffsetY = 1; cx.shadowColor = 'rgba(120,30,10,.22)'; cx.shadowBlur = 3; cx.shadowOffsetY = 1;
cx.font = '600 11px ' + FONT; cx.fillStyle = 'rgba(255,255,255,.92)'; cx.font = '600 11px ' + FONT; cx.fillStyle = 'rgba(255,255,255,.95)';
let ln = card.line; while (cx.measureText(ln).width > CW - 28 && ln.length > 2) ln = ln.slice(0, -1); let ln = card.line; while (cx.measureText(ln).width > CW - 40 && ln.length > 2) ln = ln.slice(0, -1);
cx.fillText(ln, X + 14, Y + 99); cx.fillText(ln, X + 26, Y + 101.5);
cx.shadowColor = 'transparent'; cx.shadowColor = 'transparent';
gl.bindTexture(gl.TEXTURE_2D, tex); gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, tc); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, tc);
} }
function setCard(n, line) { if (n != null) card.n = n; if (line) card.line = line; if (ready) compose(); } function setCard(line) { card.line = line; if (ready) compose(); }
Object.keys(SRC).forEach(k => { Object.keys(SRC).forEach(k => {
const im = new Image(); im.onload = () => { const im = new Image(); im.onload = () => {
IMG[k] = im; IMG[k] = im;
@@ -226,10 +225,10 @@ void main(){
/* ---------- 宝宝的各个部位:形状不同,动法不同 ---------- */ /* ---------- 宝宝的各个部位:形状不同,动法不同 ---------- */
// rx/ry 椭圆半径,sharp 尖度,toe 脚趾显隐,amp 高度,dur 停留秒数,roll 是否慢慢拱着挪 // rx/ry 椭圆半径,sharp 尖度,toe 脚趾显隐,amp 高度,dur 停留秒数,roll 是否慢慢拱着挪
const PARTS = { const PARTS = {
foot: { name: '小脚丫', verb: '蹬了一下', rx: 64, ry: 72, sharp: 1.7, toe: 1, amp: 9.5, dur: [2.4, 3.4] }, foot: { name: '小脚丫', say: ['妈妈,我在伸腿', '踢一脚打个招呼'], caught: '脚丫被你抓到啦', rx: 64, ry: 72, sharp: 1.7, toe: 1, amp: 9.5, dur: [2.4, 3.4] },
fist: { name: '小拳头', verb: '顶了一下', rx: 46, ry: 46, sharp: 1.9, toe: 0, amp: 8, dur: [1.8, 2.6] }, fist: { name: '小拳头', say: ['我在挥小拳头', '妈妈看我力气大'], caught: '小拳头被你握住啦', rx: 46, ry: 46, sharp: 1.9, toe: 0, amp: 8, dur: [1.8, 2.6] },
knee: { name: '小膝盖', verb: '拱了一下', rx: 50, ry: 86, sharp: 1.6, toe: 0, amp: 8.5, dur: [2.2, 3] }, knee: { name: '小膝盖', say: ['我在翻个身', '这里有点挤哦'], caught: '膝盖被摁回去啦', rx: 50, ry: 86, sharp: 1.6, toe: 0, amp: 8.5, dur: [2.2, 3] },
butt: { name: '小屁股', verb: '拱起来了', rx: 100, ry: 84, sharp: 1.25, toe: 0, amp: 7, dur: [3, 3.8], roll: true }, butt: { name: '小屁股', say: ['我在伸懒腰~', '换个姿势睡觉'], caught: '屁股被你拍到了', rx: 100, ry: 84, sharp: 1.25, toe: 0, amp: 7, dur: [3, 3.8], roll: true },
}; };
const ORDER = ['foot', 'fist', 'butt', 'knee']; const ORDER = ['foot', 'fist', 'butt', 'knee'];
const found = {}; const found = {};
@@ -242,8 +241,8 @@ void main(){
const jg = { x: { v: 0, vel: 0, to: 0, k: 210, c: 4.2 }, y: { v: 0, vel: 0, to: 0, k: 210, c: 4.2 } }; const jg = { x: { v: 0, vel: 0, to: 0, k: 210, c: 4.2 }, y: { v: 0, vel: 0, to: 0, k: 210, c: 4.2 } };
let mode = 'wait', wait = .5, t = 0, seq = 0; let mode = 'wait', wait = .5, t = 0, seq = 0;
// 一半以上的概率专挑字和卡片顶(大字「30」、倒计时胶囊、进度条、slogan、胎动卡),字会被撑起来变形 // 一半以上的概率专挑字和卡片顶(大字「30」、倒计时胶囊、进度条、slogan、宝宝卡),字会被撑起来变形
const TEXT_SPOTS = [[62, 120], [100, 74], [104, 124], [96, 166], [230, 26], [292, 112], [300, 84], [150, 168]]; const TEXT_SPOTS = [[62, 120], [100, 74], [104, 124], [96, 166], [230, 26], [292, 104], [300, 80], [150, 168]];
function pickSpot() { function pickSpot() {
for (let i = 0; i < 40; i++) { for (let i = 0; i < 40; i++) {
let x, y; let x, y;
@@ -264,7 +263,7 @@ void main(){
ft.h.to = P.amp; ft.h.k = P.roll ? 70 : 140; ft.h.c = P.roll ? 9 : 9; ft.h.vel += P.roll ? 30 : 120; ft.h.to = P.amp; ft.h.k = P.roll ? 70 : 140; ft.h.c = P.roll ? 9 : 9; ft.h.vel += P.roll ? 30 : 120;
ft.life = 0; ft.dur = P.dur[0] + Math.random() * (P.dur[1] - P.dur[0]); ft.kicked = false; ft.life = 0; ft.dur = P.dur[0] + Math.random() * (P.dur[1] - P.dur[0]); ft.kicked = false;
mode = 'show'; mode = 'show';
setCard(card.n + 1, '刚刚' + P.name + P.verb); setCard(P.say[Math.floor(Math.random() * P.say.length)]);
} }
function hide(fast) { function hide(fast) {
ft.h.to = 0; ft.h.to = 0;
@@ -283,7 +282,7 @@ void main(){
ft.flee = { dx: dx / l, dy: dy / l, t: 0 }; ft.flee = { dx: dx / l, dy: dy / l, t: 0 };
hide(true); mode = 'flee'; wait = 1.1 + Math.random() * .8; hide(true); mode = 'flee'; wait = 1.1 + Math.random() * .8;
if (navigator.vibrate) navigator.vibrate(15); if (navigator.vibrate) navigator.vibrate(15);
setCard(null, ft.part.name + '被摁回去啦'); setCard(ft.part.caught);
found[ft.key] = true; found[ft.key] = true;
const chip = hunt.querySelector('[data-k="' + ft.key + '"]'); if (chip) chip.classList.add('on'); const chip = hunt.querySelector('[data-k="' + ft.key + '"]'); if (chip) chip.classList.add('on');
hunt.classList.add('pop'); setTimeout(() => hunt.classList.remove('pop'), 300); hunt.classList.add('pop'); setTimeout(() => hunt.classList.remove('pop'), 300);