注释和绘图有很多用例。在本教程中,让我们看看如何使用 Javascript 制作一个简单的绘图应用程序。我们将使用纯 Javascript构建它,因此它可以让您灵活地在任何需要的地方实现它。
演示#
首先,让我们看看它是如何工作的,我已经向这个页面添加了功能。单击按钮时,您将获得绘图工具,您可以使用这些工具在页面上绘制线条和箭头。还有一个橡皮擦工具,如果你犯了错误,可以通过点击它们来擦除线条。
单击绘图框中的十字按钮可以关闭绘图工具。同样,所有图纸都保留在之后的页面上,允许您对此页面进行注释。
<button class="animated hover-button" id="start-drawing"><span>✍️ Activate Drawing</span></button><a href="https://twitter.com/thisisfjolt" target="_blank" class="link">Follow on Twitter</a>
<a href="https://fjolt.com/article/javascript-frontend-drawing-annotation-application" target="_blank" class="link">Read Article</a>
<div id="drawing-cover"></div>
<div id="drawing-layer"></div><div id="drawing-box">
<div class="tools">
<button data-tool="freeHand" data-current="true"><span><i class="fal fa-pencil-alt"></i></span></button>
<button data-tool="arrow" ><span><i class="fal fa-arrow-up"></i></span></button>
<button data-tool="eraser" ><span><i class="fal fa-eraser"></i></span></button>
</div>
<div class="colors">
<div data-color="white" data-rColor="white" data-current="true"></div>
<div data-color="black" data-rColor="#544141"></div>
<div data-color="red" data-rColor="#d83030"></div>
<div data-color="green" data-rColor="#30d97d"></div>
<div data-color="orange" data-rColor="#ff9000"></div>
<div data-color="yellow" data-rColor="#f3f326"></div>
</div>
<div class="close">
<i class="fal fa-times"></i>
</div>
</div>
<script src="https://kit.fontawesome.com/48764efa36.js" crossorigin="anonymous"></script>
body { background: rgb(10 13 37); }
#drawing-box {
background: linear-gradient(360deg, #ebf3fd, white);
position: fixed;
left: 2rem;
padding: 1rem;
display: flex;
box-shadow: 0 2px 20px #000000c4;
z-index: 9999999;
transition: all 0.2s ease-out;
transform: scale(0.5);
bottom: -4rem;
border-radius: 100px;
}
[data-drawing="true"] #drawing-box {
bottom: 3rem;
transform: scale(1);
}
#drawing-cover {
position: fixed;
top: 0;
left: 0;
pointer-events: none;
transition: all 0.3s ease-out;
width: 100%;
height: 100%;
}
[data-drawing="true"] #drawing-cover {
background: rgba(0,0,0,0.5);
pointer-events: all;
}
#drawing-box .tools i, #drawing-box .close i {
color: black;
padding: 0;
border-bottom: 2px solid transparent;
font-size: 1.25rem;
padding: 0 0 0.25rem 0;
}
#drawing-box .close {
border-left: 1px solid #00000030;
padding: 0 0 0 1rem;
display: flex;
align-items: center;
font-size: 1.5rem;
cursor: pointer;
}
#drawing-box .close i {
padding: 0;
margin: 0;
border: none;
}
#drawing-box .tools {
display: flex;
border-right: 1px solid #00000030;
margin: 0 1rem 0 0;
}
#drawing-box .tools > button {
width: 32px;
margin: 0 1rem 0 0;
padding: 0;
background: transparent;
box-shadow: none;
height: 32px;
}
#drawing-box .tools > button span {
background: transparent;
filter: none !important;
padding: 0;
border-radius: 4px;
}
#drawing-box .colors {
display: flex;
align-items: center;
}
#drawing-box .colors > div {
width: 24px;
height: 24px;
border-radius: 100px;
transition: all 0.1s ease-out;
cursor: pointer;
margin: 0 1rem 0 0;
transform: scale(1);
box-shadow: inset 0 0 0 1px rgba(0,0,0,.1);
}
#drawing-box .colors > div:hover {
transform: scale(1.05);
}
#drawing-box .colors > div[data-current="true"]:after {
content: '';
position: absolute;
top: -4px;
left: -4px;
width: calc(100% + 4px);
height: calc(100% + 4px);
border-radius: 100px;
border: 2px solid #0646ff;
}
#drawing-box i {
display: block;
}
#drawing-box [data-tool][data-current="true"] i {
color: #0646ff;
cursor: pointer;
border-bottom: 2px solid #0646ff !important;
}
#drawing-box [data-tool]:not([data-current="true"]):hover i {
color: rgba(0,0,0,0.5);
}
#drawing-layer svg {
display: block !important;
fill: transparent;
clip-path: inset(-9999px -9999px -99999px -99999px);
overflow: visible;
z-index: 999999999;
}
#drawing-layer {
overflow: visible;
pointer-events: none;
}
[data-drawing="true"] #drawing-layer {
pointer-events: all;
}
#drawing-layer .free-hand, #drawing-layer .arrow {
overflow: visible;
position: absolute;
}
#drawing-layer .free-hand.static, #drawing-layer .arrow.static {
opacity: 0;
}
#drawing-layer svg path {
stroke-linecap: round;
}
#drawing-layer svg path, #drawing-layer svg line {
cursor: pointer;
pointer-events: visiblepainted;
position: absolute;
}
#drawing-box .colors [data-color="black"] { background: #544141; }
#drawing-box .colors [data-color="red"] { background: #d83030; }
#drawing-box .colors [data-color="green"] { background: #30d97d; }
#drawing-box .colors [data-color="orange"] { background: #ff9000; }
#drawing-box .colors [data-color="yellow"] { background: #f3f326; }
@media screen and (max-width: 700px) {
body[data-drawing="true"] {
overflow: hidden;
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
#drawing-box .colors [data-color="green"],
#drawing-box .colors [data-color="orange"],
#drawing-box .colors [data-color="yellow"],
#drawing-box .colors [data-color="white"] {
display: none;
}
}
button {
background: linear-gradient(180deg, #ff7147, #e0417f);
font-size: 1.5rem;
color: white;
padding: 1rem 2rem;
line-height: 2rem;
cursor: pointer;
will-change: transform, filter;
float: none;
transition: all 0.15s ease-out;
height: auto;
border-radius: 100px;
border: none;
overflow: hidden;
display: block;
margin: 2rem;
display: block;
transform: rotateX(0deg) rotateY(0deg) scale(1);
filter: drop-shadow(0 15px 15px rgba(0,0,0,0.3));
font-weight: 500;
perspective-origin: 0 0;
letter-spacing: 0;
}
.link {
display: block;
color: rgba(255,255,255,0.7);
font-size: 1.25rem;
letter-spacing: 0.5px;
line-height: 2rem;
margin: 0 0 0 3rem;
}
.link:hover {
color: rgba(255,255,255,1);
}
// Ensure drawing layer is at root
document.body.appendChild(document.getElementById('drawing-layer'));
// Manage Main UI
// Add a pointerdown event for each color and tool.
// When a user clicks a color or tool, then we set it to our current config.color or config.tool respectively, and highlight it on the UI
[ 'data-rColor', 'data-tool' ].forEach(function(i) {
document.querySelectorAll(`[${i}]`).forEach(function(item) {
item.addEventListener('pointerdown', function(e) {
document.querySelectorAll(`[${i}]`).forEach(function(i) {
i.setAttribute('data-current', false);
});
item.setAttribute('data-current', true);
if(i == 'data-rColor') {
config.color = item.getAttribute(i);
} else if(i == 'data-tool') {
config.tool = item.getAttribute(i);
}
});
});
});
let config = {
drawing: false, // Set to true if we are drawing, false if we aren't
tool: 'freeHand', // The currently selected tool
color : 'white', // The currently selected colour
strokeWidth: 4, // The width of the lines we draw
configNormalisation: 12,// The average normalisation for pencil drawing
}
let arrow = {
// topX, Y, and bottomX, Y store information on the arrows top and bottom ends
topX: 0,
topY: 0,
bottomX: 0,
bottomY: 0,
activeDirection: 'se', // This is the current direction of the arrow, i.e. south-east
arrowClasses: [ 'nw', 'ne', 'sw', 'se' ], // These are possible arrow directions
lineAngle: 0, // This is the angle the arrow point at about the starting point
}
let freeHand = {
currentPathText: 'M0 0 ', // This is the current path of the pencil line, in text
topX: 0, // The starting X coordinate
topY: 0, // The starting Y coordinate
lastMousePoints: [ [0, 0] ], // This is the current path of the pencil line, in array
}
let svgEl = {
arrowPath: (start, dimensions, path, dummy, direction, end, angle, hyp, id) =>
`<div class="arrow drawing-el static current-item" data-id="${id}" data-direction="${direction}"
style="left: ${start[0]}px; top: ${start[1]}px; height: ${dimensions[1]}px; width: ${dimensions[0]}px;">
<div class="arrow-point arrow-point-one"></div>
<div class="arrow-point arrow-point-two" style="
transform-origin: 0 0; left: ${hyp[1]}px; top: ${hyp[2]}px; transform: rotateZ(${angle}deg) translateY(-${hyp[0]}px) translateX(-15px);
"></div>
<svg viewbox="0 0 ${dimensions[0]} ${dimensions[1]}">
<defs>
<marker id="arrow-head-${id}" class="arrow-resizer" markerWidth="10" markerHeight="10" refX="0" refY="3"
orient="auto" markerUnits="strokeWidth" viewBox="0 0 20 20">
<path d="M0 0 L0 6 L9 3 z" fill="${config.color}" />
</marker>
</defs>
<path marker-start="url(#bottom-marker)" style="stroke: ${config.color}; stroke-width: ${config.strokeWidth}" marker-end="url(#arrow-head-${id})" class="arrow-line" d="${path}"></path>
</svg>
</div>`,
drawPath: (start, dimensions, path, id) =>
`<div class="free-hand drawing-el static current-item" data-id="${id}" style="left: ${start[0]}px; top: ${start[1]}px; height: ${dimensions[1]}px; width: ${dimensions[0]}px;">
<svg viewbox="0 0 ${dimensions[0]} ${dimensions[1]}">
<path d="${path}" style="stroke: ${config.color}; stroke-width: ${config.strokeWidth}"></path>
</svg>
</div>`
}
// Set the body attribute 'data-drawing' to true or false, based on if the user clicks the 'Start Drawing' button
// Also sets config.drawing to true or false.
document.getElementById('start-drawing').addEventListener('click', function(e) {
if(config.drawing === true) {
config.drawing = false;
document.body.setAttribute('data-drawing', false)
} else {
let drawingCover = document.getElementById('drawing-cover');
document.body.setAttribute('data-drawing', true)
config.drawing = true;
}
});
// Closes the drawing box and sets 'data-drawing' on the body element to false
// Along with cofig.drawing to false.
document.querySelector('#drawing-box .close').addEventListener('click', function(e) {
document.body.setAttribute('data-drawing', false);
config.drawing = false;
})
document.body.addEventListener('pointerdown', function(e) {
// Generate id for each element
let id = helper.generateId();
if(config.tool == 'arrow' && config.drawing == true) {
// Set arrow start point
arrow.topX = e.clientX;
arrow.topY = e.clientY;
// Add element to drawing layer
document.getElementById('drawing-layer').innerHTML = document.getElementById('drawing-layer').innerHTML +
svgEl.arrowPath( [ arrow.topX + window.scrollX, arrow.topY + window.scrollY ], [ e.clientX, e.clientX ], `M0 0 L0 0`, 'arrow-item', arrow.arrowClasses[3], [ 0, 0 ], 0, [ 0, 0, 0 ], id );
}
else if(config.tool == 'freeHand' && config.drawing == true) {
// Set the drawing starting point
freeHand.topX = e.clientX;
freeHand.topY = e.clientY;
// Set the current path and most recent mouse points to whereever we are scrolled on the page
freeHand.currentPathText = `M${window.scrollX} ${window.scrollY} `;
freeHand.lastMousePoints = [[ window.scrollX, window.scrollY ]];
// Add element to the drawing layer
document.getElementById('drawing-layer').innerHTML = document.getElementById('drawing-layer').innerHTML +
svgEl.drawPath( [ e.clientX, e.clientY ], [ e.clientX, e.clientY ], ``, id);
}
else if(config.tool == 'eraser' && config.drawing == true) {
// Check if user has clicked on an svg
if(helper.parent(e.target, '.drawing-el', 1) !== null && helper.parent(e.target, '.drawing-el', 1).matches('.drawing-el')) {
// If they have, delete it
helper.parent(e.target, '.drawing-el', 1).remove();
}
}
})
document.body.addEventListener('pointermove', function(e) {
// Assuming there is a current item to in the drawing layer
if(document.querySelector('#drawing-layer .current-item') !== null) {
// If we are using the arrow tool
if(config.drawing == true && config.tool == 'arrow') {
// Then get the original start position
let startX = arrow.topX;
let startY = arrow.topY;
// Set a default angle of 90
let angleStart = 90;
// And a default direction of 'south east'
let arrowClass = arrow.arrowClasses[3];
// Calculate how far the user has moved their mouse from the original position
let endX = e.pageX - startX - window.scrollX;
let endY = e.pageY - startY - window.scrollY;
// And using that info, calculate the arrow's angle
helper.calculateArrowLineAngle(endX, endY);
// Then update the config to this new end position
arrow.bottomX = endX;
arrow.bottomY = endY;
// And update the HTML to show the new arrow to the user
document.querySelector('#drawing-layer .arrow.current-item').classList.remove('static');
document.querySelector('#drawing-layer .arrow.current-item').setAttribute('data-direction', arrow.activeDirection);
document.querySelector('#drawing-layer .arrow.current-item svg').setAttribute('viewbox', `0 ${endX} 0 ${endY}`);
document.querySelector('#drawing-layer .arrow.current-item path.arrow-line').setAttribute('d', `M0 0 L${endX} ${endY}`);
}
else if(config.drawing == true && config.tool == 'freeHand') {
// Similar to arrows, calculate the user's end position
let endX = e.pageX - freeHand.topX;
let endY = e.pageY - freeHand.topY;
// And push these new coordinates to our config
let newCoordinates = [ endX, endY ];
freeHand.lastMousePoints.push([endX, endY]);
if(freeHand.lastMousePoints.length >= config.configNormalisation) {
freeHand.lastMousePoints.shift();
}
// Then calculate the average points to display a line to the user
let avgPoint = helper.getAveragePoint(0);
if (avgPoint) {
freeHand.currentPathText += " L" + avgPoint.x + " " + avgPoint.y;
let tmpPath = '';
for (let offset = 2; offset < freeHand.lastMousePoints.length; offset += 2) {
avgPoint = helper.getAveragePoint(offset);
tmpPath += " L" + avgPoint.x + " " + avgPoint.y;
}
// Set the complete current path coordinates
document.querySelector('#drawing-layer .free-hand.current-item').classList.remove('static');
document.querySelector('#drawing-layer .free-hand.current-item svg path').setAttribute('d', freeHand.currentPathText + tmpPath);
}
}
}
});
// Whenever the user leaves the page with their mouse or lifts up their cursor
[ 'mouseleave', 'pointerup' ].forEach(function(item) {
document.body.addEventListener(item, function(e) {
// Remove current-item class from all elements, and give all SVG elements pointer-events
document.querySelectorAll('#drawing-layer > div').forEach(function(item) {
item.style.pointerEvent = 'all';
item.classList.remove('current-item');
// Delete any 'static' elements
if(item.classList.contains('static')) {
item.remove();
}
});
// Reset freeHand variables where needed
freeHand.currentPathText = 'M0 0 ';
freeHand.lastMousePoints = [ [0, 0] ];
});
});
let helper = {
// This averages out a certain number of mouse movements for free hand drawing
// To give our lines a smoother effect
getAveragePoint: function(offset) {
let len = freeHand.lastMousePoints.length;
if (len % 2 === 1 || len >= 8) {
let totalX = 0;
let totalY = 0;
let pt, i;
let count = 0;
for (i = offset; i < len; i++) {
count++;
pt = freeHand.lastMousePoints[i];
totalX += pt[0];
totalY += pt[1];
}
return {
x: totalX / count,
y: totalY / count
}
}
return null;
},
// This calculates the angle and direction of a moving arrow
calculateArrowLineAngle: function(lineEndX, lineEndY) {
var calcLineEndX = lineEndX;
var calcLineEndY = lineEndY;
var angleStart = 90;
var angle = 0;
var a = calcLineEndX;
var b = calcLineEndY;
var c = Math.sqrt(Math.pow(lineEndX, 2) + Math.pow(lineEndY, 2));
if(calcLineEndX <= 0 && calcLineEndY >= 0) {
// quadrant 3
angleStart = 180;
angle = Math.asin(a/c) * -1 * (180/Math.PI);
arrow.activeDirection = arrow.arrowClasses[2];
} else if(calcLineEndY <= 0 && calcLineEndX >= 0) {
// quadrant 1
angleStart = 0;
angle = Math.asin(a/c) * (180/Math.PI);
arrow.activeDirection = arrow.arrowClasses[1];
} else if(calcLineEndY <= 0 && calcLineEndX <= 0) {
// quadrant 4
angleStart = 270;
angle = Math.asin(b/c) * -1 * (180/Math.PI);
arrow.activeDirection = arrow.arrowClasses[0];
}
else {
// quadrant 2
angleStart = 90;
angle = Math.asin(b/c) * (180/Math.PI);
arrow.activeDirection = arrow.arrowClasses[3];
}
arrow.lineAngle = angle + angleStart;
},
// This generates a UUID for our drawn elements
generateId: function() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
},
// This function matches parent elements allowing us to select a parent element
parent: function(el, match, last) {
var result = [];
for (var p = el && el.parentElement; p; p = p.parentElement) {
result.push(p);
if(p.matches(match)) {
break;
}
}
if(last == 1) {
return result[result.length - 1];
} else {
return result;
}
}
}
跟踪用户的鼠标活动#
如前所述,此演示使用 vanilla JS,因此为了使其正常工作,我们专注于使用一些事件侦听器来跟踪用户正在做什么。
为了跟踪用户活动,我创建了 3 个对象– 一个是通用配置项,另外两个分别与铅笔和箭头绘图工具相关。下面显示了这些配置以及评论中每个项目的解释。
let config = {
drawing: false, // Set to true if we are drawing, false if we aren't
tool: 'freeHand', // The currently selected tool
color : 'white', // The currently selected colour
strokeWidth: 4, // The width of the lines we draw
configNormalisation: 12,// The average normalisation for pencil drawing
}
let arrow = {
// topX, Y, and bottomX, Y store information on the arrows top and bottom ends
topX: 0,
topY: 0,
bottomX: 0,
bottomY: 0,
activeDirection: 'se', // This is the current direction of the arrow, i.e. south-east
arrowClasses: [ 'nw', 'ne', 'sw', 'se' ], // These are possible arrow directions
lineAngle: 0, // This is the angle the arrow point at about the starting point
}
let freeHand = {
currentPathText: 'M0 0 ', // This is the current path of the pencil line, in text
topX: 0, // The starting X coordinate
topY: 0, // The starting Y coordinate
lastMousePoints: [ [0, 0] ], // This is the current path of the pencil line, in array
}
控制用户界面#
下一个主要步骤是添加 UI。我创建了一个简单的绘图 UI,当用户单击“开始绘图”时会弹出该 UI。在 HTML 中,它看起来像这样:
<button class="animated hover-button" id="start-drawing"><span>✍️ Activate Drawing</span></button>
<div id="drawing-cover"></div>
<div id="drawing-layer"></div>
<div id="drawing-box">
<div class="tools">
<button data-tool="freeHand" data-current="true"><span>Pen</span></button>
<button data-tool="arrow"><span>Arrow</span></button>
<button data-tool="eraser"><span>Eraser</span></button>
</div>
<div class="colors">
<div data-color="white" data-rColor="white" data-current="true"></div>
<div data-color="black" data-rColor="#544141"></div>
<div data-color="red" data-rColor="#d83030"></div>
<div data-color="green" data-rColor="#30d97d"></div>
<div data-color="orange" data-rColor="#ff9000"></div>
<div data-color="yellow" data-rColor="f3f326"></div>
</div>
<div class="close">
Close
</div>
</div>
注意:我添加了另外两个位于 UI 之外的元素 – 一个是drawing-cover
,它在激活绘图时简单地覆盖屏幕,另一个是drawing-layer
,它包含用户绘制的所有绘图元素。
对于每种工具和颜色,我都附加了一些数据属性:
- 对于工具– 每个项目都有一个数据工具属性。
- 对于颜色– 每个项目都有一个data-color和data-rColor属性,分别指颜色的文本名称和它的十六进制值。
我们需要数据属性,因为当用户单击项目时,我们将在代码中引用它们。下面是用于控制 UI 的代码 – 实际上,我们在这里所做的只是config
在用户单击工具或颜色时更改我们的主要对象:
// Add a pointerdown event for each color and tool.
// When a user clicks a color or tool, then we set it to our current config.color or config.tool respectively, and highlight it on the UI
[ 'data-rColor', 'data-tool' ].forEach(function(i) {
document.querySelectorAll(`[${i}]`).forEach(function(item) {
item.addEventListener('pointerdown', function(e) {
document.querySelectorAll(`[${i}]`).forEach(function(i) {
i.setAttribute('data-current', false);
});
item.setAttribute('data-current', true);
if(i == 'data-rColor') {
config.color = item.getAttribute(i);
} else if(i == 'data-tool') {
config.tool = item.getAttribute(i);
}
});
});
});
// Set the body attribute 'data-drawing' to true or false, based on if the user clicks the 'Start Drawing' button
// Also sets config.drawing to true or false.
document.getElementById('start-drawing').addEventListener('click', function(e) {
if(config.drawing === true) {
config.drawing = false;
document.body.setAttribute('data-drawing', false)
} else {
let drawingCover = document.getElementById('drawing-cover');
document.body.setAttribute('data-drawing', true)
config.drawing = true;
}
});
// Closes the drawing box and sets 'data-drawing' on the body element to false
// Along with cofig.drawing to false.
document.querySelector('#drawing-box .close').addEventListener('click', function(e) {
document.body.setAttribute('data-drawing', false);
config.drawing = false;
})
我们绘图的 HTML 元素#
要绘制,我们必须在用户屏幕上附加一些东西。为了使事情变得简单和模块化,我使用了 SVG。根据您的用例,如果需要,这也可以用画布重写 – 但是 SVG 也可以正常工作。SVG 在我们需要时为我们提供了对 DOM 元素的大量控制。我创建了两个函数,它们为箭头和线条图返回 HTML。这些如下所示。
由于我们需要将一些数字传递给这些 HTML 元素,因此我们为这些函数提供了变量(即start、dimensions、path等。这允许我们为 SVG 标签提供更新的定位,我们可以将其渲染到页面上。
let svgEl = {
arrowPath: (start, dimensions, path, dummy, direction, end, angle, hyp, id) =>
`<div class="arrow drawing-el static current-item" data-id="${id}" data-direction="${direction}" style="left: ${start[0]}px; top: ${start[1]}px; height: ${dimensions[1]}px; width: ${dimensions[0]}px;">
<div class="arrow-point arrow-point-one"></div>
<div class="arrow-point arrow-point-two" style="
transform-origin: 0 0; left: ${hyp[1]}px; top: ${hyp[2]}px; transform: rotateZ(${angle}deg) translateY(-${hyp[0]}px) translateX(-15px);
"></div>
<svg viewbox="0 0 ${dimensions[0]} ${dimensions[1]}">
<defs>
<marker id="arrow-head-${id}" class="arrow-resizer" markerWidth="10" markerHeight="10" refX="0" refY="3" orient="auto" markerUnits="strokeWidth" viewBox="0 0 20 20">
<path d="M0 0 L0 6 L9 3 z" fill="${config.color}" />
</marker>
</defs>
<path marker-start="url(#bottom-marker)" style="stroke: ${config.color}; stroke-width: ${config.strokeWidth}" marker-end="url(#arrow-head-${id})" class="arrow-line" d="${path}" />
</svg>
</div>`,
drawPath: (start, dimensions, path, id) =>
`<div class="free-hand drawing-el static current-item" data-id="${id}" style="left: ${start[0]}px; top: ${start[1]}px; height: ${dimensions[1]}px; width: ${dimensions[0]}px;">
<svg viewbox="0 0 ${dimensions[0]} ${dimensions[1]}">
<path d="${path}" style="stroke: ${config.color}; stroke-width: ${config.strokeWidth}" />
</svg>
</div>`
}
这两个函数为我们提供了正确的 SVG 用于箭头和手绘。
用户交互#
最后一步是添加用户交互。最终,这归结为三个主要功能:
- a
mousedown
,告诉我们用户何时开始绘图,假设他们点击了“开始绘图”按钮。 - a
mousemove
,跟踪用户的鼠标移动。 - a
mouseup
,当用户完成绘图时。
第 1 步:鼠标按下
第一阶段是mousedown
。这将在用户点击我们的网页时触发。因此,我们要确保用户正在绘制(即为config.drawing
true )。我们可以通过检查我们的config
对象是否config.drawing
设置为true来做到这一点。如果我们正在绘图,那么我们将用户单击的初始点存储在或freeHand
configarrow
对象中。
最后,我们将 HTML 元素附加到页面。如果我们使用橡皮擦,我们会检查用户点击的点是否为 SVG,如果点击则将其删除。为此,我们使用父辅助函数,可以在Github Repo或我们的“>Codepen示例中找到。
document.body.addEventListener('pointerdown', function(e) {
// Generate id for each element
let id = helper.generateId();
if(config.tool == 'arrow' && config.drawing == true) {
// Set arrow start point
arrow.topX = e.clientX;
arrow.topY = e.clientY;
// Add element to drawing layer
document.getElementById('drawing-layer').innerHTML = document.getElementById('drawing-layer').innerHTML +
svgEl.arrowPath( [ arrow.topX + window.scrollX, arrow.topY + window.scrollY ], [ e.clientX, e.clientX ], `M0 0 L0 0`, 'arrow-item', arrow.arrowClasses[3], [ 0, 0 ], 0, [ 0, 0, 0 ], id );
}
else if(config.tool == 'freeHand' && config.drawing == true) {
// Set the drawing starting point
freeHand.topX = e.clientX;
freeHand.topY = e.clientY;
// Set the current path and most recent mouse points to whereever we are scrolled on the page
freeHand.currentPathText = `M${window.scrollX} ${window.scrollY} `;
freeHand.lastMousePoints = [[ window.scrollX, window.scrollY ]];
// Add element to the drawing layer
document.getElementById('drawing-layer').innerHTML = document.getElementById('drawing-layer').innerHTML +
svgEl.drawPath( [ e.clientX, e.clientY ], [ e.clientX, e.clientY ], ``, id);
}
else if(config.tool == 'eraser' && config.drawing == true) {
// Check if user has clicked on an svg
if(helper.parent(e.target, '.drawing-el', 1) !== null && helper.parent(e.target, '.drawing-el', 1).matches('.drawing-el')) {
// If they have, delete it
helper.parent(e.target, '.drawing-el', 1).remove();
}
}
})
第 2 步:鼠标移动
接下来让我们研究一下用户点击然后移动鼠标时会发生什么。在这种情况下,我们想徒手延长线,或移动箭头的箭头。当前绘制的元素有一个名为current-item的类,因此我们可以使用它来更新我们的 HTML 元素。从根本上说,我们只是想根据用户鼠标的位置向 SVG 元素添加更多点。由于我们存储了用户点击的原始位置,我们可以使用它作为参考点来计算用户从那里移动config
了多少像素。为此,我们还使用了两个计算辅助函数,这两个函数都可以在Github Repo或我们的codepen示例中找到:
- 对于箭头,我们
calculateArrowLineAngle
用来计算箭头的角度和方向。 - 对于徒手,我们使用
getAveragePoint
计算最后几次鼠标移动的平均值,以创建一条平滑线。
static
移动后,我们还会从绘制的元素中删除该类。这让我们知道用户想要保留这个绘制的元素。如果他们没有移动,我们稍后会在他们将手指从鼠标上移开时将其移除,并且static
类让我们确定这一点。
document.body.addEventListener('pointermove', function(e) {
// Assuming there is a current item to in the drawing layer
if(document.querySelector('#drawing-layer .current-item') !== null) {
// If we are using the arrow tool
if(config.drawing == true && config.tool == 'arrow') {
// Then get the original start position
let startX = arrow.topX;
let startY = arrow.topY;
// Set a default angle of 90
let angleStart = 90;
// And a default direction of 'south east'
let arrowClass = arrow.arrowClasses[3];
// Calculate how far the user has moved their mouse from the original position
let endX = e.pageX - startX - window.scrollX;
let endY = e.pageY - startY - window.scrollY;
// And using that info, calculate the arrow's angle
helper.calculateArrowLineAngle(endX, endY);
// Then update the config to this new end position
arrow.bottomX = endX;
arrow.bottomY = endY;
// And update the HTML to show the new arrow to the user
document.querySelector('#drawing-layer .arrow.current-item').classList.remove('static');
document.querySelector('#drawing-layer .arrow.current-item').setAttribute('data-direction', arrow.activeDirection);
document.querySelector('#drawing-layer .arrow.current-item svg').setAttribute('viewbox', `0 ${endX} 0 ${endY}`);
document.querySelector('#drawing-layer .arrow.current-item path.arrow-line').setAttribute('d', `M0 0 L${endX} ${endY}`);
}
else if(config.drawing == true && config.tool == 'freeHand') {
// Similar to arrows, calculate the user's end position
let endX = e.pageX - freeHand.topX;
let endY = e.pageY - freeHand.topY;
// And push these new coordinates to our config
let newCoordinates = [ endX, endY ];
freeHand.lastMousePoints.push([endX, endY]);
if(freeHand.lastMousePoints.length >= config.configNormalisation) {
freeHand.lastMousePoints.shift();
}
// Then calculate the average points to display a line to the user
let avgPoint = helper.getAveragePoint(0);
if (avgPoint) {
freeHand.currentPathText += " L" + avgPoint.x + " " + avgPoint.y;
let tmpPath = '';
for (let offset = 2; offset < freeHand.lastMousePoints.length; offset += 2) {
avgPoint = helper.getAveragePoint(offset);
tmpPath += " L" + avgPoint.x + " " + avgPoint.y;
}
// Set the complete current path coordinates
document.querySelector('#drawing-layer .free-hand.current-item').classList.remove('static');
document.querySelector('#drawing-layer .free-hand.current-item svg path').setAttribute('d', freeHand.currentPathText + tmpPath);
}
}
}
});
第 3 步:鼠标上移
鼠标向上的目的是a)重置绘图配置freeHand
和b)删除用户未移动鼠标的任何元素arrow
。如果我们不做b),那么当用户点击页面时会出现随机箭头。
与其他功能相比,这相对简单,如下所示:
// Whenever the user leaves the page with their mouse or lifts up their cursor
[ 'mouseleave', 'pointerup' ].forEach(function(item) {
document.body.addEventListener(item, function(e) {
// Remove current-item class from all elements, and give all SVG elements pointer-events
document.querySelectorAll('#drawing-layer > div').forEach(function(item) {
item.style.pointerEvent = 'all';
item.classList.remove('current-item');
// Delete any 'static' elements
if(item.classList.contains('static')) {
item.remove();
}
});
// Reset freeHand variables where needed
freeHand.currentPathText = 'M0 0 ';
freeHand.lastMousePoints = [ [0, 0] ];
});
});
结论#
我们完成了。由于我们使用了 pointerdown、pointermove和pointerup,这个演示应该也可以在移动设备上运行。下面,我附上了一些有用的链接,包括 Github 和 Codepen 上的源代码。如果您有任何问题,可以通过Twitter联系我们。
javascript-frontend-drawing-annotation-application