在绘画程序中实现喷枪效果(使用压力板的倾斜度)

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>