@classytic/stage 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/dist/assets/index.d.mts +3 -0
- package/dist/assets/index.mjs +5 -0
- package/dist/assets/kit/glyphs.d.mts +61 -0
- package/dist/assets/kit/glyphs.mjs +256 -0
- package/dist/assets/kit/index.d.mts +2 -0
- package/dist/assets/kit/index.mjs +4 -0
- package/dist/assets/kit/svg-defs.d.mts +6 -0
- package/dist/assets/kit/svg-defs.mjs +137 -0
- package/dist/builder/Palette.d.mts +30 -0
- package/dist/builder/Palette.mjs +100 -0
- package/dist/builder/SceneBuilder.d.mts +26 -0
- package/dist/builder/SceneBuilder.mjs +340 -0
- package/dist/builder/controls.d.mts +18 -0
- package/dist/builder/controls.mjs +29 -0
- package/dist/builder/editor.d.mts +46 -0
- package/dist/builder/editor.mjs +130 -0
- package/dist/builder/icons.d.mts +14 -0
- package/dist/builder/icons.mjs +143 -0
- package/dist/builder/index.d.mts +6 -0
- package/dist/builder/index.mjs +7 -0
- package/dist/builder/tools.d.mts +47 -0
- package/dist/builder/tools.mjs +303 -0
- package/dist/chem/index.d.mts +118 -0
- package/dist/chem/index.mjs +131 -0
- package/dist/core/clock.d.mts +41 -0
- package/dist/core/clock.mjs +62 -0
- package/dist/core/context.d.mts +13 -0
- package/dist/core/context.mjs +29 -0
- package/dist/core/control.d.mts +58 -0
- package/dist/core/control.mjs +113 -0
- package/dist/core/coords.d.mts +51 -0
- package/dist/core/coords.mjs +44 -0
- package/dist/core/index.d.mts +8 -0
- package/dist/core/index.mjs +9 -0
- package/dist/core/learner.d.mts +44 -0
- package/dist/core/learner.mjs +23 -0
- package/dist/core/motion.d.mts +73 -0
- package/dist/core/motion.mjs +103 -0
- package/dist/core/richText.d.mts +28 -0
- package/dist/core/richText.mjs +52 -0
- package/dist/core/vec.d.mts +47 -0
- package/dist/core/vec.mjs +144 -0
- package/dist/field/index.d.mts +58 -0
- package/dist/field/index.mjs +156 -0
- package/dist/index.d.mts +54 -0
- package/dist/index.mjs +55 -0
- package/dist/interaction/MovableDot.d.mts +33 -0
- package/dist/interaction/MovableDot.mjs +66 -0
- package/dist/interaction/hitTest.d.mts +16 -0
- package/dist/interaction/hitTest.mjs +39 -0
- package/dist/interaction/index.d.mts +5 -0
- package/dist/interaction/index.mjs +6 -0
- package/dist/interaction/useDraggable.d.mts +51 -0
- package/dist/interaction/useDraggable.mjs +116 -0
- package/dist/interaction/useScreenToMath.d.mts +12 -0
- package/dist/interaction/useScreenToMath.mjs +25 -0
- package/dist/logic/ast.d.mts +29 -0
- package/dist/logic/ast.mjs +37 -0
- package/dist/logic/index.d.mts +25 -0
- package/dist/logic/index.mjs +36 -0
- package/dist/logic/latex.d.mts +6 -0
- package/dist/logic/latex.mjs +33 -0
- package/dist/logic/minimize.d.mts +37 -0
- package/dist/logic/minimize.mjs +175 -0
- package/dist/logic/parse.d.mts +6 -0
- package/dist/logic/parse.mjs +68 -0
- package/dist/logic/table.d.mts +24 -0
- package/dist/logic/table.mjs +65 -0
- package/dist/logic/tokenize.mjs +258 -0
- package/dist/math/ast.d.mts +35 -0
- package/dist/math/ast.mjs +94 -0
- package/dist/math/calculus.d.mts +9 -0
- package/dist/math/calculus.mjs +188 -0
- package/dist/math/defs.mjs +51 -0
- package/dist/math/index.d.mts +26 -0
- package/dist/math/index.mjs +42 -0
- package/dist/math/latex.d.mts +7 -0
- package/dist/math/latex.mjs +115 -0
- package/dist/math/parse.d.mts +6 -0
- package/dist/math/parse.mjs +91 -0
- package/dist/math/tokenize.mjs +112 -0
- package/dist/primitives/CanvasLayer.d.mts +26 -0
- package/dist/primitives/CanvasLayer.mjs +87 -0
- package/dist/primitives/Dot.d.mts +27 -0
- package/dist/primitives/Dot.mjs +40 -0
- package/dist/primitives/Grid.d.mts +30 -0
- package/dist/primitives/Grid.mjs +122 -0
- package/dist/primitives/Label.d.mts +33 -0
- package/dist/primitives/Label.mjs +45 -0
- package/dist/primitives/Lines.d.mts +50 -0
- package/dist/primitives/Lines.mjs +82 -0
- package/dist/primitives/Plot.d.mts +50 -0
- package/dist/primitives/Plot.mjs +87 -0
- package/dist/primitives/Shapes.d.mts +76 -0
- package/dist/primitives/Shapes.mjs +79 -0
- package/dist/primitives/Tex.d.mts +27 -0
- package/dist/primitives/Tex.mjs +54 -0
- package/dist/primitives/index.d.mts +10 -0
- package/dist/primitives/index.mjs +10 -0
- package/dist/primitives/props.d.mts +15 -0
- package/dist/scene/Scene.d.mts +31 -0
- package/dist/scene/Scene.mjs +168 -0
- package/dist/scene/assets.d.mts +37 -0
- package/dist/scene/assets.mjs +31 -0
- package/dist/scene/commands.d.mts +47 -0
- package/dist/scene/commands.mjs +162 -0
- package/dist/scene/evaluators.d.mts +17 -0
- package/dist/scene/evaluators.mjs +210 -0
- package/dist/scene/index.d.mts +13 -0
- package/dist/scene/index.mjs +13 -0
- package/dist/scene/migrate.d.mts +11 -0
- package/dist/scene/migrate.mjs +35 -0
- package/dist/scene/render.d.mts +14 -0
- package/dist/scene/render.mjs +126 -0
- package/dist/scene/resolve.d.mts +12 -0
- package/dist/scene/resolve.mjs +95 -0
- package/dist/scene/schema.d.mts +9 -0
- package/dist/scene/schema.mjs +51 -0
- package/dist/scene/sims.d.mts +18 -0
- package/dist/scene/sims.mjs +54 -0
- package/dist/scene/store.d.mts +22 -0
- package/dist/scene/store.mjs +37 -0
- package/dist/scene/types.d.mts +228 -0
- package/dist/scene/types.mjs +27 -0
- package/dist/sim/equilibrium.d.mts +36 -0
- package/dist/sim/equilibrium.mjs +54 -0
- package/dist/sim/index.d.mts +9 -0
- package/dist/sim/index.mjs +9 -0
- package/dist/sim/particles.d.mts +43 -0
- package/dist/sim/particles.mjs +136 -0
- package/dist/sim/rate.d.mts +36 -0
- package/dist/sim/rate.mjs +39 -0
- package/dist/sim/registry.d.mts +10 -0
- package/dist/sim/registry.mjs +27 -0
- package/dist/sim/sampler.d.mts +47 -0
- package/dist/sim/sampler.mjs +79 -0
- package/dist/sim/thermal.d.mts +78 -0
- package/dist/sim/thermal.mjs +172 -0
- package/dist/sim/types.d.mts +24 -0
- package/dist/sim/wave.d.mts +34 -0
- package/dist/sim/wave.mjs +61 -0
- package/dist/steps/index.d.mts +62 -0
- package/dist/steps/index.mjs +133 -0
- package/dist/thermo/index.d.mts +82 -0
- package/dist/thermo/index.mjs +133 -0
- package/dist/view/Stage.d.mts +39 -0
- package/dist/view/Stage.mjs +91 -0
- package/dist/view/index.d.mts +4 -0
- package/dist/view/index.mjs +5 -0
- package/dist/view/useElementSize.d.mts +13 -0
- package/dist/view/useElementSize.mjs +40 -0
- package/dist/view/useInView.d.mts +9 -0
- package/dist/view/useInView.mjs +33 -0
- package/package.json +141 -0
- package/styles.css +70 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { isFunction } from "./defs.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/math/tokenize.ts
|
|
4
|
+
/**
|
|
5
|
+
* expr/tokenize — source string → token stream.
|
|
6
|
+
*
|
|
7
|
+
* Handles numbers (incl. `1.5e3`, rejecting malformed `2..3`), identifiers
|
|
8
|
+
* (classified as function vs variable), operators, parens, and commas. Inserts
|
|
9
|
+
* implicit-multiplication operators where two value-producing tokens abut
|
|
10
|
+
* (`2x`, `3sin(x)`, `(x+1)(x-1)`), so the parser stays simple.
|
|
11
|
+
*/
|
|
12
|
+
const isDigit = (c) => c >= "0" && c <= "9";
|
|
13
|
+
const isAlpha = (c) => c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "_";
|
|
14
|
+
function tokenize(src) {
|
|
15
|
+
const raw = [];
|
|
16
|
+
let i = 0;
|
|
17
|
+
const s = src.trim();
|
|
18
|
+
while (i < s.length) {
|
|
19
|
+
const c = s[i];
|
|
20
|
+
if (c === " " || c === " ") {
|
|
21
|
+
i++;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (isDigit(c) || c === "." && isDigit(s[i + 1] ?? "")) {
|
|
25
|
+
let j = i;
|
|
26
|
+
let seenDot = false;
|
|
27
|
+
while (j < s.length) {
|
|
28
|
+
const ch = s[j] ?? "";
|
|
29
|
+
if (isDigit(ch)) {
|
|
30
|
+
j++;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (ch === ".") {
|
|
34
|
+
if (seenDot) throw new Error(`Malformed number near '${s.slice(i, j + 1)}'`);
|
|
35
|
+
seenDot = true;
|
|
36
|
+
j++;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
if (s[j] === "e" || s[j] === "E") {
|
|
42
|
+
let k = j + 1;
|
|
43
|
+
if (s[k] === "+" || s[k] === "-") k++;
|
|
44
|
+
if (!isDigit(s[k] ?? "")) throw new Error(`Incomplete exponent in number '${s.slice(i, k + 1)}'`);
|
|
45
|
+
k++;
|
|
46
|
+
while (isDigit(s[k] ?? "")) k++;
|
|
47
|
+
j = k;
|
|
48
|
+
}
|
|
49
|
+
raw.push({
|
|
50
|
+
t: "num",
|
|
51
|
+
v: Number.parseFloat(s.slice(i, j))
|
|
52
|
+
});
|
|
53
|
+
i = j;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (isAlpha(c)) {
|
|
57
|
+
let j = i + 1;
|
|
58
|
+
while (j < s.length && (isAlpha(s[j] ?? "") || isDigit(s[j] ?? ""))) j++;
|
|
59
|
+
const orig = s.slice(i, j);
|
|
60
|
+
const lower = orig.toLowerCase();
|
|
61
|
+
raw.push(isFunction(lower) ? {
|
|
62
|
+
t: "fn",
|
|
63
|
+
v: lower
|
|
64
|
+
} : {
|
|
65
|
+
t: "var",
|
|
66
|
+
v: orig
|
|
67
|
+
});
|
|
68
|
+
i = j;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (c === "(") {
|
|
72
|
+
raw.push({ t: "lp" });
|
|
73
|
+
i++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (c === ")") {
|
|
77
|
+
raw.push({ t: "rp" });
|
|
78
|
+
i++;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (c === ",") {
|
|
82
|
+
raw.push({ t: "comma" });
|
|
83
|
+
i++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if ("+-*/%^".includes(c)) {
|
|
87
|
+
raw.push({
|
|
88
|
+
t: "op",
|
|
89
|
+
v: c
|
|
90
|
+
});
|
|
91
|
+
i++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`Unexpected character '${c}'`);
|
|
95
|
+
}
|
|
96
|
+
const out = [];
|
|
97
|
+
for (let k = 0; k < raw.length; k++) {
|
|
98
|
+
const prev = out[out.length - 1];
|
|
99
|
+
const cur = raw[k];
|
|
100
|
+
const prevIsValue = !!prev && (prev.t === "num" || prev.t === "var" || prev.t === "rp");
|
|
101
|
+
const curStartsValue = cur.t === "num" || cur.t === "var" || cur.t === "fn" || cur.t === "lp";
|
|
102
|
+
if (prevIsValue && curStartsValue) out.push({
|
|
103
|
+
t: "op",
|
|
104
|
+
v: "*"
|
|
105
|
+
});
|
|
106
|
+
out.push(cur);
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
//#endregion
|
|
112
|
+
export { tokenize };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CoordinateSystem, CoordsOptions, ViewBox } from "../core/coords.mjs";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/primitives/CanvasLayer.d.ts
|
|
5
|
+
interface CanvasLayerProps {
|
|
6
|
+
view: ViewBox;
|
|
7
|
+
height?: number;
|
|
8
|
+
/** Draw into the HiDPI 2D context using the coordinate system (math↔px). */
|
|
9
|
+
draw: (ctx: CanvasRenderingContext2D, coords: CoordinateSystem) => void;
|
|
10
|
+
/** Pointer in MATH coords on press + drag (e.g. to set a start point). */
|
|
11
|
+
onPointerMath?: (m: [number, number]) => void;
|
|
12
|
+
coordsOptions?: CoordsOptions;
|
|
13
|
+
ariaLabel?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
declare function CanvasLayer({
|
|
17
|
+
view,
|
|
18
|
+
height,
|
|
19
|
+
draw,
|
|
20
|
+
onPointerMath,
|
|
21
|
+
coordsOptions,
|
|
22
|
+
ariaLabel,
|
|
23
|
+
className
|
|
24
|
+
}: CanvasLayerProps): ReactNode;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { CanvasLayer, CanvasLayerProps };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createCoords } from "../core/coords.mjs";
|
|
4
|
+
import { useElementSize } from "../view/useElementSize.mjs";
|
|
5
|
+
import { useEffect, useRef } from "react";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
|
|
8
|
+
//#region src/primitives/CanvasLayer.tsx
|
|
9
|
+
/**
|
|
10
|
+
* <CanvasLayer> — the engine's HIGH-ELEMENT-COUNT escape hatch: a zero-dependency
|
|
11
|
+
* HiDPI <canvas> that shares the engine's coordinate system + clock. Use it when
|
|
12
|
+
* a lab needs a heatmap, a particle/gas sim, or thousands of moving dots at 60fps
|
|
13
|
+
* — cases where one retained SVG node per element would drop frames. Declarative,
|
|
14
|
+
* low-count, accessible figures should stay on <Stage> (SVG); this is the
|
|
15
|
+
* deliberate, bundle-free alternative to a WebGL engine (no Pixi/three dependency)
|
|
16
|
+
* until a lab MEASURABLY needs shaders/tens-of-thousands of textured sprites.
|
|
17
|
+
*
|
|
18
|
+
* `draw(ctx, coords)` runs in CSS-pixel space (the context is pre-scaled for dpr)
|
|
19
|
+
* on mount, on resize, and whenever `draw`'s identity changes — so memoize it on
|
|
20
|
+
* your state (or bump it from a useFrameLoop tick) to drive animation. Overlay a
|
|
21
|
+
* sibling <Stage> with the SAME view for accessible SVG handles/labels on top.
|
|
22
|
+
*/
|
|
23
|
+
function CanvasLayer({ view, height = 320, draw, onPointerMath, coordsOptions, ariaLabel = "interactive canvas", className }) {
|
|
24
|
+
const { ref: wrapRef, size } = useElementSize();
|
|
25
|
+
const canvasRef = useRef(null);
|
|
26
|
+
const dragging = useRef(false);
|
|
27
|
+
const w = Math.max(1, Math.round(size?.width ?? 640));
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const cv = canvasRef.current;
|
|
30
|
+
if (!cv) return;
|
|
31
|
+
const dpr = Math.min(typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1, 2);
|
|
32
|
+
cv.width = Math.round(w * dpr);
|
|
33
|
+
cv.height = Math.round(height * dpr);
|
|
34
|
+
const ctx = cv.getContext("2d");
|
|
35
|
+
if (!ctx) return;
|
|
36
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
37
|
+
ctx.clearRect(0, 0, w, height);
|
|
38
|
+
draw(ctx, createCoords(w, height, view, coordsOptions));
|
|
39
|
+
}, [
|
|
40
|
+
w,
|
|
41
|
+
height,
|
|
42
|
+
view,
|
|
43
|
+
draw,
|
|
44
|
+
coordsOptions
|
|
45
|
+
]);
|
|
46
|
+
const toMath = (e) => {
|
|
47
|
+
const cv = canvasRef.current;
|
|
48
|
+
if (!cv || !onPointerMath) return null;
|
|
49
|
+
const rect = cv.getBoundingClientRect();
|
|
50
|
+
return createCoords(w, height, view, coordsOptions).toMath(e.clientX - rect.left, e.clientY - rect.top);
|
|
51
|
+
};
|
|
52
|
+
const fire = (e) => {
|
|
53
|
+
const m = toMath(e);
|
|
54
|
+
if (m) onPointerMath(m);
|
|
55
|
+
};
|
|
56
|
+
return /* @__PURE__ */ jsx("div", {
|
|
57
|
+
ref: wrapRef,
|
|
58
|
+
className,
|
|
59
|
+
style: { width: "100%" },
|
|
60
|
+
children: /* @__PURE__ */ jsx("canvas", {
|
|
61
|
+
ref: canvasRef,
|
|
62
|
+
role: "img",
|
|
63
|
+
"aria-label": ariaLabel,
|
|
64
|
+
style: {
|
|
65
|
+
display: "block",
|
|
66
|
+
width: "100%",
|
|
67
|
+
height,
|
|
68
|
+
touchAction: "none",
|
|
69
|
+
cursor: onPointerMath ? "crosshair" : "default"
|
|
70
|
+
},
|
|
71
|
+
onPointerDown: onPointerMath ? (e) => {
|
|
72
|
+
dragging.current = true;
|
|
73
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
74
|
+
fire(e);
|
|
75
|
+
} : void 0,
|
|
76
|
+
onPointerMove: onPointerMath ? (e) => {
|
|
77
|
+
if (dragging.current) fire(e);
|
|
78
|
+
} : void 0,
|
|
79
|
+
onPointerUp: onPointerMath ? () => {
|
|
80
|
+
dragging.current = false;
|
|
81
|
+
} : void 0
|
|
82
|
+
})
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
//#endregion
|
|
87
|
+
export { CanvasLayer };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { StyleProps } from "./props.mjs";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/primitives/Dot.d.ts
|
|
5
|
+
interface DotProps extends StyleProps {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
/** Pixel radius (never scaled). */
|
|
9
|
+
r?: number;
|
|
10
|
+
}
|
|
11
|
+
declare function Dot({
|
|
12
|
+
x,
|
|
13
|
+
y,
|
|
14
|
+
r,
|
|
15
|
+
color,
|
|
16
|
+
opacity
|
|
17
|
+
}: DotProps): ReactNode;
|
|
18
|
+
/** A point with a contrast ring (reads clearly over any background). */
|
|
19
|
+
declare function Point({
|
|
20
|
+
x,
|
|
21
|
+
y,
|
|
22
|
+
r,
|
|
23
|
+
color,
|
|
24
|
+
opacity
|
|
25
|
+
}: DotProps): ReactNode;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { Dot, DotProps, Point };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { fmt } from "../core/coords.mjs";
|
|
4
|
+
import { useCoords } from "../core/context.mjs";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
|
|
7
|
+
//#region src/primitives/Dot.tsx
|
|
8
|
+
function Dot({ x, y, r = 5, color = "var(--stage-accent)", opacity = 1 }) {
|
|
9
|
+
const [px, py] = useCoords().toPx(x, y);
|
|
10
|
+
return /* @__PURE__ */ jsx("circle", {
|
|
11
|
+
cx: fmt(px),
|
|
12
|
+
cy: fmt(py),
|
|
13
|
+
r,
|
|
14
|
+
fill: color,
|
|
15
|
+
opacity
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/** A point with a contrast ring (reads clearly over any background). */
|
|
19
|
+
function Point({ x, y, r = 6, color = "var(--stage-accent)", opacity = 1 }) {
|
|
20
|
+
const [px, py] = useCoords().toPx(x, y);
|
|
21
|
+
return /* @__PURE__ */ jsxs("g", {
|
|
22
|
+
opacity,
|
|
23
|
+
children: [/* @__PURE__ */ jsx("circle", {
|
|
24
|
+
cx: fmt(px),
|
|
25
|
+
cy: fmt(py),
|
|
26
|
+
r,
|
|
27
|
+
fill: color
|
|
28
|
+
}), /* @__PURE__ */ jsx("circle", {
|
|
29
|
+
cx: fmt(px),
|
|
30
|
+
cy: fmt(py),
|
|
31
|
+
r,
|
|
32
|
+
fill: "none",
|
|
33
|
+
stroke: "var(--stage-bg)",
|
|
34
|
+
strokeWidth: 1.5
|
|
35
|
+
})]
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
40
|
+
export { Dot, Point };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/primitives/Grid.d.ts
|
|
4
|
+
/** A "nice" step (1,2,5 × 10ⁿ) so ~`target` gridlines span the range. */
|
|
5
|
+
declare function niceStep(span: number, target?: number): number;
|
|
6
|
+
interface GridProps {
|
|
7
|
+
step?: number;
|
|
8
|
+
color?: string;
|
|
9
|
+
}
|
|
10
|
+
declare function Grid({
|
|
11
|
+
step,
|
|
12
|
+
color
|
|
13
|
+
}: GridProps): ReactNode;
|
|
14
|
+
interface AxesProps {
|
|
15
|
+
color?: string;
|
|
16
|
+
ticks?: boolean;
|
|
17
|
+
step?: number;
|
|
18
|
+
/** Show the numeric value at each tick (and a "0" at the origin) so learners
|
|
19
|
+
* can READ coordinates off the grid — needed for graphs where the answer is a
|
|
20
|
+
* point (systems, plotting). Default off (most figures want a clean axis). */
|
|
21
|
+
labels?: boolean;
|
|
22
|
+
}
|
|
23
|
+
declare function Axes({
|
|
24
|
+
color,
|
|
25
|
+
ticks,
|
|
26
|
+
step,
|
|
27
|
+
labels
|
|
28
|
+
}: AxesProps): ReactNode;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { Axes, AxesProps, Grid, GridProps, niceStep };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCoords } from "../core/context.mjs";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
|
|
6
|
+
//#region src/primitives/Grid.tsx
|
|
7
|
+
/** A "nice" step (1,2,5 × 10ⁿ) so ~`target` gridlines span the range. */
|
|
8
|
+
function niceStep(span, target = 8) {
|
|
9
|
+
const raw = Math.abs(span) / target || 1;
|
|
10
|
+
const magnitude = Math.pow(10, Math.floor(Math.log10(raw)));
|
|
11
|
+
const n = raw / magnitude;
|
|
12
|
+
return (n < 1.5 ? 1 : n < 3 ? 2 : n < 7 ? 5 : 10) * magnitude;
|
|
13
|
+
}
|
|
14
|
+
function tickValues(min, max, step) {
|
|
15
|
+
const out = [];
|
|
16
|
+
const start = Math.ceil(min / step) * step;
|
|
17
|
+
for (let v = start; v <= max + step * 1e-6; v += step) out.push(Math.round(v / step) * step);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
function Grid({ step, color = "var(--stage-grid)" }) {
|
|
21
|
+
const c = useCoords();
|
|
22
|
+
const s = step ?? niceStep(c.view.xMax - c.view.xMin);
|
|
23
|
+
const lines = [];
|
|
24
|
+
for (const x of tickValues(c.view.xMin, c.view.xMax, s)) {
|
|
25
|
+
const a = c.toPx(x, c.view.yMin);
|
|
26
|
+
const b = c.toPx(x, c.view.yMax);
|
|
27
|
+
lines.push(/* @__PURE__ */ jsx("line", {
|
|
28
|
+
x1: a[0],
|
|
29
|
+
y1: a[1],
|
|
30
|
+
x2: b[0],
|
|
31
|
+
y2: b[1],
|
|
32
|
+
stroke: color,
|
|
33
|
+
strokeWidth: 1
|
|
34
|
+
}, `gx${x}`));
|
|
35
|
+
}
|
|
36
|
+
for (const y of tickValues(c.view.yMin, c.view.yMax, s)) {
|
|
37
|
+
const a = c.toPx(c.view.xMin, y);
|
|
38
|
+
const b = c.toPx(c.view.xMax, y);
|
|
39
|
+
lines.push(/* @__PURE__ */ jsx("line", {
|
|
40
|
+
x1: a[0],
|
|
41
|
+
y1: a[1],
|
|
42
|
+
x2: b[0],
|
|
43
|
+
y2: b[1],
|
|
44
|
+
stroke: color,
|
|
45
|
+
strokeWidth: 1
|
|
46
|
+
}, `gy${y}`));
|
|
47
|
+
}
|
|
48
|
+
return /* @__PURE__ */ jsx("g", { children: lines });
|
|
49
|
+
}
|
|
50
|
+
const fmtTick = (v) => Number.isInteger(v) ? String(v) : v.toFixed(1);
|
|
51
|
+
function Axes({ color = "var(--stage-axis)", ticks = true, step, labels = false }) {
|
|
52
|
+
const c = useCoords();
|
|
53
|
+
const s = step ?? niceStep(c.view.xMax - c.view.xMin);
|
|
54
|
+
const xAxisA = c.toPx(c.view.xMin, 0);
|
|
55
|
+
const xAxisB = c.toPx(c.view.xMax, 0);
|
|
56
|
+
const yAxisA = c.toPx(0, c.view.yMin);
|
|
57
|
+
const yAxisB = c.toPx(0, c.view.yMax);
|
|
58
|
+
const labelStyle = {
|
|
59
|
+
paintOrder: "stroke",
|
|
60
|
+
stroke: "var(--stage-bg)",
|
|
61
|
+
strokeWidth: 3,
|
|
62
|
+
strokeLinejoin: "round"
|
|
63
|
+
};
|
|
64
|
+
const num = (key, px, py, text, anchor) => /* @__PURE__ */ jsx("text", {
|
|
65
|
+
x: px,
|
|
66
|
+
y: py,
|
|
67
|
+
fill: "var(--stage-muted)",
|
|
68
|
+
fontSize: 11,
|
|
69
|
+
textAnchor: anchor,
|
|
70
|
+
dominantBaseline: "middle",
|
|
71
|
+
style: labelStyle,
|
|
72
|
+
children: text
|
|
73
|
+
}, key);
|
|
74
|
+
const nodes = [/* @__PURE__ */ jsx("line", {
|
|
75
|
+
x1: xAxisA[0],
|
|
76
|
+
y1: xAxisA[1],
|
|
77
|
+
x2: xAxisB[0],
|
|
78
|
+
y2: xAxisB[1],
|
|
79
|
+
stroke: color,
|
|
80
|
+
strokeWidth: 1.5
|
|
81
|
+
}, "ax"), /* @__PURE__ */ jsx("line", {
|
|
82
|
+
x1: yAxisA[0],
|
|
83
|
+
y1: yAxisA[1],
|
|
84
|
+
x2: yAxisB[0],
|
|
85
|
+
y2: yAxisB[1],
|
|
86
|
+
stroke: color,
|
|
87
|
+
strokeWidth: 1.5
|
|
88
|
+
}, "ay")];
|
|
89
|
+
if (ticks) {
|
|
90
|
+
for (const x of tickValues(c.view.xMin, c.view.xMax, s)) {
|
|
91
|
+
if (Math.abs(x) < 1e-9) continue;
|
|
92
|
+
const p = c.toPx(x, 0);
|
|
93
|
+
nodes.push(/* @__PURE__ */ jsx("line", {
|
|
94
|
+
x1: p[0],
|
|
95
|
+
y1: p[1] - 4,
|
|
96
|
+
x2: p[0],
|
|
97
|
+
y2: p[1] + 4,
|
|
98
|
+
stroke: color,
|
|
99
|
+
strokeWidth: 1.5
|
|
100
|
+
}, `tx${x}`));
|
|
101
|
+
if (labels) nodes.push(num(`lx${x}`, p[0], p[1] + 14, fmtTick(x), "middle"));
|
|
102
|
+
}
|
|
103
|
+
for (const y of tickValues(c.view.yMin, c.view.yMax, s)) {
|
|
104
|
+
if (Math.abs(y) < 1e-9) continue;
|
|
105
|
+
const p = c.toPx(0, y);
|
|
106
|
+
nodes.push(/* @__PURE__ */ jsx("line", {
|
|
107
|
+
x1: p[0] - 4,
|
|
108
|
+
y1: p[1],
|
|
109
|
+
x2: p[0] + 4,
|
|
110
|
+
y2: p[1],
|
|
111
|
+
stroke: color,
|
|
112
|
+
strokeWidth: 1.5
|
|
113
|
+
}, `ty${y}`));
|
|
114
|
+
if (labels) nodes.push(num(`ly${y}`, p[0] - 8, p[1], fmtTick(y), "end"));
|
|
115
|
+
}
|
|
116
|
+
if (labels) nodes.push(num("l0", c.toPx(0, 0)[0] - 8, c.toPx(0, 0)[1] + 12, "0", "end"));
|
|
117
|
+
}
|
|
118
|
+
return /* @__PURE__ */ jsx("g", { children: nodes });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
export { Axes, Grid, niceStep };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/primitives/Label.d.ts
|
|
4
|
+
interface LabelProps {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
text: string;
|
|
8
|
+
color?: string;
|
|
9
|
+
size?: number;
|
|
10
|
+
/** Pixel offset from the anchor. */
|
|
11
|
+
dx?: number;
|
|
12
|
+
dy?: number;
|
|
13
|
+
anchor?: 'start' | 'middle' | 'end';
|
|
14
|
+
baseline?: 'auto' | 'middle' | 'hanging';
|
|
15
|
+
weight?: number;
|
|
16
|
+
}
|
|
17
|
+
/** Upright pixel-space text with a background-colored outline for legibility.
|
|
18
|
+
* Renders `_`/`^` as real SVG sub/superscripts via the shared `parseRichText`
|
|
19
|
+
* grammar (the same one labs' HTML `<RichText>` uses — one source of truth). */
|
|
20
|
+
declare function Label({
|
|
21
|
+
x,
|
|
22
|
+
y,
|
|
23
|
+
text,
|
|
24
|
+
color,
|
|
25
|
+
size,
|
|
26
|
+
dx,
|
|
27
|
+
dy,
|
|
28
|
+
anchor,
|
|
29
|
+
baseline,
|
|
30
|
+
weight
|
|
31
|
+
}: LabelProps): ReactNode;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { Label, LabelProps };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { fmt } from "../core/coords.mjs";
|
|
4
|
+
import { parseRichText } from "../core/richText.mjs";
|
|
5
|
+
import { useCoords } from "../core/context.mjs";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
|
|
8
|
+
//#region src/primitives/Label.tsx
|
|
9
|
+
/** Upright pixel-space text with a background-colored outline for legibility.
|
|
10
|
+
* Renders `_`/`^` as real SVG sub/superscripts via the shared `parseRichText`
|
|
11
|
+
* grammar (the same one labs' HTML `<RichText>` uses — one source of truth). */
|
|
12
|
+
function Label({ x, y, text, color = "var(--stage-fg)", size = 14, dx = 0, dy = 0, anchor = "middle", baseline = "middle", weight = 600 }) {
|
|
13
|
+
const [px, py] = useCoords().toPx(x, y);
|
|
14
|
+
const spans = parseRichText(text);
|
|
15
|
+
const shift = size * .32;
|
|
16
|
+
let prevLevel = 0;
|
|
17
|
+
return /* @__PURE__ */ jsx("text", {
|
|
18
|
+
x: fmt(px + dx),
|
|
19
|
+
y: fmt(py + dy),
|
|
20
|
+
fill: color,
|
|
21
|
+
fontSize: size,
|
|
22
|
+
fontWeight: weight,
|
|
23
|
+
textAnchor: anchor,
|
|
24
|
+
dominantBaseline: baseline,
|
|
25
|
+
style: {
|
|
26
|
+
paintOrder: "stroke",
|
|
27
|
+
stroke: "var(--stage-bg)",
|
|
28
|
+
strokeWidth: 3,
|
|
29
|
+
strokeLinejoin: "round"
|
|
30
|
+
},
|
|
31
|
+
children: spans.length === 1 && spans[0].script === "base" ? text : spans.map((sp, i) => {
|
|
32
|
+
const level = sp.script === "sub" ? 1 : sp.script === "sup" ? -1 : 0;
|
|
33
|
+
const dyPx = (level - prevLevel) * shift;
|
|
34
|
+
prevLevel = level;
|
|
35
|
+
return /* @__PURE__ */ jsx("tspan", {
|
|
36
|
+
dy: fmt(dyPx),
|
|
37
|
+
fontSize: sp.script === "base" ? void 0 : fmt(size * .72),
|
|
38
|
+
children: sp.text
|
|
39
|
+
}, i);
|
|
40
|
+
})
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
export { Label };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Vec2 } from "../core/vec.mjs";
|
|
2
|
+
import { StyleProps } from "./props.mjs";
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
//#region src/primitives/Lines.d.ts
|
|
6
|
+
interface SegmentProps extends StyleProps {
|
|
7
|
+
from: Vec2;
|
|
8
|
+
to: Vec2;
|
|
9
|
+
}
|
|
10
|
+
declare function Segment({
|
|
11
|
+
from,
|
|
12
|
+
to,
|
|
13
|
+
color,
|
|
14
|
+
weight,
|
|
15
|
+
opacity,
|
|
16
|
+
dashed
|
|
17
|
+
}: SegmentProps): ReactNode;
|
|
18
|
+
interface LineProps extends StyleProps {
|
|
19
|
+
from: Vec2;
|
|
20
|
+
to: Vec2;
|
|
21
|
+
}
|
|
22
|
+
declare function Line({
|
|
23
|
+
from,
|
|
24
|
+
to,
|
|
25
|
+
color,
|
|
26
|
+
weight,
|
|
27
|
+
opacity,
|
|
28
|
+
dashed
|
|
29
|
+
}: LineProps): ReactNode;
|
|
30
|
+
declare function Ray({
|
|
31
|
+
from,
|
|
32
|
+
to,
|
|
33
|
+
color,
|
|
34
|
+
weight,
|
|
35
|
+
opacity,
|
|
36
|
+
dashed
|
|
37
|
+
}: LineProps): ReactNode;
|
|
38
|
+
interface VectorProps extends StyleProps {
|
|
39
|
+
tail?: Vec2;
|
|
40
|
+
tip: Vec2;
|
|
41
|
+
}
|
|
42
|
+
declare function Vector({
|
|
43
|
+
tail,
|
|
44
|
+
tip,
|
|
45
|
+
color,
|
|
46
|
+
weight,
|
|
47
|
+
opacity
|
|
48
|
+
}: VectorProps): ReactNode;
|
|
49
|
+
//#endregion
|
|
50
|
+
export { Line, LineProps, Ray, Segment, SegmentProps, Vector, VectorProps };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { fmt } from "../core/coords.mjs";
|
|
4
|
+
import { useCoords } from "../core/context.mjs";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
|
|
7
|
+
//#region src/primitives/Lines.tsx
|
|
8
|
+
const extend = (c, a, b, back) => {
|
|
9
|
+
const dx = b[0] - a[0];
|
|
10
|
+
const dy = b[1] - a[1];
|
|
11
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
12
|
+
const ux = dx / len;
|
|
13
|
+
const uy = dy / len;
|
|
14
|
+
const big = Math.hypot(c.width, c.height) * 2;
|
|
15
|
+
return [back ? [a[0] - ux * big, a[1] - uy * big] : a, [a[0] + ux * big, a[1] + uy * big]];
|
|
16
|
+
};
|
|
17
|
+
function Segment({ from, to, color = "var(--stage-fg)", weight = 2, opacity = 1, dashed }) {
|
|
18
|
+
const c = useCoords();
|
|
19
|
+
const a = c.toPx(from.x, from.y);
|
|
20
|
+
const b = c.toPx(to.x, to.y);
|
|
21
|
+
return /* @__PURE__ */ jsx("line", {
|
|
22
|
+
x1: fmt(a[0]),
|
|
23
|
+
y1: fmt(a[1]),
|
|
24
|
+
x2: fmt(b[0]),
|
|
25
|
+
y2: fmt(b[1]),
|
|
26
|
+
stroke: color,
|
|
27
|
+
strokeWidth: weight,
|
|
28
|
+
opacity,
|
|
29
|
+
strokeLinecap: "round",
|
|
30
|
+
strokeDasharray: dashed ? "6 6" : void 0
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function Line({ from, to, color = "var(--stage-axis)", weight = 2, opacity = 1, dashed }) {
|
|
34
|
+
const c = useCoords();
|
|
35
|
+
const [p1, p2] = extend(c, c.toPx(from.x, from.y), c.toPx(to.x, to.y), true);
|
|
36
|
+
return /* @__PURE__ */ jsx("line", {
|
|
37
|
+
x1: fmt(p1[0]),
|
|
38
|
+
y1: fmt(p1[1]),
|
|
39
|
+
x2: fmt(p2[0]),
|
|
40
|
+
y2: fmt(p2[1]),
|
|
41
|
+
stroke: color,
|
|
42
|
+
strokeWidth: weight,
|
|
43
|
+
opacity,
|
|
44
|
+
strokeDasharray: dashed ? "6 6" : void 0
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function Ray({ from, to, color = "var(--stage-axis)", weight = 2, opacity = 1, dashed }) {
|
|
48
|
+
const c = useCoords();
|
|
49
|
+
const [p1, p2] = extend(c, c.toPx(from.x, from.y), c.toPx(to.x, to.y), false);
|
|
50
|
+
return /* @__PURE__ */ jsx("line", {
|
|
51
|
+
x1: fmt(p1[0]),
|
|
52
|
+
y1: fmt(p1[1]),
|
|
53
|
+
x2: fmt(p2[0]),
|
|
54
|
+
y2: fmt(p2[1]),
|
|
55
|
+
stroke: color,
|
|
56
|
+
strokeWidth: weight,
|
|
57
|
+
opacity,
|
|
58
|
+
strokeDasharray: dashed ? "6 6" : void 0
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function Vector({ tail = {
|
|
62
|
+
x: 0,
|
|
63
|
+
y: 0
|
|
64
|
+
}, tip, color = "var(--stage-accent-2)", weight = 2.5, opacity = 1 }) {
|
|
65
|
+
const c = useCoords();
|
|
66
|
+
const a = c.toPx(tail.x, tail.y);
|
|
67
|
+
const b = c.toPx(tip.x, tip.y);
|
|
68
|
+
return /* @__PURE__ */ jsx("line", {
|
|
69
|
+
x1: fmt(a[0]),
|
|
70
|
+
y1: fmt(a[1]),
|
|
71
|
+
x2: fmt(b[0]),
|
|
72
|
+
y2: fmt(b[1]),
|
|
73
|
+
stroke: color,
|
|
74
|
+
strokeWidth: weight,
|
|
75
|
+
opacity,
|
|
76
|
+
strokeLinecap: "round",
|
|
77
|
+
markerEnd: "url(#stage-arrow)"
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
//#endregion
|
|
82
|
+
export { Line, Ray, Segment, Vector };
|