HTML5 Canvas javascript 涂抹笔刷工具

HTML5 Canvas javascript smudge brush tool

我需要知道如何制作可以涂抹颜色的画笔。

图片中的示例:右侧使用两种不同颜色的基本画笔绘画,左侧也绘画但额外使用涂抹工具,结果应该类似于左侧

我需要建议如何去做

您将需要操纵像素来实现涂抹效果。

您可以使用 context.getImageData 从 canvas 中获取像素信息。

当用户在现有像素上移动假想画笔时,您可以通过以下方式模拟使用真实画笔涂抹:

  1. 使用图像数据计算用户到目前为止移动的平均颜色。

  2. fillStyle 设置为该平均颜色。

  3. 将 fillStyle 的 alpha 设置为半透明值(可能是 25%)。

  4. 当用户拖动画笔时,使用半透明、颜色平均的填充在现有像素上绘制一系列重叠的圆圈。

  5. 如果特定客户端设备具有更多处理能力,您可以通过阴影增强效果。

这是一次尝试

  1. 在 mousedown 上将鼠标下方区域的副本复制到单独的 canvas

  2. 在 mousemove 上绘制时,以 50% alpha 从前一个鼠标位置一次复制一个像素到当前鼠标位置,每次移动后抓取一个新副本。

在伪代码中

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

通过使用 globalCompositeOperation = 'destination-out'.

在其上绘制 rgba(0,0,0,0) 到 rgba(0,0,0,1) 径向渐变来羽化画笔

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>