My contribution to this month's Creative Coding Club challenge.
I used this month's challenge to learn and get comfortable with Matter.js and P5.js.
A Pen by Mariusz Dabrowski on CodePen.
My contribution to this month's Creative Coding Club challenge.
I used this month's challenge to learn and get comfortable with Matter.js and P5.js.
A Pen by Mariusz Dabrowski on CodePen.
<div class="instructions"> | |
Click and drag Sticky the marshmallow!<br> | |
See how it was built <a href="https://codepen.io/MarioD/post/hot-and-sticky-the-process" target="_blank">here</a>. | |
</div> | |
<div class="height-warning"></div> |
// Module aliases | |
var Engine = Matter.Engine, | |
World = Matter.World, | |
Bodies = Matter.Bodies, | |
Body = Matter.Body, | |
Constraint = Matter.Constraint, | |
Composite = Matter.Composite, | |
Composites = Matter.Composites, | |
MouseConstraint = Matter.MouseConstraint, | |
Mouse = Matter.Mouse, | |
Events = Matter.Events, | |
Vertices = Matter.Vertices; | |
var engine = Engine.create(); | |
var world = engine.world; | |
var floor; | |
var cup; | |
var cupLeft; | |
var cupRight; | |
var cupHandle; | |
var chain = null; | |
var heatLines = []; | |
var distanceToCup = 10000; | |
var distanceFromCup = {size: 500, towards: true}; | |
var firstAnimation = {max: 241, min: 171, percent: 1}; | |
var secondAnimation = {max: 170, min: 100, percent: 0}; | |
var thirdAnimation = {max: 241, min: 100, percent: 0}; | |
// --------------- | |
// Box Constructor | |
// --------------- | |
function Box(x, y, w, h, options) { | |
this.w = w; | |
this.h = h; | |
this.body = Bodies.rectangle(x, y, w, h, options); | |
World.add(world, this.body); | |
} | |
// | |
function calculateLinks() { | |
// 77 is the offset from the bottom and the top | |
// 200 is the amount of space we want between the marshmallow and cup when hanging | |
var spaceLeft = window.innerHeight - (125 + 100 + 77 + 200); | |
var links = spaceLeft / 26; | |
if(links < 3) { | |
return Math.ceil(3); | |
} else if(links > 6) { | |
return Math.ceil(6); | |
} else { | |
return Math.ceil(links); | |
} | |
} | |
// ------------ | |
// Create chain | |
// ------------ | |
function CreateChain(x, y, chainLinks, linkLength) { | |
this.x = x; | |
this.y = y; | |
this.hinges = []; | |
this.constraints = []; | |
this.chainLinks = chainLinks; | |
this.linkLength = linkLength; | |
} | |
CreateChain.prototype.remove = function() { | |
for(var i = 0; i < this.constraints.length; i++) { | |
World.remove(world, this.constraints[i]); | |
} | |
chain = null; | |
} | |
CreateChain.prototype.init = function() { | |
// Create hinges | |
for(var i = 0; i < this.chainLinks; i++) { | |
var static = (i === 0) ? true : false ; | |
var anchor = new Box(this.x, this.y + (this.linkLength * i), 5, 5, { | |
isStatic: static, | |
collisionFilter: { | |
category: 0x0001 | |
} | |
}); | |
this.hinges.push(anchor); | |
} | |
// Create links between hinges | |
for(var i = 0; i < this.hinges.length; i++) { | |
var constraint; | |
if(i === this.chainLinks - 1) { | |
constraint = Constraint.create({ | |
bodyA: this.hinges[i].body, | |
bodyB: marshmallow.body, | |
pointB: { x: 0, y: (marshmallow.h/2 * -1) + 12 }, | |
length: this.linkLength, | |
damping: 0.5, | |
stiffness: 0.1, | |
label: 'marshmallowAttachment' | |
}); | |
} else { | |
constraint = Constraint.create({ | |
bodyA: this.hinges[i].body, | |
bodyB: this.hinges[i + 1].body, | |
length: this.linkLength, | |
damping: 0.5, | |
stiffness: 0.1 | |
}); | |
} | |
this.constraints.push(constraint); | |
World.add(world, constraint); | |
} | |
} | |
function createChain() { | |
chain = new CreateChain(width/2, 50, calculateLinks(), 10); | |
chain.init(); | |
} | |
// -------------- | |
// Heat particles | |
// -------------- | |
function HeatParticle(x, y) { | |
this.position = createVector(x, y); | |
this.index = 0; | |
} | |
HeatParticle.prototype.render = function() { | |
push(); | |
noStroke(); | |
fill('#f0d38d'); | |
ellipse(this.position.x, this.position.y, this.parent.particleSize); | |
pop(); | |
} | |
HeatParticle.prototype.updatePos = function() { | |
this.position.y -= 0.5; | |
this.position.x = Math.sin((frameCount + this.index/0.4) / 35) * 10 + this.parent.position.x; | |
} | |
HeatParticle.prototype.checkPos = function() { | |
if(this.position.y < this.parent.position.y - this.parent.height) { | |
this.reset(); | |
} | |
} | |
HeatParticle.prototype.reset = function() { | |
this.parent.particleIndex += 1; | |
this.index = this.parent.particleIndex; | |
this.position.y = this.parent.position.y; | |
} | |
// ---------- | |
// Heat lines | |
// ---------- | |
function HeatLine(x, y, height, particleSize) { | |
this.position = createVector(x, y); | |
this.particles = []; | |
this.particleIndex = 0; | |
this.height = height; | |
this.particleSize = particleSize; | |
} | |
HeatLine.prototype.render = function() { | |
for(var i = 0; i < this.particles.length; i++) { | |
this.particles[i].updatePos(); | |
this.particles[i].render(); | |
this.particles[i].checkPos(); | |
} | |
} | |
HeatLine.prototype.init = function() { | |
var particleCount = this.height / (this.particleSize / 6); | |
for(var i = 0; i < particleCount; i++) { | |
this.particleIndex += 1; | |
var particle = new HeatParticle(this.position.x, this.position.y + (i * this.particleSize / 6)); | |
particle.index = this.particleIndex; | |
particle.parent = this; | |
this.particles.push(particle); | |
} | |
} | |
function populateHeatLines() { | |
heatLines.push(new HeatLine(cup.body.position.x, cup.body.position.y - cup.h/2, 50, 5)); | |
heatLines.push(new HeatLine(cup.body.position.x - 60, cup.body.position.y - cup.h/2, 50, 5)); | |
heatLines.push(new HeatLine(cup.body.position.x + 60, cup.body.position.y - cup.h/2, 50, 5)); | |
for(var i = 0; i < heatLines.length; i++) { | |
heatLines[i].init(); | |
} | |
} | |
// ----------- | |
// Cup + Floor | |
// ----------- | |
// Change this to an object since we don't need it to construct anything | |
function CupFloor() {} | |
CupFloor.prototype.destroy = function() { | |
World.remove(world, [ | |
floor.body, | |
cup.body, | |
cupLeft.body, | |
cupRight.body, | |
cupHandle.body, | |
]); | |
floor = null; | |
cup = null; | |
cupLeft = null; | |
cupRight = null; | |
cupHandle = null; | |
} | |
CupFloor.prototype.init = function() { | |
// All of the magic numbers here are to position the elements relative to the marshmallow body | |
floor = new Box(width/2, height - 31.75, 320, 3.5, {isStatic: true, collisionFilter: {category: 0x0002}}); | |
cup = new Box(width/2, height - 93, 259, 125.5, {isStatic: true, isSensor: true, label: 'cup', collisionFilter: {category: 0x0002}}); | |
cupLeft = new Box(width/2 - 134.5, height - 93, 10, 125.5, {isStatic: true, collisionFilter: {category: 0x0002}}); | |
cupRight = new Box(width/2 + 134.5, height - 93, 10, 125.5, {isStatic: true, collisionFilter: {category: 0x0002}}); | |
cupHandle = new Box(width/2 + 153, height - 114, 31, 60.5, {isStatic: true, collisionFilter: {category: 0x0002}}); | |
} | |
var cupFloor = new CupFloor(); | |
// --------- | |
// P5 Resize | |
// --------- | |
function windowResized() { | |
resizeCanvas(windowWidth, windowHeight); | |
cupFloor.destroy(); | |
cupFloor.init(); | |
heatLines = []; | |
populateHeatLines(); | |
marshmallow.body.isStatic = true; | |
marshmallow.body.angle = 0; | |
if(chain) { | |
chain.remove(); | |
} | |
createChain(); | |
marshmallow.body.isStatic = false; | |
Body.setVelocity(marshmallow.body, { | |
x: 0, | |
y: 0 | |
}); | |
marshmallow.angularVelocity = 0; | |
marshmallow.angularSpeed = 0; | |
firstAnimation.percent = 0; | |
secondAnimation.percent = 0; | |
thirdAnimation.percent = 0; | |
} | |
// -------- | |
// P5 Setup | |
// -------- | |
function setup() { | |
// Setup the canvas | |
var canvas = createCanvas(windowWidth, windowHeight); | |
rectMode(CENTER); | |
// Setup the mouse events | |
var mouse = Mouse.create(canvas.elt); | |
mouse.pixelRatio = pixelDensity(); | |
var mouseConstraint = MouseConstraint.create(engine, {mouse: mouse, constraint: {stiffness: 0.2}}); | |
mouseConstraint.collisionFilter.category = 0x0002; | |
World.add(world, mouseConstraint); | |
// Load all of the image assets | |
marshmallowBody = loadImage('https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/body.png'); | |
floorImg = loadImage('https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/ground.png'); | |
cupImg = loadImage('https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/cup.png'); | |
cupHandleImg = loadImage('https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/cupHandle.png'); | |
// Create the boundaries | |
cupFloor.init(); | |
// ----------- | |
// Marshmallow | |
// ----------- | |
// The marshmallow code below is very messy and should be refactored into a constructor | |
marshmallow = new Box(width/2, 0, 80, 100, { | |
density: 0.00001, | |
label: 'marshmallow', | |
collisionFilter: { | |
category: 0x0001, | |
mask: 0x0002 | |
} | |
}); | |
createChain(); | |
armLeft = Bodies.circle(width/2 - 40, 300, 5, { | |
collisionFilter: { | |
category: 0x0001 | |
}, | |
density: 0.00001 | |
}); | |
armRight = Bodies.circle(width/2 + 40, 300, 5, { | |
collisionFilter: { | |
category: 0x0001 | |
}, | |
density: 0.00001 | |
}); | |
var legRight = Bodies.circle(width/2 + 20, 300 + 50, 0.1, { | |
collisionFilter: { | |
category: 0x0001 | |
}, | |
density: 0.00001 | |
}); | |
var legLeft = Bodies.circle(width/2 - 20, 300 + 50, 0.1, { | |
collisionFilter: { | |
category: 0x0001 | |
}, | |
density: 0.00001 | |
}); | |
constraintArmLeft = Constraint.create({ | |
bodyA: marshmallow.body, | |
bodyB: armLeft, | |
pointA: { x: -39, y: -20 }, | |
length: 40, | |
damping: 0.5, | |
stiffness: 1, | |
label: 'limb' | |
}); | |
constraintArmRight = Constraint.create({ | |
bodyA: marshmallow.body, | |
bodyB: armRight, | |
pointA: { x: 39, y: -20 }, | |
length: 40, | |
damping: 0.5, | |
stiffness: 1, | |
label: 'limb' | |
}); | |
constraintLegRight = Constraint.create({ | |
bodyA: marshmallow.body, | |
bodyB: legRight, | |
pointA: { x: 20, y: 49 }, | |
length: 30, | |
damping: 0.5, | |
stiffness: 1, | |
label: 'limb' | |
}); | |
constraintLegLeft = Constraint.create({ | |
bodyA: marshmallow.body, | |
bodyB: legLeft, | |
pointA: { x: -20, y: 49 }, | |
length: 30, | |
damping: 0.5, | |
stiffness: 1, | |
label: 'limb' | |
}); | |
World.add(world, [ | |
armLeft, | |
armRight, | |
legRight, | |
legLeft, | |
constraintArmLeft, | |
constraintArmRight, | |
constraintLegRight, | |
constraintLegLeft | |
]); | |
// Create and initialize the heat lines | |
populateHeatLines(); | |
// Start the engine | |
Engine.run(engine); | |
} | |
// ------- | |
// P5 Draw | |
// ------- | |
function draw() { | |
clear(); | |
// ---------------------- | |
// Outline matter objects | |
// ---------------------- | |
// push(); | |
// var bodies = Composite.allBodies(engine.world); | |
// | |
// drawingContext.beginPath(); | |
// for (var i = 0; i < bodies.length; i += 1) { | |
// var vertices = bodies[i].vertices; | |
// drawingContext.moveTo(vertices[0].x, vertices[0].y); | |
// for (var j = 1; j < vertices.length; j += 1) { | |
// drawingContext.lineTo(vertices[j].x, vertices[j].y); | |
// } | |
// drawingContext.lineTo(vertices[0].x, vertices[0].y); | |
// } | |
// | |
// drawingContext.lineWidth = 1; | |
// drawingContext.strokeStyle = '#9e9e9e'; | |
// drawingContext.stroke(); | |
// pop(); | |
if(cup) { | |
// heatLines | |
for(var i = 0; i < heatLines.length; i++) { | |
heatLines[i].render(); | |
} | |
} | |
// -------------------- | |
// Draw the marshmallow | |
// -------------------- | |
push(); | |
translate(marshmallow.body.position.x, marshmallow.body.position.y); | |
rotate(marshmallow.body.angle); | |
image(marshmallowBody, marshmallow.w/2 * -1, marshmallow.h/2 * -1, marshmallow.w, marshmallow.h); | |
pop(); | |
// -------------- | |
// Draw the chain | |
// -------------- | |
var allConstraints = Composite.allConstraints(engine.world); | |
var marshmallowAttachment; | |
// Rope hole at the top of the page | |
if(chain) { | |
push(); | |
noStroke(); | |
fill('black'); | |
ellipse(chain.x, chain.y, 25, 6); | |
pop(); | |
} | |
for(var i = 0; i < allConstraints.length; i++) { | |
if(allConstraints[i].label === 'marshmallowAttachment') { | |
marshmallowAttachment = allConstraints[i]; | |
} | |
if(allConstraints[i].label !== 'Mouse Constraint') { | |
push(); | |
strokeWeight(2.5); | |
line( | |
allConstraints[i].bodyA.position.x + allConstraints[i].pointA.x, | |
allConstraints[i].bodyA.position.y + allConstraints[i].pointA.y, | |
allConstraints[i].bodyB.position.x + allConstraints[i].pointB.x, | |
allConstraints[i].bodyB.position.y + allConstraints[i].pointB.y | |
); | |
pop(); | |
} | |
} | |
// Rope attachment on top of head | |
if(marshmallowAttachment) { | |
push(); | |
noStroke(); | |
fill('black'); | |
translate(marshmallowAttachment.bodyB.position.x + marshmallowAttachment.pointB.x, marshmallowAttachment.bodyB.position.y + marshmallowAttachment.pointB.y); | |
rotate(marshmallow.body.angle); | |
ellipse(0, 0, 10, 3); | |
pop(); | |
} | |
// Draw arms | |
push(); | |
strokeWeight(2.5); | |
ellipse(armLeft.position.x, armLeft.position.y, 10); | |
ellipse(armRight.position.x, armRight.position.y, 10); | |
pop(); | |
// When the arms enter the cup, raise them | |
if(cup) { | |
if(marshmallow.body.position.y / height > 0.75 && marshmallow.body.position.x > cup.body.position.x - cup.w/2 && marshmallow.body.position.x < cup.body.position.x + cup.w/2) { | |
Matter.Body.setVelocity(armLeft, { x: 0, y: -3 }) | |
Matter.Body.setVelocity(armRight, { x: 0, y: -3 }) | |
} | |
} | |
// Facial expression | |
if(cup) { | |
distanceToCup = Math.sqrt(Math.pow(marshmallow.body.position.x - cup.body.position.x, 2) + Math.pow(marshmallow.body.position.y - cup.body.position.y, 2)); | |
} else { | |
distanceToCup = 1000; | |
} | |
if(distanceToCup <= firstAnimation.max && distanceToCup >= firstAnimation.min) { | |
firstAnimation.percent = (distanceToCup - firstAnimation.min) / (firstAnimation.max - firstAnimation.min); | |
} | |
if(distanceToCup < secondAnimation.max && distanceToCup >= secondAnimation.min) { | |
secondAnimation.percent = ((distanceToCup - secondAnimation.min) / (secondAnimation.max - secondAnimation.min) - 1) * -1; | |
} | |
if(distanceToCup < thirdAnimation.max && distanceToCup >= thirdAnimation.min) { | |
thirdAnimation.percent = ((distanceToCup - thirdAnimation.min) / (thirdAnimation.max - thirdAnimation.min) - 1) * -1; | |
} | |
if(distanceToCup < secondAnimation.max) { | |
firstAnimation.percent = 0; | |
} | |
if(distanceToCup > firstAnimation.max) { | |
firstAnimation.percent = 1; | |
secondAnimation.percent = 0; | |
} | |
// Marshmallow eye left | |
push(); | |
translate(marshmallow.body.position.x, marshmallow.body.position.y); | |
strokeWeight(3); | |
noFill(); | |
rotate(marshmallow.body.angle); | |
bezier( | |
-20, -5 + (secondAnimation.percent * 5) + (secondAnimation.percent * -4), | |
-20, -5 + (firstAnimation.percent * -7) + (secondAnimation.percent * 5) + (secondAnimation.percent * -4), | |
-10, -5 + (firstAnimation.percent * -7) + (secondAnimation.percent * -4), | |
-10, -5 + (secondAnimation.percent * -4) | |
); | |
pop(); | |
// Marshmallow eye right | |
// The second parameter, (secondAnimation.percent * -4), is to move the item up when the animation happens | |
push(); | |
translate(marshmallow.body.position.x, marshmallow.body.position.y); | |
strokeWeight(3); | |
noFill(); | |
rotate(marshmallow.body.angle); | |
bezier( | |
20, -5 + (secondAnimation.percent * 5) + (secondAnimation.percent * -4), | |
20, -5 + (firstAnimation.percent * -7) + (secondAnimation.percent * 5) + (secondAnimation.percent * -4), | |
10, -5 + (firstAnimation.percent * -7) + (secondAnimation.percent * -4), | |
10, -5 + (secondAnimation.percent * -4) | |
); | |
pop(); | |
// Marshmallow mouth | |
push(); | |
stroke('#000'); | |
strokeJoin(ROUND); | |
strokeWeight(2); | |
fill('black'); | |
translate(marshmallow.body.position.x, marshmallow.body.position.y); | |
rotate(marshmallow.body.angle); | |
arc(0, 12 + (thirdAnimation.percent * 5), 16, firstAnimation.percent * 14, 0, 3.14, CHORD); | |
arc(0, 12 + (thirdAnimation.percent * 5), 16, thirdAnimation.percent * 14, 3.14, 0, CHORD); | |
pop(); | |
// --- | |
// Cup | |
// --- | |
if(cup) { | |
push(); | |
noStroke(); | |
fill('#fee096'); | |
translate(cup.body.position.x, cup.body.position.y); | |
rect(0, 60, cup.w + 20, cup.h + 100); | |
pop(); | |
push(); | |
translate(floor.body.position.x, floor.body.position.y); | |
image(floorImg, floor.w/2 * -1, floor.h/2 * -1, floor.w, floor.h); | |
pop(); | |
push(); | |
translate(cup.body.position.x, cup.body.position.y); | |
image(cupImg, (cup.w/2 * -1) - 10, cup.h/2 * -1, cup.w + 20, cup.h); | |
pop(); | |
push(); | |
noFill(); | |
noStroke(); | |
translate(cupHandle.body.position.x, cupHandle.body.position.y); | |
image(cupHandleImg, cupHandle.w/2 * -1, cupHandle.h/2 * -1, cupHandle.w, cupHandle.h); | |
pop(); | |
// Outer eye Left | |
push(); | |
noStroke(); | |
fill('white'); | |
translate(cup.body.position.x - 76.5, cup.body.position.y - 5.5); | |
ellipse(0, 0, 34); | |
rotate(thirdAnimation.percent * 1.3); | |
stroke('#812d29'); | |
strokeWeight(3.5); | |
line(-24, 2, 2, -24); | |
pop(); | |
// Outer eye right | |
push(); | |
noStroke(); | |
fill('white'); | |
translate(cup.body.position.x + 76.5, cup.body.position.y - 5.5); | |
ellipse(0, 0, 34); | |
rotate(thirdAnimation.percent * -1.3); | |
stroke('#812d29'); | |
strokeWeight(3.5); | |
line(24, -2, -2, -24); | |
pop(); | |
// Cheek right | |
push(); | |
noStroke(); | |
fill('#f6554f'); | |
translate(cup.body.position.x + 76.5, cup.body.position.y); | |
ellipse(0, 10 + thirdAnimation.percent * 3, 34, 10); | |
pop(); | |
// Cheek left | |
push(); | |
noStroke(); | |
fill('#ff635b'); | |
translate(cup.body.position.x - 76.5, cup.body.position.y); | |
ellipse(0, 10 + thirdAnimation.percent * 3, 34, 10); | |
pop(); | |
// Blush left | |
push(); | |
noStroke(); | |
fill('#ff847e'); | |
translate(cup.body.position.x - 76.5, cup.body.position.y); | |
ellipse(-20, 18.5, 18.5, 11); | |
pop(); | |
// Blush right | |
push(); | |
noStroke(); | |
fill('#ff635b'); | |
translate(cup.body.position.x + 76.5, cup.body.position.y); | |
ellipse(20, 18.5, 18.5, 11); | |
pop(); | |
// Inner eyes | |
push(); | |
noStroke(); | |
fill('black'); | |
translate(cup.body.position.x, cup.body.position.y); | |
ellipse( | |
-76.5 + (marshmallow.body.position.x / width - 0.5) * 10, | |
-7 + (marshmallow.body.position.y / height - 0.5) * 10, | |
9.5 | |
); | |
ellipse( | |
76.5 + (marshmallow.body.position.x / width - 0.5) * 10, | |
-7 + (marshmallow.body.position.y / height - 0.5) * 10, | |
9.5 | |
); | |
pop(); | |
// Cup mouth | |
push(); | |
stroke('#812d29'); | |
strokeJoin(ROUND); | |
strokeWeight(2); | |
fill('#812d29'); | |
translate(cup.body.position.x, cup.body.position.y); | |
rotate(cup.body.angle); | |
arc(0, -10 + (thirdAnimation.percent * 18), 46, firstAnimation.percent * 44, 0, 3.14, CHORD); | |
arc(0, -10 + (thirdAnimation.percent * 18), 46, thirdAnimation.percent * 44, 3.14, 0, CHORD); | |
pop(); | |
} | |
} |
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.11/p5.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.12.0/matter.min.js"></script> |
/* apply a natural box layout model to all elements, but allowing components to change */ | |
html { | |
box-sizing: border-box; | |
} | |
*, | |
*:before, | |
*:after { | |
box-sizing: inherit; | |
} | |
body { | |
background: #fee096; | |
} | |
canvas { | |
display: block; | |
width: 100%; | |
height: 100%; | |
} | |
.height-warning { | |
width: 100%; | |
height: 100%; | |
position: absolute; | |
top: 0; | |
left: 0; | |
background: #fee096 url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/49240/height.png) center no-repeat; | |
background-size: auto 40%; | |
opacity: 0; | |
transition: opacity 0.4s; | |
pointer-events: none; | |
z-index: 10; | |
} | |
@media(max-height: 499px) { | |
.height-warning { | |
opacity: 1; | |
} | |
} | |
.instructions { | |
display: none; | |
line-height: 26px; | |
} | |
.instructions a { | |
color: #7f6e4a; | |
} | |
@media(min-width: 768px) { | |
.instructions { | |
z-index: 5; | |
position: absolute; | |
top: 20px; | |
left: 20px; | |
color: #b79e6a; | |
display: block; | |
font-family: 'Open Sans', sans-serif; | |
font-weight: 300; | |
font-size: 13px; | |
letter-spacing: 0.010em; | |
} | |
} |