diff --git a/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx b/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx index d3296c400..69655bfe5 100644 --- a/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx +++ b/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx @@ -16,10 +16,10 @@ class ChartEditorNotePreview extends FlxSprite // Constants // static final NOTE_WIDTH:Int = 5; - static final NOTE_HEIGHT:Int = 1; static final WIDTH:Int = NOTE_WIDTH * 9; + static final NOTE_HEIGHT:Int = 1; - static final BG_COLOR:FlxColor = FlxColor.GRAY; + static final BG_COLOR:FlxColor = 0xFF606060; static final LEFT_COLOR:FlxColor = 0xFFFF22AA; static final DOWN_COLOR:FlxColor = 0xFF00EEFF; static final UP_COLOR:FlxColor = 0xFF00CC00; diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index c26fb8998..e1ed75b06 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -1,5 +1,6 @@ package funkin.ui.debug.charting; +import flixel.math.FlxMath; import haxe.ui.components.TextField; import haxe.ui.components.DropDown; import haxe.ui.components.NumberStepper; @@ -416,6 +417,7 @@ class ChartEditorState extends HaxeUIState // Make sure view is updated when we change view modes. noteDisplayDirty = true; notePreviewDirty = true; + notePreviewViewportBoundsDirty = true; this.scrollPositionInPixels = this.scrollPositionInPixels; return isViewDownscroll; @@ -482,6 +484,7 @@ class ChartEditorState extends HaxeUIState // Make sure view is updated when the variation changes. noteDisplayDirty = true; notePreviewDirty = true; + notePreviewViewportBoundsDirty = true; return selectedVariation; } @@ -498,6 +501,7 @@ class ChartEditorState extends HaxeUIState // Make sure view is updated when the difficulty changes. noteDisplayDirty = true; notePreviewDirty = true; + notePreviewViewportBoundsDirty = true; return selectedDifficulty; } @@ -514,6 +518,7 @@ class ChartEditorState extends HaxeUIState // Make sure view is updated when the character changes. noteDisplayDirty = true; notePreviewDirty = true; + notePreviewViewportBoundsDirty = true; return selectedCharacter; } @@ -531,6 +536,7 @@ class ChartEditorState extends HaxeUIState // Make sure view is updated when we change modes. noteDisplayDirty = true; notePreviewDirty = true; + notePreviewViewportBoundsDirty = true; this.scrollPositionInPixels = 0; return isInPatternMode; @@ -550,6 +556,8 @@ class ChartEditorState extends HaxeUIState */ var notePreviewDirty:Bool = true; + var notePreviewViewportBoundsDirty:Bool = true; + /** * Whether the chart has been modified since it was last saved. * Used to determine whether to auto-save, etc. @@ -673,6 +681,12 @@ class ChartEditorState extends HaxeUIState */ var gridPlayheadScrollAreaPressed:Bool = false; + /** + * Where the user's last mouse click was on the note preview scroll area. + * `null` if the user isn't clicking on the note preview. + */ + var notePreviewScrollAreaStartPos:FlxPoint = null; + /** * The SongNoteData which is currently being placed. * As the user drags, we will update this note's sustain length. @@ -1030,6 +1044,12 @@ class ChartEditorState extends HaxeUIState */ var selectionSquareBitmap:BitmapData = null; + /** + * The IMAGE used for the note preview bitmap. Updated by ChartEditorThemeHandler. + * The image is split and used for a 9-slice sprite for the box over the note preview. + */ + var notePreviewViewportBitmap:BitmapData = null; + /** * The tiled sprite used to display the grid. * The height is the length of the song, and scrolling is done by simply the sprite. @@ -1065,6 +1085,12 @@ class ChartEditorState extends HaxeUIState */ var notePreview:ChartEditorNotePreview; + /** + * The rectangular sprite used for representing the current viewport on the note preview. + * We move this up and down and resize it to represent the visible area. + */ + var notePreviewViewport:FlxSliceSprite; + /** * The rectangular sprite used for rendering the selection box. * Uses a 9-slice to stretch the selection box to the correct size without warping. @@ -1280,10 +1306,70 @@ class ChartEditorState extends HaxeUIState function buildNotePreview():Void { - var height:Int = FlxG.height - MENU_BAR_HEIGHT - GRID_TOP_PAD - 200; + var height:Int = FlxG.height - MENU_BAR_HEIGHT - GRID_TOP_PAD - PLAYBAR_HEIGHT - GRID_TOP_PAD - GRID_TOP_PAD; notePreview = new ChartEditorNotePreview(height); + notePreview.x = 350; notePreview.y = MENU_BAR_HEIGHT + GRID_TOP_PAD; - // add(notePreview); + add(notePreview); + + notePreviewViewport.scrollFactor.set(0, 0); + add(notePreviewViewport); + notePreviewViewport.zIndex = 30; + + setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); + } + + function calculateNotePreviewViewportBounds():FlxRect + { + var bounds:FlxRect = new FlxRect(); + + // Horizontal position and width are constant. + bounds.x = notePreview.x; + bounds.width = notePreview.width; + + // Vertical position depends on scroll position. + bounds.y = notePreview.y + (notePreview.height * (scrollPositionInPixels / songLengthInPixels)); + + // Height depends on the viewport size. + bounds.height = notePreview.height * (FlxG.height / songLengthInPixels); + + // Make sure the viewport doesn't go off the top or bottom of the note preview. + if (bounds.y < notePreview.y) + { + bounds.height -= notePreview.y - bounds.y; + bounds.y = notePreview.y; + } + else if (bounds.y + bounds.height > notePreview.y + notePreview.height) + { + bounds.height -= (bounds.y + bounds.height) - (notePreview.y + notePreview.height); + } + + var MIN_HEIGHT:Int = 8; + if (bounds.height < MIN_HEIGHT) + { + bounds.y -= MIN_HEIGHT - bounds.height; + bounds.height = MIN_HEIGHT; + } + + return bounds; + } + + function setNotePreviewViewportBounds(bounds:FlxRect = null):Void + { + if (bounds == null) + { + notePreviewViewport.visible = false; + notePreviewViewport.x = -9999; + notePreviewViewport.y = -9999; + } + else + { + notePreviewViewport.visible = true; + notePreviewViewport.x = bounds.x; + notePreviewViewport.y = bounds.y; + notePreviewViewport.width = bounds.width; + notePreviewViewport.height = bounds.height; + } } function buildSpectrogram(target:FlxSound):Void @@ -1622,7 +1708,7 @@ class ChartEditorState extends HaxeUIState handleToolboxes(); handlePlaybar(); handlePlayhead(); - // handleNotePreview(); + handleNotePreview(); handleFileKeybinds(); handleEditKeybinds(); @@ -1907,6 +1993,11 @@ class ChartEditorState extends HaxeUIState { gridPlayheadScrollAreaPressed = true; } + else if (FlxG.mouse.overlaps(notePreview)) + { + // Clicked note preview + notePreviewScrollAreaStartPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY); + } else if (!overlapsGrid || overlapsSelectionBorder) { selectionBoxStartPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY); @@ -1924,6 +2015,10 @@ class ChartEditorState extends HaxeUIState { Cursor.cursorMode = Grabbing; } + else if (notePreviewScrollAreaStartPos != null) + { + Cursor.cursorMode = Pointer; + } else if (FlxG.mouse.overlaps(gridPlayheadScrollArea)) { Cursor.cursorMode = Pointer; @@ -1938,6 +2033,11 @@ class ChartEditorState extends HaxeUIState gridPlayheadScrollAreaPressed = false; } + if (notePreviewScrollAreaStartPos != null && FlxG.mouse.released) + { + notePreviewScrollAreaStartPos = null; + } + if (gridPlayheadScrollAreaPressed) { // Clicked on the playhead scroll area. @@ -2185,6 +2285,14 @@ class ChartEditorState extends HaxeUIState } } } + else if (notePreviewScrollAreaStartPos != null) + { + trace('Updating current song time while clicking and holding...'); + var clickedPosInPixels:Float = FlxMath.remapToRange(FlxG.mouse.screenY, notePreview.y, notePreview.y + notePreview.height, 0, songLengthInPixels); + + scrollPositionInPixels = clickedPosInPixels; + moveSongToScrollPosition(); + } else if (currentPlaceNoteData != null) { // Handle extending the note as you drag. @@ -3214,7 +3322,6 @@ class ChartEditorState extends HaxeUIState */ function handleNotePreview():Void { - // TODO: Finish this. if (notePreviewDirty) { notePreviewDirty = false; @@ -3224,13 +3331,12 @@ class ChartEditorState extends HaxeUIState notePreview.addNotes(currentSongChartNoteData, Std.int(songLengthInMs)); notePreview.addEvents(currentSongChartEventData, Std.int(songLengthInMs)); } - } - /** - * Perform a spot update on the note preview, by editing the note preview - * only where necessary. More efficient than a full update. - */ - function updateNotePreview(note:SongNoteData, ?deleteNote:Bool = false):Void {} + if (notePreviewViewportBoundsDirty) + { + setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); + } + } /** * Handles passive behavior of the menu bar, such as updating labels or enabled/disabled status. @@ -3336,6 +3442,9 @@ class ChartEditorState extends HaxeUIState // We need to update the note sprites. noteDisplayDirty = true; + + // Update the note preview viewport box. + setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); } } @@ -3500,6 +3609,8 @@ class ChartEditorState extends HaxeUIState // Offset the selection box start position, if we are dragging. if (selectionBoxStartPos != null) selectionBoxStartPos.y -= diff; + // Update the note preview viewport box. + setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); return this.scrollPositionInPixels; } @@ -3759,6 +3870,9 @@ class ChartEditorState extends HaxeUIState loadSong(songMetadata, songChartData); + notePreviewDirty = true; + notePreviewViewportBoundsDirty = true; + if (audioInstTrack != null) { audioInstTrack.stop(); diff --git a/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx b/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx index 40c797169..9b1e82df1 100644 --- a/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx @@ -64,6 +64,14 @@ class ChartEditorThemeHandler static final PLAYHEAD_BLOCK_BORDER_COLOR:FlxColor = 0xFF9D0011; static final PLAYHEAD_BLOCK_FILL_COLOR:FlxColor = 0xFFBD0231; + // Border on the square over the note preview. + static final NOTE_PREVIEW_VIEWPORT_BORDER_COLOR_LIGHT = 0xFFF8A657; + static final NOTE_PREVIEW_VIEWPORT_BORDER_COLOR_DARK = 0xFFF8A657; + + // Fill on the square over the note preview. + static final NOTE_PREVIEW_VIEWPORT_FILL_COLOR_LIGHT = 0x80F8A657; + static final NOTE_PREVIEW_VIEWPORT_FILL_COLOR_DARK = 0x80F8A657; + static final TOTAL_COLUMN_COUNT:Int = ChartEditorState.STRUMLINE_SIZE * 2 + 1; /** @@ -75,6 +83,7 @@ class ChartEditorThemeHandler updateBackground(state); updateGridBitmap(state); updateSelectionSquare(state); + updateNotePreview(state); } /** @@ -116,7 +125,7 @@ class ChartEditorThemeHandler // 2 * (Strumline Size) + 1 grid squares wide, by (4 * quarter notes per measure) grid squares tall. // This gets reused to fill the screen. var gridWidth:Int = Std.int(ChartEditorState.GRID_SIZE * TOTAL_COLUMN_COUNT); - var gridHeight:Int = Std.int(ChartEditorState.GRID_SIZE * Conductor.stepsPerMeasure); + var gridHeight:Int = Std.int(ChartEditorState.GRID_SIZE * Conductor.stepsPerMeasure * state.currentZoomLevel); state.gridBitmap = FlxGridOverlay.createGrid(ChartEditorState.GRID_SIZE, ChartEditorState.GRID_SIZE, gridWidth, gridHeight, true, gridColor1, gridColor2); // Selection borders @@ -234,6 +243,39 @@ class ChartEditorThemeHandler 32, 32); } + static function updateNotePreview(state:ChartEditorState):Void + { + var viewportBorderColor:FlxColor = switch (state.currentTheme) + { + case Light: NOTE_PREVIEW_VIEWPORT_BORDER_COLOR_LIGHT; + case Dark: NOTE_PREVIEW_VIEWPORT_BORDER_COLOR_DARK; + default: NOTE_PREVIEW_VIEWPORT_BORDER_COLOR_LIGHT; + }; + + var viewportFillColor:FlxColor = switch (state.currentTheme) + { + case Light: NOTE_PREVIEW_VIEWPORT_FILL_COLOR_LIGHT; + case Dark: NOTE_PREVIEW_VIEWPORT_FILL_COLOR_DARK; + default: NOTE_PREVIEW_VIEWPORT_FILL_COLOR_LIGHT; + }; + + state.notePreviewViewportBitmap = new BitmapData(ChartEditorState.GRID_SIZE, ChartEditorState.GRID_SIZE, true); + + state.notePreviewViewportBitmap.fillRect(new Rectangle(0, 0, ChartEditorState.GRID_SIZE, ChartEditorState.GRID_SIZE), viewportBorderColor); + state.notePreviewViewportBitmap.fillRect(new Rectangle(SELECTION_SQUARE_BORDER_WIDTH, SELECTION_SQUARE_BORDER_WIDTH, + ChartEditorState.GRID_SIZE - (SELECTION_SQUARE_BORDER_WIDTH * 2), ChartEditorState.GRID_SIZE - (SELECTION_SQUARE_BORDER_WIDTH * 2)), + viewportFillColor); + + state.notePreviewViewport = new FlxSliceSprite(state.notePreviewViewportBitmap, + new FlxRect(SELECTION_SQUARE_BORDER_WIDTH + + 1, SELECTION_SQUARE_BORDER_WIDTH + + 1, ChartEditorState.GRID_SIZE + - (2 * SELECTION_SQUARE_BORDER_WIDTH + 2), + ChartEditorState.GRID_SIZE + - (2 * SELECTION_SQUARE_BORDER_WIDTH + 2)), + 32, 32); + } + public static function buildPlayheadBlock():FlxSprite { var playheadBlock:FlxSprite = new FlxSprite();