如何沿SVG椭圆路径拖动形状:分步指南
#javascript #教程 #typescript #svg

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_LEFTSVG_CENTER_TOP中。 getBoundingClientRect()方法在$ SVG元素上被调用,以获取其边界矩形,其中包括lefttopwidthheight等属性。使用破坏分配,将左和顶部属性的值分别提取并分配给ABS_SVG_LEFTABS_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中心的坐标,结果值relativeMouseXrelativeMouseY表示鼠标的位置相对于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椭圆的角度和半径计算两个变量newXnewY。这些变量代表$pointer元素的新中心坐标。然后,对$指针元素的cxcy属性进行了更新以将其移至新坐标。

概括

最终结果可以在此codepen以及GitHub中找到。

通过制作一个半径而不是两个半径,可以轻松地适应圆形路径而不是椭圆路径。请检查this codepen

我希望这篇文章在您的编程旅程中很有趣并且有帮助。愉快的编码! ð

也请看一下Tool Cool Range Slider项目。这是用打字稿和使用Web组件技术编写的响应范围滑块库。它具有丰富的设置,包括任何数量的指针(旋钮),垂直和水平滑块,触摸,鼠标轮和键盘支持,本地和会话存储,范围拖动以及RTL支持。