Skip to content

How do I make circle bodies not 'clip/melt' into each other? #1332

@jeremylee34

Description

@jeremylee34

I've created a container that adds balls in it, but I want the balls not to 'melt' into each other as pictured in the attached video.

xp.mp4

Note, I checked #951 and #5 as they seem to address a similar issue but couldn't find a clear solution for my problem there.

I tried increasing position and velocity iterations but they only seem to slightly reduce clipping, and I'm worried that a high value will affect performance. I've also tried playing around with restitution, friction, frictionStatic and slop values to no success so far (although it's been trial and error as I'm new to MatterJS).

There's also a bug where the circles will occasionally fall through the floor, especially when I resize the window as it doesn't match the browser's new dimensions.

Would appreciate any help if you've faced a similar issue :)

My versions:

matter-js: 0.20.0
next: 14.2.17
node: 20.11.1
react: 18

My code:

import React, { useEffect, useRef, useState } from "react";
import Matter from "matter-js";

const STATIC_DENSITY = 15;
const PARTICLE_SIZE = 6;

interface MatterBallsType {
  particleTrigger: number
}

const VISIBLE_RES = 1280

interface ExtendedRender extends Matter.Render {
  engine: Matter.Engine;
}

export const MatterBalls = ({ particleTrigger }: MatterBallsType) => {
  const boxRef = useRef<HTMLDivElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [isVisible, setIsVisible] = useState(false);
  const [constraints, setConstraints] = useState<DOMRect>();
  const [scene, setScene] = useState<ExtendedRender>();

  const handleResize = () => {
    const boundingBox = boxRef.current?.getBoundingClientRect();
    if (boundingBox) {
      setConstraints(boundingBox);
    }
  };

  function addWalls(width: number, height: number, world: Matter.World) {
    const leftWall = Matter.Bodies.rectangle(
      -20,
      height / 2,
      40,
      height * 2,
      { 
        isStatic: true, 
        render: { fillStyle: "white" } 
      }
    );
    const rightWall = Matter.Bodies.rectangle(
      width + 20, 
      height / 2, 
      40, 
      height * 2, 
      { 
        isStatic: true, 
        render: { fillStyle: "white" } 
      }
    );
    Matter.World.add(world, [
      leftWall,
      rightWall
    ]);
  }

  function addFloor(world: Matter.World) {
    const floor = Matter.Bodies.rectangle(0, 0, 60, STATIC_DENSITY, {
      isStatic: true,
      label: "floor",
      render: {
        fillStyle: "transparent"
      }
    });
    
    Matter.World.add(world, [
      floor,
    ]);
  }

  useEffect(() => {
    setIsVisible(window.innerWidth >= VISIBLE_RES);
    
    const Engine = Matter.Engine;
    const Render = Matter.Render;
    
    const engine = Engine.create({});

    if (!boxRef.current) return;
    const { width, height } = boxRef.current.getBoundingClientRect();

    engine.positionIterations = 10;
    engine.velocityIterations = 10;

    const render = Render.create({
      element: boxRef.current,
      engine: engine,
      canvas: canvasRef.current!,
      options: {
        background: "transparent",
        wireframes: false
      }
    });

    addFloor(engine.world);
    addWalls(width, height, engine.world);

    Render.run(render);
    const runner = Matter.Runner.create();
    Matter.Runner.run(runner, engine);

    setConstraints(boxRef.current.getBoundingClientRect());
    setScene(render as ExtendedRender);

    window.addEventListener("resize", handleResize);
  }, []);

  useEffect(() => {
    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);

  useEffect(() => {
    function addBall() {
      if (!constraints || !scene) return;
      
      const { width } = constraints;
      const randomX = Math.floor(Math.random() * -width) + width;
      const randomY = Math.floor(Math.random() * -10 - 1);
      
      Matter.World.add(
        scene.engine.world,
        Matter.Bodies.circle(randomX, randomY, PARTICLE_SIZE, {
          restitution: 0,
          friction: 1,
          frictionStatic: 0,
          slop: 0,
          render: {
            fillStyle: 'white',
            sprite : {
              texture: '/White_Circle.svg',
              xScale: 0.02,
              yScale: 0.02
            }
          }
        })
      );
    }
    
    if (scene && isVisible && particleTrigger > 0) {
      addBall();
      addBall();
      addBall();
      addBall();
      addBall();
    }
  }, [particleTrigger, scene, isVisible, constraints]);

  useEffect(() => {
    if (constraints && scene) {
      const { width, height } = constraints;
  
      scene.bounds.max.x = width;
      scene.bounds.max.y = height;
      scene.options.width = width;
      scene.options.height = height;
      scene.canvas.width = width;
      scene.canvas.height = height;
  
      const floor = scene.engine.world.bodies[0];
      if (floor) {
        Matter.Body.setPosition(floor, {
          x: width / 2,
          y: height + STATIC_DENSITY / 2
        });
  
        Matter.Body.setVertices(floor, [
          { x: 0, y: height },
          { x: width, y: height },
          { x: width, y: height + STATIC_DENSITY },
          { x: 0, y: height + STATIC_DENSITY }
        ]);
      }
    }
  }, [scene, constraints]);

  return (
    <div
      ref={boxRef}
      style={{
        position: "relative",
        overflow: "hidden",
        top: 0,
        left: 0,
        width: "100%",
        height: "100%",
        zIndex: 49
      }}
      className="border-l border-border"
    >
      <canvas ref={canvasRef} />
    </div>
  );
};

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions