@spatika/react 1.4.0 → 1.5.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.
Files changed (93) hide show
  1. package/AGENTS.md +6 -3
  2. package/README.md +1 -1
  3. package/dist/composites/EventCalendar.js +3 -3
  4. package/dist/composites/GlassCard.d.ts +1 -1
  5. package/dist/composites/RichTextEditor.d.ts +1 -4
  6. package/dist/composites/RichTextEditor.d.ts.map +1 -1
  7. package/dist/composites/RichTextEditor.js +12 -19
  8. package/dist/composites/SurfaceCard.d.ts +1 -1
  9. package/dist/composites/Tag.d.ts +2 -2
  10. package/dist/index.d.ts +8 -22
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +5 -11
  13. package/dist/lib/breakpoints.d.ts +17 -0
  14. package/dist/lib/breakpoints.d.ts.map +1 -0
  15. package/dist/lib/breakpoints.js +23 -0
  16. package/dist/lib/create-theme.d.ts +162 -0
  17. package/dist/lib/create-theme.d.ts.map +1 -0
  18. package/dist/lib/create-theme.js +138 -0
  19. package/dist/lib/scheduler.js +2 -2
  20. package/dist/lib/themes.d.ts +17 -7
  21. package/dist/lib/themes.d.ts.map +1 -1
  22. package/dist/lib/themes.js +22 -8
  23. package/dist/lib/use-media-query.d.ts +9 -0
  24. package/dist/lib/use-media-query.d.ts.map +1 -1
  25. package/dist/lib/use-media-query.js +28 -1
  26. package/dist/primitives/Alert.d.ts +1 -1
  27. package/dist/primitives/Badge.d.ts +2 -2
  28. package/dist/primitives/Button.d.ts +2 -2
  29. package/dist/primitives/Container.d.ts +2 -2
  30. package/dist/primitives/Fab.d.ts +4 -4
  31. package/dist/primitives/Grid.d.ts +2 -1
  32. package/dist/primitives/Grid.d.ts.map +1 -1
  33. package/dist/primitives/Grid.js +10 -2
  34. package/dist/primitives/IconButton.d.ts +2 -2
  35. package/dist/primitives/ToggleButton.d.ts +1 -1
  36. package/dist/theme/SpatikaThemeProvider.d.ts +4 -5
  37. package/dist/theme/SpatikaThemeProvider.d.ts.map +1 -1
  38. package/dist/theme/SpatikaThemeProvider.js +28 -13
  39. package/package.json +4 -2
  40. package/skills/spatika-ui/SKILL.md +25 -6
  41. package/src/composites/EventCalendar.test.tsx +293 -0
  42. package/src/composites/EventCalendar.tsx +3 -3
  43. package/src/composites/RichTextEditor.test.tsx +51 -0
  44. package/src/composites/RichTextEditor.tsx +20 -84
  45. package/src/composites/chart-ui.test.tsx +21 -0
  46. package/src/composites/scheduler.test.tsx +0 -258
  47. package/src/index.test.ts +6 -0
  48. package/src/index.ts +138 -72
  49. package/src/lib/breakpoints.test.ts +29 -0
  50. package/src/lib/breakpoints.ts +28 -0
  51. package/src/lib/create-theme.test.ts +79 -0
  52. package/src/lib/create-theme.ts +249 -0
  53. package/src/lib/scheduler.test.ts +10 -0
  54. package/src/lib/scheduler.ts +2 -2
  55. package/src/lib/themes.test.ts +17 -0
  56. package/src/lib/themes.ts +43 -12
  57. package/src/lib/use-media-query.test.tsx +80 -0
  58. package/src/lib/use-media-query.ts +35 -1
  59. package/src/primitives/Grid.test.tsx +55 -0
  60. package/src/primitives/Grid.tsx +13 -3
  61. package/src/primitives/mui-more.test.tsx +17 -0
  62. package/src/theme/SpatikaThemeProvider.test.tsx +38 -0
  63. package/src/theme/SpatikaThemeProvider.tsx +43 -15
  64. package/src/composites/ChartDataGrid.test.tsx +0 -57
  65. package/src/composites/ChartDataGrid.tsx +0 -196
  66. package/src/composites/MapChart.test.tsx +0 -39
  67. package/src/composites/MapChart.tsx +0 -130
  68. package/src/composites/ScatterWebGL.test.tsx +0 -25
  69. package/src/composites/ScatterWebGL.tsx +0 -79
  70. package/src/composites/cartesian-charts.test.tsx +0 -136
  71. package/src/composites/cartesian-charts.tsx +0 -1355
  72. package/src/composites/chart-composition.test.tsx +0 -68
  73. package/src/composites/chart-composition.tsx +0 -1039
  74. package/src/composites/chart-interaction.ts +0 -136
  75. package/src/composites/chart-ui.tsx +0 -336
  76. package/src/composites/charts-3d.test.tsx +0 -25
  77. package/src/composites/charts-3d.tsx +0 -318
  78. package/src/composites/charts.test.tsx +0 -254
  79. package/src/composites/flow-charts.test.tsx +0 -98
  80. package/src/composites/flow-charts.tsx +0 -641
  81. package/src/composites/radial-charts.test.tsx +0 -89
  82. package/src/composites/radial-charts.tsx +0 -643
  83. package/src/lib/chart-export.test.ts +0 -20
  84. package/src/lib/chart-export.ts +0 -99
  85. package/src/lib/charts-platform.test.ts +0 -93
  86. package/src/lib/charts.test.ts +0 -132
  87. package/src/lib/charts.ts +0 -1021
  88. package/src/lib/geo.test.ts +0 -42
  89. package/src/lib/geo.ts +0 -138
  90. package/src/lib/iso.test.ts +0 -22
  91. package/src/lib/iso.ts +0 -102
  92. package/src/lib/webgl-scatter.test.ts +0 -36
  93. package/src/lib/webgl-scatter.ts +0 -230
@@ -1,42 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { featureId, fitFeatures, geoPath, mercator } from "./geo";
3
-
4
- describe("geo", () => {
5
- it("projects the equator to the mercator midpoint", () => {
6
- const [x, y] = mercator([0, 0]);
7
- expect(x).toBeCloseTo(0.5);
8
- expect(y).toBeCloseTo(0.5);
9
- });
10
-
11
- it("builds a closed polygon path fitted to a viewport", () => {
12
- const features = [
13
- {
14
- type: "Feature" as const,
15
- id: "box",
16
- properties: { name: "Box" },
17
- geometry: {
18
- type: "Polygon" as const,
19
- coordinates: [
20
- [
21
- [0, 0],
22
- [10, 0],
23
- [10, 10],
24
- [0, 10],
25
- [0, 0],
26
- ],
27
- ],
28
- },
29
- },
30
- ];
31
- const project = fitFeatures(features, 200, 100, 10);
32
- const d = geoPath(features[0]!.geometry, project);
33
- expect(d.startsWith("M")).toBe(true);
34
- expect(d.includes("Z")).toBe(true);
35
- expect(featureId(features[0]!)).toBe("box");
36
- });
37
-
38
- it("projects a point to a move-to command", () => {
39
- const d = geoPath({ type: "Point", coordinates: [0, 0] }, mercator);
40
- expect(d.startsWith("M")).toBe(true);
41
- });
42
- });
package/src/lib/geo.ts DELETED
@@ -1,138 +0,0 @@
1
- /**
2
- * Tiny GeoJSON helpers — Mercator / equirectangular, no d3-geo.
3
- */
4
-
5
- export type GeoPosition = [number, number];
6
-
7
- export type GeoJsonGeometry =
8
- | { type: "Polygon"; coordinates: GeoPosition[][] }
9
- | { type: "MultiPolygon"; coordinates: GeoPosition[][][] }
10
- | { type: "Point"; coordinates: GeoPosition }
11
- | { type: "LineString"; coordinates: GeoPosition[] }
12
- | { type: "MultiLineString"; coordinates: GeoPosition[][] };
13
-
14
- export type GeoJsonFeature = {
15
- type: "Feature";
16
- id?: string | number;
17
- properties?: Record<string, unknown> | null;
18
- geometry: GeoJsonGeometry | null;
19
- };
20
-
21
- export type GeoJsonFeatureCollection = {
22
- type: "FeatureCollection";
23
- features: GeoJsonFeature[];
24
- };
25
-
26
- export type GeoProjectionKind = "mercator" | "equirectangular";
27
-
28
- export function mercator([lon, lat]: GeoPosition): [number, number] {
29
- const x = (lon + 180) / 360;
30
- const clamped = Math.min(85.051129, Math.max(-85.051129, lat));
31
- const sin = Math.sin((clamped * Math.PI) / 180);
32
- const y = 0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI);
33
- return [x, y];
34
- }
35
-
36
- export function equirectangular([lon, lat]: GeoPosition): [number, number] {
37
- return [(lon + 180) / 360, (90 - lat) / 180];
38
- }
39
-
40
- export function projectLonLat(pos: GeoPosition, kind: GeoProjectionKind): [number, number] {
41
- return kind === "equirectangular" ? equirectangular(pos) : mercator(pos);
42
- }
43
-
44
- export function featureId(feature: GeoJsonFeature, idProperty = "id"): string {
45
- if (feature.id != null) return String(feature.id);
46
- const value = feature.properties?.[idProperty] ?? feature.properties?.name;
47
- return value != null ? String(value) : "";
48
- }
49
-
50
- function walkGeometry(geometry: GeoJsonGeometry, visit: (pos: GeoPosition) => void) {
51
- if (geometry.type === "Point") {
52
- visit(geometry.coordinates);
53
- return;
54
- }
55
- if (geometry.type === "LineString") {
56
- geometry.coordinates.forEach(visit);
57
- return;
58
- }
59
- if (geometry.type === "MultiLineString") {
60
- geometry.coordinates.forEach((line) => line.forEach(visit));
61
- return;
62
- }
63
- if (geometry.type === "Polygon") {
64
- geometry.coordinates.forEach((ring) => ring.forEach(visit));
65
- return;
66
- }
67
- geometry.coordinates.forEach((poly) => poly.forEach((ring) => ring.forEach(visit)));
68
- }
69
-
70
- export function walkFeatures(features: GeoJsonFeature[], visit: (pos: GeoPosition) => void) {
71
- for (const feature of features) {
72
- if (feature.geometry) walkGeometry(feature.geometry, visit);
73
- }
74
- }
75
-
76
- function ringPath(ring: GeoPosition[], project: (pos: GeoPosition) => [number, number]): string {
77
- if (!ring.length) return "";
78
- const pts = ring.map(project);
79
- return `M${pts.map(([x, y]) => `${x} ${y}`).join("L")}Z`;
80
- }
81
-
82
- function linePath(line: GeoPosition[], project: (pos: GeoPosition) => [number, number]): string {
83
- if (!line.length) return "";
84
- const pts = line.map(project);
85
- return `M${pts.map(([x, y]) => `${x} ${y}`).join("L")}`;
86
- }
87
-
88
- export function geoPath(
89
- geometry: GeoJsonGeometry,
90
- project: (pos: GeoPosition) => [number, number],
91
- ): string {
92
- if (geometry.type === "Polygon") {
93
- return geometry.coordinates.map((ring) => ringPath(ring, project)).join(" ");
94
- }
95
- if (geometry.type === "MultiPolygon") {
96
- return geometry.coordinates
97
- .map((poly) => poly.map((ring) => ringPath(ring, project)).join(" "))
98
- .join(" ");
99
- }
100
- if (geometry.type === "LineString") return linePath(geometry.coordinates, project);
101
- if (geometry.type === "MultiLineString") {
102
- return geometry.coordinates.map((line) => linePath(line, project)).join(" ");
103
- }
104
- const [x, y] = project(geometry.coordinates);
105
- return `M${x} ${y}m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0`;
106
- }
107
-
108
- export function fitFeatures(
109
- features: GeoJsonFeature[],
110
- width: number,
111
- height: number,
112
- padding: number,
113
- kind: GeoProjectionKind = "mercator",
114
- ): (pos: GeoPosition) => [number, number] {
115
- const raw = (pos: GeoPosition) => projectLonLat(pos, kind);
116
- let minX = Infinity;
117
- let minY = Infinity;
118
- let maxX = -Infinity;
119
- let maxY = -Infinity;
120
- walkFeatures(features, (pos) => {
121
- const [x, y] = raw(pos);
122
- minX = Math.min(minX, x);
123
- minY = Math.min(minY, y);
124
- maxX = Math.max(maxX, x);
125
- maxY = Math.max(maxY, y);
126
- });
127
- const bw = Math.max(1e-6, maxX - minX);
128
- const bh = Math.max(1e-6, maxY - minY);
129
- const innerW = Math.max(1, width - padding * 2);
130
- const innerH = Math.max(1, height - padding * 2);
131
- const scale = Math.min(innerW / bw, innerH / bh);
132
- const tx = padding + (innerW - bw * scale) / 2;
133
- const ty = padding + (innerH - bh * scale) / 2;
134
- return (pos) => {
135
- const [x, y] = raw(pos);
136
- return [tx + (x - minX) * scale, ty + (y - minY) * scale];
137
- };
138
- }
@@ -1,22 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { ellipseSlicePath, isoBoxFaces, isoPoint } from "./iso";
3
-
4
- describe("iso", () => {
5
- it("projects isometric points with y up", () => {
6
- const origin = isoPoint(0, 0, 0, 40, 80);
7
- const raised = isoPoint(0, 10, 0, 40, 80);
8
- expect(raised.y).toBeLessThan(origin.y);
9
- });
10
-
11
- it("emits three faces for a box", () => {
12
- const faces = isoBoxFaces(0, 0, 0, 10, 20, 8, 40, 80);
13
- expect(faces.front.startsWith("M")).toBe(true);
14
- expect(faces.top).not.toEqual(faces.front);
15
- expect(faces.side).not.toEqual(faces.top);
16
- });
17
-
18
- it("draws an elliptical pie slice", () => {
19
- const d = ellipseSlicePath(50, 40, 30, 12, 0, 90);
20
- expect(d.includes("A30 12")).toBe(true);
21
- });
22
- });
package/src/lib/iso.ts DELETED
@@ -1,102 +0,0 @@
1
- /**
2
- * Isometric 2.5D helpers for SVG bar and pie charts.
3
- */
4
-
5
- const COS = Math.cos(Math.PI / 6);
6
- const SIN = Math.sin(Math.PI / 6);
7
-
8
- export function isoPoint(
9
- x: number,
10
- y: number,
11
- z: number,
12
- originX: number,
13
- originY: number,
14
- ): { x: number; y: number } {
15
- return {
16
- x: originX + (x - z) * COS,
17
- y: originY - y + (x + z) * SIN,
18
- };
19
- }
20
-
21
- function poly(points: Array<{ x: number; y: number }>): string {
22
- if (!points.length) return "";
23
- return `M${points.map((p) => `${p.x} ${p.y}`).join("L")}Z`;
24
- }
25
-
26
- export type IsoBoxFaces = {
27
- front: string;
28
- top: string;
29
- side: string;
30
- };
31
-
32
- /** Axis-aligned box: x/z on the ground plane, y up. */
33
- export function isoBoxFaces(
34
- x: number,
35
- y0: number,
36
- z: number,
37
- w: number,
38
- h: number,
39
- d: number,
40
- originX: number,
41
- originY: number,
42
- ): IsoBoxFaces {
43
- const p = (px: number, py: number, pz: number) => isoPoint(px, py, pz, originX, originY);
44
- const a = p(x, y0, z);
45
- const b = p(x + w, y0, z);
46
- const c = p(x + w, y0 + h, z);
47
- const e = p(x, y0 + h, z);
48
- const f = p(x + w, y0, z + d);
49
- const g = p(x + w, y0 + h, z + d);
50
- const k = p(x, y0 + h, z + d);
51
- return {
52
- front: poly([a, b, c, e]),
53
- top: poly([e, c, g, k]),
54
- side: poly([b, f, g, c]),
55
- };
56
- }
57
-
58
- export function ellipsePoint(
59
- cx: number,
60
- cy: number,
61
- rx: number,
62
- ry: number,
63
- angleDeg: number,
64
- ): { x: number; y: number } {
65
- const rad = ((angleDeg - 90) * Math.PI) / 180;
66
- return { x: cx + rx * Math.cos(rad), y: cy + ry * Math.sin(rad) };
67
- }
68
-
69
- export function ellipseSlicePath(
70
- cx: number,
71
- cy: number,
72
- rx: number,
73
- ry: number,
74
- startAngle: number,
75
- endAngle: number,
76
- ): string {
77
- const start = ellipsePoint(cx, cy, rx, ry, startAngle);
78
- const end = ellipsePoint(cx, cy, rx, ry, endAngle);
79
- const sweep = ((endAngle - startAngle) % 360 + 360) % 360;
80
- if (sweep <= 0) return "";
81
- const large = sweep > 180 ? 1 : 0;
82
- return `M${cx} ${cy} L${start.x} ${start.y} A${rx} ${ry} 0 ${large} 1 ${end.x} ${end.y} Z`;
83
- }
84
-
85
- export function ellipseWallPath(
86
- cx: number,
87
- cy: number,
88
- rx: number,
89
- ry: number,
90
- startAngle: number,
91
- endAngle: number,
92
- thickness: number,
93
- ): string {
94
- const top0 = ellipsePoint(cx, cy, rx, ry, startAngle);
95
- const top1 = ellipsePoint(cx, cy, rx, ry, endAngle);
96
- const bot0 = { x: top0.x, y: top0.y + thickness };
97
- const bot1 = { x: top1.x, y: top1.y + thickness };
98
- const sweep = ((endAngle - startAngle) % 360 + 360) % 360;
99
- if (sweep <= 0) return "";
100
- const large = sweep > 180 ? 1 : 0;
101
- return `M${bot0.x} ${bot0.y} L${top0.x} ${top0.y} A${rx} ${ry} 0 ${large} 1 ${top1.x} ${top1.y} L${bot1.x} ${bot1.y} A${rx} ${ry} 0 ${large} 0 ${bot0.x} ${bot0.y} Z`;
102
- }
@@ -1,36 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- nearestScatterHit,
4
- parseCssColor,
5
- shouldUseWebGL,
6
- WEBGL_AUTO_THRESHOLD,
7
- } from "./webgl-scatter";
8
-
9
- describe("webgl scatter helpers", () => {
10
- it("parses hex and rgb colors", () => {
11
- expect(parseCssColor("#ff0000")).toEqual([1, 0, 0, 1]);
12
- expect(parseCssColor("rgb(0, 128, 255)")).toEqual([0, 128 / 255, 1, 1]);
13
- expect(parseCssColor("not-a-color")).toBeNull();
14
- });
15
-
16
- it("selects webgl for auto clouds above the threshold", () => {
17
- expect(shouldUseWebGL("svg", 10_000)).toBe(false);
18
- expect(shouldUseWebGL("webgl", 3)).toBe(true);
19
- expect(shouldUseWebGL("auto", WEBGL_AUTO_THRESHOLD)).toBe(true);
20
- expect(shouldUseWebGL("auto", 10)).toBe(false);
21
- });
22
-
23
- it("finds the nearest point within the hit radius", () => {
24
- const hit = nearestScatterHit(
25
- [
26
- { x: 10, y: 10, r: 4, seriesIndex: 0, pointIndex: 0 },
27
- { x: 40, y: 40, r: 4, seriesIndex: 1, pointIndex: 2 },
28
- ],
29
- 41,
30
- 39,
31
- );
32
- expect(hit?.seriesIndex).toBe(1);
33
- expect(hit?.pointIndex).toBe(2);
34
- expect(nearestScatterHit([{ x: 0, y: 0, r: 2, seriesIndex: 0, pointIndex: 0 }], 80, 80)).toBeNull();
35
- });
36
- });
@@ -1,230 +0,0 @@
1
- /**
2
- * Raw WebGL scatter renderer — no three.js.
3
- * Canvas sits behind SVG axes; hit-testing stays on the CPU.
4
- */
5
-
6
- export type ScatterRenderer = "svg" | "webgl" | "auto";
7
-
8
- export const WEBGL_AUTO_THRESHOLD = 2000;
9
-
10
- export function shouldUseWebGL(renderer: ScatterRenderer | undefined, pointCount: number): boolean {
11
- if (renderer === "webgl") return true;
12
- if (renderer === "auto") return pointCount >= WEBGL_AUTO_THRESHOLD;
13
- return false;
14
- }
15
-
16
- export type Rgba = [number, number, number, number];
17
-
18
- export function parseCssColor(css: string): Rgba | null {
19
- const hex = css.trim();
20
- const hexMatch = /^#([0-9a-f]{3,8})$/i.exec(hex);
21
- if (hexMatch) {
22
- let h = hexMatch[1]!;
23
- if (h.length === 3 || h.length === 4) h = [...h].map((c) => c + c).join("");
24
- const n = Number.parseInt(h.slice(0, 6), 16);
25
- const a = h.length === 8 ? Number.parseInt(h.slice(6, 8), 16) / 255 : 1;
26
- return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255, a];
27
- }
28
- const rgb = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i.exec(hex);
29
- if (rgb) {
30
- return [
31
- Number(rgb[1]) / 255,
32
- Number(rgb[2]) / 255,
33
- Number(rgb[3]) / 255,
34
- rgb[4] != null ? Number(rgb[4]) : 1,
35
- ];
36
- }
37
- return null;
38
- }
39
-
40
- export function resolveCssColor(color: string, el?: Element | null): Rgba {
41
- const parsed = parseCssColor(color);
42
- if (parsed) return parsed;
43
- if (typeof document === "undefined" || !el) return [0.35, 0.55, 0.95, 1];
44
- const probe = document.createElement("span");
45
- probe.style.color = color;
46
- probe.style.position = "absolute";
47
- probe.style.pointerEvents = "none";
48
- probe.style.opacity = "0";
49
- el.appendChild(probe);
50
- const computed = getComputedStyle(probe).color;
51
- probe.remove();
52
- return parseCssColor(computed) ?? [0.35, 0.55, 0.95, 1];
53
- }
54
-
55
- export type ScatterHitCandidate = {
56
- x: number;
57
- y: number;
58
- r: number;
59
- seriesIndex: number;
60
- pointIndex: number;
61
- };
62
-
63
- export function nearestScatterHit(
64
- candidates: ScatterHitCandidate[],
65
- px: number,
66
- py: number,
67
- pad = 4,
68
- ): ScatterHitCandidate | null {
69
- let best: ScatterHitCandidate | null = null;
70
- let bestDist = Infinity;
71
- for (const item of candidates) {
72
- const dx = item.x - px;
73
- const dy = item.y - py;
74
- const dist = Math.hypot(dx, dy);
75
- if (dist <= item.r + pad && dist < bestDist) {
76
- best = item;
77
- bestDist = dist;
78
- }
79
- }
80
- return best;
81
- }
82
-
83
- export function svgPointerPosition(
84
- svg: SVGSVGElement,
85
- clientX: number,
86
- clientY: number,
87
- width: number,
88
- height: number,
89
- ): { x: number; y: number } {
90
- const bounds = svg.getBoundingClientRect();
91
- const w = bounds.width || width;
92
- const h = bounds.height || height;
93
- return {
94
- x: ((clientX - bounds.left) / w) * width,
95
- y: ((clientY - bounds.top) / h) * height,
96
- };
97
- }
98
-
99
- const VS = `
100
- attribute vec2 a_position;
101
- attribute float a_size;
102
- uniform vec2 u_resolution;
103
- uniform float u_dpr;
104
- void main() {
105
- vec2 zeroToOne = a_position / u_resolution;
106
- vec2 clip = zeroToOne * 2.0 - 1.0;
107
- gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
108
- gl_PointSize = max(1.0, a_size * u_dpr);
109
- }
110
- `;
111
-
112
- const FS = `
113
- precision mediump float;
114
- uniform vec4 u_color;
115
- void main() {
116
- vec2 c = gl_PointCoord - vec2(0.5);
117
- if (dot(c, c) > 0.25) discard;
118
- gl_FragColor = u_color;
119
- }
120
- `;
121
-
122
- export type ScatterGL = {
123
- gl: WebGLRenderingContext;
124
- program: WebGLProgram;
125
- buffer: WebGLBuffer;
126
- aPosition: number;
127
- aSize: number;
128
- uResolution: WebGLUniformLocation | null;
129
- uDpr: WebGLUniformLocation | null;
130
- uColor: WebGLUniformLocation | null;
131
- };
132
-
133
- function compile(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | null {
134
- const shader = gl.createShader(type);
135
- if (!shader) return null;
136
- gl.shaderSource(shader, source);
137
- gl.compileShader(shader);
138
- if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
139
- gl.deleteShader(shader);
140
- return null;
141
- }
142
- return shader;
143
- }
144
-
145
- export function createScatterGL(canvas: HTMLCanvasElement): ScatterGL | null {
146
- const gl = canvas.getContext("webgl", {
147
- alpha: true,
148
- antialias: true,
149
- premultipliedAlpha: true,
150
- });
151
- if (!gl) return null;
152
- const vs = compile(gl, gl.VERTEX_SHADER, VS);
153
- const fs = compile(gl, gl.FRAGMENT_SHADER, FS);
154
- if (!vs || !fs) return null;
155
- const program = gl.createProgram();
156
- if (!program) return null;
157
- gl.attachShader(program, vs);
158
- gl.attachShader(program, fs);
159
- gl.linkProgram(program);
160
- if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return null;
161
- const buffer = gl.createBuffer();
162
- if (!buffer) return null;
163
- gl.enable(gl.BLEND);
164
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
165
- return {
166
- gl,
167
- program,
168
- buffer,
169
- aPosition: gl.getAttribLocation(program, "a_position"),
170
- aSize: gl.getAttribLocation(program, "a_size"),
171
- uResolution: gl.getUniformLocation(program, "u_resolution"),
172
- uDpr: gl.getUniformLocation(program, "u_dpr"),
173
- uColor: gl.getUniformLocation(program, "u_color"),
174
- };
175
- }
176
-
177
- export type ScatterGLSeries = {
178
- positions: Array<{ x: number; y: number; size: number }>;
179
- color: string;
180
- };
181
-
182
- export function drawScatterGL(
183
- gpu: ScatterGL,
184
- canvas: HTMLCanvasElement,
185
- width: number,
186
- height: number,
187
- series: ScatterGLSeries[],
188
- ): void {
189
- const dpr = typeof window !== "undefined" ? Math.min(window.devicePixelRatio || 1, 2) : 1;
190
- const pixelW = Math.max(1, Math.round(width * dpr));
191
- const pixelH = Math.max(1, Math.round(height * dpr));
192
- if (canvas.width !== pixelW) canvas.width = pixelW;
193
- if (canvas.height !== pixelH) canvas.height = pixelH;
194
- canvas.style.width = `${width}px`;
195
- canvas.style.height = `${height}px`;
196
-
197
- const { gl } = gpu;
198
- gl.viewport(0, 0, pixelW, pixelH);
199
- gl.clearColor(0, 0, 0, 0);
200
- gl.clear(gl.COLOR_BUFFER_BIT);
201
- gl.useProgram(gpu.program);
202
- gl.uniform2f(gpu.uResolution, width, height);
203
- gl.uniform1f(gpu.uDpr, dpr);
204
- gl.bindBuffer(gl.ARRAY_BUFFER, gpu.buffer);
205
-
206
- const stride = 3 * 4;
207
- for (const item of series) {
208
- if (!item.positions.length) continue;
209
- const data = new Float32Array(item.positions.length * 3);
210
- item.positions.forEach((point, i) => {
211
- data[i * 3] = point.x;
212
- data[i * 3 + 1] = point.y;
213
- data[i * 3 + 2] = point.size;
214
- });
215
- gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);
216
- gl.enableVertexAttribArray(gpu.aPosition);
217
- gl.vertexAttribPointer(gpu.aPosition, 2, gl.FLOAT, false, stride, 0);
218
- gl.enableVertexAttribArray(gpu.aSize);
219
- gl.vertexAttribPointer(gpu.aSize, 1, gl.FLOAT, false, stride, 8);
220
- const [r, g, b, a] = resolveCssColor(item.color, canvas.parentElement ?? canvas);
221
- gl.uniform4f(gpu.uColor, r, g, b, a);
222
- gl.drawArrays(gl.POINTS, 0, item.positions.length);
223
- }
224
- }
225
-
226
- export function destroyScatterGL(gpu: ScatterGL): void {
227
- const { gl } = gpu;
228
- gl.deleteBuffer(gpu.buffer);
229
- gl.deleteProgram(gpu.program);
230
- }