Skip to content

Commit

Permalink
frontend: Refactor confetti module
Browse files Browse the repository at this point in the history
Refactor confetti module for readability and finding my JS style. The JS used
in Evy is optimized for readability for Go developers so we'll take it easy
on the functional idiom - arrow functions, filter/map/reduce, etc.

There are also a couple of minor implementation fixes, such as improved side
offset for confetti, confetti size depending on viewport height, rather than
viewport with as we use in Evy. Options to configure confetti colors, print
and count.
  • Loading branch information
juliaogris committed Jan 8, 2024
1 parent 82d5037 commit 7d262d7
Showing 1 changed file with 92 additions and 57 deletions.
149 changes: 92 additions & 57 deletions frontend/module/confetti.js
Original file line number Diff line number Diff line change
@@ -1,68 +1,103 @@
export default function showConfetti() {
const names = ["🦊", "🐐"]
const colors = ["red", "purple", "blue", "orange", "gold", "green"]
let confetti = new Array(100)
.fill()
.map((_, i) => {
return {
name: names[i % names.length],
x: Math.random() * 100,
y: -20 - Math.random() * 100,
r: 0.1 + Math.random() * 1,
color: colors[i % colors.length],
}
})
.sort((a, b) => a.r - b.r)

const confettiDivs = confetti.map((c) => {
const div = document.createElement("div")
Object.assign(div.style, confettiStyle(c))
div.textContent = c.name
document.body.appendChild(div)
return div
})

let frame
const defaultOptions = {
duration: 10, // animation seconds
fadeoutAfter: 8, // fadeout start in seconds
count: 100, // confetti count
texts: ["🦊", "🐐"],
colors: ["red", "purple", "blue", "orange", "gold", "green"],
}

function loop() {
frame = requestAnimationFrame(loop)
confetti = confetti.map((c, i) => {
c.y += 0.7 * c.r
if (c.y > 120) c.y = -20
const div = confettiDivs[i]
Object.assign(div.style, { top: "" + c.y + "%" })
return c
})
export default function showConfetti(options = {}) {
options = { ...defaultOptions, ...options }
const confettis = Array(options.count)
for (let i = 0; i < options.count; i++) {
confettis[i] = newConfetti(options)
}

loop()
setTimeout(() => {
cancelAnimationFrame(frame)
confettiDivs.forEach((div) => div.remove())
}, 10000)
setTimeout(() => {
confettiDivs.forEach((div) =>
Object.assign(div.style, {
opacity: 0,
transition: "opacity 1.5s ease-in-out",
}),
)
}, 8500)
const divs = confettis.map(newDiv)
divs.forEach((div) => document.body.appendChild(div))
animate(confettis, divs, options)
}

function confettiStyle(confetti) {
function newConfetti(options) {
const { texts, colors } = options
return {
background: confetti.color,
left: "" + confetti.x + "%",
top: "" + confetti.y + "%",
transform: `scale(${confetti.r})`,
height: "7vw",
width: "7vw",
lineHeight: "7vw",
text: texts[Math.floor(Math.random() * texts.length)],
x: Math.random() * 100,
y: Math.random() * 100,
r: Math.random() + 0.1, // scale factor, see top()
color: colors[Math.floor(Math.random() * colors.length)],
}
}

function newDiv(confetti) {
const baseStyle = {
height: "14vh",
width: "14vh",
lineHeight: "14vh",
borderRadius: "50%",
position: "absolute",
fontSize: "4vw",
fontSize: "8vh",
userSelect: "none",
textAlign: "center",
}
const confettiStyle = {
background: confetti.color,
top: `${top(confetti.y, 0)}%`,
// left property offsets the center of the confetti with max radius 7vh.
left: `calc(${confetti.x}vw - 7vh)`,
transform: `scale(${confetti.r})`,
}

const div = document.createElement("div")
div.textContent = confetti.text
Object.assign(div.style, baseStyle, confettiStyle)
return div
}

function animate(confettis, divs, options) {
const fadeoutStyle = newFadeoutStyle(options)
let fading = false
const start = document.timeline.currentTime

requestAnimationFrame(onFrame)

function onFrame(ts) {
const elapsed = (ts - start) / 1000 // elapsed seconds
if (elapsed > options.duration) {
// animation done
divs.forEach((div) => div.remove())
return
}
// update offset from top
for (let i = 0; i < divs.length; i++) {
const style = { top: `${top(confettis[i], elapsed)}%` }
Object.assign(divs[i].style, style)
}
if (elapsed > options.fadeoutAfter && !fading) {
// add fadeout style
fading = true
divs.forEach((div) => Object.assign(div.style, fadeoutStyle))
}
requestAnimationFrame(onFrame)
}
}

function newFadeoutStyle(options) {
const transitionDur = options.duration - options.fadeoutAfter
return {
opacity: 0,
transition: `opacity ${transitionDur}s ease-in-out`,
}
}

// top returns offset from top of viewport.
function top(confetti, elapsed) {
// r is the scale factor [0.1,1.1). It scales down confetti size and delta y.
const r = confetti.r
// y is the initial position. It is [-120, 20) above viewport.
const maxDiameter = 14 // 14vh
const yInitial = -confetti.y - maxDiameter
// yElapsed is the position after elapsed seconds.
const yElapsed = yInitial + elapsed * 50 * r
// When yElapsed is below the viewport start over from the top.
return (yElapsed % (100 + maxDiameter)) - maxDiameter
}

0 comments on commit 7d262d7

Please sign in to comment.