@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,566 +1,506 @@
1
- import { resolveCssColor } from '@nice2dev/ui-core';
2
-
3
- /* ────────────────────────────────────────────────────────────────
4
- SELECTION MASK UTILITIES
5
- ──────────────────────────────────────────────────────────────── */
1
+ import { resolveCssColor } from "@nice2dev/ui-core";
6
2
  function createEmptyMask(width, height) {
7
- return {
8
- width,
9
- height,
10
- data: new Uint8Array(width * height),
11
- };
3
+ return {
4
+ width,
5
+ height,
6
+ data: new Uint8Array(width * height)
7
+ };
12
8
  }
13
9
  function invertMask(mask) {
14
- const result = createEmptyMask(mask.width, mask.height);
15
- for (let i = 0; i < mask.data.length; i++) {
16
- result.data[i] = 255 - mask.data[i];
17
- }
18
- return result;
10
+ const result = createEmptyMask(mask.width, mask.height);
11
+ for (let i = 0; i < mask.data.length; i++) {
12
+ result.data[i] = 255 - mask.data[i];
13
+ }
14
+ return result;
19
15
  }
20
16
  function combineMasks(maskA, maskB, mode) {
21
- if (maskA.width !== maskB.width || maskA.height !== maskB.height) {
22
- throw new Error('Mask dimensions must match');
23
- }
24
- const result = createEmptyMask(maskA.width, maskA.height);
25
- for (let i = 0; i < maskA.data.length; i++) {
26
- const a = maskA.data[i];
27
- const b = maskB.data[i];
28
- switch (mode) {
29
- case 'replace':
30
- result.data[i] = b;
31
- break;
32
- case 'add':
33
- result.data[i] = Math.min(255, a + b);
34
- break;
35
- case 'subtract':
36
- result.data[i] = Math.max(0, a - b);
37
- break;
38
- case 'intersect':
39
- result.data[i] = Math.min(a, b);
40
- break;
41
- }
17
+ if (maskA.width !== maskB.width || maskA.height !== maskB.height) {
18
+ throw new Error("Mask dimensions must match");
19
+ }
20
+ const result = createEmptyMask(maskA.width, maskA.height);
21
+ for (let i = 0; i < maskA.data.length; i++) {
22
+ const a = maskA.data[i];
23
+ const b = maskB.data[i];
24
+ switch (mode) {
25
+ case "replace":
26
+ result.data[i] = b;
27
+ break;
28
+ case "add":
29
+ result.data[i] = Math.min(255, a + b);
30
+ break;
31
+ case "subtract":
32
+ result.data[i] = Math.max(0, a - b);
33
+ break;
34
+ case "intersect":
35
+ result.data[i] = Math.min(a, b);
36
+ break;
42
37
  }
43
- return result;
38
+ }
39
+ return result;
44
40
  }
45
41
  function featherMask(mask, radius) {
46
- if (radius <= 0) {
47
- return mask;
48
- }
49
- const result = createEmptyMask(mask.width, mask.height);
50
- const kernel = createGaussianKernel(radius);
51
- const kernelSize = kernel.length;
52
- const halfKernel = Math.floor(kernelSize / 2);
53
- for (let y = 0; y < mask.height; y++) {
54
- for (let x = 0; x < mask.width; x++) {
55
- let sum = 0;
56
- let weightSum = 0;
57
- for (let ky = 0; ky < kernelSize; ky++) {
58
- for (let kx = 0; kx < kernelSize; kx++) {
59
- const sx = x + kx - halfKernel;
60
- const sy = y + ky - halfKernel;
61
- if (sx >= 0 && sx < mask.width && sy >= 0 && sy < mask.height) {
62
- const weight = kernel[ky][kx];
63
- sum += mask.data[sy * mask.width + sx] * weight;
64
- weightSum += weight;
65
- }
66
- }
67
- }
68
- result.data[y * mask.width + x] = Math.round(sum / weightSum);
42
+ if (radius <= 0) {
43
+ return mask;
44
+ }
45
+ const result = createEmptyMask(mask.width, mask.height);
46
+ const kernel = createGaussianKernel(radius);
47
+ const kernelSize = kernel.length;
48
+ const halfKernel = Math.floor(kernelSize / 2);
49
+ for (let y = 0; y < mask.height; y++) {
50
+ for (let x = 0; x < mask.width; x++) {
51
+ let sum = 0;
52
+ let weightSum = 0;
53
+ for (let ky = 0; ky < kernelSize; ky++) {
54
+ for (let kx = 0; kx < kernelSize; kx++) {
55
+ const sx = x + kx - halfKernel;
56
+ const sy = y + ky - halfKernel;
57
+ if (sx >= 0 && sx < mask.width && sy >= 0 && sy < mask.height) {
58
+ const weight = kernel[ky][kx];
59
+ sum += mask.data[sy * mask.width + sx] * weight;
60
+ weightSum += weight;
61
+ }
69
62
  }
63
+ }
64
+ result.data[y * mask.width + x] = Math.round(sum / weightSum);
70
65
  }
71
- return result;
66
+ }
67
+ return result;
72
68
  }
73
69
  function createGaussianKernel(radius) {
74
- const size = radius * 2 + 1;
75
- const kernel = [];
76
- const sigma = radius / 3;
77
- const twoSigmaSq = 2 * sigma * sigma;
78
- for (let y = 0; y < size; y++) {
79
- kernel[y] = [];
80
- for (let x = 0; x < size; x++) {
81
- const dx = x - radius;
82
- const dy = y - radius;
83
- kernel[y][x] = Math.exp(-(dx * dx + dy * dy) / twoSigmaSq);
84
- }
70
+ const size = radius * 2 + 1;
71
+ const kernel = [];
72
+ const sigma = radius / 3;
73
+ const twoSigmaSq = 2 * sigma * sigma;
74
+ for (let y = 0; y < size; y++) {
75
+ kernel[y] = [];
76
+ for (let x = 0; x < size; x++) {
77
+ const dx = x - radius;
78
+ const dy = y - radius;
79
+ kernel[y][x] = Math.exp(-(dx * dx + dy * dy) / twoSigmaSq);
85
80
  }
86
- return kernel;
81
+ }
82
+ return kernel;
87
83
  }
88
84
  const DEFAULT_WAND_OPTIONS = {
89
- tolerance: 32,
90
- contiguous: true,
91
- antiAlias: true,
92
- feather: 0,
85
+ tolerance: 32,
86
+ contiguous: true,
87
+ antiAlias: true,
88
+ feather: 0
93
89
  };
94
90
  function magicWandSelect(imageData, startX, startY, options = {}) {
95
- const opts = { ...DEFAULT_WAND_OPTIONS, ...options };
96
- const { width, height, data } = imageData;
97
- const mask = createEmptyMask(width, height);
98
- // Get target color
99
- const startIdx = (startY * width + startX) * 4;
100
- const targetR = data[startIdx];
101
- const targetG = data[startIdx + 1];
102
- const targetB = data[startIdx + 2];
103
- // Color similarity function
104
- const isColorSimilar = (idx) => {
105
- const r = data[idx];
106
- const g = data[idx + 1];
107
- const b = data[idx + 2];
108
- const diff = Math.abs(r - targetR) + Math.abs(g - targetG) + Math.abs(b - targetB);
109
- return diff <= opts.tolerance * 3;
110
- };
111
- // For anti-aliasing, calculate how similar a pixel is (0-255)
112
- const getSimilarity = (idx) => {
113
- const r = data[idx];
114
- const g = data[idx + 1];
115
- const b = data[idx + 2];
116
- const diff = Math.abs(r - targetR) + Math.abs(g - targetG) + Math.abs(b - targetB);
117
- const maxDiff = opts.tolerance * 3;
118
- if (diff >= maxDiff) {
119
- return 0;
120
- }
121
- return Math.round(255 * (1 - diff / maxDiff));
122
- };
123
- if (opts.contiguous) {
124
- // Flood fill algorithm
125
- const visited = new Uint8Array(width * height);
126
- const stack = [startX, startY];
127
- while (stack.length > 0) {
128
- const y = stack.pop();
129
- const x = stack.pop();
130
- if (x < 0 || x >= width || y < 0 || y >= height) {
131
- continue;
132
- }
133
- const pixelIdx = y * width + x;
134
- if (visited[pixelIdx]) {
135
- continue;
136
- }
137
- visited[pixelIdx] = 1;
138
- const colorIdx = pixelIdx * 4;
139
- if (!isColorSimilar(colorIdx)) {
140
- continue;
141
- }
142
- mask.data[pixelIdx] = opts.antiAlias ? getSimilarity(colorIdx) : 255;
143
- // Add neighbors
144
- stack.push(x - 1, y);
145
- stack.push(x + 1, y);
146
- stack.push(x, y - 1);
147
- stack.push(x, y + 1);
148
- }
91
+ const opts = { ...DEFAULT_WAND_OPTIONS, ...options };
92
+ const { width, height, data } = imageData;
93
+ const mask = createEmptyMask(width, height);
94
+ const startIdx = (startY * width + startX) * 4;
95
+ const targetR = data[startIdx];
96
+ const targetG = data[startIdx + 1];
97
+ const targetB = data[startIdx + 2];
98
+ const isColorSimilar = (idx) => {
99
+ const r = data[idx];
100
+ const g = data[idx + 1];
101
+ const b = data[idx + 2];
102
+ const diff = Math.abs(r - targetR) + Math.abs(g - targetG) + Math.abs(b - targetB);
103
+ return diff <= opts.tolerance * 3;
104
+ };
105
+ const getSimilarity = (idx) => {
106
+ const r = data[idx];
107
+ const g = data[idx + 1];
108
+ const b = data[idx + 2];
109
+ const diff = Math.abs(r - targetR) + Math.abs(g - targetG) + Math.abs(b - targetB);
110
+ const maxDiff = opts.tolerance * 3;
111
+ if (diff >= maxDiff) {
112
+ return 0;
149
113
  }
150
- else {
151
- // Select all similar colors regardless of position
152
- for (let i = 0; i < width * height; i++) {
153
- const colorIdx = i * 4;
154
- if (isColorSimilar(colorIdx)) {
155
- mask.data[i] = opts.antiAlias ? getSimilarity(colorIdx) : 255;
156
- }
157
- }
114
+ return Math.round(255 * (1 - diff / maxDiff));
115
+ };
116
+ if (opts.contiguous) {
117
+ const visited = new Uint8Array(width * height);
118
+ const stack = [startX, startY];
119
+ while (stack.length > 0) {
120
+ const y = stack.pop();
121
+ const x = stack.pop();
122
+ if (x < 0 || x >= width || y < 0 || y >= height) {
123
+ continue;
124
+ }
125
+ const pixelIdx = y * width + x;
126
+ if (visited[pixelIdx]) {
127
+ continue;
128
+ }
129
+ visited[pixelIdx] = 1;
130
+ const colorIdx = pixelIdx * 4;
131
+ if (!isColorSimilar(colorIdx)) {
132
+ continue;
133
+ }
134
+ mask.data[pixelIdx] = opts.antiAlias ? getSimilarity(colorIdx) : 255;
135
+ stack.push(x - 1, y);
136
+ stack.push(x + 1, y);
137
+ stack.push(x, y - 1);
138
+ stack.push(x, y + 1);
158
139
  }
159
- // Apply feather if requested
160
- if (opts.feather > 0) {
161
- return featherMask(mask, opts.feather);
140
+ } else {
141
+ for (let i = 0; i < width * height; i++) {
142
+ const colorIdx = i * 4;
143
+ if (isColorSimilar(colorIdx)) {
144
+ mask.data[i] = opts.antiAlias ? getSimilarity(colorIdx) : 255;
145
+ }
162
146
  }
163
- return mask;
147
+ }
148
+ if (opts.feather > 0) {
149
+ return featherMask(mask, opts.feather);
150
+ }
151
+ return mask;
164
152
  }
165
153
  const DEFAULT_LASSO_OPTIONS = {
166
- feather: 0,
167
- antiAlias: true,
154
+ feather: 0,
155
+ antiAlias: true
168
156
  };
169
157
  function lassoSelect(width, height, points, options = {}) {
170
- const opts = { ...DEFAULT_LASSO_OPTIONS, ...options };
171
- const mask = createEmptyMask(width, height);
172
- if (points.length < 3) {
173
- return mask;
174
- }
175
- // Use scanline fill algorithm
176
- const minY = Math.max(0, Math.floor(Math.min(...points.map((p) => p.y))));
177
- const maxY = Math.min(height - 1, Math.ceil(Math.max(...points.map((p) => p.y))));
178
- for (let y = minY; y <= maxY; y++) {
179
- // Find intersections with polygon edges
180
- const intersections = [];
181
- for (let i = 0; i < points.length; i++) {
182
- const p1 = points[i];
183
- const p2 = points[(i + 1) % points.length];
184
- // Check if edge crosses this scanline
185
- if ((p1.y <= y && p2.y > y) || (p2.y <= y && p1.y > y)) {
186
- // Calculate x intersection
187
- const x = p1.x + ((y - p1.y) / (p2.y - p1.y)) * (p2.x - p1.x);
188
- intersections.push(x);
189
- }
190
- }
191
- // Sort intersections
192
- intersections.sort((a, b) => a - b);
193
- // Fill between pairs of intersections
194
- for (let i = 0; i < intersections.length - 1; i += 2) {
195
- const x1 = Math.max(0, Math.floor(intersections[i]));
196
- const x2 = Math.min(width - 1, Math.ceil(intersections[i + 1]));
197
- for (let x = x1; x <= x2; x++) {
198
- mask.data[y * width + x] = 255;
199
- }
200
- }
158
+ const opts = { ...DEFAULT_LASSO_OPTIONS, ...options };
159
+ const mask = createEmptyMask(width, height);
160
+ if (points.length < 3) {
161
+ return mask;
162
+ }
163
+ const minY = Math.max(0, Math.floor(Math.min(...points.map((p) => p.y))));
164
+ const maxY = Math.min(height - 1, Math.ceil(Math.max(...points.map((p) => p.y))));
165
+ for (let y = minY; y <= maxY; y++) {
166
+ const intersections = [];
167
+ for (let i = 0; i < points.length; i++) {
168
+ const p1 = points[i];
169
+ const p2 = points[(i + 1) % points.length];
170
+ if (p1.y <= y && p2.y > y || p2.y <= y && p1.y > y) {
171
+ const x = p1.x + (y - p1.y) / (p2.y - p1.y) * (p2.x - p1.x);
172
+ intersections.push(x);
173
+ }
201
174
  }
202
- // Apply feather if requested
203
- if (opts.feather > 0) {
204
- return featherMask(mask, opts.feather);
175
+ intersections.sort((a, b) => a - b);
176
+ for (let i = 0; i < intersections.length - 1; i += 2) {
177
+ const x1 = Math.max(0, Math.floor(intersections[i]));
178
+ const x2 = Math.min(width - 1, Math.ceil(intersections[i + 1]));
179
+ for (let x = x1; x <= x2; x++) {
180
+ mask.data[y * width + x] = 255;
181
+ }
205
182
  }
206
- return mask;
183
+ }
184
+ if (opts.feather > 0) {
185
+ return featherMask(mask, opts.feather);
186
+ }
187
+ return mask;
207
188
  }
208
- /** Magnetic lasso - snaps to edges in image */
209
189
  function magneticLassoPoint(imageData, x, y, searchRadius = 10) {
210
- const { width, height, data } = imageData;
211
- // Find the point with highest edge strength within radius
212
- let bestX = x;
213
- let bestY = y;
214
- let bestEdge = 0;
215
- for (let dy = -searchRadius; dy <= searchRadius; dy++) {
216
- for (let dx = -searchRadius; dx <= searchRadius; dx++) {
217
- const nx = Math.round(x + dx);
218
- const ny = Math.round(y + dy);
219
- if (nx < 1 || nx >= width - 1 || ny < 1 || ny >= height - 1) {
220
- continue;
221
- }
222
- // Calculate edge strength using Sobel operator
223
- const edge = getEdgeStrength(data, width, nx, ny);
224
- // Weight by distance from cursor
225
- const dist = Math.sqrt(dx * dx + dy * dy);
226
- const weightedEdge = edge / (1 + dist * 0.2);
227
- if (weightedEdge > bestEdge) {
228
- bestEdge = weightedEdge;
229
- bestX = nx;
230
- bestY = ny;
231
- }
232
- }
190
+ const { width, height, data } = imageData;
191
+ let bestX = x;
192
+ let bestY = y;
193
+ let bestEdge = 0;
194
+ for (let dy = -searchRadius; dy <= searchRadius; dy++) {
195
+ for (let dx = -searchRadius; dx <= searchRadius; dx++) {
196
+ const nx = Math.round(x + dx);
197
+ const ny = Math.round(y + dy);
198
+ if (nx < 1 || nx >= width - 1 || ny < 1 || ny >= height - 1) {
199
+ continue;
200
+ }
201
+ const edge = getEdgeStrength(data, width, nx, ny);
202
+ const dist = Math.sqrt(dx * dx + dy * dy);
203
+ const weightedEdge = edge / (1 + dist * 0.2);
204
+ if (weightedEdge > bestEdge) {
205
+ bestEdge = weightedEdge;
206
+ bestX = nx;
207
+ bestY = ny;
208
+ }
233
209
  }
234
- return { x: bestX, y: bestY };
210
+ }
211
+ return { x: bestX, y: bestY };
235
212
  }
236
213
  function getEdgeStrength(data, width, x, y) {
237
- // Sobel operator
238
- const getGray = (px, py) => {
239
- const i = (py * width + px) * 4;
240
- return (data[i] + data[i + 1] + data[i + 2]) / 3;
241
- };
242
- const gx = -getGray(x - 1, y - 1) -
243
- 2 * getGray(x - 1, y) -
244
- getGray(x - 1, y + 1) +
245
- getGray(x + 1, y - 1) +
246
- 2 * getGray(x + 1, y) +
247
- getGray(x + 1, y + 1);
248
- const gy = -getGray(x - 1, y - 1) -
249
- 2 * getGray(x, y - 1) -
250
- getGray(x + 1, y - 1) +
251
- getGray(x - 1, y + 1) +
252
- 2 * getGray(x, y + 1) +
253
- getGray(x + 1, y + 1);
254
- return Math.sqrt(gx * gx + gy * gy);
214
+ const getGray = (px, py) => {
215
+ const i = (py * width + px) * 4;
216
+ return (data[i] + data[i + 1] + data[i + 2]) / 3;
217
+ };
218
+ const gx = -getGray(x - 1, y - 1) - 2 * getGray(x - 1, y) - getGray(x - 1, y + 1) + getGray(x + 1, y - 1) + 2 * getGray(x + 1, y) + getGray(x + 1, y + 1);
219
+ const gy = -getGray(x - 1, y - 1) - 2 * getGray(x, y - 1) - getGray(x + 1, y - 1) + getGray(x - 1, y + 1) + 2 * getGray(x, y + 1) + getGray(x + 1, y + 1);
220
+ return Math.sqrt(gx * gx + gy * gy);
255
221
  }
256
222
  function selectColorRange(imageData, targetColor, options = {}) {
257
- const opts = { fuzziness: 40, feather: 0, ...options };
258
- const { width, height, data } = imageData;
259
- const mask = createEmptyMask(width, height);
260
- for (let i = 0; i < width * height; i++) {
261
- const idx = i * 4;
262
- const r = data[idx];
263
- const g = data[idx + 1];
264
- const b = data[idx + 2];
265
- // Calculate color difference in LAB-like space for perceptual accuracy
266
- const diff = Math.sqrt(Math.pow(r - targetColor.r, 2) +
267
- Math.pow(g - targetColor.g, 2) +
268
- Math.pow(b - targetColor.b, 2));
269
- const maxDiff = opts.fuzziness * 2;
270
- if (diff < maxDiff) {
271
- mask.data[i] = Math.round(255 * (1 - diff / maxDiff));
272
- }
273
- }
274
- if (opts.feather > 0) {
275
- return featherMask(mask, opts.feather);
223
+ const opts = { fuzziness: 40, feather: 0, ...options };
224
+ const { width, height, data } = imageData;
225
+ const mask = createEmptyMask(width, height);
226
+ for (let i = 0; i < width * height; i++) {
227
+ const idx = i * 4;
228
+ const r = data[idx];
229
+ const g = data[idx + 1];
230
+ const b = data[idx + 2];
231
+ const diff = Math.sqrt(
232
+ Math.pow(r - targetColor.r, 2) + Math.pow(g - targetColor.g, 2) + Math.pow(b - targetColor.b, 2)
233
+ );
234
+ const maxDiff = opts.fuzziness * 2;
235
+ if (diff < maxDiff) {
236
+ mask.data[i] = Math.round(255 * (1 - diff / maxDiff));
276
237
  }
277
- return mask;
238
+ }
239
+ if (opts.feather > 0) {
240
+ return featherMask(mask, opts.feather);
241
+ }
242
+ return mask;
278
243
  }
279
244
  const DEFAULT_HEALING_OPTIONS = {
280
- radius: 20,
281
- hardness: 0.5,
282
- opacity: 1,
283
- mode: 'heal',
245
+ radius: 20,
246
+ hardness: 0.5,
247
+ opacity: 1,
248
+ mode: "heal"
284
249
  };
285
- /** Apply healing brush at destination using source area */
286
250
  function applyHealingBrush(imageData, srcX, srcY, dstX, dstY, options = {}) {
287
- const opts = { ...DEFAULT_HEALING_OPTIONS, ...options };
288
- const { width, height, data } = imageData;
289
- const result = new ImageData(new Uint8ClampedArray(data), width, height);
290
- const r = opts.radius;
291
- const r2 = r * r;
292
- // Calculate average color at destination for healing mode
293
- let avgR = 0, avgG = 0, avgB = 0, count = 0;
294
- if (opts.mode === 'heal') {
295
- for (let dy = -r; dy <= r; dy++) {
296
- for (let dx = -r; dx <= r; dx++) {
297
- if (dx * dx + dy * dy > r2) {
298
- continue;
299
- }
300
- const px = dstX + dx;
301
- const py = dstY + dy;
302
- if (px < 0 || px >= width || py < 0 || py >= height) {
303
- continue;
304
- }
305
- const idx = (py * width + px) * 4;
306
- avgR += data[idx];
307
- avgG += data[idx + 1];
308
- avgB += data[idx + 2];
309
- count++;
310
- }
251
+ const opts = { ...DEFAULT_HEALING_OPTIONS, ...options };
252
+ const { width, height, data } = imageData;
253
+ const result = new ImageData(new Uint8ClampedArray(data), width, height);
254
+ const r = opts.radius;
255
+ const r2 = r * r;
256
+ let avgR = 0, avgG = 0, avgB = 0, count = 0;
257
+ if (opts.mode === "heal") {
258
+ for (let dy = -r; dy <= r; dy++) {
259
+ for (let dx = -r; dx <= r; dx++) {
260
+ if (dx * dx + dy * dy > r2) {
261
+ continue;
311
262
  }
312
- if (count > 0) {
313
- avgR /= count;
314
- avgG /= count;
315
- avgB /= count;
263
+ const px = dstX + dx;
264
+ const py = dstY + dy;
265
+ if (px < 0 || px >= width || py < 0 || py >= height) {
266
+ continue;
316
267
  }
268
+ const idx = (py * width + px) * 4;
269
+ avgR += data[idx];
270
+ avgG += data[idx + 1];
271
+ avgB += data[idx + 2];
272
+ count++;
273
+ }
317
274
  }
318
- // Copy/heal pixels
319
- for (let dy = -r; dy <= r; dy++) {
320
- for (let dx = -r; dx <= r; dx++) {
321
- const distSq = dx * dx + dy * dy;
322
- if (distSq > r2) {
323
- continue;
324
- }
325
- const spx = srcX + dx;
326
- const spy = srcY + dy;
327
- const dpx = dstX + dx;
328
- const dpy = dstY + dy;
329
- if (spx < 0 || spx >= width || spy < 0 || spy >= height) {
330
- continue;
331
- }
332
- if (dpx < 0 || dpx >= width || dpy < 0 || dpy >= height) {
333
- continue;
334
- }
335
- const srcIdx = (spy * width + spx) * 4;
336
- const dstIdx = (dpy * width + dpx) * 4;
337
- // Calculate brush falloff
338
- const dist = Math.sqrt(distSq);
339
- const falloff = Math.min(1, Math.max(0, (r - dist) / (r * (1 - opts.hardness + 0.01))));
340
- const alpha = opts.opacity * falloff;
341
- if (opts.mode === 'clone') {
342
- // Simple clone
343
- result.data[dstIdx] = lerp(data[dstIdx], data[srcIdx], alpha);
344
- result.data[dstIdx + 1] = lerp(data[dstIdx + 1], data[srcIdx + 1], alpha);
345
- result.data[dstIdx + 2] = lerp(data[dstIdx + 2], data[srcIdx + 2], alpha);
275
+ if (count > 0) {
276
+ avgR /= count;
277
+ avgG /= count;
278
+ avgB /= count;
279
+ }
280
+ }
281
+ for (let dy = -r; dy <= r; dy++) {
282
+ for (let dx = -r; dx <= r; dx++) {
283
+ const distSq = dx * dx + dy * dy;
284
+ if (distSq > r2) {
285
+ continue;
286
+ }
287
+ const spx = srcX + dx;
288
+ const spy = srcY + dy;
289
+ const dpx = dstX + dx;
290
+ const dpy = dstY + dy;
291
+ if (spx < 0 || spx >= width || spy < 0 || spy >= height) {
292
+ continue;
293
+ }
294
+ if (dpx < 0 || dpx >= width || dpy < 0 || dpy >= height) {
295
+ continue;
296
+ }
297
+ const srcIdx = (spy * width + spx) * 4;
298
+ const dstIdx = (dpy * width + dpx) * 4;
299
+ const dist = Math.sqrt(distSq);
300
+ const falloff = Math.min(1, Math.max(0, (r - dist) / (r * (1 - opts.hardness + 0.01))));
301
+ const alpha = opts.opacity * falloff;
302
+ if (opts.mode === "clone") {
303
+ result.data[dstIdx] = lerp(data[dstIdx], data[srcIdx], alpha);
304
+ result.data[dstIdx + 1] = lerp(data[dstIdx + 1], data[srcIdx + 1], alpha);
305
+ result.data[dstIdx + 2] = lerp(data[dstIdx + 2], data[srcIdx + 2], alpha);
306
+ } else {
307
+ const srcR = data[srcIdx];
308
+ const srcG = data[srcIdx + 1];
309
+ const srcB = data[srcIdx + 2];
310
+ const srcAvgR = avgR;
311
+ const srcAvgG = avgG;
312
+ const srcAvgB = avgB;
313
+ let sAvgR = 0, sAvgG = 0, sAvgB = 0, sCount = 0;
314
+ for (let sy = -r; sy <= r; sy++) {
315
+ for (let sx = -r; sx <= r; sx++) {
316
+ if (sx * sx + sy * sy > r2) {
317
+ continue;
346
318
  }
347
- else {
348
- // Healing mode: match destination luminance
349
- const srcR = data[srcIdx];
350
- const srcG = data[srcIdx + 1];
351
- const srcB = data[srcIdx + 2];
352
- // Adjust source to match destination average
353
- const srcAvgR = avgR;
354
- const srcAvgG = avgG;
355
- const srcAvgB = avgB;
356
- // Calculate source average in the brush area
357
- let sAvgR = 0, sAvgG = 0, sAvgB = 0, sCount = 0;
358
- for (let sy = -r; sy <= r; sy++) {
359
- for (let sx = -r; sx <= r; sx++) {
360
- if (sx * sx + sy * sy > r2) {
361
- continue;
362
- }
363
- const px = srcX + sx;
364
- const py = srcY + sy;
365
- if (px < 0 || px >= width || py < 0 || py >= height) {
366
- continue;
367
- }
368
- const idx = (py * width + px) * 4;
369
- sAvgR += data[idx];
370
- sAvgG += data[idx + 1];
371
- sAvgB += data[idx + 2];
372
- sCount++;
373
- }
374
- }
375
- if (sCount > 0) {
376
- sAvgR /= sCount;
377
- sAvgG /= sCount;
378
- sAvgB /= sCount;
379
- }
380
- // Adjust source color to match destination
381
- const adjustedR = srcR + (srcAvgR - sAvgR);
382
- const adjustedG = srcG + (srcAvgG - sAvgG);
383
- const adjustedB = srcB + (srcAvgB - sAvgB);
384
- result.data[dstIdx] = lerp(data[dstIdx], clamp(adjustedR, 0, 255), alpha);
385
- result.data[dstIdx + 1] = lerp(data[dstIdx + 1], clamp(adjustedG, 0, 255), alpha);
386
- result.data[dstIdx + 2] = lerp(data[dstIdx + 2], clamp(adjustedB, 0, 255), alpha);
319
+ const px = srcX + sx;
320
+ const py = srcY + sy;
321
+ if (px < 0 || px >= width || py < 0 || py >= height) {
322
+ continue;
387
323
  }
324
+ const idx = (py * width + px) * 4;
325
+ sAvgR += data[idx];
326
+ sAvgG += data[idx + 1];
327
+ sAvgB += data[idx + 2];
328
+ sCount++;
329
+ }
330
+ }
331
+ if (sCount > 0) {
332
+ sAvgR /= sCount;
333
+ sAvgG /= sCount;
334
+ sAvgB /= sCount;
388
335
  }
336
+ const adjustedR = srcR + (srcAvgR - sAvgR);
337
+ const adjustedG = srcG + (srcAvgG - sAvgG);
338
+ const adjustedB = srcB + (srcAvgB - sAvgB);
339
+ result.data[dstIdx] = lerp(data[dstIdx], clamp(adjustedR, 0, 255), alpha);
340
+ result.data[dstIdx + 1] = lerp(data[dstIdx + 1], clamp(adjustedG, 0, 255), alpha);
341
+ result.data[dstIdx + 2] = lerp(data[dstIdx + 2], clamp(adjustedB, 0, 255), alpha);
342
+ }
389
343
  }
390
- return result;
344
+ }
345
+ return result;
391
346
  }
392
347
  function lerp(a, b, t) {
393
- return a + (b - a) * t;
348
+ return a + (b - a) * t;
394
349
  }
395
350
  function clamp(val, min, max) {
396
- return Math.max(min, Math.min(max, val));
351
+ return Math.max(min, Math.min(max, val));
397
352
  }
398
- /* ────────────────────────────────────────────────────────────────
399
- CONTENT-AWARE FILL (simplified)
400
- ──────────────────────────────────────────────────────────────── */
401
- /** Simple content-aware fill using surrounding pixels */
402
353
  function contentAwareFill(imageData, mask) {
403
- const { width, height, data } = imageData;
404
- const result = new ImageData(new Uint8ClampedArray(data), width, height);
405
- // Find selected pixels
406
- const selected = [];
407
- for (let i = 0; i < mask.data.length; i++) {
408
- if (mask.data[i] > 128) {
409
- selected.push(i);
410
- }
354
+ const { width, height, data } = imageData;
355
+ const result = new ImageData(new Uint8ClampedArray(data), width, height);
356
+ const selected = [];
357
+ for (let i = 0; i < mask.data.length; i++) {
358
+ if (mask.data[i] > 128) {
359
+ selected.push(i);
411
360
  }
412
- // Sort by distance from edge (process edge pixels first)
413
- selected.sort((a, b) => {
414
- const ax = a % width, ay = Math.floor(a / width);
415
- const bx = b % width, by = Math.floor(b / width);
416
- return distFromEdge(ax, ay, mask) - distFromEdge(bx, by, mask);
417
- });
418
- // Fill each pixel using nearby unselected pixels
419
- for (const pixelIdx of selected) {
420
- const x = pixelIdx % width;
421
- const y = Math.floor(pixelIdx / width);
422
- // Find nearby unselected pixels
423
- const sample = findBestMatch(result, mask, x, y, 15);
424
- if (sample) {
425
- const dstIdx = pixelIdx * 4;
426
- const srcIdx = (sample.y * width + sample.x) * 4;
427
- result.data[dstIdx] = result.data[srcIdx];
428
- result.data[dstIdx + 1] = result.data[srcIdx + 1];
429
- result.data[dstIdx + 2] = result.data[srcIdx + 2];
430
- }
361
+ }
362
+ selected.sort((a, b) => {
363
+ const ax = a % width, ay = Math.floor(a / width);
364
+ const bx = b % width, by = Math.floor(b / width);
365
+ return distFromEdge(ax, ay, mask) - distFromEdge(bx, by, mask);
366
+ });
367
+ for (const pixelIdx of selected) {
368
+ const x = pixelIdx % width;
369
+ const y = Math.floor(pixelIdx / width);
370
+ const sample = findBestMatch(result, mask, x, y, 15);
371
+ if (sample) {
372
+ const dstIdx = pixelIdx * 4;
373
+ const srcIdx = (sample.y * width + sample.x) * 4;
374
+ result.data[dstIdx] = result.data[srcIdx];
375
+ result.data[dstIdx + 1] = result.data[srcIdx + 1];
376
+ result.data[dstIdx + 2] = result.data[srcIdx + 2];
431
377
  }
432
- return result;
378
+ }
379
+ return result;
433
380
  }
434
381
  function distFromEdge(x, y, mask) {
435
- const { width, height } = mask;
436
- const minDist = Infinity;
437
- for (let r = 1; r < 50; r++) {
438
- for (let dy = -r; dy <= r; dy++) {
439
- for (let dx = -r; dx <= r; dx++) {
440
- if (Math.abs(dx) !== r && Math.abs(dy) !== r) {
441
- continue;
442
- }
443
- const nx = x + dx, ny = y + dy;
444
- if (nx < 0 || nx >= width || ny < 0 || ny >= height) {
445
- continue;
446
- }
447
- if (mask.data[ny * width + nx] < 128) {
448
- return Math.sqrt(dx * dx + dy * dy);
449
- }
450
- }
382
+ const { width, height } = mask;
383
+ const minDist = Infinity;
384
+ for (let r = 1; r < 50; r++) {
385
+ for (let dy = -r; dy <= r; dy++) {
386
+ for (let dx = -r; dx <= r; dx++) {
387
+ if (Math.abs(dx) !== r && Math.abs(dy) !== r) {
388
+ continue;
389
+ }
390
+ const nx = x + dx, ny = y + dy;
391
+ if (nx < 0 || nx >= width || ny < 0 || ny >= height) {
392
+ continue;
451
393
  }
394
+ if (mask.data[ny * width + nx] < 128) {
395
+ return Math.sqrt(dx * dx + dy * dy);
396
+ }
397
+ }
452
398
  }
453
- return minDist;
399
+ }
400
+ return minDist;
454
401
  }
455
402
  function findBestMatch(imageData, mask, x, y, radius) {
456
- const { width, height, data } = imageData;
457
- const candidates = [];
458
- // Collect unselected candidate pixels
459
- for (let dy = -radius; dy <= radius; dy++) {
460
- for (let dx = -radius; dx <= radius; dx++) {
461
- const nx = x + dx, ny = y + dy;
462
- if (nx < 0 || nx >= width || ny < 0 || ny >= height) {
463
- continue;
464
- }
465
- if (mask.data[ny * width + nx] < 128) {
466
- candidates.push({ x: nx, y: ny });
467
- }
468
- }
403
+ const { width, height, data } = imageData;
404
+ const candidates = [];
405
+ for (let dy = -radius; dy <= radius; dy++) {
406
+ for (let dx = -radius; dx <= radius; dx++) {
407
+ const nx = x + dx, ny = y + dy;
408
+ if (nx < 0 || nx >= width || ny < 0 || ny >= height) {
409
+ continue;
410
+ }
411
+ if (mask.data[ny * width + nx] < 128) {
412
+ candidates.push({ x: nx, y: ny });
413
+ }
469
414
  }
470
- if (candidates.length === 0) {
471
- return null;
472
- }
473
- // Find candidate with most similar surroundings
474
- let best = null;
475
- let bestScore = Infinity;
476
- const patchSize = 3;
477
- for (const cand of candidates) {
478
- let score = 0;
479
- let count = 0;
480
- for (let py = -patchSize; py <= patchSize; py++) {
481
- for (let px = -patchSize; px <= patchSize; px++) {
482
- const srcX = x + px, srcY = y + py;
483
- const candX = cand.x + px, candY = cand.y + py;
484
- if (srcX < 0 || srcX >= width || srcY < 0 || srcY >= height) {
485
- continue;
486
- }
487
- if (candX < 0 || candX >= width || candY < 0 || candY >= height) {
488
- continue;
489
- }
490
- // Only compare unselected pixels
491
- if (mask.data[srcY * width + srcX] > 128) {
492
- continue;
493
- }
494
- const srcIdx = (srcY * width + srcX) * 4;
495
- const candIdx = (candY * width + candX) * 4;
496
- const dr = data[srcIdx] - data[candIdx];
497
- const dg = data[srcIdx + 1] - data[candIdx + 1];
498
- const db = data[srcIdx + 2] - data[candIdx + 2];
499
- score += dr * dr + dg * dg + db * db;
500
- count++;
501
- }
415
+ }
416
+ if (candidates.length === 0) {
417
+ return null;
418
+ }
419
+ let best = null;
420
+ let bestScore = Infinity;
421
+ const patchSize = 3;
422
+ for (const cand of candidates) {
423
+ let score = 0;
424
+ let count = 0;
425
+ for (let py = -patchSize; py <= patchSize; py++) {
426
+ for (let px = -patchSize; px <= patchSize; px++) {
427
+ const srcX = x + px, srcY = y + py;
428
+ const candX = cand.x + px, candY = cand.y + py;
429
+ if (srcX < 0 || srcX >= width || srcY < 0 || srcY >= height) {
430
+ continue;
431
+ }
432
+ if (candX < 0 || candX >= width || candY < 0 || candY >= height) {
433
+ continue;
502
434
  }
503
- if (count > 0 && score / count < bestScore) {
504
- bestScore = score / count;
505
- best = cand;
435
+ if (mask.data[srcY * width + srcX] > 128) {
436
+ continue;
506
437
  }
438
+ const srcIdx = (srcY * width + srcX) * 4;
439
+ const candIdx = (candY * width + candX) * 4;
440
+ const dr = data[srcIdx] - data[candIdx];
441
+ const dg = data[srcIdx + 1] - data[candIdx + 1];
442
+ const db = data[srcIdx + 2] - data[candIdx + 2];
443
+ score += dr * dr + dg * dg + db * db;
444
+ count++;
445
+ }
507
446
  }
508
- return best;
447
+ if (count > 0 && score / count < bestScore) {
448
+ bestScore = score / count;
449
+ best = cand;
450
+ }
451
+ }
452
+ return best;
509
453
  }
510
- /* ────────────────────────────────────────────────────────────────
511
- SELECTION RENDERING
512
- ──────────────────────────────────────────────────────────────── */
513
- /** Render marching ants selection outline */
514
454
  function renderSelectionOutline(ctx, mask, offset = 0) {
515
- const { width, height, data } = mask;
516
- // Find edge pixels
517
- ctx.strokeStyle = resolveCssColor('var(--nice-bg, #fff)', ctx.canvas);
518
- ctx.lineWidth = 1;
519
- ctx.setLineDash([4, 4]);
520
- ctx.lineDashOffset = offset;
521
- ctx.beginPath();
522
- for (let y = 0; y < height; y++) {
523
- for (let x = 0; x < width; x++) {
524
- const idx = y * width + x;
525
- if (data[idx] < 128) {
526
- continue;
527
- }
528
- // Check if this is an edge pixel
529
- const isEdge = x === 0 ||
530
- data[idx - 1] < 128 ||
531
- x === width - 1 ||
532
- data[idx + 1] < 128 ||
533
- y === 0 ||
534
- data[idx - width] < 128 ||
535
- y === height - 1 ||
536
- data[idx + width] < 128;
537
- if (isEdge) {
538
- // Draw pixel outline
539
- if (x === 0 || data[idx - 1] < 128) {
540
- ctx.moveTo(x, y);
541
- ctx.lineTo(x, y + 1);
542
- }
543
- if (x === width - 1 || data[idx + 1] < 128) {
544
- ctx.moveTo(x + 1, y);
545
- ctx.lineTo(x + 1, y + 1);
546
- }
547
- if (y === 0 || data[idx - width] < 128) {
548
- ctx.moveTo(x, y);
549
- ctx.lineTo(x + 1, y);
550
- }
551
- if (y === height - 1 || data[idx + width] < 128) {
552
- ctx.moveTo(x, y + 1);
553
- ctx.lineTo(x + 1, y + 1);
554
- }
555
- }
455
+ const { width, height, data } = mask;
456
+ ctx.strokeStyle = resolveCssColor("var(--nice-bg, #fff)", ctx.canvas);
457
+ ctx.lineWidth = 1;
458
+ ctx.setLineDash([4, 4]);
459
+ ctx.lineDashOffset = offset;
460
+ ctx.beginPath();
461
+ for (let y = 0; y < height; y++) {
462
+ for (let x = 0; x < width; x++) {
463
+ const idx = y * width + x;
464
+ if (data[idx] < 128) {
465
+ continue;
466
+ }
467
+ const isEdge = x === 0 || data[idx - 1] < 128 || x === width - 1 || data[idx + 1] < 128 || y === 0 || data[idx - width] < 128 || y === height - 1 || data[idx + width] < 128;
468
+ if (isEdge) {
469
+ if (x === 0 || data[idx - 1] < 128) {
470
+ ctx.moveTo(x, y);
471
+ ctx.lineTo(x, y + 1);
472
+ }
473
+ if (x === width - 1 || data[idx + 1] < 128) {
474
+ ctx.moveTo(x + 1, y);
475
+ ctx.lineTo(x + 1, y + 1);
556
476
  }
477
+ if (y === 0 || data[idx - width] < 128) {
478
+ ctx.moveTo(x, y);
479
+ ctx.lineTo(x + 1, y);
480
+ }
481
+ if (y === height - 1 || data[idx + width] < 128) {
482
+ ctx.moveTo(x, y + 1);
483
+ ctx.lineTo(x + 1, y + 1);
484
+ }
485
+ }
557
486
  }
558
- ctx.stroke();
559
- // Draw again with black for contrast
560
- ctx.strokeStyle = resolveCssColor('var(--nice-text, #000000)', ctx.canvas);
561
- ctx.lineDashOffset = offset + 4;
562
- ctx.stroke();
563
- ctx.setLineDash([]);
487
+ }
488
+ ctx.stroke();
489
+ ctx.strokeStyle = resolveCssColor("var(--nice-text, #000000)", ctx.canvas);
490
+ ctx.lineDashOffset = offset + 4;
491
+ ctx.stroke();
492
+ ctx.setLineDash([]);
564
493
  }
565
-
566
- export { applyHealingBrush, combineMasks, contentAwareFill, createEmptyMask, featherMask, invertMask, lassoSelect, magicWandSelect, magneticLassoPoint, renderSelectionOutline, selectColorRange };
494
+ export {
495
+ applyHealingBrush,
496
+ combineMasks,
497
+ contentAwareFill,
498
+ createEmptyMask,
499
+ featherMask,
500
+ invertMask,
501
+ lassoSelect,
502
+ magicWandSelect,
503
+ magneticLassoPoint,
504
+ renderSelectionOutline,
505
+ selectColorRange
506
+ };