danceflow-geometry 1.0.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 +94 -0
- package/dist/bezier.d.ts +158 -0
- package/dist/centroid.d.ts +35 -0
- package/dist/circleArrangement.d.ts +139 -0
- package/dist/collision.d.ts +149 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +985 -0
- package/dist/lineArrangement.d.ts +92 -0
- package/dist/mirrorFlip.d.ts +159 -0
- package/dist/polygon.d.ts +76 -0
- package/dist/rotateFormation.d.ts +177 -0
- package/dist/spline.d.ts +98 -0
- package/dist/spreader.d.ts +62 -0
- package/dist/types.d.ts +98 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# danceflow-geometry
|
|
2
|
+
|
|
3
|
+
Geometry algorithms for dance choreography formations.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install danceflow-geometry
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
import {
|
|
15
|
+
calculateCentroid,
|
|
16
|
+
spreadFromCentroid,
|
|
17
|
+
arrangeInLine,
|
|
18
|
+
arrangeInCircle,
|
|
19
|
+
isPointInPolygon
|
|
20
|
+
} from 'danceflow-geometry';
|
|
21
|
+
|
|
22
|
+
// Calculate center of dancers
|
|
23
|
+
const positions = [{ x: 100, y: 100 }, { x: 200, y: 150 }];
|
|
24
|
+
const center = calculateCentroid(positions);
|
|
25
|
+
|
|
26
|
+
// Spread dancers from center
|
|
27
|
+
const spread = spreadFromCentroid(positions, 1.5);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Functions
|
|
31
|
+
|
|
32
|
+
### Centroid
|
|
33
|
+
- `calculateCentroid(positions)` - Find center point of a group of positions
|
|
34
|
+
- `scaleAroundCenter(positions, scale)` - Scale positions around their centroid
|
|
35
|
+
|
|
36
|
+
### Spreader
|
|
37
|
+
- `spreadFromCentroid(positions, factor)` - Expand/contract from center point
|
|
38
|
+
- `spreadHorizontal(positions, factor)` - Spread horizontally only
|
|
39
|
+
- `spreadVertical(positions, factor)` - Spread vertically only
|
|
40
|
+
|
|
41
|
+
### Line Arrangement
|
|
42
|
+
- `arrangeInLine(positions, start, end)` - Arrange in a straight line
|
|
43
|
+
- `arrangeInColumn(positions, start, spacing)` - Arrange in a vertical column
|
|
44
|
+
- `arrangeInDiagonal(positions, start, angle, spacing)` - Arrange diagonally
|
|
45
|
+
|
|
46
|
+
### Circle Arrangement
|
|
47
|
+
- `arrangeInCircle(positions, center, radius)` - Arrange in a circle
|
|
48
|
+
- `arrangeInArc(positions, center, radius, startAngle, endAngle)` - Arrange in an arc
|
|
49
|
+
- `arrangeInSpiral(positions, center, startRadius, endRadius)` - Arrange in a spiral
|
|
50
|
+
|
|
51
|
+
### Mirror & Flip
|
|
52
|
+
- `mirrorHorizontal(positions, axisX)` - Mirror across vertical axis
|
|
53
|
+
- `mirrorVertical(positions, axisY)` - Mirror across horizontal axis
|
|
54
|
+
- `flipFormation(positions)` - Flip entire formation
|
|
55
|
+
|
|
56
|
+
### Rotation
|
|
57
|
+
- `rotateFormation(positions, angle)` - Rotate around centroid
|
|
58
|
+
- `rotateAroundPoint(positions, point, angle)` - Rotate around specific point
|
|
59
|
+
|
|
60
|
+
### Polygon
|
|
61
|
+
- `isPointInPolygon(point, polygon)` - Point-in-polygon test (ray casting)
|
|
62
|
+
- `createPolygonPath(points)` - Create closed polygon path
|
|
63
|
+
|
|
64
|
+
### Bezier Curves
|
|
65
|
+
- `cubicBezier(t, p0, p1, p2, p3)` - Cubic bezier interpolation
|
|
66
|
+
- `quadraticBezier(t, p0, p1, p2)` - Quadratic bezier interpolation
|
|
67
|
+
- `calculateOptimalArcHeight(start, end)` - Calculate optimal arc control point
|
|
68
|
+
|
|
69
|
+
### Spline
|
|
70
|
+
- `catmullRomSpline(points, t)` - Catmull-Rom spline interpolation
|
|
71
|
+
- `createSmoothPath(points, segments)` - Create smooth path through points
|
|
72
|
+
|
|
73
|
+
### Collision Detection
|
|
74
|
+
- `detectCollisions(positions, radius)` - Find overlapping positions
|
|
75
|
+
- `resolveOverlaps(positions, minDistance)` - Push apart overlapping positions
|
|
76
|
+
|
|
77
|
+
## Types
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
interface Point {
|
|
81
|
+
x: number;
|
|
82
|
+
y: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface DancerPosition {
|
|
86
|
+
x: number;
|
|
87
|
+
y: number;
|
|
88
|
+
facing: number;
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
package/dist/bezier.d.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bezier curve mathematics for swap arc animations
|
|
3
|
+
* Implements cubic Bezier interpolation for smooth dancer transition paths
|
|
4
|
+
*
|
|
5
|
+
* Mathematical Foundation:
|
|
6
|
+
* Cubic Bezier curve formula: P(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃
|
|
7
|
+
*
|
|
8
|
+
* CRITICAL: Stage coordinates have Y-axis pointing DOWN
|
|
9
|
+
* This affects the perpendicular vector calculation for arc control points
|
|
10
|
+
*
|
|
11
|
+
* Where:
|
|
12
|
+
* - P₀ = start position (pos1)
|
|
13
|
+
* - P₃ = end position (pos2)
|
|
14
|
+
* - P₁, P₂ = control points (calculated perpendicular to pos1-pos2 line)
|
|
15
|
+
* - t ∈ [0, 1] = interpolation parameter
|
|
16
|
+
*
|
|
17
|
+
* Performance target: <5ms for typical swap operations
|
|
18
|
+
* Coordinate system: ALL calculations in Stage coordinates (0-1000)
|
|
19
|
+
*/
|
|
20
|
+
import type { Point, DancerPosition } from './types';
|
|
21
|
+
/**
|
|
22
|
+
* Calculate a single point on a cubic Bezier curve
|
|
23
|
+
*
|
|
24
|
+
* Math: P(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃
|
|
25
|
+
*
|
|
26
|
+
* @param t - Interpolation parameter [0, 1]
|
|
27
|
+
* @param p0 - Start point
|
|
28
|
+
* @param p1 - First control point
|
|
29
|
+
* @param p2 - Second control point
|
|
30
|
+
* @param p3 - End point
|
|
31
|
+
* @returns Point on curve at parameter t
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* const start = { x: 100, y: 100 };
|
|
35
|
+
* const end = { x: 200, y: 100 };
|
|
36
|
+
* const ctrl1 = { x: 125, y: 50 };
|
|
37
|
+
* const ctrl2 = { x: 175, y: 50 };
|
|
38
|
+
* const midpoint = cubicBezier(0.5, start, ctrl1, ctrl2, end);
|
|
39
|
+
* // → { x: 150, y: 62.5 } (approximate)
|
|
40
|
+
*/
|
|
41
|
+
export declare function cubicBezier(t: number, p0: Point, p1: Point, p2: Point, p3: Point): Point;
|
|
42
|
+
/**
|
|
43
|
+
* Calculate control points for a symmetric arc above the line connecting two points
|
|
44
|
+
*
|
|
45
|
+
* CRITICAL: Adapted for Stage Y-down coordinate system
|
|
46
|
+
* Positive arcHeight creates an arc UPWARD (negative Y direction)
|
|
47
|
+
* Negative arcHeight creates an arc DOWNWARD (positive Y direction)
|
|
48
|
+
*
|
|
49
|
+
* Algorithm:
|
|
50
|
+
* 1. Find midpoint M between pos1 and pos2
|
|
51
|
+
* 2. Calculate direction vector D = pos2 - pos1
|
|
52
|
+
* 3. Calculate perpendicular vector P = (-D.y, D.x) normalized
|
|
53
|
+
* 4. NEGATE P for Y-down coordinates (arc upward is negative Y)
|
|
54
|
+
* 5. Place control points symmetrically: M + P × arcHeight
|
|
55
|
+
*
|
|
56
|
+
* @param pos1 - Start position (Stage coordinates)
|
|
57
|
+
* @param pos2 - End position (Stage coordinates)
|
|
58
|
+
* @param arcHeight - Height of arc perpendicular to line (Stage units, positive = upward)
|
|
59
|
+
* @returns Tuple of [controlPoint1, controlPoint2]
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* const start = { x: 100, y: 100, facing: 0 };
|
|
63
|
+
* const end = { x: 200, y: 100, facing: 0 };
|
|
64
|
+
* const [cp1, cp2] = calculateArcControlPoints(start, end, 50);
|
|
65
|
+
* // Arc curves upward by 50 units above the midpoint (Y decreases)
|
|
66
|
+
*/
|
|
67
|
+
export declare function calculateArcControlPoints(pos1: DancerPosition, pos2: DancerPosition, arcHeight: number): [Point, Point];
|
|
68
|
+
/**
|
|
69
|
+
* Generate smooth arc path between two dancer positions
|
|
70
|
+
*
|
|
71
|
+
* This is the primary function for swap arc animations. It creates a cubic Bezier
|
|
72
|
+
* curve that arcs above the straight line between two positions.
|
|
73
|
+
*
|
|
74
|
+
* Performance: O(n) where n = number of interpolation points (default 20)
|
|
75
|
+
*
|
|
76
|
+
* @param pos1 - Start position (Stage coordinates)
|
|
77
|
+
* @param pos2 - End position (Stage coordinates)
|
|
78
|
+
* @param arcHeight - Arc height perpendicular to line (Stage units, default 80, positive = upward)
|
|
79
|
+
* @param numPoints - Number of interpolation points (default 20)
|
|
80
|
+
* @returns Array of points defining the arc path
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* const dancer1 = { x: 100, y: 500, facing: 0 };
|
|
84
|
+
* const dancer2 = { x: 900, y: 500, facing: 0 };
|
|
85
|
+
* const arcPath = calculateSwapArcPath(dancer1, dancer2);
|
|
86
|
+
* // Returns 20 points forming smooth arc from (100,500) to (900,500)
|
|
87
|
+
* // with peak at approximately (500, 420) for 80-unit arc height (upward)
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* // Custom arc height for longer distances
|
|
91
|
+
* const dancer1 = { x: 100, y: 100, facing: 0 };
|
|
92
|
+
* const dancer2 = { x: 900, y: 900, facing: 0 };
|
|
93
|
+
* const arcPath = calculateSwapArcPath(dancer1, dancer2, 120);
|
|
94
|
+
* // Higher arc for longer distance swap
|
|
95
|
+
*/
|
|
96
|
+
export declare function calculateSwapArcPath(pos1: DancerPosition, pos2: DancerPosition, arcHeight?: number, numPoints?: number): Point[];
|
|
97
|
+
/**
|
|
98
|
+
* Calculate optimal arc height based on distance between positions
|
|
99
|
+
*
|
|
100
|
+
* This provides a visually pleasing arc that scales with the swap distance.
|
|
101
|
+
* Longer swaps get proportionally higher arcs.
|
|
102
|
+
*
|
|
103
|
+
* Formula: arcHeight = min(distance × 0.15, maxHeight)
|
|
104
|
+
*
|
|
105
|
+
* @param pos1 - Start position
|
|
106
|
+
* @param pos2 - End position
|
|
107
|
+
* @param maxHeight - Maximum arc height (default 150 Stage units)
|
|
108
|
+
* @returns Optimal arc height in Stage units
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* const close = { x: 100, y: 100, facing: 0 };
|
|
112
|
+
* const nearby = { x: 150, y: 100, facing: 0 };
|
|
113
|
+
* calculateOptimalArcHeight(close, nearby); // → ~7.5 (15% of 50 units)
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* const far1 = { x: 100, y: 100, facing: 0 };
|
|
117
|
+
* const far2 = { x: 900, y: 900, facing: 0 };
|
|
118
|
+
* calculateOptimalArcHeight(far1, far2); // → 150 (clamped to max)
|
|
119
|
+
*/
|
|
120
|
+
export declare function calculateOptimalArcHeight(pos1: DancerPosition, pos2: DancerPosition, maxHeight?: number): number;
|
|
121
|
+
/**
|
|
122
|
+
* Calculate the length of a Bezier curve using numerical approximation
|
|
123
|
+
*
|
|
124
|
+
* Uses the same interpolation as the path generation to ensure consistency.
|
|
125
|
+
* This is useful for timing calculations in animations.
|
|
126
|
+
*
|
|
127
|
+
* @param pos1 - Start position
|
|
128
|
+
* @param pos2 - End position
|
|
129
|
+
* @param arcHeight - Arc height
|
|
130
|
+
* @param numSamples - Number of samples for length calculation (default 20)
|
|
131
|
+
* @returns Approximate arc length in Stage units
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* const start = { x: 100, y: 100, facing: 0 };
|
|
135
|
+
* const end = { x: 200, y: 100, facing: 0 };
|
|
136
|
+
* const length = calculateArcLength(start, end, 50);
|
|
137
|
+
* // Returns approximate arc length (>100, <150 due to arc)
|
|
138
|
+
*/
|
|
139
|
+
export declare function calculateArcLength(pos1: DancerPosition, pos2: DancerPosition, arcHeight?: number, numSamples?: number): number;
|
|
140
|
+
/**
|
|
141
|
+
* Get position along arc at specific time parameter
|
|
142
|
+
*
|
|
143
|
+
* This is useful for animation where you need a specific point along the arc
|
|
144
|
+
* without generating the full path.
|
|
145
|
+
*
|
|
146
|
+
* @param pos1 - Start position
|
|
147
|
+
* @param pos2 - End position
|
|
148
|
+
* @param t - Time parameter [0, 1]
|
|
149
|
+
* @param arcHeight - Arc height
|
|
150
|
+
* @returns Point at parameter t along the arc
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* const start = { x: 100, y: 100, facing: 0 };
|
|
154
|
+
* const end = { x: 200, y: 100, facing: 0 };
|
|
155
|
+
* const halfway = getArcPositionAtTime(start, end, 0.5, 50);
|
|
156
|
+
* // Returns point at 50% of arc (peak of curve)
|
|
157
|
+
*/
|
|
158
|
+
export declare function getArcPositionAtTime(pos1: DancerPosition, pos2: DancerPosition, t: number, arcHeight?: number): Point;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Geometric utility functions
|
|
3
|
+
* Based on plan Phase 4.2
|
|
4
|
+
*/
|
|
5
|
+
import type { Point, DancerPosition } from './types';
|
|
6
|
+
/**
|
|
7
|
+
* Calculate the geometric centroid (center) of a set of positions
|
|
8
|
+
* @param positions - Array of positions
|
|
9
|
+
* @returns Centroid point
|
|
10
|
+
*/
|
|
11
|
+
export declare function calculateCentroid(positions: readonly DancerPosition[]): Point;
|
|
12
|
+
/**
|
|
13
|
+
* Scale positions around a centroid point
|
|
14
|
+
* Used by the spreader tool
|
|
15
|
+
* @param positions - Array of positions to scale
|
|
16
|
+
* @param scaleFactor - Scale factor (1.0 = no change, >1 = spread out, <1 = bring together)
|
|
17
|
+
* @param center - Center point (optional, will calculate if not provided)
|
|
18
|
+
* @returns Scaled positions
|
|
19
|
+
*/
|
|
20
|
+
export declare function scaleAroundCenter(positions: readonly DancerPosition[], scaleFactor: number, center?: Point): DancerPosition[];
|
|
21
|
+
/**
|
|
22
|
+
* Check if a point is inside a polygon (for lasso selection)
|
|
23
|
+
* Uses ray casting algorithm
|
|
24
|
+
* @param point - Point to test
|
|
25
|
+
* @param polygon - Array of polygon vertices
|
|
26
|
+
* @returns True if point is inside polygon
|
|
27
|
+
*/
|
|
28
|
+
export declare function pointInPolygon(point: Point, polygon: readonly Point[]): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Calculate distance between two points
|
|
31
|
+
* @param p1 - First point
|
|
32
|
+
* @param p2 - Second point
|
|
33
|
+
* @returns Distance between points
|
|
34
|
+
*/
|
|
35
|
+
export declare function distance(p1: Point, p2: Point): number;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Circle/Arc Arrangement Mathematical Operations
|
|
3
|
+
*
|
|
4
|
+
* Distributes dancers evenly around a circle or arc using polar coordinates
|
|
5
|
+
*
|
|
6
|
+
* Math: Polar to Cartesian Conversion (adapted for Stage Y-down coordinates)
|
|
7
|
+
* x = cx + r * cos(θ)
|
|
8
|
+
* y = cy + r * sin(θ)
|
|
9
|
+
*
|
|
10
|
+
* CRITICAL: Stage coordinates have Y-axis pointing DOWN
|
|
11
|
+
* In standard math, 0° points East and angles increase counterclockwise
|
|
12
|
+
* In Stage coords, 0° points North (up) and angles increase clockwise
|
|
13
|
+
* We adjust angles by -90° to align with Stage coordinate system
|
|
14
|
+
*
|
|
15
|
+
* Where:
|
|
16
|
+
* (cx, cy) = Center of circle
|
|
17
|
+
* r = Radius
|
|
18
|
+
* θ = Angle in radians (adjusted for Stage Y-down)
|
|
19
|
+
*
|
|
20
|
+
* Angular Distribution:
|
|
21
|
+
* θ_i = θ_start + (i / (n-1)) * (θ_end - θ_start) [for n > 1]
|
|
22
|
+
* θ_i = θ_start + i * (2π / n) [for full circle]
|
|
23
|
+
*
|
|
24
|
+
* CRITICAL: All coordinates in Stage space (0-1000 units)
|
|
25
|
+
* Performance: O(n) - Must complete in <5ms for 50 dancers
|
|
26
|
+
*/
|
|
27
|
+
import type { DancerPosition, Point, CircleArrangementOptions } from './types';
|
|
28
|
+
/**
|
|
29
|
+
* Arrange dancers in a circle or arc formation
|
|
30
|
+
*
|
|
31
|
+
* Math: x = cx + r * cos(θ), y = cy + r * sin(θ)
|
|
32
|
+
* CRITICAL: Adapted for Stage Y-down coordinate system
|
|
33
|
+
*
|
|
34
|
+
* Handles edge cases:
|
|
35
|
+
* - 1 dancer: Places at start angle
|
|
36
|
+
* - 2 dancers: Places at start and end angles
|
|
37
|
+
* - 3+ dancers: Distributes evenly around arc
|
|
38
|
+
*
|
|
39
|
+
* Full circle mode:
|
|
40
|
+
* - Dancers evenly spaced around 360°
|
|
41
|
+
* - Last dancer does NOT overlap first dancer
|
|
42
|
+
*
|
|
43
|
+
* Arc mode:
|
|
44
|
+
* - Dancers distributed from start to end angle
|
|
45
|
+
* - If distributeEvenly=false, includes dancer at end angle
|
|
46
|
+
*
|
|
47
|
+
* @param positions - Current dancer positions (used for determining center)
|
|
48
|
+
* @param options - Circle arrangement configuration
|
|
49
|
+
* @returns New positions arranged in circle/arc
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // Full circle
|
|
53
|
+
* const circle = arrangeInCircle(currentPositions, {
|
|
54
|
+
* preset: 'full',
|
|
55
|
+
* radius: 300,
|
|
56
|
+
* faceDirection: 'center'
|
|
57
|
+
* });
|
|
58
|
+
*
|
|
59
|
+
* // Half circle (semicircle)
|
|
60
|
+
* const semicircle = arrangeInCircle(currentPositions, {
|
|
61
|
+
* preset: 'half',
|
|
62
|
+
* startAngle: 0, // Start at top
|
|
63
|
+
* faceDirection: 'outward'
|
|
64
|
+
* });
|
|
65
|
+
*
|
|
66
|
+
* // Custom arc (120°)
|
|
67
|
+
* const arc = arrangeInCircle(currentPositions, {
|
|
68
|
+
* preset: 'custom',
|
|
69
|
+
* arcAngle: 120,
|
|
70
|
+
* startAngle: 30,
|
|
71
|
+
* faceDirection: 'tangent'
|
|
72
|
+
* });
|
|
73
|
+
*
|
|
74
|
+
* // Custom center and radius
|
|
75
|
+
* const custom = arrangeInCircle(currentPositions, {
|
|
76
|
+
* preset: 'full',
|
|
77
|
+
* center: { x: 500, y: 500 },
|
|
78
|
+
* radius: 250,
|
|
79
|
+
* faceDirection: 90 // Face East
|
|
80
|
+
* });
|
|
81
|
+
*/
|
|
82
|
+
export declare function arrangeInCircle(positions: readonly DancerPosition[], options?: CircleArrangementOptions): DancerPosition[];
|
|
83
|
+
/**
|
|
84
|
+
* Arrange dancers in concentric circles
|
|
85
|
+
* Useful for large groups - distributes dancers across multiple rings
|
|
86
|
+
*
|
|
87
|
+
* @param positions - Current dancer positions
|
|
88
|
+
* @param options - Concentric circle options
|
|
89
|
+
* @returns New positions in concentric circles
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* // 20 dancers in 2 rings
|
|
93
|
+
* const concentric = arrangeInConcentricCircles(positions, {
|
|
94
|
+
* rings: 2,
|
|
95
|
+
* radiusStep: 100
|
|
96
|
+
* });
|
|
97
|
+
*/
|
|
98
|
+
export declare function arrangeInConcentricCircles(positions: readonly DancerPosition[], options: {
|
|
99
|
+
rings: number;
|
|
100
|
+
radiusStep?: number;
|
|
101
|
+
innerRadius?: number;
|
|
102
|
+
center?: Point;
|
|
103
|
+
faceDirection?: 'center' | 'outward' | 'tangent' | 'counter-tangent' | number;
|
|
104
|
+
}): DancerPosition[];
|
|
105
|
+
/**
|
|
106
|
+
* Validate that circle arrangement won't exceed stage bounds
|
|
107
|
+
*
|
|
108
|
+
* @param positions - Positions to arrange
|
|
109
|
+
* @param options - Circle arrangement options
|
|
110
|
+
* @param stageWidth - Stage width
|
|
111
|
+
* @param stageHeight - Stage height
|
|
112
|
+
* @param margin - Safety margin from edges
|
|
113
|
+
* @returns True if arrangement is safe
|
|
114
|
+
*/
|
|
115
|
+
export declare function isCircleArrangementSafe(positions: readonly DancerPosition[], options: CircleArrangementOptions, stageWidth?: number, stageHeight?: number, margin?: number): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Calculate arc parameters from three points (for UI interaction)
|
|
118
|
+
* User clicks/drags three points to define an arc
|
|
119
|
+
*
|
|
120
|
+
* @param p1 - First point (start of arc)
|
|
121
|
+
* @param p2 - Second point (point on arc)
|
|
122
|
+
* @param p3 - Third point (end of arc)
|
|
123
|
+
* @returns Center, radius, start angle, and arc angle or null if points are collinear
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* // User drags arc through three points
|
|
127
|
+
* const arc = calculateArcFromThreePoints(
|
|
128
|
+
* { x: 100, y: 500 },
|
|
129
|
+
* { x: 500, y: 200 },
|
|
130
|
+
* { x: 900, y: 500 }
|
|
131
|
+
* );
|
|
132
|
+
* // Use arc.center, arc.radius, arc.startAngle, arc.arcAngle
|
|
133
|
+
*/
|
|
134
|
+
export declare function calculateArcFromThreePoints(p1: Point, p2: Point, p3: Point): {
|
|
135
|
+
center: Point;
|
|
136
|
+
radius: number;
|
|
137
|
+
startAngle: number;
|
|
138
|
+
arcAngle: number;
|
|
139
|
+
} | null;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Efficient collision detection for circular dancers
|
|
3
|
+
* Implements AABB broad phase and spatial hashing from math_scratchpad.md
|
|
4
|
+
*/
|
|
5
|
+
import type { Point, DancerPosition } from './types';
|
|
6
|
+
export declare const DANCER_RADIUS = 20;
|
|
7
|
+
/**
|
|
8
|
+
* Axis-Aligned Bounding Box
|
|
9
|
+
*/
|
|
10
|
+
export interface AABB {
|
|
11
|
+
minX: number;
|
|
12
|
+
maxX: number;
|
|
13
|
+
minY: number;
|
|
14
|
+
maxY: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Collision pair result
|
|
18
|
+
*/
|
|
19
|
+
export interface CollisionPair {
|
|
20
|
+
id1: string;
|
|
21
|
+
id2: string;
|
|
22
|
+
distance: number;
|
|
23
|
+
penetration: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Dancer with ID for collision detection
|
|
27
|
+
*/
|
|
28
|
+
export interface DancerWithId {
|
|
29
|
+
id: string;
|
|
30
|
+
position: DancerPosition;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create AABB for a circular dancer
|
|
34
|
+
* Based on Proof 2: AABB Collision Detection in math_scratchpad.md
|
|
35
|
+
*
|
|
36
|
+
* @param position - Dancer position
|
|
37
|
+
* @param radius - Dancer radius (default: 20)
|
|
38
|
+
* @returns AABB bounding box
|
|
39
|
+
*/
|
|
40
|
+
export declare function createAABB(position: Point, radius?: number): AABB;
|
|
41
|
+
/**
|
|
42
|
+
* Check if two AABBs overlap
|
|
43
|
+
* O(1) operation for fast broad-phase culling
|
|
44
|
+
*
|
|
45
|
+
* @param aabb1 - First bounding box
|
|
46
|
+
* @param aabb2 - Second bounding box
|
|
47
|
+
* @returns True if AABBs overlap
|
|
48
|
+
*/
|
|
49
|
+
export declare function checkAABBOverlap(aabb1: AABB, aabb2: AABB): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Check if two circles collide (narrow phase)
|
|
52
|
+
* Only call this after AABB overlap test passes
|
|
53
|
+
*
|
|
54
|
+
* @param pos1 - First circle center
|
|
55
|
+
* @param pos2 - Second circle center
|
|
56
|
+
* @param radius1 - First circle radius (default: 20)
|
|
57
|
+
* @param radius2 - Second circle radius (default: 20)
|
|
58
|
+
* @returns True if circles overlap
|
|
59
|
+
*/
|
|
60
|
+
export declare function checkCircleCollision(pos1: Point, pos2: Point, radius1?: number, radius2?: number): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Calculate exact distance between two points
|
|
63
|
+
*
|
|
64
|
+
* @param pos1 - First point
|
|
65
|
+
* @param pos2 - Second point
|
|
66
|
+
* @returns Euclidean distance
|
|
67
|
+
*/
|
|
68
|
+
export declare function calculateDistance(pos1: Point, pos2: Point): number;
|
|
69
|
+
/**
|
|
70
|
+
* Detect all collisions between dancers (naive O(N²) algorithm)
|
|
71
|
+
* Use this for small dancer counts (< 50)
|
|
72
|
+
*
|
|
73
|
+
* @param dancers - Array of dancer IDs and positions
|
|
74
|
+
* @param radius - Dancer radius (default: 20)
|
|
75
|
+
* @returns Array of collision pairs
|
|
76
|
+
*/
|
|
77
|
+
export declare function detectCollisions(dancers: readonly DancerWithId[], radius?: number): CollisionPair[];
|
|
78
|
+
/**
|
|
79
|
+
* Spatial hash grid for efficient collision detection (O(N) average case)
|
|
80
|
+
* Use this for large dancer counts (>= 50)
|
|
81
|
+
*
|
|
82
|
+
* Based on spatial hashing algorithm from math_scratchpad.md
|
|
83
|
+
*/
|
|
84
|
+
export declare class SpatialHashGrid {
|
|
85
|
+
private readonly radius;
|
|
86
|
+
private readonly cellSize;
|
|
87
|
+
private grid;
|
|
88
|
+
/**
|
|
89
|
+
* Create a new spatial hash grid
|
|
90
|
+
*
|
|
91
|
+
* @param radius - Dancer radius (default: 20)
|
|
92
|
+
* @param cellSize - Grid cell size (default: 2 * radius = 40)
|
|
93
|
+
*/
|
|
94
|
+
constructor(radius?: number, cellSize?: number);
|
|
95
|
+
/**
|
|
96
|
+
* Compute hash key for a position
|
|
97
|
+
*
|
|
98
|
+
* @param x - X coordinate
|
|
99
|
+
* @param y - Y coordinate
|
|
100
|
+
* @returns Grid cell key
|
|
101
|
+
*/
|
|
102
|
+
private hashKey;
|
|
103
|
+
/**
|
|
104
|
+
* Insert a dancer into the spatial grid
|
|
105
|
+
*
|
|
106
|
+
* @param id - Dancer ID
|
|
107
|
+
* @param position - Dancer position
|
|
108
|
+
*/
|
|
109
|
+
insert(id: string, position: DancerPosition): void;
|
|
110
|
+
/**
|
|
111
|
+
* Get all dancers in the same or adjacent cells
|
|
112
|
+
*
|
|
113
|
+
* @param position - Query position
|
|
114
|
+
* @returns Array of nearby dancers
|
|
115
|
+
*/
|
|
116
|
+
getNearby(position: DancerPosition): DancerWithId[];
|
|
117
|
+
/**
|
|
118
|
+
* Clear all entries from the grid
|
|
119
|
+
*/
|
|
120
|
+
clear(): void;
|
|
121
|
+
/**
|
|
122
|
+
* Detect all collisions using spatial hashing
|
|
123
|
+
*
|
|
124
|
+
* @param dancers - Array of dancer IDs and positions
|
|
125
|
+
* @returns Array of collision pairs
|
|
126
|
+
*/
|
|
127
|
+
detectCollisions(dancers: readonly DancerWithId[]): CollisionPair[];
|
|
128
|
+
/**
|
|
129
|
+
* Get statistics about the grid (for debugging/optimization)
|
|
130
|
+
*
|
|
131
|
+
* @returns Grid statistics
|
|
132
|
+
*/
|
|
133
|
+
getStats(): {
|
|
134
|
+
totalCells: number;
|
|
135
|
+
occupiedCells: number;
|
|
136
|
+
totalDancers: number;
|
|
137
|
+
avgDancersPerCell: number;
|
|
138
|
+
maxDancersInCell: number;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Auto-select collision detection algorithm based on dancer count
|
|
143
|
+
* Uses spatial hashing for N >= 50, naive algorithm otherwise
|
|
144
|
+
*
|
|
145
|
+
* @param dancers - Array of dancer IDs and positions
|
|
146
|
+
* @param radius - Dancer radius (default: 20)
|
|
147
|
+
* @returns Array of collision pairs
|
|
148
|
+
*/
|
|
149
|
+
export declare function detectCollisionsAuto(dancers: readonly DancerWithId[], radius?: number): CollisionPair[];
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function M(t){if(t.length===0)return{x:0,y:0};const n=t.reduce((r,e)=>({x:r.x+e.x,y:r.y+e.y}),{x:0,y:0});return{x:n.x/t.length,y:n.y/t.length}}function J(t,n,r){const e=r||M(t);return t.map(c=>({x:e.x+(c.x-e.x)*n,y:e.y+(c.y-e.y)*n,facing:c.facing}))}function Q(t,n){let r=!1;for(let e=0,c=n.length-1;e<n.length;c=e++){const a=n[e].x,i=n[e].y,s=n[c].x,o=n[c].y;i>t.y!=o>t.y&&t.x<(s-a)*(t.y-i)/(o-i)+a&&(r=!r)}return r}function W(t,n){const r=n.x-t.x,e=n.y-t.y;return Math.sqrt(r*r+e*e)}function k(t,n,r,e,c=0){if(t.length===0)return[];const a=e||M(t),i=c*Math.PI/180,s=Math.cos(i),o=Math.sin(i);return t.map(l=>{let u=l.x-a.x,x=l.y-a.y;if(c!==0){const h=u*s-x*o,f=u*o+x*s;u=h,x=f}if(u*=n,x*=r,c!==0){const h=u*s+x*o,f=-u*o+x*s;u=h,x=f}return{x:u+a.x,y:x+a.y,facing:l.facing}})}function Z(t,n){const{direction:r,scaleFactor:e,center:c,aspectRatio:a=1,angle:i=0}=n;let s,o,l;switch(r){case"radial":s=e,o=e,l=0;break;case"horizontal":s=e,o=1,l=0;break;case"vertical":s=1,o=e,l=0;break;case"diagonal":s=e,o=e,l=i||45;break;default:throw new Error(`Unknown spread direction: ${r}`)}return a!==1&&(s*=a),k(t,s,o,c,l)}function p(t,n,r){if(t.length===0)return 1;const e=r||M(t),a=t.reduce((i,s)=>{const o=s.x-e.x,l=s.y-e.y;return i+Math.sqrt(o*o+l*l)},0)/t.length;return a===0?1:n/a}function tt(t){if(t.length===0)return{minX:0,maxX:0,minY:0,maxY:0,width:0,height:0};let n=1/0,r=-1/0,e=1/0,c=-1/0;return t.forEach(a=>{n=Math.min(n,a.x),r=Math.max(r,a.x),e=Math.min(e,a.y),c=Math.max(c,a.y)}),{minX:n,maxX:r,minY:e,maxY:c,width:r-n,height:c-e}}function nt(t,n,r,e,c){if(t.length===0)return!0;const a=c||M(t);return t.every(i=>{const s=i.x-a.x,o=i.y-a.y,l=a.x+s*n,u=a.y+o*n,x=20;return l>=x&&l<=r-x&&u>=x&&u<=e-x})}function j(t,n,r){if(t.length===0)throw new Error("Cannot arrange line with 0 dancers");const e=M(t),a=(r||Math.max(400,t.length*50))/2;switch(n){case"horizontal":return{start:{x:e.x-a,y:e.y},end:{x:e.x+a,y:e.y}};case"vertical":return{start:{x:e.x,y:e.y-a},end:{x:e.x,y:e.y+a}};case"diagonal-45":return{start:{x:e.x-a*Math.cos(Math.PI/4),y:e.y-a*Math.sin(Math.PI/4)},end:{x:e.x+a*Math.cos(Math.PI/4),y:e.y+a*Math.sin(Math.PI/4)}};case"diagonal-135":return{start:{x:e.x-a*Math.cos(3*Math.PI/4),y:e.y-a*Math.sin(3*Math.PI/4)},end:{x:e.x+a*Math.cos(3*Math.PI/4),y:e.y+a*Math.sin(3*Math.PI/4)}};default:throw new Error(`Unknown line orientation: ${n}`)}}function q(t,n,r,e){if(typeof e=="number")return e;const c=Math.atan2(r.y,r.x)*(180/Math.PI);switch(e){case"auto":case"forward":return(c+90)%360;case"backward":return(c+270)%360;case"center":return t<n/2?(c+90)%360:(c+270)%360;default:return 0}}function O(t,n){const r=t.length;if(r===0)return[];const{orientation:e,spacing:c,startPoint:a,endPoint:i,faceDirection:s="auto",lineLength:o}=n;let l,u;if(a&&i)l=a,u=i;else if(c&&r>1){const h=c*(r-1),f=M(t),g=h/2,d=j(t,e,h),y=d.end.x-d.start.x,A=d.end.y-d.start.y,m=Math.sqrt(y*y+A*A);if(m===0)throw new Error("Invalid line orientation");const I=y/m*g,P=A/m*g;l={x:f.x-I,y:f.y-P},u={x:f.x+I,y:f.y+P}}else{const h=j(t,e,o);l=h.start,u=h.end}const x={x:u.x-l.x,y:u.y-l.y};return r===1?[{x:(l.x+u.x)/2,y:(l.y+u.y)/2,facing:q(0,1,x,s)}]:t.map((h,f)=>{const g=f/(r-1),d=l.x+g*(u.x-l.x),y=l.y+g*(u.y-l.y);return{x:d,y,facing:q(f,r,x,s)}})}function et(t,n,r=!1){const e=n.x-t.x,c=n.y-t.y,i=(Math.atan2(c,e)*(180/Math.PI)%360+360)%360;let s,o=t,l=n;if(r)if(i>=337.5||i<22.5){s="horizontal";const u=(t.y+n.y)/2;o={x:t.x,y:u},l={x:n.x,y:u}}else if(i>=22.5&&i<67.5)s="diagonal-45";else if(i>=67.5&&i<112.5){s="vertical";const u=(t.x+n.x)/2;o={x:u,y:t.y},l={x:u,y:n.y}}else if(i>=112.5&&i<157.5)s="diagonal-135";else if(i>=157.5&&i<202.5){s="horizontal";const u=(t.y+n.y)/2;o={x:t.x,y:u},l={x:n.x,y:u}}else if(i>=202.5&&i<247.5)s="diagonal-45";else if(i>=247.5&&i<292.5){s="vertical";const u=(t.x+n.x)/2;o={x:u,y:t.y},l={x:u,y:n.y}}else s="diagonal-135";else Math.abs(e)>Math.abs(c)*2?s="horizontal":Math.abs(c)>Math.abs(e)*2?s="vertical":e*c>0?s="diagonal-45":s="diagonal-135";return{orientation:s,start:o,end:l}}function rt(t,n,r=1e3,e=1e3,c=20){return t.length===0?!0:O(t,n).every(i=>i.x>=c&&i.x<=r-c&&i.y>=c&&i.y<=e-c)}function Y(t){return t*Math.PI/180}function S(t){return t*180/Math.PI}function ct(t){switch(t){case"full":return 360;case"half":return 180;case"quarter":return 90;case"three-quarter":return 270;case"custom":return 180;default:return 360}}function at(t,n,r,e){if(typeof e=="number")return e;switch(e){case"center":{const c=n.x-t.x,a=n.y-t.y,i=Math.atan2(-a,c);return((S(i)+90)%360+360)%360}case"outward":{const c=t.x-n.x,a=t.y-n.y,i=Math.atan2(-a,c);return((S(i)+90)%360+360)%360}case"tangent":{const c=r+Math.PI/2;return((S(c)+90)%360+360)%360}case"counter-tangent":{const c=r-Math.PI/2;return((S(c)+90)%360+360)%360}default:return 0}}function it(t,n,r=1e3,e=1e3,c=50){const a=Math.min(t.x-c,r-t.x-c),i=Math.min(t.y-c,e-t.y-c),s=Math.min(a,i),l=n*40/(2*Math.PI);return Math.min(s,l,400)}function v(t,n={}){const r=t.length;if(r===0)return[];const{preset:e="full",arcAngle:c,startAngle:a=0,radius:i,center:s,faceDirection:o="center",distributeEvenly:l=!0}=n,u=s||M(t),x=c!==void 0?c:ct(e),h=i!==void 0?i:it(u,r);if(h<=0)throw new Error("Circle radius must be positive");const f=Y(a-90),g=Math.abs(x%360)<.001;let d;if(r===1)d=[f];else if(g){const y=2*Math.PI/r;d=Array.from({length:r},(A,m)=>f+m*y)}else if(l){const y=Y(x);d=Array.from({length:r},(A,m)=>{const I=m/(r-1);return f+I*y})}else{const A=Y(x)/r;d=Array.from({length:r},(m,I)=>f+I*A)}return d.map(y=>{const A=u.x+h*Math.cos(y),m=u.y+h*Math.sin(y);return{x:A,y:m,facing:at({x:A,y:m},u,y,o)}})}function st(t,n){const r=t.length;if(r===0)return[];const{rings:e,radiusStep:c=100,innerRadius:a=150,center:i,faceDirection:s="center"}=n;if(e<1)throw new Error("Must have at least 1 ring");const o=i||M(t),l=Math.ceil(r/e),u=[];let x=0;for(let h=0;h<e&&x<r;h++){const f=a+h*c,g=Math.min(l,r-x),d=Array.from({length:g},()=>({x:0,y:0,facing:0})),y=v(d,{preset:"full",radius:f,center:o,faceDirection:s});u.push(...y),x+=g}return u}function ot(t,n,r=1e3,e=1e3,c=20){return t.length===0?!0:v(t,n).every(i=>i.x>=c&&i.x<=r-c&&i.y>=c&&i.y<=e-c)}function lt(t,n,r){const e=t.x,c=t.y,a=n.x,i=n.y,s=r.x,o=r.y,l=2*(e*(i-o)+a*(o-c)+s*(c-i));if(Math.abs(l)<.001)return null;const u=((e*e+c*c)*(i-o)+(a*a+i*i)*(o-c)+(s*s+o*o)*(c-i))/l,x=((e*e+c*c)*(s-a)+(a*a+i*i)*(e-s)+(s*s+o*o)*(a-e))/l,h={x:u,y:x},f=t.x-h.x,g=t.y-h.y,d=Math.sqrt(f*f+g*g),y=Math.atan2(-(t.y-h.y),t.x-h.x);let m=Math.atan2(-(r.y-h.y),r.x-h.x)-y;m<0&&(m+=2*Math.PI);const I=(S(y)+90+360)%360,P=S(m);return{center:h,radius:d,startAngle:I,arcAngle:P}}function ut(t){return(360-t)%360}function xt(t){return(180-t+360)%360}function w(t,n){if(t.length===0)return[];const{axis:e,axisPosition:c,center:a,mirrorFacing:i=!0}=n;let s;if(c!==void 0)s=c;else if(a)s=e==="horizontal"?a.x:a.y;else{const o=M(t);s=e==="horizontal"?o.x:o.y}return t.map(o=>{let l=o.x,u=o.y,x=o.facing;return e==="horizontal"?(l=2*s-o.x,i&&(x=ut(o.facing))):(u=2*s-o.y,i&&(x=xt(o.facing))),{x:l,y:u,facing:x}})}function ht(t,n){const r=w(t,n);return[...t,...r]}function N(t,n,r=!0){if(t.length===0)return[];const c=n||M(t);return t.map(a=>{const i=2*c.x-a.x,s=2*c.y-a.y,o=r?(a.facing+180)%360:a.facing;return{x:i,y:s,facing:o}})}function ft(t,n){const r=n||M(t),e=t,c=w(t,{axis:"horizontal",center:r}),a=w(t,{axis:"vertical",center:r}),i=N(t,r);return[...e,...c,...a,...i]}function yt(t,n,r=1e3,e=1e3,c=20){return t.length===0?!0:w(t,n).every(i=>i.x>=c&&i.x<=r-c&&i.y>=c&&i.y<=e-c)}function dt(t,n){const r=Math.abs(n.x-t.x),e=Math.abs(n.y-t.y);return r<e?{axis:"horizontal",axisPosition:(t.x+n.x)/2}:{axis:"vertical",axisPosition:(t.y+n.y)/2}}function gt(t,n,r=10){const e=[],c=new Set;for(let a=0;a<t.length;a++){if(c.has(a))continue;const i=w([t[a]],n)[0];for(let s=a+1;s<t.length;s++){if(c.has(s))continue;const o=t[s].x-i.x,l=t[s].y-i.y;if(Math.sqrt(o*o+l*l)<r){e.push([a,s]),c.add(a),c.add(s);break}}}return e}function mt(t){return t*Math.PI/180}function Mt(t){return(t%360+360)%360}function At(t,n,r){const e=Math.cos(r),c=Math.sin(r),a=t.x-n.x,i=t.y-n.y;return{x:n.x+a*e-i*c,y:n.y-(a*c-i*e)}}function b(t,n){if(t.length===0)return[];const{angle:e,center:c,rotateFacing:a=!0}=n,i=c||M(t),s=mt(e);return t.map(o=>{const l=At({x:o.x,y:o.y},i,s);return{x:l.x,y:l.y,facing:a?Mt(o.facing+e):o.facing}})}function It(t,n,r){return b(t,{angle:n,center:r})}function Rt(t,n,r){if(n<1)throw new Error("Number of copies must be at least 1");if(t.length===0)return[];const e=r||M(t),c=360/n,a=[];for(let i=0;i<n;i++){const s=i*c,o=b(t,{angle:s,center:e});a.push(...o)}return a}function St(t,n,r,e,c){if(e<2)throw new Error("Animation requires at least 2 steps");const a=c||M(t),i=[];for(let s=0;s<e;s++){const o=s/(e-1),l=n+o*(r-n),u=b(t,{angle:l,center:a});i.push(u)}return i}function wt(t,n,r){const e=Math.atan2(-(n.y-t.y),n.x-t.x);let a=Math.atan2(-(r.y-t.y),r.x-t.x)-e;return a>Math.PI?a-=2*Math.PI:a<-Math.PI&&(a+=2*Math.PI),a*180/Math.PI}function Ct(t,n=45){return Math.round(t/n)*n}function bt(t,n,r=1e3,e=1e3,c=20){return t.length===0?!0:b(t,n).every(i=>i.x>=c&&i.x<=r-c&&i.y>=c&&i.y<=e-c)}function Pt(t){if(t.length===0)return 0;let n=1/0,r=-1/0,e=1/0,c=-1/0;t.forEach(s=>{n=Math.min(n,s.x),r=Math.max(r,s.x),e=Math.min(e,s.y),c=Math.max(c,s.y)});const a=r-n,i=c-e;return Math.sqrt(a*a+i*i)}function Xt(t,n=1e3,r=1e3,e=20){const c=t.x-e,a=n-t.x-e,i=t.y-e,s=r-t.y-e;return Math.min(c,a,i,s)}function z(t){if(t.length<2)return{minX:0,maxX:0,minY:0,maxY:0};let n=1/0,r=-1/0,e=1/0,c=-1/0;for(let a=0;a<t.length;a+=2){const i=t[a],s=t[a+1];n=Math.min(n,i),r=Math.max(r,i),e=Math.min(e,s),c=Math.max(c,s)}return{minX:n,maxX:r,minY:e,maxY:c}}function Yt(t,n){if(n.length<6)return!1;let r=!1;const e=n.length/2;for(let c=0,a=e-1;c<e;a=c++){const i=n[c*2],s=n[c*2+1],o=n[a*2],l=n[a*2+1];s>t.y!=l>t.y&&t.x<(o-i)*(t.y-s)/(l-s)+i&&(r=!r)}return r}function $(t,n,r){if(n.length<6)return!1;const e=r||z(n);if(t.x<e.minX||t.x>e.maxX||t.y<e.minY||t.y>e.maxY)return!1;let c=!1;const a=n.length/2;for(let i=0,s=a-1;i<a;s=i++){const o=n[i*2],l=n[i*2+1],u=n[s*2],x=n[s*2+1];if(l===x)continue;l>t.y!=x>t.y&&t.x<(u-o)*(t.y-l)/(x-l)+o&&(c=!c)}return c}function vt(t,n){if(n.length<6)return t.map(()=>!1);const r=z(n);return t.map(e=>$(e,n,r))}function zt(t){if(t.length<6)return 0;let n=0;const r=t.length/2;for(let e=0;e<r;e++){const c=(e+1)%r,a=t[e*2],i=t[e*2+1],s=t[c*2],o=t[c*2+1];n+=a*o-s*i}return n/2}function Bt(t){if(t.length<6)return{x:0,y:0};let n=0,r=0,e=0;const c=t.length/2;for(let a=0;a<c;a++){const i=(a+1)%c,s=t[a*2],o=t[a*2+1],l=t[i*2],u=t[i*2+1],x=s*u-l*o;n+=(s+l)*x,r+=(o+u)*x,e+=x}if(e/=2,Math.abs(e)<1e-4){let a=0,i=0;for(let s=0;s<t.length;s+=2)a+=t[s],i+=t[s+1];return{x:a/c,y:i/c}}return{x:n/(6*e),y:r/(6*e)}}function Ft(t,n){if(t.length<6)return[...t];const r=[];r.push(t[0],t[1]);for(let e=2;e<t.length;e+=2){const c=r[r.length-2],a=r[r.length-1],i=t[e],s=t[e+1],o=i-c,l=s-a;Math.sqrt(o*o+l*l)>=n&&r.push(i,s)}return r}const B=80,U=20;function F(t,n,r,e,c){const a=t*t,i=a*t,s=1-t,o=s*s,u=o*s,x=3*o*t,h=3*s*a,f=i;return{x:u*n.x+x*r.x+h*e.x+f*c.x,y:u*n.y+x*r.y+h*e.y+f*c.y}}function T(t,n,r){const e=(t.x+n.x)/2,c=(t.y+n.y)/2,a=n.x-t.x,s=-(n.y-t.y),o=a,l=Math.sqrt(s*s+o*o);if(l<1e-4){const y={x:e,y:c};return[y,y]}const u=s/l,x=o/l,h=-u*r,f=-x*r,g={x:e+h,y:c+f},d={x:e+h,y:c+f};return[g,d]}function _(t,n,r=B,e=U){if(e<2)throw new Error("Number of interpolation points must be at least 2");const[c,a]=T(t,n,r),i=[];for(let s=0;s<e;s++){const o=s/(e-1),l=F(o,{x:t.x,y:t.y},c,a,{x:n.x,y:n.y});i.push(l)}return i}function Tt(t,n,r=150){const e=n.x-t.x,c=n.y-t.y,i=Math.sqrt(e*e+c*c)*.15;return Math.min(i,r)}function Dt(t,n,r=B,e=U){const c=_(t,n,r,e);let a=0;for(let i=1;i<c.length;i++){const s=c[i].x-c[i-1].x,o=c[i].y-c[i-1].y;a+=Math.sqrt(s*s+o*o)}return a}function Et(t,n,r,e=B){const c=Math.max(0,Math.min(1,r)),[a,i]=T(t,n,e);return F(c,{x:t.x,y:t.y},a,i,{x:n.x,y:n.y})}function X(t,n,r,e,c){const a=c*c,i=a*c,s=.5*(2*n.x+(-t.x+r.x)*c+(2*t.x-5*n.x+4*r.x-e.x)*a+(-t.x+3*n.x-3*r.x+e.x)*i),o=.5*(2*n.y+(-t.y+r.y)*c+(2*t.y-5*n.y+4*r.y-e.y)*a+(-t.y+3*n.y-3*r.y+e.y)*i);return{x:s,y:o}}function Lt(t,n=20){if(t.length<2)return[...t];if(t.length===2)return[t[0],t[1]];const r=[];for(let e=0;e<t.length-1;e++){const c=e===0?t[0]:t[e-1],a=t[e],i=t[e+1],s=e===t.length-2?t[e+1]:t[e+2];for(let o=0;o<n;o++){const l=o/n,u=X(c,a,i,s,l);r.push(u)}}return r.push(t[t.length-1]),r}function H(t,n,r,e){const c={x:n.x+(r.x-t.x)/6,y:n.y+(r.y-t.y)/6},a={x:r.x-(e.x-n.x)/6,y:r.y-(e.y-n.y)/6};return{start:n,control1:c,control2:a,end:r}}function jt(t){if(t.length<2)return[];const n=[];for(let r=0;r<t.length-1;r++){const e=r===0?t[0]:t[r-1],c=t[r],a=t[r+1],i=r===t.length-2?t[r+1]:t[r+2];n.push(H(e,c,a,i))}return n}function K(t,n,r,e,c){const a=c*c,i=.5*(-t.x+r.x+2*(2*t.x-5*n.x+4*r.x-e.x)*c+3*(-t.x+3*n.x-3*r.x+e.x)*a),s=.5*(-t.y+r.y+2*(2*t.y-5*n.y+4*r.y-e.y)*c+3*(-t.y+3*n.y-3*r.y+e.y)*a);return{x:i,y:s}}function qt(t,n,r,e,c){const a=K(t,n,r,e,c);return Math.atan2(a.y,a.x)*180/Math.PI}function kt(t,n,r,e,c=100){let a=0,i=n;for(let s=1;s<=c;s++){const o=s/c,l=X(t,n,r,e,o),u=l.x-i.x,x=l.y-i.y;a+=Math.sqrt(u*u+x*x),i=l}return a}function Ot(t,n){if(t.length<2)return[...t];const r=[t[0]];let e=0,c=n;for(let a=0;a<t.length-1;a++){const i=a===0?t[0]:t[a-1],s=t[a],o=t[a+1],l=a===t.length-2?t[a+1]:t[a+2];let u=s;const x=100;for(let h=1;h<=x;h++){const f=h/x,g=X(i,s,o,l,f),d=g.x-u.x,y=g.y-u.y,A=Math.sqrt(d*d+y*y);e+=A,e>=c&&(r.push(g),c+=n),u=g}}return r}const R=20;function C(t,n=R){return{minX:t.x-n,maxX:t.x+n,minY:t.y-n,maxY:t.y+n}}function D(t,n){return t.maxX>=n.minX&&t.minX<=n.maxX&&t.maxY>=n.minY&&t.minY<=n.maxY}function E(t,n,r=R,e=R){const c=n.x-t.x,a=n.y-t.y,i=c*c+a*a,s=r+e;return i<s*s}function L(t,n){const r=n.x-t.x,e=n.y-t.y;return Math.sqrt(r*r+e*e)}function V(t,n=R){const r=[];for(let e=0;e<t.length;e++)for(let c=e+1;c<t.length;c++){const a=t[e],i=t[c],s=C(a.position,n),o=C(i.position,n);if(D(s,o)&&E(a.position,i.position,n,n)){const l=L(a.position,i.position),u=n*2-l;r.push({id1:a.id,id2:i.id,distance:l,penetration:u})}}return r}class G{constructor(n=R,r){this.radius=n,this.cellSize=r||n*2,this.grid=new Map}hashKey(n,r){const e=Math.floor(n/this.cellSize),c=Math.floor(r/this.cellSize);return`${e},${c}`}insert(n,r){const e=this.hashKey(r.x,r.y);this.grid.has(e)||this.grid.set(e,[]),this.grid.get(e).push({id:n,position:r})}getNearby(n){const r=[],e=Math.floor(n.x/this.cellSize),c=Math.floor(n.y/this.cellSize);for(let a=-1;a<=1;a++)for(let i=-1;i<=1;i++){const s=`${e+a},${c+i}`,o=this.grid.get(s);o&&r.push(...o)}return r}clear(){this.grid.clear()}detectCollisions(n){this.clear(),n.forEach(c=>this.insert(c.id,c.position));const r=[],e=new Set;return n.forEach(c=>{this.getNearby(c.position).forEach(i=>{if(c.id===i.id)return;const s=c.id<i.id?`${c.id}:${i.id}`:`${i.id}:${c.id}`;if(e.has(s))return;e.add(s);const o=C(c.position,this.radius),l=C(i.position,this.radius);if(D(o,l)&&E(c.position,i.position,this.radius,this.radius)){const u=L(c.position,i.position),x=this.radius*2-u;r.push({id1:c.id,id2:i.id,distance:u,penetration:x})}})}),r}getStats(){const n=this.grid.size;let r=0,e=0;return this.grid.forEach(c=>{r+=c.length,e=Math.max(e,c.length)}),{totalCells:this.grid.size*9,occupiedCells:n,totalDancers:r,avgDancersPerCell:n>0?r/n:0,maxDancersInCell:e}}}function Nt(t,n=R){return t.length<50?V(t,n):new G(n).detectCollisions(t)}const $t="1.0.0";exports.DANCER_RADIUS=R;exports.SpatialHashGrid=G;exports.VERSION=$t;exports.animateRotation=St;exports.arrangeInCircle=v;exports.arrangeInConcentricCircles=st;exports.arrangeInLine=O;exports.batchPointInPolygon=vt;exports.calculateArcControlPoints=T;exports.calculateArcFromThreePoints=lt;exports.calculateArcLength=Dt;exports.calculateCentroid=M;exports.calculateDistance=L;exports.calculateLineFromDrag=et;exports.calculateMirrorAxisFromDrag=dt;exports.calculateOptimalArcHeight=Tt;exports.calculateRotationFromDrag=wt;exports.calculateSpreadFactorForDistance=p;exports.calculateSwapArcPath=_;exports.catmullRomAngle=qt;exports.catmullRomArcLength=kt;exports.catmullRomPoint=X;exports.catmullRomSpline=Lt;exports.catmullRomSplineToBezier=jt;exports.catmullRomTangent=K;exports.catmullRomToBezier=H;exports.catmullRomUniformSample=Ot;exports.checkAABBOverlap=D;exports.checkCircleCollision=E;exports.computeBoundingBox=z;exports.createAABB=C;exports.createKaleidoscope=ft;exports.createRotationalCopies=Rt;exports.cubicBezier=F;exports.detectCollisions=V;exports.detectCollisionsAuto=Nt;exports.directionalSpread=Z;exports.distance=W;exports.ellipticalScale=k;exports.findSymmetricPairs=gt;exports.getArcPositionAtTime=Et;exports.getBoundingBox=tt;exports.getBoundingBoxDiagonal=Pt;exports.getMaxRotationRadius=Xt;exports.isCircleArrangementSafe=ot;exports.isLineArrangementSafe=rt;exports.isMirrorSafe=yt;exports.isPointInPolygon=Yt;exports.isPointInPolygonOptimized=$;exports.isRotationSafe=bt;exports.isSpreadSafe=nt;exports.mirrorAndDuplicate=ht;exports.mirrorBothAxes=N;exports.mirrorFormation=w;exports.pointInPolygon=Q;exports.polygonArea=zt;exports.polygonCentroid=Bt;exports.rotateFormation=b;exports.rotateFormationPreset=It;exports.scaleAroundCenter=J;exports.simplifyPolygon=Ft;exports.snapRotationAngle=Ct;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type { Point, DancerPosition, SpreadOptions, LineArrangementOptions, CircleArrangementOptions, RotationOptions, MirrorOptions, BezierControlPoint, BoundingBox, } from './types';
|
|
2
|
+
export { calculateCentroid, scaleAroundCenter, pointInPolygon, distance, } from './centroid';
|
|
3
|
+
export { ellipticalScale, directionalSpread, calculateSpreadFactorForDistance, getBoundingBox, isSpreadSafe, } from './spreader';
|
|
4
|
+
export { arrangeInLine, calculateLineFromDrag, isLineArrangementSafe, } from './lineArrangement';
|
|
5
|
+
export { arrangeInCircle, arrangeInConcentricCircles, isCircleArrangementSafe, calculateArcFromThreePoints, } from './circleArrangement';
|
|
6
|
+
export { mirrorFormation, mirrorAndDuplicate, mirrorBothAxes, createKaleidoscope, isMirrorSafe, calculateMirrorAxisFromDrag, findSymmetricPairs, } from './mirrorFlip';
|
|
7
|
+
export type { RotationPreset, RotateOptions } from './rotateFormation';
|
|
8
|
+
export { rotateFormation, rotateFormationPreset, createRotationalCopies, animateRotation, calculateRotationFromDrag, snapRotationAngle, isRotationSafe, getBoundingBoxDiagonal, getMaxRotationRadius, } from './rotateFormation';
|
|
9
|
+
export type { PolygonBoundingBox } from './polygon';
|
|
10
|
+
export { computeBoundingBox, isPointInPolygon, isPointInPolygonOptimized, batchPointInPolygon, polygonArea, polygonCentroid, simplifyPolygon, } from './polygon';
|
|
11
|
+
export { cubicBezier, calculateArcControlPoints, calculateSwapArcPath, calculateOptimalArcHeight, calculateArcLength, getArcPositionAtTime, } from './bezier';
|
|
12
|
+
export type { BezierControlPoints } from './spline';
|
|
13
|
+
export { catmullRomPoint, catmullRomSpline, catmullRomToBezier, catmullRomSplineToBezier, catmullRomTangent, catmullRomAngle, catmullRomArcLength, catmullRomUniformSample, } from './spline';
|
|
14
|
+
export type { AABB, CollisionPair, DancerWithId } from './collision';
|
|
15
|
+
export { DANCER_RADIUS, createAABB, checkAABBOverlap, checkCircleCollision, calculateDistance, detectCollisions, SpatialHashGrid, detectCollisionsAuto, } from './collision';
|
|
16
|
+
export declare const VERSION = "1.0.0";
|