Files
hunli/hunliji-api/520+1314特效.html

1122 lines
50 KiB
HTML
Raw Permalink Normal View History

2026-08-03 11:04:03 +08:00
<!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>