Skip to content

Instantly share code, notes, and snippets.

@devld
Created November 9, 2022 05:22
Show Gist options
  • Save devld/d3823ffbb59dc3046c062b412cf12b8f to your computer and use it in GitHub Desktop.
Save devld/d3823ffbb59dc3046c062b412cf12b8f to your computer and use it in GitHub Desktop.
画一个♥
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Heart</title>
<style>
body {
margin: 0;
}
.container {
margin: 0 auto;
}
.container canvas {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div class="container"></div>
<script>
const containerEl = document.querySelector('.container')
containerEl.style.width = `${Math.min(window.innerWidth * 0.9, window.innerHeight * 0.9)}px`
containerEl.style.height = containerEl.style.width
const canvasEl = document.createElement('canvas')
containerEl.appendChild(canvasEl)
const W = canvasEl.clientWidth * devicePixelRatio
const H = canvasEl.clientHeight * devicePixelRatio
canvasEl.width = W
canvasEl.height = H
const ctx = canvasEl.getContext('2d')
let ls, rs, skip = 6
const anim = drawHeartAnimation({
onBegin: (tp) => {
ctx.clearRect(0, 0, W, H)
ctx.save()
ctx.translate(W / 2, H / 2)
ls = skipper(skip)
rs = skipper(skip)
},
onDraw: (p, tp) => {
if (p.left && ls()) return
if (p.right && rs()) return
const startOrEndThreshold = 0.3
const startOrEnd = p.heartProgress < startOrEndThreshold || p.heartProgress > (1 - startOrEndThreshold)
const startOrEndP = Math.abs(Math.round(p.heartProgress) - p.heartProgress) / startOrEndThreshold
ctx.save()
ctx.translate(p.x, p.y)
ctx.rotate(2 * Math.PI * p.progress * (p.left ? -1 : 1))
ctx.beginPath()
sampleHeartFn((p, t) => {
if (t === 0) ctx.moveTo(p.x, p.y)
else ctx.lineTo(p.x, p.y)
}, startOrEnd ? (20 * startOrEndP) : 20, 0, 2 * Math.PI, 0.1)
ctx.fillStyle = 'pink'
ctx.fill()
ctx.restore()
},
onEnd: () => {
ctx.restore()
},
size: W / 2,
step: 0.01,
duration: 4000
})
anim.start().then(() => { console.log('finished') })
function drawHeartAnimation({ onBegin, onDraw, onEnd, size, step, duration = 3000 }) {
let startTime
let stopped = false
let _p
step = Math.abs(step)
const cb = (t) => {
if (!startTime) startTime = t
const progress = (t - startTime) / duration
const passed = Math.min(progress * Math.PI, Math.PI)
onBegin && onBegin(progress)
sampleHeartFn((p) => {
p.heartProgress = p.theta / Math.PI
p.left = true
onDraw(p, progress)
}, size, 0, passed, step)
sampleHeartFn((p) => {
p.heartProgress = (2 * Math.PI - p.theta) / Math.PI
p.right = true
onDraw(p, progress)
}, size, 2 * Math.PI, 2 * Math.PI - passed, -step)
onEnd && onEnd(progress)
if (stopped) return
if (progress <= 1) {
requestAnimationFrame(cb)
} else {
_p.resolve()
}
}
const startAnimation = () => {
stopped = false
requestAnimationFrame(cb)
return new Promise((resolve, reject) => {
_p = { resolve, reject }
})
}
const stopAnimation = () => {
stopped = true
_p.reject()
}
return { start: startAnimation, stop: stopAnimation }
}
function sampleHeartFn(fn, size, start, end, step) {
const scale = size / 20
let t = start
while ((step > 0) ? (t <= end) : (t >= end)) {
const p = sampleHeart(t, scale)
p.progress = end === start ? 0 : (t - start) / (end - start)
p.theta = t
fn(p)
t += step
}
}
function sampleHeart(t, scale = 1) {
// -16 sin^(3)(t)
const x = scale * (-16 * Math.pow(Math.sin(t), 3))
// 13 cos(t)-5 cos(2 t)-2 cos(3 t)-cos(4 t)
const y = scale * (-(13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t)))
return { x, y }
}
function skipper(skip) {
let i = 0
return (skip_) => {
i++
return i % (skip_ || skip) !== 0
}
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment