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;