在绘画程序中实现喷枪效果(使用压力板的倾斜度)
Implementing an airbrush effect in paint program (using tilt from pressure tablet)
我正在尝试在绘画程序中实现 "airbrush" 效果,它利用了支持 tiltX/tiltY 的平板电脑。在 Corel Painter 中,这样的效果看起来像
来自数位板的信息:tiltX是笔在XY平面的角度,tiltY是笔在YZ平面的角度。所以我想喷枪效果可以实现,就好像笔上有一个圆锥体,在圆锥体半径内的 canvas 范围内喷射点。从侧面看,我想象的是这样的:
有谁知道用数学方法来计算 x/y 圆锥体中以随机方式放置在 canvas 上的点的坐标。
一个"spread"值也不错,如下图:
投影点
带有半径、倾斜度、中心位置和方向的喷枪喷射。
通过将问题转换为 3d,您可以在一个平面上创建一组点(均匀分布在一个圆中),该平面的斜率为倾斜角的正弦值。
因此,通过添加根据 x 位置倾斜的 z,相对于中心的 2D 点 x,y 移动到 3d 平面。远离源的距离 >= 到圆的半径
z = radius + x * sin(tilt);
然后通过除以新的 z 除以半径将 3D 点投影回 2D 平面
x = x / (z / radius);
y = y / (z / radius);
现在您只需将二维点旋转到正确的方向即可。
首先得到喷雾的方向作为归一化向量。
nx = cos(direction);
ny = sin(direction);
然后旋转 2D 点以与该向量对齐
xx = x * nx - y * ny;
yy = x * ny + y * nx;
然后您将投影点添加到中心并绘制。
setPixel(xx + centerX, yy + centerY);
均匀分布的圆形喷雾。
在圆形区域上均匀喷洒需要非均匀随机函数
angle = rand(Math.PI * 2); // get a random direction
dist = randL(rad,0); // get a random distance.
x = cos(angle) * dist; // find the point.
y = sin(angle) * dist;
函数rand(num)
returns随机数从0到num均匀分布
函数 randL(min,max)
returns 一个随机数,它具有从最小值(最可能)到最大值(极不可能)的线性分布。查看代码了解更多信息。
JS 中的示例
由于鼠标不能倾斜,喷雾固定在中心,将鼠标从中心移开会改变倾斜度和方向。
// set up mouse
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
const ctx = canvas.getContext("2d");
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
// the random functions
const rand = (min, max = min + (min = 0)) => Math.random() * (max - min) + min;
const randL = (min, max = min + (min = 0)) => Math.abs(Math.random() + Math.random() - 1) * (max - min) + min;
// shorthand for loop
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); }; // the ; after while loop is important don't remove
// draws a set of points around cx,cy and a radius of rad
// density is the number of pixels set per pixel
// tilt is the spray tilt in radians
// dir is the direction
const rad = 40;
function spray(rad,cx,cy,density=0.2,tilt, dir){
const count = ((rad * rad * Math.PI) * density) | 0;
var xA = Math.cos(dir);
var yA = Math.sin(dir);
doFor(count,i=>{
const angle = rand(Math.PI * 2);
const dist = randL(rad,0);
var x = Math.cos(angle) * dist;
var y = Math.sin(angle) * dist;
const z = rad + x * Math.sin(tilt);
x = x / (z / rad);
y = y / (z/ rad);
var xx = x * xA - y * yA;
var yy = x * yA + y * xA;
ctx.fillRect(xx + cx, yy + cy,1,1);;
})
}
function circle(rad,cx,cy,tilt, dir){
var xA = Math.cos(dir);
var yA = Math.sin(dir);
ctx.beginPath();
for(var i = 0; i <= 100; i ++){
var ang = (i / 100) * Math.PI * 2;
var x = Math.cos(ang) * rad
var y = Math.sin(ang) * rad
var z = rad + x * Math.sin(tilt);
x = x / (z / rad);
y = y / (z/ rad);
var xx = x * xA - y * yA;
var yy = x * yA + y * xA;
ctx.lineTo(xx + cx,yy + cy);
}
ctx.stroke();
}
function update(){
if(w !== innerWidth || h !== innerHeight){
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
}else{
ctx.clearRect(0,0,w,h);
}
var dist = Math.hypot(cw-mouse.x,ch-mouse.y);
var tilt = Math.atan2(dist,100);
var dir = Math.atan2(ch-mouse.y,cw-mouse.x);
circle(rad,cw,ch,tilt,dir);
spray(rad,cw,ch,0.2,tilt,dir)
requestAnimationFrame(update);
}
update();
canvas { position : absolute; top : 0px; left: 0px; }
<canvas id="canvas"></canvas>
传播
正如评论中所要求的那样,可以很容易地将扩展值添加为倾斜角度的一个因素。它只是随着倾斜度的增加而增加圆的 y 半径。
spreadRad = rad * (1 + (tilt / PI) * spread); // PI = 3.1415...
这样点函数就变成了
angle = rand(Math.PI * 2); // get a random direction
dist = randL(rad,0); // get a random distance.
x = cos(angle) * dist; // find the point.
y = sin(angle) * dist * (1 + (tilt / PI) * spread); // PI = 3.1415...;
但这并不像喷雾那样容易实现,因为它改变了点的分布。我已将其添加到喷雾功能中。喷雾面积随扩散 radiusB
(椭圆面积 = PI * radiusA * radiusB
)而增加,因此我计算椭圆上的密度。虽然我不是 100% 确定覆盖范围是否在该地区保持不变。我将不得不进行实验以确定该解决方案是否是一个好的解决方案。
示例显示的扩散系数为1.5,红圈为原始未扩散区域。我还包括鼠标按下以添加喷雾,这样您就可以看到它是如何累积的(我已将 alpha 值设置为 0.25)。重新运行以清除。
const rad = 40; // radius of spray
const spread = 1.5; // linear spread as tilt increases
// set up mouse
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
const ctx = canvas.getContext("2d");
const image = document.createElement("canvas");
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
// the random functions
const rand = (min, max = min + (min = 0)) => Math.random() * (max - min) + min;
const randL = (min, max = min + (min = 0)) => Math.abs(Math.random() + Math.random() - 1) * (max - min) + min;
// shorthand for loop
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); }; // the ; after while loop is important don't remove
// draws a set of points around cx,cy and a radius of rad
// density is the number of pixels set per pixel
// tilt is the spray tilt in radians
// dir is the direction
function spray(ctx,rad,cx,cy,density=0.2,tilt, dir){
const spreadRad = rad * (1 + (tilt / Math.PI) * spread);
const count = ((rad * spreadRad * Math.PI) * density) | 0;
var xA = Math.cos(dir);
var yA = Math.sin(dir);
doFor(count,i=>{
const angle = rand(Math.PI * 2);
const dist = randL(rad,0);
var x = Math.cos(angle) * dist;
var y = Math.sin(angle) * dist * (1 + (tilt / Math.PI) * spread);
const z = rad + x * Math.sin(tilt);
x = x / (z / rad);
y = y / (z/ rad);
var xx = x * xA - y * yA;
var yy = x * yA + y * xA;
ctx.fillRect(xx + cx, yy + cy,1,1);;
})
}
function circle(rad,cx,cy,tilt, dir, spread){
var xA = Math.cos(dir);
var yA = Math.sin(dir);
const spreadRad = rad * (1 + (tilt / Math.PI) * spread);
ctx.globalAlpha = 0.5;
ctx.beginPath();
for(var i = 0; i <= 100; i ++){
var ang = (i / 100) * Math.PI * 2;
var x = Math.cos(ang) * rad;
var y = Math.sin(ang) * spreadRad;
var z = rad + x * Math.sin(tilt);
x = x / (z / rad);
y = y / (z/ rad);
var xx = x * xA - y * yA;
var yy = x * yA + y * xA;
ctx.lineTo(xx + cx,yy + cy);
}
ctx.stroke();
ctx.globalAlpha = 1;
}
function update(){
if(w !== innerWidth || h !== innerHeight){
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
image.width = w;
image.height = h;
}else{
ctx.clearRect(0,0,w,h);
}
ctx.drawImage(image,0,0);
var dist = Math.hypot(cw-mouse.x,ch-mouse.y);
var tilt = Math.atan2(dist,100);
var dir = Math.atan2(ch-mouse.y,cw-mouse.x);
ctx.strokeStyle = "red";
circle(rad,cw,ch,tilt,dir,0);
ctx.strokeStyle = "black";
circle(rad,cw,ch,tilt,dir,spread);
if(mouse.button){
const ct = image.getContext("2d");
ct.globalAlpha = 0.25;
spray(ct,rad,cw,ch,0.2,tilt,dir,spread);
ct.globalAlpha = 1;
}else{
spray(ctx,rad,cw,ch,0.2,tilt,dir,spread);
}
requestAnimationFrame(update);
}
update();
canvas { position : absolute; top : 0px; left: 0px; }
<canvas id="canvas"></canvas>
我正在尝试在绘画程序中实现 "airbrush" 效果,它利用了支持 tiltX/tiltY 的平板电脑。在 Corel Painter 中,这样的效果看起来像
来自数位板的信息:tiltX是笔在XY平面的角度,tiltY是笔在YZ平面的角度。所以我想喷枪效果可以实现,就好像笔上有一个圆锥体,在圆锥体半径内的 canvas 范围内喷射点。从侧面看,我想象的是这样的:
有谁知道用数学方法来计算 x/y 圆锥体中以随机方式放置在 canvas 上的点的坐标。
一个"spread"值也不错,如下图:
投影点
带有半径、倾斜度、中心位置和方向的喷枪喷射。
通过将问题转换为 3d,您可以在一个平面上创建一组点(均匀分布在一个圆中),该平面的斜率为倾斜角的正弦值。
因此,通过添加根据 x 位置倾斜的 z,相对于中心的 2D 点 x,y 移动到 3d 平面。远离源的距离 >= 到圆的半径
z = radius + x * sin(tilt);
然后通过除以新的 z 除以半径将 3D 点投影回 2D 平面
x = x / (z / radius);
y = y / (z / radius);
现在您只需将二维点旋转到正确的方向即可。
首先得到喷雾的方向作为归一化向量。
nx = cos(direction);
ny = sin(direction);
然后旋转 2D 点以与该向量对齐
xx = x * nx - y * ny;
yy = x * ny + y * nx;
然后您将投影点添加到中心并绘制。
setPixel(xx + centerX, yy + centerY);
均匀分布的圆形喷雾。
在圆形区域上均匀喷洒需要非均匀随机函数
angle = rand(Math.PI * 2); // get a random direction
dist = randL(rad,0); // get a random distance.
x = cos(angle) * dist; // find the point.
y = sin(angle) * dist;
函数rand(num)
returns随机数从0到num均匀分布
函数 randL(min,max)
returns 一个随机数,它具有从最小值(最可能)到最大值(极不可能)的线性分布。查看代码了解更多信息。
JS 中的示例
由于鼠标不能倾斜,喷雾固定在中心,将鼠标从中心移开会改变倾斜度和方向。
// set up mouse
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
const ctx = canvas.getContext("2d");
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
// the random functions
const rand = (min, max = min + (min = 0)) => Math.random() * (max - min) + min;
const randL = (min, max = min + (min = 0)) => Math.abs(Math.random() + Math.random() - 1) * (max - min) + min;
// shorthand for loop
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); }; // the ; after while loop is important don't remove
// draws a set of points around cx,cy and a radius of rad
// density is the number of pixels set per pixel
// tilt is the spray tilt in radians
// dir is the direction
const rad = 40;
function spray(rad,cx,cy,density=0.2,tilt, dir){
const count = ((rad * rad * Math.PI) * density) | 0;
var xA = Math.cos(dir);
var yA = Math.sin(dir);
doFor(count,i=>{
const angle = rand(Math.PI * 2);
const dist = randL(rad,0);
var x = Math.cos(angle) * dist;
var y = Math.sin(angle) * dist;
const z = rad + x * Math.sin(tilt);
x = x / (z / rad);
y = y / (z/ rad);
var xx = x * xA - y * yA;
var yy = x * yA + y * xA;
ctx.fillRect(xx + cx, yy + cy,1,1);;
})
}
function circle(rad,cx,cy,tilt, dir){
var xA = Math.cos(dir);
var yA = Math.sin(dir);
ctx.beginPath();
for(var i = 0; i <= 100; i ++){
var ang = (i / 100) * Math.PI * 2;
var x = Math.cos(ang) * rad
var y = Math.sin(ang) * rad
var z = rad + x * Math.sin(tilt);
x = x / (z / rad);
y = y / (z/ rad);
var xx = x * xA - y * yA;
var yy = x * yA + y * xA;
ctx.lineTo(xx + cx,yy + cy);
}
ctx.stroke();
}
function update(){
if(w !== innerWidth || h !== innerHeight){
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
}else{
ctx.clearRect(0,0,w,h);
}
var dist = Math.hypot(cw-mouse.x,ch-mouse.y);
var tilt = Math.atan2(dist,100);
var dir = Math.atan2(ch-mouse.y,cw-mouse.x);
circle(rad,cw,ch,tilt,dir);
spray(rad,cw,ch,0.2,tilt,dir)
requestAnimationFrame(update);
}
update();
canvas { position : absolute; top : 0px; left: 0px; }
<canvas id="canvas"></canvas>
传播
正如评论中所要求的那样,可以很容易地将扩展值添加为倾斜角度的一个因素。它只是随着倾斜度的增加而增加圆的 y 半径。
spreadRad = rad * (1 + (tilt / PI) * spread); // PI = 3.1415...
这样点函数就变成了
angle = rand(Math.PI * 2); // get a random direction
dist = randL(rad,0); // get a random distance.
x = cos(angle) * dist; // find the point.
y = sin(angle) * dist * (1 + (tilt / PI) * spread); // PI = 3.1415...;
但这并不像喷雾那样容易实现,因为它改变了点的分布。我已将其添加到喷雾功能中。喷雾面积随扩散 radiusB
(椭圆面积 = PI * radiusA * radiusB
)而增加,因此我计算椭圆上的密度。虽然我不是 100% 确定覆盖范围是否在该地区保持不变。我将不得不进行实验以确定该解决方案是否是一个好的解决方案。
示例显示的扩散系数为1.5,红圈为原始未扩散区域。我还包括鼠标按下以添加喷雾,这样您就可以看到它是如何累积的(我已将 alpha 值设置为 0.25)。重新运行以清除。
const rad = 40; // radius of spray
const spread = 1.5; // linear spread as tilt increases
// set up mouse
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
const ctx = canvas.getContext("2d");
const image = document.createElement("canvas");
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
// the random functions
const rand = (min, max = min + (min = 0)) => Math.random() * (max - min) + min;
const randL = (min, max = min + (min = 0)) => Math.abs(Math.random() + Math.random() - 1) * (max - min) + min;
// shorthand for loop
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); }; // the ; after while loop is important don't remove
// draws a set of points around cx,cy and a radius of rad
// density is the number of pixels set per pixel
// tilt is the spray tilt in radians
// dir is the direction
function spray(ctx,rad,cx,cy,density=0.2,tilt, dir){
const spreadRad = rad * (1 + (tilt / Math.PI) * spread);
const count = ((rad * spreadRad * Math.PI) * density) | 0;
var xA = Math.cos(dir);
var yA = Math.sin(dir);
doFor(count,i=>{
const angle = rand(Math.PI * 2);
const dist = randL(rad,0);
var x = Math.cos(angle) * dist;
var y = Math.sin(angle) * dist * (1 + (tilt / Math.PI) * spread);
const z = rad + x * Math.sin(tilt);
x = x / (z / rad);
y = y / (z/ rad);
var xx = x * xA - y * yA;
var yy = x * yA + y * xA;
ctx.fillRect(xx + cx, yy + cy,1,1);;
})
}
function circle(rad,cx,cy,tilt, dir, spread){
var xA = Math.cos(dir);
var yA = Math.sin(dir);
const spreadRad = rad * (1 + (tilt / Math.PI) * spread);
ctx.globalAlpha = 0.5;
ctx.beginPath();
for(var i = 0; i <= 100; i ++){
var ang = (i / 100) * Math.PI * 2;
var x = Math.cos(ang) * rad;
var y = Math.sin(ang) * spreadRad;
var z = rad + x * Math.sin(tilt);
x = x / (z / rad);
y = y / (z/ rad);
var xx = x * xA - y * yA;
var yy = x * yA + y * xA;
ctx.lineTo(xx + cx,yy + cy);
}
ctx.stroke();
ctx.globalAlpha = 1;
}
function update(){
if(w !== innerWidth || h !== innerHeight){
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
image.width = w;
image.height = h;
}else{
ctx.clearRect(0,0,w,h);
}
ctx.drawImage(image,0,0);
var dist = Math.hypot(cw-mouse.x,ch-mouse.y);
var tilt = Math.atan2(dist,100);
var dir = Math.atan2(ch-mouse.y,cw-mouse.x);
ctx.strokeStyle = "red";
circle(rad,cw,ch,tilt,dir,0);
ctx.strokeStyle = "black";
circle(rad,cw,ch,tilt,dir,spread);
if(mouse.button){
const ct = image.getContext("2d");
ct.globalAlpha = 0.25;
spray(ct,rad,cw,ch,0.2,tilt,dir,spread);
ct.globalAlpha = 1;
}else{
spray(ctx,rad,cw,ch,0.2,tilt,dir,spread);
}
requestAnimationFrame(update);
}
update();
canvas { position : absolute; top : 0px; left: 0px; }
<canvas id="canvas"></canvas>