@nice2dev/ui-graphics 1.3.15 → 1.3.17

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,1393 +1,1371 @@
1
- 'use strict';
2
-
3
- var uiCore = require('@nice2dev/ui-core');
4
- var React = require('react');
5
- var editorAsk = require('../core/editorAsk.js');
6
- var asepriteFormat = require('./asepriteFormat.js');
7
- var pixelEditorExports = require('./pixelEditorExports.js');
8
- var pixelEditorTypes = require('./pixelEditorTypes.js');
9
- var pixelEditorUtils = require('./pixelEditorUtils.js');
10
-
11
- /**
12
- * usePixelEditor — custom hook encapsulating ALL state, refs, callbacks,
13
- * side-effects and memos for the PixelEditor component.
14
- */
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const uiCore = require("@nice2dev/ui-core");
4
+ const React = require("react");
5
+ const editorAsk = require("../core/editorAsk.js");
6
+ const asepriteFormat = require("./asepriteFormat.js");
7
+ const pixelEditorExports = require("./pixelEditorExports.js");
8
+ const pixelEditorTypes = require("./pixelEditorTypes.js");
9
+ const pixelEditorUtils = require("./pixelEditorUtils.js");
15
10
  const log = { warn: console.warn, error: console.error };
16
- /* ═══════════════════════════════════════════
17
- Hook
18
- ═══════════════════════════════════════════ */
19
- function usePixelEditor({ initialWidth = 64, initialHeight = 64, onSaveToLibrary, }) {
20
- const { pendingAsk, ask } = editorAsk.useEditorAsk();
21
- /* ── state ── */
22
- const [dw, setDw] = React.useState(initialWidth);
23
- const [dh, setDh] = React.useState(initialHeight);
24
- const [layers, setLayers] = React.useState(() => [
25
- {
26
- id: pixelEditorTypes.lid(),
27
- name: 'Background',
28
- visible: true,
29
- opacity: 255,
30
- locked: false,
31
- blendMode: 'normal',
32
- },
33
- ]);
34
- const [aLi, setALi] = React.useState(0);
35
- const [fc, setFc] = React.useState(1);
36
- const [afi, setAfi] = React.useState(0);
37
- const [fDur, setFDur] = React.useState([100]);
38
- const [tool, setTool] = React.useState('pencil');
39
- const [col1, setCol1] = React.useState('var(--nice-text, #000000)');
40
- const [col2, setCol2] = React.useState('var(--nice-bg, #fff)');
41
- const [brush, setBrush] = React.useState({ ...pixelEditorUtils.DEFAULT_BRUSH });
42
- const [zoom, setZoom] = React.useState(8);
43
- const [px, setPx] = React.useState(0);
44
- const [py, setPy] = React.useState(0);
45
- const [grid, setGrid] = React.useState(true);
46
- const [onion, setOnion] = React.useState(false);
47
- const [fps, setFps] = React.useState(8);
48
- const [playing, setPlaying] = React.useState(false);
49
- const [pal, setPal] = React.useState(pixelEditorTypes.DEFAULT_PALETTE);
50
- const [cursor, setCursor] = React.useState(null);
51
- const [dlgNew, setDlgNew] = React.useState(false);
52
- const [nw, setNw] = React.useState(64);
53
- const [nh, setNh] = React.useState(64);
54
- const [sel, setSel] = React.useState(null);
55
- const [tick, setTick] = React.useState(0);
56
- const [symmetry, setSymmetry] = React.useState('none');
57
- const [shapeFilled, setShapeFilled] = React.useState(false);
58
- const [pressure, setPressure] = React.useState(0.5);
59
- const [showHSV, setShowHSV] = React.useState(false);
60
- const [editingLayerName, setEditingLayerName] = React.useState(null);
61
- /* ── refs ── */
62
- const cvRef = React.useRef(null);
63
- const boxRef = React.useRef(null);
64
- const cels = React.useRef(new Map());
65
- const hist = React.useRef([]);
66
- const hIdx = React.useRef(-1);
67
- const drawing = React.useRef(false);
68
- const lastPx = React.useRef(null);
69
- const tStart = React.useRef(null);
70
- const preview = React.useRef(null);
71
- const panning = React.useRef(false);
72
- const panSt = React.useRef({ mx: 0, my: 0, px: 0, py: 0 });
73
- const fInp = React.useRef(null);
74
- const btn = React.useRef(0);
75
- const curPressure = React.useRef(0.5);
76
- const clipboard = React.useRef(null);
77
- const brushDist = React.useRef(0);
78
- /* ── cel access ── */
79
- const getCel = React.useCallback((f, l) => {
80
- const k = pixelEditorTypes.celKey(f, l);
81
- let c = cels.current.get(k);
82
- if (!c) {
83
- c = pixelEditorUtils.createBlankImageData(dw, dh);
84
- cels.current.set(k, c);
85
- }
86
- return c;
87
- }, [dw, dh]);
88
- /* ── coordinate transform ── */
89
- const scr2art = React.useCallback((cx, cy) => {
90
- var _a;
91
- const r = (_a = boxRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
92
- if (!r) {
93
- return { x: 0, y: 0 };
94
- }
95
- return {
96
- x: Math.floor((cx - r.left - px - r.width / 2) / zoom + dw / 2),
97
- y: Math.floor((cy - r.top - py - r.height / 2) / zoom + dh / 2),
98
- };
99
- }, [px, py, zoom, dw, dh]);
100
- /* ── history ── */
101
- const snap = React.useCallback((desc) => {
102
- const m = new Map();
103
- cels.current.forEach((v, k) => m.set(k, pixelEditorUtils.cloneImageData(v)));
104
- hist.current = hist.current.slice(0, hIdx.current + 1);
105
- hist.current.push({
106
- desc,
107
- cels: m,
108
- layers: JSON.parse(JSON.stringify(layers)),
109
- frameCount: fc,
110
- });
111
- if (hist.current.length > 50) {
112
- hist.current.shift();
113
- }
114
- hIdx.current = hist.current.length - 1;
115
- }, [layers, fc]);
116
- const restoreEntry = React.useCallback((e) => {
117
- cels.current = new Map();
118
- e.cels.forEach((v, k) => cels.current.set(k, pixelEditorUtils.cloneImageData(v)));
119
- setLayers(JSON.parse(JSON.stringify(e.layers)));
120
- setFc(e.frameCount);
121
- setTick((t) => t + 1);
122
- }, []);
123
- const undo = React.useCallback(() => {
124
- if (hIdx.current <= 0) {
125
- return;
126
- }
127
- hIdx.current--;
128
- restoreEntry(hist.current[hIdx.current]);
129
- }, [restoreEntry]);
130
- const redo = React.useCallback(() => {
131
- if (hIdx.current >= hist.current.length - 1) {
132
- return;
133
- }
134
- hIdx.current++;
135
- restoreEntry(hist.current[hIdx.current]);
136
- }, [restoreEntry]);
137
- /* Push initial snapshot so first undo works */
138
- React.useEffect(() => {
139
- if (hist.current.length === 0) {
140
- snap('initial');
141
- }
142
- }, [snap]);
143
- /* ── composite a frame ── */
144
- const compFrame = React.useCallback((fi) => {
145
- const cv = new OffscreenCanvas(dw, dh);
146
- const ctx = cv.getContext('2d');
147
- for (let li = 0; li < layers.length; li++) {
148
- if (!layers[li].visible) {
149
- continue;
150
- }
151
- const c = cels.current.get(pixelEditorTypes.celKey(fi, li));
152
- if (!c) {
153
- continue;
154
- }
155
- const t = new OffscreenCanvas(dw, dh);
156
- t.getContext('2d').putImageData(c, 0, 0);
157
- ctx.globalAlpha = layers[li].opacity / 255;
158
- const bm = layers[li].blendMode;
159
- if (bm !== 'normal') {
160
- ctx.globalCompositeOperation = bm;
161
- }
162
- else {
163
- ctx.globalCompositeOperation = 'source-over';
164
- }
165
- ctx.drawImage(t, 0, 0);
166
- }
167
- ctx.globalCompositeOperation = 'source-over';
168
- return ctx.getImageData(0, 0, dw, dh);
169
- }, [dw, dh, layers]);
170
- /* ── rendering ── */
171
- const render = React.useCallback(() => {
172
- const canvas = cvRef.current;
173
- const box = boxRef.current;
174
- if (!canvas || !box) {
175
- return;
176
- }
177
- const cw = box.clientWidth;
178
- const ch = box.clientHeight;
179
- if (canvas.width !== cw || canvas.height !== ch) {
180
- canvas.width = cw;
181
- canvas.height = ch;
182
- }
183
- const ctx = canvas.getContext('2d');
184
- ctx.clearRect(0, 0, cw, ch);
185
- ctx.imageSmoothingEnabled = false;
186
- ctx.save();
187
- ctx.translate(cw / 2 + px, ch / 2 + py);
188
- ctx.scale(zoom, zoom);
189
- ctx.translate(-dw / 2, -dh / 2);
190
- // checkerboard
191
- for (let y = 0; y < dh; y++) {
192
- for (let x = 0; x < dw; x++) {
193
- ctx.fillStyle = (x + y) % 2 === 0 ? pixelEditorTypes.PIXEL_CHECKER_DARK[0] : pixelEditorTypes.PIXEL_CHECKER_DARK[1];
194
- ctx.fillRect(x, y, 1, 1);
195
- }
11
+ function usePixelEditor({
12
+ initialWidth = 64,
13
+ initialHeight = 64,
14
+ onSaveToLibrary
15
+ }) {
16
+ const { pendingAsk, ask } = editorAsk.useEditorAsk();
17
+ const [dw, setDw] = React.useState(initialWidth);
18
+ const [dh, setDh] = React.useState(initialHeight);
19
+ const [layers, setLayers] = React.useState(() => [
20
+ {
21
+ id: pixelEditorTypes.lid(),
22
+ name: "Background",
23
+ visible: true,
24
+ opacity: 255,
25
+ locked: false,
26
+ blendMode: "normal"
27
+ }
28
+ ]);
29
+ const [aLi, setALi] = React.useState(0);
30
+ const [fc, setFc] = React.useState(1);
31
+ const [afi, setAfi] = React.useState(0);
32
+ const [fDur, setFDur] = React.useState([100]);
33
+ const [tool, setTool] = React.useState("pencil");
34
+ const [col1, setCol1] = React.useState("var(--nice-text, #000000)");
35
+ const [col2, setCol2] = React.useState("var(--nice-bg, #fff)");
36
+ const [brush, setBrush] = React.useState({ ...pixelEditorUtils.DEFAULT_BRUSH });
37
+ const [zoom, setZoom] = React.useState(8);
38
+ const [px, setPx] = React.useState(0);
39
+ const [py, setPy] = React.useState(0);
40
+ const [grid, setGrid] = React.useState(true);
41
+ const [onion, setOnion] = React.useState(false);
42
+ const [fps, setFps] = React.useState(8);
43
+ const [playing, setPlaying] = React.useState(false);
44
+ const [pal, setPal] = React.useState(pixelEditorTypes.DEFAULT_PALETTE);
45
+ const [cursor, setCursor] = React.useState(null);
46
+ const [dlgNew, setDlgNew] = React.useState(false);
47
+ const [nw, setNw] = React.useState(64);
48
+ const [nh, setNh] = React.useState(64);
49
+ const [sel, setSel] = React.useState(null);
50
+ const [tick, setTick] = React.useState(0);
51
+ const [symmetry, setSymmetry] = React.useState("none");
52
+ const [shapeFilled, setShapeFilled] = React.useState(false);
53
+ const [pressure, setPressure] = React.useState(0.5);
54
+ const [showHSV, setShowHSV] = React.useState(false);
55
+ const [editingLayerName, setEditingLayerName] = React.useState(null);
56
+ const cvRef = React.useRef(null);
57
+ const boxRef = React.useRef(null);
58
+ const cels = React.useRef(/* @__PURE__ */ new Map());
59
+ const hist = React.useRef([]);
60
+ const hIdx = React.useRef(-1);
61
+ const drawing = React.useRef(false);
62
+ const lastPx = React.useRef(null);
63
+ const tStart = React.useRef(null);
64
+ const preview = React.useRef(
65
+ null
66
+ );
67
+ const panning = React.useRef(false);
68
+ const panSt = React.useRef({ mx: 0, my: 0, px: 0, py: 0 });
69
+ const fInp = React.useRef(null);
70
+ const btn = React.useRef(0);
71
+ const curPressure = React.useRef(0.5);
72
+ const clipboard = React.useRef(null);
73
+ const brushDist = React.useRef(0);
74
+ const getCel = React.useCallback(
75
+ (f, l) => {
76
+ const k = pixelEditorTypes.celKey(f, l);
77
+ let c = cels.current.get(k);
78
+ if (!c) {
79
+ c = pixelEditorUtils.createBlankImageData(dw, dh);
80
+ cels.current.set(k, c);
81
+ }
82
+ return c;
83
+ },
84
+ [dw, dh]
85
+ );
86
+ const scr2art = React.useCallback(
87
+ (cx, cy) => {
88
+ const r = boxRef.current?.getBoundingClientRect();
89
+ if (!r) {
90
+ return { x: 0, y: 0 };
91
+ }
92
+ return {
93
+ x: Math.floor((cx - r.left - px - r.width / 2) / zoom + dw / 2),
94
+ y: Math.floor((cy - r.top - py - r.height / 2) / zoom + dh / 2)
95
+ };
96
+ },
97
+ [px, py, zoom, dw, dh]
98
+ );
99
+ const snap = React.useCallback(
100
+ (desc) => {
101
+ const m = /* @__PURE__ */ new Map();
102
+ cels.current.forEach((v, k) => m.set(k, pixelEditorUtils.cloneImageData(v)));
103
+ hist.current = hist.current.slice(0, hIdx.current + 1);
104
+ hist.current.push({
105
+ desc,
106
+ cels: m,
107
+ layers: JSON.parse(JSON.stringify(layers)),
108
+ frameCount: fc
109
+ });
110
+ if (hist.current.length > 50) {
111
+ hist.current.shift();
112
+ }
113
+ hIdx.current = hist.current.length - 1;
114
+ },
115
+ [layers, fc]
116
+ );
117
+ const restoreEntry = React.useCallback((e) => {
118
+ cels.current = /* @__PURE__ */ new Map();
119
+ e.cels.forEach((v, k) => cels.current.set(k, pixelEditorUtils.cloneImageData(v)));
120
+ setLayers(JSON.parse(JSON.stringify(e.layers)));
121
+ setFc(e.frameCount);
122
+ setTick((t) => t + 1);
123
+ }, []);
124
+ const undo = React.useCallback(() => {
125
+ if (hIdx.current <= 0) {
126
+ return;
127
+ }
128
+ hIdx.current--;
129
+ restoreEntry(hist.current[hIdx.current]);
130
+ }, [restoreEntry]);
131
+ const redo = React.useCallback(() => {
132
+ if (hIdx.current >= hist.current.length - 1) {
133
+ return;
134
+ }
135
+ hIdx.current++;
136
+ restoreEntry(hist.current[hIdx.current]);
137
+ }, [restoreEntry]);
138
+ React.useEffect(() => {
139
+ if (hist.current.length === 0) {
140
+ snap("initial");
141
+ }
142
+ }, [snap]);
143
+ const compFrame = React.useCallback(
144
+ (fi) => {
145
+ const cv = new OffscreenCanvas(dw, dh);
146
+ const ctx = cv.getContext("2d");
147
+ for (let li = 0; li < layers.length; li++) {
148
+ if (!layers[li].visible) {
149
+ continue;
196
150
  }
197
- // onion skin (prev frame)
198
- if (onion && afi > 0) {
199
- const oc = new OffscreenCanvas(dw, dh);
200
- const ox = oc.getContext('2d');
201
- for (let li = 0; li < layers.length; li++) {
202
- if (!layers[li].visible) {
203
- continue;
204
- }
205
- const c = cels.current.get(pixelEditorTypes.celKey(afi - 1, li));
206
- if (!c) {
207
- continue;
208
- }
209
- const t = new OffscreenCanvas(dw, dh);
210
- t.getContext('2d').putImageData(c, 0, 0);
211
- ox.globalAlpha = (layers[li].opacity / 255) * 0.25;
212
- ox.drawImage(t, 0, 0);
213
- }
214
- ctx.drawImage(oc, 0, 0);
215
- }
216
- // current frame composite
217
- const comp = new OffscreenCanvas(dw, dh);
218
- const cc = comp.getContext('2d');
219
- for (let li = 0; li < layers.length; li++) {
220
- if (!layers[li].visible) {
221
- continue;
222
- }
223
- const c = cels.current.get(pixelEditorTypes.celKey(afi, li));
224
- if (!c) {
225
- continue;
226
- }
227
- const t = new OffscreenCanvas(dw, dh);
228
- t.getContext('2d').putImageData(c, 0, 0);
229
- cc.globalAlpha = layers[li].opacity / 255;
230
- const bm = layers[li].blendMode;
231
- if (bm !== 'normal') {
232
- cc.globalCompositeOperation = bm;
233
- }
234
- else {
235
- cc.globalCompositeOperation = 'source-over';
236
- }
237
- cc.drawImage(t, 0, 0);
238
- }
239
- cc.globalCompositeOperation = 'source-over';
240
- ctx.drawImage(comp, 0, 0);
241
- // tool preview (line / rect / ellipse while dragging)
242
- if (preview.current && drawing.current) {
243
- const pv = preview.current;
244
- const clr = btn.current === 2 ? pixelEditorTypes.hex2t(col2) : pixelEditorTypes.hex2t(col1);
245
- const pvData = pixelEditorUtils.cloneImageData(getCel(afi, aLi));
246
- if (tool === 'line') {
247
- pixelEditorUtils.bresenhamLine(pv.fr.x, pv.fr.y, pv.to.x, pv.to.y, (bx, by) => {
248
- if (pixelEditorTypes.inB(bx, by, dw, dh)) {
249
- pixelEditorUtils.setPixel(pvData, bx, by, clr);
250
- }
251
- });
252
- }
253
- else if (tool === 'rect') {
254
- const x1 = Math.min(pv.fr.x, pv.to.x), y1 = Math.min(pv.fr.y, pv.to.y);
255
- const x2 = Math.max(pv.fr.x, pv.to.x), y2 = Math.max(pv.fr.y, pv.to.y);
256
- if (shapeFilled) {
257
- for (let y = y1; y <= y2; y++) {
258
- for (let x = x1; x <= x2; x++) {
259
- if (pixelEditorTypes.inB(x, y, dw, dh)) {
260
- pixelEditorUtils.setPixel(pvData, x, y, clr);
261
- }
262
- }
263
- }
264
- }
265
- else {
266
- for (let x = x1; x <= x2; x++) {
267
- if (pixelEditorTypes.inB(x, y1, dw, dh)) {
268
- pixelEditorUtils.setPixel(pvData, x, y1, clr);
269
- }
270
- if (pixelEditorTypes.inB(x, y2, dw, dh)) {
271
- pixelEditorUtils.setPixel(pvData, x, y2, clr);
272
- }
273
- }
274
- for (let y = y1 + 1; y < y2; y++) {
275
- if (pixelEditorTypes.inB(x1, y, dw, dh)) {
276
- pixelEditorUtils.setPixel(pvData, x1, y, clr);
277
- }
278
- if (pixelEditorTypes.inB(x2, y, dw, dh)) {
279
- pixelEditorUtils.setPixel(pvData, x2, y, clr);
280
- }
281
- }
282
- }
283
- }
284
- else if (tool === 'ellipse') {
285
- const cx2 = (pv.fr.x + pv.to.x) / 2, cy2 = (pv.fr.y + pv.to.y) / 2;
286
- const rx = Math.abs(pv.to.x - pv.fr.x) / 2, ry = Math.abs(pv.to.y - pv.fr.y) / 2;
287
- pixelEditorTypes.plotEllipse(pvData, cx2, cy2, rx, ry, clr, dw, dh, shapeFilled);
288
- }
289
- else if (tool === 'gradient') {
290
- const c2 = btn.current === 2 ? pixelEditorTypes.hex2t(col1) : pixelEditorTypes.hex2t(col2);
291
- pixelEditorUtils.drawGradient(pvData, pv.fr.x, pv.fr.y, pv.to.x, pv.to.y, clr, c2, sel);
292
- }
293
- const pcv = new OffscreenCanvas(dw, dh);
294
- pcv.getContext('2d').putImageData(pvData, 0, 0);
295
- ctx.globalAlpha = layers[aLi].opacity / 255;
296
- ctx.drawImage(pcv, 0, 0);
297
- ctx.globalAlpha = 1;
298
- }
299
- // symmetry guides
300
- if (symmetry !== 'none') {
301
- ctx.strokeStyle = uiCore.resolveCssColor('var(--nice-danger-tint-30, rgba(255,100,100,0.3))', ctx.canvas);
302
- ctx.lineWidth = 1 / zoom;
303
- ctx.setLineDash([4 / zoom, 4 / zoom]);
304
- if (symmetry === 'h' || symmetry === 'both') {
305
- ctx.beginPath();
306
- ctx.moveTo(dw / 2, 0);
307
- ctx.lineTo(dw / 2, dh);
308
- ctx.stroke();
309
- }
310
- if (symmetry === 'v' || symmetry === 'both') {
311
- ctx.beginPath();
312
- ctx.moveTo(0, dh / 2);
313
- ctx.lineTo(dw, dh / 2);
314
- ctx.stroke();
315
- }
316
- if (symmetry === 'radial4') {
317
- ctx.beginPath();
318
- ctx.moveTo(dw / 2, 0);
319
- ctx.lineTo(dw / 2, dh);
320
- ctx.moveTo(0, dh / 2);
321
- ctx.lineTo(dw, dh / 2);
322
- ctx.stroke();
323
- }
324
- ctx.setLineDash([]);
151
+ const c = cels.current.get(pixelEditorTypes.celKey(fi, li));
152
+ if (!c) {
153
+ continue;
325
154
  }
326
- // selection marching ants
327
- if (sel) {
328
- ctx.strokeStyle = uiCore.resolveCssColor('var(--nice-overlay-light-80, rgba(255, 255, 255, 0.8))', ctx.canvas);
329
- ctx.lineWidth = 1 / zoom;
330
- ctx.setLineDash([4 / zoom, 4 / zoom]);
331
- ctx.strokeRect(sel.x, sel.y, sel.w, sel.h);
332
- ctx.setLineDash([]);
155
+ const t = new OffscreenCanvas(dw, dh);
156
+ t.getContext("2d").putImageData(c, 0, 0);
157
+ ctx.globalAlpha = layers[li].opacity / 255;
158
+ const bm = layers[li].blendMode;
159
+ if (bm !== "normal") {
160
+ ctx.globalCompositeOperation = bm;
161
+ } else {
162
+ ctx.globalCompositeOperation = "source-over";
333
163
  }
334
- // grid
335
- if (grid && zoom >= 4) {
336
- // audit-allow: literal-color — a faint light grid over the pixels, the same on every theme's canvas
337
- ctx.strokeStyle = 'rgba(255,255,255,0.07)';
338
- ctx.lineWidth = 1 / zoom;
339
- for (let x = 0; x <= dw; x++) {
340
- ctx.beginPath();
341
- ctx.moveTo(x, 0);
342
- ctx.lineTo(x, dh);
343
- ctx.stroke();
344
- }
345
- for (let y = 0; y <= dh; y++) {
346
- ctx.beginPath();
347
- ctx.moveTo(0, y);
348
- ctx.lineTo(dw, y);
349
- ctx.stroke();
350
- }
164
+ ctx.drawImage(t, 0, 0);
165
+ }
166
+ ctx.globalCompositeOperation = "source-over";
167
+ return ctx.getImageData(0, 0, dw, dh);
168
+ },
169
+ [dw, dh, layers]
170
+ );
171
+ const render = React.useCallback(() => {
172
+ const canvas = cvRef.current;
173
+ const box = boxRef.current;
174
+ if (!canvas || !box) {
175
+ return;
176
+ }
177
+ const cw = box.clientWidth;
178
+ const ch = box.clientHeight;
179
+ if (canvas.width !== cw || canvas.height !== ch) {
180
+ canvas.width = cw;
181
+ canvas.height = ch;
182
+ }
183
+ const ctx = canvas.getContext("2d");
184
+ ctx.clearRect(0, 0, cw, ch);
185
+ ctx.imageSmoothingEnabled = false;
186
+ ctx.save();
187
+ ctx.translate(cw / 2 + px, ch / 2 + py);
188
+ ctx.scale(zoom, zoom);
189
+ ctx.translate(-dw / 2, -dh / 2);
190
+ for (let y = 0; y < dh; y++) {
191
+ for (let x = 0; x < dw; x++) {
192
+ ctx.fillStyle = (x + y) % 2 === 0 ? pixelEditorTypes.PIXEL_CHECKER_DARK[0] : pixelEditorTypes.PIXEL_CHECKER_DARK[1];
193
+ ctx.fillRect(x, y, 1, 1);
194
+ }
195
+ }
196
+ if (onion && afi > 0) {
197
+ const oc = new OffscreenCanvas(dw, dh);
198
+ const ox = oc.getContext("2d");
199
+ for (let li = 0; li < layers.length; li++) {
200
+ if (!layers[li].visible) {
201
+ continue;
351
202
  }
352
- // border
353
- ctx.strokeStyle = uiCore.resolveCssColor('var(--nice-primary-tint-40, rgba(80,100,200,0.4))', ctx.canvas);
354
- ctx.lineWidth = 1 / zoom;
355
- ctx.strokeRect(-0.5 / zoom, -0.5 / zoom, dw + 1 / zoom, dh + 1 / zoom);
356
- // brush cursor preview
357
- if (cursor &&
358
- pixelEditorTypes.inB(cursor.x, cursor.y, dw, dh) &&
359
- (tool === 'pencil' || tool === 'eraser' || tool === 'dither')) {
360
- const sz = brush.pressureSize
361
- ? Math.max(1, Math.round(brush.size * curPressure.current))
362
- : brush.size;
363
- const half = Math.floor(sz / 2);
364
- ctx.strokeStyle = uiCore.resolveCssColor('var(--nice-overlay-light-50, rgba(255, 255, 255, 0.5))', ctx.canvas);
365
- ctx.lineWidth = 1 / zoom;
366
- if (brush.shape === 'circle') {
367
- ctx.beginPath();
368
- ctx.arc(cursor.x + 0.5, cursor.y + 0.5, half + 0.5, 0, Math.PI * 2);
369
- ctx.stroke();
370
- }
371
- else {
372
- ctx.strokeRect(cursor.x - half, cursor.y - half, sz, sz);
373
- }
203
+ const c = cels.current.get(pixelEditorTypes.celKey(afi - 1, li));
204
+ if (!c) {
205
+ continue;
374
206
  }
375
- ctx.restore();
376
- }, [
377
- dw,
378
- dh,
379
- zoom,
380
- px,
381
- py,
382
- layers,
383
- afi,
384
- aLi,
385
- grid,
386
- onion,
387
- tool,
388
- col1,
389
- col2,
390
- sel,
391
- getCel,
392
- symmetry,
393
- shapeFilled,
394
- brush,
395
- cursor,
396
- ]);
397
- /* ── brush drawing with symmetry ── */
398
- const applyBrushAt = React.useCallback((x, y, erase, pr) => {
399
- const cel = getCel(afi, aLi);
400
- const clr = erase ? [0, 0, 0, 0] : pixelEditorTypes.hex2t(btn.current === 2 ? col2 : col1);
401
- const pts = pixelEditorUtils.getSymmetryPoints(x, y, dw, dh, symmetry);
402
- for (const pt of pts) {
403
- if (tool === 'dither') {
404
- pixelEditorUtils.applyDitherStamp(cel, pt.x, pt.y, clr, brush.size, pr);
405
- }
406
- else {
407
- pixelEditorUtils.applyBrushStamp(cel, pt.x, pt.y, clr, brush, pr, erase);
207
+ const t = new OffscreenCanvas(dw, dh);
208
+ t.getContext("2d").putImageData(c, 0, 0);
209
+ ox.globalAlpha = layers[li].opacity / 255 * 0.25;
210
+ ox.drawImage(t, 0, 0);
211
+ }
212
+ ctx.drawImage(oc, 0, 0);
213
+ }
214
+ const comp = new OffscreenCanvas(dw, dh);
215
+ const cc = comp.getContext("2d");
216
+ for (let li = 0; li < layers.length; li++) {
217
+ if (!layers[li].visible) {
218
+ continue;
219
+ }
220
+ const c = cels.current.get(pixelEditorTypes.celKey(afi, li));
221
+ if (!c) {
222
+ continue;
223
+ }
224
+ const t = new OffscreenCanvas(dw, dh);
225
+ t.getContext("2d").putImageData(c, 0, 0);
226
+ cc.globalAlpha = layers[li].opacity / 255;
227
+ const bm = layers[li].blendMode;
228
+ if (bm !== "normal") {
229
+ cc.globalCompositeOperation = bm;
230
+ } else {
231
+ cc.globalCompositeOperation = "source-over";
232
+ }
233
+ cc.drawImage(t, 0, 0);
234
+ }
235
+ cc.globalCompositeOperation = "source-over";
236
+ ctx.drawImage(comp, 0, 0);
237
+ if (preview.current && drawing.current) {
238
+ const pv = preview.current;
239
+ const clr = btn.current === 2 ? pixelEditorTypes.hex2t(col2) : pixelEditorTypes.hex2t(col1);
240
+ const pvData = pixelEditorUtils.cloneImageData(getCel(afi, aLi));
241
+ if (tool === "line") {
242
+ pixelEditorUtils.bresenhamLine(pv.fr.x, pv.fr.y, pv.to.x, pv.to.y, (bx, by) => {
243
+ if (pixelEditorTypes.inB(bx, by, dw, dh)) {
244
+ pixelEditorUtils.setPixel(pvData, bx, by, clr);
245
+ }
246
+ });
247
+ } else if (tool === "rect") {
248
+ const x1 = Math.min(pv.fr.x, pv.to.x), y1 = Math.min(pv.fr.y, pv.to.y);
249
+ const x2 = Math.max(pv.fr.x, pv.to.x), y2 = Math.max(pv.fr.y, pv.to.y);
250
+ if (shapeFilled) {
251
+ for (let y = y1; y <= y2; y++) {
252
+ for (let x = x1; x <= x2; x++) {
253
+ if (pixelEditorTypes.inB(x, y, dw, dh)) {
254
+ pixelEditorUtils.setPixel(pvData, x, y, clr);
255
+ }
408
256
  }
409
- }
410
- }, [getCel, afi, aLi, col1, col2, brush, dw, dh, symmetry, tool]);
411
- /* ── pointer events (pressure-aware) ── */
412
- const onDown = React.useCallback((e) => {
413
- var _a, _b, _c;
414
- e.preventDefault();
415
- (_b = (_a = e.target).setPointerCapture) === null || _b === void 0 ? void 0 : _b.call(_a, e.pointerId);
416
- btn.current = e.button;
417
- const pr = e.pressure > 0 ? e.pressure : 0.5;
418
- curPressure.current = pr;
419
- setPressure(pr);
420
- if (e.button === 1) {
421
- panning.current = true;
422
- panSt.current = { mx: e.clientX, my: e.clientY, px, py };
423
- return;
424
- }
425
- const p = scr2art(e.clientX, e.clientY);
426
- setCursor(p);
427
- if ((_c = layers[aLi]) === null || _c === void 0 ? void 0 : _c.locked) {
428
- return;
429
- }
430
- switch (tool) {
431
- case 'pencil':
432
- case 'eraser':
433
- case 'dither':
434
- snap(tool === 'pencil' ? 'Draw' : tool === 'eraser' ? 'Erase' : 'Dither');
435
- drawing.current = true;
436
- lastPx.current = p;
437
- brushDist.current = 0;
438
- applyBrushAt(p.x, p.y, tool === 'eraser', pr);
439
- setTick((t) => t + 1);
440
- break;
441
- case 'fill':
442
- if (pixelEditorTypes.inB(p.x, p.y, dw, dh)) {
443
- snap('Fill');
444
- const clr = pixelEditorTypes.hex2t(e.button === 2 ? col2 : col1);
445
- const pts = pixelEditorUtils.getSymmetryPoints(p.x, p.y, dw, dh, symmetry);
446
- const cel = getCel(afi, aLi);
447
- for (const pt of pts) {
448
- if (pixelEditorTypes.inB(pt.x, pt.y, dw, dh)) {
449
- pixelEditorUtils.floodFill(cel, pt.x, pt.y, clr);
450
- }
451
- }
452
- setTick((t) => t + 1);
453
- }
454
- break;
455
- case 'eyedropper': {
456
- if (pixelEditorTypes.inB(p.x, p.y, dw, dh)) {
457
- const c = pixelEditorUtils.getPixel(getCel(afi, aLi), p.x, p.y);
458
- const hex = pixelEditorUtils.rgbaToHex(c);
459
- if (e.button === 2) {
460
- setCol2(hex);
461
- }
462
- else {
463
- setCol1(hex);
464
- }
465
- }
466
- break;
257
+ }
258
+ } else {
259
+ for (let x = x1; x <= x2; x++) {
260
+ if (pixelEditorTypes.inB(x, y1, dw, dh)) {
261
+ pixelEditorUtils.setPixel(pvData, x, y1, clr);
467
262
  }
468
- case 'line':
469
- case 'rect':
470
- case 'ellipse':
471
- case 'gradient':
472
- snap(tool[0].toUpperCase() + tool.slice(1));
473
- drawing.current = true;
474
- tStart.current = p;
475
- preview.current = { fr: p, to: p };
476
- break;
477
- case 'select':
478
- drawing.current = true;
479
- tStart.current = p;
480
- break;
481
- case 'move':
482
- drawing.current = true;
483
- lastPx.current = p;
484
- break;
485
- }
486
- }, [
487
- tool,
488
- aLi,
489
- layers,
490
- px,
491
- py,
492
- scr2art,
493
- snap,
494
- applyBrushAt,
495
- getCel,
496
- afi,
497
- col1,
498
- col2,
499
- dw,
500
- dh,
501
- symmetry,
502
- ]);
503
- const onMove = React.useCallback((e) => {
504
- const pr = e.pressure > 0 ? e.pressure : 0.5;
505
- curPressure.current = pr;
506
- setPressure(pr);
507
- if (panning.current) {
508
- setPx(panSt.current.px + (e.clientX - panSt.current.mx));
509
- setPy(panSt.current.py + (e.clientY - panSt.current.my));
510
- return;
511
- }
512
- const p = scr2art(e.clientX, e.clientY);
513
- setCursor(p);
514
- if (!drawing.current) {
515
- return;
516
- }
517
- if (tool === 'pencil' || tool === 'eraser' || tool === 'dither') {
518
- const last = lastPx.current || p;
519
- const spacing = Math.max(1, brush.size * (brush.spacing / 100));
520
- const ddx = p.x - last.x, ddy = p.y - last.y;
521
- const dist = Math.sqrt(ddx * ddx + ddy * ddy);
522
- if (dist < 0.5) {
523
- return;
263
+ if (pixelEditorTypes.inB(x, y2, dw, dh)) {
264
+ pixelEditorUtils.setPixel(pvData, x, y2, clr);
524
265
  }
525
- brushDist.current += dist;
526
- if (brushDist.current >= spacing) {
527
- pixelEditorUtils.bresenhamLine(last.x, last.y, p.x, p.y, (bx, by) => {
528
- applyBrushAt(bx, by, tool === 'eraser', pr);
529
- });
530
- brushDist.current = 0;
266
+ }
267
+ for (let y = y1 + 1; y < y2; y++) {
268
+ if (pixelEditorTypes.inB(x1, y, dw, dh)) {
269
+ pixelEditorUtils.setPixel(pvData, x1, y, clr);
531
270
  }
532
- lastPx.current = p;
533
- setTick((t) => t + 1);
534
- }
535
- else if (tool === 'line' || tool === 'rect' || tool === 'ellipse' || tool === 'gradient') {
536
- preview.current = { fr: tStart.current, to: p };
537
- setTick((t) => t + 1);
538
- }
539
- else if (tool === 'select') {
540
- const s = tStart.current;
541
- setSel({
542
- x: Math.min(s.x, p.x),
543
- y: Math.min(s.y, p.y),
544
- w: Math.abs(p.x - s.x) + 1,
545
- h: Math.abs(p.y - s.y) + 1,
546
- });
547
- }
548
- else if (tool === 'move' && sel) {
549
- const last2 = lastPx.current || p;
550
- const ddx = p.x - last2.x, ddy = p.y - last2.y;
551
- if (ddx !== 0 || ddy !== 0) {
552
- setSel((prev) => (prev ? { ...prev, x: prev.x + ddx, y: prev.y + ddy } : null));
271
+ if (pixelEditorTypes.inB(x2, y, dw, dh)) {
272
+ pixelEditorUtils.setPixel(pvData, x2, y, clr);
553
273
  }
554
- lastPx.current = p;
555
- }
556
- }, [tool, scr2art, applyBrushAt, brush, sel]);
557
- const onUp = React.useCallback((e) => {
558
- if (panning.current) {
559
- panning.current = false;
560
- return;
274
+ }
561
275
  }
562
- if (!drawing.current) {
563
- return;
276
+ } else if (tool === "ellipse") {
277
+ const cx2 = (pv.fr.x + pv.to.x) / 2, cy2 = (pv.fr.y + pv.to.y) / 2;
278
+ const rx = Math.abs(pv.to.x - pv.fr.x) / 2, ry = Math.abs(pv.to.y - pv.fr.y) / 2;
279
+ pixelEditorTypes.plotEllipse(pvData, cx2, cy2, rx, ry, clr, dw, dh, shapeFilled);
280
+ } else if (tool === "gradient") {
281
+ const c2 = btn.current === 2 ? pixelEditorTypes.hex2t(col1) : pixelEditorTypes.hex2t(col2);
282
+ pixelEditorUtils.drawGradient(pvData, pv.fr.x, pv.fr.y, pv.to.x, pv.to.y, clr, c2, sel);
283
+ }
284
+ const pcv = new OffscreenCanvas(dw, dh);
285
+ pcv.getContext("2d").putImageData(pvData, 0, 0);
286
+ ctx.globalAlpha = layers[aLi].opacity / 255;
287
+ ctx.drawImage(pcv, 0, 0);
288
+ ctx.globalAlpha = 1;
289
+ }
290
+ if (symmetry !== "none") {
291
+ ctx.strokeStyle = uiCore.resolveCssColor(
292
+ "var(--nice-danger-tint-30, rgba(255,100,100,0.3))",
293
+ ctx.canvas
294
+ );
295
+ ctx.lineWidth = 1 / zoom;
296
+ ctx.setLineDash([4 / zoom, 4 / zoom]);
297
+ if (symmetry === "h" || symmetry === "both") {
298
+ ctx.beginPath();
299
+ ctx.moveTo(dw / 2, 0);
300
+ ctx.lineTo(dw / 2, dh);
301
+ ctx.stroke();
302
+ }
303
+ if (symmetry === "v" || symmetry === "both") {
304
+ ctx.beginPath();
305
+ ctx.moveTo(0, dh / 2);
306
+ ctx.lineTo(dw, dh / 2);
307
+ ctx.stroke();
308
+ }
309
+ if (symmetry === "radial4") {
310
+ ctx.beginPath();
311
+ ctx.moveTo(dw / 2, 0);
312
+ ctx.lineTo(dw / 2, dh);
313
+ ctx.moveTo(0, dh / 2);
314
+ ctx.lineTo(dw, dh / 2);
315
+ ctx.stroke();
316
+ }
317
+ ctx.setLineDash([]);
318
+ }
319
+ if (sel) {
320
+ ctx.strokeStyle = uiCore.resolveCssColor(
321
+ "var(--nice-overlay-light-80, rgba(255, 255, 255, 0.8))",
322
+ ctx.canvas
323
+ );
324
+ ctx.lineWidth = 1 / zoom;
325
+ ctx.setLineDash([4 / zoom, 4 / zoom]);
326
+ ctx.strokeRect(sel.x, sel.y, sel.w, sel.h);
327
+ ctx.setLineDash([]);
328
+ }
329
+ if (grid && zoom >= 4) {
330
+ ctx.strokeStyle = "rgba(255,255,255,0.07)";
331
+ ctx.lineWidth = 1 / zoom;
332
+ for (let x = 0; x <= dw; x++) {
333
+ ctx.beginPath();
334
+ ctx.moveTo(x, 0);
335
+ ctx.lineTo(x, dh);
336
+ ctx.stroke();
337
+ }
338
+ for (let y = 0; y <= dh; y++) {
339
+ ctx.beginPath();
340
+ ctx.moveTo(0, y);
341
+ ctx.lineTo(dw, y);
342
+ ctx.stroke();
343
+ }
344
+ }
345
+ ctx.strokeStyle = uiCore.resolveCssColor(
346
+ "var(--nice-primary-tint-40, rgba(80,100,200,0.4))",
347
+ ctx.canvas
348
+ );
349
+ ctx.lineWidth = 1 / zoom;
350
+ ctx.strokeRect(-0.5 / zoom, -0.5 / zoom, dw + 1 / zoom, dh + 1 / zoom);
351
+ if (cursor && pixelEditorTypes.inB(cursor.x, cursor.y, dw, dh) && (tool === "pencil" || tool === "eraser" || tool === "dither")) {
352
+ const sz = brush.pressureSize ? Math.max(1, Math.round(brush.size * curPressure.current)) : brush.size;
353
+ const half = Math.floor(sz / 2);
354
+ ctx.strokeStyle = uiCore.resolveCssColor(
355
+ "var(--nice-overlay-light-50, rgba(255, 255, 255, 0.5))",
356
+ ctx.canvas
357
+ );
358
+ ctx.lineWidth = 1 / zoom;
359
+ if (brush.shape === "circle") {
360
+ ctx.beginPath();
361
+ ctx.arc(cursor.x + 0.5, cursor.y + 0.5, half + 0.5, 0, Math.PI * 2);
362
+ ctx.stroke();
363
+ } else {
364
+ ctx.strokeRect(cursor.x - half, cursor.y - half, sz, sz);
365
+ }
366
+ }
367
+ ctx.restore();
368
+ }, [
369
+ dw,
370
+ dh,
371
+ zoom,
372
+ px,
373
+ py,
374
+ layers,
375
+ afi,
376
+ aLi,
377
+ grid,
378
+ onion,
379
+ tool,
380
+ col1,
381
+ col2,
382
+ sel,
383
+ getCel,
384
+ symmetry,
385
+ shapeFilled,
386
+ brush,
387
+ cursor
388
+ ]);
389
+ const applyBrushAt = React.useCallback(
390
+ (x, y, erase, pr) => {
391
+ const cel = getCel(afi, aLi);
392
+ const clr = erase ? [0, 0, 0, 0] : pixelEditorTypes.hex2t(btn.current === 2 ? col2 : col1);
393
+ const pts = pixelEditorUtils.getSymmetryPoints(x, y, dw, dh, symmetry);
394
+ for (const pt of pts) {
395
+ if (tool === "dither") {
396
+ pixelEditorUtils.applyDitherStamp(cel, pt.x, pt.y, clr, brush.size, pr);
397
+ } else {
398
+ pixelEditorUtils.applyBrushStamp(cel, pt.x, pt.y, clr, brush, pr, erase);
564
399
  }
565
- drawing.current = false;
566
- const p = scr2art(e.clientX, e.clientY);
567
- const clr = btn.current === 2 ? pixelEditorTypes.hex2t(col2) : pixelEditorTypes.hex2t(col1);
568
- if (tool === 'line' && tStart.current) {
400
+ }
401
+ },
402
+ [getCel, afi, aLi, col1, col2, brush, dw, dh, symmetry, tool]
403
+ );
404
+ const onDown = React.useCallback(
405
+ (e) => {
406
+ e.preventDefault();
407
+ e.target.setPointerCapture?.(e.pointerId);
408
+ btn.current = e.button;
409
+ const pr = e.pressure > 0 ? e.pressure : 0.5;
410
+ curPressure.current = pr;
411
+ setPressure(pr);
412
+ if (e.button === 1) {
413
+ panning.current = true;
414
+ panSt.current = { mx: e.clientX, my: e.clientY, px, py };
415
+ return;
416
+ }
417
+ const p = scr2art(e.clientX, e.clientY);
418
+ setCursor(p);
419
+ if (layers[aLi]?.locked) {
420
+ return;
421
+ }
422
+ switch (tool) {
423
+ case "pencil":
424
+ case "eraser":
425
+ case "dither":
426
+ snap(tool === "pencil" ? "Draw" : tool === "eraser" ? "Erase" : "Dither");
427
+ drawing.current = true;
428
+ lastPx.current = p;
429
+ brushDist.current = 0;
430
+ applyBrushAt(p.x, p.y, tool === "eraser", pr);
431
+ setTick((t) => t + 1);
432
+ break;
433
+ case "fill":
434
+ if (pixelEditorTypes.inB(p.x, p.y, dw, dh)) {
435
+ snap("Fill");
436
+ const clr = pixelEditorTypes.hex2t(e.button === 2 ? col2 : col1);
437
+ const pts = pixelEditorUtils.getSymmetryPoints(p.x, p.y, dw, dh, symmetry);
569
438
  const cel = getCel(afi, aLi);
570
- pixelEditorUtils.getSymmetryPoints(0, 0, dw, dh, symmetry);
571
- if (symmetry === 'none') {
572
- pixelEditorUtils.bresenhamLine(tStart.current.x, tStart.current.y, p.x, p.y, (bx, by) => {
573
- if (pixelEditorTypes.inB(bx, by, dw, dh)) {
574
- pixelEditorUtils.setPixel(cel, bx, by, clr);
575
- }
576
- });
439
+ for (const pt of pts) {
440
+ if (pixelEditorTypes.inB(pt.x, pt.y, dw, dh)) {
441
+ pixelEditorUtils.floodFill(cel, pt.x, pt.y, clr);
442
+ }
577
443
  }
578
- else {
579
- const fromPts = pixelEditorUtils.getSymmetryPoints(tStart.current.x, tStart.current.y, dw, dh, symmetry);
580
- const toPts = pixelEditorUtils.getSymmetryPoints(p.x, p.y, dw, dh, symmetry);
581
- for (let i = 0; i < fromPts.length; i++) {
582
- pixelEditorUtils.bresenhamLine(fromPts[i].x, fromPts[i].y, toPts[i].x, toPts[i].y, (bx, by) => {
583
- if (pixelEditorTypes.inB(bx, by, dw, dh)) {
584
- pixelEditorUtils.setPixel(cel, bx, by, clr);
585
- }
586
- });
587
- }
588
- }
589
- preview.current = null;
590
444
  setTick((t) => t + 1);
591
- }
592
- else if (tool === 'rect' && tStart.current) {
593
- const cel = getCel(afi, aLi);
594
- const x1 = Math.min(tStart.current.x, p.x), y1 = Math.min(tStart.current.y, p.y);
595
- const x2 = Math.max(tStart.current.x, p.x), y2 = Math.max(tStart.current.y, p.y);
596
- if (shapeFilled) {
597
- for (let y = y1; y <= y2; y++) {
598
- for (let x = x1; x <= x2; x++) {
599
- if (pixelEditorTypes.inB(x, y, dw, dh)) {
600
- pixelEditorUtils.setPixel(cel, x, y, clr);
601
- }
602
- }
603
- }
445
+ }
446
+ break;
447
+ case "eyedropper": {
448
+ if (pixelEditorTypes.inB(p.x, p.y, dw, dh)) {
449
+ const c = pixelEditorUtils.getPixel(getCel(afi, aLi), p.x, p.y);
450
+ const hex = pixelEditorUtils.rgbaToHex(c);
451
+ if (e.button === 2) {
452
+ setCol2(hex);
453
+ } else {
454
+ setCol1(hex);
604
455
  }
605
- else {
606
- for (let x = x1; x <= x2; x++) {
607
- if (pixelEditorTypes.inB(x, y1, dw, dh)) {
608
- pixelEditorUtils.setPixel(cel, x, y1, clr);
609
- }
610
- if (pixelEditorTypes.inB(x, y2, dw, dh)) {
611
- pixelEditorUtils.setPixel(cel, x, y2, clr);
612
- }
613
- }
614
- for (let y = y1 + 1; y < y2; y++) {
615
- if (pixelEditorTypes.inB(x1, y, dw, dh)) {
616
- pixelEditorUtils.setPixel(cel, x1, y, clr);
617
- }
618
- if (pixelEditorTypes.inB(x2, y, dw, dh)) {
619
- pixelEditorUtils.setPixel(cel, x2, y, clr);
620
- }
621
- }
622
- }
623
- preview.current = null;
624
- setTick((t) => t + 1);
625
- }
626
- else if (tool === 'ellipse' && tStart.current) {
627
- const cel = getCel(afi, aLi);
628
- const cx2 = (tStart.current.x + p.x) / 2;
629
- const cy2 = (tStart.current.y + p.y) / 2;
630
- const rx = Math.abs(p.x - tStart.current.x) / 2;
631
- const ry = Math.abs(p.y - tStart.current.y) / 2;
632
- pixelEditorTypes.plotEllipse(cel, cx2, cy2, rx, ry, clr, dw, dh, shapeFilled);
633
- preview.current = null;
634
- setTick((t) => t + 1);
635
- }
636
- else if (tool === 'gradient' && tStart.current) {
637
- const cel = getCel(afi, aLi);
638
- const c2 = btn.current === 2 ? pixelEditorTypes.hex2t(col1) : pixelEditorTypes.hex2t(col2);
639
- pixelEditorUtils.drawGradient(cel, tStart.current.x, tStart.current.y, p.x, p.y, clr, c2, sel);
640
- preview.current = null;
641
- setTick((t) => t + 1);
642
- }
643
- lastPx.current = null;
644
- tStart.current = null;
645
- }, [tool, scr2art, getCel, afi, aLi, col1, col2, dw, dh, shapeFilled, symmetry, sel]);
646
- const onWheel = React.useCallback((e) => {
647
- setZoom((z) => {
648
- const n = e.deltaY < 0 ? z * 1.2 : z / 1.2;
649
- return Math.max(0.5, Math.min(64, n));
650
- });
651
- }, []);
652
- React.useEffect(() => {
653
- const el = cvRef.current;
654
- if (!el) {
655
- return;
456
+ }
457
+ break;
656
458
  }
657
- const handler = (e) => e.preventDefault();
658
- el.addEventListener('wheel', handler, { passive: false });
659
- return () => el.removeEventListener('wheel', handler);
660
- }, []);
661
- /* ── clipboard operations ── */
662
- const doCopy = React.useCallback(() => {
663
- if (!sel) {
664
- return;
459
+ case "line":
460
+ case "rect":
461
+ case "ellipse":
462
+ case "gradient":
463
+ snap(tool[0].toUpperCase() + tool.slice(1));
464
+ drawing.current = true;
465
+ tStart.current = p;
466
+ preview.current = { fr: p, to: p };
467
+ break;
468
+ case "select":
469
+ drawing.current = true;
470
+ tStart.current = p;
471
+ break;
472
+ case "move":
473
+ drawing.current = true;
474
+ lastPx.current = p;
475
+ break;
476
+ }
477
+ },
478
+ [
479
+ tool,
480
+ aLi,
481
+ layers,
482
+ px,
483
+ py,
484
+ scr2art,
485
+ snap,
486
+ applyBrushAt,
487
+ getCel,
488
+ afi,
489
+ col1,
490
+ col2,
491
+ dw,
492
+ dh,
493
+ symmetry
494
+ ]
495
+ );
496
+ const onMove = React.useCallback(
497
+ (e) => {
498
+ const pr = e.pressure > 0 ? e.pressure : 0.5;
499
+ curPressure.current = pr;
500
+ setPressure(pr);
501
+ if (panning.current) {
502
+ setPx(panSt.current.px + (e.clientX - panSt.current.mx));
503
+ setPy(panSt.current.py + (e.clientY - panSt.current.my));
504
+ return;
505
+ }
506
+ const p = scr2art(e.clientX, e.clientY);
507
+ setCursor(p);
508
+ if (!drawing.current) {
509
+ return;
510
+ }
511
+ if (tool === "pencil" || tool === "eraser" || tool === "dither") {
512
+ const last = lastPx.current || p;
513
+ const spacing = Math.max(1, brush.size * (brush.spacing / 100));
514
+ const ddx = p.x - last.x, ddy = p.y - last.y;
515
+ const dist = Math.sqrt(ddx * ddx + ddy * ddy);
516
+ if (dist < 0.5) {
517
+ return;
665
518
  }
666
- clipboard.current = pixelEditorUtils.copySelection(getCel(afi, aLi), sel);
667
- }, [sel, getCel, afi, aLi]);
668
- const doCut = React.useCallback(() => {
669
- if (!sel) {
670
- return;
671
- }
672
- snap('Cut');
673
- const cel = getCel(afi, aLi);
674
- clipboard.current = pixelEditorUtils.copySelection(cel, sel);
675
- pixelEditorUtils.clearSelection(cel, sel);
676
- setTick((t) => t + 1);
677
- }, [sel, getCel, afi, aLi, snap]);
678
- const doPaste = React.useCallback(() => {
679
- var _a, _b;
680
- if (!clipboard.current) {
681
- return;
519
+ brushDist.current += dist;
520
+ if (brushDist.current >= spacing) {
521
+ pixelEditorUtils.bresenhamLine(last.x, last.y, p.x, p.y, (bx, by) => {
522
+ applyBrushAt(bx, by, tool === "eraser", pr);
523
+ });
524
+ brushDist.current = 0;
682
525
  }
683
- snap('Paste');
684
- const cel = getCel(afi, aLi);
685
- pixelEditorUtils.pasteAt(cel, clipboard.current, (_a = sel === null || sel === void 0 ? void 0 : sel.x) !== null && _a !== void 0 ? _a : 0, (_b = sel === null || sel === void 0 ? void 0 : sel.y) !== null && _b !== void 0 ? _b : 0);
526
+ lastPx.current = p;
686
527
  setTick((t) => t + 1);
687
- }, [getCel, afi, aLi, snap, sel]);
688
- const doDeleteSel = React.useCallback(() => {
689
- if (!sel) {
690
- return;
691
- }
692
- snap('Delete Selection');
693
- pixelEditorUtils.clearSelection(getCel(afi, aLi), sel);
528
+ } else if (tool === "line" || tool === "rect" || tool === "ellipse" || tool === "gradient") {
529
+ preview.current = { fr: tStart.current, to: p };
694
530
  setTick((t) => t + 1);
695
- }, [sel, getCel, afi, aLi, snap]);
696
- const doSelectAll = React.useCallback(() => {
697
- setSel({ x: 0, y: 0, w: dw, h: dh });
698
- }, [dw, dh]);
699
- const doDeselectAll = React.useCallback(() => {
700
- setSel(null);
701
- }, []);
702
- /* ── file I/O ── */
703
- const loadAseDoc = React.useCallback((doc) => {
704
- setDw(doc.width);
705
- setDh(doc.height);
706
- cels.current = new Map();
707
- const lrs = doc.layers.map((l, i) => ({
708
- id: pixelEditorTypes.lid(),
709
- name: l.name || `Layer ${i + 1}`,
710
- visible: l.visible,
711
- opacity: l.opacity,
712
- locked: false,
713
- blendMode: 'normal',
714
- }));
715
- setLayers(lrs.length
716
- ? lrs
717
- : [
718
- {
719
- id: pixelEditorTypes.lid(),
720
- name: 'Layer 1',
721
- visible: true,
722
- opacity: 255,
723
- locked: false,
724
- blendMode: 'normal',
725
- },
726
- ]);
727
- setALi(0);
728
- setFc(doc.frames.length || 1);
729
- setAfi(0);
730
- setFDur(doc.frames.map((f) => f.duration || 100));
731
- doc.frames.forEach((frame, fi) => {
732
- frame.cels.forEach((cel) => {
733
- const data = pixelEditorUtils.createBlankImageData(doc.width, doc.height);
734
- if (cel.pixels && cel.width > 0 && cel.height > 0) {
735
- for (let y = 0; y < cel.height; y++) {
736
- for (let x = 0; x < cel.width; x++) {
737
- const dx = cel.x + x, dy = cel.y + y;
738
- if (dx >= 0 && dy >= 0 && dx < doc.width && dy < doc.height) {
739
- const si = (y * cel.width + x) * 4;
740
- pixelEditorUtils.setPixel(data, dx, dy, [
741
- cel.pixels[si],
742
- cel.pixels[si + 1],
743
- cel.pixels[si + 2],
744
- cel.pixels[si + 3],
745
- ]);
746
- }
747
- }
748
- }
749
- }
750
- cels.current.set(pixelEditorTypes.celKey(fi, cel.layerIndex), data);
751
- });
531
+ } else if (tool === "select") {
532
+ const s = tStart.current;
533
+ setSel({
534
+ x: Math.min(s.x, p.x),
535
+ y: Math.min(s.y, p.y),
536
+ w: Math.abs(p.x - s.x) + 1,
537
+ h: Math.abs(p.y - s.y) + 1
752
538
  });
753
- if (doc.palette.length) {
754
- setPal(doc.palette.map(asepriteFormat.aseColorToHex));
539
+ } else if (tool === "move" && sel) {
540
+ const last2 = lastPx.current || p;
541
+ const ddx = p.x - last2.x, ddy = p.y - last2.y;
542
+ if (ddx !== 0 || ddy !== 0) {
543
+ setSel((prev) => prev ? { ...prev, x: prev.x + ddx, y: prev.y + ddy } : null);
755
544
  }
756
- setZoom(Math.max(1, Math.floor(Math.min(512 / doc.width, 512 / doc.height))));
757
- setPx(0);
758
- setPy(0);
759
- hist.current = [];
760
- hIdx.current = -1;
761
- setTick((t) => t + 1);
762
- }, []);
763
- const openFile = React.useCallback(async (file) => {
764
- if (file.name.match(/\.ase(prite)?$/i)) {
765
- try {
766
- const doc = await asepriteFormat.parseAseFile(await file.arrayBuffer());
767
- loadAseDoc(doc);
768
- }
769
- catch (err) {
770
- log.error('ASE parse error', err);
771
- console.warn('[PixelEditor] Failed to parse Aseprite file.');
545
+ lastPx.current = p;
546
+ }
547
+ },
548
+ [tool, scr2art, applyBrushAt, brush, sel]
549
+ );
550
+ const onUp = React.useCallback(
551
+ (e) => {
552
+ if (panning.current) {
553
+ panning.current = false;
554
+ return;
555
+ }
556
+ if (!drawing.current) {
557
+ return;
558
+ }
559
+ drawing.current = false;
560
+ const p = scr2art(e.clientX, e.clientY);
561
+ const clr = btn.current === 2 ? pixelEditorTypes.hex2t(col2) : pixelEditorTypes.hex2t(col1);
562
+ if (tool === "line" && tStart.current) {
563
+ const cel = getCel(afi, aLi);
564
+ pixelEditorUtils.getSymmetryPoints(0, 0, dw, dh, symmetry);
565
+ if (symmetry === "none") {
566
+ pixelEditorUtils.bresenhamLine(tStart.current.x, tStart.current.y, p.x, p.y, (bx, by) => {
567
+ if (pixelEditorTypes.inB(bx, by, dw, dh)) {
568
+ pixelEditorUtils.setPixel(cel, bx, by, clr);
772
569
  }
570
+ });
571
+ } else {
572
+ const fromPts = pixelEditorUtils.getSymmetryPoints(tStart.current.x, tStart.current.y, dw, dh, symmetry);
573
+ const toPts = pixelEditorUtils.getSymmetryPoints(p.x, p.y, dw, dh, symmetry);
574
+ for (let i = 0; i < fromPts.length; i++) {
575
+ pixelEditorUtils.bresenhamLine(fromPts[i].x, fromPts[i].y, toPts[i].x, toPts[i].y, (bx, by) => {
576
+ if (pixelEditorTypes.inB(bx, by, dw, dh)) {
577
+ pixelEditorUtils.setPixel(cel, bx, by, clr);
578
+ }
579
+ });
580
+ }
773
581
  }
774
- else {
775
- const img = new Image();
776
- img.onload = () => {
777
- const w2 = img.naturalWidth, h2 = img.naturalHeight;
778
- const cv = new OffscreenCanvas(w2, h2);
779
- cv.getContext('2d').drawImage(img, 0, 0);
780
- const data = cv.getContext('2d').getImageData(0, 0, w2, h2);
781
- setDw(w2);
782
- setDh(h2);
783
- cels.current = new Map([[pixelEditorTypes.celKey(0, 0), data]]);
784
- setLayers([
785
- {
786
- id: pixelEditorTypes.lid(),
787
- name: 'Layer 1',
788
- visible: true,
789
- opacity: 255,
790
- locked: false,
791
- blendMode: 'normal',
792
- },
793
- ]);
794
- setALi(0);
795
- setFc(1);
796
- setAfi(0);
797
- setFDur([100]);
798
- setZoom(Math.max(1, Math.floor(Math.min(512 / w2, 512 / h2))));
799
- setPx(0);
800
- setPy(0);
801
- hist.current = [];
802
- hIdx.current = -1;
803
- setTick((t) => t + 1);
804
- };
805
- img.src = URL.createObjectURL(file);
806
- }
807
- }, [loadAseDoc]);
808
- const exportAse = React.useCallback(async () => {
809
- await pixelEditorExports.exportAse({ dw, dh, layers, fc, fDur, pal, cels: cels.current });
810
- }, [dw, dh, layers, fc, fDur, pal]);
811
- const exportPNG = React.useCallback(() => {
812
- pixelEditorExports.exportPNG(afi, dw, dh, compFrame);
813
- }, [afi, dw, dh, compFrame]);
814
- const exportJPG = React.useCallback(() => {
815
- pixelEditorExports.exportJPG(afi, dw, dh, compFrame);
816
- }, [afi, dw, dh, compFrame]);
817
- const exportWebP = React.useCallback(() => {
818
- pixelEditorExports.exportWebP(afi, dw, dh, compFrame);
819
- }, [afi, dw, dh, compFrame]);
820
- const exportBMP = React.useCallback(() => {
821
- pixelEditorExports.exportBMP(afi, dw, dh, compFrame);
822
- }, [afi, dw, dh, compFrame]);
823
- const exportSVG = React.useCallback(() => {
824
- pixelEditorExports.exportSVG(afi, dw, dh, compFrame);
825
- }, [afi, dw, dh, compFrame]);
826
- const exportSheet = React.useCallback(() => {
827
- pixelEditorExports.exportSheet(dw, dh, fc, compFrame);
828
- }, [dw, dh, fc, compFrame]);
829
- /* ── save to library ── */
830
- const saveToLibrary = React.useCallback(() => {
831
- if (!onSaveToLibrary) {
832
- return;
833
- }
834
- const data = compFrame(afi);
835
- const cv = document.createElement('canvas');
836
- cv.width = dw;
837
- cv.height = dh;
838
- cv.getContext('2d').putImageData(data, 0, 0);
839
- const dataUrl = cv.toDataURL('image/png');
840
- void ask('prompt', 'Save to library as:', `sprite-${dw}x${dh}.png`).then((name) => {
841
- if (name && name.trim()) {
842
- onSaveToLibrary(dataUrl, name.trim(), 'image/png');
843
- }
844
- });
845
- }, [onSaveToLibrary, afi, dw, dh, compFrame, ask]);
846
- /* ── layer management ── */
847
- const addLayer = React.useCallback(() => {
848
- snap('Add Layer');
849
- setLayers((ls) => [
850
- ...ls,
851
- {
852
- id: pixelEditorTypes.lid(),
853
- name: `Layer ${ls.length + 1}`,
854
- visible: true,
855
- opacity: 255,
856
- locked: false,
857
- blendMode: 'normal',
858
- },
859
- ]);
860
- setALi(layers.length);
582
+ preview.current = null;
861
583
  setTick((t) => t + 1);
862
- }, [layers, snap]);
863
- const rmLayer = React.useCallback(() => {
864
- if (layers.length <= 1) {
865
- return;
866
- }
867
- snap('Remove Layer');
868
- const idx = aLi;
869
- for (let fi = 0; fi < fc; fi++) {
870
- cels.current.delete(pixelEditorTypes.celKey(fi, idx));
871
- for (let li = idx + 1; li < layers.length; li++) {
872
- const c = cels.current.get(pixelEditorTypes.celKey(fi, li));
873
- if (c) {
874
- cels.current.set(pixelEditorTypes.celKey(fi, li - 1), c);
875
- cels.current.delete(pixelEditorTypes.celKey(fi, li));
876
- }
584
+ } else if (tool === "rect" && tStart.current) {
585
+ const cel = getCel(afi, aLi);
586
+ const x1 = Math.min(tStart.current.x, p.x), y1 = Math.min(tStart.current.y, p.y);
587
+ const x2 = Math.max(tStart.current.x, p.x), y2 = Math.max(tStart.current.y, p.y);
588
+ if (shapeFilled) {
589
+ for (let y = y1; y <= y2; y++) {
590
+ for (let x = x1; x <= x2; x++) {
591
+ if (pixelEditorTypes.inB(x, y, dw, dh)) {
592
+ pixelEditorUtils.setPixel(cel, x, y, clr);
593
+ }
877
594
  }
878
- }
879
- setLayers((ls) => ls.filter((_, i) => i !== idx));
880
- setALi(Math.min(idx, layers.length - 2));
881
- setTick((t) => t + 1);
882
- }, [layers, aLi, fc, snap]);
883
- const toggleVis = React.useCallback((i) => {
884
- setLayers((ls) => ls.map((l, j) => (j === i ? { ...l, visible: !l.visible } : l)));
885
- setTick((t) => t + 1);
886
- }, []);
887
- const toggleLock = React.useCallback((i) => {
888
- setLayers((ls) => ls.map((l, j) => (j === i ? { ...l, locked: !l.locked } : l)));
889
- }, []);
890
- const setLayerOpacity = React.useCallback((i, op) => {
891
- setLayers((ls) => ls.map((l, j) => (j === i ? { ...l, opacity: op } : l)));
892
- setTick((t) => t + 1);
893
- }, []);
894
- const setLayerBlendMode = React.useCallback((i, mode) => {
895
- setLayers((ls) => ls.map((l, j) => (j === i ? { ...l, blendMode: mode } : l)));
896
- setTick((t) => t + 1);
897
- }, []);
898
- const renameLayer = React.useCallback((i, name) => {
899
- setLayers((ls) => ls.map((l, j) => (j === i ? { ...l, name } : l)));
900
- }, []);
901
- const moveLayerUp = React.useCallback(() => {
902
- if (aLi >= layers.length - 1) {
903
- return;
904
- }
905
- snap('Move Layer Up');
906
- setLayers((ls) => {
907
- const n = [...ls];
908
- [n[aLi], n[aLi + 1]] = [n[aLi + 1], n[aLi]];
909
- return n;
910
- });
911
- for (let fi = 0; fi < fc; fi++) {
912
- const a = cels.current.get(pixelEditorTypes.celKey(fi, aLi));
913
- const b = cels.current.get(pixelEditorTypes.celKey(fi, aLi + 1));
914
- if (a) {
915
- cels.current.set(pixelEditorTypes.celKey(fi, aLi + 1), a);
595
+ }
596
+ } else {
597
+ for (let x = x1; x <= x2; x++) {
598
+ if (pixelEditorTypes.inB(x, y1, dw, dh)) {
599
+ pixelEditorUtils.setPixel(cel, x, y1, clr);
916
600
  }
917
- else {
918
- cels.current.delete(pixelEditorTypes.celKey(fi, aLi + 1));
601
+ if (pixelEditorTypes.inB(x, y2, dw, dh)) {
602
+ pixelEditorUtils.setPixel(cel, x, y2, clr);
919
603
  }
920
- if (b) {
921
- cels.current.set(pixelEditorTypes.celKey(fi, aLi), b);
604
+ }
605
+ for (let y = y1 + 1; y < y2; y++) {
606
+ if (pixelEditorTypes.inB(x1, y, dw, dh)) {
607
+ pixelEditorUtils.setPixel(cel, x1, y, clr);
922
608
  }
923
- else {
924
- cels.current.delete(pixelEditorTypes.celKey(fi, aLi));
609
+ if (pixelEditorTypes.inB(x2, y, dw, dh)) {
610
+ pixelEditorUtils.setPixel(cel, x2, y, clr);
925
611
  }
612
+ }
926
613
  }
927
- setALi(aLi + 1);
614
+ preview.current = null;
928
615
  setTick((t) => t + 1);
929
- }, [aLi, layers, fc, snap]);
930
- const moveLayerDown = React.useCallback(() => {
931
- if (aLi <= 0) {
932
- return;
933
- }
934
- snap('Move Layer Down');
935
- setLayers((ls) => {
936
- const n = [...ls];
937
- [n[aLi], n[aLi - 1]] = [n[aLi - 1], n[aLi]];
938
- return n;
939
- });
940
- for (let fi = 0; fi < fc; fi++) {
941
- const a = cels.current.get(pixelEditorTypes.celKey(fi, aLi));
942
- const b = cels.current.get(pixelEditorTypes.celKey(fi, aLi - 1));
943
- if (a) {
944
- cels.current.set(pixelEditorTypes.celKey(fi, aLi - 1), a);
945
- }
946
- else {
947
- cels.current.delete(pixelEditorTypes.celKey(fi, aLi - 1));
948
- }
949
- if (b) {
950
- cels.current.set(pixelEditorTypes.celKey(fi, aLi), b);
951
- }
952
- else {
953
- cels.current.delete(pixelEditorTypes.celKey(fi, aLi));
954
- }
955
- }
956
- setALi(aLi - 1);
616
+ } else if (tool === "ellipse" && tStart.current) {
617
+ const cel = getCel(afi, aLi);
618
+ const cx2 = (tStart.current.x + p.x) / 2;
619
+ const cy2 = (tStart.current.y + p.y) / 2;
620
+ const rx = Math.abs(p.x - tStart.current.x) / 2;
621
+ const ry = Math.abs(p.y - tStart.current.y) / 2;
622
+ pixelEditorTypes.plotEllipse(cel, cx2, cy2, rx, ry, clr, dw, dh, shapeFilled);
623
+ preview.current = null;
957
624
  setTick((t) => t + 1);
958
- }, [aLi, fc, snap]);
959
- const mergeDown = React.useCallback(() => {
960
- if (aLi <= 0) {
961
- return;
962
- }
963
- snap('Merge Down');
964
- for (let fi = 0; fi < fc; fi++) {
965
- const top = cels.current.get(pixelEditorTypes.celKey(fi, aLi));
966
- const bot = getCel(fi, aLi - 1);
967
- if (top) {
968
- const cv = new OffscreenCanvas(dw, dh);
969
- const ctx = cv.getContext('2d');
970
- ctx.putImageData(bot, 0, 0);
971
- const t = new OffscreenCanvas(dw, dh);
972
- t.getContext('2d').putImageData(top, 0, 0);
973
- ctx.globalAlpha = layers[aLi].opacity / 255;
974
- ctx.drawImage(t, 0, 0);
975
- cels.current.set(pixelEditorTypes.celKey(fi, aLi - 1), ctx.getImageData(0, 0, dw, dh));
976
- }
977
- }
978
- for (let fi = 0; fi < fc; fi++) {
979
- cels.current.delete(pixelEditorTypes.celKey(fi, aLi));
980
- for (let li = aLi + 1; li < layers.length; li++) {
981
- const c = cels.current.get(pixelEditorTypes.celKey(fi, li));
982
- if (c) {
983
- cels.current.set(pixelEditorTypes.celKey(fi, li - 1), c);
984
- cels.current.delete(pixelEditorTypes.celKey(fi, li));
985
- }
986
- }
987
- }
988
- setLayers((ls) => ls.filter((_, i) => i !== aLi));
989
- setALi(aLi - 1);
625
+ } else if (tool === "gradient" && tStart.current) {
626
+ const cel = getCel(afi, aLi);
627
+ const c2 = btn.current === 2 ? pixelEditorTypes.hex2t(col1) : pixelEditorTypes.hex2t(col2);
628
+ pixelEditorUtils.drawGradient(cel, tStart.current.x, tStart.current.y, p.x, p.y, clr, c2, sel);
629
+ preview.current = null;
990
630
  setTick((t) => t + 1);
991
- }, [aLi, fc, layers, getCel, dw, dh, snap]);
992
- const dupLayer = React.useCallback(() => {
993
- snap('Duplicate Layer');
994
- const newL = { ...layers[aLi], id: pixelEditorTypes.lid(), name: layers[aLi].name + ' copy' };
995
- setLayers((ls) => [...ls.slice(0, aLi + 1), newL, ...ls.slice(aLi + 1)]);
996
- for (let fi = 0; fi < fc; fi++) {
997
- for (let li = layers.length; li > aLi; li--) {
998
- const c = cels.current.get(pixelEditorTypes.celKey(fi, li));
999
- if (c) {
1000
- cels.current.set(pixelEditorTypes.celKey(fi, li + 1), c);
1001
- cels.current.delete(pixelEditorTypes.celKey(fi, li));
1002
- }
1003
- }
1004
- const src = cels.current.get(pixelEditorTypes.celKey(fi, aLi));
1005
- if (src) {
1006
- cels.current.set(pixelEditorTypes.celKey(fi, aLi + 1), pixelEditorUtils.cloneImageData(src));
1007
- }
631
+ }
632
+ lastPx.current = null;
633
+ tStart.current = null;
634
+ },
635
+ [tool, scr2art, getCel, afi, aLi, col1, col2, dw, dh, shapeFilled, symmetry, sel]
636
+ );
637
+ const onWheel = React.useCallback((e) => {
638
+ setZoom((z) => {
639
+ const n = e.deltaY < 0 ? z * 1.2 : z / 1.2;
640
+ return Math.max(0.5, Math.min(64, n));
641
+ });
642
+ }, []);
643
+ React.useEffect(() => {
644
+ const el = cvRef.current;
645
+ if (!el) {
646
+ return;
647
+ }
648
+ const handler = (e) => e.preventDefault();
649
+ el.addEventListener("wheel", handler, { passive: false });
650
+ return () => el.removeEventListener("wheel", handler);
651
+ }, []);
652
+ const doCopy = React.useCallback(() => {
653
+ if (!sel) {
654
+ return;
655
+ }
656
+ clipboard.current = pixelEditorUtils.copySelection(getCel(afi, aLi), sel);
657
+ }, [sel, getCel, afi, aLi]);
658
+ const doCut = React.useCallback(() => {
659
+ if (!sel) {
660
+ return;
661
+ }
662
+ snap("Cut");
663
+ const cel = getCel(afi, aLi);
664
+ clipboard.current = pixelEditorUtils.copySelection(cel, sel);
665
+ pixelEditorUtils.clearSelection(cel, sel);
666
+ setTick((t) => t + 1);
667
+ }, [sel, getCel, afi, aLi, snap]);
668
+ const doPaste = React.useCallback(() => {
669
+ if (!clipboard.current) {
670
+ return;
671
+ }
672
+ snap("Paste");
673
+ const cel = getCel(afi, aLi);
674
+ pixelEditorUtils.pasteAt(cel, clipboard.current, sel?.x ?? 0, sel?.y ?? 0);
675
+ setTick((t) => t + 1);
676
+ }, [getCel, afi, aLi, snap, sel]);
677
+ const doDeleteSel = React.useCallback(() => {
678
+ if (!sel) {
679
+ return;
680
+ }
681
+ snap("Delete Selection");
682
+ pixelEditorUtils.clearSelection(getCel(afi, aLi), sel);
683
+ setTick((t) => t + 1);
684
+ }, [sel, getCel, afi, aLi, snap]);
685
+ const doSelectAll = React.useCallback(() => {
686
+ setSel({ x: 0, y: 0, w: dw, h: dh });
687
+ }, [dw, dh]);
688
+ const doDeselectAll = React.useCallback(() => {
689
+ setSel(null);
690
+ }, []);
691
+ const loadAseDoc = React.useCallback((doc) => {
692
+ setDw(doc.width);
693
+ setDh(doc.height);
694
+ cels.current = /* @__PURE__ */ new Map();
695
+ const lrs = doc.layers.map((l, i) => ({
696
+ id: pixelEditorTypes.lid(),
697
+ name: l.name || `Layer ${i + 1}`,
698
+ visible: l.visible,
699
+ opacity: l.opacity,
700
+ locked: false,
701
+ blendMode: "normal"
702
+ }));
703
+ setLayers(
704
+ lrs.length ? lrs : [
705
+ {
706
+ id: pixelEditorTypes.lid(),
707
+ name: "Layer 1",
708
+ visible: true,
709
+ opacity: 255,
710
+ locked: false,
711
+ blendMode: "normal"
1008
712
  }
1009
- setALi(aLi + 1);
1010
- setTick((t) => t + 1);
1011
- }, [aLi, layers, fc, snap]);
1012
- /* ── frame management ── */
1013
- const addFrame = React.useCallback(() => {
1014
- snap('Add Frame');
1015
- setFc((n) => n + 1);
1016
- setFDur((d) => [...d, 100]);
1017
- setAfi(fc);
1018
- setTick((t) => t + 1);
1019
- }, [fc, snap]);
1020
- const dupFrame = React.useCallback(() => {
1021
- snap('Dup Frame');
1022
- const ni = fc;
1023
- for (let li = 0; li < layers.length; li++) {
1024
- const c = cels.current.get(pixelEditorTypes.celKey(afi, li));
1025
- if (c) {
1026
- cels.current.set(pixelEditorTypes.celKey(ni, li), pixelEditorUtils.cloneImageData(c));
713
+ ]
714
+ );
715
+ setALi(0);
716
+ setFc(doc.frames.length || 1);
717
+ setAfi(0);
718
+ setFDur(doc.frames.map((f) => f.duration || 100));
719
+ doc.frames.forEach((frame, fi) => {
720
+ frame.cels.forEach((cel) => {
721
+ const data = pixelEditorUtils.createBlankImageData(doc.width, doc.height);
722
+ if (cel.pixels && cel.width > 0 && cel.height > 0) {
723
+ for (let y = 0; y < cel.height; y++) {
724
+ for (let x = 0; x < cel.width; x++) {
725
+ const dx = cel.x + x, dy = cel.y + y;
726
+ if (dx >= 0 && dy >= 0 && dx < doc.width && dy < doc.height) {
727
+ const si = (y * cel.width + x) * 4;
728
+ pixelEditorUtils.setPixel(data, dx, dy, [
729
+ cel.pixels[si],
730
+ cel.pixels[si + 1],
731
+ cel.pixels[si + 2],
732
+ cel.pixels[si + 3]
733
+ ]);
734
+ }
1027
735
  }
736
+ }
1028
737
  }
1029
- setFc((n) => n + 1);
1030
- setFDur((d) => [...d, fDur[afi] || 100]);
1031
- setAfi(ni);
1032
- setTick((t) => t + 1);
1033
- }, [fc, afi, layers, fDur, snap]);
1034
- const rmFrame = React.useCallback(() => {
1035
- if (fc <= 1) {
1036
- return;
1037
- }
1038
- snap('Remove Frame');
1039
- for (let li = 0; li < layers.length; li++) {
1040
- cels.current.delete(pixelEditorTypes.celKey(afi, li));
1041
- for (let f = afi + 1; f < fc; f++) {
1042
- const c = cels.current.get(pixelEditorTypes.celKey(f, li));
1043
- if (c) {
1044
- cels.current.set(pixelEditorTypes.celKey(f - 1, li), c);
1045
- cels.current.delete(pixelEditorTypes.celKey(f, li));
1046
- }
1047
- }
738
+ cels.current.set(pixelEditorTypes.celKey(fi, cel.layerIndex), data);
739
+ });
740
+ });
741
+ if (doc.palette.length) {
742
+ setPal(doc.palette.map(asepriteFormat.aseColorToHex));
743
+ }
744
+ setZoom(Math.max(1, Math.floor(Math.min(512 / doc.width, 512 / doc.height))));
745
+ setPx(0);
746
+ setPy(0);
747
+ hist.current = [];
748
+ hIdx.current = -1;
749
+ setTick((t) => t + 1);
750
+ }, []);
751
+ const openFile = React.useCallback(
752
+ async (file) => {
753
+ if (file.name.match(/\.ase(prite)?$/i)) {
754
+ try {
755
+ const doc = await asepriteFormat.parseAseFile(await file.arrayBuffer());
756
+ loadAseDoc(doc);
757
+ } catch (err) {
758
+ log.error("ASE parse error", err);
759
+ console.warn("[PixelEditor] Failed to parse Aseprite file.");
1048
760
  }
1049
- setFc((n) => n - 1);
1050
- setFDur((d) => d.filter((_, i) => i !== afi));
1051
- setAfi(Math.min(afi, fc - 2));
1052
- setTick((t) => t + 1);
1053
- }, [fc, afi, layers, snap]);
1054
- /* ── canvas ops ── */
1055
- const flipCv = React.useCallback((dir) => {
1056
- snap(`Flip ${dir === 'h' ? 'H' : 'V'}`);
1057
- cels.current.forEach((c, k) => cels.current.set(k, dir === 'h' ? pixelEditorUtils.flipH(c) : pixelEditorUtils.flipV(c)));
1058
- setTick((t) => t + 1);
1059
- }, [snap]);
1060
- const rotateCv = React.useCallback(() => {
1061
- snap('Rotate 90°');
1062
- const n = new Map();
1063
- cels.current.forEach((c, k) => n.set(k, pixelEditorUtils.rotateCW(c)));
1064
- cels.current = n;
1065
- setDw(dh);
1066
- setDh(dw);
1067
- setTick((t) => t + 1);
1068
- }, [dw, dh, snap]);
1069
- const clearCv = React.useCallback(() => {
1070
- snap('Clear');
1071
- cels.current.set(pixelEditorTypes.celKey(afi, aLi), pixelEditorUtils.createBlankImageData(dw, dh));
1072
- setTick((t) => t + 1);
1073
- }, [afi, aLi, dw, dh, snap]);
1074
- const newDoc = React.useCallback((w, h) => {
1075
- setDw(w);
1076
- setDh(h);
1077
- cels.current = new Map();
1078
- setLayers([
761
+ } else {
762
+ const img = new Image();
763
+ img.onload = () => {
764
+ const w2 = img.naturalWidth, h2 = img.naturalHeight;
765
+ const cv = new OffscreenCanvas(w2, h2);
766
+ cv.getContext("2d").drawImage(img, 0, 0);
767
+ const data = cv.getContext("2d").getImageData(0, 0, w2, h2);
768
+ setDw(w2);
769
+ setDh(h2);
770
+ cels.current = /* @__PURE__ */ new Map([[pixelEditorTypes.celKey(0, 0), data]]);
771
+ setLayers([
1079
772
  {
1080
- id: pixelEditorTypes.lid(),
1081
- name: 'Background',
1082
- visible: true,
1083
- opacity: 255,
1084
- locked: false,
1085
- blendMode: 'normal',
1086
- },
1087
- ]);
1088
- setALi(0);
1089
- setFc(1);
1090
- setAfi(0);
1091
- setFDur([100]);
1092
- setSel(null);
1093
- setZoom(Math.max(1, Math.floor(Math.min(512 / w, 512 / h))));
1094
- setPx(0);
1095
- setPy(0);
1096
- hist.current = [];
1097
- hIdx.current = -1;
1098
- setDlgNew(false);
1099
- setTick((t) => t + 1);
1100
- }, []);
1101
- /* ── effects ── */
1102
- // render every tick / state change
1103
- React.useEffect(() => {
1104
- render();
1105
- });
1106
- // keyboard shortcuts
1107
- React.useEffect(() => {
1108
- const h = (e) => {
1109
- if (dlgNew) {
1110
- return;
1111
- }
1112
- const tgt = e.target;
1113
- if (tgt.tagName === 'INPUT' || tgt.tagName === 'TEXTAREA' || tgt.tagName === 'SELECT') {
1114
- return;
1115
- }
1116
- if (e.ctrlKey && e.key === 'z') {
1117
- e.preventDefault();
1118
- undo();
1119
- return;
1120
- }
1121
- if (e.ctrlKey && (e.key === 'y' || (e.shiftKey && e.key === 'Z'))) {
1122
- e.preventDefault();
1123
- redo();
1124
- return;
1125
- }
1126
- if (e.ctrlKey && e.key === 'c') {
1127
- e.preventDefault();
1128
- doCopy();
1129
- return;
1130
- }
1131
- if (e.ctrlKey && e.key === 'x') {
1132
- e.preventDefault();
1133
- doCut();
1134
- return;
1135
- }
1136
- if (e.ctrlKey && e.key === 'v') {
1137
- e.preventDefault();
1138
- doPaste();
1139
- return;
1140
- }
1141
- if (e.ctrlKey && e.key === 'a') {
1142
- e.preventDefault();
1143
- doSelectAll();
1144
- return;
1145
- }
1146
- if (e.ctrlKey && e.key === 'd') {
1147
- e.preventDefault();
1148
- doDeselectAll();
1149
- return;
1150
- }
1151
- const km = {
1152
- b: 'pencil',
1153
- e: 'eraser',
1154
- g: 'fill',
1155
- l: 'line',
1156
- u: 'rect',
1157
- o: 'ellipse',
1158
- i: 'eyedropper',
1159
- m: 'select',
1160
- v: 'move',
1161
- h: 'gradient',
1162
- d: 'dither',
1163
- };
1164
- const k = e.key.toLowerCase();
1165
- if (km[k]) {
1166
- setTool(km[k]);
1167
- return;
1168
- }
1169
- switch (k) {
1170
- case '+':
1171
- case '=':
1172
- setZoom((z) => Math.min(64, z * 1.5));
1173
- break;
1174
- case '-':
1175
- setZoom((z) => Math.max(0.5, z / 1.5));
1176
- break;
1177
- case 'arrowleft':
1178
- setAfi((f) => Math.max(0, f - 1));
1179
- break;
1180
- case 'arrowright':
1181
- setAfi((f) => Math.min(fc - 1, f + 1));
1182
- break;
1183
- case 'n':
1184
- if (!e.ctrlKey) {
1185
- addFrame();
1186
- }
1187
- break;
1188
- case 'delete':
1189
- if (sel) {
1190
- doDeleteSel();
1191
- }
1192
- else {
1193
- clearCv();
1194
- }
1195
- break;
1196
- case 'x': {
1197
- setCol1((c) => {
1198
- setCol2(c);
1199
- return col2;
1200
- });
1201
- break;
1202
- }
1203
- case '[':
1204
- setBrush((b) => ({ ...b, size: Math.max(1, b.size - 1) }));
1205
- break;
1206
- case ']':
1207
- setBrush((b) => ({ ...b, size: Math.min(64, b.size + 1) }));
1208
- break;
1209
- case 'f':
1210
- setShapeFilled((f) => !f);
1211
- break;
773
+ id: pixelEditorTypes.lid(),
774
+ name: "Layer 1",
775
+ visible: true,
776
+ opacity: 255,
777
+ locked: false,
778
+ blendMode: "normal"
1212
779
  }
780
+ ]);
781
+ setALi(0);
782
+ setFc(1);
783
+ setAfi(0);
784
+ setFDur([100]);
785
+ setZoom(Math.max(1, Math.floor(Math.min(512 / w2, 512 / h2))));
786
+ setPx(0);
787
+ setPy(0);
788
+ hist.current = [];
789
+ hIdx.current = -1;
790
+ setTick((t) => t + 1);
1213
791
  };
1214
- window.addEventListener('keydown', h);
1215
- return () => window.removeEventListener('keydown', h);
1216
- }, [
1217
- dlgNew,
1218
- undo,
1219
- redo,
1220
- fc,
1221
- addFrame,
1222
- clearCv,
1223
- col2,
1224
- doCopy,
1225
- doCut,
1226
- doPaste,
1227
- doSelectAll,
1228
- doDeselectAll,
1229
- doDeleteSel,
1230
- sel,
792
+ img.src = URL.createObjectURL(file);
793
+ }
794
+ },
795
+ [loadAseDoc]
796
+ );
797
+ const exportAse = React.useCallback(async () => {
798
+ await pixelEditorExports.exportAse({ dw, dh, layers, fc, fDur, pal, cels: cels.current });
799
+ }, [dw, dh, layers, fc, fDur, pal]);
800
+ const exportPNG = React.useCallback(() => {
801
+ pixelEditorExports.exportPNG(afi, dw, dh, compFrame);
802
+ }, [afi, dw, dh, compFrame]);
803
+ const exportJPG = React.useCallback(() => {
804
+ pixelEditorExports.exportJPG(afi, dw, dh, compFrame);
805
+ }, [afi, dw, dh, compFrame]);
806
+ const exportWebP = React.useCallback(() => {
807
+ pixelEditorExports.exportWebP(afi, dw, dh, compFrame);
808
+ }, [afi, dw, dh, compFrame]);
809
+ const exportBMP = React.useCallback(() => {
810
+ pixelEditorExports.exportBMP(afi, dw, dh, compFrame);
811
+ }, [afi, dw, dh, compFrame]);
812
+ const exportSVG = React.useCallback(() => {
813
+ pixelEditorExports.exportSVG(afi, dw, dh, compFrame);
814
+ }, [afi, dw, dh, compFrame]);
815
+ const exportSheet = React.useCallback(() => {
816
+ pixelEditorExports.exportSheet(dw, dh, fc, compFrame);
817
+ }, [dw, dh, fc, compFrame]);
818
+ const saveToLibrary = React.useCallback(() => {
819
+ if (!onSaveToLibrary) {
820
+ return;
821
+ }
822
+ const data = compFrame(afi);
823
+ const cv = document.createElement("canvas");
824
+ cv.width = dw;
825
+ cv.height = dh;
826
+ cv.getContext("2d").putImageData(data, 0, 0);
827
+ const dataUrl = cv.toDataURL("image/png");
828
+ void ask("prompt", "Save to library as:", `sprite-${dw}x${dh}.png`).then((name) => {
829
+ if (name && name.trim()) {
830
+ onSaveToLibrary(dataUrl, name.trim(), "image/png");
831
+ }
832
+ });
833
+ }, [onSaveToLibrary, afi, dw, dh, compFrame, ask]);
834
+ const addLayer = React.useCallback(() => {
835
+ snap("Add Layer");
836
+ setLayers((ls) => [
837
+ ...ls,
838
+ {
839
+ id: pixelEditorTypes.lid(),
840
+ name: `Layer ${ls.length + 1}`,
841
+ visible: true,
842
+ opacity: 255,
843
+ locked: false,
844
+ blendMode: "normal"
845
+ }
1231
846
  ]);
1232
- // animation playback
1233
- React.useEffect(() => {
1234
- if (!playing) {
1235
- return;
847
+ setALi(layers.length);
848
+ setTick((t) => t + 1);
849
+ }, [layers, snap]);
850
+ const rmLayer = React.useCallback(() => {
851
+ if (layers.length <= 1) {
852
+ return;
853
+ }
854
+ snap("Remove Layer");
855
+ const idx = aLi;
856
+ for (let fi = 0; fi < fc; fi++) {
857
+ cels.current.delete(pixelEditorTypes.celKey(fi, idx));
858
+ for (let li = idx + 1; li < layers.length; li++) {
859
+ const c = cels.current.get(pixelEditorTypes.celKey(fi, li));
860
+ if (c) {
861
+ cels.current.set(pixelEditorTypes.celKey(fi, li - 1), c);
862
+ cels.current.delete(pixelEditorTypes.celKey(fi, li));
863
+ }
864
+ }
865
+ }
866
+ setLayers((ls) => ls.filter((_, i) => i !== idx));
867
+ setALi(Math.min(idx, layers.length - 2));
868
+ setTick((t) => t + 1);
869
+ }, [layers, aLi, fc, snap]);
870
+ const toggleVis = React.useCallback((i) => {
871
+ setLayers((ls) => ls.map((l, j) => j === i ? { ...l, visible: !l.visible } : l));
872
+ setTick((t) => t + 1);
873
+ }, []);
874
+ const toggleLock = React.useCallback((i) => {
875
+ setLayers((ls) => ls.map((l, j) => j === i ? { ...l, locked: !l.locked } : l));
876
+ }, []);
877
+ const setLayerOpacity = React.useCallback((i, op) => {
878
+ setLayers((ls) => ls.map((l, j) => j === i ? { ...l, opacity: op } : l));
879
+ setTick((t) => t + 1);
880
+ }, []);
881
+ const setLayerBlendMode = React.useCallback((i, mode) => {
882
+ setLayers((ls) => ls.map((l, j) => j === i ? { ...l, blendMode: mode } : l));
883
+ setTick((t) => t + 1);
884
+ }, []);
885
+ const renameLayer = React.useCallback((i, name) => {
886
+ setLayers((ls) => ls.map((l, j) => j === i ? { ...l, name } : l));
887
+ }, []);
888
+ const moveLayerUp = React.useCallback(() => {
889
+ if (aLi >= layers.length - 1) {
890
+ return;
891
+ }
892
+ snap("Move Layer Up");
893
+ setLayers((ls) => {
894
+ const n = [...ls];
895
+ [n[aLi], n[aLi + 1]] = [n[aLi + 1], n[aLi]];
896
+ return n;
897
+ });
898
+ for (let fi = 0; fi < fc; fi++) {
899
+ const a = cels.current.get(pixelEditorTypes.celKey(fi, aLi));
900
+ const b = cels.current.get(pixelEditorTypes.celKey(fi, aLi + 1));
901
+ if (a) {
902
+ cels.current.set(pixelEditorTypes.celKey(fi, aLi + 1), a);
903
+ } else {
904
+ cels.current.delete(pixelEditorTypes.celKey(fi, aLi + 1));
905
+ }
906
+ if (b) {
907
+ cels.current.set(pixelEditorTypes.celKey(fi, aLi), b);
908
+ } else {
909
+ cels.current.delete(pixelEditorTypes.celKey(fi, aLi));
910
+ }
911
+ }
912
+ setALi(aLi + 1);
913
+ setTick((t) => t + 1);
914
+ }, [aLi, layers, fc, snap]);
915
+ const moveLayerDown = React.useCallback(() => {
916
+ if (aLi <= 0) {
917
+ return;
918
+ }
919
+ snap("Move Layer Down");
920
+ setLayers((ls) => {
921
+ const n = [...ls];
922
+ [n[aLi], n[aLi - 1]] = [n[aLi - 1], n[aLi]];
923
+ return n;
924
+ });
925
+ for (let fi = 0; fi < fc; fi++) {
926
+ const a = cels.current.get(pixelEditorTypes.celKey(fi, aLi));
927
+ const b = cels.current.get(pixelEditorTypes.celKey(fi, aLi - 1));
928
+ if (a) {
929
+ cels.current.set(pixelEditorTypes.celKey(fi, aLi - 1), a);
930
+ } else {
931
+ cels.current.delete(pixelEditorTypes.celKey(fi, aLi - 1));
932
+ }
933
+ if (b) {
934
+ cels.current.set(pixelEditorTypes.celKey(fi, aLi), b);
935
+ } else {
936
+ cels.current.delete(pixelEditorTypes.celKey(fi, aLi));
937
+ }
938
+ }
939
+ setALi(aLi - 1);
940
+ setTick((t) => t + 1);
941
+ }, [aLi, fc, snap]);
942
+ const mergeDown = React.useCallback(() => {
943
+ if (aLi <= 0) {
944
+ return;
945
+ }
946
+ snap("Merge Down");
947
+ for (let fi = 0; fi < fc; fi++) {
948
+ const top = cels.current.get(pixelEditorTypes.celKey(fi, aLi));
949
+ const bot = getCel(fi, aLi - 1);
950
+ if (top) {
951
+ const cv = new OffscreenCanvas(dw, dh);
952
+ const ctx = cv.getContext("2d");
953
+ ctx.putImageData(bot, 0, 0);
954
+ const t = new OffscreenCanvas(dw, dh);
955
+ t.getContext("2d").putImageData(top, 0, 0);
956
+ ctx.globalAlpha = layers[aLi].opacity / 255;
957
+ ctx.drawImage(t, 0, 0);
958
+ cels.current.set(pixelEditorTypes.celKey(fi, aLi - 1), ctx.getImageData(0, 0, dw, dh));
959
+ }
960
+ }
961
+ for (let fi = 0; fi < fc; fi++) {
962
+ cels.current.delete(pixelEditorTypes.celKey(fi, aLi));
963
+ for (let li = aLi + 1; li < layers.length; li++) {
964
+ const c = cels.current.get(pixelEditorTypes.celKey(fi, li));
965
+ if (c) {
966
+ cels.current.set(pixelEditorTypes.celKey(fi, li - 1), c);
967
+ cels.current.delete(pixelEditorTypes.celKey(fi, li));
1236
968
  }
1237
- const iv = setInterval(() => setAfi((f) => (f + 1) % fc), 1000 / fps);
1238
- return () => clearInterval(iv);
1239
- }, [playing, fc, fps]);
1240
- // resize observer
1241
- React.useEffect(() => {
1242
- const el = boxRef.current;
1243
- if (!el) {
1244
- return;
969
+ }
970
+ }
971
+ setLayers((ls) => ls.filter((_, i) => i !== aLi));
972
+ setALi(aLi - 1);
973
+ setTick((t) => t + 1);
974
+ }, [aLi, fc, layers, getCel, dw, dh, snap]);
975
+ const dupLayer = React.useCallback(() => {
976
+ snap("Duplicate Layer");
977
+ const newL = { ...layers[aLi], id: pixelEditorTypes.lid(), name: layers[aLi].name + " copy" };
978
+ setLayers((ls) => [...ls.slice(0, aLi + 1), newL, ...ls.slice(aLi + 1)]);
979
+ for (let fi = 0; fi < fc; fi++) {
980
+ for (let li = layers.length; li > aLi; li--) {
981
+ const c = cels.current.get(pixelEditorTypes.celKey(fi, li));
982
+ if (c) {
983
+ cels.current.set(pixelEditorTypes.celKey(fi, li + 1), c);
984
+ cels.current.delete(pixelEditorTypes.celKey(fi, li));
1245
985
  }
1246
- const obs = new ResizeObserver(() => setTick((t) => t + 1));
1247
- obs.observe(el);
1248
- return () => obs.disconnect();
1249
- }, []);
1250
- /* ── frame thumbnails ── */
1251
- const thumbs = React.useMemo(() => {
1252
- const out = [];
1253
- for (let fi = 0; fi < fc; fi++) {
1254
- const cv = document.createElement('canvas');
1255
- cv.width = dw;
1256
- cv.height = dh;
1257
- const ctx = cv.getContext('2d');
1258
- for (let li = 0; li < layers.length; li++) {
1259
- if (!layers[li].visible) {
1260
- continue;
1261
- }
1262
- const c = cels.current.get(pixelEditorTypes.celKey(fi, li));
1263
- if (!c) {
1264
- continue;
1265
- }
1266
- const t = document.createElement('canvas');
1267
- t.width = dw;
1268
- t.height = dh;
1269
- t.getContext('2d').putImageData(c, 0, 0);
1270
- ctx.globalAlpha = layers[li].opacity / 255;
1271
- ctx.drawImage(t, 0, 0);
1272
- }
1273
- out.push(cv.toDataURL());
986
+ }
987
+ const src = cels.current.get(pixelEditorTypes.celKey(fi, aLi));
988
+ if (src) {
989
+ cels.current.set(pixelEditorTypes.celKey(fi, aLi + 1), pixelEditorUtils.cloneImageData(src));
990
+ }
991
+ }
992
+ setALi(aLi + 1);
993
+ setTick((t) => t + 1);
994
+ }, [aLi, layers, fc, snap]);
995
+ const addFrame = React.useCallback(() => {
996
+ snap("Add Frame");
997
+ setFc((n) => n + 1);
998
+ setFDur((d) => [...d, 100]);
999
+ setAfi(fc);
1000
+ setTick((t) => t + 1);
1001
+ }, [fc, snap]);
1002
+ const dupFrame = React.useCallback(() => {
1003
+ snap("Dup Frame");
1004
+ const ni = fc;
1005
+ for (let li = 0; li < layers.length; li++) {
1006
+ const c = cels.current.get(pixelEditorTypes.celKey(afi, li));
1007
+ if (c) {
1008
+ cels.current.set(pixelEditorTypes.celKey(ni, li), pixelEditorUtils.cloneImageData(c));
1009
+ }
1010
+ }
1011
+ setFc((n) => n + 1);
1012
+ setFDur((d) => [...d, fDur[afi] || 100]);
1013
+ setAfi(ni);
1014
+ setTick((t) => t + 1);
1015
+ }, [fc, afi, layers, fDur, snap]);
1016
+ const rmFrame = React.useCallback(() => {
1017
+ if (fc <= 1) {
1018
+ return;
1019
+ }
1020
+ snap("Remove Frame");
1021
+ for (let li = 0; li < layers.length; li++) {
1022
+ cels.current.delete(pixelEditorTypes.celKey(afi, li));
1023
+ for (let f = afi + 1; f < fc; f++) {
1024
+ const c = cels.current.get(pixelEditorTypes.celKey(f, li));
1025
+ if (c) {
1026
+ cels.current.set(pixelEditorTypes.celKey(f - 1, li), c);
1027
+ cels.current.delete(pixelEditorTypes.celKey(f, li));
1274
1028
  }
1275
- return out;
1276
- }, [fc, dw, dh, layers, tick]);
1277
- /* ── return API ── */
1278
- return {
1279
- pendingAsk,
1280
- dw,
1281
- dh,
1282
- layers,
1283
- aLi,
1284
- setALi,
1285
- fc,
1286
- afi,
1287
- setAfi,
1288
- fDur,
1289
- tool,
1290
- setTool,
1291
- col1,
1292
- setCol1,
1293
- col2,
1294
- setCol2,
1295
- brush,
1296
- setBrush,
1297
- zoom,
1298
- setZoom,
1299
- grid,
1300
- setGrid,
1301
- onion,
1302
- setOnion,
1303
- fps,
1304
- setFps,
1305
- playing,
1306
- setPlaying,
1307
- pal,
1308
- setPal,
1309
- cursor,
1310
- setCursor,
1311
- dlgNew,
1312
- setDlgNew,
1313
- nw,
1314
- setNw,
1315
- nh,
1316
- setNh,
1317
- sel,
1318
- setSel,
1319
- symmetry,
1320
- setSymmetry,
1321
- shapeFilled,
1322
- setShapeFilled,
1323
- pressure,
1324
- showHSV,
1325
- setShowHSV,
1326
- editingLayerName,
1327
- setEditingLayerName,
1328
- cvRef,
1329
- boxRef,
1330
- fInp,
1331
- clipboard,
1332
- drawing,
1333
- panning,
1334
- undo,
1335
- redo,
1336
- openFile,
1337
- exportAse,
1338
- exportPNG,
1339
- exportJPG,
1340
- exportWebP,
1341
- exportBMP,
1342
- exportSVG,
1343
- exportSheet,
1344
- saveToLibrary,
1345
- addLayer,
1346
- rmLayer,
1347
- toggleVis,
1348
- toggleLock,
1349
- setLayerOpacity,
1350
- setLayerBlendMode,
1351
- renameLayer,
1352
- moveLayerUp,
1353
- moveLayerDown,
1354
- mergeDown,
1355
- dupLayer,
1356
- addFrame,
1357
- dupFrame,
1358
- rmFrame,
1359
- flipCv,
1360
- rotateCv,
1361
- clearCv,
1362
- newDoc,
1363
- doCopy,
1364
- doCut,
1365
- doPaste,
1366
- doDeleteSel,
1367
- doSelectAll,
1368
- doDeselectAll,
1369
- onDown,
1370
- onMove,
1371
- onUp,
1372
- onWheel,
1373
- thumbs,
1374
- importImageData: React.useCallback((img) => {
1375
- snap('import generator');
1376
- const cel = getCel(afi, aLi);
1377
- const ctx = document.createElement('canvas').getContext('2d');
1378
- ctx.canvas.width = dw;
1379
- ctx.canvas.height = dh;
1380
- ctx.putImageData(cel, 0, 0);
1381
- const src = document.createElement('canvas');
1382
- src.width = img.width;
1383
- src.height = img.height;
1384
- src.getContext('2d').putImageData(img, 0, 0);
1385
- ctx.drawImage(src, 0, 0);
1386
- const merged = ctx.getImageData(0, 0, dw, dh);
1387
- cel.data.set(merged.data);
1388
- setTick((t) => t + 1);
1389
- }, [snap, getCel, afi, aLi, dw, dh]),
1029
+ }
1030
+ }
1031
+ setFc((n) => n - 1);
1032
+ setFDur((d) => d.filter((_, i) => i !== afi));
1033
+ setAfi(Math.min(afi, fc - 2));
1034
+ setTick((t) => t + 1);
1035
+ }, [fc, afi, layers, snap]);
1036
+ const flipCv = React.useCallback(
1037
+ (dir) => {
1038
+ snap(`Flip ${dir === "h" ? "H" : "V"}`);
1039
+ cels.current.forEach((c, k) => cels.current.set(k, dir === "h" ? pixelEditorUtils.flipH(c) : pixelEditorUtils.flipV(c)));
1040
+ setTick((t) => t + 1);
1041
+ },
1042
+ [snap]
1043
+ );
1044
+ const rotateCv = React.useCallback(() => {
1045
+ snap("Rotate 90°");
1046
+ const n = /* @__PURE__ */ new Map();
1047
+ cels.current.forEach((c, k) => n.set(k, pixelEditorUtils.rotateCW(c)));
1048
+ cels.current = n;
1049
+ setDw(dh);
1050
+ setDh(dw);
1051
+ setTick((t) => t + 1);
1052
+ }, [dw, dh, snap]);
1053
+ const clearCv = React.useCallback(() => {
1054
+ snap("Clear");
1055
+ cels.current.set(pixelEditorTypes.celKey(afi, aLi), pixelEditorUtils.createBlankImageData(dw, dh));
1056
+ setTick((t) => t + 1);
1057
+ }, [afi, aLi, dw, dh, snap]);
1058
+ const newDoc = React.useCallback((w, h) => {
1059
+ setDw(w);
1060
+ setDh(h);
1061
+ cels.current = /* @__PURE__ */ new Map();
1062
+ setLayers([
1063
+ {
1064
+ id: pixelEditorTypes.lid(),
1065
+ name: "Background",
1066
+ visible: true,
1067
+ opacity: 255,
1068
+ locked: false,
1069
+ blendMode: "normal"
1070
+ }
1071
+ ]);
1072
+ setALi(0);
1073
+ setFc(1);
1074
+ setAfi(0);
1075
+ setFDur([100]);
1076
+ setSel(null);
1077
+ setZoom(Math.max(1, Math.floor(Math.min(512 / w, 512 / h))));
1078
+ setPx(0);
1079
+ setPy(0);
1080
+ hist.current = [];
1081
+ hIdx.current = -1;
1082
+ setDlgNew(false);
1083
+ setTick((t) => t + 1);
1084
+ }, []);
1085
+ React.useEffect(() => {
1086
+ render();
1087
+ });
1088
+ React.useEffect(() => {
1089
+ const h = (e) => {
1090
+ if (dlgNew) {
1091
+ return;
1092
+ }
1093
+ const tgt = e.target;
1094
+ if (tgt.tagName === "INPUT" || tgt.tagName === "TEXTAREA" || tgt.tagName === "SELECT") {
1095
+ return;
1096
+ }
1097
+ if (e.ctrlKey && e.key === "z") {
1098
+ e.preventDefault();
1099
+ undo();
1100
+ return;
1101
+ }
1102
+ if (e.ctrlKey && (e.key === "y" || e.shiftKey && e.key === "Z")) {
1103
+ e.preventDefault();
1104
+ redo();
1105
+ return;
1106
+ }
1107
+ if (e.ctrlKey && e.key === "c") {
1108
+ e.preventDefault();
1109
+ doCopy();
1110
+ return;
1111
+ }
1112
+ if (e.ctrlKey && e.key === "x") {
1113
+ e.preventDefault();
1114
+ doCut();
1115
+ return;
1116
+ }
1117
+ if (e.ctrlKey && e.key === "v") {
1118
+ e.preventDefault();
1119
+ doPaste();
1120
+ return;
1121
+ }
1122
+ if (e.ctrlKey && e.key === "a") {
1123
+ e.preventDefault();
1124
+ doSelectAll();
1125
+ return;
1126
+ }
1127
+ if (e.ctrlKey && e.key === "d") {
1128
+ e.preventDefault();
1129
+ doDeselectAll();
1130
+ return;
1131
+ }
1132
+ const km = {
1133
+ b: "pencil",
1134
+ e: "eraser",
1135
+ g: "fill",
1136
+ l: "line",
1137
+ u: "rect",
1138
+ o: "ellipse",
1139
+ i: "eyedropper",
1140
+ m: "select",
1141
+ v: "move",
1142
+ h: "gradient",
1143
+ d: "dither"
1144
+ };
1145
+ const k = e.key.toLowerCase();
1146
+ if (km[k]) {
1147
+ setTool(km[k]);
1148
+ return;
1149
+ }
1150
+ switch (k) {
1151
+ case "+":
1152
+ case "=":
1153
+ setZoom((z) => Math.min(64, z * 1.5));
1154
+ break;
1155
+ case "-":
1156
+ setZoom((z) => Math.max(0.5, z / 1.5));
1157
+ break;
1158
+ case "arrowleft":
1159
+ setAfi((f) => Math.max(0, f - 1));
1160
+ break;
1161
+ case "arrowright":
1162
+ setAfi((f) => Math.min(fc - 1, f + 1));
1163
+ break;
1164
+ case "n":
1165
+ if (!e.ctrlKey) {
1166
+ addFrame();
1167
+ }
1168
+ break;
1169
+ case "delete":
1170
+ if (sel) {
1171
+ doDeleteSel();
1172
+ } else {
1173
+ clearCv();
1174
+ }
1175
+ break;
1176
+ case "x": {
1177
+ setCol1((c) => {
1178
+ setCol2(c);
1179
+ return col2;
1180
+ });
1181
+ break;
1182
+ }
1183
+ case "[":
1184
+ setBrush((b) => ({ ...b, size: Math.max(1, b.size - 1) }));
1185
+ break;
1186
+ case "]":
1187
+ setBrush((b) => ({ ...b, size: Math.min(64, b.size + 1) }));
1188
+ break;
1189
+ case "f":
1190
+ setShapeFilled((f) => !f);
1191
+ break;
1192
+ }
1390
1193
  };
1194
+ window.addEventListener("keydown", h);
1195
+ return () => window.removeEventListener("keydown", h);
1196
+ }, [
1197
+ dlgNew,
1198
+ undo,
1199
+ redo,
1200
+ fc,
1201
+ addFrame,
1202
+ clearCv,
1203
+ col2,
1204
+ doCopy,
1205
+ doCut,
1206
+ doPaste,
1207
+ doSelectAll,
1208
+ doDeselectAll,
1209
+ doDeleteSel,
1210
+ sel
1211
+ ]);
1212
+ React.useEffect(() => {
1213
+ if (!playing) {
1214
+ return;
1215
+ }
1216
+ const iv = setInterval(() => setAfi((f) => (f + 1) % fc), 1e3 / fps);
1217
+ return () => clearInterval(iv);
1218
+ }, [playing, fc, fps]);
1219
+ React.useEffect(() => {
1220
+ const el = boxRef.current;
1221
+ if (!el) {
1222
+ return;
1223
+ }
1224
+ const obs = new ResizeObserver(() => setTick((t) => t + 1));
1225
+ obs.observe(el);
1226
+ return () => obs.disconnect();
1227
+ }, []);
1228
+ const thumbs = React.useMemo(() => {
1229
+ const out = [];
1230
+ for (let fi = 0; fi < fc; fi++) {
1231
+ const cv = document.createElement("canvas");
1232
+ cv.width = dw;
1233
+ cv.height = dh;
1234
+ const ctx = cv.getContext("2d");
1235
+ for (let li = 0; li < layers.length; li++) {
1236
+ if (!layers[li].visible) {
1237
+ continue;
1238
+ }
1239
+ const c = cels.current.get(pixelEditorTypes.celKey(fi, li));
1240
+ if (!c) {
1241
+ continue;
1242
+ }
1243
+ const t = document.createElement("canvas");
1244
+ t.width = dw;
1245
+ t.height = dh;
1246
+ t.getContext("2d").putImageData(c, 0, 0);
1247
+ ctx.globalAlpha = layers[li].opacity / 255;
1248
+ ctx.drawImage(t, 0, 0);
1249
+ }
1250
+ out.push(cv.toDataURL());
1251
+ }
1252
+ return out;
1253
+ }, [fc, dw, dh, layers, tick]);
1254
+ return {
1255
+ pendingAsk,
1256
+ dw,
1257
+ dh,
1258
+ layers,
1259
+ aLi,
1260
+ setALi,
1261
+ fc,
1262
+ afi,
1263
+ setAfi,
1264
+ fDur,
1265
+ tool,
1266
+ setTool,
1267
+ col1,
1268
+ setCol1,
1269
+ col2,
1270
+ setCol2,
1271
+ brush,
1272
+ setBrush,
1273
+ zoom,
1274
+ setZoom,
1275
+ grid,
1276
+ setGrid,
1277
+ onion,
1278
+ setOnion,
1279
+ fps,
1280
+ setFps,
1281
+ playing,
1282
+ setPlaying,
1283
+ pal,
1284
+ setPal,
1285
+ cursor,
1286
+ setCursor,
1287
+ dlgNew,
1288
+ setDlgNew,
1289
+ nw,
1290
+ setNw,
1291
+ nh,
1292
+ setNh,
1293
+ sel,
1294
+ setSel,
1295
+ symmetry,
1296
+ setSymmetry,
1297
+ shapeFilled,
1298
+ setShapeFilled,
1299
+ pressure,
1300
+ showHSV,
1301
+ setShowHSV,
1302
+ editingLayerName,
1303
+ setEditingLayerName,
1304
+ cvRef,
1305
+ boxRef,
1306
+ fInp,
1307
+ clipboard,
1308
+ drawing,
1309
+ panning,
1310
+ undo,
1311
+ redo,
1312
+ openFile,
1313
+ exportAse,
1314
+ exportPNG,
1315
+ exportJPG,
1316
+ exportWebP,
1317
+ exportBMP,
1318
+ exportSVG,
1319
+ exportSheet,
1320
+ saveToLibrary,
1321
+ addLayer,
1322
+ rmLayer,
1323
+ toggleVis,
1324
+ toggleLock,
1325
+ setLayerOpacity,
1326
+ setLayerBlendMode,
1327
+ renameLayer,
1328
+ moveLayerUp,
1329
+ moveLayerDown,
1330
+ mergeDown,
1331
+ dupLayer,
1332
+ addFrame,
1333
+ dupFrame,
1334
+ rmFrame,
1335
+ flipCv,
1336
+ rotateCv,
1337
+ clearCv,
1338
+ newDoc,
1339
+ doCopy,
1340
+ doCut,
1341
+ doPaste,
1342
+ doDeleteSel,
1343
+ doSelectAll,
1344
+ doDeselectAll,
1345
+ onDown,
1346
+ onMove,
1347
+ onUp,
1348
+ onWheel,
1349
+ thumbs,
1350
+ importImageData: React.useCallback(
1351
+ (img) => {
1352
+ snap("import generator");
1353
+ const cel = getCel(afi, aLi);
1354
+ const ctx = document.createElement("canvas").getContext("2d");
1355
+ ctx.canvas.width = dw;
1356
+ ctx.canvas.height = dh;
1357
+ ctx.putImageData(cel, 0, 0);
1358
+ const src = document.createElement("canvas");
1359
+ src.width = img.width;
1360
+ src.height = img.height;
1361
+ src.getContext("2d").putImageData(img, 0, 0);
1362
+ ctx.drawImage(src, 0, 0);
1363
+ const merged = ctx.getImageData(0, 0, dw, dh);
1364
+ cel.data.set(merged.data);
1365
+ setTick((t) => t + 1);
1366
+ },
1367
+ [snap, getCel, afi, aLi, dw, dh]
1368
+ )
1369
+ };
1391
1370
  }
1392
-
1393
1371
  exports.usePixelEditor = usePixelEditor;