diff --git a/BopVisualEffects/Effects/CameraShake/CameraShakeEffect.cs b/BopVisualEffects/Effects/CameraShake/CameraShakeEffect.cs index 55a33c2..2b17c4c 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, + ["persist_camera"] = false } }; } @@ -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 persist = entity.GetBool("persist_camera", false); var startBeat = entity.beat; var endBeat = startBeat + durationBeats; @@ -54,7 +56,7 @@ 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, persist)); } } @@ -69,11 +71,12 @@ private sealed class CameraShakeRunner : MonoBehaviour private JukeboxScript? _jukebox; private Transform? _targetTransform; private Vector3 _initialLocalPosition; + private bool _persistCamera; /// /// 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, bool persistCamera) { _loader = loader; _jukebox = jukebox; @@ -81,6 +84,7 @@ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float _endBeat = endBeat; _amplitude = Mathf.Max(0f, amplitude); _frequency = Mathf.Max(0.1f, frequency); + _persistCamera = persistCamera; InitializeTargetCamera(); } @@ -108,6 +112,9 @@ private void LateUpdate() return; } + if (_persistCamera) + RebindCameraIfNeeded(); + var currentBeat = _jukebox.CurrentBeat; if (currentBeat >= _endBeat) { @@ -150,6 +157,17 @@ private void InitializeTargetCamera() _initialized = true; } + private void RebindCameraIfNeeded() + { + Camera? camera = EffectRuntimeController.ResolveEffectCamera(_loader); + if (camera is null || _targetTransform == camera.transform) + return; + + ResetCameraTransform(); + _targetTransform = camera.transform; + _initialLocalPosition = _targetTransform.localPosition; + } + private void ResetCameraTransform() { if (_targetTransform is null) diff --git a/BopVisualEffects/Effects/CameraTilt/CameraTiltEffect.cs b/BopVisualEffects/Effects/CameraTilt/CameraTiltEffect.cs index 05a48b9..3fbb694 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, + ["persist_camera"] = false } }; } @@ -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 persist = entity.GetBool("persist_camera", false); var startBeat = entity.beat; var endBeat = startBeat + durationBeats; @@ -52,7 +54,7 @@ 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, persist)); } } @@ -66,17 +68,19 @@ private sealed class CameraTiltRunner : MonoBehaviour private JukeboxScript? _jukebox; private Transform? _targetTransform; private Quaternion _initialLocalRotation; + private bool _persistCamera; /// /// 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, bool persistCamera) { _loader = loader; _jukebox = jukebox; _startBeat = startBeat; _endBeat = endBeat; _angle = angle; + _persistCamera = persistCamera; InitializeTargetCamera(); } @@ -104,6 +108,9 @@ private void LateUpdate() return; } + if (_persistCamera) + RebindCameraIfNeeded(); + var currentBeat = _jukebox.CurrentBeat; if (currentBeat >= _endBeat) { @@ -134,6 +141,17 @@ private void InitializeTargetCamera() _initialized = true; } + private void RebindCameraIfNeeded() + { + Camera? camera = EffectRuntimeController.ResolveEffectCamera(_loader); + if (camera is null || _targetTransform == camera.transform) + return; + + ResetCameraTransform(); + _targetTransform = camera.transform; + _initialLocalRotation = _targetTransform.localRotation; + } + private void ResetCameraTransform() { if (_targetTransform is null) diff --git a/BopVisualEffects/Effects/ColorTint/ColorTintEffect.cs b/BopVisualEffects/Effects/ColorTint/ColorTintEffect.cs index 89fd3c7..b81e537 100644 --- a/BopVisualEffects/Effects/ColorTint/ColorTintEffect.cs +++ b/BopVisualEffects/Effects/ColorTint/ColorTintEffect.cs @@ -33,6 +33,7 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid) properties = new Dictionary { ["color"] = new MixtapeEventTemplates.ColorField(new Color(1.0f, 0.0f, 0.0f, 0.25f)), + ["persist_camera"] = false, ["ease_in"] = true, ["ease_out"] = true } @@ -45,6 +46,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) var log = ClassLogger.GetForClass(); var durationBeats = Mathf.Max(0.01f, entity.length); var color = entity.GetColor("color"); + var persist = entity.GetBool("persist_camera", false); var easeIn = entity.GetBool("ease_in", true); var easeOut = entity.GetBool("ease_out", true); var startBeat = entity.beat; @@ -57,7 +59,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) void SpawnAction() { EffectRuntimeController.Instance.SpawnRunner(runner => - runner.Initialize(loader, loader.jukebox, startBeat, endBeat, color, easeIn, easeOut)); + runner.Initialize(loader, loader.jukebox, startBeat, endBeat, color, persist, easeIn, easeOut)); } } @@ -70,19 +72,22 @@ private sealed class ColorTintRunner : MonoBehaviour private MixtapeLoaderCustom? _loader; private JukeboxScript? _jukebox; private ColorTintOverlay? _overlay; + private Camera? _camera; + private bool _persistCamera; private bool _easeIn; private bool _easeOut; /// /// Initializes this runner with effect parameters. /// - public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, Color color, bool easeIn, bool easeOut) + public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, Color color, bool persistCamera, bool easeIn, bool easeOut) { _loader = loader; _jukebox = jukebox; _startBeat = startBeat; _endBeat = endBeat; _color = new Color(Mathf.Clamp01(color.r), Mathf.Clamp01(color.g), Mathf.Clamp01(color.b), Mathf.Clamp01(color.a)); + _persistCamera = persistCamera; _easeIn = easeIn; _easeOut = easeOut; InitializeOverlay(); @@ -112,6 +117,9 @@ private void LateUpdate() return; } + if (_persistCamera) + RebindOverlayIfNeeded(); + var currentBeat = _jukebox.CurrentBeat; if (currentBeat >= _endBeat) { @@ -139,6 +147,7 @@ private void InitializeOverlay() return; _overlay = camera.gameObject.AddComponent(); + _camera = camera; _overlay.SetColor(_color); _initialized = true; } @@ -151,6 +160,19 @@ private void RemoveOverlay() } _overlay = null; + _camera = null; + } + + private void RebindOverlayIfNeeded() + { + Camera? camera = EffectRuntimeController.ResolveEffectCamera(_loader); + if (camera is null || _camera == camera) + return; + + RemoveOverlay(); + _overlay = camera.gameObject.AddComponent(); + _camera = camera; + _overlay.SetColor(_color); } } diff --git a/BopVisualEffects/Effects/Fog/FogEffect.cs b/BopVisualEffects/Effects/Fog/FogEffect.cs index 548a9fd..d221fbd 100644 --- a/BopVisualEffects/Effects/Fog/FogEffect.cs +++ b/BopVisualEffects/Effects/Fog/FogEffect.cs @@ -34,6 +34,7 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid) { ["color"] = new MixtapeEventTemplates.ColorField(new Color(0.8f, 0.8f, 0.9f, 0.6f)), ["height"] = 0.5f, + ["persist_camera"] = false, ["ease_in"] = true, ["ease_out"] = true } @@ -47,6 +48,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) var durationBeats = Mathf.Max(0.01f, entity.length); var color = entity.GetColor("color"); var height = entity.GetFloat("height"); + var persist = entity.GetBool("persist_camera", false); var easeIn = entity.GetBool("ease_in", true); var easeOut = entity.GetBool("ease_out", true); var startBeat = entity.beat; @@ -59,7 +61,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) void SpawnAction() { EffectRuntimeController.Instance.SpawnRunner(runner => - runner.Initialize(loader, loader.jukebox, startBeat, endBeat, color, height, easeIn, easeOut)); + runner.Initialize(loader, loader.jukebox, startBeat, endBeat, color, height, persist, easeIn, easeOut)); } } @@ -73,13 +75,15 @@ private sealed class FogRunner : MonoBehaviour private MixtapeLoaderCustom? _loader; private JukeboxScript? _jukebox; private FogOverlay? _overlay; + private Camera? _camera; + private bool _persistCamera; private bool _easeIn; private bool _easeOut; /// /// Initializes this runner with effect parameters. /// - public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, Color color, float height, bool easeIn, bool easeOut) + public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, Color color, float height, bool persistCamera, bool easeIn, bool easeOut) { _loader = loader; _jukebox = jukebox; @@ -87,6 +91,7 @@ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float _endBeat = endBeat; _color = new Color(Mathf.Clamp01(color.r), Mathf.Clamp01(color.g), Mathf.Clamp01(color.b), Mathf.Clamp01(color.a)); _height = Mathf.Clamp01(height); + _persistCamera = persistCamera; _easeIn = easeIn; _easeOut = easeOut; InitializeOverlay(); @@ -116,6 +121,9 @@ private void LateUpdate() return; } + if (_persistCamera) + RebindOverlayIfNeeded(); + var currentBeat = _jukebox.CurrentBeat; if (currentBeat >= _endBeat) { @@ -143,6 +151,7 @@ private void InitializeOverlay() return; _overlay = camera.gameObject.AddComponent(); + _camera = camera; _overlay.SetParams(_color, _height); _initialized = true; } @@ -155,6 +164,19 @@ private void RemoveOverlay() } _overlay = null; + _camera = null; + } + + private void RebindOverlayIfNeeded() + { + Camera? camera = EffectRuntimeController.ResolveEffectCamera(_loader); + if (camera is null || _camera == camera) + return; + + RemoveOverlay(); + _overlay = camera.gameObject.AddComponent(); + _camera = camera; + _overlay.SetParams(_color, _height); } } diff --git a/BopVisualEffects/Effects/HorizontalFlip/HorizontalFlipEffect.cs b/BopVisualEffects/Effects/HorizontalFlip/HorizontalFlipEffect.cs index 2afc4ed..5d67799 100644 --- a/BopVisualEffects/Effects/HorizontalFlip/HorizontalFlipEffect.cs +++ b/BopVisualEffects/Effects/HorizontalFlip/HorizontalFlipEffect.cs @@ -30,7 +30,10 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid) dataModel = $"{pluginGuid}/{Id}", length = 2.0f, resizable = true, - properties = new Dictionary() + properties = new Dictionary + { + ["persist_camera"] = false + } }; } @@ -41,6 +44,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) var durationBeats = Mathf.Max(0.01f, entity.length); var startBeat = entity.beat; var endBeat = startBeat + durationBeats; + var persist = entity.GetBool("persist_camera", false); loader.scheduler.Schedule(startBeat, (System.Action?)SpawnAction); log.Debug($"Scheduled '{DisplayName}' from beat {startBeat:0.###} to {endBeat:0.###}."); @@ -49,7 +53,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) void SpawnAction() { EffectRuntimeController.Instance.SpawnRunner(runner => - runner.Initialize(loader, loader.jukebox, endBeat)); + runner.Initialize(loader, loader.jukebox, endBeat, persist)); } } @@ -60,15 +64,17 @@ private sealed class HorizontalFlipRunner : MonoBehaviour private MixtapeLoaderCustom? _loader; private JukeboxScript? _jukebox; private Camera? _camera; + private bool _persistCamera; /// /// Initializes this runner with effect parameters. /// - public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float endBeat) + public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float endBeat, bool persistCamera) { _loader = loader; _jukebox = jukebox; _endBeat = endBeat; + _persistCamera = persistCamera; InitializeFlip(); } @@ -96,6 +102,9 @@ private void LateUpdate() return; } + if (_persistCamera) + RebindCameraIfNeeded(); + var currentBeat = _jukebox.CurrentBeat; if (currentBeat >= _endBeat) { @@ -128,6 +137,16 @@ private void RemoveFlip() CameraFlipService.RemoveHorizontalFlip(_camera); _camera = null; } + + private void RebindCameraIfNeeded() + { + Camera? camera = EffectRuntimeController.ResolveEffectCamera(_loader); + if (camera is null || _camera == camera) + return; + + RemoveFlip(); + _camera = camera; + CameraFlipService.AddHorizontalFlip(camera); + } } } - diff --git a/BopVisualEffects/Effects/Hsl/HslEffect.cs b/BopVisualEffects/Effects/Hsl/HslEffect.cs index cfe0b9d..1a1d2fa 100644 --- a/BopVisualEffects/Effects/Hsl/HslEffect.cs +++ b/BopVisualEffects/Effects/Hsl/HslEffect.cs @@ -38,6 +38,7 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid) ["saturation"] = 1.0f, ["lightness"] = 0.0f, ["intensity"] = 1.0f, + ["persist_camera"] = false, ["ease_in"] = true, ["ease_out"] = true } @@ -53,6 +54,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) var saturation = entity.GetFloat("saturation"); var lightness = entity.GetFloat("lightness"); var intensity = entity.GetFloat("intensity"); + var persist = entity.GetBool("persist_camera", false); var easeIn = entity.GetBool("ease_in", true); var easeOut = entity.GetBool("ease_out", true); var startBeat = entity.beat; @@ -65,7 +67,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) void SpawnAction() { EffectRuntimeController.Instance.SpawnRunner(runner => - runner.Initialize(loader, loader.jukebox, startBeat, endBeat, hueShift, saturation, lightness, intensity, easeIn, easeOut)); + runner.Initialize(loader, loader.jukebox, startBeat, endBeat, hueShift, saturation, lightness, intensity, persist, easeIn, easeOut)); } } @@ -82,6 +84,7 @@ private sealed class HslRunner : MonoBehaviour private JukeboxScript? _jukebox; private Camera? _camera; private HslRequest? _request; + private bool _persistCamera; private bool _easeIn; private bool _easeOut; @@ -89,7 +92,7 @@ private sealed class HslRunner : MonoBehaviour /// 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, bool easeIn, bool easeOut) + float hueShift, float saturation, float lightness, float intensity, bool persistCamera, bool easeIn, bool easeOut) { _loader = loader; _jukebox = jukebox; @@ -99,6 +102,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); + _persistCamera = persistCamera; _easeIn = easeIn; _easeOut = easeOut; InitializeRequest(); @@ -128,6 +132,9 @@ private void LateUpdate() return; } + if (_persistCamera) + RebindRequestIfNeeded(); + var currentBeat = _jukebox.CurrentBeat; if (currentBeat >= _endBeat) { @@ -168,6 +175,22 @@ private void InitializeRequest() _initialized = true; } + private void RebindRequestIfNeeded() + { + Camera? camera = EffectRuntimeController.ResolveEffectCamera(_loader); + if (camera is null || _camera == camera) + return; + + if (_camera is not null && _request is not null) + HslService.RemoveRequest(_camera, _request); + + _camera = camera; + _request = HslService.AddRequest(camera); + _request.HueShift = _hueShift; + _request.Saturation = _saturation; + _request.Lightness = _lightness; + } + private void RemoveRequest() { if (_camera != null && _request != null) diff --git a/BopVisualEffects/Effects/Letterbox/LetterboxEffect.cs b/BopVisualEffects/Effects/Letterbox/LetterboxEffect.cs index 06dcee6..915c378 100644 --- a/BopVisualEffects/Effects/Letterbox/LetterboxEffect.cs +++ b/BopVisualEffects/Effects/Letterbox/LetterboxEffect.cs @@ -33,6 +33,7 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid) properties = new Dictionary { ["size"] = 0.1f, + ["persist_camera"] = false, ["ease_in"] = true, ["ease_out"] = true } @@ -45,6 +46,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 persist = entity.GetBool("persist_camera", false); var easeIn = entity.GetBool("ease_in", true); var easeOut = entity.GetBool("ease_out", true); var startBeat = entity.beat; @@ -57,7 +59,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) void SpawnAction() { EffectRuntimeController.Instance.SpawnRunner(runner => - runner.Initialize(loader, loader.jukebox, startBeat, endBeat, size, easeIn, easeOut)); + runner.Initialize(loader, loader.jukebox, startBeat, endBeat, size, persist, easeIn, easeOut)); } } @@ -70,19 +72,22 @@ private sealed class LetterboxRunner : MonoBehaviour private MixtapeLoaderCustom? _loader; private JukeboxScript? _jukebox; private LetterboxOverlay? _overlay; + private Camera? _camera; + private bool _persistCamera; private bool _easeIn; private bool _easeOut; /// /// Initializes this runner with effect parameters. /// - public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float size, bool easeIn, bool easeOut) + public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float size, bool persistCamera, bool easeIn, bool easeOut) { _loader = loader; _jukebox = jukebox; _startBeat = startBeat; _endBeat = endBeat; _size = Mathf.Clamp(size, 0f, 0.49f); + _persistCamera = persistCamera; _easeIn = easeIn; _easeOut = easeOut; InitializeOverlay(); @@ -112,6 +117,9 @@ private void LateUpdate() return; } + if (_persistCamera) + RebindOverlayIfNeeded(); + var currentBeat = _jukebox.CurrentBeat; if (currentBeat >= _endBeat) { @@ -139,6 +147,7 @@ private void InitializeOverlay() return; _overlay = camera.gameObject.AddComponent(); + _camera = camera; _overlay.SetParams(_size); _initialized = true; } @@ -151,6 +160,19 @@ private void RemoveOverlay() } _overlay = null; + _camera = null; + } + + private void RebindOverlayIfNeeded() + { + Camera? camera = EffectRuntimeController.ResolveEffectCamera(_loader); + if (camera is null || _camera == camera) + return; + + RemoveOverlay(); + _overlay = camera.gameObject.AddComponent(); + _camera = camera; + _overlay.SetParams(_size); } } diff --git a/BopVisualEffects/Effects/PixelGrid/PixelGridEffect.cs b/BopVisualEffects/Effects/PixelGrid/PixelGridEffect.cs index d1c52b4..aa05d46 100644 --- a/BopVisualEffects/Effects/PixelGrid/PixelGridEffect.cs +++ b/BopVisualEffects/Effects/PixelGrid/PixelGridEffect.cs @@ -34,6 +34,7 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid) properties = new Dictionary { ["pixel_size"] = 4.0f, + ["persist_camera"] = false, ["ease_in"] = true, ["ease_out"] = true } @@ -46,6 +47,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 persist = entity.GetBool("persist_camera", false); var easeIn = entity.GetBool("ease_in", true); var easeOut = entity.GetBool("ease_out", true); var startBeat = entity.beat; @@ -58,7 +60,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) void SpawnAction() { EffectRuntimeController.Instance.SpawnRunner(runner => - runner.Initialize(loader, loader.jukebox, startBeat, endBeat, pixelSize, easeIn, easeOut)); + runner.Initialize(loader, loader.jukebox, startBeat, endBeat, pixelSize, persist, easeIn, easeOut)); } } @@ -71,19 +73,22 @@ private sealed class PixelGridRunner : MonoBehaviour private MixtapeLoaderCustom? _loader; private JukeboxScript? _jukebox; private PixelGridOverlay? _overlay; + private Camera? _camera; + private bool _persistCamera; private bool _easeIn; private bool _easeOut; /// /// Initializes this runner with effect parameters. /// - public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float pixelSize, bool easeIn, bool easeOut) + public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float pixelSize, bool persistCamera, bool easeIn, bool easeOut) { _loader = loader; _jukebox = jukebox; _startBeat = startBeat; _endBeat = endBeat; _pixelSize = Mathf.Clamp(Mathf.RoundToInt(pixelSize), 2, 64); + _persistCamera = persistCamera; _easeIn = easeIn; _easeOut = easeOut; InitializeOverlay(); @@ -113,6 +118,9 @@ private void LateUpdate() return; } + if (_persistCamera) + RebindOverlayIfNeeded(); + var currentBeat = _jukebox.CurrentBeat; if (currentBeat >= _endBeat) { @@ -124,8 +132,8 @@ private void LateUpdate() // block_size=1 means no pixelation; ramping from 1 → _pixelSize gives a "zooming into pixels" look. var progress = Mathf.InverseLerp(_startBeat, _endBeat, currentBeat); var envelope = EffectEnvelope.Evaluate(progress, _easeIn, _easeOut, 0.15f, 0.85f); - var currentBlockSize = Mathf.Max(1, Mathf.RoundToInt(Mathf.Lerp(1f, _pixelSize, envelope))); + if (_overlay != null) _overlay.SetBlockSize(currentBlockSize); } @@ -142,6 +150,7 @@ private void InitializeOverlay() return; _overlay = camera.gameObject.AddComponent(); + _camera = camera; _overlay.SetBlockSize(_pixelSize); _initialized = true; } @@ -154,6 +163,18 @@ private void RemoveOverlay() } _overlay = null; + _camera = null; + } + + private void RebindOverlayIfNeeded() + { + Camera? camera = EffectRuntimeController.ResolveEffectCamera(_loader); + if (camera is null || _camera == camera) + return; + + RemoveOverlay(); + _overlay = camera.gameObject.AddComponent(); + _camera = camera; } } diff --git a/BopVisualEffects/Effects/Scanlines/ScanlinesEffect.cs b/BopVisualEffects/Effects/Scanlines/ScanlinesEffect.cs index d2e4902..ce4e0ba 100644 --- a/BopVisualEffects/Effects/Scanlines/ScanlinesEffect.cs +++ b/BopVisualEffects/Effects/Scanlines/ScanlinesEffect.cs @@ -35,6 +35,7 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid) ["alpha"] = 0.35f, ["count"] = 60.0f, ["scroll_speed"] = 0.0f, + ["persist_camera"] = false, ["ease_in"] = true, ["ease_out"] = true } @@ -49,6 +50,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 persist = entity.GetBool("persist_camera", false); var easeIn = entity.GetBool("ease_in", true); var easeOut = entity.GetBool("ease_out", true); var startBeat = entity.beat; @@ -61,7 +63,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) void SpawnAction() { EffectRuntimeController.Instance.SpawnRunner(runner => - runner.Initialize(loader, loader.jukebox, startBeat, endBeat, alpha, count, scrollSpeed, easeIn, easeOut)); + runner.Initialize(loader, loader.jukebox, startBeat, endBeat, alpha, count, scrollSpeed, persist, easeIn, easeOut)); } } @@ -76,13 +78,15 @@ private sealed class ScanlinesRunner : MonoBehaviour private MixtapeLoaderCustom? _loader; private JukeboxScript? _jukebox; private ScanlinesOverlay? _overlay; + private Camera? _camera; + private bool _persistCamera; private bool _easeIn; private bool _easeOut; /// /// Initializes this runner with effect parameters. /// - public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float alpha, float count, float scrollSpeed, bool easeIn, bool easeOut) + public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float alpha, float count, float scrollSpeed, bool persistCamera, bool easeIn, bool easeOut) { _loader = loader; _jukebox = jukebox; @@ -91,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; + _persistCamera = persistCamera; _easeIn = easeIn; _easeOut = easeOut; InitializeOverlay(); @@ -120,6 +125,9 @@ private void LateUpdate() return; } + if (_persistCamera) + RebindOverlayIfNeeded(); + var currentBeat = _jukebox.CurrentBeat; if (currentBeat >= _endBeat) { @@ -147,6 +155,7 @@ private void InitializeOverlay() return; _overlay = camera.gameObject.AddComponent(); + _camera = camera; _overlay.SetParams(_alpha, _count, _scrollSpeed); _initialized = true; } @@ -159,6 +168,19 @@ private void RemoveOverlay() } _overlay = null; + _camera = null; + } + + private void RebindOverlayIfNeeded() + { + Camera? camera = EffectRuntimeController.ResolveEffectCamera(_loader); + if (camera is null || _camera == camera) + return; + + RemoveOverlay(); + _overlay = camera.gameObject.AddComponent(); + _camera = camera; + _overlay.SetParams(_alpha, _count, _scrollSpeed); } } diff --git a/BopVisualEffects/Effects/ScreenNoise/ScreenNoiseEffect.cs b/BopVisualEffects/Effects/ScreenNoise/ScreenNoiseEffect.cs index 19e856b..2cb61cf 100644 --- a/BopVisualEffects/Effects/ScreenNoise/ScreenNoiseEffect.cs +++ b/BopVisualEffects/Effects/ScreenNoise/ScreenNoiseEffect.cs @@ -35,6 +35,7 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid) ["alpha"] = 0.5f, ["count"] = 400.0f, ["size"] = 0.01f, + ["persist_camera"] = false, ["ease_in"] = true, ["ease_out"] = true } @@ -49,6 +50,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) var alpha = entity.GetFloat("alpha"); var count = entity.GetFloat("count"); var size = entity.GetFloat("size"); + var persist = entity.GetBool("persist_camera", false); var easeIn = entity.GetBool("ease_in", true); var easeOut = entity.GetBool("ease_out", true); var startBeat = entity.beat; @@ -61,7 +63,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) void SpawnAction() { EffectRuntimeController.Instance.SpawnRunner(runner => - runner.Initialize(loader, loader.jukebox, startBeat, endBeat, alpha, count, size, easeIn, easeOut)); + runner.Initialize(loader, loader.jukebox, startBeat, endBeat, alpha, count, size, persist, easeIn, easeOut)); } } @@ -76,13 +78,15 @@ private sealed class ScreenNoiseRunner : MonoBehaviour private MixtapeLoaderCustom? _loader; private JukeboxScript? _jukebox; private ScreenNoiseOverlay? _overlay; + private Camera? _camera; + private bool _persistCamera; private bool _easeIn; private bool _easeOut; /// /// Initializes this runner with effect parameters. /// - public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float alpha, float count, float size, bool easeIn, bool easeOut) + public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float alpha, float count, float size, bool persistCamera, bool easeIn, bool easeOut) { _loader = loader; _jukebox = jukebox; @@ -91,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); + _persistCamera = persistCamera; _easeIn = easeIn; _easeOut = easeOut; InitializeOverlay(); @@ -120,6 +125,9 @@ private void LateUpdate() return; } + if (_persistCamera) + RebindOverlayIfNeeded(); + var currentBeat = _jukebox.CurrentBeat; if (currentBeat >= _endBeat) { @@ -147,6 +155,7 @@ private void InitializeOverlay() return; _overlay = camera.gameObject.AddComponent(); + _camera = camera; _overlay.SetParams(_alpha, _count, _size); _initialized = true; } @@ -159,6 +168,19 @@ private void RemoveOverlay() } _overlay = null; + _camera = null; + } + + private void RebindOverlayIfNeeded() + { + Camera? camera = EffectRuntimeController.ResolveEffectCamera(_loader); + if (camera is null || _camera == camera) + return; + + RemoveOverlay(); + _overlay = camera.gameObject.AddComponent(); + _camera = camera; + _overlay.SetParams(_alpha, _count, _size); } } diff --git a/BopVisualEffects/Effects/Sepia/SepiaEffect.cs b/BopVisualEffects/Effects/Sepia/SepiaEffect.cs index 6a81cf9..016b9b1 100644 --- a/BopVisualEffects/Effects/Sepia/SepiaEffect.cs +++ b/BopVisualEffects/Effects/Sepia/SepiaEffect.cs @@ -35,6 +35,7 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid) properties = new Dictionary { ["intensity"] = 0.8f, + ["persist_camera"] = false, ["ease_in"] = true, ["ease_out"] = true } @@ -47,6 +48,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 persist = entity.GetBool("persist_camera", false); var easeIn = entity.GetBool("ease_in", true); var easeOut = entity.GetBool("ease_out", true); var startBeat = entity.beat; @@ -59,7 +61,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) void SpawnAction() { EffectRuntimeController.Instance.SpawnRunner(runner => - runner.Initialize(loader, loader.jukebox, startBeat, endBeat, intensity, easeIn, easeOut)); + runner.Initialize(loader, loader.jukebox, startBeat, endBeat, intensity, persist, easeIn, easeOut)); } } @@ -73,19 +75,21 @@ private sealed class SepiaRunner : MonoBehaviour private JukeboxScript? _jukebox; private Camera? _camera; private SepiaRequest? _request; + private bool _persistCamera; private bool _easeIn; private bool _easeOut; /// /// Initializes this runner with effect parameters. /// - public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float intensity, bool easeIn, bool easeOut) + public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float intensity, bool persistCamera, bool easeIn, bool easeOut) { _loader = loader; _jukebox = jukebox; _startBeat = startBeat; _endBeat = endBeat; _maxIntensity = Mathf.Clamp01(intensity); + _persistCamera = persistCamera; _easeIn = easeIn; _easeOut = easeOut; InitializeRequest(); @@ -115,6 +119,9 @@ private void LateUpdate() return; } + if (_persistCamera) + RebindRequestIfNeeded(); + var currentBeat = _jukebox.CurrentBeat; if (currentBeat >= _endBeat) { @@ -147,6 +154,20 @@ private void InitializeRequest() _initialized = true; } + private void RebindRequestIfNeeded() + { + Camera? camera = EffectRuntimeController.ResolveEffectCamera(_loader); + if (camera is null || _camera == camera) + return; + + if (_camera is not null && _request is not null) + SepiaService.RemoveRequest(_camera, _request); + + _camera = camera; + _request = SepiaService.AddRequest(camera); + _request.Intensity = _maxIntensity; + } + private void RemoveRequest() { if (_camera != null && _request != null) diff --git a/BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs b/BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs index 573a56a..c147192 100644 --- a/BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs +++ b/BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs @@ -39,6 +39,7 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid) ["speed"] = 3.5f, ["reach"] = DefaultReach, ["color"] = new MixtapeEventTemplates.ColorField(Color.white), + ["persist_camera"] = false, ["ease_in"] = true, ["ease_out"] = true } @@ -55,6 +56,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) var speed = entity.GetFloat("speed"); var reach = entity.GetFloat("reach"); var color = entity.GetColor("color"); + var persist = entity.GetBool("persist_camera", false); var easeIn = entity.GetBool("ease_in", true); var easeOut = entity.GetBool("ease_out", true); var startBeat = entity.beat; @@ -66,7 +68,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) void SpawnAction() { EffectRuntimeController.Instance.SpawnRunner(runner => - runner.Initialize(loader, loader.jukebox, startBeat, endBeat, alpha, count, speed, reach, color, easeIn, easeOut)); + runner.Initialize(loader, loader.jukebox, startBeat, endBeat, alpha, count, speed, reach, color, persist, easeIn, easeOut)); } } @@ -84,13 +86,14 @@ private sealed class SpeedLinesRunner : MonoBehaviour private JukeboxScript? _jukebox; private Camera? _camera; private SpeedLinesRequest? _request; + private bool _persistCamera; private bool _easeIn; private bool _easeOut; /// /// Initializes this runner with effect parameters. /// - public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float alpha, float count, float speed, float reach, Color color, bool easeIn, bool easeOut) + public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float alpha, float count, float speed, float reach, Color color, bool persistCamera, bool easeIn, bool easeOut) { _loader = loader; _jukebox = jukebox; @@ -101,6 +104,7 @@ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float _speed = Mathf.Max(0.1f, speed); _reach = Mathf.Clamp01(reach); _color = new Color(Mathf.Clamp01(color.r), Mathf.Clamp01(color.g), Mathf.Clamp01(color.b), Mathf.Clamp01(color.a)); + _persistCamera = persistCamera; _easeIn = easeIn; _easeOut = easeOut; InitializeOverlay(); @@ -130,6 +134,9 @@ private void LateUpdate() return; } + if (_persistCamera) + RebindOverlayIfNeeded(); + var currentBeat = _jukebox.CurrentBeat; if (currentBeat >= _endBeat) { @@ -172,6 +179,31 @@ private void RemoveOverlay() _camera = null; _request = null; } + + private void RebindOverlayIfNeeded() + { + Camera? camera = EffectRuntimeController.ResolveEffectCamera(_loader); + if (camera is null || _camera == camera) + return; + + RemoveOverlay(); + _camera = camera; + _request = SpeedLinesService.AddRequest(camera); + ApplyRequestParameters(); + } + + private void ApplyRequestParameters() + { + if (_request is null) + return; + + _request.Alpha = _alpha; + _request.Count = _count; + _request.Speed = _speed; + _request.Reach = _reach; + _request.Color = _color; + } + } /// diff --git a/BopVisualEffects/Effects/VerticalFlip/VerticalFlipEffect.cs b/BopVisualEffects/Effects/VerticalFlip/VerticalFlipEffect.cs index a5e0e4e..f297574 100644 --- a/BopVisualEffects/Effects/VerticalFlip/VerticalFlipEffect.cs +++ b/BopVisualEffects/Effects/VerticalFlip/VerticalFlipEffect.cs @@ -30,7 +30,10 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid) dataModel = $"{pluginGuid}/{Id}", length = 2.0f, resizable = true, - properties = new Dictionary() + properties = new Dictionary + { + ["persist_camera"] = false + } }; } @@ -41,6 +44,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) var durationBeats = Mathf.Max(0.01f, entity.length); var startBeat = entity.beat; var endBeat = startBeat + durationBeats; + var persist = entity.GetBool("persist_camera", false); loader.scheduler.Schedule(startBeat, (System.Action?)SpawnAction); log.Debug($"Scheduled '{DisplayName}' from beat {startBeat:0.###} to {endBeat:0.###}."); @@ -49,7 +53,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) void SpawnAction() { EffectRuntimeController.Instance.SpawnRunner(runner => - runner.Initialize(loader, loader.jukebox, endBeat)); + runner.Initialize(loader, loader.jukebox, endBeat, persist)); } } @@ -60,15 +64,17 @@ private sealed class VerticalFlipRunner : MonoBehaviour private MixtapeLoaderCustom? _loader; private JukeboxScript? _jukebox; private Camera? _camera; + private bool _persistCamera; /// /// Initializes this runner with effect parameters. /// - public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float endBeat) + public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float endBeat, bool persistCamera) { _loader = loader; _jukebox = jukebox; _endBeat = endBeat; + _persistCamera = persistCamera; InitializeFlip(); } @@ -96,6 +102,9 @@ private void LateUpdate() return; } + if (_persistCamera) + RebindCameraIfNeeded(); + var currentBeat = _jukebox.CurrentBeat; if (currentBeat >= _endBeat) { @@ -128,6 +137,16 @@ private void RemoveFlip() CameraFlipService.RemoveVerticalFlip(_camera); _camera = null; } + + private void RebindCameraIfNeeded() + { + Camera? camera = EffectRuntimeController.ResolveEffectCamera(_loader); + if (camera is null || _camera == camera) + return; + + RemoveFlip(); + _camera = camera; + CameraFlipService.AddVerticalFlip(camera); + } } } - diff --git a/BopVisualEffects/Effects/Vignette/VignetteEffect.cs b/BopVisualEffects/Effects/Vignette/VignetteEffect.cs index 146ac9f..a7fce10 100644 --- a/BopVisualEffects/Effects/Vignette/VignetteEffect.cs +++ b/BopVisualEffects/Effects/Vignette/VignetteEffect.cs @@ -34,6 +34,7 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid) { ["alpha"] = 0.7f, ["size"] = 0.1f, + ["persist_camera"] = false, ["ease_in"] = true, ["ease_out"] = true } @@ -47,6 +48,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 persist = entity.GetBool("persist_camera", false); var easeIn = entity.GetBool("ease_in", true); var easeOut = entity.GetBool("ease_out", true); var startBeat = entity.beat; @@ -59,7 +61,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) void SpawnAction() { EffectRuntimeController.Instance.SpawnRunner(runner => - runner.Initialize(loader, loader.jukebox, startBeat, endBeat, alpha, size, easeIn, easeOut)); + runner.Initialize(loader, loader.jukebox, startBeat, endBeat, alpha, size, persist, easeIn, easeOut)); } } @@ -73,13 +75,15 @@ private sealed class VignetteRunner : MonoBehaviour private MixtapeLoaderCustom? _loader; private JukeboxScript? _jukebox; private VignetteOverlay? _overlay; + private Camera? _camera; + private bool _persistCamera; private bool _easeIn; private bool _easeOut; /// /// Initializes this runner with effect parameters. /// - public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float alpha, float size, bool easeIn, bool easeOut) + public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float alpha, float size, bool persistCamera, bool easeIn, bool easeOut) { _loader = loader; _jukebox = jukebox; @@ -87,6 +91,7 @@ public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float _endBeat = endBeat; _alpha = Mathf.Clamp01(alpha); _size = Mathf.Clamp(size, 0f, 0.5f); + _persistCamera = persistCamera; _easeIn = easeIn; _easeOut = easeOut; InitializeOverlay(); @@ -116,6 +121,9 @@ private void LateUpdate() return; } + if (_persistCamera) + RebindOverlayIfNeeded(); + var currentBeat = _jukebox.CurrentBeat; if (currentBeat >= _endBeat) { @@ -143,6 +151,7 @@ private void InitializeOverlay() return; _overlay = camera.gameObject.AddComponent(); + _camera = camera; _overlay.SetParams(_alpha, _size); _initialized = true; } @@ -155,6 +164,19 @@ private void RemoveOverlay() } _overlay = null; + _camera = null; + } + + private void RebindOverlayIfNeeded() + { + Camera? camera = EffectRuntimeController.ResolveEffectCamera(_loader); + if (camera is null || _camera == camera) + return; + + RemoveOverlay(); + _overlay = camera.gameObject.AddComponent(); + _camera = camera; + _overlay.SetParams(_alpha, _size); } } diff --git a/BopVisualEffects/Effects/ZoomIn/ZoomInEffect.cs b/BopVisualEffects/Effects/ZoomIn/ZoomInEffect.cs index 38abf10..b1ffaf8 100644 --- a/BopVisualEffects/Effects/ZoomIn/ZoomInEffect.cs +++ b/BopVisualEffects/Effects/ZoomIn/ZoomInEffect.cs @@ -33,6 +33,7 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid) properties = new Dictionary { ["intensity"] = 0.2f, + ["persist_camera"] = false, ["ease_in"] = true, ["ease_out"] = true } @@ -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 persist = entity.GetBool("persist_camera", false); var easeIn = entity.GetBool("ease_in", true); var easeOut = entity.GetBool("ease_out", true); var startBeat = entity.beat; @@ -57,7 +59,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) void SpawnAction() { EffectRuntimeController.Instance.SpawnRunner(runner => - runner.Initialize(loader, loader.jukebox, startBeat, endBeat, intensity, easeIn, easeOut)); + runner.Initialize(loader, loader.jukebox, startBeat, endBeat, intensity, persist, easeIn, easeOut)); } } @@ -70,19 +72,21 @@ private sealed class ZoomInRunner : MonoBehaviour private MixtapeLoaderCustom? _loader; private JukeboxScript? _jukebox; private Camera? _camera; + private bool _persistCamera; private bool _easeIn; private bool _easeOut; /// /// Initializes this runner with effect parameters. /// - public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float intensity, bool easeIn, bool easeOut) + public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float intensity, bool persistCamera, bool easeIn, bool easeOut) { _loader = loader; _jukebox = jukebox; _startBeat = startBeat; _endBeat = endBeat; _intensity = Mathf.Clamp(intensity, 0f, 0.99f); + _persistCamera = persistCamera; _easeIn = easeIn; _easeOut = easeOut; InitializeTargetCamera(); @@ -113,6 +117,9 @@ private void LateUpdate() return; } + if (_persistCamera) + RebindCameraIfNeeded(); + var currentBeat = _jukebox.CurrentBeat; if (currentBeat >= _endBeat) { @@ -124,6 +131,7 @@ private void LateUpdate() var progress = Mathf.InverseLerp(_startBeat, _endBeat, currentBeat); var envelope = EffectEnvelope.Evaluate(progress, _easeIn, _easeOut); + // Zoom in: zoomFactor < 1 means smaller camera size = more zoomed in. var zoomFactor = Mathf.Clamp(1f - _intensity * envelope, 0.01f, 1f); CameraZoomService.SetFactor(_camera!, this, zoomFactor); @@ -144,5 +152,17 @@ private void InitializeTargetCamera() _camera = camera; _initialized = true; } + + private void RebindCameraIfNeeded() + { + Camera? camera = EffectRuntimeController.ResolveEffectCamera(_loader); + if (camera is null || _camera == camera) + return; + + if (_camera is not null) + CameraZoomService.RemoveFactor(_camera, this); + + _camera = camera; + } } } diff --git a/BopVisualEffects/Effects/ZoomOut/ZoomOutEffect.cs b/BopVisualEffects/Effects/ZoomOut/ZoomOutEffect.cs index 0014015..aec5ea8 100644 --- a/BopVisualEffects/Effects/ZoomOut/ZoomOutEffect.cs +++ b/BopVisualEffects/Effects/ZoomOut/ZoomOutEffect.cs @@ -33,6 +33,7 @@ public MixtapeEventTemplate CreateTemplate(string pluginGuid) properties = new Dictionary { ["intensity"] = 0.2f, + ["persist_camera"] = false, ["ease_in"] = true, ["ease_out"] = true } @@ -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 persist = entity.GetBool("persist_camera", false); var easeIn = entity.GetBool("ease_in", true); var easeOut = entity.GetBool("ease_out", true); var startBeat = entity.beat; @@ -57,7 +59,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) void SpawnAction() { EffectRuntimeController.Instance.SpawnRunner(runner => - runner.Initialize(loader, loader.jukebox, startBeat, endBeat, intensity, easeIn, easeOut)); + runner.Initialize(loader, loader.jukebox, startBeat, endBeat, intensity, persist, easeIn, easeOut)); } } @@ -70,19 +72,21 @@ private sealed class ZoomOutRunner : MonoBehaviour private MixtapeLoaderCustom? _loader; private JukeboxScript? _jukebox; private Camera? _camera; + private bool _persistCamera; private bool _easeIn; private bool _easeOut; /// /// Initializes this runner with effect parameters. /// - public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float intensity, bool easeIn, bool easeOut) + public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float intensity, bool persistCamera, bool easeIn, bool easeOut) { _loader = loader; _jukebox = jukebox; _startBeat = startBeat; _endBeat = endBeat; _intensity = Mathf.Max(0f, intensity); + _persistCamera = persistCamera; _easeIn = easeIn; _easeOut = easeOut; InitializeTargetCamera(); @@ -113,6 +117,9 @@ private void LateUpdate() return; } + if (_persistCamera) + RebindCameraIfNeeded(); + var currentBeat = _jukebox.CurrentBeat; if (currentBeat >= _endBeat) { @@ -124,6 +131,7 @@ private void LateUpdate() var progress = Mathf.InverseLerp(_startBeat, _endBeat, currentBeat); var envelope = EffectEnvelope.Evaluate(progress, _easeIn, _easeOut); + // Zoom out: zoomFactor > 1 means larger camera size = more zoomed out. var zoomFactor = 1f + _intensity * envelope; CameraZoomService.SetFactor(_camera!, this, zoomFactor); @@ -144,5 +152,17 @@ private void InitializeTargetCamera() _camera = camera; _initialized = true; } + + private void RebindCameraIfNeeded() + { + Camera? camera = EffectRuntimeController.ResolveEffectCamera(_loader); + if (camera is null || _camera == camera) + return; + + if (_camera is not null) + CameraZoomService.RemoveFactor(_camera, this); + + _camera = camera; + } } } diff --git a/docs/effects/README.md b/docs/effects/README.md index 8728d23..fa49b0c 100644 --- a/docs/effects/README.md +++ b/docs/effects/README.md @@ -45,8 +45,9 @@ Applies a sustained full-screen color tint that fades in, holds, then fades out. **Properties** - `color`: Tint color (default `#ff000040` — red with slight opacity). -- `ease_in`: Fade in from zero at the event start (default `true`). -- `ease_out`: Fade out to zero at the event end (default `true`). +- `persist_camera`: Keep the effect attached to the active camera when the minigame changes (default `false`). +- `ease_in`: Fade the effect in at the event start (default `true`). +- `ease_out`: Fade the effect out at the event end (default `true`). - `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline. **Preview** @@ -64,8 +65,9 @@ Draws a ground fog gradient overlay - opaque at the bottom of the screen, fading **Properties** - `color`: Fog color (default `#CCCCE599` - pale blue-gray). - `height`: Normalized screen height at which the fog fully fades to transparent (0–1; default `0.5`). -- `ease_in`: Fade in from zero at the event start (default `true`). -- `ease_out`: Fade out to zero at the event end (default `true`). +- `persist_camera`: Keep the effect attached to the active camera when the minigame changes (default `false`). +- `ease_in`: Fade the effect in at the event start (default `true`). +- `ease_out`: Fade the effect out at the event end (default `true`). - `length` (event length in editor): How long the fog lasts, in beats. This event is resizable in the timeline. **Preview** @@ -100,8 +102,9 @@ Adjusts hue, saturation and lightness of the whole screen for colour grading usi - `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`). -- `ease_in`: Fade in from zero at the event start (default `true`). -- `ease_out`: Fade out to zero at the event end (default `true`). +- `persist_camera`: Keep the effect attached to the active camera when the minigame changes (default `false`). +- `ease_in`: Fade the effect in at the event start (default `true`). +- `ease_out`: Fade the effect out at the event end (default `true`). - `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline. **Preview** @@ -118,8 +121,9 @@ 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`). -- `ease_in`: Slide bars in at the event start (default `true`). -- `ease_out`: Slide bars out at the event end (default `true`). +- `persist_camera`: Keep the effect attached to the active camera when the minigame changes (default `false`). +- `ease_in`: Fade the effect in at the event start (default `true`). +- `ease_out`: Fade the effect out at the event end (default `true`). - `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline. **Preview** @@ -136,8 +140,9 @@ 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. -- `ease_in`: Ramp pixelation in at the event start (default `true`). -- `ease_out`: Ramp pixelation out at the event end (default `true`). +- `persist_camera`: Keep the effect attached to the active camera when the minigame changes (default `false`). +- `ease_in`: Fade the effect in at the event start (default `true`). +- `ease_out`: Fade the effect out at the event end (default `true`). - `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline. **Preview** @@ -156,8 +161,9 @@ Draws horizontal CRT-style scan lines over the screen for a retro aesthetic. Can - `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). -- `ease_in`: Fade scanlines in at the event start (default `true`). -- `ease_out`: Fade scanlines out at the event end (default `true`). +- `persist_camera`: Keep the effect attached to the active camera when the minigame changes (default `false`). +- `ease_in`: Fade the effect in at the event start (default `true`). +- `ease_out`: Fade the effect out at the event end (default `true`). - `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline. **Preview** @@ -176,8 +182,9 @@ 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`). -- `ease_in`: Fade noise in at the event start (default `true`). -- `ease_out`: Fade noise out at the event end (default `true`). +- `persist_camera`: Keep the effect attached to the active camera when the minigame changes (default `false`). +- `ease_in`: Fade the effect in at the event start (default `true`). +- `ease_out`: Fade the effect out at the event end (default `true`). - `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline. **Preview** @@ -194,8 +201,9 @@ 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`). -- `ease_in`: Fade sepia in at the event start (default `true`). -- `ease_out`: Fade sepia out at the event end (default `true`). +- `persist_camera`: Keep the effect attached to the active camera when the minigame changes (default `false`). +- `ease_in`: Fade the effect in at the event start (default `true`). +- `ease_out`: Fade the effect out at the event end (default `true`). - `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline. **Preview** @@ -216,6 +224,7 @@ Draws an animated, noise-driven radial speed-line vignette. Thin colored streak - `speed`: Animation rate of the procedural noise in cycles per second (default `3.5`). Higher values produce faster-moving streaks. - `reach`: How far each speed line reaches toward the screen center as a normalized edge-to-center fraction (0–1; default `0.25`). `0` keeps the effect at the screen edge; `1` reaches the center. The calculation accounts for the screen aspect ratio. - `color`: Color and base opacity of the speed lines (default white). +- `persist_camera`: Keep the effect attached to the active camera when the minigame changes (default `false`). - `ease_in`: Fade speed lines in over the first 15% of the event (default `true`). - `ease_out`: Fade speed lines out over the last 15% of the event (default `true`). - `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline. @@ -250,8 +259,9 @@ 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`). -- `ease_in`: Fade vignette in at the event start (default `true`). -- `ease_out`: Fade vignette out at the event end (default `true`). +- `persist_camera`: Keep the effect attached to the active camera when the minigame changes (default `false`). +- `ease_in`: Fade the effect in at the event start (default `true`). +- `ease_out`: Fade the effect out at the event end (default `true`). - `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline. **Preview** @@ -268,8 +278,9 @@ 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]`. -- `ease_in`: Ease into the zoom at the event start (default `true`). -- `ease_out`: Ease out of the zoom at the event end (default `true`). +- `persist_camera`: Keep the effect attached to the active camera when the minigame changes (default `false`). +- `ease_in`: Fade the effect in at the event start (default `true`). +- `ease_out`: Fade the effect out at the event end (default `true`). - `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline. **Preview** @@ -286,8 +297,9 @@ Eases the camera smoothly out to a wider view, holds, then eases back in. Compos **Properties** - `intensity`: How far to zoom out, as a fraction of the camera's base size (e.g. `0.2` = 20% further out). -- `ease_in`: Ease into the zoom at the event start (default `true`). -- `ease_out`: Ease out of the zoom at the event end (default `true`). +- `persist_camera`: Keep the effect attached to the active camera when the minigame changes (default `false`). +- `ease_in`: Fade the effect in at the event start (default `true`). +- `ease_out`: Fade the effect out at the event end (default `true`). - `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline. **Preview**