Add experimental kaleidoscope and bounce effects

pull/307/head
James H Ball 2025-08-11 22:10:49 +01:00
rodzic 600d69e568
commit 2facd66ef8
5 zmienionych plików z 154 dodań i 1 usunięć

Wyświetl plik

@ -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();

Wyświetl plik

@ -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 <JuceHeader.h>
class KaleidoscopeEffect : public osci::EffectApplication {
public:
osci::Point apply(int /*index*/, osci::Point input, const std::vector<std::atomic<double>>& 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<double>::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<double>::pi) / juce::MathConstants<double>::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<double>::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<osci::Effect> build() const override {
auto eff = std::make_shared<osci::Effect>(
std::make_shared<KaleidoscopeEffect>(),
std::vector<osci::EffectParameter*>{
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;
}
};

Wyświetl plik

@ -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 <JuceHeader.h>
class PhysicsBounceEffect : public osci::EffectApplication {
public:
osci::Point apply(int index, osci::Point input, const std::vector<std::atomic<double>>& 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<double>::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<osci::Effect> build() const override {
auto eff = std::make_shared<osci::Effect>(
std::make_shared<PhysicsBounceEffect>(),
std::vector<osci::EffectParameter*>{
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;
};

Wyświetl plik

@ -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<osci::Effect> smoothEffect = SmoothEffect("visualiser", 0.0f).build();

Wyświetl plik

@ -110,6 +110,10 @@
<FILE id="ux2dO2" name="DistortEffect.h" compile="0" resource="0" file="Source/audio/DistortEffect.h"/>
<FILE id="SWC0tN" name="MultiplexEffect.h" compile="0" resource="0"
file="Source/audio/MultiplexEffect.h"/>
<FILE id="kAlEiD" name="KaleidoscopeEffect.h" compile="0" resource="0"
file="Source/audio/KaleidoscopeEffect.h"/>
<FILE id="bNcEfx" name="PhysicsBounceEffect.h" compile="0" resource="0"
file="Source/audio/PhysicsBounceEffect.h"/>
<FILE id="h0dMim" name="PerspectiveEffect.h" compile="0" resource="0"
file="Source/audio/PerspectiveEffect.h"/>
<FILE id="t5g8pf" name="PublicSynthesiser.h" compile="0" resource="0"