cosmograph 0.1.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.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * nodeRepel — nodes push each other apart when too close
3
+ *
4
+ * radius: distance threshold in px below which repulsion activates (default 60)
5
+ * strength: force multiplier (default 0.002) — stronger when nodes are closer
6
+ *
7
+ * Note: O(n²) complexity — pairs outside radius are skipped immediately,
8
+ * keeping it practical for typical particle counts (up to ~500 nodes).
9
+ */
10
+ export function nodeRepel({ radius = 60, strength = 0.002 } = {}) {
11
+ return (nodes) => {
12
+ for (let i = 0; i < nodes.length; i++) {
13
+ for (let j = i + 1; j < nodes.length; j++) {
14
+ const a = nodes[i]
15
+ const b = nodes[j]
16
+ const dx = a.x - b.x
17
+ const dy = a.y - b.y
18
+ const dist = Math.hypot(dx, dy)
19
+ if (dist >= radius || dist === 0) continue
20
+
21
+ const t = (radius - dist) / radius // 1 at center, 0 at edge
22
+ const f = t * strength / dist
23
+ const fx = dx * f
24
+ const fy = dy * f
25
+
26
+ a.vx += fx
27
+ a.vy += fy
28
+ b.vx -= fx
29
+ b.vy -= fy
30
+ }
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * noise — organic drifting via 2D Value Noise
3
+ *
4
+ * scale: frequency of the noise field — higher = finer detail (default 0.003)
5
+ * strength: force magnitude applied per frame (default 0.0008)
6
+ * speed: how fast the noise field evolves over time (default 0.0005)
7
+ *
8
+ * Internally uses a grid-based Value Noise with bilinear interpolation.
9
+ * The noise field shifts slowly over time, creating smooth, organic motion.
10
+ */
11
+ export function noise({ scale = 0.003, strength = 0.0008, speed = 0.0005 } = {}) {
12
+ // Reproducible pseudo-random values for the noise lattice
13
+ const GRID = 256
14
+ const table = new Float32Array(GRID * GRID * 2)
15
+ for (let i = 0; i < table.length; i++) {
16
+ // Simple LCG seeded by index for determinism
17
+ const h = Math.sin(i * 127.1 + 311.7) * 43758.5453
18
+ table[i] = h - Math.floor(h)
19
+ }
20
+
21
+ const latticeX = (ix, iy) => table[((ix & (GRID - 1)) + (iy & (GRID - 1)) * GRID) * 2]
22
+ const latticeY = (ix, iy) => table[((ix & (GRID - 1)) + (iy & (GRID - 1)) * GRID) * 2 + 1]
23
+
24
+ // Smoothstep for bilinear interpolation weight
25
+ const smooth = (t) => t * t * (3 - 2 * t)
26
+
27
+ const valueNoiseVec = (wx, wy) => {
28
+ const ix = Math.floor(wx)
29
+ const iy = Math.floor(wy)
30
+ const fx = wx - ix
31
+ const fy = wy - iy
32
+ const ux = smooth(fx)
33
+ const uy = smooth(fy)
34
+
35
+ const x00 = latticeX(ix, iy)
36
+ const x10 = latticeX(ix + 1, iy)
37
+ const x01 = latticeX(ix, iy + 1)
38
+ const x11 = latticeX(ix + 1, iy + 1)
39
+
40
+ const y00 = latticeY(ix, iy)
41
+ const y10 = latticeY(ix + 1, iy)
42
+ const y01 = latticeY(ix, iy + 1)
43
+ const y11 = latticeY(ix + 1, iy + 1)
44
+
45
+ const nx = (x00 + (x10 - x00) * ux + (x01 - x00) * uy + (x11 - x10 - x01 + x00) * ux * uy) * 2 - 1
46
+ const ny = (y00 + (y10 - y00) * ux + (y01 - y00) * uy + (y11 - y10 - y01 + y00) * ux * uy) * 2 - 1
47
+
48
+ return { nx, ny }
49
+ }
50
+
51
+ return (nodes, { time }) => {
52
+ const offset = time * speed
53
+
54
+ for (const node of nodes) {
55
+ const wx = node.x * scale + offset
56
+ const wy = node.y * scale + offset
57
+
58
+ const { nx, ny } = valueNoiseVec(wx, wy)
59
+
60
+ node.vx += nx * strength
61
+ node.vy += ny * strength
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * scrollDrift — reacts to page scroll
3
+ *
4
+ * mode:
5
+ * 'rotate' — nodes rotate around viewport center (default)
6
+ * 'wave' — vertical wave pushes nodes away from scroll direction
7
+ * 'scatter' — nodes scatter outward from viewport center on scroll
8
+ * 'custom' — provide your own fn(node, delta, context) => void
9
+ *
10
+ * strength: multiplier for the effect intensity (default 1.0)
11
+ */
12
+ export function scrollDrift({ mode = 'rotate', strength = 1.0, fn } = {}) {
13
+ let lastScrollY = 0
14
+
15
+ return (nodes, context) => {
16
+ const { scrollY, width, height } = context
17
+ const delta = scrollY - lastScrollY
18
+ lastScrollY = scrollY
19
+ if (delta === 0) return
20
+
21
+ if (mode === 'custom' && typeof fn === 'function') {
22
+ for (const node of nodes) fn(node, delta, context)
23
+ return
24
+ }
25
+
26
+ const cx = width / 2
27
+ const cy = height / 2
28
+
29
+ for (const node of nodes) {
30
+ if (mode === 'rotate') {
31
+ const dx = node.x - cx
32
+ const dy = node.y - cy
33
+ const angle = delta * 0.003 * strength
34
+ const cos = Math.cos(angle)
35
+ const sin = Math.sin(angle)
36
+ node.vx += (dx * cos - dy * sin + cx - node.x) * 0.06
37
+ node.vy += (dx * sin + dy * cos + cy - node.y) * 0.06
38
+
39
+ } else if (mode === 'wave') {
40
+ // Push nodes up/down opposite scroll direction, with horizontal spread based on x position
41
+ const wave = Math.sin((node.x / width) * Math.PI * 2 + scrollY * 0.01)
42
+ node.vy -= delta * 0.04 * strength
43
+ node.vx += wave * Math.abs(delta) * 0.015 * strength
44
+
45
+ } else if (mode === 'scatter') {
46
+ // Nodes scatter outward from canvas center, fall off near edges
47
+ const dx = node.x - cx
48
+ const dy = node.y - cy
49
+ const dist = Math.hypot(dx, dy) || 1
50
+ const maxDist = Math.hypot(cx, cy)
51
+ const falloff = Math.max(0, 1 - dist / maxDist)
52
+ const force = Math.abs(delta) * 0.008 * strength * falloff
53
+ node.vx += (dx / dist) * force
54
+ node.vy += (dy / dist) * force
55
+ }
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,7 @@
1
+ export function twinkle({ minBrightness = 0.5, variance = 0.5 } = {}) {
2
+ return (nodes, { time }) => {
3
+ for (const node of nodes) {
4
+ node.brightness = minBrightness + variance * ((Math.sin(time * node.twinkleSpeed + node.phase) + 1) / 2)
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * wind — applies a constant directional force to all nodes
3
+ *
4
+ * angle: direction in radians (0 = right, Math.PI/2 = down)
5
+ * strength: base force magnitude (default 0.0005)
6
+ * gust: amplitude of sinusoidal strength variation over time (default 0)
7
+ * when > 0, produces a gusting effect — actual strength oscillates
8
+ * between (strength - gust) and (strength + gust)
9
+ */
10
+ export function wind({ angle = 0, strength = 0.0005, gust = 0 } = {}) {
11
+ const cos = Math.cos(angle)
12
+ const sin = Math.sin(angle)
13
+
14
+ return (nodes, { time }) => {
15
+ const currentStrength = gust > 0
16
+ ? strength + gust * Math.sin(time * 0.001)
17
+ : strength
18
+
19
+ const fx = cos * currentStrength
20
+ const fy = sin * currentStrength
21
+
22
+ for (const node of nodes) {
23
+ node.vx += fx
24
+ node.vy += fy
25
+ }
26
+ }
27
+ }
package/src/index.js ADDED
@@ -0,0 +1,14 @@
1
+ export { World } from './World.js'
2
+ export { drift } from './forces/drift.js'
3
+ export { dampen } from './forces/dampen.js'
4
+ export { twinkle } from './forces/twinkle.js'
5
+ export { mouseRepel } from './forces/mouseRepel.js'
6
+ export { scrollDrift } from './forces/scrollDrift.js'
7
+ export { gravity } from './forces/gravity.js'
8
+ export { wind } from './forces/wind.js'
9
+ export { nodeRepel } from './forces/nodeRepel.js'
10
+ export { noise } from './forces/noise.js'
11
+ export { attract } from './forces/attract.js'
12
+ export { circle, diamond, star, cross, ring } from './shapes.js'
13
+ export { Node } from './Node.js'
14
+ export { resolveShape } from './shapes.js'
package/src/shapes.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Built-in node shape renderers.
3
+ * Each function signature: (ctx, x, y, r) => void
4
+ * ctx is already set up (fillStyle, globalAlpha) — just draw and fill/stroke.
5
+ */
6
+
7
+ export const circle = (ctx, x, y, r) => {
8
+ ctx.beginPath()
9
+ ctx.arc(x, y, r, 0, Math.PI * 2)
10
+ ctx.fill()
11
+ }
12
+
13
+ export const diamond = (ctx, x, y, r) => {
14
+ ctx.beginPath()
15
+ ctx.moveTo(x, y - r)
16
+ ctx.lineTo(x + r, y)
17
+ ctx.lineTo(x, y + r)
18
+ ctx.lineTo(x - r, y)
19
+ ctx.closePath()
20
+ ctx.fill()
21
+ }
22
+
23
+ export const star = (ctx, x, y, r, points = 5) => {
24
+ const inner = r * 0.45
25
+ ctx.beginPath()
26
+ for (let i = 0; i < points * 2; i++) {
27
+ const angle = (i * Math.PI) / points - Math.PI / 2
28
+ const radius = i % 2 === 0 ? r : inner
29
+ const px = x + Math.cos(angle) * radius
30
+ const py = y + Math.sin(angle) * radius
31
+ i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py)
32
+ }
33
+ ctx.closePath()
34
+ ctx.fill()
35
+ }
36
+
37
+ export const cross = (ctx, x, y, r) => {
38
+ const t = r * 0.35
39
+ ctx.beginPath()
40
+ ctx.rect(x - t, y - r, t * 2, r * 2)
41
+ ctx.rect(x - r, y - t, r * 2, t * 2)
42
+ ctx.fill()
43
+ }
44
+
45
+ export const ring = (ctx, x, y, r) => {
46
+ ctx.save()
47
+ ctx.beginPath()
48
+ ctx.arc(x, y, r, 0, Math.PI * 2)
49
+ ctx.lineWidth = r * 0.5
50
+ ctx.strokeStyle = ctx.fillStyle
51
+ ctx.stroke()
52
+ ctx.restore()
53
+ }
54
+
55
+ /** Resolve a shape option to a render function */
56
+ export function resolveShape(shape) {
57
+ if (typeof shape === 'function') return shape
58
+ const map = { circle, diamond, star, cross, ring }
59
+ return map[shape] ?? circle
60
+ }