gpucanvas 0.4.3 → 0.4.5
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/dist/{chunk-2EDYGRFR.js → chunk-IIRGNX76.js} +1750 -1514
- package/dist/chunk-IIRGNX76.js.map +1 -0
- package/dist/{editor-CuH4tLHg.d.ts → editor-BuoBLRhF.d.ts} +11 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +24 -10
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-2EDYGRFR.js.map +0 -1
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
interface Camera {
|
|
2
|
-
/** World coordinate shown at the viewport's top-left corner. */
|
|
3
|
-
x: number;
|
|
4
|
-
y: number;
|
|
5
|
-
zoom: number;
|
|
6
|
-
}
|
|
7
1
|
interface Rect {
|
|
8
2
|
x: number;
|
|
9
3
|
y: number;
|
|
@@ -14,6 +8,13 @@ interface Point {
|
|
|
14
8
|
x: number;
|
|
15
9
|
y: number;
|
|
16
10
|
}
|
|
11
|
+
|
|
12
|
+
interface Camera {
|
|
13
|
+
/** World coordinate shown at the viewport's top-left corner. */
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
zoom: number;
|
|
17
|
+
}
|
|
17
18
|
/** A point in surface pixels (from the surface's top left) as world units. */
|
|
18
19
|
declare const screenToWorld: (camera: Camera, point: Point) => Point;
|
|
19
20
|
/** A world point as surface pixels. */
|
|
@@ -156,7 +157,10 @@ interface GpuCanvas {
|
|
|
156
157
|
removeNode(element: HTMLElement): void;
|
|
157
158
|
/** Where `element` sits on the canvas, in world units, or null when it is not a node. */
|
|
158
159
|
getNodeRect(element: HTMLElement): Rect | null;
|
|
159
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* Nodes that overlap `rect` (world units), such as the ones under a selection box, in the order
|
|
162
|
+
* they were added: later ones draw on top.
|
|
163
|
+
*/
|
|
160
164
|
getNodesIn(rect: Rect): HTMLElement[];
|
|
161
165
|
/** The node that holds `element`: the element itself when it is a node, or null. */
|
|
162
166
|
getNodeOf(element: Element): HTMLElement | null;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { R as Rect, G as GpuCanvas, P as Point, E as Effect, a as GpuCanvasOptions } from './editor-
|
|
2
|
-
export { C as Camera, b as Editor, c as EditorOptions, d as EditorPick, e as EffectDefinition, f as GpuCanvasMode, g as GpuCanvasStats, L as Layer, T as TransformEvent, h as bindEditor, i as defineEffect, j as effects, k as fitCamera, s as screenToWorld, w as worldToScreen, z as zoomAt } from './editor-
|
|
1
|
+
import { R as Rect, G as GpuCanvas, P as Point, E as Effect, a as GpuCanvasOptions } from './editor-BuoBLRhF.js';
|
|
2
|
+
export { C as Camera, b as Editor, c as EditorOptions, d as EditorPick, e as EffectDefinition, f as GpuCanvasMode, g as GpuCanvasStats, L as Layer, T as TransformEvent, h as bindEditor, i as defineEffect, j as effects, k as fitCamera, s as screenToWorld, w as worldToScreen, z as zoomAt } from './editor-BuoBLRhF.js';
|
|
3
3
|
|
|
4
4
|
declare function supportsGpuCanvas(): boolean;
|
|
5
5
|
|
package/dist/index.js
CHANGED
package/dist/react.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode, CSSProperties } from 'react';
|
|
3
|
-
import { E as Effect, a as GpuCanvasOptions, G as GpuCanvas$1, L as Layer, c as EditorOptions } from './editor-
|
|
3
|
+
import { E as Effect, a as GpuCanvasOptions, G as GpuCanvas$1, L as Layer, c as EditorOptions } from './editor-BuoBLRhF.js';
|
|
4
4
|
|
|
5
5
|
interface GpuCanvasProps extends GpuCanvasOptions {
|
|
6
6
|
children?: ReactNode;
|
package/dist/react.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import {
|
|
3
3
|
bindEditor,
|
|
4
|
-
createGpuCanvas
|
|
5
|
-
|
|
4
|
+
createGpuCanvas,
|
|
5
|
+
defaultLabel
|
|
6
|
+
} from "./chunk-IIRGNX76.js";
|
|
6
7
|
|
|
7
8
|
// src/react.tsx
|
|
8
9
|
import {
|
|
@@ -24,18 +25,32 @@ function GpuCanvas({ children, className, style, onReady, ...options }) {
|
|
|
24
25
|
useEffect(() => {
|
|
25
26
|
let disposed = false;
|
|
26
27
|
let created = null;
|
|
27
|
-
createGpuCanvas(hostRef.current,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
createGpuCanvas(hostRef.current, {
|
|
29
|
+
...latest.current.options,
|
|
30
|
+
// The callback from the latest render, not the first.
|
|
31
|
+
onCameraChange: (camera) => latest.current.options.onCameraChange?.(camera)
|
|
32
|
+
}).then(
|
|
33
|
+
(instance) => {
|
|
34
|
+
if (disposed) return instance.destroy();
|
|
35
|
+
created = instance;
|
|
36
|
+
setCanvas(instance);
|
|
37
|
+
latest.current.onReady?.(instance);
|
|
38
|
+
},
|
|
39
|
+
(error) => console.error("gpucanvas: the canvas failed to start", error)
|
|
40
|
+
);
|
|
33
41
|
return () => {
|
|
34
42
|
disposed = true;
|
|
35
43
|
created?.destroy();
|
|
36
44
|
setCanvas(null);
|
|
37
45
|
};
|
|
38
46
|
}, []);
|
|
47
|
+
const { background } = options;
|
|
48
|
+
const shownBackground = useRef(background);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!canvas || background === void 0 || background === shownBackground.current) return;
|
|
51
|
+
shownBackground.current = background;
|
|
52
|
+
canvas.setBackground(background);
|
|
53
|
+
}, [canvas, background]);
|
|
39
54
|
return /* @__PURE__ */ jsx("div", { ref: hostRef, className, style: { position: "relative", ...style }, children: /* @__PURE__ */ jsx(CanvasContext.Provider, { value: canvas, children: canvas && children }) });
|
|
40
55
|
}
|
|
41
56
|
function CanvasNode({
|
|
@@ -93,7 +108,7 @@ function useEditor(options = {}) {
|
|
|
93
108
|
labels,
|
|
94
109
|
snap,
|
|
95
110
|
color,
|
|
96
|
-
label: (layer) => latest.current.label
|
|
111
|
+
label: (layer) => (latest.current.label ?? defaultLabel(canvas))(layer),
|
|
97
112
|
onChange(next) {
|
|
98
113
|
setSelected(next);
|
|
99
114
|
latest.current.onChange?.(next);
|
|
@@ -113,7 +128,6 @@ function useEditor(options = {}) {
|
|
|
113
128
|
clear: () => editor.current?.clear()
|
|
114
129
|
};
|
|
115
130
|
}
|
|
116
|
-
var defaultLabel = (canvas, layer) => canvas.getNodeOf(layer[0]) === layer[0] ? null : layer[0].localName;
|
|
117
131
|
var COMPONENT_TAGS = /* @__PURE__ */ new Set([0, 1, 11, 15]);
|
|
118
132
|
var PORTAL_TAG = 4;
|
|
119
133
|
var fiberOf = (element) => {
|
package/dist/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/react.tsx"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport {\n bindEditor,\n createGpuCanvas,\n type Editor,\n type EditorOptions,\n type EditorPick,\n type Effect,\n type GpuCanvas as GpuCanvasInstance,\n type GpuCanvasOptions,\n type Layer,\n} from './index';\n\nconst CanvasContext = createContext<GpuCanvasInstance | null>(null);\n\nexport interface GpuCanvasProps extends GpuCanvasOptions {\n children?: ReactNode;\n className?: string;\n style?: CSSProperties;\n onReady?: (canvas: GpuCanvasInstance) => void;\n}\n\n/** An infinite canvas. Children are `<CanvasNode>`s; they mount once the canvas is ready. */\nexport function GpuCanvas({ children, className, style, onReady, ...options }: GpuCanvasProps) {\n const hostRef = useRef<HTMLDivElement>(null);\n const [canvas, setCanvas] = useState<GpuCanvasInstance | null>(null);\n const latest = useRef({ options, onReady });\n latest.current = { options, onReady };\n\n useEffect(() => {\n let disposed = false;\n let created: GpuCanvasInstance | null = null;\n createGpuCanvas(hostRef.current!, latest.current.options).then((instance) => {\n if (disposed) return instance.destroy();\n created = instance;\n setCanvas(instance);\n latest.current.onReady?.(instance);\n });\n return () => {\n disposed = true;\n created?.destroy();\n setCanvas(null);\n };\n // The mode and GPU resources are fixed for the canvas's lifetime; remount to change them.\n }, []);\n\n return (\n <div ref={hostRef} className={className} style={{ position: 'relative', ...style }}>\n <CanvasContext.Provider value={canvas}>{canvas && children}</CanvasContext.Provider>\n </div>\n );\n}\n\nexport interface CanvasNodeProps {\n x: number;\n y: number;\n width: number;\n height: number;\n children?: ReactNode;\n className?: string;\n style?: CSSProperties;\n /** A shader the node is drawn through, such as `effects.glow`. */\n effect?: Effect | null;\n /** Up to four parameters for `effect`; its defaults otherwise. */\n effectParams?: readonly number[];\n}\n\n/** A live DOM node placed on the canvas in world units. Events, focus and state work as usual. */\nexport function CanvasNode({\n x,\n y,\n width,\n height,\n children,\n className,\n style,\n effect = null,\n effectParams,\n}: CanvasNodeProps) {\n const canvas = useContext(CanvasContext);\n if (!canvas) throw new Error('<CanvasNode> must be inside <GpuCanvas>');\n const [element] = useState(() => document.createElement('div'));\n\n useLayoutEffect(() => {\n canvas.addNode(element, { x, y, width, height });\n return () => canvas.removeNode(element);\n // Position changes go through updateNode below without remounting.\n }, [canvas, element]);\n\n const mounted = useRef(false);\n useLayoutEffect(() => {\n if (mounted.current) canvas.updateNode(element, { x, y, width, height });\n mounted.current = true;\n }, [canvas, element, x, y, width, height]);\n\n useLayoutEffect(() => {\n element.className = className ?? '';\n }, [element, className]);\n\n const paramsKey = effectParams?.join(',');\n useLayoutEffect(() => {\n canvas.setEffect(element, effect, effectParams);\n // The parameters' values, not the array's identity, decide a redraw.\n }, [canvas, element, effect, paramsKey]);\n\n return createPortal(\n <div style={{ width: '100%', height: '100%', ...style }}>{children}</div>,\n element,\n );\n}\n\nexport const useGpuCanvas = () => useContext(CanvasContext);\n\n/**\n * The editor (`bindEditor`) on the canvas around it, with its selection as React state. Use it\n * inside `<GpuCanvas>`; `onChange` lifts the selection out.\n */\nexport function useEditor(options: EditorOptions = {}) {\n const canvas = useGpuCanvas();\n const [selected, setSelected] = useState<Layer[]>([]);\n const editor = useRef<Editor | null>(null);\n const latest = useRef(options);\n latest.current = options;\n // Functions are read when they are called, so a new one each render does not rebind.\n const pick = typeof options.pick === 'function' ? 'function' : (options.pick ?? 'node');\n const transforms = !!options.onTransform;\n const { trigger, hover, marquee, labels, snap, color } = options;\n\n useEffect(() => {\n if (!canvas) return;\n const bound = bindEditor(canvas, {\n pick:\n pick === 'function'\n ? (element, at) => (latest.current.pick as PickFunction)(element, at)\n : pick,\n trigger,\n hover,\n marquee,\n labels,\n snap,\n color,\n label: (layer) =>\n latest.current.label ? latest.current.label(layer) : defaultLabel(canvas, layer),\n onChange(next) {\n setSelected(next);\n latest.current.onChange?.(next);\n },\n onTransform: transforms ? (event) => latest.current.onTransform?.(event) : undefined,\n });\n editor.current = bound;\n return () => {\n bound.destroy();\n editor.current = null;\n setSelected([]);\n };\n }, [canvas, pick, trigger, hover, marquee, labels, snap, color, transforms]);\n\n return {\n selected,\n set: (layers: Iterable<Element | Element[]>) => editor.current?.set(layers),\n clear: () => editor.current?.clear(),\n };\n}\n\ntype PickFunction = Extract<EditorPick, (...args: never[]) => unknown>;\n\n/** The editor's own label: the tag name inside nodes, nothing for nodes. */\nconst defaultLabel = (canvas: GpuCanvasInstance, layer: Layer) =>\n canvas.getNodeOf(layer[0]) === layer[0] ? null : layer[0].localName;\n\nexport interface ComponentInfo {\n /** The component's display name. Production builds usually minify it. */\n name: string;\n /** The DOM elements it renders at its top level: one, or several for a fragment. */\n elements: Element[];\n}\n\ninterface Fiber {\n tag: number;\n type: unknown;\n stateNode: unknown;\n return: Fiber | null;\n child: Fiber | null;\n sibling: Fiber | null;\n}\n\n// React's fiber tags for components: function, class, forwardRef, memo.\nconst COMPONENT_TAGS = new Set([0, 1, 11, 15]);\nconst PORTAL_TAG = 4;\n\nconst fiberOf = (element: Element): Fiber | null => {\n const key = Object.keys(element).find((name) => name.startsWith('__reactFiber$'));\n return key ? (element as unknown as Record<string, Fiber>)[key] : null;\n};\n\nconst nameOf = (type: unknown): string => {\n const t = type as {\n displayName?: string;\n name?: string;\n render?: { displayName?: string; name?: string };\n };\n return t.displayName || t.name || t.render?.displayName || t.render?.name || 'Anonymous';\n};\n\nconst topElements = (fiber: Fiber, out: Element[] = []) => {\n for (let child = fiber.child; child; child = child.sibling) {\n if (child.stateNode instanceof Element) out.push(child.stateNode);\n else if (child.tag !== PORTAL_TAG) topElements(child, out);\n }\n return out;\n};\n\n/**\n * The React components that rendered `element`, nearest first, up to the portal into its node\n * (`<CanvasNode>` renders one). It reads React's internal fibers, as click-to-component tools do,\n * so it can break with a React release.\n */\nexport function componentsOf(element: Element): ComponentInfo[] {\n const found: ComponentInfo[] = [];\n for (let fiber = fiberOf(element); fiber; fiber = fiber.return) {\n // Node content reaches its node through a portal; above it is the app around the canvas.\n if (fiber.tag === PORTAL_TAG) break;\n if (!COMPONENT_TAGS.has(fiber.tag)) continue;\n const elements = topElements(fiber);\n if (elements.length) found.push({ name: nameOf(fiber.type), elements });\n }\n return found;\n}\n\n/** The nearest React component that rendered `element`, or null. */\nexport const componentOf = (element: Element): ComponentInfo | null =>\n componentsOf(element)[0] ?? null;\n\n/**\n * A pick for `bindEditor` and `useEditor` that selects React components: a click lands on the\n * outermost component in the node, and each double-click goes one component deeper.\n */\nexport const pickComponents = (element: Element): Element[][] =>\n componentsOf(element)\n .reverse()\n .map((component) => component.elements);\n\n/** A label for components: the name of the outermost component a layer is the top of. */\nexport const labelComponent = (layer: Layer): string | null =>\n componentsOf(layer[0])\n .filter((component) => component.elements[0] === layer[0])\n .at(-1)?.name ?? null;\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/react.tsx"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport {\n bindEditor,\n createGpuCanvas,\n type Editor,\n type EditorOptions,\n type EditorPick,\n type Effect,\n type GpuCanvas as GpuCanvasInstance,\n type GpuCanvasOptions,\n type Layer,\n} from './index';\nimport { defaultLabel } from './editor';\n\nconst CanvasContext = createContext<GpuCanvasInstance | null>(null);\n\nexport interface GpuCanvasProps extends GpuCanvasOptions {\n children?: ReactNode;\n className?: string;\n style?: CSSProperties;\n onReady?: (canvas: GpuCanvasInstance) => void;\n}\n\n/** An infinite canvas. Children are `<CanvasNode>`s; they mount once the canvas is ready. */\nexport function GpuCanvas({ children, className, style, onReady, ...options }: GpuCanvasProps) {\n const hostRef = useRef<HTMLDivElement>(null);\n const [canvas, setCanvas] = useState<GpuCanvasInstance | null>(null);\n const latest = useRef({ options, onReady });\n latest.current = { options, onReady };\n\n useEffect(() => {\n let disposed = false;\n let created: GpuCanvasInstance | null = null;\n createGpuCanvas(hostRef.current!, {\n ...latest.current.options,\n // The callback from the latest render, not the first.\n onCameraChange: (camera) => latest.current.options.onCameraChange?.(camera),\n }).then(\n (instance) => {\n if (disposed) return instance.destroy();\n created = instance;\n setCanvas(instance);\n latest.current.onReady?.(instance);\n },\n (error) => console.error('gpucanvas: the canvas failed to start', error),\n );\n return () => {\n disposed = true;\n created?.destroy();\n setCanvas(null);\n };\n // The mode and GPU resources are fixed for the canvas's lifetime; remount to change them.\n }, []);\n\n // The canvas starts with the first background; later changes to the prop are applied.\n const { background } = options;\n const shownBackground = useRef(background);\n useEffect(() => {\n if (!canvas || background === undefined || background === shownBackground.current) return;\n shownBackground.current = background;\n canvas.setBackground(background);\n }, [canvas, background]);\n\n return (\n <div ref={hostRef} className={className} style={{ position: 'relative', ...style }}>\n <CanvasContext.Provider value={canvas}>{canvas && children}</CanvasContext.Provider>\n </div>\n );\n}\n\nexport interface CanvasNodeProps {\n x: number;\n y: number;\n width: number;\n height: number;\n children?: ReactNode;\n className?: string;\n style?: CSSProperties;\n /** A shader the node is drawn through, such as `effects.glow`. */\n effect?: Effect | null;\n /** Up to four parameters for `effect`; its defaults otherwise. */\n effectParams?: readonly number[];\n}\n\n/** A live DOM node placed on the canvas in world units. Events, focus and state work as usual. */\nexport function CanvasNode({\n x,\n y,\n width,\n height,\n children,\n className,\n style,\n effect = null,\n effectParams,\n}: CanvasNodeProps) {\n const canvas = useContext(CanvasContext);\n if (!canvas) throw new Error('<CanvasNode> must be inside <GpuCanvas>');\n const [element] = useState(() => document.createElement('div'));\n\n useLayoutEffect(() => {\n canvas.addNode(element, { x, y, width, height });\n return () => canvas.removeNode(element);\n // Position changes go through updateNode below without remounting.\n }, [canvas, element]);\n\n const mounted = useRef(false);\n useLayoutEffect(() => {\n if (mounted.current) canvas.updateNode(element, { x, y, width, height });\n mounted.current = true;\n }, [canvas, element, x, y, width, height]);\n\n useLayoutEffect(() => {\n element.className = className ?? '';\n }, [element, className]);\n\n const paramsKey = effectParams?.join(',');\n useLayoutEffect(() => {\n canvas.setEffect(element, effect, effectParams);\n // The parameters' values, not the array's identity, decide a redraw.\n }, [canvas, element, effect, paramsKey]);\n\n return createPortal(\n <div style={{ width: '100%', height: '100%', ...style }}>{children}</div>,\n element,\n );\n}\n\nexport const useGpuCanvas = () => useContext(CanvasContext);\n\n/**\n * The editor (`bindEditor`) on the canvas around it, with its selection as React state. Use it\n * inside `<GpuCanvas>`; `onChange` lifts the selection out.\n */\nexport function useEditor(options: EditorOptions = {}) {\n const canvas = useGpuCanvas();\n const [selected, setSelected] = useState<Layer[]>([]);\n const editor = useRef<Editor | null>(null);\n const latest = useRef(options);\n latest.current = options;\n // Functions are read when they are called, so a new one each render does not rebind.\n const pick = typeof options.pick === 'function' ? 'function' : (options.pick ?? 'node');\n const transforms = !!options.onTransform;\n const { trigger, hover, marquee, labels, snap, color } = options;\n\n useEffect(() => {\n if (!canvas) return;\n const bound = bindEditor(canvas, {\n pick:\n pick === 'function'\n ? (element, at) => (latest.current.pick as PickFunction)(element, at)\n : pick,\n trigger,\n hover,\n marquee,\n labels,\n snap,\n color,\n label: (layer) => (latest.current.label ?? defaultLabel(canvas))(layer),\n onChange(next) {\n setSelected(next);\n latest.current.onChange?.(next);\n },\n onTransform: transforms ? (event) => latest.current.onTransform?.(event) : undefined,\n });\n editor.current = bound;\n return () => {\n bound.destroy();\n editor.current = null;\n setSelected([]);\n };\n }, [canvas, pick, trigger, hover, marquee, labels, snap, color, transforms]);\n\n return {\n selected,\n set: (layers: Iterable<Element | Element[]>) => editor.current?.set(layers),\n clear: () => editor.current?.clear(),\n };\n}\n\ntype PickFunction = Extract<EditorPick, (...args: never[]) => unknown>;\n\nexport interface ComponentInfo {\n /** The component's display name. Production builds usually minify it. */\n name: string;\n /** The DOM elements it renders at its top level: one, or several for a fragment. */\n elements: Element[];\n}\n\ninterface Fiber {\n tag: number;\n type: unknown;\n stateNode: unknown;\n return: Fiber | null;\n child: Fiber | null;\n sibling: Fiber | null;\n}\n\n// React's fiber tags for components: function, class, forwardRef, memo.\nconst COMPONENT_TAGS = new Set([0, 1, 11, 15]);\nconst PORTAL_TAG = 4;\n\nconst fiberOf = (element: Element): Fiber | null => {\n const key = Object.keys(element).find((name) => name.startsWith('__reactFiber$'));\n return key ? (element as unknown as Record<string, Fiber>)[key] : null;\n};\n\nconst nameOf = (type: unknown): string => {\n const t = type as {\n displayName?: string;\n name?: string;\n render?: { displayName?: string; name?: string };\n };\n return t.displayName || t.name || t.render?.displayName || t.render?.name || 'Anonymous';\n};\n\nconst topElements = (fiber: Fiber, out: Element[] = []) => {\n for (let child = fiber.child; child; child = child.sibling) {\n if (child.stateNode instanceof Element) out.push(child.stateNode);\n else if (child.tag !== PORTAL_TAG) topElements(child, out);\n }\n return out;\n};\n\n/**\n * The React components that rendered `element`, nearest first, up to the portal into its node\n * (`<CanvasNode>` renders one). It reads React's internal fibers, as click-to-component tools do,\n * so it can break with a React release.\n */\nexport function componentsOf(element: Element): ComponentInfo[] {\n const found: ComponentInfo[] = [];\n for (let fiber = fiberOf(element); fiber; fiber = fiber.return) {\n // Node content reaches its node through a portal; above it is the app around the canvas.\n if (fiber.tag === PORTAL_TAG) break;\n if (!COMPONENT_TAGS.has(fiber.tag)) continue;\n const elements = topElements(fiber);\n if (elements.length) found.push({ name: nameOf(fiber.type), elements });\n }\n return found;\n}\n\n/** The nearest React component that rendered `element`, or null. */\nexport const componentOf = (element: Element): ComponentInfo | null =>\n componentsOf(element)[0] ?? null;\n\n/**\n * A pick for `bindEditor` and `useEditor` that selects React components: a click lands on the\n * outermost component in the node, and each double-click goes one component deeper.\n */\nexport const pickComponents = (element: Element): Element[][] =>\n componentsOf(element)\n .reverse()\n .map((component) => component.elements);\n\n/** A label for components: the name of the outermost component a layer is the top of. */\nexport const labelComponent = (layer: Layer): string | null =>\n componentsOf(layer[0])\n .filter((component) => component.elements[0] === layer[0])\n .at(-1)?.name ?? null;\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gpucanvas",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "Put thousands of live DOM nodes on an infinite canvas: the GPU draws them, an editor selects, moves and resizes them, shaders and transitions run on them, and an AI agent can read them and point at them.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"canvas",
|