1122 lines
50 KiB
HTML
1122 lines
50 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||
<title>抖音直播点赞爱心 · 520&1314独立特效</title>
|
||
<style>
|
||
:root {
|
||
--bg: #0a0a0f;
|
||
}
|
||
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
width: 100vw;
|
||
height: 100vh;
|
||
height: 100dvh;
|
||
overflow: hidden;
|
||
background: var(--bg);
|
||
font-family: 'PingFang SC', 'Helvetica Neue', 'Microsoft YaHei', sans-serif;
|
||
touch-action: manipulation;
|
||
-webkit-touch-callout: none;
|
||
-webkit-user-select: none;
|
||
user-select: none;
|
||
-webkit-tap-highlight-color: transparent;
|
||
cursor: pointer;
|
||
position: relative;
|
||
}
|
||
|
||
canvas {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 1;
|
||
}
|
||
|
||
/* ========== 底部提示 ========== */
|
||
.hint-bar {
|
||
position: absolute;
|
||
bottom: 40px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
z-index: 10;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
pointer-events: none;
|
||
transition: opacity 0.6s;
|
||
}
|
||
.hint-bar .icon-heart {
|
||
font-size: 24px;
|
||
animation: hintPulse 1.2s ease-in-out infinite;
|
||
}
|
||
.hint-bar .text {
|
||
color: rgba(255, 255, 255, 0.7);
|
||
font-size: 15px;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
@keyframes hintPulse {
|
||
0%,
|
||
100% {
|
||
transform: scale(1);
|
||
opacity: 0.7;
|
||
}
|
||
50% {
|
||
transform: scale(1.35);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
/* ========== 点赞计数器 ========== */
|
||
.like-counter {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%) translateY(20px);
|
||
z-index: 5;
|
||
pointer-events: none;
|
||
text-align: center;
|
||
transition: transform 0.15s cubic-bezier(0.18, 0.89, 0.32, 1.28);
|
||
}
|
||
.like-counter.pop {
|
||
transform: translate(-50%, -50%) translateY(20px) scale(1.25);
|
||
}
|
||
.like-counter .count {
|
||
font-size: 56px;
|
||
font-weight: 700;
|
||
color: #fff;
|
||
text-shadow: 0 0 40px rgba(255, 45, 85, 0.7), 0 0 80px rgba(255, 45, 85, 0.4), 0 4px 12px rgba(0, 0, 0, 0.5);
|
||
line-height: 1;
|
||
transition: color 0.5s, text-shadow 0.5s;
|
||
}
|
||
.like-counter .count.golden {
|
||
color: #ffd700;
|
||
text-shadow: 0 0 50px rgba(255, 200, 0, 0.9), 0 0 100px rgba(255, 180, 0, 0.6), 0 0 160px rgba(255, 160, 0, 0.35), 0 4px 12px rgba(0, 0, 0, 0.5);
|
||
}
|
||
.like-counter .count.pink {
|
||
color: #ff6b9d;
|
||
text-shadow: 0 0 50px rgba(255, 80, 130, 0.9), 0 0 100px rgba(255, 60, 110, 0.6), 0 0 140px rgba(255, 45, 95, 0.35), 0 4px 12px rgba(0, 0, 0, 0.5);
|
||
}
|
||
.like-counter .label {
|
||
font-size: 13px;
|
||
color: rgba(255, 255, 255, 0.55);
|
||
letter-spacing: 2px;
|
||
margin-top: 4px;
|
||
transition: color 0.5s;
|
||
}
|
||
|
||
/* ========== 右侧控制面板 ========== */
|
||
.control-panel {
|
||
position: absolute;
|
||
right: 16px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
z-index: 20;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
.ctrl-btn {
|
||
width: 56px;
|
||
height: 56px;
|
||
border-radius: 50%;
|
||
border: 2px solid rgba(255, 255, 255, 0.35);
|
||
background: rgba(20, 20, 30, 0.75);
|
||
backdrop-filter: blur(10px);
|
||
-webkit-backdrop-filter: blur(10px);
|
||
color: #fff;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.25s;
|
||
letter-spacing: 1px;
|
||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||
outline: none;
|
||
-webkit-tap-highlight-color: transparent;
|
||
user-select: none;
|
||
white-space: nowrap;
|
||
}
|
||
.ctrl-btn:active {
|
||
transform: scale(0.88);
|
||
border-color: rgba(255, 255, 255, 0.7);
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
|
||
}
|
||
.ctrl-btn.btn-520 {
|
||
background: rgba(255, 45, 85, 0.55);
|
||
border-color: rgba(255, 107, 157, 0.7);
|
||
text-shadow: 0 0 12px rgba(255, 150, 180, 0.8);
|
||
}
|
||
.ctrl-btn.btn-1314 {
|
||
background: rgba(200, 150, 0, 0.55);
|
||
border-color: rgba(255, 200, 0, 0.7);
|
||
text-shadow: 0 0 12px rgba(255, 220, 100, 0.8);
|
||
color: #ffd700;
|
||
}
|
||
.ctrl-btn.btn-reset {
|
||
width: 44px;
|
||
height: 44px;
|
||
font-size: 10px;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
border-color: rgba(255, 255, 255, 0.2);
|
||
color: rgba(255, 255, 255, 0.6);
|
||
letter-spacing: 0;
|
||
}
|
||
.ctrl-btn.btn-auto {
|
||
width: 44px;
|
||
height: 44px;
|
||
font-size: 10px;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
border-color: rgba(255, 255, 255, 0.2);
|
||
color: rgba(255, 255, 255, 0.6);
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
/* 移动端适配 */
|
||
@media (max-width: 640px) {
|
||
.like-counter .count {
|
||
font-size: 38px;
|
||
}
|
||
.hint-bar {
|
||
bottom: 24px;
|
||
}
|
||
.hint-bar .text {
|
||
font-size: 12px;
|
||
}
|
||
.control-panel {
|
||
right: 8px;
|
||
gap: 10px;
|
||
}
|
||
.ctrl-btn {
|
||
width: 46px;
|
||
height: 46px;
|
||
font-size: 11px;
|
||
}
|
||
.ctrl-btn.btn-reset,
|
||
.ctrl-btn.btn-auto {
|
||
width: 38px;
|
||
height: 38px;
|
||
font-size: 9px;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<canvas id="canvas"></canvas>
|
||
|
||
<!-- 点赞计数 -->
|
||
<div class="like-counter" id="likeCounter">
|
||
<div class="count" id="likeCount">0</div>
|
||
<div class="label">点 赞</div>
|
||
</div>
|
||
|
||
<!-- 底部提示 -->
|
||
<div class="hint-bar" id="hintBar">
|
||
<span class="icon-heart">❤️</span>
|
||
<span class="text">点击屏幕送出爱心</span>
|
||
</div>
|
||
|
||
<!-- 独立特效控制按钮 -->
|
||
<div class="control-panel" id="controlPanel">
|
||
<button class="ctrl-btn btn-520" id="btn520" title="触发520特效">520</button>
|
||
<button class="ctrl-btn btn-1314" id="btn1314" title="触发1314特效">1314</button>
|
||
<button class="ctrl-btn btn-auto" id="btnAuto" title="自动模拟点赞到520">➤520</button>
|
||
<button class="ctrl-btn btn-reset" id="btnReset" title="重置所有状态">↺</button>
|
||
</div>
|
||
|
||
<script>
|
||
(function() {
|
||
// ==================== DOM元素 ====================
|
||
const canvas = document.getElementById('canvas');
|
||
const ctx = canvas.getContext('2d');
|
||
const likeCountEl = document.getElementById('likeCount');
|
||
const likeCounter = document.getElementById('likeCounter');
|
||
const hintBar = document.getElementById('hintBar');
|
||
const btn520 = document.getElementById('btn520');
|
||
const btn1314 = document.getElementById('btn1314');
|
||
const btnReset = document.getElementById('btnReset');
|
||
const btnAuto = document.getElementById('btnAuto');
|
||
|
||
// ==================== 配置 ====================
|
||
const CONFIG = {
|
||
MAX_HEARTS: 55,
|
||
MAX_SPARKS: 120,
|
||
HEART_MIN_SIZE: 14,
|
||
HEART_MAX_SIZE: 34,
|
||
HEART_MIN_LIFE: 1400,
|
||
HEART_MAX_LIFE: 2600,
|
||
RISE_SPEED_MIN: 70,
|
||
RISE_SPEED_MAX: 150,
|
||
DRIFT_AMP_MIN: 8,
|
||
DRIFT_AMP_MAX: 45,
|
||
LONG_PRESS_DELAY: 130,
|
||
GLOW_BLUR: 10,
|
||
GLOW_ALPHA: 0.55,
|
||
DPR_CAP: 2,
|
||
MILESTONE_520_DURATION: 2800,
|
||
MILESTONE_1314_DURATION: 3800,
|
||
FLASH_520_DURATION: 200,
|
||
FLASH_1314_DURATION: 300,
|
||
};
|
||
|
||
// ==================== 颜色调色板 ====================
|
||
const HEART_COLORS = [
|
||
'#ff2442', '#ff2d55', '#ff3b6e', '#ff4770',
|
||
'#ff5e82', '#ff6b8a', '#ff3c5c', '#ff1a3d',
|
||
'#e8223e', '#ff5079', '#ff3860', '#ff4d6d',
|
||
'#ff3355', '#ff4466', '#ff597f',
|
||
];
|
||
const GLOW_COLORS = [
|
||
'#ff6b8a', '#ff7b99', '#ff8da6', '#ff9ab3',
|
||
'#ff7090', '#ff85a0', '#ff7795',
|
||
];
|
||
const GOLDEN_COLORS = [
|
||
'#ffd700', '#ffcc00', '#ffb800', '#ffa000',
|
||
'#ffe44d', '#ffc107', '#ffdb4d', '#ffc940',
|
||
'#f0c000', '#ffd740', '#ffab00', '#ffb300',
|
||
];
|
||
const GOLDEN_GLOW_COLORS = [
|
||
'#ffe88d', '#fff0a8', '#ffe680', '#fff3b0',
|
||
'#ffec99', '#ffeaa7', '#fff5cc',
|
||
];
|
||
|
||
function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
|
||
|
||
function rand(min, max) { return min + Math.random() * (max - min); }
|
||
|
||
// ==================== Canvas尺寸管理 ====================
|
||
let W, H, dpr;
|
||
|
||
function resizeCanvas() {
|
||
dpr = Math.min(window.devicePixelRatio || 1, CONFIG.DPR_CAP);
|
||
const rect = canvas.getBoundingClientRect();
|
||
W = rect.width;
|
||
H = rect.height;
|
||
canvas.width = W * dpr;
|
||
canvas.height = H * dpr;
|
||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||
ctx.scale(dpr, dpr);
|
||
}
|
||
window.addEventListener('resize', resizeCanvas);
|
||
window.addEventListener('orientationchange', () => setTimeout(resizeCanvas, 100));
|
||
resizeCanvas();
|
||
|
||
// ==================== 爱心绘制 ====================
|
||
function drawHeartPath(context, cx, cy, size) {
|
||
const s = size;
|
||
const bottomY = cy + s * 0.55;
|
||
const leftOuterX = cx - s * 0.95;
|
||
const leftOuterY = cy - s * 0.2;
|
||
const leftInnerX = cx - s * 0.45;
|
||
const leftInnerY = cy - s * 1.05;
|
||
const rightInnerX = cx + s * 0.45;
|
||
const rightInnerY = cy - s * 1.05;
|
||
const rightOuterX = cx + s * 0.95;
|
||
const rightOuterY = cy - s * 0.2;
|
||
context.beginPath();
|
||
context.moveTo(cx, bottomY);
|
||
context.bezierCurveTo(cx - s * 0.85, cy + s * 0.25, leftOuterX, leftOuterY, leftInnerX, leftInnerY);
|
||
context.bezierCurveTo(rightInnerX, rightInnerY, rightOuterX, rightOuterY, cx + s * 0.85, cy + s * 0.25);
|
||
context.closePath();
|
||
}
|
||
|
||
function drawGlowHeart(context, cx, cy, size, color, glowColor, opacity, glowAlpha) {
|
||
if (opacity <= 0.01 || size <= 1) return;
|
||
const globalAlpha = Math.min(opacity, 1);
|
||
const effectiveGlow = glowColor || color;
|
||
context.save();
|
||
context.globalAlpha = globalAlpha;
|
||
if (glowAlpha > 0.01) {
|
||
context.save();
|
||
context.shadowColor = effectiveGlow;
|
||
context.shadowBlur = CONFIG.GLOW_BLUR * (size / 20);
|
||
context.fillStyle = effectiveGlow;
|
||
context.globalAlpha = glowAlpha * globalAlpha;
|
||
drawHeartPath(context, cx, cy, size);
|
||
context.fill();
|
||
context.restore();
|
||
}
|
||
const gradient = context.createRadialGradient(
|
||
cx - size * 0.15, cy - size * 0.35, size * 0.08, cx, cy, size * 0.9
|
||
);
|
||
gradient.addColorStop(0, effectiveGlow);
|
||
gradient.addColorStop(0.45, color);
|
||
gradient.addColorStop(1, '#9a0018');
|
||
context.fillStyle = gradient;
|
||
context.shadowColor = 'transparent';
|
||
context.shadowBlur = 0;
|
||
drawHeartPath(context, cx, cy, size);
|
||
context.fill();
|
||
const highlightX = cx - size * 0.12;
|
||
const highlightY = cy - size * 0.42;
|
||
const highlightR = size * 0.14;
|
||
context.save();
|
||
context.globalAlpha = 0.5 * globalAlpha;
|
||
const hlGrad = context.createRadialGradient(
|
||
highlightX, highlightY, highlightR * 0.1, highlightX, highlightY, highlightR
|
||
);
|
||
hlGrad.addColorStop(0, '#ffffff');
|
||
hlGrad.addColorStop(0.5, 'rgba(255,255,255,0.4)');
|
||
hlGrad.addColorStop(1, 'rgba(255,255,255,0)');
|
||
context.fillStyle = hlGrad;
|
||
context.beginPath();
|
||
context.arc(highlightX, highlightY, highlightR, 0, Math.PI * 2);
|
||
context.fill();
|
||
context.restore();
|
||
context.restore();
|
||
}
|
||
|
||
// ==================== 粒子类 ====================
|
||
class HeartParticle {
|
||
constructor(x, y, now, colorOverride, glowColorOverride) {
|
||
this.startX = x;
|
||
this.startY = y;
|
||
this.birthTime = now;
|
||
this.maxAge = rand(CONFIG.HEART_MIN_LIFE, CONFIG.HEART_MAX_LIFE);
|
||
this.baseSize = rand(CONFIG.HEART_MIN_SIZE, CONFIG.HEART_MAX_SIZE);
|
||
this.color = colorOverride || pick(HEART_COLORS);
|
||
this.glowColor = glowColorOverride || pick(GLOW_COLORS);
|
||
this.peakScale = rand(0.75, 1.35);
|
||
this.riseSpeed = rand(CONFIG.RISE_SPEED_MIN, CONFIG.RISE_SPEED_MAX);
|
||
this.driftAmp = rand(CONFIG.DRIFT_AMP_MIN, CONFIG.DRIFT_AMP_MAX);
|
||
this.driftFreq = rand(1.2, 2.8);
|
||
this.driftPhase = rand(0, Math.PI * 2);
|
||
this.rotation = rand(-0.4, 0.4);
|
||
this.rotationSpeed = rand(-0.35, 0.35);
|
||
this.extraDrift = (Math.random() < 0.25) ? rand(-25, 25) : 0;
|
||
this.alive = true;
|
||
this.spawnedSparks = false;
|
||
}
|
||
getAge(now) { return now - this.birthTime; }
|
||
getProgress(now) { return Math.min(this.getAge(now) / this.maxAge, 1); }
|
||
getScale(now) {
|
||
const p = this.getProgress(now);
|
||
if (p < 0.18) { const t = p / 0.18;
|
||
const e = 1 - Math.pow(1 - t, 3.5); return this.peakScale * (e + Math.sin(t * Math.PI) * 0.08 * (1 -
|
||
t)); } else if (p < 0.65) { return this.peakScale * (1 - (p - 0.18) / 0.47 * 0.25); } else { const
|
||
t = (p - 0.65) / 0.35; return this.peakScale * 0.75 * (1 - t * t); }
|
||
}
|
||
getOpacity(now) {
|
||
const p = this.getProgress(now);
|
||
if (p < 0.45) return 1;
|
||
if (p < 0.78) return 1 - (p - 0.45) / 0.33 * 0.35;
|
||
const t = (p - 0.78) / 0.22; return 0.65 * (1 - t * t);
|
||
}
|
||
getGlowAlpha(now) {
|
||
const p = this.getProgress(now);
|
||
if (p < 0.5) return CONFIG.GLOW_ALPHA;
|
||
if (p < 0.8) return CONFIG.GLOW_ALPHA * (1 - (p - 0.5) / 0.3);
|
||
return 0;
|
||
}
|
||
getY(now) {
|
||
const ageSec = this.getAge(now) / 1000;
|
||
const mult = 1 + (this.getProgress(now) > 0.7 ? (this.getProgress(now) - 0.7) / 0.3 * 0.4 : 0);
|
||
return this.startY - this.riseSpeed * ageSec * mult;
|
||
}
|
||
getX(now) {
|
||
const ageSec = this.getAge(now) / 1000;
|
||
return this.startX + Math.sin(ageSec * this.driftFreq + this.driftPhase) * this.driftAmp + this
|
||
.extraDrift * Math.min(ageSec / 1.5, 1);
|
||
}
|
||
getRotation(now) { return this.rotation + this.rotationSpeed * (this.getAge(now) / 1000); }
|
||
isDead(now) { return this.getProgress(now) >= 1; }
|
||
shouldSpawnSparks(now) { return !this.spawnedSparks && this.getProgress(now) > 0.82; }
|
||
}
|
||
|
||
class SparkParticle {
|
||
constructor(x, y, color, now) {
|
||
this.x = x;
|
||
this.y = y;
|
||
const angle = rand(0, Math.PI * 2);
|
||
const speed = rand(25, 90);
|
||
this.vx = Math.cos(angle) * speed;
|
||
this.vy = Math.sin(angle) * speed - rand(15, 50);
|
||
this.birthTime = now;
|
||
this.life = rand(250, 550);
|
||
this.color = color;
|
||
this.size = rand(1.5, 4.5);
|
||
this.alive = true;
|
||
}
|
||
getAge(now) { return now - this.birthTime; }
|
||
getProgress(now) { return Math.min(this.getAge(now) / this.life, 1); }
|
||
getOpacity(now) { const p = this.getProgress(now); return 1 - p * p; }
|
||
getCurrentX(now) { return this.x + this.vx * (this.getAge(now) / 1000); }
|
||
getCurrentY(now) { return this.y + this.vy * (this.getAge(now) / 1000) + 0.5 * 80 * Math.pow(this.getAge(
|
||
now) / 1000, 2); }
|
||
isDead(now) { return this.getProgress(now) >= 1; }
|
||
}
|
||
|
||
class MilestoneTextEffect {
|
||
constructor(text, birthTime, duration, config) {
|
||
this.text = text;
|
||
this.birthTime = birthTime;
|
||
this.duration = duration;
|
||
this.fontSize = config.fontSize || 90;
|
||
this.color = config.color || '#ff2d55';
|
||
this.glowColor = config.glowColor || '#ff6b8a';
|
||
this.strokeColor = config.strokeColor || 'rgba(255,255,255,0.7)';
|
||
this.peakScale = config.peakScale || 1.35;
|
||
this.isGolden = config.isGolden || false;
|
||
this.showHeartOutline = config.showHeartOutline || false;
|
||
this.alive = true;
|
||
}
|
||
getAge(now) { return now - this.birthTime; }
|
||
getProgress(now) { return Math.min(this.getAge(now) / this.duration, 1); }
|
||
getScale(now) {
|
||
const p = this.getProgress(now);
|
||
if (p < 0.22) { const t = p / 0.22; return this.peakScale * (1 - Math.pow(1 - t, 4) + Math.sin(t * Math
|
||
.PI) * 0.15 * (1 - t)); } else if (p < 0.6) { return this.peakScale - (this.peakScale - 1) * (
|
||
p - 0.22) / 0.38 * 0.5; } else { return 1 - (p - 0.6) / 0.4 * 0.3; }
|
||
}
|
||
getOpacity(now) {
|
||
const p = this.getProgress(now);
|
||
if (p < 0.6) return 1;
|
||
if (p < 0.85) return 1 - (p - 0.6) / 0.25 * 0.5;
|
||
return 0.5 * (1 - Math.pow((p - 0.85) / 0.15, 2));
|
||
}
|
||
getGlowIntensity(now) {
|
||
const p = this.getProgress(now);
|
||
if (p < 0.25) return p / 0.25;
|
||
if (p < 0.55) return 1;
|
||
if (p < 0.8) return 1 - (p - 0.55) / 0.25 * 0.6;
|
||
return 0.4 * (1 - (p - 0.8) / 0.2);
|
||
}
|
||
getHeartOutlineScale(now) {
|
||
if (!this.showHeartOutline) return 0;
|
||
const p = this.getProgress(now);
|
||
if (p < 0.15) return p / 0.15 * 0.8;
|
||
if (p < 0.5) return 0.8 + (p - 0.15) / 0.35 * 1.2;
|
||
if (p < 0.8) return 2.0;
|
||
return 2.0 * (1 - (p - 0.8) / 0.2);
|
||
}
|
||
getHeartOutlineOpacity(now) {
|
||
if (!this.showHeartOutline) return 0;
|
||
const p = this.getProgress(now);
|
||
if (p < 0.15) return p / 0.15 * 0.7;
|
||
if (p < 0.45) return 0.7;
|
||
if (p < 0.7) return 0.7 * (1 - (p - 0.45) / 0.25);
|
||
return 0;
|
||
}
|
||
isDead(now) { return this.getProgress(now) >= 1; }
|
||
}
|
||
|
||
// ==================== 全局状态 ====================
|
||
let hearts = [];
|
||
let sparks = [];
|
||
let milestoneTexts = [];
|
||
let flashAlpha = 0;
|
||
let flashDecayRate = 0;
|
||
const triggeredMilestones = new Set();
|
||
let totalLikes = 0;
|
||
let animationId = null;
|
||
let lastFrameTime = performance.now();
|
||
let longPressTimer = null;
|
||
let isPressing = false;
|
||
let lastClickTime = 0;
|
||
let rapidClickCount = 0;
|
||
const RAPID_CLICK_WINDOW = 400;
|
||
const RAPID_CLICK_THRESHOLD = 3;
|
||
let autoSimulateTimer = null;
|
||
let isAutoSimulating = false;
|
||
|
||
// ==================== 创建爱心 ====================
|
||
function spawnHeart(x, y, now, colorOverride, glowColorOverride) {
|
||
if (hearts.length >= CONFIG.MAX_HEARTS) {
|
||
const oldest = hearts.shift();
|
||
if (oldest && !oldest.spawnedSparks) spawnSparksForHeart(oldest, now);
|
||
}
|
||
const heart = new HeartParticle(x, y, now || performance.now(), colorOverride, glowColorOverride);
|
||
hearts.push(heart);
|
||
return heart;
|
||
}
|
||
|
||
function spawnSparksForHeart(heart, now) {
|
||
if (heart.spawnedSparks) return;
|
||
heart.spawnedSparks = true;
|
||
const cx = heart.getX(now);
|
||
const cy = heart.getY(now);
|
||
const count = Math.floor(rand(3, 7));
|
||
for (let i = 0; i < count; i++) {
|
||
if (sparks.length >= CONFIG.MAX_SPARKS) sparks.shift();
|
||
sparks.push(new SparkParticle(cx, cy, heart.glowColor, now));
|
||
}
|
||
}
|
||
|
||
function burstHearts(x, y, now, count, colorOverride, glowColorOverride) {
|
||
const n = count || Math.floor(rand(2, 5));
|
||
for (let i = 0; i < n; i++) {
|
||
const heart = spawnHeart(x + rand(-35, 35), y + rand(-30, 30), now, colorOverride, glowColorOverride);
|
||
heart.riseSpeed *= rand(1.1, 1.6);
|
||
heart.driftAmp *= rand(1.2, 2.0);
|
||
heart.peakScale *= rand(0.9, 1.4);
|
||
heart.maxAge = rand(CONFIG.HEART_MIN_LIFE * 0.8, CONFIG.HEART_MAX_LIFE * 1.2);
|
||
}
|
||
}
|
||
|
||
// ==================== 里程碑特效 ====================
|
||
function triggerMilestone520(now) {
|
||
const cx = W / 2;
|
||
const cy = H / 2;
|
||
milestoneTexts.push(new MilestoneTextEffect('520', now, CONFIG.MILESTONE_520_DURATION, {
|
||
fontSize: Math.min(W * 0.22, 110),
|
||
color: '#ff2d55',
|
||
glowColor: '#ff6b9d',
|
||
strokeColor: 'rgba(255,255,255,0.8)',
|
||
peakScale: 1.35,
|
||
isGolden: false,
|
||
showHeartOutline: false,
|
||
}));
|
||
burstHearts(cx, cy, now, 25, pick(HEART_COLORS), pick(GLOW_COLORS));
|
||
burstHearts(cx - W * 0.2, cy + rand(-30, 30), now, 8, pick(HEART_COLORS), pick(GLOW_COLORS));
|
||
burstHearts(cx + W * 0.2, cy + rand(-30, 30), now, 8, pick(HEART_COLORS), pick(GLOW_COLORS));
|
||
flashAlpha = 0.7;
|
||
flashDecayRate = 0.7 / (CONFIG.FLASH_520_DURATION / 16);
|
||
likeCountEl.classList.add('pink');
|
||
setTimeout(() => likeCountEl.classList.remove('pink'), 2200);
|
||
console.log('💖 520特效触发!');
|
||
}
|
||
|
||
function triggerMilestone1314(now) {
|
||
const cx = W / 2;
|
||
const cy = H / 2;
|
||
milestoneTexts.push(new MilestoneTextEffect('1314', now, CONFIG.MILESTONE_1314_DURATION, {
|
||
fontSize: Math.min(W * 0.25, 130),
|
||
color: '#ffd700',
|
||
glowColor: '#ffe88d',
|
||
strokeColor: 'rgba(255,255,255,0.9)',
|
||
peakScale: 1.5,
|
||
isGolden: true,
|
||
showHeartOutline: true,
|
||
}));
|
||
burstHearts(cx, cy, now, 40, pick(GOLDEN_COLORS), pick(GOLDEN_GLOW_COLORS));
|
||
burstHearts(cx - W * 0.25, cy + rand(-40, 40), now, 12, pick(GOLDEN_COLORS), pick(GOLDEN_GLOW_COLORS));
|
||
burstHearts(cx + W * 0.25, cy + rand(-40, 40), now, 12, pick(GOLDEN_COLORS), pick(GOLDEN_GLOW_COLORS));
|
||
burstHearts(cx, cy - H * 0.2, now, 10, pick(GOLDEN_COLORS), pick(GOLDEN_GLOW_COLORS));
|
||
flashAlpha = 0.85;
|
||
flashDecayRate = 0.85 / (CONFIG.FLASH_1314_DURATION / 16);
|
||
likeCountEl.classList.add('golden');
|
||
setTimeout(() => likeCountEl.classList.remove('golden'), 3200);
|
||
console.log('💛✨ 1314特效触发!一生一世!');
|
||
}
|
||
|
||
function checkMilestones(oldTotal, newTotal, now) {
|
||
if (oldTotal < 520 && newTotal >= 520 && !triggeredMilestones.has(520)) {
|
||
triggeredMilestones.add(520);
|
||
triggerMilestone520(now);
|
||
}
|
||
if (oldTotal < 1314 && newTotal >= 1314 && !triggeredMilestones.has(1314)) {
|
||
triggeredMilestones.add(1314);
|
||
triggerMilestone1314(now);
|
||
}
|
||
}
|
||
|
||
// ==================== 点赞计数 ====================
|
||
function incrementLikes(count = 1) {
|
||
const oldTotal = totalLikes;
|
||
totalLikes += count;
|
||
likeCountEl.textContent = formatNumber(totalLikes);
|
||
likeCounter.classList.add('pop');
|
||
setTimeout(() => likeCounter.classList.remove('pop'), 150);
|
||
checkMilestones(oldTotal, totalLikes, performance.now());
|
||
}
|
||
|
||
function setLikesDirectly(value) {
|
||
const oldTotal = totalLikes;
|
||
totalLikes = Math.max(0, Math.floor(value));
|
||
likeCountEl.textContent = formatNumber(totalLikes);
|
||
checkMilestones(oldTotal, totalLikes, performance.now());
|
||
}
|
||
|
||
function formatNumber(num) {
|
||
if (num >= 100000) { const wan = num / 10000; return wan >= 10 ? Math.floor(wan) + '万' : wan.toFixed(1) +
|
||
'万'; }
|
||
if (num >= 10000) { return (num / 10000).toFixed(1) + '万'; }
|
||
return num.toLocaleString('zh-CN');
|
||
}
|
||
|
||
// ==================== 输入处理 ====================
|
||
function getEventPos(e) {
|
||
if (e.touches && e.touches.length > 0) return { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
||
if (e.changedTouches && e.changedTouches.length > 0) return { x: e.changedTouches[0].clientX, y: e
|
||
.changedTouches[0].clientY };
|
||
return { x: e.clientX, y: e.clientY };
|
||
}
|
||
|
||
function handlePressStart(e) {
|
||
e.preventDefault();
|
||
const pos = getEventPos(e);
|
||
const now = performance.now();
|
||
isPressing = true;
|
||
if (now - lastClickTime < RAPID_CLICK_WINDOW) { rapidClickCount++; } else { rapidClickCount = 1; }
|
||
lastClickTime = now;
|
||
if (rapidClickCount >= RAPID_CLICK_THRESHOLD) {
|
||
burstHearts(pos.x, pos.y, now, Math.floor(rand(2, 5)));
|
||
incrementLikes(Math.floor(rand(2, 5)));
|
||
rapidClickCount = 0;
|
||
} else {
|
||
spawnHeart(pos.x, pos.y, now);
|
||
incrementLikes(1);
|
||
}
|
||
if (hintBar.style.opacity !== '0') {
|
||
hintBar.style.transition = 'opacity 0.4s';
|
||
hintBar.style.opacity = '0';
|
||
}
|
||
if (longPressTimer) clearInterval(longPressTimer);
|
||
longPressTimer = setInterval(() => {
|
||
if (isPressing && hearts.length < CONFIG.MAX_HEARTS) {
|
||
spawnHeart(pos.x + rand(-20, 20), pos.y + rand(-15, 15), performance.now());
|
||
incrementLikes(1);
|
||
}
|
||
}, CONFIG.LONG_PRESS_DELAY);
|
||
}
|
||
|
||
function handlePressMove(e) {
|
||
if (!isPressing) return;
|
||
e.preventDefault();
|
||
const pos = getEventPos(e);
|
||
if (Math.random() < 0.35 && hearts.length < CONFIG.MAX_HEARTS) {
|
||
spawnHeart(pos.x, pos.y, performance.now());
|
||
incrementLikes(1);
|
||
}
|
||
}
|
||
|
||
function handlePressEnd(e) {
|
||
e.preventDefault();
|
||
isPressing = false;
|
||
rapidClickCount = 0;
|
||
if (longPressTimer) { clearInterval(longPressTimer);
|
||
longPressTimer = null; }
|
||
}
|
||
|
||
canvas.addEventListener('mousedown', handlePressStart);
|
||
canvas.addEventListener('mousemove', handlePressMove);
|
||
canvas.addEventListener('mouseup', handlePressEnd);
|
||
canvas.addEventListener('mouseleave', handlePressEnd);
|
||
canvas.addEventListener('touchstart', handlePressStart, { passive: false });
|
||
canvas.addEventListener('touchmove', handlePressMove, { passive: false });
|
||
canvas.addEventListener('touchend', handlePressEnd);
|
||
canvas.addEventListener('touchcancel', handlePressEnd);
|
||
canvas.addEventListener('dblclick', (e) => e.preventDefault());
|
||
|
||
// ==================== 按钮事件 ====================
|
||
btn520.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
e.preventDefault();
|
||
const now = performance.now();
|
||
// 直接触发520特效(不依赖点赞数)
|
||
milestoneTexts.push(new MilestoneTextEffect('520', now, CONFIG.MILESTONE_520_DURATION, {
|
||
fontSize: Math.min(W * 0.22, 110),
|
||
color: '#ff2d55',
|
||
glowColor: '#ff6b9d',
|
||
strokeColor: 'rgba(255,255,255,0.8)',
|
||
peakScale: 1.35,
|
||
isGolden: false,
|
||
showHeartOutline: false,
|
||
}));
|
||
const cx = W / 2;
|
||
const cy = H / 2;
|
||
burstHearts(cx, cy, now, 25, pick(HEART_COLORS), pick(GLOW_COLORS));
|
||
burstHearts(cx - W * 0.2, cy + rand(-30, 30), now, 8, pick(HEART_COLORS), pick(GLOW_COLORS));
|
||
burstHearts(cx + W * 0.2, cy + rand(-30, 30), now, 8, pick(HEART_COLORS), pick(GLOW_COLORS));
|
||
flashAlpha = 0.7;
|
||
flashDecayRate = 0.7 / (CONFIG.FLASH_520_DURATION / 16);
|
||
likeCountEl.classList.add('pink');
|
||
setTimeout(() => likeCountEl.classList.remove('pink'), 2200);
|
||
hintBar.style.opacity = '0';
|
||
console.log('💖 手动触发520特效!');
|
||
});
|
||
|
||
btn1314.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
e.preventDefault();
|
||
const now = performance.now();
|
||
milestoneTexts.push(new MilestoneTextEffect('1314', now, CONFIG.MILESTONE_1314_DURATION, {
|
||
fontSize: Math.min(W * 0.25, 130),
|
||
color: '#ffd700',
|
||
glowColor: '#ffe88d',
|
||
strokeColor: 'rgba(255,255,255,0.9)',
|
||
peakScale: 1.5,
|
||
isGolden: true,
|
||
showHeartOutline: true,
|
||
}));
|
||
const cx = W / 2;
|
||
const cy = H / 2;
|
||
burstHearts(cx, cy, now, 40, pick(GOLDEN_COLORS), pick(GOLDEN_GLOW_COLORS));
|
||
burstHearts(cx - W * 0.25, cy + rand(-40, 40), now, 12, pick(GOLDEN_COLORS), pick(GOLDEN_GLOW_COLORS));
|
||
burstHearts(cx + W * 0.25, cy + rand(-40, 40), now, 12, pick(GOLDEN_COLORS), pick(GOLDEN_GLOW_COLORS));
|
||
burstHearts(cx, cy - H * 0.2, now, 10, pick(GOLDEN_COLORS), pick(GOLDEN_GLOW_COLORS));
|
||
flashAlpha = 0.85;
|
||
flashDecayRate = 0.85 / (CONFIG.FLASH_1314_DURATION / 16);
|
||
likeCountEl.classList.add('golden');
|
||
setTimeout(() => likeCountEl.classList.remove('golden'), 3200);
|
||
hintBar.style.opacity = '0';
|
||
console.log('💛✨ 手动触发1314特效!');
|
||
});
|
||
|
||
btnReset.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
e.preventDefault();
|
||
totalLikes = 0;
|
||
likeCountEl.textContent = '0';
|
||
likeCountEl.classList.remove('pink', 'golden');
|
||
triggeredMilestones.clear();
|
||
milestoneTexts = [];
|
||
hearts = [];
|
||
sparks = [];
|
||
flashAlpha = 0;
|
||
flashDecayRate = 0;
|
||
if (autoSimulateTimer) { clearInterval(autoSimulateTimer);
|
||
autoSimulateTimer = null; }
|
||
isAutoSimulating = false;
|
||
btnAuto.textContent = '➤520';
|
||
btnAuto.style.background = 'rgba(255,255,255,0.08)';
|
||
hintBar.style.opacity = '1';
|
||
console.log('🔄 全部状态已重置');
|
||
});
|
||
|
||
// 自动模拟点赞到520
|
||
btnAuto.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
e.preventDefault();
|
||
if (isAutoSimulating) {
|
||
// 停止模拟
|
||
if (autoSimulateTimer) { clearInterval(autoSimulateTimer);
|
||
autoSimulateTimer = null; }
|
||
isAutoSimulating = false;
|
||
btnAuto.textContent = '➤520';
|
||
btnAuto.style.background = 'rgba(255,255,255,0.08)';
|
||
console.log('⏸ 自动模拟已停止');
|
||
return;
|
||
}
|
||
// 开始模拟
|
||
if (totalLikes >= 520) {
|
||
console.log('⚠️ 点赞数已达520,请先重置');
|
||
return;
|
||
}
|
||
isAutoSimulating = true;
|
||
btnAuto.textContent = '⏹';
|
||
btnAuto.style.background = 'rgba(255,80,80,0.45)';
|
||
hintBar.style.opacity = '0';
|
||
console.log('▶ 开始自动模拟点赞到520...');
|
||
const cx = W / 2;
|
||
const cy = H * 0.65;
|
||
autoSimulateTimer = setInterval(() => {
|
||
if (totalLikes >= 520) {
|
||
clearInterval(autoSimulateTimer);
|
||
autoSimulateTimer = null;
|
||
isAutoSimulating = false;
|
||
btnAuto.textContent = '➤520';
|
||
btnAuto.style.background = 'rgba(255,255,255,0.08)';
|
||
console.log('✅ 自动模拟完成,已到达520!');
|
||
return;
|
||
}
|
||
const now = performance.now();
|
||
const sx = cx + rand(-80, 80);
|
||
const sy = cy + rand(-60, 60);
|
||
spawnHeart(sx, sy, now);
|
||
incrementLikes(1);
|
||
}, 60);
|
||
});
|
||
|
||
// 防止按钮上的事件冒泡到canvas
|
||
document.getElementById('controlPanel').addEventListener('mousedown', (e) => e.stopPropagation());
|
||
document.getElementById('controlPanel').addEventListener('touchstart', (e) => e.stopPropagation());
|
||
|
||
// ==================== 动画循环 ====================
|
||
function animate(timestamp) {
|
||
const now = timestamp || performance.now();
|
||
const deltaTime = Math.min(now - lastFrameTime, 50);
|
||
lastFrameTime = now;
|
||
|
||
ctx.clearRect(0, 0, W, H);
|
||
|
||
// 闪白衰减
|
||
if (flashAlpha > 0.001) {
|
||
flashAlpha -= flashDecayRate;
|
||
if (flashAlpha < 0) flashAlpha = 0;
|
||
}
|
||
|
||
// 火花
|
||
for (let i = sparks.length - 1; i >= 0; i--) {
|
||
const spark = sparks[i];
|
||
if (spark.isDead(now)) { sparks.splice(i, 1); continue; }
|
||
const sx = spark.getCurrentX(now);
|
||
const sy = spark.getCurrentY(now);
|
||
const opacity = spark.getOpacity(now);
|
||
const size = spark.size * (1 - spark.getProgress(now) * 0.6);
|
||
ctx.save();
|
||
ctx.globalAlpha = opacity;
|
||
ctx.fillStyle = spark.color;
|
||
ctx.shadowColor = spark.color;
|
||
ctx.shadowBlur = 4;
|
||
ctx.beginPath();
|
||
ctx.arc(sx, sy, size, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
ctx.restore();
|
||
}
|
||
|
||
// 爱心
|
||
for (let i = hearts.length - 1; i >= 0; i--) {
|
||
const heart = hearts[i];
|
||
if (heart.shouldSpawnSparks(now)) spawnSparksForHeart(heart, now);
|
||
if (heart.isDead(now)) {
|
||
if (!heart.spawnedSparks) spawnSparksForHeart(heart, now);
|
||
hearts.splice(i, 1);
|
||
continue;
|
||
}
|
||
const cx = heart.getX(now);
|
||
const cy = heart.getY(now);
|
||
const size = heart.baseSize * heart.getScale(now);
|
||
if (cy < -size * 2 || cy > H + size * 2 || cx < -size * 2 || cx > W + size * 2) {
|
||
if (!heart.spawnedSparks) spawnSparksForHeart(heart, now);
|
||
hearts.splice(i, 1);
|
||
continue;
|
||
}
|
||
ctx.save();
|
||
ctx.translate(cx, cy);
|
||
ctx.rotate(heart.getRotation(now));
|
||
drawGlowHeart(ctx, 0, 0, size, heart.color, heart.glowColor, heart.getOpacity(now), heart.getGlowAlpha(
|
||
now));
|
||
ctx.restore();
|
||
}
|
||
|
||
// 里程碑文字
|
||
for (let i = milestoneTexts.length - 1; i >= 0; i--) {
|
||
const mt = milestoneTexts[i];
|
||
if (mt.isDead(now)) { milestoneTexts.splice(i, 1); continue; }
|
||
const cx = W / 2;
|
||
const cy = H / 2;
|
||
const scale = mt.getScale(now);
|
||
const opacity = mt.getOpacity(now);
|
||
const glowIntensity = mt.getGlowIntensity(now);
|
||
const fontSize = mt.fontSize;
|
||
if (opacity < 0.01) continue;
|
||
|
||
ctx.save();
|
||
ctx.globalAlpha = opacity;
|
||
ctx.translate(cx, cy);
|
||
ctx.scale(scale, scale);
|
||
const glowBlur = fontSize * 0.35 * glowIntensity;
|
||
ctx.shadowColor = mt.glowColor;
|
||
ctx.shadowBlur = glowBlur;
|
||
ctx.strokeStyle = mt.strokeColor;
|
||
ctx.lineWidth = fontSize * 0.06;
|
||
ctx.lineJoin = 'round';
|
||
ctx.font = `900 ${fontSize}px "PingFang SC","Helvetica Neue","Microsoft YaHei",sans-serif`;
|
||
ctx.textAlign = 'center';
|
||
ctx.textBaseline = 'middle';
|
||
ctx.strokeText(mt.text, 0, 0);
|
||
const textGrad = ctx.createLinearGradient(0, -fontSize * 0.5, 0, fontSize * 0.5);
|
||
if (mt.isGolden) {
|
||
textGrad.addColorStop(0, '#fff8c0');
|
||
textGrad.addColorStop(0.3, '#ffd700');
|
||
textGrad.addColorStop(0.6, '#ffb800');
|
||
textGrad.addColorStop(1, '#e6a000');
|
||
} else {
|
||
textGrad.addColorStop(0, '#ffb3c6');
|
||
textGrad.addColorStop(0.35, '#ff2d55');
|
||
textGrad.addColorStop(0.7, '#e61a3d');
|
||
textGrad.addColorStop(1, '#b3001e');
|
||
}
|
||
ctx.fillStyle = textGrad;
|
||
ctx.shadowBlur = glowBlur * 1.4;
|
||
ctx.fillText(mt.text, 0, 0);
|
||
ctx.shadowBlur = 0;
|
||
ctx.fillStyle = 'rgba(255,255,255,0.35)';
|
||
ctx.fillText(mt.text, -fontSize * 0.015, -fontSize * 0.025);
|
||
ctx.restore();
|
||
|
||
// 1314爱心轮廓
|
||
if (mt.showHeartOutline && mt.getHeartOutlineOpacity(now) > 0.01) {
|
||
const outlineScale = mt.getHeartOutlineScale(now);
|
||
const outlineOpacity = mt.getHeartOutlineOpacity(now);
|
||
const outlineSize = fontSize * 1.6 * outlineScale;
|
||
ctx.save();
|
||
ctx.globalAlpha = outlineOpacity;
|
||
ctx.translate(cx, cy);
|
||
ctx.strokeStyle = mt.glowColor;
|
||
ctx.lineWidth = outlineSize * 0.04;
|
||
ctx.shadowColor = mt.glowColor;
|
||
ctx.shadowBlur = outlineSize * 0.3;
|
||
drawHeartPath(ctx, 0, 0, outlineSize);
|
||
ctx.stroke();
|
||
ctx.lineWidth = outlineSize * 0.07;
|
||
ctx.shadowBlur = outlineSize * 0.5;
|
||
ctx.globalAlpha = outlineOpacity * 0.4;
|
||
ctx.stroke();
|
||
ctx.restore();
|
||
}
|
||
|
||
// 1314旋转光点
|
||
if (mt.isGolden && opacity > 0.2) {
|
||
const numOrbs = 14;
|
||
const orbRadius = fontSize * 1.1;
|
||
const orbSpeed = 1.8;
|
||
const age = mt.getAge(now) / 1000;
|
||
for (let j = 0; j < numOrbs; j++) {
|
||
const angle = age * orbSpeed + j * (Math.PI * 2 / numOrbs);
|
||
const orbX = cx + Math.cos(angle) * orbRadius;
|
||
const orbY = cy + Math.sin(angle) * orbRadius * 0.7;
|
||
const orbSize = fontSize * 0.04 * (1 + Math.sin(age * 3 + j) * 0.4);
|
||
const orbAlpha = opacity * (0.5 + Math.sin(age * 2.5 + j) * 0.35);
|
||
ctx.save();
|
||
ctx.globalAlpha = Math.max(0, orbAlpha);
|
||
ctx.fillStyle = '#fffbe6';
|
||
ctx.shadowColor = '#ffd700';
|
||
ctx.shadowBlur = orbSize * 5;
|
||
ctx.beginPath();
|
||
ctx.arc(orbX, orbY, orbSize, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
ctx.restore();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 闪白覆盖
|
||
if (flashAlpha > 0.003) {
|
||
ctx.save();
|
||
ctx.globalAlpha = flashAlpha;
|
||
ctx.fillStyle = '#ffffff';
|
||
ctx.fillRect(0, 0, W, H);
|
||
ctx.restore();
|
||
}
|
||
|
||
// 清理
|
||
while (sparks.length > CONFIG.MAX_SPARKS) sparks.shift();
|
||
|
||
// 提示显示
|
||
if (hearts.length === 0 && sparks.length === 0 && milestoneTexts.length === 0 && !isPressing && !
|
||
isAutoSimulating && flashAlpha < 0.01) {
|
||
if (hintBar.style.opacity === '0' && totalLikes === 0) {
|
||
setTimeout(() => {
|
||
if (hearts.length === 0 && sparks.length === 0 && milestoneTexts.length === 0 && !
|
||
isPressing && !isAutoSimulating && flashAlpha < 0.01 && totalLikes === 0) {
|
||
hintBar.style.opacity = '1';
|
||
}
|
||
}, 1500);
|
||
}
|
||
}
|
||
|
||
animationId = requestAnimationFrame(animate);
|
||
}
|
||
|
||
// ==================== 启动 ====================
|
||
function start() {
|
||
resizeCanvas();
|
||
lastFrameTime = performance.now();
|
||
if (animationId) cancelAnimationFrame(animationId);
|
||
animationId = requestAnimationFrame(animate);
|
||
}
|
||
|
||
hintBar.style.opacity = '1';
|
||
start();
|
||
|
||
// ==================== 暴露API ====================
|
||
console.log('💖 抖音直播点赞爱心 · 独立特效版已就绪');
|
||
console.log(' 🎯 点击屏幕送出爱心');
|
||
console.log(' 🎯 长按持续产生爱心');
|
||
console.log(' 🎯 快速连点触发爱心爆发');
|
||
console.log(' 📌 右侧按钮【520】- 随时手动触发520特效');
|
||
console.log(' 📌 右侧按钮【1314】- 随时手动触发1314特效');
|
||
console.log(' 📌 右侧按钮【➤520】- 自动模拟点赞到520');
|
||
console.log(' 📌 右侧按钮【↺】- 重置所有状态');
|
||
console.log(' 💡 也可在控制台调用:');
|
||
console.log(' heartEffect.trigger520() - 触发520特效');
|
||
console.log(' heartEffect.trigger1314() - 触发1314特效');
|
||
console.log(' heartEffect.resetAll() - 重置全部');
|
||
console.log(' heartEffect.setLikes(500) - 设置点赞数');
|
||
console.log(' 📐 Canvas:' + Math.round(W) + '×' + Math.round(H) + ' @' + dpr + 'x');
|
||
|
||
window.heartEffect = {
|
||
trigger520: () => {
|
||
const now = performance.now();
|
||
const cx = W / 2;
|
||
const cy = H / 2;
|
||
milestoneTexts.push(new MilestoneTextEffect('520', now, CONFIG.MILESTONE_520_DURATION, {
|
||
fontSize: Math.min(W * 0.22, 110),
|
||
color: '#ff2d55',
|
||
glowColor: '#ff6b9d',
|
||
strokeColor: 'rgba(255,255,255,0.8)',
|
||
peakScale: 1.35,
|
||
isGolden: false,
|
||
showHeartOutline: false,
|
||
}));
|
||
burstHearts(cx, cy, now, 25, pick(HEART_COLORS), pick(GLOW_COLORS));
|
||
burstHearts(cx - W * 0.2, cy + rand(-30, 30), now, 8, pick(HEART_COLORS), pick(GLOW_COLORS));
|
||
burstHearts(cx + W * 0.2, cy + rand(-30, 30), now, 8, pick(HEART_COLORS), pick(GLOW_COLORS));
|
||
flashAlpha = 0.7;
|
||
flashDecayRate = 0.7 / (CONFIG.FLASH_520_DURATION / 16);
|
||
likeCountEl.classList.add('pink');
|
||
setTimeout(() => likeCountEl.classList.remove('pink'), 2200);
|
||
hintBar.style.opacity = '0';
|
||
},
|
||
trigger1314: () => {
|
||
const now = performance.now();
|
||
const cx = W / 2;
|
||
const cy = H / 2;
|
||
milestoneTexts.push(new MilestoneTextEffect('1314', now, CONFIG.MILESTONE_1314_DURATION, {
|
||
fontSize: Math.min(W * 0.25, 130),
|
||
color: '#ffd700',
|
||
glowColor: '#ffe88d',
|
||
strokeColor: 'rgba(255,255,255,0.9)',
|
||
peakScale: 1.5,
|
||
isGolden: true,
|
||
showHeartOutline: true,
|
||
}));
|
||
burstHearts(cx, cy, now, 40, pick(GOLDEN_COLORS), pick(GOLDEN_GLOW_COLORS));
|
||
burstHearts(cx - W * 0.25, cy + rand(-40, 40), now, 12, pick(GOLDEN_COLORS), pick(GOLDEN_GLOW_COLORS));
|
||
burstHearts(cx + W * 0.25, cy + rand(-40, 40), now, 12, pick(GOLDEN_COLORS), pick(GOLDEN_GLOW_COLORS));
|
||
burstHearts(cx, cy - H * 0.2, now, 10, pick(GOLDEN_COLORS), pick(GOLDEN_GLOW_COLORS));
|
||
flashAlpha = 0.85;
|
||
flashDecayRate = 0.85 / (CONFIG.FLASH_1314_DURATION / 16);
|
||
likeCountEl.classList.add('golden');
|
||
setTimeout(() => likeCountEl.classList.remove('golden'), 3200);
|
||
hintBar.style.opacity = '0';
|
||
},
|
||
resetAll: () => {
|
||
totalLikes = 0;
|
||
likeCountEl.textContent = '0';
|
||
likeCountEl.classList.remove('pink', 'golden');
|
||
triggeredMilestones.clear();
|
||
milestoneTexts = [];
|
||
hearts = [];
|
||
sparks = [];
|
||
flashAlpha = 0;
|
||
flashDecayRate = 0;
|
||
if (autoSimulateTimer) { clearInterval(autoSimulateTimer);
|
||
autoSimulateTimer = null; }
|
||
isAutoSimulating = false;
|
||
btnAuto.textContent = '➤520';
|
||
btnAuto.style.background = 'rgba(255,255,255,0.08)';
|
||
hintBar.style.opacity = '1';
|
||
console.log('🔄 全部状态已重置');
|
||
},
|
||
setLikes: (value) => setLikesDirectly(value),
|
||
getTotalLikes: () => totalLikes,
|
||
getStats: () => ({
|
||
hearts: hearts.length,
|
||
sparks: sparks.length,
|
||
milestoneTexts: milestoneTexts.length,
|
||
totalLikes,
|
||
triggeredMilestones: Array.from(triggeredMilestones),
|
||
flashAlpha,
|
||
isAutoSimulating,
|
||
canvasWidth: Math.round(W),
|
||
canvasHeight: Math.round(H),
|
||
dpr,
|
||
}),
|
||
};
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html> |