canvas小画板——笔锋效果实现
效果結(jié)尾處可驗(yàn)收。
畫線準(zhǔn)備
準(zhǔn)備一個(gè)canvas
<canvas id="canvasId" width="1000" height="800"></canvas>
使用pointer事件監(jiān)聽,落筆,拖拽,收筆。
document.onpointerdown = function (e) {
if (e.type == "touchstart")
handwriting.down(e.touches[0].pageX, e.touches[0].pageY);
else
handwriting.down(e.x, e.y);
}
document.onpointermove = function (e) {
if (e.type == "touchmove")
handwriting.move(e.touches[0].pageX, e.touches[0].pageY);
else
handwriting.move(e.x, e.y);
}
document.onpointerup = function (e) {
if (e.type == "touchend")
handwriting.up(e.touches[0].pageX, e.touches[0].pageY);
else
handwriting.up(e.x, e.y);
}
主要的邏輯在Handwritinglff 上,存儲了當(dāng)前繪制中的線條的所有點(diǎn)集合,所有繪制過的線條集合pointLines 。
class Handwritinglff {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext("2d")
this.line = new Line();
this.pointLines = new Array();//Line數(shù)組
this.k = 0.5;
this.begin = null;
this.middle = null;
this.end = null;
this.lineWidth = 10;
this.isDown = false;
}
down事件的時(shí)候初始化當(dāng)前繪制線條line;
move事件的時(shí)候?qū)Ⅻc(diǎn)加入到當(dāng)前線條line,并開始繪制
up的時(shí)候?qū)Ⅻc(diǎn)加入繪制線條,并繪制完整一條線。
需要注意的點(diǎn):
加入點(diǎn)的時(shí)候,距離太近的點(diǎn)不需要重復(fù)添加;
怎么形成筆鋒效果呢
很簡單!就是在一條線段的最后幾個(gè)點(diǎn)的lineWidth不斷減小,我們這里選用了最后6個(gè)點(diǎn),如果只選用六個(gè)階梯變化,效果是很難看的,會(huì)看到一節(jié)節(jié)明顯的線條變細(xì)的過程,如下圖:
所以我們有個(gè)關(guān)鍵的補(bǔ)點(diǎn)過程,我們會(huì)再每6個(gè)像素之間補(bǔ)一個(gè)點(diǎn),根據(jù)線條粗細(xì)變化的范圍和最后計(jì)算出來的點(diǎn)數(shù),就可以知道每兩點(diǎn)連線lineWidth的粗細(xì)。
這里的補(bǔ)點(diǎn)過程我們用到了在貝塞爾曲線上補(bǔ)點(diǎn)的算法。具體不明白的可以留言哈
bezierCalculate(poss, precision) {
//維度,坐標(biāo)軸數(shù)(二維坐標(biāo),三維坐標(biāo)...)
let dimersion = 2;
//貝塞爾曲線控制點(diǎn)數(shù)(階數(shù))
let number = poss.length;
//控制點(diǎn)數(shù)不小于 2 ,至少為二維坐標(biāo)系
if (number < 2 || dimersion < 2)
return null;
let result = new Array();
//計(jì)算楊輝三角
let mi = new Array();
mi[0] = mi[1] = 1;
for (let i = 3; i <= number; i++) {
let t = new Array();
for (let j = 0; j < i - 1; j++) {
t[j] = mi[j];
}
mi[0] = mi[i - 1] = 1;
for (let j = 0; j < i - 2; j++) {
mi[j + 1] = t[j] + t[j + 1];
}
}
//計(jì)算坐標(biāo)點(diǎn)
for (let i = 0; i < precision; i++) {
let t = i / precision;
let p = new Point(0, 0);
result.push(p);
for (let j = 0; j < dimersion; j++) {
let temp = 0.0;
for (let k = 0; k < number; k++) {
temp += Math.pow(1 - t, number - k - 1) * (j == 0 ? poss[k].x : poss[k].y) * Math.pow(t, k) * mi[k];
}
j == 0 ? p.x = temp : p.y = temp;
}
p.x = this.toDecimal(p.x);
p.y = this.toDecimal(p.y);
}
return result;
}
部分代碼如下;
addPoint(p) {
if (this.line.points.length >= 1) {
let last_point = this.line.points[this.line.points.length - 1]
let distance = this.z_distance(p, last_point);
if (distance < 10) {
return;
}
}
if (this.line.points.length == 0) {
this.begin = p;
p.isControl = true;
this.pushPoint(p);
} else {
this.middle = p;
let controlPs = this.computeControlPoints(this.k, this.begin, this.middle, null);
this.pushPoint(controlPs.first);
this.pushPoint(p);
p.isControl = true;
this.begin = this.middle;
}
}
addPoint
draw(isUp = false) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.strokeStyle = "rgba(255,20,87,1)";
//繪制不包含this.line的線條
this.pointLines.forEach((line, index) => {
let points = line.points;
this.ctx.beginPath();
this.ctx.ellipse(points[0].x - 1.5, points[0].y, 6, 3, Math.PI / 4, 0, Math.PI * 2);
this.ctx.fill();
this.ctx.beginPath();
this.ctx.moveTo(points[0].x, points[0].y);
let lastW = line.lineWidth;
this.ctx.lineWidth = line.lineWidth;
this.ctx.lineJoin = "round";
this.ctx.lineCap = "round";
let minLineW = line.lineWidth / 4;
let isChangeW = false;
let changeWidthCount = line.changeWidthCount;
for (let i = 1; i <= points.length; i++) {
if (i == points.length) {
this.ctx.stroke();
break;
}
if (i > points.length - changeWidthCount) {
if (!isChangeW) {
this.ctx.stroke();//將之前的線條不變的path繪制完
isChangeW = true;
if (i > 1 && points[i - 1].isControl)
continue;
}
let w = (lastW - minLineW) / changeWidthCount * (points.length - i) + minLineW;
points[i - 1].lineWidth = w;
this.ctx.beginPath();//為了開啟新的路徑 否則每次stroke 都會(huì)把之前的路徑在描一遍
// this.ctx.strokeStyle = "rgba("+Math.random()*255+","+Math.random()*255+","+Math.random()*255+",1)";
this.ctx.lineWidth = w;
this.ctx.moveTo(points[i - 1].x, points[i - 1].y);//移動(dòng)到之前的點(diǎn)
this.ctx.lineTo(points[i].x, points[i].y);
this.ctx.stroke();//將之前的線條不變的path繪制完
} else {
if (points[i].isControl && points[i + 1]) {
this.ctx.quadraticCurveTo(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y);
} else if (i >= 1 && points[i - 1].isControl) {//上一個(gè)是控制點(diǎn) 當(dāng)前點(diǎn)已經(jīng)被繪制
} else
this.ctx.lineTo(points[i].x, points[i].y);
}
}
})
//繪制this.line線條
let points;
if (isUp)
points = this.line.points;
else
points = this.line.points.clone();
//當(dāng)前繪制的線條最后幾個(gè)補(bǔ)點(diǎn) 貝塞爾方式增加點(diǎn)
let count = 0;
let insertCount = 0;
let i = points.length - 1;
let endPoint = points[i];
let controlPoint;
let startPoint;
while (i >= 0) {
if (points[i].isControl == true) {
controlPoint = points[i];
count++;
} else {
startPoint = points[i];
}
if (startPoint && controlPoint && endPoint) {//使用貝塞爾計(jì)算補(bǔ)點(diǎn)
let dis = this.z_distance(startPoint, controlPoint) + this.z_distance(controlPoint, endPoint);
let insertPoints = this.BezierCalculate([startPoint, controlPoint, endPoint], Math.floor(dis / 6) + 1);
insertPoints.splice(0, 1);
insertCount += insertPoints.length;
var index = i;//插入位置
// 把a(bǔ)rr2 變成一個(gè)適合splice的數(shù)組(包含splice前2個(gè)參數(shù)的數(shù)組)
insertPoints.unshift(index, 1);
Array.prototype.splice.apply(points, insertPoints);
//補(bǔ)完點(diǎn)后
endPoint = startPoint;
startPoint = null;
}
if (count >= 6)
break;
i--;
}
//確定最后線寬變化的點(diǎn)數(shù)
let changeWidthCount = count + insertCount;
if (isUp)
this.line.changeWidthCount = changeWidthCount;
//制造橢圓頭
this.ctx.fillStyle = "rgba(255,20,87,1)"
this.ctx.beginPath();
this.ctx.ellipse(points[0].x - 1.5, points[0].y, 6, 3, Math.PI / 4, 0, Math.PI * 2);
this.ctx.fill();
this.ctx.beginPath();
this.ctx.moveTo(points[0].x, points[0].y);
let lastW = this.line.lineWidth;
this.ctx.lineWidth = this.line.lineWidth;
this.ctx.lineJoin = "round";
this.ctx.lineCap = "round";
let minLineW = this.line.lineWidth / 4;
let isChangeW = false;
for (let i = 1; i <= points.length; i++) {
if (i == points.length) {
this.ctx.stroke();
break;
}
//最后的一些點(diǎn)線寬變細(xì)
if (i > points.length - changeWidthCount) {
if (!isChangeW) {
this.ctx.stroke();//將之前的線條不變的path繪制完
isChangeW = true;
if (i > 1 && points[i - 1].isControl)
continue;
}
//計(jì)算線寬
let w = (lastW - minLineW) / changeWidthCount * (points.length - i) + minLineW;
points[i - 1].lineWidth = w;
this.ctx.beginPath();//為了開啟新的路徑 否則每次stroke 都會(huì)把之前的路徑在描一遍
// this.ctx.strokeStyle = "rgba(" + Math.random() * 255 + "," + Math.random() * 255 + "," + Math.random() * 255 + ",0.5)";
this.ctx.lineWidth = w;
this.ctx.moveTo(points[i - 1].x, points[i - 1].y);//移動(dòng)到之前的點(diǎn)
this.ctx.lineTo(points[i].x, points[i].y);
this.ctx.stroke();//將之前的線條不變的path繪制完
} else {
if (points[i].isControl && points[i + 1]) {
this.ctx.quadraticCurveTo(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y);
} else if (i >= 1 && points[i - 1].isControl) {//上一個(gè)是控制點(diǎn) 當(dāng)前點(diǎn)已經(jīng)被繪制
} else
this.ctx.lineTo(points[i].x, points[i].y);
}
}
}
draw
最終效果
動(dòng)手試試:拖拽寫字即可
github地址:https://github.com/fangsmile/penlineHandWriting
相關(guān)文章:
平滑曲線
實(shí)現(xiàn)蠟筆熒光筆效果
實(shí)現(xiàn)筆鋒效果
畫筆性能優(yōu)化
清除canvas畫布內(nèi)容--點(diǎn)擦除+線擦除
總結(jié)
以上是生活随笔為你收集整理的canvas小画板——笔锋效果实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Win10系统的SurfacePro4的
- 下一篇: 焊盘的solder层尺寸与焊盘实际大小的