@jiujue/react-canvas-fiber 2.0.7 → 2.0.8

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/SKILL.md ADDED
@@ -0,0 +1,291 @@
1
+ ---
2
+ name: 'react-canvas-fiber'
3
+ description: 'react-canvas-fiber 的使用与排障手册。使用 Canvas/View/Rect/Text/Image,处理布局/事件/滚动/性能与常见问题时参考。'
4
+ ---
5
+
6
+ # react-canvas-fiber 使用指南(Skill)
7
+
8
+ 这份文件面向“使用者”,用于在项目里快速上手 `@jiujue/react-canvas-fiber`:如何写 UI、如何做布局/事件/滚动,以及遇到问题时该从哪里查。
9
+
10
+ 如果你在对话式编码场景中使用它,也可以把你的诉求前缀写成:
11
+
12
+ - `使用 react-canvas-fiber:<你的需求>`(例如:使用 react-canvas-fiber:做一个可滚动列表并支持点击)
13
+
14
+ ## 最短上手
15
+
16
+ ### 安装
17
+
18
+ ```bash
19
+ npm i @jiujue/react-canvas-fiber react react-dom
20
+ ```
21
+
22
+ ### 最小示例
23
+
24
+ ```tsx
25
+ import { Canvas, Rect, Text, View } from '@jiujue/react-canvas-fiber'
26
+
27
+ export function App() {
28
+ const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1
29
+
30
+ return (
31
+ <Canvas width={720} height={320} dpr={dpr} clearColor="#0b1020" style={{ borderRadius: 12 }}>
32
+ <View style={{ width: 720, height: 320, padding: 16, flexDirection: 'column', gap: 12 }}>
33
+ <Text text="Hello Canvas" style={{ fontSize: 18, fontWeight: 700 }} color="#e5e7eb" />
34
+ <View style={{ flexDirection: 'row', gap: 12, alignItems: 'center' }}>
35
+ <Rect style={{ width: 140, height: 56 }} borderRadius={14} fill="#60a5fa" />
36
+ <Rect
37
+ style={{ flexGrow: 1, height: 56 }}
38
+ borderRadius={14}
39
+ fill="rgba(255,255,255,0.10)"
40
+ />
41
+ </View>
42
+ </View>
43
+ </Canvas>
44
+ )
45
+ }
46
+ ```
47
+
48
+ ## 核心概念(理解这 4 点就够用)
49
+
50
+ 1. 这是一个自定义 React Renderer:JSX 渲染到 Canvas2D,而不是 DOM。
51
+ 2. `View/Rect/Text/Image` 都是“Host 节点”,布局交给 Yoga(Flexbox)。
52
+ 3. `Canvas width/height` 是“逻辑尺寸”,实际渲染像素由 `dpr` 影响。
53
+ 4. React commit 会被合帧:commit 后触发下一帧执行 layout + draw。
54
+
55
+ ## API 速查
56
+
57
+ ### Canvas
58
+
59
+ - `width`, `height`: 逻辑尺寸(number)
60
+ - `dpr?: number`: 设备像素比,建议传 `window.devicePixelRatio || 1`
61
+ - `clearColor?: string`: 每帧清屏颜色
62
+ - `fontFamily/fontSize/fontWeight/lineHeight`: 默认字体参数(影响 Text 测量与绘制)
63
+ - `style?: CSSProperties`: 作用于 DOM `<canvas>`(例如圆角/边框)
64
+
65
+ ### View(容器)
66
+
67
+ - `style?: YogaStyle`: 布局与尺寸
68
+ - `background?: string`, `border?: string`, `borderRadius?: number`
69
+ - 滚动:
70
+ - `scrollX?: boolean`, `scrollY?: boolean`
71
+ - `scrollbarX/scrollbarY?: boolean`
72
+ - `scrollbarWidth?: number`, `scrollbarInset?: number`
73
+ - `scrollbarTrackColor?: string`, `scrollbarThumbColor?: string`
74
+ - `onScroll?(scrollTop: number)`, `onScrollX?(scrollLeft: number)`
75
+ - 事件:支持 `onPointer* / onClick / onPointerEnter/Leave`,也支持 `Capture` 版本
76
+ - `pointerEvents?: 'auto' | 'none'`: 设置为 `none` 可让该节点及其子树不参与命中
77
+
78
+ ### Rect(矩形)
79
+
80
+ - `fill?: string`, `stroke?: string`, `lineWidth?: number`, `borderRadius?: number`
81
+ - 事件与 `pointerEvents` 同 View
82
+
83
+ ### Text(文本)
84
+
85
+ - `text: string`:必须用这个传文本
86
+ - `children?: never`:不支持 `<Text>xxx</Text>`
87
+ - `color?: string`
88
+ - `maxWidth?: number`: 约束宽度用于测量/换行
89
+ - 字体相关放在 `style`: `fontSize/fontFamily/fontWeight/lineHeight`
90
+
91
+ ### Image(图片)
92
+
93
+ - `src: string`:必填
94
+ - `objectFit?: 'cover' | 'contain' | 'fill'`(默认 `contain`)
95
+ - `borderRadius?: number`
96
+ - `children?: never`
97
+
98
+ ## YogaStyle(支持的布局能力)
99
+
100
+ YogaStyle 接受的值基本都是 number(不支持百分比字符串)。常用字段:
101
+
102
+ - 尺寸:`width/height/minWidth/minHeight/maxWidth/maxHeight`
103
+ - Flex:`flexDirection/justifyContent/alignItems/alignContent/flexWrap`
104
+ - Flex 子项:`flexGrow/flexShrink/flexBasis`
105
+ - 间距:`padding* / margin* / gap`
106
+ - 定位:`position ('relative'|'absolute')` + `top/right/bottom/left`
107
+ - 文本:`fontSize/fontFamily/fontWeight/lineHeight`
108
+
109
+ ## 事件系统(指针与点击)
110
+
111
+ 支持事件:
112
+
113
+ - `onPointerDown/Move/Up/Cancel`(含 Capture 版本)
114
+ - `onClick`(含 Capture 版本)
115
+ - `onPointerEnter/onPointerLeave`
116
+
117
+ 事件对象关键字段:
118
+
119
+ - `x/y`: 相对 Canvas 左上角的逻辑坐标
120
+ - `target/currentTarget`: 命中的节点(运行时对象)
121
+ - `stopPropagation()`, `preventDefault()`
122
+
123
+ ### 示例:点击切换
124
+
125
+ ```tsx
126
+ import { Canvas, Rect, Text, View } from '@jiujue/react-canvas-fiber'
127
+ import { useState } from 'react'
128
+
129
+ export function ClickToggle() {
130
+ const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1
131
+ const [active, setActive] = useState(false)
132
+
133
+ return (
134
+ <Canvas width={480} height={200} dpr={dpr} clearColor="#0b1020">
135
+ <View style={{ width: 480, height: 200, padding: 16, flexDirection: 'column', gap: 12 }}>
136
+ <Text
137
+ text={active ? 'ACTIVE' : 'INACTIVE'}
138
+ style={{ fontSize: 18, fontWeight: 700 }}
139
+ color="#e5e7eb"
140
+ />
141
+ <Rect
142
+ style={{ width: 220, height: 54 }}
143
+ borderRadius={14}
144
+ fill={active ? '#22c55e' : '#ef4444'}
145
+ onClick={() => setActive((v) => !v)}
146
+ />
147
+ </View>
148
+ </Canvas>
149
+ )
150
+ }
151
+ ```
152
+
153
+ ### 示例:拖拽移动(pointerdown + pointermove)
154
+
155
+ ```tsx
156
+ import { Canvas, Rect, Text, View } from '@jiujue/react-canvas-fiber'
157
+ import { useState } from 'react'
158
+
159
+ export function DragRect() {
160
+ const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1
161
+ const width = 520
162
+ const height = 260
163
+
164
+ const [dragging, setDragging] = useState(false)
165
+ const [pos, setPos] = useState({ x: 60, y: 90 })
166
+ const [offset, setOffset] = useState({ x: 0, y: 0 })
167
+
168
+ return (
169
+ <Canvas width={width} height={height} dpr={dpr} clearColor="#0b1020">
170
+ <View style={{ width, height, padding: 16, position: 'relative' }}>
171
+ <Text
172
+ text={dragging ? 'Dragging…' : 'Drag the rect'}
173
+ style={{ fontSize: 16, fontWeight: 700 }}
174
+ color="#e5e7eb"
175
+ />
176
+ <Rect
177
+ style={{ position: 'absolute', left: pos.x, top: pos.y, width: 200, height: 54 }}
178
+ borderRadius={14}
179
+ fill="#f59e0b"
180
+ onPointerDown={(e) => {
181
+ setDragging(true)
182
+ setOffset({ x: e.x - pos.x, y: e.y - pos.y })
183
+ }}
184
+ onPointerMove={(e) => {
185
+ if (!dragging) return
186
+ setPos({ x: e.x - offset.x, y: e.y - offset.y })
187
+ }}
188
+ onPointerUp={() => setDragging(false)}
189
+ onPointerCancel={() => setDragging(false)}
190
+ />
191
+ </View>
192
+ </Canvas>
193
+ )
194
+ }
195
+ ```
196
+
197
+ ## 滚动(View 滚动容器)
198
+
199
+ 开启滚动只需要在 View 上打开 `scrollY` 或 `scrollX`,并确保该 View 具有确定的可视尺寸(例如 `style.width/style.height` 或在父布局里能推导出明确尺寸)。
200
+
201
+ ### 示例:纵向滚动列表
202
+
203
+ ```tsx
204
+ import { Canvas, Rect, Text, View } from '@jiujue/react-canvas-fiber'
205
+ import { useMemo, useState } from 'react'
206
+
207
+ export function ScrollList() {
208
+ const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1
209
+ const width = 520
210
+ const height = 360
211
+
212
+ const items = useMemo(() => Array.from({ length: 30 }, (_, i) => i + 1), [])
213
+ const [scrollTop, setScrollTop] = useState(0)
214
+
215
+ return (
216
+ <Canvas width={width} height={height} dpr={dpr} clearColor="#0b1020">
217
+ <View style={{ width, height, padding: 16, flexDirection: 'column', gap: 12 }}>
218
+ <Text
219
+ text={`scrollTop: ${Math.round(scrollTop)}`}
220
+ style={{ fontSize: 14, fontWeight: 700 }}
221
+ color="#e5e7eb"
222
+ />
223
+
224
+ <View
225
+ scrollY
226
+ scrollbarY
227
+ scrollbarWidth={10}
228
+ scrollbarInset={6}
229
+ scrollbarTrackColor="rgba(255,255,255,0.10)"
230
+ scrollbarThumbColor="rgba(255,255,255,0.35)"
231
+ onScroll={setScrollTop}
232
+ style={{ width: 360, height: 260, padding: 12, flexDirection: 'column', gap: 10 }}
233
+ background="rgba(255,255,255,0.06)"
234
+ borderRadius={14}
235
+ >
236
+ {items.map((n) => (
237
+ <View
238
+ key={n}
239
+ style={{
240
+ width: 560,
241
+ height: 44,
242
+ flexDirection: 'row',
243
+ alignItems: 'center',
244
+ gap: 10,
245
+ }}
246
+ >
247
+ <Rect
248
+ style={{ width: 44, height: 44 }}
249
+ borderRadius={12}
250
+ fill={n % 2 ? '#60a5fa' : '#22c55e'}
251
+ />
252
+ <Text text={`Item ${n}`} style={{ fontSize: 14 }} color="rgba(229,231,235,0.90)" />
253
+ </View>
254
+ ))}
255
+ </View>
256
+ </View>
257
+ </Canvas>
258
+ )
259
+ }
260
+ ```
261
+
262
+ ## 常见坑(高频)
263
+
264
+ - Text 只能用 `text`,不支持 children
265
+ - YogaStyle 里大部分字段都是 number,不支持 `'100%'` 这类字符串
266
+ - 开启 `scrollX/scrollY` 的 View 必须有明确的可视尺寸,否则“看起来像不滚动”
267
+ - 命中顺序按“从后往前遍历子节点”处理,后绘制的更容易被命中
268
+ - `pointerEvents="none"` 会让节点及其子树直接跳过命中
269
+
270
+ ## 想做更复杂的 UI(建议直接看这些)
271
+
272
+ - 使用示例与架构说明:`AI_GUIDE.md`
273
+ - dumi 文档(组件/布局/事件/滚动):仓库的 `apps/dumi-docs/docs`
274
+ - demo 代码:仓库的 `apps/demo/src/demos`
275
+
276
+ ## 需要扩展(新增一个节点类型)时怎么做
277
+
278
+ 1. 定义节点与字段:`src/types/nodes.ts`
279
+ 2. 定义 JSX props:`src/types/jsx.ts`
280
+ 3. 注册 intrinsic:`src/intrinsics.d.ts`
281
+ 4. 接入创建/更新:`src/runtime/reconciler.ts`
282
+ 5. 接入布局同步(如需参与 Yoga):`src/layout/layoutTree.ts`
283
+ 6. 接入绘制:`src/render/drawTree.ts`
284
+ 7. 接入交互(可命中/冒泡/滚动相关):`src/runtime/root.ts`
285
+
286
+ ## 排障从哪里开始查(按问题类型)
287
+
288
+ - 不重绘/合帧:`src/runtime/reconciler.ts`(commit 后 invalidate)→ `src/runtime/root.ts`(rAF)
289
+ - 布局不对:`src/layout/layoutTree.ts`
290
+ - 绘制不对:`src/render/drawTree.ts`
291
+ - 事件命中/滚动:`src/runtime/root.ts`(hitTest、滚动条命中、派发链路)
package/dist/index.cjs CHANGED
@@ -1544,9 +1544,7 @@ function createCanvasRoot(canvas, options) {
1544
1544
  stopPicker() {
1545
1545
  if (!this.picker.enabled) return;
1546
1546
  this.picker.enabled = false;
1547
- this.picker.rootInstanceId = null;
1548
1547
  this.picker.hoverId = null;
1549
- this.picker.selectedId = null;
1550
1548
  this.pickerCleanup?.();
1551
1549
  this.pickerCleanup = null;
1552
1550
  },
@@ -1556,7 +1554,12 @@ function createCanvasRoot(canvas, options) {
1556
1554
  unregisterRoot(id) {
1557
1555
  const handle = this.roots.get(id);
1558
1556
  if (!handle) return;
1559
- if (this.picker.rootInstanceId === id) this.stopPicker();
1557
+ if (this.picker.rootInstanceId === id) {
1558
+ if (this.picker.enabled) this.stopPicker();
1559
+ this.picker.rootInstanceId = null;
1560
+ this.picker.hoverId = null;
1561
+ this.picker.selectedId = null;
1562
+ }
1560
1563
  handle.unsubscribe?.();
1561
1564
  this.roots.delete(id);
1562
1565
  }