Skip to content

Commit c884ddf

Browse files
committed
web: mouse-driven FX mapping (saturation: clean↔fizz, delay: opposite corners) + inertial visual swirl (spring-damper); docs: README/TODO updates
1 parent c22a516 commit c884ddf

File tree

7 files changed

+268
-37
lines changed

7 files changed

+268
-37
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ web-sys = { version = "0.3", features = [
3939
"OscillatorNode",
4040
"OscillatorType",
4141
"GainNode",
42+
"WaveShaperNode",
4243
"AudioNode",
4344
"AudioParam",
4445
"PannerNode",

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Web front-end (WASM) is running with:
88
- 3 voices, spatial audio (Web Audio + PannerNode)
99
- Lush ambient effects: global Convolver reverb and dark feedback Delay bus with per-voice sends and a master bus
10+
- Mouse-driven FX: corner-based saturation (clean ↔ fizz) and opposite-corner delay emphasis; visuals have inertial swirl motion
1011
- Start overlay to initialize audio (Click Start; canvas-click fallback)
1112
- Drag voices in XZ plane; click to mute, Shift+Click reseed, Alt+Click solo
1213
- Keyboard: R (reseed all), Space (pause), + / - (tempo), M (master mute), O (orbit on/off)
@@ -50,6 +51,7 @@ Controls in browser:
5051
- Drag a circle to move a voice in XZ plane (updates spatialization)
5152
- Click a voice: mute; Shift+Click: reseed; Alt+Click: solo
5253
- Keys: R (reseed all), Space (pause/resume), + / - (tempo), M (master mute), O (orbit on/off)
54+
- Mouse position maps to master saturation and delay; moving the pointer leaves a “water-like” trailing swirl in visuals
5355

5456
Headless test:
5557

crates/app-core/shaders/post.wgsl

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,19 @@ fn hash2(p: vec2<f32>) -> f32 {
9797
return fract(sin(h) * 43758.5453123);
9898
}
9999

100+
// Simple FBM using sin/cos mixing for soft, wispy noise
101+
fn fbm(p: vec2<f32>) -> f32 {
102+
var a = 0.0;
103+
var b = 0.5;
104+
var f = p;
105+
for (var i = 0; i < 5; i = i + 1) {
106+
a += b * sin(f.x) * cos(f.y);
107+
f *= 2.17;
108+
b *= 0.55;
109+
}
110+
return a;
111+
}
112+
100113
// COMPOSITE: tone-map HDR + add bloom + color grading and grain
101114
@fragment
102115
fn fs_composite(inp: VsOut) -> @location(0) vec4<f32> {
@@ -110,16 +123,41 @@ fn fs_composite(inp: VsOut) -> @location(0) vec4<f32> {
110123
let hue = vec3<f32>(sin(t * 1.2) + 1.0, sin(t * 1.5 + 2.0) + 1.0, sin(t * 1.8 + 4.0) + 1.0) * 0.05 * ambient;
111124
base *= (vec3<f32>(1.0) + hue);
112125

126+
// Darken exposure slightly before tonemapping to give a deeper look
127+
base *= 0.9;
128+
113129
// Tonemap
114130
var mapped = aces_tonemap(base);
115131

116-
// Vignette
132+
// Contrast and gamma to increase punch without blowing highlights
133+
let contrast = 0.15; // positive increases contrast
134+
mapped = clamp((mapped - vec3<f32>(0.5)) * (1.0 + contrast) + vec3<f32>(0.5), vec3<f32>(0.0), vec3<f32>(1.0));
135+
mapped = pow(mapped, vec3<f32>(1.07));
136+
137+
// Stronger vignette for moodier edges
117138
let vig = vignette(inp.uv);
118-
mapped *= mix(1.0, 0.85, vig);
139+
mapped *= mix(1.0, 0.75, vig);
140+
141+
// Smoky darkening using low-frequency FBM modulated by radius
142+
let uv = inp.uv;
143+
let r = length(uv - 0.5);
144+
let smokeField = 0.5 + 0.5 * fbm(uv * 2.6 + vec2<f32>(u_post.time * 0.05, -u_post.time * 0.04));
145+
let smokeField2 = 0.5 + 0.5 * fbm((uv.yx + vec2<f32>(0.17, -0.09)) * 3.1 + vec2<f32>(-u_post.time * 0.035, u_post.time * 0.045));
146+
let smoke = clamp(0.5 * smokeField + 0.5 * smokeField2, 0.0, 1.0);
147+
let radial = smoothstep(0.2, 0.95, r);
148+
let smokeStrength = 0.18; // overall intensity
149+
let k = smokeStrength * radial * smoke;
150+
// Darken multiplicatively; tiny bluish tint in the darkening
151+
let smokeTint = vec3<f32>(0.03, 0.04, 0.06);
152+
mapped = mapped * (1.0 - k) + smokeTint * (k * 0.25);
119153

120154
// Film grain
121155
let noise = hash2(inp.uv * u_post.resolution + u_post.time);
122-
mapped += (noise - 0.5) * 0.02;
156+
mapped += (noise - 0.5) * 0.022;
157+
158+
// Slight desaturation for a smokier palette
159+
let luma = luminance(mapped);
160+
mapped = mix(vec3<f32>(luma), mapped, 0.9);
123161

124162
return vec4<f32>(mapped, 1.0);
125163
}

crates/app-core/shaders/waves.wgsl

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -68,21 +68,69 @@ fn sd_segment(p: vec2<f32>, a: vec2<f32>, b: vec2<f32>) -> f32 {
6868
return length(pa - ba * h);
6969
}
7070

71-
fn wireframe_bg(p: vec2<f32>) -> f32 {
72-
var d = 1e9;
73-
let centers = array<vec2<f32>, 3>(
71+
// Returns a stylized intensity (0..1) for animated rings and lines.
72+
// It adds subtle orbital drift, pulsing radii, and moving dash highlights.
73+
fn wireframe_styled(p: vec2<f32>, t: f32) -> f32 {
74+
var m = 0.0;
75+
76+
// --- Animated concentric rings ---
77+
let base_centers = array<vec2<f32>, 3>(
7478
vec2<f32>(0.62, 0.28),
7579
vec2<f32>(0.80, 0.44),
7680
vec2<f32>(0.70, 0.72),
7781
);
78-
let radii = array<f32, 3>(0.28, 0.18, 0.12);
82+
let base_radii = array<f32, 3>(0.28, 0.18, 0.12);
7983
for (var i = 0; i < 3; i = i + 1) {
80-
d = min(d, abs(length(p - centers[i]) - radii[i]));
84+
let w = 0.7 + 0.3 * f32(i);
85+
let c = base_centers[i] + 0.01 * vec2<f32>(
86+
sin(t * (0.8 + 0.1 * f32(i)) + 3.0 * f32(i)),
87+
cos(t * (0.7 + 0.13 * f32(i)) + 1.7 * f32(i)),
88+
);
89+
let r = base_radii[i] + 0.03 * sin(t * (0.9 + 0.17 * f32(i)) + 2.1 * f32(i)) + 0.015 * sin(5.0 * t + 0.7 * f32(i));
90+
91+
let v = p - c;
92+
let ed = abs(length(v) - r);
93+
let ang = atan2(v.y, v.x);
94+
// Angular dash pattern that orbits over time
95+
let dash = 0.5 + 0.5 * sin(ang * 14.0 - t * (1.6 + 0.2 * f32(i)) + f32(i));
96+
let thickness = mix(0.010, 0.004, 0.5 + 0.5 * sin(t * 1.2 + f32(i)));
97+
let mask = smoothstep(thickness, 0.0, ed) * pow(dash, 1.1);
98+
m = max(m, mask * w);
8199
}
82-
d = min(d, sd_segment(p, vec2<f32>(0.55, 0.20), vec2<f32>(0.85, 0.50)));
83-
d = min(d, sd_segment(p, vec2<f32>(0.62, 0.28), vec2<f32>(0.80, 0.44)));
84-
d = min(d, sd_segment(p, vec2<f32>(0.72, 0.70), vec2<f32>(0.86, 0.52)));
85-
return d;
100+
101+
// --- Elegant connecting lines with traveling glints ---
102+
let segs_a = array<vec2<f32>, 3>(
103+
vec2<f32>(0.55, 0.20),
104+
vec2<f32>(0.62, 0.28),
105+
vec2<f32>(0.72, 0.70),
106+
);
107+
let segs_b = array<vec2<f32>, 3>(
108+
vec2<f32>(0.85, 0.50),
109+
vec2<f32>(0.80, 0.44),
110+
vec2<f32>(0.86, 0.52),
111+
);
112+
for (var i = 0; i < 3; i = i + 1) {
113+
// slight endpoint drift
114+
let a = segs_a[i] + 0.01 * vec2<f32>(sin(t * 0.6 + f32(i)), cos(t * 0.7 + 2.1 * f32(i)));
115+
let b = segs_b[i] + 0.01 * vec2<f32>(cos(t * 0.5 + 1.3 * f32(i)), sin(t * 0.65 + f32(i)));
116+
117+
// Distance to the segment
118+
let pa = p - a;
119+
let ba = b - a;
120+
let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
121+
let q = a + ba * h;
122+
let ed = length(p - q);
123+
124+
// Position along the segment for dash animation
125+
let along = h; // 0..1 along the segment
126+
let dash = 0.5 + 0.5 * sin(along * 30.0 - t * (2.3 + 0.2 * f32(i)));
127+
let glint = smoothstep(0.96, 1.0, sin(along * 6.28318 - t * 1.8 + f32(i)));
128+
let thickness = 0.006;
129+
let mask = smoothstep(thickness, 0.0, ed) * (0.65 * dash + 0.35 * glint);
130+
m = max(m, mask * (0.85 + 0.15 * f32(i)));
131+
}
132+
133+
return clamp(m, 0.0, 1.0);
86134
}
87135

88136
@fragment
@@ -92,13 +140,9 @@ fn fs_waves(inp: VsOut) -> @location(0) vec4<f32> {
92140
let cuv0 = (uv - 0.5) * vec2<f32>(aspect, 1.0);
93141
let t = u.time;
94142

95-
// Background wireframe
143+
// Background base only (remove decorative rings/lines for clarity)
96144
let gold = vec3<f32>(1.00, 0.86, 0.46);
97-
let d = wireframe_bg(uv);
98145
var col = vec3<f32>(0.04, 0.055, 0.10);
99-
let line = smoothstep(0.012, 0.002, d);
100-
// Make wireframe more visible; it will bloom subtly
101-
col += gold * line * (0.30 + 0.60 * u.ambient);
102146

103147
// Three layered waves with parallax
104148
for (var L = 0; L < 3; L = L + 1) {

crates/app-native/src/main.rs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -752,7 +752,29 @@ fn mix_sample_stereo(oscillators: &mut Vec<ActiveOscillator>) -> (f32, f32) {
752752
}
753753
i += 1;
754754
}
755-
(left.tanh(), right.tanh())
755+
// Return linear mix; master saturation is applied downstream
756+
(left, right)
757+
}
758+
759+
fn saturate_sample_arctan(input: f32, drive: f32) -> f32 {
760+
// Soft, analog-like symmetrical arctan curve
761+
(2.0 / std::f32::consts::PI) * (drive * input).atan()
762+
}
763+
764+
fn apply_master_saturation(left: f32, right: f32) -> (f32, f32) {
765+
// Tuned for subtle warmth and gentle compression
766+
let drive = 1.6f32; // input drive into shaper
767+
let wet = 0.35f32; // wet mix amount
768+
let pre_gain = 0.9f32; // headroom before shaping
769+
let post_gain = 1.05f32; // slight makeup gain
770+
771+
let l_in = left * pre_gain;
772+
let r_in = right * pre_gain;
773+
let l_sat = saturate_sample_arctan(l_in, drive);
774+
let r_sat = saturate_sample_arctan(r_in, drive);
775+
let l_out = (wet * l_sat + (1.0 - wet) * l_in) * post_gain;
776+
let r_out = (wet * r_sat + (1.0 - wet) * r_in) * post_gain;
777+
(l_out.clamp(-1.0, 1.0), r_out.clamp(-1.0, 1.0))
756778
}
757779

758780
fn build_stream_f32(
@@ -769,7 +791,8 @@ fn build_stream_f32(
769791
let oscillators = &mut guard.oscillators;
770792
let mut frame = 0usize;
771793
while frame < data.len() {
772-
let (l, r) = mix_sample_stereo(oscillators);
794+
let (l_raw, r_raw) = mix_sample_stereo(oscillators);
795+
let (l, r) = apply_master_saturation(l_raw, r_raw);
773796
if channels >= 2 {
774797
if frame < data.len() {
775798
data[frame] = l;
@@ -802,9 +825,10 @@ fn build_stream_i16(
802825
let oscillators = &mut guard.oscillators;
803826
let mut frame = 0usize;
804827
while frame < data.len() {
805-
let (l, r) = mix_sample_stereo(oscillators);
806-
let vl = (l * i16::MAX as f32) as i16;
807-
let vr = (r * i16::MAX as f32) as i16;
828+
let (l_raw, r_raw) = mix_sample_stereo(oscillators);
829+
let (l, r) = apply_master_saturation(l_raw, r_raw);
830+
let vl = (l.clamp(-1.0, 1.0) * i16::MAX as f32) as i16;
831+
let vr = (r.clamp(-1.0, 1.0) * i16::MAX as f32) as i16;
808832
if channels >= 2 {
809833
if frame < data.len() {
810834
data[frame] = vl;
@@ -837,7 +861,8 @@ fn build_stream_u16(
837861
let oscillators = &mut guard.oscillators;
838862
let mut frame = 0usize;
839863
while frame < data.len() {
840-
let (l, r) = mix_sample_stereo(oscillators);
864+
let (l_raw, r_raw) = mix_sample_stereo(oscillators);
865+
let (l, r) = apply_master_saturation(l_raw, r_raw);
841866
let vl = (((l * 0.5 + 0.5).clamp(0.0, 1.0)) * u16::MAX as f32) as u16;
842867
let vr = (((r * 0.5 + 0.5).clamp(0.0, 1.0)) * u16::MAX as f32) as u16;
843868
if channels >= 2 {

0 commit comments

Comments
 (0)