@nice2dev/ui-graphics 1.3.15 → 1.3.16

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