@nice2dev/ui-graphics 1.3.14 → 1.3.16

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 (294) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/cjs/animation/AnimatedPerson.js +470 -143
  3. package/dist/cjs/animation/AnimationEditor.js +289 -101
  4. package/dist/cjs/animation/Audience.js +83 -61
  5. package/dist/cjs/animation/BodyRenderer.js +69 -28
  6. package/dist/cjs/animation/animationHelper.js +281 -288
  7. package/dist/cjs/animation/characterTypes.js +200 -203
  8. package/dist/cjs/animation/choreoDSL.js +205 -219
  9. package/dist/cjs/animation/choreography.js +4 -32
  10. package/dist/cjs/animation/karaokeIntegration.js +68 -77
  11. package/dist/cjs/animation/lottieExport.js +108 -114
  12. package/dist/cjs/animation/motionPath.js +225 -248
  13. package/dist/cjs/animation/physics.js +227 -254
  14. package/dist/cjs/animation/rig/RigPlayer.js +53 -56
  15. package/dist/cjs/animation/rig/RiggedBody.js +71 -26
  16. package/dist/cjs/animation/rig/choreoRigBridge.js +72 -77
  17. package/dist/cjs/animation/rig/ik.js +13 -18
  18. package/dist/cjs/animation/rig/poseOps.js +34 -41
  19. package/dist/cjs/animation/rig/presets.js +64 -72
  20. package/dist/cjs/animation/rig/rigMath.js +25 -32
  21. package/dist/cjs/animation/rig/rigTypes.js +59 -63
  22. package/dist/cjs/animation/shapes/arms.js +31 -13
  23. package/dist/cjs/animation/shapes/eyes.js +129 -47
  24. package/dist/cjs/animation/shapes/hair.js +72 -45
  25. package/dist/cjs/animation/shapes/heads.js +131 -57
  26. package/dist/cjs/animation/shapes/headwear.js +47 -38
  27. package/dist/cjs/animation/shapes/legs.js +31 -13
  28. package/dist/cjs/animation/shapes/mouths.js +120 -44
  29. package/dist/cjs/animation/shapes/noses.js +80 -25
  30. package/dist/cjs/animation/shapes/outfits.js +101 -39
  31. package/dist/cjs/animation/shapes/torsos.js +15 -11
  32. package/dist/cjs/animation/spineExport.js +103 -121
  33. package/dist/cjs/core/LocalUI.js +108 -43
  34. package/dist/cjs/core/NiceBrushEngine.js +758 -552
  35. package/dist/cjs/core/collaboration.js +249 -251
  36. package/dist/cjs/core/colorBlindness.js +100 -115
  37. package/dist/cjs/core/dragDrop.js +155 -158
  38. package/dist/cjs/core/editorAsk.js +51 -41
  39. package/dist/cjs/core/fixes.js +140 -171
  40. package/dist/cjs/core/gridGuides.js +166 -201
  41. package/dist/cjs/core/historyVisual.js +156 -135
  42. package/dist/cjs/core/i18n.js +13 -14
  43. package/dist/cjs/core/integrations.js +238 -249
  44. package/dist/cjs/core/minimap.js +149 -151
  45. package/dist/cjs/core/plugins.js +98 -113
  46. package/dist/cjs/core/rtl.js +71 -83
  47. package/dist/cjs/core/shortcuts.js +196 -225
  48. package/dist/cjs/core/theme.js +142 -149
  49. package/dist/cjs/core/yjsCollaboration.js +232 -293
  50. package/dist/cjs/design/design-tools.js +12 -22
  51. package/dist/cjs/font/NiceFontEditor.js +877 -316
  52. package/dist/cjs/font/fontFormatUtils.js +388 -448
  53. package/dist/cjs/game/GameAsset2dEditor.js +1201 -392
  54. package/dist/cjs/game/GameAsset2dEditor.module.css.js +146 -5
  55. package/dist/cjs/game/gameAssetTypes.js +7 -16
  56. package/dist/cjs/game/gameAssetUtils.js +390 -413
  57. package/dist/cjs/game/useGameAssetEditor.js +820 -763
  58. package/dist/cjs/index.js +81 -84
  59. package/dist/cjs/nice2dev-ui-graphics.css +1 -1
  60. package/dist/cjs/node_modules/lib0/array.js +8 -70
  61. package/dist/cjs/node_modules/lib0/binary.js +3 -20
  62. package/dist/cjs/node_modules/lib0/broadcastchannel.js +24 -86
  63. package/dist/cjs/node_modules/lib0/buffer.js +15 -82
  64. package/dist/cjs/node_modules/lib0/conditions.js +3 -16
  65. package/dist/cjs/node_modules/lib0/decoding.js +108 -287
  66. package/dist/cjs/node_modules/lib0/dom.js +14 -44
  67. package/dist/cjs/node_modules/lib0/encoding.js +68 -418
  68. package/dist/cjs/node_modules/lib0/environment.js +26 -92
  69. package/dist/cjs/node_modules/lib0/error.js +5 -29
  70. package/dist/cjs/node_modules/lib0/function.js +27 -66
  71. package/dist/cjs/node_modules/lib0/iterator.js +8 -33
  72. package/dist/cjs/node_modules/lib0/logging.common.js +12 -22
  73. package/dist/cjs/node_modules/lib0/logging.js +32 -71
  74. package/dist/cjs/node_modules/lib0/map.js +16 -86
  75. package/dist/cjs/node_modules/lib0/math.js +3 -32
  76. package/dist/cjs/node_modules/lib0/number.js +4 -15
  77. package/dist/cjs/node_modules/lib0/object.js +14 -88
  78. package/dist/cjs/node_modules/lib0/observable.js +38 -78
  79. package/dist/cjs/node_modules/lib0/pair.js +3 -20
  80. package/dist/cjs/node_modules/lib0/prng.js +8 -77
  81. package/dist/cjs/node_modules/lib0/promise.js +6 -30
  82. package/dist/cjs/node_modules/lib0/random.js +8 -23
  83. package/dist/cjs/node_modules/lib0/schema.js +313 -687
  84. package/dist/cjs/node_modules/lib0/set.js +3 -10
  85. package/dist/cjs/node_modules/lib0/storage.js +20 -55
  86. package/dist/cjs/node_modules/lib0/string.js +17 -72
  87. package/dist/cjs/node_modules/lib0/symbol.js +2 -12
  88. package/dist/cjs/node_modules/lib0/time.js +2 -15
  89. package/dist/cjs/node_modules/lib0/trait/equality.js +3 -28
  90. package/dist/cjs/node_modules/lib0/url.js +4 -18
  91. package/dist/cjs/node_modules/lib0/webcrypto.js +2 -5
  92. package/dist/cjs/node_modules/y-protocols/auth.js +5 -19
  93. package/dist/cjs/node_modules/y-protocols/awareness.js +53 -116
  94. package/dist/cjs/node_modules/y-protocols/sync.js +16 -103
  95. package/dist/cjs/node_modules/y-websocket/src/y-websocket.js +77 -219
  96. package/dist/cjs/node_modules/yjs/dist/yjs.js +2128 -4868
  97. package/dist/cjs/photo/FilterThumb.js +37 -25
  98. package/dist/cjs/photo/NiceAITools.js +679 -467
  99. package/dist/cjs/photo/PhotoEditor.js +34 -20
  100. package/dist/cjs/photo/PhotoEditor.module.css.js +296 -5
  101. package/dist/cjs/photo/PhotoEditorCanvas.js +250 -91
  102. package/dist/cjs/photo/PhotoEditorLeftPanel.js +1086 -224
  103. package/dist/cjs/photo/PhotoEditorToolbar.js +135 -19
  104. package/dist/cjs/photo/photoEditorActions.js +89 -72
  105. package/dist/cjs/photo/photoEditorTypes.js +29 -32
  106. package/dist/cjs/photo/photoFilters.js +959 -985
  107. package/dist/cjs/photo/photoOverlays.js +899 -945
  108. package/dist/cjs/photo/photoSelection.js +427 -499
  109. package/dist/cjs/photo/usePhotoEditor.js +739 -741
  110. package/dist/cjs/pixel/HSVPicker.js +191 -175
  111. package/dist/cjs/pixel/PixelEditor.js +159 -48
  112. package/dist/cjs/pixel/PixelEditor.module.css.js +209 -5
  113. package/dist/cjs/pixel/PixelEditorCanvas.js +29 -16
  114. package/dist/cjs/pixel/PixelEditorMenuBar.js +160 -162
  115. package/dist/cjs/pixel/PixelEditorRightPanel.js +377 -45
  116. package/dist/cjs/pixel/PixelEditorStatusBar.js +47 -20
  117. package/dist/cjs/pixel/PixelEditorTimeline.js +106 -23
  118. package/dist/cjs/pixel/PixelEditorToolbar.js +99 -21
  119. package/dist/cjs/pixel/SpriteGeneratorPanel.js +483 -303
  120. package/dist/cjs/pixel/animalGeneratorPresets.js +776 -601
  121. package/dist/cjs/pixel/asepriteFormat.js +511 -560
  122. package/dist/cjs/pixel/buildingGeneratorPresets.js +693 -520
  123. package/dist/cjs/pixel/characterGeneratorPresets.js +644 -544
  124. package/dist/cjs/pixel/pixel-pro.js +52 -63
  125. package/dist/cjs/pixel/pixelEditorExports.js +388 -428
  126. package/dist/cjs/pixel/pixelEditorTypes.js +91 -108
  127. package/dist/cjs/pixel/pixelEditorUtils.js +519 -561
  128. package/dist/cjs/pixel/spriteGeneratorCore.js +206 -257
  129. package/dist/cjs/pixel/usePixelEditor.js +1321 -1343
  130. package/dist/cjs/texture/Nice3DTexturePainter.js +707 -440
  131. package/dist/cjs/ui/NiceUIDesigner.js +954 -527
  132. package/dist/cjs/vector/NiceSvgToCode.js +586 -446
  133. package/dist/cjs/vector/VectorEditor.js +290 -117
  134. package/dist/cjs/vector/VectorEditor.module.css.js +86 -5
  135. package/dist/cjs/vector/VectorEditorMenuBar.js +333 -275
  136. package/dist/cjs/vector/VectorEditorRightPanel.js +1094 -371
  137. package/dist/cjs/vector/VectorEditorShapeRenderer.js +198 -60
  138. package/dist/cjs/vector/VectorEditorStatusBar.js +39 -13
  139. package/dist/cjs/vector/useVectorEditor.js +848 -889
  140. package/dist/cjs/vector/vectorAnimation.js +110 -137
  141. package/dist/cjs/vector/vectorBooleanOps.js +398 -485
  142. package/dist/cjs/vector/vectorEditorExport.js +201 -203
  143. package/dist/cjs/vector/vectorEditorImport.js +173 -189
  144. package/dist/cjs/vector/vectorEditorTypes.js +42 -49
  145. package/dist/cjs/vector/vectorGradients.js +151 -174
  146. package/dist/cjs/vector/vectorIconExport.js +52 -59
  147. package/dist/cjs/vector/vectorIconImport.js +169 -219
  148. package/dist/esm/animation/AnimatedPerson.js +470 -141
  149. package/dist/esm/animation/AnimationEditor.js +290 -100
  150. package/dist/esm/animation/Audience.js +83 -59
  151. package/dist/esm/animation/BodyRenderer.js +71 -25
  152. package/dist/esm/animation/animationHelper.js +284 -287
  153. package/dist/esm/animation/characterTypes.js +204 -202
  154. package/dist/esm/animation/choreoDSL.js +212 -218
  155. package/dist/esm/animation/choreography.js +8 -31
  156. package/dist/esm/animation/karaokeIntegration.js +74 -76
  157. package/dist/esm/animation/lottieExport.js +112 -113
  158. package/dist/esm/animation/motionPath.js +235 -247
  159. package/dist/esm/animation/physics.js +235 -253
  160. package/dist/esm/animation/rig/RigPlayer.js +53 -54
  161. package/dist/esm/animation/rig/RiggedBody.js +71 -24
  162. package/dist/esm/animation/rig/choreoRigBridge.js +74 -76
  163. package/dist/esm/animation/rig/ik.js +14 -17
  164. package/dist/esm/animation/rig/poseOps.js +37 -40
  165. package/dist/esm/animation/rig/presets.js +70 -71
  166. package/dist/esm/animation/rig/rigMath.js +28 -31
  167. package/dist/esm/animation/rig/rigTypes.js +62 -62
  168. package/dist/esm/animation/shapes/arms.js +32 -12
  169. package/dist/esm/animation/shapes/eyes.js +135 -46
  170. package/dist/esm/animation/shapes/hair.js +80 -44
  171. package/dist/esm/animation/shapes/heads.js +140 -56
  172. package/dist/esm/animation/shapes/headwear.js +54 -37
  173. package/dist/esm/animation/shapes/legs.js +32 -12
  174. package/dist/esm/animation/shapes/mouths.js +127 -43
  175. package/dist/esm/animation/shapes/noses.js +87 -24
  176. package/dist/esm/animation/shapes/outfits.js +106 -38
  177. package/dist/esm/animation/shapes/torsos.js +16 -10
  178. package/dist/esm/animation/spineExport.js +106 -120
  179. package/dist/esm/core/LocalUI.js +111 -42
  180. package/dist/esm/core/NiceBrushEngine.js +761 -551
  181. package/dist/esm/core/collaboration.js +255 -250
  182. package/dist/esm/core/colorBlindness.js +106 -114
  183. package/dist/esm/core/dragDrop.js +157 -157
  184. package/dist/esm/core/editorAsk.js +53 -40
  185. package/dist/esm/core/fixes.js +145 -170
  186. package/dist/esm/core/gridGuides.js +178 -200
  187. package/dist/esm/core/historyVisual.js +158 -134
  188. package/dist/esm/core/i18n.js +15 -13
  189. package/dist/esm/core/integrations.js +244 -248
  190. package/dist/esm/core/minimap.js +151 -150
  191. package/dist/esm/core/plugins.js +102 -112
  192. package/dist/esm/core/rtl.js +77 -82
  193. package/dist/esm/core/shortcuts.js +200 -224
  194. package/dist/esm/core/theme.js +150 -148
  195. package/dist/esm/core/yjsCollaboration.js +240 -292
  196. package/dist/esm/design/design-tools.js +13 -21
  197. package/dist/esm/font/NiceFontEditor.js +880 -315
  198. package/dist/esm/font/fontFormatUtils.js +394 -447
  199. package/dist/esm/game/GameAsset2dEditor.js +1202 -391
  200. package/dist/esm/game/GameAsset2dEditor.module.css.js +146 -3
  201. package/dist/esm/game/gameAssetTypes.js +8 -15
  202. package/dist/esm/game/gameAssetUtils.js +408 -412
  203. package/dist/esm/game/useGameAssetEditor.js +821 -762
  204. package/dist/esm/index.js +332 -69
  205. package/dist/esm/nice2dev-ui-graphics.css +1 -1
  206. package/dist/esm/node_modules/lib0/array.js +15 -69
  207. package/dist/esm/node_modules/lib0/binary.js +14 -19
  208. package/dist/esm/node_modules/lib0/broadcastchannel.js +27 -85
  209. package/dist/esm/node_modules/lib0/buffer.js +20 -80
  210. package/dist/esm/node_modules/lib0/conditions.js +4 -15
  211. package/dist/esm/node_modules/lib0/decoding.js +128 -286
  212. package/dist/esm/node_modules/lib0/dom.js +20 -43
  213. package/dist/esm/node_modules/lib0/encoding.js +90 -415
  214. package/dist/esm/node_modules/lib0/environment.js +33 -91
  215. package/dist/esm/node_modules/lib0/error.js +8 -28
  216. package/dist/esm/node_modules/lib0/function.js +31 -65
  217. package/dist/esm/node_modules/lib0/iterator.js +11 -32
  218. package/dist/esm/node_modules/lib0/logging.common.js +22 -21
  219. package/dist/esm/node_modules/lib0/logging.js +44 -70
  220. package/dist/esm/node_modules/lib0/map.js +21 -85
  221. package/dist/esm/node_modules/lib0/math.js +9 -31
  222. package/dist/esm/node_modules/lib0/number.js +7 -14
  223. package/dist/esm/node_modules/lib0/object.js +26 -87
  224. package/dist/esm/node_modules/lib0/observable.js +40 -77
  225. package/dist/esm/node_modules/lib0/pair.js +5 -19
  226. package/dist/esm/node_modules/lib0/prng.js +15 -76
  227. package/dist/esm/node_modules/lib0/promise.js +7 -29
  228. package/dist/esm/node_modules/lib0/random.js +10 -22
  229. package/dist/esm/node_modules/lib0/schema.js +367 -686
  230. package/dist/esm/node_modules/lib0/set.js +4 -9
  231. package/dist/esm/node_modules/lib0/storage.js +23 -54
  232. package/dist/esm/node_modules/lib0/string.js +26 -71
  233. package/dist/esm/node_modules/lib0/symbol.js +3 -11
  234. package/dist/esm/node_modules/lib0/time.js +3 -14
  235. package/dist/esm/node_modules/lib0/trait/equality.js +5 -27
  236. package/dist/esm/node_modules/lib0/url.js +5 -17
  237. package/dist/esm/node_modules/lib0/webcrypto.js +3 -4
  238. package/dist/esm/node_modules/y-protocols/auth.js +7 -18
  239. package/dist/esm/node_modules/y-protocols/awareness.js +58 -115
  240. package/dist/esm/node_modules/y-protocols/sync.js +26 -102
  241. package/dist/esm/node_modules/y-websocket/src/y-websocket.js +82 -218
  242. package/dist/esm/node_modules/yjs/dist/yjs.js +2263 -4899
  243. package/dist/esm/photo/FilterThumb.js +37 -23
  244. package/dist/esm/photo/NiceAITools.js +685 -466
  245. package/dist/esm/photo/PhotoEditor.js +34 -18
  246. package/dist/esm/photo/PhotoEditor.module.css.js +296 -3
  247. package/dist/esm/photo/PhotoEditorCanvas.js +250 -89
  248. package/dist/esm/photo/PhotoEditorLeftPanel.js +1086 -222
  249. package/dist/esm/photo/PhotoEditorToolbar.js +135 -17
  250. package/dist/esm/photo/photoEditorActions.js +93 -71
  251. package/dist/esm/photo/photoEditorTypes.js +32 -31
  252. package/dist/esm/photo/photoFilters.js +967 -984
  253. package/dist/esm/photo/photoOverlays.js +913 -944
  254. package/dist/esm/photo/photoSelection.js +438 -498
  255. package/dist/esm/photo/usePhotoEditor.js +740 -740
  256. package/dist/esm/pixel/HSVPicker.js +192 -174
  257. package/dist/esm/pixel/PixelEditor.js +160 -47
  258. package/dist/esm/pixel/PixelEditor.module.css.js +209 -3
  259. package/dist/esm/pixel/PixelEditorCanvas.js +29 -14
  260. package/dist/esm/pixel/PixelEditorMenuBar.js +160 -160
  261. package/dist/esm/pixel/PixelEditorRightPanel.js +377 -43
  262. package/dist/esm/pixel/PixelEditorStatusBar.js +47 -18
  263. package/dist/esm/pixel/PixelEditorTimeline.js +106 -21
  264. package/dist/esm/pixel/PixelEditorToolbar.js +99 -19
  265. package/dist/esm/pixel/SpriteGeneratorPanel.js +483 -301
  266. package/dist/esm/pixel/animalGeneratorPresets.js +782 -600
  267. package/dist/esm/pixel/asepriteFormat.js +517 -559
  268. package/dist/esm/pixel/buildingGeneratorPresets.js +699 -519
  269. package/dist/esm/pixel/characterGeneratorPresets.js +650 -543
  270. package/dist/esm/pixel/pixel-pro.js +53 -62
  271. package/dist/esm/pixel/pixelEditorExports.js +397 -427
  272. package/dist/esm/pixel/pixelEditorTypes.js +103 -107
  273. package/dist/esm/pixel/pixelEditorUtils.js +551 -560
  274. package/dist/esm/pixel/spriteGeneratorCore.js +223 -256
  275. package/dist/esm/pixel/usePixelEditor.js +1322 -1342
  276. package/dist/esm/texture/Nice3DTexturePainter.js +709 -439
  277. package/dist/esm/ui/NiceUIDesigner.js +957 -526
  278. package/dist/esm/vector/NiceSvgToCode.js +589 -445
  279. package/dist/esm/vector/VectorEditor.js +291 -116
  280. package/dist/esm/vector/VectorEditor.module.css.js +86 -3
  281. package/dist/esm/vector/VectorEditorMenuBar.js +333 -273
  282. package/dist/esm/vector/VectorEditorRightPanel.js +1094 -369
  283. package/dist/esm/vector/VectorEditorShapeRenderer.js +199 -59
  284. package/dist/esm/vector/VectorEditorStatusBar.js +39 -11
  285. package/dist/esm/vector/useVectorEditor.js +849 -888
  286. package/dist/esm/vector/vectorAnimation.js +115 -136
  287. package/dist/esm/vector/vectorBooleanOps.js +401 -484
  288. package/dist/esm/vector/vectorEditorExport.js +207 -202
  289. package/dist/esm/vector/vectorEditorImport.js +174 -188
  290. package/dist/esm/vector/vectorEditorTypes.js +50 -48
  291. package/dist/esm/vector/vectorGradients.js +158 -173
  292. package/dist/esm/vector/vectorIconExport.js +53 -58
  293. package/dist/esm/vector/vectorIconImport.js +171 -218
  294. package/package.json +9 -15
@@ -1,656 +1,647 @@
1
- import '@nice2dev/ui-core';
2
-
3
- /**
4
- * pixelEditorUtils.ts — Drawing algorithms for the pixel editor.
5
- * Bresenham line, midpoint ellipse, flood fill, selection, brush shapes,
6
- * pressure-aware blending, dithering, gradient, symmetry etc.
7
- */
1
+ import "@nice2dev/ui-core";
8
2
  const DEFAULT_BRUSH = {
9
- size: 1,
10
- shape: 'square',
11
- opacity: 100,
12
- flow: 100,
13
- spacing: 25,
14
- pressureSize: true,
15
- pressureOpacity: false,
16
- hardness: 100,
3
+ size: 1,
4
+ shape: "square",
5
+ opacity: 100,
6
+ flow: 100,
7
+ spacing: 25,
8
+ pressureSize: true,
9
+ pressureOpacity: false,
10
+ hardness: 100
17
11
  };
18
- /** Read a pixel from ImageData at (x,y). Returns [r,g,b,a]. */
19
12
  function getPixel(data, x, y) {
20
- if (x < 0 || y < 0 || x >= data.width || y >= data.height) {
21
- return [0, 0, 0, 0];
22
- }
23
- const i = (y * data.width + x) * 4;
24
- return [data.data[i], data.data[i + 1], data.data[i + 2], data.data[i + 3]];
13
+ if (x < 0 || y < 0 || x >= data.width || y >= data.height) {
14
+ return [0, 0, 0, 0];
15
+ }
16
+ const i = (y * data.width + x) * 4;
17
+ return [data.data[i], data.data[i + 1], data.data[i + 2], data.data[i + 3]];
25
18
  }
26
- /** Set a pixel in ImageData at (x,y). */
27
19
  function setPixel(data, x, y, color) {
28
- if (x < 0 || y < 0 || x >= data.width || y >= data.height) {
29
- return;
30
- }
31
- const i = (y * data.width + x) * 4;
32
- data.data[i] = color[0];
33
- data.data[i + 1] = color[1];
34
- data.data[i + 2] = color[2];
35
- data.data[i + 3] = color[3];
20
+ if (x < 0 || y < 0 || x >= data.width || y >= data.height) {
21
+ return;
22
+ }
23
+ const i = (y * data.width + x) * 4;
24
+ data.data[i] = color[0];
25
+ data.data[i + 1] = color[1];
26
+ data.data[i + 2] = color[2];
27
+ data.data[i + 3] = color[3];
36
28
  }
37
- /** Blend a pixel onto ImageData at (x,y) with alpha compositing. */
38
29
  function blendPixel(data, x, y, color, strength = 1) {
39
- if (x < 0 || y < 0 || x >= data.width || y >= data.height) {
40
- return;
41
- }
42
- const i = (y * data.width + x) * 4;
43
- const sa = (color[3] / 255) * strength;
44
- if (sa <= 0) {
45
- return;
46
- }
47
- const da = data.data[i + 3] / 255;
48
- const oa = sa + da * (1 - sa);
49
- if (oa <= 0) {
50
- return;
51
- }
52
- data.data[i] = Math.round((color[0] * sa + data.data[i] * da * (1 - sa)) / oa);
53
- data.data[i + 1] = Math.round((color[1] * sa + data.data[i + 1] * da * (1 - sa)) / oa);
54
- data.data[i + 2] = Math.round((color[2] * sa + data.data[i + 2] * da * (1 - sa)) / oa);
55
- data.data[i + 3] = Math.round(oa * 255);
30
+ if (x < 0 || y < 0 || x >= data.width || y >= data.height) {
31
+ return;
32
+ }
33
+ const i = (y * data.width + x) * 4;
34
+ const sa = color[3] / 255 * strength;
35
+ if (sa <= 0) {
36
+ return;
37
+ }
38
+ const da = data.data[i + 3] / 255;
39
+ const oa = sa + da * (1 - sa);
40
+ if (oa <= 0) {
41
+ return;
42
+ }
43
+ data.data[i] = Math.round((color[0] * sa + data.data[i] * da * (1 - sa)) / oa);
44
+ data.data[i + 1] = Math.round((color[1] * sa + data.data[i + 1] * da * (1 - sa)) / oa);
45
+ data.data[i + 2] = Math.round((color[2] * sa + data.data[i + 2] * da * (1 - sa)) / oa);
46
+ data.data[i + 3] = Math.round(oa * 255);
56
47
  }
57
- /** Generate brush mask: returns array of {dx, dy, strength} offsets. */
58
48
  function getBrushMask(cfg, pressure = 0.5) {
59
- const sz = cfg.pressureSize ? Math.max(1, Math.round(cfg.size * pressure)) : cfg.size;
60
- const half = Math.floor(sz / 2);
61
- const flowMul = cfg.flow / 100;
62
- const opaMul = (cfg.opacity / 100) * (cfg.pressureOpacity ? pressure : 1);
63
- const mask = [];
64
- for (let dy = -half; dy < sz - half; dy++) {
65
- for (let dx = -half; dx < sz - half; dx++) {
66
- let inBrush = false;
67
- let dist = 0;
68
- switch (cfg.shape) {
69
- case 'square':
70
- inBrush = true;
71
- dist = Math.max(Math.abs(dx), Math.abs(dy)) / Math.max(half, 0.5);
72
- break;
73
- case 'circle': {
74
- const r = Math.sqrt(dx * dx + dy * dy);
75
- inBrush = r <= half + 0.5;
76
- dist = half > 0 ? r / half : 0;
77
- break;
78
- }
79
- case 'diamond':
80
- inBrush = Math.abs(dx) + Math.abs(dy) <= half;
81
- dist = half > 0 ? (Math.abs(dx) + Math.abs(dy)) / half : 0;
82
- break;
83
- case 'spray':
84
- inBrush = Math.random() < 0.3 && Math.sqrt(dx * dx + dy * dy) <= half + 0.5;
85
- dist = 0;
86
- break;
87
- }
88
- if (!inBrush) {
89
- continue;
90
- }
91
- // hardness: 100 = hard edge, 0 = soft falloff
92
- const hardFactor = cfg.hardness / 100;
93
- const edge = dist > hardFactor ? Math.max(0, 1 - (dist - hardFactor) / (1 - hardFactor + 0.001)) : 1;
94
- mask.push({ dx, dy, strength: edge * flowMul * opaMul });
49
+ const sz = cfg.pressureSize ? Math.max(1, Math.round(cfg.size * pressure)) : cfg.size;
50
+ const half = Math.floor(sz / 2);
51
+ const flowMul = cfg.flow / 100;
52
+ const opaMul = cfg.opacity / 100 * (cfg.pressureOpacity ? pressure : 1);
53
+ const mask = [];
54
+ for (let dy = -half; dy < sz - half; dy++) {
55
+ for (let dx = -half; dx < sz - half; dx++) {
56
+ let inBrush = false;
57
+ let dist = 0;
58
+ switch (cfg.shape) {
59
+ case "square":
60
+ inBrush = true;
61
+ dist = Math.max(Math.abs(dx), Math.abs(dy)) / Math.max(half, 0.5);
62
+ break;
63
+ case "circle": {
64
+ const r = Math.sqrt(dx * dx + dy * dy);
65
+ inBrush = r <= half + 0.5;
66
+ dist = half > 0 ? r / half : 0;
67
+ break;
95
68
  }
69
+ case "diamond":
70
+ inBrush = Math.abs(dx) + Math.abs(dy) <= half;
71
+ dist = half > 0 ? (Math.abs(dx) + Math.abs(dy)) / half : 0;
72
+ break;
73
+ case "spray":
74
+ inBrush = Math.random() < 0.3 && Math.sqrt(dx * dx + dy * dy) <= half + 0.5;
75
+ dist = 0;
76
+ break;
77
+ }
78
+ if (!inBrush) {
79
+ continue;
80
+ }
81
+ const hardFactor = cfg.hardness / 100;
82
+ const edge = dist > hardFactor ? Math.max(0, 1 - (dist - hardFactor) / (1 - hardFactor + 1e-3)) : 1;
83
+ mask.push({ dx, dy, strength: edge * flowMul * opaMul });
96
84
  }
97
- return mask;
85
+ }
86
+ return mask;
98
87
  }
99
- /** Apply brush stamp at (x,y) with all brush settings. */
100
88
  function applyBrushStamp(data, x, y, color, cfg, pressure = 0.5, erase = false) {
101
- const mask = getBrushMask(cfg, pressure);
102
- for (const { dx, dy, strength } of mask) {
103
- const gx = x + dx, gy = y + dy;
104
- if (gx < 0 || gy < 0 || gx >= data.width || gy >= data.height) {
105
- continue;
106
- }
107
- if (erase) {
108
- const cur = getPixel(data, gx, gy);
109
- const newA = Math.max(0, cur[3] - Math.round(255 * strength));
110
- setPixel(data, gx, gy, [cur[0], cur[1], cur[2], newA]);
111
- }
112
- else {
113
- blendPixel(data, gx, gy, color, strength);
114
- }
89
+ const mask = getBrushMask(cfg, pressure);
90
+ for (const { dx, dy, strength } of mask) {
91
+ const gx = x + dx, gy = y + dy;
92
+ if (gx < 0 || gy < 0 || gx >= data.width || gy >= data.height) {
93
+ continue;
94
+ }
95
+ if (erase) {
96
+ const cur = getPixel(data, gx, gy);
97
+ const newA = Math.max(0, cur[3] - Math.round(255 * strength));
98
+ setPixel(data, gx, gy, [cur[0], cur[1], cur[2], newA]);
99
+ } else {
100
+ blendPixel(data, gx, gy, color, strength);
115
101
  }
102
+ }
116
103
  }
117
- /** Get symmetry points for a coordinate. */
118
104
  function getSymmetryPoints(x, y, w, h, mode) {
119
- const pts = [{ x, y }];
120
- switch (mode) {
121
- case 'h':
122
- pts.push({ x: w - 1 - x, y });
123
- break;
124
- case 'v':
125
- pts.push({ x, y: h - 1 - y });
126
- break;
127
- case 'both':
128
- pts.push({ x: w - 1 - x, y }, { x, y: h - 1 - y }, { x: w - 1 - x, y: h - 1 - y });
129
- break;
130
- case 'radial4': {
131
- const cx = w / 2, cy = h / 2;
132
- const dx = x - cx, dy = y - cy;
133
- pts.push({ x: Math.round(cx - dy), y: Math.round(cy + dx) }, { x: Math.round(cx - dx), y: Math.round(cy - dy) }, { x: Math.round(cx + dy), y: Math.round(cy - dx) });
134
- break;
135
- }
105
+ const pts = [{ x, y }];
106
+ switch (mode) {
107
+ case "h":
108
+ pts.push({ x: w - 1 - x, y });
109
+ break;
110
+ case "v":
111
+ pts.push({ x, y: h - 1 - y });
112
+ break;
113
+ case "both":
114
+ pts.push({ x: w - 1 - x, y }, { x, y: h - 1 - y }, { x: w - 1 - x, y: h - 1 - y });
115
+ break;
116
+ case "radial4": {
117
+ const cx = w / 2, cy = h / 2;
118
+ const dx = x - cx, dy = y - cy;
119
+ pts.push(
120
+ { x: Math.round(cx - dy), y: Math.round(cy + dx) },
121
+ { x: Math.round(cx - dx), y: Math.round(cy - dy) },
122
+ { x: Math.round(cx + dy), y: Math.round(cy - dx) }
123
+ );
124
+ break;
136
125
  }
137
- return pts;
126
+ }
127
+ return pts;
138
128
  }
139
- /** Draw a linear gradient between two points on ImageData. */
140
129
  function drawGradient(data, x0, y0, x1, y1, color1, color2, sel) {
141
- var _a, _b, _c, _d;
142
- const dx = x1 - x0, dy = y1 - y0;
143
- const len = Math.sqrt(dx * dx + dy * dy);
144
- if (len === 0) {
145
- return;
146
- }
147
- const sx = (_a = sel === null || sel === void 0 ? void 0 : sel.x) !== null && _a !== void 0 ? _a : 0, sy = (_b = sel === null || sel === void 0 ? void 0 : sel.y) !== null && _b !== void 0 ? _b : 0;
148
- const sw = (_c = sel === null || sel === void 0 ? void 0 : sel.w) !== null && _c !== void 0 ? _c : data.width, sh = (_d = sel === null || sel === void 0 ? void 0 : sel.h) !== null && _d !== void 0 ? _d : data.height;
149
- for (let py = sy; py < sy + sh; py++) {
150
- for (let px = sx; px < sx + sw; px++) {
151
- const t = Math.max(0, Math.min(1, ((px - x0) * dx + (py - y0) * dy) / (len * len)));
152
- const c = [
153
- Math.round(color1[0] + (color2[0] - color1[0]) * t),
154
- Math.round(color1[1] + (color2[1] - color1[1]) * t),
155
- Math.round(color1[2] + (color2[2] - color1[2]) * t),
156
- Math.round(color1[3] + (color2[3] - color1[3]) * t),
157
- ];
158
- setPixel(data, px, py, c);
159
- }
130
+ const dx = x1 - x0, dy = y1 - y0;
131
+ const len = Math.sqrt(dx * dx + dy * dy);
132
+ if (len === 0) {
133
+ return;
134
+ }
135
+ const sx = sel?.x ?? 0, sy = sel?.y ?? 0;
136
+ const sw = sel?.w ?? data.width, sh = sel?.h ?? data.height;
137
+ for (let py = sy; py < sy + sh; py++) {
138
+ for (let px = sx; px < sx + sw; px++) {
139
+ const t = Math.max(0, Math.min(1, ((px - x0) * dx + (py - y0) * dy) / (len * len)));
140
+ const c = [
141
+ Math.round(color1[0] + (color2[0] - color1[0]) * t),
142
+ Math.round(color1[1] + (color2[1] - color1[1]) * t),
143
+ Math.round(color1[2] + (color2[2] - color1[2]) * t),
144
+ Math.round(color1[3] + (color2[3] - color1[3]) * t)
145
+ ];
146
+ setPixel(data, px, py, c);
160
147
  }
148
+ }
161
149
  }
162
- /** Ordered dithering 4x4 Bayer matrix. */
163
150
  const BAYER4 = [
164
- [0, 8, 2, 10],
165
- [12, 4, 14, 6],
166
- [3, 11, 1, 9],
167
- [15, 7, 13, 5],
151
+ [0, 8, 2, 10],
152
+ [12, 4, 14, 6],
153
+ [3, 11, 1, 9],
154
+ [15, 7, 13, 5]
168
155
  ];
169
- /** Apply dithered brush stamp. */
170
156
  function applyDitherStamp(data, x, y, color, size, pressure = 0.5) {
171
- const half = Math.floor(size / 2);
172
- const threshold = pressure * 16;
173
- for (let dy = -half; dy < size - half; dy++) {
174
- for (let dx = -half; dx < size - half; dx++) {
175
- const gx = x + dx, gy = y + dy;
176
- if (gx < 0 || gy < 0 || gx >= data.width || gy >= data.height) {
177
- continue;
178
- }
179
- const bayer = BAYER4[((gy % 4) + 4) % 4][((gx % 4) + 4) % 4];
180
- if (bayer < threshold) {
181
- setPixel(data, gx, gy, color);
182
- }
183
- }
157
+ const half = Math.floor(size / 2);
158
+ const threshold = pressure * 16;
159
+ for (let dy = -half; dy < size - half; dy++) {
160
+ for (let dx = -half; dx < size - half; dx++) {
161
+ const gx = x + dx, gy = y + dy;
162
+ if (gx < 0 || gy < 0 || gx >= data.width || gy >= data.height) {
163
+ continue;
164
+ }
165
+ const bayer = BAYER4[(gy % 4 + 4) % 4][(gx % 4 + 4) % 4];
166
+ if (bayer < threshold) {
167
+ setPixel(data, gx, gy, color);
168
+ }
184
169
  }
170
+ }
185
171
  }
186
- /** Copy pixels from a selection region. */
187
172
  function copySelection(data, sel) {
188
- const out = new ImageData(sel.w, sel.h);
189
- for (let y = 0; y < sel.h; y++) {
190
- for (let x = 0; x < sel.w; x++) {
191
- const px = getPixel(data, sel.x + x, sel.y + y);
192
- setPixel(out, x, y, px);
193
- }
173
+ const out = new ImageData(sel.w, sel.h);
174
+ for (let y = 0; y < sel.h; y++) {
175
+ for (let x = 0; x < sel.w; x++) {
176
+ const px = getPixel(data, sel.x + x, sel.y + y);
177
+ setPixel(out, x, y, px);
194
178
  }
195
- return out;
179
+ }
180
+ return out;
196
181
  }
197
- /** Paste ImageData at position. */
198
182
  function pasteAt(dst, src, ox, oy) {
199
- for (let y = 0; y < src.height; y++) {
200
- for (let x = 0; x < src.width; x++) {
201
- const px = getPixel(src, x, y);
202
- if (px[3] > 0) {
203
- blendPixel(dst, ox + x, oy + y, px, 1);
204
- }
205
- }
183
+ for (let y = 0; y < src.height; y++) {
184
+ for (let x = 0; x < src.width; x++) {
185
+ const px = getPixel(src, x, y);
186
+ if (px[3] > 0) {
187
+ blendPixel(dst, ox + x, oy + y, px, 1);
188
+ }
206
189
  }
190
+ }
207
191
  }
208
- /** Clear pixels in selection region. */
209
192
  function clearSelection(data, sel) {
210
- for (let y = sel.y; y < sel.y + sel.h; y++) {
211
- for (let x = sel.x; x < sel.x + sel.w; x++) {
212
- setPixel(data, x, y, [0, 0, 0, 0]);
213
- }
193
+ for (let y = sel.y; y < sel.y + sel.h; y++) {
194
+ for (let x = sel.x; x < sel.x + sel.w; x++) {
195
+ setPixel(data, x, y, [0, 0, 0, 0]);
214
196
  }
197
+ }
215
198
  }
216
- /** Bresenham line — calls callback for each pixel along the line. */
217
199
  function bresenhamLine(x0, y0, x1, y1, cb) {
218
- const dx = Math.abs(x1 - x0), dy = -Math.abs(y1 - y0);
219
- const sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1;
220
- let err = dx + dy;
221
- for (;;) {
222
- cb(x0, y0);
223
- if (x0 === x1 && y0 === y1) {
224
- break;
225
- }
226
- const e2 = 2 * err;
227
- if (e2 >= dy) {
228
- err += dy;
229
- x0 += sx;
230
- }
231
- if (e2 <= dx) {
232
- err += dx;
233
- y0 += sy;
234
- }
200
+ const dx = Math.abs(x1 - x0), dy = -Math.abs(y1 - y0);
201
+ const sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1;
202
+ let err = dx + dy;
203
+ for (; ; ) {
204
+ cb(x0, y0);
205
+ if (x0 === x1 && y0 === y1) {
206
+ break;
207
+ }
208
+ const e2 = 2 * err;
209
+ if (e2 >= dy) {
210
+ err += dy;
211
+ x0 += sx;
212
+ }
213
+ if (e2 <= dx) {
214
+ err += dx;
215
+ y0 += sy;
235
216
  }
217
+ }
236
218
  }
237
- /** Flood fill — fills connected region of same color with new color. */
238
219
  function floodFill(data, startX, startY, fillColor) {
239
- const w = data.width, h = data.height;
240
- if (startX < 0 || startY < 0 || startX >= w || startY >= h) {
241
- return;
220
+ const w = data.width, h = data.height;
221
+ if (startX < 0 || startY < 0 || startX >= w || startY >= h) {
222
+ return;
223
+ }
224
+ const target = getPixel(data, startX, startY);
225
+ if (colorsEqual(target, fillColor)) {
226
+ return;
227
+ }
228
+ const stack = [startX, startY];
229
+ const visited = new Uint8Array(w * h);
230
+ while (stack.length > 0) {
231
+ const y = stack.pop();
232
+ const x = stack.pop();
233
+ const key = y * w + x;
234
+ if (x < 0 || y < 0 || x >= w || y >= h) {
235
+ continue;
242
236
  }
243
- const target = getPixel(data, startX, startY);
244
- if (colorsEqual(target, fillColor)) {
245
- return;
237
+ if (visited[key]) {
238
+ continue;
246
239
  }
247
- const stack = [startX, startY];
248
- const visited = new Uint8Array(w * h);
249
- while (stack.length > 0) {
250
- const y = stack.pop();
251
- const x = stack.pop();
252
- const key = y * w + x;
253
- if (x < 0 || y < 0 || x >= w || y >= h) {
254
- continue;
255
- }
256
- if (visited[key]) {
257
- continue;
258
- }
259
- visited[key] = 1;
260
- const current = getPixel(data, x, y);
261
- if (!colorsEqual(current, target)) {
262
- continue;
263
- }
264
- setPixel(data, x, y, fillColor);
265
- stack.push(x - 1, y, x + 1, y, x, y - 1, x, y + 1);
240
+ visited[key] = 1;
241
+ const current = getPixel(data, x, y);
242
+ if (!colorsEqual(current, target)) {
243
+ continue;
266
244
  }
245
+ setPixel(data, x, y, fillColor);
246
+ stack.push(x - 1, y, x + 1, y, x, y - 1, x, y + 1);
247
+ }
267
248
  }
268
- /** Check if two RGBA colors are equal (with small tolerance for alpha). */
269
249
  function colorsEqual(a, b, tolerance = 0) {
270
- return (Math.abs(a[0] - b[0]) <= tolerance &&
271
- Math.abs(a[1] - b[1]) <= tolerance &&
272
- Math.abs(a[2] - b[2]) <= tolerance &&
273
- Math.abs(a[3] - b[3]) <= tolerance);
250
+ return Math.abs(a[0] - b[0]) <= tolerance && Math.abs(a[1] - b[1]) <= tolerance && Math.abs(a[2] - b[2]) <= tolerance && Math.abs(a[3] - b[3]) <= tolerance;
274
251
  }
275
- /** Parse hex color string to RGBA. */
276
252
  function hexToRGBA(hex, alpha = 255) {
277
- const h = hex.startsWith('#') ? hex.slice(1) : hex;
278
- return [
279
- parseInt(h.slice(0, 2), 16) || 0,
280
- parseInt(h.slice(2, 4), 16) || 0,
281
- parseInt(h.slice(4, 6), 16) || 0,
282
- alpha,
283
- ];
253
+ const h = hex.startsWith("#") ? hex.slice(1) : hex;
254
+ return [
255
+ parseInt(h.slice(0, 2), 16) || 0,
256
+ parseInt(h.slice(2, 4), 16) || 0,
257
+ parseInt(h.slice(4, 6), 16) || 0,
258
+ alpha
259
+ ];
284
260
  }
285
- /** Convert RGBA to hex string. */
286
261
  function rgbaToHex(c) {
287
- return '#' + [c[0], c[1], c[2]].map((v) => v.toString(16).padStart(2, '0')).join('');
262
+ return "#" + [c[0], c[1], c[2]].map((v) => v.toString(16).padStart(2, "0")).join("");
288
263
  }
289
- /** Create blank ImageData. */
290
264
  function createBlankImageData(width, height) {
291
- return new ImageData(width, height);
265
+ return new ImageData(width, height);
292
266
  }
293
- /** Clone ImageData. */
294
267
  function cloneImageData(src) {
295
- const dst = new ImageData(src.width, src.height);
296
- dst.data.set(src.data);
297
- return dst;
268
+ const dst = new ImageData(src.width, src.height);
269
+ dst.data.set(src.data);
270
+ return dst;
298
271
  }
299
- /** Flip ImageData horizontally. */
300
272
  function flipH(src) {
301
- const dst = new ImageData(src.width, src.height);
302
- for (let y = 0; y < src.height; y++) {
303
- for (let x = 0; x < src.width; x++) {
304
- const si = (y * src.width + x) * 4;
305
- const di = (y * src.width + (src.width - 1 - x)) * 4;
306
- dst.data[di] = src.data[si];
307
- dst.data[di + 1] = src.data[si + 1];
308
- dst.data[di + 2] = src.data[si + 2];
309
- dst.data[di + 3] = src.data[si + 3];
310
- }
273
+ const dst = new ImageData(src.width, src.height);
274
+ for (let y = 0; y < src.height; y++) {
275
+ for (let x = 0; x < src.width; x++) {
276
+ const si = (y * src.width + x) * 4;
277
+ const di = (y * src.width + (src.width - 1 - x)) * 4;
278
+ dst.data[di] = src.data[si];
279
+ dst.data[di + 1] = src.data[si + 1];
280
+ dst.data[di + 2] = src.data[si + 2];
281
+ dst.data[di + 3] = src.data[si + 3];
311
282
  }
312
- return dst;
283
+ }
284
+ return dst;
313
285
  }
314
- /** Flip ImageData vertically. */
315
286
  function flipV(src) {
316
- const dst = new ImageData(src.width, src.height);
317
- for (let y = 0; y < src.height; y++) {
318
- for (let x = 0; x < src.width; x++) {
319
- const si = (y * src.width + x) * 4;
320
- const di = ((src.height - 1 - y) * src.width + x) * 4;
321
- dst.data[di] = src.data[si];
322
- dst.data[di + 1] = src.data[si + 1];
323
- dst.data[di + 2] = src.data[si + 2];
324
- dst.data[di + 3] = src.data[si + 3];
325
- }
287
+ const dst = new ImageData(src.width, src.height);
288
+ for (let y = 0; y < src.height; y++) {
289
+ for (let x = 0; x < src.width; x++) {
290
+ const si = (y * src.width + x) * 4;
291
+ const di = ((src.height - 1 - y) * src.width + x) * 4;
292
+ dst.data[di] = src.data[si];
293
+ dst.data[di + 1] = src.data[si + 1];
294
+ dst.data[di + 2] = src.data[si + 2];
295
+ dst.data[di + 3] = src.data[si + 3];
326
296
  }
327
- return dst;
297
+ }
298
+ return dst;
328
299
  }
329
- /** Rotate ImageData 90° clockwise. Returns new ImageData with swapped dimensions. */
330
300
  function rotateCW(src) {
331
- const dst = new ImageData(src.height, src.width);
332
- for (let y = 0; y < src.height; y++) {
333
- for (let x = 0; x < src.width; x++) {
334
- const si = (y * src.width + x) * 4;
335
- const di = (x * dst.width + (dst.width - 1 - y)) * 4;
336
- dst.data[di] = src.data[si];
337
- dst.data[di + 1] = src.data[si + 1];
338
- dst.data[di + 2] = src.data[si + 2];
339
- dst.data[di + 3] = src.data[si + 3];
340
- }
301
+ const dst = new ImageData(src.height, src.width);
302
+ for (let y = 0; y < src.height; y++) {
303
+ for (let x = 0; x < src.width; x++) {
304
+ const si = (y * src.width + x) * 4;
305
+ const di = (x * dst.width + (dst.width - 1 - y)) * 4;
306
+ dst.data[di] = src.data[si];
307
+ dst.data[di + 1] = src.data[si + 1];
308
+ dst.data[di + 2] = src.data[si + 2];
309
+ dst.data[di + 3] = src.data[si + 3];
341
310
  }
342
- return dst;
311
+ }
312
+ return dst;
343
313
  }
344
- // Bayer matrices for ordered dithering
345
314
  const BAYER_2x2 = [
346
- [0, 2],
347
- [3, 1],
315
+ [0, 2],
316
+ [3, 1]
348
317
  ];
349
318
  const BAYER_8x8 = [
350
- [0, 32, 8, 40, 2, 34, 10, 42],
351
- [48, 16, 56, 24, 50, 18, 58, 26],
352
- [12, 44, 4, 36, 14, 46, 6, 38],
353
- [60, 28, 52, 20, 62, 30, 54, 22],
354
- [3, 35, 11, 43, 1, 33, 9, 41],
355
- [51, 19, 59, 27, 49, 17, 57, 25],
356
- [15, 47, 7, 39, 13, 45, 5, 37],
357
- [63, 31, 55, 23, 61, 29, 53, 21],
319
+ [0, 32, 8, 40, 2, 34, 10, 42],
320
+ [48, 16, 56, 24, 50, 18, 58, 26],
321
+ [12, 44, 4, 36, 14, 46, 6, 38],
322
+ [60, 28, 52, 20, 62, 30, 54, 22],
323
+ [3, 35, 11, 43, 1, 33, 9, 41],
324
+ [51, 19, 59, 27, 49, 17, 57, 25],
325
+ [15, 47, 7, 39, 13, 45, 5, 37],
326
+ [63, 31, 55, 23, 61, 29, 53, 21]
358
327
  ];
359
- /** Find closest color in palette */
360
328
  function findClosestPaletteColor(r, g, b, palette) {
361
- let minDist = Infinity;
362
- let closest = palette[0];
363
- for (const color of palette) {
364
- const dr = r - color[0];
365
- const dg = g - color[1];
366
- const db = b - color[2];
367
- const dist = dr * dr + dg * dg + db * db;
368
- if (dist < minDist) {
369
- minDist = dist;
370
- closest = color;
371
- }
329
+ let minDist = Infinity;
330
+ let closest = palette[0];
331
+ for (const color of palette) {
332
+ const dr = r - color[0];
333
+ const dg = g - color[1];
334
+ const db = b - color[2];
335
+ const dist = dr * dr + dg * dg + db * db;
336
+ if (dist < minDist) {
337
+ minDist = dist;
338
+ closest = color;
372
339
  }
373
- return closest;
374
- }
375
- /** Apply ordered dithering with Bayer matrix */
376
- function ditherOrdered(data, palette, matrix = BAYER4, strength = 1.0) {
377
- const result = cloneImageData(data);
378
- const matrixSize = matrix.length;
379
- const maxMatrixValue = matrixSize * matrixSize;
380
- for (let y = 0; y < data.height; y++) {
381
- for (let x = 0; x < data.width; x++) {
382
- const i = (y * data.width + x) * 4;
383
- const a = data.data[i + 3];
384
- if (a < 128) {
385
- continue;
386
- } // Skip transparent pixels
387
- const threshold = (matrix[y % matrixSize][x % matrixSize] / maxMatrixValue - 0.5) * strength * 64;
388
- const r = Math.max(0, Math.min(255, data.data[i] + threshold));
389
- const g = Math.max(0, Math.min(255, data.data[i + 1] + threshold));
390
- const b = Math.max(0, Math.min(255, data.data[i + 2] + threshold));
391
- const closest = findClosestPaletteColor(r, g, b, palette);
392
- result.data[i] = closest[0];
393
- result.data[i + 1] = closest[1];
394
- result.data[i + 2] = closest[2];
395
- }
340
+ }
341
+ return closest;
342
+ }
343
+ function ditherOrdered(data, palette, matrix = BAYER4, strength = 1) {
344
+ const result = cloneImageData(data);
345
+ const matrixSize = matrix.length;
346
+ const maxMatrixValue = matrixSize * matrixSize;
347
+ for (let y = 0; y < data.height; y++) {
348
+ for (let x = 0; x < data.width; x++) {
349
+ const i = (y * data.width + x) * 4;
350
+ const a = data.data[i + 3];
351
+ if (a < 128) {
352
+ continue;
353
+ }
354
+ const threshold = (matrix[y % matrixSize][x % matrixSize] / maxMatrixValue - 0.5) * strength * 64;
355
+ const r = Math.max(0, Math.min(255, data.data[i] + threshold));
356
+ const g = Math.max(0, Math.min(255, data.data[i + 1] + threshold));
357
+ const b = Math.max(0, Math.min(255, data.data[i + 2] + threshold));
358
+ const closest = findClosestPaletteColor(r, g, b, palette);
359
+ result.data[i] = closest[0];
360
+ result.data[i + 1] = closest[1];
361
+ result.data[i + 2] = closest[2];
396
362
  }
397
- return result;
363
+ }
364
+ return result;
398
365
  }
399
- /** Floyd-Steinberg error diffusion dithering */
400
366
  function ditherFloydSteinberg(data, palette) {
401
- const result = cloneImageData(data);
402
- const errors = new Float32Array(data.width * data.height * 3);
403
- for (let y = 0; y < data.height; y++) {
404
- for (let x = 0; x < data.width; x++) {
405
- const idx = y * data.width + x;
406
- const i = idx * 4;
407
- const ei = idx * 3;
408
- if (data.data[i + 3] < 128) {
409
- continue;
410
- }
411
- const oldR = Math.max(0, Math.min(255, data.data[i] + errors[ei]));
412
- const oldG = Math.max(0, Math.min(255, data.data[i + 1] + errors[ei + 1]));
413
- const oldB = Math.max(0, Math.min(255, data.data[i + 2] + errors[ei + 2]));
414
- const newColor = findClosestPaletteColor(oldR, oldG, oldB, palette);
415
- result.data[i] = newColor[0];
416
- result.data[i + 1] = newColor[1];
417
- result.data[i + 2] = newColor[2];
418
- const errR = oldR - newColor[0];
419
- const errG = oldG - newColor[1];
420
- const errB = oldB - newColor[2];
421
- // Distribute error: [7/16, 3/16, 5/16, 1/16]
422
- const distribute = (dx, dy, factor) => {
423
- const nx = x + dx, ny = y + dy;
424
- if (nx >= 0 && nx < data.width && ny >= 0 && ny < data.height) {
425
- const ni = (ny * data.width + nx) * 3;
426
- errors[ni] += errR * factor;
427
- errors[ni + 1] += errG * factor;
428
- errors[ni + 2] += errB * factor;
429
- }
430
- };
431
- distribute(1, 0, 7 / 16);
432
- distribute(-1, 1, 3 / 16);
433
- distribute(0, 1, 5 / 16);
434
- distribute(1, 1, 1 / 16);
367
+ const result = cloneImageData(data);
368
+ const errors = new Float32Array(data.width * data.height * 3);
369
+ for (let y = 0; y < data.height; y++) {
370
+ for (let x = 0; x < data.width; x++) {
371
+ const idx = y * data.width + x;
372
+ const i = idx * 4;
373
+ const ei = idx * 3;
374
+ if (data.data[i + 3] < 128) {
375
+ continue;
376
+ }
377
+ const oldR = Math.max(0, Math.min(255, data.data[i] + errors[ei]));
378
+ const oldG = Math.max(0, Math.min(255, data.data[i + 1] + errors[ei + 1]));
379
+ const oldB = Math.max(0, Math.min(255, data.data[i + 2] + errors[ei + 2]));
380
+ const newColor = findClosestPaletteColor(oldR, oldG, oldB, palette);
381
+ result.data[i] = newColor[0];
382
+ result.data[i + 1] = newColor[1];
383
+ result.data[i + 2] = newColor[2];
384
+ const errR = oldR - newColor[0];
385
+ const errG = oldG - newColor[1];
386
+ const errB = oldB - newColor[2];
387
+ const distribute = (dx, dy, factor) => {
388
+ const nx = x + dx, ny = y + dy;
389
+ if (nx >= 0 && nx < data.width && ny >= 0 && ny < data.height) {
390
+ const ni = (ny * data.width + nx) * 3;
391
+ errors[ni] += errR * factor;
392
+ errors[ni + 1] += errG * factor;
393
+ errors[ni + 2] += errB * factor;
435
394
  }
395
+ };
396
+ distribute(1, 0, 7 / 16);
397
+ distribute(-1, 1, 3 / 16);
398
+ distribute(0, 1, 5 / 16);
399
+ distribute(1, 1, 1 / 16);
436
400
  }
437
- return result;
401
+ }
402
+ return result;
438
403
  }
439
- /** Jarvis-Judice-Ninke dithering (larger error diffusion kernel) */
440
404
  function ditherJarvisJudiceNinke(data, palette) {
441
- const result = cloneImageData(data);
442
- const errors = new Float32Array(data.width * data.height * 3);
443
- for (let y = 0; y < data.height; y++) {
444
- for (let x = 0; x < data.width; x++) {
445
- const idx = y * data.width + x;
446
- const i = idx * 4;
447
- const ei = idx * 3;
448
- if (data.data[i + 3] < 128) {
449
- continue;
450
- }
451
- const oldR = Math.max(0, Math.min(255, data.data[i] + errors[ei]));
452
- const oldG = Math.max(0, Math.min(255, data.data[i + 1] + errors[ei + 1]));
453
- const oldB = Math.max(0, Math.min(255, data.data[i + 2] + errors[ei + 2]));
454
- const newColor = findClosestPaletteColor(oldR, oldG, oldB, palette);
455
- result.data[i] = newColor[0];
456
- result.data[i + 1] = newColor[1];
457
- result.data[i + 2] = newColor[2];
458
- const errR = oldR - newColor[0];
459
- const errG = oldG - newColor[1];
460
- const errB = oldB - newColor[2];
461
- const distribute = (dx, dy, factor) => {
462
- const nx = x + dx, ny = y + dy;
463
- if (nx >= 0 && nx < data.width && ny >= 0 && ny < data.height) {
464
- const ni = (ny * data.width + nx) * 3;
465
- errors[ni] += errR * factor;
466
- errors[ni + 1] += errG * factor;
467
- errors[ni + 2] += errB * factor;
468
- }
469
- };
470
- // JJN kernel: /48
471
- distribute(1, 0, 7 / 48);
472
- distribute(2, 0, 5 / 48);
473
- distribute(-2, 1, 3 / 48);
474
- distribute(-1, 1, 5 / 48);
475
- distribute(0, 1, 7 / 48);
476
- distribute(1, 1, 5 / 48);
477
- distribute(2, 1, 3 / 48);
478
- distribute(-2, 2, 1 / 48);
479
- distribute(-1, 2, 3 / 48);
480
- distribute(0, 2, 5 / 48);
481
- distribute(1, 2, 3 / 48);
482
- distribute(2, 2, 1 / 48);
405
+ const result = cloneImageData(data);
406
+ const errors = new Float32Array(data.width * data.height * 3);
407
+ for (let y = 0; y < data.height; y++) {
408
+ for (let x = 0; x < data.width; x++) {
409
+ const idx = y * data.width + x;
410
+ const i = idx * 4;
411
+ const ei = idx * 3;
412
+ if (data.data[i + 3] < 128) {
413
+ continue;
414
+ }
415
+ const oldR = Math.max(0, Math.min(255, data.data[i] + errors[ei]));
416
+ const oldG = Math.max(0, Math.min(255, data.data[i + 1] + errors[ei + 1]));
417
+ const oldB = Math.max(0, Math.min(255, data.data[i + 2] + errors[ei + 2]));
418
+ const newColor = findClosestPaletteColor(oldR, oldG, oldB, palette);
419
+ result.data[i] = newColor[0];
420
+ result.data[i + 1] = newColor[1];
421
+ result.data[i + 2] = newColor[2];
422
+ const errR = oldR - newColor[0];
423
+ const errG = oldG - newColor[1];
424
+ const errB = oldB - newColor[2];
425
+ const distribute = (dx, dy, factor) => {
426
+ const nx = x + dx, ny = y + dy;
427
+ if (nx >= 0 && nx < data.width && ny >= 0 && ny < data.height) {
428
+ const ni = (ny * data.width + nx) * 3;
429
+ errors[ni] += errR * factor;
430
+ errors[ni + 1] += errG * factor;
431
+ errors[ni + 2] += errB * factor;
483
432
  }
433
+ };
434
+ distribute(1, 0, 7 / 48);
435
+ distribute(2, 0, 5 / 48);
436
+ distribute(-2, 1, 3 / 48);
437
+ distribute(-1, 1, 5 / 48);
438
+ distribute(0, 1, 7 / 48);
439
+ distribute(1, 1, 5 / 48);
440
+ distribute(2, 1, 3 / 48);
441
+ distribute(-2, 2, 1 / 48);
442
+ distribute(-1, 2, 3 / 48);
443
+ distribute(0, 2, 5 / 48);
444
+ distribute(1, 2, 3 / 48);
445
+ distribute(2, 2, 1 / 48);
484
446
  }
485
- return result;
447
+ }
448
+ return result;
486
449
  }
487
- /** Atkinson dithering (preserves details better) */
488
450
  function ditherAtkinson(data, palette) {
489
- const result = cloneImageData(data);
490
- const errors = new Float32Array(data.width * data.height * 3);
491
- for (let y = 0; y < data.height; y++) {
492
- for (let x = 0; x < data.width; x++) {
493
- const idx = y * data.width + x;
494
- const i = idx * 4;
495
- const ei = idx * 3;
496
- if (data.data[i + 3] < 128) {
497
- continue;
498
- }
499
- const oldR = Math.max(0, Math.min(255, data.data[i] + errors[ei]));
500
- const oldG = Math.max(0, Math.min(255, data.data[i + 1] + errors[ei + 1]));
501
- const oldB = Math.max(0, Math.min(255, data.data[i + 2] + errors[ei + 2]));
502
- const newColor = findClosestPaletteColor(oldR, oldG, oldB, palette);
503
- result.data[i] = newColor[0];
504
- result.data[i + 1] = newColor[1];
505
- result.data[i + 2] = newColor[2];
506
- // Atkinson only diffuses 6/8 = 3/4 of error
507
- const errR = (oldR - newColor[0]) / 8;
508
- const errG = (oldG - newColor[1]) / 8;
509
- const errB = (oldB - newColor[2]) / 8;
510
- const distribute = (dx, dy) => {
511
- const nx = x + dx, ny = y + dy;
512
- if (nx >= 0 && nx < data.width && ny >= 0 && ny < data.height) {
513
- const ni = (ny * data.width + nx) * 3;
514
- errors[ni] += errR;
515
- errors[ni + 1] += errG;
516
- errors[ni + 2] += errB;
517
- }
518
- };
519
- distribute(1, 0);
520
- distribute(2, 0);
521
- distribute(-1, 1);
522
- distribute(0, 1);
523
- distribute(1, 1);
524
- distribute(0, 2);
451
+ const result = cloneImageData(data);
452
+ const errors = new Float32Array(data.width * data.height * 3);
453
+ for (let y = 0; y < data.height; y++) {
454
+ for (let x = 0; x < data.width; x++) {
455
+ const idx = y * data.width + x;
456
+ const i = idx * 4;
457
+ const ei = idx * 3;
458
+ if (data.data[i + 3] < 128) {
459
+ continue;
460
+ }
461
+ const oldR = Math.max(0, Math.min(255, data.data[i] + errors[ei]));
462
+ const oldG = Math.max(0, Math.min(255, data.data[i + 1] + errors[ei + 1]));
463
+ const oldB = Math.max(0, Math.min(255, data.data[i + 2] + errors[ei + 2]));
464
+ const newColor = findClosestPaletteColor(oldR, oldG, oldB, palette);
465
+ result.data[i] = newColor[0];
466
+ result.data[i + 1] = newColor[1];
467
+ result.data[i + 2] = newColor[2];
468
+ const errR = (oldR - newColor[0]) / 8;
469
+ const errG = (oldG - newColor[1]) / 8;
470
+ const errB = (oldB - newColor[2]) / 8;
471
+ const distribute = (dx, dy) => {
472
+ const nx = x + dx, ny = y + dy;
473
+ if (nx >= 0 && nx < data.width && ny >= 0 && ny < data.height) {
474
+ const ni = (ny * data.width + nx) * 3;
475
+ errors[ni] += errR;
476
+ errors[ni + 1] += errG;
477
+ errors[ni + 2] += errB;
525
478
  }
479
+ };
480
+ distribute(1, 0);
481
+ distribute(2, 0);
482
+ distribute(-1, 1);
483
+ distribute(0, 1);
484
+ distribute(1, 1);
485
+ distribute(0, 2);
526
486
  }
527
- return result;
487
+ }
488
+ return result;
528
489
  }
529
- /** Random dithering */
530
490
  function ditherRandom(data, palette, strength = 32) {
531
- const result = cloneImageData(data);
532
- for (let y = 0; y < data.height; y++) {
533
- for (let x = 0; x < data.width; x++) {
534
- const i = (y * data.width + x) * 4;
535
- if (data.data[i + 3] < 128) {
536
- continue;
537
- }
538
- const noise = (Math.random() - 0.5) * strength * 2;
539
- const r = Math.max(0, Math.min(255, data.data[i] + noise));
540
- const g = Math.max(0, Math.min(255, data.data[i + 1] + noise));
541
- const b = Math.max(0, Math.min(255, data.data[i + 2] + noise));
542
- const closest = findClosestPaletteColor(r, g, b, palette);
543
- result.data[i] = closest[0];
544
- result.data[i + 1] = closest[1];
545
- result.data[i + 2] = closest[2];
546
- }
491
+ const result = cloneImageData(data);
492
+ for (let y = 0; y < data.height; y++) {
493
+ for (let x = 0; x < data.width; x++) {
494
+ const i = (y * data.width + x) * 4;
495
+ if (data.data[i + 3] < 128) {
496
+ continue;
497
+ }
498
+ const noise = (Math.random() - 0.5) * strength * 2;
499
+ const r = Math.max(0, Math.min(255, data.data[i] + noise));
500
+ const g = Math.max(0, Math.min(255, data.data[i + 1] + noise));
501
+ const b = Math.max(0, Math.min(255, data.data[i + 2] + noise));
502
+ const closest = findClosestPaletteColor(r, g, b, palette);
503
+ result.data[i] = closest[0];
504
+ result.data[i + 1] = closest[1];
505
+ result.data[i + 2] = closest[2];
547
506
  }
548
- return result;
507
+ }
508
+ return result;
549
509
  }
550
- /** Apply dithering with specified algorithm */
551
510
  function applyDithering(data, palette, algorithm) {
552
- switch (algorithm) {
553
- case 'none':
554
- // Just quantize without dithering
555
- return ditherOrdered(data, palette, [[0]], 0);
556
- case 'ordered-2x2':
557
- return ditherOrdered(data, palette, BAYER_2x2);
558
- case 'ordered-4x4':
559
- return ditherOrdered(data, palette, BAYER4);
560
- case 'ordered-8x8':
561
- return ditherOrdered(data, palette, BAYER_8x8);
562
- case 'floyd-steinberg':
563
- return ditherFloydSteinberg(data, palette);
564
- case 'jarvis-judice-ninke':
565
- return ditherJarvisJudiceNinke(data, palette);
566
- case 'atkinson':
567
- return ditherAtkinson(data, palette);
568
- case 'random':
569
- return ditherRandom(data, palette);
570
- default:
571
- return ditherFloydSteinberg(data, palette);
572
- }
511
+ switch (algorithm) {
512
+ case "none":
513
+ return ditherOrdered(data, palette, [[0]], 0);
514
+ case "ordered-2x2":
515
+ return ditherOrdered(data, palette, BAYER_2x2);
516
+ case "ordered-4x4":
517
+ return ditherOrdered(data, palette, BAYER4);
518
+ case "ordered-8x8":
519
+ return ditherOrdered(data, palette, BAYER_8x8);
520
+ case "floyd-steinberg":
521
+ return ditherFloydSteinberg(data, palette);
522
+ case "jarvis-judice-ninke":
523
+ return ditherJarvisJudiceNinke(data, palette);
524
+ case "atkinson":
525
+ return ditherAtkinson(data, palette);
526
+ case "random":
527
+ return ditherRandom(data, palette);
528
+ default:
529
+ return ditherFloydSteinberg(data, palette);
530
+ }
573
531
  }
574
- /** Generate palette from image using median cut algorithm */
575
532
  function extractPalette(data, maxColors) {
576
- const colors = [];
577
- // Collect all non-transparent pixels
578
- for (let i = 0; i < data.data.length; i += 4) {
579
- if (data.data[i + 3] >= 128) {
580
- colors.push([data.data[i], data.data[i + 1], data.data[i + 2]]);
581
- }
533
+ const colors = [];
534
+ for (let i = 0; i < data.data.length; i += 4) {
535
+ if (data.data[i + 3] >= 128) {
536
+ colors.push([data.data[i], data.data[i + 1], data.data[i + 2]]);
582
537
  }
583
- if (colors.length === 0) {
584
- return [[0, 0, 0, 255]];
585
- }
586
- // Simple median cut
587
- function medianCut(colors, depth) {
588
- if (depth === 0 || colors.length <= 1) {
589
- // Return average color
590
- const r = Math.round(colors.reduce((s, c) => s + c[0], 0) / colors.length);
591
- const g = Math.round(colors.reduce((s, c) => s + c[1], 0) / colors.length);
592
- const b = Math.round(colors.reduce((s, c) => s + c[2], 0) / colors.length);
593
- return [[r, g, b]];
594
- }
595
- // Find channel with largest range
596
- const ranges = [0, 1, 2].map((ch) => {
597
- const vals = colors.map((c) => c[ch]);
598
- return Math.max(...vals) - Math.min(...vals);
599
- });
600
- const channel = ranges.indexOf(Math.max(...ranges));
601
- // Sort by channel and split
602
- colors.sort((a, b) => a[channel] - b[channel]);
603
- const mid = Math.floor(colors.length / 2);
604
- return [
605
- ...medianCut(colors.slice(0, mid), depth - 1),
606
- ...medianCut(colors.slice(mid), depth - 1),
607
- ];
538
+ }
539
+ if (colors.length === 0) {
540
+ return [[0, 0, 0, 255]];
541
+ }
542
+ function medianCut(colors2, depth2) {
543
+ if (depth2 === 0 || colors2.length <= 1) {
544
+ const r = Math.round(colors2.reduce((s, c) => s + c[0], 0) / colors2.length);
545
+ const g = Math.round(colors2.reduce((s, c) => s + c[1], 0) / colors2.length);
546
+ const b = Math.round(colors2.reduce((s, c) => s + c[2], 0) / colors2.length);
547
+ return [[r, g, b]];
608
548
  }
609
- const depth = Math.ceil(Math.log2(maxColors));
610
- const palette = medianCut(colors, depth).slice(0, maxColors);
611
- return palette.map((c) => [c[0], c[1], c[2], 255]);
549
+ const ranges = [0, 1, 2].map((ch) => {
550
+ const vals = colors2.map((c) => c[ch]);
551
+ return Math.max(...vals) - Math.min(...vals);
552
+ });
553
+ const channel = ranges.indexOf(Math.max(...ranges));
554
+ colors2.sort((a, b) => a[channel] - b[channel]);
555
+ const mid = Math.floor(colors2.length / 2);
556
+ return [
557
+ ...medianCut(colors2.slice(0, mid), depth2 - 1),
558
+ ...medianCut(colors2.slice(mid), depth2 - 1)
559
+ ];
560
+ }
561
+ const depth = Math.ceil(Math.log2(maxColors));
562
+ const palette = medianCut(colors, depth).slice(0, maxColors);
563
+ return palette.map((c) => [c[0], c[1], c[2], 255]);
612
564
  }
613
- /** Convert hex palette to RGBA array */
614
565
  function hexPaletteToRGBA(hexColors) {
615
- return hexColors.map((hex) => hexToRGBA(hex));
566
+ return hexColors.map((hex) => hexToRGBA(hex));
616
567
  }
617
- /** Create reference overlay from file */
618
568
  async function createReferenceOverlay(file, targetWidth, targetHeight) {
619
- return new Promise((resolve, reject) => {
620
- const img = new Image();
621
- img.onload = () => {
622
- // Scale to fit within target dimensions
623
- const scale = Math.min(targetWidth / img.width, targetHeight / img.height, 1);
624
- const canvas = document.createElement('canvas');
625
- canvas.width = Math.round(img.width * scale);
626
- canvas.height = Math.round(img.height * scale);
627
- const ctx = canvas.getContext('2d');
628
- ctx.imageSmoothingEnabled = false;
629
- ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
630
- resolve({
631
- image: ctx.getImageData(0, 0, canvas.width, canvas.height),
632
- x: 0,
633
- y: 0,
634
- opacity: 0.4,
635
- scale: 1,
636
- locked: false,
637
- });
638
- };
639
- img.onerror = reject;
640
- img.src = URL.createObjectURL(file);
641
- });
569
+ return new Promise((resolve, reject) => {
570
+ const img = new Image();
571
+ img.onload = () => {
572
+ const scale = Math.min(
573
+ targetWidth / img.width,
574
+ targetHeight / img.height,
575
+ 1
576
+ // Don't scale up
577
+ );
578
+ const canvas = document.createElement("canvas");
579
+ canvas.width = Math.round(img.width * scale);
580
+ canvas.height = Math.round(img.height * scale);
581
+ const ctx = canvas.getContext("2d");
582
+ ctx.imageSmoothingEnabled = false;
583
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
584
+ resolve({
585
+ image: ctx.getImageData(0, 0, canvas.width, canvas.height),
586
+ x: 0,
587
+ y: 0,
588
+ opacity: 0.4,
589
+ scale: 1,
590
+ locked: false
591
+ });
592
+ };
593
+ img.onerror = reject;
594
+ img.src = URL.createObjectURL(file);
595
+ });
642
596
  }
643
- /** Render reference overlay onto canvas */
644
597
  function renderReferenceOverlay(ctx, ref, zoom, panX, panY) {
645
- ctx.save();
646
- ctx.globalAlpha = ref.opacity;
647
- const tempCanvas = document.createElement('canvas');
648
- tempCanvas.width = ref.image.width;
649
- tempCanvas.height = ref.image.height;
650
- tempCanvas.getContext('2d').putImageData(ref.image, 0, 0);
651
- ctx.imageSmoothingEnabled = false;
652
- ctx.drawImage(tempCanvas, (ref.x + panX) * zoom, (ref.y + panY) * zoom, ref.image.width * ref.scale * zoom, ref.image.height * ref.scale * zoom);
653
- ctx.restore();
654
- }
655
-
656
- export { DEFAULT_BRUSH, applyBrushStamp, applyDitherStamp, applyDithering, blendPixel, bresenhamLine, clearSelection, cloneImageData, colorsEqual, copySelection, createBlankImageData, createReferenceOverlay, ditherAtkinson, ditherFloydSteinberg, ditherJarvisJudiceNinke, ditherOrdered, ditherRandom, drawGradient, extractPalette, flipH, flipV, floodFill, getBrushMask, getPixel, getSymmetryPoints, hexPaletteToRGBA, hexToRGBA, pasteAt, renderReferenceOverlay, rgbaToHex, rotateCW, setPixel };
598
+ ctx.save();
599
+ ctx.globalAlpha = ref.opacity;
600
+ const tempCanvas = document.createElement("canvas");
601
+ tempCanvas.width = ref.image.width;
602
+ tempCanvas.height = ref.image.height;
603
+ tempCanvas.getContext("2d").putImageData(ref.image, 0, 0);
604
+ ctx.imageSmoothingEnabled = false;
605
+ ctx.drawImage(
606
+ tempCanvas,
607
+ (ref.x + panX) * zoom,
608
+ (ref.y + panY) * zoom,
609
+ ref.image.width * ref.scale * zoom,
610
+ ref.image.height * ref.scale * zoom
611
+ );
612
+ ctx.restore();
613
+ }
614
+ export {
615
+ DEFAULT_BRUSH,
616
+ applyBrushStamp,
617
+ applyDitherStamp,
618
+ applyDithering,
619
+ blendPixel,
620
+ bresenhamLine,
621
+ clearSelection,
622
+ cloneImageData,
623
+ colorsEqual,
624
+ copySelection,
625
+ createBlankImageData,
626
+ createReferenceOverlay,
627
+ ditherAtkinson,
628
+ ditherFloydSteinberg,
629
+ ditherJarvisJudiceNinke,
630
+ ditherOrdered,
631
+ ditherRandom,
632
+ drawGradient,
633
+ extractPalette,
634
+ flipH,
635
+ flipV,
636
+ floodFill,
637
+ getBrushMask,
638
+ getPixel,
639
+ getSymmetryPoints,
640
+ hexPaletteToRGBA,
641
+ hexToRGBA,
642
+ pasteAt,
643
+ renderReferenceOverlay,
644
+ rgbaToHex,
645
+ rotateCW,
646
+ setPixel
647
+ };