Damian Grzanka
Damian Grzanka

Reputation: 305

Shift line without tearing its ends in html canvas

I am trying to create an application where the line follows y of my mouse and it's shifted to the left every x ms.

The problem is that there are artifacts on the joins of each line, like in the picture below.

How can I prevent it?

And potentially smooth the lines. The main part of the question is how to prevent tearing but if you have an idea of how to smooth this line let me know.

enter image description here

Code below or alternativly JsFiddle

let y = null

const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');

ctx.strokeStyle = 'red';
ctx.lineCap = 'round';
ctx.lineJoin = 'round'

ctx.lineWidth = 5;

const width = ctx.canvas.width
const height = ctx.canvas.height

let prevY = null;


const onMouseUpdate = (e) => {
    y = e.pageY;
}
const shift = 10

canvas.addEventListener('mousemove', onMouseUpdate, false);
canvas.addEventListener('mouseenter', onMouseUpdate, false);

function draw() {
    const rect = canvas.getBoundingClientRect()
    const yTarget = y - rect.top

    ctx.beginPath()
    ctx.moveTo(width - shift, prevY);
    ctx.lineTo(width, yTarget);
    ctx.stroke();
    prevY = yTarget

    const imageData = ctx.getImageData(shift, 0, width - shift, height);
    ctx.putImageData(imageData, 0, 0);
    ctx.clearRect(width - shift, 0, shift, height);
}
setInterval(draw, 300)

Upvotes: 1

Views: 115

Answers (2)

Kaiido
Kaiido

Reputation: 136716

The best option for this is to draw a single path: You store all your points in an Array, then at every new frame you clear the whole context and trace the whole path again.

Here is a sample code where I remove from the Array the points that went out of screen. Note that I also make use of requestAnimationFrame() which schedules a callback to fire at the next painting frame, allowing for smooth animations.

const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');

let y = canvas.height / 2;
let data = [];

ctx.strokeStyle = 'red';
ctx.lineCap = 'round';
ctx.lineJoin = 'round'
ctx.lineWidth = 5;

const onMouseUpdate = (e) => {
  const rect = canvas.getBoundingClientRect();
  const yTarget = y - rect.top;
  y = e.pageY;
}

canvas.addEventListener('mousemove', onMouseUpdate, false);
canvas.addEventListener('mouseenter', onMouseUpdate, false);

let lastTime = performance.now();
const speed = 33.3; // 10px per 300ms -> 33.3px per s
requestAnimationFrame(loop);

function loop(now) {
  // calculate x based on how much time elapsed since the last frame
  // we store the distance from the last point
  const x = ((now - lastTime) / 1000) * speed;
  lastTime = now;

  data.push({x, y});

  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.beginPath()
  let currentX = canvas.width;
  // iterate from end to start
  for (let i = data.length - 1; i >= 0; i--) {
    const {x, y} = data[i];
    currentX -= data[i].x;
    if (currentX < 0) { // the first points are outside the screen
      data = data.slice(i); // update our data array
      break;
    }
    ctx.lineTo(currentX, y);
  }
  // paint all in one call
  ctx.stroke();
  requestAnimationFrame(loop);
}
<canvas id="canvas" height="400" width="500"> </canvas>

Upvotes: 2

the Hutt
the Hutt

Reputation: 18408

The issue is because you are drawing till the edge of the canvas. So the rounded end part worth lineWidth/2 gets drawn outside of the canvas and gets clipped. You need to draw till canvasWidth - lineWidth/2 only:

let y = null;

// resolution of the canvas
const scale = 2;
const lineWidth = 6;
const shift = 7;

const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');

canvas.width = canvas.clientWidth * scale;
canvas.height = canvas.clientHeight * scale;

ctx.strokeStyle = 'red';
ctx.lineCap = 'round';
ctx.lineJoin = 'round'
ctx.lineWidth = lineWidth;

const width = ctx.canvas.width - lineWidth / 2;
const height = ctx.canvas.height
const rect = canvas.getBoundingClientRect();

let prevY = null;

const onMouseUpdate = (e) => {
  y = (e.clientY - rect.top) * scale;
}

canvas.addEventListener('mousemove', onMouseUpdate, false);
canvas.addEventListener('mouseenter', onMouseUpdate, false);

let xOff = 0

function draw() {
  const yTarget = y;

  ctx.beginPath()
  ctx.moveTo(width - shift, prevY);
  ctx.lineTo(width, yTarget);
  ctx.stroke();
  prevY = yTarget

  const imageData = ctx.getImageData(shift, 0, width + +20 + lineWidth / 2, height);
  ctx.clearRect(0, 0, rect.width, rect.height);
  ctx.putImageData(imageData, 0, 0);
}

let i = setInterval(draw, 200);
canvas {
  height: 90vh;
  border: 1px solid blue;
  width: 80vw;
}
<canvas id="canvas"> </canvas>

I've increased the canvas resolution. For more info refer this discussion.

Upvotes: 1

Related Questions