document.body.addEventListener('touchmove', e => {e.preventDefault()}, {passive: false});
document.body.style.WebkitUserSelect = 'none';
document.body.style.userSelect = 'none';
woodImg = loadImage('wood.jpg');
rayuImg = loadImage('rayu.png');
hashiImg = loadImage('hashi.png');
createCanvas(windowWidth, windowHeight);
rayuBtn = createButton('ラー油');
hashiBtn = createButton('箸');
rayuBtn.position(20, 20);
hashiBtn.position(20, 70);
rayuBtn.style('font-size: 16px');
hashiBtn.style('font-size: 16px');
rayuBtn.mouseClicked(() => { blocking = true; cursor = 'rayu'; window.setTimeout(() => {blocking = false}, 10); });
hashiBtn.mouseClicked(() => { blocking = true; cursor = 'hashi'; window.setTimeout(() => {blocking = false}, 10); });
rayuBtn.mousePressed(() => { blocking = true; });
hashiBtn.mousePressed(() => { blocking = true; });
image(woodImg, 0, 0, width, height);
for(let i = rayus.length - 1; i >= 0; i--) {
if (rayus[i] === undefined) {
for(let j = i + 1; j < rayus.length; j++) {
if (rayus[i].size / 2 + rayus[j].size / 2 - rayus[i].position.dist(rayus[j].position) > 0) {
let relativeVelocity = p5.Vector.sub(rayus[i].velocity, rayus[j].velocity);
let relativePosition = p5.Vector.sub(rayus[i].position, rayus[j].position);
if (project(relativePosition, relativeVelocity).mag() < 0.6) {
validateRayuCross(rayus[i], rayus[j]);
let Mi = (rayus[i].size / 2)**2;
let Mj = (rayus[j].size / 2)**2;
let projectedI = project(relativePosition, rayus[i].velocity);
let projectedJ = project(relativePosition, rayus[j].velocity);
let Vi = fixedMag(relativePosition, projectedI);
let Vj = fixedMag(relativePosition, projectedJ);
let VI = (Mi*Vi+Mj*(Vj+k*(Vi-Vj)))/(Mi+Mj);
rayus[i].velocity.add(multMag(projectedI, -1)).add(relativePosition.copy().setMag(VI));
rayus[j].velocity.add(multMag(projectedJ, -1)).add(relativePosition.copy().setMag(VJ));
let newSize = sqrt((rayus[i].size / 2)**2 + (rayus[j].size / 2)**2) * 2;
let newPosVec = rayus[i].position.mult(rayus[i].size / 2).add(rayus[j].position.mult(rayus[j].size / 2))
.div(rayus[i].size / 2 + rayus[j].size / 2);
validatePos(newPosVec, newSize);
let newVelocity = rayus[i].velocity.mult(rayus[i].size).add(rayus[j].velocity.mult(rayus[j].size)).div(newSize);
rayus[i].position = newPosVec;
rayus[i].velocity = newVelocity;
rayus[i].radiuses = new Array(VERTEX_NUM).fill(newSize / 2);
image(rayuImg, mouseX + 10, mouseY - 120, rayuImg.width * 0.2, rayuImg.height * 0.2);
image(hashiImg, mouseX - 7, mouseY - 460, hashiImg.width * 0.2, hashiImg.height * 0.2);
image(hashiImg, mouseX, mouseY - 490, hashiImg.width * 0.2, hashiImg.height * 0.2);
function multMag(vec, count) {
return vec.copy().setMag(vec.mag() * count);
function addMag(vec, px) {
return vec.copy().setMag(vec.mag() + px);
function project(baseVec, targetVec) {
return baseVec.copy().setMag((p5.Vector.dot(baseVec, targetVec) / baseVec.mag() ** 2) * baseVec.mag());
function fixedMag(baseVec, targetVec) {
let angle = baseVec.angleBetween(targetVec);
if(angle > -HALF_PI && angle < HALF_PI) {
function validatePos(posVec, size) {
let diff = (posVec.dist(createVector(width / 2, height / 2)) + size / 2) - (min(width, height) * 0.35);
posVec.add(createVector(width / 2, height / 2).sub(posVec).setMag(diff));
function validateRayuCross(rayu1, rayu2) {
let subVec = p5.Vector.sub(rayu1.position, rayu2.position);
let distance = subVec.mag();
let radiusTotal = rayu1.size / 2 + rayu2.size / 2;
let diff = radiusTotal - distance;
rayu1.position.add(subVec.setMag(diff * (rayu2.size / (rayu1.size + rayu2.size))));
rayu2.position.add(subVec.setMag(-diff * (rayu1.size / (rayu1.size + rayu2.size))));
function mouseClicked() {
if(!blocking && cursor === 'rayu') {
rayus.push(new Rayu(createVector(mouseX, mouseY),
createVector(random(-1, 1), random(-1, 1)),
for(let i = 0; i < 20; i++) {
circle(width / 2 - 10, height / 2 + 10, min(width, height) * 0.9 * (1 + i * 0.02));
circle(width / 2, height / 2, min(width, height) * 0.9);
for(let i = 0; i < 20; i++) {
circle(width / 2 + 5, height / 2 - 5, min(width, height) * 0.8 * (1 - i * 0.02));
fill(color(189, 114, 29, 100));
for(let i = 0; i < 15; i++) {
circle(width / 2, height / 2, min(width, height) * 0.7 * (1 - i * 0.015));
fill(color(154, 50, 19, 40));
for(let i = 0; i < 20; i++) {
circle(width / 2, height / 2, min(width, height) * 0.65 * (1 - i * 0.015));
stroke(color(255, 255, 255, 150));
arc(width / 2, height / 2, min(width, height) * 0.67, min(width, height) * 0.67,
1.5 - noise(1 + frameCount * 0.01) * 0.1, 2.5 - noise(2 + frameCount * 0.01) * 0.1);
arc(width / 2, height / 2, min(width, height) * 0.67, min(width, height) * 0.67,
3 - noise(2.1 + frameCount * 0.01) * 0.1, 3.5 - noise(3 + frameCount * 0.01) * 0.1);
stroke(color(250, 250, 250, 50));
strokeWeight(min(width, height) * 0.25);
arc(width / 2, height / 2, min(width, height) * 0.4, min(width, height) * 0.4, 5, 6.2);
constructor(position, velocity, size) {
this.position = position;
this.velocity = velocity;
this.radiuses = new Array(VERTEX_NUM).fill((size / 2) * 0.5);
let mouseVec = createVector(mouseX, mouseY);
if (cursor === 'hashi' && mouseIsPressed && (mouseVec.dist(this.position) < this.size / 2) && !this.catching) {
let catchingRadiusBuffer = Math.min(30, Math.max(7, (this.size / 2) * 0.5));
if(cursor === 'hashi' && mouseIsPressed && (mouseVec.dist(this.position) < this.size / 2 + catchingRadiusBuffer) && this.catching) {
let subVec = mouseVec.copy().sub(this.position);
this.velocity.add(subVec.setMag(Math.min(subVec.mag(), 0.01))).mult(0.99);
this.radiuses = convex(this.radiuses, this.size, addMag(mouseVec.copy().sub(this.position), mouseMargin));
for (let i = 0; i < rayus.length; i++) {
if (rayu.position.copy().sub(mouseVec).mag() < rayu.size / 2 + mouseMargin) {
let newSize = sqrt((this.size / 2)**2 + (rayu.size / 2)**2) * 2;
let newPosVec = this.position.mult(this.size / 2).add(rayu.position.mult(rayu.size / 2))
.div(this.size / 2 + rayu.size / 2);
validatePos(newPosVec, newSize);
let newVelocity = this.velocity.mult(this.size).add(rayu.velocity.mult(rayu.size)).div(newSize);
this.position = newPosVec;
this.velocity = newVelocity;
} else if (mouseIsPressed && this.catching) {
let mouseOutVelocity = mouseVec.copy().sub(createVector(pmouseX, pmouseY));
let newSize = max(this.size / 2 - mouseOutVelocity.mag(), this.size / 4);
this.size = sqrt((this.size / 2) ** 2 - (newSize / 2) ** 2) * 2;
rayus.push(new Rayu(createVector(this.position.x, this.position.y)
.add(mouseVec.copy().sub(this.position).setMag(this.size / 2 + newSize / 2 + catchingRadiusBuffer)),
mouseOutVelocity.mult(0.1), newSize));
} else if (!mouseIsPressed && this.catching) {
this.position.add(this.velocity);
this.velocity.mult(0.993);
if(this.position.dist(createVector(width / 2, height / 2)) > min(width, height) * 0.35 - this.size / 2) {
this.velocity = multMag(this.velocity.reflect(createVector(width / 2, height / 2).sub(this.position)), 0.5);
validatePos(this.position, this.size);
fill(color(117, 33, 4, 50));
let curveIndexes = [VERTEX_NUM - 1, ...Array(VERTEX_NUM).keys(), 0, 1];
curveIndexes.forEach(i => {
let r = this.radiuses[i];
let rad = (TWO_PI / this.radiuses.length) * i;
curveVertex(this.position.x - 4 + cos(rad) * r, this.position.y + 4 + sin(rad) * r);
fill(color(249, 70, 5, 60));
stroke(color(248, 161, 63, 150));
curveIndexes.forEach(i => {
let r = this.radiuses[i];
let rad = (TWO_PI / this.radiuses.length) * i;
curveVertex(this.position.x + cos(rad) * r, this.position.y + sin(rad) * r);
this.radiuses = this.radiuses.map(r => {
return r - (r - this.size / 2) / 10;
function convex(radiuses, size, targetVec) {
let targetAngle = targetVec.heading();
let targetRadius = targetVec.mag();
return radiuses.map((r, i) => {
let angle = (TWO_PI / radiuses.length) * i;
let angleBetween = Math.abs(calcAngleBetween(angle, targetAngle));
let allowedAngle = HALF_PI * 0.7;
let contribution = Math.max(0, (allowedAngle - angleBetween) / allowedAngle);
return Math.max(r, size / 2 + (targetRadius - size / 2) * contribution);
function calcAngleBetween(angle1, angle2) {
let sub = (angle2 - angle1) % TWO_PI;
return sub < -PI ? sub + TWO_PI : sub;
return sub > PI ? sub - TWO_PI : sub;