@jiujue/react-canvas-fiber 2.0.4 → 2.0.6
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 +89 -6
- package/README.zh.md +89 -6
- package/dist/index.cjs +50 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +50 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# react-canvas-fiber
|
|
2
2
|
|
|
3
|
-
[中文](./README.zh.md
|
|
3
|
+
[中文](./README.zh.md) | English
|
|
4
4
|
|
|
5
5
|
A Canvas custom renderer (2D) based on `react-reconciler`, integrated with Yoga Flexbox layout.
|
|
6
6
|
|
|
@@ -19,7 +19,7 @@ Inside this monorepo, it is referenced via pnpm workspace (see the demo).
|
|
|
19
19
|
## Usage
|
|
20
20
|
|
|
21
21
|
```tsx
|
|
22
|
-
import { Canvas, Rect, Text, View } from '@jiujue/react-canvas-fiber'
|
|
22
|
+
import { Canvas, Image, Rect, Text, View } from '@jiujue/react-canvas-fiber'
|
|
23
23
|
|
|
24
24
|
export function Example() {
|
|
25
25
|
return (
|
|
@@ -27,17 +27,99 @@ export function Example() {
|
|
|
27
27
|
<View style={{ width: 600, height: 400, padding: 16, flexDirection: 'column', gap: 12 }}>
|
|
28
28
|
<Text text="Hello" style={{ fontSize: 24, fontWeight: 700 }} color="#e6edf7" />
|
|
29
29
|
<Rect style={{ width: 180, height: 44 }} borderRadius={10} fill="#2b6cff" />
|
|
30
|
+
<Image
|
|
31
|
+
src="https://gw.alipayobjects.com/zos/antfincdn/LlvErxo8H9/photo-1503149779833-1de50ebe5f8a.webp"
|
|
32
|
+
style={{ width: 100, height: 100 }}
|
|
33
|
+
borderRadius={12}
|
|
34
|
+
objectFit="cover"
|
|
35
|
+
/>
|
|
30
36
|
</View>
|
|
31
37
|
</Canvas>
|
|
32
38
|
)
|
|
33
39
|
}
|
|
34
40
|
```
|
|
35
41
|
|
|
36
|
-
##
|
|
42
|
+
## Components
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
### `Canvas`
|
|
45
|
+
|
|
46
|
+
Bridge component that owns a DOM `<canvas>`, creates the custom renderer root, and forwards pointer/wheel events.
|
|
47
|
+
|
|
48
|
+
Common props:
|
|
49
|
+
|
|
50
|
+
- `width: number` / `height: number`: logical size (CSS pixels)
|
|
51
|
+
- `dpr?: number` (default `1`): device pixel ratio; the backing canvas uses `width * dpr`, `height * dpr`
|
|
52
|
+
- `clearColor?: string`: clears the whole canvas each frame
|
|
53
|
+
- `fontFamily?: string`, `fontSize?: number`, `fontWeight?: number | string`, `lineHeight?: number`: default text style (used when `Text`/ancestors don't provide them)
|
|
54
|
+
- `style?: CSSProperties`: applied to the DOM `<canvas>` (logical size, display, etc.)
|
|
55
|
+
- `children?: ReactNode`: scene tree
|
|
56
|
+
|
|
57
|
+
### `View`
|
|
58
|
+
|
|
59
|
+
Layout container (Yoga) + optional background/border + optional scrolling.
|
|
60
|
+
|
|
61
|
+
Common props:
|
|
62
|
+
|
|
63
|
+
- `style?: YogaStyle`: layout box; see Layout section below
|
|
64
|
+
- `background?: string`: background fill color
|
|
65
|
+
- `border?: string`: CSS-like border. Supported forms:
|
|
66
|
+
- `"<number>px solid <color>"` (e.g. `1px solid rgba(255,255,255,0.2)`)
|
|
67
|
+
- `"<number> <color>"` (e.g. `2 #fff`)
|
|
68
|
+
- `"<color>"` (means `1px`, e.g. `#fff`)
|
|
69
|
+
- `borderRadius?: number`
|
|
70
|
+
- `scrollX?: boolean`, `scrollY?: boolean`: enable scrolling + clipping
|
|
71
|
+
- `scrollbarX?: boolean`, `scrollbarY?: boolean`: show scrollbars (default `true` when scrolling is enabled)
|
|
72
|
+
- `scrollbarWidth?: number` (default `10`)
|
|
73
|
+
- `scrollbarInset?: number` (default `6`)
|
|
74
|
+
- `scrollbarTrackColor?: string` (default `rgba(255,255,255,0.12)`)
|
|
75
|
+
- `scrollbarThumbColor?: string` (default `rgba(255,255,255,0.35)`)
|
|
76
|
+
- `onScrollX?: (scrollLeft: number) => void`
|
|
77
|
+
- `onScroll?: (scrollTop: number) => void`
|
|
78
|
+
|
|
79
|
+
### `Rect`
|
|
80
|
+
|
|
81
|
+
Rounded rectangle primitive (can also contain children).
|
|
82
|
+
|
|
83
|
+
Common props:
|
|
84
|
+
|
|
85
|
+
- `style?: YogaStyle`
|
|
86
|
+
- `fill?: string` (default `#ffffff`)
|
|
87
|
+
- `stroke?: string`
|
|
88
|
+
- `lineWidth?: number` (default `1`)
|
|
89
|
+
- `borderRadius?: number`
|
|
90
|
+
|
|
91
|
+
### `Text`
|
|
92
|
+
|
|
93
|
+
Text primitive. Use `text`, do not pass children.
|
|
94
|
+
|
|
95
|
+
Common props:
|
|
96
|
+
|
|
97
|
+
- `text: string` (required). `\n` is supported for manual line breaks.
|
|
98
|
+
- `style?: YogaStyle`: supports `fontSize/fontFamily/fontWeight/lineHeight` (also inherited from ancestors / `Canvas` defaults)
|
|
99
|
+
- `color?: string` (default `#ffffff`)
|
|
100
|
+
- `maxWidth?: number`: used by layout measurement to clamp measured width (does not auto-wrap during drawing)
|
|
101
|
+
|
|
102
|
+
### `Image`
|
|
103
|
+
|
|
104
|
+
Image primitive. Use `src`, do not pass children.
|
|
105
|
+
|
|
106
|
+
Common props:
|
|
107
|
+
|
|
108
|
+
- `src: string` (required)
|
|
109
|
+
- `style?: YogaStyle`
|
|
110
|
+
- `objectFit?: 'cover' | 'contain' | 'fill'` (default `contain`)
|
|
111
|
+
- `borderRadius?: number`
|
|
112
|
+
|
|
113
|
+
## Events
|
|
114
|
+
|
|
115
|
+
`View` / `Rect` / `Text` / `Image` support pointer events:
|
|
116
|
+
|
|
117
|
+
- Hit test respects `pointerEvents?: 'auto' | 'none'` (`none` makes the node transparent to hit testing).
|
|
118
|
+
- Bubble + capture events: `onPointerDown/Move/Up/Cancel`, `onClick`, plus `*Capture` variants.
|
|
119
|
+
- Hover events: `onPointerEnter`, `onPointerLeave` (no capture/bubble).
|
|
120
|
+
- `click` fires on `pointerup` when the down/up target is the same node and `button === 0`.
|
|
121
|
+
|
|
122
|
+
If an event handler calls `event.preventDefault()`, the DOM event will be `preventDefault()`'d on the underlying `<canvas>` when possible.
|
|
41
123
|
|
|
42
124
|
## Layout (Yoga Subset)
|
|
43
125
|
|
|
@@ -47,6 +129,7 @@ Provided mainly via `style`:
|
|
|
47
129
|
- Flex: `flexDirection/justifyContent/alignItems/flexGrow/flexShrink/flexBasis/flexWrap/gap`
|
|
48
130
|
- Spacing: `padding*`, `margin*`
|
|
49
131
|
- Positioning: `position/top/right/bottom/left`
|
|
132
|
+
- Text style (used by `Text`): `fontSize/fontFamily/fontWeight/lineHeight`
|
|
50
133
|
|
|
51
134
|
## Implementation Notes
|
|
52
135
|
|
package/README.zh.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# react-canvas-fiber
|
|
2
2
|
|
|
3
|
-
中文 | [English](./README.
|
|
3
|
+
中文 | [English](./README.md)
|
|
4
4
|
|
|
5
5
|
基于 `react-reconciler` 的 Canvas 自定义渲染器(2D),并集成 Yoga Flexbox 布局。
|
|
6
6
|
|
|
@@ -19,7 +19,7 @@ pnpm add @jiujue/react-canvas-fiber
|
|
|
19
19
|
## 使用
|
|
20
20
|
|
|
21
21
|
```tsx
|
|
22
|
-
import { Canvas, Rect, Text, View } from '@jiujue/react-canvas-fiber'
|
|
22
|
+
import { Canvas, Image, Rect, Text, View } from '@jiujue/react-canvas-fiber'
|
|
23
23
|
|
|
24
24
|
export function Example() {
|
|
25
25
|
return (
|
|
@@ -27,17 +27,99 @@ export function Example() {
|
|
|
27
27
|
<View style={{ width: 600, height: 400, padding: 16, flexDirection: 'column', gap: 12 }}>
|
|
28
28
|
<Text text="Hello" style={{ fontSize: 24, fontWeight: 700 }} color="#e6edf7" />
|
|
29
29
|
<Rect style={{ width: 180, height: 44 }} borderRadius={10} fill="#2b6cff" />
|
|
30
|
+
<Image
|
|
31
|
+
src="https://gw.alipayobjects.com/zos/antfincdn/LlvErxo8H9/photo-1503149779833-1de50ebe5f8a.webp"
|
|
32
|
+
style={{ width: 100, height: 100 }}
|
|
33
|
+
borderRadius={12}
|
|
34
|
+
objectFit="cover"
|
|
35
|
+
/>
|
|
30
36
|
</View>
|
|
31
37
|
</Canvas>
|
|
32
38
|
)
|
|
33
39
|
}
|
|
34
40
|
```
|
|
35
41
|
|
|
36
|
-
##
|
|
42
|
+
## 组件与属性
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
### `Canvas`
|
|
45
|
+
|
|
46
|
+
桥接组件:负责创建/管理 DOM `<canvas>`、初始化自定义 renderer root,并转发 pointer/wheel 事件。
|
|
47
|
+
|
|
48
|
+
常用 props:
|
|
49
|
+
|
|
50
|
+
- `width: number` / `height: number`:逻辑尺寸(CSS 像素)
|
|
51
|
+
- `dpr?: number`(默认 `1`):设备像素比;真实像素尺寸为 `width * dpr`、`height * dpr`
|
|
52
|
+
- `clearColor?: string`:每帧清屏颜色
|
|
53
|
+
- `fontFamily?: string`、`fontSize?: number`、`fontWeight?: number | string`、`lineHeight?: number`:文本默认样式(当 `Text`/祖先未提供时生效)
|
|
54
|
+
- `style?: CSSProperties`:应用到 DOM `<canvas>` 的样式(逻辑尺寸、display 等)
|
|
55
|
+
- `children?: ReactNode`:场景树
|
|
56
|
+
|
|
57
|
+
### `View`
|
|
58
|
+
|
|
59
|
+
布局容器(Yoga)+ 可选背景/边框 + 可选滚动。
|
|
60
|
+
|
|
61
|
+
常用 props:
|
|
62
|
+
|
|
63
|
+
- `style?: YogaStyle`:布局样式,见下方「布局(Yoga 子集)」
|
|
64
|
+
- `background?: string`:背景填充色
|
|
65
|
+
- `border?: string`:类似 CSS 的 border,支持:
|
|
66
|
+
- `"<number>px solid <color>"`(例如 `1px solid rgba(255,255,255,0.2)`)
|
|
67
|
+
- `"<number> <color>"`(例如 `2 #fff`)
|
|
68
|
+
- `"<color>"`(等价于 `1px`,例如 `#fff`)
|
|
69
|
+
- `borderRadius?: number`
|
|
70
|
+
- `scrollX?: boolean`、`scrollY?: boolean`:开启滚动与裁剪
|
|
71
|
+
- `scrollbarX?: boolean`、`scrollbarY?: boolean`:是否显示滚动条(开启滚动时默认 `true`)
|
|
72
|
+
- `scrollbarWidth?: number`(默认 `10`)
|
|
73
|
+
- `scrollbarInset?: number`(默认 `6`)
|
|
74
|
+
- `scrollbarTrackColor?: string`(默认 `rgba(255,255,255,0.12)`)
|
|
75
|
+
- `scrollbarThumbColor?: string`(默认 `rgba(255,255,255,0.35)`)
|
|
76
|
+
- `onScrollX?: (scrollLeft: number) => void`
|
|
77
|
+
- `onScroll?: (scrollTop: number) => void`
|
|
78
|
+
|
|
79
|
+
### `Rect`
|
|
80
|
+
|
|
81
|
+
圆角矩形图元(也可以作为容器包含 children)。
|
|
82
|
+
|
|
83
|
+
常用 props:
|
|
84
|
+
|
|
85
|
+
- `style?: YogaStyle`
|
|
86
|
+
- `fill?: string`(默认 `#ffffff`)
|
|
87
|
+
- `stroke?: string`
|
|
88
|
+
- `lineWidth?: number`(默认 `1`)
|
|
89
|
+
- `borderRadius?: number`
|
|
90
|
+
|
|
91
|
+
### `Text`
|
|
92
|
+
|
|
93
|
+
文本图元。使用 `text`,不要传 children。
|
|
94
|
+
|
|
95
|
+
常用 props:
|
|
96
|
+
|
|
97
|
+
- `text: string`(必填)。支持 `\n` 手动换行。
|
|
98
|
+
- `style?: YogaStyle`:支持 `fontSize/fontFamily/fontWeight/lineHeight`(也会从祖先 / `Canvas` 默认值继承)
|
|
99
|
+
- `color?: string`(默认 `#ffffff`)
|
|
100
|
+
- `maxWidth?: number`:用于布局测量时限制测得的宽度(绘制阶段不会自动换行)
|
|
101
|
+
|
|
102
|
+
### `Image`
|
|
103
|
+
|
|
104
|
+
图片图元。使用 `src`,不要传 children。
|
|
105
|
+
|
|
106
|
+
常用 props:
|
|
107
|
+
|
|
108
|
+
- `src: string`(必填)
|
|
109
|
+
- `style?: YogaStyle`
|
|
110
|
+
- `objectFit?: 'cover' | 'contain' | 'fill'`(默认 `contain`)
|
|
111
|
+
- `borderRadius?: number`
|
|
112
|
+
|
|
113
|
+
## 事件
|
|
114
|
+
|
|
115
|
+
`View` / `Rect` / `Text` / `Image` 支持 pointer 事件:
|
|
116
|
+
|
|
117
|
+
- 命中测试受 `pointerEvents?: 'auto' | 'none'` 影响(`none` 会让节点对命中测试“透明”)。
|
|
118
|
+
- 支持冒泡 + 捕获:`onPointerDown/Move/Up/Cancel`、`onClick`,以及对应的 `*Capture`。
|
|
119
|
+
- Hover 事件:`onPointerEnter`、`onPointerLeave`(不走捕获/冒泡)。
|
|
120
|
+
- `click` 会在 `pointerup` 时触发:按下与抬起命中的是同一节点且 `button === 0`。
|
|
121
|
+
|
|
122
|
+
如果在事件回调里调用 `event.preventDefault()`,底层 `<canvas>` 对应的 DOM 事件会尽可能执行 `preventDefault()`。
|
|
41
123
|
|
|
42
124
|
## 布局(Yoga 子集)
|
|
43
125
|
|
|
@@ -47,6 +129,7 @@ export function Example() {
|
|
|
47
129
|
- Flex:`flexDirection/justifyContent/alignItems/flexGrow/flexShrink/flexBasis/flexWrap/gap`
|
|
48
130
|
- 边距:`padding*`、`margin*`
|
|
49
131
|
- 定位:`position/top/right/bottom/left`
|
|
132
|
+
- 文本样式(供 `Text` 使用):`fontSize/fontFamily/fontWeight/lineHeight`
|
|
50
133
|
|
|
51
134
|
## 实现说明
|
|
52
135
|
|
package/dist/index.cjs
CHANGED
|
@@ -94,14 +94,56 @@ function drawRoundedRect(ctx, x, y, w, h, r) {
|
|
|
94
94
|
function rectsIntersect(ax, ay, aw, ah, bx, by, bw, bh) {
|
|
95
95
|
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
|
|
96
96
|
}
|
|
97
|
+
function resolveBorder(value) {
|
|
98
|
+
if (typeof value !== "string") return null;
|
|
99
|
+
const raw = value.trim();
|
|
100
|
+
if (!raw) return null;
|
|
101
|
+
const normalized = raw.replace(/\s+/g, " ");
|
|
102
|
+
const match = normalized.match(/^([0-9]*\.?[0-9]+)(px)?\s+(?:solid\s+)?(.+)$/i);
|
|
103
|
+
if (match) {
|
|
104
|
+
const width = Number(match[1]);
|
|
105
|
+
const color = match[3]?.trim();
|
|
106
|
+
if (Number.isFinite(width) && width > 0 && color) return { width, color };
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const withoutSolid = normalized.replace(/\bsolid\b/gi, " ").replace(/\s+/g, " ").trim();
|
|
110
|
+
const match2 = withoutSolid.match(/^([0-9]*\.?[0-9]+)(px)?\s+(.+)$/i);
|
|
111
|
+
if (match2) {
|
|
112
|
+
const width = Number(match2[1]);
|
|
113
|
+
const color = match2[3]?.trim();
|
|
114
|
+
if (Number.isFinite(width) && width > 0 && color) return { width, color };
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
if (/^[0-9]/.test(withoutSolid)) return null;
|
|
118
|
+
if (!withoutSolid) return null;
|
|
119
|
+
return { width: 1, color: withoutSolid };
|
|
120
|
+
}
|
|
121
|
+
function drawBorder(ctx, x, y, w, h, radius, border) {
|
|
122
|
+
if (!Number.isFinite(border.width) || border.width <= 0) return;
|
|
123
|
+
const inset = border.width / 2;
|
|
124
|
+
const bw = w - border.width;
|
|
125
|
+
const bh = h - border.width;
|
|
126
|
+
if (bw <= 0 || bh <= 0) return;
|
|
127
|
+
ctx.save();
|
|
128
|
+
ctx.strokeStyle = border.color;
|
|
129
|
+
ctx.lineWidth = border.width;
|
|
130
|
+
drawRoundedRect(ctx, x + inset, y + inset, bw, bh, Math.max(0, radius - inset));
|
|
131
|
+
ctx.stroke();
|
|
132
|
+
ctx.restore();
|
|
133
|
+
}
|
|
97
134
|
function drawNode(state, node, offsetX, offsetY) {
|
|
98
135
|
const { ctx } = state;
|
|
99
136
|
const x = offsetX + node.layout.x;
|
|
100
137
|
const y = offsetY + node.layout.y;
|
|
101
138
|
const w = node.layout.width;
|
|
102
139
|
const h = node.layout.height;
|
|
140
|
+
let viewBorder = null;
|
|
141
|
+
let viewRadius = 0;
|
|
142
|
+
let viewIsScroll = false;
|
|
103
143
|
if (node.type === "View") {
|
|
104
144
|
const background = node.props.background;
|
|
145
|
+
viewBorder = resolveBorder(node.props.border);
|
|
146
|
+
viewRadius = node.props.borderRadius ?? 0;
|
|
105
147
|
const scrollX = !!node.props?.scrollX;
|
|
106
148
|
const scrollY = !!node.props?.scrollY;
|
|
107
149
|
const scrollLeft = scrollX ? node.scrollLeft ?? 0 : 0;
|
|
@@ -117,14 +159,14 @@ function drawNode(state, node, offsetX, offsetY) {
|
|
|
117
159
|
const maxScrollLeft = Math.max(0, contentWidth - w);
|
|
118
160
|
const maxScrollTop = Math.max(0, contentHeight - h);
|
|
119
161
|
if (background) {
|
|
120
|
-
const radius = node.props.borderRadius ?? 0;
|
|
121
162
|
ctx.save();
|
|
122
163
|
ctx.fillStyle = background;
|
|
123
|
-
drawRoundedRect(ctx, x, y, w, h,
|
|
164
|
+
drawRoundedRect(ctx, x, y, w, h, viewRadius);
|
|
124
165
|
ctx.fill();
|
|
125
166
|
ctx.restore();
|
|
126
167
|
}
|
|
127
168
|
if (scrollX || scrollY) {
|
|
169
|
+
viewIsScroll = true;
|
|
128
170
|
ctx.save();
|
|
129
171
|
ctx.beginPath();
|
|
130
172
|
ctx.rect(x, y, w, h);
|
|
@@ -201,6 +243,7 @@ function drawNode(state, node, offsetX, offsetY) {
|
|
|
201
243
|
ctx.fill();
|
|
202
244
|
ctx.restore();
|
|
203
245
|
}
|
|
246
|
+
if (viewBorder) drawBorder(ctx, x, y, w, h, viewRadius, viewBorder);
|
|
204
247
|
return;
|
|
205
248
|
}
|
|
206
249
|
}
|
|
@@ -240,6 +283,7 @@ function drawNode(state, node, offsetX, offsetY) {
|
|
|
240
283
|
const { imageInstance } = node;
|
|
241
284
|
if (imageInstance && imageInstance.complete && imageInstance.naturalWidth > 0) {
|
|
242
285
|
const objectFit = node.props.objectFit || "contain";
|
|
286
|
+
const radius = node.props.borderRadius ?? 0;
|
|
243
287
|
const srcW = imageInstance.naturalWidth;
|
|
244
288
|
const srcH = imageInstance.naturalHeight;
|
|
245
289
|
let dstX = x;
|
|
@@ -267,7 +311,7 @@ function drawNode(state, node, offsetX, offsetY) {
|
|
|
267
311
|
}
|
|
268
312
|
ctx.save();
|
|
269
313
|
ctx.beginPath();
|
|
270
|
-
drawRoundedRect(ctx, x, y, w, h,
|
|
314
|
+
drawRoundedRect(ctx, x, y, w, h, radius);
|
|
271
315
|
ctx.clip();
|
|
272
316
|
ctx.drawImage(imageInstance, srcX, srcY, finalSrcW, finalSrcH, dstX, dstY, dstW, dstH);
|
|
273
317
|
ctx.restore();
|
|
@@ -276,6 +320,9 @@ function drawNode(state, node, offsetX, offsetY) {
|
|
|
276
320
|
for (const child of node.children) {
|
|
277
321
|
drawNode(state, child, x, y);
|
|
278
322
|
}
|
|
323
|
+
if (node.type === "View" && !viewIsScroll && viewBorder) {
|
|
324
|
+
drawBorder(ctx, x, y, w, h, viewRadius, viewBorder);
|
|
325
|
+
}
|
|
279
326
|
}
|
|
280
327
|
function drawTree(root, ctx, dpr, clearColor, defaults) {
|
|
281
328
|
const w = ctx.canvas.width;
|