马黑黑 发表于 2023-9-18 17:53

svg+html : 曲线进度条的实现

本帖最后由 马黑黑 于 2023-9-18 18:39 编辑 <br /><br /><style>
.code { tab-size: 4; font: normal 14px/20px Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; background: #efefef; padding: 10px; white-space: pre-wrap; word-break: break-word; }
.zs { color: green; }
</style>
<p>总体思路:<br><br></p>
<p>借助 svg 的 path 元素创建曲线路径 d,d 路径将显示在容器元素之上,它还将成为模拟进度滑块的 html 元素的 offset-path 值,即 d 路径同时也是描述滑块行进的路线。<br><br></p>
<p>路径设计:<br><br></p>
<p>考虑到易用性和复用性,容器元素应可以自定义宽高,为此,路径应能依据容器元素的尺寸动态生成。svg d 路径不支持百分比,为此只能在运行时根据容器元素尺寸设计路径,出于简化目的,可以使用二次贝塞尔曲线来完成曲线设计:从容器的左上角开始到容器的右上角终止,控制点 (x,y) 的 x 为容器宽度的一半、y 为容器高度减去 20。<br><br></p>
<p>实现细节:<br><br></p>
<p>一、CSS样式<br><br></p>
<p>#mydiv 选择器指向容器元素,#ball 选择器是模拟滑块,#svg 选择器是 svg 元素,#mypath 是在 svg 里设计的 path 路径。<br><br></p>
<pre class="code">
&lt;style&gt;

#mydiv {
        margin: 20px auto;
        width: 200px;
        height: 120px;
        box-sizing: border-box;
        position: relative;
        --prog: 0%;
}

#ball, #svg { position: absolute; }

#ball {
        position: absolute;
        width: 10px;
        height: 10px;
        background: green;
        pointer-events: none;
        offset-distance: var(--prog);
}

#mypath { cursor: pointer; }

&lt;/style&gt;
</pre>
<p>二、HTML结构<br><br></p>
<p>容器元素下包裹一个 svg 元素和 一个 div 模拟滑块,svg 标签下有一个 path 路径元素,其 d 路径可以随意设置,它将被后面 JS 动态生成的实际使用路径所覆盖。<br><br></p>
<pre class="code">
&lt;div id="mydiv"&gt;
        &lt;svg id="svg" width="100%" height="100%"&gt;
                &lt;path id="mypath" d="M0 0 Q100 140 200 0" fill="none" stroke="silver" stroke-width="4" /&gt;
        &lt;/svg&gt;
        &lt;div id="ball"&gt;&lt;/div&gt;
&lt;/div&gt;
</pre>
<p>三、JS完成路径生成与进度交互<br><br></p>
<p>首先,声明两个变量:<br><br></p>
<pre class="code">let posAr = [], len = 0;</pre>
<p>posAr 数组变量用来记录路径上每一个点的 x 坐标,将来进度交互会用到它;len 是曲线长度,0 表示它是一个数值,将来会被实际长度覆盖。<br><br></p>
<p>接下来写一个自执行匿名函数,它完成两个功能:一是根据容器元素的尺寸生成我们前面设计好大体模样的路径,并将路径加诸于 mypath 元素 和 ball 元素;二是获得曲线每一个像素的 X 坐标值,存入 posAr 数组中。<br><br></p>
<pre class="code">
(function() {
        let ww = mydiv.offsetWidth, hh = mydiv.offsetHeight;
        let d = `M0 0 Q${ww/2} ${hh * 2 - 20} ${ww} 0`;
        ball.style.setProperty('offset-path', `path('${d}')`);
        mypath.setAttribute('d', d);
        len = mypath.getTotalLength(); <span class="zs">//路径总长度</span>
        <span class="zs">//遍历路径长度每一个像素单位,储存其所对应的X坐标</span>
        for(let j = 0; j &lt; len; j++) {
                posAr.push(mypath.getPointAtLength(j).x);
        };
})();
</pre>
<p>以上匿名函数,关键在于基于 svg 的两个内置 API 函数,getTotalLength 和 getPointAtLength,前者获得 svg 内部元素的总长度,后者获取内部元素某个长度下的XY坐标,其值以对象 {x: n1, y: n2} 返回。<br><br></p>
<p>最后,通过 mypath 路径的点击事件,动态更改进度:<br><br></p>
<pre class="code">
mypath.onclick = (e) => {
        for(let j = 0; j &lt; len; j++) {
                if(e.offsetX &lt;= posAr) {
                        mydiv.style.setProperty('--prog', j / len * 100 + '%');
                        break;
                }
        }
};
</pre>
<p>这里,被点击对象(mypath)的 e.offsetX 指向点击处的X坐标值,如果该值在数组 posAr 遍历其元素时小于等于某个数组元素所记录的坐标值,则终止for循环(break),终止循环前给 CSS变量 --prog 赋值,这样,html 元素 ball 就会移动到这个 X 坐标值上。<br><br></p>
<p>效果:<br><br></p>

<style>

#mydiv {
        margin: 20px auto;
        width: 200px;
        height: 120px;
        box-sizing: border-box;
        position: relative;
        --prog: 0%;
}

#ball, #svg { position: absolute; }

#ball {
        position: absolute;
        width: 10px;
        height: 10px;
        background: green;
        pointer-events: none;
        offset-distance: var(--prog);
}

#mypath { cursor: pointer; }
</style>

<div id="mydiv">
        <svg id="svg" width="100%" height="100%">
                <path id="mypath" d="M0 0 Q100 140 200 0" fill="none" stroke="silver" stroke-width="4" />
        </svg>
        <div id="ball"></div>
</div>

<script>

let posAr = [], len = 0;

(function() {
        let ww = mydiv.offsetWidth, hh = mydiv.offsetHeight;
        let d = `M0 0 Q${ww/2} ${hh * 2 - 20} ${ww} 0`;
        ball.style.setProperty('offset-path', `path('${d}')`);
        mypath.setAttribute('d', d);
        len = mypath.getTotalLength();
        for(let j = 0; j < len; j++) {
                posAr.push(mypath.getPointAtLength(j).x);
        };
})();

mypath.onclick = (e) => {
        for(let j = 0; j < len; j++) {
                if(e.offsetX <= posAr) {
                        mydiv.style.setProperty('--prog', j / len * 100 + '%');
                        break;
                }
        }
};

</script>

红影 发表于 2023-9-18 20:17

这个好,不再是直线的了,可以是曲线的进度了,会更有特色{:4_187:}

红影 发表于 2023-9-18 20:23

“控制点 (x,y) 的 x 为容器宽度的一半、y 为容器高度减去 20。”这个减去20是从margin: 20px auto;中来的么?

红影 发表于 2023-9-18 20:26

这个JS还挺复杂的。

马黑黑 发表于 2023-9-18 21:06

红影 发表于 2023-9-18 20:17
这个好,不再是直线的了,可以是曲线的进度了,会更有特色

曲线其实是不好做的,比圆和椭圆更复杂。曲线可能是规则的,可能不是,尚且,路径也不一定非得是曲线、弧线不可,这个时候,要通过公式去计算路径上的每一个点的XY坐标就非常麻烦,所以,把路径上的基于每一个像素的点的XY坐标储存到一个数组里,是最简单的解决问题的方法了。

马黑黑 发表于 2023-9-18 21:07

红影 发表于 2023-9-18 20:23
“控制点 (x,y) 的 x 为容器宽度的一半、y 为容器高度减去 20。”这个减去20是从margin: 20px auto;中来的 ...

不是。这是基于svg画布的,不减去一定的数量,曲线的凸点会越位看不见。

马黑黑 发表于 2023-9-18 21:10

红影 发表于 2023-9-18 20:26
这个JS还挺复杂的。
你从代码量看,JS已经是很简单了。自执行匿名函数做两件事:画路径、把路径的每一个像素点的X坐标记录下来,用到两个svg api 的函数;路径点击事件只做一件事:比较点击点的X坐标和记录里对应的坐标值,依此确定进度位置。

红影 发表于 2023-9-18 23:23

马黑黑 发表于 2023-9-18 21:06
曲线其实是不好做的,比圆和椭圆更复杂。曲线可能是规则的,可能不是,尚且,路径也不一定非得是曲线、弧 ...

这个“每一个像素的点的XY坐标储存到一个数组里”还真不懂{:4_173:}

红影 发表于 2023-9-18 23:24

马黑黑 发表于 2023-9-18 21:07
不是。这是基于svg画布的,不减去一定的数量,曲线的凸点会越位看不见。

哦,还有这样的问题啊,这个也是一点都不知道呢。

红影 发表于 2023-9-18 23:25

马黑黑 发表于 2023-9-18 21:10
你从代码量看,JS已经是很简单了。自执行匿名函数做两件事:画路径、把路径的每一个像素点的X坐标记录下 ...

这个也是这种进度最难的地方了吧。

马黑黑 发表于 2023-9-19 12:06

红影 发表于 2023-9-18 23:25
这个也是这种进度最难的地方了吧。

getPointsAtLength 能够捕捉到任意线段长度的XY坐标,所以就不是难题了:按像素遍历线段长度,记下所有的坐标值,然后调节进度时再去循环比对。这是小事了。

马黑黑 发表于 2023-9-19 12:08

红影 发表于 2023-9-18 23:24
哦,还有这样的问题啊,这个也是一点都不知道呢。

想象一下都可以知道的:一根钢线在一个矩形框里,从中间外上或往下拉扯,拉过头了弧形的凸点就会越过矩形的上或下边缘。

马黑黑 发表于 2023-9-19 12:11

红影 发表于 2023-9-18 23:23
这个“每一个像素的点的XY坐标储存到一个数组里”还真不懂

这个好简单的呀。比方一根曲线,长度为100像素,则,用 for 语句遍历100次,用 getPointAtLength 取得坐标值,然后存入数组中。

红影 发表于 2023-9-19 15:25

马黑黑 发表于 2023-9-19 12:06
getPointsAtLength 能够捕捉到任意线段长度的XY坐标,所以就不是难题了:按像素遍历线段长度,记下所有的 ...

嗯嗯,有这个东西就变简单了{:4_173:}

红影 发表于 2023-9-19 15:31

马黑黑 发表于 2023-9-19 12:08
想象一下都可以知道的:一根钢线在一个矩形框里,从中间外上或往下拉扯,拉过头了弧形的凸点就会越过矩形 ...

饿呢,知道了。

红影 发表于 2023-9-19 15:32

马黑黑 发表于 2023-9-19 12:11
这个好简单的呀。比方一根曲线,长度为100像素,则,用 for 语句遍历100次,用 getPointAtLength 取得坐 ...

嗯嗯,这个 getPointAtLength 挺厉害,主要对这个不熟悉啊{:4_173:}

马黑黑 发表于 2023-9-19 18:06

红影 发表于 2023-9-19 15:32
嗯嗯,这个 getPointAtLength 挺厉害,主要对这个不熟悉啊

这是 svg 的 API,之前我也不知道,我只知道有一个 getTotalLength 的函数。然后要处理曲线点击的问题,就去查文档,这才知道的。

马黑黑 发表于 2023-9-19 18:07

红影 发表于 2023-9-19 15:31
饿呢,知道了。

我不用橡皮筋做比喻,是因为橡皮筋不合适,钢丝很合适的

马黑黑 发表于 2023-9-19 18:09

红影 发表于 2023-9-19 15:25
嗯嗯,有这个东西就变简单了

用JS解决问题,其实与列方程解应用题是一个道理的。在已知条件和可用公式、定律基础上找到方法,列出解题的方程,然后解这个方程 。

红影 发表于 2023-9-19 21:24

马黑黑 发表于 2023-9-19 18:06
这是 svg 的 API,之前我也不知道,我只知道有一个 getTotalLength 的函数。然后要处理曲线点击的问题, ...

这还是因为你熟悉,要让我去查,我都不知道查什么{:4_173:}
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: svg+html : 曲线进度条的实现