From 00252616532fdea21d38b0922041347b21973ced Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Tue, 22 Aug 2023 11:27:34 -0700 Subject: [PATCH] [PAY-1632] Clean up and improve performance of music confetti (#3921) --- .../img/particles/particleHeartPrimary.svg | 3 - .../img/particles/particleHeartSecondary.svg | 3 - .../img/particles/particleListensPrimary.svg | 3 - .../particles/particleListensSecondary.svg | 3 - .../img/particles/particleNotePrimary.svg | 3 - .../img/particles/particleNoteSecondary.svg | 3 - .../img/particles/particlePlaylistPrimary.svg | 3 - .../particles/particlePlaylistSecondary.svg | 3 - .../background-animations/MusicConfetti.tsx | 239 +++++------------- .../music-confetti/ConnectedMusicConfetti.tsx | 34 ++- .../src/utils/animations/music-confetti.js | 92 ++++--- packages/web/src/utils/zIndex.ts | 1 + 12 files changed, 149 insertions(+), 241 deletions(-) delete mode 100644 packages/web/src/assets/img/particles/particleHeartPrimary.svg delete mode 100644 packages/web/src/assets/img/particles/particleHeartSecondary.svg delete mode 100644 packages/web/src/assets/img/particles/particleListensPrimary.svg delete mode 100644 packages/web/src/assets/img/particles/particleListensSecondary.svg delete mode 100644 packages/web/src/assets/img/particles/particleNotePrimary.svg delete mode 100644 packages/web/src/assets/img/particles/particleNoteSecondary.svg delete mode 100644 packages/web/src/assets/img/particles/particlePlaylistPrimary.svg delete mode 100644 packages/web/src/assets/img/particles/particlePlaylistSecondary.svg diff --git a/packages/web/src/assets/img/particles/particleHeartPrimary.svg b/packages/web/src/assets/img/particles/particleHeartPrimary.svg deleted file mode 100644 index 14b47bb567..0000000000 --- a/packages/web/src/assets/img/particles/particleHeartPrimary.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/web/src/assets/img/particles/particleHeartSecondary.svg b/packages/web/src/assets/img/particles/particleHeartSecondary.svg deleted file mode 100644 index 47f3d1b353..0000000000 --- a/packages/web/src/assets/img/particles/particleHeartSecondary.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/web/src/assets/img/particles/particleListensPrimary.svg b/packages/web/src/assets/img/particles/particleListensPrimary.svg deleted file mode 100644 index b219329fe6..0000000000 --- a/packages/web/src/assets/img/particles/particleListensPrimary.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/web/src/assets/img/particles/particleListensSecondary.svg b/packages/web/src/assets/img/particles/particleListensSecondary.svg deleted file mode 100644 index 7d3974682a..0000000000 --- a/packages/web/src/assets/img/particles/particleListensSecondary.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/web/src/assets/img/particles/particleNotePrimary.svg b/packages/web/src/assets/img/particles/particleNotePrimary.svg deleted file mode 100644 index 7f472433e1..0000000000 --- a/packages/web/src/assets/img/particles/particleNotePrimary.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/web/src/assets/img/particles/particleNoteSecondary.svg b/packages/web/src/assets/img/particles/particleNoteSecondary.svg deleted file mode 100644 index c62a5706f1..0000000000 --- a/packages/web/src/assets/img/particles/particleNoteSecondary.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/web/src/assets/img/particles/particlePlaylistPrimary.svg b/packages/web/src/assets/img/particles/particlePlaylistPrimary.svg deleted file mode 100644 index b9b665a0ee..0000000000 --- a/packages/web/src/assets/img/particles/particlePlaylistPrimary.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/web/src/assets/img/particles/particlePlaylistSecondary.svg b/packages/web/src/assets/img/particles/particlePlaylistSecondary.svg deleted file mode 100644 index edfc99cb46..0000000000 --- a/packages/web/src/assets/img/particles/particlePlaylistSecondary.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/web/src/components/background-animations/MusicConfetti.tsx b/packages/web/src/components/background-animations/MusicConfetti.tsx index a484c01609..905893d996 100644 --- a/packages/web/src/components/background-animations/MusicConfetti.tsx +++ b/packages/web/src/components/background-animations/MusicConfetti.tsx @@ -1,202 +1,105 @@ -import { useRef, useState, useEffect, useCallback } from 'react' +import { useRef, useCallback, useEffect, useState } from 'react' -import heartIconPrimary from 'assets/img/particles/particleHeartPrimary.svg' -import heartIconSecondary from 'assets/img/particles/particleHeartSecondary.svg' -import listensIconPrimary from 'assets/img/particles/particleListensPrimary.svg' -import listensIconSecondary from 'assets/img/particles/particleListensSecondary.svg' -import noteIconPrimary from 'assets/img/particles/particleNotePrimary.svg' -import noteIconSecondary from 'assets/img/particles/particleNoteSecondary.svg' -import playlistsIconPrimary from 'assets/img/particles/particlePlaylistPrimary.svg' -import playlistsIconSecondary from 'assets/img/particles/particlePlaylistSecondary.svg' -import Confetti from 'utils/animations/music-confetti' -import { useOnResizeEffect } from 'utils/effects' - -const DEFAULT_IMAGES = [ - heartIconPrimary, - heartIconSecondary, - noteIconSecondary, - noteIconPrimary, - listensIconPrimary, - listensIconSecondary, - playlistsIconPrimary, - playlistsIconSecondary -] - -async function startConfettiAnimation( - canvasRef: HTMLCanvasElement, - recycle: boolean, - limit: number, - friction: number, - gravity: number, - rotate: number, - swing: number, - particleRate: number, - onCompletion: () => void, - isMatrix: boolean -) { - if (!canvasRef) return - let images = null - if (!isMatrix) { - images = DEFAULT_IMAGES.map((icon) => { - const img = new Image() - img.src = icon - return img - }) - await Promise.all( - images.map( - (img) => - new Promise((resolve) => { - img.onload = () => { - resolve(true) - } - }) - ) - ) - } - - const confetti = new Confetti( - canvasRef, - images, - recycle, - limit, - friction, - gravity, - rotate, - swing, - particleRate, - onCompletion - ) - confetti.run() - return confetti -} +import { Theme } from '@audius/common' -type MusicConfettiProps = { - withBackground?: boolean - recycle?: boolean - limit?: number - friction?: number - gravity?: number - rotate?: number - swing?: number - particleRate?: number - zIndex?: number - onCompletion?: () => void - isMatrix?: boolean -} +import Confetti from 'utils/animations/music-confetti' +import { getCurrentThemeColors } from 'utils/theme/theme' -const defaultProps: MusicConfettiProps = { - withBackground: false, - recycle: false, - limit: 250, +const DEFAULTS = { + limit: 200, friction: 0.99, gravity: 0.2, rotate: 0.1, swing: 0.01, - particleRate: 0.1, - zIndex: 16, - onCompletion: () => {}, - isMatrix: false + particleRate: 0.1 } -const MusicConfetti = (props: MusicConfettiProps) => { - const newProps = { ...props } as Required - - if (props.isMatrix) { - newProps.swing = 0 - newProps.rotate = 0 - } +const PATHS = [ + // Heart + new Path2D( + 'M4.8294,0 C1.83702857,0 0,2.65379464 0,4.61012277 C0,8.84082589 5.27554286,12.500625 9,15 C12.7244571,12.4996875 18,8.84082589 18,4.61012277 C18,2.65363058 16.1638457,0 13.1706,0 C11.4991714,0 10.0707429,1.21490625 9,2.36835938 C7.92822857,1.21478906 6.50088,0 4.8294,0 Z' + ), + // Listens + new Path2D( + 'M9.01215343,0 C4.03768506,0 0,4.14271233 0,9.24657534 L0,13.9315068 C0.0244073147,16.175737 1.82684761,18 4.013172,18 L4.54169274,18 C4.997924,18 5.38282706,17.6050849 5.38282706,17.1369863 L5.38282706,10.6027397 C5.38282706,10.1346411 4.997924,9.73972603 4.54169274,9.73972603 L4.013172,9.73972603 C3.14762075,9.73972603 2.33090336,10.0354192 1.65780365,10.5285699 L1.65780365,9.24657534 C1.65780365,5.10386301 4.95000337,1.7260274 8.98768843,1.7260274 C13.0253735,1.7260274 16.3420863,5.10386301 16.3420863,9.24657534 L16.3420863,10.5285699 C15.6689866,10.0354192 14.8757248,9.73972603 13.9867179,9.73972603 L13.4581972,9.73972603 C13.0019659,9.73972603 12.6170629,10.1346411 12.6170629,10.6027397 L12.6170629,17.1369863 C12.6170629,17.6050849 13.0019659,18 13.4581972,18 L13.9867179,18 C16.1975073,18 17.9765785,16.175737 17.9998899,13.9315068 L17.9998899,9.24657534 C18.0242972,4.14271233 13.9867179,0 9.01224955,0 L9.01215343,0 Z' + ), + // Note + new Path2D( + 'M3,13.0400072 L3,3.61346039 C3,3.16198653 3.28424981,2.76289841 3.70172501,2.62823505 L12.701725,0.0477059646 C13.3456556,-0.160004241 14,0.3365598 14,1.0329313 L14,12.9033651 C14,12.9179063 13.9997087,12.9323773 13.9991318,12.9467722 C13.9997094,12.9644586 14,12.9822021 14,13 C14,14.1045695 12.8807119,15 11.5,15 C10.1192881,15 9,14.1045695 9,13 C9,11.8954305 10.1192881,11 11.5,11 C11.6712329,11 11.838445,11.0137721 12,11.0400072 L12,3.61346039 L5,5.5488572 L5,14.9677884 C5,14.9726204 4.99996783,14.9774447 4.99990371,14.9822611 C4.99996786,14.988168 5,14.994081 5,15 C5,16.1045695 3.88071187,17 2.5,17 C1.11928813,17 0,16.1045695 0,15 C0,13.8954305 1.11928813,13 2.5,13 C2.67123292,13 2.83844503,13.0137721 3,13.0400072 Z' + ), + // Playlists + new Path2D( + 'M5.46563786,16.9245466 C4.92388449,17.5744882 4.02157241,18 3,18 C1.34314575,18 0,16.8807119 0,15.5 C0,14.1192881 1.34314575,13 3,13 C3.35063542,13 3.68722107,13.0501285 4,13.1422548 L4,3.02055066 C4,2.49224061 4.31978104,2.02523181 4.78944063,1.86765013 L10.5394406,0.0558250276 C11.2638626,-0.187235311 12,0.393838806 12,1.20872555 C12,2.01398116 12,2.61792286 12,3.02055066 C12,3.62449236 11.4511634,4.01020322 11,4.12121212 C10.3508668,4.28093157 8.68420009,4.62436591 6,5.15151515 L6,16.0224658 C6,16.5009995 5.80514083,16.7960063 5.46563786,16.9245466 Z M13,6 L17,6 C17.5522847,6 18,6.44771525 18,7 C18,7.55228475 17.5522847,8 17,8 L13,8 C12.4477153,8 12,7.55228475 12,7 C12,6.44771525 12.4477153,6 13,6 Z M11,10 L17,10 C17.5522847,10 18,10.4477153 18,11 C18,11.5522847 17.5522847,12 17,12 L11,12 C10.4477153,12 10,11.5522847 10,11 C10,10.4477153 10.4477153,10 11,10 Z M11,14 L17,14 C17.5522847,14 18,14.4477153 18,15 C18,15.5522847 17.5522847,16 17,16 L11,16 C10.4477153,16 10,15.5522847 10,15 C10,14.4477153 10.4477153,14 11,14 Z' + ) +] - return +type MusicConfettiProps = { + withBackground?: boolean + zIndex?: number + onCompletion?: () => void + theme?: Theme + isMobile?: boolean } -MusicConfetti.defaultProps = defaultProps +export const MusicConfetti = ({ + zIndex, + withBackground, + onCompletion, + theme, + isMobile +}: MusicConfettiProps) => { + const confettiRef = useRef(null) + const [colors, setColors] = useState([ + getCurrentThemeColors()['--primary'], + getCurrentThemeColors()['--secondary'] + ]) + const isMatrix = theme === Theme.MATRIX -const UnconditionalMusicConfetti = (props: Required) => { - const confettiPromiseRef = useRef | null>(null) - - // When we mount canvas, start confetti and set the confettiPromiseRef - const { - recycle, - limit, - friction, - gravity, - rotate, - swing, - particleRate, - onCompletion, - isMatrix - } = props + useEffect(() => { + setColors([ + getCurrentThemeColors()['--primary'], + getCurrentThemeColors()['--secondary'] + ]) + }, [theme]) + // When we mount canvas, start confetti and set the confettiRef const setCanvasRef = useCallback( (node: HTMLCanvasElement) => { if (!node) return - const confetti = startConfettiAnimation( + + const confetti = new Confetti( node, - recycle, - limit, - friction, - gravity, - rotate, - swing, - particleRate, - onCompletion, - isMatrix + isMatrix ? undefined : PATHS, + isMatrix ? undefined : colors, + false, + isMatrix ? (isMobile ? 200 : 500) : DEFAULTS.limit, + DEFAULTS.friction, + isMatrix ? 0.25 : DEFAULTS.gravity, + isMatrix ? 0 : DEFAULTS.rotate, + isMatrix ? 0 : DEFAULTS.swing, + DEFAULTS.particleRate, + onCompletion ) - confettiPromiseRef.current = confetti + confetti.run() + confettiRef.current = confetti }, - [ - recycle, - limit, - friction, - gravity, - rotate, - swing, - particleRate, - onCompletion, - isMatrix - ] + [onCompletion, isMatrix, isMobile, colors] ) - // When we unmount, stop the canvas animation - useEffect(() => { - return () => { - if (confettiPromiseRef.current) { - confettiPromiseRef.current.then((animation) => - animation ? animation.stop() : null - ) - } - } - }, []) - - const [sizing, setSize] = useState({}) - - useEffect(() => { - setSize({ - width: window.innerWidth, - height: window.innerHeight - }) - }, []) - - useOnResizeEffect(() => { - setSize({ - width: window.innerWidth, - height: window.innerHeight - }) - }) - return ( ) => { /> ) } - -export default MusicConfetti diff --git a/packages/web/src/components/music-confetti/ConnectedMusicConfetti.tsx b/packages/web/src/components/music-confetti/ConnectedMusicConfetti.tsx index 86e38c5b84..93d690de27 100644 --- a/packages/web/src/components/music-confetti/ConnectedMusicConfetti.tsx +++ b/packages/web/src/components/music-confetti/ConnectedMusicConfetti.tsx @@ -1,13 +1,23 @@ import { useCallback } from 'react' -import { musicConfettiActions, musicConfettiSelectors } from '@audius/common' +import { + musicConfettiActions, + musicConfettiSelectors, + themeSelectors, + Theme +} from '@audius/common' import { useDispatch } from 'react-redux' -import MusicConfetti from 'components/background-animations/MusicConfetti' +import { MusicConfetti } from 'components/background-animations/MusicConfetti' import { useIsMobile } from 'utils/clientUtil' import { useSelector } from 'utils/reducer' -import { isMatrix } from 'utils/theme/theme' +import zIndex from 'utils/zIndex' +// Re-enable for easy debugging +// import useHotkeys from 'hooks/useHotkey' +// const { show } = musicConfettiActions + +const { getTheme } = themeSelectors const { hide } = musicConfettiActions const { getIsVisible } = musicConfettiSelectors @@ -17,21 +27,23 @@ const ConnectedMusicConfetti = () => { dispatch(hide()) }, [dispatch]) + // Re-enable for easy debugging + // useHotkeys({ + // 88: () => dispatch(show()) + // }) + const isVisible = useSelector(getIsVisible) - const isMatrixMode = isMatrix() const isMobile = useIsMobile() + const theme = useSelector(getTheme) return isVisible ? ( - ) : ( - <> - ) + ) : null } export default ConnectedMusicConfetti diff --git a/packages/web/src/utils/animations/music-confetti.js b/packages/web/src/utils/animations/music-confetti.js index e005f5a53f..434d10019c 100644 --- a/packages/web/src/utils/animations/music-confetti.js +++ b/packages/web/src/utils/animations/music-confetti.js @@ -1,23 +1,31 @@ -import _ from 'lodash' - // Utility Functions to compute confetti physics -export function degreesToRads(degrees) { +function degreesToRads(degrees) { return degrees / (180 * Math.PI) } -export function radsToDegrees(radians) { - return (radians * 180) / Math.PI -} - -export function randomRange(min, max) { +function randomRange(min, max) { return min + Math.random() * (max - min) } -export function randomInt(min, max) { +function randomInt(min, max) { return Math.floor(min + Math.random() * (max - min + 1)) } +function range(start, end, step) { + const arr = [] + for (let i = start; i < end; i += step) { + arr.push(i) + } + return arr +} + +function sample(array) { + if (!Array.isArray(array) || array.length === 0) return undefined + const randomIndex = Math.floor(Math.random() * array.length) + return array[randomIndex] +} + const RANDOM_LETTERS = 'abcdefghijklmnoqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890~!@#$%^&*()`™£¢∞§¶•ªº' @@ -26,11 +34,23 @@ const COLUMN_SPACING = 50 // Particle class representing a piece of confetti // The particle holds the image, postion, and phsyics for moving in the animation -export class Particle { - constructor(img, x, y, opacity, sizeRatio, friction, gravity, rotate, swing) { +class Particle { + constructor( + path, + color, + x, + y, + opacity, + sizeRatio, + friction, + gravity, + rotate, + swing + ) { this.randomLetter = RANDOM_LETTERS[Math.floor(Math.random() * RANDOM_LETTERS.length)] - this.img = img + this.path = path + this.color = color this.center = x this.maxVx = swing > 0 ? randomRange(-10, 10) * sizeRatio : 0 this.maxVy = randomRange(-4, -0.3) * sizeRatio @@ -63,8 +83,10 @@ export class Particle { ctx.translate(Math.floor(this.x), Math.floor(this.y)) ctx.rotate(this.angle) ctx.globalAlpha = this.opacity - if (this.img) { - ctx.drawImage(this.img, 0, 0) + if (this.path) { + ctx.scale(this.sizeRatio, this.sizeRatio) + ctx.fillStyle = this.color + ctx.fill(this.path) } else { // If no image, we must be in matrix mode @@ -92,7 +114,8 @@ export class Particle { export default class Confetti { constructor( canvas, - images, + paths, + colors, recycle = false, limit = 100, friction = 0.99, @@ -104,12 +127,17 @@ export default class Confetti { ) { const { clientWidth: width, clientHeight: height } = canvas this.width = width + this.height = height + window.addEventListener('resize', () => { + this.width = window.innerWidth + this.height = window.innerHeight + }) this.particleRate = particleRate - this.images = images + this.paths = paths + this.colors = colors this.runAnimation = false this.friction = friction this.gravity = gravity - this.height = height this.limit = limit this.rotate = rotate this.swing = swing @@ -121,7 +149,7 @@ export default class Confetti { // For matrix, generate columns by iterating over width with a // COLUMN_SPACING step and then perturbing the values somewhat - this.particleColumns = _.range(0, width, COLUMN_SPACING).map( + this.particleColumns = range(0, width, COLUMN_SPACING).map( (c) => c + Math.random() * COLUMN_SPACING - COLUMN_SPACING * 0.4 ) } @@ -129,31 +157,21 @@ export default class Confetti { generateParticle = (source, number) => { const x = this.swing ? randomRange(0, this.width) - : _.sample(this.particleColumns) + : sample(this.particleColumns) const y = -30 // Start the particle 30px above const opacity = this.swing ? randomRange(0.3, 0.9) : randomRange(0.8, 1) // Range from 0.3 to 0.9 const size = this.swing ? ((opacity + 0.2) * 5) / 6 : 1 // range 0.5 to 1 - let imageCanvas = null - - if (this.images) { - const particleImg = this.images[randomInt(0, this.images.length - 1)] - - imageCanvas = document.createElement('canvas') - const imageContext = imageCanvas.getContext('2d') - imageCanvas.height = particleImg.height * size - imageCanvas.width = particleImg.width * size - imageContext.drawImage( - particleImg, - 0, - 0, - imageCanvas.width, - imageCanvas.height - ) - } + const path = this.paths + ? this.paths[randomInt(0, this.paths.length - 1)] + : null + const color = this.colors + ? this.colors[randomInt(0, this.colors.length - 1)] + : null return new Particle( - imageCanvas, + path, + color, x, y, opacity, diff --git a/packages/web/src/utils/zIndex.ts b/packages/web/src/utils/zIndex.ts index 7fb410dafd..b22c13dcb1 100644 --- a/packages/web/src/utils/zIndex.ts +++ b/packages/web/src/utils/zIndex.ts @@ -27,6 +27,7 @@ export enum zIndex { IMAGE_SELECTION_POPUP = 1002, // Web3 wallet connect modal + MUSIC_CONFETTI = 10000, WEB3_WALLET_CONNECT_MODAL = 10001, ARTIST_POPOVER_POPUP = 20000,