@@ -16,6 +16,10 @@ const editorState = {
1616 gridSize : DEFAULT_GRID_SIZE ,
1717 paintedPixels : new Map ( ) ,
1818} ;
19+ const editorHistory = {
20+ redoStack : [ ] ,
21+ undoStack : [ ] ,
22+ } ;
1923
2024function gridLabel ( size ) {
2125 return `Sprite Creator ${ size } by ${ size } pixel canvas` ;
@@ -29,6 +33,49 @@ function pixelKey(row, column) {
2933 return `${ row } :${ column } ` ;
3034}
3135
36+ function stateSnapshot ( ) {
37+ return {
38+ gridSize : editorState . gridSize ,
39+ paintedPixels : Array . from ( editorState . paintedPixels . entries ( ) ) ,
40+ } ;
41+ }
42+
43+ function sameSnapshot ( left , right ) {
44+ return JSON . stringify ( left ) === JSON . stringify ( right ) ;
45+ }
46+
47+ function pushUndoSnapshot ( ) {
48+ const snapshot = stateSnapshot ( ) ;
49+ const lastSnapshot = editorHistory . undoStack [ editorHistory . undoStack . length - 1 ] ;
50+ if ( ! lastSnapshot || ! sameSnapshot ( snapshot , lastSnapshot ) ) {
51+ editorHistory . undoStack . push ( snapshot ) ;
52+ }
53+ editorHistory . redoStack = [ ] ;
54+ updateHistoryControls ( ) ;
55+ }
56+
57+ function historyStatusText ( ) {
58+ if ( editorHistory . undoStack . length === 0 && editorHistory . redoStack . length === 0 ) {
59+ return "Undo history is empty." ;
60+ }
61+ return `Undo steps: ${ editorHistory . undoStack . length } . Redo steps: ${ editorHistory . redoStack . length } .` ;
62+ }
63+
64+ function updateHistoryControls ( ) {
65+ const undoButton = document . querySelector ( "[data-sprites-undo]" ) ;
66+ const redoButton = document . querySelector ( "[data-sprites-redo]" ) ;
67+ const status = document . querySelector ( "[data-sprites-history-status]" ) ;
68+ if ( undoButton ) {
69+ undoButton . disabled = editorHistory . undoStack . length === 0 ;
70+ }
71+ if ( redoButton ) {
72+ redoButton . disabled = editorHistory . redoStack . length === 0 ;
73+ }
74+ if ( status ) {
75+ status . textContent = historyStatusText ( ) ;
76+ }
77+ }
78+
3279function draftStatusText ( ) {
3380 const count = editorState . paintedPixels . size ;
3481 if ( count === 0 ) {
@@ -54,6 +101,30 @@ function setCellColor(cell, colorKey) {
54101 cell . dataset . spriteColorKey = normalizedColorKey ;
55102}
56103
104+ function applyPaintedPixelsToGrid ( ) {
105+ const grid = document . querySelector ( "[data-sprites-pixel-grid]" ) ;
106+ if ( ! grid ) {
107+ return ;
108+ }
109+ grid . querySelectorAll ( "[data-sprite-pixel-row]" ) . forEach ( ( cell ) => {
110+ const key = pixelKey ( cell . dataset . spritePixelRow , cell . dataset . spritePixelColumn ) ;
111+ const colorKey = editorState . paintedPixels . get ( key ) ;
112+ if ( colorKey ) {
113+ setCellColor ( cell , colorKey ) ;
114+ } else {
115+ clearCellColor ( cell ) ;
116+ }
117+ } ) ;
118+ }
119+
120+ function applySnapshot ( snapshot ) {
121+ setGridSize ( snapshot . gridSize , { recordHistory : false } ) ;
122+ editorState . paintedPixels = new Map ( snapshot . paintedPixels ) ;
123+ applyPaintedPixelsToGrid ( ) ;
124+ updateDraftStatus ( ) ;
125+ updateHistoryControls ( ) ;
126+ }
127+
57128function updateDraftStatus ( ) {
58129 const status = document . querySelector ( "[data-sprites-draft-status]" ) ;
59130 if ( status ) {
@@ -99,9 +170,17 @@ function setActiveTool(toolName) {
99170function paintCell ( cell ) {
100171 const key = pixelKey ( cell . dataset . spritePixelRow , cell . dataset . spritePixelColumn ) ;
101172 if ( editorState . activeTool === "eraser" ) {
173+ if ( ! editorState . paintedPixels . has ( key ) ) {
174+ return ;
175+ }
176+ pushUndoSnapshot ( ) ;
102177 editorState . paintedPixels . delete ( key ) ;
103178 clearCellColor ( cell ) ;
104179 } else {
180+ if ( editorState . paintedPixels . get ( key ) === editorState . activeColor ) {
181+ return ;
182+ }
183+ pushUndoSnapshot ( ) ;
105184 editorState . paintedPixels . set ( key , editorState . activeColor ) ;
106185 setCellColor ( cell , editorState . activeColor ) ;
107186 }
@@ -113,6 +192,7 @@ function fillGrid() {
113192 if ( ! grid ) {
114193 return ;
115194 }
195+ pushUndoSnapshot ( ) ;
116196 editorState . paintedPixels . clear ( ) ;
117197 grid . querySelectorAll ( "[data-sprite-pixel-row]" ) . forEach ( ( cell ) => {
118198 const key = pixelKey ( cell . dataset . spritePixelRow , cell . dataset . spritePixelColumn ) ;
@@ -122,8 +202,11 @@ function fillGrid() {
122202 updateDraftStatus ( ) ;
123203}
124204
125- function clearCanvas ( ) {
205+ function clearCanvas ( options = { } ) {
126206 const grid = document . querySelector ( "[data-sprites-pixel-grid]" ) ;
207+ if ( options . recordHistory !== false && editorState . paintedPixels . size > 0 ) {
208+ pushUndoSnapshot ( ) ;
209+ }
127210 editorState . paintedPixels . clear ( ) ;
128211 if ( grid ) {
129212 grid . querySelectorAll ( "[data-sprite-pixel-row]" ) . forEach ( clearCellColor ) ;
@@ -132,8 +215,9 @@ function clearCanvas() {
132215}
133216
134217function resetGridToDefault ( ) {
135- setGridSize ( DEFAULT_GRID_SIZE ) ;
136- clearCanvas ( ) ;
218+ pushUndoSnapshot ( ) ;
219+ setGridSize ( DEFAULT_GRID_SIZE , { recordHistory : false } ) ;
220+ clearCanvas ( { recordHistory : false } ) ;
137221}
138222
139223function editorColorValue ( colorKey ) {
@@ -194,13 +278,17 @@ function exportPreviewPng() {
194278 } , "image/png" ) ;
195279}
196280
197- function setGridSize ( size ) {
281+ function setGridSize ( size , options = { } ) {
198282 const grid = document . querySelector ( "[data-sprites-pixel-grid]" ) ;
199283 const status = document . querySelector ( "[data-sprites-grid-status]" ) ;
200284 if ( ! grid || ! SUPPORTED_GRID_SIZES . includes ( size ) ) {
201285 return ;
202286 }
203287
288+ if ( options . recordHistory && ( editorState . gridSize !== size || editorState . paintedPixels . size > 0 ) ) {
289+ pushUndoSnapshot ( ) ;
290+ }
291+
204292 grid . replaceChildren ( ) ;
205293 editorState . gridSize = size ;
206294 editorState . paintedPixels . clear ( ) ;
@@ -239,7 +327,7 @@ function wireGridControls() {
239327 if ( ! button ) {
240328 return ;
241329 }
242- button . addEventListener ( "click" , ( ) => setGridSize ( size ) ) ;
330+ button . addEventListener ( "click" , ( ) => setGridSize ( size , { recordHistory : true } ) ) ;
243331 } ) ;
244332}
245333
@@ -278,6 +366,34 @@ function wireCanvasActions() {
278366 }
279367}
280368
369+ function wireHistoryActions ( ) {
370+ const undoButton = document . querySelector ( "[data-sprites-undo]" ) ;
371+ if ( undoButton ) {
372+ undoButton . addEventListener ( "click" , ( ) => {
373+ const snapshot = editorHistory . undoStack . pop ( ) ;
374+ if ( ! snapshot ) {
375+ updateHistoryControls ( ) ;
376+ return ;
377+ }
378+ editorHistory . redoStack . push ( stateSnapshot ( ) ) ;
379+ applySnapshot ( snapshot ) ;
380+ } ) ;
381+ }
382+
383+ const redoButton = document . querySelector ( "[data-sprites-redo]" ) ;
384+ if ( redoButton ) {
385+ redoButton . addEventListener ( "click" , ( ) => {
386+ const snapshot = editorHistory . redoStack . pop ( ) ;
387+ if ( ! snapshot ) {
388+ updateHistoryControls ( ) ;
389+ return ;
390+ }
391+ editorHistory . undoStack . push ( stateSnapshot ( ) ) ;
392+ applySnapshot ( snapshot ) ;
393+ } ) ;
394+ }
395+ }
396+
281397function wireExportButton ( ) {
282398 const button = document . querySelector ( "[data-sprites-export-png]" ) ;
283399 if ( button ) {
@@ -289,8 +405,10 @@ wireGridControls();
289405wireDrawingTools ( ) ;
290406wirePaletteButtons ( ) ;
291407wireCanvasActions ( ) ;
408+ wireHistoryActions ( ) ;
292409wireExportButton ( ) ;
293410setGridSize ( DEFAULT_GRID_SIZE ) ;
294411setActiveTool ( editorState . activeTool ) ;
295412setActiveColor ( editorState . activeColor ) ;
413+ updateHistoryControls ( ) ;
296414renderPreview ( ) ;
0 commit comments