@cazala/party 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,6 +12,7 @@ A high-performance TypeScript particle physics engine with dual runtime support
12
12
  - **Advanced Rendering**: Trails, particle instancing, line rendering with multiple color modes
13
13
  - **Export/Import Presets**: Export/import module settings (inputs + enabled state)
14
14
  - **Cross-platform**: Works in all modern browsers with automatic feature detection
15
+ - **Spawner Utility**: Generate particle shapes, including text and images
15
16
 
16
17
  ## Installation
17
18
 
@@ -189,6 +190,58 @@ engine.unpinParticles([0, 1, 2]);
189
190
  engine.unpinAll();
190
191
  ```
191
192
 
193
+ ### Spawner
194
+
195
+ Generate particle arrays from common shapes (including text and images) using `Spawner`:
196
+
197
+ ```typescript
198
+ import { Spawner } from "@cazala/party";
199
+
200
+ const spawner = new Spawner();
201
+ const particles = spawner.initParticles({
202
+ count: 5000,
203
+ shape: "text",
204
+ center: { x: 0, y: 0 },
205
+ position: { x: 0, y: 0 },
206
+ align: { horizontal: "center", vertical: "center" },
207
+ text: "Party",
208
+ font: "sans-serif",
209
+ textSize: 80,
210
+ size: 3,
211
+ mass: 1,
212
+ colors: ["#ffffff"],
213
+ });
214
+
215
+ engine.setParticles(particles);
216
+ ```
217
+
218
+ Notes:
219
+
220
+ - `size` controls particle radius; `textSize` is the font size used to rasterize text.
221
+ - Playground font options: `sans-serif`, `serif`, `monospace`.
222
+
223
+ Image example:
224
+
225
+ ```typescript
226
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
227
+ const imageParticles = spawner.initParticles({
228
+ count: 12000,
229
+ shape: "image",
230
+ center: { x: 0, y: 0 },
231
+ position: { x: 0, y: 0 },
232
+ align: { horizontal: "center", vertical: "center" },
233
+ imageData,
234
+ imageSize: 400, // scales to this max dimension
235
+ size: 3,
236
+ mass: 1,
237
+ });
238
+ ```
239
+
240
+ Notes:
241
+
242
+ - `imageData` must be provided synchronously (no URL fetching inside the spawner).
243
+ - Fully transparent pixels are skipped; particle colors come from image pixels.
244
+
192
245
  ### Modules
193
246
 
194
247
  Modules are pluggable components that contribute to simulation or rendering:
package/dist/index.js CHANGED
@@ -5431,6 +5431,7 @@ class Engine {
5431
5431
  }
5432
5432
  }
5433
5433
 
5434
+ const TEXT_SPAWNER_FONTS = ["sans-serif", "serif", "monospace"];
5434
5435
  function calculateVelocity(position, center, cfg) {
5435
5436
  if (!cfg || cfg.speed === 0)
5436
5437
  return { vx: 0, vy: 0 };
@@ -5472,7 +5473,7 @@ function calculateVelocity(position, center, cfg) {
5472
5473
  }
5473
5474
  class Spawner {
5474
5475
  initParticles(options) {
5475
- const { count, shape, center, spacing = 25, radius = 100, innerRadius = 50, squareSize = 200, cornerRadius = 0, size = 5, mass = 1, bounds, velocity, colors, } = options;
5476
+ const { count, shape, center, spacing = 25, radius = 100, innerRadius = 50, squareSize = 200, cornerRadius = 0, size = 5, mass = 1, bounds, velocity, colors, text = "Party", font = "sans-serif", textSize = 64, position, align, imageData, imageSize, } = options;
5476
5477
  const particles = [];
5477
5478
  if (count <= 0)
5478
5479
  return particles;
@@ -5499,6 +5500,186 @@ class Spawner {
5499
5500
  return { r: 1, g: 1, b: 1, a: 1 };
5500
5501
  return toColor(colors[Math.floor(Math.random() * colors.length)]);
5501
5502
  };
5503
+ if (shape === "text") {
5504
+ const textValue = text.trim();
5505
+ if (!textValue)
5506
+ return particles;
5507
+ const createCanvas = () => {
5508
+ if (typeof OffscreenCanvas !== "undefined") {
5509
+ return new OffscreenCanvas(1, 1);
5510
+ }
5511
+ if (typeof document !== "undefined") {
5512
+ return document.createElement("canvas");
5513
+ }
5514
+ return null;
5515
+ };
5516
+ const canvas = createCanvas();
5517
+ if (!canvas)
5518
+ return particles;
5519
+ const ctx = canvas.getContext("2d");
5520
+ if (!ctx)
5521
+ return particles;
5522
+ const fontSize = Math.max(1, Math.floor(textSize));
5523
+ const fontSpec = `${fontSize}px ${font}`;
5524
+ ctx.font = fontSpec;
5525
+ ctx.textBaseline = "alphabetic";
5526
+ ctx.textAlign = "left";
5527
+ const metrics = ctx.measureText(textValue);
5528
+ const ascent = metrics.actualBoundingBoxAscent || fontSize;
5529
+ const descent = metrics.actualBoundingBoxDescent || fontSize * 0.25;
5530
+ const left = metrics.actualBoundingBoxLeft || 0;
5531
+ const right = metrics.actualBoundingBoxRight || metrics.width;
5532
+ const textWidth = Math.max(1, Math.ceil(left + right));
5533
+ const textHeight = Math.max(1, Math.ceil(ascent + descent));
5534
+ const padding = Math.ceil(fontSize * 0.25);
5535
+ canvas.width = textWidth + padding * 2;
5536
+ canvas.height = textHeight + padding * 2;
5537
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
5538
+ ctx.font = fontSpec;
5539
+ ctx.textBaseline = "alphabetic";
5540
+ ctx.textAlign = "left";
5541
+ ctx.fillStyle = "#ffffff";
5542
+ ctx.fillText(textValue, padding + left, padding + ascent);
5543
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
5544
+ const data = imageData.data;
5545
+ const sampleStep = Math.max(1, Math.round(size));
5546
+ const points = [];
5547
+ for (let y = 0; y < canvas.height; y += sampleStep) {
5548
+ for (let x = 0; x < canvas.width; x += sampleStep) {
5549
+ const idx = (y * canvas.width + x) * 4 + 3;
5550
+ if (data[idx] > 0) {
5551
+ points.push({ x, y });
5552
+ }
5553
+ }
5554
+ }
5555
+ const maxCount = Math.min(count, points.length);
5556
+ if (maxCount <= 0)
5557
+ return particles;
5558
+ const textPosition = position ?? center;
5559
+ const horizontal = align?.horizontal ?? "center";
5560
+ const vertical = align?.vertical ?? "center";
5561
+ const originX = horizontal === "left"
5562
+ ? textPosition.x
5563
+ : horizontal === "right"
5564
+ ? textPosition.x - textWidth
5565
+ : textPosition.x - textWidth / 2;
5566
+ const originY = vertical === "top"
5567
+ ? textPosition.y
5568
+ : vertical === "bottom"
5569
+ ? textPosition.y - textHeight
5570
+ : textPosition.y - textHeight / 2;
5571
+ const stride = points.length / maxCount;
5572
+ for (let i = 0; i < maxCount; i++) {
5573
+ const idx = Math.floor(i * stride);
5574
+ const point = points[idx];
5575
+ if (!point)
5576
+ continue;
5577
+ const x = originX + (point.x - padding);
5578
+ const y = originY + (point.y - padding);
5579
+ const { vx, vy } = calculateVelocity({ x, y }, textPosition, velocity);
5580
+ particles.push({
5581
+ position: { x, y },
5582
+ velocity: { x: vx, y: vy },
5583
+ size,
5584
+ mass,
5585
+ color: getColor(),
5586
+ });
5587
+ }
5588
+ return particles;
5589
+ }
5590
+ if (shape === "image") {
5591
+ if (!imageData)
5592
+ return particles;
5593
+ const width = Math.floor(imageData.width);
5594
+ const height = Math.floor(imageData.height);
5595
+ if (!Number.isFinite(width) ||
5596
+ !Number.isFinite(height) ||
5597
+ width <= 0 ||
5598
+ height <= 0)
5599
+ return particles;
5600
+ const data = imageData.data;
5601
+ const sampleStepTarget = Math.max(1, Math.round(size));
5602
+ const targetSize = typeof imageSize === "number" && Number.isFinite(imageSize) && imageSize > 0
5603
+ ? imageSize
5604
+ : Math.max(width, height);
5605
+ const scale = Math.max(0.0001, targetSize / Math.max(width, height));
5606
+ const sampleStepSource = Math.max(1, Math.round(sampleStepTarget / scale));
5607
+ const points = [];
5608
+ for (let y = 0; y < height; y += sampleStepSource) {
5609
+ for (let x = 0; x < width; x += sampleStepSource) {
5610
+ const idx = (y * width + x) * 4;
5611
+ const alpha = data[idx + 3];
5612
+ if (alpha === 0)
5613
+ continue;
5614
+ points.push({
5615
+ x: x * scale,
5616
+ y: y * scale,
5617
+ color: {
5618
+ r: data[idx] / 255,
5619
+ g: data[idx + 1] / 255,
5620
+ b: data[idx + 2] / 255,
5621
+ a: alpha / 255,
5622
+ },
5623
+ });
5624
+ }
5625
+ }
5626
+ const maxCount = Math.max(0, Math.floor(count));
5627
+ if (maxCount <= 0 || points.length === 0)
5628
+ return particles;
5629
+ const imagePosition = position ?? center;
5630
+ const horizontal = align?.horizontal ?? "center";
5631
+ const vertical = align?.vertical ?? "center";
5632
+ const scaledWidth = width * scale;
5633
+ const scaledHeight = height * scale;
5634
+ const originX = horizontal === "left"
5635
+ ? imagePosition.x
5636
+ : horizontal === "right"
5637
+ ? imagePosition.x - scaledWidth
5638
+ : imagePosition.x - scaledWidth / 2;
5639
+ const originY = vertical === "top"
5640
+ ? imagePosition.y
5641
+ : vertical === "bottom"
5642
+ ? imagePosition.y - scaledHeight
5643
+ : imagePosition.y - scaledHeight / 2;
5644
+ const baseCount = Math.min(maxCount, points.length);
5645
+ const stride = points.length / baseCount;
5646
+ for (let i = 0; i < baseCount; i++) {
5647
+ const idx = Math.floor(i * stride);
5648
+ const point = points[idx];
5649
+ if (!point)
5650
+ continue;
5651
+ const x = originX + point.x;
5652
+ const y = originY + point.y;
5653
+ const { vx, vy } = calculateVelocity({ x, y }, imagePosition, velocity);
5654
+ particles.push({
5655
+ position: { x, y },
5656
+ velocity: { x: vx, y: vy },
5657
+ size,
5658
+ mass,
5659
+ color: point.color,
5660
+ });
5661
+ }
5662
+ const extraCount = maxCount - baseCount;
5663
+ if (extraCount > 0) {
5664
+ const jitter = sampleStepTarget * 0.4;
5665
+ for (let i = 0; i < extraCount; i++) {
5666
+ const point = points[Math.floor(Math.random() * points.length)];
5667
+ if (!point)
5668
+ continue;
5669
+ const x = originX + point.x + (Math.random() - 0.5) * jitter;
5670
+ const y = originY + point.y + (Math.random() - 0.5) * jitter;
5671
+ const { vx, vy } = calculateVelocity({ x, y }, imagePosition, velocity);
5672
+ particles.push({
5673
+ position: { x, y },
5674
+ velocity: { x: vx, y: vy },
5675
+ size,
5676
+ mass,
5677
+ color: point.color,
5678
+ });
5679
+ }
5680
+ }
5681
+ return particles;
5682
+ }
5502
5683
  if (shape === "grid") {
5503
5684
  const cols = Math.ceil(Math.sqrt(count));
5504
5685
  const rows = Math.ceil(count / cols);
@@ -10392,5 +10573,5 @@ class Particles extends Module {
10392
10573
  }
10393
10574
  }
10394
10575
 
10395
- export { AbstractEngine, Behavior, Boundary, CanvasComposition, Collisions, DEFAULT_BEHAVIOR_ALIGNMENT, DEFAULT_BEHAVIOR_AVOID, DEFAULT_BEHAVIOR_CHASE, DEFAULT_BEHAVIOR_COHESION, DEFAULT_BEHAVIOR_REPULSION, DEFAULT_BEHAVIOR_SEPARATION, DEFAULT_BEHAVIOR_VIEW_ANGLE, DEFAULT_BEHAVIOR_VIEW_RADIUS, DEFAULT_BEHAVIOR_WANDER, DEFAULT_BOUNDARY_FRICTION, DEFAULT_BOUNDARY_MODE, DEFAULT_BOUNDARY_REPEL_DISTANCE, DEFAULT_BOUNDARY_REPEL_STRENGTH, DEFAULT_BOUNDARY_RESTITUTION, DEFAULT_COLLISIONS_RESTITUTION, DEFAULT_ENVIRONMENT_DAMPING, DEFAULT_ENVIRONMENT_FRICTION, DEFAULT_ENVIRONMENT_GRAVITY_ANGLE, DEFAULT_ENVIRONMENT_GRAVITY_DIRECTION, DEFAULT_ENVIRONMENT_GRAVITY_STRENGTH, DEFAULT_ENVIRONMENT_INERTIA, DEFAULT_FLUIDS_ENABLE_NEAR_PRESSURE, DEFAULT_FLUIDS_INFLUENCE_RADIUS, DEFAULT_FLUIDS_MAX_ACCELERATION, DEFAULT_FLUIDS_NEAR_PRESSURE_MULTIPLIER, DEFAULT_FLUIDS_NEAR_THRESHOLD, DEFAULT_FLUIDS_PRESSURE_MULTIPLIER, DEFAULT_FLUIDS_TARGET_DENSITY, DEFAULT_FLUIDS_VISCOSITY, DEFAULT_GRAB_GRABBED_INDEX, DEFAULT_GRAB_POSITION_X, DEFAULT_GRAB_POSITION_Y, DEFAULT_INTERACTION_MODE, DEFAULT_INTERACTION_RADIUS, DEFAULT_INTERACTION_STRENGTH, DEFAULT_JOINTS_ENABLE_JOINT_COLLISIONS, DEFAULT_JOINTS_ENABLE_PARTICLE_COLLISIONS, DEFAULT_JOINTS_FRICTION, DEFAULT_JOINTS_MOMENTUM, DEFAULT_JOINTS_RESTITUTION, DEFAULT_JOINTS_SEPARATION, DEFAULT_JOINTS_STEPS, DEFAULT_PICFLIP_DENSITY, DEFAULT_PICFLIP_FLIP_RATIO, DEFAULT_PICFLIP_INFLUENCE_RADIUS, DEFAULT_PICFLIP_INTERNAL_MAX_ACCELERATION, DEFAULT_PICFLIP_PRESSURE, DEFAULT_PICFLIP_PRESSURE_MULTIPLIER, DEFAULT_PICFLIP_RADIUS, DEFAULT_PICFLIP_TARGET_DENSITY, DEFAULT_SENSORS_COLOR_SIMILARITY_THRESHOLD, DEFAULT_SENSORS_FLEE_ANGLE, DEFAULT_SENSORS_FLEE_BEHAVIOR, DEFAULT_SENSORS_FOLLOW_BEHAVIOR, DEFAULT_SENSORS_SENSOR_ANGLE, DEFAULT_SENSORS_SENSOR_DISTANCE, DEFAULT_SENSORS_SENSOR_RADIUS, DEFAULT_SENSORS_SENSOR_STRENGTH, DEFAULT_SENSORS_SENSOR_THRESHOLD, DEFAULT_TRAILS_TRAIL_DECAY, DEFAULT_TRAILS_TRAIL_DIFFUSE, DataType, Engine, Environment, Fluids, FluidsMethod, Grab, Interaction, Joints, Lines, Module, ModuleRole, OscillatorManager, Particles, ParticlesColorType, RenderPassKind, Sensors, Spawner, Trails, Vector, degToRad, radToDeg };
10576
+ export { AbstractEngine, Behavior, Boundary, CanvasComposition, Collisions, DEFAULT_BEHAVIOR_ALIGNMENT, DEFAULT_BEHAVIOR_AVOID, DEFAULT_BEHAVIOR_CHASE, DEFAULT_BEHAVIOR_COHESION, DEFAULT_BEHAVIOR_REPULSION, DEFAULT_BEHAVIOR_SEPARATION, DEFAULT_BEHAVIOR_VIEW_ANGLE, DEFAULT_BEHAVIOR_VIEW_RADIUS, DEFAULT_BEHAVIOR_WANDER, DEFAULT_BOUNDARY_FRICTION, DEFAULT_BOUNDARY_MODE, DEFAULT_BOUNDARY_REPEL_DISTANCE, DEFAULT_BOUNDARY_REPEL_STRENGTH, DEFAULT_BOUNDARY_RESTITUTION, DEFAULT_COLLISIONS_RESTITUTION, DEFAULT_ENVIRONMENT_DAMPING, DEFAULT_ENVIRONMENT_FRICTION, DEFAULT_ENVIRONMENT_GRAVITY_ANGLE, DEFAULT_ENVIRONMENT_GRAVITY_DIRECTION, DEFAULT_ENVIRONMENT_GRAVITY_STRENGTH, DEFAULT_ENVIRONMENT_INERTIA, DEFAULT_FLUIDS_ENABLE_NEAR_PRESSURE, DEFAULT_FLUIDS_INFLUENCE_RADIUS, DEFAULT_FLUIDS_MAX_ACCELERATION, DEFAULT_FLUIDS_NEAR_PRESSURE_MULTIPLIER, DEFAULT_FLUIDS_NEAR_THRESHOLD, DEFAULT_FLUIDS_PRESSURE_MULTIPLIER, DEFAULT_FLUIDS_TARGET_DENSITY, DEFAULT_FLUIDS_VISCOSITY, DEFAULT_GRAB_GRABBED_INDEX, DEFAULT_GRAB_POSITION_X, DEFAULT_GRAB_POSITION_Y, DEFAULT_INTERACTION_MODE, DEFAULT_INTERACTION_RADIUS, DEFAULT_INTERACTION_STRENGTH, DEFAULT_JOINTS_ENABLE_JOINT_COLLISIONS, DEFAULT_JOINTS_ENABLE_PARTICLE_COLLISIONS, DEFAULT_JOINTS_FRICTION, DEFAULT_JOINTS_MOMENTUM, DEFAULT_JOINTS_RESTITUTION, DEFAULT_JOINTS_SEPARATION, DEFAULT_JOINTS_STEPS, DEFAULT_PICFLIP_DENSITY, DEFAULT_PICFLIP_FLIP_RATIO, DEFAULT_PICFLIP_INFLUENCE_RADIUS, DEFAULT_PICFLIP_INTERNAL_MAX_ACCELERATION, DEFAULT_PICFLIP_PRESSURE, DEFAULT_PICFLIP_PRESSURE_MULTIPLIER, DEFAULT_PICFLIP_RADIUS, DEFAULT_PICFLIP_TARGET_DENSITY, DEFAULT_SENSORS_COLOR_SIMILARITY_THRESHOLD, DEFAULT_SENSORS_FLEE_ANGLE, DEFAULT_SENSORS_FLEE_BEHAVIOR, DEFAULT_SENSORS_FOLLOW_BEHAVIOR, DEFAULT_SENSORS_SENSOR_ANGLE, DEFAULT_SENSORS_SENSOR_DISTANCE, DEFAULT_SENSORS_SENSOR_RADIUS, DEFAULT_SENSORS_SENSOR_STRENGTH, DEFAULT_SENSORS_SENSOR_THRESHOLD, DEFAULT_TRAILS_TRAIL_DECAY, DEFAULT_TRAILS_TRAIL_DIFFUSE, DataType, Engine, Environment, Fluids, FluidsMethod, Grab, Interaction, Joints, Lines, Module, ModuleRole, OscillatorManager, Particles, ParticlesColorType, RenderPassKind, Sensors, Spawner, TEXT_SPAWNER_FONTS, Trails, Vector, degToRad, radToDeg };
10396
10577
  //# sourceMappingURL=index.js.map