diff --git a/examples/node/midi_particles.coffee b/examples/node/midi_particles.coffee index 53fce3f384cc6a64793a8a5acd21c7de8b966b7d..010b8e2dac22a4df02a6fa1ae3540c42b975f27b 100755 --- a/examples/node/midi_particles.coffee +++ b/examples/node/midi_particles.coffee @@ -39,7 +39,16 @@ particleNotes = {} # Adjustable parameters particleLifetime = 1.0 brightness = 1.0 -spinRate = 1.0 +spinRate = 0 +noteSustain = 1.6 +wobbleAmount = 24.0 +origin = [0, 0, 0] + +# Physics +numPhysicsTimesteps = 20 +frameDelay = 5 +timestepSize = 0.010 +gain = 0.1 previousNow = 0 spinAngle = 0 @@ -55,7 +64,6 @@ midiToAngle = (key) -> (2 * Math.PI / 24) * key # Boundary between the left-hand and right-hand patterns. LIMINAL_KEY = 40 -SMOOTH_FACTOR = 0.1 input.on 'message', (deltaTime, message) -> console.log message @@ -89,86 +97,93 @@ input.on 'message', (deltaTime, message) -> when 0xe0 # Voice 0, Pitch Bend # Default spin 1.0, but allow forward/backward - spinRate = 1.0 + (message[2] - 64) * 20.0 / 64 + spinRate = (message[2] - 64) * 10.0 / 64 draw = () -> # Time delta calculations now = clock() - timeStep = now - previousNow - previousNow = now # Global spin update - spinAngle = (spinAngle + timeStep * spinRate) % (Math.PI * 2) + spinAngle = (spinAngle + timestepSize * spinRate) % (Math.PI * 2) # Experiment - moves the origin in a circle - theta = previousNow * 1.5 - initX = 0.5 * Math.cos theta - initY = 0.5 * Math.sin theta + theta = now * 2.5 + origin[0] = 0.2 * Math.cos theta + origin[2] = 0.2 * Math.sin theta # Launch new particles for all active notes for key, note of particleNotes particles.push life: 1 - point: [initX, 0, initY] note: note + point: origin.slice 0 + velocity: [0, 0, 0] timestamp: note.timestamp # Update appearance of all particles for p in particles # Angle: Global spin, thne positional mapping to key - theta = midiToAngle p.note.key + theta = spinAngle + midiToAngle p.note.key # Radius: Particles spawn in center, fly outward radius = 3.0 * (1 - p.life) # Positioned in polar coordinates - x = radius * Math.cos(theta) + initX - y = radius * Math.sin(theta) + initY + x = radius * Math.cos(theta) + y = radius * Math.sin(theta) - # One rainbow per octave - hue = (p.note.key - LIMINAL_KEY + 0.1) / 12.0 - p.color = OPC.hsv hue, 0.3, 0.8 + # Hop around between almost-opposing colors, eventually going + # around the rainbow. These ratios control what kinds of color + # schemes we get for different chords. + hue = (p.note.key - LIMINAL_KEY + 0.1) * (7 / 12.0) + p.color = OPC.hsv hue, 0.5, 0.8 # Intensity mapped to velocity, nonlinear - p.intensity = Math.pow(p.note.velocity / 100, 3.0) * 0.25 * brightness + p.intensity = Math.pow(p.note.velocity / 100, 2.0) * 0.2 * brightness + + # Fade with age + noteAge = now - p.note.timestamp + p.intensity *= Math.max(0, 1 - (noteAge / noteSustain)) # Falloff gets sharper as the note gets higher - p.falloff = 20 + (p.note.key - LIMINAL_KEY) * 20 + p.falloff = 15 * Math.pow(2, (p.note.key - LIMINAL_KEY) / 6) # Add influence of LFOs for key, note of lfoNotes - age = now - note.timestamp + lfoAge = now - note.timestamp hz = midiToHz key lfoAngle = midiToAngle key # Amplitude starts with left hand velocity - wobbleAmp = Math.pow(note.velocity / 100, 3.0) + wobbleAmp = Math.pow(note.velocity / 100, 2.0) * wobbleAmount # Scale based on particle fuzziness - wobbleAmp *= 100.0 / p.falloff + wobbleAmp /= p.falloff # Fade over time - wobbleAmp /= 1 + age + wobbleAmp /= 1 + lfoAge # Wobble - wobbleAmp *= Math.sin(p.life * Math.pow(3, (p.note.key - 35) / 12.0)) + wobbleAmp *= Math.sin(p.life * Math.pow(3, (p.note.key - LIMINAL_KEY/2) / 12.0)) # Wobble angle driven by LFO note and particle life x += wobbleAmp * Math.cos lfoAngle y += wobbleAmp * Math.sin lfoAngle - # Use the XZ plane - [oldx, _, oldy] = p.point - p.point = [oldx + (oldx - x)*SMOOTH_FACTOR, - 0, - oldy + (oldy - y)*SMOOTH_FACTOR] - # p.point += ([x, 0, y] - p.point) * SMOOTH_FACTOR - # p.point = [x, 0, y] + # Update velocity; use the XZ plane + p.velocity[0] += (x + origin[0] - p.point[0]) * (gain / numPhysicsTimesteps) + p.velocity[2] += (y + origin[2] - p.point[2]) * (gain / numPhysicsTimesteps) + + # Fixed timestep physics + for i in [1 .. numPhysicsTimesteps] + p.point[0] += p.velocity[0] + p.point[1] += p.velocity[1] + p.point[2] += p.velocity[2] - p.life -= timeStep / particleLifetime + p.life -= timestepSize / particleLifetime # Filter out dead particles particles = particles.filter (p) -> p.life > 0 @@ -176,4 +191,4 @@ draw = () -> # Render particles to the LEDs client.mapParticles particles, model -setInterval draw, 10 +setInterval draw, frameDelay