-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
82d5037
commit 7d262d7
Showing
1 changed file
with
92 additions
and
57 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |