【第2章 绘制】2.12 高级路径操作
文章目录
- 前言
- 拖动多边形对象
- 编辑贝塞尔曲线
前言
下面的示例只做了解,知道绘制逻辑就行,在项目中遇到需求后都是结合框架使用。
拖动多边形对象
<!DOCTYPE html>
<!-- 定义 HTML 文档的语言为英语 -->
<html lang="en"><head><!-- 设置文档的字符编码为 UTF-8 --><meta charset="UTF-8" /><!-- 设置视口,确保页面在不同设备上正确显示 --><meta name="viewport" content="width=device-width, initial-scale=1.0" /><!-- 设置页面标题 --><title>2-28-用于处理拖动多边形操作</title><style>/* 设置页面背景颜色 */body {background: #eeeeee;}/* 设置控制面板的位置 */#controls {position: absolute;left: 25px;top: 25px;}/* 设置画布的样式,包括背景颜色、光标样式和阴影 */#canvas {background: #ffffff;cursor: pointer;margin-left: 10px;margin-top: 10px;-webkit-box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.5);-moz-box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.5);box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.5);}</style></head><body><div class="container"><div id="controls"><!-- 选择多边形边框颜色 -->Stroke color:<select id="strokeStyleSelect"><option value="red">red</option><option value="green">green</option><option value="blue">blue</option><option value="orange">orange</option><option value="cornflowerblue" selected>cornflowerblue</option><option value="goldenrod">goldenrod</option><option value="navy">navy</option><option value="purple">purple</option></select><!-- 选择多边形填充颜色 -->Fill color:<select id="fillStyleSelect"><option value="rgba(255,0,0,0.5)">semi-transparent red</option><option value="green">green</option><option value="rgba(0,0,255,0.5)">semi-transparent blue</option><option value="orange">orange</option><option value="rgba(100,140,230,0.5)">semi-transparent cornflowerblue</option><option value="goldenrod" selected>goldenrod</option><option value="navy">navy</option><option value="purple">purple</option></select><!-- 选择多边形的边数 -->Sides:<select id="sidesSelect"><option value="3" selected>3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option><option value="9">9</option></select><!-- 选择多边形的起始角度 -->Start angle:<select id="startAngleSelect"><option value="0" selected>0</option><option value="22.5">22.5</option><option value="45">45</option><option value="67.5">67.5</option><option value="90">90</option></select><!-- 选择是否填充多边形 -->Fill<input id="fillCheckbox" type="checkbox" checked /><!-- 选择是否进入编辑模式 -->Edit<input id="editCheckbox" type="checkbox" /><!-- 点击按钮清除画布内容 --><input id="eraseAllButton" type="button" value="Erase all" /></div><!-- 创建画布元素,若浏览器不支持则显示提示信息 --><canvas id="canvas" width="1000" height="600">canvas not supported in your brower, please use a new version of brower</canvas></div><script>// 获取画布元素和绘图上下文const canvas = document.getElementById('canvas'),context = canvas.getContext('2d')// 获取各个控件元素const eraseAllButton = document.getElementById('eraseAllButton'),strokeStyleSelect = document.getElementById('strokeStyleSelect'),fillStyleSelect = document.getElementById('fillStyleSelect'),fillCheckbox = document.getElementById('fillCheckbox'),editCheckbox = document.getElementById('editCheckbox'),sidesSelect = document.getElementById('sidesSelect')// 定义全局变量let drawingSurfaceImageData,mousedown = {},rubberbandRect = {},dragging = false,draggingOffsetX,draggingOffsetY,sides = 8,startAngle = 0,guidewires = true,editing = false,polygons = []// Functions ....../*** 绘制网格线* @param {CanvasRenderingContext2D} context - 画布的绘图上下文* @param {string} color - 网格线的颜色* @param {number} stepX - 垂直网格线的间隔* @param {number} stepY - 水平网格线的间隔*/const drawGrid = (context, color, stepX, stepY) => {// 保存当前绘图状态context.save()// 设置网格线的样式context.strokeStyle = colorcontext.lineWidth = 0.5context.shadowColor = nullcontext.shadowBlur = 0context.shadowOffsetX = 0context.shadowOffsetY = 0// 绘制垂直线for (let i = stepX + 0.5; i < canvas.width; i += stepX) {context.beginPath()context.moveTo(i, 0)context.lineTo(i, canvas.height)context.stroke()}// 绘制水平线for (let i = stepY + 0.5; i < canvas.height; i += stepY) {context.beginPath()context.moveTo(0, i)context.lineTo(canvas.width, i)context.stroke()}// 恢复之前保存的绘图状态context.restore()}/*** 将窗口坐标转换为画布坐标* @param {number} x - 窗口的 x 坐标* @param {number} y - 窗口的 y 坐标* @returns {{x: number, y: number}} - 画布上对应的坐标*/const windowToCanvas = (x, y) => {// 获取画布相对于窗口的边界框const bbox = canvas.getBoundingClientRect()return {x: x - bbox.left * (canvas.width / bbox.width),y: y - bbox.top * (canvas.height / bbox.height),}}// Save and restore drawing surface ....../*** 保存当前画布的图像数据*/const saveDrawingSurface = () => {drawingSurfaceImageData = context.getImageData(0, 0, canvas.width, canvas.height)}/*** 恢复之前保存的画布图像数据*/const restoreDrawingSurface = () => {context.putImageData(drawingSurfaceImageData, 0, 0)}// Point constructor ....../*** 点对象的构造函数* @param {number} x - 点的 x 坐标* @param {number} y - 点的 y 坐标*/const Point = function (x, y) {this.x = xthis.y = y}// Polygon constructor ....../*** 多边形对象的构造函数* @param {number} centerX - 多边形中心点的 x 坐标* @param {number} centerY - 多边形中心点的 y 坐标* @param {number} radius - 多边形的半径* @param {number} sides - 多边形的边数* @param {number} startAngle - 多边形的起始角度* @param {string} strokeStyle - 多边形边框的颜色* @param {string} fillStyle - 多边形填充的颜色* @param {boolean} filled - 是否填充多边形*/const Polygon = function (centerX, centerY, radius, sides, startAngle, strokeStyle, fillStyle, filled) {this.x = centerXthis.y = centerYthis.radius = radiusthis.sides = sidesthis.startAngle = startAnglethis.strokeStyle = strokeStylethis.fillStyle = fillStylethis.filled = filled}// Polygon prototype......Polygon.prototype = {/*** 获取多边形各个顶点的坐标* @returns {Point[]} - 多边形顶点的坐标数组*/getPoints: function () {let points = [],// 这里的 angle 是基于钟表 0 点的位置开始计算,0 点位置为 0 度,3 点位置为 π/2 度angle = this.startAngle || 0for (let i = 0; i < sides; ++i) {points.push(new Point(this.x + this.radius * Math.sin(angle), this.y - this.radius * Math.cos(angle)))angle += (2 * Math.PI) / this.sides}return points},/*** 在画布上创建多边形的路径* @param {CanvasRenderingContext2D} context - 画布的绘图上下文*/createPath: function (context) {const points = this.getPoints()context.beginPath()context.moveTo(points[0].x, points[0].y)for (let i = 0; i < this.sides; ++i) {context.lineTo(points[i].x, points[i].y)}context.closePath()},/*** 在画布上绘制多边形的边框* @param {CanvasRenderingContext2D} context - 画布的绘图上下文*/stroke: function (context) {// 保存当前绘图状态context.save()this.createPath(context)context.strokeStyle = this.strokeStylecontext.stroke()// 恢复之前保存的绘图状态context.restore()},/*** 在画布上填充多边形* @param {CanvasRenderingContext2D} context - 画布的绘图上下文*/fill: function (context) {// 保存当前绘图状态context.save()this.createPath(context)context.fillStyle = this.fillStylecontext.fill()// 恢复之前保存的绘图状态context.restore()},/*** 移动多边形到指定位置* @param {number} x - 新的中心点 x 坐标* @param {number} y - 新的中心点 y 坐标*/move: function (x, y) {this.x = xthis.y = y},}// Draw polygon....../*** 绘制多边形* @param {Polygon} polygon - 要绘制的多边形对象*/const drawPolygon = (polygon) => {context.beginPath()polygon.createPath(context)polygon.stroke(context)if (fillCheckbox.checked) {polygon.fill(context)}}// Rubber bands/*** 更新橡皮筋矩形的属性* @param {{x: number, y: number}} loc - 当前鼠标位置*/const updateRubberbandRectangle = (loc) => {rubberbandRect.width = Math.abs(loc.x - mousedown.x)rubberbandRect.height = Math.abs(loc.y - mousedown.y)if (loc.x > mousedown.x) {rubberbandRect.left = mousedown.x} else {rubberbandRect.left = loc.x}if (loc.y > mousedown.y) {rubberbandRect.top = mousedown.y} else {rubberbandRect.top = loc.y}}/*** 绘制橡皮筋形状(多边形)*/const drawRubberbandShape = () => {const polygon = new Polygon(mousedown.x,mousedown.y,rubberbandRect.width,parseInt(sidesSelect.value),(Math.PI / 180) * parseInt(startAngleSelect.value),strokeStyleSelect.value,fillStyleSelect.value,fillCheckbox.checked)drawPolygon(polygon)if (!dragging) {polygons.push(polygon)}}/*** 更新橡皮筋的状态* @param {{x: number, y: number}} loc - 当前鼠标位置*/const updateRubberband = (loc) => {updateRubberbandRectangle(loc)drawRubberbandShape()}// Guidewires ....../*** 绘制水平参考线* @param {number} y - 参考线的 y 坐标*/const drawHorizontalLine = (y) => {context.beginPath()context.moveTo(0, y + 0.5)context.lineTo(canvas.width, y + 0.5)context.stroke()}/*** 绘制垂直参考线* @param {number} x - 参考线的 x 坐标*/const drawVerticalLine = (x) => {context.beginPath()context.moveTo(x + 0.5, 0)context.lineTo(x + 0.5, canvas.height)context.stroke()}/*** 绘制参考线* @param {number} x - 参考线的 x 坐标* @param {number} y - 参考线的 y 坐标*/const drawGuidewires = (x, y) => {// 保存当前绘图状态context.save()context.strokeStyle = 'rgba(0,0,230,0.4)'context.lineWidth = 0.5drawHorizontalLine(y)drawVerticalLine(x)// 恢复之前保存的绘图状态context.restore()}/*** 绘制所有多边形*/const drawPolygons = () => {polygons.forEach((polygon) => {drawPolygon(polygon)})}// Dragging ....../*** 开始拖动操作* @param {{x: number, y: number}} loc - 鼠标按下的位置*/const startDragging = (loc) => {saveDrawingSurface()mousedown.x = loc.xmousedown.y = loc.y}/*** 开始编辑模式* @param {{x: number, y: number}} loc - 鼠标位置*/const startEditing = (loc) => {editing = truecanvas.style.cursor = 'pointer'}/*** 停止编辑模式*/const stopEditing = () => {editing = falsecanvas.style.cursor = 'crosshair'}// Event handlers....../*** 鼠标按下事件处理函数* @param {MouseEvent} e - 鼠标事件对象*/canvas.onmousedown = (e) => {const loc = windowToCanvas(e.clientX, e.clientY)// 阻止默认事件e.preventDefault()if (editing) {polygons.forEach((polygon) => {polygon.createPath(context)if (context.isPointInPath(loc.x, loc.y)) {startDragging(loc)dragging = polygondraggingOffsetX = loc.x - polygon.xdraggingOffsetY = loc.y - polygon.yreturn}})} else {startDragging(loc)dragging = true}}/*** 鼠标移动事件处理函数* @param {MouseEvent} e - 鼠标事件对象*/canvas.onmousemove = (e) => {const loc = windowToCanvas(e.clientX, e.clientY)// 阻止默认事件e.preventDefault()if (editing && dragging) {dragging.x = loc.x - draggingOffsetXdragging.y = loc.y - draggingOffsetYcontext.clearRect(0, 0, canvas.width, canvas.height)drawGrid(context, 'lightgray', 10, 10)drawPolygons()} else {if (dragging) {restoreDrawingSurface()updateRubberband(loc)if (guidewires) {drawGuidewires(mousedown.x, mousedown.y)}}}}/*** 鼠标松开事件处理函数* @param {MouseEvent} e - 鼠标事件对象*/canvas.onmouseup = (e) => {const loc = windowToCanvas(e.clientX, e.clientY)// 阻止默认事件e.preventDefault()dragging = falseif (!editing) {restoreDrawingSurface()updateRubberband(loc)}}/*** 点击清除按钮事件处理函数*/eraseAllButton.onclick = () => {context.clearRect(0, 0, canvas.width, canvas.height)drawGrid(context, 'lightgray', 10, 10)// polygons = []saveDrawingSurface()}/*** 边框颜色选择框变化事件处理函数*/strokeStyleSelect.onchange = () => {context.strokeStyle = strokeStyleSelect.value}/*** 填充颜色选择框变化事件处理函数*/fillStyleSelect.onchange = () => {context.fillStyle = fillStyleSelect.value}/*** 编辑复选框变化事件处理函数*/editCheckbox.onchange = () => {editing = editCheckbox.checkedif (editing) {startEditing()} else {stopEditing()}}// Initialization......// 初始化绘图样式context.strokeStyle = strokeStyleSelect.valuecontext.fillStyle = fillStyleSelect.valuecontext.shadowColor = 'rgba(0,0,0,0.4)'context.shadowOffsetX = 2context.shadowOffsetY = 2context.shadowBlur = 4// 绘制网格线drawGrid(context, 'lightgray', 10, 10)</script></body>
</html>
编辑贝塞尔曲线
<!DOCTYPE html>
<!-- 设置文档的语言为英语 -->
<html lang="en"><head><!-- 设置文档的字符编码为 UTF-8 --><meta charset="UTF-8" /><!-- 设置页面标题 --><title>可编辑的贝塞尔曲线</title><style>/* 设置页面背景颜色 */body {background: #eeeeee;}/* 浮动控制面板的样式 */.floatingControls {/* 绝对定位 */position: absolute;left: 150px;top: 100px;width: 300px;padding: 20px;/* 边框样式 */border: thin solid rgba(0, 0, 0, 0.3);/* 背景颜色 */background: rgba(0, 0, 200, 0.1);color: blue;font: 14px Arial;/* 阴影效果 */-webkit-box-shadow: rgba(0, 0, 0, 0.2) 6px 6px 8px;-moz-box-shadow: rgba(0, 0, 0, 0.2) 6px 6px 8px;box-shadow: rgba(0, 0, 0, 0.2) 6px 6px 8px;/* 默认隐藏 */display: none;}/* 浮动控制面板内段落的样式 */.floatingControls p {margin-top: 0px;margin-bottom: 20px;}/* 常规控制面板的样式 */#controls {/* 绝对定位 */position: absolute;left: 25px;top: 25px;}/* 画布的样式 */#canvas {background: #ffffff;cursor: pointer;margin-left: 10px;margin-top: 10px;/* 阴影效果 */-webkit-box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.5);-moz-box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.5);box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.5);}</style></head><body><!-- 创建画布元素,若浏览器不支持则显示提示信息 --><canvas id="canvas" width="605" height="400">Canvas not supported</canvas><!-- 常规控制面板 --><div id="controls"><!-- 选择边框颜色 -->Stroke color:<select id="strokeStyleSelect"><option value="red">red</option><option value="green">green</option><option value="blue">blue</option><option value="orange">orange</option><option value="cornflowerblue">cornflowerblue</option><option value="goldenrod">goldenrod</option><option value="navy" selected>navy</option><option value="purple">purple</option></select><!-- 选择是否显示辅助线 -->Guidewires: <input id="guidewireCheckbox" type="checkbox" checked /><!-- 清除画布按钮 --><input id="eraseAllButton" type="button" value="Erase all" /></div><!-- 浮动提示框 --><div id="instructions" class="floatingControls"><!-- 提示信息 --><p>拖动贝塞尔曲线的锚点和控制点以改变曲线的形状</p><p>当你完成曲线形状的编辑后,点击曲线外的一点,以完成此图像</p><!-- 确认按钮 --><input id="instructionsOkayButton" type="button" value="好的" autofocus /><!-- 不再提示按钮 --><input id="instructionsNoMoreButton" type="button" value="不再提示" /></div></body><script>// 使用严格模式'use strict'// 获取 DOM 元素和初始化变量let canvas = document.getElementById('canvas'),context = canvas.getContext('2d'),eraseAllButton = document.getElementById('eraseAllButton'),strokeStyleSelect = document.getElementById('strokeStyleSelect'),guidewireCheckbox = document.getElementById('guidewireCheckbox'),instructions = document.getElementById('instructions'),instructionsOkayButton = document.getElementById('instructionsOkayButton'),instructionsNoMoreButton = document.getElementById('instructionsNoMoreButton'),showInstructions = true,// 网格线颜色GRID_STROKE_STYLE = 'lightblue',// 网格线间距GRID_SPACING = 10,// 控制点半径CONTROL_POINT_RADIUS = 5,// 控制点边框颜色CONTROL_POINT_STROKE_STYLE = 'blue',// 控制点填充颜色CONTROL_POINT_FILL_STYLE = 'rgba(255, 255, 0, 0.5)',// 端点边框颜色END_POINT_STROKE_STYLE = 'navy',// 端点填充颜色END_POINT_FILL_STYLE = 'rgba(0, 255, 0, 0.5)',// 辅助线颜色GUIDEWIRE_STROKE_STYLE = 'rgba(0,0,230,0.4)',// 保存绘图表面的图像数据drawingImageData,// 鼠标按下时的位置mousedown = {},// 橡皮筋矩形的属性rubberbandRect = {},// 是否正在拖动dragging = false,// 是否正在拖动点draggingPoint = false,// 贝塞尔曲线的端点endPoints = [{}, {}],// 贝塞尔曲线的控制点controlPoints = [{}, {}],// 是否正在编辑editing = false,// 是否显示辅助线guidewires = guidewireCheckbox.checked//Function……/*** 画网格线* @param {string} color - 网格线的颜色* @param {number} stepX - 垂直网格线的间隔* @param {number} stepY - 水平网格线的间隔*/let drawGrid = (color, stepX, stepY) => {// 保存当前绘图状态context.save()// 设置网格线的样式context.strokeStyle = colorcontext.lineWidth = 0.5// 清除画布context.clearRect(0, 0, context.canvas.width, context.canvas.height)// 绘制垂直网格线for (var i = stepX + 0.5; i < context.canvas.width; i += stepX) {context.beginPath()context.moveTo(i, 0)context.lineTo(i, context.canvas.height)context.stroke()}// 绘制水平网格线for (var i = stepY + 0.5; i < context.canvas.height; i += stepY) {context.beginPath()context.moveTo(0, i)context.lineTo(context.canvas.width, i)context.stroke()}// 恢复之前保存的绘图状态context.restore()}/*** 更新橡皮筋矩形的属性* @param {{x: number, y: number}} loc - 当前鼠标位置*/let updateRubberbandRectangle = (loc) => {// 计算橡皮筋矩形的宽度和高度rubberbandRect.width = Math.abs(loc.x - mousedown.x)rubberbandRect.height = Math.abs(loc.y - mousedown.y)// 确定橡皮筋矩形的左边界if (loc.x > mousedown.x) rubberbandRect.left = mousedown.xelse rubberbandRect.left = loc.x// 确定橡皮筋矩形的上边界if (loc.y > mousedown.y) rubberbandRect.top = mousedown.yelse rubberbandRect.top = loc.y}/*** 将窗口坐标转换为 Canvas 坐标* @param {number} x - 窗口的 x 坐标* @param {number} y - 窗口的 y 坐标* @returns {{x: number, y: number}} - Canvas 上对应的坐标*/let windowToCanvas = (x, y) => {// 获取画布相对于窗口的边界框let bbox = canvas.getBoundingClientRect()return {x: x - bbox.left * (canvas.width / bbox.width),y: y - bbox.top * (canvas.height / bbox.height),}}//保存和恢复绘图表面/*** 保存当前绘图表面的图像数据*/let saveDrawingSurface = () => {drawingImageData = context.getImageData(0, 0, canvas.width, canvas.height)}/*** 恢复之前保存的绘图表面的图像数据*/let restoreDrawingSurface = () => {context.putImageData(drawingImageData, 0, 0)}/*** 绘制贝塞尔曲线*/let drawBezierCurve = () => {// 开始新路径context.beginPath()// 移动画笔到起始端点context.moveTo(endPoints[0].x, endPoints[0].y)// 绘制贝塞尔曲线context.bezierCurveTo(controlPoints[0].x,controlPoints[0].y,controlPoints[1].x,controlPoints[1].y,endPoints[1].x,endPoints[1].y)// 描边context.stroke()}/*** 更新贝塞尔曲线的端点和控制点的位置*/let updateEndAndControlPoints = () => {// 设置起始端点的位置endPoints[0].x = rubberbandRect.leftendPoints[0].y = rubberbandRect.top// 设置结束端点的位置endPoints[1].x = rubberbandRect.left + rubberbandRect.widthendPoints[1].y = rubberbandRect.top + rubberbandRect.height// 设置第一个控制点的位置controlPoints[0].x = rubberbandRect.leftcontrolPoints[0].y = rubberbandRect.top + rubberbandRect.height// 设置第二个控制点的位置controlPoints[1].x = rubberbandRect.left + rubberbandRect.widthcontrolPoints[1].y = rubberbandRect.top}/*** 绘制橡皮筋形状(贝塞尔曲线)* @param {{x: number, y: number}} loc - 当前鼠标位置*/let drawRubberbandShape = (loc) => {// 更新端点和控制点的位置updateEndAndControlPoints()// 绘制贝塞尔曲线drawBezierCurve()}/*** 更新橡皮筋的状态* @param {{x: number, y: number}} loc - 当前鼠标位置*/let updateRubberband = (loc) => {// 更新橡皮筋矩形的属性updateRubberbandRectangle(loc)// 绘制橡皮筋形状drawRubberbandShape(loc)}//辅助线/*** 绘制水平辅助线* @param {number} y - 辅助线的 y 坐标*/let drawHorizontalGuidewire = (y) => {// 开始新路径context.beginPath()// 移动画笔到起点context.moveTo(0, y + 0.5)// 绘制直线到终点context.lineTo(context.canvas.width, y + 0.5)// 描边context.stroke()}/*** 绘制垂直辅助线* @param {number} x - 辅助线的 x 坐标*/let drawVerticalGuidewire = (x) => {// 开始新路径context.beginPath()// 移动画笔到起点context.moveTo(x + 0.5, 0)// 绘制直线到终点context.lineTo(x + 0.5, context.canvas.height)// 描边context.stroke()}/*** 绘制辅助线* @param {number} x - 辅助线的 x 坐标* @param {number} y - 辅助线的 y 坐标*/let drawGuidewires = (x, y) => {// 保存当前绘图状态context.save()// 设置辅助线的颜色context.strokeStyle = GUIDEWIRE_STROKE_STYLE// 设置辅助线的线宽context.lineWidth = 0.5// 绘制垂直辅助线drawVerticalGuidewire(x)// 绘制水平辅助线drawHorizontalGuidewire(y)// 恢复之前保存的绘图状态context.restore()}//绘制锚点和控制点/*** 绘制单个控制点* @param {number} index - 控制点的索引*/let drawControlPoint = (index) => {// 开始新路径context.beginPath()// 绘制控制点的圆形context.arc(controlPoints[index].x, controlPoints[index].y, CONTROL_POINT_RADIUS, 0, Math.PI * 2, false)// 描边context.stroke()// 填充context.fill()}/*** 绘制所有控制点*/let drawControlPoints = () => {// 保存当前绘图状态context.save()// 设置控制点的边框颜色context.strokeStyle = CONTROL_POINT_STROKE_STYLE// 设置控制点的填充颜色context.fillStyle = CONTROL_POINT_FILL_STYLE// 绘制第一个控制点drawControlPoint(0)// 绘制第二个控制点drawControlPoint(1)// 描边context.stroke()// 填充context.fill()// 恢复之前保存的绘图状态context.restore()}/*** 绘制单个端点* @param {number} index - 端点的索引*/let drawEndPoint = (index) => {// 开始新路径context.beginPath()// 绘制端点的圆形context.arc(endPoints[index].x, endPoints[index].y, CONTROL_POINT_RADIUS, 0, Math.PI * 2, false)// 描边context.stroke()// 填充context.fill()}/*** 绘制所有端点*/let drawEndPoints = () => {// 保存当前绘图状态context.save()// 设置端点的边框颜色context.strokeStyle = END_POINT_STROKE_STYLE// 设置端点的填充颜色context.fillStyle = END_POINT_FILL_STYLE// 绘制第一个端点drawEndPoint(0)// 绘制第二个端点drawEndPoint(1)// 描边context.stroke()// 填充context.fill()// 恢复之前保存的绘图状态context.restore()}/*** 绘制所有控制点和端点*/let drawControlAndEndPoints = () => {// 绘制控制点drawControlPoints()// 绘制端点drawEndPoints()}/*** 检查鼠标是否在端点内* @param {{x: number, y: number}} loc - 当前鼠标位置* @returns {Object|undefined} - 如果在端点内返回端点对象,否则返回 undefined*/let cursorInEndPoint = (loc) => {var pt// 遍历所有端点endPoints.forEach(function (point) {// 开始新路径context.beginPath()// 绘制端点的圆形context.arc(point.x, point.y, CONTROL_POINT_RADIUS, 0, Math.PI * 2, false)// 检查鼠标是否在路径内if (context.isPointInPath(loc.x, loc.y)) {pt = point}})return pt}/*** 检查鼠标是否在控制点内* @param {{x: number, y: number}} loc - 当前鼠标位置* @returns {Object|undefined} - 如果在控制点内返回控制点对象,否则返回 undefined*/let cursorInControlPoint = (loc) => {var pt// 遍历所有控制点controlPoints.forEach(function (point) {// 开始新路径context.beginPath()// 绘制控制点的圆形context.arc(point.x, point.y, CONTROL_POINT_RADIUS, 0, Math.PI * 2, false)// 检查鼠标是否在路径内if (context.isPointInPath(loc.x, loc.y)) {pt = point}})return pt}/*** 更新正在拖动的点的位置* @param {{x: number, y: number}} loc - 当前鼠标位置*/let updateDraggingPoint = (loc) => {draggingPoint.x = loc.xdraggingPoint.y = loc.y}//事件/*** 鼠标按下事件处理函数* @param {MouseEvent} e - 鼠标事件对象*/canvas.onmousedown = (e) => {// 获取当前鼠标在画布上的位置let loc = windowToCanvas(e.clientX, e.clientY)// 阻止默认事件e.preventDefault()if (!editing) {// 保存当前绘图表面的图像数据saveDrawingSurface()// 记录鼠标按下的位置mousedown.x = loc.xmousedown.y = loc.y// 更新橡皮筋矩形的属性updateRubberbandRectangle(loc)// 开始拖动dragging = true} else {// 检查鼠标是否在控制点内draggingPoint = cursorInControlPoint(loc)if (!draggingPoint) {// 检查鼠标是否在端点内draggingPoint = cursorInEndPoint(loc)}}}/*** 鼠标移动事件处理函数* @param {MouseEvent} e - 鼠标事件对象*/canvas.onmousemove = (e) => {// 获取当前鼠标在画布上的位置let loc = windowToCanvas(e.clientX, e.clientY)if (dragging || draggingPoint) {// 阻止默认事件e.preventDefault()// 恢复之前保存的绘图表面的图像数据restoreDrawingSurface()if (guidewires) {// 绘制辅助线drawGuidewires(loc.x, loc.y)}}if (dragging) {// 更新橡皮筋的状态updateRubberband(loc)// 绘制控制点和端点drawControlAndEndPoints()} else if (draggingPoint) {// 更新正在拖动的点的位置updateDraggingPoint(loc)// 绘制控制点和端点drawControlAndEndPoints()// 绘制贝塞尔曲线drawBezierCurve()}}/*** 鼠标松开事件处理函数* @param {MouseEvent} e - 鼠标事件对象*/canvas.onmouseup = (e) => {// 获取当前鼠标在画布上的位置let loc = windowToCanvas(e.clientX, e.clientY)// 恢复之前保存的绘图表面的图像数据restoreDrawingSurface()if (!editing) {// 更新橡皮筋的状态updateRubberband(loc)// 绘制控制点和端点drawControlAndEndPoints()// 停止拖动dragging = false// 进入编辑模式editing = trueif (showInstructions) {// 显示提示框instructions.style.display = 'inline'}} else {if (draggingPoint) {// 绘制控制点和端点drawControlAndEndPoints()} else {// 退出编辑模式editing = false}// 绘制贝塞尔曲线drawBezierCurve()// 清空正在拖动的点draggingPoint = undefined}}// 给控件添加监听/*** 点击清除按钮事件处理函数*/eraseAllButton.onclick = () => {// 清除画布context.clearRect(0, 0, canvas.width, canvas.height)// 绘制网格线drawGrid(GRID_STROKE_STYLE, GRID_SPACING, GRID_SPACING)// 保存当前绘图表面的图像数据saveDrawingSurface()// 退出编辑模式editing = false// 停止拖动dragging = false// 清空正在拖动的点draggingPoint = undefined}/*** 边框颜色选择框变化事件处理函数*/strokeStyleSelect.onchange = () => {// 设置绘图的边框颜色context.strokeStyle = strokeStyleSelect.value}/*** 辅助线复选框变化事件处理函数*/guidewireCheckbox.onchange = () => {// 更新是否显示辅助线的状态guidewires = guidewireCheckbox.checked}// 给介绍框添加监听/*** 提示框确认按钮点击事件处理函数*/instructionsOkayButton.onclick = () => {// 隐藏提示框instructions.style.display = 'none'}/*** 提示框不再提示按钮点击事件处理函数*/instructionsNoMoreButton.onclick = () => {// 隐藏提示框instructions.style.display = 'none'// 不再显示提示框showInstructions = false}// 初始化// 设置绘图的边框颜色context.strokeStyle = strokeStyleSelect.value// 绘制网格线drawGrid(GRID_STROKE_STYLE, GRID_SPACING, GRID_SPACING)</script>
</html>