Инструмент кисти для растушевки HTML5 Canvas JavaScript

Мне нужна идея, как я могу сделать кисть, которая может растушевывать цвета.

Пример на картинке: рисование правой стороны основной кистью с двумя разными цветами слева также рисование, но с дополнительным использованием инструмента «Размазывание», результат должен быть примерно таким же, как слева

введите здесь описание изображения

Мне нужен совет, как я могу попробовать это сделать


person Your choice    schedule 28.01.2015    source источник
comment
Я думаю, вы ищете математику. Математика, я думаю, это то, что вам нужно здесь.   -  person sircodesalot    schedule 28.01.2015


Ответы (2)


Вам нужно будет манипулировать пикселями, чтобы добиться эффекта размытия.

Вы можете получить информацию о пикселях с холста, используя context.getImageData.

Когда пользователь перемещает воображаемую кисть по существующим пикселям, вы можете имитировать размазывание настоящей кистью следующим образом:

  1. Используйте данные изображения, чтобы вычислить средний цвет, который пользователь уже переместил.

  2. Установите fillStyle на этот средний цвет.

  3. Установите для альфы fillStyle полупрозрачное значение (возможно, 25%).

  4. Когда пользователь перетаскивает кисть, нарисуйте серию перекрывающихся кругов поверх существующих пикселей, используя полупрозрачную заливку с усреднением цвета.

  5. Если конкретное клиентское устройство имеет большую вычислительную мощность, вы можете усилить эффект с помощью затенения.

person markE    schedule 28.01.2015

Вот одна попытка

  1. При нажатии мыши захватите копию области под мышью на отдельный холст.

  2. При перемещении мыши рисуйте, копируя по одному пикселю из предыдущей позиции мыши в текущую позицию мыши с альфа-каналом 50%, захватывая новую копию после каждого перемещения.

В псевдокоде

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

Кисть растушевывается путем рисования радиального градиента от rgba(0,0,0,0) до rgba(0,0,0,1) с помощью 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>

person gman    schedule 23.05.2020