How do i reach the path elements inside of a SVG and create Vertices to use in Matter.js?

110 views Asked by At

When starting my new React + Vite and Matter.js project, I encountered a problem pretty soon in the process.

I am making a front page for a portfolio. SVGs will be rendered as Matter.js objects on this page, being affected by gravity and bouncing against each other. The Matter.js code is written inside the Logo.jsx component, which in turn is rendered inside the App.jsx component.

App.jsx

import { useEffect, useRef, useState } from "react";
import { Routes, Route } from "react-router-dom";

// Component imports
import Logo from "./Logo";
import Navbar from "./Navbar";
import Projects from "./Projects";
import Info from "./info";
import Contact from "./Contact";
import About from "./About";
import Testmatter from "./Testmatter";

const App = () => {
  const matterContainerRef = useRef(null);
  const [logoVisible, setLogoVisible] = useState(true);

  // The logo is not visible when a link is clicked
  const handleLinkClick = () => {
    setLogoVisible(false);
  };

  // Listen for route changes
  useEffect(() => {
    if (location.pathname === "/") {
      setLogoVisible(true); // Show the logo when on the homepage
    } else {
      setLogoVisible(false); // Hide the logo for other routes
    }
  }, [location.pathname]);

  return (
    <div className="app">
      <Navbar onLinkClick={handleLinkClick} matterContainerRef={matterContainerRef} />
      <div>
        <Routes>
          <Route path="/projects" element={<Projects />} />
          <Route path="/info" element={<Info />} />
          <Route path="/contact" element={<Contact />} />
          <Route path="/about" element={<About />} />
          <Route path="/testmatter" element={<Testmatter matterContainerRef={matterContainerRef} />} />
        </Routes>
      </div>
      {logoVisible && <Logo matterContainerRef={matterContainerRef} logoVisible={logoVisible} />}
    </div>
  );
};

export default App;

I have a couple of different SVGs, I call them 'letters'. They are located in my public folder, hence why I can reach them only using 'Z.svg'. As I already stated, the code necessary for creating the Matter.js canvas and its objects will all be inside of the Logo.jsx component.

Logo.jsx

import React, { useEffect, useRef, useState } from "react";
import { Link } from "react-router-dom";
import Matter from "matter-js";
import LogoCSS from "../css/logo.module.css";
import "pathseg";

const Logo = ({ matterContainerRef }) => {
  useEffect(() => {
    // Module aliases
    let Engine = Matter.Engine,
      Render = Matter.Render,
      Runner = Matter.Runner,
      Bodies = Matter.Bodies,
      Composite = Matter.Composite,
      World = Matter.World,
      Svg = Matter.Svg,
      Vector = Matter.Vector,
      Vertices = Matter.Vertices;

    let path = document.getElementById("svg_path");

    // Window dimensions, width declareances
    const windowWidth = matterContainerRef.current.clientWidth;
    const windowHeight = matterContainerRef.current.clientHeight;
    const desiredWidth = 800;
    const desiredHeight = 950;
    let scaledImageWidth, scaledImageHeight;

    // Declare letterBody, array and sizes
    let letterBody;
    const letterBodies = [];

    // Create an engine
    const engine = Engine.create();
    engine.gravity.y = -0.3;

    // Create a renderer
    const render = Render.create({
      element: matterContainerRef.current,
      engine: engine,
      options: {
        width: matterContainerRef.current.clientWidth,
        height: matterContainerRef.current.clientHeight,
        wireframes: false,
        background: "white",
        pixelRatio: 2,
      },
    });

    const canvasElement = render.canvas;
    if (canvasElement) {
      canvasElement.classList.add("canvas");
    }

    // Define an array of letters with their properties
    const letters = [
      { text: "Z", sprite: "Z.svg", x: 230, y: 900, angle: 0 },
      { text: "A", sprite: "A.svg", x: 230, y: 900, angle: 0 },
      { text: "A", sprite: "A2.svg", x: 230, y: 900, angle: 0 },
      { text: "R", sprite: "R.svg", x: 230, y: 900, angle: 0 },
      { text: "<3", sprite: "<3.svg", x: 700, y: 1000, angle: 0 },
    ];

    // Set the desired horizontal spacing between letters
    const letterSpacing = 320;

    // Set the x value for the letters
    let x = 230;

    for (const letter of letters) {
      // Load the image for the letter
      const letterImage = new Image();
      letterImage.src = `/${letter.sprite}`;

      letterImage.onload = () => {
        // Retrieve the original letter image dimensions
        const letterImageWidth = letterImage.width;
        const letterImageHeight = letterImage.height;

        // Calculate scaling factors for both width and height
        const scaleX = desiredWidth / letterImageWidth;
        const scaleY = desiredHeight / letterImageHeight;

        // Update the scaleX and scaleY properties of the current letter object
        letter.scaleX = scaleX;
        letter.scaleY = scaleY;

        // Create vertices for the body based on the scaled image dimensions
        const vertices = Vertices.fromPath(`M0 0 L${letterImageWidth} 0 L${letterImageWidth} ${letterImageHeight} L0 ${letterImageHeight}`);

        letterBody = Bodies.fromVertices(x, letter.y, vertices, {
          render: {
            sprite: {
              texture: letterImage.src,
              xScale: scaleX,
              yScale: scaleY,
            },
          },
          className: LogoCSS["matter-box"],
          frictionAir: 0.03,
        });

        // Push the created letterBody into the array
        letterBodies.push(letterBody);
        x += letterSpacing;

        World.add(engine.world, [letterBody]);
      };
    }

    //  Create a constraint to allow dragging
    const mouseConstraint = Matter.MouseConstraint.create(engine, {
      element: matterContainerRef.current,
      constraint: {
        stiffness: 1,
        render: {
          visible: false,
        },
      },
    });
    World.add(engine.world, mouseConstraint);

    // Create the ceiling and boundaries
    const boundaryOptions = {
      isStatic: true,
      render: {
        visible: false,
      },
    };

    const ceiling = Bodies.rectangle(windowWidth / 2, 100, windowWidth, 10, boundaryOptions);
    const leftWall = Bodies.rectangle(0, windowHeight / 2, 10, windowHeight, boundaryOptions);
    const rightWall = Bodies.rectangle(windowWidth, windowHeight / 2, 10, windowHeight, boundaryOptions);

    // Add the walls and ceiling to the world
    World.add(engine.world, [ceiling, leftWall, rightWall]);

    // Run the renderer
    Render.run(render);

    // Create a runner
    const runner = Runner.create();

    // Run the engine
    Matter.Runner.run(runner, engine);

    // Teleport each letter back to its original position if it goes out of bounds
    Matter.Events.on(engine, "afterUpdate", () => {
      for (const letterBody of letterBodies) {
        const { x, y } = letterBody.position;

        if (x < -10 || x > windowWidth || y > windowHeight * 1.35 || y < -250) {
          let randomNumber = Math.floor(Math.random() * (1000 - 200 + 1)) + 200;
          Matter.Body.setPosition(letterBody, { x: randomNumber, y: 1000 });
          Matter.Body.setVelocity(letterBody, { x: 0, y: 0 });
        }

        // Check if the letter has crossed the y threshold
        if (y > 800) {
          // Calculate the force components (x and y) based on the angle and magnitude
          const randomAngle = 2.7;
          let angleOffset = 1 + 1;

          const adjustedAngle = randomAngle + angleOffset;
          const randomForceMagnitude = Math.random() * 0.1;

          // Calculate the force components (x and y) based on the adjusted angle and magnitude
          const forceX = Math.cos(adjustedAngle) * randomForceMagnitude;
          const forceY = Math.sin(adjustedAngle) * randomForceMagnitude;

          // Apply the force to the letter body
          Matter.Body.applyForce(letterBody, { x, y }, { x: forceX, y: forceY });
        }
      }
    });

    // Add a resize event listener
    const handleResize = () => {
      const newWindowWidth = matterContainerRef.current.clientWidth;
      const newWindowHeight = matterContainerRef.current.clientHeight;

      // Update the boundaries with the new window dimensions
      updateBoundaries(engine, newWindowWidth, newWindowHeight);

      // // // Update the canvas size
      // render.canvas.width = newWindowWidth;
      // render.canvas.height = newWindowHeight;

      // Update the scaling of letter bodies
      for (const letter of letters) {
        const { scaleX, scaleY } = letter;
        for (const letterBody of letterBodies) {
          if (letterBody.render.sprite.texture === letter.sprite) {
            letterBody.render.sprite.xScale = scaleX;
            letterBody.render.sprite.yScale = scaleY;
          }
        }
      }
    };

    // Attach the event listener
    window.addEventListener("resize", handleResize);

    // Update all the boundaries. This gets called on resize
    const updateBoundaries = (engine, windowWidth, windowHeight) => {
      const boundaryOptions = {
        isStatic: true,
        render: {
          visible: false,
        },
      };

      const ceiling = Bodies.rectangle(windowWidth / 2, 0, windowWidth, 10, boundaryOptions);
      const leftWall = Bodies.rectangle(0, windowHeight / 2, 10, windowHeight, boundaryOptions);
      const rightWall = Bodies.rectangle(windowWidth, windowHeight / 2, 10, windowHeight, boundaryOptions);

      // Remove the existing boundaries from the world
      World.remove(engine.world, [ceiling, leftWall, rightWall]);

      // Add the updated boundaries to the world
      World.add(engine.world, [ceiling, leftWall, rightWall]);
    };

    return () => {
      // Stop the renderer and clear all eventlisteners on unmount
      Matter.Render.stop(render);
      window.removeEventListener("resize", handleResize);

      const container = matterContainerRef.current;
      if (container) {
        container.innerHTML = "";
      }
    };
  }, []);

  return <div ref={matterContainerRef} className={LogoCSS.container}></div>;
};

export default Logo;

I decided to include the entire component, since there might be a different reason for my code not being correct than the reasons I have thought of. As you can see, I am looping through the letters array and creating an image object for each one of the letter SVGs. I am using Matter.js functions to create Vertices, and using those to create new Matter objects. These are added to the engine and then rendered out by Matter.js.

Here is my problem: the vertices aren't accurate, and I can't seem to reach the path elements inside of the SVG's. I want the vertices to be accurate so that the letters' boundaries don't overlap.

I have tried the following:

  • Embedding the entire SVG into my JSX code doesn't seem to work for some reason, the SVG just won't appear in the DOM and I can't reach the <path> elements.
  • There is a function called Matter.Svg.pathToVertices(path, [sampleLength=15]) that takes in the path from the SVG as parameter. I tested out the function using different SVGs, and it results in a Polyfill being required. I honestly couldn't find a solution to this problem either, even after having to download two external libraries.
  • I have played around with the scales of the images, giving them different values and changing the way I calculate the values, but to no avail.

I think reaching the <path> elements inside of my SVGs and fixing the error the function is giving me will be the way to go. Can somebody help me with this?

0

There are 0 answers