Get occasional updates about new fonts, designs and other interesting things.
import helpers from "/scratchpad/_lib/line.asset.mjs"
import palette from "/scratchpad/_lib/palette.asset.mjs"
import * as random from "/scratchpad/_lib/random.asset.mjs"
const ease = (t, p = 5) =>
t <= 0.5 ? Math.pow(t * 2, p) / 2 : 1 - Math.pow(2 - t * 2, p) / 2
const colors = {
bg: palette.dark,
fg: palette.canvas,
}
const connect = (ctx, points) => {
points.forEach(({ x, y }, i) => {
ctx[i === 0 ? "moveTo" : "lineTo"](x, y)
})
}
const setup = (canvas) => {
canvas.width = canvas.height = canvas.offsetWidth * window.devicePixelRatio
}
const pose = (w, h) => {
const eye = (dx, dy) => {
const [x, y] = [random.wobble(dx, w * 0.04), random.wobble(dy, w * 0.05)]
const f = w * 0.007
const o = w * 0.006
return [
[x + random.wobble(o, f), y + random.wobble(o, f)],
[x + random.wobble(-o, f), y + random.wobble(o, f)],
[x + random.wobble(-o, f), y + random.wobble(-o, f)],
[x + random.wobble(o, f), y + random.wobble(-o, f)],
]
}
const moustache = () => {
const l = [
random.wobble(w * 0.37, w * 0.05),
random.wobble(h * 0.61, h * 0.04),
]
const r = [
random.wobble(w * 0.63, w * 0.05),
random.wobble(h * 0.61, h * 0.04),
]
return [
[l[0], l[1] - random.wobble(h * 0.07, h * 0.03)],
l,
[random.wobble(w * 0.5, w * 0.05), random.wobble(h * 0.66, h * 0.05)],
r,
[r[0], r[1] - random.wobble(h * 0.07, h * 0.03)],
[random.wobble(w * 0.5, w * 0.05), random.wobble(h * 0.53, h * 0.02)],
]
}
const paw = (x, y) => {
const l = [
x - random.wobble(w * 0.1, w * 0.05),
y + random.wobble(0, h * 0.04),
]
const r = [
x + random.wobble(w * 0.1, w * 0.05),
y + random.wobble(0, h * 0.04),
]
return [
[l[0], l[1] - random.wobble(h * 0.04, h * 0.01)],
[l[0], l[1] + random.wobble(h * 0.04, h * 0.01)],
[r[0], r[1] + random.wobble(h * 0.04, h * 0.01)],
[r[0], r[1] - random.wobble(h * 0.04, h * 0.01)],
[random.wobble(x, w * 0.05), y - random.wobble(h * 0.07, h * 0.01)],
].reverse()
}
return {
body: [
// Left cheek
[random.wobble(w * 0.21, w * 0.1), random.wobble(h * 0.67, h * 0.07)],
// Crown
[random.wobble(w * 0.26, w * 0.08), random.wobble(h * 0.15, h * 0.05)],
[random.wobble(w * 0.74, w * 0.08), random.wobble(h * 0.15, h * 0.05)],
// Right cheek
[random.wobble(w * 0.79, w * 0.1), random.wobble(h * 0.67, h * 0.07)],
// Chin
[random.wobble(w * 0.5, w * 0.1), random.wobble(h * 0.81, h * 0.05)],
],
eyeL: eye(w * 0.34, h * 0.4),
eyeR: eye(w * 0.66, h * 0.4),
handR: paw(
w * 0.15,
h * 0.6 + (random.maybe(0.3) ? 0 : random.number(-h * 0.33))
),
handL: paw(
w * 0.85,
h * 0.6 + (random.maybe(0.3) ? 0 : random.number(-h * 0.33))
),
footR: paw(w * 0.35 - (random.maybe(0.1) ? w * 0.2 : 0), h * 0.9),
footL: paw(w * 0.65 + (random.maybe(0.1) ? w * 0.2 : 0), h * 0.9),
moustache: moustache(),
mouth: random.maybe(0.3)
? eye(w * 0.5, h * 0.66)
: [
[
random.wobble(w * 0.43, w * 0.05),
random.wobble(h * 0.66, h * 0.01),
],
[random.wobble(w * 0.5, w * 0.05), random.wobble(h * 0.68, h * 0.01)],
[
random.wobble(w * 0.57, w * 0.05),
random.wobble(h * 0.66, h * 0.01),
],
[random.wobble(w * 0.5, w * 0.05), random.wobble(h * 0.69, h * 0.01)],
],
}
}
const interpolate = (f1, f2, t) =>
Object.fromEntries(
Object.entries(f1).map(([key, val]) => [
key,
val.map(([x, y], i) => [
x + (f2[key][i][0] - x) * t,
y + (f2[key][i][1] - y) * t,
]),
])
)
const draw = (ctx, head, t) => {
const { width: w, height: h } = ctx.canvas
ctx.clearRect(0, 0, w, h)
ctx.save()
Object.entries(head).forEach(([key, shape]) => {
shape.push(shape[0])
shape.push(shape[1])
shape.push(shape[2])
head[key] = helpers.spline(helpers.objectify(shape)).slice(1, -1)
})
const y = Math.sin(Math.PI * t) * h * 0.04
ctx.translate(0, y)
connect(ctx, head.body)
connect(ctx, head.handL)
connect(ctx, head.handR)
ctx.translate(0, -y)
connect(ctx, head.footR)
connect(ctx, head.footL)
ctx.fillStyle = colors.fg
ctx.fill()
ctx.translate(0, y)
ctx.beginPath()
connect(ctx, head.eyeL)
connect(ctx, head.eyeR)
ctx.lineJoin = "round"
ctx.lineCap = "round"
ctx.strokeStyle = colors.bg
ctx.lineWidth = w * 0.05
ctx.stroke()
ctx.beginPath()
ctx.fillStyle = colors.bg
connect(ctx, head.moustache)
ctx.translate(w * 0.2, h * 0.3)
ctx.scale(0.6, 0.6)
connect(ctx, head.moustache)
ctx.fill()
ctx.restore(0, -y)
}
export default (canvas) => {
setup(canvas)
let playing = true
let start = Date.now()
let duration = random.maybe(0.2) ? 280 : 420
let { width, height } = canvas
let poses = [pose(width, height), pose(width, height)]
const ctx = canvas.getContext("2d")
;(function loop() {
if (playing) {
const t = Math.min((Date.now() - start) / duration, 1)
draw(ctx, interpolate(poses, ease(t, 1.5)), t)
window.requestAnimationFrame(loop)
if (t === 1) {
start = Date.now()
poses.shift()
poses.push(pose(width, height))
}
}
})()
return {
cancel: () => {
playing = false
},
}
}