gpucanvas 0.4.4 → 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.
@@ -157,7 +157,10 @@ interface GpuCanvas {
157
157
  removeNode(element: HTMLElement): void;
158
158
  /** Where `element` sits on the canvas, in world units, or null when it is not a node. */
159
159
  getNodeRect(element: HTMLElement): Rect | null;
160
- /** Nodes that overlap `rect` (world units), such as the ones under a selection box. */
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
+ */
161
164
  getNodesIn(rect: Rect): HTMLElement[];
162
165
  /** The node that holds `element`: the element itself when it is a node, or null. */
163
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-B5nVs54L.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-B5nVs54L.js';
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
@@ -16,7 +16,7 @@ import {
16
16
  toBlob,
17
17
  worldToScreen,
18
18
  zoomAt
19
- } from "./chunk-IJ3R6X75.js";
19
+ } from "./chunk-IIRGNX76.js";
20
20
  export {
21
21
  animateEffect,
22
22
  bindEditor,
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-B5nVs54L.js';
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
@@ -3,7 +3,7 @@ import {
3
3
  bindEditor,
4
4
  createGpuCanvas,
5
5
  defaultLabel
6
- } from "./chunk-IJ3R6X75.js";
6
+ } from "./chunk-IIRGNX76.js";
7
7
 
8
8
  // src/react.tsx
9
9
  import {
@@ -25,18 +25,32 @@ function GpuCanvas({ children, className, style, onReady, ...options }) {
25
25
  useEffect(() => {
26
26
  let disposed = false;
27
27
  let created = null;
28
- createGpuCanvas(hostRef.current, latest.current.options).then((instance) => {
29
- if (disposed) return instance.destroy();
30
- created = instance;
31
- setCanvas(instance);
32
- latest.current.onReady?.(instance);
33
- });
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
+ );
34
41
  return () => {
35
42
  disposed = true;
36
43
  created?.destroy();
37
44
  setCanvas(null);
38
45
  };
39
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]);
40
54
  return /* @__PURE__ */ jsx("div", { ref: hostRef, className, style: { position: "relative", ...style }, children: /* @__PURE__ */ jsx(CanvasContext.Provider, { value: canvas, children: canvas && children }) });
41
55
  }
42
56
  function CanvasNode({
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';\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!, 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) => (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":[]}
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.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",