1. 礼物特效
This commit is contained in:
@@ -10,6 +10,15 @@ import { isIOS } from '@/composables/useAndroidOptimize'
|
||||
const emit = defineEmits(['play-start'])
|
||||
const canvasRef = ref(null)
|
||||
|
||||
// ========================================================
|
||||
// [高级特效开关控制] (可自由组合)
|
||||
// ========================================================
|
||||
const ENABLE_BLEND_MODES = true // 特效一:叠加发光 (仙气、刺眼的高亮光感)
|
||||
const ENABLE_MOTION_BLUR = true // 特效二:物理拉丝拖尾 (运动模糊、极速流星感)
|
||||
const ENABLE_NEON_GLOW = true // 特效三:[全新] 霓虹流光场 (将原本难看的线条,升级为多层发光的能量轨迹)
|
||||
const ENABLE_TURBULENCE = true // 特效四:流体力学场 (S型盘旋、引力扰动空间)
|
||||
// ========================================================
|
||||
|
||||
const MAX_PARTICLES = isIOS ? 72 : 180
|
||||
const MAX_RINGS = isIOS ? 6 : 12
|
||||
const MAX_TEXTS = 3
|
||||
@@ -262,24 +271,35 @@ function resizeCanvas() {
|
||||
if (typeof ctx.direction === 'string') ctx.direction = 'ltr'
|
||||
}
|
||||
|
||||
/**
|
||||
* [优化] 完美心形比例计算:移除原本生硬的锚点,
|
||||
* 使用更圆润的黄金分割贝塞尔曲线,还原极其逼真的心形
|
||||
*/
|
||||
function drawHeartPath(context, cx, cy, size) {
|
||||
const s = size
|
||||
context.beginPath()
|
||||
context.moveTo(cx, cy + s * 0.55)
|
||||
context.bezierCurveTo(cx - s * 0.85, cy + s * 0.25, cx - s * 0.95, cy - s * 0.2, cx - s * 0.45, cy - s * 1.05)
|
||||
context.bezierCurveTo(cx, cy - s * 0.55, cx + s * 0.45, cy - s * 1.05, cx + s * 0.45, cy - s * 1.05)
|
||||
context.bezierCurveTo(cx + s * 0.95, cy - s * 0.2, cx + s * 0.85, cy + s * 0.25, cx, cy + s * 0.55)
|
||||
context.moveTo(cx, cy - size * 0.25)
|
||||
// 右半心
|
||||
context.bezierCurveTo(cx + size * 0.5, cy - size * 0.65, cx + size * 1.1, cy + size * 0.15, cx, cy + size * 0.85)
|
||||
// 左半心
|
||||
context.bezierCurveTo(cx - size * 1.1, cy + size * 0.15, cx - size * 0.5, cy - size * 0.65, cx, cy - size * 0.25)
|
||||
context.closePath()
|
||||
}
|
||||
|
||||
/**
|
||||
* [优化] 真实花瓣比例计算:水滴与樱花的结合体,
|
||||
* 底部尖锐,顶部圆润,伴有自然凹陷。
|
||||
*/
|
||||
function drawPetal(context, x, y, size, rot) {
|
||||
context.save()
|
||||
context.translate(x, y)
|
||||
context.rotate(rot)
|
||||
context.beginPath()
|
||||
context.moveTo(0, -size)
|
||||
context.quadraticCurveTo(size * 0.7, -size * 0.2, 0, size)
|
||||
context.quadraticCurveTo(-size * 0.7, -size * 0.2, 0, -size)
|
||||
// 花瓣根部
|
||||
context.moveTo(0, size * 0.5)
|
||||
// 左侧弧线
|
||||
context.bezierCurveTo(-size * 0.8, size * 0.2, -size * 0.6, -size * 0.8, 0, -size)
|
||||
// 右侧弧线
|
||||
context.bezierCurveTo(size * 0.6, -size * 0.8, size * 0.8, size * 0.2, 0, size * 0.5)
|
||||
context.closePath()
|
||||
context.fill()
|
||||
context.restore()
|
||||
@@ -328,11 +348,41 @@ function scrolledCount(mt, now) {
|
||||
return Math.max(from, Math.round(from + (target - from) * e))
|
||||
}
|
||||
|
||||
const FONT_STACK = '"Ma Shan Zheng", "PingFang SC", "Hiragino Sans GB", sans-serif'
|
||||
const FONT_STACK_FALLBACK = '"Ma Shan Zheng", "Great Vibes", cursive, serif'
|
||||
let cachedCalligraphyFontStack = ''
|
||||
|
||||
/**
|
||||
* Canvas 不解析 CSS 变量。
|
||||
* --font-calligraphy 首位是 Great Vibes(拉丁花体,无中文毛笔字形),
|
||||
* 礼物文案是中文,需把 Ma Shan Zheng 提到最前,否则易落到 cursive/系统字体,没有毛笔感。
|
||||
*/
|
||||
function calligraphyFontStack() {
|
||||
if (cachedCalligraphyFontStack) return cachedCalligraphyFontStack
|
||||
let raw = ''
|
||||
try {
|
||||
raw = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--font-calligraphy')
|
||||
.trim()
|
||||
} catch {
|
||||
raw = ''
|
||||
}
|
||||
const base = raw || FONT_STACK_FALLBACK
|
||||
// 已含马善政则提到最前;否则插入最前
|
||||
if (/ma\s*shan\s*zheng/i.test(base)) {
|
||||
const rest = base
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s && !/ma\s*shan\s*zheng/i.test(s))
|
||||
cachedCalligraphyFontStack = [`"Ma Shan Zheng"`, ...rest].join(', ')
|
||||
} else {
|
||||
cachedCalligraphyFontStack = `"Ma Shan Zheng", ${base}`
|
||||
}
|
||||
return cachedCalligraphyFontStack
|
||||
}
|
||||
|
||||
function setMilestoneFont(context, fontSize) {
|
||||
// Ma Shan Zheng 只有 400;用 700 时 iOS 会假粗并打乱字距/排版
|
||||
context.font = `400 ${fontSize}px ${FONT_STACK}`
|
||||
// Ma Shan Zheng 只有 400;用 700 时 iOS 会假粗并打乱字距
|
||||
context.font = `400 ${fontSize}px ${calligraphyFontStack()}`
|
||||
if (typeof context.direction === 'string') context.direction = 'ltr'
|
||||
if (typeof context.letterSpacing === 'string') context.letterSpacing = '0px'
|
||||
}
|
||||
@@ -426,6 +476,7 @@ function drawMilestoneLine(context, text, fontSize, glowBlur, gradient, strokeCo
|
||||
context.fillStyle = textGrad
|
||||
if (blur > 0) context.shadowBlur = blur * 1.15
|
||||
context.fillText(ch, cx, y)
|
||||
|
||||
context.shadowBlur = 0
|
||||
// 细高光
|
||||
context.globalAlpha *= 0.35
|
||||
@@ -588,8 +639,8 @@ function spawnParticles(giftType, now, wave = 0) {
|
||||
for (let side = -1; side <= 1; side += 2) {
|
||||
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)
|
||||
? 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),
|
||||
@@ -777,8 +828,8 @@ function playOne(item) {
|
||||
activeMeta = { giftType, name, mt }
|
||||
spawnParticles(giftType, now, 0)
|
||||
const waves = style.shockwaves != null
|
||||
? style.shockwaves
|
||||
: (style.showHeartOutline && !isIOS ? 3 : 2)
|
||||
? 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)
|
||||
@@ -825,10 +876,10 @@ function tryBumpSame(payload) {
|
||||
}
|
||||
// 叠加正在播放的特效
|
||||
if (
|
||||
activeMeta
|
||||
&& activeMeta.giftType === payload.giftType
|
||||
&& activeMeta.name === payload.name
|
||||
&& texts.includes(activeMeta.mt)
|
||||
activeMeta
|
||||
&& activeMeta.giftType === payload.giftType
|
||||
&& activeMeta.name === payload.name
|
||||
&& texts.includes(activeMeta.mt)
|
||||
) {
|
||||
const mt = activeMeta.mt
|
||||
const now = performance.now()
|
||||
@@ -887,8 +938,8 @@ const play = (item) => {
|
||||
if (!queueTimer) {
|
||||
const active = texts[0]
|
||||
const remain = active
|
||||
? Math.max(200, active.duration - (performance.now() - active.birthTime) + 200)
|
||||
: 400
|
||||
? Math.max(200, active.duration - (performance.now() - active.birthTime) + 200)
|
||||
: 400
|
||||
scheduleNextFromQueue(remain)
|
||||
}
|
||||
return
|
||||
@@ -904,9 +955,9 @@ const play = (item) => {
|
||||
const playSequence = (items, opts = {}) => {
|
||||
const max = opts.max != null ? opts.max : 10
|
||||
const list = (Array.isArray(items) ? items : [])
|
||||
.map(normalizePlayItem)
|
||||
.filter(Boolean)
|
||||
.slice(0, max)
|
||||
.map(normalizePlayItem)
|
||||
.filter(Boolean)
|
||||
.slice(0, max)
|
||||
if (!list.length) return
|
||||
clearTimeout(queueTimer)
|
||||
queueTimer = null
|
||||
@@ -989,15 +1040,30 @@ function animate(timestamp) {
|
||||
const age = (now - p.birthTime) / 1000
|
||||
// 飘逸减速:速度随时间衰减,更高级
|
||||
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
|
||||
|
||||
let vortexX = 0
|
||||
let vortexY = 0
|
||||
// 特效四:流体力学场 (Turbulence)
|
||||
if (ENABLE_TURBULENCE) {
|
||||
vortexX = Math.sin(age * 3.0 + p.birthTime) * 15 * damp
|
||||
vortexY = Math.cos(age * 2.5 + p.rot) * 10 * damp
|
||||
}
|
||||
|
||||
const x = p.x + p.vx * age * damp + Math.sin(age * 2.2 + p.rot) * (p.drift || 0) * 0.45 + vortexX
|
||||
const y = p.y + p.vy * age * damp + 0.5 * p.gravity * age * age * 0.75 + vortexY
|
||||
const opacity = progress < 0.12
|
||||
? easeOutCubic(progress / 0.12)
|
||||
: 1 - ((progress - 0.12) / 0.88) ** 1.15
|
||||
? 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()
|
||||
|
||||
// 特效一:叠加发光混合模式 (Blend Modes)
|
||||
if (ENABLE_BLEND_MODES && (p.kind === 'spark' || p.kind === 'star' || p.kind === 'dot')) {
|
||||
ctx.globalCompositeOperation = 'lighter'
|
||||
}
|
||||
|
||||
ctx.globalAlpha = Math.max(0, opacity)
|
||||
ctx.fillStyle = p.color
|
||||
ctx.shadowColor = p.glow || p.color
|
||||
@@ -1024,9 +1090,28 @@ function animate(timestamp) {
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
} else if (p.kind === 'spark') {
|
||||
ctx.translate(x, y)
|
||||
ctx.rotate(rot)
|
||||
ctx.fillRect(-size * 0.28, -size * 1.15, size * 0.56, size * 2.3)
|
||||
// 特效二:物理拉丝拖尾 (Motion Blur)
|
||||
if (ENABLE_MOTION_BLUR) {
|
||||
const speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy) * damp;
|
||||
const stretch = Math.max(1, speed * 0.15);
|
||||
const angle = Math.atan2(p.vy, p.vx);
|
||||
|
||||
ctx.translate(x, y)
|
||||
ctx.rotate(angle)
|
||||
|
||||
const tailGrad = ctx.createLinearGradient(0, 0, -size * stretch, 0);
|
||||
tailGrad.addColorStop(0, p.color);
|
||||
tailGrad.addColorStop(1, 'rgba(255,255,255,0)');
|
||||
|
||||
ctx.fillStyle = tailGrad;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 0, size * stretch, size * 0.4, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else {
|
||||
ctx.translate(x, y)
|
||||
ctx.rotate(rot)
|
||||
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)
|
||||
@@ -1052,7 +1137,7 @@ function animate(timestamp) {
|
||||
if (opacity < 0.01) continue
|
||||
const wobble = Math.sin((now - mt.birthTime) / 180) * mt.wobble
|
||||
|
||||
// soft aura behind text(iOS 跳过径向渐变,避免卡顿/错乱)
|
||||
// soft aura behind text
|
||||
if (mt.aura && !SKIP_AURA) {
|
||||
const auraR = Math.max(0.1, Number(fontSize) * 1.8 * Math.max(0, scale) || 0)
|
||||
if (auraR > 0.1) {
|
||||
@@ -1079,20 +1164,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)
|
||||
@@ -1104,42 +1189,70 @@ 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) * 0.7
|
||||
const outlineOpacity = heartOutlineOpacity(mt, now) * (ENABLE_NEON_GLOW ? 1.0 : 0.7)
|
||||
const outlineSize = fontSize * 1.55 * outlineScale
|
||||
|
||||
ctx.save()
|
||||
ctx.globalAlpha = outlineOpacity
|
||||
ctx.translate(cx, cy)
|
||||
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()
|
||||
|
||||
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))
|
||||
@@ -1147,32 +1260,54 @@ function animate(timestamp) {
|
||||
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()
|
||||
|
||||
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)
|
||||
? (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) {
|
||||
@@ -1223,6 +1358,15 @@ const onResize = () => resizeCanvas()
|
||||
|
||||
onMounted(() => {
|
||||
resizeCanvas()
|
||||
// 预加载书法字体,避免首帧特效落到系统兜底字体
|
||||
if (document.fonts?.load) {
|
||||
const stack = calligraphyFontStack()
|
||||
Promise.all([
|
||||
document.fonts.load(`400 64px ${stack}`),
|
||||
document.fonts.load('400 64px "Great Vibes"'),
|
||||
document.fonts.load('400 64px "Ma Shan Zheng"'),
|
||||
]).catch(() => {})
|
||||
}
|
||||
window.addEventListener('resize', onResize)
|
||||
window.visualViewport?.addEventListener('resize', onResize)
|
||||
window.visualViewport?.addEventListener('scroll', onResize)
|
||||
@@ -1282,4 +1426,4 @@ defineExpose({ play, playSequence, clear: clearAll, stop: clearAll })
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -696,6 +696,20 @@ function polaroidTransform(i) {
|
||||
border-radius: 10px; scrollbar-width: none;
|
||||
}
|
||||
.g-carousel-slide { flex: 0 0 100%; min-width: 100%; height: 52vh; scroll-snap-align: center; }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.gallery {
|
||||
max-width: 400px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
.overlap-wrap {
|
||||
max-width: 400px;
|
||||
height: clamp(340px, 48vh, 460px);
|
||||
}
|
||||
.g-carousel-slide {
|
||||
height: min(52vh, 420px);
|
||||
}
|
||||
}
|
||||
.g-carousel-slide img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.g-arrow {
|
||||
position: absolute; top: 50%; transform: translateY(-50%);
|
||||
|
||||
@@ -2472,7 +2472,7 @@ watch(
|
||||
text-shadow:
|
||||
0 0 6px rgba(253, 251, 247, 0.95),
|
||||
0 1px 0 rgba(255, 255, 255, 0.8);
|
||||
transform: translate3d(calc(var(--scroll-p, 0) * (100vw - 1.1em) + 0.55em - 50%), 0, 0);
|
||||
transform: translate3d(calc(var(--scroll-p, 0) * (100% - 1.1em) + 0.55em - 50%), 0, 0);
|
||||
will-change: transform;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
@@ -3582,4 +3582,97 @@ watch(
|
||||
12% { opacity: 1; transform: translateY(4px) scale(.98); filter: blur(0); }
|
||||
100% { opacity: 0; transform: translateY(-12px) scale(.94); filter: blur(1.4px); }
|
||||
}
|
||||
|
||||
/* ---------- PC:内容列限宽居中(缩略图不宜铺满大屏) ---------- */
|
||||
@media (min-width: 768px) {
|
||||
.wrapper {
|
||||
--pc-invite-width: 430px;
|
||||
background:
|
||||
radial-gradient(ellipse at 50% 12%, rgba(232, 201, 210, 0.12), transparent 42%),
|
||||
linear-gradient(180deg, #f3eee6 0%, #ebe4d8 100%);
|
||||
}
|
||||
.scroll-transform-host {
|
||||
max-width: var(--pc-invite-width);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background: #fdfbf7;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(168, 140, 107, 0.12),
|
||||
0 28px 80px rgba(95, 70, 39, 0.12);
|
||||
}
|
||||
.scroll-progress {
|
||||
left: 50%;
|
||||
right: auto;
|
||||
width: var(--pc-invite-width);
|
||||
max-width: 100%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.action-dock {
|
||||
left: 50%;
|
||||
right: auto;
|
||||
width: var(--pc-invite-width);
|
||||
max-width: 100%;
|
||||
transform: translateX(-50%);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 日历 PC:略放大卡片与选中爱心,移动端样式不变 */
|
||||
.wedding-calendar-page {
|
||||
min-height: 100dvh;
|
||||
padding-top: 48px;
|
||||
padding-bottom: 48px;
|
||||
}
|
||||
.calendar-card {
|
||||
width: min(100%, 392px);
|
||||
padding: 36px 28px 34px;
|
||||
}
|
||||
.calendar-card::before {
|
||||
inset: 14px;
|
||||
}
|
||||
.calendar-kicker {
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.5em;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
.calendar-title {
|
||||
margin-top: 14px;
|
||||
font-size: 34px;
|
||||
gap: 16px;
|
||||
}
|
||||
.calendar-title i {
|
||||
width: 40px;
|
||||
}
|
||||
.calendar-date-line {
|
||||
margin: 12px auto 26px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.calendar-weekdays,
|
||||
.calendar-grid {
|
||||
gap: 10px;
|
||||
}
|
||||
.calendar-weekdays {
|
||||
margin-bottom: 11px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.calendar-day {
|
||||
font-size: 16px;
|
||||
}
|
||||
.calendar-day.is-selected .calendar-day-num {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 17px;
|
||||
}
|
||||
.heart-ring {
|
||||
inset: -12px;
|
||||
}
|
||||
.heart-ring svg {
|
||||
width: 74px;
|
||||
height: 72px;
|
||||
}
|
||||
.calendar-note {
|
||||
margin-top: 28px;
|
||||
font-size: 14px;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user