starflock 0.2.3 → 0.2.4

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/index.d.ts CHANGED
@@ -24,6 +24,8 @@ export declare class Node {
24
24
  angle?: number
25
25
  angularVelocity?: number
26
26
  shape?: string | ShapeFn
27
+ /** @internal assigned by World for spatial index lookups */
28
+ _index: number
27
29
  constructor(opts: NodeOptions)
28
30
  }
29
31
 
@@ -174,4 +176,4 @@ export interface AttractOptions {
174
176
  export declare function attract(opts?: AttractOptions): Force
175
177
 
176
178
  // React adapter
177
- export declare function useCosmograph(options?: Omit<WorldOptions, 'canvas'>): React.RefObject<HTMLCanvasElement>
179
+ export declare function useStarflock(options?: Omit<WorldOptions, 'canvas'>): import('react').RefObject<HTMLCanvasElement>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starflock",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Lightweight, zero-dependency canvas particle library. Composable forces, configurable everything.",
5
5
  "keywords": ["canvas", "particles", "animation", "constellation", "webgl", "background", "force", "interactive"],
6
6
  "author": "Corvin Burmeister",
package/src/Node.js CHANGED
@@ -9,5 +9,6 @@ export class Node {
9
9
  this.phase = phase
10
10
  this.twinkleSpeed = twinkleSpeed
11
11
  this.brightness = 1
12
+ this._index = 0
12
13
  }
13
14
  }
package/src/World.js CHANGED
@@ -21,6 +21,7 @@ function hexToRgb(hex) {
21
21
  }
22
22
 
23
23
  function lerpColor(colors, t) {
24
+ if (colors.length === 0) return '#ffffff'
24
25
  if (colors.length === 1) return colors[0]
25
26
  const scaled = Math.max(0, Math.min(1, t)) * (colors.length - 1)
26
27
  const i = Math.min(Math.floor(scaled), colors.length - 2)
@@ -110,6 +111,7 @@ export class World {
110
111
  this.ctx = canvas.getContext('2d')
111
112
  this.forces = forces
112
113
  this.options = { ...DEFAULTS, ...options }
114
+ this._edgeColorsExplicit = !!options.edgeColors
113
115
  if (!this.options.edgeColors) {
114
116
  this.options.edgeColors = this.options.colors
115
117
  }
@@ -118,6 +120,7 @@ export class World {
118
120
  this.raf = null
119
121
  this.mouse = null
120
122
  this.scrollY = 0
123
+ this._started = false
121
124
  this._hoveredNode = null
122
125
 
123
126
  this._onMouseMove = this._onMouseMove.bind(this)
@@ -221,8 +224,10 @@ export class World {
221
224
  my = e.clientY + window.scrollY
222
225
  } else {
223
226
  const rect = this.canvas.getBoundingClientRect()
224
- mx = e.clientX - rect.left
225
- my = e.clientY - rect.top
227
+ const scaleX = this.canvas.width / rect.width
228
+ const scaleY = this.canvas.height / rect.height
229
+ mx = (e.clientX - rect.left) * scaleX
230
+ my = (e.clientY - rect.top) * scaleY
226
231
  }
227
232
  this.mouse = { x: mx, y: my }
228
233
 
@@ -266,8 +271,10 @@ export class World {
266
271
  my = e.clientY + window.scrollY
267
272
  } else {
268
273
  const rect = this.canvas.getBoundingClientRect()
269
- mx = e.clientX - rect.left
270
- my = e.clientY - rect.top
274
+ const scaleX = this.canvas.width / rect.width
275
+ const scaleY = this.canvas.height / rect.height
276
+ mx = (e.clientX - rect.left) * scaleX
277
+ my = (e.clientY - rect.top) * scaleY
271
278
  }
272
279
  for (const node of this.nodes) {
273
280
  if (Math.hypot(node.x - mx, node.y - my) < node.r * 3) {
@@ -283,15 +290,18 @@ export class World {
283
290
 
284
291
  update(newOptions) {
285
292
  Object.assign(this.options, newOptions)
286
- if (newOptions.colors && !newOptions.edgeColors) {
293
+ if (newOptions.colors && !newOptions.edgeColors && !this._edgeColorsExplicit) {
287
294
  this.options.edgeColors = newOptions.colors
288
295
  }
296
+ if (newOptions.edgeColors) {
297
+ this._edgeColorsExplicit = true
298
+ }
289
299
  if (newOptions.forces !== undefined) {
290
300
  this.forces = newOptions.forces
291
301
  }
292
302
  }
293
303
 
294
- _resolveEdgeColor(ctx, a, b, i, j, edgeColors) {
304
+ _resolveEdgeColor(a, b, i, j, edgeColors) {
295
305
  const mode = this.options.edgeColorMode
296
306
  if (typeof mode === 'function') return mode(a, b, i, j)
297
307
  if (mode === 'source') return a.color
@@ -305,7 +315,7 @@ export class World {
305
315
  if (dist >= edgeMaxDist) return false
306
316
 
307
317
  const opacity = (1 - dist / edgeMaxDist) * edgeMaxOpacity
308
- const color = this._resolveEdgeColor(ctx, a, b, i, j, edgeColors)
318
+ const color = this._resolveEdgeColor(a, b, i, j, edgeColors)
309
319
 
310
320
  ctx.beginPath()
311
321
  ctx.moveTo(a.x, a.y)
@@ -352,29 +362,31 @@ export class World {
352
362
 
353
363
  if (opts.edgeStyle === 'dashed') ctx.setLineDash([4, 6])
354
364
 
355
- if (spatialIndex) {
356
- const qt = new QuadTree(-1, -1, width + 2, height + 2)
357
- for (const node of nodes) qt.insert(node)
358
-
359
- for (let i = 0; i < nodes.length; i++) {
360
- if (maxEdgesPerFrame !== null && totalEdges >= maxEdgesPerFrame) return
361
- if (edgeCounts && maxEdgesPerNode !== null && edgeCounts[i] >= maxEdgesPerNode) continue
362
- const a = nodes[i]
363
- const candidates = qt.queryRadius(a.x, a.y, edgeMaxDist)
364
- for (const b of candidates) {
365
- const j = b._index
366
- if (j <= i) continue
367
- if (maxEdgesPerFrame !== null && totalEdges >= maxEdgesPerFrame) return
368
- if (edgeCounts && maxEdgesPerNode !== null && (edgeCounts[i] >= maxEdgesPerNode || edgeCounts[j] >= maxEdgesPerNode)) continue
369
- if (this._drawEdge(ctx, a, b, i, j, opts, edgeCounts)) totalEdges++
365
+ mainPass: {
366
+ if (spatialIndex) {
367
+ const qt = new QuadTree(-1, -1, width + 2, height + 2)
368
+ for (const node of nodes) qt.insert(node)
369
+
370
+ for (let i = 0; i < nodes.length; i++) {
371
+ if (maxEdgesPerFrame !== null && totalEdges >= maxEdgesPerFrame) break mainPass
372
+ if (edgeCounts && maxEdgesPerNode !== null && edgeCounts[i] >= maxEdgesPerNode) continue
373
+ const a = nodes[i]
374
+ const candidates = qt.queryRadius(a.x, a.y, edgeMaxDist)
375
+ for (const b of candidates) {
376
+ const j = b._index
377
+ if (j <= i) continue
378
+ if (maxEdgesPerFrame !== null && totalEdges >= maxEdgesPerFrame) break mainPass
379
+ if (edgeCounts && maxEdgesPerNode !== null && (edgeCounts[i] >= maxEdgesPerNode || edgeCounts[j] >= maxEdgesPerNode)) continue
380
+ if (this._drawEdge(ctx, a, b, i, j, opts, edgeCounts)) totalEdges++
381
+ }
370
382
  }
371
- }
372
- } else {
373
- for (let i = 0; i < nodes.length; i++) {
374
- for (let j = i + 1; j < nodes.length; j++) {
375
- if (maxEdgesPerFrame !== null && totalEdges >= maxEdgesPerFrame) return
376
- if (edgeCounts && maxEdgesPerNode !== null && (edgeCounts[i] >= maxEdgesPerNode || edgeCounts[j] >= maxEdgesPerNode)) continue
377
- if (this._drawEdge(ctx, nodes[i], nodes[j], i, j, opts, edgeCounts)) totalEdges++
383
+ } else {
384
+ for (let i = 0; i < nodes.length; i++) {
385
+ for (let j = i + 1; j < nodes.length; j++) {
386
+ if (maxEdgesPerFrame !== null && totalEdges >= maxEdgesPerFrame) break mainPass
387
+ if (edgeCounts && maxEdgesPerNode !== null && (edgeCounts[i] >= maxEdgesPerNode || edgeCounts[j] >= maxEdgesPerNode)) continue
388
+ if (this._drawEdge(ctx, nodes[i], nodes[j], i, j, opts, edgeCounts)) totalEdges++
389
+ }
378
390
  }
379
391
  }
380
392
  }
@@ -494,6 +506,8 @@ export class World {
494
506
  }
495
507
 
496
508
  start() {
509
+ if (this._started) return
510
+ this._started = true
497
511
  this._resize()
498
512
  window.addEventListener('resize', this._onResize)
499
513
  window.addEventListener('mousemove', this._onMouseMove)
@@ -522,6 +536,7 @@ export class World {
522
536
  }
523
537
 
524
538
  stop() {
539
+ this._started = false
525
540
  cancelAnimationFrame(this.raf)
526
541
  this.raf = null
527
542
  if (this._io) { this._io.disconnect(); this._io = null }
@@ -8,10 +8,10 @@ import { World } from '../World.js'
8
8
  * All World options are supported as props.
9
9
  *
10
10
  * Example:
11
- * const ref = useCosmograph({ nodeCount: 60, colors: ['#fff'], forces: [drift()] })
11
+ * const ref = useStarflock({ nodeCount: 60, colors: ['#fff'], forces: [drift()] })
12
12
  * return <canvas ref={ref} style={{ width: '100%', height: '100%' }} />
13
13
  */
14
- export function useCosmograph(options = {}) {
14
+ export function useStarflock(options = {}) {
15
15
  const canvasRef = useRef(null)
16
16
  const worldRef = useRef(null)
17
17
 
@@ -11,8 +11,8 @@
11
11
  export function attract({ x = 0.5, y = 0.5, radius = 200, strength = 0.001 } = {}) {
12
12
  return (nodes, context) => {
13
13
  const { width, height } = context
14
- const tx = typeof x === 'function' ? x(width, height) : (x <= 1 ? x * width : x)
15
- const ty = typeof y === 'function' ? y(width, height) : (y <= 1 ? y * height : y)
14
+ const tx = typeof x === 'function' ? x(width, height) : (x >= 0 && x <= 1 ? x * width : x)
15
+ const ty = typeof y === 'function' ? y(width, height) : (y >= 0 && y <= 1 ? y * height : y)
16
16
 
17
17
  for (const node of nodes) {
18
18
  const dx = tx - node.x