Creating a Vue Canvas with Endless Grid Dots and a Dynamic Panning Feature

I'm currently focused on integrating a panning system into the canvas of my Vue application. Although I have successfully implemented the panning system, I am encountering difficulties in efficiently rendering an infinite grid of dots.

Below is the code for my component:

<template>
  <div>
    <div>
      <canvas
        @mousedown="baseEditorMouseDown"
        @mouseup="baseEditorMouseUp"
        @mousemove="baseEditorMouseMove"
        ref="baseEditor"
      ></canvas>
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref, type Ref, computed, onMounted } from "vue";

const baseEditor: Ref<HTMLCanvasElement | null> = ref(null);

// other variables and functions here...

The challenge lies in efficiently rendering an infinite grid of dots based on the user's window size within the computedGridCircles function. While I have explored various resources such as:

  • HTML infinite pan-able canvas
  • How can I make infinite grid with canvas?
  • Make a canvas infinite

Despite these references, I seem to be missing key calculations in achieving an infinite grid. If you could provide guidance or insights on how to achieve this goal effectively, it would be greatly appreciated. Thank you for your assistance!

Answer №1

I have successfully pinpointed the solution.

In my analysis, I discovered that a user is restricted to grabbing only within their screen resolution limits. Hence, I deduced that the gap between the starting and ending coordinates must surpass the screen resolution for optimal functionality.

For example, with a 1920x1080 monitor, the disparity between start and stop X positions should exceed 1920 to simulate an infinite scroll effect. To guarantee this outcome, I also included five times the grid's dimensions to the difference. This same logic extends to vertical positions.

To prevent performance issues like lag or freezing post excessive grabbing, I utilize rectangles equivalent to the visible area.

This led me to modifying my drawGridCircles function as follows:

function drawGridCircles() {
  if (context.value) {
    context.value.beginPath();

    const startX = Math.floor(-currentOffsetPositions.value.x / gridSize.value) * gridSize.value + gridSize.value / 2;
    const startY = Math.floor(-currentOffsetPositions.value.y / gridSize.value) * gridSize.value + gridSize.value / 2;

    const finishX = startX + window.innerWidth + gridSize.value * 5
    const finishY = startY + window.innerHeight + gridSize.value * 5

    for (let x = startX; x < finishX; x += gridSize.value) {
      for (let y = startY; y < finishY; y += gridSize.value) {
        context.value.rect(x + currentOffsetPositions.value.x, y + currentOffsetPositions.value.y, 3, 3)
      }
    }

    context.value.fillStyle = "#333";
    context.value.fill();
  }
}

You can access the complete code at: https://stackblitz.com/edit/vue3-vite-typescript-starter-qf1cey?file=src%2FApp.vue

Answer №2

Infinite specks

The following technique generates an infinite scroll speck field (Though not truly infinite since JavaScript employs doubles which have a limited range, the size of the scroll area spans from Number.MIN_SAFE_INTEGER to Number.MAX_SAFE_INTEGER pixels in width and height)

This method produces a random set of specks, with each speck having an x and y modulo value that is at least the size of the canvas.

An example of obtaining the speck's x position and checking if it is visible:

const mod = speck.mx; // modulo > canvas.width + DOT_SIZE
const x = ((speck.x - origin.x) % mod + mod) % mod - HALF_DOT_SIZE;
const visible = x >= -HALF_DOT_SIZE && x < canvas.width + HALF_DOT_SIZE;

Perform this procedure for both x and y as shown in the sample code snippet within the function drawDots

As a result, each speck is randomly repeated, creating an almost infinite, non-repeating pattern of specks when combined together.

This approach works effectively for a fixed-size canvas but fails if the canvas undergoes changes such as zooming in or out.

Example

Key constants utilized:

  • DOT_PER_PIXEL: Denotes the density of specks

    Note: This value should be kept small as the canvas can accommodate millions of pixels. The value used in the sample is 1 speck per 10,000 pixels. Since the speck pattern is random, this serves as an approximation.

    The total number of specks required to maintain that density while panning is automatically computed;

  • DOT_SIZE: Represents the size of the speck necessary to prevent specks from appearing and disappearing at the edges.

In the provided example, scrolling is achieved by shifting the origin. The origin moves in a direction and at a speed that varies over time.

requestAnimationFrame(mainLoop);
const ctx = canvas.getContext("2d");
var width = canvas.width;
var height = canvas.height;
const DOT_SIZE = 4, HALF_DOT_SIZE = DOT_SIZE * 0.5;
const rnd = (min, max) => (max - min) * Math.random() + min;
const lerp = (a, b, u) => (b - a) * u + a;
const DOTS_PER_PIXEL = 2 / 10000; // WARNING dots per pixel should be much smaller than 1
var dir = 0;                      // Origin movement dir and speed
var speed = 1;
const origin = {x: 0, y: 0};      // Any start pos will do
const dots = [];
const resized = (() => {
  var maxDistBetween;
  var minDistBetween;
  var dotsPerCanvas;
  var dotCount;
  function createDots(count) {
    const Dot = () => ({
      x: rnd(-HALF_DOT_SIZE, maxDistBetween),
      y: rnd(-HALF_DOT_SIZE, maxDistBetween),
      mx: rnd(minDistBetween, maxDistBetween),
      my: rnd(minDistBetween, maxDistBetween),
    });  
    while (count-- > 0) { dots.push(Dot()); }
  }
  return () => {
    width = innerWidth;
    height = innerHeight;
    canvas.width = width;
    canvas.height = height;
    ctx.strokeStyle = "#FFF";
    ctx.fillStyle = "#FFF";
    maxDistBetween = Math.max(width + DOT_SIZE, height + DOT_SIZE) * 2;
    minDistBetween = maxDistBetween * 0.6; // scale must be > 0.5
    dotsPerCanvas = width * height * DOTS_PER_PIXEL;
    dotCount = ((maxDistBetween * maxDistBetween) / ((width + DOT_SIZE) * (height + DOT_SIZE))) * dotsPerCanvas | 0;
    dots.length = 0;
    createDots(dotCount)
  }
})();

const updateOrigin = (() => {
    var turnFrom = 0;
    var turnTo = 0;
    var speedFrom = 0;
    var speedTo = 0;
    var timeFrom = 0;
    var timeTo = 0;
    return time => {
        if (timeTo < time) {
          timeFrom = time;
          timeTo = time + rnd(2000, 5000);
          turnFrom = turnTo;
          speedFrom = speedTo;
          turnTo += rnd(-1, 1);
          speedTo = rnd(5, 10);
       }
       const u = (time - timeFrom) / (timeTo - timeFrom);
       dir = lerp(turnFrom, turnTo, u);
       speed = lerp(speedFrom, speedTo, u);
       origin.x += Math.cos(dir) * speed;
       origin.y += Math.sin(dir) * speed;
    }
})();

function drawDots(origin, dots) {
  ctx.beginPath();
  for (const dot of dots) {
    const x = ((dot.x - origin.x) % dot.mx + dot.mx) % dot.mx - HALF_DOT_SIZE;
    if (x > -HALF_DOT_SIZE && x < width + HALF_DOT_SIZE) {
      const y = ((dot.y - origin.y) % dot.my + dot.my) % dot.my - HALF_DOT_SIZE;
      if (y > -HALF_DOT_SIZE && y < height + HALF_DOT_SIZE) {
        ctx.rect(x - HALF_DOT_SIZE, y - HALF_DOT_SIZE, DOT_SIZE, DOT_SIZE);    
      }
    }  
  }  
  ctx.fill();
}

function mainLoop(time) {
    if (width !== innerWidth || height !== innerHeight) { resized() }
    ctx.clearRect(0, 0, width, height);
    updateOrigin(time);
    drawDots(origin, dots);
    requestAnimationFrame(mainLoop);
}
body { background: #000; padding: 0px; margin: 0px; }
canvas {
  position: absolute;
  left: 0px;
  top: 0px;
  padding: 0px; 
  margin: 0px;
}
<canvas id="canvas" width="512" height="512"></canvas>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Display the element following a specific component while looping through an array in Vue.js

Currently, I am facing an issue while using a for-loop on the component element. My goal is to display a <p> element next to the <component> element during the loop's third iteration. The challenge lies in accessing the iteration variable ...

Protecting Your Routes with Guards in Framework 7 Vue

Whenever I utilize the following code snippet: { path: '/chat/', async(routeTo, routeFrom, resolve, reject) { if (localStorage.getItem('token')) { resolve({ component: require('./assets/vu ...

Is there a problem encountered when attempting to pass an array value as a string using props in Vue.js?

<template> <div> <div v-for="piza in pizas" :key="piza.pname"> {{ piza.pname }} <List :content="matchpizza" :pname="piza.pname" :qname="quantitys.qname" /> </div> </div> </template> <scr ...

Vue function that inserts <br> tags for addresses

My Vue filter retrieves and combines address details with a , Vue.filter('address', (address, countryNames = []) => { const formattedAddress = [ address?.name, address?.company, address?.add1, address?.add2, address?.town ...

Tips for modifying and removing the information within a card: Combining Laravel with vue.js

I've made some changes to my discussion forum setup by switching from displaying comments and replies in a traditional table format to using cards for a more visually appealing layout, like this: https://i.stack.imgur.com/vjIXN.jpg While the card di ...

In my database, I have two tables: Table1 and Table2. To better illustrate my issue, I have included a picture for reference

https://i.stack.imgur.com/GQAMy.jpg I am facing a challenge with joining two tables, table1 and table2, in MySQL. I need assistance in creating the resultant table as shown in the attached picture. Your help in solving this issue would be greatly appreci ...

Encountering CORS Error while trying to access Guest App in Virtualbox using Vue, Express, and Axios

I encountered an issue while trying to access my Vue app in Virtualbox from the host, both running on Linux Mint 20. Although I can now reach the login page from my host, I am consistently faced with a CORS error during login attempts: Cross-Origin Request ...

Guide to Removing Nuxt Modules and Packages

Currently using Nuxt 2.13 and looking to remove some packages from my project. In the past, I simply deleted them from the package.json file and ran npm i to uninstall the package. However, now I am encountering an error: Module @nuxtjs/google-gtag not fou ...

The chosen option in the q-select is extending beyond the boundaries of the input field

Here's the code snippet I used for the q-select element: <q-select square outlined fill-input standout="bg-grey-3 text-white" v-model="unit_selection" :options="units&qu ...

Issue with component not properly reflecting changes made to array

I recently started using Vue and came across an interesting behavior that I wanted to clarify. I have a component that receives a list of facets and organizes them for display purposes. Here is a snippet of the code: // Code snippet The above code showcas ...

"When a Vuex mutation modifies the state, the computed property fails to accurately represent the changes in the markup

I've encountered a perplexing issue with using a computed property for a textarea value that hasn't been addressed in a while. My setup involves a textarea where user input is updated in Vuex: <textarea ref="inputText" :value="getInputText" ...

vuejs function for modifying an existing note

Method for editing an existing note with Vue.js in a web application This particular application enables users to perform the following tasks: Create a new note View a list of all created notes Edit an existing note Delete an existing note Prog ...

Creating a Vue.js component during the rendering process of a Laravel Blade partial view

In my Vue.js project, I have a component that is used in a partial view called question.blade.php: {{--HTML code--}} <my-component type='question'> <div class="question">[Very long text content...]</div> </my-component& ...

"What is the best way to manipulate arrays in vue.js using the map function

I'm currently dealing with a Vue code that incorporates anime.js. My code has grown substantially to over 1500 lines. In order for Stack Overflow to accept my question, I have only included 5 items of my sampleText, even though it actually consists of ...

Executing an external script in Nuxt after re-rendering: Best practices?

Looking for a solution in Nuxt/Vue to properly execute an external script (hosted by a third party) after the DOM has successfully rerendered on every route. The challenge arises when using a script that dynamically adds elements to the dom, causing confl ...

The Nuxt Content Shiki plugin encountered an ERROR: "Page not found at /home"

I'm having issues getting syntax highlighter to work with @nuxt/content and Shiki. Once I installed the shiki package in my project's nuxt.config.js file. import shiki from 'shiki' ... export default { modules: ['@nuxt/content ...

Monitoring Vue for ongoing HTTP requests

Upon mounting a component, it initiates 4 HTTP requests (using Axios) to fetch the necessary data. Is there a method to monitor for any outstanding HTTP requests? To simplify: Are there any pending HTTP requests? yes -> Loading=true no -> Loading ...

Using Vue.js in conjunction with Gulp: A Beginner's Guide

Currently, I am working on a small project that involves gulp. I have a desire to incorporate vue.js into this project but do not have the knowledge on how to configure vue.js in the gulpfile.js. I am seeking guidance on the configuration of gulpfile to i ...

The pie chart fails to fully display

Check out the repro example I made here. The legend is displaying properly, but the graph is not showing up. What could be causing this unexpected issue? ...

Modifying .vue files within Laravel seems to unexpectedly impact unrelated CSS styles

I've been working on a Laravel project that involves building Vue components. Strangely, every time I update a .vue file and add it for a commit, modifications are also made to the app.css and app.js files. These changes seem to be undoing a particula ...