//----------------------------------------------------------------------------- // name: fireworks.ck // desc: a fireworks sequencer! // // author: Renee Qin // date: Fall 2024 //----------------------------------------------------------------------------- // Initialize Mouse Manager =================================================== Mouse mouse; spork ~ mouse.selfUpdate(); // start updating mouse position // Global Sequencer Params ==================================================== 30 => int BPM; // beats per minute (1.0/BPM)::minute / 2.0 => dur STEP; // step duration 8 => int NUM_STEPS; // steps per sequence] [ 13, 15, 18, 20, 22, 25 ] @=> int SCALE[]; // relative MIDI offsets for minor pentatonic scale -0.1 => float gravity; // Scene setup ================================================================ GG.scene() @=> GScene @ scene; GG.camera() @=> GCamera @ cam; cam.orthographic(); // Orthographic camera mode for 2D scene GGen melodyGroups[NUM_STEPS]; // one group per column for (auto group : melodyGroups) group --> GG.scene(); // lead pads GPad melodyPads[NUM_STEPS][SCALE.size()]; // update pad positions on window resize fun void resizeListener() { WindowResizeEvent e; // now listens to the window resize event while (true) { e => now; // window has been resized! <<< GG.windowWidth(), " , ", GG.windowHeight() >>>; placePads(); } } spork ~ resizeListener(); // place pads based on window size fun void placePads() { // recalculate aspect (GG.frameWidth() * 1.0) / (GG.frameHeight() * 1.0) => float aspect; // calculate ratio between old and new height/width cam.viewSize() => float frustrumHeight; // height of screen in world-space units frustrumHeight * aspect => float frustrumWidth; // widht of the screen in world-space units frustrumWidth / NUM_STEPS => float padSpacing; // place lead pads for (0 => int i; i < NUM_STEPS; i++) { placePadsVertical( melodyPads[i], melodyGroups[i], frustrumHeight, i * 1.0 - 3.5 ); } } // places along vertical axis fun void placePadsVertical(GPad pads[], GGen @ parent, float height, float x) { // scale height down a smidge // .95 *=> height; height / pads.size() => float padSpacing; for (0 => int i; i < pads.size(); i++) { pads[i] @=> GPad pad; // initialize pad pad.init(mouse); // connect to scene pad --> parent; // set transform using fixed offsets pad.sca(padSpacing * .3); pad.posY(padSpacing * i - height / 2.0 + padSpacing / 2.0); pad.posX(x / 2 + pad.xOffset); } parent.posX(x); // position entire column } // Instruments ================================================================== class Melody extends Chugraph { SinOsc sin1, sin2; ADSR env; // amplitude EG Step step => Envelope filterEnv => blackhole; // filter cutoff EG LPF filter; TriOsc freqLFO => blackhole; // LFO to modulate filter frequency TriOsc qLFO => blackhole; // LFO to modulate filter resonance sin1 => env => filter => Gain g => outlet; sin2 => env => filter; // initialize amp EG env.set(80::ms, 30::ms, 0.6, 250::ms); filterEnv.duration(100::ms); freqLFO.period(4::second); qLFO.period(5::second); // spork to play! fun void play(int note) { Std.mtof(note) => float freq; // set frequencies sin1.freq(freq); sin2.freq(2 * freq * 1.01); // slight detune for more harmonic content // activate EGs env.keyOn(); filterEnv.keyOn(); // wait for note to hit sustain portion of ADSR env.attackTime() + env.decayTime() => now; // deactivate EGs env.keyOff(); filterEnv.keyOff(); // wait for note to finish env.releaseTime() => now; } } // Sequencer =================================================================== Gain main => JCRev rev => dac; // main bus with reverb 0.6 => rev.mix; // Gain main => dac; // main bus .8 => main.gain; // initialzie lead instrument Melody melodies[SCALE.size()]; for (auto note : melodies) { note => main; note.gain(3.0 / melodies.size()); // reduce gain according to # of voices } // sequence lead (polyphonic) fun void sequenceLead(Melody leads[], GPad pads[][], int scale[], int root, dur step) { while (true) { for (0 => int i; i < pads.size(); i++) { pads[i] @=> GPad col[]; // play all active pads in column for (0 => int j; j < col.size(); j++) { if (col[j].active()) { col[j].play(true); // shift each note up by an octave (12 semitones) scale[j] + 24 => int higherPitchNote; // transpose by one octave spork ~ leads[j].play(root + higherPitchNote); // play shifted note } } // pass time step => now; // stop all animations for (0 => int j; j < col.size(); j++) { col[j].stop(); } } } } spork ~ sequenceLead(melodies, melodyPads, SCALE, 60 - 2 * 12, STEP / 2.0); // Background Music Loop ========================================================= // function to create and play ethereal background tones fun void playBackgroundMusic() { // initialize background oscillators and parameters SinOsc osc1 => NRev reverb1 => dac; SinOsc osc2 => NRev reverb2 => dac; // SinOsc osc3 => JCRev reverb3 => dac; // set reverb mix for ethereal feel 0.3 => reverb1.mix; 0.3 => reverb2.mix; // 0.3 => reverb3.mix; // randomly play few notes, each two octaves lower while (true) { SCALE[Math.random2(0, SCALE.size() - 1)] + 48 => int note1; SCALE[Math.random2(0, SCALE.size() - 1)] + 48 => int note2; // SCALE[Math.random2(0, SCALE.size() - 1)] + 48 => int note3; // set oscillator frequencies based on MIDI to frequency conversion Std.mtof(note1) => osc1.freq; 1::second => now; // Small space between notes Std.mtof(note2) => osc2.freq; // 1::second => now; // Small space between notes // Std.mtof(note3) => osc3.freq; // set amplitudes to keep the background soft 0.2 => osc1.gain; 0.2 => osc2.gain; // 0.2 => osc3.gain; // play each note for a randomized duration to create variety (Math.random2f(2.0, 4.0)::second) => now; } } spork ~ playBackgroundMusic(); // Visuals ==================================================================== // Visually, the entire sequencer is made up of "GPads" which are meant to read // as the MIDI pads you'd see on a drum machine. // Each pad is a GPlane + mouse listener + state machine for handling input // and transitioning between various modes e.g. hovered, active, playing, etc. // ============================================================================ //-------------------------- // BLOOM | //-------------------------- // setup bloom GG.renderPass() --> BloomPass bloom_pass --> GG.outputPass(); bloom_pass.input( GG.renderPass().colorOutput() ); GG.outputPass().input( bloom_pass.colorOutput() ); // adjust bloom bloom_pass.intensity(6); bloom_pass.radius(0.6); bloom_pass.levels(9); class Particle extends GGen { GCircle particleShape; // shape of each particle FlatMaterial particleMaterial; vec3 velocity; // direction and speed of particle float lifespan; // how long particle lasts Math.random2f(0.03, 0.1) => float fade_rate; vec3 particle_color; // constructor fun void init(vec3 startPos, vec3 startVelocity, float duration, vec3 color) { particleShape --> this; particleShape.sca(0.05); // Size of particle color => particle_color; particleMaterial.color(particle_color); particleShape.mat(particleMaterial); particleShape.pos(startPos); startVelocity => velocity; duration => lifespan; } // update particle position and transparency fun void updateParticle(float dt) { if (lifespan > 0) { velocity.y + gravity * dt => velocity.y; // <<< pos() >>>; particleShape.pos(particleShape.pos() + velocity * dt); // pos(pos() + (velocity * dt)); particle_color * (1.0 - fade_rate) => particle_color; Math.random2f(0.0, 1.0) => float flicker; if (flicker < 0.3) { particleMaterial.color(particle_color * 8); } else { particleMaterial.color(particle_color); } lifespan - dt => lifespan; } else { particleShape.detach(); } } } class GPad extends GGen { // initialize mesh GCircle pad --> this; FlatMaterial mat; pad.mat(mat); Particle particles[50]; // list of particles per pad 3.0 => float particleDuration; // particle lifespan in seconds float xOffset; // x offset for slight variation float yOffset; // y offset for slight variation // reference to a mouse Mouse @ mouse; // events Event onHoverEvent, onClickEvent; // onExit, onRelease // states 0 => static int NONE; // not hovered or active 1 => static int HOVERED; // hovered 2 => static int ACTIVE; // clicked 3 => static int PLAYING; // makine sound! 0 => int state; // current state // input types 0 => static int MOUSE_HOVER; 1 => static int MOUSE_EXIT; 2 => static int MOUSE_CLICK; 3 => static int NOTE_ON; 4 => static int NOTE_OFF; Color.random() => vec3 temp; // color map [ @(2.53, 2.53, 1.50), // NONE temp * 3, // HOVERED temp, // ACTIVE temp * 3 // PLAYING ] @=> vec3 colorMap[]; // constructor fun void init(Mouse @ m) { if (mouse != null) return; m @=> this.mouse; Math.random2f(-0.3, 0.3) * 2.2 => xOffset; Math.random2f(-0.3, 0.3) * 2.2 => yOffset; spork ~ this.clickListener(); } fun void spawnParticles() { Math.random2(0, 3) => int shapeType; for (0 => int i; i < particles.size(); i++) { particles.size() => float s; (i / s) * 2 * Math.pi => float t; vec3 startPos; vec3 velocity; // assign startPos and velocity based on chosen shapeType if (shapeType == 0) { // circle shape @(pad.posWorld().x + (pad.scaX() * 0.3 / 2) * Math.sin(t), pad.posWorld().y + (pad.scaX() * 0.3 / 2) * Math.cos(t), pad.posWorld().z) => startPos; @(Math.sin(t), Math.cos(t), 0.0) => velocity; } else if (shapeType == 1) { // flower petal shape 8.0 => float b; @(pad.posWorld().x + (pad.scaX() * 0.3 / 2) * (1 + Math.sin(b * t)) * Math.cos(t), pad.posWorld().y + (pad.scaX() * 0.3 / 2) * (1 + Math.sin(b * t)) * Math.sin(t), pad.posWorld().z) => startPos; @((1 + Math.sin(b * t)) * Math.cos(t), (1 + Math.sin(b * t)) * Math.sin(t), 0.0) => velocity; } else if (shapeType == 2) { // star shape @(pad.posWorld().x + (pad.scaX() * 0.3 / 2) * (2 * Math.cos(3 * t) + 5 * Math.cos(2 * t)) / 5, pad.posWorld().y + (pad.scaX() * 0.3 / 2) * (2 * Math.sin(3 * t) - 5 * Math.sin(2 * t)) / 5, pad.posWorld().z) => startPos; @((2 * Math.cos(3 * t) + 5 * Math.cos(2 * t)) / 5, (2 * Math.sin(3 * t) - 5 * Math.sin(2 * t)) / 5, 0.0) => velocity; } else if (shapeType == 3) { // heart shape @(pad.posWorld().x + (pad.scaX() * 0.3 / 2) * 16 * Math.sin(t) * Math.sin(t) * Math.sin(t) / 10, pad.posWorld().y + (pad.scaX() * 0.3 / 2) * (13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t)) / 10, pad.posWorld().z) => startPos; @(16 * Math.sin(t) * Math.sin(t) * Math.sin(t) / 10, (13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t)) / 10, 0.0) => velocity; } // normalize and randomize velocity slightly for variety // velocity.normalize(); // velocity * Math.random2f(0.9, 1.1) => velocity; // initialize and add particle to scene particles[i].init(startPos, velocity, particleDuration, colorMap[HOVERED]); particles[i] --> GG.scene(); } } // check if state is active (i.e. should play sound) fun int active() { return state == ACTIVE; } // set color fun void color(vec3 c) { mat.color(c); } // returns true if mouse is hovering over pad fun int isHovered() { pad.scaWorld() => vec3 worldScale; // get dimensions worldScale.x / 2.0 => float halfWidth; worldScale.y / 2.0 => float halfHeight; pad.posWorld() => vec3 worldPos; // get position if (mouse.worldPos.x > worldPos.x - halfWidth && mouse.worldPos.x < worldPos.x + halfWidth && mouse.worldPos.y > worldPos.y - halfHeight && mouse.worldPos.y < worldPos.y + halfHeight) { return true; } return false; } // poll for hover events fun void pollHover() { if (isHovered()) { onHoverEvent.broadcast(); handleInput(MOUSE_HOVER); } else { if (state == HOVERED) handleInput(MOUSE_EXIT); } } // handle mouse clicks fun void clickListener() { now => time lastClick; while (true) { GG.nextFrame() => now; if (GWindow.mouseLeftDown() && isHovered()) { onClickEvent.broadcast(); handleInput(MOUSE_CLICK); } } } // animation when playing // set juice = true to animate fun void play(int juice) { handleInput(NOTE_ON); if (juice) { // pad.sca(0.5); // pad.rotZ(Math.random2f(-.5, .2)); } } fun void updateParticles(float dt) { for (0 => int i; i < particles.size(); i++) { particles[i].updateParticle(dt); } } // stop play animation (called by sequencer on note off) fun void stop() { handleInput(NOTE_OFF); } // activate pad, meaning it should be played when the sequencer hits it fun void activate() { enter(ACTIVE); } 0 => int lastState; // enter state, remember last state fun void enter(int s) { state => lastState; s => state; // uncomment to randomize color when playing // if (state == PLAYING) Color.random() => colorMap[PLAYING]; } // basic state machine for handling input fun void handleInput(int input) { if (input == NOTE_ON) { spawnParticles(); enter(PLAYING); return; } if (input == NOTE_OFF) { enter(lastState); return; } if (state == NONE) { if (input == MOUSE_HOVER) { enter(HOVERED); } else if (input == MOUSE_CLICK) { enter(ACTIVE); } } else if (state == HOVERED) { if (input == MOUSE_EXIT) enter(NONE); else if (input == MOUSE_CLICK) { enter(ACTIVE); spawnParticles(); pad.sca(0.5); } } else if (state == ACTIVE) { if (input == MOUSE_CLICK) enter(NONE); } else if (state == PLAYING) { if (input == MOUSE_CLICK) enter(NONE); if (input == NOTE_OFF) { enter(ACTIVE); } } } 0 => int t_flicker; 0 => int t_scale; 0 => int t_pos; Math.random2(50, 150) => int t_max_flicker; Math.random2(50, 150) => int t_max_scale; Math.random2(50, 150) => int t_max_pos; float flickerIntensity; float randomScale; float randomPos; 1.0 => float pre_intensity; 1.0 => float pre_scale; 0.0 => float pre_pos; // override ggen update fun void update(float dt) { // check if hovered pollHover(); // update state this.color(colorMap[state]); if (t_flicker == 0) { Math.random2f(0.0, 1.0) => float random_number; if (random_number < 0.5) { Math.random2f(0.5, 1.5) => flickerIntensity; } else { 0.0 => flickerIntensity; } } if (t_scale == 0) { Math.random2f(0.3, 1) => randomScale; } if (t_pos == 0) { Math.random2f(-0.5, 0.5) => randomPos; } mat.color(mat.color() * (t_flicker * flickerIntensity + (t_max_flicker - t_flicker) * pre_intensity) / t_max_flicker); pad.sca(0.1 * (t_scale * randomScale + (t_max_scale - t_scale) * pre_scale) / t_max_scale); pad.posY((t_pos * randomPos + (t_max_pos - t_pos) * pre_pos) / t_max_pos); t_flicker + 1 => t_flicker; t_scale + 1 => t_scale; t_pos + 1 => t_pos; if (t_flicker == t_max_flicker) { 0 => t_flicker; flickerIntensity => pre_intensity; } if (t_scale == t_max_scale) { 0 => t_scale; randomScale => pre_scale; } if (t_pos == t_max_pos) { 0 => t_pos; randomPos => pre_pos; } // interpolate back towards uniform scale (handles animation) // much less cursed pad.scaX() + .05 * (1.0 - pad.scaX()) => pad.sca; pad.rot().z + .06 * (0.0 - pad.rot().z) => pad.rotZ; updateParticles(dt); } } // simplified Mouse class from examples/input/Mouse.ck ======================= class Mouse { vec3 worldPos; // update mouse world position fun void selfUpdate() { while (true) { GG.nextFrame() => now; // calculate mouse world X and Y coords GG.camera().screenCoordToWorldPos(GWindow.mousePos(), 1.0) => worldPos; } } } // Game loop ====================================================================== while (true) { GG.nextFrame() => now; // place pads *after* GG.nextFrame() is called and window is created placePads(); // uncomment if you want to unravel the scenegraph to see all the GGens // if (UI.begin("") { UI.scenegraph(scene); } UI.end(); }