Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

HTML5 Canvas javascript smudge brush tool

I need idea how i can make brush who can color smudge.

Example in picture: right side painting with basic brush with two different colors in left also painting but additional use smudge tool, the result should be something like left side

enter image description here

I need advice how i can try to do it

like image 274
Your choice Avatar asked Sep 02 '26 13:09

Your choice


1 Answers

Here is one attempt

  1. On mousedown grab a copy of the area under the mouse into a separate canvas

  2. On mousemove draw that copy one pixel at a time from the previous mouse position to the current mouse position at 50% alpha, grabbing a new copy after each move.

In pseudo code

on mouse down
   grab copy of canvas at mouse position
   prevMousePos = currentMousePos

on mouse move
  for (pos = prevMousePos to currentMousePos step 1 pixel) 
    draw copy at pos with 50% alpha
    grab new copy of canvas at pos
  prevMousePos = currentMousePos

The brush is feathered by drawing a rgba(0,0,0,0) to rgba(0,0,0,1) radial gradient over it using globalCompositeOperation = 'destination-out'.

const ctx = document.querySelector('#canvas').getContext('2d');
const brushDisplayCtx = document.querySelector('#brush-display').getContext('2d');

function reset() {
  const {width, height} = ctx.canvas;
  const wd2 = width / 2
  ctx.globalAlpha = 1;
  ctx.fillStyle = 'white';
  ctx.fillRect(wd2, 0, wd2, height);

  const gradient = ctx.createLinearGradient(0, 0, 0, height);
  gradient.addColorStop(0, 'red');
  gradient.addColorStop(0.5, 'yellow');
  gradient.addColorStop(1, 'blue');
  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, wd2, height);
}
reset();

function getCanvasRelativePosition(e, canvas) {
  const rect = canvas.getBoundingClientRect();
  return {
    x: (e.clientX - rect.left) / rect.width  * canvas.width,
    y: (e.clientY - rect.top ) / rect.height * canvas.height,
  };
}

function lerp(a, b, t) {
  return a + (b - a) * t;
}

function setupLine(x, y, targetX, targetY) {
  const deltaX = targetX - x;
  const deltaY = targetY - y;
  const deltaRow = Math.abs(deltaX);
  const deltaCol = Math.abs(deltaY);
  const counter = Math.max(deltaCol, deltaRow);
  const axis = counter == deltaCol ? 1 : 0;

  // setup a line draw.
  return {
    position: [x, y],
    delta: [deltaX, deltaY],
    deltaPerp: [deltaRow, deltaCol],
    inc: [Math.sign(deltaX), Math.sign(deltaY)],
    accum: Math.floor(counter / 2),
    counter: counter,
    endPnt: counter,
    axis: axis,
    u: 0,
  };
};

function advanceLine(line) {
  --line.counter;
  line.u = 1 - line.counter / line.endPnt;
  if (line.counter <= 0) {
    return false;
  }
  const axis = line.axis;
  const perp = 1 - axis;
  line.accum += line.deltaPerp[perp];
  if (line.accum >= line.endPnt) {
    line.accum -= line.endPnt;
    line.position[perp] += line.inc[perp];
  }
  line.position[axis] += line.inc[axis];
  return true;
}

let lastX;
let lastY;
let lastForce;
let drawing = false;
let alpha = 0.5;

const brushCtx = document.createElement('canvas').getContext('2d');
let featherGradient;

function createFeatherGradient(radius, hardness) {
  const innerRadius = Math.min(radius * hardness, radius - 1);
  const gradient = brushCtx.createRadialGradient(
    0, 0, innerRadius,
    0, 0, radius);
  gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
  gradient.addColorStop(1, 'rgba(0, 0, 0, 1)');
  return gradient;
}

const radiusElem = document.querySelector('#radius');
const hardnessElem = document.querySelector('#hardness');
const alphaElem = document.querySelector('#alpha');
radiusElem.addEventListener('input', updateBrushSettings);
hardnessElem.addEventListener('input', updateBrushSettings);
alphaElem.addEventListener('input', updateBrushSettings);
document.querySelector('#reset').addEventListener('click', reset);

function updateBrushSettings() {
  const radius = radiusElem.value;
  const hardness = hardnessElem.value;
  alpha = alphaElem.value;
  featherGradient = createFeatherGradient(radius, hardness);
  brushCtx.canvas.width = radius * 2;
  brushCtx.canvas.height = radius * 2;
  
  {
    const ctx = brushDisplayCtx;
    const {width, height} = ctx.canvas;
    ctx.clearRect(0, 0, width, height);
    ctx.fillStyle = `rgba(0, 0, 0, ${alpha})`;
    ctx.fillRect(width / 2 - radius, height / 2 - radius, radius * 2, radius * 2);
    feather(ctx);
  }
}
updateBrushSettings();
 
function feather(ctx) {
  // feather the brush
  ctx.save();
  ctx.fillStyle = featherGradient;
  ctx.globalCompositeOperation = 'destination-out';
  const {width, height} = ctx.canvas;
  ctx.translate(width / 2, height / 2);
  ctx.fillRect(-width / 2, -height / 2, width, height);  
  ctx.restore();
}
 
function updateBrush(x, y) {
  let width = brushCtx.canvas.width;
  let height = brushCtx.canvas.height;
  let srcX = x - width / 2;
  let srcY = y - height / 2;
  // draw it in the middle of the brush
  let dstX = (brushCtx.canvas.width - width) / 2;
  let dstY = (brushCtx.canvas.height - height) / 2;

  // clear the brush canvas
  brushCtx.clearRect(0, 0, brushCtx.canvas.width, brushCtx.canvas.height);

  // clip the rectangle to be
  // inside
  if (srcX < 0) {
    width += srcX;
    dstX -= srcX;
    srcX = 0;
  }
  const overX = srcX + width - ctx.canvas.width;
  if (overX > 0) {
    width -= overX;
  }

  if (srcY < 0) {
    dstY -= srcY;
    height += srcY;
    srcY = 0;
  }
  const overY = srcY + height - ctx.canvas.height;
  if (overY > 0) {
    height -= overY;
  }

  if (width <= 0 || height <= 0) {
    return;
  }

  brushCtx.drawImage(
    ctx.canvas,
    srcX, srcY, width, height,
    dstX, dstY, width, height);    
  
  feather(brushCtx);
}

function start(e) {
  const pos = getCanvasRelativePosition(e, ctx.canvas);
  lastX = pos.x;
  lastY = pos.y;
  lastForce = e.force || 1;
  drawing = true;
  updateBrush(pos.x, pos.y);
}

function draw(e) {
  if (!drawing) {
    return;
  }
  const pos = getCanvasRelativePosition(e, ctx.canvas);
  const force = e.force || 1;
  
  const line = setupLine(lastX, lastY, pos.x, pos.y);  
  for (let more = true; more;) {
    more = advanceLine(line);
    ctx.globalAlpha = alpha * lerp(lastForce, force, line.u);
    ctx.drawImage(
       brushCtx.canvas,
       line.position[0] - brushCtx.canvas.width / 2,
       line.position[1] - brushCtx.canvas.height / 2);
     updateBrush(line.position[0], line.position[1]);
  } 
  lastX = pos.x;
  lastY = pos.y;
  lastForce = force;
}

function stop() {
  drawing = false;
}

window.addEventListener('mousedown', start);
window.addEventListener('mousemove', draw);
window.addEventListener('mouseup', stop);
window.addEventListener('touchstart', e => {
  e.preventDefault();
  start(e.touches[0]);
}, {passive: false});
window.addEventListener('touchmove', e => {
  e.preventDefault();
  draw(e.touches[0]);
}, {passive: false});
#canvas { border: 1px solid black; }
.controls { margin-left: 5px; }
.split { display: flex; }
* { user-select: none; }
<div class="split">
  <canvas id="canvas"></canvas>
  <div>
    <div class="controls">
      <div>
        <div><input type="range" id="radius" min="2" max="40" value="16"><label for="radius">radius</label></div>
        <div><input type="range" id="hardness" min="0" max="1" step="0.01" value="0.5"><label for="radius">hardness</label></div>
        <div><input type="range" id="alpha" min="0" max="1" step="0.01" value="0.5"><label for="alpha">alpha</label></div>
        <button type="button" id="reset">reset</button>
      </div>
      <div style="text-align: right;">
        <canvas id="brush-display" width="80" height="80"></canvas>
      </div>
    </div>
  </div>
</div>

Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!