@@ -28,7 +28,8 @@ export type SpawnShape =
2828 | "circle"
2929 | "donut"
3030 | "square"
31- | "text" ;
31+ | "text"
32+ | "image" ;
3233
3334export type TextHorizontalAlign = "left" | "center" | "right" ;
3435export type TextVerticalAlign = "top" | "center" | "bottom" ;
@@ -56,6 +57,9 @@ export interface SpawnOptions {
5657 textSize ?: number ;
5758 position ?: { x : number ; y : number } ;
5859 align ?: { horizontal : TextHorizontalAlign ; vertical : TextVerticalAlign } ;
60+ // image
61+ imageData ?: ImageData | null ;
62+ imageSize ?: number ;
5963}
6064
6165function calculateVelocity (
@@ -125,6 +129,8 @@ export class Spawner {
125129 textSize = 64 ,
126130 position,
127131 align,
132+ imageData,
133+ imageSize,
128134 } = options ;
129135
130136 const particles : IParticle [ ] = [ ] ;
@@ -258,6 +264,119 @@ export class Spawner {
258264 return particles ;
259265 }
260266
267+ if ( shape === "image" ) {
268+ if ( ! imageData ) return particles ;
269+ const width = Math . floor ( imageData . width ) ;
270+ const height = Math . floor ( imageData . height ) ;
271+ if (
272+ ! Number . isFinite ( width ) ||
273+ ! Number . isFinite ( height ) ||
274+ width <= 0 ||
275+ height <= 0
276+ )
277+ return particles ;
278+
279+ const data = imageData . data ;
280+ const sampleStepTarget = Math . max ( 1 , Math . round ( size ) ) ;
281+ const targetSize =
282+ typeof imageSize === "number" && Number . isFinite ( imageSize ) && imageSize > 0
283+ ? imageSize
284+ : Math . max ( width , height ) ;
285+ const scale = Math . max ( 0.0001 , targetSize / Math . max ( width , height ) ) ;
286+ const sampleStepSource = Math . max ( 1 , Math . round ( sampleStepTarget / scale ) ) ;
287+ const points : {
288+ x : number ;
289+ y : number ;
290+ color : { r : number ; g : number ; b : number ; a : number } ;
291+ } [ ] = [ ] ;
292+
293+ for ( let y = 0 ; y < height ; y += sampleStepSource ) {
294+ for ( let x = 0 ; x < width ; x += sampleStepSource ) {
295+ const idx = ( y * width + x ) * 4 ;
296+ const alpha = data [ idx + 3 ] ;
297+ if ( alpha === 0 ) continue ;
298+ points . push ( {
299+ x : x * scale ,
300+ y : y * scale ,
301+ color : {
302+ r : data [ idx ] / 255 ,
303+ g : data [ idx + 1 ] / 255 ,
304+ b : data [ idx + 2 ] / 255 ,
305+ a : alpha / 255 ,
306+ } ,
307+ } ) ;
308+ }
309+ }
310+
311+ const maxCount = Math . max ( 0 , Math . floor ( count ) ) ;
312+ if ( maxCount <= 0 || points . length === 0 ) return particles ;
313+
314+ const imagePosition = position ?? center ;
315+ const horizontal = align ?. horizontal ?? "center" ;
316+ const vertical = align ?. vertical ?? "center" ;
317+ const scaledWidth = width * scale ;
318+ const scaledHeight = height * scale ;
319+ const originX =
320+ horizontal === "left"
321+ ? imagePosition . x
322+ : horizontal === "right"
323+ ? imagePosition . x - scaledWidth
324+ : imagePosition . x - scaledWidth / 2 ;
325+ const originY =
326+ vertical === "top"
327+ ? imagePosition . y
328+ : vertical === "bottom"
329+ ? imagePosition . y - scaledHeight
330+ : imagePosition . y - scaledHeight / 2 ;
331+
332+ const baseCount = Math . min ( maxCount , points . length ) ;
333+ const stride = points . length / baseCount ;
334+ for ( let i = 0 ; i < baseCount ; i ++ ) {
335+ const idx = Math . floor ( i * stride ) ;
336+ const point = points [ idx ] ;
337+ if ( ! point ) continue ;
338+ const x = originX + point . x ;
339+ const y = originY + point . y ;
340+ const { vx, vy } = calculateVelocity (
341+ { x, y } ,
342+ imagePosition ,
343+ velocity
344+ ) ;
345+ particles . push ( {
346+ position : { x, y } ,
347+ velocity : { x : vx , y : vy } ,
348+ size,
349+ mass,
350+ color : point . color ,
351+ } ) ;
352+ }
353+
354+ const extraCount = maxCount - baseCount ;
355+ if ( extraCount > 0 ) {
356+ const jitter = sampleStepTarget * 0.4 ;
357+ for ( let i = 0 ; i < extraCount ; i ++ ) {
358+ const point = points [ Math . floor ( Math . random ( ) * points . length ) ] ;
359+ if ( ! point ) continue ;
360+ const x = originX + point . x + ( Math . random ( ) - 0.5 ) * jitter ;
361+ const y = originY + point . y + ( Math . random ( ) - 0.5 ) * jitter ;
362+ const { vx, vy } = calculateVelocity (
363+ { x, y } ,
364+ imagePosition ,
365+ velocity
366+ ) ;
367+ particles . push ( {
368+ position : { x, y } ,
369+ velocity : { x : vx , y : vy } ,
370+ size,
371+ mass,
372+ color : point . color ,
373+ } ) ;
374+ }
375+ }
376+
377+ return particles ;
378+ }
379+
261380 if ( shape === "grid" ) {
262381 const cols = Math . ceil ( Math . sqrt ( count ) ) ;
263382 const rows = Math . ceil ( count / cols ) ;
0 commit comments