Meta Description" name="description" />
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<title>Love You - Pink Heart</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: #000;
overflow: hidden;
}
canvas {
display: block;
background: #000;
}
.hint {
position: fixed;
bottom: 14px;
left: 0;
right: 0;
text-align: center;
color: rgba(255,255,255,0.25);
font-family: monospace;
font-size: 11px;
letter-spacing: 1px;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="hint">tap layar untuk mengulang animasi</div>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let W, H, scale;
let heartPolygon = null;
let particles = [];
function heartPoint(t) {
const x = 16 * Math.pow(Math.sin(t), 3);
const y = -(13 * Math.cos(t) - 5 * Math.cos(2*t) - 2 * Math.cos(3*t) - Math.cos(4*t));
return { x: x / 17, y: y / 17 };
}
function buildHeartPolygon() {
heartPolygon = [];
const steps = 220;
for (let i = 0; i <= steps; i++) {
const t = (i / steps) * Math.PI * 2;
heartPolygon.push(heartPoint(t));
}
}
function pointInHeartPolygon(px, py) {
let inside = false;
for (let i = 0, j = heartPolygon.length - 1; i < heartPolygon.length; j = i++) {
const xi = heartPolygon[i].x, yi = heartPolygon[i].y;
const xj = heartPolygon[j].x, yj = heartPolygon[j].y;
const intersect = ((yi > py) !== (yj > py)) &&
(px < (xj - xi) * (py - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
}
// bangun satu lapisan grid teks (baris x kolom), hanya sel yg jatuh di dalam hati yg dipakai
function buildGridLayer(cfg, cx, cy, heartScale) {
const list = [];
const rowStart = -1.4 * heartScale + (cfg.rowOffset || 0);
const rowEnd = 1.3 * heartScale;
const colStart = -1.5 * heartScale + (cfg.colOffset || 0);
const colEnd = 1.5 * heartScale;
for (let y = rowStart; y <= rowEnd; y += cfg.rowStep) {
for (let x = colStart; x <= colEnd; x += cfg.colStep) {
const lx = x / heartScale;
const ly = y / heartScale;
if (pointInHeartPolygon(lx, ly)) {
const jx = (Math.random() * 2 - 1) * cfg.jitter;
const jy = (Math.random() * 2 - 1) * cfg.jitter * 0.5;
list.push({
x: cx + x + jx,
y: cy + y + jy,
word: cfg.words[Math.floor(Math.random() * cfg.words.length)],
bold: cfg.bold,
baseSize: cfg.fontSize * (0.92 + Math.random() * 0.16),
phase: Math.random() * Math.PI * 2,
speed: Math.random() * 0.5 + 0.25,
flickerAmt: Math.random() * 0.45 + 0.25,
maxAlpha: cfg.maxAlpha[0] + Math.random() * (cfg.maxAlpha[1] - cfg.maxAlpha[0])
});
}
}
}
return list;
}
function buildPoints() {
if (!heartPolygon) buildHeartPolygon();
const cx = W / 2;
const cy = H / 2 - H * 0.05;
const heartScale = Math.min(W, H) * 0.42;
// lapisan besar & tebal: LOVE YOU / Love You, jarang, terang
const coarse = buildGridLayer({
rowStep: 20 * scale,
colStep: 56 * scale,
fontSize: 12.5 * scale,
words: ['LOVE YOU', 'Love You'],
bold: true,
maxAlpha: [0.8, 1.0],
jitter: 4 * scale
}, cx, cy, heartScale);
// lapisan kecil pengisi: love you / Love You, rapat, agak redup, offset setengah sel
const fine = buildGridLayer({
rowStep: 12 * scale,
colStep: 37 * scale,
fontSize: 7.5 * scale,
words: ['love you', 'Love You'],
bold: false,
maxAlpha: [0.3, 0.6],
jitter: 3 * scale,
rowOffset: 6 * scale,
colOffset: 18.5 * scale
}, cx, cy, heartScale);
particles = coarse.concat(fine);
}
function resize() {
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W * devicePixelRatio;
canvas.height = H * devicePixelRatio;
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
scale = Math.min(W, H) / 480;
buildPoints();
}
let t0 = performance.now();
let buildStart = performance.now();
const BUILD_DURATION = 3800; // total waktu sampai hati penuh terbentuk (ms)
let appearDelay = 0;
function assignAppearOrder() {
const order = [...particles.keys()];
for (let i = order.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[order[i], order[j]] = [order[j], order[i]];
}
order.forEach((particleIdx, orderIdx) => {
particles[particleIdx].appearAt = orderIdx;
});
appearDelay = particles.length > 0 ? BUILD_DURATION / particles.length : 0;
}
function draw(now) {
const time = (now - t0) / 1000;
const buildElapsed = now - buildStart;
ctx.clearRect(0, 0, W, H);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
for (const p of particles) {
const revealTime = p.appearAt * appearDelay;
if (buildElapsed < revealTime) continue;
// fade-in halus (tanpa efek pantul/pop), sesuai video: alpha naik pelan lalu berkedip
const fadeProgress = Math.min(1, (buildElapsed - revealTime) / 500);
const flicker = 0.5 + 0.5 * Math.sin(time * p.speed + p.phase);
const alpha = p.maxAlpha * (1 - p.flickerAmt + p.flickerAmt * flicker) * fadeProgress;
const fontSize = p.baseSize;
ctx.save();
ctx.translate(p.x, p.y);
ctx.font = `${p.bold ? '700' : '500'} ${fontSize}px 'Segoe UI', Arial, sans-serif`;
ctx.shadowColor = `rgba(255, 45, 130, ${Math.min(1, alpha + 0.25)})`;
ctx.shadowBlur = fontSize * (p.bold ? 1.1 : 0.7);
ctx.fillStyle = `rgba(255, 160, 205, ${alpha})`;
ctx.fillText(p.word, 0, 0);
if (alpha > 0.7) {
ctx.shadowBlur = fontSize * 1.5;
ctx.fillStyle = `rgba(255, 225, 238, ${(alpha - 0.7) * 2})`;
ctx.fillText(p.word, 0, 0);
}
ctx.restore();
}
requestAnimationFrame(draw);
}
window.addEventListener('resize', () => {
resize();
assignAppearOrder();
buildStart = performance.now();
});
canvas.addEventListener('click', () => {
buildPoints();
assignAppearOrder();
buildStart = performance.now();
});
canvas.addEventListener('touchstart', () => {
buildPoints();
assignAppearOrder();
buildStart = performance.now();
});
resize();
assignAppearOrder();
requestAnimationFrame(draw);
</script>
</body>
</html>
1
1
7KB
7KB
134.0ms
224.0ms
134.0ms