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.
- package/LICENSE +21 -0
- package/README.md +282 -0
- package/index.d.ts +176 -0
- package/package.json +30 -0
- package/src/Node.js +13 -0
- package/src/QuadTree.js +65 -0
- package/src/World.js +534 -0
- package/src/adapters/react.js +37 -0
- package/src/forces/attract.js +26 -0
- package/src/forces/dampen.js +8 -0
- package/src/forces/drift.js +11 -0
- package/src/forces/gravity.js +30 -0
- package/src/forces/mouseRepel.js +49 -0
- package/src/forces/nodeRepel.js +33 -0
- package/src/forces/noise.js +64 -0
- package/src/forces/scrollDrift.js +58 -0
- package/src/forces/twinkle.js +7 -0
- package/src/forces/wind.js +27 -0
- package/src/index.js +14 -0
- package/src/shapes.js +60 -0
|
@@ -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,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
|
+
}
|