import javax.sound.midi.*; class Vector2D { float x; float y; Vector2D(float x, float y) { this.x = x; this.y = y; } Vector2D dividedBy(float aFloat) { return new Vector2D(x / aFloat, y / aFloat); } float dot(Vector2D other) { return x * other.x + y * other.y; } float length() { return sqrt(x * x + y * y); } Vector2D minus(Vector2D other) { return new Vector2D(x - other.x, y - other.y); } Vector2D normalized() { return this.dividedBy(this.length()); } Vector2D plus(Vector2D other) { return new Vector2D(x + other.x, y + other.y); } Vector2D reflectionWith(Vector2D normal) { Vector2D reversed = this.times(-1); return normal.times(2).times(reversed.dot(normal)).minus(reversed); } Vector2D times(float aFloat) { return new Vector2D(x * aFloat, y * aFloat); } public String toString() { return "(" + x + ", " + y + ")"; } } class PhysicalState { Vector2D location; Vector2D velocity; PhysicalState(Vector2D location, Vector2D velocity) { this.location = location; this.velocity = velocity; } PhysicalState stateAfterStep(float stepSize) { return new PhysicalState(location.plus(velocity.times(stepSize)), velocity); } } class Collision { Vector2D location; Vector2D normal; Collision(Vector2D location, Vector2D normal) { this.location = location; this.normal = normal; } boolean xMajor() { return abs(normal.x) < abs(normal.y); } } class Pulse { int totalWeight = 30; Vector2D location; color pulseColor; int slotNumber; int weight; Pulse(Vector2D location, color pulseColor, int slotNumber) { this.location = location; this.pulseColor = pulseColor; this.slotNumber = slotNumber; weight = totalWeight; } void draw() { color effectiveColor = color(red(pulseColor), green(pulseColor), blue(pulseColor), 1.0 / totalWeight * weight * 255); noStroke(); fill(effectiveColor); float width = (totalWeight - weight) * 1.0 / totalWeight * 42 + 8; ellipse(location.x, location.y, width, width); weight--; } } int ballRadius = 16; int borderWidth = 16; int stepsPerSecond = 30; Synthesizer synthesizer; Sequencer sequencer; Sequence sequence; MidiChannel channel; Vector2D up = new Vector2D(0, -1); Vector2D left = new Vector2D(-1, 0); Vector2D down = new Vector2D(0, 1); Vector2D right = new Vector2D(1, 0); Vector2D mouseDownInBall; Vector2D lastMouseDelta; PhysicalState ballState; color[] pulseColors = {#555555, #5555FF, #55FF55, #55FFFF, #FF5555, #FF55FF, #FFFF55}; int[] pulseNotes = {57, 59, 60, 62, 64, 65, 67}; ArrayList pulses; void setup() { size(320, 240); smooth(); frameRate(stepsPerSecond); try { synthesizer = MidiSystem.getSynthesizer(); synthesizer.open(); sequencer = MidiSystem.getSequencer(); sequence = new Sequence(Sequence.PPQ, 10); Soundbank soundbank = synthesizer.getDefaultSoundbank(); Instrument[] instruments = soundbank.getInstruments(); synthesizer.loadInstrument(instruments[0]); MidiChannel[] channels = synthesizer.getChannels(); channel = channels[0]; } catch (Exception e) { throw new RuntimeException("Rethrowing", e); } lastMouseDelta = new Vector2D(0, 0); ballState = new PhysicalState(new Vector2D(width / 2.0, 30), new Vector2D(100, 0)); pulses = new ArrayList(); } void draw() { background(255); noStroke(); fill(128); ellipse(ballState.location.x, ballState.location.y, ballRadius * 2, ballRadius * 2); for (int i = pulses.size() - 1; i >= 0; i--) { Pulse each = (Pulse)pulses.get(i); each.draw(); if (each.weight == 0) { pulses.remove(each); boolean foundAnother = false; for (int j = 0; j < pulses.size(); j++) { Pulse eachOther = (Pulse)pulses.get(j); if (eachOther.slotNumber == each.slotNumber) foundAnother = true; } if (!foundAnother) channel.noteOff(pulseNotes[each.slotNumber], 75); } } ballState = ballState.stateAfterStep(1.0 / stepsPerSecond); if (mouseDownInBall == null) { Collision collision = detectCollision(ballState); if (collision != null) { if (collision.xMajor()) { if (abs(ballState.velocity.y) < 50) ballState.velocity.y = 0; } else { if (abs(ballState.velocity.x) < 50) ballState.velocity.x = 0; } float pulsePlacement; float pulseRange; if (collision.xMajor()) { pulsePlacement = collision.location.x; pulseRange = width - borderWidth * 2; } else { pulsePlacement = collision.location.y; pulseRange = height - borderWidth * 2; } int pulseSlot = (int)((pulsePlacement - borderWidth) / (pulseRange / (float)pulseColors.length)); pulseSlot = constrain(pulseSlot, 0, pulseColors.length - 1); if (collision.normal.x > 0 || collision.normal.y > 0) pulseSlot = pulseColors.length - 1 - pulseSlot; pulses.add(new Pulse(collision.location, pulseColors[pulseSlot], pulseSlot)); channel.noteOn(pulseNotes[pulseSlot], (int)constrain(ballState.velocity.length() / 4, 0, 127)); ballState.velocity = ballState.velocity.reflectionWith(collision.normal).times(0.9); } if ((ballState.location.y + ballRadius) < height - borderWidth) ballState.velocity.y += 9.8; } lastMouseDelta = new Vector2D(mouseX - pmouseX, mouseY - pmouseY); } void mouseDragged() { if (mouseDownInBall != null) { ballState.location.x = constrain(mouseX - mouseDownInBall.x, borderWidth + ballRadius, width - borderWidth - ballRadius); ballState.location.y = constrain(mouseY - mouseDownInBall.y, borderWidth + ballRadius, height - borderWidth - ballRadius); } } void mousePressed() { Vector2D mouseDownLocation = new Vector2D(mouseX, mouseY); Vector2D differenceFromCenter = mouseDownLocation.minus(ballState.location); if (differenceFromCenter.length() < ballRadius) { mouseDownInBall = differenceFromCenter; ballState.velocity = new Vector2D(0, 0); } } void mouseReleased() { if (mouseDownInBall != null) { mouseDownInBall = null; ballState.velocity = lastMouseDelta.times(stepsPerSecond); } } Collision detectCollision(PhysicalState aState) { if (aState.location.y + ballRadius >= height - borderWidth && aState.velocity.y > 0) { return new Collision(new Vector2D(aState.location.x, height - borderWidth), up); } else if (aState.location.x + ballRadius >= width - borderWidth && aState.velocity.x > 0) { return new Collision(new Vector2D(width - borderWidth, aState.location.y), left); } else if (aState.location.y - ballRadius < 0 + borderWidth && aState.velocity.y < 0) { return new Collision(new Vector2D(aState.location.x, 0 + borderWidth), down); } else if (aState.location.x - ballRadius < 0 + borderWidth && aState.velocity.x < 0) { return new Collision(new Vector2D(0 + borderWidth, aState.location.y), right); } else return null; }