From: Mark Slee Date: Thu, 23 May 2013 01:37:07 +0000 (-0700) Subject: SugarCubes sketch initial code dump X-Git-Url: https://git.piment-noir.org/?p=SugarCubes.git;a=commitdiff_plain;h=49815cc001318d4cd59874b41cc874a259900f14 SugarCubes sketch initial code dump --- diff --git a/MarkSlee.pde b/MarkSlee.pde new file mode 100644 index 0000000..2df823a --- /dev/null +++ b/MarkSlee.pde @@ -0,0 +1,199 @@ +class SpaceTime extends SCPattern { + + SinLFO pos = new SinLFO(0, 15, 3000); + SinLFO rate = new SinLFO(1000, 9000, 13000); + SinLFO falloff = new SinLFO(10, 70, 5000); + float sat = 0; + + BasicKnob rateKnob = new BasicKnob("RATE", 0.5); + BasicKnob sizeKnob = new BasicKnob("SIZE", 0.5); + + public SpaceTime(GLucose glucose) { + super(glucose); + addModulator(pos).trigger(); + addModulator(rate).trigger(); + addModulator(falloff).trigger(); + pos.modulateDurationBy(rate); + addKnob(rateKnob); + addKnob(sizeKnob); + } + + public void onKnobChange(Knob knob) { + if (knob == rateKnob) { + rate.stop().setValue(9000 - 8000*knob.getValuef()); + } else if (knob == sizeKnob) { + falloff.stop().setValue(70 - 60*knob.getValuef()); + } + } + + void run(int deltaMs) { + sat += deltaMs * 0.00004; + float sVal1 = Strip.list.size() * (0.5 + 0.5*sin(sat)); + float sVal2 = Strip.list.size() * (0.5 + 0.5*cos(sat)); + + float pVal = pos.getValuef(); + float fVal = falloff.getValuef(); + + int s = 0; + for (Strip strip : Strip.list) { + int i = 0; + for (Point p : strip.points) { + colors[p.index] = color( + (lx.getBaseHuef() + s*.2 + i*3) % 360, + min(100, min(abs(s - sVal1), abs(s - sVal2))), + max(0, 100 - fVal*abs(i - pVal)) + ); + ++i; + } + } + } +} + +class Swarm extends SCPattern { + + SawLFO offset = new SawLFO(0, 16, 1000); + SinLFO rate = new SinLFO(350, 1200, 63000); + SinLFO falloff = new SinLFO(15, 50, 17000); + SinLFO fY = new SinLFO(0, 250, 19000); + SinLFO fZ = new SinLFO(0, 127, 11000); + SinLFO hOffY = new SinLFO(0, 255, 13000); + + public Swarm(GLucose glucose) { + super(glucose); + addModulator(offset).trigger(); + addModulator(rate).trigger(); + addModulator(falloff).trigger(); + addModulator(fY).trigger(); + addModulator(fZ).trigger(); + addModulator(hOffY).trigger(); + offset.modulateDurationBy(rate); + } + + float modDist(float v1, float v2, float mod) { + v1 = v1 % mod; + v2 = v2 % mod; + if (v2 > v1) { + return min(v2-v1, v1+mod-v2); + } + else { + return min(v1-v2, v2+mod-v1); + } + } + + void run(int deltaMs) { + float s = 0; + for (Strip strip : Strip.list) { + int i = 0; + for (Point p : strip.points) { + float fV = max(-1, 1 - dist(p.fy/2., p.fz, fY.getValuef()/2., fZ.getValuef()) / 64.); + colors[p.index] = color( + (lx.getBaseHuef() + 0.3 * abs(p.fy - hOffY.getValuef())) % 360, + constrain(80 + 40 * fV, 0, 100), + constrain(100 - (30 - fV * falloff.getValuef()) * modDist(i + (s*63)%61, offset.getValuef(), 16), 0, 100) + ); + ++i; + } + ++s; + } + } +} + +class SwipeTransition extends SCTransition { + SwipeTransition(GLucose glucose) { + super(glucose); + setDuration(5000); + } + + void computeBlend(int[] c1, int[] c2, double progress) { + float bleed = 50.; + float yPos = (float) (-bleed + progress * (255. + bleed)); + for (Point p : Point.list) { + float d = (p.fy - yPos) / 50.; + if (d < 0) { + colors[p.index] = c2[p.index]; + } else if (d > 1) { + colors[p.index] = c1[p.index]; + } else { + colors[p.index] = lerpColor(c2[p.index], c1[p.index], d, RGB); + } + } + } +} + +class CubeEQ extends SCPattern { + + private FFT fft = null; + private LinearEnvelope[] bandVals = null; + private int avgSize; + + private final BasicKnob thrsh = new BasicKnob("LVL", 0.35); + private final BasicKnob range = new BasicKnob("RANG", 0.45); + private final BasicKnob edge = new BasicKnob("EDGE", 0.5); + private final BasicKnob speed = new BasicKnob("SPD", 0.5); + private final BasicKnob tone = new BasicKnob("TONE", 0.5); + private final BasicKnob clr = new BasicKnob("CLR", 0.5); + + public CubeEQ(GLucose glucose) { + super(glucose); + addKnob(thrsh); + addKnob(range); + addKnob(edge); + addKnob(speed); + addKnob(tone); + addKnob(clr); + } + + protected void onActive() { + if (this.fft == null) { + this.fft = new FFT(lx.audioInput().bufferSize(), lx.audioInput().sampleRate()); + this.fft.window(FFT.HAMMING); + this.fft.logAverages(40, 1); + this.avgSize = this.fft.avgSize(); + this.bandVals = new LinearEnvelope[this.avgSize]; + for (int i = 0; i < this.bandVals.length; ++i) { + this.addModulator(this.bandVals[i] = (new LinearEnvelope(0, 0, 700+i*4))).trigger(); + } + } + } + + public void run(int deltaMs) { + this.fft.forward(this.lx.audioInput().mix); + float toneConst = .35 + .4 * (tone.getValuef() - 0.5); + float edgeConst = 2 + 30*(edge.getValuef()*edge.getValuef()*edge.getValuef()); + + for (int i = 0; i < avgSize; ++i) { + float value = this.fft.getAvg(i); + value = 20*log(1 + sqrt(value)); + float sqdist = avgSize - i; + value -= toneConst*sqdist*sqdist + .5*sqdist; + value *= 6; + if (value > this.bandVals[i].getValue()) { + this.bandVals[i].setEndVal(value, 40).trigger(); + } else { + this.bandVals[i].setEndVal(value, 1000 - 900*speed.getValuef()).trigger(); + } + } + + float jBase = 120 - 360*thrsh.getValuef(); + float jConst = 300.*(1-range.getValuef()); + float clrConst = 1.1 + clr.getValuef(); + + for (Point p : Point.list) { + float avgIndex = constrain((p.fy / 256. * avgSize), 0, avgSize-2); + int avgFloor = (int) avgIndex; + float j = jBase + jConst * (p.fz / 128.); + float value = lerp( + this.bandVals[avgFloor].getValuef(), + this.bandVals[avgFloor+1].getValuef(), + avgIndex-avgFloor + ); + + float b = constrain(edgeConst * (value - j), 0, 100); + colors[p.index] = color( + (480 + lx.getBaseHuef() - min(clrConst*p.fz, 120)) % 360, + 100, + b); + } + } +} + diff --git a/SugarCubes.pde b/SugarCubes.pde new file mode 100644 index 0000000..8428367 --- /dev/null +++ b/SugarCubes.pde @@ -0,0 +1,54 @@ +/** + * +-+-+-+-+-+ +-+-+-+-+-+ + * / /| |\ \ + * / / + + \ \ + * +-+-+-+-+-+ | +-+-+-+-+ | +-+-+-+-+-+ + * | | + / \ + | | + * + THE + / / \ \ + CUBES + + * | |/ +-+-+-+-+-+-+-+ \| | + * +-+-+-+-+-+ | | +-+-+-+-+-+ + * + + + * | SUGAR | + * + + + * | | + * +-+-+-+-+-+-+-+ + * + * Welcome to the Sugar Cubes! This Processing sketch is a fun place to build + * animations, effects, and interactions for the platform. Most of the icky + * code guts are embedded in the GLucose library extension. If you're an + * artist, you shouldn't need to worry about any of that. + * + * Below, you will find definitions of the Patterns, Effects, and Interactions. + * If you're an artist, create a new tab in the Processing environment with + * your name. Implement your classes there, and add them to the list below. + */ + +LXPattern[] patterns(GLucose glucose) { + return new LXPattern[] { + new SpaceTime(glucose), + new Swarm(glucose), + new CubeEQ(glucose), + + // Basic test patterns for reference, not art +// new TestHuePattern(glucose), +// new TestXPattern(glucose), +// new TestYPattern(glucose), +// new TestZPattern(glucose), + }; +} + +LXTransition[] transitions(GLucose glucose) { + return new LXTransition[] { + new DissolveTransition(lx).setDuration(3000), + new SwipeTransition(glucose), + new FadeTransition(lx).setDuration(2000), + }; +} + +LXEffect[] effects(GLucose glucose) { + return new LXEffect[] { + new FlashEffect(lx), + new DesaturationEffect(lx), + }; +} + diff --git a/TestPatterns.pde b/TestPatterns.pde new file mode 100644 index 0000000..f54e888 --- /dev/null +++ b/TestPatterns.pde @@ -0,0 +1,61 @@ +class TestHuePattern extends SCPattern { + public TestHuePattern(GLucose glucose) { + super(glucose); + } + public void run(int deltaMs) { + for (int i = 0; i < colors.length; ++i) { + colors[i] = color(lx.getBaseHuef(), 100, 100); + } + } +} + +class TestXPattern extends SCPattern { + private SinLFO xPos = new SinLFO(0, 127, 4000); + public TestXPattern(GLucose glucose) { + super(glucose); + addModulator(xPos).trigger(); + } + public void run(int deltaMs) { + for (Point p : Point.list) { + colors[p.index] = color( + lx.getBaseHuef(), + 100, + max(0, 100 - abs(p.fx - xPos.getValuef())) + ); + } + } +} + +class TestYPattern extends SCPattern { + private SinLFO yPos = new SinLFO(0, 255, 4000); + public TestYPattern(GLucose glucose) { + super(glucose); + addModulator(yPos).trigger(); + } + public void run(int deltaMs) { + for (Point p : Point.list) { + colors[p.index] = color( + lx.getBaseHuef(), + 100, + max(0, 100 - abs(p.fy - yPos.getValuef())) + ); + } + } +} + +class TestZPattern extends SCPattern { + private SinLFO zPos = new SinLFO(0, 127, 4000); + public TestZPattern(GLucose glucose) { + super(glucose); + addModulator(zPos).trigger(); + } + public void run(int deltaMs) { + for (Point p : Point.list) { + colors[p.index] = color( + lx.getBaseHuef(), + 100, + max(0, 100 - abs(p.fz - zPos.getValuef())) + ); + } + } +} diff --git a/_Internals.pde b/_Internals.pde new file mode 100644 index 0000000..d040f11 --- /dev/null +++ b/_Internals.pde @@ -0,0 +1,95 @@ +/** + * If you are an artist, you may ignore this file! It just sets + * up the framework to run the patterns. Should not need modification + * for general animation work. + */ + +import glucose.*; +import glucose.control.*; +import glucose.pattern.*; +import glucose.transition.*; +import glucose.model.*; +import heronarts.lx.*; +import heronarts.lx.effect.*; +import heronarts.lx.pattern.*; +import heronarts.lx.modulator.*; +import heronarts.lx.transition.*; +import ddf.minim.*; +import ddf.minim.analysis.*; +import processing.opengl.*; +import java.lang.reflect.*; + +final int VIEWPORT_WIDTH = 900; +final int VIEWPORT_HEIGHT = 700; +final int TARGET_FRAMERATE = 45; + +int startMillis, lastMillis; +GLucose glucose; +HeronLX lx; +LXPattern[] patterns; +LXTransition[] transitions; +LXEffect[] effects; +OverlayUI ui; +int activeTransitionIndex = 0; + +void setup() { + startMillis = lastMillis = millis(); + + // Initialize the Processing graphics environment + size(VIEWPORT_WIDTH, VIEWPORT_HEIGHT, OPENGL); + frameRate(TARGET_FRAMERATE); + // hint(ENABLE_OPENGL_4X_SMOOTH); // no discernable improvement? + logTime("Created viewport"); + + // Create the GLucose engine to run the cubes + glucose = new GLucose(this); + lx = glucose.lx; + logTime("Built GLucose engine"); + + // Set the patterns + glucose.lx.setPatterns(patterns = patterns(glucose)); + logTime("Built patterns"); + glucose.lx.addEffects(effects = effects(glucose)); + logTime("Built effects"); + transitions = transitions(glucose); + logTime("Built transitions"); + + // Build overlay UI + ui = new OverlayUI(); + logTime("Built overlay UI"); + + // MIDI devices + MidiKnobController.initializeStandardDevices(glucose); + logTime("Setup MIDI controllers"); + + println("Total setup: " + (millis() - startMillis) + "ms"); +} + +void logTime(String evt) { + int now = millis(); + println(evt + ": " + (now - lastMillis) + "ms"); + lastMillis = now; +} + +void draw() { + // The glucose engine deals with the core simulation here, we don't need + // to do anything specific. This method just needs to exist. +} + +void drawUI() { + if (uiOn) { + ui.draw(); + } else { + ui.drawHelpTip(); + } + ui.drawFPS(); +} + +boolean uiOn = true; +void keyPressed() { + if (key == 'u') { + uiOn = !uiOn; + } +} + + diff --git a/_Overlay.pde b/_Overlay.pde new file mode 100644 index 0000000..e212e1e --- /dev/null +++ b/_Overlay.pde @@ -0,0 +1,328 @@ +/** + * Overlay UI that indicates pattern control, etc. This will be moved + * into the Processing library once it is stabilized and need not be + * regularly modified. + */ +class OverlayUI { + + private final PFont titleFont = createFont("Myriad Pro", 10); + private final PFont itemFont = createFont("Myriad Pro", 11); + private final PFont knobFont = titleFont; + private final int w = 140; + private final int leftPos; + private final int leftTextPos; + private final int lineHeight = 20; + private final int sectionSpacing = 12; + private final int tempoHeight = 20; + private final int knobSize = 28; + private final int knobSpacing = 6; + private final int knobLabelHeight = 14; + private final color lightBlue = #666699; + private final color lightGreen = #669966; + + private final String[] patternNames; + private final String[] transitionNames; + private final String[] effectNames; + + private PImage logo; + + private int firstPatternY; + private int firstTransitionY; + private int firstEffectY; + private int firstKnobY; + private int tempoY; + + private Method patternStateMethod; + private Method transitionStateMethod; + private Method effectStateMethod; + + OverlayUI() { + leftPos = width - w; + leftTextPos = leftPos + 4; + logo = loadImage("logo-sm.png"); + + patternNames = classNameArray(patterns); + transitionNames = classNameArray(transitions); + effectNames = classNameArray(effects); + + try { + patternStateMethod = getClass().getMethod("getState", LXPattern.class); + effectStateMethod = getClass().getMethod("getState", LXEffect.class); + transitionStateMethod = getClass().getMethod("getState", LXTransition.class); + } catch (Exception x) { + throw new RuntimeException(x); + } + } + + void drawHelpTip() { + textFont(itemFont); + textAlign(RIGHT); + text("Tap 'u' to restore UI", width-4, height-6); + } + + void draw() { + image(logo, 4, 4); + + stroke(color(0, 0, 100)); + // fill(color(0, 0, 50, 50)); // alpha is bad for perf + fill(color(0, 0, 30)); + rect(leftPos-1, -1, w+2, height+2); + + int yPos = 0; + firstPatternY = yPos + lineHeight + 6; + yPos = drawObjectList(yPos, "PATTERN", patterns, patternNames, patternStateMethod); + + yPos += sectionSpacing; + yPos = drawObjectList(yPos, "CONTROL", null, null, null); + yPos += 6; + firstKnobY = yPos; + int xPos = leftTextPos; + for (int i = 0; i < glucose.NUM_PATTERN_KNOBS/2; ++i) { + drawKnob(xPos, yPos, knobSize, glucose.patternKnobs[i]); + drawKnob(xPos, yPos + knobSize + knobSpacing + knobLabelHeight, knobSize, glucose.patternKnobs[glucose.NUM_PATTERN_KNOBS/2 + i]); + xPos += knobSize + knobSpacing; + } + yPos += 2*(knobSize + knobLabelHeight) + knobSpacing; + + yPos += sectionSpacing; + firstTransitionY = yPos + lineHeight + 6; + yPos = drawObjectList(yPos, "TRANSITION", transitions, transitionNames, transitionStateMethod); + + yPos += sectionSpacing; + firstEffectY = yPos + lineHeight + 6; + yPos = drawObjectList(yPos, "FX", effects, effectNames, effectStateMethod); + + yPos += sectionSpacing; + yPos = drawObjectList(yPos, "TEMPO", null, null, null); + yPos += 6; + tempoY = yPos; + stroke(#111111); + fill(tempoDown ? lightGreen : color(0, 0, 35 - 8*lx.tempo.rampf())); + rect(leftPos + 4, yPos, w - 8, tempoHeight); + fill(0); + textAlign(CENTER); + text("" + ((int)(lx.tempo.bpmf() * 100) / 100.), leftPos + w/2., yPos + tempoHeight - 6); + yPos += tempoHeight; + + fill(#999999); + textFont(itemFont); + textAlign(LEFT); + text("Tap 'u' to hide UI (~+3FPS)", leftTextPos, height-6); + } + + public Knob getOrNull(List items, int index) { + if (index < items.size()) { + return items.get(index); + } + return null; + } + + public void drawFPS() { + textFont(titleFont); + textAlign(LEFT); + fill(#666666); + text("FPS: " + (((int)(frameRate * 10)) / 10.), 4, height-6); + } + + private final int STATE_DEFAULT = 0; + private final int STATE_ACTIVE = 1; + private final int STATE_PENDING = 2; + + public int getState(LXPattern p) { + if (p == lx.getPattern()) { + return STATE_ACTIVE; + } else if (p == lx.getNextPattern()) { + return STATE_PENDING; + } + return STATE_DEFAULT; + } + + public int getState(LXEffect e) { + return e.isEnabled() ? STATE_ACTIVE : STATE_DEFAULT; + } + + public int getState(LXTransition t) { + if (t == lx.getTransition()) { + return STATE_PENDING; + } else if (t == transitions[activeTransitionIndex]) { + return STATE_ACTIVE; + } + return STATE_DEFAULT; + } + + protected int drawObjectList(int yPos, String title, Object[] items, Method stateMethod) { + return drawObjectList(yPos, title, items, classNameArray(items), stateMethod); + } + + private int drawObjectList(int yPos, String title, Object[] items, String[] names, Method stateMethod) { + noStroke(); + fill(#aaaaaa); + textFont(titleFont); + textAlign(LEFT); + text(title, leftTextPos, yPos += lineHeight); + if (items != null) { + textFont(itemFont); + color textColor; + boolean even = true; + for (int i = 0; i < items.length; ++i) { + Object o = items[i]; + int state = STATE_DEFAULT; + try { + state = ((Integer) stateMethod.invoke(this, o)).intValue(); + } catch (Exception x) { + throw new RuntimeException(x); + } + switch (state) { + case STATE_ACTIVE: + fill(lightGreen); + textColor = #eeeeee; + break; + case STATE_PENDING: + fill(lightBlue); + textColor = color(0, 0, 75 + 15*sin(millis()/200.));; + break; + default: + textColor = 0; + fill(even ? #666666 : #777777); + break; + } + rect(leftPos, yPos+6, width, lineHeight); + fill(textColor); + text(names[i], leftTextPos, yPos += lineHeight); + even = !even; + } + } + return yPos; + } + + private void drawKnob(int xPos, int yPos, int knobSize, Knob knob) { + final float knobIndent = .4; + final float knobValue = knob.getValuef(); + String knobLabel = knob.getLabel(); + if (knobLabel.length() > 4) { + knobLabel = knobLabel.substring(0, 4); + } else if (knobLabel.length() == 0) { + knobLabel = "-"; + } + + ellipseMode(CENTER); + fill(#222222); + arc(xPos + knobSize/2, yPos + knobSize/2, knobSize, knobSize, HALF_PI + knobIndent, HALF_PI + knobIndent + (TWO_PI-2*knobIndent)); + + fill(lightGreen); + arc(xPos + knobSize/2, yPos + knobSize/2, knobSize, knobSize, HALF_PI + knobIndent, HALF_PI + knobIndent + (TWO_PI-2*knobIndent)*knobValue); + + fill(#333333); + ellipse(xPos + knobSize/2, yPos + knobSize/2, knobSize/2, knobSize/2); + + fill(0); + rect(xPos, yPos + knobSize + 2, knobSize, knobLabelHeight - 2); + fill(#999999); + textAlign(CENTER); + textFont(knobFont); + text(knobLabel, xPos + knobSize/2, yPos + knobSize + knobLabelHeight - 2); + + } + + private String[] classNameArray(Object[] objects) { + if (objects == null) { + return null; + } + String[] names = new String[objects.length]; + for (int i = 0; i < objects.length; ++i) { + names[i] = className(objects[i]); + } + return names; + } + + private String className(Object p) { + String s = p.getClass().getName(); + int li; + if ((li = s.lastIndexOf(".")) > 0) { + s = s.substring(li + 1); + } + if (s.indexOf("SugarCubes$") == 0) { + return s.substring("SugarCubes$".length()); + } + return s; + } + + private int knobIndex = -1; + private int lastY; + private int releaseEffect = -1; + private boolean tempoDown = false; + + public void mousePressed() { + lastY = mouseY; + knobIndex = -1; + releaseEffect = -1; + if (mouseY > tempoY) { + if (mouseY - tempoY < tempoHeight) { + lx.tempo.tap(); + tempoDown = true; + } + } else if (mouseY > firstEffectY) { + int effectIndex = (mouseY - firstEffectY) / lineHeight; + if (effectIndex < effects.length) { + if (effects[effectIndex].isMomentary()) { + effects[effectIndex].enable(); + releaseEffect = effectIndex; + } else { + effects[effectIndex].toggle(); + } + } + } else if (mouseY > firstTransitionY) { + int transitionIndex = (mouseY - firstTransitionY) / lineHeight; + if (transitionIndex < transitions.length) { + activeTransitionIndex = transitionIndex; + } + } else if ((mouseY >= firstKnobY) && (mouseY < firstKnobY + 2*(knobSize+knobLabelHeight) + knobSpacing)) { + knobIndex = (mouseX - leftTextPos) / (knobSize + knobSpacing); + if (mouseY >= firstKnobY + knobSize + knobLabelHeight + knobSpacing) { + knobIndex += glucose.NUM_PATTERN_KNOBS / 2; + } + } else if (mouseY > firstPatternY) { + int patternIndex = (mouseY - firstPatternY) / lineHeight; + if (patternIndex < patterns.length) { + patterns[patternIndex].setTransition(transitions[activeTransitionIndex]); + lx.goIndex(patternIndex); + } + } + } + + public void mouseDragged() { + int dy = lastY - mouseY; + lastY = mouseY; + if (knobIndex >= 0 && knobIndex < glucose.NUM_PATTERN_KNOBS) { + Knob k = glucose.patternKnobs[knobIndex]; + k.setValue(k.getValuef() + dy*.01); + } + } + + public void mouseReleased() { + tempoDown = false; + if (releaseEffect >= 0) { + effects[releaseEffect].trigger(); + releaseEffect = -1; + } + } +} + +void mousePressed() { + if (mouseX > ui.leftPos) { + ui.mousePressed(); + } +} + +void mouseReleased() { + if (mouseX > ui.leftPos) { + ui.mouseReleased(); + } +} + +void mouseDragged() { + if (mouseX > ui.leftPos) { + ui.mouseDragged(); + } +} + diff --git a/code/GLucose.jar b/code/GLucose.jar new file mode 100644 index 0000000..6ae9494 Binary files /dev/null and b/code/GLucose.jar differ diff --git a/code/HeronLX.jar b/code/HeronLX.jar new file mode 100644 index 0000000..2724cc6 Binary files /dev/null and b/code/HeronLX.jar differ diff --git a/code/rwmidi.jar b/code/rwmidi.jar new file mode 100644 index 0000000..4788ff6 Binary files /dev/null and b/code/rwmidi.jar differ diff --git a/logo-sm.png b/logo-sm.png new file mode 100644 index 0000000..3c7aa83 Binary files /dev/null and b/logo-sm.png differ