From 3c218ec01cfd96930dfa2f5717ec835ce6391656 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Sat, 22 Jul 2023 20:16:43 -0400
Subject: [PATCH] Done with BPM change fixes, currently working on rendering
 efficiency

---
 source/funkin/Conductor.hx                    |  25 +--
 source/funkin/FreeplayState.hx                |   2 +-
 source/funkin/InitState.hx                    |   2 +-
 source/funkin/MainMenuState.hx                |   2 +-
 source/funkin/TitleState.hx                   |   2 +-
 source/funkin/play/PlayState.hx               |  10 +-
 source/funkin/play/notes/Strumline.hx         |   8 +-
 source/funkin/play/notes/SustainTrail.hx      |  17 +-
 source/funkin/play/song/SongData.hx           |  14 ++
 .../debug/charting/ChartEditorEventSprite.hx  |  39 +++-
 .../charting/ChartEditorHoldNoteSprite.hx     | 143 +++++++++++++
 .../debug/charting/ChartEditorNoteSprite.hx   |  29 ++-
 .../ui/debug/charting/ChartEditorState.hx     | 199 ++++++++++--------
 .../ui/stageBuildShit/StageBuilderState.hx    |   2 +-
 source/funkin/util/Constants.hx               | 134 ++++++------
 15 files changed, 425 insertions(+), 203 deletions(-)
 create mode 100644 source/funkin/ui/debug/charting/ChartEditorHoldNoteSprite.hx

diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx
index c1d841623..3c6470373 100644
--- a/source/funkin/Conductor.hx
+++ b/source/funkin/Conductor.hx
@@ -3,7 +3,6 @@ package funkin;
 import funkin.util.Constants;
 import flixel.util.FlxSignal;
 import flixel.math.FlxMath;
-import funkin.SongLoad.SwagSong;
 import funkin.play.song.Song.SongDifficulty;
 import funkin.play.song.SongData.SongTimeChange;
 
@@ -13,12 +12,6 @@ import funkin.play.song.SongData.SongTimeChange;
  */
 class Conductor
 {
-  public static final PIXELS_PER_MS:Float = 0.45;
-  public static final HIT_WINDOW_MS:Float = 160;
-  public static final SECONDS_PER_MINUTE:Float = 60;
-  public static final MILLIS_PER_SECOND:Float = 1000;
-  public static final STEPS_PER_BEAT:Int = 4;
-
   // onBeatHit is called every quarter note
   // onStepHit is called every sixteenth note
   // 4/4 = 4 beats per measure = 16 steps per measure
@@ -82,18 +75,18 @@ class Conductor
   }
 
   /**
-   * Duration of a beat in milliseconds. Calculated based on bpm.
+   * Duration of a beat (quarter note) in milliseconds. Calculated based on bpm.
    */
   public static var beatLengthMs(get, null):Float;
 
   static function get_beatLengthMs():Float
   {
     // Tied directly to BPM.
-    return ((SECONDS_PER_MINUTE / bpm) * MILLIS_PER_SECOND);
+    return ((Constants.SECS_PER_MIN / bpm) * Constants.MS_PER_SEC);
   }
 
   /**
-   * Duration of a step (quarter) in milliseconds. Calculated based on bpm.
+   * Duration of a step (sixtennth note) in milliseconds. Calculated based on bpm.
    */
   public static var stepLengthMs(get, null):Float;
 
@@ -280,7 +273,8 @@ class Conductor
           {
             var prevTimeChange:SongTimeChange = timeChanges[timeChanges.length - 1];
             currentTimeChange.beatTime = prevTimeChange.beatTime
-              + ((currentTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC);
+              + ((currentTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC)
+              + 0.01;
           }
         }
       }
@@ -323,7 +317,8 @@ class Conductor
         }
       }
 
-      var resultFractionalStep:Float = (ms - lastTimeChange.timeStamp) / stepLengthMs;
+      var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
+      var resultFractionalStep:Float = (ms - lastTimeChange.timeStamp) / lastStepLengthMs;
       resultStep += resultFractionalStep; // Math.floor();
 
       return resultStep;
@@ -359,7 +354,8 @@ class Conductor
         }
       }
 
-      resultMs += (stepTime - lastTimeChange.beatTime * 4) * stepLengthMs;
+      var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
+      resultMs += (stepTime - lastTimeChange.beatTime * 4) * lastStepLengthMs;
 
       return resultMs;
     }
@@ -394,7 +390,8 @@ class Conductor
         }
       }
 
-      resultMs += (beatTime - lastTimeChange.beatTime) * stepLengthMs * Constants.STEPS_PER_BEAT;
+      var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
+      resultMs += (beatTime - lastTimeChange.beatTime) * lastStepLengthMs * Constants.STEPS_PER_BEAT;
 
       return resultMs;
     }
diff --git a/source/funkin/FreeplayState.hx b/source/funkin/FreeplayState.hx
index 1c226dbb5..608898a5f 100644
--- a/source/funkin/FreeplayState.hx
+++ b/source/funkin/FreeplayState.hx
@@ -127,7 +127,7 @@ class FreeplayState extends MusicBeatSubState
 
     if (FlxG.sound.music != null)
     {
-      if (!FlxG.sound.music.playing) FlxG.sound.playMusic(Paths.music('freakyMenu'));
+      if (!FlxG.sound.music.playing) FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
     }
 
     // if (StoryMenuState.weekUnlocked[2] || isDebug)
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index ce863bd0b..eeffebdb1 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -261,7 +261,7 @@ class InitState extends FlxTransitionableState
    */
   function startGameNormally():Void
   {
-    FlxG.sound.cache(Paths.music('freakyMenu'));
+    FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu'));
     FlxG.switchState(new TitleState());
   }
 
diff --git a/source/funkin/MainMenuState.hx b/source/funkin/MainMenuState.hx
index 020a121c0..2c251635c 100644
--- a/source/funkin/MainMenuState.hx
+++ b/source/funkin/MainMenuState.hx
@@ -56,7 +56,7 @@ class MainMenuState extends MusicBeatState
 
     if (!FlxG.sound.music.playing)
     {
-      FlxG.sound.playMusic(Paths.music('freakyMenu'));
+      FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
     }
 
     persistentUpdate = persistentDraw = true;
diff --git a/source/funkin/TitleState.hx b/source/funkin/TitleState.hx
index a19d09473..8ba5121fa 100644
--- a/source/funkin/TitleState.hx
+++ b/source/funkin/TitleState.hx
@@ -49,7 +49,7 @@ class TitleState extends MusicBeatState
     swagShader = new ColorSwap();
 
     curWacky = FlxG.random.getObject(getIntroTextShit());
-    FlxG.sound.cache(Paths.music('freakyMenu'));
+    FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu'));
 
     // DEBUG BULLSHIT
 
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index ae8d9ae86..c0705bd96 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1637,9 +1637,9 @@ class PlayState extends MusicBeatState
     {
       if (note == null) continue;
 
-      var hitWindowStart = note.strumTime - Conductor.HIT_WINDOW_MS;
+      var hitWindowStart = note.strumTime - Constants.HIT_WINDOW_MS;
       var hitWindowCenter = note.strumTime;
-      var hitWindowEnd = note.strumTime + Conductor.HIT_WINDOW_MS;
+      var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS;
 
       if (Conductor.songPosition > hitWindowEnd)
       {
@@ -1714,9 +1714,9 @@ class PlayState extends MusicBeatState
     {
       if (note == null || note.hasBeenHit) continue;
 
-      var hitWindowStart = note.strumTime - Conductor.HIT_WINDOW_MS;
+      var hitWindowStart = note.strumTime - Constants.HIT_WINDOW_MS;
       var hitWindowCenter = note.strumTime;
-      var hitWindowEnd = note.strumTime + Conductor.HIT_WINDOW_MS;
+      var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS;
 
       if (Conductor.songPosition > hitWindowEnd)
       {
@@ -2367,7 +2367,7 @@ class PlayState extends MusicBeatState
 
       if (targetSongId == null)
       {
-        FlxG.sound.playMusic(Paths.music('freakyMenu'));
+        FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
 
         transIn = FlxTransitionableState.defaultTransIn;
         transOut = FlxTransitionableState.defaultTransOut;
diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx
index 4fdf5afe3..454ec13e1 100644
--- a/source/funkin/play/notes/Strumline.hx
+++ b/source/funkin/play/notes/Strumline.hx
@@ -213,7 +213,7 @@ class Strumline extends FlxSpriteGroup
     var vwoosh:Float = (strumTime < Conductor.songPosition) && vwoosh ? 2.0 : 1.0;
     var scrollSpeed:Float = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0;
 
-    return Conductor.PIXELS_PER_MS * (Conductor.songPosition - strumTime) * scrollSpeed * vwoosh * (PreferencesMenu.getPref('downscroll') ? 1 : -1);
+    return Constants.PIXELS_PER_MS * (Conductor.songPosition - strumTime) * scrollSpeed * vwoosh * (PreferencesMenu.getPref('downscroll') ? 1 : -1);
   }
 
   function updateNotes():Void
@@ -273,7 +273,7 @@ class Strumline extends FlxSpriteGroup
         }
       }
 
-      var renderWindowEnd = holdNote.strumTime + holdNote.fullSustainLength + Conductor.HIT_WINDOW_MS + RENDER_DISTANCE_MS / 8;
+      var renderWindowEnd = holdNote.strumTime + holdNote.fullSustainLength + Constants.HIT_WINDOW_MS + RENDER_DISTANCE_MS / 8;
 
       if (holdNote.missedNote && Conductor.songPosition >= renderWindowEnd)
       {
@@ -308,7 +308,7 @@ class Strumline extends FlxSpriteGroup
         // Hold note was dropped before completing, keep it in its clipped state.
         holdNote.visible = true;
 
-        var yOffset:Float = (holdNote.fullSustainLength - holdNote.sustainLength) * Conductor.PIXELS_PER_MS;
+        var yOffset:Float = (holdNote.fullSustainLength - holdNote.sustainLength) * Constants.PIXELS_PER_MS;
 
         trace('yOffset: ' + yOffset);
         trace('holdNote.fullSustainLength: ' + holdNote.fullSustainLength);
@@ -678,7 +678,7 @@ class Strumline extends FlxSpriteGroup
     {
       // The note sprite pool is full and all note splashes are active.
       // We have to create a new note.
-      result = new SustainTrail(0, 100, noteStyle.getHoldNoteAssetPath(), noteStyle);
+      result = new SustainTrail(0, 100, noteStyle);
       this.holdNotes.add(result);
     }
 
diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx
index d9f1aab6e..fdd613667 100644
--- a/source/funkin/play/notes/SustainTrail.hx
+++ b/source/funkin/play/notes/SustainTrail.hx
@@ -88,9 +88,9 @@ class SustainTrail extends FlxSprite
    * @param SustainLength Length in milliseconds.
    * @param fileName
    */
-  public function new(noteDirection:NoteDirection, sustainLength:Float, fileName:String, noteStyle:NoteStyle)
+  public function new(noteDirection:NoteDirection, sustainLength:Float, noteStyle:NoteStyle)
   {
-    super(0, 0, fileName);
+    super(0, 0, noteStyle.getHoldNoteAssetPath());
 
     antialiasing = true;
 
@@ -111,7 +111,7 @@ class SustainTrail extends FlxSprite
 
     // CALCULATE SIZE
     width = graphic.width / 8 * zoom; // amount of notes * 2
-    height = sustainHeight(sustainLength, PlayState.instance.currentChart.scrollSpeed);
+    height = sustainHeight(sustainLength, getScrollSpeed());
     // instead of scrollSpeed, PlayState.SONG.speed
 
     flipY = PreferencesMenu.getPref('downscroll');
@@ -123,6 +123,13 @@ class SustainTrail extends FlxSprite
 
     updateClipping();
     indices = new DrawData<Int>(12, true, TRIANGLE_VERTEX_INDICES);
+
+    this.active = true; // This NEEDS to be true for the note to be drawn!
+  }
+
+  function getScrollSpeed():Float
+  {
+    return PlayState?.instance?.currentChart?.scrollSpeed ?? 1.0;
   }
 
   /**
@@ -139,7 +146,7 @@ class SustainTrail extends FlxSprite
   {
     if (s < 0) s = 0;
 
-    height = sustainHeight(s, PlayState.instance.currentChart.scrollSpeed);
+    height = sustainHeight(s, getScrollSpeed());
     updateColorTransform();
     updateClipping();
     return sustainLength = s;
@@ -152,7 +159,7 @@ class SustainTrail extends FlxSprite
    */
   public function updateClipping(songTime:Float = 0):Void
   {
-    var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), PlayState.instance.currentChart.scrollSpeed), 0, height);
+    var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), getScrollSpeed()), 0, height);
     if (clipHeight == 0)
     {
       visible = false;
diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx
index 5398f6c4b..c2a701ce9 100644
--- a/source/funkin/play/song/SongData.hx
+++ b/source/funkin/play/song/SongData.hx
@@ -386,6 +386,9 @@ abstract SongNoteData(RawSongNoteData)
       };
   }
 
+  /**
+   * The timestamp of the note, in milliseconds.
+   */
   public var time(get, set):Float;
 
   public function get_time():Float
@@ -398,6 +401,9 @@ abstract SongNoteData(RawSongNoteData)
     return this.t = value;
   }
 
+  /**
+   * The timestamp of the note, in steps.
+   */
   public var stepTime(get, never):Float;
 
   public function get_stepTime():Float
@@ -470,6 +476,10 @@ abstract SongNoteData(RawSongNoteData)
     return getStrumlineIndex(strumlineSize) == 0;
   }
 
+  /**
+   * If this is a hold note, this is the length of the hold note in milliseconds.
+   * @default 0 (not a hold note)
+   */
   public var length(get, set):Float;
 
   function get_length():Float
@@ -482,6 +492,10 @@ abstract SongNoteData(RawSongNoteData)
     return this.l = value;
   }
 
+  /**
+   * If this is a hold note, this is the length of the hold note in steps.
+   * @default 0 (not a hold note)
+   */
   public var stepLength(get, set):Float;
 
   function get_stepLength():Float
diff --git a/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx b/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx
index f671e98e1..0abee3715 100644
--- a/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx
@@ -67,8 +67,12 @@ class ChartEditorEventSprite extends FlxSprite
     // Push all the other events as frames.
     for (eventName in SongEventParser.listEventIds())
     {
+      var exists:Bool = Assets.exists(Paths.image('ui/chart-editor/events/$eventName'));
+      if (!exists) continue; // No graphic for this event.
+
       var frames:FlxAtlasFrames = Paths.getSparrowAtlas('ui/chart-editor/events/$eventName');
-      if (frames == null) continue; // No graphic for this event.
+      if (frames == null) continue; // Could not load graphic for this event.
+
       frames.parent.persist = true;
       for (frame in frames.frames)
       {
@@ -140,19 +144,34 @@ class ChartEditorEventSprite extends FlxSprite
   }
 
   /**
-   * Return whether this note (or its parent) is currently visible.
+   * Return whether this event is currently visible.
    */
-  public function isEventVisible(viewAreaBottom:Float, viewAreaTop:Float):Bool
+  public function isNoteVisible(viewAreaBottom:Float, viewAreaTop:Float):Bool
   {
-    var outsideViewArea = (this.y + this.height < viewAreaTop || this.y > viewAreaBottom);
+    // True if the note is above the view area.
+    var aboveViewArea = (this.y + this.height < viewAreaTop);
 
-    if (!outsideViewArea)
-    {
-      return true;
-    }
+    // True if the note is below the view area.
+    var belowViewArea = (this.y > viewAreaBottom);
 
-    // TODO: Check if this note's parent or child is visible.
+    return !aboveViewArea && !belowViewArea;
+  }
 
-    return false;
+  /**
+   * Return whether an event, if placed in the scene, would be visible.
+   */
+  public static function wouldNoteBeVisible(viewAreaBottom:Float, viewAreaTop:Float, eventData:SongEventData, ?origin:FlxObject):Bool
+  {
+    var noteHeight:Float = ChartEditorState.GRID_SIZE;
+    var notePosY:Float = eventData.stepTime * ChartEditorState.GRID_SIZE;
+    if (origin != null) notePosY += origin.y;
+
+    // True if the note is above the view area.
+    var aboveViewArea = (notePosY + noteHeight < viewAreaTop);
+
+    // True if the note is below the view area.
+    var belowViewArea = (notePosY > viewAreaBottom);
+
+    return !aboveViewArea && !belowViewArea;
   }
 }
diff --git a/source/funkin/ui/debug/charting/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/ChartEditorHoldNoteSprite.hx
new file mode 100644
index 000000000..38cdaffeb
--- /dev/null
+++ b/source/funkin/ui/debug/charting/ChartEditorHoldNoteSprite.hx
@@ -0,0 +1,143 @@
+package funkin.ui.debug.charting;
+
+import funkin.play.notes.Strumline;
+import funkin.data.notestyle.NoteStyleRegistry;
+import flixel.FlxObject;
+import flixel.FlxSprite;
+import flixel.graphics.frames.FlxFramesCollection;
+import flixel.graphics.frames.FlxTileFrames;
+import flixel.math.FlxPoint;
+import funkin.play.notes.SustainTrail;
+import funkin.play.song.SongData.SongNoteData;
+
+/**
+ * A hold note sprite that can be used to display a note in a chart.
+ * Designed to be used and reused efficiently. Has no gameplay functionality.
+ */
+class ChartEditorHoldNoteSprite extends SustainTrail
+{
+  /**
+   * The ChartEditorState this note belongs to.
+   */
+  public var parentState:ChartEditorState;
+
+  public function new(parent:ChartEditorState)
+  {
+    var noteStyle = NoteStyleRegistry.instance.fetchDefault();
+
+    super(0, 100, noteStyle);
+
+    this.parentState = parent;
+
+    zoom = 1.0;
+    zoom *= noteStyle.fetchHoldNoteScale();
+    zoom *= 0.7;
+    zoom *= ChartEditorState.GRID_SIZE / Strumline.STRUMLINE_SIZE;
+
+    setup();
+  }
+
+  /**
+   * Set the height directly, to a value in pixels.
+   * @param h The desired height in pixels.
+   */
+  public function setHeightDirectly(h:Float)
+  {
+    sustainLength = h / (getScrollSpeed() * Constants.PIXELS_PER_MS);
+    fullSustainLength = sustainLength;
+  }
+
+  function setup():Void
+  {
+    strumTime = 999999999;
+    missedNote = false;
+    hitNote = false;
+    visible = true;
+    alpha = 1.0;
+    width = graphic.width / 8 * zoom; // amount of notes * 2
+  }
+
+  public override function revive():Void
+  {
+    super.revive();
+
+    setup();
+  }
+
+  /**
+   * Return whether this note is currently visible.
+   */
+  public function isHoldNoteVisible(viewAreaBottom:Float, viewAreaTop:Float):Bool
+  {
+    // True if the note is above the view area.
+    var aboveViewArea = (this.y + this.height < viewAreaTop);
+
+    // True if the note is below the view area.
+    var belowViewArea = (this.y > viewAreaBottom);
+
+    return !aboveViewArea && !belowViewArea;
+  }
+
+  /**
+   * Return whether a hold note, if placed in the scene, would be visible.
+   */
+  public static function wouldHoldNoteBeVisible(viewAreaBottom:Float, viewAreaTop:Float, noteData:SongNoteData, ?origin:FlxObject):Bool
+  {
+    var noteHeight:Float = noteData.stepLength * ChartEditorState.GRID_SIZE;
+    var notePosY:Float = noteData.stepTime * ChartEditorState.GRID_SIZE;
+    if (origin != null) notePosY += origin.y;
+
+    // True if the note is above the view area.
+    var aboveViewArea = (notePosY + noteHeight < viewAreaTop);
+
+    // True if the note is below the view area.
+    var belowViewArea = (notePosY > viewAreaBottom);
+
+    return !aboveViewArea && !belowViewArea;
+  }
+
+  public function updateHoldNotePosition(?origin:FlxObject)
+  {
+    var cursorColumn:Int = this.noteData.data;
+
+    if (cursorColumn < 0) cursorColumn = 0;
+    if (cursorColumn >= (ChartEditorState.STRUMLINE_SIZE * 2 + 1))
+    {
+      cursorColumn = (ChartEditorState.STRUMLINE_SIZE * 2 + 1);
+    }
+    else
+    {
+      // Invert player and opponent columns.
+      if (cursorColumn >= ChartEditorState.STRUMLINE_SIZE)
+      {
+        cursorColumn -= ChartEditorState.STRUMLINE_SIZE;
+      }
+      else
+      {
+        cursorColumn += ChartEditorState.STRUMLINE_SIZE;
+      }
+    }
+
+    this.x = cursorColumn * ChartEditorState.GRID_SIZE;
+
+    // Notes far in the song will start far down, but the group they belong to will have a high negative offset.
+    if (this.noteData.stepTime >= 0)
+    {
+      // noteData.stepTime is a calculated value which accounts for BPM changes
+      var stepTime:Float = this.noteData.stepTime;
+      var roundedStepTime:Float = Math.floor(stepTime + 0.01); // Add epsilon to fix rounding issues
+      this.y = roundedStepTime * ChartEditorState.GRID_SIZE;
+    }
+
+    this.x += ChartEditorState.GRID_SIZE / 2;
+    this.x -= this.width / 2;
+
+    this.y += ChartEditorState.GRID_SIZE / 2;
+
+    if (origin != null)
+    {
+      this.x += origin.x;
+      this.y += origin.y;
+    }
+  }
+}
diff --git a/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx b/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx
index aa0b97270..14ffa3a76 100644
--- a/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx
@@ -204,15 +204,30 @@ class ChartEditorNoteSprite extends FlxSprite
    */
   public function isNoteVisible(viewAreaBottom:Float, viewAreaTop:Float):Bool
   {
-    var outsideViewArea = (this.y + this.height < viewAreaTop || this.y > viewAreaBottom);
+    // True if the note is above the view area.
+    var aboveViewArea = (this.y + this.height < viewAreaTop);
 
-    if (!outsideViewArea)
-    {
-      return true;
-    }
+    // True if the note is below the view area.
+    var belowViewArea = (this.y > viewAreaBottom);
 
-    // TODO: Check if this note's parent or child is visible.
+    return !aboveViewArea && !belowViewArea;
+  }
 
-    return false;
+  /**
+   * Return whether a note, if placed in the scene, would be visible.
+   */
+  public static function wouldNoteBeVisible(viewAreaBottom:Float, viewAreaTop:Float, noteData:SongNoteData, ?origin:FlxObject):Bool
+  {
+    var noteHeight:Float = ChartEditorState.GRID_SIZE;
+    var notePosY:Float = noteData.stepTime * ChartEditorState.GRID_SIZE;
+    if (origin != null) notePosY += origin.y;
+
+    // True if the note is above the view area.
+    var aboveViewArea = (notePosY + noteHeight < viewAreaTop);
+
+    // True if the note is below the view area.
+    var belowViewArea = (notePosY > viewAreaBottom);
+
+    return !aboveViewArea && !belowViewArea;
   }
 }
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 41dbb0e0f..cda2b09df 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -1,19 +1,10 @@
 package funkin.ui.debug.charting;
 
-import funkin.graphics.rendering.SustainTrail;
-import funkin.util.SortUtil;
-import funkin.data.notestyle.NoteStyleRegistry;
-import funkin.ui.debug.charting.ChartEditorCommand;
-import flixel.input.keyboard.FlxKey;
-import funkin.input.TurboKeyHandler;
-import haxe.ui.notifications.NotificationType;
-import haxe.ui.notifications.NotificationManager;
-import haxe.DynamicAccess;
-import haxe.io.Path;
 import flixel.addons.display.FlxSliceSprite;
 import flixel.addons.display.FlxTiledSprite;
 import flixel.FlxSprite;
 import flixel.group.FlxSpriteGroup;
+import flixel.input.keyboard.FlxKey;
 import flixel.math.FlxPoint;
 import flixel.math.FlxRect;
 import flixel.sound.FlxSound;
@@ -22,10 +13,13 @@ import flixel.util.FlxSort;
 import flixel.util.FlxTimer;
 import funkin.audio.visualize.PolygonSpectogram;
 import funkin.audio.VoicesGroup;
+import funkin.data.notestyle.NoteStyleRegistry;
 import funkin.input.Cursor;
+import funkin.input.TurboKeyHandler;
 import funkin.modding.events.ScriptEvent;
 import funkin.play.HealthIcon;
 import funkin.play.notes.NoteSprite;
+import funkin.play.notes.Strumline;
 import funkin.play.song.Song;
 import funkin.play.song.SongData.SongChartData;
 import funkin.play.song.SongData.SongDataParser;
@@ -33,13 +27,18 @@ import funkin.play.song.SongData.SongEventData;
 import funkin.play.song.SongData.SongMetadata;
 import funkin.play.song.SongData.SongNoteData;
 import funkin.play.song.SongDataUtils;
+import funkin.ui.debug.charting.ChartEditorCommand;
 import funkin.ui.debug.charting.ChartEditorThemeHandler.ChartEditorTheme;
 import funkin.ui.debug.charting.ChartEditorToolboxHandler.ChartEditorToolMode;
 import funkin.ui.haxeui.components.CharacterPlayer;
 import funkin.ui.haxeui.HaxeUIState;
-import funkin.util.FileUtil;
 import funkin.util.DateUtil;
+import funkin.util.FileUtil;
 import funkin.util.SerializerUtil;
+import funkin.util.SortUtil;
+import funkin.util.WindowUtil;
+import haxe.DynamicAccess;
+import haxe.io.Path;
 import haxe.ui.components.Label;
 import haxe.ui.components.Slider;
 import haxe.ui.containers.dialogs.Dialog;
@@ -50,7 +49,8 @@ import haxe.ui.core.Component;
 import haxe.ui.core.Screen;
 import haxe.ui.events.DragEvent;
 import haxe.ui.events.UIEvent;
-import funkin.util.WindowUtil;
+import haxe.ui.notifications.NotificationManager;
+import haxe.ui.notifications.NotificationType;
 import openfl.display.BitmapData;
 import openfl.geom.Rectangle;
 
@@ -112,7 +112,12 @@ class ChartEditorState extends HaxeUIState
   /**
    * The height of the menu bar in the layout.
    */
-  static final MENU_BAR_HEIGHT = 32;
+  static final MENU_BAR_HEIGHT:Int = 32;
+
+  /**
+   * The height of the playbar in the layout.
+   */
+  static final PLAYBAR_HEIGHT:Int = 48;
 
   /**
    * Duration to wait before autosaving the chart.
@@ -946,7 +951,12 @@ class ChartEditorState extends HaxeUIState
    */
   var renderedNotes:FlxTypedSpriteGroup<ChartEditorNoteSprite>;
 
-  var renderedHoldNotes:FlxTypedSpriteGroup<SustainTrail>;
+  /**
+   * The sprite group containing the hold note graphics.
+   * Only displays a subset of the data from `currentSongChartNoteData`,
+   * and kills notes that are off-screen to be recycled later.
+   */
+  var renderedHoldNotes:FlxTypedSpriteGroup<ChartEditorHoldNoteSprite>;
 
   /**
    * The sprite group containing the song events.
@@ -1032,7 +1042,7 @@ class ChartEditorState extends HaxeUIState
 
     gridGhostNote = new ChartEditorNoteSprite(this);
     gridGhostNote.alpha = 0.6;
-    gridGhostNote.noteData = new SongNoteData(-1, -1, 0, "");
+    gridGhostNote.noteData = new SongNoteData(0, 0, 0, "");
     gridGhostNote.visible = false;
     add(gridGhostNote);
 
@@ -1127,6 +1137,10 @@ class ChartEditorState extends HaxeUIState
    */
   function buildNoteGroup():Void
   {
+    renderedHoldNotes = new FlxTypedSpriteGroup<ChartEditorHoldNoteSprite>();
+    renderedHoldNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y);
+    add(renderedHoldNotes);
+
     renderedNotes = new FlxTypedSpriteGroup<ChartEditorNoteSprite>();
     renderedNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y);
     add(renderedNotes);
@@ -1704,12 +1718,9 @@ class ChartEditorState extends HaxeUIState
         moveSongToScrollPosition();
       }
 
-      // Cursor position snapped to the grid.
-
       // The song position of the cursor, in steps.
       var cursorFractionalStep:Float = cursorY / GRID_SIZE / (16 / noteSnapQuant);
-      var cursorStep:Int = Std.int(Math.floor(cursorFractionalStep));
-      var cursorMs:Float = Conductor.getStepTimeInMs(cursorStep);
+      var cursorMs:Float = Conductor.getStepTimeInMs(cursorFractionalStep);
       // The direction value for the column at the cursor.
       var cursorColumn:Int = Math.floor(cursorX / GRID_SIZE);
       if (cursorColumn < 0) cursorColumn = 0;
@@ -2145,10 +2156,10 @@ class ChartEditorState extends HaxeUIState
       // Update for whether downscroll is enabled.
       renderedNotes.flipX = (isViewDownscroll);
 
-      // Calculate the view bounds.
-      var viewAreaTop:Float = this.scrollPositionInPixels - GRID_TOP_PAD;
-      var viewHeight:Float = (FlxG.height - MENU_BAR_HEIGHT);
-      var viewAreaBottom:Float = this.scrollPositionInPixels + viewHeight;
+      // Calculate the top and bottom of the view area.
+      var viewAreaTopPixels:Float = MENU_BAR_HEIGHT;
+      var visibleGridHeightPixels:Float = FlxG.height - MENU_BAR_HEIGHT - PLAYBAR_HEIGHT; // The area underneath the menu bar and playbar is not visible.
+      var viewAreaBottomPixels:Float = viewAreaTopPixels + visibleGridHeightPixels;
 
       // Remove notes that are no longer visible and list the ones that are.
       var displayedNoteData:Array<SongNoteData> = [];
@@ -2156,7 +2167,7 @@ class ChartEditorState extends HaxeUIState
       {
         if (noteSprite == null || !noteSprite.exists || !noteSprite.visible) continue;
 
-        if (!noteSprite.isNoteVisible(viewAreaBottom, viewAreaTop))
+        if (!noteSprite.isNoteVisible(viewAreaBottomPixels, viewAreaTopPixels))
         {
           // This sprite is off-screen.
           // Kill the note sprite and recycle it.
@@ -2168,18 +2179,6 @@ class ChartEditorState extends HaxeUIState
           // Kill the note sprite and recycle it.
           noteSprite.noteData = null;
         }
-          // else if (noteSprite.noteData.length > 0 && (noteSprite.parentNoteSprite == null && noteSprite.childNoteSprite == null))
-          // {
-          //   // Note was extended.
-          //   // Kill the note sprite and recycle it.
-          //   noteSprite.noteData = null;
-          // }
-          // else if (noteSprite.noteData.length == 0 && (noteSprite.parentNoteSprite != null || noteSprite.childNoteSprite != null))
-          // {
-          //   // Note was shortened.
-          //   // Kill the note sprite and recycle it.
-          //   noteSprite.noteData = null;
-        // }
         else
         {
           // Note is already displayed and should remain displayed.
@@ -2190,13 +2189,42 @@ class ChartEditorState extends HaxeUIState
         }
       }
 
+      var displayedHoldNoteData:Array<SongNoteData> = [];
+      for (holdNoteSprite in renderedHoldNotes.members)
+      {
+        if (holdNoteSprite == null || !holdNoteSprite.exists || !holdNoteSprite.visible) continue;
+
+        if (!holdNoteSprite.isHoldNoteVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD))
+        {
+          holdNoteSprite.kill();
+        }
+        else if (currentSongChartNoteData.indexOf(holdNoteSprite.noteData) == -1 || holdNoteSprite.noteData.length == 0)
+        {
+          // This hold note was deleted.
+          // Kill the hold note sprite and recycle it.
+          holdNoteSprite.kill();
+        }
+        else if (displayedHoldNoteData.indexOf(holdNoteSprite.noteData) != -1)
+        {
+          // This hold note is a duplicate.
+          // Kill the hold note sprite and recycle it.
+          holdNoteSprite.kill();
+        }
+        else
+        {
+          displayedHoldNoteData.push(holdNoteSprite.noteData);
+          // Update the event sprite's position.
+          holdNoteSprite.updateHoldNotePosition(renderedNotes);
+        }
+      }
+
       // Remove events that are no longer visible and list the ones that are.
       var displayedEventData:Array<SongEventData> = [];
       for (eventSprite in renderedEvents.members)
       {
         if (eventSprite == null || !eventSprite.exists || !eventSprite.visible) continue;
 
-        if (!eventSprite.isEventVisible(viewAreaBottom, viewAreaTop))
+        if (!eventSprite.isEventVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD))
         {
           // This sprite is off-screen.
           // Kill the event sprite and recycle it.
@@ -2227,63 +2255,36 @@ class ChartEditorState extends HaxeUIState
           continue;
         }
 
-        // Get the position the note should be at.
-        var noteTimePixels:Float = noteData.stepTime * GRID_SIZE;
-
-        // Make sure the note appears when scrolling up.
-        var modifiedViewAreaTop = viewAreaTop - GRID_SIZE;
-
-        if (noteTimePixels < modifiedViewAreaTop || noteTimePixels > viewAreaBottom) continue;
-
-        // Else, this note is visible and we need to render it!
+        if (!ChartEditorNoteSprite.wouldNoteBeVisible(viewAreaBottomPixels, viewAreaTopPixels, noteData,
+          renderedNotes)) continue; // Else, this note is visible and we need to render it!
 
         // Get a note sprite from the pool.
         // If we can reuse a deleted note, do so.
         // If a new note is needed, call buildNoteSprite.
         var noteSprite:ChartEditorNoteSprite = renderedNotes.recycle(() -> new ChartEditorNoteSprite(this));
+        trace('Creating new Note... (${renderedNotes.members.length})');
         noteSprite.parentState = this;
 
         // The note sprite handles animation playback and positioning.
         noteSprite.noteData = noteData;
 
         // Setting note data resets position relative to the grid so we fix that.
-        noteSprite.x += renderedNotes.x;
-        noteSprite.y += renderedNotes.y;
+        noteSprite.updateNotePosition(renderedNotes);
 
-        // TODO: Replace this with SustainTrail.
-        if (noteSprite.noteData.length > 0)
+        // Add hold notes that are now visible (and not already displayed).
+        if (noteSprite.noteData.length > 0 && displayedHoldNoteData.indexOf(noteData) == -1)
         {
-          var holdNoteSprite:SustainTrail = renderedHoldNotes.recycle(() -> new SustainTrail(this));
+          var holdNoteSprite:ChartEditorHoldNoteSprite = renderedHoldNotes.recycle(() -> new ChartEditorHoldNoteSprite(this));
+          trace('Creating new HoldNote... (${renderedHoldNotes.members.length})');
 
           var noteLengthPixels:Float = noteSprite.noteData.stepLength * GRID_SIZE;
 
-          // If the note is a hold, we need to make sure it's long enough.
-          // var noteLengthSteps:Float = ;
-          // var lastNoteSprite:ChartEditorNoteSprite = noteSprite;
-          //
-          // while (noteLengthSteps > 0)
-          // {
-          //   if (noteLengthSteps <= 1.0)
-          //   {
-          //     // Last note in the hold.
-          //     // TODO: We may need to make it shorter and clip it visually.
-          //   }
-          //
-          //   var nextNoteSprite:ChartEditorNoteSprite = renderedNotes.recycle(ChartEditorNoteSprite);
-          //   nextNoteSprite.parentState = this;
-          //   nextNoteSprite.parentNoteSprite = lastNoteSprite;
-          //   lastNoteSprite.childNoteSprite = nextNoteSprite;
-          //
-          //   lastNoteSprite = nextNoteSprite;
-          //
-          //   noteLengthSteps -= 1;
-          // }
-          //
-          // // Make sure the last note sprite shows the end cap properly.
-          // lastNoteSprite.childNoteSprite = null;
+          holdNoteSprite.noteData = noteSprite.noteData;
+          holdNoteSprite.noteDirection = noteSprite.noteData.getDirection();
 
-          // var noteLengthPixels:Float = (noteLengthMs / Conductor.stepLengthMs + 1) * GRID_SIZE;
-          // add(new FlxSprite(noteSprite.x, noteSprite.y - renderedNotes.y + noteLengthPixels).makeGraphic(40, 2, 0xFFFF0000));
+          holdNoteSprite.setHeightDirectly(noteLengthPixels);
+
+          holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
         }
       }
 
@@ -2296,13 +2297,7 @@ class ChartEditorState extends HaxeUIState
           continue;
         }
 
-        // Get the position the event should be at.
-        var eventTimePixels:Float = eventData.stepTime * GRID_SIZE;
-
-        // Make sure the event appears when scrolling up.
-        var modifiedViewAreaTop = viewAreaTop - GRID_SIZE;
-
-        if (eventTimePixels < modifiedViewAreaTop || eventTimePixels > viewAreaBottom) continue;
+        if (!ChartEditorEventSprite.wouldEventBeVisible(viewAreaBottomPixels, viewAreaTopPixels, eventData, renderedNotes)) continue;
 
         // Else, this event is visible and we need to render it!
 
@@ -2311,6 +2306,7 @@ class ChartEditorState extends HaxeUIState
         // If a new event is needed, call buildEventSprite.
         var eventSprite:ChartEditorEventSprite = renderedEvents.recycle(() -> new ChartEditorEventSprite(this), false, true);
         eventSprite.parentState = this;
+        trace('Creating new Event... (${renderedEvents.members.length})');
 
         // The event sprite handles animation playback and positioning.
         eventSprite.eventData = eventData;
@@ -2320,6 +2316,34 @@ class ChartEditorState extends HaxeUIState
         eventSprite.y += renderedEvents.y;
       }
 
+      // Add hold notes that have been made visible (but not their parents)
+      for (noteData in currentSongChartNoteData)
+      {
+        // Is the note a hold note?
+        if (noteData.length <= 0) continue;
+
+        // Is the hold note rendered already?
+        if (displayedHoldNoteData.indexOf(noteData) != -1) continue;
+
+        // Is the hold note offscreen?
+        if (!ChartEditorHoldNoteSprite.wouldHoldNoteBeVisible(viewAreaBottomPixels, viewAreaTopPixels, noteData, renderedHoldNotes)) continue;
+
+        // Hold note should be rendered.
+        var holdNoteSprite:ChartEditorHoldNoteSprite = renderedHoldNotes.recycle(() -> new ChartEditorHoldNoteSprite(this));
+        trace('Creating new HoldNote... (${renderedHoldNotes.members.length})');
+
+        var noteLengthPixels:Float = noteData.stepLength * GRID_SIZE;
+
+        holdNoteSprite.noteData = noteData;
+        holdNoteSprite.noteDirection = noteData.getDirection();
+
+        holdNoteSprite.setHeightDirectly(noteLengthPixels);
+
+        holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
+
+        displayedHoldNoteData.push(noteData);
+      }
+
       // Destroy all existing selection squares.
       for (member in renderedSelectionSquares.members)
       {
@@ -2958,6 +2982,7 @@ class ChartEditorState extends HaxeUIState
     }
     // Move the rendered notes to the correct position.
     renderedNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y);
+    renderedHoldNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y);
     renderedEvents.setPosition(gridTiledSprite.x, gridTiledSprite.y);
     renderedSelectionSquares.setPosition(gridTiledSprite.x, gridTiledSprite.y);
     if (gridSpectrogram != null)
@@ -2974,6 +2999,7 @@ class ChartEditorState extends HaxeUIState
    * Loads an instrumental from an absolute file path, replacing the current instrumental.
    *
    * @param path The absolute path to the audio file.
+   *
    * @return Success or failure.
    */
   public function loadInstrumentalFromPath(path:Path):Bool
@@ -3114,7 +3140,7 @@ class ChartEditorState extends HaxeUIState
     for (metadata in rawSongMetadata)
     {
       var variation = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation;
-      this.songMetadata.set(variation, metadata);
+      this.songMetadata.set(variation, Reflect.copy(metadata));
     }
 
     this.songChartData = new Map<String, SongChartData>();
@@ -3154,7 +3180,8 @@ class ChartEditorState extends HaxeUIState
   function moveSongToScrollPosition():Void
   {
     // Update the songPosition in the Conductor.
-    Conductor.update(scrollPositionInMs);
+    var targetPos = scrollPositionInMs;
+    Conductor.update(targetPos);
 
     // Update the songPosition in the audio tracks.
     if (audioInstTrack != null) audioInstTrack.time = scrollPositionInMs + playheadPositionInMs;
diff --git a/source/funkin/ui/stageBuildShit/StageBuilderState.hx b/source/funkin/ui/stageBuildShit/StageBuilderState.hx
index de874e5ff..31a73ff8f 100644
--- a/source/funkin/ui/stageBuildShit/StageBuilderState.hx
+++ b/source/funkin/ui/stageBuildShit/StageBuilderState.hx
@@ -66,7 +66,7 @@ class StageBuilderState extends MusicBeatState
 
     // snd.addEventListener(SampleDataEvent.SAMPLE_DATA, sineShit);
     // snd.__buffer.
-    // snd = Assets.getSound(Paths.music('freakyMenu'));
+    // snd = Assets.getSound(Paths.music('freakyMenu/freakyMenu'));
     // for (thing in snd.load)
     // thing = Std.int(thing / 2);
     // snd.play();
diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx
index d5174e74f..a0063741b 100644
--- a/source/funkin/util/Constants.hx
+++ b/source/funkin/util/Constants.hx
@@ -122,6 +122,69 @@ class Constants
    */
   public static final DEFAULT_VARIATION:String = 'default';
 
+  /**
+   * The default intensity for camera zooms.
+   */
+  public static final DEFAULT_ZOOM_INTENSITY:Float = 0.015;
+
+  /**
+   * The default rate for camera zooms (in beats per zoom).
+   */
+  public static final DEFAULT_ZOOM_RATE:Int = 4;
+
+  /**
+   * The default BPM for charts, so things don't break if none is specified.
+   */
+  public static final DEFAULT_BPM:Int = 100;
+
+  /**
+   * Default numerator for the time signature.
+   */
+  public static final DEFAULT_TIME_SIGNATURE_NUM:Int = 4;
+
+  /**
+   * Default denominator for the time signature.
+   */
+  public static final DEFAULT_TIME_SIGNATURE_DEN:Int = 4;
+
+  /**
+   * TIMING
+   */
+  // ==============================
+
+  /**
+   * A magic number used when calculating scroll speed and note distances.
+   */
+  public static final PIXELS_PER_MS:Float = 0.45;
+
+  /**
+   * The maximum interval within which a note can be hit, in milliseconds.
+   */
+  public static final HIT_WINDOW_MS:Float = 160;
+
+  /**
+   * Constant for the number of seconds in a minute.
+   */
+  public static final SECS_PER_MIN:Float = 60;
+
+  /**
+   * Constant for the number of milliseconds in a second.
+   */
+  public static final MS_PER_SEC:Float = 1000;
+
+  /**
+   * The number of steps in one beat.
+   *
+   * Each beat represents ONE quarter note, so one step is one sixteenth note!
+   */
+  public static final STEPS_PER_BEAT:Int = 4;
+
+  /**
+   * All MP3 decoders introduce a playback delay of `528` samples,
+   * which at 44,100 Hz (samples per second) is ~12 ms.
+   */
+  public static final MP3_DELAY_MS:Float = 528 / 44100 * Constants.MS_PER_SEC;
+
   /**
    * HEALTH VALUES
    */
@@ -205,65 +268,12 @@ class Constants
    * OTHER
    */
   // ==============================
+
+  /**
+   * The separator between an asset library and the asset path.
+   */
   public static final LIBRARY_SEPARATOR:String = ':';
 
-  /**
-   * The number of seconds in a minute.
-   */
-  public static final SECS_PER_MIN:Int = 60;
-
-  /**
-   * The number of milliseconds in a second.
-   */
-  public static final MS_PER_SEC:Int = 1000;
-
-  /**
-   * The number of microseconds in a millisecond.
-   */
-  public static final US_PER_MS:Int = 1000;
-
-  /**
-   * The number of microseconds in a second.
-   */
-  public static final US_PER_SEC:Int = US_PER_MS * MS_PER_SEC;
-
-  /**
-   * The number of nanoseconds in a microsecond.
-   */
-  public static final NS_PER_US:Int = 1000;
-
-  /**
-   * The number of nanoseconds in a millisecond.
-   */
-  public static final NS_PER_MS:Int = NS_PER_US * US_PER_MS;
-
-  /**
-   * The number of nanoseconds in a second.
-   */
-  public static final NS_PER_SEC:Int = NS_PER_US * US_PER_MS * MS_PER_SEC;
-
-  /**
-   * All MP3 decoders introduce a playback delay of `528` samples,
-   * which at 44,100 Hz (samples per second) is ~12 ms.
-   */
-  public static final MP3_DELAY_MS:Float = 528 / 44100 * MS_PER_SEC;
-
-  /**
-   * The default BPM of the conductor.
-   */
-  public static final DEFAULT_BPM:Float = 100.0;
-
-  public static final DEFAULT_TIME_SIGNATURE_NUM:Int = 4;
-
-  public static final DEFAULT_TIME_SIGNATURE_DEN:Int = 4;
-
-  public static final STEPS_PER_BEAT:Int = 4;
-
-  /**
-   * OTHER
-   */
-  // ==============================
-
   /**
    * The scale factor to use when increasing the size of pixel art graphics.
    */
@@ -276,14 +286,4 @@ class Constants
 
   public static final STRUMLINE_X_OFFSET:Float = 48;
   public static final STRUMLINE_Y_OFFSET:Float = 24;
-
-  /**
-   * The default intensity for camera zooms.
-   */
-  public static final DEFAULT_ZOOM_INTENSITY:Float = 0.015;
-
-  /**
-   * The default rate for camera zooms (in beats per zoom).
-   */
-  public static final DEFAULT_ZOOM_RATE:Int = 4;
 }