canvas应用之写字板

584 阅读1分钟

往期文章

本文将讲解通过 canvas 实现写字的效果

效果

7upmo-m54k6.gif

代码

<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8">
	<meta name="viewport"
		content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
	<title>学写一个字</title>
	<style>
		body {
			overflow-x: hidden;
		}

		#canvas {
			display: block;
			margin: 25px auto 0;
		}

		#controller {
			width: 100%;
			padding: 10px;
			margin: 0 auto;
		}

		#controller .op_btn {
			float: right;
			margin: 10px 0 0 10px;
			border: 2px solid #aaa;
			width: 40px;
			height: 20px;
			line-height: 20px;
			font-size: 14px;
			text-align: center;
			-webkit-border-radius: 5px;
			-moz-border-radius: 5px;
			border-radius: 5px;
			cursor: pointer;
			background: #fff;
			font-weight: bold;
			font-family: Miscrosoft Yahei, Arial;
			*zoom: 1;
			margin-right: 3%;
		}

		#controller .op_btn:before,
		#controller .op_btn:after {
			display: table;
			content: '';
		}

		#controller .op_btn:after {
			clear: both;
		}

		#controller .op_btn:hover {
			background: #def;
		}

		#controller .color_btn {
			float: left;
			margin: 10px 10px 0 0;
			border: 5px solid #fff;
			width: 20px;
			height: 20px;
			-webkit-border-radius: 5px;
			-moz-border-radius: 5px;
			border-radius: 5px;
			cursor: pointer;
		}

		#controller .color_btn:hover {
			border: 5px solid violet;
		}

		#controller .color_btn_selected {
			border: 5px solid blueviolet;
		}

		#controller #black_btn {
			background: black;
		}

		#controller #blue_btn {
			background: blue;
		}

		#controller #green_btn {
			background: green;
		}

		#controller #red_btn {
			background: red;
		}

		#controller #orange_btn {
			background: orange;
		}

		#controller #yellow_btn {
			background: yellow;
		}
	</style>
</head>

<body>
	<canvas id="canvas">该浏览器不支持canvas</canvas>
	<div id="controller">
		<div id="black_btn" class="color_btn color_btn_selected"></div>
		<div id="blue_btn" class="color_btn"></div>
		<div id="green_btn" class="color_btn"></div>
		<div id="red_btn" class="color_btn"></div>
		<div id="orange_btn" class="color_btn"></div>
		<div id="yellow_btn" class="color_btn"></div>
		<div id="clear_btn" class="op_btn">清 除</div>
	</div>

	<script>
		class Handwrite {
			constructor(el) {
				this.canvas = el;
				this.ctx = this.canvas.getContext('2d');
				this.canvasWidth = Math.min(800, document.body.clientWidth - 20);
				this.canvasHeight = this.canvasWidth;
				this.strokeColor = 'black';
				this.isMouseDown = false;
				this.lastLoc = { x: 0, y: 0 }; //上一次鼠标挪动的位置
				this.lastTimeStmp = 0; // 上一次时间戳
				this.lastLineWidth = -1; // 上一次的线宽
				this.canvas.width = this.canvasWidth;
				this.canvas.height = this.canvasHeight;
				this._drawGrid();
				this._initEvent(el);
			}

			// 画面板及米字格
			_drawGrid() {
				this.ctx.save();

				//画一个矩形框
				this.ctx.strokeStyle = 'red';
				this.ctx.beginPath();
				this.ctx.moveTo(3, 3);
				this.ctx.lineTo(this.canvasWidth - 3, 3);
				this.ctx.lineTo(this.canvasWidth - 3, this.canvasHeight - 3);
				this.ctx.lineTo(3, this.canvasHeight - 3);
				this.ctx.closePath();
				this.ctx.lineWidth = 6;
				this.ctx.stroke();

				//画一个米字格
				this.ctx.beginPath();
				this.ctx.setLineDash([5, 5]); //参数:第一个是虚线的长度,第二个是两虚线段的间隔长度
				this.ctx.moveTo(0, 0);
				this.ctx.lineTo(this.canvasWidth, this.canvasHeight);
				this.ctx.moveTo(this.canvasWidth, 0);
				this.ctx.lineTo(0, this.canvasHeight);
				this.ctx.moveTo(this.canvasWidth / 2, 0);
				this.ctx.lineTo(this.canvasWidth / 2, this.canvasHeight);
				this.ctx.moveTo(0, this.canvasHeight / 2);
				this.ctx.lineTo(this.canvasWidth, this.canvasHeight / 2);
				this.ctx.lineWidth = 1;
				this.ctx.stroke();

				this.ctx.restore();
			}

			// 将鼠标位置转换成在canvas中的位置
			_windowToCanvas(x, y) {
				const { top, left } = this.canvas.getBoundingClientRect() || this.canvas.getClientRect();
				return {
					x: Math.round(x - left),
					y: Math.round(y - top)
				};
			}

			// 计算两点之间的直线距离: (x*x+ y*y)开平方 */
			_calcDistance(loc1, loc2) {
				return Math.sqrt((loc1.x - loc2.x) * (loc1.x - loc2.x) + (loc1.y - loc2.y) * (loc1.y - loc2.y));
			}
			/**
			 * 计算笔画的粗细
			 * 停留时间越长,笔画越粗
			 * @param  {[type]} t 时间
			 * @param  {[type]} s 距离
			 */
			_calcLineWidth(t, s) {
				const v = s / t;
				let res;
				var line = this.lastLineWidth || -1;
				if (v < 0.1) {
					res = 15;//最粗为15
				} else if (v > 10) {
					res = 1;//最细为1
				} else {
					//         1     v      15
					// res ------    |    -------
					//     v    |---------|
					//         0.1        10
					res = 15 - (v - 0.1) / (10 - 0.1) * (15 - 5);
				}

				if (line === -1) {//第一次
					return res;
				} else {//不至于前后变化太大
					return (line * 2 / 3 + res / 3);
				}
			}

			_draw(curLoc, lineWidth) {
				this.ctx.beginPath();
				this.ctx.moveTo(this.lastLoc.x, this.lastLoc.y);
				this.ctx.lineTo(curLoc.x, curLoc.y);
				this.ctx.strokeStyle = this.strokeColor;
				this.ctx.lineWidth = lineWidth;
				this.ctx.lineCap = 'round';//解决毛边问题
				this.ctx.lineJoin = 'round';//效果更佳平滑
				this.ctx.stroke();
			}

			_beginStroke(x, y) {
				this.isMouseDown = true;
				this.lastLoc = this._windowToCanvas(x, y);
				this.lastTimeStmp = new Date().getTime();
			}

			_endStroke() {
				this.isMouseDown = false;
			}

			_moveStroke(ex, ey) {
				var curLoc = this._windowToCanvas(ex, ey);
				var curTimeStmp = new Date().getTime();
				/*通过两点距离和时间实现笔画的粗细*/
				var s = this._calcDistance(curLoc, this.lastLoc);
				var t = curTimeStmp - this.lastTimeStmp;
				var lineWidth = this._calcLineWidth(t, s);

				this._draw(curLoc, lineWidth);

				this.lastLoc = curLoc;
				this.lastTimeStmp = curTimeStmp;
				this.lastLineWidth = lineWidth;
			}
			_initEvent(el) {
				const me = this;
				/*pc*/
				el.addEventListener('mousedown', (e) => {
					e.preventDefault();
					me._beginStroke(e.clientX, e.clientY);
				}, false);

				el.addEventListener('mouseup', (e) => {
					e.preventDefault();
					me._endStroke();
				}, false);

				el.addEventListener('mouseout', (e) => {
					e.preventDefault();
					me._endStroke();
				}, false);

				el.addEventListener('mousemove', (e) => {
					e.preventDefault();
					if (me.isMouseDown) {
						me._moveStroke(e.clientX, e.clientY);
					}
				}, false);


				/*mobile*/
				el.addEventListener('touchstart', (e) => {
					e.preventDefault();
					const { pageX, pageY } = e.touches[0];//不需要多点触控
					me._beginStroke(pageX, pageY);
				}, false);

				el.addEventListener('touchend', (e) => {
					e.preventDefault();
					me._endStroke();
				}, false);

				el.addEventListener('touchmove', (e) => {
					e.preventDefault();
					if (me.isMouseDown) {
						const { pageX, pageY } = e.touches[0];
						me._moveStroke(pageX, pageY);
					}
				}, false);
			}

			clear() {
				this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
				this._drawGrid();
			}

			selColor(color) {
				this.strokeColor = color;
			}

		}
	</script>

	<script>
		const canvas = document.getElementById('canvas');
		const controller = document.getElementById('controller');
		const hand = new Handwrite(canvas);

		controller.addEventListener('click', e => {
			const target = e.target;
			const id = target.id;

			if (id === 'controller') {
				return;
			}

			if (id === 'clear_btn') {
				hand.clear();
			} else {
				const color = getComputedStyle(target, null).getPropertyValue('background-color');
				color && hand.selColor(color);

				const hasSelected = document.querySelector('.color_btn_selected');
				if (hasSelected.id !== id) {
					hasSelected.classList.remove('color_btn_selected')
					target.classList.add('color_btn_selected');
				}
			}

		}, false);

	</script>
</body>

</html>