diff --git a/src/entity/BaseEntity.ts b/src/entity/BaseEntity.ts index f57868a..baff969 100644 --- a/src/entity/BaseEntity.ts +++ b/src/entity/BaseEntity.ts @@ -7,6 +7,8 @@ export default abstract class BaseEntity { radius: number; alpha: number; lineWidth?: number; + endPos?: { x: number; y: number }; + endRotation?: number; constructor( ctx: CanvasRenderingContext2D, diff --git a/src/factory.ts b/src/factory.ts index f4aa750..ad0bc19 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -6,7 +6,7 @@ import Circle from "./entity/Circle"; import Polygon from "./entity/Polygon"; import Star from "./entity/Star"; import { ParticleOptions, StarOptions, PolygonOptions } from "./types"; -import { formatAlpha, sample } from "./utils"; +import { formatAlpha, sample, setEndPos, setEndRotation } from "./utils"; const ENTITY_MAP = { circle: Circle, @@ -59,6 +59,9 @@ const preProcess = ( // @ts-expect-error const p = new shapeType(...shapeArgs, sample(lineWidth)); + setEndPos(p, particle); + setEndRotation(p, particle); + shapes.push(p); } return shapes; diff --git a/src/index.ts b/src/index.ts index f43c087..9408a53 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,8 @@ import type { RotateOptions, FireworkOptions, ParticleOptions, + Move, + MoveOptions, } from "./types"; import { formatAlpha, hasAncestor, sample } from "./utils"; import BaseEntity from "./entity/BaseEntity"; @@ -48,31 +50,23 @@ const setParticleMovement = ( x: number, y: number ) => { - let { move, moveOptions } = particle; - if (!Array.isArray(move)) { - move = [move]; - } - if (!moveOptions) moveOptions = []; - if (!Array.isArray(moveOptions)) { - moveOptions = [moveOptions]; - } + const { move, moveOptions } = particle as { + move: Move[], + moveOptions: MoveOptions[], + }; let dist: Record = {}; move.forEach((m, i) => { if (m === "emit") { const { - emitRadius = [50, 180], radius = 0.1, alphaChange = false, alphaEasing = "linear", alphaDuration = [600, 800], alpha = 0, } = (moveOptions[i] as EmitOptions) ?? {}; - const emitAngle = (anime.random(0, 360) * Math.PI) / 180; - const sampledEmitRadius = - [-1, 1][anime.random(0, 1)] * sample(emitRadius); dist = { - x: () => x + sampledEmitRadius * Math.cos(emitAngle), - y: () => y + sampledEmitRadius * Math.sin(emitAngle), + x: (p: BaseEntity) => p.endPos!.x, + y: (p: BaseEntity) => p.endPos!.y, radius: sample(radius), }; if (alphaChange) { @@ -100,8 +94,7 @@ const setParticleMovement = ( }, }; } else if (m === "rotate") { - const { angle = [-180, 180] } = (moveOptions[i] as RotateOptions) ?? {}; - dist.rotation = () => sample(angle); + dist.rotation = (p: BaseEntity) => p.endRotation!; } }); return dist; @@ -152,6 +145,13 @@ const animateParticles = (x: number, y: number): void => { const { particles } = globalOptions; const timeLine = anime().timeline(); particles.forEach((particle) => { + if (!Array.isArray(particle.move)) { + particle.move = [particle.move]; + } + if (!particle.moveOptions) particle.moveOptions = []; + if (!Array.isArray(particle.moveOptions)) { + particle.moveOptions = [particle.moveOptions]; + } timeLine.add({ targets: entityFactory(ctx, x, y, particle), duration: sample(particle.duration), diff --git a/src/types.ts b/src/types.ts index 1cd70e2..e7f553a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -35,12 +35,12 @@ export type RotateOptions = { angle?: number | [number, number]; // default [-180, 180] }; -type Move = "emit" | "diffuse" | "rotate"; -type moveOptions = EmitOptions | DiffuseOptions | RotateOptions; +export type Move = "emit" | "diffuse" | "rotate"; +export type MoveOptions = EmitOptions | DiffuseOptions | RotateOptions; -interface BaseParticleOptions { +export interface BaseParticleOptions { move: Move | Move[]; - moveOptions?: moveOptions | moveOptions[]; + moveOptions?: MoveOptions | MoveOptions[]; easing?: EasingTypes; colors: string[]; number: number | [number, number]; diff --git a/src/utils.ts b/src/utils.ts index 0ab8b31..c54fdfa 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,7 @@ import anime from "theme-shokax-anime"; // import anime from "./anime"; +import BaseEntity from "./entity/BaseEntity"; +import { ParticleOptions, EmitOptions, RotateOptions } from "./types"; export const sample = (raw: number | [number, number]): number => { return Array.isArray(raw) ? anime.random(raw[0], raw[1]) : raw; @@ -14,9 +16,34 @@ export const hasAncestor = (node: Element, name: string): boolean => { return false; }; -export const formatAlpha = (alpha: number | [number, number]): [number, number] => { +export const setEndPos = (p: BaseEntity, particle: ParticleOptions) => { + let index; + if ((index = particle.move.indexOf("emit")) !== -1) { + const { emitRadius = [50, 180] } = + (particle.moveOptions as EmitOptions[])[index] ?? {}; + const angle = (anime.random(0, 360) * Math.PI) / 180; + const radius = [-1, 1][anime.random(0, 1)] * sample(emitRadius); + p.endPos = { + x: p.x + radius * Math.cos(angle), + y: p.y + radius * Math.sin(angle), + }; + } +}; + +export const setEndRotation = (p: BaseEntity, particle: ParticleOptions) => { + let index; + if ((index = particle.move.indexOf("rotate")) !== -1) { + const { angle = [-180, 180] } = + (particle.moveOptions as RotateOptions[])[index] ?? {}; + p.endRotation = sample(angle); + } +}; + +export const formatAlpha = ( + alpha: number | [number, number] +): [number, number] => { if (Array.isArray(alpha)) { return alpha.map((a) => a * 100) as [number, number]; } return [alpha * 100, alpha * 100]; -} \ No newline at end of file +}; diff --git a/test/index.ts b/test/index.ts index 8b50485..6b4a4f5 100644 --- a/test/index.ts +++ b/test/index.ts @@ -434,7 +434,7 @@ describe("firework", () => { const rotateSpy = sinon.spy(); mockCanvas.rotate = (...args) => { rotateSpy(...args); - rotateSpy.args[i][0].should.eql(Math.PI / 5 * i); + rotateSpy.args[i][0].should.eql((Math.PI / 5) * i); i++; }; @@ -468,4 +468,114 @@ describe("firework", () => { document.dispatchEvent(new window.MouseEvent("click", { bubbles: true })); await wait(1200); }); + + it("move with string type", async () => { + let i = 0; + const translateSpy = sinon.spy(); + const arcSpy = sinon.spy(); + mockCanvas.rotate = () => {}; + mockCanvas.translate = translateSpy; + mockCanvas.arc = (...args) => { + arcSpy(...args); + const radius = + translateSpy.args[i][0] ** 2 + translateSpy.args[i][1] ** 2; + (Math.abs(radius - (i * 2) ** 2) < 1e-10).should.be.true; + arcSpy.args[i][2].should.eql(10 - 2 * i); + if (i < 3) { + mockCanvas.globalAlpha.should.eql(1 - i / 3); + } + i++; + }; + + Date.now = () => i * 200; + global.requestAnimationFrame = (cb) => { + if (i > 5) { + global.requestAnimationFrame = () => 0; + } + setTimeout(cb, 0); + return 0; + }; + const { default: firework } = await import("../src/index"); + firework({ + excludeElements: ["button"], + particles: [ + { + shape: "circle", + move: "emit", + colors: ["rgba(255,182,185)"], + number: 1, + duration: 1000, + shapeOptions: { + radius: 10, + }, + moveOptions: { + emitRadius: 10, + radius: 0, + alphaChange: true, + alpha: 0, + alphaEasing: "linear", + alphaDuration: 600, + }, + }, + ], + }); + document.dispatchEvent(new window.MouseEvent("click", { bubbles: true })); + await wait(1200); + }); + + it("moveOptions with array type", async () => { + let i = 0; + const translateSpy = sinon.spy(); + const arcSpy = sinon.spy(); + mockCanvas.rotate = () => {}; + mockCanvas.translate = translateSpy; + mockCanvas.arc = (...args) => { + arcSpy(...args); + const radius = + translateSpy.args[i][0] ** 2 + translateSpy.args[i][1] ** 2; + (Math.abs(radius - (i * 2) ** 2) < 1e-10).should.be.true; + arcSpy.args[i][2].should.eql(10 - 2 * i); + if (i < 3) { + mockCanvas.globalAlpha.should.eql(1 - i / 3); + } + i++; + }; + + Date.now = () => i * 200; + global.requestAnimationFrame = (cb) => { + if (i > 5) { + global.requestAnimationFrame = () => 0; + } + setTimeout(cb, 0); + return 0; + }; + const { default: firework } = await import("../src/index"); + firework({ + excludeElements: ["button"], + particles: [ + { + shape: "circle", + move: "emit", + colors: ["rgba(255,182,185)"], + number: 1, + duration: 1000, + shapeOptions: { + radius: 10, + }, + moveOptions: [ + { + emitRadius: 10, + radius: 0, + alphaChange: true, + alpha: 0, + alphaEasing: "linear", + alphaDuration: 600, + }, + ], + }, + ], + }); + document.dispatchEvent(new window.MouseEvent("click", { bubbles: true })); + await wait(1200); + }); });