Merge pull request #328 from ahall99/mirror-kaleidoscope

Add Mirror Kaleidoscope, rename old "Kaleidoscope" to "Bloom"
pull/330/head
James H Ball 2025-09-13 13:30:33 +01:00 zatwierdzone przez GitHub
commit b713516888
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
8 zmienionych plików z 175 dodań i 65 usunięć

Wyświetl plik

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16,4L20,8V4M20,16L16,20H20M8,20L4,16V20M4,8L8,4H4M16.95,7.05C14.22,4.32 9.78,4.32 7.05,7.05C4.32,9.78 4.32,14.22 7.05,16.95C9.78,19.68 14.22,19.68 16.95,16.95C19.68,14.22 19.68,9.79 16.95,7.05M15.85,15.85C13.72,18 10.28,18 8.15,15.85C6,13.72 6,10.28 8.15,8.15C10.28,6 13.72,6 15.85,8.15C18,10.28 18,13.72 15.85,15.85Z" /></svg>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M120-120v-240h80v104l124-124 56 56-124 124h104v80H120Zm480 0v-80h104L580-324l56-56 124 124v-104h80v240H600ZM324-580 200-704v104h-80v-240h240v80H256l124 124-56 56Zm312 0-56-56 124-124H600v-80h240v240h-80v-104L636-580ZM480-400q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Z"/></svg>

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 397 B

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 443 B

Wyświetl plik

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M480-28 346-160H160v-186L28-480l132-134v-186h186l134-132 134 132h186v186l132 134-132 134v186H614L480-28Zm0-112 100-100h140v-140l100-100-100-100v-140H580L480-820 380-720H240v140L140-480l100 100v140h140l100 100Zm0-340Z"/></svg>

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 341 B

Wyświetl plik

@ -15,7 +15,7 @@
#include "audio/PolygonBitCrushEffect.h"
#include "audio/SpiralBitCrushEffect.h"
#include "audio/DistortEffect.h"
#include "audio/KaleidoscopeEffect.h"
#include "audio/UnfoldEffect.h"
#include "audio/MultiplexEffect.h"
#include "audio/SmoothEffect.h"
#include "audio/WobbleEffect.h"
@ -29,6 +29,7 @@
#include "audio/SwirlEffect.h"
#include "audio/BounceEffect.h"
#include "audio/SkewEffect.h"
#include "audio/KaleidoscopeEffect.h"
#include "audio/VortexEffect.h"
#include "audio/GodRayEffect.h"
#include "parser/FileParser.h"
@ -58,11 +59,12 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse
#if OSCI_PREMIUM
toggleableEffects.push_back(MultiplexEffect(*this).build());
toggleableEffects.push_back(KaleidoscopeEffect(*this).build());
toggleableEffects.push_back(UnfoldEffect(*this).build());
toggleableEffects.push_back(BounceEffect().build());
toggleableEffects.push_back(TwistEffect().build());
toggleableEffects.push_back(SkewEffect().build());
toggleableEffects.push_back(PolygonBitCrushEffect().build());
toggleableEffects.push_back(KaleidoscopeEffect(*this).build());
toggleableEffects.push_back(VortexEffect().build());
toggleableEffects.push_back(GodRayEffect().build());
toggleableEffects.push_back(SpiralBitCrushEffect().build());

Wyświetl plik

@ -0,0 +1,4 @@
// Deprecated header retained temporarily for backward compatibility.
// Use UnfoldEffect.h instead. This file will be removed in a future cleanup.
#pragma once
#include "UnfoldEffect.h"

Wyświetl plik

@ -12,8 +12,7 @@ public:
// Bias values toward 0 or 1 based on sign
if (bias > 0.0) {
noise = std::pow(noise, biasExponent);
}
else {
} else {
noise = 1 - std::pow(1 - noise, biasExponent);
}
double scale = (1 - noiseAmp) + noise * noiseAmp;

Wyświetl plik

@ -1,75 +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>
#include "../PluginProcessor.h"
class KaleidoscopeEffect : public osci::EffectApplication {
public:
explicit KaleidoscopeEffect(OscirenderAudioProcessor &p) : audioProcessor(p) {}
KaleidoscopeEffect(OscirenderAudioProcessor& p) : audioProcessor(p) {}
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::jmax(values[0].load(), 1.0); // ensure at least 1 segment
osci::Point apply(int index, osci::Point input, const std::vector<std::atomic<double>>& values, double sampleRate) override {
const double pi = juce::MathConstants<double>::pi;
const double twoPi = juce::MathConstants<double>::twoPi;
double segments = juce::jmax(1.0, values[0].load());
double mirror = values[1].load();
double spread = juce::jlimit(0.0, 1.0, values[2].load());
double clip = juce::jlimit(0.0, 1.0, values[3].load());
// 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);
// To start, treat everything as if we are on the +X (theta = 0) segment
// Rotate input shape 90 deg CW so the shape is always "upright"
// relative to the radius
// Then apply spread
input = osci::Point(input.y, -input.x, input.z);
osci::Point output = (1 - spread) * input;
output.x += spread;
int fullSegments = (int)std::floor(segments);
double fractionalPart = segments - fullSegments; // in [0,1)
fullSegments = fractionalPart > 1e-3 ? fullSegments : fullSegments - 1;
phase = nextPhase(audioProcessor.frequency / (fullSegments + 1), sampleRate) / (2.0 * std::numbers::pi);
// Use 'segments' for timing so partial segment gets proportionally shorter time.
double currentSegmentFloat = phase * segments; // [0, segments)
int currentSegmentIndex = (int)std::floor(currentSegmentFloat);
if (currentSegmentIndex > fullSegments) currentSegmentIndex = fullSegments; // 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-3) ? 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;
// Mirror the y of every other segment if enabled
double currentSegment = std::floor(framePhase * segments);
if ((int)currentSegment % 2 == 1) {
output.y *= (1 - 2 * mirror);
}
// 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);
// Clip the shape to remain within this radial segment
double segmentSize = twoPi / segments; // Angular size
osci::Point upperPlaneNormal(std::sin(0.5 * segmentSize), -std::cos(0.5 * segmentSize), 0);
osci::Point lowerPlaneNormal(upperPlaneNormal.x, -upperPlaneNormal.y, 0);
osci::Point clippedOutput;
if (segmentSize < pi) {
clippedOutput = clipToPlane(output, upperPlaneNormal);
clippedOutput = clipToPlane(clippedOutput, lowerPlaneNormal);
// Point could still lie left of the origin along the lower plane
if (clippedOutput.x < 0) {
clippedOutput.x = 0;
clippedOutput.y = 0;
}
} else {
// segmentSize is wider than 180 degrees
// If the point is clipped to both planes like above, the actual result region
// will be less than 180 degrees
// So only clip to one plane at a time
if (output.y > 0) {
clippedOutput = clipToPlane(output, upperPlaneNormal);
} else {
clippedOutput = clipToPlane(output, lowerPlaneNormal);
}
}
output = (1 - clip) * output + clip * clippedOutput;
// Finally, rotate this radial segment to its actual location
double rotTheta = (currentSegment / segments) * twoPi;
output.rotate(0, 0, rotTheta);
double freqDivisor = std::ceil(segments - 1e-3);
framePhase += audioProcessor.frequency / freqDivisor / sampleRate;
framePhase = framePhase - std::floor(framePhase);
return output;
}
std::shared_ptr<osci::Effect> build() const override {
auto eff = std::make_shared<osci::Effect>(
std::make_shared<KaleidoscopeEffect>(audioProcessor),
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,
3.0, // default
1.0, // min
5.0, // max
0.0001f, // step
osci::LfoType::Sine,
0.25f // LFO frequency (Hz) – slow, visible rotation
),
new osci::EffectParameter("Kaeidoscope Segments",
"Controls how many times the input shape is rotationally duplicated around the centre.",
"kaleidoscopeSegments", VERSION_HINT, 6.0, 1.0, 6.0, 0.0001, osci::LfoType::Sine, 0.2),
new osci::EffectParameter("Mirror",
"Mirrors every other segment like a real kaleidoscope. Best used in combination with an even number of segments.",
"kaleidoscopeMirror", VERSION_HINT, 1.0, 0.0, 1.0),
new osci::EffectParameter("Spread",
"Controls the radial spread of each segment.",
"kaleidoscopeSpread", VERSION_HINT, 0.4, 0.0, 1.0),
new osci::EffectParameter("Clip",
"Clips each copy of the input shape within its own segment.",
"kaleidoscopeClip", VERSION_HINT, 1.0, 0.0, 1.0)
}
);
eff->setName("Kaleidoscope");
@ -79,5 +90,15 @@ public:
private:
OscirenderAudioProcessor &audioProcessor;
double phase = 0.0;
double framePhase = 0.0; // [0, 1]
// Clips points behind the plane to the plane
// Leaves points in front of the plane untouched
osci::Point clipToPlane(osci::Point input, osci::Point planeNormal)
{
double distToPlane = input.innerProduct(planeNormal);
double clippedDist = std::max(0.0, distToPlane);
double distAdjustment = clippedDist - distToPlane;
return input + distAdjustment * planeNormal;
}
};

Wyświetl plik

@ -0,0 +1,82 @@
// UnfoldEffect.h
// Repeats and mirrors the input around the origin to create a kaleidoscope pattern.
// Previously named BloomEffect. Renamed to UnfoldEffect.
#pragma once
#include <JuceHeader.h>
class UnfoldEffect : public osci::EffectApplication {
public:
explicit UnfoldEffect(OscirenderAudioProcessor &p) : audioProcessor(p) {}
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::jmax(values[0].load(), 1.0); // ensure at least 1 segment
// 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)
fullSegments = fractionalPart > 1e-3 ? fullSegments : fullSegments - 1;
phase = nextPhase(audioProcessor.frequency / (fullSegments + 1), sampleRate) / (2.0 * std::numbers::pi);
// Use 'segments' for timing so partial segment gets proportionally shorter time.
double currentSegmentFloat = phase * segments; // [0, segments)
int currentSegmentIndex = (int)std::floor(currentSegmentFloat);
if (currentSegmentIndex > fullSegments) currentSegmentIndex = fullSegments; // 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-3) ? 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<UnfoldEffect>(audioProcessor),
std::vector<osci::EffectParameter*>{
new osci::EffectParameter(
"Unfold Segments",
"Controls how many times the image is rotationally repeated around the centre. Fractional values smoothly morph the repetition.",
"unfoldSegments",
VERSION_HINT,
3.0, // default
1.0, // min
5.0, // max
0.0001f, // step
osci::LfoType::Sine,
0.25f // LFO frequency (Hz) – slow, visible rotation
),
}
);
eff->setName("Unfold");
eff->setIcon(BinaryData::unfold_svg);
return eff;
}
private:
OscirenderAudioProcessor &audioProcessor;
double phase = 0.0;
};

Wyświetl plik

@ -155,6 +155,7 @@
<FILE id="ESR7bv" name="trace.svg" compile="0" resource="1" file="Resources/svg/trace.svg"/>
<FILE id="ID1vTS" name="translate.svg" compile="0" resource="1" file="Resources/svg/translate.svg"/>
<FILE id="Sw4WWb" name="twist.svg" compile="0" resource="1" file="Resources/svg/twist.svg"/>
<FILE id="Y90Viq" name="unfold.svg" compile="0" resource="1" file="Resources/svg/unfold.svg"/>
<FILE id="praXUY" name="vector-cancelling.svg" compile="0" resource="1"
file="Resources/svg/vector-cancelling.svg"/>
<FILE id="qC6QiP" name="volume.svg" compile="0" resource="1" file="Resources/svg/volume.svg"/>
@ -173,6 +174,9 @@
</GROUP>
<GROUP id="{75439074-E50C-362F-1EDF-8B4BE9011259}" name="Source">
<GROUP id="{85A33213-D880-BD92-70D8-1901DA6D23F0}" name="audio">
<FILE id="XSUjDz" name="KaleidoscopeEffect.h" compile="0" resource="0"
file="Source/audio/KaleidoscopeEffect.h"/>
<FILE id="mREEpc" name="UnfoldEffect.h" compile="0" resource="0" file="Source/audio/UnfoldEffect.h"/>
<FILE id="jfRtxu" name="VortexEffect.h" compile="0" resource="0" file="Source/audio/VortexEffect.h"/>
<FILE id="de5H36" name="GodRayEffect.h" compile="0" resource="0" file="Source/audio/GodRayEffect.h"/>
<FILE id="tU2pQl" name="SkewEffect.h" compile="0" resource="0" file="Source/audio/SkewEffect.h"/>
@ -195,8 +199,6 @@
<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="BounceEffect.h" compile="0" resource="0" file="Source/audio/BounceEffect.h"/>
<FILE id="h0dMim" name="PerspectiveEffect.h" compile="0" resource="0"
file="Source/audio/PerspectiveEffect.h"/>
@ -916,4 +918,3 @@
<MODULE id="osci_render_core" showAllCode="1" useLocalCopy="0" useGlobalPath="0"/>
</MODULES>
</JUCERPROJECT>