From 2facd66ef8bff8113b599126227715c7865ee5eb Mon Sep 17 00:00:00 2001 From: James H Ball Date: Mon, 11 Aug 2025 22:10:49 +0100 Subject: [PATCH] Add experimental kaleidoscope and bounce effects --- Source/PluginProcessor.cpp | 4 ++ Source/audio/KaleidoscopeEffect.h | 86 ++++++++++++++++++++++++ Source/audio/PhysicsBounceEffect.h | 59 ++++++++++++++++ Source/visualiser/VisualiserParameters.h | 2 +- osci-render.jucer | 4 ++ 5 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 Source/audio/KaleidoscopeEffect.h create mode 100644 Source/audio/PhysicsBounceEffect.h diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 8872dcf..0a6457e 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -12,6 +12,7 @@ #include "audio/BitCrushEffect.h" #include "audio/BulgeEffect.h" #include "audio/DistortEffect.h" +#include "audio/KaleidoscopeEffect.h" #include "audio/MultiplexEffect.h" #include "audio/SmoothEffect.h" #include "audio/VectorCancellingEffect.h" @@ -20,6 +21,7 @@ #include "audio/TranslateEffect.h" #include "audio/RippleEffect.h" #include "audio/SwirlEffect.h" +#include "audio/PhysicsBounceEffect.h" #include "parser/FileParser.h" #include "parser/FrameProducer.h" @@ -35,6 +37,8 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse toggleableEffects.push_back(BulgeEffect().build()); toggleableEffects.push_back(MultiplexEffect().build()); + toggleableEffects.push_back(KaleidoscopeEffect().build()); + toggleableEffects.push_back(PhysicsBounceEffect().build()); toggleableEffects.push_back(VectorCancellingEffect().build()); { auto scaleEffect = ScaleEffectApp().build(); diff --git a/Source/audio/KaleidoscopeEffect.h b/Source/audio/KaleidoscopeEffect.h new file mode 100644 index 0000000..b6c2684 --- /dev/null +++ b/Source/audio/KaleidoscopeEffect.h @@ -0,0 +1,86 @@ +// KaleidoscopeEffect.h +// Repeats and mirrors the input around the origin to create a kaleidoscope pattern. +// The effect supports a floating point number of segments, allowing smooth morphing +// between different symmetry counts. +#pragma once + +#include + +class KaleidoscopeEffect : public osci::EffectApplication { +public: + osci::Point apply(int /*index*/, osci::Point input, const std::vector>& values, double /*sampleRate*/) override { + // values[0] = segments (can be fractional) + // values[1] = phase (0-1) selecting which segment is currently being drawn + double segments = juce::jlimit(1.0, 256.0, values[0].load()); + double phase = values.size() > 1 ? values[1].load() : 0.0; + + // Polar conversion + double r = std::sqrt(input.x * input.x + input.y * input.y); + if (r < 1e-12) return input; + double theta = std::atan2(input.y, input.x); + + int fullSegments = (int)std::floor(segments); + double fractionalPart = segments - fullSegments; // in [0,1) + + // Use 'segments' for timing so partial segment gets proportionally shorter time. + double currentSegmentFloat = phase * segments; // [0, segments) + int currentSegmentIndex = (int)std::floor(currentSegmentFloat); + int maxIndex = fractionalPart > 1e-9 ? fullSegments : fullSegments - 1; // include partial index if exists + if (currentSegmentIndex > maxIndex) currentSegmentIndex = maxIndex; // safety + + // Base full wedge angle (all full wedges) and size of partial wedge + double baseWedgeAngle = juce::MathConstants::twoPi / segments; // size of a "unit" wedge + double partialScale = (currentSegmentIndex == fullSegments && fractionalPart > 1e-9) ? fractionalPart : 1.0; + double wedgeAngle = baseWedgeAngle * partialScale; + + // Normalize theta to [0,1) for compression + double thetaNorm = (theta + juce::MathConstants::pi) / juce::MathConstants::twoPi; // 0..1 + + // Offset for this segment: each preceding full segment occupies baseWedgeAngle + double segmentOffset = 0.0; + if (currentSegmentIndex < fullSegments) { + segmentOffset = currentSegmentIndex * baseWedgeAngle; + } else { // partial segment + segmentOffset = fullSegments * baseWedgeAngle; + } + // Map entire original angle range into [segmentOffset, segmentOffset + wedgeAngle) so edges line up exactly. + double finalTheta = segmentOffset + thetaNorm * wedgeAngle - juce::MathConstants::pi; // constant 180° rotation + + double newX = r * std::cos(finalTheta); + double newY = r * std::sin(finalTheta); + return osci::Point(newX, newY, input.z); + } + + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(), + std::vector{ + new osci::EffectParameter( + "Kaleidoscope Segments", + "Controls how many times the image is rotationally repeated around the centre. Fractional values smoothly morph the repetition.", + "kaleidoscopeSegments", + VERSION_HINT, + 6.0, // default + 1.0, // min + 32.0 // max + ), + new osci::EffectParameter( + "Kaleidoscope Phase", + "Selects which kaleidoscope segment is currently being drawn (time-multiplexed). Animate to sweep around the circle.", + "kaleidoscopePhase", + VERSION_HINT, + 0.0, // default + 0.0, // min + 1.0, // max + 0.0001f, // step + osci::LfoType::Sawtooth, + 0.5f // LFO frequency (Hz) – slow, visible rotation + ), + } + ); + eff->setName("Kaleidoscope"); + // Reuse an existing icon for now (repeat). A dedicated kaleidoscope icon can be added later. + eff->setIcon(BinaryData::repeat_svg); + return eff; + } +}; diff --git a/Source/audio/PhysicsBounceEffect.h b/Source/audio/PhysicsBounceEffect.h new file mode 100644 index 0000000..9626304 --- /dev/null +++ b/Source/audio/PhysicsBounceEffect.h @@ -0,0 +1,59 @@ +// PhysicsBounceEffect.h (Simplified DVD-style 2D bounce) +// Scales the original shape, then translates it within [-1,1] x [-1,1] using +// constant-velocity motion that bounces off the edges. Z coordinate is unchanged. +#pragma once + +#include + +class PhysicsBounceEffect : public osci::EffectApplication { +public: + osci::Point apply(int index, osci::Point input, const std::vector>& values, double sampleRate) override { + // values[0] = size (0.05..1.0) + // values[1] = speed (0..2) + // values[2] = angle (0..1 -> 0..2π) + double size = juce::jlimit(0.05, 1.0, values[0].load()); + double speed = juce::jlimit(0.0, 2.0, values[1].load()); + double angle = values[2].load() * juce::MathConstants::twoPi; + + // Base direction from user + double dirX = std::cos(angle); + double dirY = std::sin(angle); + if (flipX) dirX = -dirX; + if (flipY) dirY = -dirY; + + double dt = 1.0 / sampleRate; + position.x += dirX * speed * dt; + position.y += dirY * speed * dt; + + double maxP = 1.0 - size; + double minP = -1.0 + size; + if (position.x > maxP) { position.x = maxP; flipX = !flipX; } + else if (position.x < minP) { position.x = minP; flipX = !flipX; } + if (position.y > maxP) { position.y = maxP; flipY = !flipY; } + else if (position.y < minP) { position.y = minP; flipY = !flipY; } + + osci::Point scaled = input * size; + osci::Point out = scaled + osci::Point(position.x, position.y, 0.0); + out.z = input.z; // preserve original Z + return out; + } + + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(), + std::vector{ + new osci::EffectParameter("Bounce Size", "Size (scale) of the bouncing object.", "bounceSize", VERSION_HINT, 0.3, 0.05, 1.0), + new osci::EffectParameter("Bounce Speed", "Speed of motion.", "bounceSpeed", VERSION_HINT, 0.6, 0.0, 2.0), + new osci::EffectParameter("Bounce Angle", "Direction of travel (0..1 -> 0..360°).", "bounceAngle", VERSION_HINT, 0.125, 0.0, 1.0, 0.0001f, osci::LfoType::Sawtooth, 0.05f), + } + ); + eff->setName("Bounce"); + eff->setIcon(BinaryData::random_svg); + return eff; + } + +private: + osci::Point position { 0.0, 0.0, 0.0 }; + bool flipX = false; + bool flipY = false; +}; diff --git a/Source/visualiser/VisualiserParameters.h b/Source/visualiser/VisualiserParameters.h index 31bd90c..5a515cc 100644 --- a/Source/visualiser/VisualiserParameters.h +++ b/Source/visualiser/VisualiserParameters.h @@ -322,7 +322,7 @@ public: "Ambient Light", "Controls how much ambient light is added to the oscilloscope display.", "ambient", - VERSION_HINT, 0.7, 0.0, 5.0 + VERSION_HINT, 0.0, 0.0, 5.0 ) ); std::shared_ptr smoothEffect = SmoothEffect("visualiser", 0.0f).build(); diff --git a/osci-render.jucer b/osci-render.jucer index 5e9a88a..8e02eef 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -110,6 +110,10 @@ + +