SVG(可扩展的向量图形)是在网络上创建交互式和动态图形的强大工具。一个有趣的功能是能够沿着预定义的路径进行动画和操纵形状。在本文中,我们将探讨使用TypeScript实现此功能的逐步过程。
目录
HTML标记
首先,让我们定义一个称为index.html
的HTML文件,该文件将用作我们的SVG元素以及随附的JavaScript和CSS文件的容器。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1">
<title>Tutorial</title>
</head>
<body>
</body>
</html>
让我们使用以下代码添加一个宽度为500像素和300像素的SVG element:
<svg
id="svg"
xmlns="http://www.w3.org/2000/svg"
width="500"
height="300">
</svg>
<svg>
标签可作为我们即将到来的动画沿SVG椭圆路径的容器。
现在,让我们创建将用作运动路径的an ellipse element。椭圆定义为坐标处的中心点(250,150),这是SVG容器的高度和宽度的一半。
<svg
id="svg"
xmlns="http://www.w3.org/2000/svg"
width="500"
height="300">
<ellipse
cx="250"
cy="150"
rx="245"
ry="145"
fill="none"
stroke="#3d8ea7"
stroke-width="10"></ellipse>
</svg>
椭圆的笔触决定了形状的轮廓或边界。在我们的情况下,椭圆的中风由stroke="#3d8ea7"
和stroke-width="10"
属性定义。这意味着笔触颜色设置为绿色的阴影,并将中风宽度设置为10个单位。
将行程应用于椭圆形时,通常遵循形状的边界。在这种情况下,带有10 units
的中风宽度,中风将在椭圆形内延伸5 units
在椭圆形外。
这会导致一半的中风在椭圆体内呈现,而另一半则在外面呈现。因此,水平半径和垂直半径应为5个单位,较小的(245, 145)
,否则,椭圆将移到SVG容器外。
现在,让我们添加将沿椭圆路径拖动的circle shape:
<svg
id="svg"
xmlns="http://www.w3.org/2000/svg"
width="500"
height="300">
<ellipse
cx="250"
cy="150"
rx="245"
ry="145"
fill="none"
stroke="#3d8ea7"
stroke-width="10"></ellipse>
<circle
id="pointer"
cx="495"
cy="150"
r="30"
cursor="pointer"
fill="#efefef"></circle>
</svg>
此圆元素位于坐标(495, 150)
,半径为30个单位的(r)
。它具有光标样式设置为“指针”,表明悬停在盘旋时应该更改为指针光标。
x位置为495,因为我们是从容器的宽度(500)中减去stroke-width
(5)的一半,因此圆圈位于路径的中间。
但是,在我们查看结果之后,事实证明圆圈位于SVG容器之外。要解决此问题,我们可以这样更改标记:
<svg
id="svg"
xmlns="http://www.w3.org/2000/svg"
width="500"
height="300">
<ellipse
cx="250"
cy="150"
rx="220"
ry="120"
fill="none"
stroke="#3d8ea7"
stroke-width="10"></ellipse>
<circle
id="pointer"
cx="470"
cy="150"
r="30"
cursor="pointer"
fill="#efefef"></circle>
</svg>
在椭圆形中,水平半径220 = (500/2 - 30)
等于SVG容器的宽度一半,减去指针圆的宽度。垂直半径120 = (300/2 - 30)
。
指针圆的水平位置等于
svg宽度减去圆圈的宽度。
打字稿
在本教程中,我们将使用TypeScript。 Typescript是一种编程语言,是JavaScript的超集,它添加了静态键入层,允许开发人员为变量,函数参数和返回值定义类型。这有助于在开发过程中捕获错误,并为代码完成和重构提供更好的工具支持。
我将使用Typescript与esbuild bundler一起使用,这是JavaScript和Typescript的快速且高效的构建器。
确保已安装了node.js并在项目文件夹中打开终端:
npm init -y
npm install esbuild typescript --save-dev
tsc --init
命令npm init -y
用于初始化新的NPM
带有默认设置的项目。当您在项目的根目录中运行此命令时,它将创建一个包装。
命令npm install esbuild typescript --save-dev
用于将“ esbuild”和“ typeScript”软件包安装为npm项目中的devDepentencies。
命令tsc --init
用于通过在当前目录中生成tsconfig.json文件来初始化打字稿项目。该文件包含有关打字稿编译器的各种配置选项。通过自定义tsconfig.json文件,您可以配置打字稿编译设置,定义项目的文件结构,启用特定功能等。
让我们创建一个带有以下内容的index.ts
文件:
console.log('test');
现在我们可以将 start 命令添加到package.json
:
{
"name": "tutorial",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "./node_modules/.bin/esbuild index.ts --bundle --watch --outfile=index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"esbuild": "^0.17.19",
"typescript": "^5.1.3"
}
}
命令./node_modules/.bin/esbuild index.ts --bundle --watch --outfile=index.js
使用本地安装的Esbuild软件包来捆绑index.ts
文件并生成index.js
输出文件。这是命令的故障:
-
./node_modules/.bin/esbuild
:这指定了位于项目的node_modules/.bin目录中的Esbuild可执行脚本的路径。通过运行此脚本,您可以调用Esbuild Bundler。 -
index.ts
:这是捆绑过程的输入文件。它指定了将要处理和捆绑的打字稿文件。 -
--bundle
:此标志告诉Esbuild执行捆绑过程,该过程将多个文件结合到单个输出文件中。 -
--watch
:此标志指示Esbuild注意源文件的更改,并在检测到更改时自动触发新构建。 -
--outfile=index.js
:此标志指定捆绑的JavaScript文件的输出文件名和路径。在这种情况下,输出文件将命名为index.js
。
现在,您可以在终端中运行npm start
命令来生成 index.js 文件。 Esbuild将注意源文件的更改,并自动重新编译代码。要退出手表模式,只需按终端中的Ctrl C
。
拖放
要在SVG内实现拖放功能,我们可以从getElementById中选择SVG和圆圈元素:
/**
* This is where we place all the initialization logic.
*/
const init = () => {
const $svg = document.getElementById('svg');
const $pointer = document.getElementById('pointer');
if(!$svg || !$pointer) return;
};
init();
HTML文件现在应该有一个JavaScript参考:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1">
<title>Tutorial</title>
</head>
<body>
<svg
id="svg"
xmlns="http://www.w3.org/2000/svg"
width="500"
height="300">
<ellipse
cx="250"
cy="150"
rx="220"
ry="120"
fill="none"
stroke="#3d8ea7"
stroke-width="10"></ellipse>
<circle
id="pointer"
cx="470"
cy="150"
r="30"
cursor="pointer"
fill="#efefef"></circle>
</svg>
<!-- Reference to the compiled JavaScript file -->
<script src="index.js"></script>
</body>
</html>
现在,让我们实现基本的拖放流。下面的代码将事件侦听器附加到$指针元素以处理鼠标和触摸事件。
/**
* This is where we place all the initialization logic.
*/
const init = () => {
const $svg = document.getElementById('svg');
const $pointer = document.getElementById('pointer');
if(!$svg || !$pointer) return;
/**
* Here will be all the logic related to pointer movement.
*/
const onValueChange = (evt: MouseEvent | TouchEvent) => {
console.log(evt);
}
/**
* Add event listeners as soon as
* the user presses the mouse button.
*/
const onMouseDown = (evt: MouseEvent) => {
evt.preventDefault();
onValueChange(evt);
window.addEventListener('mousemove', onValueChange);
window.addEventListener('mouseup', onMouseUp);
};
/**
* Remove event listeners as soon as
* the user releases the mouse button.
*/
const onMouseUp = () => {
window.removeEventListener('mousemove', onValueChange);
window.removeEventListener('mouseup', onValueChange);
};
// Attach event listeners to the $pointer element
// to handle mouse and touch events.
$pointer.addEventListener('mousedown', onMouseDown);
$pointer.addEventListener('mouseup', onMouseUp);
$pointer.addEventListener('touchmove', onValueChange);
$pointer.addEventListener('touchstart', onValueChange);
};
init();
在上面的代码中,我们在$指针元素上添加了鼠标和触摸事件的事件侦听器。当用户按或发布元素上的鼠标按钮时,将调用相关功能来处理事件。
椭圆运动
首先,我们需要找出鼠标坐标。对于鼠标事件和触摸事件,确定的方式将有所不同。一种方法是使用evt.type
Property,它代表发生的事件类型:
const onValueChange = (evt: MouseEvent | TouchEvent) => {
let mouseX, mouseY;
const isMouse = evt.type.indexOf('mouse') !== -1;
if(isMouse){
// Mouse events
mouseX = (evt as MouseEvent).clientX;
mouseY = (evt as MouseEvent).clientY;
}
else{
// Touch events
mouseX = (evt as TouchEvent).touches[0].clientX;
mouseY = (evt as TouchEvent).touches[0].clientY;
}
console.log(mouseX, mouseY);
}
我们还定义一些常数,例如椭圆的半径及其中心。在实际生产代码中,我们可以动态地找到这些值,但是在此演示中,我们将它们定义为保持代码简单的常数。
// The ellipse path radii constants.
const RADIUS_X = 220;
const RADIUS_Y = 120;
// The absolute top left coordinates of the SVG.
const {
left: ABS_SVG_LEFT,
top: ABS_SVG_TOP,
width: SVG_WIDTH,
height: SVG_HEIGHT
} = $svg.getBoundingClientRect();
// The center of the SVG.
const SVG_CENTER_LEFT = SVG_WIDTH/2;
const SVG_CENTER_TOP = SVG_HEIGHT/2;
这些线计算SVG元素的中心坐标,并将它们存储在变量SVG_CENTER_LEFT
和SVG_CENTER_TOP
中。 getBoundingClientRect()方法在$ SVG元素上被调用,以获取其边界矩形,其中包括left
,top
,width
和height
等属性。使用破坏分配,将左和顶部属性的值分别提取并分配给ABS_SVG_LEFT
和ABS_SVG_TOP
变量。
const onValueChange = (evt: MouseEvent | TouchEvent) => {
let mouseX, mouseY;
const isMouse = evt.type.indexOf('mouse') !== -1;
if(isMouse){
mouseX = (evt as MouseEvent).clientX;
mouseY = (evt as MouseEvent).clientY;
}
else{
mouseX = (evt as TouchEvent).touches[0].clientX;
mouseY = (evt as TouchEvent).touches[0].clientY;
}
// Calculate the relative mouse position.
const relativeMouseX = mouseX - ABS_SVG_LEFT - SVG_CENTER_LEFT;
const relativeMouseY = mouseY - ABS_SVG_TOP - SVG_CENTER_TOP;
}
通过从鼠标坐标中减去SVG中心的坐标,结果值relativeMouseX
和relativeMouseY
表示鼠标的位置相对于SVG元素的中心。
现在,我们需要计算代表当前鼠标位置的椭圆内部的角度。我们可以使用Math.atan2()
函数,该函数返回正x轴和鼠标表示的点之间的弧度的角度。
const angle = Math.atan2(
relativeMouseY / RADIUS_Y,
relativeMouseX / RADIUS_X
);
-
relativeMouseX / RADIUS_X
:此表达式计算沿X轴从SVG中心的相对水平距离。 -
relativeMouseY / RADIUS_Y
:此表达式计算沿y轴的SVG中心的相对垂直距离。
现在我们有了角度,我们可以沿椭圆路径计算指针中心的新坐标。为此,我们可以使用parametric equation of an ellipse:
x(angle) = radius1 * cos(angle)
y(angle) = radius2 * sin(angle)
因此,onValueChange()
函数的最后部分是:
const newX = SVG_CENTER_LEFT + Math.cos(angle) * RADIUS_X;
const newY = SVG_CENTER_TOP + Math.sin(angle) * RADIUS_Y;
// Update the pointer's center coordinates.
$pointer.setAttribute('cx', newX.toString());
$pointer.setAttribute('cy', newY.toString());
在提供的代码中,根据SVG椭圆的角度和半径计算两个变量newX
和newY
。这些变量代表$pointer
元素的新中心坐标。然后,对$指针元素的cx
和cy
属性进行了更新以将其移至新坐标。
概括
通过制作一个半径而不是两个半径,可以轻松地适应圆形路径而不是椭圆路径。请检查this codepen。
我希望这篇文章在您的编程旅程中很有趣并且有帮助。愉快的编码! ð
也请看一下Tool Cool Range Slider项目。这是用打字稿和使用Web组件技术编写的响应范围滑块库。它具有丰富的设置,包括任何数量的指针(旋钮),垂直和水平滑块,触摸,鼠标轮和键盘支持,本地和会话存储,范围拖动以及RTL支持。