1. 礼物特效
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 爆闪:中心高亮 + 全屏一层,保证冲击感
|
||||
|
||||
Reference in New Issue
Block a user