HTML5 Canvas javascript 涂抹笔刷工具
HTML5 Canvas javascript smudge brush tool
我需要知道如何制作可以涂抹颜色的画笔。
图片中的示例:右侧使用两种不同颜色的基本画笔绘画,左侧也绘画但额外使用涂抹工具,结果应该类似于左侧
我需要建议如何去做
您将需要操纵像素来实现涂抹效果。
您可以使用 context.getImageData
从 canvas 中获取像素信息。
当用户在现有像素上移动假想画笔时,您可以通过以下方式模拟使用真实画笔涂抹:
使用图像数据计算用户到目前为止移动的平均颜色。
将 fillStyle
设置为该平均颜色。
将 fillStyle 的 alpha 设置为半透明值(可能是 25%)。
当用户拖动画笔时,使用半透明、颜色平均的填充在现有像素上绘制一系列重叠的圆圈。
如果特定客户端设备具有更多处理能力,您可以通过阴影增强效果。
这是一次尝试
在 mousedown 上将鼠标下方区域的副本复制到单独的 canvas
在 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>
我需要知道如何制作可以涂抹颜色的画笔。
图片中的示例:右侧使用两种不同颜色的基本画笔绘画,左侧也绘画但额外使用涂抹工具,结果应该类似于左侧
我需要建议如何去做
您将需要操纵像素来实现涂抹效果。
您可以使用 context.getImageData
从 canvas 中获取像素信息。
当用户在现有像素上移动假想画笔时,您可以通过以下方式模拟使用真实画笔涂抹:
使用图像数据计算用户到目前为止移动的平均颜色。
将
fillStyle
设置为该平均颜色。将 fillStyle 的 alpha 设置为半透明值(可能是 25%)。
当用户拖动画笔时,使用半透明、颜色平均的填充在现有像素上绘制一系列重叠的圆圈。
如果特定客户端设备具有更多处理能力,您可以通过阴影增强效果。
这是一次尝试
在 mousedown 上将鼠标下方区域的副本复制到单独的 canvas
在 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'
.
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>