xxxxxxxxxx
445
/*
* Main Sketch
* Jack B. Du (github@jackbdu.com)
* https://instagram.com/jackbdu/
*/
// matterjs walls not responsive
// mouse moving too fast may skip tiles
// a random seed essentially defines a music piece
// agent wandering mode
// use mouse to control direction and speed
// multiple agents
// use previous sound visualization for agent
// agent potentially stuck outside canvas while resizing
// doesn't work on mobile
const sketch = ( p ) => {
p.specs = {
fps: 60,
numLoopFrames: 720,
colorBackground: 0,
outputWidth: 'auto',
outputHeight: 'auto',
}
p.options = {
physics: {
bodiesNum: 8,
radius: 0.1,
maxSides: 64,
initSpeedRange: [0.01, 0.02],
bodyOptions: {
label: 'ball',
inertia: Infinity,
restitution: 1,
friction: 0.000,
frictionAir: 0.002,
// render: {
// fillStyle: [255,150],
// }
},
wallOptions: {
label: 'wall',
isStatic: true,
restitution: 1,
friction: 0,
render: {
visible: false,
}
}
},
sound: {
urlMap: {
'mid-tom': 'assets/MT00.mp3',
'rim-shot': 'assets/RS.mp3',
'cowbell': 'assets/CB.mp3',
'clap': 'assets/CP.mp3',
},
meter: {
smoothing: 0.9,
normalRange: true,
channels: 1,
},
bpm: 120,
barsNum: 2,
beatsPerMeasure: 8,
timeSignature: 4,
interval: '8n',
},
};
p.env = {
canvas: undefined,
canvasShort: undefined,
canvasLong: undefined,
font: undefined,
message: 'Loading...',
activeTiles: [],
randomSeed: 20240228,
};
p.physics = {
bodies: [],
runner: Matter.Runner.create(),
engine: Matter.Engine.create(),
mouseConstraint: undefined,
calculateWallProperties: function(w, h, thickness = 1000) {
// return {
// topWall: {x: 0+w/2, y: -h/2-thickness/2+h/2, w: w, h: thickness},
// bottomWall: {x: 0+w/2, y: h/2+thickness/2+h/2, w: w, h: thickness},
// leftWall: {x: -w/2-thickness/2+w/2, y: 0+h/2, w: thickness, h: h},
// rightWall: {x: w/2+thickness/2+w/2, y: 0+h/2, w: thickness, h: h},
// };
return {
topWall: {x: 0, y: -h/2-thickness/2, w: w, h: thickness},
bottomWall: {x: 0, y: h/2+thickness/2, w: w, h: thickness},
leftWall: {x: -w/2-thickness/2, y: 0, w: thickness, h: h},
rightWall: {x: w/2+thickness/2, y: 0, w: thickness, h: h},
};
},
setup: function(w, h, options, env) {
const Runner = Matter.Runner,
MouseConstraint = Matter.MouseConstraint,
Mouse = Matter.Mouse,
Composite = Matter.Composite,
Bodies = Matter.Bodies,
Body = Matter.Body;
Runner.run(this.runner, this.engine);
// Runner.run(this.engine);
this.bodies = [];
const wallProperties = this.calculateWallProperties(w, h);
for (let k of Object.keys(wallProperties)) {
const wp = wallProperties[k];
const body = Bodies.rectangle(wp.x, wp.y, wp.w, wp.h, options.wallOptions);
body.customProperties = {
label: k,
w: wp.w,
h: wp.h,
}
this.bodies.push(body);
}
for (let i = 0; i < options.bodiesNum; i++) {
const body = Bodies.circle(0, 0, options.radius*env.canvasShort, options.bodyOptions, options.maxSides);
// const body = Bodies.circle(0+w/2, 0+h/2, options.radius*env.canvasShort, options.bodyOptions);
const randomDirection = p.random(Math.PI * 2);
const minSpeed = options.initSpeedRange[0]*env.canvasShort;
const maxSpeed = options.initSpeedRange[1]*env.canvasShort;
const randomMagnitude = p.random(minSpeed, maxSpeed);
const randomVel = p.createVector(0, randomMagnitude).setHeading(randomDirection);
Body.setVelocity(body, {x: randomVel.x, y: randomVel.y});
body.customProperties = {
refCanvasShort: env.canvasShort,
fillHue: Math.random()*360,
};
this.bodies.push(body);
}
const world = this.engine.world;
this.engine.gravity.y = 0;
Composite.add(world, this.bodies);
// console.log(env.canvas.elt);
const mouse = Mouse.create(document.getElementById('defaultCanvas0'));
this.mouseConstraint = MouseConstraint.create(this.engine, {
mouse: mouse,
constraint: {
stiffness: 0.2,
render: {
visible: false
}
}
});
this.mouseConstraint.mouse.pixelRatio = p.pixelDensity();
this.updateMouseOffset();
// console.log(this.composite);
Composite.add(world, this.mouseConstraint);
},
update: function(time) {
Matter.Runner.tick(this.runner, this.engine, time);
// Matter.Engine.update(this.engine, time);
},
updateMouseOffset() {
const canvasElement = this.mouseConstraint.mouse.element;
Matter.Mouse.setOffset(this.mouseConstraint.mouse, {x: -canvasElement.clientWidth/2, y: -canvasElement.clientHeight/2});
},
updateDimensions(w, h) {
const wallProperties = this.calculateWallProperties(w, h);
for (let body of this.bodies) {
if (body.label === 'wall') {
const wp = wallProperties[body.customProperties.label];
Matter.Body.setPosition(body, {x: wp.x, y: wp.y});
Matter.Body.scale(
body,
wp.w/body.customProperties.w,
wp.h/body.customProperties.h
);
body.customProperties.w = wp.w;
body.customProperties.h = wp.h;
} else if (body.label === 'ball') {
const canvasShort = Math.min(w,h);
const scaleFactor = canvasShort/body.customProperties.refCanvasShort;
Matter.Body.scale(
body, scaleFactor, scaleFactor
);
body.customProperties.refCanvasShort = canvasShort;
}
}
this.updateMouseOffset();
},
draw: function() {
// const canvasElement = this.mouseConstraint.mouse.element;
// p.push();
// p.translate(-canvasElement.clientWidth/2, -canvasElement.clientHeight/2);
for (let body of this.bodies) {
this.drawBody(body);
}
// p.pop();
},
drawBody: function(b) {
if (b && b.render.visible) {
if (b.render.lineWidth) p.strokeWeight(b.render.lineWidth);
if (b.render.strokeStyle) p.stroke(b.render.strokeStyle);
if (b.render.fillStyle) p.fill(b.render.fillStyle);
if (b.parts.length > 1) {
for (let i = 1; i < b.parts.length; i++) {
this.drawBody(b.parts[i]);
}
} else {
if (b.vertices) {
p.push();
p.beginShape();
for (let i = 0; i < b.vertices.length; i++) {
const vtx = b.vertices[i];
if (b.customProperties.fillHue) {
const precentage = i/b.vertices.length;
const h = b.customProperties.fillHue;
const s = Math.sin(Math.abs(precentage-0.5)*Math.PI*0.2+1)*100;
const l = Math.sin(Math.abs(precentage-0.5)*Math.PI*0.2+2)*100;
const a = 0.9;
p.colorMode(p.HSL);
p.fill(h, s, l, a);
}
p.vertex(vtx.x, vtx.y);
}
p.endShape(p.CLOSE);
p.pop();
}
}
}
},
// for more creative body
drawResponsive: function(sizeFactor = 0, animOffset = 0) {
for (let body of this.bodies) {
this.drawResponsiveBody(body, sizeFactor, animOffset);
}
},
drawResponsiveBody: function(body, sizeFactor = 0, animOffset = 0) {
if (body && body.label === 'ball') {
const d = p.dist(body.bounds.min.x,body.bounds.min.y,body.bounds.max.x,body.bounds.max.y)/2;
// console.log(body);
const cx = body.position.x;
const cy = body.position.y;
const verticesNum = 64;
p.push();
p.beginShape();
for (let i = 0; i < verticesNum; i++) {
const precentage = i/verticesNum;
const x = Math.sin(precentage*Math.PI*2)*(d/2+d*sizeFactor);
const y = Math.cos(precentage*Math.PI*2)*(d/2+d*sizeFactor);
const h = animOffset*360;
const s = Math.sin(Math.abs(precentage-0.5)*Math.PI*0.2+1)*100;
const l = Math.sin(Math.abs(precentage-0.5)*Math.PI*0.2+2)*100;
p.colorMode(p.HSL);
p.fill(h, s, l);
// p.noStroke();
p.vertex(x+cx, y+cy);
}
p.endShape(p.CLOSE);
p.pop();
}
},
}
p.sound = {
players: new Tone.Players(p.options.sound.urlMap),
meter: new Tone.Meter(p.options.sound.meter),
names: Object.keys(p.options.sound.urlMap),
isLoaded: false,
onLoad: function() {
p.sound.isLoaded = true;
p.env.message = 'Click to start!';
},
setup: function(options) {
this.players.toDestination();
this.players.connect(this.meter);
Tone.loaded().then(this.onLoad);
Tone.Transport.bpm.value = options.bpm;
Tone.Transport.timeSignature = options.timeSignature;
Tone.Transport.scheduleRepeat((time) => {
const barsBeatsSixteens = Tone.Transport.position.split(':');
const bar = p.int(barsBeatsSixteens[0]);
const beat = p.int(barsBeatsSixteens[1]*2+p.floor(barsBeatsSixteens[2]/2));
// console.log(barsBeatsSixteens, beat);
const activeBeat = (bar % options.barsNum) * options.beatsPerMeasure + beat;
const soundNamesToPlay = [];
for (let tile of p.env.activeTiles) {
if (activeBeat === tile.c) {
soundNamesToPlay.push(p.sound.names[tile.r]);
}
}
for (let name of soundNamesToPlay) {
p.sound.players.player(name).start(time);
}
p.grid.highlightCol(activeBeat);
}, options.interval);
}
};
p.preload = () => {
p.env.font = p.loadFont('assets/Ubuntu-Bold.ttf');
}
p.setup = () => {
p.updateCanvas(p.specs.outputWidth, p.specs.outputHeight);
p.frameRate(p.specs.fps);
p.smooth();
p.randomSeed(p.env.randomSeed);
p.sound.setup(p.options.sound);
p.physics.setup(p.width, p.height, p.options.physics, p.env);
const soundsNum = p.sound.names.length;
const beatsNum = p.options.sound.barsNum * p.options.sound.beatsPerMeasure;
p.grid = new Grid(0, 0, p.width, p.height, beatsNum, soundsNum, p);
};
p.draw = () => {
if (p.beforeDraw) p.beforeDraw();
const soundAmp = p.sound.meter.getValue();
const animOffset = p.frameCount%p.specs.numLoopFrames/p.specs.numLoopFrames;
p.background(p.specs.colorBackground);
p.grid.display();
if (p.sound.isLoaded) {
p.physics.draw();
// p.physics.drawResponsive(soundAmp, animOffset);
p.physics.update(p.deltaTime);
// console.log(p.physics.mouseConstraint.mouse);
// console.log(p.physics.mouseConstraint.mouse.element);
if (p.env.message === '') {
p.checkInteraction(p.physics.bodies);
}
}
p.displayInfo(p.env.message, p.env.canvasShort);
if (p.afterDraw) p.afterDraw();
};
p.displayInfo = (message, size) => {
p.textFont(p.env.font);
p.textSize(size/message.length);
p.textAlign(p.CENTER, p.CENTER);
p.fill(255);
p.noStroke();
p.text(message, 0, 0);
}
p.toggleTiles = (tiles, activeTiles) => {
for (let tile of tiles) {
let alreayActive = false;
for (let i = 0; i < activeTiles.length; i++) {
if (activeTiles[i].equals(tile)) {
alreayActive = true;
activeTiles.splice(i, 1);
break;
}
}
if (!alreayActive) {
activeTiles.push(tile);
}
tile.toggle();
}
}
p.highlightTiles = (tiles) => {
for (let tile of tiles) {
tile.setHover(true);
}
}
p.dehighlightTiles = (tiles) => {
for (let tile of tiles) {
tile.setHover(false);
}
}
p.checkInteraction = (bodies) => {
for (let body of bodies) {
if (body.previouslyIntersectedTiles === undefined || body.intersectedTiles === undefined) {
body.previouslyIntersectedTiles = [];
body.intersectedTiles = [];
}
body.previouslyIntersectedTiles = body.intersectedTiles;
body.intersectedTiles = p.grid.getIntersected(body.position.x, body.position.y);
p.dehighlightTiles(body.previouslyIntersectedTiles);
p.highlightTiles(body.intersectedTiles);
const newlyIntersectedTiles = [];
for (let tile of body.intersectedTiles) {
let isNew = true;
for (let ptile of body.previouslyIntersectedTiles) {
if (tile.equals(ptile)) {
isNew = false;
break;
}
}
if (isNew) newlyIntersectedTiles.push(tile);
}
p.toggleTiles(newlyIntersectedTiles, p.env.activeTiles);
}
}
p.mousePressed = () => {
if (p5sketch.sound.isLoaded && p5sketch.env.message !== '') {
// avoid browser suspending audio context, still doesn't work on mobile
Tone.start().then(() => {
Tone.Transport.start();
p5sketch.env.message = '';
});
}
}
p.beforeDraw = () => {
if (p.beginCapture) p.beginCapture();
}
p.afterDraw = () => {
if (p.endCapture) p.endCapture();
}
p.windowResized = () => {
p.updateCanvas(p.specs.outputWidth, p.specs.outputHeight);
}
p.updateCanvas = (outputWidth = 'auto', outputHeight = 'auto') => {
const pd = p.pixelDensity();
const canvasWidth = outputWidth && outputWidth !== 'auto' ? outputWidth/pd : p.windowWidth;
const canvasHeight = outputHeight && outputHeight !== 'auto' ? outputHeight/pd : p.windowHeight;
if (canvasWidth !== p.width || canvasHeight !== p.height) {
if (!p.hasCanvas) {
p.env.canvas = p.createCanvas(canvasWidth, canvasHeight, p.WEBGL);
p.hasCanvas = true;
} else {
p.resizeCanvas(canvasWidth, canvasHeight);
p.grid.updateDimensions(canvasWidth, canvasHeight);
p.physics.updateDimensions(canvasWidth, canvasHeight, p.options.physics);
}
}
p.env.canvasShort = p.min(canvasWidth, canvasHeight);
p.env.canvasLong = p.max(canvasWidth, canvasHeight);
}
};
let p5sketch = new p5(sketch);