@@ -1032,6 +1032,44 @@ function ensureEditingEnabled(state, blockedMessage = "Select and lock a palette
10321032 return false ;
10331033}
10341034
1035+ function getSelectedFrame ( state ) {
1036+ const frames = Array . isArray ( state . project ?. frames ) ? state . project . frames : [ ] ;
1037+ if ( frames . length <= 0 ) {
1038+ return null ;
1039+ }
1040+ const index = clamp ( state . project ?. currentFrameIndex , 0 , frames . length - 1 , 0 ) ;
1041+ return frames [ index ] ?? null ;
1042+ }
1043+
1044+ function ensureFrameSelection ( state , options = { } ) {
1045+ const preferFirstFrame = options . preferFirstFrame === true ;
1046+ const syncPreview = options . syncPreview !== false ;
1047+ const frames = Array . isArray ( state . project ?. frames ) ? state . project . frames : [ ] ;
1048+ if ( frames . length <= 0 ) {
1049+ state . project . currentFrameIndex = 0 ;
1050+ state . preview . frameIndex = 0 ;
1051+ return false ;
1052+ }
1053+ let changed = false ;
1054+ const nextCurrentIndex = preferFirstFrame
1055+ ? 0
1056+ : clamp ( state . project . currentFrameIndex , 0 , frames . length - 1 , 0 ) ;
1057+ if ( nextCurrentIndex !== state . project . currentFrameIndex ) {
1058+ state . project . currentFrameIndex = nextCurrentIndex ;
1059+ changed = true ;
1060+ }
1061+ if ( syncPreview ) {
1062+ const nextPreviewIndex = state . preview . playing
1063+ ? clamp ( state . preview . frameIndex , 0 , frames . length - 1 , nextCurrentIndex )
1064+ : nextCurrentIndex ;
1065+ if ( nextPreviewIndex !== state . preview . frameIndex ) {
1066+ state . preview . frameIndex = nextPreviewIndex ;
1067+ changed = true ;
1068+ }
1069+ }
1070+ return changed ;
1071+ }
1072+
10351073function renderPaletteSelect ( state ) {
10361074 const select = state . elements . paletteSelect ;
10371075 select . innerHTML = "" ;
@@ -1137,6 +1175,8 @@ function hydratePaletteFromRefIfPossible(state) {
11371175function updateEditGateDisabledState ( state ) {
11381176 const editable = isEditingEnabled ( state ) ;
11391177 const paletteLocked = isPaletteLocked ( state . project ) ;
1178+ const hasFrameSelection = Boolean ( getSelectedFrame ( state ) ) ;
1179+ const frameActionEnabled = editable && hasFrameSelection ;
11401180 const toolButtons = state . elements . toolButtons . querySelectorAll ( "[data-tool]" ) ;
11411181
11421182 toolButtons . forEach ( ( button ) => {
@@ -1147,18 +1187,22 @@ function updateEditGateDisabledState(state) {
11471187
11481188 state . elements . canvasWidthInput . disabled = ! editable ;
11491189 state . elements . canvasHeightInput . disabled = ! editable ;
1150- state . elements . addFrameButton . disabled = ! editable ;
1151- state . elements . duplicateFrameButton . disabled = ! editable ;
1152- state . elements . deleteFrameButton . disabled = ! editable ;
1153- state . elements . prevFrameButton . disabled = ! editable ;
1154- state . elements . nextFrameButton . disabled = ! editable ;
1155- state . elements . importPngButton . disabled = ! editable ;
1156- state . elements . exportPngButton . disabled = ! editable ;
1157- state . elements . exportSheetButton . disabled = ! editable ;
1190+ state . elements . addFrameButton . disabled = ! frameActionEnabled ;
1191+ state . elements . duplicateFrameButton . disabled = ! frameActionEnabled ;
1192+ state . elements . deleteFrameButton . disabled = ! frameActionEnabled ;
1193+ state . elements . prevFrameButton . disabled = ! frameActionEnabled ;
1194+ state . elements . nextFrameButton . disabled = ! frameActionEnabled ;
1195+ state . elements . importPngButton . disabled = ! frameActionEnabled ;
1196+ state . elements . exportPngButton . disabled = ! frameActionEnabled ;
1197+ state . elements . exportSheetButton . disabled = ! frameActionEnabled ;
11581198 state . elements . gridToggle . disabled = ! editable ;
11591199 state . elements . onionSkinToggle . disabled = ! editable ;
11601200 state . elements . undoButton . disabled = ! editable || state . history . undoStack . length === 0 ;
11611201 state . elements . redoButton . disabled = ! editable || state . history . redoStack . length === 0 ;
1202+ state . elements . playPreviewButton . disabled = ! hasFrameSelection ;
1203+ state . elements . pausePreviewButton . disabled = ! hasFrameSelection ;
1204+ state . elements . resetPreviewButton . disabled = ! hasFrameSelection ;
1205+ state . elements . fpsInput . disabled = ! hasFrameSelection ;
11621206 state . elements . color1SelectorButton . disabled = ! paletteLocked || ! normalizeProjectColor ( state . elements . color1SelectorButton . dataset . color ) ;
11631207 state . elements . color2SelectorButton . disabled = ! paletteLocked || ! normalizeProjectColor ( state . elements . color2SelectorButton . dataset . color ) ;
11641208 state . elements . colorPicker . disabled = true ;
@@ -1387,6 +1431,7 @@ function renderHud(state) {
13871431 state . elements . activeColorSwatch . style . backgroundSize = "12px 12px" ;
13881432
13891433 state . elements . frameCounter . textContent = `Frame ${ state . project . currentFrameIndex + 1 } / ${ state . project . frames . length } ` ;
1434+ state . elements . frameCounter . classList . toggle ( "is-frame-selected" , Boolean ( getSelectedFrame ( state ) ) ) ;
13901435 state . elements . pixelSizeValue . textContent = String ( state . project . pixelSize ) ;
13911436 state . elements . fpsValue . textContent = String ( state . preview . fps ) ;
13921437 state . elements . colorPicker . value = typeof state . project . activeColor === "string"
@@ -1442,31 +1487,37 @@ function renderEditor(state) {
14421487function renderPreview ( state ) {
14431488 const { previewCanvas } = state . elements ;
14441489 const project = state . project ;
1490+ const selectedFrame = getSelectedFrame ( state ) ;
14451491
14461492 const maxTarget = 220 ;
14471493 const previewScale = Math . max ( 1 , Math . floor ( maxTarget / Math . max ( project . width , project . height ) ) ) ;
1448- previewCanvas . width = project . width * previewScale ;
1449- previewCanvas . height = project . height * previewScale ;
1494+ const targetWidth = project . width * previewScale ;
1495+ const targetHeight = project . height * previewScale ;
1496+ if ( previewCanvas . width !== targetWidth ) {
1497+ previewCanvas . width = targetWidth ;
1498+ }
1499+ if ( previewCanvas . height !== targetHeight ) {
1500+ previewCanvas . height = targetHeight ;
1501+ }
14501502
14511503 const context = previewCanvas . getContext ( "2d" ) ;
14521504 if ( ! context ) {
14531505 return ;
14541506 }
14551507
1508+ context . clearRect ( 0 , 0 , previewCanvas . width , previewCanvas . height ) ;
14561509 createCheckerboard ( context , previewCanvas . width , previewCanvas . height , Math . max ( 6 , Math . floor ( previewScale * 1.5 ) ) ) ;
1510+ if ( ! selectedFrame ) {
1511+ return ;
1512+ }
14571513
14581514 const frameIndexToRender = state . preview . playing
1459- ? state . preview . frameIndex
1515+ ? clamp ( state . preview . frameIndex , 0 , project . frames . length - 1 , project . currentFrameIndex )
14601516 : project . currentFrameIndex ;
1461-
1462- drawFramePixels (
1463- context ,
1464- project . frames [ frameIndexToRender ] ,
1465- project . width ,
1466- project . height ,
1467- previewScale ,
1468- 1
1469- ) ;
1517+ const frameToRender = project . frames [ frameIndexToRender ] ?? selectedFrame ;
1518+ const frameCanvas = createImageFromFrame ( frameToRender , project . width , project . height ) ;
1519+ context . imageSmoothingEnabled = false ;
1520+ context . drawImage ( frameCanvas , 0 , 0 , project . width , project . height , 0 , 0 , previewCanvas . width , previewCanvas . height ) ;
14701521}
14711522
14721523function emitSpriteEditorControlReadiness ( state , options = { } ) {
@@ -1650,6 +1701,7 @@ function emitSpriteEditorControlReadiness(state, options = {}) {
16501701}
16511702
16521703function renderAll ( state ) {
1704+ ensureFrameSelection ( state ) ;
16531705 renderHud ( state ) ;
16541706 renderEditor ( state ) ;
16551707 renderPreview ( state ) ;
@@ -1952,8 +2004,8 @@ async function loadProjectJson(state, file) {
19522004
19532005 const validation = validateSpriteProjectAssets ( state ) ;
19542006 setSampleSource ( state , { mode : "tool" , fileName : file ?. name || "" } ) ;
2007+ ensureFrameSelection ( state , { preferFirstFrame : true } ) ;
19552008 syncControlsFromProject ( state ) ;
1956- state . preview . frameIndex = state . project . currentFrameIndex ;
19572009 setStatus ( state , `Loaded ${ file . name } (${ state . project . width } x${ state . project . height } , ${ state . project . frames . length } frames, ${ lockMessage } , validation: ${ summarizeAssetValidation ( validation ) } ).` ) ;
19582010 renderAll ( state ) ;
19592011}
@@ -2030,6 +2082,7 @@ function applySamplePreset(state, rawPreset, sampleId, samplePresetPath, sampleT
20302082 }
20312083
20322084 state . project = ensureProjectShape ( presetProject ) ;
2085+ ensureFrameSelection ( state , { preferFirstFrame : true } ) ;
20332086
20342087 const presetAssetRegistry = extractSpriteAssetRegistryFromSamplePreset ( rawPreset ) ;
20352088 const presetTitle = typeof sampleTitleHint === "string" && sampleTitleHint . trim ( )
@@ -3093,6 +3146,7 @@ export function initializeSpriteEditorApp() {
30933146 state . preview . fps = Number . isFinite ( Number ( snapshot ?. preview ?. fps ) ) ? Number ( snapshot . preview . fps ) : DEFAULT_FPS ;
30943147 state . preview . frameIndex = Number . isFinite ( Number ( snapshot ?. preview ?. frameIndex ) ) ? Number ( snapshot . preview . frameIndex ) : state . project . currentFrameIndex ;
30953148 state . preview . playing = snapshot ?. preview ?. playing === true ;
3149+ ensureFrameSelection ( state ) ;
30963150 setSampleSource ( state , { mode : "workspace" } ) ;
30973151 validateSpriteProjectAssets ( state ) ;
30983152 syncControlsFromProject ( state ) ;
0 commit comments