@blinkorb/rcx 0.0.2 → 0.0.3
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 +460 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
# RCX
|
|
2
|
+
|
|
3
|
+
**Reactive JSX-based library for creating HTML5 canvas applications**
|
|
4
|
+
|
|
5
|
+
## Preamble
|
|
6
|
+
|
|
7
|
+
This library is in early development, and so the interfaces you interact with may change. We'll setup full documentation when the API stabilizes. For now the readme should give you enough info to get started.
|
|
8
|
+
|
|
9
|
+
## About
|
|
10
|
+
|
|
11
|
+
RCX closely resembles other JSX-based view libraries such as React/Vue, but allows you to render to canvas. It can even be used in conjunction with other view libraries (see [Integrating With React](#integrating-with-react) example).
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm i @blinkorb/rcx -P
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## The Basics
|
|
20
|
+
|
|
21
|
+
### TypeScript Config
|
|
22
|
+
|
|
23
|
+
In order to use RCX with TypeScript (if you are not already using another JSX-based library), you should set the following `compilerOptions` in your `tsconfig.json`:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"compilerOptions": {
|
|
28
|
+
"jsx": "react-jsx",
|
|
29
|
+
"jsxImportSource": "@blinkorb/rcx"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Entry Point
|
|
35
|
+
|
|
36
|
+
All components in RCX are function components. To get started you should create an `App` component that renders the `Canvas` component.
|
|
37
|
+
|
|
38
|
+
The canvas component allows you to control things like the size and pixel ratio of your canvas, and provides context to other components to inform them of its size, pixel ratio, etc.
|
|
39
|
+
|
|
40
|
+
A `pixelRatio` of 2 and `width` of `100` will actually render a canvas that is `200` in width, but scale your drawings so you don't have to manually scale everything - allows for crisper drawings on high density/retina displays. You can use the `getRecommendedPixelRatio` util to use our recommendation (`2` for any devices with a `devicePixelRatio` greater than or equal to 2, and `1` for every other device).
|
|
41
|
+
|
|
42
|
+
If you set the `width` and or `height` to `"auto"` then the canvas' pixels will match the actual size of the canvas on the screen (scaled by pixel ratio). This way you can have your canvas automatically scale to fill its parent (using CSS) for example.
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { Canvas } from '@blinkorb/rcx';
|
|
46
|
+
|
|
47
|
+
const App = () => {
|
|
48
|
+
return (
|
|
49
|
+
<Canvas pixelRatio={getRecommendedPixelRatio()}>
|
|
50
|
+
{/* Your component here */}
|
|
51
|
+
</Canvas>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
You can then render this component using the `render` function. The render function's second argument is the DOM node where you would like the canvas to appear. If the node is already a canvas RCX will render to that canvas, otherwise it will add a canvas within that node.
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { render } from '@blinkorb/rcx';
|
|
60
|
+
|
|
61
|
+
render(<App />, document.body);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Basic Components
|
|
65
|
+
|
|
66
|
+
#### Transform Components
|
|
67
|
+
|
|
68
|
+
We provide `Translate`, `Scale`, and `Rotate` components that will transform any of their children.
|
|
69
|
+
|
|
70
|
+
In the below example the `Offset` component will be offset by 10 pixels in both the `x` and `y` axis. The `NoOffset` component will not be affected by the transform.
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
<>
|
|
74
|
+
<Translate x={10} y={10}>
|
|
75
|
+
<Offset />
|
|
76
|
+
</Translate>
|
|
77
|
+
<NoOffset />
|
|
78
|
+
</>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
#### Shape Components
|
|
82
|
+
|
|
83
|
+
We provide `Circle`, `Ellipse`, and `Rectangle` components for rendering some basic shapes. Each of these can receive a style prop to apply a stroke/border, and fill. You can also define (for some of these components) if the shape should continue from any existing drawings, or begin a new path by setting the `beginPath` prop. You can also choose to close these shapes by setting the `closePath` prop.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
<>
|
|
87
|
+
<Circle
|
|
88
|
+
x={50}
|
|
89
|
+
y={50}
|
|
90
|
+
radius={50}
|
|
91
|
+
beginPath
|
|
92
|
+
closePath
|
|
93
|
+
style={{
|
|
94
|
+
strokeWidth: 1,
|
|
95
|
+
stroke: 'black',
|
|
96
|
+
}}
|
|
97
|
+
/>
|
|
98
|
+
<Ellipse
|
|
99
|
+
x={50}
|
|
100
|
+
y={50}
|
|
101
|
+
radiusX={20}
|
|
102
|
+
radiusY={50}
|
|
103
|
+
beginPath
|
|
104
|
+
closePath
|
|
105
|
+
style={{
|
|
106
|
+
strokeWidth: 1,
|
|
107
|
+
stroke: 'black',
|
|
108
|
+
fill: 'red',
|
|
109
|
+
}}
|
|
110
|
+
/>
|
|
111
|
+
<Rectangle
|
|
112
|
+
x={0}
|
|
113
|
+
y={0}
|
|
114
|
+
width={100}
|
|
115
|
+
height={50}
|
|
116
|
+
beginPath
|
|
117
|
+
style={{ fill: 'blue' }}
|
|
118
|
+
/>
|
|
119
|
+
</>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### Path Components
|
|
123
|
+
|
|
124
|
+
We provide a selection of components for drawing paths. These components can be combined to draw more complex shapes.
|
|
125
|
+
|
|
126
|
+
All path plotting components can have stroke styles. `Path` and `ArcTo` components can also have fill styles (fills are excluded from `Line` as it is more performant to use `Path`).
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
<>
|
|
130
|
+
{/* Plot a single line */}
|
|
131
|
+
<Line
|
|
132
|
+
startX={0}
|
|
133
|
+
startY={0}
|
|
134
|
+
endX={10}
|
|
135
|
+
endY={10}
|
|
136
|
+
beginPath
|
|
137
|
+
style={{
|
|
138
|
+
strokeWidth: 2,
|
|
139
|
+
stroke: 'black',
|
|
140
|
+
}}
|
|
141
|
+
/>
|
|
142
|
+
{/* Plot an arc */}
|
|
143
|
+
<ArcTo
|
|
144
|
+
startControlX={0}
|
|
145
|
+
startControlY={0}
|
|
146
|
+
endControlX={10}
|
|
147
|
+
endControlY={10}
|
|
148
|
+
radius={10}
|
|
149
|
+
style={{
|
|
150
|
+
strokeWidth: 2,
|
|
151
|
+
stroke: 'black',
|
|
152
|
+
}}
|
|
153
|
+
/>
|
|
154
|
+
{/* Plot a path from an array of points */}
|
|
155
|
+
<Path
|
|
156
|
+
points={[
|
|
157
|
+
{
|
|
158
|
+
x: 0,
|
|
159
|
+
y: 0,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
x: 10,
|
|
163
|
+
y: 10,
|
|
164
|
+
},
|
|
165
|
+
]}
|
|
166
|
+
beginPath
|
|
167
|
+
style={{
|
|
168
|
+
strokeWidth: 2,
|
|
169
|
+
stroke: 'black',
|
|
170
|
+
}}
|
|
171
|
+
/>
|
|
172
|
+
{/* Plot a path from an array of points using the Point component */}
|
|
173
|
+
<Path
|
|
174
|
+
beginPath
|
|
175
|
+
style={{
|
|
176
|
+
strokeWidth: 2,
|
|
177
|
+
stroke: 'black',
|
|
178
|
+
}}
|
|
179
|
+
>
|
|
180
|
+
{points.map((point, index) => (
|
|
181
|
+
<Point $key={index} x={point.x} x={point.y} lineTo={index > 0} />
|
|
182
|
+
))}
|
|
183
|
+
</Path>
|
|
184
|
+
{/* Plot a path using manually specified Points */}
|
|
185
|
+
<Path
|
|
186
|
+
beginPath
|
|
187
|
+
style={{
|
|
188
|
+
strokeWidth: 2,
|
|
189
|
+
stroke: 'black',
|
|
190
|
+
}}
|
|
191
|
+
>
|
|
192
|
+
<Point x={0} x={0} lineTo={false} />
|
|
193
|
+
<Point x={10} x={10} lineTo={true} />
|
|
194
|
+
</Path>
|
|
195
|
+
</>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
In addition to the path plotting components we provide, we also have a `Clip` component that can be used to apply a clipping mask to future drawings.
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
<>
|
|
202
|
+
<Circle x={50} y={50} radius={50}>
|
|
203
|
+
<Clip>
|
|
204
|
+
<ComponentWillOnlyDrawInsideCircle />
|
|
205
|
+
</Clip>
|
|
206
|
+
</Circle>
|
|
207
|
+
</>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
#### Text Components
|
|
211
|
+
|
|
212
|
+
We currently only provide a single `Text` component that will render a single line of raw text. We hope to add multi-line and rich text components in the future. You can also render components that contain text or number within a `Text` component.
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
<Text x={10} y={10} style={{
|
|
216
|
+
fill: 'black',
|
|
217
|
+
align: 'center,
|
|
218
|
+
}}>
|
|
219
|
+
The count is {count}
|
|
220
|
+
<ContainsSomeText />
|
|
221
|
+
</Text>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Custom Components
|
|
225
|
+
|
|
226
|
+
You can define your own components with complex drawing logic directly applied via canvas context using the `useRenderBeforeChildren` and `useRenderAfterChildren` hooks.
|
|
227
|
+
|
|
228
|
+
It is highly recommended to `.save()` the canvas state before beginning drawing in `useRenderBeforeChildren` and to `.restore()` the canvas state after drawing in the `useRenderAfterChildren`.
|
|
229
|
+
|
|
230
|
+
We also provide some utils for resolving and applying styles as styles can be provided as an array, and all fills and strokes are always applied in the same way.
|
|
231
|
+
|
|
232
|
+
Here's an example that draws a rectangle with rounded corners.
|
|
233
|
+
|
|
234
|
+
```tsx
|
|
235
|
+
interface RoundedRectangleProps extends RectangleProps {
|
|
236
|
+
radius: number;
|
|
237
|
+
closePath?: boolean;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const RoundedRectangle: RCXComponent<RoundedRectangleProps> = (props) => {
|
|
241
|
+
useRenderBeforeChildren((renderingContext) => {
|
|
242
|
+
const { x, y, width, height, radius, beginPath = true, closePath } = props;
|
|
243
|
+
|
|
244
|
+
renderingContext.ctx2d.save();
|
|
245
|
+
|
|
246
|
+
if (beginPath) {
|
|
247
|
+
renderingContext.ctx2d.beginPath();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
renderingContext.ctx2d.moveTo(x + radius, y);
|
|
251
|
+
renderingContext.ctx2d.lineTo(x + width - radius, y);
|
|
252
|
+
renderingContext.ctx2d.arcTo(x + width, y, x + width, y + radius, radius);
|
|
253
|
+
renderingContext.ctx2d.lineTo(x + width, y + height - radius);
|
|
254
|
+
renderingContext.ctx2d.arcTo(
|
|
255
|
+
x + width,
|
|
256
|
+
y + height,
|
|
257
|
+
x + width - radius,
|
|
258
|
+
y + height,
|
|
259
|
+
radius
|
|
260
|
+
);
|
|
261
|
+
renderingContext.ctx2d.lineTo(x + radius, y + height);
|
|
262
|
+
renderingContext.ctx2d.arcTo(x, y + height, x, y + height - radius, radius);
|
|
263
|
+
renderingContext.ctx2d.lineTo(x, y + radius);
|
|
264
|
+
renderingContext.ctx2d.arcTo(x, y, x + radius, y, radius);
|
|
265
|
+
|
|
266
|
+
if (closePath) {
|
|
267
|
+
renderingContext.ctx2d.closePath();
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
useRenderAfterChildren((renderingContext) => {
|
|
272
|
+
applyFillAndStrokeStyles(renderingContext, resolveStyles(props.style));
|
|
273
|
+
|
|
274
|
+
renderingContext.ctx2d.restore();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
return props.children;
|
|
278
|
+
};
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Hooks
|
|
282
|
+
|
|
283
|
+
### useCanvasContext
|
|
284
|
+
|
|
285
|
+
Provides the context from the current canvas including its `pixelRatio`, `width` and `height` (scaled by `pixelRatio`), and actual width/height (e.g. with a `pixelRatio` of `2` and `width` of `100` the `actualWidth` of the canvas will be `200` - you should generally avoid using the actual sizes and rely on the scaled `width` and `height` values).
|
|
286
|
+
|
|
287
|
+
### useRenderBeforeChildren
|
|
288
|
+
|
|
289
|
+
Used for creating custom components with complex rendering logic. Takes a callback that receives the current canvas rendering context to allow manually drawing with the canvas context. The callback is called before any children are rendered. See [Custom Components](#custom-components) for a full example.
|
|
290
|
+
|
|
291
|
+
### useRenderAfterChildren
|
|
292
|
+
|
|
293
|
+
Used for creating custom components with complex rendering logic. Takes a callback that receives the current canvas rendering context to allow manually drawing with the canvas context. The callback is called after any children are rendered. See [Custom Components](#custom-components) for a full example.
|
|
294
|
+
|
|
295
|
+
### useLinearGradient
|
|
296
|
+
|
|
297
|
+
Can be used to create a linear gradient that can then be applied as a fill/stroke style.
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
const stroke = useLinearGradient({
|
|
301
|
+
startX: 0,
|
|
302
|
+
startY: 0,
|
|
303
|
+
endX: 10,
|
|
304
|
+
endY: 10,
|
|
305
|
+
stops: [
|
|
306
|
+
{
|
|
307
|
+
offset: 0,
|
|
308
|
+
color: '#f00',
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
offset: 1,
|
|
312
|
+
color: '#000',
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### useRadialGradient
|
|
319
|
+
|
|
320
|
+
Can be used to create a radial gradient that can then be applied as a fill/stroke style.
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
const fill = useRadialGradient({
|
|
324
|
+
startX: 10,
|
|
325
|
+
startY: 10,
|
|
326
|
+
startRadius: 0,
|
|
327
|
+
endX: 0,
|
|
328
|
+
endY: 0,
|
|
329
|
+
endRadius: 10,
|
|
330
|
+
stops: [
|
|
331
|
+
{
|
|
332
|
+
offset: 0,
|
|
333
|
+
color: '#000',
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
offset: 1,
|
|
337
|
+
color: '#00f',
|
|
338
|
+
},
|
|
339
|
+
],
|
|
340
|
+
});
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### useLoop
|
|
344
|
+
|
|
345
|
+
Takes a callback and calls this in an infinite `requestAnimationFrame` loop.
|
|
346
|
+
|
|
347
|
+
```tsx
|
|
348
|
+
useLoop(() => {
|
|
349
|
+
// Your logic here
|
|
350
|
+
});
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### useOnMount
|
|
354
|
+
|
|
355
|
+
Takes a callback that is executed when the component mounts. This callback can return another callback that is executed when a component unmounts e.g. to cleanup listeners.
|
|
356
|
+
|
|
357
|
+
```tsx
|
|
358
|
+
useOnMount(() => {
|
|
359
|
+
// Logic on mount
|
|
360
|
+
|
|
361
|
+
return () => {
|
|
362
|
+
// Logic on unmount
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### useOnUnmount
|
|
368
|
+
|
|
369
|
+
Takes a callback that is executed when the component is mounted.
|
|
370
|
+
|
|
371
|
+
```tsx
|
|
372
|
+
useOnUnmount(() => {
|
|
373
|
+
// Logic on unmount
|
|
374
|
+
});
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### useReactive
|
|
378
|
+
|
|
379
|
+
Receives an object that will be wrapped in a JavaScript proxy. Any mutations to this object will cause a re-render.
|
|
380
|
+
|
|
381
|
+
Note: you must use either a `useReactive` or `useUnreactive` for any state values you want to persist. When a component re-renders any state that is defined as basic variables will be recreated.
|
|
382
|
+
|
|
383
|
+
```tsx
|
|
384
|
+
const state = useReactive({ count: 0 });
|
|
385
|
+
|
|
386
|
+
// This will reset to zero every render
|
|
387
|
+
const notState = { count: 0 };
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### useUnreactive
|
|
391
|
+
|
|
392
|
+
Receives an object that will be available until the component is unmounted. Unlike `useReactive` any mutations on this object will not cause a re-render.
|
|
393
|
+
|
|
394
|
+
Note: you must use either a `useReactive` or `useUnreactive` for any state values you want to persist. When a component re-renders any state that is defined as basic variables will be recreated.
|
|
395
|
+
|
|
396
|
+
```tsx
|
|
397
|
+
const state = useUnreactive({ count: 0 });
|
|
398
|
+
|
|
399
|
+
// This will reset to zero every render
|
|
400
|
+
const notState = { count: 0 };
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### useWindowSize
|
|
404
|
+
|
|
405
|
+
Returns the current window size. This will update when the window is resized.
|
|
406
|
+
|
|
407
|
+
## Integrating With React
|
|
408
|
+
|
|
409
|
+
### TypeScript Config With React
|
|
410
|
+
|
|
411
|
+
If you're already using JSX with another view library, such as React, you'll likely already have your `tsconfig.json` setup to handle JSX for React e.g.
|
|
412
|
+
|
|
413
|
+
```json
|
|
414
|
+
{
|
|
415
|
+
"compilerOptions": {
|
|
416
|
+
"jsx": "react-jsx",
|
|
417
|
+
"jsxImportSource": "react"
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
You can tell TypeScript to treat your RCX components differently (using the JSX types from RCX itself) by adding the following to the top of any RCX component files:
|
|
423
|
+
|
|
424
|
+
```tsx
|
|
425
|
+
/** @jsxImportSource @blinkorb/rcx */
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Rendering RCX Components Within React
|
|
429
|
+
|
|
430
|
+
You can render an RCX component within a React app by getting a `ref` to a canvas node and rendering/unmounting the RCX tree at this node within a `useEffect`.
|
|
431
|
+
|
|
432
|
+
Make sure you're importing the correct `jsx` function from RCX.
|
|
433
|
+
|
|
434
|
+
```tsx
|
|
435
|
+
import { render } from '@blinkorb/rcx';
|
|
436
|
+
import { jsx } from '@blinkorb/rcx/jsx-runtime';
|
|
437
|
+
import App from './your-canvas-app-component';
|
|
438
|
+
|
|
439
|
+
const ReactComponent = () => {
|
|
440
|
+
const [canvas, setCanvas] = useState<HTMLCanvasElement | null>(null);
|
|
441
|
+
|
|
442
|
+
useEffect(() => {
|
|
443
|
+
const unmountOrError = canvas ? render(jsx(App, {}), canvas) : null;
|
|
444
|
+
|
|
445
|
+
// Check if we have an error
|
|
446
|
+
if (unmountOrError && 'error' in unmountOrError) {
|
|
447
|
+
console.error(unmountOrError.error);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return () => {
|
|
451
|
+
// Ensure we have the unmount function (no errors)
|
|
452
|
+
if (unmountOrError && !('error' in unmountOrError)) {
|
|
453
|
+
unmountOrError();
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
}, [canvas]);
|
|
457
|
+
|
|
458
|
+
return <canvas ref={setCanvas} />;
|
|
459
|
+
};
|
|
460
|
+
```
|