From 19e83ad5a2fda251209659f2e4fb3287ae77a4f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 05:50:23 +0000 Subject: [PATCH 1/5] Initial plan From 5ad42220e56bad00391e6f19f092d6edf8d40840 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 06:03:29 +0000 Subject: [PATCH 2/5] feat: add Speed Lines effect Co-authored-by: Brollyy <12004018+Brollyy@users.noreply.github.com> --- .../Core/EffectDefinitionRegistry.cs | 2 + .../Effects/SpeedLines/SpeedLinesEffect.cs | 310 ++++++++++++++++++ docs/effects/README.md | 18 + 3 files changed, 330 insertions(+) create mode 100644 BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs diff --git a/BopVisualEffects/Core/EffectDefinitionRegistry.cs b/BopVisualEffects/Core/EffectDefinitionRegistry.cs index 92c05f4..34d4552 100644 --- a/BopVisualEffects/Core/EffectDefinitionRegistry.cs +++ b/BopVisualEffects/Core/EffectDefinitionRegistry.cs @@ -13,6 +13,7 @@ using BopVisualEffects.Effects.Scanlines; using BopVisualEffects.Effects.ScreenNoise; using BopVisualEffects.Effects.Sepia; +using BopVisualEffects.Effects.SpeedLines; using BopVisualEffects.Effects.VerticalFlip; using BopVisualEffects.Effects.Vignette; using BopVisualEffects.Effects.ZoomIn; @@ -65,6 +66,7 @@ public static void Initialize(string pluginGuid, ClassLogger log, ConfigFile con registry.Register(new ScanlinesEffect()); registry.Register(new ScreenNoiseEffect()); registry.Register(new SepiaEffect()); + registry.Register(new SpeedLinesEffect()); registry.Register(new VerticalFlipEffect()); registry.Register(new VignetteEffect()); registry.Register(new ZoomInEffect()); diff --git a/BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs b/BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs new file mode 100644 index 0000000..5648d17 --- /dev/null +++ b/BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs @@ -0,0 +1,310 @@ +using System; +using System.Collections.Generic; +using BopVisualEffects.Core; +using UnityEngine; +using UnityEngine.Rendering; + +namespace BopVisualEffects.Effects.SpeedLines; + +/// +/// Effect definition for animated speed-line triangles radiating inward from the screen edges. +/// +public sealed class SpeedLinesEffect : IVisualEffectDefinition +{ + /// + public string Id => "speed lines"; + + /// + public string DisplayName => "Speed Lines"; + + /// + public string ConfigKey => "SpeedLines"; + + /// + public string Description => "Draws animated black triangles radiating inward from the screen edges for a high-speed rush feeling."; + + /// + public MixtapeEventTemplate CreateTemplate(string pluginGuid) + { + return new MixtapeEventTemplate + { + dataModel = $"{pluginGuid}/{Id}", + length = 2.0f, + resizable = true, + properties = new Dictionary + { + ["alpha"] = 0.8f, + ["count"] = 20.0f, + ["speed"] = 3.5f + } + }; + } + + /// + public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) + { + var log = ClassLogger.GetForClass(); + var durationBeats = Mathf.Max(0.01f, entity.length); + var alpha = entity.GetFloat("alpha"); + var count = entity.GetFloat("count"); + var speed = entity.GetFloat("speed"); + var startBeat = entity.beat; + var endBeat = startBeat + durationBeats; + + loader.scheduler.Schedule(startBeat, (Action?)SpawnAction); + log.Debug($"Scheduled '{DisplayName}' from beat {startBeat:0.###} to {endBeat:0.###}."); + return true; + + void SpawnAction() + { + EffectRuntimeController.Instance.SpawnRunner(runner => + runner.Initialize(loader, loader.jukebox, startBeat, endBeat, alpha, count, speed)); + } + } + + private sealed class SpeedLinesRunner : MonoBehaviour + { + private bool _initialized; + private float _alpha; + private int _count; + private float _speed; + private float _startBeat; + private float _endBeat; + private MixtapeLoaderCustom? _loader; + private JukeboxScript? _jukebox; + private SpeedLinesOverlay? _overlay; + + /// + /// Initializes this runner with effect parameters. + /// + public void Initialize(MixtapeLoaderCustom loader, JukeboxScript? jukebox, float startBeat, float endBeat, float alpha, float count, float speed) + { + _loader = loader; + _jukebox = jukebox; + _startBeat = startBeat; + _endBeat = endBeat; + _alpha = Mathf.Clamp01(alpha); + _count = Mathf.Clamp(Mathf.RoundToInt(count), 4, 64); + _speed = Mathf.Max(0.1f, speed); + InitializeOverlay(); + } + + /// + /// Stops this effect instance. + /// + public void Stop() + { + RemoveOverlay(); + Destroy(this); + } + + private void LateUpdate() + { + if (_jukebox is null) + { + Stop(); + return; + } + + if (!_initialized) + { + InitializeOverlay(); + if (!_initialized) + return; + } + + var currentBeat = _jukebox.CurrentBeat; + if (currentBeat >= _endBeat) + { + Stop(); + return; + } + + // 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; + + if (_overlay != null) + _overlay.SetParams(_alpha * envelope, _speed); + } + + private void OnDisable() + { + RemoveOverlay(); + } + + private void InitializeOverlay() + { + Camera? camera = EffectRuntimeController.ResolveEffectCamera(_loader); + if (camera is null) + return; + + _overlay = camera.gameObject.AddComponent(); + _overlay.Initialize(_count, _startBeat); + _overlay.SetParams(_alpha, _speed); + _initialized = true; + } + + private void RemoveOverlay() + { + if (_overlay != null) + Destroy(_overlay); + _overlay = null; + } + } + + /// + /// Draws animated black speed-line triangles in OnPostRender. + /// Each triangle points inward from the screen edge toward the center, + /// flickering at an independently randomised phase to create a high-speed rush effect. + /// Must be attached to a Camera's GameObject. + /// + private sealed class SpeedLinesOverlay : MonoBehaviour + { + // Distance from screen centre to the triangle tip, in screen-height fractions. + private const float InnerRadius = 0.12f; + + // Half-width of the triangle base at the screen boundary, in screen-height fractions. + private const float HalfWidth = 0.025f; + + private static Material? _material; + private float _alpha; + private float _speed; + private float[]? _angles; + private float[]? _phases; + + /// + /// Generates the per-triangle angular positions and flicker phase offsets + /// using a deterministic seed so the layout is stable for each effect instance. + /// + public void Initialize(int count, float seed) + { + var rng = new Random(Mathf.FloorToInt(seed * 100f) + count * 7919); + _angles = new float[count]; + _phases = new float[count]; + for (var i = 0; i < count; i++) + { + // Stratified random angles: distribute evenly around the screen with a small + // random jitter so the layout looks natural but not perfectly uniform. + var baseAngle = 2f * Mathf.PI * i / count; + var jitter = (float)(rng.NextDouble() - 0.5) * (2f * Mathf.PI / count) * 0.5f; + _angles[i] = baseAngle + jitter; + _phases[i] = (float)(rng.NextDouble() * 2.0 * Math.PI); + } + } + + /// + /// Updates the maximum opacity and flicker speed applied to all speed lines. + /// + public void SetParams(float alpha, float speed) + { + _alpha = alpha; + _speed = speed; + } + + private static Material? GetMaterial() + { + if (!_material) + { + var shader = Shader.Find("Hidden/Internal-Colored"); + if (shader is null) + return null; + + _material = new Material(shader) { hideFlags = HideFlags.HideAndDontSave }; + _material.SetInt("_SrcBlend", (int)BlendMode.SrcAlpha); + _material.SetInt("_DstBlend", (int)BlendMode.OneMinusSrcAlpha); + _material.SetInt("_Cull", (int)CullMode.Off); + _material.SetInt("_ZWrite", 0); + } + + return _material; + } + + // Returns the screen-boundary intersection from the centre (0.5, 0.5) in + // direction (dx, dy), expressed in GL ortho space (x: 0..1, y: 0..1). + private static void ScreenBoundary(float dx, float dy, out float bx, out float by) + { + var tx = (dx != 0f) ? 0.5f / Mathf.Abs(dx) : float.MaxValue; + var ty = (dy != 0f) ? 0.5f / Mathf.Abs(dy) : float.MaxValue; + var t = Mathf.Min(tx, ty); + bx = 0.5f + t * dx; + by = 0.5f + t * dy; + } + + // Unity calls this on the camera's GameObject after it finishes rendering the scene. + private void OnPostRender() + { + if (_alpha <= 0f || _angles is null || _phases is null) + return; + + var mat = GetMaterial(); + if (mat is null) + return; + + var cam = GetComponent(); + // invAspect is used to convert the visual direction to GL ortho space. + var aspect = (cam != null) ? cam.aspect : (16f / 9f); + var invAspect = (aspect > 0f) ? 1f / aspect : 1f; + + var time = Time.time; + + mat.SetPass(0); + GL.PushMatrix(); + GL.LoadOrtho(); + GL.Begin(GL.TRIANGLES); + + for (var i = 0; i < _angles.Length; i++) + { + // Squaring the sine value produces sharper bright pulses separated by longer dark + // periods, reinforcing the high-speed feel. Each triangle uses a unique phase + // so they flash at different times rather than all at once. + var squaredSin = Mathf.Sin(time * _speed + _phases[i]); + squaredSin *= squaredSin; + var flickerAlpha = squaredSin * _alpha; + if (flickerAlpha <= 0.005f) + continue; + + var angle = _angles[i]; + var cosA = Mathf.Cos(angle); + var sinA = Mathf.Sin(angle); + + // Direction and perpendicular in GL ortho space (x-axis scaled by invAspect + // so angles and widths appear visually correct on screen). + var dirX = cosA * invAspect; + var dirY = sinA; + + // Triangle tip: InnerRadius away from screen centre along the visual direction. + var tipX = 0.5f + cosA * InnerRadius * invAspect; + var tipY = 0.5f + sinA * InnerRadius; + + // Triangle base: centred on the screen boundary at this angle. + ScreenBoundary(dirX, dirY, out var bx, out var by); + + // Perpendicular in GL ortho space (visual unit vector → GL representation). + var perpX = -sinA * invAspect; + var perpY = cosA; + + // Offset the base centre by ±HalfWidth along the perpendicular. + var base1X = bx + perpX * HalfWidth; + var base1Y = by + perpY * HalfWidth; + var base2X = bx - perpX * HalfWidth; + var base2Y = by - perpY * HalfWidth; + + var color = new Color(0f, 0f, 0f, flickerAlpha); + GL.Color(color); + GL.Vertex3(tipX, tipY, 0f); + GL.Vertex3(base1X, base1Y, 0f); + GL.Vertex3(base2X, base2Y, 0f); + } + + GL.End(); + GL.PopMatrix(); + } + } +} diff --git a/docs/effects/README.md b/docs/effects/README.md index f9cb92a..584b74c 100644 --- a/docs/effects/README.md +++ b/docs/effects/README.md @@ -186,6 +186,24 @@ Applies a warm vintage sepia-tone filter using a per-pixel shader (standard Adob https://github.com/user-attachments/assets/ff1025b3-1f4b-44c8-982f-6691fe861277 +## Speed Lines + +**DisplayName:** `Speed Lines` + +**Config Key:** `SpeedLines.Enabled` + +Draws animated black triangles radiating inward from the screen edges to convey a feeling of high speed. Each triangle independently flickers at its own phase, creating a dynamic rush effect. Fades in and out smoothly. + +**Properties** +- `alpha`: Maximum opacity of the triangles (0–1; default `0.8`). +- `count`: Number of speed-line triangles (4–64; default `20`). +- `speed`: Flicker rate of each triangle in cycles per second (default `3.5`). Higher values produce more frantic flickering; lower values give slower, more rhythmic pulses. +- `length` (event length in editor): How long the effect lasts, in beats. This event is resizable in the timeline. + +**Preview** + + + ## Vertical Flip **DisplayName:** `Vertical Flip` From 75d3c4d41358d1fabd2de84e38f36e42d3a83c11 Mon Sep 17 00:00:00 2001 From: Brollyy Date: Wed, 18 Mar 2026 07:36:54 +0100 Subject: [PATCH 3/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs b/BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs index 5648d17..6aa27f8 100644 --- a/BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs +++ b/BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs @@ -264,7 +264,7 @@ private void OnPostRender() // Squaring the sine value produces sharper bright pulses separated by longer dark // periods, reinforcing the high-speed feel. Each triangle uses a unique phase // so they flash at different times rather than all at once. - var squaredSin = Mathf.Sin(time * _speed + _phases[i]); + var squaredSin = Mathf.Sin(time * _speed * (2f * Mathf.PI) + _phases[i]); squaredSin *= squaredSin; var flickerAlpha = squaredSin * _alpha; if (flickerAlpha <= 0.005f) From 0a753eeb2b26ef9fb5ac03d340d3460c0df6933b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 06:41:56 +0000 Subject: [PATCH 4/5] fix: resolve build errors in SpeedLinesEffect Co-authored-by: Brollyy <12004018+Brollyy@users.noreply.github.com> --- BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs b/BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs index 6aa27f8..e8fa2b3 100644 --- a/BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs +++ b/BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using BopVisualEffects.Core; using UnityEngine; @@ -51,7 +50,7 @@ public bool TrySchedule(Entity entity, MixtapeLoaderCustom loader) var startBeat = entity.beat; var endBeat = startBeat + durationBeats; - loader.scheduler.Schedule(startBeat, (Action?)SpawnAction); + loader.scheduler.Schedule(startBeat, (System.Action?)SpawnAction); log.Debug($"Scheduled '{DisplayName}' from beat {startBeat:0.###} to {endBeat:0.###}."); return true; @@ -185,7 +184,7 @@ private sealed class SpeedLinesOverlay : MonoBehaviour /// public void Initialize(int count, float seed) { - var rng = new Random(Mathf.FloorToInt(seed * 100f) + count * 7919); + var rng = new System.Random(Mathf.FloorToInt(seed * 100f) + count * 7919); _angles = new float[count]; _phases = new float[count]; for (var i = 0; i < count; i++) @@ -195,7 +194,7 @@ public void Initialize(int count, float seed) var baseAngle = 2f * Mathf.PI * i / count; var jitter = (float)(rng.NextDouble() - 0.5) * (2f * Mathf.PI / count) * 0.5f; _angles[i] = baseAngle + jitter; - _phases[i] = (float)(rng.NextDouble() * 2.0 * Math.PI); + _phases[i] = (float)(rng.NextDouble() * 2.0 * System.Math.PI); } } From 03b095dbca3ed38eb8e11198b37c26b06b757136 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 08:57:26 +0000 Subject: [PATCH 5/5] style: use American English spelling in SpeedLinesEffect comments Co-authored-by: Brollyy <12004018+Brollyy@users.noreply.github.com> --- .../Effects/SpeedLines/SpeedLinesEffect.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs b/BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs index e8fa2b3..81bdd1d 100644 --- a/BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs +++ b/BopVisualEffects/Effects/SpeedLines/SpeedLinesEffect.cs @@ -161,12 +161,12 @@ private void RemoveOverlay() /// /// Draws animated black speed-line triangles in OnPostRender. /// Each triangle points inward from the screen edge toward the center, - /// flickering at an independently randomised phase to create a high-speed rush effect. + /// flickering at an independently randomized phase to create a high-speed rush effect. /// Must be attached to a Camera's GameObject. /// private sealed class SpeedLinesOverlay : MonoBehaviour { - // Distance from screen centre to the triangle tip, in screen-height fractions. + // Distance from screen center to the triangle tip, in screen-height fractions. private const float InnerRadius = 0.12f; // Half-width of the triangle base at the screen boundary, in screen-height fractions. @@ -225,7 +225,7 @@ public void SetParams(float alpha, float speed) return _material; } - // Returns the screen-boundary intersection from the centre (0.5, 0.5) in + // Returns the screen-boundary intersection from the center (0.5, 0.5) in // direction (dx, dy), expressed in GL ortho space (x: 0..1, y: 0..1). private static void ScreenBoundary(float dx, float dy, out float bx, out float by) { @@ -278,18 +278,18 @@ private void OnPostRender() var dirX = cosA * invAspect; var dirY = sinA; - // Triangle tip: InnerRadius away from screen centre along the visual direction. + // Triangle tip: InnerRadius away from screen center along the visual direction. var tipX = 0.5f + cosA * InnerRadius * invAspect; var tipY = 0.5f + sinA * InnerRadius; - // Triangle base: centred on the screen boundary at this angle. + // Triangle base: centered on the screen boundary at this angle. ScreenBoundary(dirX, dirY, out var bx, out var by); // Perpendicular in GL ortho space (visual unit vector → GL representation). var perpX = -sinA * invAspect; var perpY = cosA; - // Offset the base centre by ±HalfWidth along the perpendicular. + // Offset the base center by ±HalfWidth along the perpendicular. var base1X = bx + perpX * HalfWidth; var base1Y = by + perpY * HalfWidth; var base2X = bx - perpX * HalfWidth;