畫線準備
準備一個canvas
<canvas id="canvasId" width="1000" height="800"></canvas>
使用pointer事件監聽,落筆,拖拽,收筆。
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 上,儲存了當前繪製中的線條的所有點集合,所有繪製過的線條集合pointLines 。
class Handwritinglff { constructor(canvas) { this.canvas = canvas; this.ctx = canvas.getContext("2d") this.line = new Line(); this.pointLines = new Array();//Line陣列 this.k = 0.5; this.begin = null; this.middle = null; this.end = null; this.lineWidth = 10; this.isDown = false; }
down事件的時候初始化當前繪製線條line;
move事件的時候將點加入到當前線條line,並開始繪製
up的時候將點加入繪製線條,並繪製完整一條線。
需要注意的點:
加入點的時候,距離太近的點不需要重複新增;
怎麼形成筆鋒效果呢
很簡單!就是在一條線段的最後幾個點的lineWidth不斷減小,我們這裡選用了最後6個點,如果只選用六個階梯變化,效果是很難看的,會看到一節節明顯的線條變細的過程,如下圖:
所以我們有個關鍵的補點過程,我們會再每6個畫素之間補一個點,根據線條粗細變化的範圍和最後計算出來的點數,就可以知道每兩點連線lineWidth的粗細。
這裡的補點過程我們用到了在貝塞爾曲線上補點的演算法。具體不明白的可以留言哈
bezierCalculate(poss, precision) { //維度,座標軸數(二維座標,三維座標...) let dimersion = 2; //貝塞爾曲線控制點數(階數) let number = poss.length; //控制點數不小於 2 ,至少為二維座標系 if (number < 2 || dimersion < 2) return null; let result = new Array(); //計算楊輝三角 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]; } } //計算座標點 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; } }
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 都會把之前的路徑在描一遍 // 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);//移動到之前的點 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) {//上一個是控制點 當前點已經被繪製 } 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(); //當前繪製的線條最後幾個補點 貝塞爾方式增加點 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) {//使用貝塞爾計算補點 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;//插入位置 // 把arr2 變成一個適合splice的陣列(包含splice前2個引數的陣列) insertPoints.unshift(index, 1); Array.prototype.splice.apply(points, insertPoints); //補完點後 endPoint = startPoint; startPoint = null; } if (count >= 6) break; i--; } //確定最後線寬變化的點數 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; } //最後的一些點線寬變細 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 都會把之前的路徑在描一遍 // 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);//移動到之前的點 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) {//上一個是控制點 當前點已經被繪製 } else this.ctx.lineTo(points[i].x, points[i].y); } } }
最終效果
動手試試:拖拽寫字即可
相關文章: