Files
hunli/hunliji-api/520+1314特效.html
2026-08-03 11:04:03 +08:00

1122 lines
50 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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&amp;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>