0.7 C
New York
Thursday, February 6, 2025

Constructing a Playful Cease-Movement Crayon Cursor in p5.js


Hey, it’s me once more—Jorge Toloza! I’m the Co-Founder and Artistic Director at DDS Studio. Right this moment, I’ve bought a enjoyable thought to share that got here to me whereas I used to be messing round with p5 Brush and sketching in my pocket book.

We’re going to create a cool stop-motion crayon cursor impact utilizing p5.brush.js—a neat assortment of capabilities for p5.js that allows you to draw on a canvas—plus slightly little bit of math magic.

Let’s get began!

The HTML Markup

<div id="canvas-container"></div>

Fairly easy, proper? We solely want a container for the p5 canvas.

CSS Kinds

#canvas-container {
    width: 100%;
    peak: 100%;
}

The identical goes for the CSS—simply set the dimensions.et the dimensions.

Our Canvas Supervisor

Right here’s the construction for our canvas class, the place we’ll deal with all of the calculations and requestAnimationFrame (RAF) calls. The plan is simple: we’ll draw a fluid polygon and create a listing of trails that comply with the cursor.

import * as brush from 'p5.brush';
import p5 from 'p5';

export default class CanvasManager {
  constructor() {
    this.width = window.innerWidth;
    this.peak = window.innerHeight;
    this.trails = [];
    this.activeTrail = null;
    this.mouse = {
      x: { c: -100, t: -100 },
      y: { c: -100, t: -100 },
      delta: { c: 0, t: 0 },
    };
    this.polygonHover = { c: 0, t: 0 };
    this.maxTrailLength = 500;

    this.t = 0;
    this.el = doc.getElementById('canvas-container');

    this.render = this.render.bind(this);
    this.sketch = this.sketch.bind(this);
    this.initBrush = this.initBrush.bind(this);
    this.resize = this.resize.bind(this);
    this.mousemove = this.mousemove.bind(this);
    this.mousedown = this.mousedown.bind(this);
    this.mouseup = this.mouseup.bind(this);

    window.addEventListener('resize', this.resize);
    doc.addEventListener('mousedown', this.mousedown);
    doc.addEventListener('mousemove', this.mousemove);
    doc.addEventListener('mouseup', this.mouseup);

    this.resize();
    this.initCanvas();
  }

  resize() {
    this.width = window.innerWidth;
    this.peak = window.innerHeight;
    this.polygon = this.initPolygon();
    if (this.app) this.app.resizeCanvas(this.width, this.peak, true);
  }
  initCanvas() {
    this.app = new p5(this.sketch, this.el);
    requestAnimationFrame(this.render);
  }
  ...

The constructor is pretty normal—we’re organising all of the properties and including some objects for linear interpolations. Right here, I’m utilizing c for present and t for goal.

Let’s begin with the polygon. I rapidly sketched a polygon in Figma, copied the vertices, and famous the dimensions of the Figma canvas.

We now have this array of factors. The plan is to create two states for the polygon: a relaxation state and a hover state, with completely different vertex positions for every. We then course of every level, normalizing the coordinates by dividing them by the grid dimension or Figma canvas dimension, making certain they vary from 0 to 1. After that, we multiply these normalized values by the canvas width and peak to make all of the coordinates relative to our viewport. Lastly, we set the present and goal states and return our factors.

initPolygon() {
  const gridSize = { x: 1440, y: 930 };
  const basePolygon = [
    { x: { c: 0, t: 0, rest: 494, hover: 550 }, y: { c: 0, t: 0, rest: 207, hover: 310 } },
    { x: { c: 0, t: 0, rest: 1019, hover: 860 }, y: { c: 0, t: 0, rest: 137, hover: 290 } },
    { x: { c: 0, t: 0, rest: 1035, hover: 820 }, y: { c: 0, t: 0, rest: 504, hover: 520 } },
    { x: { c: 0, t: 0, rest: 377, hover: 620 }, y: { c: 0, t: 0, rest: 531, hover: 560 } },
  ];

  basePolygon.forEach((p) => {
    p.x.relaxation /= gridSize.x;
    p.y.relaxation /= gridSize.y;

    p.x.hover /= gridSize.x;
    p.y.hover /= gridSize.y;

    p.x.relaxation *= this.width;
    p.y.relaxation *= this.peak;

    p.x.hover *= this.width;
    p.y.hover *= this.peak;

    p.x.t = p.x.c = p.x.relaxation;
    p.y.t = p.y.c = p.y.relaxation;
  });

  return basePolygon;
}

The mouse capabilities

Subsequent, we’ve the mouse capabilities. We have to hear for the next occasions: mousedown, mousemove, and mouseup. The person will solely draw when the mouse is pressed down.

Right here’s the logic: when the person presses the mouse down, we add a brand new path to the listing, permitting us to protect the shapes. Because the mouse strikes, we test whether or not the present mouse place is contained in the polygon. Whereas there are numerous methods to optimize efficiency—like utilizing a bounding field for the polygon and performing calculations provided that the mouse is contained in the field—we’ll hold it easy for this exploration. As an alternative, we’ll use a small operate to carry out this test.

We map the present values for every level and go them to the operate together with the mouse place. Based mostly on the isHover variable, we then set the goal values for every vertex. We’ll additionally replace the polygonHover goal and the mouse goal coordinates, which we’ll use to animate the paths and the mouse circle on the canvas.

mousedown(e) {
  if (this.mouseupTO) clearTimeout(this.mouseupTO);
  const newTrail = [];
  this.trails.push(newTrail);
  this.activeTrail = newTrail;
}
mousemove(e) {
  const isHover = this.inPolygon(e.clientX, e.clientY, this.polygon.map((p) => [p.x.c, p.y.c]));
  this.polygon.forEach((p) => {
    if (isHover) {
      p.x.t = p.x.hover;
      p.y.t = p.y.hover;
    } else {
      p.x.t = p.x.relaxation;
      p.y.t = p.y.relaxation;
    }
  });
  this.polygonHover.t = isHover ? 1 : 0;
  this.mouse.x.t = e.clientX;
  this.mouse.y.t = e.clientY;
}
mouseup() {
  if (this.mouseupTO) clearTimeout(this.mouseupTO);
  this.mouseupTO = setTimeout(() => {
    this.activeTrail = null;
  }, 300);
}
inPolygon(x, y, polygon) {
  let inside = false;
  for (let i = 0, j = polygon.size - 1; i < polygon.size; j = i++) {
    const xi = polygon[i][0], yi = polygon[i][1];
    const xj = polygon[j][0], yj = polygon[j][1];
    const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
    if (intersect) inside = !inside;
  }
  return inside;
}

Lastly, we are able to set the activeTrail to null, however we’ll add a small delay to introduce some inertia.

Okay, time for the loops

This class has two fundamental loops: the render operate and the draw operate from p5. Let’s begin with the render operate.

The render operate is without doubt one of the most vital components of the category. Right here, we’ll deal with all our linear interpolations and replace the paths.

render(time) {
  this.t = time * 0.001;
  this.mouse.x.c += (this.mouse.x.t - this.mouse.x.c) * 0.08;
  this.mouse.y.c += (this.mouse.y.t - this.mouse.y.c) * 0.08;
  this.mouse.delta.t = Math.sqrt(Math.pow(this.mouse.x.t - this.mouse.x.c, 2) + Math.pow(this.mouse.y.t - this.mouse.y.c, 2));
  this.mouse.delta.c += (this.mouse.delta.t - this.mouse.delta.c) * 0.08;
  this.polygonHover.c += (this.polygonHover.t - this.polygonHover.c) * 0.08;

  if (this.activeTrail) {
    this.activeTrail.push({ x: this.mouse.x.c, y: this.mouse.y.c });
    if (this.activeTrail.size > this.maxTrailLength) this.activeTrail.shift();
  }
  this.trails.forEach((path) => {
    if(this.activeTrail === path) return;
    path.shift();
  });

  this.trails = this.trails.filter((path) => path && path.size > 0);

  this.polygon.forEach((p, i) => {
    p.x.c += (p.x.t - p.x.c) * (0.07 - i * 0.01);
    p.y.c += (p.y.t - p.y.c) * (0.07 - i * 0.01);
  });

  requestAnimationFrame(this.render);
}

Let’s dive deeper. First, we’ve a time variable, which we’ll use to offer the polygon an natural, dynamic motion. After that, we replace the present values utilizing linear interpolations (lerps). For the mouse’s delta/velocity worth, we’ll use the traditional method for locating the gap between two factors.

Now, for the paths, right here’s the logic: if there’s an energetic path, we begin pushing the mouse’s present positions into it. If the energetic path exceeds the utmost size, we start eradicating older factors. For the inactive trails, we additionally take away factors over time and take away any “lifeless” trails—these with no remaining factors—from the listing.

Lastly, we replace the polygon utilizing a lerp, including a small delay between every level primarily based on its index. This creates a smoother and extra pure hover conduct.

p5 logic

We’re nearly there! With all the mandatory knowledge in place, we are able to begin drawing.

Within the initBrush operate, we set the sizes for the canvas and take away the fields, as we don’t need any distortion in our curves this time. Subsequent, we configure the comb. There are many choices to select from, however be aware of efficiency when choosing sure options. Lastly, we scale the comb primarily based on the window dimension to make sure every thing adjusts correctly.

initCanvas() {
  this.app = new p5(this.sketch, this.el);
  requestAnimationFrame(this.render);
}
initBrush(p) {
  brush.occasion(p);
  p.setup = () => {
    p.createCanvas(this.width, this.peak, p.WEBGL);
    p.angleMode(p.DEGREES);
    brush.noField();
    brush.set('2B');
    brush.scaleBrushes(window.innerWidth <= 1024 ? 2.5 : 0.9);
  };
}
sketch(p) {
  this.initBrush(p);
  p.draw = () => {
    p.frameRate(30);
    p.translate(-this.width / 2, -this.peak / 2);
    p.background('#FC0E49');

    brush.stroke('#7A200C');
    brush.strokeWeight(1);
    brush.noFill();
    brush.setHatch("HB", "#7A200C", 1);
    brush.hatch(15, 45);
    const time = this.t * 0.01;
    brush.polygon(
      this.polygon.map((p, i) => [
        p.x.c + Math.sin(time * (80 + i * 2)) * (30 + i * 5),
        p.y.c + Math.cos(time * (80 + i * 2)) * (20 + i * 5),
      ])
    );

    brush.strokeWeight(1 + 0.005 * this.mouse.delta.c);
    this.trails.forEach((path) => {
      if (path.size > 0) {
        brush.spline(path.map((t) => [t.x, t.y]), 1);
      }
    });

    brush.noFill();
    brush.stroke('#FF7EBE');
    brush.setHatch("HB", "#FFAABF", 1);
    brush.hatch(5, 30, { rand: 0.1, steady: true, gradient: 0.3 })
    const r = 5 + 0.05 * this.mouse.delta.c + this.polygonHover.c * (100 + this.mouse.delta.c * 0.5);
    brush.circle(this.mouse.x.c, this.mouse.y.c, r);
  };
}

Lastly, we’ve the sketch operate, which accommodates the drawing loop and implements all of the logic from our earlier calculations.

First, we set the FPS and select the instruments we’ll use for drawing. We start with the polygon: setting the stroke coloration and weight, and eradicating the fill since we’ll use a hatch sample to fill the form. You may discover the complete configurations for instruments of their documentation, however our settings are easy: brush: HB, coloration: #7A200C, and weight: 1. After that, we configure the hatch operate, setting the gap and angle. The final parameter is non-compulsory—a configuration object with extra choices.

With our brush prepared, we are able to now draw the polygon. Utilizing the polygon operate, we ship an array of factors to p5, which paints them onto the canvas. We map our present level coordinates and add clean motion utilizing Math.sin and Math.cos, with variations primarily based on the index and the time variable for a extra natural really feel.

For the paths, we regulate the strokeWeight primarily based on the mouse delta. For every path, we use the spline operate, passing a listing of factors and the curvature. Then, for the mouse circle, we take away the fill, set the stroke, and apply the hatch. The circle’s radius is dynamic: it scales primarily based on the mouse delta, including a way of responsiveness. Moreover, the radius will increase when the mouse is contained in the polygon, creating an immersive animation impact.

The end result ought to look one thing like this:

That’s it for at this time! Thanks for studying. To see extra explorations and experiments, be at liberty to comply with me on Instagram.





Supply hyperlink

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles