Temporal Plurality (WIP)
Time as Material
Note: This tutorial is a work in progress. More cards, examples and demos will be updated soon.
The first three tutorials in this series taught you what data is, how it transforms, and how it becomes geometry. Every example ran continuously: nodes processed samples, buffers cycled, the engine drove everything at its own rate. You declared structure and the system ran it.
This tutorial takes that question back. Not by overriding the engine, but by choosing precisely when your code runs, for how long, under what conditions, and in response to what. The mechanisms range from a single callback attached to a node that was already computing, to a coroutine you write from scratch that suspends and resumes at sample-accurate positions in time. Between those two ends is everything you need to make computation temporal rather than just continuous.
The eight cards that follow move from the simplest attachment (a callback on an existing node's tick) through increasingly direct control, ending at the raw coroutine infrastructure that everything else is built from. By the end, none of the earlier cards will be opaque: you will know what runs underneath `schedule_metro`, `EventChain`, `line`, `Trigger`, and `Fabric`, because you will have written equivalents yourself.

Card 1: Declare Once vs. Control
Click this card to reveal full explanation
Tutorial: Metro
void compose() {
auto window = MayaFlux::create_window({ "Time", 1200, 800 });
auto points = vega.PointCollectionNode() | Graphics;
auto buffer = vega.GeometryBuffer(points) | Graphics;
buffer->setup_rendering({ .target_window = window });
window->show();
auto& angle = make_persistent(0.0f);
auto& point_count = make_persistent(0U);
MayaFlux::schedule_metro(0.016, [points, &angle, &point_count]() {
float spiral_offset = 0.25f * point_count;
float r = 0.6f + 0.3f * std::sin((angle + spiral_offset) * 0.4f);
float x = r * std::cos(angle + spiral_offset);
float y = r * std::sin(angle + spiral_offset);
float hue = (angle + spiral_offset) / (2.0f * M_PI);
glm::vec3 color(
std::abs(std::sin(hue * 3.14f)),
std::abs(std::sin(hue * 3.14f + 2.09f)),
std::abs(std::sin(hue * 3.14f + 4.19f)));
points->add_point({ glm::vec3(x, y, 0.0f), color, 6.0f });
angle += 0.18f;
++point_count;
});
}Run this. A spiral of colored points grows across the window at a fixed pace. One point every 16ms, regardless of anything else in the system. The rate is a wall-time promise measured in samples internally, but from the outside it behaves like a fixed interval.
Tutorial: Impulse node with on_impulse
void compose() {
auto window = MayaFlux::create_window({ "Time", 1200, 800 });
auto points = vega.PointCollectionNode() | Graphics;
auto buffer = vega.GeometryBuffer(points) | Graphics;
buffer->setup_rendering({ .target_window = window });
window->show();
auto clock = vega.Impulse(2.0f) | Graphics;
auto rate_mod = vega.Sine(0.1f) | Graphics;
clock->set_frequency_modulator(rate_mod);
auto& angle = make_persistent(0.0f);
auto& count = make_persistent(0U);
clock->on_impulse([points, clock, &angle, &count](const Nodes::NodeContext& ctx) {
float r = 0.6f + 0.3f * std::sin(angle * 0.4f);
float x = r * std::cos(angle);
float y = r * std::sin(angle);
float hue = angle / (2.0f * M_PI);
glm::vec3 color(
std::abs(std::sin(hue * 3.14f)),
std::abs(std::sin(hue * 3.14f + 2.09f)),
std::abs(std::sin(hue * 3.14f + 4.19f)));
points->add_point({ glm::vec3(x, y, 0.0f), color, 6.0f });
angle += 0.18f;
++count;
if (count == 200) {
clock->set_frequency(6.0f);
}
});
}Run this. The same spiral grows, but the pace breathes. It slows down, speeds up, slows again.
After 200 points the frequency jumps from 2 Hz to 6 Hz and stays there, triple the original rate,
with no teardown and no new callback. The clock capture lets the callback reach back
into the node that fired it and change its own future rate on the fly.
Change 0.1f to 0.5f on the Sine. The breathing is faster and more dramatic.
Change the Impulse base frequency from 2.0f to 6.0f. More points, still
breathing. The modulator and the base rate are independent parameters, both live, both modifiable
without touching the callback.

Card 2: Control as Side Effect
Click this card to reveal full explanation
Important: Quick explaination of timing models
void compose() {
auto sine = vega.Sine(2.0f) | Audio[0];
sine->on_tick([](auto& ctx) {
// fires 48000 times per second
// (void)ctx;
std::cout << ctx.value << " at audio rate\n";
});
auto point_node = vega.PointNode() | Graphics;
point_node->on_tick([](auto& ctx) {
// fires 60 times per second
// (void)ctx;
std::cout << ctx.value << " at graphics rate\n";
});
}The Audio node ticks at sample rate. The Graphics node ticks at frame rate. Same method, two orders
of magnitude apart in frequency. | Audio[0] and | Graphics do not just
register the node with different subsystems; they determine the entire temporal contract of every
hook attached to that node. on_tick on a Graphics node is frame-rate code.
on_tick on an Audio node is DSP-rate code. Putting expensive work into an audio-rate
on_tick stalls the audio thread. This is why on_impulse,
on_count, and on_increment exist: they give you audio-rate precision
with call frequency you can reason about.
The previous example with the metro and on_impulse did not explore the side effects of time.
For metro, there is none as time is fixed. But for nodes, while on_tick itself does not beyond
rate of the backend/subsystem responsible for ticking, on_impulse and similar hooks are dependent on the logic of the signal.
For instance, the impulse rate is not constant if it were frequency modulated. Or if the node in question were inherently dependent on other signals,
input sources, non deterministic logic, or any other factor that could affect the timing of the used hook callback.
Tutorial: Counter driving a path
void compose() {
auto window = MayaFlux::create_window({ "Time", 1200, 800 });
window->show();
auto path = vega.PathGeneratorNode(
Kinesis::InterpolationMode::CATMULL_ROM, 32, 128
) | Graphics;
path->set_path_color(glm::vec3(0.4f, 0.8f, 1.0f));
path->set_path_thickness(2.0f);
auto buffer = vega.GeometryBuffer(path) | Graphics;
buffer->setup_rendering({ .target_window = window });
auto clock = vega.Impulse(0.5f);
clock->set_frequency_modulator(vega.Sine(0.08f));
auto counter = vega.Counter(64) | Audio[0];
counter->set_reset_trigger(clock);
counter->on_increment([path](auto& ctx) {
float t = static_cast<float>(ctx.phase);
float angle = t * 2.0f * M_PI / 64.0f;
float r = 0.3f + 0.5f * std::sin(t * 0.4f);
glm::vec3 pos(r * std::cos(angle), r * std::sin(angle), 0.0f);
path->add_control_point({ pos, glm::vec3(1.0f - t / 64.0f, t / 64.0f, 0.6f), 2.0f });
});
counter->on_wrap([path](auto&) {
path->set_path_color(glm::vec3(
get_uniform_random(0.3f, 1.0f),
get_uniform_random(0.3f, 1.0f),
get_uniform_random(0.3f, 1.0f)
));
});
}Run this. A curved path grows across the window, tracing a slowly shifting orbit. The
PathGeneratorNode ring holds 128 points; old ones age out as new ones arrive.
On each wrap the color randomizes - a structural moment with no geometry destroyed.
The Impulse and its Sine modulator need no domain registration - they are processed because
the Counter registers the Impulse as its reset trigger, and the Impulse registers the Sine
as its frequency modulator. Only the Counter needs | Audio[0].
on_increment fires once per counter tick. on_wrap fires exactly once
at the modulo boundary. Neither is audio-rate in practice: the Impulse fires at 0.5Hz modulated,
so on_increment fires at that rate regardless of the 48000Hz substrate underneath.
Example: Counter with on_count for structural moments
void compose() {
auto window = MayaFlux::create_window({ "Time", 1200, 800 });
window->show();
auto path = vega.PathGeneratorNode(
Kinesis::InterpolationMode::CATMULL_ROM, 32, 128
) | Graphics;
path->set_path_color(glm::vec3(1.0f, 0.5f, 0.2f));
path->set_path_thickness(2.5f);
auto buffer = vega.GeometryBuffer(path) | Graphics;
buffer->setup_rendering({ .target_window = window });
auto clock = vega.Impulse(0.5f);
auto counter = vega.Counter(32) | Audio[0];
counter->set_reset_trigger(clock);
counter->on_increment([path](auto& ctx) {
float t = static_cast<float>(ctx.phase);
float angle = t * 2.0f * M_PI / 32.0f;
float r = 0.5f + 0.2f * std::cos(t * 1.3f);
path->add_control_point({
glm::vec3(r * std::cos(angle), r * std::sin(angle), 0.0f),
glm::vec3(0.9f, 0.5f, 0.2f),
2.0f
});
});
counter->on_count(8, [path](auto&) { path->set_path_color(glm::vec3(0.2f, 0.9f, 0.5f)); });
counter->on_count(16, [path](auto&) { path->set_path_color(glm::vec3(0.5f, 0.2f, 0.9f)); });
counter->on_count(24, [path](auto&) { path->set_path_color(glm::vec3(0.9f, 0.2f, 0.2f)); });
counter->on_wrap([path](auto&) {
path->set_path_color(glm::vec3(1.0f, 0.5f, 0.2f));
});
}The path grows in four color phases: orange through 8 steps, then green, then purple, then red, then wraps and resets the color. Change the Impulse frequency and the phase structure stays identical - only the wall-time duration of each phase changes. Sequence structure is decoupled from speed.
Example: Temporal Logic as a gate
void compose() {
auto window = MayaFlux::create_window({ "Time", 1200, 800 });
window->show();
auto path = vega.PathGeneratorNode(
Kinesis::InterpolationMode::CATMULL_ROM, 20, 256
) | Graphics;
auto gbuf = vega.GeometryBuffer(path) | Graphics;
gbuf->setup_rendering({ .target_window = window });
auto lfo = vega.Sine(0.3f) | Audio[0];
auto gate = vega.Logic(
[](double input, double elapsed) -> bool {
double wn = std::fmod(elapsed, 6.0);
return wn > 1.0 && wn < 4.0;
}
) | Audio[1];
gate->set_input_node(lfo);
auto& n = make_persistent(0U);
auto& x_pos = make_persistent(-0.8f);
gate->while_true([&n, path, lfo, &x_pos](auto&) {
if (++n % 800 != 0) return;
float v = static_cast<float>(lfo->get_last_output());
float x = std::fmod(static_cast<float>(n) / 800.0f, 533.0f) / 533.0f * 1.6f - 0.8f;
path->add_control_point({
.position = glm::vec3((x * x_pos) + get_uniform_random(-0.05f, 0.5f),
v * 0.6f + get_uniform_random(-0.05f, 0.05f), 0.0f),
.color = glm::vec3(0.9f, get_exponential_random(), 0.5f + 0.4f * std::abs(v)),
.thickness = (float)get_uniform_random(0.02f, 1.6f)
});
x_pos += 0.006f;
});
gate->on_change_to(false, [path, &n, &x_pos](auto&) {
auto pts = path->get_control_points();
for (auto& pt : pts) {
pt.color = glm::vec3(
get_uniform_random(0.3f, 1.0f),
get_uniform_random(0.3f, 1.0f),
get_uniform_random(0.3f, 1.0f));
}
path->set_control_points(pts);
n = 0;
x_pos = -0.8f;
});
}For the first second of every six nothing draws. From second 1 to 4 the gate opens and the path accumulates. At second 4 the gate closes: existing points are recolored in place and cursors reset. The LFO is not the criterion - elapsed time is. The signal is only the material being drawn.
gate needs | Audio[1] because Logic is a Generator the scheduler
owns independently. It pulls its source via set_input_node.
The elapsed parameter is m_temporal_time, which increments by
1.0 / sample_rate on every sample - sample-counted elapsed time, not wall-clock.

Card 3: Tiered Control
Click this card to reveal full explanation
Tutorial: Sequence
void compose() {
auto window = MayaFlux::create_window({ "Time", 1200, 800 });
auto particles = vega.ParticleNetwork(
300,
glm::vec3(-1.5f, -1.5f, -0.5f),
glm::vec3(1.5f, 1.5f, 0.5f),
Kinesis::SpatialDistribution::RANDOM_VOLUME
) | Graphics;
auto* physics = particles->create_operator<PhysicsOperator>();
physics->set_drag(0.01f);
physics->set_bounds_mode(PhysicsOperator::BoundsMode::BOUNCE);
auto buffer = vega.NetworkGeometryBuffer(particles) | Graphics;
buffer->setup_rendering({ .target_window = window });
window->show();
MayaFlux::schedule_sequence({
{ 0.0, [physics]() {
physics->set_turbulence_strength(0.8f);
}},
{ 2.0, [physics]() {
physics->set_turbulence_strength(0.0f);
physics->enable_spatial_interactions(true);
physics->set_interaction_radius(0.4f);
physics->set_spring_stiffness(0.6f);
}},
{ 2.5, [physics]() {
physics->set_repulsion_strength(2.0f);
physics->set_interaction_radius(0.2f);
}},
{ 2.0, [physics]() {
physics->enable_spatial_interactions(false);
physics->set_drag(0.08f);
}},
{ 2.0, [physics]() {
physics->set_drag(0.01f);
physics->set_turbulence_strength(0.4f);
}},
});
}Run this. Particles start in chaotic turbulence, then settle as spatial interactions switch on and spring forces pull them into loose clusters. Repulsion tightens, clusters compress. Interactions cut and drag bleeds off momentum. Then turbulence returns at half strength and the arc ends.
The sequence is a vector of (delay, callback) pairs. Each delay is the interval
after the previous event. Cumulative time: 0 + 2.0 + 2.5 + 2.0 + 2.0 = 8.5 seconds total.
Tutorial: EventChain
void compose() {
auto window = MayaFlux::create_window({ "Time", 1200, 800 });
auto particles = vega.ParticleNetwork(
300,
glm::vec3(-1.5f, -1.5f, -0.5f),
glm::vec3(1.5f, 1.5f, 0.5f),
Kinesis::SpatialDistribution::RANDOM_VOLUME
) | Graphics;
auto* physics = particles->create_operator<PhysicsOperator>();
physics->set_drag(0.01f);
physics->set_bounds_mode(PhysicsOperator::BoundsMode::BOUNCE);
auto buffer = vega.NetworkGeometryBuffer(particles) | Graphics;
buffer->setup_rendering({ .target_window = window });
window->show();
Kriya::EventChain chain(*MayaFlux::get_scheduler());
chain.then([physics]() {
physics->set_turbulence_strength(0.8f);
})
.then([physics]() {
physics->set_turbulence_strength(0.0f);
physics->enable_spatial_interactions(true);
physics->set_interaction_radius(0.4f);
physics->set_spring_stiffness(0.6f);
}, 2.0)
.then([physics]() {
physics->set_repulsion_strength(2.0f);
physics->set_interaction_radius(0.2f);
}, 2.5)
.then([physics]() {
physics->enable_spatial_interactions(false);
physics->set_drag(0.08f);
}, 2.0)
.then([physics]() {
physics->set_drag(0.01f);
physics->set_turbulence_strength(0.4f);
}, 2.0)
.start();
}Run this. The same arc as the sequence above, same timing, same result.
The structural difference is readability and composability. EventChain is a
builder: each .then() returns the same chain, so you can read the choreography
top to bottom. For short sequences the difference is minor; for longer choreographies with
repetition or a completion callback, the chain's additional methods become significant.
Example: EventChain with repetition and completion
void compose() {
auto window = MayaFlux::create_window({ "Time", 1200, 800 });
auto particles = vega.ParticleNetwork(
300,
glm::vec3(-1.5f, -1.5f, -0.5f),
glm::vec3(1.5f, 1.5f, 0.5f),
Kinesis::SpatialDistribution::RANDOM_VOLUME
) | Graphics;
auto* physics = particles->create_operator<PhysicsOperator>();
physics->set_drag(0.02f);
physics->set_bounds_mode(PhysicsOperator::BoundsMode::WRAP);
auto buffer = vega.NetworkGeometryBuffer(particles) | Graphics;
buffer->setup_rendering({ .target_window = window });
window->show();
Kriya::EventChain chain(*MayaFlux::get_scheduler());
chain.then([physics]() {
physics->set_attraction_point(glm::vec3(
get_uniform_random(-0.8f, 0.8f),
get_uniform_random(-0.8f, 0.8f),
0.0f
));
}, 1.2)
.repeat(3)
.then([physics]() {
physics->clear_attraction_point();
physics->set_turbulence_strength(0.6f);
}, 1.5)
.on_complete([physics]() {
physics->set_turbulence_strength(0.0f);
physics->set_drag(0.02f);
})
.times(3)
.start();
}The attraction point jumps to four random positions 1.2 seconds apart, pulling the cloud
toward each in sequence. Then attraction clears and turbulence scatters everything. That
full arc runs 3 times. After the third pass on_complete cuts the turbulence
and resets drag.
.repeat(3) appends 3 more copies of the first event: 1 + 3 = 4 attraction
moves total. .times(3) runs the whole list 3 times from the start.
Card 4: ONLYWHENs
Click this card to reveal full explanation
Tutorial: TimedAction : bracketed state
void compose() {
const std::vector<float> freqs = { 220.0f, 277.0f, 330.0f, 370.0f, 440.0f, 554.0f, 660.0f };
std::vector<std::shared_ptr<Sine>> sines;
for (auto f : freqs) {
auto s = vega.Sine(f) | Audio[0];
s->set_amplitude(0.08);
sines.push_back(s);
}
auto action = std::make_shared<Kriya::TimedAction>(*get_scheduler());
store(action);
auto clock = vega.Impulse(0.15f) | Audio[0];
clock->on_impulse([sines, freqs, action](const Nodes::NodeContext&) {
action->execute(
[sines]() {
for (auto& s : sines)
s->set_frequency(220.0f);
},
[sines, freqs]() {
for (int i = 0; i < (int)sines.size(); ++i)
sines[i]->set_frequency(freqs[i]);
},
get_uniform_random(0.5, 2.0)
);
});
}Seven sines tuned to a major seventh chord run continuously. Every ~6.5 seconds the Impulse
fires and the chord collapses: all voices snap to 220Hz. After a random 0.5 to 2 second
window they restore to their original frequencies. The collapse and restore are
sample-accurate. Rapid impulses do not stack brackets; a new execute call
cancels the pending restore and restarts the duration from the new trigger point.
Try: change the Impulse to 0.08f and the duration to
get_uniform_random(4.0, 8.0). The bracket now outlasts the impulse period.
The next impulse fires while still collapsed, restarting from unison again. The chord
barely has time to breathe before the next collapse.
Tutorial: >> Time(N) — rotating partials
void compose() {
auto base = vega.Sine(110.0f) | Audio[0];
base->set_amplitude(0.3);
const std::vector<float> partials = { 330.0f, 550.0f, 770.0f, 990.0f, 1320.0f, 1650.0f, 2200.0f };
auto clock = vega.Impulse(1.2f) | Audio[0];
clock->set_frequency_modulator(vega.Sine(0.05f));
clock->on_impulse([partials](auto&) {
float dur = get_uniform_random(0.3, 1.5);
auto idx = (int)get_uniform_random(0, 6);
auto s0 = vega.Sine(partials[idx]);
auto s1 = vega.Sine(partials[(idx + 2) % 7]);
auto s2 = vega.Sine(partials[(idx + 4) % 7]);
s0->set_amplitude(0.10);
s1->set_amplitude(0.07);
s2->set_amplitude(0.05);
s0 >> Time(dur) | Audio[0];
s1 >> Time(dur * 0.6f) | Audio[1];
s2 >> Time(dur * 0.35f) | Audio[0];
});
}A 110Hz base sustains permanently. Each impulse fires three partials from the array, selected by stepping two positions apart so they always form a consistent intervallic relationship. Each partial has its own duration: the longest on channel 0, a mid-length on channel 1, the shortest back on channel 0. They enter together and leave at different times, so the gesture decays in layers rather than cutting simultaneously.
The slow Sine modulating the clock breathes the impulse density over roughly 20 second
cycles. At peak density, partial clusters overlap: several are still alive when the next
impulse fires its own set. Change (idx + 2) % 7 and
(idx + 4) % 7 to (idx + 1) % 7 and (idx + 6) % 7
for a tighter cluster plus a distant partial instead of even thirds.

Card 5: External Time
Click this card to reveal full explanation
All previous cards drew time from internal sources: oscillators, counters, coroutines, signal conditions. This card is about input that arrives from outside the processing graph entirely: a key press, a mouse position, an OSC message. None of these has a sample-accurate clock. They arrive when they arrive.
The mechanism is the same regardless of source. External events convert into either window event
coroutines or InputNode values. From that point the hook vocabulary from the previous
cards applies normally.
Tutorial: Keyboard events reshaping mesh slot transforms
void compose() {
auto window = MayaFlux::create_window({ "External", 1200, 800 });
auto make_face = [](glm::vec3 color, glm::vec3 normal, glm::vec3 tangent,
std::array<glm::vec3, 4> corners)
-> std::pair<std::vector<MeshVertex>, std::vector<uint32_t>> {
std::vector<MeshVertex> verts;
verts.reserve(4);
const std::array<glm::vec2, 4> uvs = { glm::vec2{0,0},{1,0},{1,1},{0,1} };
for (int i = 0; i < 4; ++i)
verts.push_back({ corners[i], color, 1.f, uvs[i], normal, tangent });
return { verts, { 0, 1, 2, 2, 3, 0 } };
};
constexpr float H = 0.5f;
struct FaceSpec {
std::string name;
glm::vec3 color, normal, tangent;
std::array<glm::vec3, 4> corners;
};
const std::vector<FaceSpec> specs = {
{ "front", {0.9f,0.3f,0.2f}, { 0, 0, 1}, { 1, 0, 0}, {{{-H,-H, H},{ H,-H, H},{ H, H, H},{-H, H, H}}} },
{ "back", {0.2f,0.5f,0.9f}, { 0, 0,-1}, {-1, 0, 0}, {{{ H,-H,-H},{-H,-H,-H},{-H, H,-H},{ H, H,-H}}} },
{ "top", {0.2f,0.9f,0.3f}, { 0, 1, 0}, { 1, 0, 0}, {{{-H, H,-H},{ H, H,-H},{ H, H, H},{-H, H, H}}} },
{ "bottom", {0.9f,0.8f,0.1f}, { 0,-1, 0}, { 1, 0, 0}, {{{-H,-H, H},{ H,-H, H},{ H,-H,-H},{-H,-H,-H}}} },
{ "right", {0.7f,0.2f,0.9f}, { 1, 0, 0}, { 0, 0,-1}, {{{ H,-H, H},{ H,-H,-H},{ H, H,-H},{ H, H, H}}} },
{ "left", {0.9f,0.5f,0.1f}, {-1, 0, 0}, { 0, 0, 1}, {{{-H,-H,-H},{-H,-H, H},{-H, H, H},{-H, H,-H}}} },
};
auto net = vega.MeshNetwork() | Graphics;
struct SlotState {
glm::vec3 normal;
std::atomic<float> offset { 0.f };
std::atomic<float> velocity { 0.f };
std::atomic<bool> held { false };
};
auto states = std::make_shared<std::vector<SlotState>>(specs.size());
for (size_t i = 0; i < specs.size(); ++i) {
auto [verts, indices] = make_face(
specs[i].color, specs[i].normal, specs[i].tangent, specs[i].corners);
auto node = std::make_shared<Nodes::GpuSync::MeshWriterNode>(4);
node->set_mesh(verts, indices);
net->add_slot(specs[i].name, node);
(*states)[i].normal = specs[i].normal;
}
auto buf = vega.MeshNetworkBuffer(net) | Graphics;
buf->setup_rendering({ .target_window = window });
buf->get_render_processor()->set_view_transform(
Kinesis::look_at_perspective(
{2.5f, 2.0f, 3.5f}, {0,0,0},
glm::radians(50.f), 1200.f/800.f, 0.01f, 1000.f));
window->show();
MayaFlux::schedule_metro(1.0 / 60.0, [net, states]() {
auto& slots = net->slots();
for (size_t i = 0; i < slots.size(); ++i) {
auto& s = (*states)[i];
float v = s.velocity.load();
float o = s.offset.load();
if (s.held.load()) v += 0.012f;
o += v;
v *= 0.88f;
o *= 0.94f;
s.offset.store(o);
s.velocity.store(v);
slots[i].local_transform = glm::translate(glm::mat4(1.f), s.normal * o);
slots[i].dirty = true;
}
});
MayaFlux::on_key_pressed(window, IO::Keys::I, [states]() { (*states)[0].held.store(true, std::memory_order_relaxed); });
MayaFlux::on_key_released(window, IO::Keys::I, [states]() { (*states)[0].held.store(false, std::memory_order_relaxed); });
MayaFlux::on_key_pressed(window, IO::Keys::O, [states]() { (*states)[1].held.store(true, std::memory_order_relaxed); });
MayaFlux::on_key_released(window, IO::Keys::O, [states]() { (*states)[1].held.store(false, std::memory_order_relaxed); });
MayaFlux::on_key_pressed(window, IO::Keys::K, [states]() { (*states)[2].held.store(true, std::memory_order_relaxed); });
MayaFlux::on_key_released(window, IO::Keys::K, [states]() { (*states)[2].held.store(false, std::memory_order_relaxed); });
MayaFlux::on_key_pressed(window, IO::Keys::J, [states]() { (*states)[3].held.store(true, std::memory_order_relaxed); });
MayaFlux::on_key_released(window, IO::Keys::J, [states]() { (*states)[3].held.store(false, std::memory_order_relaxed); });
MayaFlux::on_key_pressed(window, IO::Keys::H, [states]() { (*states)[4].held.store(true, std::memory_order_relaxed); });
MayaFlux::on_key_released(window, IO::Keys::H, [states]() { (*states)[4].held.store(false, std::memory_order_relaxed); });
MayaFlux::on_key_pressed(window, IO::Keys::L, [states]() { (*states)[5].held.store(true, std::memory_order_relaxed); });
MayaFlux::on_key_released(window, IO::Keys::L, [states]() { (*states)[5].held.store(false, std::memory_order_relaxed); });
bind_viewport_preset(window,
buf->get_render_processor(), ViewportPresetMode::Fly, {}, "cube_keys");
}Run this. Six colored faces sit assembled as a cube. Press I and the front face slides outward along its normal. Hold it and it continues to push. Release and it decays back. H/J/K/L/I/O each control one face independently. Multiple keys held simultaneously push multiple faces outward. The Fly preset binds its own camera keys separately; both sets of bindings coexist and do different things.
Tutorial: Mouse position deforming mesh vertices
void compose() {
auto window = MayaFlux::create_window({ "External", 1200, 800 });
constexpr int N = 24;
constexpr float SIZE = 2.0f;
constexpr float STEP = SIZE / static_cast<float>(N - 1);
std::vector<uint32_t> indices;
for (int row = 0; row < N - 1; ++row)
for (int col = 0; col < N - 1; ++col) {
uint32_t tl = row * N + col;
indices.insert(indices.end(), { tl, tl+N, tl+1, tl+1, tl+N, tl+N+1 });
}
std::vector<MeshVertex> verts(N * N);
for (int row = 0; row < N; ++row)
for (int col = 0; col < N; ++col) {
auto& v = verts[row * N + col];
v.position = { col * STEP - SIZE * 0.5f, 0.f, row * STEP - SIZE * 0.5f };
v.color = { 0.3f, 0.6f, 0.9f };
v.normal = { 0.f, 1.f, 0.f };
v.tangent = { 1.f, 0.f, 0.f };
v.uv = { float(col)/(N-1), float(row)/(N-1) };
v.weight = 0.f;
}
auto mesh = vega.MeshWriterNode(N * N) | Graphics;
mesh->set_mesh(verts, indices);
auto buf = vega.GeometryBuffer(mesh) | Graphics;
buf->setup_rendering({ .target_window = window });
buf->get_render_processor()->set_view_transform(
Kinesis::look_at_perspective(
{0.f, 3.f, 3.f}, {0.f, 0.f, 0.f},
glm::radians(50.f), 1200.f/800.f, 0.1f, 100.f));
window->show();
auto mouse_x = std::make_shared<std::atomic<float>>(0.f);
auto mouse_y = std::make_shared<std::atomic<float>>(0.f);
MayaFlux::on_mouse_move(window, [mouse_x, mouse_y, window](double x, double y) {
const auto& ws = window->get_state();
mouse_x->store(float(x / ws.current_width) * 2.f - 1.f, std::memory_order_relaxed);
mouse_y->store(float(y / ws.current_height) * 2.f - 1.f, std::memory_order_relaxed);
});
MayaFlux::schedule_metro(1.0 / 60.0, [mesh, verts, mouse_x, mouse_y]() mutable {
const float mx = mouse_x->load(std::memory_order_relaxed);
const float my = mouse_y->load(std::memory_order_relaxed);
for (auto& v : verts) {
const float dx = v.position.x - mx;
const float dz = v.position.z - my;
const float y = std::exp(-(dx*dx + dz*dz) * 2.5f) * 0.6f;
v.position.y = y;
v.weight = y / 0.6f;
v.color = glm::mix(
glm::vec3(0.1f, 0.3f, 0.8f),
glm::vec3(0.9f, 0.7f, 0.2f),
v.weight);
}
mesh->set_mesh_vertices(verts);
});
}Run this. A flat grid deforms under a Gaussian hill centered on the mouse cursor. Move the
mouse and the hill follows, coloring from blue to gold at the peak. Change 2.5f
in the exponent to 8.0f for a sharper, narrower peak. Change 0.6f
for a taller hill.
Tutorial: OSC driving mesh slot transforms
OSC example with optional internal sender for testing without hardware
// Not needed if you have an actual OSC hardware sending messages to port 9000
void osc_sender() {
auto sink = make_persistent_shared<Portal::Network::NetworkSink>(
Portal::Network::StreamConfig {
.name = "osc_loopback",
.endpoint = { .address = "127.0.0.1", .port = 9000 },
.profile = Portal::Network::StreamProfile::REALTIME_SMALL,
.transport = Portal::Network::NetworkTransportHint::UDP,
});
float t = 0.f;
schedule_metro(0.05, [&t, sink]() {
if (!sink->is_open()) return;
float rotate_val = (std::sin(t * 0.4f) + 1.f) * 0.5f;
float scale_val = (std::sin(t * 0.17f) + 1.f) * 0.5f;
auto r = Portal::Network::serialize_osc("/rotate", {{ rotate_val }});
auto s = Portal::Network::serialize_osc("/scale", {{ scale_val }});
sink->send({ r.data(), r.size() });
sink->send({ s.data(), s.size() });
t += 0.05f;
}, "osc_loopback_sender");
}
void settings() {
auto& cfg = MayaFlux::Config::get_global_stream_info();
cfg.osc.enabled = true;
cfg.osc.port = 9000;
}
void compose() {
osc_sender(); // ONLY NEEDED if you do not want to use an actual OSC hardware
auto window = MayaFlux::create_window({ "External", 1200, 800 });
auto make_tetra = [](glm::vec3 color)
-> std::pair<std::vector<MeshVertex>, std::vector<uint32_t>>
{
std::vector<MeshVertex> v = {
{{ 0.f, 0.6f, 0.f }, color, 1.f, {0.5f,1.f}, { 0, 1, 0}, {1,0,0}},
{{-0.5f,-0.3f,-0.5f}, color, 1.f, {0.f, 0.f}, { 0,-1, 0}, {1,0,0}},
{{ 0.5f,-0.3f,-0.5f}, color, 1.f, {1.f, 0.f}, { 0,-1, 0}, {1,0,0}},
{{ 0.f, -0.3f, 0.5f}, color, 1.f, {0.5f,0.f}, { 0,-1, 0}, {1,0,0}},
};
return { v, { 0,1,2, 0,2,3, 0,3,1, 1,3,2 } };
};
auto net = vega.MeshNetwork() | Graphics;
for (auto [color, tx] : std::array{
std::pair{ glm::vec3{0.8f,0.3f,0.2f}, -0.8f },
std::pair{ glm::vec3{0.2f,0.5f,0.9f}, 0.8f } })
{
auto [verts, indices] = make_tetra(color);
auto node = std::make_shared<Nodes::GpuSync::MeshWriterNode>(4);
node->set_mesh(verts, indices);
net->add_slot("", node);
net->get_slot(net->slot_count() - 1).local_transform =
glm::translate(glm::mat4(1.f), {tx, 0.f, 0.f});
}
auto buf = vega.MeshNetworkBuffer(net) | Graphics;
buf->setup_rendering({ .target_window = window });
buf->get_render_processor()->set_view_transform(
Kinesis::look_at_perspective(
{0.f, 2.f, 4.f}, {0.f, 0.f, 0.f},
glm::radians(45.f), 1200.f/800.f, 0.1f, 100.f));
window->show();
auto rotate_ctrl = vega.read_osc(
OSCConfig::normalized(0.0, 1.0),
Core::InputBinding::osc("/rotate"));
auto scale_ctrl = vega.read_osc(
OSCConfig::normalized(0.0, 1.0),
Core::InputBinding::osc("/scale"));
float angle = 0.f;
MayaFlux::schedule_metro(1.0 / 60.0, [net, rotate_ctrl, scale_ctrl, &angle]() {
const float speed = static_cast<float>(rotate_ctrl->get_last_output());
const float scale = 0.4f + static_cast<float>(scale_ctrl->get_last_output()) * 1.2f;
angle += speed * 0.08f;
auto& slot_a = net->get_slot(0);
slot_a.local_transform =
glm::translate(glm::mat4(1.f), {-0.8f, 0.f, 0.f}) *
glm::rotate(glm::mat4(1.f), angle, {0.f, 1.f, 0.f});
slot_a.dirty = true;
auto& slot_b = net->get_slot(1);
slot_b.local_transform =
glm::translate(glm::mat4(1.f), {0.8f, 0.f, 0.f}) *
glm::scale(glm::mat4(1.f), glm::vec3(scale));
slot_b.dirty = true;
});
}Run this. The left tetrahedron spins at a rate driven by /rotate, the right one
breathes in scale driven by /scale. The loopback sender in osc_sender()
generates both values internally so no external client is needed. Replace it with any OSC sender
targeting port 9000 and the same addresses to control it from a phone, another program, or hardware.
read_osc registers the node internally before returning. No | Audio pipe
is needed. get_last_output() on the graphics metro thread is a lock-free read of an
atomic written by the OSC receive thread.
Card 6: Creative Variations
Click this card to reveal full explanation
The previous cards all react: to clocks, to conditions, to external events. This card generates.
schedule_pattern and line produce values according to computational rules,
not in response to anything. Everything downstream reads from them.
Tutorial: Pattern as a generative sequencer
void compose() {
auto window = MayaFlux::create_window({ "Time", 1400, 900 });
window->show();
// Kick: pitched envelope on a sine
auto kick_env = vega.Phasor(0.0f) | Audio[0];
auto kick_shape = vega.Polynomial([](double x) {
return std::exp(-x * 18.0);
}) | Audio[0];
kick_shape->set_input_node(kick_env);
auto kick = vega.Sine(55.0f) | Audio[0];
kick->set_amplitude_modulator(kick_shape);
// Snare: filtered noise with exponential decay
auto noise = vega.Random();
noise->set_amplitude(0.08);
std::vector<double> b = { 0.0675, 0.0, -0.0675 };
std::vector<double> a = { 1.0, -1.1430, 0.4128 };
auto snare_iir = vega.IIR(noise, b, a);
snare_iir->set_gain(0.15);
auto snare_shape = vega.Polynomial([](double x) { return std::exp(-x * 30.0); });
snare_shape->set_input_node(snare_iir);
auto snare_env = vega.Phasor(0.0f) | Audio[1];
snare_env->set_amplitude_modulator(snare_shape);
snare_env->set_frequency_modulator(snare_shape);
// Hat: high-frequency sine, very short decay
auto hat_env = vega.Phasor(0.0f) | Audio[0];
auto hat_shape = vega.Polynomial([](double x) { return std::exp(-x * 60.0); }) | Audio[0];
hat_shape->set_input_node(hat_env);
auto hat = vega.Sine(6000.0f) | Audio[0];
hat->set_amplitude_modulator(hat_shape);
// Visual: three geometry layers in one composite buffer
auto hits = vega.PointCollectionNode() | Graphics;
auto connector = vega.PathGeneratorNode(Kinesis::InterpolationMode::CATMULL_ROM, 16, 64) | Graphics;
auto sweep_mesh = vega.MeshWriterNode(64) | Graphics;
{
std::vector<MeshVertex> v;
std::vector<uint32_t> idx;
const int SEGS = 16;
v.push_back({ {0.f,0.f,0.f}, {0.5f,0.5f,1.f}, 1.f, {}, {0,0,1}, {1,0,0} });
for (int i = 0; i <= SEGS; ++i) {
float a = float(i) / SEGS * 2.f * M_PI;
v.push_back({ {0.35f*std::cos(a), 0.35f*std::sin(a), 0.f},
{0.2f,0.6f,0.9f}, 0.f, {}, {0,0,1}, {1,0,0} });
if (i < SEGS)
idx.insert(idx.end(), { 0u, uint32_t(i+1), uint32_t(i+2) });
}
sweep_mesh->set_mesh(v, idx);
}
auto composite = vega.CompositeGeometryBuffer() | Graphics;
composite->add_geometry("hits", hits,
Portal::Graphics::PrimitiveTopology::POINT_LIST, window);
composite->add_geometry("path", connector,
Portal::Graphics::PrimitiveTopology::LINE_STRIP, window);
composite->add_geometry("sweep", sweep_mesh,
Portal::Graphics::PrimitiveTopology::TRIANGLE_LIST, window);
struct StepData {
bool kick, snare, hat;
float x, y;
glm::vec3 color;
};
float orbit_angle = 0.f;
MayaFlux::schedule_pattern(
[](uint64_t step) -> std::any {
const uint64_t s = step % 8;
StepData d;
d.kick = (s == 0 || s == 3 || s == 6);
d.snare = (s == 2 || s == 6);
d.hat = (s % 2 == 1);
const float angle = float(step) * 0.37f;
const float r = 0.5f + 0.2f * std::sin(float(step) * 0.13f);
d.x = r * std::cos(angle);
d.y = r * std::sin(angle);
d.color = {
std::abs(std::sin(float(step) * 0.11f)),
std::abs(std::sin(float(step) * 0.07f + 1.f)),
std::abs(std::sin(float(step) * 0.05f + 2.f)),
};
return d;
},
[kick_env, snare_env, hat_env,
hits, connector, sweep_mesh, &orbit_angle](std::any val) {
const auto& d = std::any_cast<const StepData&>(val);
if (d.kick) kick_env->reset();
if (d.snare) snare_env->reset();
if (d.hat) hat_env->reset();
hits->add_point({ glm::vec3(d.x, d.y, 0.f), d.color, 8.f });
connector->add_control_point({ glm::vec3(d.x, d.y, 0.f), d.color, 1.5f });
orbit_angle += d.kick ? 0.4f : 0.08f;
if (d.kick) {
auto verts = sweep_mesh->get_mesh_vertices();
verts[0].color = d.color;
sweep_mesh->set_mesh_vertices(verts);
}
},
0.125
);
}Run this. You hear a Euclidean rhythm: kick on beats 0, 3, 6 of 8; snare on 2 and 6; hat on odd steps. Simultaneously, colored points accumulate at positions computed from the same step index, a Catmull-Rom path connects them, and a triangle fan rotates faster on kick steps. The rhythm and the visual share one source: the pattern function.
Change the step period from 0.125 to 0.0833. Both audio and visual
accelerate together. Change % 8 to % 12. The rhythm changes shape and
so does the visual orbit. The function is the single point of compositional control.
Tutorial: line as a parametric parameter
void compose() {
auto window = MayaFlux::create_window({ "Time", 1200, 800 });
auto points = vega.PointCollectionNode() | Graphics;
auto buf = vega.GeometryBuffer(points) | Graphics;
buf->setup_rendering({ .target_window = window });
window->show();
auto phasor = vega.Phasor(0.0f) | Audio[0];
auto env = vega.Polynomial([](double x) { return std::exp(-x * 20.0); }) | Audio[0];
env->set_input_node(phasor);
auto drum = vega.Sine(110.0f) | Audio[0];
drum->set_amplitude_modulator(env);
// Threshold rises from 0.0 to 1.0 over 8 seconds
MayaFlux::schedule_task("reveal",
MayaFlux::create_line(0.0f, 1.0f, 8.0f, 128, true));
MayaFlux::schedule_pattern(
[](uint64_t step) -> std::any {
auto is_prime = [](uint64_t n) {
if (n < 2) return false;
for (uint64_t i = 2; i * i <= n; ++i)
if (n % i == 0) return false;
return true;
};
const float density = is_prime(step % 32) ? 0.9f : 0.3f;
const float angle = float(step) * 0.41f;
const float r = 0.4f + 0.35f * std::abs(std::sin(float(step) * 0.17f));
return std::make_tuple(density, r * std::cos(angle), r * std::sin(angle));
},
[points, phasor](std::any val) {
auto [density, x, y] =
std::any_cast<std::tuple<float, float, float>>(val);
phasor->reset();
float* threshold = MayaFlux::get_line_value("reveal");
if (threshold && density > *threshold) {
points->add_point({
glm::vec3(x, y, 0.f),
glm::vec3(density, 1.f - density, 0.3f),
6.f + density * 8.f
});
}
},
0.1
);
}Run this. You hear a steady rhythm. The window starts empty. Over eight seconds, points appear progressively: first only the sparse non-prime steps (low density, revealed early), then gradually the prime steps emerge (high density, revealed later). Audio and visual were always computing the same thing; only the threshold determined visibility.
The line is not an event. It does not call anything. get_line_value("reveal")
is a pointer into the coroutine frame advancing on each step. No push, no subscription, no
coordination. The parametric value is ambient state.
Card 7: Declarative Compositions
Every card so far has been imperative. You decide when things run, you hold the state, you
write the callbacks. This card takes a different posture. You describe a flow or a spatial
relationship and the system runs it. You do not call process(). You do not count
cycles. You declare what should happen and step back.
Two systems. BufferPipeline for data flows that span more than one buffer cycle.
Fabric for spatial entities whose position is the only thing audio and graphics
share.
Pipelines: declared data flows
Click this card to reveal full explanation
Tutorial: Live waveshaper, one cycle in one cycle out
void settings() {
auto& stream = MayaFlux::Config::get_global_stream_info();
stream.input.enabled = true;
stream.input.channels = 1;
}
void compose() {
auto mic = MayaFlux::create_input_listener_buffer(0, true);
auto pipeline = MayaFlux::create_buffer_pipeline();
pipeline->with_strategy(Kriya::ExecutionStrategy::STREAMING);
pipeline
>> BufferOperation::capture_from(mic).for_cycles(1)
>> BufferOperation::modify_buffer(mic,
[](const std::shared_ptr<Buffers::AudioBuffer>& buf) {
for (auto& s : buf->get_data()) {
s = std::tanh(s * 6.0) * 0.7;
}
}).as_streaming();
pipeline->execute_buffer_rate();
}Run this with a microphone connected. You hear your input through heavy tanh
saturation. The pipeline captures one buffer cycle from the mic, modifies it in place, and the
modified data leaves through the mic buffer's normal routing. No explicit loop, no cycle
counter, no callback registered on any node.
Change 6.0 to 1.5 and the saturation softens toward gentle clipping.
Change it to 20.0 for hard rectangular clipping. The lambda is the only thing that
changes. The pipeline structure stays identical.
Tutorial: Mouse-painted accumulating looper
void settings() {
auto& stream = MayaFlux::Config::get_global_stream_info();
stream.input.enabled = true;
stream.input.channels = 1;
}
void compose() {
auto window = MayaFlux::create_window({ "Looper", 800, 600 });
window->show();
auto mic = MayaFlux::create_input_listener_buffer(0, true);
const uint32_t buf_size = MayaFlux::get_buffer_manager()
->get_buffer_size(Buffers::ProcessingToken::AUDIO_BACKEND);
const uint64_t loop_frames = static_cast<uint64_t>(buf_size) * 64;
auto loop = std::make_shared<Kakshya::DynamicSoundStream>(48000, 1);
loop->set_auto_resize(false);
loop->ensure_capacity(loop_frames);
auto layer = MayaFlux::create_sampler_from_stream(loop, 0);
layer->play_continuous(0, layer->slice_from_stream());
store(layer);
auto& brush_pos = make_persistent(0.5f);
auto& brush_width = make_persistent(0.5f);
auto& wipe = make_persistent(false);
MayaFlux::on_mouse_move(window, [&brush_pos, &brush_width, window]
(double x, double y) {
const auto ndc = normalize_coords(x, y, window);
brush_pos = ndc.x;
brush_width = 0.05f + 0.6f * ndc.y;
});
MayaFlux::on_key_pressed(window, IO::Keys::Space, [&wipe]() { wipe = true; });
auto pipeline = MayaFlux::create_buffer_pipeline();
pipeline->with_strategy(Kriya::ExecutionStrategy::PHASED)
.capture_timing(Vruta::DelayContext::BUFFER_BASED);
pipeline
>> BufferOperation::capture_from(mic).for_cycles(64)
>> BufferOperation::dispatch_to(
[loop, loop_frames, &brush_pos, &brush_width, &wipe]
(Kakshya::DataVariant& data, uint32_t) {
const auto& fresh = std::get<std::vector<double>>(data);
std::vector<double> existing(loop_frames, 0.0);
loop->get_channel_frames(existing, 0, 0);
if (wipe) {
wipe = false;
std::ranges::fill(existing, 0.0);
}
const size_t n = std::min<size_t>(fresh.size(), loop_frames);
std::vector<double> mixed(loop_frames, 0.0);
for (size_t i = 0; i < loop_frames; ++i) {
const double pos = static_cast<double>(i) / loop_frames;
const double d = std::abs(pos - brush_pos);
const double brush = std::exp(-(d * d) / (brush_width * brush_width));
const double keep = 0.98 - 0.5 * brush;
const double prev = existing[i] * keep;
const double live = (i < n) ? fresh[i] * brush : 0.0;
mixed[i] = std::tanh(prev + live);
}
loop->write_frames(
std::span<const double>(mixed.data(), mixed.size()), 0, 0);
});
pipeline->execute_buffer_rate();
}Run this with a microphone and make some sound. An empty window opens. The loop is a strip of time laid left to right across the window, played back continuously underneath you. Wherever the pointer sits, that region of the loop receives the fresh input painted in while the rest decays. Drag across the window and you smear new sound into the part of the loop the cursor passes over, leaving the rest to fade. Move down for a wider brush, up for a narrow one. Space wipes the loop.
Stop moving and the loop stabilises into whatever you last painted, repeating. Keep painting the same spot and that region thickens toward saturation while the rest thins out. The texture is the record of where your cursor has been.
Fabric: one gesture for sound and light
Click this card to reveal full explanation
Tutorial: Fabric, one gesture for sound and light
Vruta::GraphicsRoutine weave_mode(
Vruta::TaskScheduler&,
std::shared_ptr<Nodes::GpuSync::PathGeneratorNode> path,
std::shared_ptr<Nodes::Network::ModalNetwork> modal,
size_t mode_index,
glm::vec3 color) {
auto& p = co_await Kriya::GetGraphicsPromise{};
glm::vec3 pos(0.0f);
while (!p.should_terminate) {
const auto& modes = modal->get_modes();
if (mode_index < modes.size()) {
const float a = static_cast<float>(modes[mode_index].amplitude);
const float angle = static_cast<float>(mode_index) / modes.size()
* glm::two_pi<float>();
const glm::vec3 target(
std::cos(angle) * (0.3f + a * 2.5f),
std::sin(angle) * (0.3f + a * 2.5f),
0.0f);
pos = glm::mix(pos, target, 0.05f);
path->add_control_point({ pos, color, 1.0f + a * 4.0f });
}
co_await Kriya::FrameDelay { .frames_to_wait = 2 };
}
}
void compose() {
auto window = MayaFlux::create_window({ "Gesture", 1200, 800 });
window->show();
auto modal = vega.ModalNetwork(8, 110.0,
Nodes::Network::ModalNetwork::Spectrum::INHARMONIC, 8.0) | Audio[{ 0, 1 }];
modal->set_coupling_enabled(true);
auto composite = vega.CompositeGeometryBuffer() | Graphics;
std::vector<std::shared_ptr<Nodes::GpuSync::PathGeneratorNode>> paths;
for (size_t i = 0; i < 8; ++i) {
const glm::vec3 color = glm::mix(
glm::vec3(0.2f, 0.4f, 0.9f),
glm::vec3(0.9f, 0.3f, 0.2f),
static_cast<float>(i) / 7.0f);
auto path = vega.PathGeneratorNode(
Kinesis::InterpolationMode::CATMULL_ROM, 4, 256) | Graphics;
path->add_control_point({ glm::vec3(0.0f), color, 1.0f });
paths.push_back(path);
composite->add_geometry("m" + std::to_string(i), path,
Portal::Graphics::PrimitiveTopology::LINE_STRIP,
{ .target_window = window,
.vertex_shader = "line_lit.vert",
.fragment_shader = "line_lit.frag",
.geometry_shader = "line_lit.geom" });
}
auto& sched = *MayaFlux::get_scheduler();
auto& evmgr = *MayaFlux::get_event_manager();
auto fabric = std::make_shared<Nexus::Fabric>(sched, evmgr);
for (size_t i = 0; i < 8; ++i) {
auto driver = std::make_shared<Nexus::Emitter>(
[](const Nexus::InfluenceContext&) {});
fabric->wire(driver)
.use([paths, modal, i](Vruta::TaskScheduler& s) -> Vruta::GraphicsRoutine {
const glm::vec3 c = glm::mix(
glm::vec3(0.2f, 0.4f, 0.9f),
glm::vec3(0.9f, 0.3f, 0.2f),
static_cast<float>(i) / 7.0f);
return weave_mode(s, paths[i], modal, i, c);
})
.finalise();
}
auto cursor = std::make_shared<Nexus::Emitter>(
[modal](const Nexus::InfluenceContext& ctx) {
const float t = (ctx.position.x + 1.0f) * 0.5f;
const float strength = (ctx.position.y + 1.0f) * 0.5f;
modal->excite_at_position(t, strength * 0.6f);
});
cursor->set_position(glm::vec3(0.0f));
cursor->set_color(glm::vec3(1.0f, 0.9f, 0.7f));
cursor->set_radius(1.5f);
for (const auto& proc : composite->get_render_processors()) {
cursor->set_influence_target(proc);
}
fabric->wire(cursor).every(1.0 / 60.0).finalise();
MayaFlux::on_mouse_move(window, [cursor, window](double x, double y) {
const auto& ws = window->get_state();
cursor->set_position(glm::vec3(
static_cast<float>(x / ws.current_width) * 2.0f - 1.0f,
1.0f - static_cast<float>(y / ws.current_height) * 2.0f,
0.0f));
});
auto commit_loop = [](Vruta::TaskScheduler&,
std::shared_ptr<Nexus::Fabric> fab) -> Vruta::GraphicsRoutine {
auto& p = co_await Kriya::GetGraphicsPromise {};
while (!p.should_terminate) {
fab->commit();
co_await Kriya::FrameDelay { .frames_to_wait = 1 };
}
};
MayaFlux::schedule_task("gesture_commit",
commit_loop(sched, fabric), false);
}Run this and move the mouse across the window. You strike a resonant body. Where the cursor sits decides which modes ring and how hard: horizontal position sweeps which part of the body is struck, vertical position sets how hard. The eight curves are woven from the network's own mode amplitudes, so the louder a mode rings the further its curve reaches out. And the geometry lights up around the cursor, because the same position that strikes the sound also drives the shader.
One Emitter does all of this. Its position is the only input. From that single
position the sound is struck, the curves are shaped by what rings, and the light falls where the
strike lands. Move the cursor and all three move together because they are the same number.