@lvl99dotcom/paintcanvas 0.0.0-stage → 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 (284) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +131 -2
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +5 -0
  5. package/dist/lib/brushes/BrushTipCache/index.d.ts +32 -0
  6. package/dist/lib/brushes/BrushTipCache/index.js +60 -0
  7. package/dist/lib/brushes/StrokeEngine/index.d.ts +171 -0
  8. package/dist/lib/brushes/StrokeEngine/index.js +324 -0
  9. package/dist/lib/brushes/StrokePath/index.d.ts +53 -0
  10. package/dist/lib/brushes/StrokePath/index.js +161 -0
  11. package/dist/lib/brushes/StrokeSampler/index.d.ts +55 -0
  12. package/dist/lib/brushes/StrokeSampler/index.js +75 -0
  13. package/dist/lib/brushes/authors/index.d.ts +55 -0
  14. package/dist/lib/brushes/authors/index.js +60 -0
  15. package/dist/lib/brushes/configRanges/index.d.ts +52 -0
  16. package/dist/lib/brushes/configRanges/index.js +206 -0
  17. package/dist/lib/brushes/index.d.ts +5 -0
  18. package/dist/lib/brushes/index.js +19 -0
  19. package/dist/lib/brushes/params/index.d.ts +108 -0
  20. package/dist/lib/brushes/params/index.js +139 -0
  21. package/dist/lib/brushes/presetFile/index.d.ts +32 -0
  22. package/dist/lib/brushes/presetFile/index.js +55 -0
  23. package/dist/lib/brushes/presets/drawing/brush-pen.js +3 -0
  24. package/dist/lib/brushes/presets/drawing/felt-pen.js +3 -0
  25. package/dist/lib/brushes/presets/drawing/index.d.ts +13 -0
  26. package/dist/lib/brushes/presets/drawing/index.js +88 -0
  27. package/dist/lib/brushes/presets/drawing/pencil-2b.js +3 -0
  28. package/dist/lib/brushes/presets/drawing/pencil-hb.js +3 -0
  29. package/dist/lib/brushes/presets/painting/grass.js +3 -0
  30. package/dist/lib/brushes/presets/painting/hair.js +3 -0
  31. package/dist/lib/brushes/presets/painting/index.d.ts +18 -0
  32. package/dist/lib/brushes/presets/painting/index.js +118 -0
  33. package/dist/lib/brushes/presets/painting/splatter.js +3 -0
  34. package/dist/lib/brushes/presets/painting/textured-paint.js +3 -0
  35. package/dist/lib/brushes/presets/painting/watery-paint.js +3 -0
  36. package/dist/lib/brushes/presets/round/index.d.ts +9 -0
  37. package/dist/lib/brushes/presets/round/index.js +44 -0
  38. package/dist/lib/brushes/presets/round/round.js +3 -0
  39. package/dist/lib/brushes/presets/round/soft-round.js +3 -0
  40. package/dist/lib/brushes/presets/sized/index.d.ts +20 -0
  41. package/dist/lib/brushes/presets/sized/index.js +30 -0
  42. package/dist/lib/brushes/presets/sketching/index.d.ts +14 -0
  43. package/dist/lib/brushes/presets/sketching/index.js +107 -0
  44. package/dist/lib/brushes/presets/square/index.d.ts +7 -0
  45. package/dist/lib/brushes/presets/square/index.js +22 -0
  46. package/dist/lib/brushes/presets/square/square.js +3 -0
  47. package/dist/lib/brushes/shapeImage/index.d.ts +40 -0
  48. package/dist/lib/brushes/shapeImage/index.js +61 -0
  49. package/dist/lib/brushes/types/index.d.ts +243 -0
  50. package/dist/lib/brushes/types/index.js +20 -0
  51. package/dist/lib/classes/BackgroundWork/index.d.ts +55 -0
  52. package/dist/lib/classes/BackgroundWork/index.js +97 -0
  53. package/dist/lib/classes/BrushSettings/index.d.ts +91 -0
  54. package/dist/lib/classes/BrushSettings/index.js +152 -0
  55. package/dist/lib/classes/CustomPresets/index.d.ts +53 -0
  56. package/dist/lib/classes/CustomPresets/index.js +71 -0
  57. package/dist/lib/classes/ExternalSheets/index.d.ts +64 -0
  58. package/dist/lib/classes/ExternalSheets/index.js +112 -0
  59. package/dist/lib/classes/GestureTracker/index.d.ts +62 -0
  60. package/dist/lib/classes/GestureTracker/index.js +122 -0
  61. package/dist/lib/classes/History/index.d.ts +83 -0
  62. package/dist/lib/classes/History/index.js +126 -0
  63. package/dist/lib/classes/HistoryGuard/index.d.ts +58 -0
  64. package/dist/lib/classes/HistoryGuard/index.js +97 -0
  65. package/dist/lib/classes/InputSettings/index.d.ts +74 -0
  66. package/dist/lib/classes/InputSettings/index.js +101 -0
  67. package/dist/lib/classes/KeyboardShortcuts/index.d.ts +48 -0
  68. package/dist/lib/classes/KeyboardShortcuts/index.js +137 -0
  69. package/dist/lib/classes/Layer/index.d.ts +86 -0
  70. package/dist/lib/classes/Layer/index.js +111 -0
  71. package/dist/lib/classes/LayerAutosave/index.d.ts +180 -0
  72. package/dist/lib/classes/LayerAutosave/index.js +346 -0
  73. package/dist/lib/classes/LayerStack/index.d.ts +177 -0
  74. package/dist/lib/classes/LayerStack/index.js +394 -0
  75. package/dist/lib/classes/PaintCanvas/index.d.ts +537 -0
  76. package/dist/lib/classes/PaintCanvas/index.js +965 -0
  77. package/dist/lib/classes/StrokeLog/index.d.ts +149 -0
  78. package/dist/lib/classes/StrokeLog/index.js +257 -0
  79. package/dist/lib/classes/Viewport/index.d.ts +108 -0
  80. package/dist/lib/classes/Viewport/index.js +194 -0
  81. package/dist/lib/document/index.d.ts +17 -0
  82. package/dist/lib/document/index.js +25 -0
  83. package/dist/lib/document/schema.d.ts +132 -0
  84. package/dist/lib/document/schema.js +110 -0
  85. package/dist/lib/i18n/index.d.ts +94 -0
  86. package/dist/lib/i18n/index.js +182 -0
  87. package/dist/lib/image/index.d.ts +90 -0
  88. package/dist/lib/image/index.js +147 -0
  89. package/dist/lib/messages/PatchSender/index.d.ts +75 -0
  90. package/dist/lib/messages/PatchSender/index.js +125 -0
  91. package/dist/lib/messages/index.d.ts +26 -0
  92. package/dist/lib/messages/index.js +59 -0
  93. package/dist/lib/messages/schema.d.ts +1775 -0
  94. package/dist/lib/messages/schema.js +192 -0
  95. package/dist/lib/presets/index.d.ts +32 -0
  96. package/dist/lib/presets/index.js +35 -0
  97. package/dist/lib/presets/schema.d.ts +753 -0
  98. package/dist/lib/presets/schema.js +181 -0
  99. package/dist/lib/rendering/BrushTip/index.d.ts +63 -0
  100. package/dist/lib/rendering/BrushTip/index.js +112 -0
  101. package/dist/lib/rendering/checkerboard/index.d.ts +22 -0
  102. package/dist/lib/rendering/checkerboard/index.js +31 -0
  103. package/dist/lib/rendering/compositing/index.d.ts +64 -0
  104. package/dist/lib/rendering/compositing/index.js +124 -0
  105. package/dist/lib/rendering/cursor/index.d.ts +14 -0
  106. package/dist/lib/rendering/cursor/index.js +39 -0
  107. package/dist/lib/rendering/cursors/index.d.ts +18 -0
  108. package/dist/lib/rendering/cursors/index.js +23 -0
  109. package/dist/lib/rendering/maskOutline/index.d.ts +22 -0
  110. package/dist/lib/rendering/maskOutline/index.js +78 -0
  111. package/dist/lib/rendering/outline/index.d.ts +36 -0
  112. package/dist/lib/rendering/outline/index.js +120 -0
  113. package/dist/lib/rendering/sample/index.d.ts +55 -0
  114. package/dist/lib/rendering/sample/index.js +83 -0
  115. package/dist/lib/rendering/screenshot/index.d.ts +67 -0
  116. package/dist/lib/rendering/screenshot/index.js +96 -0
  117. package/dist/lib/rendering/transformBox/index.d.ts +10 -0
  118. package/dist/lib/rendering/transformBox/index.js +57 -0
  119. package/dist/lib/selection/Lasso/index.d.ts +70 -0
  120. package/dist/lib/selection/Lasso/index.js +156 -0
  121. package/dist/lib/selection/Selection/index.d.ts +86 -0
  122. package/dist/lib/selection/Selection/index.js +228 -0
  123. package/dist/lib/selection/geometry/index.d.ts +37 -0
  124. package/dist/lib/selection/geometry/index.js +57 -0
  125. package/dist/lib/storage/index.d.ts +63 -0
  126. package/dist/lib/storage/index.js +114 -0
  127. package/dist/lib/transform/Transformer/index.d.ts +124 -0
  128. package/dist/lib/transform/Transformer/index.js +286 -0
  129. package/dist/lib/transform/geometry/index.d.ts +95 -0
  130. package/dist/lib/transform/geometry/index.js +258 -0
  131. package/dist/lib/utils/array/index.d.ts +10 -0
  132. package/dist/lib/utils/array/index.js +21 -0
  133. package/dist/lib/utils/gestures/index.d.ts +35 -0
  134. package/dist/lib/utils/gestures/index.js +81 -0
  135. package/dist/lib/utils/navigation/index.d.ts +51 -0
  136. package/dist/lib/utils/navigation/index.js +59 -0
  137. package/dist/lib/utils/random/index.d.ts +18 -0
  138. package/dist/lib/utils/random/index.js +27 -0
  139. package/dist/lib/utils/shortcuts/index.d.ts +74 -0
  140. package/dist/lib/utils/shortcuts/index.js +189 -0
  141. package/dist/lib/validation/validators.generated.d.ts +27 -0
  142. package/dist/lib/validation/validators.generated.js +19826 -0
  143. package/dist/locales/en-GB.js +188 -0
  144. package/dist/locales/en-US.js +188 -0
  145. package/dist/locales/fr.js +188 -0
  146. package/dist/locales/ja.js +188 -0
  147. package/dist/locales/ko.js +188 -0
  148. package/dist/locales/zh-Hans.js +188 -0
  149. package/dist/locales/zh-Hant.js +188 -0
  150. package/dist/package.js +3 -0
  151. package/dist/testing/app.d.ts +7 -0
  152. package/dist/testing/app.js +16 -0
  153. package/dist/testing/elements.d.ts +42 -0
  154. package/dist/testing/elements.js +58 -0
  155. package/dist/testing/fakeCanvas.d.ts +21 -0
  156. package/dist/testing/fakeCanvas.js +26 -0
  157. package/dist/testing/layerStore.d.ts +26 -0
  158. package/dist/testing/layerStore.js +43 -0
  159. package/dist/testing/locks.d.ts +15 -0
  160. package/dist/testing/locks.js +41 -0
  161. package/dist/testing/recordingCanvas.d.ts +14 -0
  162. package/dist/testing/recordingCanvas.js +28 -0
  163. package/dist/testing/recordingContext.d.ts +10 -0
  164. package/dist/testing/recordingContext.js +19 -0
  165. package/dist/ui/ChangeController/index.d.ts +40 -0
  166. package/dist/ui/ChangeController/index.js +62 -0
  167. package/dist/ui/base/index.d.ts +69 -0
  168. package/dist/ui/base/index.js +94 -0
  169. package/dist/ui/elements/PaintCanvas/index.d.ts +249 -0
  170. package/dist/ui/elements/PaintCanvas/index.js +584 -0
  171. package/dist/ui/elements/PaintCanvas/style.js +3 -0
  172. package/dist/ui/elements/PcBrushConfig/index.d.ts +130 -0
  173. package/dist/ui/elements/PcBrushConfig/index.js +463 -0
  174. package/dist/ui/elements/PcBrushConfig/style.js +3 -0
  175. package/dist/ui/elements/PcBrushMenu/index.d.ts +121 -0
  176. package/dist/ui/elements/PcBrushMenu/index.js +362 -0
  177. package/dist/ui/elements/PcBrushMenu/style.js +3 -0
  178. package/dist/ui/elements/PcBrushTest/index.d.ts +71 -0
  179. package/dist/ui/elements/PcBrushTest/index.js +136 -0
  180. package/dist/ui/elements/PcBrushTest/style.js +3 -0
  181. package/dist/ui/elements/PcButton/index.d.ts +57 -0
  182. package/dist/ui/elements/PcButton/index.js +124 -0
  183. package/dist/ui/elements/PcButtonGroup/index.d.ts +44 -0
  184. package/dist/ui/elements/PcButtonGroup/index.js +93 -0
  185. package/dist/ui/elements/PcCanvas/index.d.ts +356 -0
  186. package/dist/ui/elements/PcCanvas/index.js +1004 -0
  187. package/dist/ui/elements/PcCanvas/style.js +3 -0
  188. package/dist/ui/elements/PcCanvasMap/index.d.ts +96 -0
  189. package/dist/ui/elements/PcCanvasMap/index.js +187 -0
  190. package/dist/ui/elements/PcCanvasMap/style.js +3 -0
  191. package/dist/ui/elements/PcContextMenu/index.d.ts +72 -0
  192. package/dist/ui/elements/PcContextMenu/index.js +148 -0
  193. package/dist/ui/elements/PcDialog/index.d.ts +63 -0
  194. package/dist/ui/elements/PcDialog/index.js +114 -0
  195. package/dist/ui/elements/PcIcon/index.d.ts +36 -0
  196. package/dist/ui/elements/PcIcon/index.js +43 -0
  197. package/dist/ui/elements/PcIconToggle/index.d.ts +46 -0
  198. package/dist/ui/elements/PcIconToggle/index.js +87 -0
  199. package/dist/ui/elements/PcInfoTip/index.d.ts +40 -0
  200. package/dist/ui/elements/PcInfoTip/index.js +95 -0
  201. package/dist/ui/elements/PcInputSettings/index.d.ts +69 -0
  202. package/dist/ui/elements/PcInputSettings/index.js +150 -0
  203. package/dist/ui/elements/PcLayerItem/index.d.ts +221 -0
  204. package/dist/ui/elements/PcLayerItem/index.js +561 -0
  205. package/dist/ui/elements/PcLayerItem/style.js +3 -0
  206. package/dist/ui/elements/PcLayersMenu/index.d.ts +108 -0
  207. package/dist/ui/elements/PcLayersMenu/index.js +208 -0
  208. package/dist/ui/elements/PcLayersMenu/style.js +3 -0
  209. package/dist/ui/elements/PcMatch/index.d.ts +31 -0
  210. package/dist/ui/elements/PcMatch/index.js +58 -0
  211. package/dist/ui/elements/PcPanel/index.d.ts +43 -0
  212. package/dist/ui/elements/PcPanel/index.js +51 -0
  213. package/dist/ui/elements/PcPopover/index.d.ts +48 -0
  214. package/dist/ui/elements/PcPopover/index.js +86 -0
  215. package/dist/ui/elements/PcSlider/index.d.ts +57 -0
  216. package/dist/ui/elements/PcSlider/index.js +88 -0
  217. package/dist/ui/elements/PcStats/index.d.ts +54 -0
  218. package/dist/ui/elements/PcStats/index.js +141 -0
  219. package/dist/ui/elements/PcThemePicker/index.d.ts +56 -0
  220. package/dist/ui/elements/PcThemePicker/index.js +134 -0
  221. package/dist/ui/elements/PcToast/index.d.ts +58 -0
  222. package/dist/ui/elements/PcToast/index.js +110 -0
  223. package/dist/ui/elements/PcToggleCheckbox/index.d.ts +37 -0
  224. package/dist/ui/elements/PcToggleCheckbox/index.js +105 -0
  225. package/dist/ui/elements/PcToolSettings/index.d.ts +128 -0
  226. package/dist/ui/elements/PcToolSettings/index.js +543 -0
  227. package/dist/ui/elements/PcToolSettings/style.js +3 -0
  228. package/dist/ui/elements/PcToolbar/index.d.ts +71 -0
  229. package/dist/ui/elements/PcToolbar/index.js +190 -0
  230. package/dist/ui/elements/PcToolbar/style.js +3 -0
  231. package/dist/ui/icons/images/brush.js +3 -0
  232. package/dist/ui/icons/images/circle-mask.js +3 -0
  233. package/dist/ui/icons/images/clipboard.js +3 -0
  234. package/dist/ui/icons/images/cursor.js +3 -0
  235. package/dist/ui/icons/images/eraser.js +3 -0
  236. package/dist/ui/icons/images/eyedropper.js +3 -0
  237. package/dist/ui/icons/images/flip-horizontal.js +3 -0
  238. package/dist/ui/icons/images/flip-vertical.js +3 -0
  239. package/dist/ui/icons/images/focus-pen.js +3 -0
  240. package/dist/ui/icons/images/freehand-mask.js +3 -0
  241. package/dist/ui/icons/images/hidden.js +3 -0
  242. package/dist/ui/icons/images/layers.js +3 -0
  243. package/dist/ui/icons/images/linked.js +3 -0
  244. package/dist/ui/icons/images/locked.js +3 -0
  245. package/dist/ui/icons/images/moon.js +3 -0
  246. package/dist/ui/icons/images/pan.js +3 -0
  247. package/dist/ui/icons/images/polygon-mask.js +3 -0
  248. package/dist/ui/icons/images/pressure.js +3 -0
  249. package/dist/ui/icons/images/randomise.js +3 -0
  250. package/dist/ui/icons/images/rectangle-mask.js +3 -0
  251. package/dist/ui/icons/images/redo.js +3 -0
  252. package/dist/ui/icons/images/rotate-90-left.js +3 -0
  253. package/dist/ui/icons/images/rotate-90-right.js +3 -0
  254. package/dist/ui/icons/images/smoothing-bezier-cubic.js +3 -0
  255. package/dist/ui/icons/images/smoothing-bezier-quad.js +3 -0
  256. package/dist/ui/icons/images/smoothing-linear.js +3 -0
  257. package/dist/ui/icons/images/star.js +3 -0
  258. package/dist/ui/icons/images/sun.js +3 -0
  259. package/dist/ui/icons/images/tilt.js +3 -0
  260. package/dist/ui/icons/images/undo.js +3 -0
  261. package/dist/ui/icons/images/unlinked.js +3 -0
  262. package/dist/ui/icons/images/unlocked.js +3 -0
  263. package/dist/ui/icons/images/visible.js +3 -0
  264. package/dist/ui/icons/images/zoom-in.js +3 -0
  265. package/dist/ui/icons/images/zoom-out.js +3 -0
  266. package/dist/ui/icons/images/zoom.js +3 -0
  267. package/dist/ui/icons/index.d.ts +42 -0
  268. package/dist/ui/icons/index.js +130 -0
  269. package/dist/ui/index.d.ts +21 -0
  270. package/dist/ui/index.js +22 -0
  271. package/dist/ui/layout/index.d.ts +88 -0
  272. package/dist/ui/layout/index.js +96 -0
  273. package/dist/ui/styles/index.d.ts +13 -0
  274. package/dist/ui/styles/index.js +195 -0
  275. package/dist/ui/tokens/index.d.ts +29 -0
  276. package/dist/ui/tokens/index.js +114 -0
  277. package/locales/en-GB.json +186 -0
  278. package/locales/en-US.json +186 -0
  279. package/locales/fr.json +186 -0
  280. package/locales/ja.json +186 -0
  281. package/locales/ko.json +186 -0
  282. package/locales/zh-Hans.json +186 -0
  283. package/locales/zh-Hant.json +186 -0
  284. package/package.json +97 -3
@@ -0,0 +1,23 @@
1
+ // src/lib/rendering/cursors/index.ts
2
+ /**
3
+ * Build a CSS cursor from SVG path data drawn on a 24×24 grid: black lines with a white outline so it shows on any colour.
4
+ *
5
+ * @param hotspot - the point (in the 24×24 grid) that is the cursor's exact position
6
+ * @param fallback - standard cursor used if the image cannot be shown
7
+ *
8
+ * @example
9
+ * svgCursor("M4 4 L20 20", [4, 4], "crosshair"); // 'url("data:image/svg+xml,...") 4 4, crosshair'
10
+ */
11
+ var svgCursor = (path, hotspot, fallback) => {
12
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="${path}" stroke="#ffffff" stroke-width="3.5"/><path d="${path}" stroke="#000000" stroke-width="1.5"/></svg>`;
13
+ return `url("data:image/svg+xml,${encodeURIComponent(svg)}") ${hotspot[0]} ${hotspot[1]}, ${fallback}`;
14
+ };
15
+ /** Colour sample (eyedropper): a pipette pointing down to the left; the point is at its tip */
16
+ var EYEDROPPER_CURSOR = svgCursor("M2 22 L3 18 L12 9 L15 12 L6 21 Z M10 7 L17 14 M14 10 L18 6 C19 5 20 4 21 5 C22 6 21 7 20 8 L16 12", [2, 22], "crosshair");
17
+ /** Polygon lasso: a straight-edged loop with a tail; the point is at the tail's end */
18
+ var POLYGON_LASSO_CURSOR = svgCursor("M3 21 L7 15 M7 15 L5 6 L13 2 L21 7 L17 15 Z", [3, 21], "crosshair");
19
+ /** Freeform lasso: an irregular hand-drawn loop with a tail; the point is at the tail's end */
20
+ var FREEFORM_LASSO_CURSOR = svgCursor("M3 21 C3 19 5 18 7 17 C7 17 5 16 4 14 C2 10 4 6 8 5 C11 4 12 6 15 4 C18 3 22 6 20 10 C19 13 21 15 18 16 C15 17 11 15 9 16 C8 16.5 7.5 17 7 17", [3, 21], "crosshair");
21
+ /** Rotate: the browser's grab cursor, as browsers have no standard rotate cursor */
22
+ var ROTATE_CURSOR = "grab";
23
+ export { EYEDROPPER_CURSOR, FREEFORM_LASSO_CURSOR, POLYGON_LASSO_CURSOR, ROTATE_CURSOR, svgCursor };
@@ -0,0 +1,22 @@
1
+ import type Viewport from "../../classes/Viewport";
2
+ import type { Point } from "../../classes/Viewport";
3
+ import type { OutlineLoop } from "../outline";
4
+ import type { MaskShape } from "../../selection/Selection";
5
+ /** The drawing operations mask outlines need */
6
+ export type OutlineContext = Pick<CanvasRenderingContext2D, "save" | "restore" | "beginPath" | "moveTo" | "lineTo" | "rect" | "ellipse" | "stroke" | "fill" | "fillRect" | "strokeRect" | "setLineDash" | "strokeStyle" | "fillStyle" | "lineWidth" | "lineDashOffset">;
7
+ /** Length of each dash, in CSS pixels */
8
+ export declare const DASH_LENGTH = 4;
9
+ /** Size of polygon node handles, in CSS pixels */
10
+ export declare const NODE_SIZE = 5;
11
+ /** Colours of the alternating dashes: white, then black */
12
+ export declare const DASH_COLOURS: readonly ["#ffffff", "#000000"];
13
+ /**
14
+ * Draw the mask's outline as "crawling ants": alternating white and black dashes. Animate by increasing `dashOffset` over time.
15
+ *
16
+ * @param loops - outline loops in canvas pixels
17
+ */
18
+ export declare const drawMaskOutline: (ctx: OutlineContext, loops: readonly OutlineLoop[], view: Viewport, pixelRatio: number, dashOffset: number) => void;
19
+ /**
20
+ * Draw a lasso shape in progress as alternating white and black dashes, with handles (white with a black border) on polygon nodes.
21
+ */
22
+ export declare const drawLassoPreview: (ctx: OutlineContext, shape: MaskShape, nodes: readonly Point[], view: Viewport, pixelRatio: number) => void;
@@ -0,0 +1,78 @@
1
+ // src/lib/rendering/maskOutline/index.ts
2
+ /** Length of each dash, in CSS pixels */
3
+ var DASH_LENGTH = 4;
4
+ /** Size of polygon node handles, in CSS pixels */
5
+ var NODE_SIZE = 5;
6
+ /** Colours of the alternating dashes: white, then black */
7
+ var DASH_COLOURS = ["#ffffff", "#000000"];
8
+ /**
9
+ * Stroke the current path as alternating white and black dashes, 1 px wide in device pixels.
10
+ * One of the two colours always shows, on any background, without blend modes.
11
+ */
12
+ var strokeAlternating = (ctx, pixelRatio, dashOffset) => {
13
+ ctx.lineWidth = pixelRatio;
14
+ ctx.setLineDash([]);
15
+ ctx.strokeStyle = DASH_COLOURS[0];
16
+ ctx.stroke();
17
+ ctx.setLineDash([4 * pixelRatio, 4 * pixelRatio]);
18
+ ctx.lineDashOffset = dashOffset * pixelRatio;
19
+ ctx.strokeStyle = DASH_COLOURS[1];
20
+ ctx.stroke();
21
+ };
22
+ /**
23
+ * Draw the mask's outline as "crawling ants": alternating white and black dashes. Animate by increasing `dashOffset` over time.
24
+ *
25
+ * @param loops - outline loops in canvas pixels
26
+ */
27
+ var drawMaskOutline = (ctx, loops, view, pixelRatio, dashOffset) => {
28
+ ctx.save();
29
+ ctx.beginPath();
30
+ for (const loop of loops) for (let i = 0; i < loop.length; i += 2) {
31
+ const { x, y } = view.canvasToScreen({
32
+ x: loop[i],
33
+ y: loop[i + 1]
34
+ });
35
+ if (i === 0) ctx.moveTo(x * pixelRatio, y * pixelRatio);
36
+ else ctx.lineTo(x * pixelRatio, y * pixelRatio);
37
+ }
38
+ strokeAlternating(ctx, pixelRatio, dashOffset);
39
+ ctx.restore();
40
+ };
41
+ /**
42
+ * Draw a lasso shape in progress as alternating white and black dashes, with handles (white with a black border) on polygon nodes.
43
+ */
44
+ var drawLassoPreview = (ctx, shape, nodes, view, pixelRatio) => {
45
+ const toDevice = (point) => {
46
+ const { x, y } = view.canvasToScreen(point);
47
+ return {
48
+ x: x * pixelRatio,
49
+ y: y * pixelRatio
50
+ };
51
+ };
52
+ ctx.save();
53
+ ctx.beginPath();
54
+ if (shape.type === "polygon") shape.points.forEach((point, i) => {
55
+ const { x, y } = toDevice(point);
56
+ if (i === 0) ctx.moveTo(x, y);
57
+ else ctx.lineTo(x, y);
58
+ });
59
+ else {
60
+ const { x, y } = toDevice(shape.rect);
61
+ const width = shape.rect.width * view.zoom * pixelRatio;
62
+ const height = shape.rect.height * view.zoom * pixelRatio;
63
+ if (shape.type === "rect") ctx.rect(x, y, width, height);
64
+ else ctx.ellipse(x + width / 2, y + height / 2, width / 2, height / 2, 0, 0, Math.PI * 2);
65
+ }
66
+ strokeAlternating(ctx, pixelRatio, 0);
67
+ const size = 5 * pixelRatio;
68
+ ctx.setLineDash([]);
69
+ ctx.fillStyle = DASH_COLOURS[0];
70
+ ctx.strokeStyle = DASH_COLOURS[1];
71
+ for (const node of nodes) {
72
+ const { x, y } = toDevice(node);
73
+ ctx.fillRect(x - size / 2, y - size / 2, size, size);
74
+ ctx.strokeRect(x - size / 2, y - size / 2, size, size);
75
+ }
76
+ ctx.restore();
77
+ };
78
+ export { DASH_COLOURS, DASH_LENGTH, NODE_SIZE, drawLassoPreview, drawMaskOutline };
@@ -0,0 +1,36 @@
1
+ /** A line from (x1, y1) to (x2, y2), in coordinates from 0 to 1 across the shape (or pixels, if not normalised) */
2
+ export type OutlineSegment = [x1: number, y1: number, x2: number, y2: number];
3
+ /** Options for tracing an outline */
4
+ export interface TraceOptions {
5
+ /** Alpha value (0–255) at or above which a pixel is inside */
6
+ threshold?: number;
7
+ /** Return coordinates from 0 to 1 (default) or in pixels */
8
+ normalise?: boolean;
9
+ /** Only trace within this pixel area (e.g. the mask's bounding box) */
10
+ bounds?: {
11
+ x: number;
12
+ y: number;
13
+ width: number;
14
+ height: number;
15
+ };
16
+ }
17
+ /**
18
+ * Trace the outline of a shape from its alpha values (marching squares). Pixels at or above the threshold are inside.
19
+ * Returns line segments in coordinates from 0 to 1 (so the outline can be drawn at any size), or in pixels.
20
+ * Outline points lie halfway between pixel centres, so in pixels they are always whole or half numbers.
21
+ *
22
+ * @param alpha - one alpha value (0–255) per pixel, row by row
23
+ *
24
+ * @example
25
+ * traceOutline(new Uint8ClampedArray([0, 0, 0, 0, 255, 0, 0, 0, 0]), 3, 3); // segments around the centre pixel
26
+ */
27
+ export declare const traceOutline: (alpha: ArrayLike<number>, width: number, height: number, { threshold, normalise, bounds }?: TraceOptions) => OutlineSegment[];
28
+ /** A closed outline as a list of points, `[x0, y0, x1, y1, ...]` */
29
+ export type OutlineLoop = number[];
30
+ /**
31
+ * Join outline segments into closed loops, so dashed lines flow continuously along the outline. Straight runs are merged into single lines.
32
+ *
33
+ * @example
34
+ * chainSegments(traceOutline(alpha, width, height, { normalise: false }));
35
+ */
36
+ export declare const chainSegments: (segments: readonly OutlineSegment[]) => OutlineLoop[];
@@ -0,0 +1,120 @@
1
+ // src/lib/rendering/outline/index.ts
2
+ /**
3
+ * Trace the outline of a shape from its alpha values (marching squares). Pixels at or above the threshold are inside.
4
+ * Returns line segments in coordinates from 0 to 1 (so the outline can be drawn at any size), or in pixels.
5
+ * Outline points lie halfway between pixel centres, so in pixels they are always whole or half numbers.
6
+ *
7
+ * @param alpha - one alpha value (0–255) per pixel, row by row
8
+ *
9
+ * @example
10
+ * traceOutline(new Uint8ClampedArray([0, 0, 0, 0, 255, 0, 0, 0, 0]), 3, 3); // segments around the centre pixel
11
+ */
12
+ var traceOutline = (alpha, width, height, { threshold = 128, normalise = true, bounds } = {}) => {
13
+ const inside = (x, y) => x >= 0 && y >= 0 && x < width && y < height && alpha[y * width + x] >= threshold;
14
+ const segments = [];
15
+ const scaleX = normalise ? width : 1;
16
+ const scaleY = normalise ? height : 1;
17
+ const add = (x1, y1, x2, y2) => segments.push([
18
+ x1 / scaleX,
19
+ y1 / scaleY,
20
+ x2 / scaleX,
21
+ y2 / scaleY
22
+ ]);
23
+ const fromX = bounds ? Math.max(-1, bounds.x - 1) : -1;
24
+ const fromY = bounds ? Math.max(-1, bounds.y - 1) : -1;
25
+ const toX = bounds ? Math.min(width, bounds.x + bounds.width) : width;
26
+ const toY = bounds ? Math.min(height, bounds.y + bounds.height) : height;
27
+ for (let y = fromY; y < toY; y++) for (let x = fromX; x < toX; x++) {
28
+ const tl = inside(x, y);
29
+ const tr = inside(x + 1, y);
30
+ const br = inside(x + 1, y + 1);
31
+ const bl = inside(x, y + 1);
32
+ const cell = (tl ? 8 : 0) | (tr ? 4 : 0) | (br ? 2 : 0) | (bl ? 1 : 0);
33
+ if (cell === 0 || cell === 15) continue;
34
+ const top = [x + 1, y + .5];
35
+ const right = [x + 1.5, y + 1];
36
+ const bottom = [x + 1, y + 1.5];
37
+ const left = [x + .5, y + 1];
38
+ const line = (a, b) => add(a[0], a[1], b[0], b[1]);
39
+ switch (cell) {
40
+ case 1:
41
+ case 14:
42
+ line(left, bottom);
43
+ break;
44
+ case 2:
45
+ case 13:
46
+ line(bottom, right);
47
+ break;
48
+ case 3:
49
+ case 12:
50
+ line(left, right);
51
+ break;
52
+ case 4:
53
+ case 11:
54
+ line(top, right);
55
+ break;
56
+ case 6:
57
+ case 9:
58
+ line(top, bottom);
59
+ break;
60
+ case 7:
61
+ case 8:
62
+ line(left, top);
63
+ break;
64
+ case 5:
65
+ line(left, top);
66
+ line(bottom, right);
67
+ break;
68
+ case 10:
69
+ line(top, right);
70
+ line(left, bottom);
71
+ }
72
+ }
73
+ return segments;
74
+ };
75
+ /**
76
+ * Join outline segments into closed loops, so dashed lines flow continuously along the outline. Straight runs are merged into single lines.
77
+ *
78
+ * @example
79
+ * chainSegments(traceOutline(alpha, width, height, { normalise: false }));
80
+ */
81
+ var chainSegments = (segments) => {
82
+ const key = (x, y) => `${x},${y}`;
83
+ const ends = /* @__PURE__ */ new Map();
84
+ segments.forEach(([x1, y1, x2, y2], i) => {
85
+ for (const k of [key(x1, y1), key(x2, y2)]) {
86
+ const list = ends.get(k);
87
+ if (list) list.push(i);
88
+ else ends.set(k, [i]);
89
+ }
90
+ });
91
+ const used = new Uint8Array(segments.length);
92
+ const loops = [];
93
+ for (let start = 0; start < segments.length; start++) {
94
+ if (used[start]) continue;
95
+ used[start] = 1;
96
+ const [sx, sy, ex, ey] = segments[start];
97
+ const loop = [sx, sy];
98
+ let x = ex;
99
+ let y = ey;
100
+ for (;;) {
101
+ const n = loop.length;
102
+ if (n >= 4) {
103
+ const ax = loop[n - 4];
104
+ const ay = loop[n - 3];
105
+ const bx = loop[n - 2];
106
+ const by = loop[n - 1];
107
+ if ((bx - ax) * (y - ay) === (by - ay) * (x - ax)) loop.length -= 2;
108
+ }
109
+ loop.push(x, y);
110
+ const next = ends.get(key(x, y)).find((i) => !used[i]);
111
+ if (next === void 0) break;
112
+ used[next] = 1;
113
+ const [x1, y1, x2, y2] = segments[next];
114
+ [x, y] = x1 === x && y1 === y ? [x2, y2] : [x1, y1];
115
+ }
116
+ loops.push(loop);
117
+ }
118
+ return loops;
119
+ };
120
+ export { chainSegments, traceOutline };
@@ -0,0 +1,55 @@
1
+ import type { CanvasFactory } from "../../classes/Layer";
2
+ import { type DrawableLayer } from "../compositing";
3
+ /** How many canvas pixels across the sampling window enlarges */
4
+ export declare const SAMPLE_PIXELS = 10;
5
+ /** How much the sampling window enlarges each canvas pixel */
6
+ export declare const SAMPLE_SCALE = 5;
7
+ /** The most pixels across that an area's average is worked out from while dragging (the final average uses them all) */
8
+ export declare const PREVIEW_MAX_PIXELS = 100;
9
+ /** A rectangle of whole canvas pixels */
10
+ export interface PixelRect {
11
+ x: number;
12
+ y: number;
13
+ width: number;
14
+ height: number;
15
+ }
16
+ /**
17
+ * Format a colour as `#rrggbb`.
18
+ *
19
+ * @example
20
+ * toHex(255, 0, 128); // "#ff0080"
21
+ */
22
+ export declare const toHex: (r: number, g: number, b: number) => string;
23
+ /**
24
+ * The average colour of RGBA pixels, counting every pixel that isn't fully transparent (transparent pixels are outside
25
+ * the canvas). Undefined if all are transparent.
26
+ *
27
+ * @example
28
+ * averageColour(new Uint8ClampedArray([255, 0, 0, 255, 0, 0, 255, 255])); // "#800080"
29
+ */
30
+ export declare const averageColour: (pixels: Uint8ClampedArray) => string | undefined;
31
+ /**
32
+ * The whole canvas pixels a rectangle between two canvas points covers, cut to the canvas. Undefined if it's outside.
33
+ *
34
+ * @example
35
+ * pixelRect({ x: 2.5, y: 8 }, { x: -3, y: 1.2 }, 100, 100); // { x: 0, y: 1, width: 3, height: 8 }
36
+ */
37
+ export declare const pixelRect: (a: {
38
+ x: number;
39
+ y: number;
40
+ }, b: {
41
+ x: number;
42
+ y: number;
43
+ }, canvasWidth: number, canvasHeight: number) => PixelRect | undefined;
44
+ /**
45
+ * Draw an area of the canvas as it's shown (the white background, then every layer mixed as shown) onto a new canvas,
46
+ * scaled to fit `width` × `height`. Parts outside the canvas stay transparent.
47
+ *
48
+ * @example
49
+ * flattenArea(app.drawables, app.createCanvas, app.width, app.height, { x: 10, y: 10, width: 10, height: 10 });
50
+ */
51
+ export declare const flattenArea: (layers: readonly DrawableLayer[], createCanvas: CanvasFactory, canvasWidth: number, canvasHeight: number, area: PixelRect, width?: number, height?: number) => OffscreenCanvas;
52
+ /**
53
+ * Read all of a canvas's pixels as RGBA.
54
+ */
55
+ export declare const readPixels: (canvas: OffscreenCanvas) => Uint8ClampedArray;
@@ -0,0 +1,83 @@
1
+ import { CANVAS_BACKGROUND, drawLayers } from "../compositing/index.js";
2
+ // src/lib/rendering/sample/index.ts
3
+ /** How many canvas pixels across the sampling window enlarges */
4
+ var SAMPLE_PIXELS = 10;
5
+ /** How much the sampling window enlarges each canvas pixel */
6
+ var SAMPLE_SCALE = 5;
7
+ /** The most pixels across that an area's average is worked out from while dragging (the final average uses them all) */
8
+ var PREVIEW_MAX_PIXELS = 100;
9
+ /**
10
+ * Format a colour as `#rrggbb`.
11
+ *
12
+ * @example
13
+ * toHex(255, 0, 128); // "#ff0080"
14
+ */
15
+ var toHex = (r, g, b) => `#${[
16
+ r,
17
+ g,
18
+ b
19
+ ].map((c) => Math.round(c).toString(16).padStart(2, "0")).join("")}`;
20
+ /**
21
+ * The average colour of RGBA pixels, counting every pixel that isn't fully transparent (transparent pixels are outside
22
+ * the canvas). Undefined if all are transparent.
23
+ *
24
+ * @example
25
+ * averageColour(new Uint8ClampedArray([255, 0, 0, 255, 0, 0, 255, 255])); // "#800080"
26
+ */
27
+ var averageColour = (pixels) => {
28
+ let r = 0;
29
+ let g = 0;
30
+ let b = 0;
31
+ let count = 0;
32
+ for (let i = 0; i < pixels.length; i += 4) {
33
+ if (pixels[i + 3] === 0) continue;
34
+ r += pixels[i];
35
+ g += pixels[i + 1];
36
+ b += pixels[i + 2];
37
+ count++;
38
+ }
39
+ return count > 0 ? toHex(r / count, g / count, b / count) : void 0;
40
+ };
41
+ /**
42
+ * The whole canvas pixels a rectangle between two canvas points covers, cut to the canvas. Undefined if it's outside.
43
+ *
44
+ * @example
45
+ * pixelRect({ x: 2.5, y: 8 }, { x: -3, y: 1.2 }, 100, 100); // { x: 0, y: 1, width: 3, height: 8 }
46
+ */
47
+ var pixelRect = (a, b, canvasWidth, canvasHeight) => {
48
+ const left = Math.max(0, Math.floor(Math.min(a.x, b.x)));
49
+ const top = Math.max(0, Math.floor(Math.min(a.y, b.y)));
50
+ const right = Math.min(canvasWidth, Math.floor(Math.max(a.x, b.x)) + 1);
51
+ const bottom = Math.min(canvasHeight, Math.floor(Math.max(a.y, b.y)) + 1);
52
+ if (right <= left || bottom <= top) return void 0;
53
+ return {
54
+ x: left,
55
+ y: top,
56
+ width: right - left,
57
+ height: bottom - top
58
+ };
59
+ };
60
+ /**
61
+ * Draw an area of the canvas as it's shown (the white background, then every layer mixed as shown) onto a new canvas,
62
+ * scaled to fit `width` × `height`. Parts outside the canvas stay transparent.
63
+ *
64
+ * @example
65
+ * flattenArea(app.drawables, app.createCanvas, app.width, app.height, { x: 10, y: 10, width: 10, height: 10 });
66
+ */
67
+ var flattenArea = (layers, createCanvas, canvasWidth, canvasHeight, area, width = area.width, height = area.height) => {
68
+ const canvas = createCanvas(width, height);
69
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
70
+ const scaleX = width / area.width;
71
+ const scaleY = height / area.height;
72
+ ctx.setTransform(scaleX, 0, 0, scaleY, -area.x * scaleX, -area.y * scaleY);
73
+ ctx.imageSmoothingEnabled = scaleX < 1 || scaleY < 1;
74
+ ctx.fillStyle = CANVAS_BACKGROUND;
75
+ ctx.fillRect(0, 0, canvasWidth, canvasHeight);
76
+ drawLayers(ctx, layers);
77
+ return canvas;
78
+ };
79
+ /**
80
+ * Read all of a canvas's pixels as RGBA.
81
+ */
82
+ var readPixels = (canvas) => canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height).data;
83
+ export { PREVIEW_MAX_PIXELS, SAMPLE_PIXELS, SAMPLE_SCALE, averageColour, flattenArea, pixelRect, readPixels, toHex };
@@ -0,0 +1,67 @@
1
+ import { type CanvasFactory } from "../../classes/Layer";
2
+ import { type DrawableLayer, type LayerOverride } from "../compositing";
3
+ /** The parts of the File System Access API used to choose where to save (Chromium browsers only) */
4
+ export interface SaveFilePicker {
5
+ (options: {
6
+ suggestedName: string;
7
+ types: {
8
+ description: string;
9
+ accept: Record<string, string[]>;
10
+ }[];
11
+ }): Promise<{
12
+ createWritable(): Promise<{
13
+ write(data: Blob): Promise<void>;
14
+ close(): Promise<void>;
15
+ }>;
16
+ }>;
17
+ }
18
+ /** A kind of file the save dialog offers */
19
+ export interface SaveFileType {
20
+ description: string;
21
+ accept: Record<string, string[]>;
22
+ }
23
+ /** PNG images (screenshots) */
24
+ export declare const PNG_FILE_TYPE: SaveFileType;
25
+ /** JSON files (brush presets) */
26
+ export declare const JSON_FILE_TYPE: SaveFileType;
27
+ /** How a file is saved: with a save dialog (folder picker) or as a browser download */
28
+ export type SaveMethod = "picker" | "download";
29
+ /**
30
+ * File name for a screenshot taken at a time, starting with a prefix (see `PaintCanvasNames.filePrefix`).
31
+ *
32
+ * @example
33
+ * screenshotName(new Date(2026, 9, 19, 14, 30, 5)); // "paintcanvas-2026-10-19-143005.png"
34
+ */
35
+ export declare const screenshotName: (date?: Date, prefix?: string) => string;
36
+ /**
37
+ * Flatten all visible layers onto white at full canvas size and encode the result as a PNG.
38
+ *
39
+ * @param override - draw this image instead of one layer's canvas (e.g. a transform in progress)
40
+ */
41
+ export declare const renderScreenshot: (layers: readonly DrawableLayer[], width: number, height: number, override?: LayerOverride, createCanvas?: CanvasFactory) => Promise<Blob>;
42
+ /** A compressed snapshot: a JPEG and its size in pixels */
43
+ export interface JpegImage {
44
+ blob: Blob;
45
+ width: number;
46
+ height: number;
47
+ }
48
+ /** JPEG qualities tried for a snapshot, best first, before making it smaller */
49
+ export declare const SNAPSHOT_QUALITIES: readonly [0.92, 0.8, 0.7];
50
+ /** How much smaller a snapshot gets each time it is still too large at the lowest quality */
51
+ export declare const SNAPSHOT_SHRINK = 0.75;
52
+ /**
53
+ * Flatten all visible layers onto white and encode the result as a JPEG no larger than `maxBytes` (e.g. an upload
54
+ * limit): full size at the best quality that fits, otherwise smaller.
55
+ *
56
+ * @param override - draw this image instead of one layer's canvas (e.g. a transform in progress)
57
+ * @param maxBytes - the largest file allowed (no limit by default)
58
+ */
59
+ export declare const renderJpegImage: (layers: readonly DrawableLayer[], width: number, height: number, override?: LayerOverride, createCanvas?: CanvasFactory, maxBytes?: number) => Promise<JpegImage>;
60
+ /**
61
+ * Save a file. Where the browser supports it, a save dialog lets the user choose the folder; otherwise it downloads.
62
+ * Returns how it was saved, or undefined if the user cancelled the dialog.
63
+ *
64
+ * @param picker - `window.showSaveFilePicker` (replaceable for tests)
65
+ * @param fileType - the kind of file the save dialog offers (PNG by default)
66
+ */
67
+ export declare const saveFile: (blob: Blob, name: string, picker?: SaveFilePicker | undefined, fileType?: SaveFileType) => Promise<SaveMethod | undefined>;
@@ -0,0 +1,96 @@
1
+ import { createOffscreenCanvas } from "../../classes/Layer/index.js";
2
+ import { drawFlattened } from "../compositing/index.js";
3
+ // src/lib/rendering/screenshot/index.ts
4
+ /** PNG images (screenshots) */
5
+ var PNG_FILE_TYPE = {
6
+ description: "PNG image",
7
+ accept: { "image/png": [".png"] }
8
+ };
9
+ /** JSON files (brush presets) */
10
+ var JSON_FILE_TYPE = {
11
+ description: "JSON file",
12
+ accept: { "application/json": [".json"] }
13
+ };
14
+ /**
15
+ * File name for a screenshot taken at a time, starting with a prefix (see `PaintCanvasNames.filePrefix`).
16
+ *
17
+ * @example
18
+ * screenshotName(new Date(2026, 9, 19, 14, 30, 5)); // "paintcanvas-2026-10-19-143005.png"
19
+ */
20
+ var screenshotName = (date = /* @__PURE__ */ new Date(), prefix = "paintcanvas") => {
21
+ const pad = (n) => String(n).padStart(2, "0");
22
+ return `${prefix}-${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}.png`;
23
+ };
24
+ /**
25
+ * Flatten all visible layers onto white at full canvas size and encode the result as a PNG.
26
+ *
27
+ * @param override - draw this image instead of one layer's canvas (e.g. a transform in progress)
28
+ */
29
+ var renderScreenshot = async (layers, width, height, override, createCanvas = createOffscreenCanvas) => {
30
+ const canvas = createCanvas(width, height);
31
+ drawFlattened(canvas.getContext("2d"), layers, width, height, width, height, override);
32
+ return canvas.convertToBlob({ type: "image/png" });
33
+ };
34
+ /** JPEG qualities tried for a snapshot, best first, before making it smaller */
35
+ var SNAPSHOT_QUALITIES = [
36
+ .92,
37
+ .8,
38
+ .7
39
+ ];
40
+ /** How much smaller a snapshot gets each time it is still too large at the lowest quality */
41
+ var SNAPSHOT_SHRINK = .75;
42
+ /**
43
+ * Flatten all visible layers onto white and encode the result as a JPEG no larger than `maxBytes` (e.g. an upload
44
+ * limit): full size at the best quality that fits, otherwise smaller.
45
+ *
46
+ * @param override - draw this image instead of one layer's canvas (e.g. a transform in progress)
47
+ * @param maxBytes - the largest file allowed (no limit by default)
48
+ */
49
+ var renderJpegImage = async (layers, width, height, override, createCanvas = createOffscreenCanvas, maxBytes = Infinity) => {
50
+ for (let scale = 1;; scale *= SNAPSHOT_SHRINK) {
51
+ const targetWidth = Math.max(1, Math.round(width * scale));
52
+ const targetHeight = Math.max(1, Math.round(height * scale));
53
+ const canvas = createCanvas(targetWidth, targetHeight);
54
+ drawFlattened(canvas.getContext("2d"), layers, width, height, targetWidth, targetHeight, override);
55
+ for (const quality of SNAPSHOT_QUALITIES) {
56
+ const blob = await canvas.convertToBlob({
57
+ type: "image/jpeg",
58
+ quality
59
+ });
60
+ if (blob.size <= maxBytes || targetWidth === 1 && targetHeight === 1) return {
61
+ blob,
62
+ width: targetWidth,
63
+ height: targetHeight
64
+ };
65
+ }
66
+ }
67
+ };
68
+ /**
69
+ * Save a file. Where the browser supports it, a save dialog lets the user choose the folder; otherwise it downloads.
70
+ * Returns how it was saved, or undefined if the user cancelled the dialog.
71
+ *
72
+ * @param picker - `window.showSaveFilePicker` (replaceable for tests)
73
+ * @param fileType - the kind of file the save dialog offers (PNG by default)
74
+ */
75
+ var saveFile = async (blob, name, picker = window.showSaveFilePicker?.bind(window), fileType = PNG_FILE_TYPE) => {
76
+ if (picker) try {
77
+ const writable = await (await picker({
78
+ suggestedName: name,
79
+ types: [fileType]
80
+ })).createWritable();
81
+ await writable.write(blob);
82
+ await writable.close();
83
+ return "picker";
84
+ } catch (error) {
85
+ if (error.name === "AbortError") return void 0;
86
+ throw error;
87
+ }
88
+ const url = URL.createObjectURL(blob);
89
+ const link = document.createElement("a");
90
+ link.href = url;
91
+ link.download = name;
92
+ link.click();
93
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
94
+ return "download";
95
+ };
96
+ export { JSON_FILE_TYPE, PNG_FILE_TYPE, SNAPSHOT_QUALITIES, SNAPSHOT_SHRINK, renderJpegImage, renderScreenshot, saveFile, screenshotName };
@@ -0,0 +1,10 @@
1
+ import type Viewport from "../../classes/Viewport";
2
+ import { type Handle, type TransformBox } from "../../transform/geometry";
3
+ import { type OutlineContext } from "../maskOutline";
4
+ /** Size of transform handles, in CSS pixels */
5
+ export declare const HANDLE_SIZE: number;
6
+ /**
7
+ * Draw a transform box: a white outline with a black inner line (visible on any colour), square handles (white with a
8
+ * black border), and the round rotate handle on a line out from the top-centre handle.
9
+ */
10
+ export declare const drawTransformBox: (ctx: OutlineContext, box: TransformBox, handles: readonly Handle[], view: Viewport, pixelRatio: number) => void;
@@ -0,0 +1,57 @@
1
+ import { boxCorners, handlePosition, rotateHandle } from "../../transform/geometry/index.js";
2
+ import { DASH_COLOURS } from "../maskOutline/index.js";
3
+ // src/lib/rendering/transformBox/index.ts
4
+ /** Size of transform handles, in CSS pixels */
5
+ var HANDLE_SIZE = 7;
6
+ /**
7
+ * Draw a transform box: a white outline with a black inner line (visible on any colour), square handles (white with a
8
+ * black border), and the round rotate handle on a line out from the top-centre handle.
9
+ */
10
+ var drawTransformBox = (ctx, box, handles, view, pixelRatio) => {
11
+ const toDevice = (point) => {
12
+ const { x, y } = view.canvasToScreen(point);
13
+ return {
14
+ x: x * pixelRatio,
15
+ y: y * pixelRatio
16
+ };
17
+ };
18
+ ctx.save();
19
+ ctx.setLineDash([]);
20
+ ctx.beginPath();
21
+ boxCorners(box).forEach((corner, i) => {
22
+ const { x, y } = toDevice(corner);
23
+ if (i === 0) ctx.moveTo(x, y);
24
+ else ctx.lineTo(x, y);
25
+ });
26
+ const first = toDevice(boxCorners(box)[0]);
27
+ ctx.lineTo(first.x, first.y);
28
+ ctx.lineWidth = pixelRatio * 3;
29
+ ctx.strokeStyle = DASH_COLOURS[0];
30
+ ctx.stroke();
31
+ ctx.lineWidth = pixelRatio;
32
+ ctx.strokeStyle = DASH_COLOURS[1];
33
+ ctx.stroke();
34
+ const rotation = rotateHandle(box, toDevice, 24 * pixelRatio);
35
+ ctx.beginPath();
36
+ ctx.moveTo(rotation.anchor.x, rotation.anchor.y);
37
+ ctx.lineTo(rotation.handle.x, rotation.handle.y);
38
+ ctx.lineWidth = pixelRatio * 3;
39
+ ctx.strokeStyle = DASH_COLOURS[0];
40
+ ctx.stroke();
41
+ ctx.lineWidth = pixelRatio;
42
+ ctx.strokeStyle = DASH_COLOURS[1];
43
+ ctx.stroke();
44
+ const size = HANDLE_SIZE * pixelRatio;
45
+ ctx.fillStyle = DASH_COLOURS[0];
46
+ ctx.beginPath();
47
+ ctx.ellipse(rotation.handle.x, rotation.handle.y, size / 2, size / 2, 0, 0, Math.PI * 2);
48
+ ctx.fill();
49
+ ctx.stroke();
50
+ for (const handle of handles) {
51
+ const { x, y } = toDevice(handlePosition(box, handle));
52
+ ctx.fillRect(x - size / 2, y - size / 2, size, size);
53
+ ctx.strokeRect(x - size / 2, y - size / 2, size, size);
54
+ }
55
+ ctx.restore();
56
+ };
57
+ export { HANDLE_SIZE, drawTransformBox };