Skip to content

Instantly share code, notes, and snippets.

@rampol
Created June 23, 2018 15:19
Show Gist options
  • Save rampol/ec2f992e9860bab4aa4ce3738d62eeaa to your computer and use it in GitHub Desktop.
Save rampol/ec2f992e9860bab4aa4ce3738d62eeaa to your computer and use it in GitHub Desktop.
Hot and Sticky

Hot and Sticky

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.

License.

<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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment