From 50f1e09569543a172265175036d4e568a68533f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Mon, 3 Aug 2026 12:34:55 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E7=A4=BC=E7=89=A9=E7=89=B9=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hunliji-api/router/routers.go | 4 +- .../src/components/GiftEffect.vue | 719 +++++++++++------- vite-tailwindcss/src/utils/giftTypes.js | 4 +- vite-tailwindcss/src/views/index/index.vue | 44 +- 4 files changed, 505 insertions(+), 266 deletions(-) diff --git a/hunliji-api/router/routers.go b/hunliji-api/router/routers.go index 4a1a3d5..7fbc051 100644 --- a/hunliji-api/router/routers.go +++ b/hunliji-api/router/routers.go @@ -86,16 +86,16 @@ func loadGiftCounts() map[string]int64 { return counts } -// defaultGiftCosts 默认三阶梯:50 / 100 / 200 +// defaultGiftCosts 默认三阶梯:50 / 100 / 200(永结同心为终章,默认 200) var defaultGiftCosts = map[string]int64{ "520": 50, "forever": 50, "bald": 50, - "concentric": 50, "1314": 100, "heaven": 100, "moisten": 100, "match": 200, + "concentric": 200, } const ( diff --git a/vite-tailwindcss/src/components/GiftEffect.vue b/vite-tailwindcss/src/components/GiftEffect.vue index a2e88d5..d700a7e 100644 --- a/vite-tailwindcss/src/components/GiftEffect.vue +++ b/vite-tailwindcss/src/components/GiftEffect.vue @@ -10,166 +10,211 @@ import { isIOS } from '@/composables/useAndroidOptimize' const emit = defineEmits(['play-start']) const canvasRef = ref(null) -const MAX_PARTICLES = isIOS ? 100 : 280 +const MAX_PARTICLES = isIOS ? 72 : 180 const MAX_RINGS = isIOS ? 6 : 12 const MAX_TEXTS = 3 const DPR_CAP = isIOS ? 1.5 : 2 -const SHADOW_MUL = isIOS ? 0.2 : 1 +const SHADOW_MUL = isIOS ? 0 : 0.55 const SKIP_AURA = isIOS -const PARTICLE_SCALE = isIOS ? 0.55 : 1 +const PARTICLE_SCALE = isIOS ? 0.5 : 0.9 +/** + * 高级婚礼质感:香槟玫瑰配色 + 书法层次;粒子 / 爆闪 / 冲击环保持存在感 + * secondaryDelays: 二次点缀 + */ const GIFT_STYLE = { '520': { - duration: 3000, - fontSizeRatio: 0.24, - fontSizeMax: 120, - peakScale: 1.42, - flash: 0.75, - flashMs: 220, - flashColor: 'rgba(255, 80, 130, 0.55)', + duration: 3600, + fontSizeRatio: 0.22, + fontSizeMax: 108, + peakScale: 1.22, + flash: 0.42, + flashMs: 260, + flashColor: 'rgba(255, 200, 214, 0.55)', particleMode: 'heartBurst', - palette: ['#ff2442', '#ff2d55', '#ff6b8a', '#ff4770', '#ff1a3d'], - glowPalette: ['#ff8da6', '#ffb0c4', '#ff6b9d'], - gradient: ['#ffb3c6', '#ff2d55', '#e61a3d', '#b3001e'], - glowColor: '#ff6b9d', - strokeColor: 'rgba(255,255,255,0.85)', + palette: ['#e8a0b0', '#d4788c', '#c45c72', '#f0c4ce', '#b84d63'], + glowPalette: ['#f5d0d8', '#ffe8ee'], + gradient: ['#fff0f3', '#f0b8c4', '#d4788c', '#9e3d52'], + glowColor: '#e8a8b6', + strokeColor: 'rgba(255,248,250,0.92)', + underStroke: 'rgba(90,40,55,0.35)', showHeartOutline: false, + showInterlockHearts: false, orb: false, aura: true, - secondaryBurst: true, + shockwaves: 2, + secondaryDelays: [480], + iosSecondary: false, + charGap: 0.08, }, '1314': { - duration: 4000, - fontSizeRatio: 0.26, - fontSizeMax: 136, - peakScale: 1.55, - flash: 0.9, - flashMs: 320, - flashColor: 'rgba(255, 220, 100, 0.6)', + duration: 4400, + fontSizeRatio: 0.24, + fontSizeMax: 120, + peakScale: 1.26, + flash: 0.5, + flashMs: 300, + flashColor: 'rgba(255, 232, 180, 0.6)', particleMode: 'goldenRain', - palette: ['#ffd700', '#ffcc00', '#ffb800', '#ffe44d', '#ffab00'], - glowPalette: ['#ffe88d', '#fff0a8', '#fff5cc'], - gradient: ['#fff8c0', '#ffd700', '#ffb800', '#e6a000'], - glowColor: '#ffe88d', - strokeColor: 'rgba(255,255,255,0.92)', + palette: ['#e8c878', '#d4b060', '#c9a24a', '#f0e0a8', '#b8923a'], + glowPalette: ['#f5e6b8', '#fff6d6'], + gradient: ['#fff8e6', '#f0d78a', '#d4b060', '#9a7a28'], + glowColor: '#e8d090', + strokeColor: 'rgba(255,252,240,0.95)', + underStroke: 'rgba(90,70,20,0.3)', showHeartOutline: true, + showInterlockHearts: false, orb: true, aura: true, - secondaryBurst: true, + shockwaves: 3, + secondaryDelays: [560], + iosSecondary: false, + charGap: 0.1, }, forever: { - duration: 3600, + duration: 4000, fontSizeRatio: 0.17, - fontSizeMax: 78, - peakScale: 1.32, - flash: 0.6, - flashMs: 260, - flashColor: 'rgba(196, 30, 58, 0.45)', + fontSizeMax: 76, + peakScale: 1.18, + flash: 0.38, + flashMs: 280, + flashColor: 'rgba(255, 210, 220, 0.5)', particleMode: 'dualWave', - palette: ['#c41e3a', '#9b1b30', '#e63946', '#ff6b6b', '#ff2442'], - glowPalette: ['#ff8a9a', '#ffb3c1'], - gradient: ['#ffb3c1', '#c41e3a', '#9b1b30', '#6b0f1a'], - glowColor: '#ff4d6d', - strokeColor: 'rgba(255,240,240,0.88)', + palette: ['#c97a86', '#b85f6c', '#a84d5c', '#e0a8b0', '#8f3d4a'], + glowPalette: ['#f0c8d0', '#ffe4e8'], + gradient: ['#fff2f4', '#e0a0aa', '#b85f6c', '#7a303c'], + glowColor: '#d4909c', + strokeColor: 'rgba(255,245,245,0.92)', + underStroke: 'rgba(70,30,40,0.32)', showHeartOutline: false, + showInterlockHearts: false, orb: false, aura: true, - secondaryBurst: true, + shockwaves: 2, + secondaryDelays: [520], + iosSecondary: false, + charGap: 0.06, }, bald: { - duration: 3800, + duration: 4200, fontSizeRatio: 0.17, - fontSizeMax: 78, - peakScale: 1.26, - flash: 0.45, + fontSizeMax: 76, + peakScale: 1.16, + flash: 0.34, flashMs: 300, - flashColor: 'rgba(245, 240, 224, 0.5)', + flashColor: 'rgba(255, 248, 235, 0.48)', particleMode: 'softPetals', - palette: ['#f5f0e0', '#e8e4d9', '#d4c5a0', '#c9b896', '#fffaf0'], - glowPalette: ['#fffaf0', '#f0e6c8'], - gradient: ['#ffffff', '#f5f0e0', '#d4c5a0', '#b8a878'], - glowColor: '#f5f0e0', - strokeColor: 'rgba(180,160,100,0.55)', + palette: ['#f2ebe0', '#e6dcc8', '#d4c4a4', '#c8b890', '#f8f4ec'], + glowPalette: ['#fffaf2', '#f0e6d4'], + gradient: ['#ffffff', '#f4efe6', '#ddd0b4', '#b8a878'], + glowColor: '#ebe2d2', + strokeColor: 'rgba(255,252,245,0.9)', + underStroke: 'rgba(100,85,55,0.28)', showHeartOutline: false, + showInterlockHearts: false, orb: false, aura: true, - secondaryBurst: false, - }, - concentric: { - duration: 3800, - fontSizeRatio: 0.17, - fontSizeMax: 78, - peakScale: 1.34, - flash: 0.55, - flashMs: 240, - flashColor: 'rgba(212, 165, 116, 0.45)', - particleMode: 'ring', - palette: ['#d4a574', '#c9896b', '#e8b4b8', '#b76e79', '#f0d5b8'], - glowPalette: ['#f0d5b8', '#f5c6c8'], - gradient: ['#f5dcc4', '#d4a574', '#b76e79', '#8b4a52'], - glowColor: '#e8c4a0', - strokeColor: 'rgba(255,255,255,0.8)', - showHeartOutline: false, - orb: true, - aura: true, - secondaryBurst: true, + shockwaves: 2, + secondaryDelays: [640], + iosSecondary: false, + charGap: 0.06, }, heaven: { - duration: 3600, + duration: 4000, fontSizeRatio: 0.17, - fontSizeMax: 78, - peakScale: 1.3, - flash: 0.5, + fontSizeMax: 76, + peakScale: 1.18, + flash: 0.38, flashMs: 280, - flashColor: 'rgba(90, 160, 255, 0.4)', + flashColor: 'rgba(200, 225, 255, 0.5)', particleMode: 'stars', - palette: ['#3d8bfd', '#5ba3ff', '#89c2ff', '#4a90a4', '#cce4ff'], - glowPalette: ['#a8d4ff', '#cce4ff'], - gradient: ['#cce4ff', '#5ba3ff', '#3d8bfd', '#1a5bb5'], - glowColor: '#7eb6ff', - strokeColor: 'rgba(255,255,255,0.88)', + palette: ['#9eb8d8', '#7a9ec4', '#b8d0e8', '#6a8eb0', '#d4e4f4'], + glowPalette: ['#e8f2fa', '#c8dcf0'], + gradient: ['#f2f7fc', '#a8c4e0', '#6a92b8', '#3d6288'], + glowColor: '#a8c4dc', + strokeColor: 'rgba(255,252,255,0.92)', + underStroke: 'rgba(40,60,90,0.3)', showHeartOutline: false, + showInterlockHearts: false, orb: true, aura: true, - secondaryBurst: true, + shockwaves: 2, + secondaryDelays: [500], + iosSecondary: false, + charGap: 0.06, }, moisten: { - duration: 3800, + duration: 4200, fontSizeRatio: 0.17, - fontSizeMax: 78, - peakScale: 1.28, - flash: 0.42, + fontSizeMax: 76, + peakScale: 1.16, + flash: 0.34, flashMs: 300, - flashColor: 'rgba(64, 180, 166, 0.4)', + flashColor: 'rgba(190, 235, 230, 0.48)', particleMode: 'ripple', - palette: ['#2a9d8f', '#40b4a6', '#5ecfc2', '#1d7874', '#9ee8de'], - glowPalette: ['#9ee8de', '#c5f5ef'], - gradient: ['#c5f5ef', '#40b4a6', '#2a9d8f', '#165e5a'], - glowColor: '#7dd3c7', - strokeColor: 'rgba(255,255,255,0.85)', + palette: ['#7eb8ae', '#5a9e94', '#9ed0c8', '#4a8a82', '#c4e8e2'], + glowPalette: ['#e0f5f0', '#b8e4dc'], + gradient: ['#f0faf8', '#8ec8be', '#5a9e94', '#2f6a62'], + glowColor: '#8ec8be', + strokeColor: 'rgba(255,255,255,0.9)', + underStroke: 'rgba(30,70,65,0.28)', showHeartOutline: false, + showInterlockHearts: false, orb: false, aura: true, - secondaryBurst: true, + shockwaves: 3, + secondaryDelays: [600], + iosSecondary: false, + charGap: 0.06, }, match: { - duration: 3600, + duration: 4000, fontSizeRatio: 0.17, - fontSizeMax: 78, - peakScale: 1.36, - flash: 0.58, - flashMs: 260, - flashColor: 'rgba(232, 93, 117, 0.45)', + fontSizeMax: 76, + peakScale: 1.2, + flash: 0.4, + flashMs: 280, + flashColor: 'rgba(255, 220, 210, 0.52)', particleMode: 'twin', - palette: ['#e85d75', '#f0a060', '#ff8a65', '#d4a574', '#ffb3c1'], - glowPalette: ['#ffb3c1', '#ffd4a8'], - gradient: ['#ffd4a8', '#e85d75', '#d4a574', '#b5475a'], - glowColor: '#f0a060', - strokeColor: 'rgba(255,255,255,0.88)', + palette: ['#d4a090', '#c48878', '#e0b8a8', '#b87068', '#f0d4c8'], + glowPalette: ['#f8e8e0', '#ecd0c4'], + gradient: ['#fff6f0', '#e0b0a0', '#c48878', '#8a5048'], + glowColor: '#d4a898', + strokeColor: 'rgba(255,250,246,0.92)', + underStroke: 'rgba(80,45,40,0.3)', showHeartOutline: false, + showInterlockHearts: false, orb: false, aura: true, - secondaryBurst: true, + shockwaves: 2, + secondaryDelays: [520], + iosSecondary: false, + charGap: 0.06, + }, + // 终章:粒子 + 爆闪 + 冲击环最丰富 + concentric: { + duration: 5600, + fontSizeRatio: 0.185, + fontSizeMax: 84, + peakScale: 1.24, + flash: 0.52, + flashMs: 340, + flashColor: 'rgba(255, 230, 200, 0.62)', + particleMode: 'interlock', + palette: ['#d4b896', '#c4a078', '#e0c8b0', '#b89078', '#f0dcc8', '#c9a090'], + glowPalette: ['#f8ecd8', '#efe0c8', '#fff6e8'], + gradient: ['#fffaf2', '#ecd8bc', '#d4b080', '#a87860'], + glowColor: '#e0c8a8', + strokeColor: 'rgba(255,252,245,0.95)', + underStroke: 'rgba(90,65,40,0.32)', + showHeartOutline: false, + showInterlockHearts: true, + orb: true, + aura: true, + shockwaves: isIOS ? 3 : 4, + secondaryDelays: isIOS ? [520, 1100] : [420, 900, 1600], + iosSecondary: true, + charGap: 0.08, }, } @@ -258,12 +303,15 @@ function createTextEffect(giftType, now, count = 1) { fontSize, glowColor: style.glowColor, strokeColor: style.strokeColor, + underStroke: style.underStroke || 'rgba(40,30,20,0.28)', peakScale: style.peakScale, gradient: style.gradient, showHeartOutline: style.showHeartOutline, + showInterlockHearts: !!style.showInterlockHearts, orb: style.orb, aura: style.aura, - wobble: rand(-0.04, 0.04), + charGap: style.charGap != null ? style.charGap : 0.06, + wobble: rand(-0.006, 0.006), } } @@ -302,87 +350,127 @@ function fitMilestoneFontSize(context, text, desired, maxWidth) { return size } -function drawMilestoneLine(context, text, fontSize, glowBlur, gradient, strokeColor, glowColor, y) { +function easeOutCubic(t) { + const u = Math.min(1, Math.max(0, t)) + return 1 - (1 - u) ** 3 +} + +function easeOutQuint(t) { + const u = Math.min(1, Math.max(0, t)) + return 1 - (1 - u) ** 5 +} + +/** + * 高级毛笔字:底描 + 亮描 + 渐变填充 + 高光;逐字淡入 + * opts: { underStroke, charGap, birthTime, now } + */ +function drawMilestoneLine(context, text, fontSize, glowBlur, gradient, strokeColor, glowColor, y, opts = {}) { const raw = String(text || '').replace(/[\r\n\u2028\u2029]/g, '') if (!raw) return - // iOS canvas shadowBlur 在中文笔画下会拖出脏光晕,艺术字直接关掉阴影 - const blur = isIOS ? 0 : Math.max(0, glowBlur * SHADOW_MUL) - const maxW = Math.max(40, W * 0.88) + const blur = isIOS ? 0 : Math.max(0, glowBlur * SHADOW_MUL * 0.55) + const maxW = Math.max(40, W * 0.9) const size = fitMilestoneFontSize(context, raw, fontSize, maxW) setMilestoneFont(context, size) context.textAlign = 'center' context.textBaseline = 'middle' context.lineJoin = 'round' context.miterLimit = 2 - context.lineWidth = size * (isIOS ? 0.08 : 0.065) context.shadowColor = 'transparent' context.shadowBlur = 0 context.shadowOffsetX = 0 context.shadowOffsetY = 0 - context.strokeStyle = strokeColor const chars = Array.from(raw) - // 逐字测宽后强制横排,避免 iOS 把四字折成 2×2 + const gap = size * (opts.charGap != null ? opts.charGap : 0.06) const widths = chars.map((ch) => context.measureText(ch).width) - const total = widths.reduce((a, b) => a + b, 0) - const textGrad = context.createLinearGradient(0, y - size * 0.55, 0, y + size * 0.55) + const total = widths.reduce((a, b) => a + b, 0) + gap * Math.max(0, chars.length - 1) + const textGrad = context.createLinearGradient(0, y - size * 0.58, 0, y + size * 0.58) textGrad.addColorStop(0, gradient[0]) - textGrad.addColorStop(0.35, gradient[1]) - textGrad.addColorStop(0.7, gradient[2]) + textGrad.addColorStop(0.4, gradient[1]) + textGrad.addColorStop(0.72, gradient[2]) textGrad.addColorStop(1, gradient[3]) + const birth = opts.birthTime || 0 + const now = opts.now || birth + const under = opts.underStroke || 'rgba(40,30,20,0.28)' + let x = -total / 2 for (let i = 0; i < chars.length; i += 1) { const ch = chars[i] const cw = widths[i] const cx = x + cw / 2 + // 逐字优雅浮现 + const stagger = easeOutCubic((now - birth - i * 70) / 420) + if (stagger <= 0.01) { + x += cw + gap + continue + } + const lift = (1 - stagger) * size * 0.18 + context.save() + context.globalAlpha *= stagger + context.translate(0, -lift) + + // 暗底描边(层次) + context.lineWidth = size * (isIOS ? 0.1 : 0.085) + context.strokeStyle = under + context.strokeText(ch, cx, y) + // 亮描边 + context.lineWidth = size * (isIOS ? 0.055 : 0.042) + context.strokeStyle = strokeColor if (blur > 0) { context.shadowColor = glowColor context.shadowBlur = blur } context.strokeText(ch, cx, y) + // 填充 context.fillStyle = textGrad - if (blur > 0) context.shadowBlur = blur * 1.5 + if (blur > 0) context.shadowBlur = blur * 1.15 context.fillText(ch, cx, y) - context.shadowColor = 'transparent' context.shadowBlur = 0 - context.fillStyle = 'rgba(255,255,255,0.4)' - context.fillText(ch, cx - size * 0.018, y - size * 0.028) - x += cw + // 细高光 + context.globalAlpha *= 0.35 + context.fillStyle = 'rgba(255,255,255,0.75)' + context.fillText(ch, cx - size * 0.012, y - size * 0.022) + context.restore() + x += cw + gap } context.shadowColor = 'transparent' context.shadowBlur = 0 - context.shadowOffsetX = 0 - context.shadowOffsetY = 0 } function textProgress(t, now) { return Math.min((now - t.birthTime) / t.duration, 1) } +/** 电影感缩放:轻缓入场 → 稳定驻留 → 柔和收束 */ function textScale(t, now) { const p = textProgress(t, now) - if (p < 0.18) { - const u = p / 0.18 - return t.peakScale * (1 - (1 - u) ** 4 + Math.sin(u * Math.PI) * 0.18 * (1 - u)) + if (p < 0.22) { + const u = easeOutQuint(p / 0.22) + return 0.88 + (t.peakScale - 0.88) * u } - if (p < 0.55) return t.peakScale - (t.peakScale - 1.05) * ((p - 0.18) / 0.37) * 0.45 - return 1.05 - ((p - 0.55) / 0.45) * 0.28 + if (p < 0.68) { + const hold = (p - 0.22) / 0.46 + return t.peakScale - (t.peakScale - 1.02) * hold * 0.35 + } + const u = easeOutCubic((p - 0.68) / 0.32) + return 1.02 - 0.08 * u } function textOpacity(t, now) { const p = textProgress(t, now) - if (p < 0.55) return 1 - if (p < 0.82) return 1 - ((p - 0.55) / 0.27) * 0.45 - return 0.55 * (1 - ((p - 0.82) / 0.18) ** 2) + if (p < 0.08) return easeOutCubic(p / 0.08) + if (p < 0.7) return 1 + if (p < 0.88) return 1 - ((p - 0.7) / 0.18) * 0.35 + return 0.65 * (1 - easeOutCubic((p - 0.88) / 0.12)) } function textGlow(t, now) { const p = textProgress(t, now) - if (p < 0.2) return p / 0.2 - if (p < 0.5) return 1 - if (p < 0.78) return 1 - ((p - 0.5) / 0.28) * 0.55 - return 0.45 * (1 - (p - 0.78) / 0.22) + if (p < 0.18) return easeOutCubic(p / 0.18) + if (p < 0.55) return 1 + if (p < 0.8) return 1 - ((p - 0.55) / 0.25) * 0.4 + return 0.6 * (1 - (p - 0.8) / 0.2) } function heartOutlineScale(t, now) { @@ -429,8 +517,8 @@ function makeParticle(x, y, now, palette, glowPalette, opts = {}) { kind: opts.kind || 'dot', gravity: opts.gravity != null ? opts.gravity : 70, rot: rand(0, Math.PI * 2), - rotSpeed: rand(-2.5, 2.5), - drift: rand(-18, 18), + rotSpeed: rand(-2.2, 2.2), + drift: rand(-20, 20), }) } @@ -439,11 +527,11 @@ function spawnShockwave(cx, cy, now, color, count = 2) { pushRing({ x: cx, y: cy, - birthTime: now + i * 90, - life: 900 + i * 120, + birthTime: now + i * 95, + life: 900 + i * 140, maxR: Math.min(W, H) * (0.28 + i * 0.12), color, - lineWidth: 3.5 - i * 0.8, + lineWidth: 3.4 - i * 0.7, }) } } @@ -458,26 +546,26 @@ function spawnParticles(giftType, now, wave = 0) { const boost = (wave > 0 ? 0.75 : 1) * PARTICLE_SCALE if (mode === 'heartBurst') { - const n = Math.floor(42 * boost) + const n = Math.floor(46 * boost) for (let i = 0; i < n; i += 1) { makeParticle(cx + rand(-50, 50), cy + rand(-40, 40), now, palette, glowPalette, { - speed: rand(80, 220), + speed: rand(90, 240), size: rand(8, 18), kind: 'heart', life: rand(1100, 2000), gravity: 55, }) } - for (let i = 0; i < 24 * boost; i += 1) { + for (let i = 0; i < Math.floor(28 * boost); i += 1) { makeParticle(cx, cy, now, palette, glowPalette, { - speed: rand(100, 260), + speed: rand(110, 280), size: rand(1.5, 4), kind: 'spark', life: rand(400, 900), }) } } else if (mode === 'goldenRain') { - for (let i = 0; i < 48 * boost; i += 1) { + for (let i = 0; i < Math.floor(52 * boost); i += 1) { makeParticle(cx + rand(-60, 60), cy + rand(-50, 50), now, palette, glowPalette, { speed: rand(90, 240), size: rand(7, 16), @@ -486,111 +574,149 @@ function spawnParticles(giftType, now, wave = 0) { gravity: 45, }) } - for (let i = 0; i < 30 * boost; i += 1) { - makeParticle(cx + rand(-W * 0.2, W * 0.2), cy - H * 0.08, now, palette, glowPalette, { - angle: rand(-Math.PI * 0.7, -Math.PI * 0.3), - speed: rand(40, 120), - size: rand(2, 5), + for (let i = 0; i < Math.floor(36 * boost); i += 1) { + makeParticle(cx + rand(-W * 0.28, W * 0.28), cy - H * 0.12, now, palette, glowPalette, { + angle: rand(-Math.PI * 0.75, -Math.PI * 0.25), + speed: rand(45, 130), + size: rand(2, 5.5), kind: 'spark', - gravity: 90, - life: rand(800, 1500), + gravity: 95, + life: rand(900, 1600), }) } } else if (mode === 'dualWave') { for (let side = -1; side <= 1; side += 2) { - for (let i = 0; i < 22 * boost; i += 1) { - makeParticle(cx + side * W * 0.2 + rand(-30, 30), cy + rand(-50, 50), now, palette, glowPalette, { - speed: rand(70, 180), + for (let i = 0; i < Math.floor(26 * boost); i += 1) { + const a = side < 0 + ? rand(-Math.PI * 0.35, Math.PI * 0.35) + : rand(Math.PI * 0.65, Math.PI * 1.35) + makeParticle(cx + side * W * 0.28 + rand(-20, 20), cy + rand(-60, 60), now, palette, glowPalette, { + angle: a, + speed: rand(90, 200), size: rand(6, 14), kind: 'heart', - life: rand(1000, 1900), + life: rand(1100, 2000), }) } } - for (let i = 0; i < 20 * boost; i += 1) { - makeParticle(cx, cy, now, palette, glowPalette, { kind: 'spark', speed: rand(60, 160) }) + for (let i = 0; i < Math.floor(22 * boost); i += 1) { + makeParticle(cx, cy, now, palette, glowPalette, { kind: 'spark', speed: rand(70, 170) }) } } else if (mode === 'softPetals') { - for (let i = 0; i < 36 * boost; i += 1) { - makeParticle(cx + rand(-100, 100), cy + rand(-70, 70), now, palette, glowPalette, { - speed: rand(25, 90), - size: rand(8, 18), + for (let i = 0; i < Math.floor(42 * boost); i += 1) { + makeParticle(cx + rand(-W * 0.32, W * 0.32), cy - H * 0.1 + rand(-40, 40), now, palette, glowPalette, { + angle: rand(Math.PI * 0.15, Math.PI * 0.85), + speed: rand(18, 70), + size: rand(9, 20), kind: 'petal', - life: rand(1400, 2400), - gravity: 20, + life: rand(1600, 2800), + gravity: 18, }) } - for (let i = 0; i < 18 * boost; i += 1) { + for (let i = 0; i < Math.floor(16 * boost); i += 1) { makeParticle(cx, cy, now, palette, glowPalette, { - speed: rand(30, 80), + speed: rand(25, 70), size: rand(2, 4), kind: 'spark', - gravity: 15, - life: rand(1000, 1800), - }) - } - } else if (mode === 'ring') { - for (let ring = 1; ring <= 4; ring += 1) { - const r = 28 + ring * 32 - const count = 12 + ring * 5 - for (let i = 0; i < count * boost; i += 1) { - const a = (i / count) * Math.PI * 2 + ring * 0.2 - makeParticle(cx + Math.cos(a) * r, cy + Math.sin(a) * r * 0.72, now, palette, glowPalette, { - angle: a + rand(-0.15, 0.15), - speed: rand(40, 110), - size: rand(3, 7), - kind: ring % 2 ? 'dot' : 'spark', - life: rand(900, 1600), - }) - } - } - spawnShockwave(cx, cy, now, style.glowColor, 3) - } else if (mode === 'stars') { - for (let i = 0; i < 40 * boost; i += 1) { - makeParticle(cx + rand(-W * 0.38, W * 0.38), cy + rand(-H * 0.28, H * 0.28), now, palette, glowPalette, { - speed: rand(15, 70), - size: rand(2, 6), - kind: 'star', - life: rand(1000, 2200), gravity: 12, + life: rand(1100, 1900), }) } - for (let i = 0; i < 16 * boost; i += 1) { + } else if (mode === 'stars') { + for (let i = 0; i < Math.floor(48 * boost); i += 1) { + makeParticle(cx + rand(-W * 0.4, W * 0.4), cy + rand(-H * 0.3, H * 0.22), now, palette, glowPalette, { + speed: rand(12, 65), + size: rand(2, 7), + kind: 'star', + life: rand(1200, 2400), + gravity: 10, + }) + } + for (let i = 0; i < Math.floor(14 * boost); i += 1) { makeParticle(cx, cy, now, palette, glowPalette, { - speed: rand(60, 150), - size: rand(5, 12), + speed: rand(55, 140), + size: rand(5, 11), kind: 'heart', life: rand(900, 1600), }) } } else if (mode === 'ripple') { - for (let waveIdx = 0; waveIdx < 4; waveIdx += 1) { - const count = 18 - for (let i = 0; i < count * boost; i += 1) { - const a = (i / count) * Math.PI * 2 + const waves = isIOS ? 3 : 5 + for (let waveIdx = 0; waveIdx < waves; waveIdx += 1) { + const count = isIOS ? 14 : 20 + for (let i = 0; i < Math.floor(count * boost); i += 1) { + const a = (i / count) * Math.PI * 2 + waveIdx * 0.18 makeParticle(cx, cy, now, palette, glowPalette, { angle: a, - speed: 35 + waveIdx * 38 + rand(0, 25), - life: 1000 + waveIdx * 180, - size: rand(2.5, 5.5), - gravity: 8, + speed: 28 + waveIdx * 36 + rand(0, 22), + life: 1100 + waveIdx * 160, + size: rand(2.2, 5.2), + gravity: 6, kind: waveIdx % 2 ? 'spark' : 'dot', }) } } - spawnShockwave(cx, cy, now, style.glowColor, 3) + spawnShockwave(cx, cy, now, style.glowColor, style.shockwaves || 3) } else if (mode === 'twin') { for (let side = -1; side <= 1; side += 2) { - for (let i = 0; i < 20 * boost; i += 1) { - makeParticle(cx + side * W * 0.14 + rand(-28, 28), cy + rand(-40, 40), now, palette, glowPalette, { - speed: rand(70, 180), + for (let i = 0; i < Math.floor(24 * boost); i += 1) { + const toward = side < 0 ? rand(-0.35, 0.35) : rand(Math.PI - 0.35, Math.PI + 0.35) + makeParticle(cx + side * W * 0.22 + rand(-24, 24), cy + rand(-48, 48), now, palette, glowPalette, { + angle: toward, + speed: rand(85, 200), size: rand(7, 15), kind: 'heart', - life: rand(1100, 1900), + life: rand(1100, 2000), }) } } - spawnShockwave(cx, cy, now, style.glowColor, 2) + for (let i = 0; i < Math.floor(16 * boost); i += 1) { + makeParticle(cx, cy, now, palette, glowPalette, { + speed: rand(40, 120), + size: rand(2, 5), + kind: 'spark', + life: rand(700, 1300), + }) + } + spawnShockwave(cx, cy, now, style.glowColor, style.shockwaves || 2) + } else if (mode === 'interlock') { + const ringsN = isIOS ? 4 : 6 + for (let ring = 1; ring <= ringsN; ring += 1) { + const r = 22 + ring * (isIOS ? 26 : 30) + const count = isIOS ? 10 + ring * 3 : 14 + ring * 5 + for (let i = 0; i < Math.floor(count * boost); i += 1) { + const a = (i / count) * Math.PI * 2 + ring * 0.22 + makeParticle(cx + Math.cos(a) * r, cy + Math.sin(a) * r * 0.7, now, palette, glowPalette, { + angle: a + rand(-0.12, 0.12), + speed: rand(45, 130) + ring * 8, + size: rand(2.5, 7), + kind: ring % 3 === 0 ? 'heart' : (ring % 2 ? 'spark' : 'dot'), + life: rand(1000, 1900), + gravity: 35, + }) + } + } + for (let side = -1; side <= 1; side += 2) { + for (let i = 0; i < Math.floor((isIOS ? 12 : 20) * boost); i += 1) { + makeParticle(cx + side * W * 0.18, cy + rand(-30, 30), now, palette, glowPalette, { + angle: side < 0 ? 0 : Math.PI, + speed: rand(70, 160), + size: rand(8, 16), + kind: 'heart', + life: rand(1200, 2100), + gravity: 40, + }) + } + } + for (let i = 0; i < Math.floor((isIOS ? 18 : 32) * boost); i += 1) { + makeParticle(cx + rand(-40, 40), cy + rand(-30, 30), now, palette, glowPalette, { + speed: rand(100, 260), + size: rand(1.5, 4), + kind: 'spark', + life: rand(500, 1100), + }) + } + spawnShockwave(cx, cy, now, style.glowColor, style.shockwaves || 4) } } @@ -650,16 +776,26 @@ function playOne(item) { texts.push(mt) activeMeta = { giftType, name, mt } spawnParticles(giftType, now, 0) - spawnShockwave(W / 2, H * 0.46, now, style.glowColor, style.showHeartOutline && !isIOS ? 3 : 2) - flashAlpha = isIOS ? Math.min(style.flash, 0.45) : style.flash + const waves = style.shockwaves != null + ? style.shockwaves + : (style.showHeartOutline && !isIOS ? 3 : 2) + spawnShockwave(W / 2, H * 0.46, now, style.glowColor, waves) + flashAlpha = isIOS ? Math.min(style.flash, giftType === 'concentric' ? 0.48 : 0.4) : style.flash flashDecay = flashAlpha / Math.max(1, style.flashMs / 16) flashColor = style.flashColor - if (style.secondaryBurst && !isIOS) { - const t = setTimeout(() => { - spawnParticles(giftType, performance.now(), 1) - ensureLoop() - }, 280) - secondaryTimers.push(t) + const delays = Array.isArray(style.secondaryDelays) ? style.secondaryDelays : [] + const allowSecondary = delays.length > 0 && (!isIOS || style.iosSecondary) + if (allowSecondary) { + delays.forEach((ms, idx) => { + const t = setTimeout(() => { + spawnParticles(giftType, performance.now(), 1 + idx) + if (giftType === 'concentric') { + spawnShockwave(W / 2, H * 0.46, performance.now(), style.glowColor, isIOS ? 1 : 2) + } + ensureLoop() + }, ms) + secondaryTimers.push(t) + }) } emit('play-start', { giftType, name, count, phase: 'main' }) if (count > 1) { @@ -788,21 +924,37 @@ function animate(timestamp) { ctx.clearRect(0, 0, W, H) - // 特效期间压暗背景(纯色半透明,不加模糊) + // 高级暗场:轻压暗 + 柔和中心留白(iOS 用扁平,避免径向卡顿) if (texts.length) { const mt = texts[0] const op = textOpacity(mt, now) - const dim = Math.min(0.4, 0.12 + op * 0.28) + const dim = Math.min(0.38, 0.1 + op * 0.26) if (dim > 0.02) { ctx.save() ctx.globalAlpha = dim - ctx.fillStyle = '#0c0c10' + ctx.fillStyle = '#121014' ctx.fillRect(0, 0, W, H) ctx.restore() + if (!SKIP_AURA) { + const cx = W / 2 + const cy = H * 0.46 + const r = Math.min(W, H) * 0.42 + ctx.save() + ctx.globalAlpha = op * 0.18 + const soft = ctx.createRadialGradient(cx, cy, r * 0.15, cx, cy, r) + soft.addColorStop(0, mt.glowColor) + soft.addColorStop(0.55, 'rgba(0,0,0,0)') + soft.addColorStop(1, 'rgba(0,0,0,0)') + ctx.fillStyle = soft + ctx.beginPath() + ctx.arc(cx, cy, r, 0, Math.PI * 2) + ctx.fill() + ctx.restore() + } } } - // rings + // rings — 冲击环:清晰外扩 + 光晕 for (let i = rings.length - 1; i >= 0; i -= 1) { const r = rings[i] const age = now - r.birthTime @@ -817,7 +969,7 @@ function animate(timestamp) { ctx.save() ctx.globalAlpha = alpha ctx.strokeStyle = r.color - ctx.lineWidth = r.lineWidth * (1 - p * 0.5) + ctx.lineWidth = r.lineWidth * (1 - p * 0.45) ctx.shadowColor = r.color ctx.shadowBlur = 12 * SHADOW_MUL ctx.beginPath() @@ -835,21 +987,25 @@ function animate(timestamp) { continue } const age = (now - p.birthTime) / 1000 - const x = p.x + p.vx * age + Math.sin(age * 3 + p.rot) * (p.drift || 0) * 0.35 - const y = p.y + p.vy * age + 0.5 * p.gravity * age * age - const opacity = progress < 0.15 ? progress / 0.15 : 1 - ((progress - 0.15) / 0.85) ** 1.4 - const size = p.size * (progress < 0.2 ? 0.6 + progress * 2 : 1 - progress * 0.45) - const rot = p.rot + p.rotSpeed * age + // 飘逸减速:速度随时间衰减,更高级 + const damp = 1 / (1 + age * 0.55) + const x = p.x + p.vx * age * damp + Math.sin(age * 2.2 + p.rot) * (p.drift || 0) * 0.45 + const y = p.y + p.vy * age * damp + 0.5 * p.gravity * age * age * 0.75 + const opacity = progress < 0.12 + ? easeOutCubic(progress / 0.12) + : 1 - ((progress - 0.12) / 0.88) ** 1.15 + const size = p.size * (progress < 0.15 ? 0.7 + progress * 1.6 : 1 - progress * 0.35) + const rot = p.rot + p.rotSpeed * age * 0.65 ctx.save() ctx.globalAlpha = Math.max(0, opacity) ctx.fillStyle = p.color ctx.shadowColor = p.glow || p.color - ctx.shadowBlur = (p.kind === 'spark' || p.kind === 'star' ? 10 : 6) * SHADOW_MUL + ctx.shadowBlur = (p.kind === 'spark' || p.kind === 'star' ? 7 : 4) * SHADOW_MUL if (p.kind === 'heart') { ctx.translate(x, y) - ctx.rotate(rot * 0.35) + ctx.rotate(rot * 0.22) drawHeartPath(ctx, 0, 0, size) ctx.fill() } else if (p.kind === 'petal') { @@ -858,7 +1014,7 @@ function animate(timestamp) { } else if (p.kind === 'star') { ctx.beginPath() for (let s = 0; s < 8; s += 1) { - const rr = s % 2 === 0 ? size : size * 0.38 + const rr = s % 2 === 0 ? size : size * 0.36 const a = (s / 8) * Math.PI * 2 - Math.PI / 2 + rot const px = x + Math.cos(a) * rr const py = y + Math.sin(a) * rr @@ -870,7 +1026,7 @@ function animate(timestamp) { } else if (p.kind === 'spark') { ctx.translate(x, y) ctx.rotate(rot) - ctx.fillRect(-size * 0.35, -size * 1.4, size * 0.7, size * 2.8) + ctx.fillRect(-size * 0.28, -size * 1.15, size * 0.56, size * 2.3) } else { ctx.beginPath() ctx.arc(x, y, size, 0, Math.PI * 2) @@ -901,9 +1057,10 @@ function animate(timestamp) { const auraR = Math.max(0.1, Number(fontSize) * 1.8 * Math.max(0, scale) || 0) if (auraR > 0.1) { ctx.save() - ctx.globalAlpha = opacity * 0.22 * glowIntensity + ctx.globalAlpha = opacity * 0.14 * glowIntensity const aura = ctx.createRadialGradient(cx, cy, 0, cx, cy, auraR) aura.addColorStop(0, mt.glowColor) + aura.addColorStop(0.65, 'rgba(0,0,0,0)') aura.addColorStop(1, 'rgba(0,0,0,0)') ctx.fillStyle = aura ctx.beginPath() @@ -930,14 +1087,19 @@ function animate(timestamp) { mt.strokeColor, mt.glowColor, mainY, + { + underStroke: mt.underStroke, + charGap: mt.charGap, + birthTime: mt.birthTime, + now, + }, ) if (showMult) { const n = scrolledCount(mt, now) - const multSize = fontSize * 0.72 - const multY = fontSize * 0.55 - // 滚动过程轻微上浮 + const multSize = fontSize * 0.68 + const multY = fontSize * 0.52 const rollP = Math.min(1, (now - (mt.countRevealAt || now)) / COUNT_ROLL_MS) - const pop = 0.85 + 0.15 * (1 - (1 - rollP) ** 2) + const pop = 0.9 + 0.1 * easeOutCubic(rollP) ctx.save() ctx.translate(0, multY) ctx.scale(pop, pop) @@ -945,11 +1107,17 @@ function animate(timestamp) { ctx, `×${n}`, multSize, - glowBlur * 0.85, + glowBlur * 0.7, mt.gradient, mt.strokeColor, mt.glowColor, 0, + { + underStroke: mt.underStroke, + charGap: 0.04, + birthTime: mt.countRevealAt || now, + now, + }, ) ctx.restore() } @@ -957,52 +1125,89 @@ function animate(timestamp) { if (mt.showHeartOutline && heartOutlineOpacity(mt, now) > 0.01) { const outlineScale = heartOutlineScale(mt, now) - const outlineOpacity = heartOutlineOpacity(mt, now) - const outlineSize = fontSize * 1.7 * outlineScale + const outlineOpacity = heartOutlineOpacity(mt, now) * 0.7 + const outlineSize = fontSize * 1.55 * outlineScale ctx.save() ctx.globalAlpha = outlineOpacity ctx.translate(cx, cy) ctx.strokeStyle = mt.glowColor - ctx.lineWidth = outlineSize * 0.035 + ctx.lineWidth = Math.max(1, outlineSize * 0.022) ctx.shadowColor = mt.glowColor - ctx.shadowBlur = outlineSize * 0.28 + ctx.shadowBlur = (isIOS ? 0 : outlineSize * 0.16) * SHADOW_MUL drawHeartPath(ctx, 0, 0, outlineSize) ctx.stroke() - ctx.lineWidth = outlineSize * 0.065 - ctx.shadowBlur = outlineSize * 0.45 - ctx.globalAlpha = outlineOpacity * 0.35 - ctx.stroke() + ctx.restore() + } + + // 永结同心:双心相扣,慢呼吸而非炫转 + if (mt.showInterlockHearts && opacity > 0.12) { + const p = textProgress(mt, now) + const grow = easeOutCubic(Math.min(1, p / 0.28)) + const fade = p > 0.78 ? Math.max(0, 1 - (p - 0.78) / 0.22) : 1 + const breath = Math.sin((now - mt.birthTime) / 900) * 0.04 + const hs = fontSize * (1.05 + 0.28 * grow) * scale + const ox = fontSize * 0.38 * scale + ctx.save() + ctx.translate(cx, cy) + ctx.globalAlpha = opacity * 0.38 * fade + ctx.strokeStyle = mt.glowColor + ctx.lineWidth = Math.max(1, hs * 0.022) + ctx.shadowColor = mt.glowColor + ctx.shadowBlur = isIOS ? 0 : 6 * SHADOW_MUL + ctx.save() + ctx.translate(-ox, 0) + ctx.rotate(-0.2 + breath) + drawHeartPath(ctx, 0, 0, hs) + ctx.stroke() + ctx.restore() + ctx.save() + ctx.translate(ox, 0) + ctx.rotate(0.2 - breath) + drawHeartPath(ctx, 0, 0, hs) + ctx.stroke() + ctx.restore() ctx.restore() } if (mt.orb && opacity > 0.18) { - const numOrbs = mt.showHeartOutline ? 16 : 12 - const orbRadius = fontSize * (1.05 + 0.15 * glowIntensity) + const numOrbs = mt.showInterlockHearts + ? (isIOS ? 8 : 12) + : (mt.showHeartOutline ? 10 : 8) + const orbRadius = fontSize * (mt.showInterlockHearts ? 1.15 : 1.0 + 0.12 * glowIntensity) const age = (now - mt.birthTime) / 1000 for (let j = 0; j < numOrbs; j += 1) { - const angle = age * 1.9 + j * ((Math.PI * 2) / numOrbs) + const angle = age * (mt.showInterlockHearts ? 0.85 : 0.7) + j * ((Math.PI * 2) / numOrbs) const orbX = cx + Math.cos(angle) * orbRadius - const orbY = cy + Math.sin(angle) * orbRadius * 0.68 - const orbSize = fontSize * 0.035 * (1 + Math.sin(age * 3 + j) * 0.45) + const orbY = cy + Math.sin(angle) * orbRadius * 0.66 + const orbSize = fontSize * 0.028 * (1 + Math.sin(age * 1.6 + j) * 0.28) ctx.save() - ctx.globalAlpha = opacity * (0.55 + Math.sin(age * 2.4 + j) * 0.3) - ctx.fillStyle = '#fffbe6' + ctx.globalAlpha = opacity * (0.28 + Math.sin(age * 1.4 + j) * 0.14) + ctx.fillStyle = mt.glowColor ctx.shadowColor = mt.glowColor - ctx.shadowBlur = orbSize * 6 + ctx.shadowBlur = isIOS ? 0 : orbSize * 4 ctx.beginPath() - ctx.arc(orbX, orbY, Math.max(2, orbSize), 0, Math.PI * 2) + ctx.arc(orbX, orbY, Math.max(1.5, orbSize), 0, Math.PI * 2) ctx.fill() ctx.restore() } } } - // colored flash on top + // 爆闪:中心高亮 + 全屏一层,保证冲击感 if (flashAlpha > 0.01) { ctx.save() ctx.globalAlpha = Math.min(flashAlpha, 1) ctx.fillStyle = flashColor ctx.fillRect(0, 0, W, H) + if (!SKIP_AURA) { + ctx.globalAlpha = Math.min(flashAlpha * 0.85, 0.7) + const soft = ctx.createRadialGradient(W / 2, H * 0.46, 0, W / 2, H * 0.46, Math.min(W, H) * 0.5) + soft.addColorStop(0, 'rgba(255,255,255,0.85)') + soft.addColorStop(0.45, flashColor) + soft.addColorStop(1, 'rgba(255,255,255,0)') + ctx.fillStyle = soft + ctx.fillRect(0, 0, W, H) + } ctx.restore() flashAlpha = Math.max(0, flashAlpha - flashDecay) } diff --git a/vite-tailwindcss/src/utils/giftTypes.js b/vite-tailwindcss/src/utils/giftTypes.js index 527b765..fbebbbd 100644 --- a/vite-tailwindcss/src/utils/giftTypes.js +++ b/vite-tailwindcss/src/utils/giftTypes.js @@ -1,13 +1,13 @@ -/** 礼物类型常量表(与后端 giftTypeWhitelist / defaultGiftCosts 一致) */ +/** 礼物类型常量表(与后端 giftTypeWhitelist / defaultGiftCosts 一致;永结同心放最后) */ export const GIFT_TYPES = [ { key: '520', label: '520', cost: 50, preview: 'linear-gradient(135deg, #ff8da6 0%, #ff2d55 55%, #c41e3a 100%)' }, { key: '1314', label: '1314', cost: 100, preview: 'linear-gradient(135deg, #fff3b0 0%, #ffd700 50%, #e6a000 100%)' }, { key: 'forever', label: '长长久久', cost: 50, preview: 'linear-gradient(135deg, #ffb3c1 0%, #c41e3a 55%, #6b0f1a 100%)' }, { key: 'bald', label: '白头偕老', cost: 50, preview: 'linear-gradient(135deg, #ffffff 0%, #e8e4d9 45%, #b8a878 100%)' }, - { key: 'concentric', label: '永结同心', cost: 50, preview: 'linear-gradient(135deg, #f5dcc4 0%, #d4a574 50%, #b76e79 100%)' }, { key: 'heaven', label: '天作之合', cost: 100, preview: 'linear-gradient(135deg, #cce4ff 0%, #5ba3ff 50%, #1a5bb5 100%)' }, { key: 'moisten', label: '相濡以沫', cost: 100, preview: 'linear-gradient(135deg, #c5f5ef 0%, #40b4a6 50%, #165e5a 100%)' }, { key: 'match', label: '佳偶天成', cost: 200, preview: 'linear-gradient(135deg, #ffd4a8 0%, #e85d75 50%, #b5475a 100%)' }, + { key: 'concentric', label: '永结同心', cost: 200, preview: 'linear-gradient(135deg, #f5dcc4 0%, #d4a574 50%, #b76e79 100%)' }, ] export const GIFT_LABEL_MAP = Object.fromEntries(GIFT_TYPES.map((g) => [g.key, g.label])) diff --git a/vite-tailwindcss/src/views/index/index.vue b/vite-tailwindcss/src/views/index/index.vue index 51c155f..8535563 100644 --- a/vite-tailwindcss/src/views/index/index.vue +++ b/vite-tailwindcss/src/views/index/index.vue @@ -1088,6 +1088,8 @@ let freeScrollLastTs = 0 let freeScrollLastApplied = -1 /** 进度条节流 */ let freeScrollProgressTick = 0 +/** 底部自动回顶动画中:暂停自由滚,并自行驱动进度条 */ +let isReturningToTop = false let scrollHeightObserver = null /** * iOS 自由自动滚:用 GPU transform 平移内容,全程不写 scrollTop。 @@ -1396,16 +1398,39 @@ const scheduleBottomReturn = (reset = false) => { const smoothScrollTo = (target, duration) => { cancelAnimationFrame(animId) if (iosTransformActive) exitIosTransformScroll() - const start = pageScrollTopNative(), dist = target - start - if (!dist) return + const start = pageScrollTopNative() + const dist = target - start + const finishReturn = () => { + isReturningToTop = false + invalidateFreeScrollAnchor() + if (target <= 0) { + freeScrollOffset = 0 + freeScrollLastApplied = 0 + progress.value = 0 + } else { + syncProgressFromY(target) + } + } + if (!dist) { + finishReturn() + return + } invalidateFreeScrollAnchor() - if (duration <= 0) { window.scrollTo(0, target); return } + isReturningToTop = true + if (duration <= 0) { + window.scrollTo(0, target) + finishReturn() + return + } const t0 = performance.now() const tick = (now) => { const p = Math.min(1, (now - t0) / duration) const e = p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2 - window.scrollTo(0, start + dist * e) + const y = start + dist * e + window.scrollTo(0, y) + syncProgressFromY(y) if (p < 1) animId = requestAnimationFrame(tick) + else finishReturn() } animId = requestAnimationFrame(tick) } @@ -1427,6 +1452,7 @@ const canAutoAdvance = () => isAutoScroll.value && !isInteracting.value && !isSnapping.value + && !isReturningToTop && !isDrawerOpen.value && !showBlessingDrawer.value && !showGiftPanel.value @@ -1434,6 +1460,11 @@ const canAutoAdvance = () => && !showMapOptions.value && !showOpenInBrowser.value +const syncProgressFromY = (y) => { + const max = pageScrollMax() + progress.value = max > 0 ? Math.min(1, Math.max(0, y / max)) : 0 +} + /** 图挂载后刷新文档高度(画廊不停滚,仅校正 scrollMax) */ const onGalleryReady = () => { invalidateScrollMax() @@ -1512,6 +1543,9 @@ const scrollPageToTop = () => { document.documentElement.scrollTop = 0 document.body.scrollTop = 0 freeScrollOffset = 0 + freeScrollLastApplied = 0 + progress.value = 0 + isReturningToTop = false } const toggleAutoScroll = () => { @@ -2434,7 +2468,7 @@ watch( left: 0; font-size: 11px; line-height: 1; - color: #a88c6b; + color: #c62828; text-shadow: 0 0 6px rgba(253, 251, 247, 0.95), 0 1px 0 rgba(255, 255, 255, 0.8);