From 59d4715d826e2bf368c08b7e46f5abee5d9d8e76 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 24 Feb 2026 21:13:56 +0000
Subject: [PATCH 1/3] Initial plan
From 3e8701de631d70a7112e31c13fa4da31b3dca948 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 24 Feb 2026 21:31:12 +0000
Subject: [PATCH 2/3] Add configurable easing_curve property to all visual
effects
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Replace hard-coded envelope/easing logic in every effect runner with a
configurable AnimationCurve property named easing_curve.
- Add EntityCurveExtensions.cs with GetAnimationCurve extension method
- Add GetAnimationCurve stub to Entity in CiGameTypeStubs.cs
- For all 13 effects: add easing_curve to template properties, read it in
TrySchedule, pass to runner Initialize, store in _easingCurve field, and
use _easingCurve.Evaluate(progress) in LateUpdate
- Default curves exactly replicate the original hard-coded behaviour:
- CameraShake: linear decay (0,1)→(1,0) tangents -1
- CameraTilt: sine bell via keyframes at 0/0.5/1 with π tangents
- ColorTint/Fog/Hsl/Sepia/Vignette: 20%/80% trapezoid slopes ±5
- Letterbox/PixelGrid/Scanlines/ScreenNoise: 15%/85% trapezoid slopes ±20/3
- ZoomIn/ZoomOut: SmoothStep 20%/80% trapezoid (all tangents 0)
- ZoomPulse: triangle peaking at 30%, slopes 10/3 and -10/7
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Core/EntityCurveExtensions.cs | 22 ++++++++++++++++
.../Effects/CameraShake/CameraShakeEffect.cs | 17 ++++++++++---
.../Effects/CameraTilt/CameraTiltEffect.cs | 20 +++++++++++----
.../Effects/ColorTint/ColorTintEffect.cs | 25 +++++++++++--------
BopVisualEffects/Effects/Fog/FogEffect.cs | 25 +++++++++++--------
BopVisualEffects/Effects/Hsl/HslEffect.cs | 25 +++++++++++--------
.../Effects/Letterbox/LetterboxEffect.cs | 25 +++++++++++--------
.../Effects/PixelGrid/PixelGridEffect.cs | 25 +++++++++++--------
.../Effects/Scanlines/ScanlinesEffect.cs | 25 +++++++++++--------
.../Effects/ScreenNoise/ScreenNoiseEffect.cs | 25 +++++++++++--------
BopVisualEffects/Effects/Sepia/SepiaEffect.cs | 25 +++++++++++--------
.../Effects/Vignette/VignetteEffect.cs | 25 +++++++++++--------
.../Effects/ZoomIn/ZoomInEffect.cs | 25 +++++++++++--------
.../Effects/ZoomOut/ZoomOutEffect.cs | 25 +++++++++++--------
.../Effects/ZoomPulse/ZoomPulseEffect.cs | 20 ++++++++++-----
BopVisualEffects/Stubs/CiGameTypeStubs.cs | 12 +++++++++
16 files changed, 241 insertions(+), 125 deletions(-)
create mode 100644 BopVisualEffects/Core/EntityCurveExtensions.cs
diff --git a/BopVisualEffects/Core/EntityCurveExtensions.cs b/BopVisualEffects/Core/EntityCurveExtensions.cs
new file mode 100644
index 0000000..51f32dd
--- /dev/null
+++ b/BopVisualEffects/Core/EntityCurveExtensions.cs
@@ -0,0 +1,22 @@
+using UnityEngine;
+
+namespace BopVisualEffects.Core;
+
+///
+/// Extension helper for reading values from properties.
+///
+internal static class EntityCurveExtensions
+{
+ ///
+ /// Returns the stored under , or
+ /// when the key is absent or its value is not an
+ /// .
+ ///
+ internal static AnimationCurve GetAnimationCurve(this Entity entity, string key, AnimationCurve fallback)
+ {
+ if (entity.properties.TryGetValue(key, out var value) && value is AnimationCurve curve)
+ return curve;
+
+ return fallback;
+ }
+}
diff --git a/BopVisualEffects/Effects/CameraShake/CameraShakeEffect.cs b/BopVisualEffects/Effects/CameraShake/CameraShakeEffect.cs
index 55a33c2..a6edee7 100644
--- a/BopVisualEffects/Effects/CameraShake/CameraShakeEffect.cs
+++ b/BopVisualEffects/Effects/CameraShake/CameraShakeEffect.cs
@@ -32,7 +32,8 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid)
properties = new Dictionary
{
["amplitude"] = 0.1f,
- ["frequency"] = 18.0f
+ ["frequency"] = 18.0f,
+ ["easing_curve"] = DefaultEasingCurve()
}
};
}
@@ -44,6 +45,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
var durationBeats = Mathf.Max(0.01f, entity.length);
var amplitude = entity.GetFloat("amplitude");
var frequency = entity.GetFloat("frequency");
+ var easingCurve = entity.GetAnimationCurve("easing_curve", DefaultEasingCurve());
var startBeat = entity.beat;
var endBeat = startBeat + durationBeats;
@@ -54,10 +56,15 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
void SpawnAction()
{
EffectRuntimeController.Instance.SpawnRunner(runner =>
- runner.Initialize(loader, loader.jukebox, startBeat, endBeat, amplitude, frequency));
+ runner.Initialize(loader, loader.jukebox, startBeat, endBeat, amplitude, frequency, easingCurve));
}
}
+ private static AnimationCurve DefaultEasingCurve() => new AnimationCurve(
+ new Keyframe(0f, 1f, 0f, -1f),
+ new Keyframe(1f, 0f, -1f, 0f)
+ );
+
private sealed class CameraShakeRunner : MonoBehaviour
{
private bool _initialized;
@@ -69,11 +76,12 @@ private sealed class CameraShakeRunner : MonoBehaviour
private JukeboxScript? _jukebox;
private Transform? _targetTransform;
private Vector3 _initialLocalPosition;
+ private AnimationCurve _easingCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
///
/// Initializes this runner with effect parameters.
///
- public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float amplitude, float frequency)
+ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float amplitude, float frequency, AnimationCurve easingCurve)
{
_loader = loader;
_jukebox = jukebox;
@@ -81,6 +89,7 @@ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float
_endBeat = endBeat;
_amplitude = Mathf.Max(0f, amplitude);
_frequency = Mathf.Max(0.1f, frequency);
+ _easingCurve = easingCurve;
InitializeTargetCamera();
}
@@ -116,7 +125,7 @@ private void LateUpdate()
}
var progress = Mathf.InverseLerp(_startBeat, _endBeat, currentBeat);
- var envelope = 1f - progress;
+ var envelope = _easingCurve.Evaluate(progress);
if (envelope <= 0f)
{
Stop();
diff --git a/BopVisualEffects/Effects/CameraTilt/CameraTiltEffect.cs b/BopVisualEffects/Effects/CameraTilt/CameraTiltEffect.cs
index 05a48b9..d22a991 100644
--- a/BopVisualEffects/Effects/CameraTilt/CameraTiltEffect.cs
+++ b/BopVisualEffects/Effects/CameraTilt/CameraTiltEffect.cs
@@ -31,7 +31,8 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid)
resizable = true,
properties = new Dictionary
{
- ["angle"] = 5.0f
+ ["angle"] = 5.0f,
+ ["easing_curve"] = DefaultEasingCurve()
}
};
}
@@ -42,6 +43,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
var log = ClassLogger.GetForClass();
var durationBeats = Mathf.Max(0.01f, entity.length);
var angle = entity.GetFloat("angle");
+ var easingCurve = entity.GetAnimationCurve("easing_curve", DefaultEasingCurve());
var startBeat = entity.beat;
var endBeat = startBeat + durationBeats;
@@ -52,10 +54,16 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
void SpawnAction()
{
EffectRuntimeController.Instance.SpawnRunner(runner =>
- runner.Initialize(loader, loader.jukebox, startBeat, endBeat, angle));
+ runner.Initialize(loader, loader.jukebox, startBeat, endBeat, angle, easingCurve));
}
}
+ private static AnimationCurve DefaultEasingCurve() => new AnimationCurve(
+ new Keyframe(0f, 0f, 0f, Mathf.PI),
+ new Keyframe(0.5f, 1f, 0f, 0f),
+ new Keyframe(1f, 0f, -Mathf.PI, 0f)
+ );
+
private sealed class CameraTiltRunner : MonoBehaviour
{
private bool _initialized;
@@ -66,17 +74,19 @@ private sealed class CameraTiltRunner : MonoBehaviour
private JukeboxScript? _jukebox;
private Transform? _targetTransform;
private Quaternion _initialLocalRotation;
+ private AnimationCurve _easingCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
///
/// Initializes this runner with effect parameters.
///
- public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float angle)
+ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float angle, AnimationCurve easingCurve)
{
_loader = loader;
_jukebox = jukebox;
_startBeat = startBeat;
_endBeat = endBeat;
_angle = angle;
+ _easingCurve = easingCurve;
InitializeTargetCamera();
}
@@ -111,9 +121,9 @@ private void LateUpdate()
return;
}
- // Swing tilt: rotate in one direction and back (full sine wave over the duration).
+ // Swing tilt: rotate in one direction and back (configurable curve over the duration).
var progress = Mathf.InverseLerp(_startBeat, _endBeat, currentBeat);
- var tiltAngle = _angle * Mathf.Sin(progress * Mathf.PI);
+ var tiltAngle = _angle * _easingCurve.Evaluate(progress);
_targetTransform!.localRotation = _initialLocalRotation * Quaternion.Euler(0f, 0f, tiltAngle);
}
diff --git a/BopVisualEffects/Effects/ColorTint/ColorTintEffect.cs b/BopVisualEffects/Effects/ColorTint/ColorTintEffect.cs
index c3c3216..8e685b2 100644
--- a/BopVisualEffects/Effects/ColorTint/ColorTintEffect.cs
+++ b/BopVisualEffects/Effects/ColorTint/ColorTintEffect.cs
@@ -35,7 +35,8 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid)
["r"] = 1.0f,
["g"] = 0.0f,
["b"] = 0.0f,
- ["alpha"] = 0.25f
+ ["alpha"] = 0.25f,
+ ["easing_curve"] = DefaultEasingCurve()
}
};
}
@@ -49,6 +50,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
var g = entity.GetFloat("g");
var b = entity.GetFloat("b");
var alpha = entity.GetFloat("alpha");
+ var easingCurve = entity.GetAnimationCurve("easing_curve", DefaultEasingCurve());
var startBeat = entity.beat;
var endBeat = startBeat + durationBeats;
@@ -59,10 +61,17 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
void SpawnAction()
{
EffectRuntimeController.Instance.SpawnRunner(runner =>
- runner.Initialize(loader, loader.jukebox, startBeat, endBeat, r, g, b, alpha));
+ runner.Initialize(loader, loader.jukebox, startBeat, endBeat, r, g, b, alpha, easingCurve));
}
}
+ private static AnimationCurve DefaultEasingCurve() => new AnimationCurve(
+ new Keyframe(0f, 0f, 0f, 5f),
+ new Keyframe(0.2f, 1f, 5f, 0f),
+ new Keyframe(0.8f, 1f, 0f, -5f),
+ new Keyframe(1f, 0f, -5f, 0f)
+ );
+
private sealed class ColorTintRunner : MonoBehaviour
{
private bool _initialized;
@@ -75,11 +84,12 @@ private sealed class ColorTintRunner : MonoBehaviour
private MixtapeLoaderCustom? _loader;
private JukeboxScript? _jukebox;
private ColorTintOverlay? _overlay;
+ private AnimationCurve _easingCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
///
/// Initializes this runner with effect parameters.
///
- public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float r, float g, float b, float alpha)
+ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float r, float g, float b, float alpha, AnimationCurve easingCurve)
{
_loader = loader;
_jukebox = jukebox;
@@ -89,6 +99,7 @@ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float
_g = Mathf.Clamp01(g);
_b = Mathf.Clamp01(b);
_maxAlpha = Mathf.Clamp01(alpha);
+ _easingCurve = easingCurve;
InitializeOverlay();
}
@@ -125,13 +136,7 @@ private void LateUpdate()
// Fade in over first 20%, hold, fade out over last 20%.
var progress = Mathf.InverseLerp(_startBeat, _endBeat, currentBeat);
- float envelope;
- if (progress < 0.2f)
- envelope = Mathf.InverseLerp(0f, 0.2f, progress);
- else if (progress > 0.8f)
- envelope = 1f - Mathf.InverseLerp(0.8f, 1f, progress);
- else
- envelope = 1f;
+ var envelope = _easingCurve.Evaluate(progress);
if (_overlay != null)
_overlay.SetColor(_r, _g, _b, _maxAlpha * envelope);
diff --git a/BopVisualEffects/Effects/Fog/FogEffect.cs b/BopVisualEffects/Effects/Fog/FogEffect.cs
index 5e1a8c0..cabfa4d 100644
--- a/BopVisualEffects/Effects/Fog/FogEffect.cs
+++ b/BopVisualEffects/Effects/Fog/FogEffect.cs
@@ -36,7 +36,8 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid)
["g"] = 0.8f,
["b"] = 0.9f,
["alpha"] = 0.6f,
- ["height"] = 0.5f
+ ["height"] = 0.5f,
+ ["easing_curve"] = DefaultEasingCurve()
}
};
}
@@ -51,6 +52,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
var b = entity.GetFloat("b");
var alpha = entity.GetFloat("alpha");
var height = entity.GetFloat("height");
+ var easingCurve = entity.GetAnimationCurve("easing_curve", DefaultEasingCurve());
var startBeat = entity.beat;
var endBeat = startBeat + durationBeats;
@@ -61,10 +63,17 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
void SpawnAction()
{
EffectRuntimeController.Instance.SpawnRunner(runner =>
- runner.Initialize(loader, loader.jukebox, startBeat, endBeat, r, g, b, alpha, height));
+ runner.Initialize(loader, loader.jukebox, startBeat, endBeat, r, g, b, alpha, height, easingCurve));
}
}
+ private static AnimationCurve DefaultEasingCurve() => new AnimationCurve(
+ new Keyframe(0f, 0f, 0f, 5f),
+ new Keyframe(0.2f, 1f, 5f, 0f),
+ new Keyframe(0.8f, 1f, 0f, -5f),
+ new Keyframe(1f, 0f, -5f, 0f)
+ );
+
private sealed class FogRunner : MonoBehaviour
{
private bool _initialized;
@@ -78,11 +87,12 @@ private sealed class FogRunner : MonoBehaviour
private MixtapeLoaderCustom? _loader;
private JukeboxScript? _jukebox;
private FogOverlay? _overlay;
+ private AnimationCurve _easingCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
///
/// Initializes this runner with effect parameters.
///
- public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float r, float g, float b, float alpha, float height)
+ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float r, float g, float b, float alpha, float height, AnimationCurve easingCurve)
{
_loader = loader;
_jukebox = jukebox;
@@ -93,6 +103,7 @@ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float
_b = Mathf.Clamp01(b);
_maxAlpha = Mathf.Clamp01(alpha);
_height = Mathf.Clamp01(height);
+ _easingCurve = easingCurve;
InitializeOverlay();
}
@@ -129,13 +140,7 @@ private void LateUpdate()
// Fade in over first 20%, hold, fade out over last 20%.
var progress = Mathf.InverseLerp(_startBeat, _endBeat, currentBeat);
- float envelope;
- if (progress < 0.2f)
- envelope = Mathf.InverseLerp(0f, 0.2f, progress);
- else if (progress > 0.8f)
- envelope = 1f - Mathf.InverseLerp(0.8f, 1f, progress);
- else
- envelope = 1f;
+ var envelope = _easingCurve.Evaluate(progress);
if (_overlay != null)
_overlay.SetParams(_r, _g, _b, _maxAlpha * envelope, _height);
diff --git a/BopVisualEffects/Effects/Hsl/HslEffect.cs b/BopVisualEffects/Effects/Hsl/HslEffect.cs
index d10126e..f45457c 100644
--- a/BopVisualEffects/Effects/Hsl/HslEffect.cs
+++ b/BopVisualEffects/Effects/Hsl/HslEffect.cs
@@ -37,7 +37,8 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid)
["hue_shift"] = 0.0f,
["saturation"] = 1.0f,
["lightness"] = 0.0f,
- ["intensity"] = 1.0f
+ ["intensity"] = 1.0f,
+ ["easing_curve"] = DefaultEasingCurve()
}
};
}
@@ -51,6 +52,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
var saturation = entity.GetFloat("saturation");
var lightness = entity.GetFloat("lightness");
var intensity = entity.GetFloat("intensity");
+ var easingCurve = entity.GetAnimationCurve("easing_curve", DefaultEasingCurve());
var startBeat = entity.beat;
var endBeat = startBeat + durationBeats;
@@ -61,10 +63,17 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
void SpawnAction()
{
EffectRuntimeController.Instance.SpawnRunner(runner =>
- runner.Initialize(loader, loader.jukebox, startBeat, endBeat, hueShift, saturation, lightness, intensity));
+ runner.Initialize(loader, loader.jukebox, startBeat, endBeat, hueShift, saturation, lightness, intensity, easingCurve));
}
}
+ private static AnimationCurve DefaultEasingCurve() => new AnimationCurve(
+ new Keyframe(0f, 0f, 0f, 5f),
+ new Keyframe(0.2f, 1f, 5f, 0f),
+ new Keyframe(0.8f, 1f, 0f, -5f),
+ new Keyframe(1f, 0f, -5f, 0f)
+ );
+
private sealed class HslRunner : MonoBehaviour
{
private bool _initialized;
@@ -78,12 +87,13 @@ private sealed class HslRunner : MonoBehaviour
private JukeboxScript? _jukebox;
private Camera? _camera;
private HslRequest? _request;
+ private AnimationCurve _easingCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
///
/// Initializes this runner with effect parameters.
///
public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat,
- float hueShift, float saturation, float lightness, float intensity)
+ float hueShift, float saturation, float lightness, float intensity, AnimationCurve easingCurve)
{
_loader = loader;
_jukebox = jukebox;
@@ -93,6 +103,7 @@ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float
_saturation = Mathf.Max(0f, saturation);
_lightness = Mathf.Clamp(lightness, -0.5f, 0.5f);
_maxIntensity = Mathf.Clamp01(intensity);
+ _easingCurve = easingCurve;
InitializeRequest();
}
@@ -129,13 +140,7 @@ private void LateUpdate()
// Fade in over first 20%, hold, fade out over last 20%.
var progress = Mathf.InverseLerp(_startBeat, _endBeat, currentBeat);
- float envelope;
- if (progress < 0.2f)
- envelope = Mathf.InverseLerp(0f, 0.2f, progress);
- else if (progress > 0.8f)
- envelope = 1f - Mathf.InverseLerp(0.8f, 1f, progress);
- else
- envelope = 1f;
+ var envelope = _easingCurve.Evaluate(progress);
if (_request != null)
{
diff --git a/BopVisualEffects/Effects/Letterbox/LetterboxEffect.cs b/BopVisualEffects/Effects/Letterbox/LetterboxEffect.cs
index 0cf0057..6cca885 100644
--- a/BopVisualEffects/Effects/Letterbox/LetterboxEffect.cs
+++ b/BopVisualEffects/Effects/Letterbox/LetterboxEffect.cs
@@ -32,7 +32,8 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid)
resizable = true,
properties = new Dictionary
{
- ["size"] = 0.1f
+ ["size"] = 0.1f,
+ ["easing_curve"] = DefaultEasingCurve()
}
};
}
@@ -43,6 +44,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
var log = ClassLogger.GetForClass();
var durationBeats = Mathf.Max(0.01f, entity.length);
var size = entity.GetFloat("size");
+ var easingCurve = entity.GetAnimationCurve("easing_curve", DefaultEasingCurve());
var startBeat = entity.beat;
var endBeat = startBeat + durationBeats;
@@ -53,10 +55,17 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
void SpawnAction()
{
EffectRuntimeController.Instance.SpawnRunner(runner =>
- runner.Initialize(loader, loader.jukebox, startBeat, endBeat, size));
+ runner.Initialize(loader, loader.jukebox, startBeat, endBeat, size, easingCurve));
}
}
+ private static AnimationCurve DefaultEasingCurve() => new AnimationCurve(
+ new Keyframe(0f, 0f, 0f, 20f / 3f),
+ new Keyframe(0.15f, 1f, 20f / 3f, 0f),
+ new Keyframe(0.85f, 1f, 0f, -20f / 3f),
+ new Keyframe(1f, 0f, -20f / 3f, 0f)
+ );
+
private sealed class LetterboxRunner : MonoBehaviour
{
private bool _initialized;
@@ -66,17 +75,19 @@ private sealed class LetterboxRunner : MonoBehaviour
private MixtapeLoaderCustom? _loader;
private JukeboxScript? _jukebox;
private LetterboxOverlay? _overlay;
+ private AnimationCurve _easingCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
///
/// Initializes this runner with effect parameters.
///
- public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float size)
+ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float size, AnimationCurve easingCurve)
{
_loader = loader;
_jukebox = jukebox;
_startBeat = startBeat;
_endBeat = endBeat;
_size = Mathf.Clamp(size, 0f, 0.49f);
+ _easingCurve = easingCurve;
InitializeOverlay();
}
@@ -113,13 +124,7 @@ private void LateUpdate()
// Slide bars in over first 15%, hold, slide out over last 15%.
var progress = Mathf.InverseLerp(_startBeat, _endBeat, currentBeat);
- float envelope;
- if (progress < 0.15f)
- envelope = Mathf.InverseLerp(0f, 0.15f, progress);
- else if (progress > 0.85f)
- envelope = 1f - Mathf.InverseLerp(0.85f, 1f, progress);
- else
- envelope = 1f;
+ var envelope = _easingCurve.Evaluate(progress);
if (_overlay != null)
_overlay.SetParams(_size * envelope);
diff --git a/BopVisualEffects/Effects/PixelGrid/PixelGridEffect.cs b/BopVisualEffects/Effects/PixelGrid/PixelGridEffect.cs
index d1e5f08..094edba 100644
--- a/BopVisualEffects/Effects/PixelGrid/PixelGridEffect.cs
+++ b/BopVisualEffects/Effects/PixelGrid/PixelGridEffect.cs
@@ -33,7 +33,8 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid)
resizable = true,
properties = new Dictionary
{
- ["pixel_size"] = 4.0f
+ ["pixel_size"] = 4.0f,
+ ["easing_curve"] = DefaultEasingCurve()
}
};
}
@@ -44,6 +45,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
var log = ClassLogger.GetForClass();
var durationBeats = Mathf.Max(0.01f, entity.length);
var pixelSize = entity.GetFloat("pixel_size");
+ var easingCurve = entity.GetAnimationCurve("easing_curve", DefaultEasingCurve());
var startBeat = entity.beat;
var endBeat = startBeat + durationBeats;
@@ -54,10 +56,17 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
void SpawnAction()
{
EffectRuntimeController.Instance.SpawnRunner(runner =>
- runner.Initialize(loader, loader.jukebox, startBeat, endBeat, pixelSize));
+ runner.Initialize(loader, loader.jukebox, startBeat, endBeat, pixelSize, easingCurve));
}
}
+ private static AnimationCurve DefaultEasingCurve() => new AnimationCurve(
+ new Keyframe(0f, 0f, 0f, 20f / 3f),
+ new Keyframe(0.15f, 1f, 20f / 3f, 0f),
+ new Keyframe(0.85f, 1f, 0f, -20f / 3f),
+ new Keyframe(1f, 0f, -20f / 3f, 0f)
+ );
+
private sealed class PixelGridRunner : MonoBehaviour
{
private bool _initialized;
@@ -67,17 +76,19 @@ private sealed class PixelGridRunner : MonoBehaviour
private MixtapeLoaderCustom? _loader;
private JukeboxScript? _jukebox;
private PixelGridOverlay? _overlay;
+ private AnimationCurve _easingCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
///
/// Initializes this runner with effect parameters.
///
- public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float pixelSize)
+ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float pixelSize, AnimationCurve easingCurve)
{
_loader = loader;
_jukebox = jukebox;
_startBeat = startBeat;
_endBeat = endBeat;
_pixelSize = Mathf.Clamp(Mathf.RoundToInt(pixelSize), 2, 64);
+ _easingCurve = easingCurve;
InitializeOverlay();
}
@@ -115,13 +126,7 @@ private void LateUpdate()
// Ramp block size up over first 15%, hold, ramp down over last 15%.
// block_size=1 means no pixelation; ramping from 1 → _pixelSize gives a "zooming into pixels" look.
var progress = Mathf.InverseLerp(_startBeat, _endBeat, currentBeat);
- float envelope;
- if (progress < 0.15f)
- envelope = Mathf.InverseLerp(0f, 0.15f, progress);
- else if (progress > 0.85f)
- envelope = 1f - Mathf.InverseLerp(0.85f, 1f, progress);
- else
- envelope = 1f;
+ var envelope = _easingCurve.Evaluate(progress);
var currentBlockSize = Mathf.Max(1, Mathf.RoundToInt(Mathf.Lerp(1f, _pixelSize, envelope)));
if (_overlay != null)
diff --git a/BopVisualEffects/Effects/Scanlines/ScanlinesEffect.cs b/BopVisualEffects/Effects/Scanlines/ScanlinesEffect.cs
index af16a71..b93437f 100644
--- a/BopVisualEffects/Effects/Scanlines/ScanlinesEffect.cs
+++ b/BopVisualEffects/Effects/Scanlines/ScanlinesEffect.cs
@@ -34,7 +34,8 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid)
{
["alpha"] = 0.35f,
["count"] = 60.0f,
- ["scroll_speed"] = 0.0f
+ ["scroll_speed"] = 0.0f,
+ ["easing_curve"] = DefaultEasingCurve()
}
};
}
@@ -47,6 +48,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
var alpha = entity.GetFloat("alpha");
var count = entity.GetFloat("count");
var scrollSpeed = entity.GetFloat("scroll_speed");
+ var easingCurve = entity.GetAnimationCurve("easing_curve", DefaultEasingCurve());
var startBeat = entity.beat;
var endBeat = startBeat + durationBeats;
@@ -57,10 +59,17 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
void SpawnAction()
{
EffectRuntimeController.Instance.SpawnRunner(runner =>
- runner.Initialize(loader, loader.jukebox, startBeat, endBeat, alpha, count, scrollSpeed));
+ runner.Initialize(loader, loader.jukebox, startBeat, endBeat, alpha, count, scrollSpeed, easingCurve));
}
}
+ private static AnimationCurve DefaultEasingCurve() => new AnimationCurve(
+ new Keyframe(0f, 0f, 0f, 20f / 3f),
+ new Keyframe(0.15f, 1f, 20f / 3f, 0f),
+ new Keyframe(0.85f, 1f, 0f, -20f / 3f),
+ new Keyframe(1f, 0f, -20f / 3f, 0f)
+ );
+
private sealed class ScanlinesRunner : MonoBehaviour
{
private bool _initialized;
@@ -72,11 +81,12 @@ private sealed class ScanlinesRunner : MonoBehaviour
private MixtapeLoaderCustom? _loader;
private JukeboxScript? _jukebox;
private ScanlinesOverlay? _overlay;
+ private AnimationCurve _easingCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
///
/// Initializes this runner with effect parameters.
///
- public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float alpha, float count, float scrollSpeed)
+ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float alpha, float count, float scrollSpeed, AnimationCurve easingCurve)
{
_loader = loader;
_jukebox = jukebox;
@@ -85,6 +95,7 @@ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float
_alpha = Mathf.Clamp01(alpha);
_count = Mathf.Clamp(Mathf.RoundToInt(count), 4, 2000);
_scrollSpeed = scrollSpeed;
+ _easingCurve = easingCurve;
InitializeOverlay();
}
@@ -121,13 +132,7 @@ private void LateUpdate()
// Fade in over first 15%, hold, fade out over last 15%.
var progress = Mathf.InverseLerp(_startBeat, _endBeat, currentBeat);
- float envelope;
- if (progress < 0.15f)
- envelope = Mathf.InverseLerp(0f, 0.15f, progress);
- else if (progress > 0.85f)
- envelope = 1f - Mathf.InverseLerp(0.85f, 1f, progress);
- else
- envelope = 1f;
+ var envelope = _easingCurve.Evaluate(progress);
if (_overlay != null)
_overlay.SetParams(_alpha * envelope, _count, _scrollSpeed);
diff --git a/BopVisualEffects/Effects/ScreenNoise/ScreenNoiseEffect.cs b/BopVisualEffects/Effects/ScreenNoise/ScreenNoiseEffect.cs
index 4b0c240..b93486d 100644
--- a/BopVisualEffects/Effects/ScreenNoise/ScreenNoiseEffect.cs
+++ b/BopVisualEffects/Effects/ScreenNoise/ScreenNoiseEffect.cs
@@ -34,7 +34,8 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid)
{
["alpha"] = 0.5f,
["count"] = 400.0f,
- ["size"] = 0.01f
+ ["size"] = 0.01f,
+ ["easing_curve"] = DefaultEasingCurve()
}
};
}
@@ -47,6 +48,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
var alpha = entity.GetFloat("alpha");
var count = entity.GetFloat("count");
var size = entity.GetFloat("size");
+ var easingCurve = entity.GetAnimationCurve("easing_curve", DefaultEasingCurve());
var startBeat = entity.beat;
var endBeat = startBeat + durationBeats;
@@ -57,10 +59,17 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
void SpawnAction()
{
EffectRuntimeController.Instance.SpawnRunner(runner =>
- runner.Initialize(loader, loader.jukebox, startBeat, endBeat, alpha, count, size));
+ runner.Initialize(loader, loader.jukebox, startBeat, endBeat, alpha, count, size, easingCurve));
}
}
+ private static AnimationCurve DefaultEasingCurve() => new AnimationCurve(
+ new Keyframe(0f, 0f, 0f, 20f / 3f),
+ new Keyframe(0.15f, 1f, 20f / 3f, 0f),
+ new Keyframe(0.85f, 1f, 0f, -20f / 3f),
+ new Keyframe(1f, 0f, -20f / 3f, 0f)
+ );
+
private sealed class ScreenNoiseRunner : MonoBehaviour
{
private bool _initialized;
@@ -72,11 +81,12 @@ private sealed class ScreenNoiseRunner : MonoBehaviour
private MixtapeLoaderCustom? _loader;
private JukeboxScript? _jukebox;
private ScreenNoiseOverlay? _overlay;
+ private AnimationCurve _easingCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
///
/// Initializes this runner with effect parameters.
///
- public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float alpha, float count, float size)
+ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float alpha, float count, float size, AnimationCurve easingCurve)
{
_loader = loader;
_jukebox = jukebox;
@@ -85,6 +95,7 @@ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float
_alpha = Mathf.Clamp01(alpha);
_count = Mathf.Clamp(Mathf.RoundToInt(count), 10, 2000);
_size = Mathf.Clamp(size, 0.005f, 0.1f);
+ _easingCurve = easingCurve;
InitializeOverlay();
}
@@ -121,13 +132,7 @@ private void LateUpdate()
// Fade in over first 15%, hold, fade out over last 15%.
var progress = Mathf.InverseLerp(_startBeat, _endBeat, currentBeat);
- float envelope;
- if (progress < 0.15f)
- envelope = Mathf.InverseLerp(0f, 0.15f, progress);
- else if (progress > 0.85f)
- envelope = 1f - Mathf.InverseLerp(0.85f, 1f, progress);
- else
- envelope = 1f;
+ var envelope = _easingCurve.Evaluate(progress);
if (_overlay != null)
_overlay.SetParams(_alpha * envelope, _count, _size);
diff --git a/BopVisualEffects/Effects/Sepia/SepiaEffect.cs b/BopVisualEffects/Effects/Sepia/SepiaEffect.cs
index f2a8592..bbe3850 100644
--- a/BopVisualEffects/Effects/Sepia/SepiaEffect.cs
+++ b/BopVisualEffects/Effects/Sepia/SepiaEffect.cs
@@ -34,7 +34,8 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid)
resizable = true,
properties = new Dictionary
{
- ["intensity"] = 0.8f
+ ["intensity"] = 0.8f,
+ ["easing_curve"] = DefaultEasingCurve()
}
};
}
@@ -45,6 +46,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
var log = ClassLogger.GetForClass();
var durationBeats = Mathf.Max(0.01f, entity.length);
var intensity = entity.GetFloat("intensity");
+ var easingCurve = entity.GetAnimationCurve("easing_curve", DefaultEasingCurve());
var startBeat = entity.beat;
var endBeat = startBeat + durationBeats;
@@ -55,10 +57,17 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
void SpawnAction()
{
EffectRuntimeController.Instance.SpawnRunner(runner =>
- runner.Initialize(loader, loader.jukebox, startBeat, endBeat, intensity));
+ runner.Initialize(loader, loader.jukebox, startBeat, endBeat, intensity, easingCurve));
}
}
+ private static AnimationCurve DefaultEasingCurve() => new AnimationCurve(
+ new Keyframe(0f, 0f, 0f, 5f),
+ new Keyframe(0.2f, 1f, 5f, 0f),
+ new Keyframe(0.8f, 1f, 0f, -5f),
+ new Keyframe(1f, 0f, -5f, 0f)
+ );
+
private sealed class SepiaRunner : MonoBehaviour
{
private bool _initialized;
@@ -69,17 +78,19 @@ private sealed class SepiaRunner : MonoBehaviour
private JukeboxScript? _jukebox;
private Camera? _camera;
private SepiaRequest? _request;
+ private AnimationCurve _easingCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
///
/// Initializes this runner with effect parameters.
///
- public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float intensity)
+ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float intensity, AnimationCurve easingCurve)
{
_loader = loader;
_jukebox = jukebox;
_startBeat = startBeat;
_endBeat = endBeat;
_maxIntensity = Mathf.Clamp01(intensity);
+ _easingCurve = easingCurve;
InitializeRequest();
}
@@ -116,13 +127,7 @@ private void LateUpdate()
// Fade in over first 20%, hold, fade out over last 20%.
var progress = Mathf.InverseLerp(_startBeat, _endBeat, currentBeat);
- float envelope;
- if (progress < 0.2f)
- envelope = Mathf.InverseLerp(0f, 0.2f, progress);
- else if (progress > 0.8f)
- envelope = 1f - Mathf.InverseLerp(0.8f, 1f, progress);
- else
- envelope = 1f;
+ var envelope = _easingCurve.Evaluate(progress);
if (_request != null)
_request.Intensity = _maxIntensity * envelope;
diff --git a/BopVisualEffects/Effects/Vignette/VignetteEffect.cs b/BopVisualEffects/Effects/Vignette/VignetteEffect.cs
index d872743..b0c0c9f 100644
--- a/BopVisualEffects/Effects/Vignette/VignetteEffect.cs
+++ b/BopVisualEffects/Effects/Vignette/VignetteEffect.cs
@@ -33,7 +33,8 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid)
properties = new Dictionary
{
["alpha"] = 0.7f,
- ["size"] = 0.1f
+ ["size"] = 0.1f,
+ ["easing_curve"] = DefaultEasingCurve()
}
};
}
@@ -45,6 +46,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
var durationBeats = Mathf.Max(0.01f, entity.length);
var alpha = entity.GetFloat("alpha");
var size = entity.GetFloat("size");
+ var easingCurve = entity.GetAnimationCurve("easing_curve", DefaultEasingCurve());
var startBeat = entity.beat;
var endBeat = startBeat + durationBeats;
@@ -55,10 +57,17 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
void SpawnAction()
{
EffectRuntimeController.Instance.SpawnRunner(runner =>
- runner.Initialize(loader, loader.jukebox, startBeat, endBeat, alpha, size));
+ runner.Initialize(loader, loader.jukebox, startBeat, endBeat, alpha, size, easingCurve));
}
}
+ private static AnimationCurve DefaultEasingCurve() => new AnimationCurve(
+ new Keyframe(0f, 0f, 0f, 5f),
+ new Keyframe(0.2f, 1f, 5f, 0f),
+ new Keyframe(0.8f, 1f, 0f, -5f),
+ new Keyframe(1f, 0f, -5f, 0f)
+ );
+
private sealed class VignetteRunner : MonoBehaviour
{
private bool _initialized;
@@ -69,11 +78,12 @@ private sealed class VignetteRunner : MonoBehaviour
private MixtapeLoaderCustom? _loader;
private JukeboxScript? _jukebox;
private VignetteOverlay? _overlay;
+ private AnimationCurve _easingCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
///
/// Initializes this runner with effect parameters.
///
- public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float alpha, float size)
+ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float alpha, float size, AnimationCurve easingCurve)
{
_loader = loader;
_jukebox = jukebox;
@@ -81,6 +91,7 @@ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float
_endBeat = endBeat;
_alpha = Mathf.Clamp01(alpha);
_size = Mathf.Clamp(size, 0f, 0.5f);
+ _easingCurve = easingCurve;
InitializeOverlay();
}
@@ -117,13 +128,7 @@ private void LateUpdate()
// Fade in over first 20%, hold, fade out over last 20%.
var progress = Mathf.InverseLerp(_startBeat, _endBeat, currentBeat);
- float envelope;
- if (progress < 0.2f)
- envelope = Mathf.InverseLerp(0f, 0.2f, progress);
- else if (progress > 0.8f)
- envelope = 1f - Mathf.InverseLerp(0.8f, 1f, progress);
- else
- envelope = 1f;
+ var envelope = _easingCurve.Evaluate(progress);
if (_overlay != null)
_overlay.SetParams(_alpha * envelope, _size);
diff --git a/BopVisualEffects/Effects/ZoomIn/ZoomInEffect.cs b/BopVisualEffects/Effects/ZoomIn/ZoomInEffect.cs
index 403b489..6ecc942 100644
--- a/BopVisualEffects/Effects/ZoomIn/ZoomInEffect.cs
+++ b/BopVisualEffects/Effects/ZoomIn/ZoomInEffect.cs
@@ -32,7 +32,8 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid)
resizable = true,
properties = new Dictionary
{
- ["intensity"] = 0.2f
+ ["intensity"] = 0.2f,
+ ["easing_curve"] = DefaultEasingCurve()
}
};
}
@@ -43,6 +44,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
var log = ClassLogger.GetForClass();
var durationBeats = Mathf.Max(0.01f, entity.length);
var intensity = entity.GetFloat("intensity");
+ var easingCurve = entity.GetAnimationCurve("easing_curve", DefaultEasingCurve());
var startBeat = entity.beat;
var endBeat = startBeat + durationBeats;
@@ -53,10 +55,17 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
void SpawnAction()
{
EffectRuntimeController.Instance.SpawnRunner(runner =>
- runner.Initialize(loader, loader.jukebox, startBeat, endBeat, intensity));
+ runner.Initialize(loader, loader.jukebox, startBeat, endBeat, intensity, easingCurve));
}
}
+ private static AnimationCurve DefaultEasingCurve() => new AnimationCurve(
+ new Keyframe(0f, 0f, 0f, 0f),
+ new Keyframe(0.2f, 1f, 0f, 0f),
+ new Keyframe(0.8f, 1f, 0f, 0f),
+ new Keyframe(1f, 0f, 0f, 0f)
+ );
+
private sealed class ZoomInRunner : MonoBehaviour
{
private bool _initialized;
@@ -66,17 +75,19 @@ private sealed class ZoomInRunner : MonoBehaviour
private MixtapeLoaderCustom? _loader;
private JukeboxScript? _jukebox;
private Camera? _camera;
+ private AnimationCurve _easingCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
///
/// Initializes this runner with effect parameters.
///
- public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float intensity)
+ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float intensity, AnimationCurve easingCurve)
{
_loader = loader;
_jukebox = jukebox;
_startBeat = startBeat;
_endBeat = endBeat;
_intensity = Mathf.Clamp(intensity, 0f, 0.99f);
+ _easingCurve = easingCurve;
InitializeTargetCamera();
}
@@ -114,13 +125,7 @@ private void LateUpdate()
// Ease in over first 20%, hold zoomed in from 20-80%, ease back out over last 20%.
var progress = Mathf.InverseLerp(_startBeat, _endBeat, currentBeat);
- float envelope;
- if (progress < 0.2f)
- envelope = Mathf.SmoothStep(0f, 1f, Mathf.InverseLerp(0f, 0.2f, progress));
- else if (progress > 0.8f)
- envelope = Mathf.SmoothStep(0f, 1f, 1f - Mathf.InverseLerp(0.8f, 1f, progress));
- else
- envelope = 1f;
+ var envelope = _easingCurve.Evaluate(progress);
// Zoom in: zoomFactor < 1 means smaller camera size = more zoomed in.
var zoomFactor = Mathf.Clamp(1f - _intensity * envelope, 0.01f, 1f);
diff --git a/BopVisualEffects/Effects/ZoomOut/ZoomOutEffect.cs b/BopVisualEffects/Effects/ZoomOut/ZoomOutEffect.cs
index ff25b25..46b00c2 100644
--- a/BopVisualEffects/Effects/ZoomOut/ZoomOutEffect.cs
+++ b/BopVisualEffects/Effects/ZoomOut/ZoomOutEffect.cs
@@ -32,7 +32,8 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid)
resizable = true,
properties = new Dictionary
{
- ["intensity"] = 0.2f
+ ["intensity"] = 0.2f,
+ ["easing_curve"] = DefaultEasingCurve()
}
};
}
@@ -43,6 +44,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
var log = ClassLogger.GetForClass();
var durationBeats = Mathf.Max(0.01f, entity.length);
var intensity = entity.GetFloat("intensity");
+ var easingCurve = entity.GetAnimationCurve("easing_curve", DefaultEasingCurve());
var startBeat = entity.beat;
var endBeat = startBeat + durationBeats;
@@ -53,10 +55,17 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
void SpawnAction()
{
EffectRuntimeController.Instance.SpawnRunner(runner =>
- runner.Initialize(loader, loader.jukebox, startBeat, endBeat, intensity));
+ runner.Initialize(loader, loader.jukebox, startBeat, endBeat, intensity, easingCurve));
}
}
+ private static AnimationCurve DefaultEasingCurve() => new AnimationCurve(
+ new Keyframe(0f, 0f, 0f, 0f),
+ new Keyframe(0.2f, 1f, 0f, 0f),
+ new Keyframe(0.8f, 1f, 0f, 0f),
+ new Keyframe(1f, 0f, 0f, 0f)
+ );
+
private sealed class ZoomOutRunner : MonoBehaviour
{
private bool _initialized;
@@ -66,17 +75,19 @@ private sealed class ZoomOutRunner : MonoBehaviour
private MixtapeLoaderCustom? _loader;
private JukeboxScript? _jukebox;
private Camera? _camera;
+ private AnimationCurve _easingCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
///
/// Initializes this runner with effect parameters.
///
- public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float intensity)
+ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float intensity, AnimationCurve easingCurve)
{
_loader = loader;
_jukebox = jukebox;
_startBeat = startBeat;
_endBeat = endBeat;
_intensity = Mathf.Max(0f, intensity);
+ _easingCurve = easingCurve;
InitializeTargetCamera();
}
@@ -114,13 +125,7 @@ private void LateUpdate()
// Ease out over first 20%, hold zoomed out from 20-80%, ease back in over last 20%.
var progress = Mathf.InverseLerp(_startBeat, _endBeat, currentBeat);
- float envelope;
- if (progress < 0.2f)
- envelope = Mathf.SmoothStep(0f, 1f, Mathf.InverseLerp(0f, 0.2f, progress));
- else if (progress > 0.8f)
- envelope = Mathf.SmoothStep(0f, 1f, 1f - Mathf.InverseLerp(0.8f, 1f, progress));
- else
- envelope = 1f;
+ var envelope = _easingCurve.Evaluate(progress);
// Zoom out: zoomFactor > 1 means larger camera size = more zoomed out.
var zoomFactor = 1f + _intensity * envelope;
diff --git a/BopVisualEffects/Effects/ZoomPulse/ZoomPulseEffect.cs b/BopVisualEffects/Effects/ZoomPulse/ZoomPulseEffect.cs
index 125cd7a..903b696 100644
--- a/BopVisualEffects/Effects/ZoomPulse/ZoomPulseEffect.cs
+++ b/BopVisualEffects/Effects/ZoomPulse/ZoomPulseEffect.cs
@@ -32,7 +32,8 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid)
resizable = true,
properties = new Dictionary
{
- ["intensity"] = 0.15f
+ ["intensity"] = 0.15f,
+ ["easing_curve"] = DefaultEasingCurve()
}
};
}
@@ -43,6 +44,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
var log = ClassLogger.GetForClass();
var durationBeats = Mathf.Max(0.01f, entity.length);
var intensity = entity.GetFloat("intensity");
+ var easingCurve = entity.GetAnimationCurve("easing_curve", DefaultEasingCurve());
var startBeat = entity.beat;
var endBeat = startBeat + durationBeats;
@@ -53,10 +55,16 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader)
void SpawnAction()
{
EffectRuntimeController.Instance.SpawnRunner(runner =>
- runner.Initialize(loader, loader.jukebox, startBeat, endBeat, intensity));
+ runner.Initialize(loader, loader.jukebox, startBeat, endBeat, intensity, easingCurve));
}
}
+ private static AnimationCurve DefaultEasingCurve() => new AnimationCurve(
+ new Keyframe(0f, 0f, 0f, 10f / 3f),
+ new Keyframe(0.3f, 1f, 10f / 3f, -10f / 7f),
+ new Keyframe(1f, 0f, -10f / 7f, 0f)
+ );
+
private sealed class ZoomPulseRunner : MonoBehaviour
{
private bool _initialized;
@@ -66,17 +74,19 @@ private sealed class ZoomPulseRunner : MonoBehaviour
private MixtapeLoaderCustom? _loader;
private JukeboxScript? _jukebox;
private Camera? _camera;
+ private AnimationCurve _easingCurve = AnimationCurve.Linear(0f, 1f, 1f, 1f);
///
/// Initializes this runner with effect parameters.
///
- public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float intensity)
+ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float intensity, AnimationCurve easingCurve)
{
_loader = loader;
_jukebox = jukebox;
_startBeat = startBeat;
_endBeat = endBeat;
_intensity = Mathf.Max(0f, intensity);
+ _easingCurve = easingCurve;
InitializeTargetCamera();
}
@@ -114,9 +124,7 @@ private void LateUpdate()
// Zoom in quickly then ease back out (attack/decay envelope shaped like a triangle peaking at 30%).
var progress = Mathf.InverseLerp(_startBeat, _endBeat, currentBeat);
- var envelope = progress < 0.3f
- ? Mathf.InverseLerp(0f, 0.3f, progress)
- : 1f - Mathf.InverseLerp(0.3f, 1f, progress);
+ var envelope = _easingCurve.Evaluate(progress);
var zoomDelta = _intensity * envelope;
diff --git a/BopVisualEffects/Stubs/CiGameTypeStubs.cs b/BopVisualEffects/Stubs/CiGameTypeStubs.cs
index 9a7e70a..139aa85 100644
--- a/BopVisualEffects/Stubs/CiGameTypeStubs.cs
+++ b/BopVisualEffects/Stubs/CiGameTypeStubs.cs
@@ -45,6 +45,18 @@ public float GetFloat(string key)
_ => 0f
};
}
+
+ public AnimationCurve GetAnimationCurve(string key, AnimationCurve fallback)
+ {
+ if (!properties.TryGetValue(key, out object? value) || value is null)
+ return fallback;
+
+ return value switch
+ {
+ AnimationCurve curveValue => curveValue,
+ _ => fallback
+ };
+ }
}
public sealed class MixtapeLoaderCustom : MonoBehaviour
From d7acfff4a8a41b103e7a75bd8136eb01280bb3e5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 24 Feb 2026 21:35:57 +0000
Subject: [PATCH 3/3] Add easing_curve property to all effects with
configurable AnimationCurve
Co-authored-by: Brollyy <12004018+Brollyy@users.noreply.github.com>
---
docs/effects/README.md | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/docs/effects/README.md b/docs/effects/README.md
index e11d8fd..05bb44a 100644
--- a/docs/effects/README.md
+++ b/docs/effects/README.md
@@ -13,6 +13,7 @@ Adds a temporary camera shake effect for impact, hits, drops, or strong rhythm a
**Properties**
- `amplitude`: Strength of the shake.
- `frequency`: Speed of the shake movement.
+- `easing_curve`: Animation curve controlling the shake strength over the event duration (default: linear decay from full strength at the start to zero at the end).
- `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline.
**Preview media**
@@ -31,6 +32,7 @@ Briefly tilts the camera on its Z-axis with a smooth swing arc. Great for expres
**Properties**
- `angle`: Maximum tilt angle in degrees (positive = counter-clockwise).
+- `easing_curve`: Animation curve controlling the tilt angle over the event duration (default: smooth bell curve peaking at the midpoint, approximating a sine wave).
- `length` (event length in editor): How long the tilt lasts, in beats. This event is resizable in the timeline.
## Color Tint
@@ -44,6 +46,7 @@ Applies a sustained full-screen color tint that fades in, holds, then fades out.
**Properties**
- `r`, `g`, `b`: Tint color (0–1 each; default `1, 0, 0` — red).
- `alpha`: Maximum opacity (0–1; default `0.25`).
+- `easing_curve`: Animation curve controlling the tint opacity over the event duration (default: linear fade in over the first 20%, hold, linear fade out over the last 20%).
- `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline.
## Fog
@@ -58,6 +61,7 @@ Draws a ground fog gradient overlay — opaque at the bottom of the screen, fadi
- `r`, `g`, `b`: Fog color (0–1 each; default `0.8, 0.8, 0.9` — pale blue-grey).
- `alpha`: Maximum opacity at the bottom of the screen (0–1; default `0.6`).
- `height`: Normalized screen height at which the fog fully fades to transparent (0–1; default `0.5`).
+- `easing_curve`: Animation curve controlling the fog opacity over the event duration (default: linear fade in over the first 20%, hold, linear fade out over the last 20%).
- `length` (event length in editor): How long the fog lasts, in beats. This event is resizable in the timeline.
## Horizontal Flip
@@ -84,6 +88,7 @@ Adjusts hue, saturation and lightness of the whole screen for creative colour gr
- `saturation`: Saturation multiplier (0 = fully greyscale, 1 = unchanged, >1 = boosted; default `1.0`). Boost above 1 requires shader bundle.
- `lightness`: Additive lightness offset (-0.5–0.5; default `0`). Positive values brighten, negative values darken.
- `intensity`: Overall blend strength (0–1; default `1.0`).
+- `easing_curve`: Animation curve controlling the effect intensity over the event duration (default: linear fade in over the first 20%, hold, linear fade out over the last 20%).
- `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline.
## Letterbox
@@ -96,6 +101,7 @@ Adds cinematic black bars at the top and bottom of the screen for a dramatic wid
**Properties**
- `size`: Height of each bar as a normalized screen fraction (0–0.49; default `0.1`).
+- `easing_curve`: Animation curve controlling the bar height over the event duration (default: linear slide in over the first 15%, hold, linear slide out over the last 15%).
- `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline.
## Pixel Grid
@@ -108,6 +114,7 @@ Pixelates the screen by downsampling the camera's rendered output to a low-resol
**Properties**
- `pixel_size`: Size of each pixel block in screen pixels (2–64; default `4`). Larger values produce a more pronounced 8-bit look.
+- `easing_curve`: Animation curve controlling the pixel block size over the event duration (default: linear ramp up over the first 15%, hold, linear ramp down over the last 15%).
- `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline.
## Scanlines
@@ -122,6 +129,7 @@ Draws horizontal CRT-style scan lines over the screen for a retro 8-bit aestheti
- `alpha`: Darkness of each scan line (0–1; default `0.35`).
- `count`: Number of scan lines (default `60`, clamped 4–2000).
- `scroll_speed`: Speed at which lines scroll upward, in cells per second (default `0` = static; negative = scroll downward).
+- `easing_curve`: Animation curve controlling the scan line opacity over the event duration (default: linear fade in over the first 15%, hold, linear fade out over the last 15%).
- `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline.
## Screen Noise
@@ -136,6 +144,7 @@ Draws animated TV static noise specks over the screen for a glitchy atmosphere.
- `alpha`: Maximum opacity of the noise specks (0–1; default `0.5`).
- `count`: Number of noise specks drawn per frame (10–2000; default `400`).
- `size`: Physical size of each speck in normalized screen coordinates (0.005–0.1; default `0.01`).
+- `easing_curve`: Animation curve controlling the noise opacity over the event duration (default: linear fade in over the first 15%, hold, linear fade out over the last 15%).
- `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline.
## Sepia
@@ -148,6 +157,7 @@ Applies a warm vintage sepia-tone filter using a per-pixel shader (standard Adob
**Properties**
- `intensity`: Strength of the sepia toning (0–1; default `0.8`).
+- `easing_curve`: Animation curve controlling the effect intensity over the event duration (default: linear fade in over the first 20%, hold, linear fade out over the last 20%).
- `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline.
## Vertical Flip
@@ -172,6 +182,7 @@ Darkens the screen edges with a smooth radial/elliptical gradient using a 32-seg
**Properties**
- `alpha`: Darkness of the edge (0–1; default `0.7`).
- `size`: How far the darkening extends inward as a fraction of screen half-height (0–0.5; default `0.1`).
+- `easing_curve`: Animation curve controlling the vignette opacity over the event duration (default: linear fade in over the first 20%, hold, linear fade out over the last 20%).
- `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline.
## Zoom In
@@ -184,6 +195,7 @@ Eases the camera smoothly into a zoomed-in view, holds at the target level, then
**Properties**
- `intensity`: How far to zoom in, as a fraction of the camera's base size (e.g. `0.2` = 20% closer). Clamped to `[0, 0.99]`.
+- `easing_curve`: Animation curve controlling the zoom intensity over the event duration (default: smooth ease in over the first 20%, hold, smooth ease out over the last 20%).
- `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline.
## Zoom Out
@@ -196,6 +208,7 @@ Eases the camera smoothly out to a wider view, holds, then eases back in. Great
**Properties**
- `intensity`: How far to zoom out, as a fraction of the camera's base size (e.g. `0.2` = 20% further out).
+- `easing_curve`: Animation curve controlling the zoom intensity over the event duration (default: smooth ease out over the first 20%, hold, smooth ease back in over the last 20%).
- `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline.
## Zoom Pulse
@@ -208,5 +221,6 @@ Rapidly zooms the camera in and then eases it back out, creating a punchy "push-
**Properties**
- `intensity`: How much to zoom in, expressed as a fraction of the camera's base size (e.g. `0.15` = 15% zoom).
+- `easing_curve`: Animation curve controlling the zoom intensity over the event duration (default: linear ramp up to peak at 30%, then linear ramp back down).
- `length` (event length in editor): How long the pulse lasts, in beats. This event is resizable in the timeline.