Tunnel mouse trail/follow

Howdy codeheads, hope you’re all keeping well.
I’m hoping someone can help me ‘hack’ this thing together. As a visual designer (not a coder, but I do have a decent level of understanding code … for a visual person), I was in two minds as to whether to get someone to just code this for the project or … why not learn some stuff myself.

It’s ultimately a mousetrail, but rather than trailing, creates a ‘tunnel’ like effect due to the scaling, easing and layering of the assets. The basic idea is a psychedelic, beer can adventure (see image).
I figure it should be able to be achieved with HTML/CSS/Java combo, but might be totally wrong.

Some starting questions (yell out if they’re being asked too early) …
can someone give me a hand to dive in?
can/should the assets be svgs?
what’s the best approach given this will be full width across a site (on both desktop and mobile) DOM or Canvas?
is it possible to add some subtle texture or “line boil” to the assets (giving it a hand drawn look), or does that get too intense?
should the colours cycle on the asset, or cycle at the creation stage of the asset (this will come down to how psychedelic it looks … it tipping ‘cycle on the asset’.

Appreciate any and all insight, tips, knowledge and ideas.
Thanks in advance.

This sounds really cool! Can you clarify in more detail what this effect will look like? I am not able to visualize where the mouse trail will kick in or how the mouse interaction will work in general. Is it more like this effect except the color of the rings will vary:

The king himself! Thanks.
yes actually, I was looking at that effect yesterday. The “difference” is that the cans would zoom/extend to the bounds of the darkest/black area (i.e, edge of screen).
I was going to mock up an example (as a motion designer).
Maybe I should do that.

If you can provide a really quick mockup, then that would help greatly! Thanks :slight_smile:

Hope this helps … it’s not perfect, but should offer a bit more insight. Ignore the colours too … just threw it together.

Thanks! This will be a fun one. I will try to fiddle with that in the next few days. Will you be having any DOM content in front of it?

My current thinking is to just handle this as a canvas-related animation.

:package:

Not entirely sure about the DOM content. Perhaps a logo (of the brewery) but nothing crazy. Is clickable stuff an issue? Wouldn’t necessarily need clickable things over the top.

Also hoping that it could react to finger drag on devices too.
I can explain the ‘line boil’ thing in more detail too … that’s more a general question for me, about finding a way to bring constant life to in browser anims. That said, if it can’t be done procedurally it no doubt makes assets more complex.

You are amazing @kirupa . Thanks for your time and input

@buggles - I haven’t forgotten about this btw! A combination of a new job and moving to a new home have dented my availability a bit :truck: :house: :baggage_claim:

@kirupa … I appreciate the time mate. We’ve got an 11 week old, and I’m balancing a chunk of new work too — no complaints from me.
Good luck with the move :slight_smile:

11 months old? I remember those days…:sleeping: ! Our daughter is now three, so that keeps us busy chasing after her haha.

I had some time during lunch to fiddle with this, and here is a very hacky starting point:

Code for this below:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Zooming Tunnel</title>

  <style>
    #myCanvas {
      width: 800px;
      height: 600px;
      border: 2px solid black;
    }
  </style>
</head>

<body>
  <h1>Zooming Tunnel</h1>
  <canvas id="myCanvas">

  </canvas>

  <script>
    var canvasElement = document.querySelector("#myCanvas");
    var context = canvasElement.getContext("2d");

    var mouseX = 0;
    var mouseY = 0;

    normalizeCanvasSize(canvasElement);

    function drawRectangle(xPos, yPos, color, isAnimating, step) {
      let rectangleWidth = 250;
      let rectangleHeight = 150;

      let rectangleX = xPos - rectangleWidth / 2;
      let rectangleY = yPos - rectangleHeight / 2;

      // the rectangle
      context.beginPath();

      if (isAnimating) {
        context.rect(xPos - (rectangleWidth / 2) - step / 2, yPos - (rectangleHeight / 2) - step / 2 , rectangleWidth + step, rectangleHeight + step);
      } else {
        context.rect(rectangleX, rectangleY, rectangleWidth, rectangleHeight);
      }
      context.closePath();

      // the outline
      /*
      context.lineWidth = 10;
      context.strokeStyle = '#666666';
      context.stroke();
      */

      // the fill color
      context.fillStyle = color;
      context.fill();
    }

    canvasElement.addEventListener("mousemove", mouseMoving, false);
    
    function mouseMoving(e) {
        let mousePosition = getPosition(canvasElement);
        mouseX = e.clientX - mousePosition.x;
        mouseY = e.clientY - mousePosition.y;
    }

    let count = 0;
    let speed = 8;

    function animate() {
      count++;

      context.clearRect(0, 0, canvasElement.width, canvasElement.height);

      drawRectangle(mouseX, mouseY, "#C2BB33", true, count * speed);

      if (count > 40) {
        drawRectangle(mouseX, mouseY, "#C06F2E", true, (count - 20) * speed);
      }

      if (count > 80) {
        drawRectangle(mouseX, mouseY, "#C23233", true, (count - 40) * speed);
      }

      if (count > 120) {
        drawRectangle(mouseX, mouseY, "#E7D285", true, (count - 60) * speed);
      }

      if (count > 160) {
        drawRectangle(mouseX, mouseY, "#C1BC32", true, (count - 80) * speed);
      }

      //drawRectangle(mouseX, mouseY, "#C2BB33", false, 0);

      requestAnimationFrame(animate);
    }

    requestAnimationFrame(animate);

    function normalizeCanvasSize(canvas) {
      // look up the size the canvas is being displayed
      let rect = canvas.getBoundingClientRect();

      // increase the actual size of our canvas
      canvas.width = rect.width * devicePixelRatio;
      canvas.height = rect.height * devicePixelRatio;

      // ensure all drawing operations are scaled
      context.scale(devicePixelRatio, devicePixelRatio);

      // scale everything down using CSS
      canvas.style.width = rect.width + 'px';
      canvas.style.height = rect.height + 'px';
    }

    // Helper function to get an element's exact position
    function getPosition(el) {
      var xPos = 0;
      var yPos = 0;
    
      while (el) {
        if (el.tagName == "BODY") {
          // deal with browser quirks with body/window/document and page scroll
          var xScroll = el.scrollLeft || document.documentElement.scrollLeft;
          var yScroll = el.scrollTop || document.documentElement.scrollTop;
    
          xPos += (el.offsetLeft - xScroll + el.clientLeft);
          yPos += (el.offsetTop - yScroll + el.clientTop);
        } else {
          // for all other non-BODY elements
          xPos += (el.offsetLeft - el.scrollLeft + el.clientLeft);
          yPos += (el.offsetTop - el.scrollTop + el.clientTop);
        }
    
        el = el.offsetParent;
      }
      return {
        x: xPos,
        y: yPos
      };
    }
  </script>
</body>

</html>

Ahhh, 3 years … I’m sure that’s flown by!
Nice start. Thanks.
It creates some interesting dilemmas doesn’t it? My initial thought was … well it’s basically just like the reverse layering of any old mouse trail … with the movement of each instance just scaling up. But … of course it’s not that simple!

I don’t know why it didn’t occur to me before (to seek stock footage) … I include these here as possible inspiration and reveals of approaches.
Maybe the assets as rectangles isn’t the right approach, maybe if it were the reverse cutout shape. Then … it kinda starts melting my brain!

I do think it is as simple as what you are looking for. I had to run to something else, so I just threw my code in here in its interim state. Having it look exactly like what you describe is completely doable :slight_smile:

(Could be famous last words, but I’ll have some time to fiddle with it some more today!)

@buggles - I am 99% close to getting this effect finished:

The full code is here:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Zooming Tunnel</title>

  <style>
    body {
      padding: 100px;
    }

    h1 {
      font-family: sans-serif;
      color: #333;
    }

    #myCanvas {
      width: 800px;
      height: 400px;
      border: 5px solid black;
      border-radius: 10px;
    }
  </style>
</head>

<body>
  <h1>Zooming Tunnel</h1>
  <canvas id="myCanvas">

  </canvas>

  <script>
    let canvasElement = document.querySelector("#myCanvas");
    let context = canvasElement.getContext("2d");

    let mouseX = 0;
    let mouseY = 0;

    normalizeCanvasSize(canvasElement);

    canvasElement.addEventListener("mousemove", mouseMoving, false);

    function mouseMoving(e) {
      let mousePosition = getPosition(canvasElement);
      mouseX = e.clientX - mousePosition.x;
      mouseY = e.clientY - mousePosition.y;
    }

    let rectangles = [];

    class Rectangle {
      constructor(context, color, delay) {
        this.context = context;
        this.step = 0;
        this.color = color;
        this.rectangleWidth = 10;
        this.rectangleHeight = 50;
        this.running = true;
      }

      draw(xPos, yPos) {
        if (this.running) {
          this.step += 5;

          if (this.step > 2000) {
            this.stop();
          }

          // the rectangle
          this.context.beginPath();

          this.context.rect(xPos - (this.rectangleWidth / 2) - this.step / 2, yPos - (this.rectangleHeight / 2) - this.step / 2, this.rectangleWidth + this.step, this.rectangleHeight + this.step);

          this.context.fillStyle = this.color;
          this.context.fill();
        }
      }

      stop() {
        this.running = false;
      }
    }

    function animate() {

      context.clearRect(0, 0, canvasElement.width, canvasElement.height);

      for (let i = 0; i < rectangles.length; i++) {
        let r = rectangles[i];
        r.draw(mouseX, mouseY);
      }

      requestAnimationFrame(animate);
    }

    let count = 0;
    //let colors = ["#FFAF87", "#FF8E72", "#ED6A5E", "#4CE0B3", "#377771", "#0D0221", "#0F084B", "#26408B"];

    let h_range = [0, 45];
    let s_range = [70, 90];
    let l_range = [0, 90];
    let a_range = [1, 1];

    function addRectangle() {
      let delay = 0;

      //let index = count % colors.length;
      let color = getRandomColor(h_range, s_range, l_range, a_range);
      console.log(color);

      let myRectangle = new Rectangle(context, color.hslaValue, delay);
      rectangles.push(myRectangle);

      count++;
    }

    function setup() {
      setInterval(addRectangle, 200);

      requestAnimationFrame(animate);
    }
    setup();



    function normalizeCanvasSize(canvas) {
      // look up the size the canvas is being displayed
      let rect = canvas.getBoundingClientRect();

      // increase the actual size of our canvas
      canvas.width = rect.width * devicePixelRatio;
      canvas.height = rect.height * devicePixelRatio;

      // ensure all drawing operations are scaled
      context.scale(devicePixelRatio, devicePixelRatio);

      // scale everything down using CSS
      canvas.style.width = rect.width + 'px';
      canvas.style.height = rect.height + 'px';
    }

    // Helper function to get an element's exact position
    function getPosition(el) {
      let xPos = 0;
      let yPos = 0;

      while (el) {
        if (el.tagName == "BODY") {
          // deal with browser quirks with body/window/document and page scroll
          let xScroll = el.scrollLeft || document.documentElement.scrollLeft;
          let yScroll = el.scrollTop || document.documentElement.scrollTop;

          xPos += (el.offsetLeft - xScroll + el.clientLeft);
          yPos += (el.offsetTop - yScroll + el.clientTop);
        } else {
          // for all other non-BODY elements
          xPos += (el.offsetLeft - el.scrollLeft + el.clientLeft);
          yPos += (el.offsetTop - el.scrollTop + el.clientTop);
        }

        el = el.offsetParent;
      }
      return {
        x: xPos,
        y: yPos
      };
    }

    function getRandomColor(h, s, l, a) {
      let hue = getRandomNumber(h[0], h[1]);
      let saturation = getRandomNumber(s[0], s[1]);
      let lightness = getRandomNumber(l[0], l[1]);
      let alpha = getRandomNumber(a[0] * 100, a[1] * 100) / 100;

      return {
        h: hue,
        s: saturation,
        l: lightness,
        a: alpha,
        hslaValue: getHSLAColor(hue, saturation, lightness, alpha)
      }
    }

    function getRandomNumber(low, high) {
      let r = Math.floor(Math.random() * (high - low + 1)) + low;
      return r;
    }

    function getHSLAColor(h, s, l, a) {
      return `hsl(${h}, ${s}%, ${l}%, ${a})`;
    }
  </script>
</body>

</html>

I am not happy about the infinitely growing rectangles array, but that is something I will deal with later. The code isn’t commenting, so do let me know if I can clarify anything there.

Cheers,
Kirupa :train2:

1 Like