@octane-xplat/canvas 0.14.0 → 0.15.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/README.md CHANGED
@@ -30,7 +30,7 @@ export function Drawing() {
30
30
  Pass `context` to have the leaf resolve it before `onReady`, or omit it and
31
31
  call `getContext` yourself — one kind per canvas, decided on first call.
32
32
  `onReady` fires after mount on web and on the plugin's `ready` event on
33
- native (earlier `getContext` calls can return null). For WebGPU,
33
+ iOS/Android (earlier `getContext` calls can return null). For WebGPU,
34
34
  `getGPU()` normalizes adapter/device acquisition across targets.
35
35
 
36
36
  ```tsx
@@ -51,6 +51,50 @@ const adapter = await gpu?.requestAdapter()
51
51
  const device = await adapter?.requestDevice()
52
52
  ```
53
53
 
54
+ ## Keep a fallback when drawing is unavailable
55
+
56
+ Native macOS/AppKit imports and mounts safely, but drawing is unsupported.
57
+ `onReady` reports `context: null` after a neutral frame mounts; it does not
58
+ announce a drawing surface. Its unavailable `canvas` handle returns `null`
59
+ from `getContext()` for every kind, including `2d`, `webgl`, `webgl2`,
60
+ `webgpu`, and `bitmaprenderer`. `getGPU()` also returns `null`, so no adapter
61
+ or device can be acquired. Check these results before drawing and keep your
62
+ own static fallback. For example, put this component in your app:
63
+
64
+ ```tsx
65
+ import { Canvas, getGPU } from '@octane-xplat/canvas'
66
+ import { Text, View } from '@octane-xplat/ui'
67
+ import { useState } from 'octane'
68
+
69
+ export function DrawingWithFallback() {
70
+ const [drawn, setDrawn] = useState(false)
71
+ return (
72
+ <View>
73
+ {!drawn && <Text>Blue square illustration</Text>}
74
+ <Canvas
75
+ context="2d"
76
+ width={80}
77
+ height={80}
78
+ onReady={({ canvas, context }) => {
79
+ const drawing = context ?? canvas.getContext('2d')
80
+ if (!drawing) return
81
+ drawing.fillStyle = '#2563eb'
82
+ drawing.fillRect(0, 0, 80, 80)
83
+ setDrawn(true)
84
+ }}
85
+ />
86
+ </View>
87
+ )
88
+ }
89
+
90
+ // AppKit returns null here, before adapter/device acquisition.
91
+ const gpu = getGPU()
92
+ ```
93
+
94
+ The AppKit backend for 2D, WebGL, and WebGPU remains deferred until a concrete
95
+ consumer needs one and the implementation is approved. Native desktop shader
96
+ effects remain unsupported. A DOM webview uses the separate web implementation.
97
+
54
98
  Per-target limits are recorded in
55
99
  [known limits](../../docs/verify/known-limits.md). Exercised by
56
100
  [`CanvasDemo`](../demos/src/CanvasDemo.tsrx) (including a WGSL path).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octane-xplat/canvas",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Canvas/GPU surface for Octane xplat — web <canvas> + @nativescript/canvas (2D, WebGL/WebGL2, WebGPU/WGSL)",
5
5
  "keywords": [
6
6
  "canvas",
@@ -23,17 +23,19 @@
23
23
  "directory": "packages/canvas"
24
24
  },
25
25
  "files": [
26
- "dist",
27
26
  "types",
28
- "src"
27
+ "src",
28
+ "!src/**/*.test.*",
29
+ "!src/**/*.spec.*"
29
30
  ],
30
31
  "type": "module",
31
32
  "exports": {
32
33
  ".": {
33
34
  "types": "./types/index.d.ts",
34
- "web": "./dist/web/index.js",
35
- "native": "./dist/native/index.js",
36
- "default": "./dist/native/index.js"
35
+ "web": "./src/index.web.ts",
36
+ "macos": "./src/index.macos.ts",
37
+ "native": "./src/index.ts",
38
+ "default": "./src/index.ts"
37
39
  }
38
40
  },
39
41
  "publishConfig": {
@@ -43,17 +45,19 @@
43
45
  "@nativescript/canvas": "3.0.0-alpha.15"
44
46
  },
45
47
  "devDependencies": {
46
- "@nativescript-community/octane": "0.2.4",
48
+ "@nativescript-community/octane": "0.2.5",
47
49
  "@nativescript/core": "9.1.3",
50
+ "@octane-xplat/macos-renderer": "0.15.0",
48
51
  "@octanejs/vite-plugin": "0.1.61",
49
52
  "octane": "0.11.0",
50
- "tsrx-typegen": "0.2.2",
53
+ "tsrx-typegen": "0.3.0",
51
54
  "typescript": "5.9.3",
52
55
  "vite": "8.3.0"
53
56
  },
54
57
  "peerDependencies": {
55
58
  "@nativescript-community/octane": ">=0.2.1 <1",
56
59
  "@nativescript/core": ">=9.1.0 <10",
60
+ "@octane-xplat/macos-renderer": "0.15.0",
57
61
  "octane": ">=0.6.3 <1"
58
62
  },
59
63
  "peerDependenciesMeta": {
@@ -62,12 +66,15 @@
62
66
  },
63
67
  "@nativescript/core": {
64
68
  "optional": true
69
+ },
70
+ "@octane-xplat/macos-renderer": {
71
+ "optional": true
65
72
  }
66
73
  },
67
74
  "scripts": {
68
- "build": "vite build && vite build --mode native && pnpm typegen && node ../../scripts/check-native-dist.mjs",
69
75
  "typegen": "tsc -p tsconfig.types.json",
70
76
  "test:packed": "node tests/packed-consumer.mjs",
77
+ "test:macos-runtime": "node ../../scripts/probe.mjs run tests/unsupported.macos.tsrx --target macos --deps @octane-xplat/canvas",
71
78
  "pack:check": "tsrx-typegen --pack-check"
72
79
  }
73
80
  }
@@ -0,0 +1,23 @@
1
+ /** @jsxImportSource @octane-xplat/macos-renderer */
2
+ import { useEffect } from 'octane';
3
+ import type { CanvasContextKind, CanvasProps } from './props';
4
+
5
+ /** AppKit mounts a neutral frame. No drawing surface or GPU is created. */
6
+ export function Canvas(props: CanvasProps) @{
7
+ useEffect(() => {
8
+ // Preserve deferred acquisition without pretending this is a native view.
9
+ const canvas = Object.freeze({
10
+ getContext: (_kind: CanvasContextKind, _options?: any): null => null,
11
+ });
12
+
13
+ props.onReady?.({ canvas, context: null });
14
+ }, []);
15
+
16
+ <gridlayout
17
+ id={props.id}
18
+ className={props.className}
19
+ style={props.style}
20
+ width={props.width}
21
+ height={props.height}
22
+ />;
23
+ }
@@ -1,23 +1,39 @@
1
+ import { useMemo, useRef } from 'octane';
1
2
  import type { CanvasProps } from './props';
2
3
 
3
4
  /** Canvas — a plain <canvas>. `onReady` fires at mount with the element
4
5
  * and the resolved context. */
5
6
  export function Canvas(props: CanvasProps) @{
6
- <canvas
7
- id={props.id}
8
- className={props.className}
9
- style={props.style}
10
- width={typeof props.width === 'number' ? props.width : undefined}
11
- height={typeof props.height === 'number' ? props.height : undefined}
12
- ref={(el: HTMLCanvasElement | null) => {
7
+ // Keep the element callback referentially stable — a fresh closure each
8
+ // render makes the web renderer detach/reattach it, refiring onReady.
9
+ const propsRef = useRef(props);
10
+ propsRef.current = props;
11
+ const canvasRef = useMemo(
12
+ () => (el: HTMLCanvasElement | null) => {
13
13
  if (!el) {
14
14
  return;
15
15
  }
16
16
 
17
- props.onReady?.({
17
+ propsRef.current.onReady?.({
18
18
  canvas: el,
19
- context: props.context ? el.getContext(props.context) : null,
19
+ context: propsRef.current.context
20
+ ? el.getContext(propsRef.current.context)
21
+ : null,
20
22
  });
23
+ },
24
+ [],
25
+ );
26
+
27
+ <canvas
28
+ id={props.id}
29
+ className={props.className}
30
+ style={{
31
+ width: typeof props.width === 'string' ? props.width : undefined,
32
+ height: typeof props.height === 'string' ? props.height : undefined,
33
+ ...props.style,
21
34
  }}
35
+ width={typeof props.width === 'number' ? props.width : undefined}
36
+ height={typeof props.height === 'number' ? props.height : undefined}
37
+ ref={canvasRef}
22
38
  />
23
39
  }
@@ -0,0 +1,4 @@
1
+ /** AppKit has no Canvas WebGPU backend. Returns null before device acquisition. */
2
+ export function getGPU(): any {
3
+ return null
4
+ }
@@ -0,0 +1,3 @@
1
+ export { Canvas } from './Canvas.macos.tsrx'
2
+ export { getGPU } from './gpu.macos'
3
+ export type { CanvasProps, CanvasContextKind, CanvasReady } from './props'
package/src/props.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  export type CanvasContextKind = '2d' | 'webgl' | 'webgl2' | 'webgpu' | 'bitmaprenderer'
2
2
 
3
3
  export interface CanvasReady {
4
- /** HTMLCanvasElement on web, the plugin's Canvas view on native —
5
- * both expose `getContext()`. */
4
+ /** HTMLCanvasElement on web, the plugin's Canvas view on iOS/Android.
5
+ * AppKit supplies an unavailable handle whose `getContext()` always
6
+ * returns null; it is not a native drawing surface. */
6
7
  canvas: any
7
8
  /** The resolved context for the `context` prop, or null when that
8
9
  * kind is unsupported/unavailable. WebGPU still needs a device —
@@ -17,8 +18,10 @@ export interface CanvasProps {
17
18
  /** Context kind resolved before `onReady` fires. Omit to defer
18
19
  * `getContext` to user code (only one kind per canvas — call once). */
19
20
  context?: CanvasContextKind
20
- /** Web: after mount. Native: the plugin's `ready` event (fires when the
21
- * GPU surface exists — earlier `getContext` calls can return null). */
21
+ /** Web: after mount. iOS/Android: the plugin's `ready` event (earlier
22
+ * `getContext` calls can return null). AppKit: after the neutral frame
23
+ * mounts, with context null. This callback does not guarantee drawing
24
+ * or GPU readiness; check context or call canvas.getContext first. */
22
25
  onReady?: (ready: CanvasReady) => void
23
26
  /** Numeric values map to the element's width/height — on web that is
24
27
  * the canvas *drawing buffer* size (pair with style for display
package/types/index.d.ts CHANGED
@@ -7,7 +7,8 @@ import type { CanvasProps } from './props.js'
7
7
  export declare const Canvas: UniversalComponent<CanvasProps>
8
8
 
9
9
  /** Returns the WebGPU entry point — `navigator.gpu` on web, the plugin's
10
- * GPU shim on native — or null where WebGPU is unavailable. */
10
+ * GPU shim on iOS/Android — or null where WebGPU is unavailable.
11
+ * Native AppKit always returns null. */
11
12
  export declare function getGPU(): any
12
13
 
13
14
  export type { CanvasProps, CanvasContextKind, CanvasReady } from './props.js'
package/types/props.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  export type CanvasContextKind = '2d' | 'webgl' | 'webgl2' | 'webgpu' | 'bitmaprenderer';
2
2
  export interface CanvasReady {
3
- /** HTMLCanvasElement on web, the plugin's Canvas view on native —
4
- * both expose `getContext()`. */
3
+ /** HTMLCanvasElement on web, the plugin's Canvas view on iOS/Android.
4
+ * AppKit supplies an unavailable handle whose `getContext()` always
5
+ * returns null; it is not a native drawing surface. */
5
6
  canvas: any;
6
7
  /** The resolved context for the `context` prop, or null when that
7
8
  * kind is unsupported/unavailable. WebGPU still needs a device —
@@ -15,8 +16,10 @@ export interface CanvasProps {
15
16
  /** Context kind resolved before `onReady` fires. Omit to defer
16
17
  * `getContext` to user code (only one kind per canvas — call once). */
17
18
  context?: CanvasContextKind;
18
- /** Web: after mount. Native: the plugin's `ready` event (fires when the
19
- * GPU surface exists — earlier `getContext` calls can return null). */
19
+ /** Web: after mount. iOS/Android: the plugin's `ready` event (earlier
20
+ * `getContext` calls can return null). AppKit: after the neutral frame
21
+ * mounts, with context null. This callback does not guarantee drawing
22
+ * or GPU readiness; check context or call canvas.getContext first. */
20
23
  onReady?: (ready: CanvasReady) => void;
21
24
  /** Numeric values map to the element's width/height — on web that is
22
25
  * the canvas *drawing buffer* size (pair with style for display
@@ -1,35 +0,0 @@
1
- import { defineUniversalComponent, registerElement, universalPlan, universalValue } from "@nativescript-community/octane";
2
- import { Canvas } from "@nativescript/canvas";
3
- //#region src/Canvas.tsrx
4
- var __octaneUniversalPlan0 = universalPlan("nativescript", {
5
- "kind": "host",
6
- "type": "nscanvas",
7
- "bindings": [
8
- ["id", 0],
9
- ["className", 1],
10
- ["style", 2],
11
- ["width", 3],
12
- ["height", 4],
13
- ["onReady", 5]
14
- ]
15
- });
16
- registerElement("nscanvas", Canvas);
17
- var Canvas$1 = defineUniversalComponent("nativescript", function Canvas(props) {
18
- const ready = (event) => {
19
- const canvas = event.object;
20
- props.onReady?.({
21
- canvas,
22
- context: props.context ? canvas.getContext(props.context) : null
23
- });
24
- };
25
- return universalValue(__octaneUniversalPlan0, [
26
- props.id,
27
- props.className,
28
- props.style,
29
- props.width,
30
- props.height,
31
- ready
32
- ]);
33
- }, { "module": "@nativescript-community/octane" });
34
- //#endregion
35
- export { Canvas$1 as Canvas };
@@ -1,10 +0,0 @@
1
- import { GPU } from "@nativescript/canvas";
2
- //#region src/gpu.ts
3
- /** getGPU — returns the plugin's GPU shim (wgpu over Metal/Vulkan). Same
4
- * spec-shaped surface as web's `navigator.gpu`: `requestAdapter()` +
5
- * `getPreferredCanvasFormat()`. */
6
- function getGPU() {
7
- return new GPU();
8
- }
9
- //#endregion
10
- export { getGPU };
@@ -1,3 +0,0 @@
1
- import { Canvas } from "./Canvas.tsrx.js";
2
- import { getGPU } from "./gpu.js";
3
- export { Canvas, getGPU };
@@ -1,8 +0,0 @@
1
- {
2
- "version": 1,
3
- "buildId": "1e4d34bf-eb79-4940-8b5c-3b5fef84dd1e",
4
- "mode": "production",
5
- "capabilities": {
6
- "independentHydration": false
7
- }
8
- }
@@ -1,48 +0,0 @@
1
- import { __s, bag7, clone, queueRefAttach, queueRefDetach, setStyle, template } from "octane";
2
- import { replaceRef, setClassNameIfChanged, setPlainAttributeIfChanged } from "octane/internal/client";
3
- //#region src/Canvas.web.tsrx
4
- var _t$0 = /* @__PURE__ */ template("<canvas></canvas>");
5
- var _rm$0 = [
6
- "r",
7
- "g",
8
- "b"
9
- ];
10
- var Canvas = /* @__PURE__ */ __s(function Canvas(props, __s, __extra) {
11
- let _b = __s.slots[0];
12
- if (_b === void 0) {
13
- let _m6;
14
- const _root = clone(_t$0);
15
- queueRefAttach(__s, _m6 = (el) => {
16
- if (!el) return;
17
- props.onReady?.({
18
- canvas: el,
19
- context: props.context ? el.getContext(props.context) : null
20
- });
21
- }, _root);
22
- (__s.cleanups ??= []).push(() => _b && queueRefDetach(_b.g, _b.b));
23
- _b = bag7(__s, _root, void 0, _root, void 0, void 0, void 0, void 0, _m6);
24
- __s.refFields = _rm$0;
25
- } else {
26
- const _r = (el) => {
27
- if (!el) return;
28
- props.onReady?.({
29
- canvas: el,
30
- context: props.context ? el.getContext(props.context) : null
31
- });
32
- };
33
- if (_r !== _b.g) _b.g = replaceRef(__s, _b.g, _r, _b.b);
34
- }
35
- _b.a = setPlainAttributeIfChanged(props.id, _b.a, _b.b, "id");
36
- _b.c = setClassNameIfChanged(props.className, _b.c, _b.b);
37
- {
38
- const _v = props.style;
39
- if (_b.d !== _v) {
40
- setStyle(_b.b, _v, _b.d);
41
- _b.d = _v;
42
- }
43
- }
44
- _b.e = setPlainAttributeIfChanged(typeof props.width === "number" ? props.width : void 0, _b.e, _b.b, "width");
45
- _b.f = setPlainAttributeIfChanged(typeof props.height === "number" ? props.height : void 0, _b.f, _b.b, "height");
46
- });
47
- //#endregion
48
- export { Canvas };
@@ -1,10 +0,0 @@
1
- //#region src/gpu.web.ts
2
- /** getGPU — returns the WebGPU entry point (`navigator.gpu`) or null when
3
- * WebGPU is unavailable (insecure context, unsupported browser). The
4
- * returned object is spec-shaped: `requestAdapter()` +
5
- * `getPreferredCanvasFormat()`. */
6
- function getGPU() {
7
- return globalThis.navigator?.gpu ?? null;
8
- }
9
- //#endregion
10
- export { getGPU };
package/dist/web/index.js DELETED
@@ -1,3 +0,0 @@
1
- import { Canvas } from "./Canvas.web.tsrx.js";
2
- import { getGPU } from "./gpu.web.js";
3
- export { Canvas, getGPU };
@@ -1,8 +0,0 @@
1
- {
2
- "version": 1,
3
- "buildId": "2d603450-70e6-42b6-9dd0-f2c0cd432389",
4
- "mode": "production",
5
- "capabilities": {
6
- "independentHydration": false
7
- }
8
- }