@vector-display/core 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rick Segrest
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # @vector-display/core
2
+
3
+ Renderer-independent core of **vector-display**, a set of libraries for drawing graphics that look like vector arcade monitors (*Asteroids*, *Tempest*, *Battlezone*).
4
+
5
+ This package holds shapes and the per-frame **display list**: the transformed line segments a renderer draws. It has no rendering code, so it also runs in Node and tests. Pair it with [`@vector-display/webgl`](https://www.npmjs.com/package/@vector-display/webgl) to draw.
6
+
7
+ > **Status:** early (0.x). The API may change between minor versions.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install @vector-display/core
13
+ ```
14
+
15
+ ES modules only, with TypeScript declarations.
16
+
17
+ ## Usage
18
+
19
+ ```ts
20
+ import { DisplayList, Shape } from "@vector-display/core";
21
+
22
+ // Build shapes once. Points only need x and y, so es-vector-math Vector/Point objects work too.
23
+ const ship = Shape.fromPolyline({
24
+ points: [{ x: 0, y: -10 }, { x: -7.5, y: 10 }, { x: -5, y: 5 }, { x: 5, y: 5 }, { x: 7.5, y: 10 }],
25
+ isClosed: true,
26
+ });
27
+ const bullet = Shape.createDot();
28
+
29
+ // Rebuild the display list every frame; it reuses its memory.
30
+ const displayList = new DisplayList();
31
+ displayList.clear();
32
+ displayList.setColor({ red: 0.85, green: 0.95, blue: 1 });
33
+ displayList.addShape(ship, { x: 512, y: 384, rotation: 0, scale: 2, intensity: 1 });
34
+ displayList.addShape(bullet, { x: 540, y: 360, rotation: 0, scale: 1, intensity: 1.4 });
35
+ ```
36
+
37
+ ## API
38
+
39
+ | Export | Purpose |
40
+ | --- | --- |
41
+ | `Shape.fromPolyline(polyline)` / `Shape.fromPolylines(polylines)` | Line geometry from points; closed polylines connect back to their first point |
42
+ | `Shape.fromSegmentCoordinates([x0, y0, x1, y1, ...])` | Geometry from raw segments; touching segments are connected |
43
+ | `Shape.createDot()` | A zero-length segment, drawn as a round dot |
44
+ | `DisplayList` | `clear()`, `setColor(color)`, `addShape(shape, placement)`, `getSegmentData()`, `segmentCount` |
45
+ | `ShapeGeometry` | Interface for any shape-like geometry, including geometry rebuilt every frame |
46
+ | `SCREEN_SPACE_PLACEMENT` | Placement for geometry already in world coordinates |
47
+
48
+ **Coordinates:** world units with a top-left origin and y pointing down (like Canvas 2D and p5). Positive rotation turns clockwise on screen.
49
+
50
+ ## Related packages
51
+
52
+ - [`@vector-display/webgl`](https://www.npmjs.com/package/@vector-display/webgl): WebGL2 renderer
53
+ - [`@vector-display/beam-fx`](https://www.npmjs.com/package/@vector-display/beam-fx): phosphor trails, bloom and flicker
54
+ - [`@vector-display/font`](https://www.npmjs.com/package/@vector-display/font): Atari-style stroke font
55
+ - [`@vector-display/3d`](https://www.npmjs.com/package/@vector-display/3d): 3D wireframes
56
+
57
+ ## License
58
+
59
+ MIT © Rick Segrest
@@ -0,0 +1,16 @@
1
+ import type { BeamColor, ShapeGeometry, ShapePlacement } from "./geometryTypes.js";
2
+ export declare const FLOATS_PER_BEAM_SEGMENT = 14;
3
+ export declare class DisplayList {
4
+ private segmentData;
5
+ private usedSegmentCount;
6
+ private currentColor;
7
+ get segmentCount(): number;
8
+ getSegmentData(): Float32Array;
9
+ clear(): void;
10
+ setColor(color: BeamColor): void;
11
+ addShape(shape: ShapeGeometry, placement: ShapePlacement): void;
12
+ private ensureCapacity;
13
+ private writeTransformedSegments;
14
+ }
15
+ export default DisplayList;
16
+ //# sourceMappingURL=DisplayList.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DisplayList.d.ts","sourceRoot":"","sources":["../src/DisplayList.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AASnF,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAM1C,qBAAa,WAAW;IACpB,OAAO,CAAC,WAAW,CAAwE;IAC3F,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,YAAY,CAAoB;IAExC,IAAW,YAAY,IAAI,MAAM,CAEhC;IAEM,cAAc,IAAI,YAAY,CAEpC;IAEM,KAAK,IAAI,IAAI,CAEnB;IAEM,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAEtC;IAEM,QAAQ,CAAC,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,cAAc,GAAG,IAAI,CAIrE;IAED,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,wBAAwB;CAyCnC;eAEc,WAAW"}
@@ -0,0 +1,88 @@
1
+ import { NO_NEIGHBOR_SEGMENT } from "./Shape.js";
2
+ // Packed per segment (world units):
3
+ // 0-3 x0, y0, x1, y1
4
+ // 4-5 start point of the connected previous segment
5
+ // 6-7 end point of the connected next segment
6
+ // 8-11 red, green, blue, intensity
7
+ // 12-13 hasPreviousNeighbor, hasNextNeighbor (1 or 0)
8
+ export const FLOATS_PER_BEAM_SEGMENT = 14;
9
+ const INITIAL_SEGMENT_CAPACITY = 1024;
10
+ const WHITE = { red: 1, green: 1, blue: 1 };
11
+ // The beam's drawing program for one frame, in draw order. Reused across frames to avoid allocation.
12
+ export class DisplayList {
13
+ segmentData = new Float32Array(INITIAL_SEGMENT_CAPACITY * FLOATS_PER_BEAM_SEGMENT);
14
+ usedSegmentCount = 0;
15
+ currentColor = WHITE;
16
+ get segmentCount() {
17
+ return this.usedSegmentCount;
18
+ }
19
+ getSegmentData() {
20
+ return this.segmentData.subarray(0, this.usedSegmentCount * FLOATS_PER_BEAM_SEGMENT);
21
+ }
22
+ clear() {
23
+ this.usedSegmentCount = 0;
24
+ }
25
+ setColor(color) {
26
+ this.currentColor = color;
27
+ }
28
+ addShape(shape, placement) {
29
+ this.ensureCapacity(this.usedSegmentCount + shape.segmentCount);
30
+ this.writeTransformedSegments(shape, placement);
31
+ this.usedSegmentCount += shape.segmentCount;
32
+ }
33
+ ensureCapacity(requiredSegmentCount) {
34
+ const currentCapacity = this.segmentData.length / FLOATS_PER_BEAM_SEGMENT;
35
+ if (requiredSegmentCount <= currentCapacity)
36
+ return;
37
+ let newCapacity = currentCapacity;
38
+ while (newCapacity < requiredSegmentCount)
39
+ newCapacity *= 2;
40
+ const grownData = new Float32Array(newCapacity * FLOATS_PER_BEAM_SEGMENT);
41
+ grownData.set(this.getSegmentData());
42
+ this.segmentData = grownData;
43
+ }
44
+ // World coordinates use a y-down origin at the top left, so positive rotation turns clockwise on screen.
45
+ writeTransformedSegments(shape, placement) {
46
+ const { x, y, rotation, scale, intensity } = placement;
47
+ const { red, green, blue } = this.currentColor;
48
+ const scaledCosine = Math.cos(rotation) * scale;
49
+ const scaledSine = Math.sin(rotation) * scale;
50
+ const source = shape.segmentCoordinates;
51
+ const neighbors = shape.segmentNeighbors;
52
+ const target = this.segmentData;
53
+ let targetIndex = this.usedSegmentCount * FLOATS_PER_BEAM_SEGMENT;
54
+ for (let segmentIndex = 0; segmentIndex < shape.segmentCount; segmentIndex++) {
55
+ const sourceIndex = segmentIndex * 4;
56
+ const previousSegment = neighbors[segmentIndex * 2];
57
+ const nextSegment = neighbors[segmentIndex * 2 + 1];
58
+ // Fall back to this segment's own points so unused neighbor slots hold finite values.
59
+ const previousStartIndex = previousSegment === NO_NEIGHBOR_SEGMENT ? sourceIndex : previousSegment * 4;
60
+ const nextEndIndex = nextSegment === NO_NEIGHBOR_SEGMENT ? sourceIndex + 2 : nextSegment * 4 + 2;
61
+ const startX = source[sourceIndex];
62
+ const startY = source[sourceIndex + 1];
63
+ const endX = source[sourceIndex + 2];
64
+ const endY = source[sourceIndex + 3];
65
+ const previousStartX = source[previousStartIndex];
66
+ const previousStartY = source[previousStartIndex + 1];
67
+ const nextEndX = source[nextEndIndex];
68
+ const nextEndY = source[nextEndIndex + 1];
69
+ target[targetIndex] = x + startX * scaledCosine - startY * scaledSine;
70
+ target[targetIndex + 1] = y + startX * scaledSine + startY * scaledCosine;
71
+ target[targetIndex + 2] = x + endX * scaledCosine - endY * scaledSine;
72
+ target[targetIndex + 3] = y + endX * scaledSine + endY * scaledCosine;
73
+ target[targetIndex + 4] = x + previousStartX * scaledCosine - previousStartY * scaledSine;
74
+ target[targetIndex + 5] = y + previousStartX * scaledSine + previousStartY * scaledCosine;
75
+ target[targetIndex + 6] = x + nextEndX * scaledCosine - nextEndY * scaledSine;
76
+ target[targetIndex + 7] = y + nextEndX * scaledSine + nextEndY * scaledCosine;
77
+ target[targetIndex + 8] = red;
78
+ target[targetIndex + 9] = green;
79
+ target[targetIndex + 10] = blue;
80
+ target[targetIndex + 11] = intensity;
81
+ target[targetIndex + 12] = previousSegment === NO_NEIGHBOR_SEGMENT ? 0 : 1;
82
+ target[targetIndex + 13] = nextSegment === NO_NEIGHBOR_SEGMENT ? 0 : 1;
83
+ targetIndex += FLOATS_PER_BEAM_SEGMENT;
84
+ }
85
+ }
86
+ }
87
+ export default DisplayList;
88
+ //# sourceMappingURL=DisplayList.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DisplayList.js","sourceRoot":"","sources":["../src/DisplayList.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { Polyline, ShapeGeometry } from "./geometryTypes.js";
2
+ export declare const FLOATS_PER_SHAPE_SEGMENT = 4;
3
+ export declare const NO_NEIGHBOR_SEGMENT = -1;
4
+ export declare class Shape implements ShapeGeometry {
5
+ readonly segmentCoordinates: Float32Array;
6
+ readonly segmentNeighbors: Int32Array;
7
+ private constructor();
8
+ get segmentCount(): number;
9
+ static fromPolyline(polyline: Polyline): Shape;
10
+ static fromPolylines(polylines: readonly Polyline[]): Shape;
11
+ static fromSegmentCoordinates(segmentCoordinates: ArrayLike<number>): Shape;
12
+ static createDot(): Shape;
13
+ }
14
+ export default Shape;
15
+ //# sourceMappingURL=Shape.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Shape.d.ts","sourceRoot":"","sources":["../src/Shape.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAElE,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAC1C,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAOtC,qBAAa,KAAM,YAAW,aAAa;IACvC,SAAgB,kBAAkB,EAAE,YAAY,CAAC;IAEjD,SAAgB,gBAAgB,EAAE,UAAU,CAAC;IAE7C,OAAO,eAGN;IAED,IAAW,YAAY,IAAI,MAAM,CAEhC;IAED,OAAc,YAAY,CAAC,QAAQ,EAAE,QAAQ,GAAG,KAAK,CAEpD;IAED,OAAc,aAAa,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,GAAG,KAAK,CAMjE;IAGD,OAAc,sBAAsB,CAAC,kBAAkB,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG,KAAK,CAQjF;IAGD,OAAc,SAAS,IAAI,KAAK,CAE/B;CACJ;eA2Dc,KAAK"}
package/dist/Shape.js ADDED
@@ -0,0 +1,92 @@
1
+ export const FLOATS_PER_SHAPE_SEGMENT = 4;
2
+ export const NO_NEIGHBOR_SEGMENT = -1;
3
+ export class Shape {
4
+ segmentCoordinates;
5
+ // Two entries per segment: the index of the connected previous and next segment, or NO_NEIGHBOR_SEGMENT.
6
+ segmentNeighbors;
7
+ constructor(buffers) {
8
+ this.segmentCoordinates = Float32Array.from(buffers.coordinates);
9
+ this.segmentNeighbors = Int32Array.from(buffers.neighborIndices);
10
+ }
11
+ get segmentCount() {
12
+ return this.segmentCoordinates.length / FLOATS_PER_SHAPE_SEGMENT;
13
+ }
14
+ static fromPolyline(polyline) {
15
+ return Shape.fromPolylines([polyline]);
16
+ }
17
+ static fromPolylines(polylines) {
18
+ const buffers = { coordinates: [], neighborIndices: [] };
19
+ for (const polyline of polylines) {
20
+ appendPolylineSegments(buffers, polyline);
21
+ }
22
+ return new Shape(buffers);
23
+ }
24
+ // Consecutive segments whose end and start coincide are treated as connected, including last-to-first.
25
+ static fromSegmentCoordinates(segmentCoordinates) {
26
+ if (segmentCoordinates.length % FLOATS_PER_SHAPE_SEGMENT !== 0) {
27
+ throw new Error(`Segment coordinates must come in groups of ${FLOATS_PER_SHAPE_SEGMENT} (x0, y0, x1, y1), but received ${segmentCoordinates.length} values`);
28
+ }
29
+ const coordinates = Array.from(segmentCoordinates);
30
+ return new Shape({ coordinates, neighborIndices: findTouchingNeighbors(coordinates) });
31
+ }
32
+ // A zero-length segment renders as a round dot, like a bullet on a vector monitor.
33
+ static createDot() {
34
+ return new Shape({ coordinates: [0, 0, 0, 0], neighborIndices: [NO_NEIGHBOR_SEGMENT, NO_NEIGHBOR_SEGMENT] });
35
+ }
36
+ }
37
+ function appendPolylineSegments(buffers, polyline) {
38
+ const { points, isClosed } = polyline;
39
+ if (points.length === 0)
40
+ return;
41
+ if (points.length === 1) {
42
+ buffers.coordinates.push(points[0].x, points[0].y, points[0].x, points[0].y);
43
+ buffers.neighborIndices.push(NO_NEIGHBOR_SEGMENT, NO_NEIGHBOR_SEGMENT);
44
+ return;
45
+ }
46
+ const firstSegmentIndex = buffers.coordinates.length / FLOATS_PER_SHAPE_SEGMENT;
47
+ for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex++) {
48
+ const start = points[pointIndex];
49
+ const end = points[pointIndex + 1];
50
+ buffers.coordinates.push(start.x, start.y, end.x, end.y);
51
+ }
52
+ const wrapsAround = isClosed && points.length > 2;
53
+ if (wrapsAround) {
54
+ const last = points[points.length - 1];
55
+ buffers.coordinates.push(last.x, last.y, points[0].x, points[0].y);
56
+ }
57
+ const segmentCount = buffers.coordinates.length / FLOATS_PER_SHAPE_SEGMENT - firstSegmentIndex;
58
+ appendChainNeighbors(buffers.neighborIndices, { firstSegmentIndex, segmentCount, wrapsAround });
59
+ }
60
+ function appendChainNeighbors(neighborIndices, chain) {
61
+ const { firstSegmentIndex, segmentCount, wrapsAround } = chain;
62
+ const lastSegmentIndex = firstSegmentIndex + segmentCount - 1;
63
+ for (let segmentIndex = firstSegmentIndex; segmentIndex <= lastSegmentIndex; segmentIndex++) {
64
+ const isFirst = segmentIndex === firstSegmentIndex;
65
+ const isLast = segmentIndex === lastSegmentIndex;
66
+ const previousIndex = isFirst ? (wrapsAround ? lastSegmentIndex : NO_NEIGHBOR_SEGMENT) : segmentIndex - 1;
67
+ const nextIndex = isLast ? (wrapsAround ? firstSegmentIndex : NO_NEIGHBOR_SEGMENT) : segmentIndex + 1;
68
+ neighborIndices.push(previousIndex, nextIndex);
69
+ }
70
+ }
71
+ function findTouchingNeighbors(coordinates) {
72
+ const segmentCount = coordinates.length / FLOATS_PER_SHAPE_SEGMENT;
73
+ const neighborIndices = new Array(segmentCount * 2).fill(NO_NEIGHBOR_SEGMENT);
74
+ for (let segmentIndex = 0; segmentIndex < segmentCount; segmentIndex++) {
75
+ const nextIndex = (segmentIndex + 1) % segmentCount;
76
+ if (nextIndex === segmentIndex)
77
+ continue;
78
+ if (nextIndex === 0 && segmentCount <= 2)
79
+ continue;
80
+ const endOffset = segmentIndex * FLOATS_PER_SHAPE_SEGMENT + 2;
81
+ const nextStartOffset = nextIndex * FLOATS_PER_SHAPE_SEGMENT;
82
+ const touches = coordinates[endOffset] === coordinates[nextStartOffset] &&
83
+ coordinates[endOffset + 1] === coordinates[nextStartOffset + 1];
84
+ if (!touches)
85
+ continue;
86
+ neighborIndices[segmentIndex * 2 + 1] = nextIndex;
87
+ neighborIndices[nextIndex * 2] = segmentIndex;
88
+ }
89
+ return neighborIndices;
90
+ }
91
+ export default Shape;
92
+ //# sourceMappingURL=Shape.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Shape.js","sourceRoot":"","sources":["../src/Shape.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ export interface PointLike {
2
+ readonly x: number;
3
+ readonly y: number;
4
+ }
5
+ export interface Polyline {
6
+ readonly points: readonly PointLike[];
7
+ readonly isClosed: boolean;
8
+ }
9
+ export interface BeamColor {
10
+ readonly red: number;
11
+ readonly green: number;
12
+ readonly blue: number;
13
+ }
14
+ export interface ShapePlacement {
15
+ readonly x: number;
16
+ readonly y: number;
17
+ readonly rotation: number;
18
+ readonly scale: number;
19
+ readonly intensity: number;
20
+ }
21
+ export interface ShapeGeometry {
22
+ readonly segmentCoordinates: Float32Array;
23
+ readonly segmentNeighbors: Int32Array;
24
+ readonly segmentCount: number;
25
+ }
26
+ export declare const SCREEN_SPACE_PLACEMENT: ShapePlacement;
27
+ //# sourceMappingURL=geometryTypes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometryTypes.d.ts","sourceRoot":"","sources":["../src/geometryTypes.ts"],"names":[],"mappings":"AACA,MAAM,WAAW,SAAS;IACtB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,QAAQ;IACrB,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,SAAS;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC3B,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC9B;AAGD,MAAM,WAAW,aAAa;IAC1B,QAAQ,CAAC,kBAAkB,EAAE,YAAY,CAAC;IAC1C,QAAQ,CAAC,gBAAgB,EAAE,UAAU,CAAC;IACtC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CACjC;AAGD,eAAO,MAAM,sBAAsB,EAAE,cAAoE,CAAC"}
@@ -0,0 +1,3 @@
1
+ // For geometry that is already in world coordinates.
2
+ export const SCREEN_SPACE_PLACEMENT = { x: 0, y: 0, rotation: 0, scale: 1, intensity: 1 };
3
+ //# sourceMappingURL=geometryTypes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometryTypes.js","sourceRoot":"","sources":["../src/geometryTypes.ts"],"names":[],"mappings":"AAgCA,qDAAqD;AACrD,MAAM,CAAC,MAAM,sBAAsB,GAAmB,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { Shape, FLOATS_PER_SHAPE_SEGMENT, NO_NEIGHBOR_SEGMENT } from "./Shape.js";
2
+ export { DisplayList, FLOATS_PER_BEAM_SEGMENT } from "./DisplayList.js";
3
+ export { SCREEN_SPACE_PLACEMENT } from "./geometryTypes.js";
4
+ export type { BeamColor, PointLike, Polyline, ShapeGeometry, ShapePlacement } from "./geometryTypes.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAClF,OAAO,EAAE,WAAW,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { Shape, FLOATS_PER_SHAPE_SEGMENT, NO_NEIGHBOR_SEGMENT } from "./Shape.js";
2
+ export { DisplayList, FLOATS_PER_BEAM_SEGMENT } from "./DisplayList.js";
3
+ export { SCREEN_SPACE_PLACEMENT } from "./geometryTypes.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAClF,OAAO,EAAE,WAAW,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@vector-display/core",
3
+ "version": "0.1.0",
4
+ "description": "Renderer-independent display lists of beam strokes for vector-arcade-style graphics.",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "Rick Segrest",
8
+ "email": "rsegrest77@gmail.com",
9
+ "url": "https://www.ricksegrest.com"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/rsegrest/vector-display.git",
14
+ "directory": "packages/core"
15
+ },
16
+ "homepage": "https://github.com/rsegrest/vector-display/tree/main/packages/core#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/rsegrest/vector-display/issues"
19
+ },
20
+ "type": "module",
21
+ "sideEffects": false,
22
+ "exports": {
23
+ ".": {
24
+ "@vector-display/source": "./src/index.ts",
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js"
27
+ }
28
+ },
29
+ "scripts": {
30
+ "prepublishOnly": "tsc -b"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "src",
35
+ "!src/**/*.test.ts"
36
+ ],
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "dependencies": {}
41
+ }
@@ -0,0 +1,97 @@
1
+ import type { BeamColor, ShapeGeometry, ShapePlacement } from "./geometryTypes.js";
2
+ import { NO_NEIGHBOR_SEGMENT } from "./Shape.js";
3
+
4
+ // Packed per segment (world units):
5
+ // 0-3 x0, y0, x1, y1
6
+ // 4-5 start point of the connected previous segment
7
+ // 6-7 end point of the connected next segment
8
+ // 8-11 red, green, blue, intensity
9
+ // 12-13 hasPreviousNeighbor, hasNextNeighbor (1 or 0)
10
+ export const FLOATS_PER_BEAM_SEGMENT = 14;
11
+
12
+ const INITIAL_SEGMENT_CAPACITY = 1024;
13
+ const WHITE: BeamColor = { red: 1, green: 1, blue: 1 };
14
+
15
+ // The beam's drawing program for one frame, in draw order. Reused across frames to avoid allocation.
16
+ export class DisplayList {
17
+ private segmentData = new Float32Array(INITIAL_SEGMENT_CAPACITY * FLOATS_PER_BEAM_SEGMENT);
18
+ private usedSegmentCount = 0;
19
+ private currentColor: BeamColor = WHITE;
20
+
21
+ public get segmentCount(): number {
22
+ return this.usedSegmentCount;
23
+ }
24
+
25
+ public getSegmentData(): Float32Array {
26
+ return this.segmentData.subarray(0, this.usedSegmentCount * FLOATS_PER_BEAM_SEGMENT);
27
+ }
28
+
29
+ public clear(): void {
30
+ this.usedSegmentCount = 0;
31
+ }
32
+
33
+ public setColor(color: BeamColor): void {
34
+ this.currentColor = color;
35
+ }
36
+
37
+ public addShape(shape: ShapeGeometry, placement: ShapePlacement): void {
38
+ this.ensureCapacity(this.usedSegmentCount + shape.segmentCount);
39
+ this.writeTransformedSegments(shape, placement);
40
+ this.usedSegmentCount += shape.segmentCount;
41
+ }
42
+
43
+ private ensureCapacity(requiredSegmentCount: number): void {
44
+ const currentCapacity = this.segmentData.length / FLOATS_PER_BEAM_SEGMENT;
45
+ if (requiredSegmentCount <= currentCapacity) return;
46
+ let newCapacity = currentCapacity;
47
+ while (newCapacity < requiredSegmentCount) newCapacity *= 2;
48
+ const grownData = new Float32Array(newCapacity * FLOATS_PER_BEAM_SEGMENT);
49
+ grownData.set(this.getSegmentData());
50
+ this.segmentData = grownData;
51
+ }
52
+
53
+ // World coordinates use a y-down origin at the top left, so positive rotation turns clockwise on screen.
54
+ private writeTransformedSegments(shape: ShapeGeometry, placement: ShapePlacement): void {
55
+ const { x, y, rotation, scale, intensity } = placement;
56
+ const { red, green, blue } = this.currentColor;
57
+ const scaledCosine = Math.cos(rotation) * scale;
58
+ const scaledSine = Math.sin(rotation) * scale;
59
+ const source = shape.segmentCoordinates;
60
+ const neighbors = shape.segmentNeighbors;
61
+ const target = this.segmentData;
62
+ let targetIndex = this.usedSegmentCount * FLOATS_PER_BEAM_SEGMENT;
63
+ for (let segmentIndex = 0; segmentIndex < shape.segmentCount; segmentIndex++) {
64
+ const sourceIndex = segmentIndex * 4;
65
+ const previousSegment = neighbors[segmentIndex * 2];
66
+ const nextSegment = neighbors[segmentIndex * 2 + 1];
67
+ // Fall back to this segment's own points so unused neighbor slots hold finite values.
68
+ const previousStartIndex = previousSegment === NO_NEIGHBOR_SEGMENT ? sourceIndex : previousSegment * 4;
69
+ const nextEndIndex = nextSegment === NO_NEIGHBOR_SEGMENT ? sourceIndex + 2 : nextSegment * 4 + 2;
70
+ const startX = source[sourceIndex];
71
+ const startY = source[sourceIndex + 1];
72
+ const endX = source[sourceIndex + 2];
73
+ const endY = source[sourceIndex + 3];
74
+ const previousStartX = source[previousStartIndex];
75
+ const previousStartY = source[previousStartIndex + 1];
76
+ const nextEndX = source[nextEndIndex];
77
+ const nextEndY = source[nextEndIndex + 1];
78
+ target[targetIndex] = x + startX * scaledCosine - startY * scaledSine;
79
+ target[targetIndex + 1] = y + startX * scaledSine + startY * scaledCosine;
80
+ target[targetIndex + 2] = x + endX * scaledCosine - endY * scaledSine;
81
+ target[targetIndex + 3] = y + endX * scaledSine + endY * scaledCosine;
82
+ target[targetIndex + 4] = x + previousStartX * scaledCosine - previousStartY * scaledSine;
83
+ target[targetIndex + 5] = y + previousStartX * scaledSine + previousStartY * scaledCosine;
84
+ target[targetIndex + 6] = x + nextEndX * scaledCosine - nextEndY * scaledSine;
85
+ target[targetIndex + 7] = y + nextEndX * scaledSine + nextEndY * scaledCosine;
86
+ target[targetIndex + 8] = red;
87
+ target[targetIndex + 9] = green;
88
+ target[targetIndex + 10] = blue;
89
+ target[targetIndex + 11] = intensity;
90
+ target[targetIndex + 12] = previousSegment === NO_NEIGHBOR_SEGMENT ? 0 : 1;
91
+ target[targetIndex + 13] = nextSegment === NO_NEIGHBOR_SEGMENT ? 0 : 1;
92
+ targetIndex += FLOATS_PER_BEAM_SEGMENT;
93
+ }
94
+ }
95
+ }
96
+
97
+ export default DisplayList;
package/src/Shape.ts ADDED
@@ -0,0 +1,111 @@
1
+ import type { Polyline, ShapeGeometry } from "./geometryTypes.js";
2
+
3
+ export const FLOATS_PER_SHAPE_SEGMENT = 4;
4
+ export const NO_NEIGHBOR_SEGMENT = -1;
5
+
6
+ interface ShapeBuffers {
7
+ readonly coordinates: number[];
8
+ readonly neighborIndices: number[];
9
+ }
10
+
11
+ export class Shape implements ShapeGeometry {
12
+ public readonly segmentCoordinates: Float32Array;
13
+ // Two entries per segment: the index of the connected previous and next segment, or NO_NEIGHBOR_SEGMENT.
14
+ public readonly segmentNeighbors: Int32Array;
15
+
16
+ private constructor(buffers: ShapeBuffers) {
17
+ this.segmentCoordinates = Float32Array.from(buffers.coordinates);
18
+ this.segmentNeighbors = Int32Array.from(buffers.neighborIndices);
19
+ }
20
+
21
+ public get segmentCount(): number {
22
+ return this.segmentCoordinates.length / FLOATS_PER_SHAPE_SEGMENT;
23
+ }
24
+
25
+ public static fromPolyline(polyline: Polyline): Shape {
26
+ return Shape.fromPolylines([polyline]);
27
+ }
28
+
29
+ public static fromPolylines(polylines: readonly Polyline[]): Shape {
30
+ const buffers: ShapeBuffers = { coordinates: [], neighborIndices: [] };
31
+ for (const polyline of polylines) {
32
+ appendPolylineSegments(buffers, polyline);
33
+ }
34
+ return new Shape(buffers);
35
+ }
36
+
37
+ // Consecutive segments whose end and start coincide are treated as connected, including last-to-first.
38
+ public static fromSegmentCoordinates(segmentCoordinates: ArrayLike<number>): Shape {
39
+ if (segmentCoordinates.length % FLOATS_PER_SHAPE_SEGMENT !== 0) {
40
+ throw new Error(
41
+ `Segment coordinates must come in groups of ${FLOATS_PER_SHAPE_SEGMENT} (x0, y0, x1, y1), but received ${segmentCoordinates.length} values`,
42
+ );
43
+ }
44
+ const coordinates = Array.from(segmentCoordinates);
45
+ return new Shape({ coordinates, neighborIndices: findTouchingNeighbors(coordinates) });
46
+ }
47
+
48
+ // A zero-length segment renders as a round dot, like a bullet on a vector monitor.
49
+ public static createDot(): Shape {
50
+ return new Shape({ coordinates: [0, 0, 0, 0], neighborIndices: [NO_NEIGHBOR_SEGMENT, NO_NEIGHBOR_SEGMENT] });
51
+ }
52
+ }
53
+
54
+ function appendPolylineSegments(buffers: ShapeBuffers, polyline: Polyline): void {
55
+ const { points, isClosed } = polyline;
56
+ if (points.length === 0) return;
57
+ if (points.length === 1) {
58
+ buffers.coordinates.push(points[0].x, points[0].y, points[0].x, points[0].y);
59
+ buffers.neighborIndices.push(NO_NEIGHBOR_SEGMENT, NO_NEIGHBOR_SEGMENT);
60
+ return;
61
+ }
62
+ const firstSegmentIndex = buffers.coordinates.length / FLOATS_PER_SHAPE_SEGMENT;
63
+ for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex++) {
64
+ const start = points[pointIndex];
65
+ const end = points[pointIndex + 1];
66
+ buffers.coordinates.push(start.x, start.y, end.x, end.y);
67
+ }
68
+ const wrapsAround = isClosed && points.length > 2;
69
+ if (wrapsAround) {
70
+ const last = points[points.length - 1];
71
+ buffers.coordinates.push(last.x, last.y, points[0].x, points[0].y);
72
+ }
73
+ const segmentCount = buffers.coordinates.length / FLOATS_PER_SHAPE_SEGMENT - firstSegmentIndex;
74
+ appendChainNeighbors(buffers.neighborIndices, { firstSegmentIndex, segmentCount, wrapsAround });
75
+ }
76
+
77
+ function appendChainNeighbors(
78
+ neighborIndices: number[],
79
+ chain: { firstSegmentIndex: number; segmentCount: number; wrapsAround: boolean },
80
+ ): void {
81
+ const { firstSegmentIndex, segmentCount, wrapsAround } = chain;
82
+ const lastSegmentIndex = firstSegmentIndex + segmentCount - 1;
83
+ for (let segmentIndex = firstSegmentIndex; segmentIndex <= lastSegmentIndex; segmentIndex++) {
84
+ const isFirst = segmentIndex === firstSegmentIndex;
85
+ const isLast = segmentIndex === lastSegmentIndex;
86
+ const previousIndex = isFirst ? (wrapsAround ? lastSegmentIndex : NO_NEIGHBOR_SEGMENT) : segmentIndex - 1;
87
+ const nextIndex = isLast ? (wrapsAround ? firstSegmentIndex : NO_NEIGHBOR_SEGMENT) : segmentIndex + 1;
88
+ neighborIndices.push(previousIndex, nextIndex);
89
+ }
90
+ }
91
+
92
+ function findTouchingNeighbors(coordinates: readonly number[]): number[] {
93
+ const segmentCount = coordinates.length / FLOATS_PER_SHAPE_SEGMENT;
94
+ const neighborIndices = new Array<number>(segmentCount * 2).fill(NO_NEIGHBOR_SEGMENT);
95
+ for (let segmentIndex = 0; segmentIndex < segmentCount; segmentIndex++) {
96
+ const nextIndex = (segmentIndex + 1) % segmentCount;
97
+ if (nextIndex === segmentIndex) continue;
98
+ if (nextIndex === 0 && segmentCount <= 2) continue;
99
+ const endOffset = segmentIndex * FLOATS_PER_SHAPE_SEGMENT + 2;
100
+ const nextStartOffset = nextIndex * FLOATS_PER_SHAPE_SEGMENT;
101
+ const touches =
102
+ coordinates[endOffset] === coordinates[nextStartOffset] &&
103
+ coordinates[endOffset + 1] === coordinates[nextStartOffset + 1];
104
+ if (!touches) continue;
105
+ neighborIndices[segmentIndex * 2 + 1] = nextIndex;
106
+ neighborIndices[nextIndex * 2] = segmentIndex;
107
+ }
108
+ return neighborIndices;
109
+ }
110
+
111
+ export default Shape;
@@ -0,0 +1,34 @@
1
+ // Structural types so es-vector-math Vector/Point instances (or any {x, y}) can be passed directly.
2
+ export interface PointLike {
3
+ readonly x: number;
4
+ readonly y: number;
5
+ }
6
+
7
+ export interface Polyline {
8
+ readonly points: readonly PointLike[];
9
+ readonly isClosed: boolean;
10
+ }
11
+
12
+ export interface BeamColor {
13
+ readonly red: number;
14
+ readonly green: number;
15
+ readonly blue: number;
16
+ }
17
+
18
+ export interface ShapePlacement {
19
+ readonly x: number;
20
+ readonly y: number;
21
+ readonly rotation: number;
22
+ readonly scale: number;
23
+ readonly intensity: number;
24
+ }
25
+
26
+ // Anything shaped like a Shape can be drawn, including geometry rebuilt each frame (e.g. projected 3D wireframes).
27
+ export interface ShapeGeometry {
28
+ readonly segmentCoordinates: Float32Array;
29
+ readonly segmentNeighbors: Int32Array;
30
+ readonly segmentCount: number;
31
+ }
32
+
33
+ // For geometry that is already in world coordinates.
34
+ export const SCREEN_SPACE_PLACEMENT: ShapePlacement = { x: 0, y: 0, rotation: 0, scale: 1, intensity: 1 };
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { Shape, FLOATS_PER_SHAPE_SEGMENT, NO_NEIGHBOR_SEGMENT } from "./Shape.js";
2
+ export { DisplayList, FLOATS_PER_BEAM_SEGMENT } from "./DisplayList.js";
3
+ export { SCREEN_SPACE_PLACEMENT } from "./geometryTypes.js";
4
+ export type { BeamColor, PointLike, Polyline, ShapeGeometry, ShapePlacement } from "./geometryTypes.js";