├── .prettierignore ├── typings.d.ts ├── src ├── ConstantsInternal.ts ├── stories │ ├── images │ │ ├── circle.svg │ │ ├── square.svg │ │ └── duck.svg │ ├── Stories.module.css │ ├── components │ │ ├── SpriteCanvas.stories.tsx │ │ └── ConfettiCanvas.stories.tsx │ ├── examples │ │ ├── Basic.stories.tsx │ │ ├── ConditionalRender.stories.tsx │ │ ├── Dragging.stories.tsx │ │ ├── Static.stories.tsx │ │ └── MultipleCannons.stories.tsx │ └── Playground.stories.tsx ├── Environment.ts ├── Types.ts ├── easing.tsx ├── Constants.ts ├── index.ts ├── useReady.ts ├── UpdatableValue.tsx ├── Utils.ts ├── UpdatableValueImplementations.tsx ├── components │ ├── useConfettiCannon.tsx │ ├── SpriteCanvas.tsx │ └── ConfettiCanvas.tsx ├── Confetti.ts └── createConfetti.ts ├── .gitignore ├── .prettierrc.json ├── dist ├── cjs │ ├── types │ │ ├── ConstantsInternal.d.ts │ │ ├── easing.d.ts │ │ ├── useReady.d.ts │ │ ├── Environment.d.ts │ │ ├── Constants.d.ts │ │ ├── Types.d.ts │ │ ├── stories │ │ │ ├── examples │ │ │ │ ├── Basic.stories.d.ts │ │ │ │ ├── Dragging.stories.d.ts │ │ │ │ ├── MultipleCannons.stories.d.ts │ │ │ │ ├── ConditionalRender.stories.d.ts │ │ │ │ └── Static.stories.d.ts │ │ │ ├── components │ │ │ │ ├── ConfettiCanvas.stories.d.ts │ │ │ │ └── SpriteCanvas.stories.d.ts │ │ │ └── Playground.stories.d.ts │ │ ├── index.d.ts │ │ ├── Utils.d.ts │ │ ├── components │ │ │ ├── SpriteCanvas.d.ts │ │ │ ├── useConfettiCannon.d.ts │ │ │ └── ConfettiCanvas.d.ts │ │ ├── UpdatableValueImplementations.d.ts │ │ ├── UpdatableValue.d.ts │ │ ├── Confetti.d.ts │ │ └── createConfetti.d.ts │ └── index.js ├── esm │ ├── types │ │ ├── ConstantsInternal.d.ts │ │ ├── easing.d.ts │ │ ├── useReady.d.ts │ │ ├── Environment.d.ts │ │ ├── Constants.d.ts │ │ ├── Types.d.ts │ │ ├── stories │ │ │ ├── examples │ │ │ │ ├── Basic.stories.d.ts │ │ │ │ ├── Dragging.stories.d.ts │ │ │ │ ├── MultipleCannons.stories.d.ts │ │ │ │ ├── ConditionalRender.stories.d.ts │ │ │ │ └── Static.stories.d.ts │ │ │ ├── components │ │ │ │ ├── ConfettiCanvas.stories.d.ts │ │ │ │ └── SpriteCanvas.stories.d.ts │ │ │ └── Playground.stories.d.ts │ │ ├── index.d.ts │ │ ├── Utils.d.ts │ │ ├── components │ │ │ ├── SpriteCanvas.d.ts │ │ │ ├── useConfettiCannon.d.ts │ │ │ └── ConfettiCanvas.d.ts │ │ ├── UpdatableValueImplementations.d.ts │ │ ├── UpdatableValue.d.ts │ │ ├── Confetti.d.ts │ │ └── createConfetti.d.ts │ └── index.js └── index.d.ts ├── example.gif ├── .babelrc.json ├── .storybook ├── preview.ts └── main.ts ├── .eslintrc.json ├── tsconfig.json ├── LICENSE.md ├── rollup.config.js ├── package.json └── readme.md /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.module.css"; 2 | -------------------------------------------------------------------------------- /src/ConstantsInternal.ts: -------------------------------------------------------------------------------- 1 | export const SPRITE_SPACING = 2; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules 4 | 5 | storybook-static/ -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-organize-imports"] 3 | } 4 | -------------------------------------------------------------------------------- /dist/cjs/types/ConstantsInternal.d.ts: -------------------------------------------------------------------------------- 1 | export declare const SPRITE_SPACING = 2; 2 | -------------------------------------------------------------------------------- /dist/esm/types/ConstantsInternal.d.ts: -------------------------------------------------------------------------------- 1 | export declare const SPRITE_SPACING = 2; 2 | -------------------------------------------------------------------------------- /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discord/confetti-cannon/HEAD/example.gif -------------------------------------------------------------------------------- /src/stories/images/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/stories/images/square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /dist/cjs/types/easing.d.ts: -------------------------------------------------------------------------------- 1 | export type EasingFunction = (timePassed: number, startValue: number, changeInValue: number, totalDuration: number) => number; 2 | export declare const easeInOutQuad: EasingFunction; 3 | -------------------------------------------------------------------------------- /dist/esm/types/easing.d.ts: -------------------------------------------------------------------------------- 1 | export type EasingFunction = (timePassed: number, startValue: number, changeInValue: number, totalDuration: number) => number; 2 | export declare const easeInOutQuad: EasingFunction; 3 | -------------------------------------------------------------------------------- /dist/cjs/types/useReady.d.ts: -------------------------------------------------------------------------------- 1 | export default function useReady(): { 2 | isReady: boolean; 3 | addReadyListener: (listener: (isReady: boolean) => void) => string; 4 | removeReadyListener: (listenerId: string) => void; 5 | setIsReady: (newIsReady: boolean) => void; 6 | }; 7 | -------------------------------------------------------------------------------- /dist/esm/types/useReady.d.ts: -------------------------------------------------------------------------------- 1 | export default function useReady(): { 2 | isReady: boolean; 3 | addReadyListener: (listener: (isReady: boolean) => void) => string; 4 | removeReadyListener: (listenerId: string) => void; 5 | setIsReady: (newIsReady: boolean) => void; 6 | }; 7 | -------------------------------------------------------------------------------- /src/stories/Stories.module.css: -------------------------------------------------------------------------------- 1 | .bordered { 2 | border: 1px solid black; 3 | } 4 | 5 | .sized { 6 | width: 600px; 7 | height: 300px; 8 | } 9 | 10 | .sizedSmall { 11 | width: 300px; 12 | height: 150px; 13 | } 14 | 15 | .sizedLarge { 16 | width: 800px; 17 | height: 800px; 18 | } 19 | -------------------------------------------------------------------------------- /dist/cjs/types/Environment.d.ts: -------------------------------------------------------------------------------- 1 | export default class Environment { 2 | gravity: number; 3 | wind: number; 4 | density: number; 5 | constructor({ gravity, wind, density, }?: { 6 | gravity?: number; 7 | wind?: number; 8 | density?: number; 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /dist/esm/types/Environment.d.ts: -------------------------------------------------------------------------------- 1 | export default class Environment { 2 | gravity: number; 3 | wind: number; 4 | density: number; 5 | constructor({ gravity, wind, density, }?: { 6 | gravity?: number; 7 | wind?: number; 8 | density?: number; 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /dist/cjs/types/Constants.d.ts: -------------------------------------------------------------------------------- 1 | import { CreateConfettiArgs } from "./createConfetti"; 2 | export type CreateConfettiArgsDefaults = Pick, "velocity" | "rotation" | "dragCoefficient" | "airResistanceArea" | "opacity">; 3 | export declare const CREATE_CONFETTI_DEFAULTS: CreateConfettiArgsDefaults; 4 | -------------------------------------------------------------------------------- /dist/esm/types/Constants.d.ts: -------------------------------------------------------------------------------- 1 | import { CreateConfettiArgs } from "./createConfetti"; 2 | export type CreateConfettiArgsDefaults = Pick, "velocity" | "rotation" | "dragCoefficient" | "airResistanceArea" | "opacity">; 3 | export declare const CREATE_CONFETTI_DEFAULTS: CreateConfettiArgsDefaults; 4 | -------------------------------------------------------------------------------- /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceType": "unambiguous", 3 | "presets": [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "chrome": 100 9 | } 10 | } 11 | ], 12 | "@babel/preset-typescript", 13 | "@babel/preset-react" 14 | ], 15 | "plugins": [] 16 | } 17 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/react"; 2 | 3 | const preview: Preview = { 4 | parameters: { 5 | actions: { argTypesRegex: "^on[A-Z].*" }, 6 | controls: { 7 | matchers: { 8 | color: /(background|color)$/i, 9 | date: /Date$/, 10 | }, 11 | }, 12 | }, 13 | }; 14 | 15 | export default preview; 16 | -------------------------------------------------------------------------------- /dist/cjs/types/Types.d.ts: -------------------------------------------------------------------------------- 1 | export interface Vector2Template { 2 | x: T; 3 | y: T; 4 | } 5 | export interface Vector3Template extends Vector2Template { 6 | z: T; 7 | } 8 | export type Vector2 = Vector2Template; 9 | export type Vector3 = Vector3Template; 10 | export type SpriteProp = { 11 | src: string; 12 | colorize: boolean; 13 | } | string; 14 | -------------------------------------------------------------------------------- /dist/esm/types/Types.d.ts: -------------------------------------------------------------------------------- 1 | export interface Vector2Template { 2 | x: T; 3 | y: T; 4 | } 5 | export interface Vector3Template extends Vector2Template { 6 | z: T; 7 | } 8 | export type Vector2 = Vector2Template; 9 | export type Vector3 = Vector3Template; 10 | export type SpriteProp = { 11 | src: string; 12 | colorize: boolean; 13 | } | string; 14 | -------------------------------------------------------------------------------- /dist/cjs/types/stories/examples/Basic.stories.d.ts: -------------------------------------------------------------------------------- 1 | import type { StoryObj } from "@storybook/react"; 2 | import * as React from "react"; 3 | declare function BasicStory(): React.JSX.Element; 4 | declare const meta: { 5 | title: string; 6 | component: typeof BasicStory; 7 | }; 8 | export default meta; 9 | type Story = StoryObj; 10 | export declare const Example: Story; 11 | -------------------------------------------------------------------------------- /dist/esm/types/stories/examples/Basic.stories.d.ts: -------------------------------------------------------------------------------- 1 | import type { StoryObj } from "@storybook/react"; 2 | import * as React from "react"; 3 | declare function BasicStory(): React.JSX.Element; 4 | declare const meta: { 5 | title: string; 6 | component: typeof BasicStory; 7 | }; 8 | export default meta; 9 | type Story = StoryObj; 10 | export declare const Example: Story; 11 | -------------------------------------------------------------------------------- /dist/cjs/types/stories/examples/Dragging.stories.d.ts: -------------------------------------------------------------------------------- 1 | import type { StoryObj } from "@storybook/react"; 2 | import * as React from "react"; 3 | declare function DraggingStory(): React.JSX.Element; 4 | declare const meta: { 5 | title: string; 6 | component: typeof DraggingStory; 7 | }; 8 | export default meta; 9 | type Story = StoryObj; 10 | export declare const Example: Story; 11 | -------------------------------------------------------------------------------- /dist/esm/types/stories/examples/Dragging.stories.d.ts: -------------------------------------------------------------------------------- 1 | import type { StoryObj } from "@storybook/react"; 2 | import * as React from "react"; 3 | declare function DraggingStory(): React.JSX.Element; 4 | declare const meta: { 5 | title: string; 6 | component: typeof DraggingStory; 7 | }; 8 | export default meta; 9 | type Story = StoryObj; 10 | export declare const Example: Story; 11 | -------------------------------------------------------------------------------- /src/Environment.ts: -------------------------------------------------------------------------------- 1 | export default class Environment { 2 | gravity = -9.8; 3 | wind = 0; 4 | density = 1.2041; 5 | 6 | constructor({ 7 | gravity, 8 | wind, 9 | density, 10 | }: { gravity?: number; wind?: number; density?: number } = {}) { 11 | this.gravity = gravity ?? this.gravity; 12 | this.wind = wind ?? this.wind; 13 | this.density = density ?? this.density; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Types.ts: -------------------------------------------------------------------------------- 1 | export interface Vector2Template { 2 | x: T; 3 | y: T; 4 | } 5 | 6 | export interface Vector3Template extends Vector2Template { 7 | z: T; 8 | } 9 | 10 | export type Vector2 = Vector2Template; 11 | export type Vector3 = Vector3Template; 12 | 13 | export type SpriteProp = 14 | | { 15 | src: string; 16 | colorize: boolean; 17 | } 18 | | string; 19 | -------------------------------------------------------------------------------- /dist/cjs/types/stories/examples/MultipleCannons.stories.d.ts: -------------------------------------------------------------------------------- 1 | import type { StoryObj } from "@storybook/react"; 2 | import * as React from "react"; 3 | declare function MultipleCannonsStory(): React.JSX.Element; 4 | declare const meta: { 5 | title: string; 6 | component: typeof MultipleCannonsStory; 7 | }; 8 | export default meta; 9 | type Story = StoryObj; 10 | export declare const Example: Story; 11 | -------------------------------------------------------------------------------- /dist/esm/types/stories/examples/MultipleCannons.stories.d.ts: -------------------------------------------------------------------------------- 1 | import type { StoryObj } from "@storybook/react"; 2 | import * as React from "react"; 3 | declare function MultipleCannonsStory(): React.JSX.Element; 4 | declare const meta: { 5 | title: string; 6 | component: typeof MultipleCannonsStory; 7 | }; 8 | export default meta; 9 | type Story = StoryObj; 10 | export declare const Example: Story; 11 | -------------------------------------------------------------------------------- /dist/cjs/types/stories/examples/ConditionalRender.stories.d.ts: -------------------------------------------------------------------------------- 1 | import type { StoryObj } from "@storybook/react"; 2 | import * as React from "react"; 3 | declare function ConditionalRenderStory(): React.JSX.Element; 4 | declare const meta: { 5 | title: string; 6 | component: typeof ConditionalRenderStory; 7 | }; 8 | export default meta; 9 | type Story = StoryObj; 10 | export declare const Example: Story; 11 | -------------------------------------------------------------------------------- /dist/esm/types/stories/examples/ConditionalRender.stories.d.ts: -------------------------------------------------------------------------------- 1 | import type { StoryObj } from "@storybook/react"; 2 | import * as React from "react"; 3 | declare function ConditionalRenderStory(): React.JSX.Element; 4 | declare const meta: { 5 | title: string; 6 | component: typeof ConditionalRenderStory; 7 | }; 8 | export default meta; 9 | type Story = StoryObj; 10 | export declare const Example: Story; 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | "overrides": [], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": "latest", 15 | "sourceType": "module" 16 | }, 17 | "plugins": ["react", "@typescript-eslint", "react-hooks"], 18 | "rules": { 19 | "react-hooks/exhaustive-deps": "error" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/react-webpack5"; 2 | const config: StorybookConfig = { 3 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], 4 | addons: [ 5 | "@storybook/addon-links", 6 | "@storybook/addon-essentials", 7 | "@storybook/addon-interactions", 8 | "storybook-css-modules", 9 | ], 10 | framework: { 11 | name: "@storybook/react-webpack5", 12 | options: {}, 13 | }, 14 | docs: { 15 | autodocs: "tag", 16 | }, 17 | }; 18 | export default config; 19 | -------------------------------------------------------------------------------- /src/easing.tsx: -------------------------------------------------------------------------------- 1 | export type EasingFunction = ( 2 | timePassed: number, 3 | startValue: number, 4 | changeInValue: number, 5 | totalDuration: number 6 | ) => number; 7 | 8 | export const easeInOutQuad: EasingFunction = ( 9 | timePassed, 10 | startValue, 11 | changeInValue, 12 | totalDuration 13 | ) => { 14 | if ((timePassed /= totalDuration / 2) < 1) { 15 | return (changeInValue / 2) * timePassed * timePassed + startValue; 16 | } 17 | return ( 18 | (-changeInValue / 2) * (--timePassed * (timePassed - 2) - 1) + startValue 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "strict": true, 5 | "skipLibCheck": true, 6 | "jsx": "react", 7 | "module": "ESNext", 8 | "declaration": true, 9 | "declarationDir": "types", 10 | "sourceMap": true, 11 | "outDir": "dist", 12 | "moduleResolution": "node", 13 | "emitDeclarationOnly": true, 14 | "allowSyntheticDefaultImports": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "plugins": [{ "name": "typescript-plugin-css-modules" }] 17 | }, 18 | "exclude": ["dist", "node_modules"] 19 | } 20 | -------------------------------------------------------------------------------- /src/Constants.ts: -------------------------------------------------------------------------------- 1 | import { CreateConfettiArgs } from "./createConfetti"; 2 | 3 | export type CreateConfettiArgsDefaults = Pick< 4 | Required, 5 | "velocity" | "rotation" | "dragCoefficient" | "airResistanceArea" | "opacity" 6 | >; 7 | 8 | export const CREATE_CONFETTI_DEFAULTS: CreateConfettiArgsDefaults = { 9 | velocity: { 10 | type: "static", 11 | value: 0, 12 | }, 13 | rotation: { 14 | type: "static", 15 | value: 0, 16 | }, 17 | dragCoefficient: { 18 | type: "static", 19 | value: 1.66, 20 | }, 21 | airResistanceArea: { 22 | type: "static", 23 | value: 0.001, 24 | }, 25 | opacity: { 26 | type: "static", 27 | value: 1, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /dist/cjs/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export { default as Confetti } from "./Confetti"; 2 | export * from "./Constants"; 3 | export { default as Environment } from "./Environment"; 4 | export * from "./Types"; 5 | export * from "./UpdatableValueImplementations"; 6 | export { default as ConfettiCanvas, ConfettiCanvasHandle, } from "./components/ConfettiCanvas"; 7 | export { default as SpriteCanvas, SpriteCanvasHandle, } from "./components/SpriteCanvas"; 8 | export { default as useConfettiCannon, type ConfettiCannon, type CreateConfettiRequestedOptions, } from "./components/useConfettiCannon"; 9 | export { default as createConfetti, getUpdatableValueNumber, getUpdatableValueVector2, getUpdatableValueVector3, type CreateConfettiArgs, } from "./createConfetti"; 10 | export * from "./easing"; 11 | -------------------------------------------------------------------------------- /dist/esm/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export { default as Confetti } from "./Confetti"; 2 | export * from "./Constants"; 3 | export { default as Environment } from "./Environment"; 4 | export * from "./Types"; 5 | export * from "./UpdatableValueImplementations"; 6 | export { default as ConfettiCanvas, ConfettiCanvasHandle, } from "./components/ConfettiCanvas"; 7 | export { default as SpriteCanvas, SpriteCanvasHandle, } from "./components/SpriteCanvas"; 8 | export { default as useConfettiCannon, type ConfettiCannon, type CreateConfettiRequestedOptions, } from "./components/useConfettiCannon"; 9 | export { default as createConfetti, getUpdatableValueNumber, getUpdatableValueVector2, getUpdatableValueVector3, type CreateConfettiArgs, } from "./createConfetti"; 10 | export * from "./easing"; 11 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Confetti } from "./Confetti"; 2 | export * from "./Constants"; 3 | export { default as Environment } from "./Environment"; 4 | export * from "./Types"; 5 | export * from "./UpdatableValueImplementations"; 6 | export { 7 | default as ConfettiCanvas, 8 | ConfettiCanvasHandle, 9 | } from "./components/ConfettiCanvas"; 10 | export { 11 | default as SpriteCanvas, 12 | SpriteCanvasHandle, 13 | } from "./components/SpriteCanvas"; 14 | export { 15 | default as useConfettiCannon, 16 | type ConfettiCannon, 17 | type CreateConfettiRequestedOptions, 18 | } from "./components/useConfettiCannon"; 19 | export { 20 | default as createConfetti, 21 | getUpdatableValueNumber, 22 | getUpdatableValueVector2, 23 | getUpdatableValueVector3, 24 | type CreateConfettiArgs, 25 | } from "./createConfetti"; 26 | export * from "./easing"; 27 | -------------------------------------------------------------------------------- /dist/cjs/types/stories/components/ConfettiCanvas.stories.d.ts: -------------------------------------------------------------------------------- 1 | import type { StoryObj } from "@storybook/react"; 2 | import * as React from "react"; 3 | declare function ConfettiCanvasStory({ className, ...args }: { 4 | className: string; 5 | gravity: number; 6 | density: number; 7 | wind: number; 8 | }): React.JSX.Element; 9 | declare const meta: { 10 | title: string; 11 | component: typeof ConfettiCanvasStory; 12 | tags: string[]; 13 | parameters: { 14 | docs: { 15 | description: { 16 | component: string; 17 | }; 18 | }; 19 | }; 20 | args: { 21 | gravity: number; 22 | wind: number; 23 | density: number; 24 | className: string; 25 | }; 26 | }; 27 | export default meta; 28 | type Story = StoryObj; 29 | export declare const Example: Story; 30 | -------------------------------------------------------------------------------- /dist/esm/types/stories/components/ConfettiCanvas.stories.d.ts: -------------------------------------------------------------------------------- 1 | import type { StoryObj } from "@storybook/react"; 2 | import * as React from "react"; 3 | declare function ConfettiCanvasStory({ className, ...args }: { 4 | className: string; 5 | gravity: number; 6 | density: number; 7 | wind: number; 8 | }): React.JSX.Element; 9 | declare const meta: { 10 | title: string; 11 | component: typeof ConfettiCanvasStory; 12 | tags: string[]; 13 | parameters: { 14 | docs: { 15 | description: { 16 | component: string; 17 | }; 18 | }; 19 | }; 20 | args: { 21 | gravity: number; 22 | wind: number; 23 | density: number; 24 | className: string; 25 | }; 26 | }; 27 | export default meta; 28 | type Story = StoryObj; 29 | export declare const Example: Story; 30 | -------------------------------------------------------------------------------- /dist/cjs/types/Utils.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { Vector2 } from "./Types"; 3 | export declare function setCanvasSize(canvas: HTMLCanvasElement | null): void; 4 | export declare function hexToRgb(hex: string): { 5 | r: number; 6 | g: number; 7 | b: number; 8 | }; 9 | export declare function mapFind(map: Map, predicate: (item: T) => boolean): T | null; 10 | export declare function isInRect({ x, y }: Vector2, rect: { 11 | x: number; 12 | y: number; 13 | width: number; 14 | height: number; 15 | }): boolean; 16 | export declare function getClickPosition(e: React.MouseEvent | MouseEvent, element: HTMLElement | null | undefined): { 17 | x: number; 18 | y: number; 19 | }; 20 | export declare function calculateAirResistance(dragCoefficient: number, velocity: number, airResistanceArea: number, fluidDensity: number): number; 21 | -------------------------------------------------------------------------------- /dist/esm/types/Utils.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { Vector2 } from "./Types"; 3 | export declare function setCanvasSize(canvas: HTMLCanvasElement | null): void; 4 | export declare function hexToRgb(hex: string): { 5 | r: number; 6 | g: number; 7 | b: number; 8 | }; 9 | export declare function mapFind(map: Map, predicate: (item: T) => boolean): T | null; 10 | export declare function isInRect({ x, y }: Vector2, rect: { 11 | x: number; 12 | y: number; 13 | width: number; 14 | height: number; 15 | }): boolean; 16 | export declare function getClickPosition(e: React.MouseEvent | MouseEvent, element: HTMLElement | null | undefined): { 17 | x: number; 18 | y: number; 19 | }; 20 | export declare function calculateAirResistance(dragCoefficient: number, velocity: number, airResistanceArea: number, fluidDensity: number): number; 21 | -------------------------------------------------------------------------------- /dist/cjs/types/stories/components/SpriteCanvas.stories.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import type { StoryObj } from "@storybook/react"; 3 | declare const meta: { 4 | title: string; 5 | component: import("react").ForwardRefExoticComponent>; 6 | tags: string[]; 7 | parameters: { 8 | docs: { 9 | description: { 10 | component: string; 11 | }; 12 | }; 13 | }; 14 | args: { 15 | visible: true; 16 | spriteWidth: number; 17 | spriteHeight: number; 18 | colors: string[]; 19 | sprites: any[]; 20 | className: any; 21 | }; 22 | }; 23 | export default meta; 24 | type Story = StoryObj; 25 | export declare const Example: Story; 26 | -------------------------------------------------------------------------------- /dist/esm/types/stories/components/SpriteCanvas.stories.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import type { StoryObj } from "@storybook/react"; 3 | declare const meta: { 4 | title: string; 5 | component: import("react").ForwardRefExoticComponent>; 6 | tags: string[]; 7 | parameters: { 8 | docs: { 9 | description: { 10 | component: string; 11 | }; 12 | }; 13 | }; 14 | args: { 15 | visible: true; 16 | spriteWidth: number; 17 | spriteHeight: number; 18 | colors: string[]; 19 | sprites: any[]; 20 | className: any; 21 | }; 22 | }; 23 | export default meta; 24 | type Story = StoryObj; 25 | export declare const Example: Story; 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2023 Discord, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /dist/cjs/types/components/SpriteCanvas.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { SpriteProp } from "../Types"; 3 | export interface Sprite { 4 | image: HTMLImageElement; 5 | colorize: boolean; 6 | src: string; 7 | } 8 | export interface SpriteCanvasProps { 9 | className?: string; 10 | visible?: boolean; 11 | sprites: SpriteProp[]; 12 | colors: string[]; 13 | spriteWidth: number; 14 | spriteHeight: number; 15 | } 16 | export interface SpriteCanvasData { 17 | sprites: Sprite[]; 18 | colors: string[]; 19 | spriteWidth: number; 20 | spriteHeight: number; 21 | } 22 | export interface SpriteCanvasHandle { 23 | getCanvas: () => HTMLCanvasElement | null; 24 | getCreateData: () => SpriteCanvasData; 25 | addReadyListener: (listener: (isReady: boolean) => void) => string; 26 | removeReadyListener: (listenerId: string) => void; 27 | isReady: boolean; 28 | } 29 | declare const _default: React.ForwardRefExoticComponent>; 30 | export default _default; 31 | -------------------------------------------------------------------------------- /dist/esm/types/components/SpriteCanvas.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { SpriteProp } from "../Types"; 3 | export interface Sprite { 4 | image: HTMLImageElement; 5 | colorize: boolean; 6 | src: string; 7 | } 8 | export interface SpriteCanvasProps { 9 | className?: string; 10 | visible?: boolean; 11 | sprites: SpriteProp[]; 12 | colors: string[]; 13 | spriteWidth: number; 14 | spriteHeight: number; 15 | } 16 | export interface SpriteCanvasData { 17 | sprites: Sprite[]; 18 | colors: string[]; 19 | spriteWidth: number; 20 | spriteHeight: number; 21 | } 22 | export interface SpriteCanvasHandle { 23 | getCanvas: () => HTMLCanvasElement | null; 24 | getCreateData: () => SpriteCanvasData; 25 | addReadyListener: (listener: (isReady: boolean) => void) => string; 26 | removeReadyListener: (listenerId: string) => void; 27 | isReady: boolean; 28 | } 29 | declare const _default: React.ForwardRefExoticComponent>; 30 | export default _default; 31 | -------------------------------------------------------------------------------- /dist/cjs/types/components/useConfettiCannon.d.ts: -------------------------------------------------------------------------------- 1 | import Confetti from "../Confetti"; 2 | import { SpriteProp } from "../Types"; 3 | import { CreateConfettiArgs } from "../createConfetti"; 4 | import { ConfettiCanvasHandle } from "./ConfettiCanvas"; 5 | import { SpriteCanvasHandle } from "./SpriteCanvas"; 6 | export interface CreateConfettiRequestedOptions { 7 | sprite?: SpriteProp; 8 | color?: string; 9 | } 10 | export interface ConfettiCannon { 11 | createConfetti: (createConfettiArgs: CreateConfettiArgs, createConfettiRequestedOptions?: CreateConfettiRequestedOptions) => Confetti | undefined; 12 | createMultipleConfetti: (createConfettiArgs: CreateConfettiArgs, numberToFire: number, createConfettiRequestedOptions?: CreateConfettiRequestedOptions) => Confetti[]; 13 | addConfetti: (confetti: Confetti) => void; 14 | deleteConfetti: (id: string) => void; 15 | clearConfetti: () => void; 16 | isReady: boolean; 17 | } 18 | export default function useConfettiCannon(confettiCanvas: ConfettiCanvasHandle | null, spriteCanvas: SpriteCanvasHandle | null): ConfettiCannon; 19 | -------------------------------------------------------------------------------- /dist/esm/types/components/useConfettiCannon.d.ts: -------------------------------------------------------------------------------- 1 | import Confetti from "../Confetti"; 2 | import { SpriteProp } from "../Types"; 3 | import { CreateConfettiArgs } from "../createConfetti"; 4 | import { ConfettiCanvasHandle } from "./ConfettiCanvas"; 5 | import { SpriteCanvasHandle } from "./SpriteCanvas"; 6 | export interface CreateConfettiRequestedOptions { 7 | sprite?: SpriteProp; 8 | color?: string; 9 | } 10 | export interface ConfettiCannon { 11 | createConfetti: (createConfettiArgs: CreateConfettiArgs, createConfettiRequestedOptions?: CreateConfettiRequestedOptions) => Confetti | undefined; 12 | createMultipleConfetti: (createConfettiArgs: CreateConfettiArgs, numberToFire: number, createConfettiRequestedOptions?: CreateConfettiRequestedOptions) => Confetti[]; 13 | addConfetti: (confetti: Confetti) => void; 14 | deleteConfetti: (id: string) => void; 15 | clearConfetti: () => void; 16 | isReady: boolean; 17 | } 18 | export default function useConfettiCannon(confettiCanvas: ConfettiCanvasHandle | null, spriteCanvas: SpriteCanvasHandle | null): ConfettiCannon; 19 | -------------------------------------------------------------------------------- /dist/cjs/types/UpdatableValueImplementations.d.ts: -------------------------------------------------------------------------------- 1 | import { UpdatableValue } from "./UpdatableValue"; 2 | import { EasingFunction } from "./easing"; 3 | export declare class StaticUpdatableValue extends UpdatableValue { 4 | update(): void; 5 | previewUpdate(): number; 6 | } 7 | export declare class LinearUpdatableValue extends UpdatableValue { 8 | addValue: number; 9 | constructor(value: number, addValue: number); 10 | update(deltaTime: number): void; 11 | previewUpdate(deltaTime: number): number; 12 | } 13 | export type Direction = 1 | -1; 14 | export declare class OscillatingUpdatableValue extends UpdatableValue { 15 | min: number; 16 | max: number; 17 | duration: number; 18 | timePassed: number; 19 | directionMultiplier: Direction; 20 | easingFunction: EasingFunction; 21 | constructor(value: number, min: number, max: number, duration: number, directionMultiplier: Direction, easingFunction: EasingFunction); 22 | update(deltaTime: number): void; 23 | previewUpdate(deltaTime: number): number; 24 | doUpdate(deltaTime: number): [number, number, Direction]; 25 | } 26 | -------------------------------------------------------------------------------- /dist/esm/types/UpdatableValueImplementations.d.ts: -------------------------------------------------------------------------------- 1 | import { UpdatableValue } from "./UpdatableValue"; 2 | import { EasingFunction } from "./easing"; 3 | export declare class StaticUpdatableValue extends UpdatableValue { 4 | update(): void; 5 | previewUpdate(): number; 6 | } 7 | export declare class LinearUpdatableValue extends UpdatableValue { 8 | addValue: number; 9 | constructor(value: number, addValue: number); 10 | update(deltaTime: number): void; 11 | previewUpdate(deltaTime: number): number; 12 | } 13 | export type Direction = 1 | -1; 14 | export declare class OscillatingUpdatableValue extends UpdatableValue { 15 | min: number; 16 | max: number; 17 | duration: number; 18 | timePassed: number; 19 | directionMultiplier: Direction; 20 | easingFunction: EasingFunction; 21 | constructor(value: number, min: number, max: number, duration: number, directionMultiplier: Direction, easingFunction: EasingFunction); 22 | update(deltaTime: number): void; 23 | previewUpdate(deltaTime: number): number; 24 | doUpdate(deltaTime: number): [number, number, Direction]; 25 | } 26 | -------------------------------------------------------------------------------- /dist/cjs/types/UpdatableValue.d.ts: -------------------------------------------------------------------------------- 1 | export declare abstract class UpdatableValue { 2 | value: number; 3 | constructor(value: number); 4 | abstract update(deltaTime: number): void; 5 | abstract previewUpdate(deltaTime: number): number; 6 | } 7 | export declare class UpdatableVector2Value { 8 | _x: UpdatableValue; 9 | _y: UpdatableValue; 10 | constructor(x: UpdatableValue, y: UpdatableValue, uniformVectorValues: boolean | undefined); 11 | update(deltaTime: number): void; 12 | previewUpdate(deltaTime: number): { 13 | x: number; 14 | y: number; 15 | }; 16 | get x(): number; 17 | set x(x: number); 18 | get y(): number; 19 | set y(y: number); 20 | } 21 | export declare class UpdatableVector3Value extends UpdatableVector2Value { 22 | _z: UpdatableValue; 23 | constructor(x: UpdatableValue, y: UpdatableValue, z: UpdatableValue, uniformVectorValues: boolean | undefined); 24 | update(deltaTime: number): void; 25 | previewUpdate(deltaTime: number): { 26 | z: number; 27 | x: number; 28 | y: number; 29 | }; 30 | get z(): number; 31 | set z(z: number); 32 | } 33 | -------------------------------------------------------------------------------- /dist/esm/types/UpdatableValue.d.ts: -------------------------------------------------------------------------------- 1 | export declare abstract class UpdatableValue { 2 | value: number; 3 | constructor(value: number); 4 | abstract update(deltaTime: number): void; 5 | abstract previewUpdate(deltaTime: number): number; 6 | } 7 | export declare class UpdatableVector2Value { 8 | _x: UpdatableValue; 9 | _y: UpdatableValue; 10 | constructor(x: UpdatableValue, y: UpdatableValue, uniformVectorValues: boolean | undefined); 11 | update(deltaTime: number): void; 12 | previewUpdate(deltaTime: number): { 13 | x: number; 14 | y: number; 15 | }; 16 | get x(): number; 17 | set x(x: number); 18 | get y(): number; 19 | set y(y: number); 20 | } 21 | export declare class UpdatableVector3Value extends UpdatableVector2Value { 22 | _z: UpdatableValue; 23 | constructor(x: UpdatableValue, y: UpdatableValue, z: UpdatableValue, uniformVectorValues: boolean | undefined); 24 | update(deltaTime: number): void; 25 | previewUpdate(deltaTime: number): { 26 | z: number; 27 | x: number; 28 | y: number; 29 | }; 30 | get z(): number; 31 | set z(z: number); 32 | } 33 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from "@rollup/plugin-commonjs"; 2 | import resolve from "@rollup/plugin-node-resolve"; 3 | import terser from "@rollup/plugin-terser"; 4 | import typescript from "@rollup/plugin-typescript"; 5 | import dts from "rollup-plugin-dts"; 6 | import peerDepsExternal from "rollup-plugin-peer-deps-external"; 7 | import postcss from "rollup-plugin-postcss"; 8 | import packageJson from "./package.json"; 9 | 10 | export default [ 11 | { 12 | input: "src/index.ts", 13 | output: [ 14 | { 15 | file: packageJson.main, 16 | format: "cjs", 17 | sourcemap: true, 18 | }, 19 | { 20 | file: packageJson.module, 21 | format: "esm", 22 | sourcemap: true, 23 | }, 24 | ], 25 | plugins: [ 26 | peerDepsExternal(), 27 | resolve(), 28 | commonjs(), 29 | typescript({ tsconfig: "./tsconfig.json" }), 30 | postcss({ modules: true }), 31 | terser(), 32 | ], 33 | external: ["react", "react-dom"], 34 | }, 35 | { 36 | input: "src/index.ts", 37 | output: [{ file: "dist/index.d.ts", format: "es" }], 38 | plugins: [dts.default()], 39 | }, 40 | ]; 41 | -------------------------------------------------------------------------------- /src/stories/components/SpriteCanvas.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { SpriteCanvas } from "../../"; 3 | import styles from "../Stories.module.css"; 4 | 5 | const SPRITES = [ 6 | require("../images/square.svg"), 7 | require("../images/circle.svg"), 8 | { src: require("../images/duck.svg"), colorize: false }, 9 | ]; 10 | 11 | const meta = { 12 | title: "Components/SpriteCanvas", 13 | component: SpriteCanvas, 14 | tags: ["autodocs"], 15 | parameters: { 16 | docs: { 17 | description: { 18 | component: 19 | "This component is used to efficiently render your confetti by rendering a sprite sheet.", 20 | }, 21 | }, 22 | }, 23 | args: { 24 | visible: true, 25 | spriteWidth: 20, 26 | spriteHeight: 20, 27 | colors: [ 28 | "#FF73FA", 29 | "#FFC0FF", 30 | "#FFD836", 31 | "#FF9A15", 32 | "#A5F7DE", 33 | "#51BC9D", 34 | "#AEC7FF", 35 | "#3E70DD", 36 | ], 37 | sprites: SPRITES, 38 | className: styles.bordered, 39 | }, 40 | } satisfies Meta; 41 | 42 | export default meta; 43 | type Story = StoryObj; 44 | 45 | export const Example: Story = {}; 46 | -------------------------------------------------------------------------------- /dist/cjs/types/stories/examples/Static.stories.d.ts: -------------------------------------------------------------------------------- 1 | import type { StoryObj } from "@storybook/react"; 2 | import * as React from "react"; 3 | interface StaticStoryProps { 4 | showSpriteCanvas: boolean; 5 | size: number; 6 | positionAddX: number; 7 | positionAddY: number; 8 | velocityAddX: number; 9 | velocityAddY: number; 10 | rotateAddX: number; 11 | rotateAddY: number; 12 | rotateAddZ: number; 13 | opacityAdd: number; 14 | sizeAdd: number; 15 | } 16 | declare function StaticStory({ showSpriteCanvas, size, positionAddX, positionAddY, velocityAddX, velocityAddY, rotateAddX, rotateAddY, rotateAddZ, opacityAdd, sizeAdd, }: StaticStoryProps): React.JSX.Element; 17 | declare const meta: { 18 | title: string; 19 | component: typeof StaticStory; 20 | args: { 21 | showSpriteCanvas: false; 22 | size: number; 23 | positionAddX: number; 24 | positionAddY: number; 25 | velocityAddX: number; 26 | velocityAddY: number; 27 | rotateAddX: number; 28 | rotateAddY: number; 29 | rotateAddZ: number; 30 | opacityAdd: number; 31 | sizeAdd: number; 32 | }; 33 | }; 34 | export default meta; 35 | type Story = StoryObj; 36 | export declare const Example: Story; 37 | -------------------------------------------------------------------------------- /dist/esm/types/stories/examples/Static.stories.d.ts: -------------------------------------------------------------------------------- 1 | import type { StoryObj } from "@storybook/react"; 2 | import * as React from "react"; 3 | interface StaticStoryProps { 4 | showSpriteCanvas: boolean; 5 | size: number; 6 | positionAddX: number; 7 | positionAddY: number; 8 | velocityAddX: number; 9 | velocityAddY: number; 10 | rotateAddX: number; 11 | rotateAddY: number; 12 | rotateAddZ: number; 13 | opacityAdd: number; 14 | sizeAdd: number; 15 | } 16 | declare function StaticStory({ showSpriteCanvas, size, positionAddX, positionAddY, velocityAddX, velocityAddY, rotateAddX, rotateAddY, rotateAddZ, opacityAdd, sizeAdd, }: StaticStoryProps): React.JSX.Element; 17 | declare const meta: { 18 | title: string; 19 | component: typeof StaticStory; 20 | args: { 21 | showSpriteCanvas: false; 22 | size: number; 23 | positionAddX: number; 24 | positionAddY: number; 25 | velocityAddX: number; 26 | velocityAddY: number; 27 | rotateAddX: number; 28 | rotateAddY: number; 29 | rotateAddZ: number; 30 | opacityAdd: number; 31 | sizeAdd: number; 32 | }; 33 | }; 34 | export default meta; 35 | type Story = StoryObj; 36 | export declare const Example: Story; 37 | -------------------------------------------------------------------------------- /src/useReady.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { v4 as uuid } from "uuid"; 3 | 4 | export default function useReady() { 5 | const isReady = React.useRef(false); 6 | const onReadyListeners = React.useRef<{ 7 | [id: string]: (isReady: boolean) => void; 8 | }>({}); 9 | 10 | const callReadyListeners = React.useCallback((isReady: boolean) => { 11 | for (const listenerId in onReadyListeners.current) { 12 | onReadyListeners.current[listenerId](isReady); 13 | } 14 | }, []); 15 | 16 | React.useEffect(() => { 17 | return () => callReadyListeners(false); 18 | }, [callReadyListeners]); 19 | 20 | return React.useMemo(() => { 21 | return { 22 | isReady: isReady.current, 23 | addReadyListener: (listener: (isReady: boolean) => void) => { 24 | const listenerId = uuid(); 25 | onReadyListeners.current[listenerId] = listener; 26 | 27 | if (isReady.current) { 28 | listener(isReady.current); 29 | } 30 | 31 | return listenerId; 32 | }, 33 | removeReadyListener: (listenerId: string) => { 34 | delete onReadyListeners.current[listenerId]; 35 | }, 36 | setIsReady: (newIsReady: boolean) => { 37 | isReady.current = newIsReady; 38 | callReadyListeners(newIsReady); 39 | }, 40 | }; 41 | }, [callReadyListeners]); 42 | } 43 | -------------------------------------------------------------------------------- /src/stories/components/ConfettiCanvas.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import classNames from "classnames"; 3 | import * as React from "react"; 4 | import { ConfettiCanvas, Environment } from "../../"; 5 | import styles from "../Stories.module.css"; 6 | 7 | function ConfettiCanvasStory({ 8 | className, 9 | ...args 10 | }: { 11 | className: string; 12 | gravity: number; 13 | density: number; 14 | wind: number; 15 | }) { 16 | return ( 17 | 18 | ); 19 | } 20 | 21 | const meta = { 22 | title: "Components/ConfettiCanvas", 23 | component: ConfettiCanvasStory, 24 | tags: ["autodocs"], 25 | parameters: { 26 | docs: { 27 | description: { 28 | component: `This component will render a canvas that will render your confetti. 29 | This story won't display anything since we need a \`SpriteCanvas\` 30 | to render confetti. To this this in action see 31 | [Playground](/docs/playground--docs)`, 32 | }, 33 | }, 34 | }, 35 | args: { 36 | gravity: -9.8, 37 | wind: 2, 38 | density: 1.2041, 39 | className: classNames(styles.sized, styles.bordered), 40 | }, 41 | } satisfies Meta; 42 | 43 | export default meta; 44 | type Story = StoryObj; 45 | 46 | export const Example: Story = {}; 47 | -------------------------------------------------------------------------------- /dist/cjs/types/Confetti.d.ts: -------------------------------------------------------------------------------- 1 | import Environment from "./Environment"; 2 | import { Vector2 } from "./Types"; 3 | import { UpdatableValue, UpdatableVector2Value, UpdatableVector3Value } from "./UpdatableValue"; 4 | type ConfettiArgs = { 5 | id: string; 6 | position: UpdatableVector2Value; 7 | velocity: UpdatableVector2Value; 8 | rotation: UpdatableVector3Value; 9 | size: UpdatableVector2Value; 10 | dragCoefficient: UpdatableVector2Value; 11 | opacity: UpdatableValue; 12 | airResistanceArea: UpdatableVector2Value; 13 | spriteX: number; 14 | spriteY: number; 15 | spriteWidth: number; 16 | spriteHeight: number; 17 | }; 18 | export default class Confetti { 19 | id: string; 20 | position: UpdatableVector2Value; 21 | velocity: UpdatableVector2Value; 22 | rotation: UpdatableVector3Value; 23 | size: UpdatableVector2Value; 24 | dragCoefficient: UpdatableVector2Value; 25 | opacity: UpdatableValue; 26 | airResistanceArea: UpdatableVector2Value; 27 | spriteX: number; 28 | spriteY: number; 29 | spriteWidth: number; 30 | spriteHeight: number; 31 | _lastUpdatedAt: number; 32 | constructor(args: ConfettiArgs); 33 | getNewForces(environment: Environment, deltaTime: number): { 34 | x: number; 35 | y: number; 36 | }; 37 | update(environment: Environment): void; 38 | previewPositionUpdate(environment: Environment, deltaTimeMS: number): { 39 | x: number; 40 | y: number; 41 | }; 42 | draw(spriteCanvas: HTMLCanvasElement, context: CanvasRenderingContext2D): void; 43 | shouldDestroy(canvas: HTMLCanvasElement, environment: Environment): boolean; 44 | get width(): number; 45 | get height(): number; 46 | addForce(force: Vector2): void; 47 | } 48 | export {}; 49 | -------------------------------------------------------------------------------- /dist/esm/types/Confetti.d.ts: -------------------------------------------------------------------------------- 1 | import Environment from "./Environment"; 2 | import { Vector2 } from "./Types"; 3 | import { UpdatableValue, UpdatableVector2Value, UpdatableVector3Value } from "./UpdatableValue"; 4 | type ConfettiArgs = { 5 | id: string; 6 | position: UpdatableVector2Value; 7 | velocity: UpdatableVector2Value; 8 | rotation: UpdatableVector3Value; 9 | size: UpdatableVector2Value; 10 | dragCoefficient: UpdatableVector2Value; 11 | opacity: UpdatableValue; 12 | airResistanceArea: UpdatableVector2Value; 13 | spriteX: number; 14 | spriteY: number; 15 | spriteWidth: number; 16 | spriteHeight: number; 17 | }; 18 | export default class Confetti { 19 | id: string; 20 | position: UpdatableVector2Value; 21 | velocity: UpdatableVector2Value; 22 | rotation: UpdatableVector3Value; 23 | size: UpdatableVector2Value; 24 | dragCoefficient: UpdatableVector2Value; 25 | opacity: UpdatableValue; 26 | airResistanceArea: UpdatableVector2Value; 27 | spriteX: number; 28 | spriteY: number; 29 | spriteWidth: number; 30 | spriteHeight: number; 31 | _lastUpdatedAt: number; 32 | constructor(args: ConfettiArgs); 33 | getNewForces(environment: Environment, deltaTime: number): { 34 | x: number; 35 | y: number; 36 | }; 37 | update(environment: Environment): void; 38 | previewPositionUpdate(environment: Environment, deltaTimeMS: number): { 39 | x: number; 40 | y: number; 41 | }; 42 | draw(spriteCanvas: HTMLCanvasElement, context: CanvasRenderingContext2D): void; 43 | shouldDestroy(canvas: HTMLCanvasElement, environment: Environment): boolean; 44 | get width(): number; 45 | get height(): number; 46 | addForce(force: Vector2): void; 47 | } 48 | export {}; 49 | -------------------------------------------------------------------------------- /dist/cjs/types/components/ConfettiCanvas.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Confetti from "../Confetti"; 3 | import Environment from "../Environment"; 4 | import { SpriteProp } from "../Types"; 5 | import { CreateConfettiArgs } from "../createConfetti"; 6 | import { SpriteCanvasData } from "./SpriteCanvas"; 7 | type ClickListener = (e: MouseEvent, confetti: Confetti | null) => void; 8 | type MouseListener = (e: MouseEvent) => void; 9 | interface ConfettiCanvasProps extends Omit, "onClick" | "onMouseDown" | "onMouseMove" | "onMouseUp"> { 10 | className?: string; 11 | environment: Environment; 12 | onClick?: ClickListener; 13 | onMouseDown?: ClickListener; 14 | onMouseMove?: MouseListener; 15 | onMouseUp?: MouseListener; 16 | requestAnimationFrame?: (handler: FrameRequestCallback) => number; 17 | cancelAnimationFrame?: (id: number) => void; 18 | onBeforeRender?: (context: CanvasRenderingContext2D) => void; 19 | onAfterRender?: (context: CanvasRenderingContext2D) => void; 20 | } 21 | export interface ConfettiCanvasHandle { 22 | createConfetti: (args: CreateConfettiArgs, spriteCanvas: HTMLCanvasElement, SpriteCanvasData: SpriteCanvasData, sprite?: SpriteProp, color?: string | null) => Confetti; 23 | addConfetti: (confetti: Confetti, spriteCanvas: HTMLCanvasElement) => void; 24 | deleteConfetti: (id: string) => void; 25 | clearConfetti: () => void; 26 | getCanvas: () => HTMLCanvasElement | null; 27 | addReadyListener: (listener: (isReady: boolean) => void) => string; 28 | removeReadyListener: (listenerId: string) => void; 29 | isReady: boolean; 30 | } 31 | declare const _default: React.ForwardRefExoticComponent>; 32 | export default _default; 33 | -------------------------------------------------------------------------------- /dist/esm/types/components/ConfettiCanvas.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Confetti from "../Confetti"; 3 | import Environment from "../Environment"; 4 | import { SpriteProp } from "../Types"; 5 | import { CreateConfettiArgs } from "../createConfetti"; 6 | import { SpriteCanvasData } from "./SpriteCanvas"; 7 | type ClickListener = (e: MouseEvent, confetti: Confetti | null) => void; 8 | type MouseListener = (e: MouseEvent) => void; 9 | interface ConfettiCanvasProps extends Omit, "onClick" | "onMouseDown" | "onMouseMove" | "onMouseUp"> { 10 | className?: string; 11 | environment: Environment; 12 | onClick?: ClickListener; 13 | onMouseDown?: ClickListener; 14 | onMouseMove?: MouseListener; 15 | onMouseUp?: MouseListener; 16 | requestAnimationFrame?: (handler: FrameRequestCallback) => number; 17 | cancelAnimationFrame?: (id: number) => void; 18 | onBeforeRender?: (context: CanvasRenderingContext2D) => void; 19 | onAfterRender?: (context: CanvasRenderingContext2D) => void; 20 | } 21 | export interface ConfettiCanvasHandle { 22 | createConfetti: (args: CreateConfettiArgs, spriteCanvas: HTMLCanvasElement, SpriteCanvasData: SpriteCanvasData, sprite?: SpriteProp, color?: string | null) => Confetti; 23 | addConfetti: (confetti: Confetti, spriteCanvas: HTMLCanvasElement) => void; 24 | deleteConfetti: (id: string) => void; 25 | clearConfetti: () => void; 26 | getCanvas: () => HTMLCanvasElement | null; 27 | addReadyListener: (listener: (isReady: boolean) => void) => string; 28 | removeReadyListener: (listenerId: string) => void; 29 | isReady: boolean; 30 | } 31 | declare const _default: React.ForwardRefExoticComponent>; 32 | export default _default; 33 | -------------------------------------------------------------------------------- /src/UpdatableValue.tsx: -------------------------------------------------------------------------------- 1 | export abstract class UpdatableValue { 2 | value: number; 3 | 4 | constructor(value: number) { 5 | this.value = value; 6 | } 7 | 8 | abstract update(deltaTime: number): void; 9 | abstract previewUpdate(deltaTime: number): number; 10 | } 11 | 12 | export class UpdatableVector2Value { 13 | _x: UpdatableValue; 14 | _y: UpdatableValue; 15 | 16 | constructor( 17 | x: UpdatableValue, 18 | y: UpdatableValue, 19 | uniformVectorValues: boolean | undefined 20 | ) { 21 | this._x = x; 22 | this._y = uniformVectorValues ? x : y; 23 | } 24 | 25 | update(deltaTime: number) { 26 | this._x.update(deltaTime); 27 | this._y.update(deltaTime); 28 | } 29 | 30 | previewUpdate(deltaTime: number) { 31 | return { 32 | x: this._x.previewUpdate(deltaTime), 33 | y: this._y.previewUpdate(deltaTime), 34 | }; 35 | } 36 | 37 | get x() { 38 | return this._x.value; 39 | } 40 | 41 | set x(x: number) { 42 | this._x.value = x; 43 | } 44 | 45 | get y() { 46 | return this._y.value; 47 | } 48 | 49 | set y(y: number) { 50 | this._y.value = y; 51 | } 52 | } 53 | 54 | export class UpdatableVector3Value extends UpdatableVector2Value { 55 | _z: UpdatableValue; 56 | 57 | constructor( 58 | x: UpdatableValue, 59 | y: UpdatableValue, 60 | z: UpdatableValue, 61 | uniformVectorValues: boolean | undefined 62 | ) { 63 | super(x, y, uniformVectorValues); 64 | this._z = uniformVectorValues ? x : z; 65 | } 66 | 67 | update(deltaTime: number) { 68 | super.update(deltaTime); 69 | this._z.update(deltaTime); 70 | } 71 | 72 | previewUpdate(deltaTime: number) { 73 | const superUpdate = super.previewUpdate(deltaTime); 74 | return { 75 | ...superUpdate, 76 | z: this._z.previewUpdate(deltaTime), 77 | }; 78 | } 79 | 80 | get z() { 81 | return this._z.value; 82 | } 83 | 84 | set z(z: number) { 85 | this._z.value = z; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Utils.ts: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "./Types"; 2 | 3 | export function setCanvasSize(canvas: HTMLCanvasElement | null) { 4 | if (canvas != null) { 5 | const { width, height } = canvas.getBoundingClientRect(); 6 | canvas.width = width * global.devicePixelRatio; 7 | canvas.height = height * global.devicePixelRatio; 8 | } 9 | } 10 | 11 | export function hexToRgb(hex: string) { 12 | if (hex[0] === "#") { 13 | hex = hex.slice(1); 14 | } 15 | const bigint = parseInt(hex, 16); 16 | const r = (bigint >> 16) & 255; 17 | const g = (bigint >> 8) & 255; 18 | const b = bigint & 255; 19 | 20 | return { r, g, b }; 21 | } 22 | 23 | export function mapFind( 24 | map: Map, 25 | predicate: (item: T) => boolean 26 | ): T | null { 27 | for (const entry of Array.from(map.values())) { 28 | if (entry != null && predicate(entry)) { 29 | return entry; 30 | } 31 | } 32 | return null; 33 | } 34 | 35 | export function isInRect( 36 | { x, y }: Vector2, 37 | rect: { x: number; y: number; width: number; height: number } 38 | ) { 39 | return ( 40 | x > rect.x && 41 | x < rect.x + rect.width && 42 | y > rect.y && 43 | y < rect.y + rect.height 44 | ); 45 | } 46 | 47 | export function getClickPosition( 48 | e: React.MouseEvent | MouseEvent, 49 | element: HTMLElement | null | undefined 50 | ) { 51 | if (element == null) { 52 | throw new Error("element should not be null"); 53 | } 54 | 55 | const rect = element.getBoundingClientRect(); 56 | 57 | return { 58 | x: e.clientX - rect.left, 59 | y: e.clientY - rect.top, 60 | }; 61 | } 62 | 63 | export function calculateAirResistance( 64 | dragCoefficient: number, 65 | velocity: number, 66 | airResistanceArea: number, 67 | fluidDensity: number 68 | ) { 69 | const directionMultiplier = velocity > 0 ? -1 : 1; 70 | const absoluteVelocity = Math.abs(velocity); 71 | return ( 72 | 0.5 * 73 | dragCoefficient * 74 | fluidDensity * 75 | airResistanceArea * 76 | absoluteVelocity * 77 | absoluteVelocity * 78 | directionMultiplier 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /src/stories/images/duck.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "confetti-cannon", 3 | "version": "2.3.2", 4 | "description": "", 5 | "homepage": "https://discord.github.io/confetti-cannon/", 6 | "main": "dist/cjs/index.js", 7 | "module": "dist/esm/index.js", 8 | "types": "dist/index.d.ts", 9 | "scripts": { 10 | "format": "prettier --write .", 11 | "build": "rm -rf ./dist && rollup -c --bundleConfigAsCjs", 12 | "test": "echo \"Error: no test specified\" && exit 1", 13 | "storybook": "storybook dev -p 6006", 14 | "predeploy": "npm run build-storybook", 15 | "deploy-storybook": "gh-pages -d storybook-static", 16 | "build-storybook": "storybook build" 17 | }, 18 | "repository": "github:discord/confetti-cannon", 19 | "author": "@discord", 20 | "keywords": [ 21 | "confetti", 22 | "cannon" 23 | ], 24 | "license": "MIT", 25 | "devDependencies": { 26 | "@babel/preset-env": "^7.21.5", 27 | "@babel/preset-react": "^7.18.6", 28 | "@babel/preset-typescript": "^7.21.5", 29 | "@rollup/plugin-commonjs": "^25.0.0", 30 | "@rollup/plugin-node-resolve": "^15.0.2", 31 | "@rollup/plugin-terser": "^0.4.3", 32 | "@rollup/plugin-typescript": "^11.1.1", 33 | "@storybook/addon-essentials": "^7.0.12", 34 | "@storybook/addon-interactions": "^7.0.12", 35 | "@storybook/addon-links": "^7.0.12", 36 | "@storybook/blocks": "^7.0.12", 37 | "@storybook/react": "^7.0.12", 38 | "@storybook/react-webpack5": "^7.0.12", 39 | "@storybook/testing-library": "^0.0.14-next.2", 40 | "@types/invariant": "^2.2.35", 41 | "@types/react": "^18.2.6", 42 | "@types/uuid": "^9.0.1", 43 | "@typescript-eslint/eslint-plugin": "^5.59.6", 44 | "@typescript-eslint/parser": "^5.59.6", 45 | "eslint": "^8.41.0", 46 | "eslint-plugin-react": "^7.32.2", 47 | "eslint-plugin-react-hooks": "^4.6.0", 48 | "gh-pages": "^5.0.0", 49 | "prettier": "2.8.8", 50 | "prettier-plugin-organize-imports": "^3.2.2", 51 | "prop-types": "^15.8.1", 52 | "rollup": "^3.22.0", 53 | "rollup-plugin-dts": "^5.3.0", 54 | "rollup-plugin-peer-deps-external": "^2.2.4", 55 | "rollup-plugin-postcss": "^4.0.2", 56 | "storybook": "^7.0.12", 57 | "storybook-css-modules": "^1.0.8", 58 | "tslib": "^2.5.2", 59 | "typescript": "^5.0.4", 60 | "typescript-plugin-css-modules": "^5.0.1" 61 | }, 62 | "peerDependencies": { 63 | "classnames": "^2.3.2", 64 | "invariant": "^2.2.4", 65 | "react": "^18.2.0", 66 | "react-dom": "^18.2.0", 67 | "uuid": "^9.0.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/UpdatableValueImplementations.tsx: -------------------------------------------------------------------------------- 1 | import { UpdatableValue } from "./UpdatableValue"; 2 | import { EasingFunction } from "./easing"; 3 | 4 | export class StaticUpdatableValue extends UpdatableValue { 5 | update() { 6 | /* Static */ 7 | } 8 | 9 | previewUpdate(): number { 10 | return this.value; 11 | } 12 | } 13 | 14 | export class LinearUpdatableValue extends UpdatableValue { 15 | addValue: number; 16 | 17 | constructor(value: number, addValue: number) { 18 | super(value); 19 | this.addValue = addValue; 20 | } 21 | 22 | update(deltaTime: number) { 23 | this.value = this.previewUpdate(deltaTime); 24 | } 25 | 26 | previewUpdate(deltaTime: number) { 27 | return this.value + this.addValue * deltaTime; 28 | } 29 | } 30 | 31 | export type Direction = 1 | -1; 32 | 33 | export class OscillatingUpdatableValue extends UpdatableValue { 34 | min: number; 35 | max: number; 36 | duration: number; 37 | timePassed: number; 38 | directionMultiplier: Direction; 39 | easingFunction: EasingFunction; 40 | 41 | constructor( 42 | value: number, 43 | min: number, 44 | max: number, 45 | duration: number, 46 | directionMultiplier: Direction, 47 | easingFunction: EasingFunction 48 | ) { 49 | super(value); 50 | this.min = min; 51 | this.max = max; 52 | this.duration = duration; 53 | const timePassedCalculated = 54 | (this.value / (this.max - this.min)) * this.duration; 55 | const timePassed = isNaN(timePassedCalculated) ? 0 : timePassedCalculated; 56 | this.timePassed = timePassed < 0 ? this.duration - timePassed : timePassed; 57 | this.directionMultiplier = directionMultiplier; 58 | this.easingFunction = easingFunction; 59 | } 60 | 61 | update(deltaTime: number) { 62 | const [value, timePassed, directionMultiplier] = this.doUpdate(deltaTime); 63 | this.value = value; 64 | this.directionMultiplier = directionMultiplier; 65 | this.timePassed = timePassed; 66 | } 67 | 68 | previewUpdate(deltaTime: number): number { 69 | return this.doUpdate(deltaTime)[0]; 70 | } 71 | 72 | doUpdate(deltaTime: number): [number, number, Direction] { 73 | const distance = this.max - this.min; 74 | const timeDiff = this.timePassed + deltaTime * this.directionMultiplier; 75 | const timePassed = Math.min(Math.max(timeDiff, 0), this.duration); 76 | 77 | const directionMultiplier = ( 78 | timeDiff < 0 || timeDiff > this.duration 79 | ? this.directionMultiplier * -1 80 | : this.directionMultiplier 81 | ) as Direction; 82 | 83 | const newValue = this.easingFunction( 84 | timePassed, 85 | this.min, 86 | distance, 87 | this.duration 88 | ); 89 | return [isNaN(newValue) ? 0 : newValue, timePassed, directionMultiplier]; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/stories/examples/Basic.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import classNames from "classnames"; 3 | import * as React from "react"; 4 | import { 5 | ConfettiCanvas, 6 | ConfettiCanvasHandle, 7 | CreateConfettiArgs, 8 | Environment, 9 | SpriteCanvas, 10 | SpriteCanvasHandle, 11 | useConfettiCannon, 12 | } from "../../"; 13 | import { getClickPosition } from "../../Utils"; 14 | import styles from "../Stories.module.css"; 15 | 16 | const SPRITES = [ 17 | require("../images/square.svg"), 18 | require("../images/circle.svg"), 19 | { src: require("../images/duck.svg"), colorize: false }, 20 | ]; 21 | 22 | const COLORS = [ 23 | "#FF73FA", 24 | "#FFC0FF", 25 | "#FFD836", 26 | "#FF9A15", 27 | "#A5F7DE", 28 | "#51BC9D", 29 | "#AEC7FF", 30 | "#3E70DD", 31 | ]; 32 | 33 | const MAX_SIZE = 40; 34 | 35 | function BasicStory() { 36 | const [confettiCanvas, setConfettiCanvas] = 37 | React.useState(null); 38 | const [spriteCanvas, setSpriteCanvas] = 39 | React.useState(null); 40 | const environment = React.useMemo(() => new Environment(), []); 41 | const cannon = useConfettiCannon(confettiCanvas, spriteCanvas); 42 | 43 | const addConfetti = React.useCallback( 44 | (x: number, y: number) => { 45 | const createConfettiArgs: CreateConfettiArgs = { 46 | position: { 47 | type: "static-random", 48 | minValue: { x, y }, 49 | maxValue: { x, y }, 50 | }, 51 | velocity: { 52 | type: "static-random", 53 | minValue: { x: 5, y: -50 }, 54 | maxValue: { x: 5, y: -75 }, 55 | }, 56 | rotation: { 57 | type: "linear-random", 58 | minValue: 0, 59 | maxValue: 360, 60 | minAddValue: -25, 61 | maxAddValue: 25, 62 | }, 63 | size: { 64 | type: "static-random", 65 | minValue: 20, 66 | maxValue: MAX_SIZE, 67 | }, 68 | }; 69 | 70 | cannon.createConfetti(createConfettiArgs); 71 | }, 72 | [cannon] 73 | ); 74 | 75 | const handleClick = (e: MouseEvent) => { 76 | const { x, y } = getClickPosition(e, confettiCanvas?.getCanvas()); 77 | addConfetti(x, y); 78 | }; 79 | 80 | return ( 81 | <> 82 | 90 | 96 | 97 | ); 98 | } 99 | 100 | const meta = { 101 | title: "Examples/Basic", 102 | component: BasicStory, 103 | } satisfies Meta; 104 | 105 | export default meta; 106 | type Story = StoryObj; 107 | 108 | export const Example: Story = {}; 109 | -------------------------------------------------------------------------------- /dist/cjs/types/stories/Playground.stories.d.ts: -------------------------------------------------------------------------------- 1 | import type { StoryObj } from "@storybook/react"; 2 | import * as React from "react"; 3 | import { SpriteProp } from "../"; 4 | interface PlaygroundStoryProps { 5 | autoFire: boolean; 6 | numberToFire: number; 7 | showSpriteCanvas: boolean; 8 | gravity: number; 9 | wind: number; 10 | density: number; 11 | positionSpreadX: number; 12 | positionSpreadY: number; 13 | minVelocityX: number; 14 | maxVelocityX: number; 15 | minVelocityY: number; 16 | maxVelocityY: number; 17 | minRotationX: number; 18 | maxRotationX: number; 19 | minRotationY: number; 20 | maxRotationY: number; 21 | minRotationZ: number; 22 | maxRotationZ: number; 23 | minRotationAddValueX: number; 24 | maxRotationAddValueX: number; 25 | minRotationAddValueY: number; 26 | maxRotationAddValueY: number; 27 | minRotationAddValueZ: number; 28 | maxRotationAddValueZ: number; 29 | dragCoefficientX: number; 30 | dragCoefficientY: number; 31 | airResistanceAreaX: number; 32 | airResistanceAreaY: number; 33 | opacity: number; 34 | opacityAddValue: number; 35 | minSize: number; 36 | maxSize: number; 37 | sprites: SpriteProp[]; 38 | colors: string[]; 39 | } 40 | declare function PlaygroundStory({ autoFire, numberToFire, showSpriteCanvas, gravity, wind, density, positionSpreadX, positionSpreadY, minVelocityX, maxVelocityX, minVelocityY, maxVelocityY, minRotationX, maxRotationX, minRotationY, maxRotationY, minRotationZ, maxRotationZ, minRotationAddValueX, maxRotationAddValueX, minRotationAddValueY, maxRotationAddValueY, minRotationAddValueZ, maxRotationAddValueZ, dragCoefficientX, dragCoefficientY, airResistanceAreaX, airResistanceAreaY, opacity, opacityAddValue, minSize, maxSize, sprites, colors, }: PlaygroundStoryProps): React.JSX.Element; 41 | declare const meta: { 42 | title: string; 43 | component: typeof PlaygroundStory; 44 | args: { 45 | autoFire: false; 46 | numberToFire: number; 47 | showSpriteCanvas: false; 48 | gravity: number; 49 | wind: number; 50 | density: number; 51 | positionSpreadX: number; 52 | positionSpreadY: number; 53 | minVelocityX: number; 54 | maxVelocityX: number; 55 | minVelocityY: number; 56 | maxVelocityY: number; 57 | minRotationX: number; 58 | maxRotationX: number; 59 | minRotationY: number; 60 | maxRotationY: number; 61 | minRotationZ: number; 62 | maxRotationZ: number; 63 | minRotationAddValueX: number; 64 | maxRotationAddValueX: number; 65 | minRotationAddValueY: number; 66 | maxRotationAddValueY: number; 67 | minRotationAddValueZ: number; 68 | maxRotationAddValueZ: number; 69 | dragCoefficientX: number; 70 | dragCoefficientY: number; 71 | airResistanceAreaX: number; 72 | airResistanceAreaY: number; 73 | opacity: number; 74 | opacityAddValue: number; 75 | minSize: number; 76 | maxSize: number; 77 | sprites: any[]; 78 | colors: string[]; 79 | }; 80 | }; 81 | export default meta; 82 | type Story = StoryObj; 83 | export declare const Example: Story; 84 | -------------------------------------------------------------------------------- /dist/esm/types/stories/Playground.stories.d.ts: -------------------------------------------------------------------------------- 1 | import type { StoryObj } from "@storybook/react"; 2 | import * as React from "react"; 3 | import { SpriteProp } from "../"; 4 | interface PlaygroundStoryProps { 5 | autoFire: boolean; 6 | numberToFire: number; 7 | showSpriteCanvas: boolean; 8 | gravity: number; 9 | wind: number; 10 | density: number; 11 | positionSpreadX: number; 12 | positionSpreadY: number; 13 | minVelocityX: number; 14 | maxVelocityX: number; 15 | minVelocityY: number; 16 | maxVelocityY: number; 17 | minRotationX: number; 18 | maxRotationX: number; 19 | minRotationY: number; 20 | maxRotationY: number; 21 | minRotationZ: number; 22 | maxRotationZ: number; 23 | minRotationAddValueX: number; 24 | maxRotationAddValueX: number; 25 | minRotationAddValueY: number; 26 | maxRotationAddValueY: number; 27 | minRotationAddValueZ: number; 28 | maxRotationAddValueZ: number; 29 | dragCoefficientX: number; 30 | dragCoefficientY: number; 31 | airResistanceAreaX: number; 32 | airResistanceAreaY: number; 33 | opacity: number; 34 | opacityAddValue: number; 35 | minSize: number; 36 | maxSize: number; 37 | sprites: SpriteProp[]; 38 | colors: string[]; 39 | } 40 | declare function PlaygroundStory({ autoFire, numberToFire, showSpriteCanvas, gravity, wind, density, positionSpreadX, positionSpreadY, minVelocityX, maxVelocityX, minVelocityY, maxVelocityY, minRotationX, maxRotationX, minRotationY, maxRotationY, minRotationZ, maxRotationZ, minRotationAddValueX, maxRotationAddValueX, minRotationAddValueY, maxRotationAddValueY, minRotationAddValueZ, maxRotationAddValueZ, dragCoefficientX, dragCoefficientY, airResistanceAreaX, airResistanceAreaY, opacity, opacityAddValue, minSize, maxSize, sprites, colors, }: PlaygroundStoryProps): React.JSX.Element; 41 | declare const meta: { 42 | title: string; 43 | component: typeof PlaygroundStory; 44 | args: { 45 | autoFire: false; 46 | numberToFire: number; 47 | showSpriteCanvas: false; 48 | gravity: number; 49 | wind: number; 50 | density: number; 51 | positionSpreadX: number; 52 | positionSpreadY: number; 53 | minVelocityX: number; 54 | maxVelocityX: number; 55 | minVelocityY: number; 56 | maxVelocityY: number; 57 | minRotationX: number; 58 | maxRotationX: number; 59 | minRotationY: number; 60 | maxRotationY: number; 61 | minRotationZ: number; 62 | maxRotationZ: number; 63 | minRotationAddValueX: number; 64 | maxRotationAddValueX: number; 65 | minRotationAddValueY: number; 66 | maxRotationAddValueY: number; 67 | minRotationAddValueZ: number; 68 | maxRotationAddValueZ: number; 69 | dragCoefficientX: number; 70 | dragCoefficientY: number; 71 | airResistanceAreaX: number; 72 | airResistanceAreaY: number; 73 | opacity: number; 74 | opacityAddValue: number; 75 | minSize: number; 76 | maxSize: number; 77 | sprites: any[]; 78 | colors: string[]; 79 | }; 80 | }; 81 | export default meta; 82 | type Story = StoryObj; 83 | export declare const Example: Story; 84 | -------------------------------------------------------------------------------- /src/stories/examples/ConditionalRender.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import classNames from "classnames"; 3 | import * as React from "react"; 4 | import { 5 | ConfettiCanvas, 6 | ConfettiCanvasHandle, 7 | CreateConfettiArgs, 8 | Environment, 9 | SpriteCanvas, 10 | SpriteCanvasHandle, 11 | useConfettiCannon, 12 | } from "../../"; 13 | import styles from "../Stories.module.css"; 14 | 15 | const SPRITES = [ 16 | require("../images/square.svg"), 17 | require("../images/circle.svg"), 18 | { src: require("../images/duck.svg"), colorize: false }, 19 | ]; 20 | 21 | const COLORS = [ 22 | "#FF73FA", 23 | "#FFC0FF", 24 | "#FFD836", 25 | "#FF9A15", 26 | "#A5F7DE", 27 | "#51BC9D", 28 | "#AEC7FF", 29 | "#3E70DD", 30 | ]; 31 | 32 | const MAX_SIZE = 40; 33 | 34 | function ConditionalRenderStory() { 35 | const [shouldRender, setShouldRender] = React.useState(false); 36 | 37 | const [confettiCanvas, setConfettiCanvas] = 38 | React.useState(null); 39 | const [spriteCanvas, setSpriteCanvas] = 40 | React.useState(null); 41 | const environment = React.useMemo(() => new Environment(), []); 42 | const cannon = useConfettiCannon(confettiCanvas, spriteCanvas); 43 | 44 | const addConfetti = React.useCallback( 45 | (x: number, y: number) => { 46 | const createConfettiArgs: CreateConfettiArgs = { 47 | position: { 48 | type: "static-random", 49 | minValue: { x, y }, 50 | maxValue: { x, y }, 51 | }, 52 | velocity: { 53 | type: "static-random", 54 | minValue: { x: 5, y: -50 }, 55 | maxValue: { x: 5, y: -75 }, 56 | }, 57 | rotation: { 58 | type: "linear-random", 59 | minValue: 0, 60 | maxValue: 360, 61 | minAddValue: -25, 62 | maxAddValue: 25, 63 | }, 64 | size: { 65 | type: "static-random", 66 | minValue: 20, 67 | maxValue: MAX_SIZE, 68 | }, 69 | }; 70 | 71 | cannon.createConfetti(createConfettiArgs); 72 | }, 73 | [cannon] 74 | ); 75 | 76 | React.useEffect(() => { 77 | addConfetti(20, 20); 78 | }, [addConfetti]); 79 | 80 | return ( 81 | <> 82 | 83 | {shouldRender ? ( 84 | <> 85 | 93 | 98 | 99 | ) : null} 100 | 101 | ); 102 | } 103 | 104 | const meta = { 105 | title: "Examples/ConditionalRenderStory", 106 | component: ConditionalRenderStory, 107 | } satisfies Meta; 108 | 109 | export default meta; 110 | type Story = StoryObj; 111 | 112 | export const Example: Story = {}; 113 | -------------------------------------------------------------------------------- /dist/cjs/types/createConfetti.d.ts: -------------------------------------------------------------------------------- 1 | import Confetti from "./Confetti"; 2 | import { SpriteProp, Vector2, Vector3 } from "./Types"; 3 | import { UpdatableVector2Value, UpdatableVector3Value } from "./UpdatableValue"; 4 | import { LinearUpdatableValue, OscillatingUpdatableValue, StaticUpdatableValue } from "./UpdatableValueImplementations"; 5 | import { SpriteCanvasData } from "./components/SpriteCanvas"; 6 | import { EasingFunction } from "./easing"; 7 | interface StaticConfigConstant { 8 | type: "static"; 9 | value: T; 10 | } 11 | interface StaticConfigRandom { 12 | type: "static-random"; 13 | minValue: T; 14 | maxValue: T; 15 | } 16 | type StaticConfig = StaticConfigConstant | StaticConfigRandom; 17 | interface LinearConfigConstant { 18 | type: "linear"; 19 | value: T; 20 | addValue: T; 21 | } 22 | interface LinearConfigRandom { 23 | type: "linear-random"; 24 | minValue: T; 25 | maxValue: T; 26 | minAddValue: T; 27 | maxAddValue: T; 28 | } 29 | type LinearConfig = LinearConfigConstant | LinearConfigRandom; 30 | type Direction = 1 | -1; 31 | type DirectionVector2 = { 32 | x: Direction; 33 | y: Direction; 34 | }; 35 | type DirectionVector3 = DirectionVector2 & { 36 | z: Direction; 37 | }; 38 | interface OscillatingConfigConstant { 39 | type: "oscillating"; 40 | value: T; 41 | start: T; 42 | final: T; 43 | duration: T; 44 | direction: TDirection; 45 | easingFunction: EasingFunction; 46 | } 47 | interface OscillatingConfigRandom { 48 | type: "oscillating-random"; 49 | minValue: T; 50 | maxValue: T; 51 | minStart: T; 52 | maxStart: T; 53 | minFinal: T; 54 | maxFinal: T; 55 | minDuration: T; 56 | maxDuration: T; 57 | minDirection: TDirection; 58 | maxDirection: TDirection; 59 | easingFunctions: EasingFunction[]; 60 | } 61 | type OscillatingConfig = OscillatingConfigConstant | OscillatingConfigRandom; 62 | type Config = StaticConfig | LinearConfig | OscillatingConfig; 63 | type ConfigNumber = Config; 64 | type ConfigVector2 = Config; 65 | type ConfigVector3 = Config; 66 | type ConfigNumberInput = ConfigNumber; 67 | type ConfigVector2Input = (ConfigVector2 | ConfigNumber) & { 68 | uniformVectorValues?: boolean; 69 | }; 70 | type ConfigVector3Input = (ConfigVector3 | ConfigNumber) & { 71 | uniformVectorValues?: boolean; 72 | }; 73 | export interface CreateConfettiArgsFull { 74 | id?: string; 75 | position: ConfigVector2; 76 | velocity: ConfigVector2; 77 | rotation: ConfigVector3; 78 | dragCoefficient: ConfigVector2; 79 | airResistanceArea?: ConfigVector2Input; 80 | size: ConfigNumber; 81 | opacity: ConfigNumber; 82 | } 83 | export type CreateConfettiArgs = { 84 | id?: string; 85 | position: ConfigVector2Input; 86 | velocity?: ConfigVector2Input; 87 | rotation?: ConfigVector3Input; 88 | dragCoefficient?: ConfigVector2Input; 89 | airResistanceArea?: ConfigVector2Input; 90 | size: ConfigVector2Input; 91 | opacity?: ConfigNumberInput; 92 | }; 93 | export declare function getUpdatableValueNumber(config: ConfigNumber): StaticUpdatableValue | LinearUpdatableValue | OscillatingUpdatableValue; 94 | export declare function getUpdatableValueVector2(config: ConfigVector2Input): UpdatableVector2Value; 95 | export declare function getUpdatableValueVector3(config: ConfigVector3Input): UpdatableVector3Value; 96 | export default function createConfetti(id: string, rawArgs: CreateConfettiArgs, spriteCanvasData: SpriteCanvasData, requestedSprite?: SpriteProp, requestedColor?: string | null): Confetti; 97 | export {}; 98 | -------------------------------------------------------------------------------- /dist/esm/types/createConfetti.d.ts: -------------------------------------------------------------------------------- 1 | import Confetti from "./Confetti"; 2 | import { SpriteProp, Vector2, Vector3 } from "./Types"; 3 | import { UpdatableVector2Value, UpdatableVector3Value } from "./UpdatableValue"; 4 | import { LinearUpdatableValue, OscillatingUpdatableValue, StaticUpdatableValue } from "./UpdatableValueImplementations"; 5 | import { SpriteCanvasData } from "./components/SpriteCanvas"; 6 | import { EasingFunction } from "./easing"; 7 | interface StaticConfigConstant { 8 | type: "static"; 9 | value: T; 10 | } 11 | interface StaticConfigRandom { 12 | type: "static-random"; 13 | minValue: T; 14 | maxValue: T; 15 | } 16 | type StaticConfig = StaticConfigConstant | StaticConfigRandom; 17 | interface LinearConfigConstant { 18 | type: "linear"; 19 | value: T; 20 | addValue: T; 21 | } 22 | interface LinearConfigRandom { 23 | type: "linear-random"; 24 | minValue: T; 25 | maxValue: T; 26 | minAddValue: T; 27 | maxAddValue: T; 28 | } 29 | type LinearConfig = LinearConfigConstant | LinearConfigRandom; 30 | type Direction = 1 | -1; 31 | type DirectionVector2 = { 32 | x: Direction; 33 | y: Direction; 34 | }; 35 | type DirectionVector3 = DirectionVector2 & { 36 | z: Direction; 37 | }; 38 | interface OscillatingConfigConstant { 39 | type: "oscillating"; 40 | value: T; 41 | start: T; 42 | final: T; 43 | duration: T; 44 | direction: TDirection; 45 | easingFunction: EasingFunction; 46 | } 47 | interface OscillatingConfigRandom { 48 | type: "oscillating-random"; 49 | minValue: T; 50 | maxValue: T; 51 | minStart: T; 52 | maxStart: T; 53 | minFinal: T; 54 | maxFinal: T; 55 | minDuration: T; 56 | maxDuration: T; 57 | minDirection: TDirection; 58 | maxDirection: TDirection; 59 | easingFunctions: EasingFunction[]; 60 | } 61 | type OscillatingConfig = OscillatingConfigConstant | OscillatingConfigRandom; 62 | type Config = StaticConfig | LinearConfig | OscillatingConfig; 63 | type ConfigNumber = Config; 64 | type ConfigVector2 = Config; 65 | type ConfigVector3 = Config; 66 | type ConfigNumberInput = ConfigNumber; 67 | type ConfigVector2Input = (ConfigVector2 | ConfigNumber) & { 68 | uniformVectorValues?: boolean; 69 | }; 70 | type ConfigVector3Input = (ConfigVector3 | ConfigNumber) & { 71 | uniformVectorValues?: boolean; 72 | }; 73 | export interface CreateConfettiArgsFull { 74 | id?: string; 75 | position: ConfigVector2; 76 | velocity: ConfigVector2; 77 | rotation: ConfigVector3; 78 | dragCoefficient: ConfigVector2; 79 | airResistanceArea?: ConfigVector2Input; 80 | size: ConfigNumber; 81 | opacity: ConfigNumber; 82 | } 83 | export type CreateConfettiArgs = { 84 | id?: string; 85 | position: ConfigVector2Input; 86 | velocity?: ConfigVector2Input; 87 | rotation?: ConfigVector3Input; 88 | dragCoefficient?: ConfigVector2Input; 89 | airResistanceArea?: ConfigVector2Input; 90 | size: ConfigVector2Input; 91 | opacity?: ConfigNumberInput; 92 | }; 93 | export declare function getUpdatableValueNumber(config: ConfigNumber): StaticUpdatableValue | LinearUpdatableValue | OscillatingUpdatableValue; 94 | export declare function getUpdatableValueVector2(config: ConfigVector2Input): UpdatableVector2Value; 95 | export declare function getUpdatableValueVector3(config: ConfigVector3Input): UpdatableVector3Value; 96 | export default function createConfetti(id: string, rawArgs: CreateConfettiArgs, spriteCanvasData: SpriteCanvasData, requestedSprite?: SpriteProp, requestedColor?: string | null): Confetti; 97 | export {}; 98 | -------------------------------------------------------------------------------- /src/stories/examples/Dragging.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import classNames from "classnames"; 3 | import * as React from "react"; 4 | import { 5 | Confetti, 6 | ConfettiCanvas, 7 | ConfettiCanvasHandle, 8 | CreateConfettiArgs, 9 | Environment, 10 | SpriteCanvas, 11 | SpriteCanvasHandle, 12 | useConfettiCannon, 13 | } from "../../"; 14 | import { getClickPosition } from "../../Utils"; 15 | import styles from "../Stories.module.css"; 16 | import SpriteCanvasStory from "../components/SpriteCanvas.stories"; 17 | 18 | const SIZE = 40; 19 | 20 | function DraggingStory() { 21 | const [confettiCanvas, setConfettiCanvas] = 22 | React.useState(null); 23 | const [spriteCanvas, setSpriteCanvas] = 24 | React.useState(null); 25 | const environment = React.useMemo( 26 | () => new Environment({ gravity: 0, wind: 0 }), 27 | [] 28 | ); 29 | const cannon = useConfettiCannon(confettiCanvas, spriteCanvas); 30 | const lastMousePosition = React.useRef({ x: 0, y: 0 }); 31 | const lastMouseEventTime = React.useRef(0); 32 | const draggingConfetti = React.useRef(null); 33 | 34 | const addConfetti = React.useCallback( 35 | (x: number, y: number) => { 36 | const createConfettiArgs: CreateConfettiArgs = { 37 | position: { 38 | type: "static", 39 | value: { x, y }, 40 | }, 41 | size: { 42 | type: "static", 43 | value: SIZE, 44 | }, 45 | }; 46 | 47 | cannon.createConfetti(createConfettiArgs); 48 | }, 49 | [cannon] 50 | ); 51 | 52 | const handleMouseDown = (e: MouseEvent, confetti: Confetti | null) => { 53 | if (confetti != null) { 54 | confetti.velocity.x = 0; 55 | confetti.velocity.y = 0; 56 | draggingConfetti.current = confetti; 57 | lastMouseEventTime.current = Date.now(); 58 | return; 59 | } 60 | const { x, y } = getClickPosition(e, confettiCanvas?.getCanvas()); 61 | lastMousePosition.current = { x, y }; 62 | addConfetti(x, y); 63 | }; 64 | 65 | const handleMouseMove = (e: MouseEvent) => { 66 | if (draggingConfetti.current != null) { 67 | const { x, y } = getClickPosition(e, confettiCanvas?.getCanvas()); 68 | draggingConfetti.current.position.x = x; 69 | draggingConfetti.current.position.y = y; 70 | lastMouseEventTime.current = Date.now(); 71 | } 72 | }; 73 | 74 | const handleMouseUp = (e: MouseEvent) => { 75 | const canvas = confettiCanvas?.getCanvas(); 76 | if (draggingConfetti.current == null || canvas == null) { 77 | return; 78 | } 79 | 80 | const { x, y } = getClickPosition(e, canvas); 81 | 82 | const deltaTime = Math.max(Date.now() - lastMouseEventTime.current ?? 1, 1); 83 | const velocityX = (x - lastMousePosition.current.x) / deltaTime; 84 | const velocityY = (y - lastMousePosition.current.y) / deltaTime; 85 | draggingConfetti.current.velocity.x = velocityX; 86 | draggingConfetti.current.velocity.y = velocityY; 87 | 88 | draggingConfetti.current = null; 89 | }; 90 | 91 | return ( 92 | <> 93 | 101 | 109 |
110 | 111 |
112 | 113 | ); 114 | } 115 | 116 | const meta = { 117 | title: "Examples/Dragging", 118 | component: DraggingStory, 119 | } satisfies Meta; 120 | 121 | export default meta; 122 | type Story = StoryObj; 123 | 124 | export const Example: Story = {}; 125 | -------------------------------------------------------------------------------- /src/stories/examples/Static.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import classNames from "classnames"; 3 | import * as React from "react"; 4 | import { 5 | Confetti, 6 | ConfettiCanvas, 7 | ConfettiCanvasHandle, 8 | CreateConfettiArgs, 9 | Environment, 10 | SpriteCanvas, 11 | SpriteCanvasHandle, 12 | useConfettiCannon, 13 | } from "../../"; 14 | import { getClickPosition } from "../../Utils"; 15 | import styles from "../Stories.module.css"; 16 | import SpriteCanvasStory from "../components/SpriteCanvas.stories"; 17 | 18 | interface StaticStoryProps { 19 | showSpriteCanvas: boolean; 20 | size: number; 21 | positionAddX: number; 22 | positionAddY: number; 23 | velocityAddX: number; 24 | velocityAddY: number; 25 | rotateAddX: number; 26 | rotateAddY: number; 27 | rotateAddZ: number; 28 | opacityAdd: number; 29 | sizeAdd: number; 30 | } 31 | 32 | function StaticStory({ 33 | showSpriteCanvas, 34 | size, 35 | positionAddX, 36 | positionAddY, 37 | velocityAddX, 38 | velocityAddY, 39 | rotateAddX, 40 | rotateAddY, 41 | rotateAddZ, 42 | opacityAdd, 43 | sizeAdd, 44 | }: StaticStoryProps) { 45 | const [confettiCanvas, setConfettiCanvas] = 46 | React.useState(null); 47 | const [spriteCanvas, setSpriteCanvas] = 48 | React.useState(null); 49 | const environment = React.useMemo( 50 | () => new Environment({ gravity: 0, wind: 0 }), 51 | [] 52 | ); 53 | const [isSmall, setIsSmall] = React.useState(false); 54 | const cannon = useConfettiCannon(confettiCanvas, spriteCanvas); 55 | 56 | const addConfetti = React.useCallback( 57 | (x: number, y: number) => { 58 | const createConfettiArgs: CreateConfettiArgs = { 59 | position: { 60 | type: "linear", 61 | value: { x, y }, 62 | addValue: { x: positionAddX, y: positionAddY }, 63 | }, 64 | velocity: { 65 | type: "linear", 66 | value: { x: 0, y: 0 }, 67 | addValue: { x: velocityAddX, y: velocityAddY }, 68 | }, 69 | rotation: { 70 | type: "linear", 71 | value: { x: 0, y: 0, z: 0 }, 72 | addValue: { x: rotateAddX, y: rotateAddY, z: rotateAddZ }, 73 | }, 74 | opacity: { 75 | type: "linear", 76 | value: 1, 77 | addValue: opacityAdd, 78 | }, 79 | size: { 80 | type: "linear", 81 | value: size, 82 | addValue: sizeAdd, 83 | }, 84 | }; 85 | 86 | cannon.createConfetti(createConfettiArgs); 87 | }, 88 | [ 89 | cannon, 90 | opacityAdd, 91 | positionAddX, 92 | positionAddY, 93 | rotateAddX, 94 | rotateAddY, 95 | rotateAddZ, 96 | size, 97 | sizeAdd, 98 | velocityAddX, 99 | velocityAddY, 100 | ] 101 | ); 102 | 103 | const handleClick = (e: MouseEvent, confetti: Confetti | null) => { 104 | if (confetti != null) { 105 | cannon.deleteConfetti(confetti.id); 106 | return; 107 | } 108 | const { x, y } = getClickPosition(e, confettiCanvas?.getCanvas()); 109 | addConfetti(x, y); 110 | }; 111 | 112 | return ( 113 | <> 114 | 123 | 132 |
133 | 136 | 137 | 138 |
139 | 140 | ); 141 | } 142 | 143 | const meta = { 144 | title: "Examples/Static", 145 | component: StaticStory, 146 | args: { 147 | showSpriteCanvas: false, 148 | size: 40, 149 | positionAddX: 0, 150 | positionAddY: 0, 151 | velocityAddX: 0, 152 | velocityAddY: 0, 153 | rotateAddX: 0, 154 | rotateAddY: 0, 155 | rotateAddZ: 0, 156 | opacityAdd: 0, 157 | sizeAdd: 0, 158 | }, 159 | } satisfies Meta; 160 | 161 | export default meta; 162 | type Story = StoryObj; 163 | 164 | export const Example: Story = {}; 165 | -------------------------------------------------------------------------------- /src/components/useConfettiCannon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Confetti from "../Confetti"; 3 | import { SpriteProp } from "../Types"; 4 | import { CreateConfettiArgs } from "../createConfetti"; 5 | import { ConfettiCanvasHandle } from "./ConfettiCanvas"; 6 | import { SpriteCanvasHandle } from "./SpriteCanvas"; 7 | 8 | export interface CreateConfettiRequestedOptions { 9 | sprite?: SpriteProp; 10 | color?: string; 11 | } 12 | 13 | export interface ConfettiCannon { 14 | createConfetti: ( 15 | createConfettiArgs: CreateConfettiArgs, 16 | createConfettiRequestedOptions?: CreateConfettiRequestedOptions 17 | ) => Confetti | undefined; 18 | createMultipleConfetti: ( 19 | createConfettiArgs: CreateConfettiArgs, 20 | numberToFire: number, 21 | createConfettiRequestedOptions?: CreateConfettiRequestedOptions 22 | ) => Confetti[]; 23 | addConfetti: (confetti: Confetti) => void; 24 | deleteConfetti: (id: string) => void; 25 | clearConfetti: () => void; 26 | isReady: boolean; 27 | } 28 | 29 | export default function useConfettiCannon( 30 | confettiCanvas: ConfettiCanvasHandle | null, 31 | spriteCanvas: SpriteCanvasHandle | null 32 | ): ConfettiCannon { 33 | const [isSpriteCanvasReady, setIsSpriteCanvasReady] = React.useState( 34 | spriteCanvas?.isReady ?? false 35 | ); 36 | const [isConfettiCanvasReady, setIsConfettiCanvasReady] = React.useState( 37 | confettiCanvas?.isReady ?? false 38 | ); 39 | 40 | React.useEffect(() => { 41 | const listenerId = spriteCanvas?.addReadyListener(setIsSpriteCanvasReady); 42 | 43 | return () => { 44 | if (listenerId != null) { 45 | spriteCanvas?.removeReadyListener(listenerId); 46 | } 47 | }; 48 | }, [spriteCanvas]); 49 | 50 | React.useEffect(() => { 51 | const listenerId = confettiCanvas?.addReadyListener( 52 | setIsConfettiCanvasReady 53 | ); 54 | 55 | return () => { 56 | if (listenerId != null) { 57 | confettiCanvas?.removeReadyListener(listenerId); 58 | } 59 | }; 60 | }, [confettiCanvas]); 61 | 62 | const createConfetti = React.useCallback( 63 | ( 64 | createConfettiArgs: CreateConfettiArgs, 65 | { sprite, color }: CreateConfettiRequestedOptions = {} 66 | ) => { 67 | const spriteData = spriteCanvas?.getCreateData(); 68 | const spriteCanvasRef = spriteCanvas?.getCanvas(); 69 | 70 | if ( 71 | spriteCanvasRef == null || 72 | spriteData == null || 73 | spriteData.sprites.length === 0 74 | ) { 75 | return; 76 | } 77 | 78 | return confettiCanvas?.createConfetti( 79 | createConfettiArgs, 80 | spriteCanvasRef, 81 | spriteData, 82 | sprite, 83 | color 84 | ); 85 | }, 86 | [confettiCanvas, spriteCanvas] 87 | ); 88 | const createMultipleConfetti = React.useCallback( 89 | ( 90 | createConfettiArgs: CreateConfettiArgs, 91 | numConfetti: number, 92 | createConfettiRequestedOptions?: CreateConfettiRequestedOptions 93 | ) => { 94 | const createdConfetti: Confetti[] = []; 95 | 96 | for (let i = 0; i < numConfetti; i++) { 97 | const confetti = createConfetti( 98 | createConfettiArgs, 99 | createConfettiRequestedOptions 100 | ); 101 | if (confetti) { 102 | createdConfetti.push(confetti); 103 | } 104 | } 105 | 106 | return createdConfetti; 107 | }, 108 | [createConfetti] 109 | ); 110 | 111 | const addConfetti = React.useCallback( 112 | (confetti: Confetti) => { 113 | const spriteCanvasRef = spriteCanvas?.getCanvas(); 114 | if (spriteCanvasRef != null) { 115 | confettiCanvas?.addConfetti(confetti, spriteCanvasRef); 116 | } 117 | }, 118 | [confettiCanvas, spriteCanvas] 119 | ); 120 | 121 | const deleteConfetti = React.useCallback( 122 | (id: string) => { 123 | confettiCanvas?.deleteConfetti(id); 124 | }, 125 | [confettiCanvas] 126 | ); 127 | 128 | const clearConfetti = React.useCallback( 129 | () => confettiCanvas?.clearConfetti(), 130 | [confettiCanvas] 131 | ); 132 | 133 | return React.useMemo( 134 | () => ({ 135 | createConfetti, 136 | createMultipleConfetti, 137 | addConfetti, 138 | clearConfetti, 139 | deleteConfetti, 140 | isReady: 141 | spriteCanvas != null && 142 | confettiCanvas != null && 143 | isConfettiCanvasReady && 144 | isSpriteCanvasReady, 145 | }), 146 | [ 147 | addConfetti, 148 | clearConfetti, 149 | confettiCanvas, 150 | createConfetti, 151 | createMultipleConfetti, 152 | deleteConfetti, 153 | isConfettiCanvasReady, 154 | isSpriteCanvasReady, 155 | spriteCanvas, 156 | ] 157 | ); 158 | } 159 | -------------------------------------------------------------------------------- /src/components/SpriteCanvas.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { SPRITE_SPACING } from "../ConstantsInternal"; 3 | import { SpriteProp } from "../Types"; 4 | import { hexToRgb } from "../Utils"; 5 | import useReady from "../useReady"; 6 | 7 | const CANVAS_HIDDEN_STYLES: React.CSSProperties = { 8 | display: "none", 9 | position: "absolute", 10 | width: 0, 11 | height: 0, 12 | left: "-100%", 13 | }; 14 | 15 | export interface Sprite { 16 | image: HTMLImageElement; 17 | colorize: boolean; 18 | src: string; 19 | } 20 | 21 | export interface SpriteCanvasProps { 22 | className?: string; 23 | visible?: boolean; 24 | sprites: SpriteProp[]; 25 | colors: string[]; 26 | spriteWidth: number; 27 | spriteHeight: number; 28 | } 29 | 30 | export interface SpriteCanvasData { 31 | sprites: Sprite[]; 32 | colors: string[]; 33 | spriteWidth: number; 34 | spriteHeight: number; 35 | } 36 | 37 | export interface SpriteCanvasHandle { 38 | getCanvas: () => HTMLCanvasElement | null; 39 | getCreateData: () => SpriteCanvasData; 40 | addReadyListener: (listener: (isReady: boolean) => void) => string; 41 | removeReadyListener: (listenerId: string) => void; 42 | isReady: boolean; 43 | } 44 | 45 | const SpriteCanvas: React.ForwardRefRenderFunction< 46 | SpriteCanvasHandle, 47 | SpriteCanvasProps 48 | > = ( 49 | { 50 | className, 51 | visible = false, 52 | sprites: spriteProps, 53 | colors, 54 | spriteWidth, 55 | spriteHeight, 56 | }, 57 | forwardedRef 58 | ) => { 59 | const canvas = React.useRef(null); 60 | const sprites = React.useRef([]); 61 | const { isReady, addReadyListener, removeReadyListener, setIsReady } = 62 | useReady(); 63 | 64 | React.useImperativeHandle( 65 | forwardedRef, 66 | () => { 67 | return { 68 | getCanvas: () => canvas.current, 69 | getCreateData: () => ({ 70 | sprites: sprites.current, 71 | colors, 72 | spriteWidth, 73 | spriteHeight, 74 | }), 75 | addReadyListener, 76 | removeReadyListener, 77 | isReady, 78 | }; 79 | }, 80 | [ 81 | addReadyListener, 82 | colors, 83 | isReady, 84 | removeReadyListener, 85 | spriteHeight, 86 | spriteWidth, 87 | ] 88 | ); 89 | 90 | const drawSprites = React.useCallback(() => { 91 | const canvasRef = canvas.current; 92 | const context = canvasRef?.getContext("2d", { 93 | willReadFrequently: true, 94 | }); 95 | if (context == null || canvasRef == null) { 96 | return; 97 | } 98 | context.clearRect(0, 0, canvasRef.width, canvasRef.height); 99 | 100 | sprites.current.forEach((sprite, spriteIndex) => { 101 | const drawSprite = (color: string | null, colorIndex: number) => { 102 | const x = spriteWidth * colorIndex + SPRITE_SPACING * colorIndex; 103 | const y = spriteHeight * spriteIndex + SPRITE_SPACING * spriteIndex; 104 | 105 | context.drawImage(sprite.image, x, y, spriteWidth, spriteHeight); 106 | 107 | if (color != null) { 108 | const imageData = context.getImageData( 109 | x, 110 | y, 111 | spriteWidth, 112 | spriteHeight 113 | ); 114 | const rgb = hexToRgb(color); 115 | 116 | for (let i = 0; i < imageData.data.length; i += 4) { 117 | imageData.data[i] = rgb.r; 118 | imageData.data[i + 1] = rgb.g; 119 | imageData.data[i + 2] = rgb.b; 120 | } 121 | 122 | context.putImageData(imageData, x, y); 123 | } 124 | }; 125 | 126 | if (sprite.colorize) { 127 | colors.forEach((color, colorIndex) => drawSprite(color, colorIndex)); 128 | } else { 129 | drawSprite(null, 0); 130 | } 131 | }); 132 | }, [colors, spriteHeight, spriteWidth]); 133 | 134 | const createSprites = React.useCallback(() => { 135 | const loadingSprites = spriteProps.map((sprite) => { 136 | const image = new Image(); 137 | const src = typeof sprite === "string" ? sprite : sprite.src; 138 | const colorize = typeof sprite === "string" ? true : sprite.colorize; 139 | 140 | image.src = src; 141 | const loadPromise = new Promise((resolve) => { 142 | image.onload = resolve; 143 | }); 144 | return { colorize, image, src, loadPromise }; 145 | }); 146 | 147 | return Promise.all(loadingSprites.map((sprite) => sprite.loadPromise)).then( 148 | () => { 149 | sprites.current = loadingSprites.map((sprite) => ({ 150 | colorize: sprite.colorize, 151 | image: sprite.image, 152 | src: sprite.src, 153 | })); 154 | } 155 | ); 156 | }, [spriteProps]); 157 | 158 | const getCanvasReady = React.useCallback(async () => { 159 | await createSprites(); 160 | drawSprites(); 161 | setIsReady(true); 162 | }, [createSprites, drawSprites, setIsReady]); 163 | 164 | React.useEffect(() => { 165 | getCanvasReady(); 166 | }, [getCanvasReady]); 167 | 168 | React.useEffect(() => { 169 | if (canvas.current != null) { 170 | canvas.current.width = 171 | (spriteWidth + SPRITE_SPACING) * Math.max(colors.length, 1); 172 | canvas.current.height = 173 | (spriteHeight + SPRITE_SPACING) * spriteProps.length; 174 | } 175 | }, [colors.length, spriteHeight, spriteWidth, spriteProps.length]); 176 | 177 | return ( 178 | 183 | ); 184 | }; 185 | 186 | export default React.forwardRef(SpriteCanvas); 187 | -------------------------------------------------------------------------------- /src/Confetti.ts: -------------------------------------------------------------------------------- 1 | import Environment from "./Environment"; 2 | import { Vector2 } from "./Types"; 3 | import { 4 | UpdatableValue, 5 | UpdatableVector2Value, 6 | UpdatableVector3Value, 7 | } from "./UpdatableValue"; 8 | import { calculateAirResistance } from "./Utils"; 9 | 10 | type ConfettiArgs = { 11 | id: string; 12 | 13 | position: UpdatableVector2Value; 14 | velocity: UpdatableVector2Value; 15 | rotation: UpdatableVector3Value; 16 | size: UpdatableVector2Value; 17 | dragCoefficient: UpdatableVector2Value; 18 | opacity: UpdatableValue; 19 | airResistanceArea: UpdatableVector2Value; 20 | 21 | spriteX: number; 22 | spriteY: number; 23 | spriteWidth: number; 24 | spriteHeight: number; 25 | }; 26 | 27 | export default class Confetti { 28 | id: string; 29 | 30 | position: UpdatableVector2Value; 31 | velocity: UpdatableVector2Value; 32 | rotation: UpdatableVector3Value; 33 | size: UpdatableVector2Value; 34 | dragCoefficient: UpdatableVector2Value; 35 | opacity: UpdatableValue; 36 | airResistanceArea: UpdatableVector2Value; 37 | 38 | spriteX: number; 39 | spriteY: number; 40 | spriteWidth: number; 41 | spriteHeight: number; 42 | 43 | _lastUpdatedAt: number; 44 | 45 | constructor(args: ConfettiArgs) { 46 | this.id = args.id; 47 | 48 | this.position = args.position; 49 | this.velocity = args.velocity; 50 | this.rotation = args.rotation; 51 | this.dragCoefficient = args.dragCoefficient; 52 | this.airResistanceArea = args.airResistanceArea; 53 | 54 | this.size = args.size; 55 | 56 | this.opacity = args.opacity; 57 | 58 | this.spriteX = args.spriteX; 59 | this.spriteY = args.spriteY; 60 | this.spriteWidth = args.spriteWidth; 61 | this.spriteHeight = args.spriteHeight; 62 | 63 | this._lastUpdatedAt = Date.now(); 64 | } 65 | 66 | getNewForces(environment: Environment, deltaTime: number) { 67 | const windForce = environment.wind * deltaTime; 68 | const gravityForce = -environment.gravity * deltaTime; 69 | 70 | const airResistanceForceX = calculateAirResistance( 71 | this.dragCoefficient.x, 72 | this.velocity.x, 73 | this.airResistanceArea.x, 74 | environment.density 75 | ); 76 | 77 | const airResistanceForceY = calculateAirResistance( 78 | this.dragCoefficient.y, 79 | this.velocity.y, 80 | this.airResistanceArea.y, 81 | environment.density 82 | ); 83 | 84 | return { 85 | x: windForce + airResistanceForceX, 86 | y: gravityForce + airResistanceForceY, 87 | }; 88 | } 89 | 90 | update(environment: Environment) { 91 | const newUpdateTime = Date.now(); 92 | const deltaTime = (newUpdateTime - this._lastUpdatedAt) / 100; 93 | 94 | this.rotation.update(deltaTime); 95 | 96 | this.dragCoefficient.update(deltaTime); 97 | 98 | const { x: forceX, y: forceY } = this.getNewForces(environment, deltaTime); 99 | 100 | this.velocity.update(deltaTime); 101 | this.velocity.x += forceX; 102 | this.velocity.y += forceY; 103 | 104 | this.position.update(deltaTime); 105 | this.position.x += this.velocity.x * deltaTime; 106 | this.position.y += this.velocity.y * deltaTime; 107 | 108 | this.size.update(deltaTime); 109 | 110 | this.opacity.update(deltaTime); 111 | this.opacity.value = Math.max(this.opacity.value, 0); 112 | 113 | this._lastUpdatedAt = newUpdateTime; 114 | } 115 | 116 | previewPositionUpdate(environment: Environment, deltaTimeMS: number) { 117 | const deltaTime = deltaTimeMS / 100; 118 | const velocity = this.velocity.previewUpdate(deltaTime); 119 | const { x: forceX, y: forceY } = this.getNewForces(environment, deltaTime); 120 | velocity.x += forceX; 121 | velocity.y += forceY; 122 | 123 | const position = this.position.previewUpdate(deltaTime); 124 | position.x += velocity.x * deltaTime; 125 | position.y += velocity.y * deltaTime; 126 | 127 | return position; 128 | } 129 | 130 | draw(spriteCanvas: HTMLCanvasElement, context: CanvasRenderingContext2D) { 131 | context.save(); 132 | 133 | context.globalAlpha = this.opacity.value; 134 | 135 | context.setTransform( 136 | new DOMMatrix() 137 | .translateSelf( 138 | this.position.x * global.devicePixelRatio, 139 | this.position.y * global.devicePixelRatio 140 | ) 141 | .rotateSelf(this.rotation.x, this.rotation.y, this.rotation.z) 142 | ); 143 | 144 | context.drawImage( 145 | spriteCanvas, 146 | this.spriteX, 147 | this.spriteY, 148 | this.spriteWidth, 149 | this.spriteHeight, 150 | (-this.width / 2) * global.devicePixelRatio, 151 | (-this.height / 2) * global.devicePixelRatio, 152 | this.width * global.devicePixelRatio, 153 | this.height * global.devicePixelRatio 154 | ); 155 | 156 | context.restore(); 157 | } 158 | 159 | shouldDestroy(canvas: HTMLCanvasElement, environment: Environment) { 160 | return ( 161 | // opacity 162 | this.opacity.value < 0 || 163 | // top 164 | (environment.gravity >= 0 && 165 | this.velocity.y < 0 && 166 | this.position.y + this.height < 0) || 167 | // bottom 168 | (environment.gravity <= 0 && 169 | this.velocity.y > 0 && 170 | this.position.y - this.height > canvas.height) || 171 | // left 172 | (environment.wind >= 0 && 173 | this.velocity.x > 0 && 174 | this.position.x - this.width > canvas.width) || 175 | // right 176 | (environment.wind <= 0 && 177 | this.velocity.x < 0 && 178 | this.position.x + this.width < 0) 179 | ); 180 | } 181 | 182 | get width() { 183 | return this.size.x; 184 | } 185 | 186 | get height() { 187 | return this.size.y; 188 | } 189 | 190 | addForce(force: Vector2) { 191 | this.velocity.x += force.x; 192 | this.velocity.y += force.y; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/stories/examples/MultipleCannons.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import classNames from "classnames"; 3 | import * as React from "react"; 4 | import { v4 as uuid } from "uuid"; 5 | import { 6 | Confetti, 7 | ConfettiCanvas, 8 | ConfettiCanvasHandle, 9 | CreateConfettiArgs, 10 | Environment, 11 | SpriteCanvas, 12 | SpriteCanvasHandle, 13 | easeInOutQuad, 14 | getUpdatableValueVector2, 15 | getUpdatableValueVector3, 16 | useConfettiCannon, 17 | } from "../../"; 18 | import { getClickPosition } from "../../Utils"; 19 | import styles from "../Stories.module.css"; 20 | 21 | const FALLING_CHARACTER_SPRITE = { 22 | src: require("../images/duck.svg"), 23 | colorize: false, 24 | }; 25 | const FALLING_CHARACTER_SPRITES = [FALLING_CHARACTER_SPRITE]; 26 | const FALLING_CHARACTER_COLORS: string[] = []; 27 | 28 | const SPRITES = [ 29 | require("../images/square.svg"), 30 | require("../images/circle.svg"), 31 | ]; 32 | 33 | const COLORS = [ 34 | "#FF73FA", 35 | "#FFC0FF", 36 | "#FFD836", 37 | "#FF9A15", 38 | "#A5F7DE", 39 | "#51BC9D", 40 | "#AEC7FF", 41 | "#3E70DD", 42 | ]; 43 | 44 | const FALLING_CHARACTER_SIZE = 80; 45 | const MAX_CONFETTI_SIZE = 40; 46 | 47 | const FALLING_CHARACTER_ID_PREFIX = "FALLING_CHARACTER"; 48 | const FALLING_CHARACTER_CONFETTI_CONFIG: Partial & 49 | Pick = { 50 | velocity: { 51 | type: "static-random", 52 | minValue: { x: -5, y: 0 }, 53 | maxValue: { x: -2, y: 0 }, 54 | }, 55 | rotation: { 56 | type: "oscillating-random", 57 | minValue: { x: 0, y: 0, z: -20 }, 58 | maxValue: { x: 0, y: 0, z: 20 }, 59 | minStart: { x: 0, y: 0, z: -20 }, 60 | maxStart: { x: 0, y: 0, z: -10 }, 61 | minFinal: { x: 0, y: 0, z: 10 }, 62 | maxFinal: { x: 0, y: 0, z: 20 }, 63 | minDuration: { x: 0, y: 0, z: 5 }, 64 | maxDuration: { x: 0, y: 0, z: 8 }, 65 | minDirection: { x: 1, y: 1, z: -1 }, 66 | maxDirection: { x: 1, y: 1, z: 1 }, 67 | easingFunctions: [easeInOutQuad], 68 | }, 69 | size: { 70 | type: "static", 71 | value: FALLING_CHARACTER_SIZE, 72 | }, 73 | dragCoefficient: { 74 | type: "static", 75 | value: { x: 0.001, y: 0.05 }, 76 | }, 77 | }; 78 | 79 | const CONFETTI_CONFETTI_CONFIG: Partial & 80 | Pick = { 81 | size: { 82 | type: "static-random", 83 | minValue: 20, 84 | maxValue: MAX_CONFETTI_SIZE, 85 | }, 86 | velocity: { 87 | type: "static-random", 88 | minValue: { x: -25, y: -75 }, 89 | maxValue: { x: 25, y: -50 }, 90 | }, 91 | rotation: { 92 | type: "linear-random", 93 | minValue: 0, 94 | maxValue: 360, 95 | minAddValue: 5, 96 | maxAddValue: 10, 97 | }, 98 | }; 99 | 100 | function MultipleCannonsStory() { 101 | const [confettiCanvas, setConfettiCanvas] = 102 | React.useState(null); 103 | 104 | const [confettiSpriteCanvas, setConfettiSpriteCanvas] = 105 | React.useState(null); 106 | const [fallingCharacterSpriteCanvas, setFallingCharacterSpriteCanvas] = 107 | React.useState(null); 108 | 109 | const environment = React.useMemo(() => new Environment({ wind: -5 }), []); 110 | 111 | const fallingCharacterCannon = useConfettiCannon( 112 | confettiCanvas, 113 | fallingCharacterSpriteCanvas 114 | ); 115 | const confettiCannon = useConfettiCannon( 116 | confettiCanvas, 117 | confettiSpriteCanvas 118 | ); 119 | 120 | const addConfetti = React.useCallback( 121 | (x: number, y: number) => { 122 | const createConfettiArgs: CreateConfettiArgs = { 123 | ...CONFETTI_CONFETTI_CONFIG, 124 | position: { 125 | type: "static-random", 126 | minValue: { x: x - 5, y: y - 5 }, 127 | maxValue: { x: x + 5, y: y + 5 }, 128 | }, 129 | }; 130 | confettiCannon.createMultipleConfetti(createConfettiArgs, 5); 131 | }, 132 | [confettiCannon] 133 | ); 134 | 135 | const addFallingCharacter = React.useCallback( 136 | (x: number, y: number) => { 137 | const createConfettiArgs: CreateConfettiArgs = { 138 | id: `${FALLING_CHARACTER_ID_PREFIX}-${uuid()}`, 139 | ...FALLING_CHARACTER_CONFETTI_CONFIG, 140 | position: { 141 | type: "static", 142 | value: { x, y }, 143 | }, 144 | }; 145 | 146 | return fallingCharacterCannon.createConfetti(createConfettiArgs); 147 | }, 148 | [fallingCharacterCannon] 149 | ); 150 | 151 | const handleClickFallingCharacter = React.useCallback( 152 | (confetti: Confetti) => { 153 | const prevRotation = confetti.rotation.z; 154 | const futureRotation = confetti.rotation.previewUpdate(0.1).z; 155 | const direction = prevRotation - futureRotation > 0 ? -1 : 1; 156 | confetti.rotation = getUpdatableValueVector3({ 157 | type: "linear-random", 158 | minValue: confetti.rotation, 159 | maxValue: confetti.rotation, 160 | minAddValue: { x: 0, y: 0, z: 5 * direction }, 161 | maxAddValue: { x: 0, y: 0, z: 10 * direction }, 162 | }); 163 | confetti.dragCoefficient = getUpdatableValueVector2({ 164 | type: "static", 165 | value: 0.001, 166 | }); 167 | 168 | confetti.addForce({ x: 0, y: -100 }); 169 | addConfetti( 170 | confetti.position.x + confetti.width / 2, 171 | confetti.position.y + confetti.height / 2 172 | ); 173 | }, 174 | [addConfetti] 175 | ); 176 | 177 | const handleClick = (e: MouseEvent, confetti: Confetti | null) => { 178 | if ( 179 | confetti != null && 180 | confetti.id.startsWith(FALLING_CHARACTER_ID_PREFIX) 181 | ) { 182 | return handleClickFallingCharacter(confetti); 183 | } 184 | const { x, y } = getClickPosition(e, confettiCanvas?.getCanvas()); 185 | addFallingCharacter(x, y); 186 | }; 187 | 188 | return ( 189 | <> 190 | 197 | 204 | 210 | 211 | ); 212 | } 213 | 214 | const meta = { 215 | title: "Examples/MultipleCannons", 216 | component: MultipleCannonsStory, 217 | } satisfies Meta; 218 | 219 | export default meta; 220 | type Story = StoryObj; 221 | 222 | export const Example: Story = {}; 223 | -------------------------------------------------------------------------------- /src/stories/Playground.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import classNames from "classnames"; 3 | import * as React from "react"; 4 | import { 5 | ConfettiCanvas, 6 | ConfettiCanvasHandle, 7 | CreateConfettiArgs, 8 | Environment, 9 | SpriteCanvas, 10 | SpriteCanvasHandle, 11 | SpriteProp, 12 | useConfettiCannon, 13 | } from "../"; 14 | import { getClickPosition } from "../Utils"; 15 | import styles from "./Stories.module.css"; 16 | import ConfettiCanvasStory from "./components/ConfettiCanvas.stories"; 17 | import SpriteCanvasStory from "./components/SpriteCanvas.stories"; 18 | 19 | interface PlaygroundStoryProps { 20 | autoFire: boolean; 21 | numberToFire: number; 22 | showSpriteCanvas: boolean; 23 | gravity: number; 24 | wind: number; 25 | density: number; 26 | positionSpreadX: number; 27 | positionSpreadY: number; 28 | minVelocityX: number; 29 | maxVelocityX: number; 30 | minVelocityY: number; 31 | maxVelocityY: number; 32 | minRotationX: number; 33 | maxRotationX: number; 34 | minRotationY: number; 35 | maxRotationY: number; 36 | minRotationZ: number; 37 | maxRotationZ: number; 38 | minRotationAddValueX: number; 39 | maxRotationAddValueX: number; 40 | minRotationAddValueY: number; 41 | maxRotationAddValueY: number; 42 | minRotationAddValueZ: number; 43 | maxRotationAddValueZ: number; 44 | dragCoefficientX: number; 45 | dragCoefficientY: number; 46 | airResistanceAreaX: number; 47 | airResistanceAreaY: number; 48 | opacity: number; 49 | opacityAddValue: number; 50 | minSize: number; 51 | maxSize: number; 52 | sprites: SpriteProp[]; 53 | colors: string[]; 54 | } 55 | 56 | function PlaygroundStory({ 57 | autoFire, 58 | numberToFire, 59 | showSpriteCanvas, 60 | gravity, 61 | wind, 62 | density, 63 | positionSpreadX, 64 | positionSpreadY, 65 | minVelocityX, 66 | maxVelocityX, 67 | minVelocityY, 68 | maxVelocityY, 69 | minRotationX, 70 | maxRotationX, 71 | minRotationY, 72 | maxRotationY, 73 | minRotationZ, 74 | maxRotationZ, 75 | minRotationAddValueX, 76 | maxRotationAddValueX, 77 | minRotationAddValueY, 78 | maxRotationAddValueY, 79 | minRotationAddValueZ, 80 | maxRotationAddValueZ, 81 | dragCoefficientX, 82 | dragCoefficientY, 83 | airResistanceAreaX, 84 | airResistanceAreaY, 85 | opacity, 86 | opacityAddValue, 87 | minSize, 88 | maxSize, 89 | sprites, 90 | colors, 91 | }: PlaygroundStoryProps) { 92 | const [confettiCanvas, setConfettiCanvas] = 93 | React.useState(null); 94 | const [spriteCanvas, setSpriteCanvas] = 95 | React.useState(null); 96 | const environment = React.useMemo( 97 | () => new Environment({ gravity, wind, density }), 98 | [gravity, wind, density] 99 | ); 100 | const cannon = useConfettiCannon(confettiCanvas, spriteCanvas); 101 | 102 | const addConfetti = React.useCallback( 103 | (x: number, y: number) => { 104 | const createConfettiArgs: CreateConfettiArgs = { 105 | position: { 106 | type: "static-random", 107 | minValue: { x: x - positionSpreadX, y: y - positionSpreadY }, 108 | maxValue: { x: x + positionSpreadX, y: y + positionSpreadY }, 109 | }, 110 | velocity: { 111 | type: "static-random", 112 | minValue: { x: minVelocityX, y: minVelocityY }, 113 | maxValue: { x: maxVelocityX, y: maxVelocityY }, 114 | }, 115 | rotation: { 116 | type: "linear-random", 117 | minValue: { x: minRotationX, y: minRotationY, z: minRotationZ }, 118 | maxValue: { x: maxRotationX, y: maxRotationY, z: maxRotationZ }, 119 | minAddValue: { 120 | x: minRotationAddValueX, 121 | y: minRotationAddValueY, 122 | z: minRotationAddValueZ, 123 | }, 124 | maxAddValue: { 125 | x: maxRotationAddValueX, 126 | y: maxRotationAddValueY, 127 | z: maxRotationAddValueZ, 128 | }, 129 | }, 130 | dragCoefficient: { 131 | type: "static", 132 | value: { x: dragCoefficientX, y: dragCoefficientY }, 133 | }, 134 | airResistanceArea: { 135 | type: "static", 136 | value: { x: airResistanceAreaX, y: airResistanceAreaY }, 137 | }, 138 | opacity: { 139 | type: "linear", 140 | value: opacity, 141 | addValue: opacityAddValue, 142 | }, 143 | size: { 144 | type: "static-random", 145 | minValue: minSize, 146 | maxValue: maxSize, 147 | }, 148 | }; 149 | 150 | cannon.createMultipleConfetti(createConfettiArgs, numberToFire); 151 | }, 152 | [ 153 | airResistanceAreaX, 154 | airResistanceAreaY, 155 | cannon, 156 | dragCoefficientX, 157 | dragCoefficientY, 158 | maxRotationAddValueX, 159 | maxRotationAddValueY, 160 | maxRotationAddValueZ, 161 | maxRotationX, 162 | maxRotationY, 163 | maxRotationZ, 164 | maxSize, 165 | maxVelocityX, 166 | maxVelocityY, 167 | minRotationAddValueX, 168 | minRotationAddValueY, 169 | minRotationAddValueZ, 170 | minRotationX, 171 | minRotationY, 172 | minRotationZ, 173 | minSize, 174 | minVelocityX, 175 | minVelocityY, 176 | numberToFire, 177 | opacity, 178 | opacityAddValue, 179 | positionSpreadX, 180 | positionSpreadY, 181 | ] 182 | ); 183 | 184 | const handleClick = (e: MouseEvent) => { 185 | const { x, y } = getClickPosition(e, confettiCanvas?.getCanvas()); 186 | addConfetti(x, y); 187 | }; 188 | 189 | React.useEffect(() => { 190 | let interval: NodeJS.Timer; 191 | if (autoFire) { 192 | interval = setInterval(() => addConfetti(100, 100), 500); 193 | } 194 | return () => clearInterval(interval); 195 | }, [addConfetti, autoFire]); 196 | 197 | return ( 198 | <> 199 | 208 | 214 | 215 | ); 216 | } 217 | 218 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction 219 | const meta = { 220 | title: "Playground", 221 | component: PlaygroundStory, 222 | args: { 223 | autoFire: false, 224 | numberToFire: 5, 225 | showSpriteCanvas: false, 226 | gravity: ConfettiCanvasStory.args.gravity, 227 | wind: ConfettiCanvasStory.args.wind, 228 | density: ConfettiCanvasStory.args.density, 229 | positionSpreadX: 25, 230 | positionSpreadY: 25, 231 | minVelocityX: -20, 232 | maxVelocityX: 20, 233 | minVelocityY: -50, 234 | maxVelocityY: -75, 235 | minRotationX: 0, 236 | maxRotationX: 360, 237 | minRotationY: 0, 238 | maxRotationY: 360, 239 | minRotationZ: 0, 240 | maxRotationZ: 360, 241 | minRotationAddValueX: -25, 242 | maxRotationAddValueX: 25, 243 | minRotationAddValueY: -25, 244 | maxRotationAddValueY: 25, 245 | minRotationAddValueZ: -25, 246 | maxRotationAddValueZ: 25, 247 | dragCoefficientX: 1.66, 248 | dragCoefficientY: 1.66, 249 | airResistanceAreaX: 0.001, 250 | airResistanceAreaY: 0.001, 251 | opacity: 1, 252 | opacityAddValue: -0.01, 253 | minSize: 20, 254 | maxSize: 40, 255 | sprites: SpriteCanvasStory.args.sprites, 256 | colors: SpriteCanvasStory.args.colors, 257 | }, 258 | } satisfies Meta; 259 | 260 | export default meta; 261 | type Story = StoryObj; 262 | 263 | export const Example: Story = {}; 264 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Confetti Cannon 2 | 3 | Launch Confetti 4 | 5 | ![Example](https://github.com/discord/confetti-cannon/blob/main/example.gif) 6 | 7 | # Install 8 | `npm i confetti-cannon` 9 | 10 | # How to use 11 | 12 | This is the basic use of the cannon where we render everything we need and then create confetti on the canvas where the user clicks. For more advanced uses, check out the stories. 13 | 14 | ```tsx 15 | import { 16 | ConfettiCanvas, 17 | Environment, 18 | SpriteCanvas, 19 | useConfettiCannon, 20 | ConfettiCanvasHandle, 21 | SpriteCanvasHandle, 22 | } from "confetti-cannon"; 23 | 24 | const SPRITES = [ 25 | require("./images/square.svg"), 26 | require("./images/circle.svg"), 27 | ]; 28 | const COLORS = ["#FF73FA", "#FFC0FF"]; 29 | const SIZE = 40; 30 | 31 | function Example() { 32 | const [confettiCanvas, setConfettiCanvas] = 33 | React.useState(null); 34 | const [spriteCanvas, setSpriteCanvas] = 35 | React.useState(null); 36 | const environment = React.useMemo(() => new Environment(), []); 37 | const cannon = useConfettiCannon(confettiCanvas, spriteCanvas); 38 | 39 | const addConfetti = React.useCallback( 40 | (x: number, y: number) => { 41 | cannon.createConfetti({ 42 | position: { 43 | type: "static", 44 | value: { x, y }, 45 | }, 46 | size: { 47 | type: "static", 48 | value: SIZE, 49 | }, 50 | }); 51 | }, 52 | [cannon] 53 | ); 54 | 55 | const handleClick = (e: MouseEvent) => { 56 | const { x, y } = getClickPosition(e, confettiCanvas?.getCanvas()); 57 | addConfetti(x, y); 58 | }; 59 | 60 | return ( 61 | <> 62 | 69 | 74 | 75 | ); 76 | } 77 | ``` 78 | 79 | # Components 80 | 81 | ## SpriteCanvas 82 | 83 | A `SpriteCanvas` is used to pre-render your confetti. You'll need to render this somewhere in your app in order to launch confetti on an `ConfettiCanvas`. This will not be visible to users. 84 | 85 | | Prop | Type | Description | 86 | | -------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | 87 | | `spriteWidth` | `number` | The width to render the confetti at. Should be the largest width your confetti will be rendered at if possible | 88 | | `spriteHeight` | `number` | The height to render the confetti at. Should be the largest width your confetti will be rendered at if possible | 89 | | `colors` | `string[]` | The colors your confetti will be rendered as | 90 | | `sprites` | `Array` | The sources of your confetti images. If you do not want to color an image, use `{src, colorize: false}` | 91 | | `visible` | `boolean` | Used for debugging if you'd like to see the `SpriteCanvas` on screen | 92 | 93 | ## `ConfettiCanvas` 94 | 95 | A `ConfettiCanvas` is the canvas that will render your confetti on screen 96 | 97 | | Prop | Type | Description | 98 | | ------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | 99 | | `environment` | `Environment` | An object representing the environment effecting the confetti | 100 | | `onClick` | `(e: MouseEvent, confetti: Confetti \| null) => void` | Event fired when the user clicks the canvas, if they clicked a confetti, that confetti is included | 101 | | `onMouseDown` | `(e: MouseEvent, confetti: Confetti \| null) => void` | Event fired when the user mouses down on the canvas, if they moused down on a confetti, that confetti is included | 102 | 103 | to create an `Environment`, use `new Environment()`. 104 | 105 | | Arg | Type | Default | Description | 106 | | --------- | -------- | ------- | ------------------------------------------- | 107 | | `gravity` | `number` | `-9.8` | How confetti will be effected on the y axis | 108 | | `wind` | `number` | `0` | How confetti will be effected on the x axis | 109 | 110 | ## useConfettiCannon 111 | 112 | `useConfettiCannon` is the hook that will allow you to launch confetti. This takes a `ConfettiCanvas` and a `SpriteCanvas` and provides a few functions to create confetti. 113 | 114 | ### Cannon methods 115 | 116 | There's several methods available to add and manage confetti on the canvas. Typically, you'll want to use `createMultipleConfetti`. 117 | 118 | | Method | Description | 119 | | ------------------------ | --------------------------------------------------- | 120 | | `createConfetti` | Create a single confetti and add it to the canvas | 121 | | `createMultipleConfetti` | Create multiple confetti and add them to the canvas | 122 | | `addConfetti` | Add a confetti on the canvas | 123 | | `deleteConfetti` | Delete a confetti from the canvas | 124 | | `clearConfetti` | Delete all confetti from the canvas | 125 | 126 | ### CreateConfettiArgs 127 | 128 | These are passed to `createConfetti` methods to create a confetti and define how it is updated. Each includes a `type`, which then defines the rest of the args. 129 | 130 | | Arg | Type | Default | Description | 131 | | ----------------- | --------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------- | 132 | | `id` | `?string` | A uuid | A unique id to a confetti if you want to reference it later, will be a uuid if not specified | 133 | | `position` | `ConfigVector2Input` | N/A | The position to launch the confetti at and how it will be updated | 134 | | `velocity` | `?ConfigVector2Input` | static, 0 | The velocity to launch the confetti at and how it will be updated | 135 | | `rotation` | `?ConfigVector3Input` | static, 0 | The rotation to launch the confetti at and how it will be updated | 136 | | `dragCoefficient` | `?ConfigVector2Input` | static, 0.001 | The drag coefficient to launch the confetti at and how it will be updated. This effects how much gravity and wind effect the confetti | 137 | | `size` | `ConfigVector2Input` | N/A | The size to launch the confetti at and how it will be updated | 138 | | `opacity` | `?ConfigNumberInput` | static 1 | The opacity to launch the confetti at and how it will be updated | 139 | 140 | **Config Inputs** 141 | Config inputs are helper objects that will eventually create an `UpdatableValue` that lives on `Confetti` that tells the `Confetti` how to update on every tick. 142 | 143 | Valid types include: 144 | 145 | - `static`: Will not change on updates (gravity and wind will still effect relevant fields) 146 | - `linear`: Will update linearly on every update 147 | - `oscillating`: Will oscillate between two values with a given easing 148 | 149 | Each type also includes a `-random` option (ex: `static-random`) which allows you to add randomization with the initial value and the update values. 150 | 151 | Any `Vector2` or `Vector3` will also accept a `number` as a shortcut to set all `x`, `y`, and `z` values to that number. 152 | 153 | ### CreateConfettiRequestedOptions 154 | 155 | This is an optional object that will request that the canvas create a specific sprite or color. The sprite and color must be included on your sprite canvas for this to work. 156 | 157 | | Arg | Type | Description | 158 | | -------- | ----------------------------------------------------- | -------------------- | 159 | | `sprite` | `?Array` | The requested sprite | 160 | | `color` | `?string` | The requested color | 161 | -------------------------------------------------------------------------------- /src/components/ConfettiCanvas.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { v4 as uuid } from "uuid"; 3 | import Confetti from "../Confetti"; 4 | import Environment from "../Environment"; 5 | import { SpriteProp } from "../Types"; 6 | import { getClickPosition, isInRect, mapFind, setCanvasSize } from "../Utils"; 7 | import createConfettiHelper, { CreateConfettiArgs } from "../createConfetti"; 8 | import useReady from "../useReady"; 9 | import { SpriteCanvasData } from "./SpriteCanvas"; 10 | 11 | type ClickListener = (e: MouseEvent, confetti: Confetti | null) => void; 12 | type MouseListener = (e: MouseEvent) => void; 13 | 14 | interface ConfettiCanvasProps 15 | extends Omit< 16 | React.HTMLAttributes, 17 | "onClick" | "onMouseDown" | "onMouseMove" | "onMouseUp" 18 | > { 19 | className?: string; 20 | environment: Environment; 21 | onClick?: ClickListener; 22 | onMouseDown?: ClickListener; 23 | onMouseMove?: MouseListener; 24 | onMouseUp?: MouseListener; 25 | requestAnimationFrame?: (handler: FrameRequestCallback) => number; 26 | cancelAnimationFrame?: (id: number) => void; 27 | onBeforeRender?: (context: CanvasRenderingContext2D) => void; 28 | onAfterRender?: (context: CanvasRenderingContext2D) => void; 29 | } 30 | 31 | export interface ConfettiCanvasHandle { 32 | createConfetti: ( 33 | args: CreateConfettiArgs, 34 | spriteCanvas: HTMLCanvasElement, 35 | SpriteCanvasData: SpriteCanvasData, 36 | sprite?: SpriteProp, 37 | color?: string | null 38 | ) => Confetti; 39 | addConfetti: (confetti: Confetti, spriteCanvas: HTMLCanvasElement) => void; 40 | deleteConfetti: (id: string) => void; 41 | clearConfetti: () => void; 42 | getCanvas: () => HTMLCanvasElement | null; 43 | addReadyListener: (listener: (isReady: boolean) => void) => string; 44 | removeReadyListener: (listenerId: string) => void; 45 | isReady: boolean; 46 | } 47 | 48 | const CLICK_BUFFER_FRAME_COUNT = 2; 49 | 50 | const ConfettiCanvas: React.ForwardRefRenderFunction< 51 | ConfettiCanvasHandle, 52 | ConfettiCanvasProps 53 | > = ( 54 | { 55 | className, 56 | environment, 57 | onClick, 58 | onMouseDown, 59 | onMouseMove, 60 | onMouseUp, 61 | onBeforeRender, 62 | onAfterRender, 63 | requestAnimationFrame = window.requestAnimationFrame, 64 | cancelAnimationFrame = window.cancelAnimationFrame, 65 | ...props 66 | }, 67 | forwardedRef 68 | ) => { 69 | const canvas = React.useRef(null); 70 | const { isReady, addReadyListener, removeReadyListener, setIsReady } = 71 | useReady(); 72 | 73 | const allConfetti = React.useRef< 74 | Map 75 | >(new Map()); 76 | 77 | const animationFrameRequestId = React.useRef(null); 78 | const lastFrameUpdatedAt = React.useRef(0); 79 | const frameRate = React.useRef(0); 80 | 81 | const handleTick = React.useCallback(() => { 82 | const canvasRef = canvas.current; 83 | if (canvasRef == null) { 84 | return; 85 | } 86 | 87 | const context = canvasRef.getContext("2d"); 88 | if (context == null) { 89 | return; 90 | } 91 | 92 | context.clearRect(0, 0, canvasRef.width, canvasRef.height); 93 | 94 | onBeforeRender?.(context); 95 | 96 | allConfetti.current.forEach(({ confetti, spriteCanvas }, id) => { 97 | confetti.update(environment); 98 | confetti.draw(spriteCanvas, context); 99 | 100 | if (confetti.shouldDestroy(canvasRef, environment)) { 101 | allConfetti.current.delete(id); 102 | } 103 | }); 104 | 105 | onAfterRender?.(context); 106 | 107 | if (allConfetti.current.size > 0) { 108 | animationFrameRequestId.current = requestAnimationFrame(handleTick); 109 | } else { 110 | context.clearRect(0, 0, canvasRef.width, canvasRef.height); 111 | animationFrameRequestId.current = null; 112 | } 113 | 114 | const now = Date.now(); 115 | if (lastFrameUpdatedAt.current !== 0) { 116 | frameRate.current = 1000 / (now - lastFrameUpdatedAt.current); 117 | } 118 | lastFrameUpdatedAt.current = now; 119 | }, [environment, onAfterRender, onBeforeRender, requestAnimationFrame]); 120 | 121 | React.useEffect(() => { 122 | if (animationFrameRequestId.current != null) { 123 | cancelAnimationFrame(animationFrameRequestId.current); 124 | animationFrameRequestId.current = requestAnimationFrame(handleTick); 125 | } 126 | }, [cancelAnimationFrame, handleTick, requestAnimationFrame]); 127 | 128 | const addConfetti = React.useCallback( 129 | (confetti: Confetti, spriteCanvas: HTMLCanvasElement) => { 130 | allConfetti.current.set(confetti.id, { 131 | confetti, 132 | spriteCanvas, 133 | }); 134 | 135 | if (animationFrameRequestId.current == null) { 136 | handleTick(); 137 | } 138 | }, 139 | [handleTick] 140 | ); 141 | 142 | const createConfetti = React.useCallback( 143 | ( 144 | args: CreateConfettiArgs, 145 | spriteCanvas: HTMLCanvasElement, 146 | SpriteCanvasData: SpriteCanvasData, 147 | sprite?: SpriteProp, 148 | color?: string | null 149 | ) => { 150 | const confetti = createConfettiHelper( 151 | args.id ?? uuid(), 152 | args, 153 | SpriteCanvasData, 154 | sprite, 155 | color 156 | ); 157 | 158 | addConfetti(confetti, spriteCanvas); 159 | 160 | return confetti; 161 | }, 162 | [addConfetti] 163 | ); 164 | 165 | const deleteConfetti = React.useCallback((id: string) => { 166 | allConfetti.current.delete(id); 167 | }, []); 168 | 169 | const clearConfetti = React.useCallback( 170 | () => allConfetti.current.clear(), 171 | [] 172 | ); 173 | 174 | const getCanvas = React.useCallback(() => canvas.current, []); 175 | 176 | React.useImperativeHandle( 177 | forwardedRef, 178 | () => { 179 | return { 180 | createConfetti, 181 | addConfetti, 182 | deleteConfetti, 183 | clearConfetti, 184 | getCanvas, 185 | addReadyListener, 186 | removeReadyListener, 187 | isReady, 188 | }; 189 | }, 190 | [ 191 | createConfetti, 192 | addConfetti, 193 | deleteConfetti, 194 | clearConfetti, 195 | getCanvas, 196 | addReadyListener, 197 | removeReadyListener, 198 | isReady, 199 | ] 200 | ); 201 | 202 | const handleMouseEvent = React.useCallback( 203 | ( 204 | e: MouseEvent, 205 | { 206 | clickHandler, 207 | mouseHandler, 208 | }: { 209 | clickHandler?: ClickListener; 210 | mouseHandler?: MouseListener; 211 | } 212 | ) => { 213 | if (clickHandler == null && mouseHandler == null) { 214 | return; 215 | } 216 | 217 | const canvasRect = canvas.current?.getBoundingClientRect(); 218 | if (canvasRect == null) { 219 | return; 220 | } 221 | 222 | const clickPosition = getClickPosition(e, canvas.current); 223 | if ( 224 | !isInRect(clickPosition, { 225 | x: canvasRect.left, 226 | y: canvasRect.top, 227 | width: canvasRect.width, 228 | height: canvasRect.height, 229 | }) 230 | ) { 231 | return; 232 | } 233 | 234 | if (mouseHandler != null) { 235 | return mouseHandler(e); 236 | } 237 | 238 | if (clickHandler == null) { 239 | return; 240 | } 241 | 242 | const deltaTime = -(1000 / frameRate.current) * CLICK_BUFFER_FRAME_COUNT; 243 | const confetti = mapFind(allConfetti.current, ({ confetti }) => { 244 | const confettiPosition = confetti.previewPositionUpdate( 245 | environment, 246 | deltaTime 247 | ); 248 | 249 | return isInRect(clickPosition, { 250 | x: confettiPosition.x - confetti.width / 2, 251 | y: confettiPosition.y - confetti.height / 2, 252 | width: confetti.width, 253 | height: confetti.height, 254 | }); 255 | }); 256 | clickHandler(e, confetti?.confetti ?? null); 257 | }, 258 | [environment] 259 | ); 260 | 261 | const handleClick: MouseListener = React.useCallback( 262 | (e) => handleMouseEvent(e, { clickHandler: onClick }), 263 | [handleMouseEvent, onClick] 264 | ); 265 | 266 | const handleMouseDown: MouseListener = React.useCallback( 267 | (e) => handleMouseEvent(e, { clickHandler: onMouseDown }), 268 | [handleMouseEvent, onMouseDown] 269 | ); 270 | 271 | const handleMouseMove: MouseListener = React.useCallback( 272 | (e) => handleMouseEvent(e, { mouseHandler: onMouseMove }), 273 | [handleMouseEvent, onMouseMove] 274 | ); 275 | 276 | const handleMouseUp: MouseListener = React.useCallback( 277 | (e) => handleMouseEvent(e, { mouseHandler: onMouseUp }), 278 | [handleMouseEvent, onMouseUp] 279 | ); 280 | 281 | React.useEffect(() => { 282 | const possiblyAddEventListener = ( 283 | event: "click" | "mousedown" | "mousemove" | "mouseup", 284 | globalListener: (e: MouseEvent) => void, 285 | propListener: ClickListener | MouseListener | undefined 286 | ) => { 287 | if (propListener != null) { 288 | window.addEventListener(event, globalListener); 289 | } 290 | }; 291 | 292 | possiblyAddEventListener("click", handleClick, onClick); 293 | possiblyAddEventListener("mousedown", handleMouseDown, onMouseDown); 294 | possiblyAddEventListener("mousemove", handleMouseMove, onMouseMove); 295 | possiblyAddEventListener("mouseup", handleMouseUp, onMouseUp); 296 | 297 | return () => { 298 | window.removeEventListener("click", handleClick); 299 | window.removeEventListener("mousedown", handleMouseDown); 300 | window.removeEventListener("mousemove", handleMouseMove); 301 | window.removeEventListener("mouseup", handleMouseMove); 302 | }; 303 | }, [ 304 | handleClick, 305 | handleMouseDown, 306 | handleMouseMove, 307 | handleMouseUp, 308 | onClick, 309 | onMouseDown, 310 | onMouseMove, 311 | onMouseUp, 312 | ]); 313 | 314 | React.useEffect(() => { 315 | const canvasRef = canvas.current; 316 | const observer = new ResizeObserver(() => { 317 | setCanvasSize(canvas.current); 318 | setIsReady(true); 319 | }); 320 | if (canvasRef != null) { 321 | observer.observe(canvasRef); 322 | } 323 | return () => { 324 | if (canvasRef != null) { 325 | observer.unobserve(canvasRef); 326 | } 327 | }; 328 | }, [setIsReady]); 329 | 330 | return ; 331 | }; 332 | 333 | export default React.forwardRef(ConfettiCanvas); 334 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | declare class Environment { 4 | gravity: number; 5 | wind: number; 6 | density: number; 7 | constructor({ gravity, wind, density, }?: { 8 | gravity?: number; 9 | wind?: number; 10 | density?: number; 11 | }); 12 | } 13 | 14 | interface Vector2Template { 15 | x: T; 16 | y: T; 17 | } 18 | interface Vector3Template extends Vector2Template { 19 | z: T; 20 | } 21 | type Vector2 = Vector2Template; 22 | type Vector3 = Vector3Template; 23 | type SpriteProp = { 24 | src: string; 25 | colorize: boolean; 26 | } | string; 27 | 28 | declare abstract class UpdatableValue { 29 | value: number; 30 | constructor(value: number); 31 | abstract update(deltaTime: number): void; 32 | abstract previewUpdate(deltaTime: number): number; 33 | } 34 | declare class UpdatableVector2Value { 35 | _x: UpdatableValue; 36 | _y: UpdatableValue; 37 | constructor(x: UpdatableValue, y: UpdatableValue, uniformVectorValues: boolean | undefined); 38 | update(deltaTime: number): void; 39 | previewUpdate(deltaTime: number): { 40 | x: number; 41 | y: number; 42 | }; 43 | get x(): number; 44 | set x(x: number); 45 | get y(): number; 46 | set y(y: number); 47 | } 48 | declare class UpdatableVector3Value extends UpdatableVector2Value { 49 | _z: UpdatableValue; 50 | constructor(x: UpdatableValue, y: UpdatableValue, z: UpdatableValue, uniformVectorValues: boolean | undefined); 51 | update(deltaTime: number): void; 52 | previewUpdate(deltaTime: number): { 53 | z: number; 54 | x: number; 55 | y: number; 56 | }; 57 | get z(): number; 58 | set z(z: number); 59 | } 60 | 61 | type ConfettiArgs = { 62 | id: string; 63 | position: UpdatableVector2Value; 64 | velocity: UpdatableVector2Value; 65 | rotation: UpdatableVector3Value; 66 | size: UpdatableVector2Value; 67 | dragCoefficient: UpdatableVector2Value; 68 | opacity: UpdatableValue; 69 | airResistanceArea: UpdatableVector2Value; 70 | spriteX: number; 71 | spriteY: number; 72 | spriteWidth: number; 73 | spriteHeight: number; 74 | }; 75 | declare class Confetti { 76 | id: string; 77 | position: UpdatableVector2Value; 78 | velocity: UpdatableVector2Value; 79 | rotation: UpdatableVector3Value; 80 | size: UpdatableVector2Value; 81 | dragCoefficient: UpdatableVector2Value; 82 | opacity: UpdatableValue; 83 | airResistanceArea: UpdatableVector2Value; 84 | spriteX: number; 85 | spriteY: number; 86 | spriteWidth: number; 87 | spriteHeight: number; 88 | _lastUpdatedAt: number; 89 | constructor(args: ConfettiArgs); 90 | getNewForces(environment: Environment, deltaTime: number): { 91 | x: number; 92 | y: number; 93 | }; 94 | update(environment: Environment): void; 95 | previewPositionUpdate(environment: Environment, deltaTimeMS: number): { 96 | x: number; 97 | y: number; 98 | }; 99 | draw(spriteCanvas: HTMLCanvasElement, context: CanvasRenderingContext2D): void; 100 | shouldDestroy(canvas: HTMLCanvasElement, environment: Environment): boolean; 101 | get width(): number; 102 | get height(): number; 103 | addForce(force: Vector2): void; 104 | } 105 | 106 | type EasingFunction = (timePassed: number, startValue: number, changeInValue: number, totalDuration: number) => number; 107 | declare const easeInOutQuad: EasingFunction; 108 | 109 | declare class StaticUpdatableValue extends UpdatableValue { 110 | update(): void; 111 | previewUpdate(): number; 112 | } 113 | declare class LinearUpdatableValue extends UpdatableValue { 114 | addValue: number; 115 | constructor(value: number, addValue: number); 116 | update(deltaTime: number): void; 117 | previewUpdate(deltaTime: number): number; 118 | } 119 | type Direction$1 = 1 | -1; 120 | declare class OscillatingUpdatableValue extends UpdatableValue { 121 | min: number; 122 | max: number; 123 | duration: number; 124 | timePassed: number; 125 | directionMultiplier: Direction$1; 126 | easingFunction: EasingFunction; 127 | constructor(value: number, min: number, max: number, duration: number, directionMultiplier: Direction$1, easingFunction: EasingFunction); 128 | update(deltaTime: number): void; 129 | previewUpdate(deltaTime: number): number; 130 | doUpdate(deltaTime: number): [number, number, Direction$1]; 131 | } 132 | 133 | interface Sprite { 134 | image: HTMLImageElement; 135 | colorize: boolean; 136 | src: string; 137 | } 138 | interface SpriteCanvasProps { 139 | className?: string; 140 | visible?: boolean; 141 | sprites: SpriteProp[]; 142 | colors: string[]; 143 | spriteWidth: number; 144 | spriteHeight: number; 145 | } 146 | interface SpriteCanvasData { 147 | sprites: Sprite[]; 148 | colors: string[]; 149 | spriteWidth: number; 150 | spriteHeight: number; 151 | } 152 | interface SpriteCanvasHandle { 153 | getCanvas: () => HTMLCanvasElement | null; 154 | getCreateData: () => SpriteCanvasData; 155 | addReadyListener: (listener: (isReady: boolean) => void) => string; 156 | removeReadyListener: (listenerId: string) => void; 157 | isReady: boolean; 158 | } 159 | declare const _default$1: React.ForwardRefExoticComponent>; 160 | 161 | interface StaticConfigConstant { 162 | type: "static"; 163 | value: T; 164 | } 165 | interface StaticConfigRandom { 166 | type: "static-random"; 167 | minValue: T; 168 | maxValue: T; 169 | } 170 | type StaticConfig = StaticConfigConstant | StaticConfigRandom; 171 | interface LinearConfigConstant { 172 | type: "linear"; 173 | value: T; 174 | addValue: T; 175 | } 176 | interface LinearConfigRandom { 177 | type: "linear-random"; 178 | minValue: T; 179 | maxValue: T; 180 | minAddValue: T; 181 | maxAddValue: T; 182 | } 183 | type LinearConfig = LinearConfigConstant | LinearConfigRandom; 184 | type Direction = 1 | -1; 185 | type DirectionVector2 = { 186 | x: Direction; 187 | y: Direction; 188 | }; 189 | type DirectionVector3 = DirectionVector2 & { 190 | z: Direction; 191 | }; 192 | interface OscillatingConfigConstant { 193 | type: "oscillating"; 194 | value: T; 195 | start: T; 196 | final: T; 197 | duration: T; 198 | direction: TDirection; 199 | easingFunction: EasingFunction; 200 | } 201 | interface OscillatingConfigRandom { 202 | type: "oscillating-random"; 203 | minValue: T; 204 | maxValue: T; 205 | minStart: T; 206 | maxStart: T; 207 | minFinal: T; 208 | maxFinal: T; 209 | minDuration: T; 210 | maxDuration: T; 211 | minDirection: TDirection; 212 | maxDirection: TDirection; 213 | easingFunctions: EasingFunction[]; 214 | } 215 | type OscillatingConfig = OscillatingConfigConstant | OscillatingConfigRandom; 216 | type Config = StaticConfig | LinearConfig | OscillatingConfig; 217 | type ConfigNumber = Config; 218 | type ConfigVector2 = Config; 219 | type ConfigVector3 = Config; 220 | type ConfigNumberInput = ConfigNumber; 221 | type ConfigVector2Input = (ConfigVector2 | ConfigNumber) & { 222 | uniformVectorValues?: boolean; 223 | }; 224 | type ConfigVector3Input = (ConfigVector3 | ConfigNumber) & { 225 | uniformVectorValues?: boolean; 226 | }; 227 | type CreateConfettiArgs = { 228 | id?: string; 229 | position: ConfigVector2Input; 230 | velocity?: ConfigVector2Input; 231 | rotation?: ConfigVector3Input; 232 | dragCoefficient?: ConfigVector2Input; 233 | airResistanceArea?: ConfigVector2Input; 234 | size: ConfigVector2Input; 235 | opacity?: ConfigNumberInput; 236 | }; 237 | declare function getUpdatableValueNumber(config: ConfigNumber): StaticUpdatableValue | LinearUpdatableValue | OscillatingUpdatableValue; 238 | declare function getUpdatableValueVector2(config: ConfigVector2Input): UpdatableVector2Value; 239 | declare function getUpdatableValueVector3(config: ConfigVector3Input): UpdatableVector3Value; 240 | declare function createConfetti(id: string, rawArgs: CreateConfettiArgs, spriteCanvasData: SpriteCanvasData, requestedSprite?: SpriteProp, requestedColor?: string | null): Confetti; 241 | 242 | type CreateConfettiArgsDefaults = Pick, "velocity" | "rotation" | "dragCoefficient" | "airResistanceArea" | "opacity">; 243 | declare const CREATE_CONFETTI_DEFAULTS: CreateConfettiArgsDefaults; 244 | 245 | type ClickListener = (e: MouseEvent, confetti: Confetti | null) => void; 246 | type MouseListener = (e: MouseEvent) => void; 247 | interface ConfettiCanvasProps extends Omit, "onClick" | "onMouseDown" | "onMouseMove" | "onMouseUp"> { 248 | className?: string; 249 | environment: Environment; 250 | onClick?: ClickListener; 251 | onMouseDown?: ClickListener; 252 | onMouseMove?: MouseListener; 253 | onMouseUp?: MouseListener; 254 | requestAnimationFrame?: (handler: FrameRequestCallback) => number; 255 | cancelAnimationFrame?: (id: number) => void; 256 | onBeforeRender?: (context: CanvasRenderingContext2D) => void; 257 | onAfterRender?: (context: CanvasRenderingContext2D) => void; 258 | } 259 | interface ConfettiCanvasHandle { 260 | createConfetti: (args: CreateConfettiArgs, spriteCanvas: HTMLCanvasElement, SpriteCanvasData: SpriteCanvasData, sprite?: SpriteProp, color?: string | null) => Confetti; 261 | addConfetti: (confetti: Confetti, spriteCanvas: HTMLCanvasElement) => void; 262 | deleteConfetti: (id: string) => void; 263 | clearConfetti: () => void; 264 | getCanvas: () => HTMLCanvasElement | null; 265 | addReadyListener: (listener: (isReady: boolean) => void) => string; 266 | removeReadyListener: (listenerId: string) => void; 267 | isReady: boolean; 268 | } 269 | declare const _default: React.ForwardRefExoticComponent>; 270 | 271 | interface CreateConfettiRequestedOptions { 272 | sprite?: SpriteProp; 273 | color?: string; 274 | } 275 | interface ConfettiCannon { 276 | createConfetti: (createConfettiArgs: CreateConfettiArgs, createConfettiRequestedOptions?: CreateConfettiRequestedOptions) => Confetti | undefined; 277 | createMultipleConfetti: (createConfettiArgs: CreateConfettiArgs, numberToFire: number, createConfettiRequestedOptions?: CreateConfettiRequestedOptions) => Confetti[]; 278 | addConfetti: (confetti: Confetti) => void; 279 | deleteConfetti: (id: string) => void; 280 | clearConfetti: () => void; 281 | isReady: boolean; 282 | } 283 | declare function useConfettiCannon(confettiCanvas: ConfettiCanvasHandle | null, spriteCanvas: SpriteCanvasHandle | null): ConfettiCannon; 284 | 285 | export { CREATE_CONFETTI_DEFAULTS, Confetti, ConfettiCannon, _default as ConfettiCanvas, ConfettiCanvasHandle, CreateConfettiArgs, CreateConfettiArgsDefaults, CreateConfettiRequestedOptions, Direction$1 as Direction, EasingFunction, Environment, LinearUpdatableValue, OscillatingUpdatableValue, _default$1 as SpriteCanvas, SpriteCanvasHandle, SpriteProp, StaticUpdatableValue, Vector2, Vector2Template, Vector3, Vector3Template, createConfetti, easeInOutQuad, getUpdatableValueNumber, getUpdatableValueVector2, getUpdatableValueVector3, useConfettiCannon }; 286 | -------------------------------------------------------------------------------- /dist/esm/index.js: -------------------------------------------------------------------------------- 1 | import*as e from"react";import{v4 as t}from"uuid";function n(e,t){var n=e.x,i=e.y;return n>t.x&&nt.y&&i0?-1:1,a=Math.abs(t);return.5*e*i*n*a*a*r}var r=function(){function e(e){this.id=e.id,this.position=e.position,this.velocity=e.velocity,this.rotation=e.rotation,this.dragCoefficient=e.dragCoefficient,this.airResistanceArea=e.airResistanceArea,this.size=e.size,this.opacity=e.opacity,this.spriteX=e.spriteX,this.spriteY=e.spriteY,this.spriteWidth=e.spriteWidth,this.spriteHeight=e.spriteHeight,this._lastUpdatedAt=Date.now()}return e.prototype.getNewForces=function(e,t){var n=e.wind*t,r=-e.gravity*t;return{x:n+i(this.dragCoefficient.x,this.velocity.x,this.airResistanceArea.x,e.density),y:r+i(this.dragCoefficient.y,this.velocity.y,this.airResistanceArea.y,e.density)}},e.prototype.update=function(e){var t=Date.now(),n=(t-this._lastUpdatedAt)/100;this.rotation.update(n),this.dragCoefficient.update(n);var i=this.getNewForces(e,n),r=i.x,a=i.y;this.velocity.update(n),this.velocity.x+=r,this.velocity.y+=a,this.position.update(n),this.position.x+=this.velocity.x*n,this.position.y+=this.velocity.y*n,this.size.update(n),this.opacity.update(n),this.opacity.value=Math.max(this.opacity.value,0),this._lastUpdatedAt=t},e.prototype.previewPositionUpdate=function(e,t){var n=t/100,i=this.velocity.previewUpdate(n),r=this.getNewForces(e,n),a=r.x,o=r.y;i.x+=a,i.y+=o;var u=this.position.previewUpdate(n);return u.x+=i.x*n,u.y+=i.y*n,u},e.prototype.draw=function(e,t){t.save(),t.globalAlpha=this.opacity.value,t.setTransform((new DOMMatrix).translateSelf(this.position.x*global.devicePixelRatio,this.position.y*global.devicePixelRatio).rotateSelf(this.rotation.x,this.rotation.y,this.rotation.z)),t.drawImage(e,this.spriteX,this.spriteY,this.spriteWidth,this.spriteHeight,-this.width/2*global.devicePixelRatio,-this.height/2*global.devicePixelRatio,this.width*global.devicePixelRatio,this.height*global.devicePixelRatio),t.restore()},e.prototype.shouldDestroy=function(e,t){return this.opacity.value<0||t.gravity>=0&&this.velocity.y<0&&this.position.y+this.height<0||t.gravity<=0&&this.velocity.y>0&&this.position.y-this.height>e.height||t.wind>=0&&this.velocity.x>0&&this.position.x-this.width>e.width||t.wind<=0&&this.velocity.x<0&&this.position.x+this.width<0},Object.defineProperty(e.prototype,"width",{get:function(){return this.size.x},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"height",{get:function(){return this.size.y},enumerable:!1,configurable:!0}),e.prototype.addForce=function(e){this.velocity.x+=e.x,this.velocity.y+=e.y},e}(),a={velocity:{type:"static",value:0},rotation:{type:"static",value:0},dragCoefficient:{type:"static",value:1.66},airResistanceArea:{type:"static",value:.001},opacity:{type:"static",value:1}},o=function(e){var t=void 0===e?{}:e,n=t.gravity,i=t.wind,r=t.density;this.gravity=-9.8,this.wind=0,this.density=1.2041,this.gravity=null!=n?n:this.gravity,this.wind=null!=i?i:this.wind,this.density=null!=r?r:this.density},u=function(e,t){return u=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},u(e,t)};function l(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}u(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}var s=function(){return s=Object.assign||function(e){for(var t,n=1,i=arguments.length;n0&&r[r.length-1])||6!==u[0]&&2!==u[0])){o=0;continue}if(3===u[0]&&(!r||u[1]>r[0]&&u[1]this.duration?-1*this.directionMultiplier:this.directionMultiplier,a=this.easingFunction(i,this.min,t,this.duration);return[isNaN(a)?0:a,i,r]},t}(d);function w(e,t){return e===t?e:Math.random()*(t-e+1)+e}function x(e){var t=Math.floor(w(0,e.length-1));return[e[t],t]}function g(e,t){return x([e,t])[0]}function b(e){return"number"==typeof e?{x:e,y:e}:e}function R(e){return"number"==typeof e?{x:e,y:e,z:e}:e}function C(e){return function(e){switch(e.type){case"static":return new v(e.value);case"static-random":return new v(w(e.minValue,e.maxValue));case"linear":return new y(e.value,e.addValue);case"linear-random":return new y(w(e.minValue,e.maxValue),w(e.minAddValue,e.maxAddValue));case"oscillating":return new m(e.value,e.start,e.final,e.duration,e.direction,e.easingFunction);case"oscillating-random":return new m(w(e.minValue,e.maxValue),w(e.minStart,e.maxStart),w(e.minFinal,e.maxFinal),w(e.minDuration,e.maxDuration),g(e.minDirection,e.maxDirection),x(e.easingFunctions)[0])}}(s(s({},e),{valueType:"number"}))}function V(e){return function(e){switch(e.type){case"static":var t=b(e.value);return new h(new v(t.x),new v(t.y),e.uniformVectorValues);case"static-random":var n=b(e.minValue),i=b(e.maxValue);return new h(new v(w(n.x,i.x)),new v(w(n.y,i.y)),e.uniformVectorValues);case"linear":t=b(e.value);var r=b(e.addValue);return new h(new y(t.x,r.x),new y(t.y,r.y),e.uniformVectorValues);case"linear-random":n=b(e.minValue),i=b(e.maxValue);var a=b(e.minAddValue),o=b(e.maxAddValue);return new h(new y(w(n.x,i.x),w(a.x,o.x)),new y(w(n.y,i.y),w(a.x,o.x)),e.uniformVectorValues);case"oscillating":t=b(e.value);var u=b(e.start),l=b(e.final),s=b(e.duration),c=b(e.direction);return new h(new m(t.x,u.x,l.x,s.x,c.x,e.easingFunction),new m(t.y,u.y,l.y,s.x,c.y,e.easingFunction),e.uniformVectorValues);case"oscillating-random":n=b(e.minValue),i=b(e.maxValue);var f=b(e.minStart),d=b(e.maxStart),p=b(e.minFinal),R=b(e.maxFinal),C=b(e.minDuration),V=b(e.maxDuration),z=b(e.minDirection),k=b(e.maxDirection);return new h(new m(w(n.x,i.x),w(f.x,d.x),w(p.x,R.x),w(C.x,V.x),g(z.x,k.x),x(e.easingFunctions)[0]),new m(w(n.y,i.y),w(f.y,d.y),w(p.y,R.y),w(C.y,V.y),g(z.y,k.y),x(e.easingFunctions)[0]),e.uniformVectorValues)}}(s(s({},e),{valueType:"Vector2"}))}function z(e){return function(e){switch(e.type){case"static":var t=R(e.value);return new p(new v(t.x),new v(t.y),new v(t.z),e.uniformVectorValues);case"static-random":var n=R(e.minValue),i=R(e.maxValue);return new p(new v(w(n.x,i.x)),new v(w(n.y,i.y)),new v(w(n.z,i.z)),e.uniformVectorValues);case"linear":t=R(e.value);var r=R(e.addValue);return new p(new y(t.x,r.x),new y(t.y,r.y),new y(t.z,r.z),e.uniformVectorValues);case"linear-random":n=R(e.minValue),i=R(e.maxValue);var a=R(e.minAddValue),o=R(e.maxAddValue);return new p(new y(w(n.x,i.x),w(a.x,o.x)),new y(w(n.y,i.y),w(a.y,o.y)),new y(w(n.z,i.z),w(a.z,o.z)),e.uniformVectorValues);case"oscillating":t=R(e.value);var u=R(e.start),l=R(e.final),s=R(e.duration),c=R(e.direction);return new p(new m(t.x,u.x,l.x,s.x,c.x,e.easingFunction),new m(t.y,u.y,l.y,s.z,c.y,e.easingFunction),new m(t.z,u.z,l.z,s.z,c.z,e.easingFunction),e.uniformVectorValues);case"oscillating-random":n=R(e.minValue),i=R(e.maxValue);var f=R(e.minStart),d=R(e.maxStart),h=R(e.minFinal),b=R(e.maxFinal),C=R(e.minDuration),V=R(e.maxDuration),z=R(e.minDirection),k=R(e.maxDirection);return new p(new m(w(n.x,i.x),w(f.x,d.x),w(h.x,b.x),w(C.x,V.x),g(z.x,k.x),x(e.easingFunctions)[0]),new m(w(n.y,i.y),w(f.y,d.y),w(h.y,b.y),w(C.y,V.y),g(z.y,k.y),x(e.easingFunctions)[0]),new m(w(n.z,i.z),w(f.z,d.z),w(h.z,b.z),w(C.z,V.z),g(z.z,k.z),x(e.easingFunctions)[0]),e.uniformVectorValues)}}(s(s({},e),{valueType:"Vector3"}))}function k(e,t,n,i,o){var u=function(e,t){return s(s({id:t},a),e)}(t,e),l=function(e,t){if(null!=e){var n=t.sprites.findIndex((function(t){return n=t,"string"==typeof(i=e)?n.src===i&&n.colorize:n.src===i.src&&n.colorize===i.colorize;var n,i}));if(-1!==n)return[e,n]}return x(t.sprites)}(i,n),c=l[0],f=l[1],d=function(e,t,n){if(!function(e){return"string"==typeof e||e.colorize}(e))return 0;var i=null!=t?n.colors.findIndex((function(e){return e===t})):-1;return-1!==i?i:Math.floor(w(0,n.colors.length-1))}(null!=i?i:c,o,n);return new r({id:e,position:V(u.position),velocity:V(u.velocity),rotation:z(u.rotation),dragCoefficient:V(u.dragCoefficient),size:V(u.size),opacity:C(u.opacity),airResistanceArea:V(u.airResistanceArea),spriteX:d*n.spriteWidth+2*d,spriteY:f*n.spriteHeight+2*f,spriteWidth:n.spriteWidth,spriteHeight:n.spriteHeight})}function F(){var n=e.useRef(!1),i=e.useRef({}),r=e.useCallback((function(e){for(var t in i.current)i.current[t](e)}),[]);return e.useEffect((function(){return function(){return r(!1)}}),[r]),e.useMemo((function(){return{isReady:n.current,addReadyListener:function(e){var r=t();return i.current[r]=e,n.current&&e(n.current),r},removeReadyListener:function(e){delete i.current[e]},setIsReady:function(e){n.current=e,r(e)}}}),[r])}var P=e.forwardRef((function(i,r){var a=i.className,o=i.environment,u=i.onClick,l=i.onMouseDown,c=i.onMouseMove,f=i.onMouseUp,d=i.onBeforeRender,h=i.onAfterRender,p=i.requestAnimationFrame,v=void 0===p?window.requestAnimationFrame:p,y=i.cancelAnimationFrame,m=void 0===y?window.cancelAnimationFrame:y,w=function(e,t){var n={};for(var i in e)Object.prototype.hasOwnProperty.call(e,i)&&t.indexOf(i)<0&&(n[i]=e[i]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols){var r=0;for(i=Object.getOwnPropertySymbols(e);r0?P.current=v(_):(t.clearRect(0,0,e.width,e.height),P.current=null);var n=Date.now();0!==A.current&&(M.current=1e3/(n-A.current)),A.current=n}}}),[o,h,d,v]);e.useEffect((function(){null!=P.current&&(m(P.current),P.current=v(_))}),[m,_,v]);var D=e.useCallback((function(e,t){z.current.set(e.id,{confetti:e,spriteCanvas:t}),null==P.current&&_()}),[_]),O=e.useCallback((function(e,n,i,r,a){var o,u=k(null!==(o=e.id)&&void 0!==o?o:t(),e,i,r,a);return D(u,n),u}),[D]),E=e.useCallback((function(e){z.current.delete(e)}),[]),U=e.useCallback((function(){return z.current.clear()}),[]),L=e.useCallback((function(){return x.current}),[]);e.useImperativeHandle(r,(function(){return{createConfetti:O,addConfetti:D,deleteConfetti:E,clearConfetti:U,getCanvas:L,addReadyListener:R,removeReadyListener:C,isReady:b}}),[O,D,E,U,L,R,C,b]);var H=e.useCallback((function(e,t){var i,r,a=t.clickHandler,u=t.mouseHandler;if(null!=a||null!=u){var l=null===(i=x.current)||void 0===i?void 0:i.getBoundingClientRect();if(null!=l){var s=function(e,t){if(null==t)throw new Error("element should not be null");var n=t.getBoundingClientRect();return{x:e.clientX-n.left,y:e.clientY-n.top}}(e,x.current);if(n(s,{x:l.left,y:l.top,width:l.width,height:l.height})){if(null!=u)return u(e);if(null!=a){var c=-1e3/M.current*2,f=function(e,t){for(var n=0,i=Array.from(e.values());n>16&255,g:t>>8&255,b:255&t}}(i),f=0;ft.x&&nt.y&&i0?-1:1,a=Math.abs(t);return.5*e*i*n*a*a*r}var o=function(){function e(e){this.id=e.id,this.position=e.position,this.velocity=e.velocity,this.rotation=e.rotation,this.dragCoefficient=e.dragCoefficient,this.airResistanceArea=e.airResistanceArea,this.size=e.size,this.opacity=e.opacity,this.spriteX=e.spriteX,this.spriteY=e.spriteY,this.spriteWidth=e.spriteWidth,this.spriteHeight=e.spriteHeight,this._lastUpdatedAt=Date.now()}return e.prototype.getNewForces=function(e,t){var n=e.wind*t,i=-e.gravity*t;return{x:n+a(this.dragCoefficient.x,this.velocity.x,this.airResistanceArea.x,e.density),y:i+a(this.dragCoefficient.y,this.velocity.y,this.airResistanceArea.y,e.density)}},e.prototype.update=function(e){var t=Date.now(),n=(t-this._lastUpdatedAt)/100;this.rotation.update(n),this.dragCoefficient.update(n);var i=this.getNewForces(e,n),r=i.x,a=i.y;this.velocity.update(n),this.velocity.x+=r,this.velocity.y+=a,this.position.update(n),this.position.x+=this.velocity.x*n,this.position.y+=this.velocity.y*n,this.size.update(n),this.opacity.update(n),this.opacity.value=Math.max(this.opacity.value,0),this._lastUpdatedAt=t},e.prototype.previewPositionUpdate=function(e,t){var n=t/100,i=this.velocity.previewUpdate(n),r=this.getNewForces(e,n),a=r.x,o=r.y;i.x+=a,i.y+=o;var u=this.position.previewUpdate(n);return u.x+=i.x*n,u.y+=i.y*n,u},e.prototype.draw=function(e,t){t.save(),t.globalAlpha=this.opacity.value,t.setTransform((new DOMMatrix).translateSelf(this.position.x*global.devicePixelRatio,this.position.y*global.devicePixelRatio).rotateSelf(this.rotation.x,this.rotation.y,this.rotation.z)),t.drawImage(e,this.spriteX,this.spriteY,this.spriteWidth,this.spriteHeight,-this.width/2*global.devicePixelRatio,-this.height/2*global.devicePixelRatio,this.width*global.devicePixelRatio,this.height*global.devicePixelRatio),t.restore()},e.prototype.shouldDestroy=function(e,t){return this.opacity.value<0||t.gravity>=0&&this.velocity.y<0&&this.position.y+this.height<0||t.gravity<=0&&this.velocity.y>0&&this.position.y-this.height>e.height||t.wind>=0&&this.velocity.x>0&&this.position.x-this.width>e.width||t.wind<=0&&this.velocity.x<0&&this.position.x+this.width<0},Object.defineProperty(e.prototype,"width",{get:function(){return this.size.x},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"height",{get:function(){return this.size.y},enumerable:!1,configurable:!0}),e.prototype.addForce=function(e){this.velocity.x+=e.x,this.velocity.y+=e.y},e}(),u={velocity:{type:"static",value:0},rotation:{type:"static",value:0},dragCoefficient:{type:"static",value:1.66},airResistanceArea:{type:"static",value:.001},opacity:{type:"static",value:1}},l=function(e){var t=void 0===e?{}:e,n=t.gravity,i=t.wind,r=t.density;this.gravity=-9.8,this.wind=0,this.density=1.2041,this.gravity=null!=n?n:this.gravity,this.wind=null!=i?i:this.wind,this.density=null!=r?r:this.density},s=function(e,t){return s=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},s(e,t)};function c(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}s(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}var f=function(){return f=Object.assign||function(e){for(var t,n=1,i=arguments.length;n0&&r[r.length-1])||6!==u[0]&&2!==u[0])){o=0;continue}if(3===u[0]&&(!r||u[1]>r[0]&&u[1]this.duration?-1*this.directionMultiplier:this.directionMultiplier,a=this.easingFunction(i,this.min,t,this.duration);return[isNaN(a)?0:a,i,r]},t}(h);function g(e,t){return e===t?e:Math.random()*(t-e+1)+e}function b(e){var t=Math.floor(g(0,e.length-1));return[e[t],t]}function C(e,t){return b([e,t])[0]}function R(e){return"number"==typeof e?{x:e,y:e}:e}function V(e){return"number"==typeof e?{x:e,y:e,z:e}:e}function z(e){return function(e){switch(e.type){case"static":return new m(e.value);case"static-random":return new m(g(e.minValue,e.maxValue));case"linear":return new x(e.value,e.addValue);case"linear-random":return new x(g(e.minValue,e.maxValue),g(e.minAddValue,e.maxAddValue));case"oscillating":return new w(e.value,e.start,e.final,e.duration,e.direction,e.easingFunction);case"oscillating-random":return new w(g(e.minValue,e.maxValue),g(e.minStart,e.maxStart),g(e.minFinal,e.maxFinal),g(e.minDuration,e.maxDuration),C(e.minDirection,e.maxDirection),b(e.easingFunctions)[0])}}(f(f({},e),{valueType:"number"}))}function F(e){return function(e){switch(e.type){case"static":var t=R(e.value);return new v(new m(t.x),new m(t.y),e.uniformVectorValues);case"static-random":var n=R(e.minValue),i=R(e.maxValue);return new v(new m(g(n.x,i.x)),new m(g(n.y,i.y)),e.uniformVectorValues);case"linear":t=R(e.value);var r=R(e.addValue);return new v(new x(t.x,r.x),new x(t.y,r.y),e.uniformVectorValues);case"linear-random":n=R(e.minValue),i=R(e.maxValue);var a=R(e.minAddValue),o=R(e.maxAddValue);return new v(new x(g(n.x,i.x),g(a.x,o.x)),new x(g(n.y,i.y),g(a.x,o.x)),e.uniformVectorValues);case"oscillating":t=R(e.value);var u=R(e.start),l=R(e.final),s=R(e.duration),c=R(e.direction);return new v(new w(t.x,u.x,l.x,s.x,c.x,e.easingFunction),new w(t.y,u.y,l.y,s.x,c.y,e.easingFunction),e.uniformVectorValues);case"oscillating-random":n=R(e.minValue),i=R(e.maxValue);var f=R(e.minStart),d=R(e.maxStart),p=R(e.minFinal),h=R(e.maxFinal),y=R(e.minDuration),V=R(e.maxDuration),z=R(e.minDirection),F=R(e.maxDirection);return new v(new w(g(n.x,i.x),g(f.x,d.x),g(p.x,h.x),g(y.x,V.x),C(z.x,F.x),b(e.easingFunctions)[0]),new w(g(n.y,i.y),g(f.y,d.y),g(p.y,h.y),g(y.y,V.y),C(z.y,F.y),b(e.easingFunctions)[0]),e.uniformVectorValues)}}(f(f({},e),{valueType:"Vector2"}))}function O(e){return function(e){switch(e.type){case"static":var t=V(e.value);return new y(new m(t.x),new m(t.y),new m(t.z),e.uniformVectorValues);case"static-random":var n=V(e.minValue),i=V(e.maxValue);return new y(new m(g(n.x,i.x)),new m(g(n.y,i.y)),new m(g(n.z,i.z)),e.uniformVectorValues);case"linear":t=V(e.value);var r=V(e.addValue);return new y(new x(t.x,r.x),new x(t.y,r.y),new x(t.z,r.z),e.uniformVectorValues);case"linear-random":n=V(e.minValue),i=V(e.maxValue);var a=V(e.minAddValue),o=V(e.maxAddValue);return new y(new x(g(n.x,i.x),g(a.x,o.x)),new x(g(n.y,i.y),g(a.y,o.y)),new x(g(n.z,i.z),g(a.z,o.z)),e.uniformVectorValues);case"oscillating":t=V(e.value);var u=V(e.start),l=V(e.final),s=V(e.duration),c=V(e.direction);return new y(new w(t.x,u.x,l.x,s.x,c.x,e.easingFunction),new w(t.y,u.y,l.y,s.z,c.y,e.easingFunction),new w(t.z,u.z,l.z,s.z,c.z,e.easingFunction),e.uniformVectorValues);case"oscillating-random":n=V(e.minValue),i=V(e.maxValue);var f=V(e.minStart),d=V(e.maxStart),p=V(e.minFinal),h=V(e.maxFinal),v=V(e.minDuration),R=V(e.maxDuration),z=V(e.minDirection),F=V(e.maxDirection);return new y(new w(g(n.x,i.x),g(f.x,d.x),g(p.x,h.x),g(v.x,R.x),C(z.x,F.x),b(e.easingFunctions)[0]),new w(g(n.y,i.y),g(f.y,d.y),g(p.y,h.y),g(v.y,R.y),C(z.y,F.y),b(e.easingFunctions)[0]),new w(g(n.z,i.z),g(f.z,d.z),g(p.z,h.z),g(v.z,R.z),C(z.z,F.z),b(e.easingFunctions)[0]),e.uniformVectorValues)}}(f(f({},e),{valueType:"Vector3"}))}function k(e,t,n,i,r){var a=function(e,t){return f(f({id:t},u),e)}(t,e),l=function(e,t){if(null!=e){var n=t.sprites.findIndex((function(t){return n=t,"string"==typeof(i=e)?n.src===i&&n.colorize:n.src===i.src&&n.colorize===i.colorize;var n,i}));if(-1!==n)return[e,n]}return b(t.sprites)}(i,n),s=l[0],c=l[1],d=function(e,t,n){if(!function(e){return"string"==typeof e||e.colorize}(e))return 0;var i=null!=t?n.colors.findIndex((function(e){return e===t})):-1;return-1!==i?i:Math.floor(g(0,n.colors.length-1))}(null!=i?i:s,r,n);return new o({id:e,position:F(a.position),velocity:F(a.velocity),rotation:O(a.rotation),dragCoefficient:F(a.dragCoefficient),size:F(a.size),opacity:z(a.opacity),airResistanceArea:F(a.airResistanceArea),spriteX:d*n.spriteWidth+2*d,spriteY:c*n.spriteHeight+2*c,spriteWidth:n.spriteWidth,spriteHeight:n.spriteHeight})}function P(){var e=i.useRef(!1),n=i.useRef({}),r=i.useCallback((function(e){for(var t in n.current)n.current[t](e)}),[]);return i.useEffect((function(){return function(){return r(!1)}}),[r]),i.useMemo((function(){return{isReady:e.current,addReadyListener:function(i){var r=t.v4();return n.current[r]=i,e.current&&i(e.current),r},removeReadyListener:function(e){delete n.current[e]},setIsReady:function(t){e.current=t,r(t)}}}),[r])}var A=i.forwardRef((function(e,n){var a=e.className,o=e.environment,u=e.onClick,l=e.onMouseDown,s=e.onMouseMove,c=e.onMouseUp,d=e.onBeforeRender,p=e.onAfterRender,h=e.requestAnimationFrame,v=void 0===h?window.requestAnimationFrame:h,y=e.cancelAnimationFrame,m=void 0===y?window.cancelAnimationFrame:y,x=function(e,t){var n={};for(var i in e)Object.prototype.hasOwnProperty.call(e,i)&&t.indexOf(i)<0&&(n[i]=e[i]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols){var r=0;for(i=Object.getOwnPropertySymbols(e);r0?F.current=v(U):(t.clearRect(0,0,e.width,e.height),F.current=null);var n=Date.now();0!==O.current&&(A.current=1e3/(n-O.current)),O.current=n}}}),[o,p,d,v]);i.useEffect((function(){null!=F.current&&(m(F.current),F.current=v(U))}),[m,U,v]);var E=i.useCallback((function(e,t){z.current.set(e.id,{confetti:e,spriteCanvas:t}),null==F.current&&U()}),[U]),_=i.useCallback((function(e,n,i,r,a){var o,u=k(null!==(o=e.id)&&void 0!==o?o:t.v4(),e,i,r,a);return E(u,n),u}),[E]),M=i.useCallback((function(e){z.current.delete(e)}),[]),D=i.useCallback((function(){return z.current.clear()}),[]),L=i.useCallback((function(){return w.current}),[]);i.useImperativeHandle(n,(function(){return{createConfetti:_,addConfetti:E,deleteConfetti:M,clearConfetti:D,getCanvas:L,addReadyListener:C,removeReadyListener:R,isReady:b}}),[_,E,M,D,L,C,R,b]);var j=i.useCallback((function(e,t){var n,i,a=t.clickHandler,u=t.mouseHandler;if(null!=a||null!=u){var l=null===(n=w.current)||void 0===n?void 0:n.getBoundingClientRect();if(null!=l){var s=function(e,t){if(null==t)throw new Error("element should not be null");var n=t.getBoundingClientRect();return{x:e.clientX-n.left,y:e.clientY-n.top}}(e,w.current);if(r(s,{x:l.left,y:l.top,width:l.width,height:l.height})){if(null!=u)return u(e);if(null!=a){var c=-1e3/A.current*2,f=function(e,t){for(var n=0,i=Array.from(e.values());n>16&255,g:t>>8&255,b:255&t}}(i),f=0;f { 21 | type: "static"; 22 | value: T; 23 | } 24 | 25 | interface StaticConfigRandom { 26 | type: "static-random"; 27 | minValue: T; 28 | maxValue: T; 29 | } 30 | 31 | type StaticConfig = StaticConfigConstant | StaticConfigRandom; 32 | 33 | interface LinearConfigConstant { 34 | type: "linear"; 35 | value: T; 36 | addValue: T; 37 | } 38 | 39 | interface LinearConfigRandom { 40 | type: "linear-random"; 41 | minValue: T; 42 | maxValue: T; 43 | minAddValue: T; 44 | maxAddValue: T; 45 | } 46 | 47 | type LinearConfig = LinearConfigConstant | LinearConfigRandom; 48 | 49 | type Direction = 1 | -1; 50 | type DirectionVector2 = { x: Direction; y: Direction }; 51 | type DirectionVector3 = DirectionVector2 & { z: Direction }; 52 | 53 | interface OscillatingConfigConstant { 54 | type: "oscillating"; 55 | value: T; 56 | start: T; 57 | final: T; 58 | duration: T; 59 | direction: TDirection; 60 | easingFunction: EasingFunction; 61 | } 62 | 63 | interface OscillatingConfigRandom { 64 | type: "oscillating-random"; 65 | minValue: T; 66 | maxValue: T; 67 | minStart: T; 68 | maxStart: T; 69 | minFinal: T; 70 | maxFinal: T; 71 | minDuration: T; 72 | maxDuration: T; 73 | minDirection: TDirection; 74 | maxDirection: TDirection; 75 | easingFunctions: EasingFunction[]; 76 | } 77 | 78 | type OscillatingConfig = 79 | | OscillatingConfigConstant 80 | | OscillatingConfigRandom; 81 | 82 | type Config = 83 | | StaticConfig 84 | | LinearConfig 85 | | OscillatingConfig; 86 | 87 | type ConfigNumber = Config; 88 | type ConfigVector2 = Config; 89 | type ConfigVector3 = Config; 90 | 91 | type ConfigNumberInput = ConfigNumber; 92 | type ConfigVector2Input = (ConfigVector2 | ConfigNumber) & { 93 | uniformVectorValues?: boolean; 94 | }; 95 | type ConfigVector3Input = (ConfigVector3 | ConfigNumber) & { 96 | uniformVectorValues?: boolean; 97 | }; 98 | 99 | export interface CreateConfettiArgsFull { 100 | id?: string; 101 | position: ConfigVector2; 102 | velocity: ConfigVector2; 103 | rotation: ConfigVector3; 104 | dragCoefficient: ConfigVector2; 105 | airResistanceArea?: ConfigVector2Input; 106 | size: ConfigNumber; 107 | opacity: ConfigNumber; 108 | } 109 | 110 | export type CreateConfettiArgs = { 111 | id?: string; 112 | position: ConfigVector2Input; 113 | velocity?: ConfigVector2Input; 114 | rotation?: ConfigVector3Input; 115 | dragCoefficient?: ConfigVector2Input; 116 | airResistanceArea?: ConfigVector2Input; 117 | size: ConfigVector2Input; 118 | opacity?: ConfigNumberInput; 119 | }; 120 | 121 | type CreateConfettiArgsFullInput = Required; 122 | 123 | type ConfigNumberAnnotated = ConfigNumberInput & { 124 | valueType: "number"; 125 | }; 126 | type ConfigVector2Annotated = ConfigVector2Input & { 127 | valueType: "Vector2"; 128 | }; 129 | type ConfigVector3Annotated = ConfigVector3Input & { 130 | valueType: "Vector3"; 131 | }; 132 | 133 | function getRandomValue(min: number, max: number) { 134 | if (min === max) { 135 | return min; 136 | } 137 | return Math.random() * (max - min + 1) + min; 138 | } 139 | 140 | function getRandomFromList(list: T[]): [T, number] { 141 | const index = Math.floor(getRandomValue(0, list.length - 1)); 142 | const value = list[index]; 143 | return [value, index]; 144 | } 145 | 146 | function getRandomDirection(min: Direction, max: Direction): Direction { 147 | return getRandomFromList([min, max])[0]; 148 | } 149 | 150 | function getVector2(input: Vector2Template | T) { 151 | if (typeof input === "number") { 152 | return { x: input, y: input }; 153 | } 154 | return input; 155 | } 156 | 157 | function getVector3(input: Vector3Template | T) { 158 | if (typeof input === "number") { 159 | return { x: input, y: input, z: input }; 160 | } 161 | return input; 162 | } 163 | 164 | function getValueNumberAnnotated(config: ConfigNumberAnnotated) { 165 | switch (config.type) { 166 | case "static": 167 | return new StaticUpdatableValue(config.value); 168 | case "static-random": 169 | return new StaticUpdatableValue( 170 | getRandomValue(config.minValue, config.maxValue) 171 | ); 172 | case "linear": 173 | return new LinearUpdatableValue(config.value, config.addValue); 174 | case "linear-random": 175 | return new LinearUpdatableValue( 176 | getRandomValue(config.minValue, config.maxValue), 177 | getRandomValue(config.minAddValue, config.maxAddValue) 178 | ); 179 | case "oscillating": 180 | return new OscillatingUpdatableValue( 181 | config.value, 182 | config.start, 183 | config.final, 184 | config.duration, 185 | config.direction, 186 | config.easingFunction 187 | ); 188 | case "oscillating-random": 189 | return new OscillatingUpdatableValue( 190 | getRandomValue(config.minValue, config.maxValue), 191 | getRandomValue(config.minStart, config.maxStart), 192 | getRandomValue(config.minFinal, config.maxFinal), 193 | getRandomValue(config.minDuration, config.maxDuration), 194 | getRandomDirection(config.minDirection, config.maxDirection), 195 | getRandomFromList(config.easingFunctions)[0] 196 | ); 197 | } 198 | } 199 | 200 | function getValueVector2Annotated(config: ConfigVector2Annotated) { 201 | switch (config.type) { 202 | case "static": { 203 | const value = getVector2(config.value); 204 | return new UpdatableVector2Value( 205 | new StaticUpdatableValue(value.x), 206 | new StaticUpdatableValue(value.y), 207 | config.uniformVectorValues 208 | ); 209 | } 210 | case "static-random": { 211 | const minValue = getVector2(config.minValue); 212 | const maxValue = getVector2(config.maxValue); 213 | return new UpdatableVector2Value( 214 | new StaticUpdatableValue(getRandomValue(minValue.x, maxValue.x)), 215 | new StaticUpdatableValue(getRandomValue(minValue.y, maxValue.y)), 216 | config.uniformVectorValues 217 | ); 218 | } 219 | case "linear": { 220 | const value = getVector2(config.value); 221 | const addValue = getVector2(config.addValue); 222 | return new UpdatableVector2Value( 223 | new LinearUpdatableValue(value.x, addValue.x), 224 | new LinearUpdatableValue(value.y, addValue.y), 225 | config.uniformVectorValues 226 | ); 227 | } 228 | case "linear-random": { 229 | const minValue = getVector2(config.minValue); 230 | const maxValue = getVector2(config.maxValue); 231 | const minAddValue = getVector2(config.minAddValue); 232 | const maxAddValue = getVector2(config.maxAddValue); 233 | return new UpdatableVector2Value( 234 | new LinearUpdatableValue( 235 | getRandomValue(minValue.x, maxValue.x), 236 | getRandomValue(minAddValue.x, maxAddValue.x) 237 | ), 238 | new LinearUpdatableValue( 239 | getRandomValue(minValue.y, maxValue.y), 240 | getRandomValue(minAddValue.x, maxAddValue.x) 241 | ), 242 | config.uniformVectorValues 243 | ); 244 | } 245 | case "oscillating": { 246 | const value = getVector2(config.value); 247 | const start = getVector2(config.start); 248 | const final = getVector2(config.final); 249 | const duration = getVector2(config.duration); 250 | const direction = getVector2(config.direction); 251 | return new UpdatableVector2Value( 252 | new OscillatingUpdatableValue( 253 | value.x, 254 | start.x, 255 | final.x, 256 | duration.x, 257 | direction.x, 258 | config.easingFunction 259 | ), 260 | new OscillatingUpdatableValue( 261 | value.y, 262 | start.y, 263 | final.y, 264 | duration.x, 265 | direction.y, 266 | config.easingFunction 267 | ), 268 | config.uniformVectorValues 269 | ); 270 | } 271 | case "oscillating-random": { 272 | const minValue = getVector2(config.minValue); 273 | const maxValue = getVector2(config.maxValue); 274 | const minStart = getVector2(config.minStart); 275 | const maxStart = getVector2(config.maxStart); 276 | const minFinal = getVector2(config.minFinal); 277 | const maxFinal = getVector2(config.maxFinal); 278 | const minDuration = getVector2(config.minDuration); 279 | const maxDuration = getVector2(config.maxDuration); 280 | const minDirection = getVector2(config.minDirection); 281 | const maxDirection = getVector2(config.maxDirection); 282 | return new UpdatableVector2Value( 283 | new OscillatingUpdatableValue( 284 | getRandomValue(minValue.x, maxValue.x), 285 | getRandomValue(minStart.x, maxStart.x), 286 | getRandomValue(minFinal.x, maxFinal.x), 287 | getRandomValue(minDuration.x, maxDuration.x), 288 | getRandomDirection(minDirection.x, maxDirection.x), 289 | getRandomFromList(config.easingFunctions)[0] 290 | ), 291 | new OscillatingUpdatableValue( 292 | getRandomValue(minValue.y, maxValue.y), 293 | getRandomValue(minStart.y, maxStart.y), 294 | getRandomValue(minFinal.y, maxFinal.y), 295 | getRandomValue(minDuration.y, maxDuration.y), 296 | getRandomDirection(minDirection.y, maxDirection.y), 297 | getRandomFromList(config.easingFunctions)[0] 298 | ), 299 | config.uniformVectorValues 300 | ); 301 | } 302 | } 303 | } 304 | 305 | function getValueVector3Annotated(config: ConfigVector3Annotated) { 306 | switch (config.type) { 307 | case "static": { 308 | const value = getVector3(config.value); 309 | return new UpdatableVector3Value( 310 | new StaticUpdatableValue(value.x), 311 | new StaticUpdatableValue(value.y), 312 | new StaticUpdatableValue(value.z), 313 | config.uniformVectorValues 314 | ); 315 | } 316 | case "static-random": { 317 | const minValue = getVector3(config.minValue); 318 | const maxValue = getVector3(config.maxValue); 319 | return new UpdatableVector3Value( 320 | new StaticUpdatableValue(getRandomValue(minValue.x, maxValue.x)), 321 | new StaticUpdatableValue(getRandomValue(minValue.y, maxValue.y)), 322 | new StaticUpdatableValue(getRandomValue(minValue.z, maxValue.z)), 323 | config.uniformVectorValues 324 | ); 325 | } 326 | case "linear": { 327 | const value = getVector3(config.value); 328 | const addValue = getVector3(config.addValue); 329 | return new UpdatableVector3Value( 330 | new LinearUpdatableValue(value.x, addValue.x), 331 | new LinearUpdatableValue(value.y, addValue.y), 332 | new LinearUpdatableValue(value.z, addValue.z), 333 | config.uniformVectorValues 334 | ); 335 | } 336 | case "linear-random": { 337 | const minValue = getVector3(config.minValue); 338 | const maxValue = getVector3(config.maxValue); 339 | const minAddValue = getVector3(config.minAddValue); 340 | const maxAddValue = getVector3(config.maxAddValue); 341 | return new UpdatableVector3Value( 342 | new LinearUpdatableValue( 343 | getRandomValue(minValue.x, maxValue.x), 344 | getRandomValue(minAddValue.x, maxAddValue.x) 345 | ), 346 | new LinearUpdatableValue( 347 | getRandomValue(minValue.y, maxValue.y), 348 | getRandomValue(minAddValue.y, maxAddValue.y) 349 | ), 350 | new LinearUpdatableValue( 351 | getRandomValue(minValue.z, maxValue.z), 352 | getRandomValue(minAddValue.z, maxAddValue.z) 353 | ), 354 | config.uniformVectorValues 355 | ); 356 | } 357 | case "oscillating": { 358 | const value = getVector3(config.value); 359 | const start = getVector3(config.start); 360 | const final = getVector3(config.final); 361 | const duration = getVector3(config.duration); 362 | const direction = getVector3(config.direction); 363 | return new UpdatableVector3Value( 364 | new OscillatingUpdatableValue( 365 | value.x, 366 | start.x, 367 | final.x, 368 | duration.x, 369 | direction.x, 370 | config.easingFunction 371 | ), 372 | new OscillatingUpdatableValue( 373 | value.y, 374 | start.y, 375 | final.y, 376 | duration.z, 377 | direction.y, 378 | config.easingFunction 379 | ), 380 | new OscillatingUpdatableValue( 381 | value.z, 382 | start.z, 383 | final.z, 384 | duration.z, 385 | direction.z, 386 | config.easingFunction 387 | ), 388 | config.uniformVectorValues 389 | ); 390 | } 391 | case "oscillating-random": { 392 | const minValue = getVector3(config.minValue); 393 | const maxValue = getVector3(config.maxValue); 394 | const minStart = getVector3(config.minStart); 395 | const maxStart = getVector3(config.maxStart); 396 | const minFinal = getVector3(config.minFinal); 397 | const maxFinal = getVector3(config.maxFinal); 398 | const minDuration = getVector3(config.minDuration); 399 | const maxDuration = getVector3(config.maxDuration); 400 | const minDirection = getVector3(config.minDirection); 401 | const maxDirection = getVector3(config.maxDirection); 402 | return new UpdatableVector3Value( 403 | new OscillatingUpdatableValue( 404 | getRandomValue(minValue.x, maxValue.x), 405 | getRandomValue(minStart.x, maxStart.x), 406 | getRandomValue(minFinal.x, maxFinal.x), 407 | getRandomValue(minDuration.x, maxDuration.x), 408 | getRandomDirection(minDirection.x, maxDirection.x), 409 | getRandomFromList(config.easingFunctions)[0] 410 | ), 411 | new OscillatingUpdatableValue( 412 | getRandomValue(minValue.y, maxValue.y), 413 | getRandomValue(minStart.y, maxStart.y), 414 | getRandomValue(minFinal.y, maxFinal.y), 415 | getRandomValue(minDuration.y, maxDuration.y), 416 | getRandomDirection(minDirection.y, maxDirection.y), 417 | getRandomFromList(config.easingFunctions)[0] 418 | ), 419 | new OscillatingUpdatableValue( 420 | getRandomValue(minValue.z, maxValue.z), 421 | getRandomValue(minStart.z, maxStart.z), 422 | getRandomValue(minFinal.z, maxFinal.z), 423 | getRandomValue(minDuration.z, maxDuration.z), 424 | getRandomDirection(minDirection.z, maxDirection.z), 425 | getRandomFromList(config.easingFunctions)[0] 426 | ), 427 | config.uniformVectorValues 428 | ); 429 | } 430 | } 431 | } 432 | 433 | function provideDefaults( 434 | args: CreateConfettiArgs, 435 | id: string 436 | ): CreateConfettiArgsFullInput { 437 | return { 438 | id, 439 | ...CREATE_CONFETTI_DEFAULTS, 440 | ...args, 441 | }; 442 | } 443 | 444 | function shouldColorizeSprite(sprite: SpriteProp) { 445 | if (typeof sprite === "string") { 446 | return true; 447 | } 448 | return sprite.colorize; 449 | } 450 | 451 | function spriteEquals(spriteA: Sprite, spriteB: SpriteProp) { 452 | if (typeof spriteB === "string") { 453 | return spriteA.src === spriteB && spriteA.colorize; 454 | } 455 | return spriteA.src === spriteB.src && spriteA.colorize === spriteB.colorize; 456 | } 457 | 458 | function getSpriteWithIndex( 459 | requestedSprite: SpriteProp | undefined, 460 | spriteCanvasData: SpriteCanvasData 461 | ): [SpriteProp, number] { 462 | if (requestedSprite != null) { 463 | const index = spriteCanvasData.sprites.findIndex((sprite) => 464 | spriteEquals(sprite, requestedSprite) 465 | ); 466 | if (index !== -1) { 467 | return [requestedSprite, index]; 468 | } 469 | } 470 | return getRandomFromList(spriteCanvasData.sprites); 471 | } 472 | 473 | function getColorIndex( 474 | sprite: SpriteProp, 475 | requestedColor: string | undefined | null, 476 | spriteCanvasData: SpriteCanvasData 477 | ) { 478 | if (!shouldColorizeSprite(sprite)) { 479 | return 0; 480 | } 481 | const index = 482 | requestedColor != null 483 | ? spriteCanvasData.colors.findIndex((color) => color === requestedColor) 484 | : -1; 485 | return index !== -1 486 | ? index 487 | : Math.floor(getRandomValue(0, spriteCanvasData.colors.length - 1)); 488 | } 489 | 490 | export function getUpdatableValueNumber(config: ConfigNumber) { 491 | return getValueNumberAnnotated({ ...config, valueType: "number" }); 492 | } 493 | 494 | export function getUpdatableValueVector2(config: ConfigVector2Input) { 495 | return getValueVector2Annotated({ ...config, valueType: "Vector2" }); 496 | } 497 | 498 | export function getUpdatableValueVector3(config: ConfigVector3Input) { 499 | return getValueVector3Annotated({ ...config, valueType: "Vector3" }); 500 | } 501 | 502 | export default function createConfetti( 503 | id: string, 504 | rawArgs: CreateConfettiArgs, 505 | spriteCanvasData: SpriteCanvasData, 506 | requestedSprite?: SpriteProp, 507 | requestedColor?: string | null 508 | ) { 509 | const args = provideDefaults(rawArgs, id); 510 | 511 | const [sprite, spriteIndex] = getSpriteWithIndex( 512 | requestedSprite, 513 | spriteCanvasData 514 | ); 515 | const colorIndex = getColorIndex( 516 | requestedSprite ?? sprite, 517 | requestedColor, 518 | spriteCanvasData 519 | ); 520 | 521 | return new Confetti({ 522 | id, 523 | position: getUpdatableValueVector2(args.position), 524 | velocity: getUpdatableValueVector2(args.velocity), 525 | rotation: getUpdatableValueVector3(args.rotation), 526 | dragCoefficient: getUpdatableValueVector2(args.dragCoefficient), 527 | size: getUpdatableValueVector2(args.size), 528 | opacity: getUpdatableValueNumber(args.opacity), 529 | airResistanceArea: getUpdatableValueVector2(args.airResistanceArea), 530 | spriteX: 531 | colorIndex * spriteCanvasData.spriteWidth + colorIndex * SPRITE_SPACING, 532 | spriteY: 533 | spriteIndex * spriteCanvasData.spriteHeight + 534 | spriteIndex * SPRITE_SPACING, 535 | spriteWidth: spriteCanvasData.spriteWidth, 536 | spriteHeight: spriteCanvasData.spriteHeight, 537 | }); 538 | } 539 | --------------------------------------------------------------------------------