1. 礼物特效

This commit is contained in:
李琦
2026-08-03 13:19:09 +08:00
parent abcb53b88e
commit 8a4b3f4ab7
8 changed files with 532 additions and 173 deletions

View File

@@ -1155,6 +1155,96 @@ function animate(timestamp) {
}
}
// 背景装饰先画:爱心 / 轨道光点(不压字)
if (mt.showHeartOutline && heartOutlineOpacity(mt, now) > 0.01) {
const outlineScale = heartOutlineScale(mt, now)
const outlineOpacity = heartOutlineOpacity(mt, now) * (ENABLE_NEON_GLOW ? 0.75 : 0.65)
const outlineSize = fontSize * 1.75 * outlineScale
ctx.save()
ctx.translate(cx, cy)
if (ENABLE_NEON_GLOW) {
const neonLayers = [
{ widthRatio: 0.038, alpha: 0.16, blur: 14 * SHADOW_MUL },
{ widthRatio: 0.016, alpha: 0.4, blur: 7 * SHADOW_MUL },
{ widthRatio: 0.005, alpha: 0.7, blur: 2 * SHADOW_MUL },
]
neonLayers.forEach((layer, idx) => {
ctx.globalAlpha = outlineOpacity * layer.alpha
ctx.strokeStyle = idx === 2 ? '#ffffff' : mt.glowColor
ctx.lineWidth = Math.max(1, outlineSize * layer.widthRatio)
ctx.shadowColor = mt.glowColor
ctx.shadowBlur = layer.blur
drawHeartPath(ctx, 0, 0, outlineSize)
ctx.stroke()
})
} else {
ctx.globalAlpha = outlineOpacity
ctx.strokeStyle = mt.glowColor
ctx.lineWidth = Math.max(1, outlineSize * 0.022)
ctx.shadowColor = mt.glowColor
ctx.shadowBlur = (isIOS ? 0 : outlineSize * 0.16) * SHADOW_MUL
drawHeartPath(ctx, 0, 0, outlineSize)
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.35 + 0.22 * grow) * scale
const ox = fontSize * 0.58 * scale
ctx.save()
ctx.translate(cx, cy)
const drawInterlockHeart = (offsetX, rotateAngle) => {
ctx.save()
ctx.translate(offsetX, 0)
ctx.rotate(rotateAngle)
ctx.globalAlpha = opacity * 0.28 * fade
ctx.strokeStyle = mt.glowColor
ctx.lineWidth = Math.max(1.2, hs * 0.028)
ctx.shadowColor = mt.glowColor
ctx.shadowBlur = isIOS ? 0 : 8 * SHADOW_MUL
drawHeartPath(ctx, 0, 0, hs)
ctx.stroke()
ctx.globalAlpha = opacity * 0.16 * fade
ctx.lineWidth = Math.max(0.8, hs * 0.012)
ctx.shadowBlur = isIOS ? 0 : 3 * SHADOW_MUL
drawHeartPath(ctx, 0, 0, hs * 0.92)
ctx.stroke()
ctx.restore()
}
drawInterlockHeart(-ox, -0.18 + breath)
drawInterlockHeart(ox, 0.18 - breath)
ctx.restore()
}
if (mt.orb && opacity > 0.18) {
const numOrbs = mt.showInterlockHearts
? (isIOS ? 8 : 12)
: (mt.showHeartOutline ? 10 : 8)
const orbRadius = fontSize * (mt.showInterlockHearts ? 1.45 : 1.0 + 0.12 * glowIntensity)
const age = (now - mt.birthTime) / 1000
for (let j = 0; j < numOrbs; j += 1) {
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.66
const orbSize = fontSize * 0.028 * (1 + Math.sin(age * 1.6 + j) * 0.28)
ctx.save()
ctx.globalAlpha = opacity * (mt.showInterlockHearts ? 0.18 : 0.28 + Math.sin(age * 1.4 + j) * 0.14)
ctx.fillStyle = mt.glowColor
ctx.shadowColor = mt.glowColor
ctx.shadowBlur = isIOS ? 0 : orbSize * 4
ctx.beginPath()
ctx.arc(orbX, orbY, Math.max(1.5, orbSize), 0, Math.PI * 2)
ctx.fill()
ctx.restore()
}
}
// 书法文字(最上层)
ctx.save()
ctx.globalAlpha = opacity
ctx.translate(cx, cy)
@@ -1164,20 +1254,20 @@ function animate(timestamp) {
const showMult = mt.countRevealed && mt.count > 1
const mainY = showMult ? -fontSize * 0.42 : 0
drawMilestoneLine(
ctx,
mt.baseText || mt.text,
fontSize,
glowBlur,
mt.gradient,
mt.strokeColor,
mt.glowColor,
mainY,
{
underStroke: mt.underStroke,
charGap: mt.charGap,
birthTime: mt.birthTime,
now,
},
ctx,
mt.baseText || mt.text,
fontSize,
glowBlur,
mt.gradient,
mt.strokeColor,
mt.glowColor,
mainY,
{
underStroke: mt.underStroke,
charGap: mt.charGap,
birthTime: mt.birthTime,
now,
},
)
if (showMult) {
const n = scrolledCount(mt, now)
@@ -1189,143 +1279,24 @@ function animate(timestamp) {
ctx.translate(0, multY)
ctx.scale(pop, pop)
drawMilestoneLine(
ctx,
`×${n}`,
multSize,
glowBlur * 0.7,
mt.gradient,
mt.strokeColor,
mt.glowColor,
0,
{
underStroke: mt.underStroke,
charGap: 0.04,
birthTime: mt.countRevealAt || now,
now,
},
ctx,
`×${n}`,
multSize,
glowBlur * 0.7,
mt.gradient,
mt.strokeColor,
mt.glowColor,
0,
{
underStroke: mt.underStroke,
charGap: 0.04,
birthTime: mt.countRevealAt || now,
now,
},
)
ctx.restore()
}
ctx.restore()
// 核心心形轮廓重构:将难看的单线条换成多层高质感的霓虹流光轮廓
if (mt.showHeartOutline && heartOutlineOpacity(mt, now) > 0.01) {
const outlineScale = heartOutlineScale(mt, now)
const outlineOpacity = heartOutlineOpacity(mt, now) * (ENABLE_NEON_GLOW ? 1.0 : 0.7)
const outlineSize = fontSize * 1.55 * outlineScale
ctx.save()
ctx.translate(cx, cy)
if (ENABLE_NEON_GLOW) {
// [新增] 特效三:惊艳的霓虹流光多层绘制
ctx.globalCompositeOperation = 'lighter'
// 渲染 3 层不同的发光和线宽来模拟极具科技感和仙气的霓虹能量管
const neonLayers = [
{ widthRatio: 0.045, alpha: 0.25, blur: 20 * SHADOW_MUL }, // 底层弥散光晕
{ widthRatio: 0.02, alpha: 0.6, blur: 10 * SHADOW_MUL }, // 中层亮色核心
{ widthRatio: 0.006, alpha: 1.0, blur: 2 * SHADOW_MUL } // 顶层高亮白心
]
neonLayers.forEach((layer, idx) => {
ctx.globalAlpha = outlineOpacity * layer.alpha
// 顶层使用纯白,底层使用礼物的主题发光色
ctx.strokeStyle = idx === 2 ? '#ffffff' : mt.glowColor
ctx.lineWidth = Math.max(1, outlineSize * layer.widthRatio)
ctx.shadowColor = mt.glowColor
ctx.shadowBlur = layer.blur
drawHeartPath(ctx, 0, 0, outlineSize)
ctx.stroke()
})
} else {
// 降级旧方案(普通单线条描边)
ctx.globalAlpha = outlineOpacity
ctx.strokeStyle = mt.glowColor
ctx.lineWidth = Math.max(1, outlineSize * 0.022)
ctx.shadowColor = mt.glowColor
ctx.shadowBlur = (isIOS ? 0 : outlineSize * 0.16) * SHADOW_MUL
drawHeartPath(ctx, 0, 0, outlineSize)
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)
const drawInterlockHeart = (offsetX, rotateAngle) => {
ctx.save()
ctx.translate(offsetX, 0)
ctx.rotate(rotateAngle)
if (ENABLE_NEON_GLOW) {
ctx.globalCompositeOperation = 'lighter'
const neonLayers = [
{ widthRatio: 0.045, alpha: 0.2, blur: 15 * SHADOW_MUL },
{ widthRatio: 0.02, alpha: 0.5, blur: 8 * SHADOW_MUL },
{ widthRatio: 0.006, alpha: 0.9, blur: 2 * SHADOW_MUL }
]
neonLayers.forEach((layer, idx) => {
ctx.globalAlpha = opacity * fade * layer.alpha
ctx.strokeStyle = idx === 2 ? '#ffffff' : mt.glowColor
ctx.lineWidth = Math.max(1, hs * layer.widthRatio)
ctx.shadowColor = mt.glowColor
ctx.shadowBlur = layer.blur
drawHeartPath(ctx, 0, 0, hs)
ctx.stroke()
})
} else {
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
drawHeartPath(ctx, 0, 0, hs)
ctx.stroke()
}
ctx.restore()
}
// 绘制左心和右心
drawInterlockHeart(-ox, -0.2 + breath)
drawInterlockHeart(ox, 0.2 - breath)
ctx.restore()
}
if (mt.orb && opacity > 0.18) {
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 * (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.66
const orbSize = fontSize * 0.028 * (1 + Math.sin(age * 1.6 + j) * 0.28)
ctx.save()
ctx.globalAlpha = opacity * (0.28 + Math.sin(age * 1.4 + j) * 0.14)
ctx.fillStyle = mt.glowColor
ctx.shadowColor = mt.glowColor
ctx.shadowBlur = isIOS ? 0 : orbSize * 4
ctx.beginPath()
ctx.arc(orbX, orbY, Math.max(1.5, orbSize), 0, Math.PI * 2)
ctx.fill()
ctx.restore()
}
}
}
// 爆闪:中心高亮 + 全屏一层,保证冲击感