213 lines
5.5 KiB
JavaScript
213 lines
5.5 KiB
JavaScript
// toybox.js by Dante Scanline 2022
|
||
// UNIVERSAL PUBLIC DOMAIN LICENSE - This code and everything else in the universe is in the public domain
|
||
|
||
/// ------------
|
||
|
||
// CONFIGURATION VARIABLES, please change these!
|
||
const BUBBLE_IN = true; //makes the toys come in one at a time
|
||
const BUBBLE_DELAY = 250; // if bubbling in, how much time in miliseconds between each new toy. bigger number is slowe
|
||
|
||
const PUSH_SPEED = 3; // base amount that toys push each other, bigger number means quicker stronger pushing
|
||
const RANDOM_START_OFFSET = 200; // random distance from center in pixels where toys are spawned inside off
|
||
|
||
/// ------------
|
||
|
||
let toyContainer;
|
||
let items = []; // array of dom elements
|
||
|
||
// Fisher–Yates knuth shuffle
|
||
function shuffle(array) {
|
||
let currentIndex = array.length,
|
||
randomIndex;
|
||
|
||
// While there remain elements to shuffle.
|
||
while (currentIndex != 0) {
|
||
// Pick a remaining element.
|
||
randomIndex = Math.floor(Math.random() * currentIndex);
|
||
currentIndex--;
|
||
|
||
// And swap it with the current element.
|
||
[array[currentIndex], array[randomIndex]] = [
|
||
array[randomIndex],
|
||
array[currentIndex],
|
||
];
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// reenable items sequentially for bubbling
|
||
function reEnable(index) {
|
||
if (index > items.length - 1) {
|
||
return;
|
||
}
|
||
|
||
items[index].domElement.style.display = "initial";
|
||
|
||
setTimeout(() => {
|
||
reEnable(index + 1);
|
||
}, BUBBLE_DELAY);
|
||
}
|
||
|
||
// the object, with width height from DOM and then a position we set :)
|
||
class Toy {
|
||
constructor(domElement) {
|
||
this.domElement = domElement;
|
||
this.domElement.style.position = "absolute";
|
||
|
||
let rect = toyContainer.getBoundingClientRect();
|
||
this.x = rect.width / 2 + 100 - Math.random() * 200;
|
||
this.y = rect.height / 2 + 100 - Math.random() * 200;
|
||
|
||
this.visualX = this.x;
|
||
this.visualY = this.y;
|
||
|
||
this.updateSize();
|
||
this.updatePosition();
|
||
}
|
||
|
||
updatePosition() {
|
||
this.visualX += (this.x - this.visualX) * 0.1;
|
||
this.visualY += (this.y - this.visualY) * 0.1;
|
||
this.domElement.style.left = `${this.x}px`;
|
||
this.domElement.style.top = `${this.y}px`;
|
||
}
|
||
|
||
updateSize() {
|
||
this.width = this.domElement.clientWidth;
|
||
if (this.width == 0 || this.width == undefined) {
|
||
console.error("no size for element yet");
|
||
}
|
||
this.height = this.domElement.clientHeight;
|
||
if (this.width < 65 || this.height < 65) {
|
||
this.domElement.style["z-index"] = "20";
|
||
}
|
||
this.domElement.style.marginLeft = `${-this.width / 2}px`;
|
||
this.domElement.style.marginTop = `${-this.height / 2}px`;
|
||
// this.domElement.style.transform = `translate(${-this.width / 2}px, ${-this.height / 2}px)`
|
||
}
|
||
|
||
pointOverlaps(xx, yy) {
|
||
return (
|
||
xx > this.x - this.width / 2 &&
|
||
xx < this.x + this.width / 2 &&
|
||
yy > this.y - this.height / 2 &&
|
||
yy < this.y + this.height / 2
|
||
);
|
||
}
|
||
|
||
toyOverlaps(otherToy) {
|
||
const corners = [
|
||
[otherToy.x - otherToy.width / 2, otherToy.y - otherToy.height / 2],
|
||
[otherToy.x + otherToy.width / 2, otherToy.y - otherToy.height / 2],
|
||
[
|
||
otherToy.x - otherToy.height / 2,
|
||
otherToy.y + otherToy.height / 2,
|
||
],
|
||
[otherToy.x + otherToy.width / 2, otherToy.y + otherToy.height / 2],
|
||
];
|
||
|
||
// check if the other toys corners are inside our rect
|
||
for (let i = 0; i < corners.length; i++) {
|
||
let pair = corners[i];
|
||
if (this.pointOverlaps(pair[0], pair[1])) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// edge case, where we are fit entirely inside the other rect, so check one of our points on them
|
||
return otherToy.pointOverlaps(this.x, this.y);
|
||
}
|
||
|
||
getPushed(otherToy) {
|
||
let distance = PUSH_SPEED;
|
||
|
||
let centerX = this.x;
|
||
let centerY = this.y;
|
||
|
||
let otherCenterX = otherToy.x;
|
||
let otherCenterY = otherToy.y;
|
||
|
||
let angle = Math.atan2(centerY - otherCenterY, centerX - otherCenterX);
|
||
|
||
let finalStrength = distance;
|
||
|
||
// if the toy pushing us is roughly bigger than us, get pushed more to help offset some biasing
|
||
if (
|
||
otherToy.width + otherToy.height >
|
||
(this.width + this.height) * 1.3
|
||
) {
|
||
finalStrength *= 3;
|
||
}
|
||
|
||
this.x += Math.cos(angle) * finalStrength;
|
||
this.y += Math.sin(angle) * finalStrength;
|
||
this.updatePosition();
|
||
}
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", init);
|
||
|
||
function init() {
|
||
toyContainer = document.querySelector("#toybox");
|
||
let nodes = Array.from(toyContainer.childNodes);
|
||
nodes = nodes.filter((node) => node.nodeType == 1);
|
||
nodes = shuffle(nodes); // help prevent biases
|
||
|
||
for (let i = 0; i < nodes.length; i++) {
|
||
items.push(new Toy(nodes[i]));
|
||
|
||
if (BUBBLE_IN) {
|
||
if (i > 3) {
|
||
items[i].domElement.style.display = "none";
|
||
}
|
||
}
|
||
}
|
||
|
||
if (BUBBLE_IN) {
|
||
reEnable(3);
|
||
}
|
||
requestAnimationFrame(main);
|
||
}
|
||
|
||
function main() {
|
||
items.map((item) => item.updateSize());
|
||
|
||
for (let i = 0; i < items.length; i++) {
|
||
const itemA = items[i];
|
||
|
||
for (let k = 0; k < items.length; k++) {
|
||
if (i == k) continue;
|
||
|
||
const itemB = items[k];
|
||
if (itemA.toyOverlaps(itemB)) {
|
||
itemB.getPushed(itemA);
|
||
}
|
||
}
|
||
}
|
||
|
||
let rect = toyContainer.getBoundingClientRect();
|
||
|
||
for (let i = 0; i < items.length; i++) {
|
||
const itemA = items[i];
|
||
if (itemA.x - itemA.width / 2 < 0) {
|
||
itemA.x += 10;
|
||
itemA.updatePosition();
|
||
}
|
||
if (itemA.y - itemA.height / 2 < 0) {
|
||
itemA.y += 10;
|
||
itemA.updatePosition();
|
||
}
|
||
if (itemA.x + itemA.width / 2 > rect.width) {
|
||
itemA.x -= 10;
|
||
itemA.updatePosition();
|
||
}
|
||
if (itemA.y + itemA.height / 2 > rect.height) {
|
||
itemA.y -= 5;
|
||
itemA.updatePosition();
|
||
}
|
||
}
|
||
|
||
requestAnimationFrame(main);
|
||
}
|