@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 +108 -0
- package/Axis.ts +58 -0
- package/CartesianSpace.module.css +13 -0
- package/CartesianSpace.ts +129 -0
- package/Defs.ts +35 -0
- package/EditablePoint.module.css +7 -0
- package/EditablePoint.ts +87 -0
- package/Grid.ts +48 -0
- package/LinePath.ts +33 -0
- package/ResizingSpace.ts +52 -0
- package/dimensions.ts +10 -0
- package/elements.ts +120 -0
- package/index.ts +15 -0
- package/mod.doc.ts +173 -0
- package/package.json +15 -0
- package/readme.md +28 -0
- package/rect.ts +7 -0
- package/structures/index.ts +1 -0
- package/structures/ring.ts +73 -0
- package/structures.md +24 -0
- package/useDrag.ts +105 -0
- package/useElementSize.ts +27 -0
- package/vector.ts +89 -0
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,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
|
+
}
|
package/EditablePoint.ts
ADDED
|
@@ -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
|
+
}
|
package/ResizingSpace.ts
ADDED
|
@@ -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
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 @@
|
|
|
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
|
+
}
|