So I settled on creating a physics based string lights effect. I found a great little 2D rigid-body physics engine in vanilla Javascript. This allowed me to create a rigid chain of small circular bodies, the small bodies would be set to not render. Meaning, it was a rigid line, connected to an invisible circle, then to another rigid line, and so on. Then at each invisible circle, I create a new, larger circle, connected (via a constraint, visualized as a thicker black line) to the invisible circle. Then I randomly positioned those larger, visible circles so that when the page was loaded, it would have a nice, falling effect.
function initString() {
var groupId = Body.nextGroupId();
var stack = Composites.stack(0, -10, 10, 1, 0, 0, function(x, y, column, row) {
var isStatic = false;
x = ((column/9) * $string.width());
y = (column/9) * 200;
if (column == 0) {
x = 50;
y = -10;
isStatic = true;
}
if (column == 9) {
isStatic = true;
}
return Bodies.circle(x, y, 5, {
groupId: groupId,
isStatic: isStatic,
frictionAir: 0.3,
render: {
visible: false,
}
});
});
string = Composites.chain(stack, 0, 0, 0, 0, {
groupId: groupId,
stiffness: 0.9,
render: {
strokeStyle: "#231f20",
lineWidth: 3
}
});
// add string
World.add(engine.world, string);
}
Now. I could have created more points along the chain, giving the rope a more fluid feel, but I hedged away from this, due to the fact that in my cross browser testing, I noticed that some browsers would slow down dramatically due to the added physics calculations.
Now I wanted to give the user a hint that they could interact with it. I came up with the idea of doing a wind effect. The problem was Matter did not have any sort of additional force effects. So I decided to shift gravity. At a random interval (between 0 and 3 seconds), and at a random duration (between 0 and 3 seconds), and at a random intensity (between 0.25 and 0.5), and at a random duration (between 10 and 20 frames), and in a random direction (half the length), I would ease the gravity from straight down, to the chosen direction, imitating a gust of wind.
function initWind() {
wind = window.setTimeout(function() {
gust();
}, 2000);
function gust() {
var gravity = 0;
var i = 0;
var intensity = 0.25 - (Math.random() * 0.5);
var length = Math.ceil(Math.random() * 20) + 10;
var halflength = length/2;
guster = window.setInterval(function() {
if (i <= halflength) {
gravity = ease(i, 0, intensity*halflength, halflength);
} else {
gravity = ease(i - halflength, intensity*halflength, 0 - intensity*halflength, halflength);
}
if (i == length) {
window.clearInterval(guster);
wind = window.setTimeout(function() {
gust();
}, Math.random() * 3000);
}
i++;
engine.world.gravity.x = gravity;
}, 100);
}
}
Then the last part, creating interaction. For desktop I created an invisible circle that would simply track with the mouse, and repel other circles.
function initMouse() {
var mouser = Matter.MouseConstraint.create(engine, {
constraint: {
render: {
visible: false
}
}
});
var groupId2 = Body.nextGroupId();
var mover = Bodies.circle(0, 0, 10, {
groupId: groupId2,
isStatic: true,
render: {
visible: false,
}
});
var mover2 = Bodies.circle(0, 0, 20, {
groupId: groupId2,
isStatic: false,
render: {
visible: false,
}
});
var mouseConstraint = Constraint.create({
bodyA: mover,
bodyB: mover2,
length: 0,
render: {
visible: false,
}
});
Matter.Events.on(engine, 'mousemove', function(event) {
mover.position = {x: event.mouse.position.x, y: event.mouse.position.y};
});
World.add(engine.world, [mouser, mover, mover2, mouseConstraint]);
}
And for Mobile I removed the wind, and instead opted to shift the gravity in the direction the user tilted their phone.
function initMobile() {
$("#string").css("pointer-events", "none");
window.addEventListener('deviceorientation', updateGravity, true);
function updateGravity() {
oldorient = orientation;
orientation = window.orientation,
gravity = engine.world.gravity;
if (orientation != oldorient)
resize();
if (orientation === 0) {
gravity.x = Common.clamp(event.gamma, -90, 90) / 90;
gravity.y = Common.clamp(event.beta, -90, 90) / 90;
} else if (orientation === 180) {
gravity.x = Common.clamp(event.gamma, -90, 90) / 90;
gravity.y = Common.clamp(-event.beta, -90, 90) / 90;
} else if (orientation === 90) {
gravity.x = Common.clamp(event.beta, -90, 90) / 90;
gravity.y = Common.clamp(-event.gamma, -90, 90) / 90;
} else if (orientation === -90) {
gravity.x = Common.clamp(-event.beta, -90, 90) / 90;
gravity.y = Common.clamp(event.gamma, -90, 90) / 90;
}
};
}