habibhassani
habibhassani

Reputation: 506

How to hide the path immediate overlap without hiding intersection overlap?

I'm making a painting app, and using the canvas path as a brush:

function onMove(){
    ctx.beginPath();
    ctx.moveTo(pos.x, pos.y); // from position
    pos=getPosition(event); 
    ctx.lineTo(pos.x, pos.y); // to position 
    ctx.stroke(); // draw it!
}

I'm stuck between in a dilemma, if I join all the paths into one I lose intersection overlap, and if I draw them real time separately I get those start/end overlaps spheres.

enter image description here

Is there a clean way I can solve this problem? Thank you.

Upvotes: 0

Views: 136

Answers (2)

Henrique Oliveira
Henrique Oliveira

Reputation: 77

I had a similar problem making a brush for an app draw, so since it was a pain to solve this problem I would like to share the solution here.

The solution i found was to instead of draw only one point when the move event is trigger, I draw multiple points between de last draw point and the current one, this makes that there is not gaps and gives the impression of a continuous line and even allows a dynamic opacity and size of the stroke.

const canvas = document.querySelector("canvas")
const ctx = canvas.getContext("2d")

// prevent swap bug when use touch or pen
canvas.addEventListener('touchmove', e => {
    e.preventDefault()
})

let lastX = 0
let lastY = 0
let lastPressure = 0

let drawing = false

function onMove(e) {
  if (!drawing) return

  const startX = lastX
  const startY = lastY
  const startPressure = lastPressure

  const endX = e.offsetX
  const endY = e.offsetY
  const endPressure = e.pressure

  const gap = 0.05

  const distance = Math.sqrt((endX - startX) ** 2 + (endY - startY) ** 2)
  const steps = Math.floor(distance / gap)
  const points = []

  for (let i = 0; i < steps; i++) {
    const t = i / steps
    const x = startX + t * (endX - startX)
    const y = startY + t * (endY - startY)
   const pressure = startPressure + t * (endPressure - startPressure)

    points.push({ x, y, pressure })
  }

  for (let i = 0; i < points.length - 1; i++) {
    const p1 = points[i]
    const p2 = points[i + 1]

    const size = 20
    const opacity = Math.min(p1.pressure, .1)

    ctx.globalAlpha = opacity
    ctx.strokeStyle = `rgba(0, 0, 0, ${opacity})`
    ctx.lineWidth = size
    ctx.lineCap = "round"
    ctx.lineJoin = "round"

    ctx.beginPath()
    ctx.moveTo(p1.x, p1.y)
    ctx.lineTo(p2.x, p2.y)
    ctx.stroke()
    ctx.closePath()
  }

  lastX = e.offsetX
  lastY = e.offsetY
}

canvas.addEventListener("pointerdown", (e) => {
  drawing = true
  
  lastX = e.offsetX
  lastY = e.offsetY
  lastPressure = e.pressure
})

canvas.addEventListener("pointerup", () => {
  drawing = false
})

canvas.addEventListener("pointeout", () => {
  drawing = false
})

canvas.addEventListener("pointermove", onMove)
<canvas width="500" height="500" style="border: 1px solid black"></canvas>

This code can be optimized increasing the gap var value, the less the gap the more points are draw.

Upvotes: 1

Bellian
Bellian

Reputation: 2140

Okey, then it is getting complicated since we are dealing with alpha values here..

Basicly you can draw the "missing part" of the new line in a buffer canvas by using destination-in and source-in blend modes.

The idea is: to "compute" the overlapp of the N-1 the Nth line and then only draw line N outside this overlap: (for demonstation the Nth line will be drawn in red.)

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

const canvas2 = document.createElement('canvas')
canvas2.width = 300
canvas2.height = 300
const ctx2 = canvas2.getContext('2d');

ctx.lineWidth = 30
ctx.lineCap = 'round';
ctx.strokeStyle = 'rgba(0,0,0,0.5)';
ctx.imageSmoothingEnabled = false;
ctx.lineJoin = "round"



function drawLine1(ctx) {
  ctx.beginPath();
  ctx.moveTo(250, 250);
  ctx.lineTo(100, 250);
  ctx.stroke();
}
function drawLine2(ctx) {
  ctx.beginPath();
  ctx.moveTo(100, 250);
  ctx.lineTo(100, 50);
  ctx.stroke();
}

// draw image until line n-1 on screen
ctx.beginPath();
ctx.moveTo(0, 100);
ctx.lineTo(250, 100); // 1st
ctx.lineTo(250, 250); // 2nd
ctx.lineTo(100, 250); // n-1
ctx.stroke();

// clear buffer canvas and setup
ctx2.clearRect(0, 0, canvas2.width, canvas2.height);
ctx2.lineWidth = 30
ctx2.lineCap = 'round';
ctx2.imageSmoothingEnabled = false;
// draw line n-1 on buffer in opaque color
ctx2.strokeStyle = 'rgba(0,0,0,1)';
drawLine1(ctx2)

// draw line n on buffer in "destination-in" blend mode opaque
ctx2.globalCompositeOperation = 'destination-in';
ctx2.strokeStyle = 'rgba(0,0,0,1)';
drawLine2(ctx2)

// draw line n on buffer in "source-out" blend mode and original color
ctx2.globalCompositeOperation = 'source-out';
ctx2.strokeStyle = 'rgba(255,0,0,0.5)';
drawLine2(ctx2)

ctx.drawImage(canvas2, 0 ,0)
<canvas id="canvas" width="400" height="400"></canvas>

It is not perfect, but it is a starting point..

Upvotes: 1

Related Questions