@canvas-commons/2d 0.2.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.
Files changed (183) hide show
  1. package/LICENSE +22 -0
  2. package/editor/index.css +40 -0
  3. package/editor/index.js +525 -0
  4. package/editor/index.js.map +1 -0
  5. package/lib/index-OvpZ-GsA.d.ts +5673 -0
  6. package/lib/index-OvpZ-GsA.d.ts.map +1 -0
  7. package/lib/index.d.ts +2 -0
  8. package/lib/index.js +10326 -0
  9. package/lib/index.js.map +1 -0
  10. package/lib/jsx-dev-runtime.d.ts +2 -0
  11. package/lib/jsx-dev-runtime.js +2 -0
  12. package/lib/jsx-runtime.d.ts +2 -0
  13. package/lib/jsx-runtime.js +28 -0
  14. package/lib/jsx-runtime.js.map +1 -0
  15. package/package.json +79 -0
  16. package/src/editor/NodeInspectorConfig.tsx +76 -0
  17. package/src/editor/PreviewOverlayConfig.tsx +65 -0
  18. package/src/editor/Provider.tsx +109 -0
  19. package/src/editor/SceneGraphTabConfig.tsx +87 -0
  20. package/src/editor/icons/CircleIcon.tsx +7 -0
  21. package/src/editor/icons/CodeIcon.tsx +8 -0
  22. package/src/editor/icons/CurveIcon.tsx +7 -0
  23. package/src/editor/icons/GridIcon.tsx +7 -0
  24. package/src/editor/icons/IconMap.ts +35 -0
  25. package/src/editor/icons/ImgIcon.tsx +8 -0
  26. package/src/editor/icons/LayoutIcon.tsx +9 -0
  27. package/src/editor/icons/LineIcon.tsx +7 -0
  28. package/src/editor/icons/NodeIcon.tsx +7 -0
  29. package/src/editor/icons/RayIcon.tsx +7 -0
  30. package/src/editor/icons/RectIcon.tsx +7 -0
  31. package/src/editor/icons/ShapeIcon.tsx +7 -0
  32. package/src/editor/icons/TxtIcon.tsx +8 -0
  33. package/src/editor/icons/VideoIcon.tsx +7 -0
  34. package/src/editor/icons/View2DIcon.tsx +10 -0
  35. package/src/editor/index.css +0 -0
  36. package/src/editor/index.ts +19 -0
  37. package/src/editor/shortcuts.ts +27 -0
  38. package/src/editor/tree/DetachedRoot.tsx +27 -0
  39. package/src/editor/tree/NodeElement.tsx +72 -0
  40. package/src/editor/tree/TreeElement.tsx +70 -0
  41. package/src/editor/tree/TreeRoot.tsx +10 -0
  42. package/src/editor/tree/ViewRoot.tsx +20 -0
  43. package/src/editor/tree/index.module.scss +45 -0
  44. package/src/editor/tree/index.ts +4 -0
  45. package/src/editor/tree/navigation.ts +145 -0
  46. package/src/editor/tsconfig.build.json +5 -0
  47. package/src/editor/tsconfig.json +12 -0
  48. package/src/editor/tsdoc.json +4 -0
  49. package/src/editor/utils/SignalSet.ts +37 -0
  50. package/src/editor/utils/index.ts +1 -0
  51. package/src/editor/vite-env.d.ts +1 -0
  52. package/src/lib/code/CodeCursor.ts +468 -0
  53. package/src/lib/code/CodeDiffer.ts +77 -0
  54. package/src/lib/code/CodeFragment.ts +96 -0
  55. package/src/lib/code/CodeHighlighter.ts +73 -0
  56. package/src/lib/code/CodeMetrics.ts +47 -0
  57. package/src/lib/code/CodeRange.test.ts +113 -0
  58. package/src/lib/code/CodeRange.ts +222 -0
  59. package/src/lib/code/CodeScope.ts +100 -0
  60. package/src/lib/code/CodeSelection.ts +28 -0
  61. package/src/lib/code/CodeSignal.ts +348 -0
  62. package/src/lib/code/CodeTokenizer.ts +54 -0
  63. package/src/lib/code/DefaultHighlightStyle.ts +98 -0
  64. package/src/lib/code/LezerHighlighter.ts +113 -0
  65. package/src/lib/code/diff.test.ts +311 -0
  66. package/src/lib/code/diff.ts +319 -0
  67. package/src/lib/code/extractRange.ts +125 -0
  68. package/src/lib/code/index.ts +13 -0
  69. package/src/lib/components/Bezier.ts +103 -0
  70. package/src/lib/components/Camera.ts +401 -0
  71. package/src/lib/components/Circle.ts +310 -0
  72. package/src/lib/components/Code.ts +532 -0
  73. package/src/lib/components/CubicBezier.ts +115 -0
  74. package/src/lib/components/Curve.ts +460 -0
  75. package/src/lib/components/Grid.ts +134 -0
  76. package/src/lib/components/Icon.ts +153 -0
  77. package/src/lib/components/Img.ts +328 -0
  78. package/src/lib/components/Knot.ts +156 -0
  79. package/src/lib/components/Latex.ts +537 -0
  80. package/src/lib/components/Layout.ts +1101 -0
  81. package/src/lib/components/Line.ts +394 -0
  82. package/src/lib/components/Node.ts +1941 -0
  83. package/src/lib/components/Path.ts +132 -0
  84. package/src/lib/components/Polygon.ts +266 -0
  85. package/src/lib/components/QuadBezier.ts +103 -0
  86. package/src/lib/components/Ray.ts +126 -0
  87. package/src/lib/components/Rect.ts +219 -0
  88. package/src/lib/components/SVG.ts +853 -0
  89. package/src/lib/components/Shape.ts +322 -0
  90. package/src/lib/components/Spline.ts +318 -0
  91. package/src/lib/components/Txt.test.tsx +81 -0
  92. package/src/lib/components/Txt.ts +204 -0
  93. package/src/lib/components/TxtLeaf.ts +210 -0
  94. package/src/lib/components/Video.ts +368 -0
  95. package/src/lib/components/View2D.ts +85 -0
  96. package/src/lib/components/__logs__/image-without-source.ts +18 -0
  97. package/src/lib/components/__logs__/line-without-points.ts +31 -0
  98. package/src/lib/components/__logs__/reactive-playback-rate.ts +22 -0
  99. package/src/lib/components/__logs__/spline-with-insufficient-knots.ts +25 -0
  100. package/src/lib/components/__tests__/Camera.test.tsx +73 -0
  101. package/src/lib/components/__tests__/children.test.tsx +142 -0
  102. package/src/lib/components/__tests__/clone.test.tsx +126 -0
  103. package/src/lib/components/__tests__/fontInheritance.test.tsx +102 -0
  104. package/src/lib/components/__tests__/generatorTest.ts +27 -0
  105. package/src/lib/components/__tests__/mockScene2D.ts +50 -0
  106. package/src/lib/components/__tests__/query.test.tsx +122 -0
  107. package/src/lib/components/__tests__/state.test.tsx +60 -0
  108. package/src/lib/components/index.ts +26 -0
  109. package/src/lib/components/types.ts +35 -0
  110. package/src/lib/curves/ArcSegment.ts +169 -0
  111. package/src/lib/curves/CircleSegment.ts +99 -0
  112. package/src/lib/curves/CubicBezierSegment.ts +80 -0
  113. package/src/lib/curves/CurveDrawingInfo.ts +11 -0
  114. package/src/lib/curves/CurvePoint.ts +15 -0
  115. package/src/lib/curves/CurveProfile.ts +28 -0
  116. package/src/lib/curves/KnotInfo.ts +10 -0
  117. package/src/lib/curves/LineSegment.ts +75 -0
  118. package/src/lib/curves/Polynomial.ts +355 -0
  119. package/src/lib/curves/Polynomial2D.ts +62 -0
  120. package/src/lib/curves/PolynomialSegment.ts +151 -0
  121. package/src/lib/curves/QuadBezierSegment.ts +66 -0
  122. package/src/lib/curves/Segment.ts +31 -0
  123. package/src/lib/curves/UniformPolynomialCurveSampler.ts +93 -0
  124. package/src/lib/curves/createCurveProfileLerp.ts +471 -0
  125. package/src/lib/curves/getBezierSplineProfile.ts +227 -0
  126. package/src/lib/curves/getCircleProfile.ts +86 -0
  127. package/src/lib/curves/getPathProfile.ts +177 -0
  128. package/src/lib/curves/getPointAtDistance.ts +21 -0
  129. package/src/lib/curves/getPolylineProfile.test.ts +21 -0
  130. package/src/lib/curves/getPolylineProfile.ts +88 -0
  131. package/src/lib/curves/getRectProfile.ts +138 -0
  132. package/src/lib/curves/index.ts +16 -0
  133. package/src/lib/decorators/canvasStyleSignal.ts +15 -0
  134. package/src/lib/decorators/colorSignal.ts +9 -0
  135. package/src/lib/decorators/compound.ts +85 -0
  136. package/src/lib/decorators/computed.ts +18 -0
  137. package/src/lib/decorators/defaultStyle.ts +15 -0
  138. package/src/lib/decorators/filtersSignal.ts +133 -0
  139. package/src/lib/decorators/index.ts +10 -0
  140. package/src/lib/decorators/initializers.ts +32 -0
  141. package/src/lib/decorators/nodeName.ts +13 -0
  142. package/src/lib/decorators/signal.test.ts +89 -0
  143. package/src/lib/decorators/signal.ts +348 -0
  144. package/src/lib/decorators/spacingSignal.ts +15 -0
  145. package/src/lib/decorators/transformSignals.test.ts +858 -0
  146. package/src/lib/decorators/transformSignals.ts +1633 -0
  147. package/src/lib/decorators/vector2Signal.ts +35 -0
  148. package/src/lib/globals.d.ts +2 -0
  149. package/src/lib/index.ts +9 -0
  150. package/src/lib/jsx-dev-runtime.ts +2 -0
  151. package/src/lib/jsx-runtime.ts +45 -0
  152. package/src/lib/morphers/PathMorpher.ts +8 -0
  153. package/src/lib/morphers/defaultMorpher.ts +43 -0
  154. package/src/lib/morphers/index.ts +4 -0
  155. package/src/lib/morphers/manimMorpher.ts +658 -0
  156. package/src/lib/parse-svg-path.d.ts +14 -0
  157. package/src/lib/partials/Filter.ts +185 -0
  158. package/src/lib/partials/Gradient.ts +103 -0
  159. package/src/lib/partials/Pattern.ts +35 -0
  160. package/src/lib/partials/RoughConfig.ts +180 -0
  161. package/src/lib/partials/ShaderConfig.ts +122 -0
  162. package/src/lib/partials/index.ts +5 -0
  163. package/src/lib/partials/types.ts +58 -0
  164. package/src/lib/scenes/Scene2D.ts +167 -0
  165. package/src/lib/scenes/index.ts +3 -0
  166. package/src/lib/scenes/makeScene2D.ts +19 -0
  167. package/src/lib/scenes/useScene2D.ts +6 -0
  168. package/src/lib/tsconfig.build.json +5 -0
  169. package/src/lib/tsconfig.json +11 -0
  170. package/src/lib/tsdoc.json +4 -0
  171. package/src/lib/utils/CanvasUtils.ts +304 -0
  172. package/src/lib/utils/PathDataBuilder.ts +320 -0
  173. package/src/lib/utils/diff.test.ts +453 -0
  174. package/src/lib/utils/diff.ts +148 -0
  175. package/src/lib/utils/index.ts +5 -0
  176. package/src/lib/utils/is.ts +11 -0
  177. package/src/lib/utils/makeSignalExtensions.ts +29 -0
  178. package/src/lib/utils/rough.ts +513 -0
  179. package/src/lib/utils/withDefaults.tsx +26 -0
  180. package/src/tsconfig.base.json +18 -0
  181. package/src/tsconfig.build.json +8 -0
  182. package/src/tsconfig.json +5 -0
  183. package/tsconfig.project.json +7 -0
@@ -0,0 +1,322 @@
1
+ import {
2
+ BBox,
3
+ SignalValue,
4
+ SimpleSignal,
5
+ createSignal,
6
+ easeOutExpo,
7
+ linear,
8
+ map,
9
+ threadable,
10
+ useRandom,
11
+ } from '@canvas-commons/core';
12
+ import {computed, initial, nodeName, signal} from '../decorators';
13
+ import {
14
+ CanvasStyleSignal,
15
+ canvasStyleSignal,
16
+ } from '../decorators/canvasStyleSignal';
17
+ import {PossibleCanvasStyle, RoughFillStyle} from '../partials';
18
+ import {createRoughConfig, drawRoughPath, resolveCanvasStyle} from '../utils';
19
+ import {Layout, LayoutProps} from './Layout';
20
+
21
+ export interface ShapeProps extends LayoutProps {
22
+ fill?: SignalValue<PossibleCanvasStyle>;
23
+ stroke?: SignalValue<PossibleCanvasStyle>;
24
+ strokeFirst?: SignalValue<boolean>;
25
+ lineWidth?: SignalValue<number>;
26
+ lineJoin?: SignalValue<CanvasLineJoin>;
27
+ lineCap?: SignalValue<CanvasLineCap>;
28
+ lineDash?: SignalValue<number[]>;
29
+ lineDashOffset?: SignalValue<number>;
30
+ antialiased?: SignalValue<boolean>;
31
+
32
+ /**
33
+ * Enable rough.js rendering.
34
+ */
35
+ rough?: SignalValue<boolean>;
36
+ /**
37
+ * {@inheritDoc RoughConfig.roughness}
38
+ */
39
+ roughness?: SignalValue<number>;
40
+ /**
41
+ * {@inheritDoc RoughConfig.bowing}
42
+ */
43
+ bowing?: SignalValue<number>;
44
+ /**
45
+ * {@inheritDoc RoughConfig.fillStyle}
46
+ */
47
+ roughFillStyle?: SignalValue<RoughFillStyle>;
48
+ /**
49
+ * {@inheritDoc RoughConfig.fillWeight}
50
+ */
51
+ roughFillWeight?: SignalValue<number>;
52
+ /**
53
+ * {@inheritDoc RoughConfig.hachureAngle}
54
+ */
55
+ roughHachureAngle?: SignalValue<number>;
56
+ /**
57
+ * {@inheritDoc RoughConfig.hachureGap}
58
+ */
59
+ roughHachureGap?: SignalValue<number>;
60
+ /**
61
+ * {@inheritDoc RoughConfig.seed}
62
+ */
63
+ roughSeed?: SignalValue<number>;
64
+ /**
65
+ * {@inheritDoc RoughConfig.disableMultiStroke}
66
+ */
67
+ roughDisableMultiStroke?: SignalValue<boolean>;
68
+ /**
69
+ * {@inheritDoc RoughConfig.disableMultiStrokeFill}
70
+ */
71
+ roughDisableMultiStrokeFill?: SignalValue<boolean>;
72
+ }
73
+
74
+ @nodeName('Shape')
75
+ export abstract class Shape extends Layout {
76
+ @canvasStyleSignal()
77
+ declare public readonly fill: CanvasStyleSignal<this>;
78
+ @canvasStyleSignal()
79
+ declare public readonly stroke: CanvasStyleSignal<this>;
80
+ @initial(false)
81
+ @signal()
82
+ declare public readonly strokeFirst: SimpleSignal<boolean, this>;
83
+ @initial(0)
84
+ @signal()
85
+ declare public readonly lineWidth: SimpleSignal<number, this>;
86
+ @initial('miter')
87
+ @signal()
88
+ declare public readonly lineJoin: SimpleSignal<CanvasLineJoin, this>;
89
+ @initial('butt')
90
+ @signal()
91
+ declare public readonly lineCap: SimpleSignal<CanvasLineCap, this>;
92
+ @initial([])
93
+ @signal()
94
+ declare public readonly lineDash: SimpleSignal<number[], this>;
95
+ @initial(0)
96
+ @signal()
97
+ declare public readonly lineDashOffset: SimpleSignal<number, this>;
98
+ @initial(true)
99
+ @signal()
100
+ declare public readonly antialiased: SimpleSignal<boolean, this>;
101
+
102
+ // Rough.js signals
103
+ /**
104
+ * Enable rough.js rendering.
105
+ */
106
+ @initial(false)
107
+ @signal()
108
+ declare public readonly rough: SimpleSignal<boolean, this>;
109
+ /**
110
+ * {@inheritDoc RoughConfig.roughness}
111
+ */
112
+ @initial(1)
113
+ @signal()
114
+ declare public readonly roughness: SimpleSignal<number, this>;
115
+ /**
116
+ * {@inheritDoc RoughConfig.bowing}
117
+ */
118
+ @initial(1)
119
+ @signal()
120
+ declare public readonly bowing: SimpleSignal<number, this>;
121
+ /**
122
+ * {@inheritDoc RoughConfig.fillStyle}
123
+ */
124
+ @initial('hachure')
125
+ @signal()
126
+ declare public readonly roughFillStyle: SimpleSignal<RoughFillStyle, this>;
127
+ /**
128
+ * {@inheritDoc RoughConfig.fillWeight}
129
+ */
130
+ @signal()
131
+ declare public readonly roughFillWeight: SimpleSignal<
132
+ number | undefined,
133
+ this
134
+ >;
135
+ /**
136
+ * {@inheritDoc RoughConfig.hachureAngle}
137
+ */
138
+ @initial(-41)
139
+ @signal()
140
+ declare public readonly roughHachureAngle: SimpleSignal<number, this>;
141
+ /**
142
+ * {@inheritDoc RoughConfig.hachureGap}
143
+ */
144
+ @initial(4)
145
+ @signal()
146
+ declare public readonly roughHachureGap: SimpleSignal<number, this>;
147
+ /**
148
+ * {@inheritDoc RoughConfig.seed}
149
+ */
150
+ @signal()
151
+ declare public readonly roughSeed: SimpleSignal<number | undefined, this>;
152
+ /**
153
+ * {@inheritDoc RoughConfig.disableMultiStroke}
154
+ */
155
+ @initial(false)
156
+ @signal()
157
+ declare public readonly roughDisableMultiStroke: SimpleSignal<boolean, this>;
158
+ /**
159
+ * {@inheritDoc RoughConfig.disableMultiStrokeFill}
160
+ */
161
+ @initial(false)
162
+ @signal()
163
+ declare public readonly roughDisableMultiStrokeFill: SimpleSignal<
164
+ boolean,
165
+ this
166
+ >;
167
+
168
+ protected readonly rippleStrength = createSignal<number, this>(0);
169
+
170
+ @computed()
171
+ protected rippleSize() {
172
+ return easeOutExpo(this.rippleStrength(), 0, 50);
173
+ }
174
+
175
+ public constructor(props: ShapeProps) {
176
+ super(props);
177
+ if (props.roughSeed === undefined) {
178
+ this.roughSeed(useRandom().nextInt(0, Number.MAX_SAFE_INTEGER));
179
+ }
180
+ }
181
+
182
+ protected applyText(context: CanvasRenderingContext2D) {
183
+ context.direction = this.textDirection();
184
+ this.element.dir = this.textDirection();
185
+ }
186
+
187
+ protected applyStyle(context: CanvasRenderingContext2D) {
188
+ context.fillStyle = resolveCanvasStyle(this.fill(), context);
189
+ context.strokeStyle = resolveCanvasStyle(this.stroke(), context);
190
+ context.lineWidth = this.lineWidth();
191
+ context.lineJoin = this.lineJoin();
192
+ context.lineCap = this.lineCap();
193
+ context.setLineDash(this.lineDash());
194
+ context.lineDashOffset = this.lineDashOffset();
195
+ if (!this.antialiased()) {
196
+ // from https://stackoverflow.com/a/68372384
197
+ context.filter =
198
+ 'url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxmaWx0ZXIgaWQ9ImZpbHRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj48ZmVDb21wb25lbnRUcmFuc2Zlcj48ZmVGdW5jUiB0eXBlPSJpZGVudGl0eSIvPjxmZUZ1bmNHIHR5cGU9ImlkZW50aXR5Ii8+PGZlRnVuY0IgdHlwZT0iaWRlbnRpdHkiLz48ZmVGdW5jQSB0eXBlPSJkaXNjcmV0ZSIgdGFibGVWYWx1ZXM9IjAgMSIvPjwvZmVDb21wb25lbnRUcmFuc2Zlcj48L2ZpbHRlcj48L3N2Zz4=#filter)';
199
+ }
200
+ }
201
+
202
+ protected override draw(context: CanvasRenderingContext2D) {
203
+ this.drawShape(context);
204
+ if (this.clip()) {
205
+ context.clip(this.getPath());
206
+ }
207
+ this.drawChildren(context);
208
+ }
209
+
210
+ protected drawShape(context: CanvasRenderingContext2D) {
211
+ if (this.rough()) {
212
+ this.drawShapeRough(context);
213
+ } else {
214
+ const path = this.getPath();
215
+ const hasStroke = this.lineWidth() > 0 && this.stroke() !== null;
216
+ const hasFill = this.fill() !== null;
217
+ context.save();
218
+ this.applyStyle(context);
219
+ this.drawRipple(context);
220
+ if (this.strokeFirst()) {
221
+ hasStroke && context.stroke(path);
222
+ hasFill && context.fill(path);
223
+ } else {
224
+ hasFill && context.fill(path);
225
+ hasStroke && context.stroke(path);
226
+ }
227
+ context.restore();
228
+ }
229
+ }
230
+
231
+ protected drawShapeRough(context: CanvasRenderingContext2D) {
232
+ const pathData = this.getPathData();
233
+ if (!pathData) {
234
+ return;
235
+ }
236
+
237
+ context.save();
238
+
239
+ if (!this.antialiased()) {
240
+ context.filter =
241
+ 'url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxmaWx0ZXIgaWQ9ImZpbHRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj48ZmVDb21wb25lbnRUcmFuc2Zlcj48ZmVGdW5jUiB0eXBlPSJpZGVudGl0eSIvPjxmZUZ1bmNHIHR5cGU9ImlkZW50aXR5Ii8+PGZlRnVuY0IgdHlwZT0iaWRlbnRpdHkiLz48ZmVGdW5jQSB0eXBlPSJkaXNjcmV0ZSIgdGFibGVWYWx1ZXM9IjAgMSIvPjwvZmVDb21wb25lbnRUcmFuc2Zlcj48L2ZpbHRlcj48L3N2Zz4=#filter)';
242
+ }
243
+
244
+ const seed = this.roughSeed();
245
+ if (seed === undefined) {
246
+ context.restore();
247
+ return;
248
+ }
249
+
250
+ const roughConfig = createRoughConfig(
251
+ this.roughness(),
252
+ this.bowing(),
253
+ this.roughFillStyle(),
254
+ this.roughFillWeight(),
255
+ this.roughHachureAngle(),
256
+ this.roughHachureGap(),
257
+ seed,
258
+ this.roughDisableMultiStroke(),
259
+ this.roughDisableMultiStrokeFill(),
260
+ );
261
+
262
+ drawRoughPath(
263
+ context,
264
+ pathData,
265
+ roughConfig,
266
+ this.fill(),
267
+ this.stroke(),
268
+ this.lineWidth(),
269
+ );
270
+
271
+ context.restore();
272
+ }
273
+
274
+ protected override getCacheBBox(): BBox {
275
+ return super.getCacheBBox().expand(this.lineWidth() / 2);
276
+ }
277
+
278
+ @computed()
279
+ protected getPath(): Path2D {
280
+ return new Path2D();
281
+ }
282
+
283
+ /**
284
+ * Get the SVG path data string for this shape.
285
+ *
286
+ * @remarks
287
+ * This method returns an SVG path data string that represents the shape's
288
+ * geometry. The default implementation returns an empty string. Subclasses
289
+ * should override this method to provide their actual path data.
290
+ *
291
+ * The path data can be used to create Path2D objects, passed to Rough.js,
292
+ * or used for SVG export.
293
+ *
294
+ * @returns An SVG path data string (e.g., "M 0 0 L 100 100 Z")
295
+ */
296
+ @computed()
297
+ protected getPathData(): string {
298
+ return '';
299
+ }
300
+
301
+ protected getRipplePath(): Path2D {
302
+ return new Path2D();
303
+ }
304
+
305
+ protected drawRipple(context: CanvasRenderingContext2D) {
306
+ const rippleStrength = this.rippleStrength();
307
+ if (rippleStrength > 0) {
308
+ const ripplePath = this.getRipplePath();
309
+ context.save();
310
+ context.globalAlpha *= map(0.54, 0, rippleStrength);
311
+ context.fill(ripplePath);
312
+ context.restore();
313
+ }
314
+ }
315
+
316
+ @threadable()
317
+ public *ripple(duration = 1) {
318
+ this.rippleStrength(0);
319
+ yield* this.rippleStrength(1, duration, linear);
320
+ this.rippleStrength(0);
321
+ }
322
+ }
@@ -0,0 +1,318 @@
1
+ import {
2
+ BBox,
3
+ PossibleVector2,
4
+ SerializedVector2,
5
+ SignalValue,
6
+ SimpleSignal,
7
+ Vector2,
8
+ unwrap,
9
+ useLogger,
10
+ } from '@canvas-commons/core';
11
+ import {
12
+ CubicBezierSegment,
13
+ CurveProfile,
14
+ KnotInfo,
15
+ getBezierSplineProfile,
16
+ } from '../curves';
17
+ import {PolynomialSegment} from '../curves/PolynomialSegment';
18
+ import {computed, initial, signal} from '../decorators';
19
+ import {DesiredLength} from '../partials';
20
+ import {
21
+ arc,
22
+ bezierCurveTo,
23
+ drawLine,
24
+ drawPivot,
25
+ lineTo,
26
+ moveTo,
27
+ quadraticCurveTo,
28
+ } from '../utils';
29
+ import splineWithInsufficientKnots from './__logs__/spline-with-insufficient-knots';
30
+ import {Curve, CurveProps} from './Curve';
31
+ import {Knot} from './Knot';
32
+ import {Node} from './Node';
33
+
34
+ export interface SplineProps extends CurveProps {
35
+ /**
36
+ * {@inheritDoc Spline.smoothness}
37
+ */
38
+ smoothness?: SignalValue<number>;
39
+
40
+ /**
41
+ * {@inheritDoc Spline.points}
42
+ */
43
+ points?: SignalValue<SignalValue<PossibleVector2[]>>;
44
+ }
45
+
46
+ /**
47
+ * A node for drawing a smooth line through a number of points.
48
+ *
49
+ * @remarks
50
+ * This node uses Bézier curves for drawing each segment of the spline.
51
+ *
52
+ * @example
53
+ * Defining knots using the `points` property. This will automatically
54
+ * calculate the handle positions for each knot do draw a smooth curve. You
55
+ * can control the smoothness of the resulting curve via the
56
+ * {@link Spline.smoothness} property:
57
+ *
58
+ * ```tsx
59
+ * <Spline
60
+ * lineWidth={4}
61
+ * stroke={'white'}
62
+ * smoothness={0.4}
63
+ * points={[
64
+ * [-400, 0],
65
+ * [-200, -300],
66
+ * [0, 0],
67
+ * [200, -300],
68
+ * [400, 0],
69
+ * ]}
70
+ * />
71
+ * ```
72
+ *
73
+ * Defining knots with {@link Knot} nodes:
74
+ *
75
+ * ```tsx
76
+ * <Spline lineWidth={4} stroke={'white'}>
77
+ * <Knot position={[-400, 0]} />
78
+ * <Knot position={[-200, -300]} />
79
+ * <Knot
80
+ * position={[0, 0]}
81
+ * startHandle={[-100, 200]}
82
+ * endHandle={[100, 200]}
83
+ * />
84
+ * <Knot position={[200, -300]} />
85
+ * <Knot position={[400, 0]} />
86
+ * </Spline>
87
+ * ```
88
+ */
89
+ export class Spline extends Curve {
90
+ /**
91
+ * The smoothness of the spline when using auto-calculated handles.
92
+ *
93
+ * @remarks
94
+ * This property is only applied to knots that don't use explicit handles.
95
+ *
96
+ * @defaultValue 0.4
97
+ */
98
+ @initial(0.4)
99
+ @signal()
100
+ declare public readonly smoothness: SimpleSignal<number>;
101
+
102
+ /**
103
+ * The knots of the spline as an array of knots with auto-calculated handles.
104
+ *
105
+ * @remarks
106
+ * You can control the smoothness of the resulting curve
107
+ * via the {@link smoothness} property.
108
+ */
109
+ @initial(null)
110
+ @signal()
111
+ declare public readonly points: SimpleSignal<
112
+ SignalValue<PossibleVector2>[] | null,
113
+ this
114
+ >;
115
+
116
+ public constructor(props: SplineProps) {
117
+ super(props);
118
+
119
+ if (
120
+ (props.children === undefined ||
121
+ !Array.isArray(props.children) ||
122
+ props.children.length < 2) &&
123
+ (props.points === undefined ||
124
+ (typeof props.points !== 'function' && props.points.length < 2)) &&
125
+ props.spawner === undefined
126
+ ) {
127
+ useLogger().warn({
128
+ message:
129
+ 'Insufficient number of knots specified for spline. A spline needs at least two knots.',
130
+ remarks: splineWithInsufficientKnots,
131
+ inspect: this.key,
132
+ });
133
+ }
134
+ }
135
+
136
+ @computed()
137
+ public override profile(): CurveProfile {
138
+ return getBezierSplineProfile(
139
+ this.knots(),
140
+ this.closed(),
141
+ this.smoothness(),
142
+ );
143
+ }
144
+
145
+ @computed()
146
+ public knots(): KnotInfo[] {
147
+ const points = this.points();
148
+
149
+ if (points) {
150
+ return points.map(signal => {
151
+ const point = new Vector2(unwrap(signal));
152
+
153
+ return {
154
+ position: point,
155
+ startHandle: point,
156
+ endHandle: point,
157
+ auto: {start: 1, end: 1},
158
+ };
159
+ });
160
+ }
161
+
162
+ return this.children()
163
+ .filter(this.isKnot)
164
+ .map(knot => knot.points());
165
+ }
166
+
167
+ @computed()
168
+ protected childrenBBox() {
169
+ const points = (this.profile().segments as PolynomialSegment[]).flatMap(
170
+ segment => segment.points,
171
+ );
172
+ return BBox.fromPoints(...points);
173
+ }
174
+
175
+ protected override lineWidthCoefficient(): number {
176
+ const join = this.lineJoin();
177
+
178
+ let coefficient = super.lineWidthCoefficient();
179
+
180
+ if (join !== 'miter') {
181
+ return coefficient;
182
+ }
183
+
184
+ const {minSin} = this.profile();
185
+ if (minSin > 0) {
186
+ coefficient = Math.max(coefficient, 0.5 / minSin);
187
+ }
188
+
189
+ return coefficient;
190
+ }
191
+
192
+ protected override desiredSize(): SerializedVector2<DesiredLength> {
193
+ return this.getTightBBox().size;
194
+ }
195
+
196
+ protected override offsetComputedLayout(box: BBox): BBox {
197
+ box.position = box.position.sub(this.getTightBBox().center);
198
+ return box;
199
+ }
200
+
201
+ @computed()
202
+ private getTightBBox(): BBox {
203
+ const bounds = (this.profile().segments as PolynomialSegment[]).map(
204
+ segment => segment.getBBox(),
205
+ );
206
+ return BBox.fromBBoxes(...bounds);
207
+ }
208
+
209
+ public override drawOverlay(
210
+ context: CanvasRenderingContext2D,
211
+ matrix: DOMMatrix,
212
+ ) {
213
+ const size = this.computedSize();
214
+ const box = this.childrenBBox().transformCorners(matrix);
215
+ const offset = size.mul(this.anchor()).scale(0.5).transformAsPoint(matrix);
216
+ const segments = this.profile().segments as PolynomialSegment[];
217
+
218
+ context.lineWidth = 1;
219
+ context.strokeStyle = 'white';
220
+ context.fillStyle = 'white';
221
+
222
+ const splinePath = new Path2D();
223
+
224
+ // Draw the actual spline first so that all control points get drawn on top of it.
225
+ for (let i = 0; i < segments.length; i++) {
226
+ const segment = segments[i];
227
+ const [from, startHandle, endHandle, to] =
228
+ segment.transformPoints(matrix);
229
+
230
+ moveTo(splinePath, from);
231
+ if (segment instanceof CubicBezierSegment) {
232
+ bezierCurveTo(splinePath, startHandle, endHandle, to as Vector2);
233
+ } else {
234
+ quadraticCurveTo(splinePath, startHandle, endHandle);
235
+ }
236
+ }
237
+ context.stroke(splinePath);
238
+
239
+ for (let i = 0; i < segments.length; i++) {
240
+ const segment = segments[i];
241
+ context.fillStyle = 'white';
242
+
243
+ const [from, startHandle, endHandle, to] =
244
+ segment.transformPoints(matrix);
245
+
246
+ const handlePath = new Path2D();
247
+
248
+ context.globalAlpha = 0.5;
249
+ // Line from p0 to p1
250
+ moveTo(handlePath, from);
251
+ lineTo(handlePath, startHandle);
252
+
253
+ if (segment instanceof CubicBezierSegment) {
254
+ // Line from p2 to p3
255
+ moveTo(handlePath, endHandle);
256
+ lineTo(handlePath, to as Vector2);
257
+ context.beginPath();
258
+ context.stroke(handlePath);
259
+ } else {
260
+ // Line from p1 to p2
261
+ lineTo(handlePath, endHandle);
262
+ context.beginPath();
263
+ context.stroke(handlePath);
264
+ }
265
+
266
+ context.globalAlpha = 1;
267
+ context.lineWidth = 2;
268
+
269
+ // Draw first point of segment
270
+ moveTo(context, from);
271
+ context.beginPath();
272
+ arc(context, from, 4);
273
+ context.closePath();
274
+ context.stroke();
275
+ context.fill();
276
+
277
+ // Draw final point of segment only if we're on the last segment.
278
+ // Otherwise, it will get drawn as the start point of the next segment.
279
+ if (i === segments.length - 1) {
280
+ if (to !== undefined) {
281
+ moveTo(context, to);
282
+ context.beginPath();
283
+ arc(context, to, 4);
284
+ context.closePath();
285
+ context.stroke();
286
+ context.fill();
287
+ }
288
+ }
289
+
290
+ // Draw the control points
291
+ context.fillStyle = 'black';
292
+ for (const point of [startHandle, endHandle]) {
293
+ if (point.magnitude > 0) {
294
+ moveTo(context, point);
295
+ context.beginPath();
296
+ arc(context, point, 4);
297
+ context.closePath();
298
+ context.fill();
299
+ context.stroke();
300
+ }
301
+ }
302
+ }
303
+
304
+ context.lineWidth = 1;
305
+ context.beginPath();
306
+ drawPivot(context, offset);
307
+ context.stroke();
308
+
309
+ context.beginPath();
310
+ drawLine(context, box);
311
+ context.closePath();
312
+ context.stroke();
313
+ }
314
+
315
+ private isKnot(node: Node): node is Knot {
316
+ return node instanceof Knot;
317
+ }
318
+ }
@@ -0,0 +1,81 @@
1
+ import {linear, waitFor} from '@canvas-commons/core';
2
+ import {describe, expect, it, vi} from 'vitest';
3
+ import {Txt} from './Txt';
4
+ import {TxtLeaf} from './TxtLeaf';
5
+ import {generatorTest} from './__tests__/generatorTest';
6
+ import {mockScene2D} from './__tests__/mockScene2D';
7
+
8
+ describe('Txt', () => {
9
+ mockScene2D();
10
+
11
+ it('Handle plain text', () => {
12
+ const node = (<Txt lineWidth={8}>test</Txt>) as Txt;
13
+
14
+ const parseSpy = vi.spyOn(node as any, 'parseChildren');
15
+ const leaf = node.childAs<TxtLeaf>(0);
16
+
17
+ expect(node.text()).toBe('test');
18
+ expect(node.lineWidth()).toBe(8);
19
+ expect(node.children().length).toBe(1);
20
+ expect(leaf).toBeInstanceOf(TxtLeaf);
21
+ expect(leaf!.text()).toBe('test');
22
+ expect(leaf!.lineWidth()).toBe(8);
23
+
24
+ node.lineWidth(16);
25
+ node.text('changed');
26
+
27
+ expect(node.childAs(0)).toBe(leaf);
28
+ expect(leaf!.lineWidth()).toBe(16);
29
+ expect(leaf!.text()).toBe('changed');
30
+
31
+ // Parsing should not happen when operating exclusively on simple text
32
+ expect(parseSpy).toHaveBeenCalledTimes(0);
33
+ });
34
+
35
+ it('Handle complex text', () => {
36
+ const node = (
37
+ <Txt lineWidth={8}>
38
+ Apple <Txt>Banana</Txt> Cherry
39
+ </Txt>
40
+ ) as Txt;
41
+
42
+ const first = node.childAs<TxtLeaf>(0);
43
+ const second = node.childAs<Txt>(1);
44
+ const third = node.childAs<TxtLeaf>(2);
45
+
46
+ expect(node.text()).toBe('Apple Banana Cherry');
47
+ expect(node.lineWidth()).toBe(8);
48
+ expect(node.children().length).toBe(3);
49
+ expect(first).toBeInstanceOf(TxtLeaf);
50
+ expect(first!.text()).toBe('Apple ');
51
+ expect(first!.lineWidth()).toBe(8);
52
+
53
+ expect(second).toBeInstanceOf(Txt);
54
+ expect(second!.text()).toBe('Banana');
55
+ expect(second!.lineWidth()).toBe(8);
56
+
57
+ expect(third).toBeInstanceOf(TxtLeaf);
58
+ expect(third!.text()).toBe(' Cherry');
59
+ expect(third!.lineWidth()).toBe(8);
60
+ });
61
+
62
+ it(
63
+ 'Tween complex to simple text',
64
+ generatorTest(function* () {
65
+ const node = (
66
+ <Txt>
67
+ <Txt>Apple</Txt> Banana
68
+ </Txt>
69
+ ) as Txt;
70
+
71
+ yield node.text('Simple', 2, linear);
72
+ yield* waitFor(1);
73
+
74
+ const leaf = node.childAs<TxtLeaf>(0)!;
75
+
76
+ expect(node.children().length).toBe(1);
77
+ expect(node.text()).toBe('Apple Ban');
78
+ expect(leaf.text()).toBe('Apple Ban');
79
+ }),
80
+ );
81
+ });