@lukekaalim/act-graphit 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/Axes.ts ADDED
@@ -0,0 +1,108 @@
1
+ import { Component, Deps, h, Node, useEffect, useRef, useState } from "@lukekaalim/act"
2
+ import { Vector } from "./vector"
3
+ import { Group, Line, Rect } from "./elements"
4
+ import { Vector2D } from "@lukekaalim/act-curve"
5
+ import { assertRefs } from "./ResizingSpace"
6
+
7
+ export type PositiveAxesProps = {
8
+ size: Vector<2>,
9
+ position: Vector<2>,
10
+ axes?: { x: Node, y: Node }
11
+ }
12
+
13
+ export const PositiveAxes: Component<PositiveAxesProps> = ({ size, position, children, axes }) => {
14
+ return [
15
+ !!axes && h(Group, { position }, [
16
+ h('text', { fill: 'red', x: 0, y: -30 }, axes.x),
17
+ h('text', { fill: 'blue', x: 0, y: -30, transform: `rotate(-90)`, 'text-anchor': 'end' }, axes.y),
18
+ ]),
19
+ h(Line, {
20
+ start: { x: position.x, y: position.y },
21
+ end: { x: size.x + position.x, y: position.y },
22
+ stroke: 'red',
23
+ }),
24
+ h(Line, {
25
+ start: { x: position.x, y: position.y },
26
+ end: { x: position.x, y: size.y + position.y },
27
+ stroke: 'blue',
28
+ }),
29
+ h(Rect, { position: { x: position.x + size.x - 3, y: position.y - 3 }, size: { x: 6, y: 6 }, fill: 'red', stroke: 'none' }),
30
+ h(Rect, { position: { x: position.x - 3, y: position.y + size.y - 3 }, size: { x: 6, y: 6 }, fill: 'blue', stroke: 'none' }),
31
+ h(Group, { position }, children)
32
+ ]
33
+ }
34
+
35
+ export type ZeroBasedAxesProps = {
36
+ size: Vector<2>,
37
+ position: Vector<2>,
38
+ }
39
+
40
+ export const ZeroBasedAxes: Component<ZeroBasedAxesProps> = ({
41
+ size,
42
+ position,
43
+ children,
44
+ }) => {
45
+
46
+
47
+ return [
48
+ h(Line, {
49
+ start: { x: position.x, y: position.y + (size.y / 2) },
50
+ end: { x: size.x + position.x, y: position.y + (size.y / 2) },
51
+ stroke: 'red',
52
+ }),
53
+ h(Line, {
54
+ start: { x: position.x + (size.x / 2), y: position.y },
55
+ end: { x: position.x + (size.x / 2), y: size.y + position.y },
56
+ stroke: 'blue',
57
+ }),
58
+
59
+ h('g', { transform: `translate(${position.x} ${position.y})` },
60
+ children
61
+ ),
62
+ ]
63
+ }
64
+
65
+
66
+ export type UnitSizeProps = {
67
+ size: Vector<2>,
68
+ deps?: Deps,
69
+ }
70
+
71
+ export const UnitSize: Component<UnitSizeProps> = ({ size, children, deps = [] }) => {
72
+ const ref = useRef<SVGGElement | null>(null);
73
+
74
+ useEffect(() => {
75
+ const { group } = assertRefs({ group: ref });
76
+ const svg = group.ownerSVGElement;
77
+ const parent = group.parentNode;
78
+ if (!svg || !(parent instanceof SVGElement))
79
+ return;
80
+
81
+
82
+ group.transform.baseVal.clear();
83
+
84
+ const parentSpace = parent.getBoundingClientRect();
85
+ const bounds = group.getBoundingClientRect();
86
+
87
+ const scale = svg.createSVGTransform();
88
+ const translate = svg.createSVGTransform();
89
+
90
+ scale.setScale(
91
+ (1 / bounds.width) * size.x,
92
+ (1 / bounds.height) * size.y
93
+ )
94
+ translate.setTranslate(
95
+ bounds.x - parentSpace.x,
96
+ bounds.y - parentSpace.y
97
+ )
98
+ console.log(parent, {
99
+ x: bounds.x - parentSpace.x,
100
+ y: bounds.y - parentSpace.y
101
+ })
102
+
103
+ group.transform.baseVal.appendItem(translate);
104
+ group.transform.baseVal.appendItem(scale);
105
+ }, deps)
106
+
107
+ return h('g', { ref }, children)
108
+ }
package/Axis.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { Component, h, Ref, useMemo, useRef } from "@lukekaalim/act";
2
+ import { Vector } from "./vector";
3
+ import { assertRefs } from "./ResizingSpace";
4
+
5
+ export type AxisProps = {
6
+ offset: number,
7
+
8
+ axis: Vector<2>,
9
+ size: Vector<2>,
10
+
11
+ ref?: Ref<null | SVGLineElement>,
12
+ controllerRef?: Ref<null | AxisController>,
13
+ }
14
+
15
+ export type AxisController = {
16
+ offset: number,
17
+ size: Vector<2>,
18
+ update(): void,
19
+ }
20
+
21
+ export const Axis: Component<AxisProps> = ({ axis, size, offset, ref, controllerRef }) => {
22
+ const localRef = useRef<SVGLineElement | null>(null);
23
+
24
+ const finalRef = ref || localRef;
25
+
26
+ const controller = useMemo(() => {
27
+ const controller = {
28
+ offset,
29
+ size,
30
+ update() {
31
+ const { line } = assertRefs({ line: finalRef });
32
+
33
+ line.x1.baseVal.value = (axis.y * controller.offset)
34
+ line.x2.baseVal.value = (axis.x * controller.size.x) + (axis.y * controller.offset)
35
+
36
+ line.y1.baseVal.value = (axis.x * controller.offset)
37
+ line.y2.baseVal.value = (axis.x * controller.offset) + (axis.y * controller.size.y)
38
+ }
39
+ }
40
+
41
+ if (controllerRef)
42
+ controllerRef.current = controller;
43
+
44
+ return controller;
45
+ }, []);
46
+
47
+ return h('line', {
48
+ x1: (axis.y * controller.offset),
49
+ x2: (axis.x * controller.size.x) + (axis.y * controller.offset),
50
+
51
+ y1: (axis.x * controller.offset),
52
+ y2: (axis.x * controller.offset) + (axis.y * controller.size.y),
53
+
54
+ stroke: 'lightblue',
55
+ 'stroke-width': '4px',
56
+ ref: finalRef,
57
+ })
58
+ };
@@ -0,0 +1,13 @@
1
+ .cartesianSpace {
2
+ flex: 1;
3
+
4
+ overflow: hidden;
5
+
6
+ border: 1px solid rgb(193, 193, 193);
7
+ border-radius: 4px;
8
+ }
9
+
10
+
11
+ .cartesianSpace.dragging {
12
+ cursor: grab;
13
+ }
@@ -0,0 +1,129 @@
1
+ import { Component, h, Node, Ref, useEffect, useMemo, useRef, useState } from "@lukekaalim/act"
2
+ import { SVG } from "@lukekaalim/act-web"
3
+ import { useElementSize } from "./useElementSize";
4
+ import { Axis, AxisController } from "./Axis";
5
+
6
+ import classes from './CartesianSpace.module.css';
7
+ import { Grid } from "./Grid";
8
+ import { Vector } from "./vector";
9
+ import { off } from "process";
10
+ import { useDrag } from "./useDrag";
11
+ import { Vector2D } from "@lukekaalim/act-curve";
12
+ import { assertRefs } from "./ResizingSpace";
13
+
14
+ export type CartesianSpaceController = {
15
+ position: Vector<2>,
16
+ size: Vector<2>,
17
+
18
+ update(): void,
19
+ };
20
+
21
+ export type CartesianSpaceProps = {
22
+ offset?: Vector<2>,
23
+
24
+ style?: unknown,
25
+ overlay?: Node,
26
+ initialPosition?: Vector<2>,
27
+ onDragComplete?: (position: Vector<2>) => void,
28
+
29
+ refs?: {
30
+ controller?: Ref<null | CartesianSpaceController>,
31
+ axisX?: Ref<null | SVGLineElement>,
32
+ axisY?: Ref<null | SVGLineElement>,
33
+ pattern?: Ref<null | SVGPatternElement>,
34
+ }
35
+ }
36
+
37
+ const createSpaceController = () => {
38
+
39
+ };
40
+
41
+ export const CartesianSpace: Component<CartesianSpaceProps> = ({
42
+ children, offset = Vector(2).create(), style, overlay,
43
+ onDragComplete, initialPosition,
44
+ refs: { controller: controllerRef } = {},
45
+ ...props
46
+ }) => {
47
+ const ref = useRef<SVGSVGElement | null>(null);
48
+
49
+ const size = useElementSize(ref);
50
+
51
+ const onDrag = useMemo(() => {
52
+ return (delta: Vector<2>) => {
53
+ controller.position.x += delta.x;
54
+ controller.position.y += delta.y;
55
+ controller.update();
56
+ }
57
+ }, [onDragComplete])
58
+
59
+
60
+ const onFinishDrag = useMemo(() => () => {
61
+ if (onDragComplete)
62
+ onDragComplete(controller.position)
63
+ }, [onDragComplete])
64
+
65
+ const patternRef = useRef<SVGPatternElement | null>(null);
66
+ const groupRef = useRef<SVGGElement | null>(null);
67
+ const XAxisRef = useRef<AxisController | null>(null);
68
+ const YAxisRef = useRef<AxisController | null>(null);
69
+
70
+ useEffect(() => {
71
+ const { xAxis, yAxis } = assertRefs({ xAxis: XAxisRef, yAxis: YAxisRef });
72
+ xAxis.size = size;
73
+ yAxis.size = size;
74
+ xAxis.update();
75
+ yAxis.update();
76
+ }, [size])
77
+
78
+ const controller = useMemo(() => {
79
+ const controller = {
80
+ position: { ...(initialPosition || Vector2D.ZERO) },
81
+ size: { ...Vector2D.ZERO },
82
+ update() {
83
+ const { pattern, group, svg, xAxis, yAxis } = assertRefs({
84
+ pattern: patternRef, group: groupRef, svg: ref, xAxis: XAxisRef, yAxis: YAxisRef
85
+ });
86
+
87
+ pattern.x.baseVal.value = controller.position.x;
88
+ pattern.y.baseVal.value = controller.position.y;
89
+
90
+ const transform = svg.createSVGTransform()
91
+ transform.setTranslate(controller.position.x, controller.position.y);
92
+
93
+ group.transform.baseVal.initialize(transform);
94
+ xAxis.offset = controller.position.y;
95
+ yAxis.offset = controller.position.x;
96
+
97
+ xAxis.update();
98
+ yAxis.update();
99
+ },
100
+ }
101
+ if (controllerRef)
102
+ controllerRef.current = controller;
103
+
104
+ return controller;
105
+ }, [])
106
+
107
+ const dragging = useDrag(ref, onDrag, event => {
108
+ if (event.target instanceof HTMLElement)
109
+ switch (event.target.tagName) {
110
+ case 'BUTTON':
111
+ case 'INPUT':
112
+ case 'A':
113
+ return false;
114
+ }
115
+ return true;
116
+ }, { onFinishDrag })
117
+
118
+ //const combinedOffset = Vector(2).add(dragOffset, offset);
119
+
120
+ return h(SVG, {}, h('svg', { ...props, ref, class: [classes.cartesianSpace, dragging && classes.dragging].join(' '), style }, [
121
+ h(Grid, { offset: controller.position, strokeWidth: 1, scale: { x: 50, y: 50 }, refs: { pattern: patternRef } }),
122
+ h(Axis, { axis: { x: 1, y: 0 }, size, offset: 0, controllerRef: XAxisRef }),
123
+ h(Axis, { axis: { x: 0, y: 1 }, size, offset: 0, controllerRef: YAxisRef }),
124
+ h('g', { transform: `translate(${controller.position.x} ${controller.position.y})`, ref: groupRef }, [
125
+ children,
126
+ ]),
127
+ overlay || null,
128
+ ]))
129
+ }
package/Defs.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { Node, h, useEffect, useState } from "@lukekaalim/act";
2
+ import { SVG } from "@lukekaalim/act-web";
3
+
4
+ export const globalDefs = new Set<Node>();
5
+
6
+ const defSubscribers = new Set<() => void>();
7
+
8
+ export const registerSVGDef = (def: Node) => {
9
+ globalDefs.add(def);
10
+
11
+ for (const subscriber of defSubscribers)
12
+ subscriber();
13
+ }
14
+
15
+ const useGlobalSVGDefs = () => {
16
+ const [defs, setDefs] = useState(globalDefs);
17
+
18
+ useEffect(() => {
19
+ const subscription = () => {
20
+ setDefs(new Set(globalDefs))
21
+ };
22
+ defSubscribers.add(subscription);
23
+ return () => defSubscribers.delete(subscription);
24
+ }, []);
25
+
26
+ return defs;
27
+ };
28
+
29
+ export const DefProvider = () => {
30
+ const defs = useGlobalSVGDefs();
31
+
32
+ return h(SVG, {}, [
33
+ h('svg', {}, h('defs', {}, [...defs]))
34
+ ])
35
+ }
@@ -0,0 +1,7 @@
1
+ .editablePointInput {
2
+
3
+ }
4
+
5
+ .editablePointDiv {
6
+
7
+ }
@@ -0,0 +1,87 @@
1
+ import { Component, h, Node, useEffect, useMemo, useRef } from "@lukekaalim/act"
2
+ import { Vector } from "./vector"
3
+ import { HTML } from "@lukekaalim/act-web"
4
+ import { useDrag } from "./useDrag"
5
+ import { Animation1D, Animation2D, Curve2D, useAnimatedValue } from "@lukekaalim/act-curve"
6
+
7
+ export type EditablePointProps = {
8
+ point: Vector<2>,
9
+ onPointEdit?: (updater: (prev: Vector<2>) => Vector<2>) => void,
10
+
11
+ fill?: string,
12
+ }
13
+
14
+ export const EditablePoint: Component<EditablePointProps> = ({
15
+ point,
16
+ onPointEdit = () => {},
17
+ children,
18
+ fill = 'black'
19
+ }) => {
20
+ const ref = useRef<SVGCircleElement | null>(null);
21
+ const dottedRef = useRef<SVGCircleElement | null>(null);
22
+ const [value, setValue] = useAnimatedValue(5, 150);
23
+
24
+ const onDrag = useMemo(() => {
25
+ return (delta: Vector<2>) => {
26
+ onPointEdit(prev => Vector(2).add(prev, delta))
27
+ }
28
+ }, [])
29
+
30
+ // so stupid...
31
+ useDrag(ref, onDrag);
32
+
33
+ useEffect(() => {
34
+ const draggable = ref.current;
35
+ const dotted = dottedRef.current;
36
+ if (!dotted || !draggable)
37
+ return;
38
+
39
+ const onPointerEnter = () => {
40
+ setValue(15, performance.now())
41
+ }
42
+ const onPointerLeave = () => {
43
+ setValue(5, performance.now())
44
+ }
45
+
46
+ draggable.addEventListener('pointerenter', onPointerEnter)
47
+ draggable.addEventListener('pointerleave', onPointerLeave)
48
+ return () => {
49
+ draggable.removeEventListener('pointerenter', onPointerEnter)
50
+ draggable.removeEventListener('pointerleave', onPointerLeave)
51
+ }
52
+ }, [])
53
+
54
+ Animation1D.Bezier4.useAnimation(value, point => {
55
+ const dotted = dottedRef.current;
56
+ if (!dotted)
57
+ return;
58
+
59
+ dotted.setAttribute('r', Math.max(0, point.x).toFixed())
60
+ })
61
+
62
+ return [
63
+ h('circle', { cx: point.x, cy: point.y, ref: dottedRef, style:
64
+ { 'pointer-events': 'none' }, fill: 'none', stroke: 'black', 'stroke-dasharray': 4 }),
65
+ h('circle', { cx: point.x, cy: point.y, r: 15, ref, fill: 'none', stroke: 'none' }),
66
+ h('circle', { cx: point.x, cy: point.y, r: 6, fill, style: { 'pointer-events': 'none' } }),
67
+ isEmptyNode(children) && h('g', { transform: `translate(${point.x + 15} ${point.y - 15})` }, children)
68
+ ]
69
+ }
70
+
71
+ const isEmptyNode = (node: Node) => {
72
+ switch (typeof node) {
73
+ case 'object':
74
+ if (Array.isArray(node))
75
+ return node.length > 0;
76
+ return true;
77
+ default:
78
+ return true;
79
+ case 'boolean':
80
+ return node;
81
+ case 'string':
82
+ case 'number':
83
+ return !!node;
84
+ case 'undefined':
85
+ return false;
86
+ }
87
+ }
package/Grid.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { Component, createId, h, ReadonlyRef, Ref, useMemo, useState } from "@lukekaalim/act";
2
+ import { Vector } from "./vector";
3
+
4
+ export type GridProps = {
5
+ offset?: Vector<2>,
6
+ scale?: Vector<2>,
7
+
8
+ stroke?: string,
9
+ strokeWidth?: number,
10
+
11
+ ref?: ReadonlyRef<SVGRectElement>,
12
+ refs?: {
13
+ pattern?: Ref<null | SVGPatternElement>
14
+ }
15
+ };
16
+
17
+ export const Grid: Component<GridProps> = ({
18
+ offset = Vector(2).create(),
19
+ scale = Vector(2).scalar.add(Vector(2).create(), 100),
20
+ stroke = "grey",
21
+ strokeWidth = 1,
22
+ ref = {},
23
+ refs = {}
24
+ }) => {
25
+ const [gridId] = useState(createId());
26
+
27
+ return [
28
+ h('defs', {}, [
29
+ h('pattern', {
30
+ id: gridId,
31
+ width: `${scale.x}px`,
32
+ height: `${scale.y}px`,
33
+ x: offset.x,
34
+ y: offset.y,
35
+ patternUnits: 'userSpaceOnUse',
36
+ ref: refs.pattern || {},
37
+ }, [
38
+ h('path', {
39
+ d: `M ${scale.x} 0 L 0 0 0 ${scale.y}`,
40
+ fill: 'none',
41
+ stroke,
42
+ 'stroke-width': strokeWidth
43
+ })
44
+ ])
45
+ ]),
46
+ h('rect', { ref, width: '100%', height: '100%', fill: `url(#${gridId})` })
47
+ ]
48
+ }
package/LinePath.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { Component, h, useMemo } from "@lukekaalim/act"
2
+ import { Vector } from "./vector"
3
+
4
+ export type LinePathProps = {
5
+ calcPoint: (progress: number) => Vector<2>,
6
+ resolution?: number,
7
+
8
+ stroke?: string,
9
+ strokeWidth?: number,
10
+ }
11
+
12
+ export const LinePath: Component<LinePathProps> = ({
13
+ calcPoint,
14
+ resolution = 20,
15
+ stroke = 'black',
16
+ strokeWidth = 1,
17
+ }) => {
18
+ const points = useMemo(() => {
19
+ const points = [];
20
+ for (let i = 0; i < resolution+1; i++) {
21
+ const progress = i/resolution;
22
+ points.push(calcPoint(progress));
23
+ }
24
+ return points;
25
+ }, [calcPoint]);
26
+
27
+ return h('polyline', {
28
+ points: points.map(({ x, y }) => `${x.toFixed(4)},${y.toFixed(4)}`).join(' '),
29
+ stroke,
30
+ 'stroke-width': strokeWidth,
31
+ fill: 'none'
32
+ })
33
+ }
@@ -0,0 +1,52 @@
1
+ import { Component, h, ReadonlyRef, Ref, useEffect, useRef } from "@lukekaalim/act";
2
+ import { Grid } from "./Grid";
3
+
4
+ type AssertRefsExtends =
5
+ { [key: string]: ReadonlyRef<unknown | null> };
6
+
7
+ type AssertRefsReturn<T> =
8
+ { [K in keyof T]: T[K] extends Ref<infer X> ? NonNullable<X> : never };
9
+
10
+ export const assertRefs = <T extends AssertRefsExtends>(
11
+ refs: T
12
+ ): AssertRefsReturn<T> => {
13
+ const values: Partial<AssertRefsReturn<T>> = {};
14
+ for (const key in refs) {
15
+ const value = refs[key].current;
16
+ if (!value)
17
+ throw new Error();
18
+
19
+ values[key] = value as any;
20
+ }
21
+
22
+ return values as AssertRefsReturn<T>;
23
+ }
24
+ export const assertSVGParent = <T extends SVGElement>(el: T): SVGSVGElement => {
25
+ const parent = el.ownerSVGElement;
26
+ if (!parent)
27
+ throw new Error();
28
+ return parent;
29
+ }
30
+
31
+ export const ResizingSpace: Component = ({ children }) => {
32
+ const gRef = useRef<null | SVGGElement>(null);
33
+ const svgRef = useRef<null | SVGSVGElement>(null);
34
+
35
+ useEffect(() => {
36
+ const { group, svg } = assertRefs({ group: gRef, svg: svgRef });
37
+
38
+ const rect = group.getBoundingClientRect();
39
+
40
+ svg.viewBox.baseVal.width = rect.width;
41
+ svg.viewBox.baseVal.height = rect.height;
42
+
43
+ svg.viewBox.baseVal.x = rect.x;
44
+ svg.viewBox.baseVal.y = rect.y;
45
+ }, []);
46
+
47
+ return h('svg', { ref: svgRef }, [
48
+ h('g', { ref: gRef }, children),
49
+ h(Grid, { }),
50
+ ])
51
+ };
52
+
package/dimensions.ts ADDED
@@ -0,0 +1,10 @@
1
+ export type Dimension = 1 | 2 | 3 | 4 | 5;
2
+ export namespace Dimension {
3
+ export type Name = {
4
+ [1]: 'x',
5
+ [2]: 'y',
6
+ [3]: 'z',
7
+ [4]: 'u',
8
+ [5]: 'v',
9
+ }
10
+ }
package/elements.ts ADDED
@@ -0,0 +1,120 @@
1
+ import { Component, h, Ref } from "@lukekaalim/act";
2
+ import { Vector } from "./vector";
3
+
4
+ export type SVGCoreProps<T extends SVGElement = SVGElement> = {
5
+ ref?: Ref<null | T>,
6
+ stroke?: string,
7
+ strokeWidth?: number,
8
+ strokeDashArray?: number[],
9
+
10
+ fill?: string,
11
+ }
12
+
13
+ export const remapCoreProps = <T extends SVGElement>(props: SVGCoreProps<T>): Record<string, unknown> => {
14
+ const attributes: Record<string, unknown> = {};
15
+
16
+ if (props.stroke)
17
+ attributes['stroke'] = props.stroke;
18
+ else
19
+ attributes['stroke'] = 'black';
20
+
21
+ if (props.strokeWidth)
22
+ attributes['stroke-width'] = props.strokeWidth.toString();
23
+ else
24
+ attributes['stroke-width'] = '1';
25
+
26
+ if (props.fill) {
27
+ attributes['fill'] = props.fill;
28
+ }
29
+ if (props.strokeDashArray) {
30
+ attributes['stroke-dasharray'] = props.strokeDashArray.join(' ');
31
+ }
32
+ if (props.ref)
33
+ attributes['ref'] = props.ref;
34
+
35
+ return attributes;
36
+ }
37
+
38
+ export type LineProps = (
39
+ | { x1: number, y1: number, x2: number, y2: number }
40
+ | { start: Vector<2>, end: Vector<2> }
41
+ ) & SVGCoreProps<SVGLineElement>;
42
+
43
+ export const Line: Component<LineProps> = (props) => {
44
+ if ('x1' in props) {
45
+ const { x1, x2, y1, y2, ...extraProps } = props
46
+ return h('line', { x1, x2, y1, y2, ...remapCoreProps(extraProps) });
47
+ } else {
48
+ const { start, end, ...extraProps } = props
49
+ return h('line', { x1: start.x, x2: end.x, y1: start.y, y2: end.y, ...remapCoreProps(extraProps) });
50
+ }
51
+ }
52
+
53
+ export type CircleProps = (
54
+ | { cx?: number, cy?: number, r?: number, }
55
+ | { center: Vector<2>, radius: number }
56
+ ) & SVGCoreProps<SVGCircleElement>
57
+
58
+ export const Circle: Component<CircleProps> = (props) => {
59
+ if ("center" in props) {
60
+ const { center, radius, ...otherProps } = props;
61
+ return h('circle', {
62
+ cx: center.x,
63
+ cy: center.y,
64
+ r: radius,
65
+ ...remapCoreProps(otherProps),
66
+ })
67
+ } else {
68
+ const { cx, cy, r, ...otherProps } = props
69
+ return h('circle', { cx, cy, r, ...remapCoreProps(otherProps) })
70
+ }
71
+ }
72
+
73
+ export type RectProps = (
74
+ | { x?: number, y?: number, width?: number, height?: number }
75
+ | { position: Vector<2>, size: Vector<2> }
76
+ ) & SVGCoreProps<SVGRectElement>
77
+
78
+ export const Rect: Component<RectProps> = (props) => {
79
+ if ("position" in props) {
80
+ const { position, size, ...otherProps } = props;
81
+ return h('rect', {
82
+ x: position.x,
83
+ y: position.y,
84
+ width: size.x,
85
+ height: size.y,
86
+ ...remapCoreProps(otherProps)
87
+ })
88
+ } else {
89
+ const { x, y, width, height, ...otherProps } = props
90
+ return h('circle', { x, y, width, height, ...remapCoreProps(otherProps) })
91
+ }
92
+ }
93
+
94
+ export type GroupProps = {
95
+ position?: Vector<2>,
96
+ } & SVGCoreProps<SVGGElement>
97
+
98
+ export const Group: Component<GroupProps> = ({ position, children, ...extraProps }) => {
99
+ if (position)
100
+ return h('g', { transform: `translate(${position.x} ${position.y})`, ...extraProps }, children)
101
+
102
+ return h('g', { ...extraProps }, children)
103
+ }
104
+
105
+
106
+ export type ForeignObjectProps = (
107
+ | { position: Vector<2>, size: Vector<2> }
108
+ | { x: number, y: number, width: number, height: number }
109
+ )
110
+
111
+ export const ForeignObject: Component<ForeignObjectProps> = (props) => {
112
+ if ("position" in props) {
113
+ const { position, size, children } = props;
114
+ return h('foreignObject', { x: position.x, y: position.y, width: size.x, height: size.y }, children)
115
+ } else {
116
+ const { x, y, width, height, children } = props;
117
+ return h('foreignObject', { x, y, width, height }, children)
118
+ }
119
+
120
+ }
package/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ export * from './EditablePoint';
2
+ export * from './Defs';
3
+
4
+ export * from './CartesianSpace';
5
+ export * from './ResizingSpace';
6
+ export * from './Axes';
7
+
8
+ export * from './LinePath';
9
+
10
+ export * from './structures';
11
+ export * from './vector';
12
+
13
+ export * from './elements';
14
+
15
+ export * from './useDrag';
package/mod.doc.ts ADDED
@@ -0,0 +1,173 @@
1
+ import { Component, h, Node, useEffect, useRef, useState } from "@lukekaalim/act";
2
+ import { SVG } from '@lukekaalim/act-web';
3
+ import { DocApp, MarkdownArticle, RAFBeat } from "@lukekaalim/grimoire";
4
+
5
+ import { CartesianSpace, CartesianSpaceController } from "./CartesianSpace";
6
+ import { LinePath } from "./LinePath";
7
+ import { TypeDocPlugin } from "@lukekaalim/grimoire-ts";
8
+
9
+ import readmeMd from './readme.md?raw';
10
+ import structuresMd from './structures.md?raw';
11
+ import projectJSON from 'typedoc:index.ts';
12
+ import { Ring } from "./structures";
13
+ import { Vector2D } from "@lukekaalim/act-curve";
14
+ import { Circle } from "./elements";
15
+ import { Vector } from "./vector";
16
+ import { assertRefs } from "./ResizingSpace";
17
+
18
+ const CartesianSpaceDemo: Component = () => {
19
+ const [x, setX] = useState(0);
20
+ const [y, setY] = useState(0);
21
+
22
+ return [
23
+ h('div', { style: {
24
+ display: 'flex'
25
+ }}, [
26
+ h('input', {
27
+ type: 'range',
28
+ min: 0,
29
+ max: 100,
30
+ value: x,
31
+ onInput: (e: InputEvent) => (setX((e.target as HTMLInputElement).valueAsNumber))
32
+ }),
33
+ h('input', {
34
+ type: 'range',
35
+ min: 0,
36
+ max: 100,
37
+ value: y,
38
+ onInput: (e: InputEvent) => (setY((e.target as HTMLInputElement).valueAsNumber))
39
+ }),
40
+ ]),
41
+ h('div', { style: {
42
+ height: '400px',
43
+ display: 'flex'
44
+ }}, h(CartesianSpace, { offset: { x, y } }),)
45
+ ];
46
+ }
47
+
48
+ const LinePathDemo = () => {
49
+ return h('div', { style: {
50
+ height: '400px',
51
+ display: 'flex',
52
+ }}, h(CartesianSpace, { offset: { x: 0, y: 0 } }, [
53
+ h(LinePath, {
54
+ stroke: 'red',
55
+ strokeWidth: 2,
56
+ calcPoint(progress) {
57
+ return {
58
+ x: (Math.sin(progress * Math.PI * 2) * 100) + 150,
59
+ y: (Math.cos(progress * Math.PI * 2) * 100) + 150
60
+ }
61
+ },
62
+ resolution: 16
63
+ }),
64
+ h(LinePath, {
65
+ stroke: 'red',
66
+ strokeWidth: 2,
67
+ calcPoint(progress) {
68
+ return {
69
+ x: (progress * 300) + 300,
70
+ y: (Math.cos((progress + (1/8)) * Math.PI * 4) * 100) + 150
71
+ }
72
+ },
73
+ resolution: 32
74
+ }),
75
+ ]))
76
+ }
77
+
78
+ export default h(MarkdownArticle, {
79
+ content: await import('./readme.md?raw').then(m => m.default),
80
+ //options: {
81
+ // components: {
82
+ // CartesianSpaceDemo,
83
+ // LinePathDemo
84
+ // }
85
+ //}
86
+ })
87
+
88
+ export const buildGraphitDocs = (doc: DocApp<[TypeDocPlugin]>) => {
89
+ doc.typedoc.addProjectJSON('@lukekaalim/act-graphit', projectJSON);
90
+
91
+ doc.article.add('graphit.readme', readmeMd, '/packages/@lukekaalim/act-graphit')
92
+ doc.article.add('graphit.structs', structuresMd, '/packages/@lukekaalim/act-graphit/structures');
93
+
94
+ doc.demos.add('CartesianSpaceDemo', CartesianSpaceDemo)
95
+ doc.demos.add('LinePathDemo', LinePathDemo)
96
+ doc.demos.add('Ring', () => {
97
+ let output: Node[] = [];
98
+
99
+ const a = new Ring<number>(8);
100
+ const b = new Ring<number>(8);
101
+ const c = new Ring<number>(8);
102
+
103
+ for (let i = 1; i <= 12; i++) {
104
+ a.push(i)
105
+ }
106
+ b.index = 7
107
+ for (let i = 1; i <= 5; i++) {
108
+ b.push(i)
109
+ }
110
+ c.index = 1;
111
+ for (let i = 1; i <= 3; i++) {
112
+ c.push(i)
113
+ }
114
+
115
+ output.push(Array.from(a.map(v => h('span', { style: { padding: '2px' }}, v))));
116
+ output.push(Array.from(b.map(v => h('span', { style: { padding: '2px' }}, v))));
117
+ output.push(Array.from(c.map(v => h('span', { style: { padding: '2px' }}, v))));
118
+
119
+ return h('ol', {}, output.map(line => h('li', {}, line)))
120
+ });
121
+
122
+ doc.demos.add('Worm', () => {
123
+ const ringRef = useRef(new Ring<Vector2D>(128))
124
+ const [points, setPoints] = useState('')
125
+
126
+ const [start, setStart] = useState<Vector<2>>(Vector2D.ZERO);
127
+ const [end, setEnd] = useState<Vector<2>>(Vector2D.ZERO);
128
+
129
+ const controllerRef = useRef<CartesianSpaceController | null>(null)
130
+ const polylineRef = useRef<SVGPolylineElement | null>(null);
131
+
132
+ RAFBeat.useCallback(({ setCallback }) => {
133
+ let velocity = Vector2D.create(0, 0);
134
+ let position = Vector2D.create(0, 0);
135
+
136
+ const { controller, polyline } = assertRefs({ controller: controllerRef, polyline: polylineRef });
137
+
138
+ const svg = polyline.ownerSVGElement;
139
+ if (!svg)
140
+ return;
141
+
142
+ setCallback(({ now, delta }) => {
143
+ velocity.x += (Math.random() * 8) - 4
144
+ velocity.y += (Math.random() * 8) - 4;
145
+
146
+ velocity = Vector2D.scalar.multiply(velocity, (1/Vector2D.ScalarAPI.length(velocity) * 5));
147
+
148
+ position.x += velocity.x;
149
+ position.y += velocity.y;
150
+
151
+ ringRef.current.push({ x: position.x, y: position.y });
152
+
153
+ controller.position = Vector2D.add(Vector2D.subtract(Vector2D.ZERO, position), { x: 256, y: 256 })
154
+ controller.update();
155
+
156
+
157
+ polyline.points.clear();
158
+ for (const point of ringRef.current.values()) {
159
+ const svgPoint = svg.createSVGPoint();
160
+ svgPoint.x = point.x;
161
+ svgPoint.y = point.y;
162
+ polyline.points.appendItem(svgPoint);
163
+ }
164
+ })
165
+ }, [])
166
+
167
+ return h(CartesianSpace, { style: { width: '100%', height: '512px' }, refs: { controller: controllerRef } }, [
168
+ h('polyline', { ref: polylineRef, fill: 'none', stroke: 'black', 'stroke-width': '2px' }),
169
+ //h(Circle, { center: start, radius: 5 }),
170
+ //h(Circle, { center: end, radius: 5 }),
171
+ ])
172
+ });
173
+ }
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@lukekaalim/act-graphit",
3
+ "version": "1.0.0",
4
+ "description": "An Act-based graphing component library.",
5
+ "main": "index.ts",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "author": "",
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "@lukekaalim/act": "^3.2.1",
13
+ "@lukekaalim/act-web": "^3.4.0-alpha.2"
14
+ }
15
+ }
package/readme.md ADDED
@@ -0,0 +1,28 @@
1
+ # @lukekaalim/act-graphit
2
+
3
+ An Act-based graphing component library.
4
+
5
+ ## Components
6
+
7
+ <TypeDoc project="@lukekaalim/act-graphit" name="CartesianSpace" extras="CartesianSpaceProps" />
8
+ Move around the space by clicking and dragging
9
+ <Demo demo="CartesianSpaceDemo" />
10
+
11
+
12
+ <TypeDoc project="@lukekaalim/act-graphit" name="LinePath" />
13
+
14
+ Here we draw a circle and a sine wave.
15
+ <Demo demo="LinePathDemo" />
16
+
17
+ Draw a PolyLine using a progress function and a resolution.
18
+
19
+ ## Utils
20
+
21
+ <TypeDoc project="@lukekaalim/act-graphit" name="subscribeMap" />
22
+
23
+ <TypeDoc project="@lukekaalim/act-graphit" name="useDrag" />
24
+
25
+ ## Comments
26
+
27
+ > Graphs have a `positive-y = up` display instead of the traditional
28
+ > DOM `positive-y = down`.
package/rect.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { Dimension } from "./dimensions";
2
+ import { Vector } from "./vector";
3
+
4
+ export type Box<D extends Dimension> = {
5
+ position: Vector<D>,
6
+ size: Vector<D>,
7
+ };
@@ -0,0 +1 @@
1
+ export * from './ring';
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Simple data structure that stores T
3
+ * in ring slots.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import { Ring } from '@lukekaalim/act-graphit';
8
+ * const my_ring = new Ring<string>(2);
9
+ *
10
+ * my_ring.push("Hello");
11
+ * my_ring.push("World");
12
+ *
13
+ * // after this push, "Hello" is dropped
14
+ * my_ring.push("Star");
15
+ *
16
+ * // prints "World", "Star"
17
+ * console.log(Array.from(my_ring.values()))
18
+ * ```
19
+ */
20
+ export class Ring<T> {
21
+ /** @private */
22
+ readonly backend: T[];
23
+
24
+ /** @private */
25
+ index: number;
26
+ /** @private */
27
+ size: number;
28
+
29
+ /**
30
+ *
31
+ * @param capacity The maximum amount of items that can be
32
+ * inside the Ring at once. Once items are pushed beyond
33
+ * the capacity, the oldest one is removed.
34
+ */
35
+ constructor(capacity: number) {
36
+ this.backend = Array.from({ length: capacity });
37
+ this.index = 0;
38
+ this.size = 0;
39
+ }
40
+
41
+ push(item: T) {
42
+ this.backend[this.index] = item;
43
+ this.index = (this.index + 1) % this.backend.length;
44
+
45
+ if (this.size < this.backend.length)
46
+ this.size++;
47
+ }
48
+
49
+ *map<Output>(transformer: (item: T) => Output): Generator<Output, void, void> {
50
+ const { index, size, backend: { length: capacity }} = this;
51
+ const startIndex = (index + capacity - size) % capacity;
52
+
53
+ for (let i = 0; i < this.size; i++) {
54
+ yield transformer(this.backend[(startIndex + i) % this.backend.length])
55
+ }
56
+ return;
57
+ }
58
+ values() {
59
+ return this.map(x => x);
60
+ }
61
+ item(itemIndex: number) {
62
+ const { index, size, backend: { length: capacity }} = this;
63
+ const mappedIndex = (itemIndex + index + capacity - size) % capacity;
64
+
65
+ return this.backend[mappedIndex];
66
+ }
67
+ head() {
68
+ return this.item(this.index - 1);
69
+ }
70
+ tail() {
71
+ return this.item(0);
72
+ }
73
+ }
package/structures.md ADDED
@@ -0,0 +1,24 @@
1
+ # Structures
2
+
3
+ Some common data structures to store/load data
4
+ from and display them on graphs.
5
+
6
+ More useful for a closer-to-realtime scenario,
7
+ as regular arrays will be fine for most data.
8
+
9
+ `Graphit` has support for rendering each
10
+ of these structures.
11
+
12
+ <TypeDoc project="@lukekaalim/act-graphit" name="Ring" />
13
+
14
+ > This "Worm" demo generates pairs of random numbers,
15
+ > groups them into <Reference key="ts:@lukekaalim/act-curve.Vector2D">Vector2D</Reference>
16
+ > pushes them to the <Reference key="ts:@lukekaalim/act-graphit.Ring">Ring</Reference>,
17
+ > and then renders the Ring's contents
18
+ > as a PolyLine.
19
+ >
20
+ > You can see the Ring "forgetting" older elements as the line generates
21
+ > new ones, but still keeps the elements in the insertion order.
22
+
23
+
24
+ <Demo demo="Worm" />
package/useDrag.ts ADDED
@@ -0,0 +1,105 @@
1
+ import { ReadonlyRef, useEffect, useState } from "@lukekaalim/act";
2
+ import { Vector } from "./vector";
3
+
4
+ type EventMap = {
5
+ [K in keyof (HTMLElementEventMap & SVGElementEventMap)]?:
6
+ (event: (HTMLElementEventMap & HTMLElementEventMap)[K]) => void
7
+ };
8
+
9
+ /**
10
+ * Subscribe to a lot of events on an element,
11
+ * and then unsubscribe to them all at once
12
+ * when you are done.
13
+ *
14
+ * @param element The element you want to subscribe to events for
15
+ * @param eventMap A object where every key is the "type" of event and the value is
16
+ * the event handler function itself
17
+ * @returns An object with an "unsubscribe" function attached. Call that function
18
+ * to remove all event listeners
19
+ *
20
+ * @throws If the provided element is null
21
+ */
22
+ export const subscribeMap = (
23
+ element: HTMLElement | SVGElement | null,
24
+ eventMap: EventMap
25
+ ) => {
26
+ type K = keyof EventMap;
27
+
28
+ if (!element)
29
+ throw new Error();
30
+
31
+ for (const key in eventMap) {
32
+ element.addEventListener(key as K, eventMap[key as K] as any);
33
+ }
34
+ return {
35
+ unsubscribe() {
36
+ for (const key in eventMap) {
37
+ element.removeEventListener(key as K, eventMap[key as K] as any);
38
+ }
39
+ }
40
+ }
41
+ };
42
+
43
+ type DragOptions = {
44
+ onFinishDrag?: () => void
45
+ }
46
+
47
+ export const useDrag = (
48
+ ref: ReadonlyRef<null | HTMLElement | SVGElement>,
49
+ onElementMove: (positionDelta: Vector<2>) => void,
50
+ shouldStartDrag: (event: PointerEvent) => boolean = () => true,
51
+ { onFinishDrag }: DragOptions = {}
52
+ ) => {
53
+ const [dragging, setDragging] = useState(false);
54
+
55
+ useEffect(() => {
56
+ const el = ref.current as HTMLElement;
57
+ if (!el)
58
+ return;
59
+ let dragging = false;
60
+
61
+ const sub = subscribeMap(ref.current, {
62
+ wheel(event) {
63
+ if (event.defaultPrevented)
64
+ return;
65
+ event.preventDefault();
66
+ onElementMove({ x: -event.deltaX, y: -event.deltaY });
67
+ },
68
+ pointerdown(event) {
69
+ if (event.defaultPrevented)
70
+ return;
71
+ if (!shouldStartDrag(event))
72
+ return;
73
+ if (event.button > 1)
74
+ return;
75
+ event.preventDefault();
76
+ setDragging(true);
77
+ dragging = true;
78
+ el.setPointerCapture(event.pointerId);
79
+ },
80
+ pointermove(event) {
81
+ if (!dragging || event.defaultPrevented)
82
+ return;
83
+
84
+ event.preventDefault();
85
+ onElementMove({ x: event.movementX, y: event.movementY });
86
+ },
87
+ pointerup(event) {
88
+ onElementMove({ x: event.movementX, y: event.movementY });
89
+ setDragging(false);
90
+ dragging = false;
91
+ el.releasePointerCapture(event.pointerId);
92
+ console.log('finish drag')
93
+
94
+ if (onFinishDrag)
95
+ onFinishDrag()
96
+ },
97
+ })
98
+
99
+ return () => {
100
+ sub.unsubscribe();
101
+ }
102
+ }, [onElementMove, onFinishDrag]);
103
+
104
+ return dragging;
105
+ }
@@ -0,0 +1,27 @@
1
+ import { ReadonlyRef, Ref, useEffect, useState } from "@lukekaalim/act";
2
+ import { Vector } from "./vector";
3
+
4
+ export const useElementSize = (ref: ReadonlyRef<Element | null>) => {
5
+ const [size, setSize] = useState(Vector(2).create());
6
+
7
+ useEffect(() => {
8
+ const element = ref.current;
9
+ if (!element)
10
+ return;
11
+
12
+ const observer = new ResizeObserver(() => {
13
+ const rect = element.getBoundingClientRect();
14
+ setSize(prev => {
15
+ if (prev.x !== rect.width && prev.y !== rect.height)
16
+ return { x: rect.width, y: rect.height };
17
+ return prev;
18
+ })
19
+ });
20
+ observer.observe(element, { });
21
+ return () => {
22
+ observer.disconnect()
23
+ }
24
+ }, []);
25
+
26
+ return size;
27
+ };
package/vector.ts ADDED
@@ -0,0 +1,89 @@
1
+ import { Dimension } from "./dimensions";
2
+
3
+ // THESE VECTORS SUCK
4
+ // YOU JUST NEED 2D VECTORS
5
+
6
+ type VectorDimensions = {
7
+ [1]: 1,
8
+ [2]: 1 | 2,
9
+ [3]: 1 | 2 | 3,
10
+ [4]: 1 | 2 | 3 | 4,
11
+ [5]: 1 | 2 | 3 | 4 | 5,
12
+ };
13
+
14
+ export type Vector<D extends Dimension> = {
15
+ [key in Dimension.Name[VectorDimensions[D]]]: number
16
+ };
17
+
18
+ export type AnyVector = Vector<1> | Vector<2> | Vector<3> | Vector<4> | Vector<5>;
19
+
20
+ export type VectorOps<D extends Dimension> = {
21
+ create(): Vector<D>,
22
+ operate(vector: Vector<D>, op: (value: number, field: Dimension.Name[VectorDimensions[D]]) => number): Vector<D>,
23
+
24
+ add(a: Vector<D>, b: Vector<D>): Vector<D>,
25
+ mult(a: Vector<D>, b: Vector<D>): Vector<D>,
26
+
27
+ scalar: {
28
+ add(a: Vector<D>, b: number): Vector<D>,
29
+ mult(a: Vector<D>, b: number): Vector<D>,
30
+ }
31
+ }
32
+
33
+ export const Vector = <D extends Dimension>(d: D): VectorOps<D> => {
34
+ const create = (): Vector<D> => {
35
+ switch (d) {
36
+ case 1:
37
+ return { x: 0 } as Vector<D>;
38
+ case 2:
39
+ return { x: 0, y: 0 } as Vector<D>;
40
+ case 3:
41
+ return { x: 0, y: 0, z: 0 } as Vector<D>;
42
+ case 4:
43
+ return { x: 0, y: 0, z: 0, u: 0 } as Vector<D>;
44
+ case 5:
45
+ return { x: 0, y: 0, z: 0, u: 0, v: 0 } as Vector<D>;
46
+ default:
47
+ const _: never = d;
48
+ throw new Error();
49
+ }
50
+ }
51
+
52
+ const operate = (v: Vector<D>, op: (field: number, axis: Dimension.Name[VectorDimensions[D]]) => number): Vector<D> => {
53
+ const result = create();
54
+ switch (d) {
55
+ case 5:
56
+ (result as Vector<5>).v = op((v as Vector<5>).v, 'v' as Dimension.Name[VectorDimensions[D]]);
57
+ case 4:
58
+ (result as Vector<4>).u = op((v as Vector<4>).u, 'u' as Dimension.Name[VectorDimensions[D]]);
59
+ case 3:
60
+ (result as Vector<3>).z = op((v as Vector<3>).z, 'z' as Dimension.Name[VectorDimensions[D]]);
61
+ case 2:
62
+ (result as Vector<2>).y = op((v as Vector<2>).y, 'y' as Dimension.Name[VectorDimensions[D]]);
63
+ case 1:
64
+ (result as Vector<1>).x = op((v as Vector<1>).x, 'x' as Dimension.Name[VectorDimensions[D]]);
65
+ return result;
66
+ default:
67
+ const _: never = d;
68
+ throw new Error();
69
+ }
70
+ }
71
+ return {
72
+ create,
73
+ operate,
74
+ add(a, b) {
75
+ return operate(a, (v1, f) => v1 + b[f]);
76
+ },
77
+ mult(a, b) {
78
+ return operate(a, (v1, f) => v1 * b[f]);
79
+ },
80
+ scalar: {
81
+ add(a, b) {
82
+ return operate(a, (v1) => v1 + b);
83
+ },
84
+ mult(a, b) {
85
+ return operate(a, (v1) => v1 * b);
86
+ },
87
+ }
88
+ }
89
+ }