马黑黑 发表于 2023-9-23 08:20

判断不规则svg路径被点击点的路径节点长度

<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; }
#mydiv {
        margin: 40px auto;
        width: 300px;
        height: 160px;
        position: relative;
}
#cap {
        position: absolute;
        width: 15px;
        height: 15px;
        background: green;
        pointer-events: none;
        offset-path: path('M0 100 H80 Q-100 -60 220 100 H300');
        offset-distance: var(--prg);
        --prg: 0%;
}
#mysvg {
        position: absolute;
        width: 100%;
        height: 100%;
        border: 1px solid gray;
}
#sPath { cursor: pointer; }
</style>

<p>我们通过按像素遍历svg的path路径的长度,可以收集该路径上每一个像素点的XY坐标值,然后就可以在路径的点击事件中确定当前点击点在路径上离起点的距离。这个实现机制我们在曲线进度条播放器插件中已经投入使用,该插件没有开放自定义路径,是为了确保路径的每一个像素点的X坐标是不同的。但像下面的路径:</p>
<svg width="300" height="120">
        <path d="M0 100 H80 Q-100 -60 220 100 H300" fill="none" stroke="lightgreen" stroke-width="4"></path>
        <line x1="50" y1="0" x2="50" y2="120" stroke="red" />
</svg>
<p>上面,红色直线与浅绿色路径存在三个交叉点,这三个点在路径上的X坐标一致、Y坐标不同,如果我们还是依据曲线进度条播放器插件所使用的方法去判断这三个点离路径起点的长度,显然行不通,我们需要额外的算法。</p>
<p>我们依然需要存储路径每一个像素的坐标点,不同于曲线进度条播放器插件,XY坐标值都存储到数组中去(设路径id为sPath):</p>
<pre class="code">
let len = sPath.getTotalLength();
let posAr = [];

for(j = 0; j < len; j ++) {
        posAr.push(sPath.getPointAtLength(j));
}
</pre>
<p>这样,当路径 sPath 被点击时,我们将被点击点的X坐标(ex)去和存储与数组中的路径上的X坐标(px)进行比对,二者值相等就将包括对应路径点的Y坐标等信息一同保存在一个临时数组中,遍历完成后对临时数组进行处理,找到被点击点的Y坐标(ey),将之和路径上存储的相应点的Y坐标(py)进行比对,取最接近ey或相等的py值,则 px 和 py 所指向的 len 值,就是路径被点击点的点击长度。</p>

<pre class="code">
sPath.onclick = (e) => {
        let yAr = []; <span class="zs">//临时数组</span>
        let ex = Math.round(e.offsetX), ey = Math.round(e.offsetY); <span class="zs">//被点击点XY坐标变量</span>
        for(j = 0; j < len; j ++) {
                let px = Math.round(posAr.x), <span class="zs">/* 路径像素点数组中j像素长度的X坐标变量 */</span>
                        py = Math.round(posAr.y); <span class="zs">/* 路径像素点数组中j像素长度的Y坐标变量 */</span>
                if(px === ex) { <span class="zs">//如果二者相等</span>
                        let ar = ; <span class="zs">//构建一个数组元素,存储 py - ey 的绝对值和该路径点的像素长度j</span>
                        yAr.push(ar); <span class="zs">//存入临时数组</span>
                }
        }
        <span class="zs">//将临时数组交给自定义函数处理,得到被点击点的长度并计算占整个路径的百分比</span>
        let prg = getMinItem(yAr) / len * 100 + '%';
        cap.style.setProperty('--prg', prg); <span class="zs">//显示进度</span>
};
</pre>
<p>自定义函数 getMinItem 则是这样:</p>
<pre class="code">
let getMinItem = (ar) => {
        ar.sort((a,b) => a - b);
        return ar;
};
</pre>
<p>函数很简单,就是将传递过来的二维数组按数组元素的第一个数组元素进行升序排序,这样最小的排在前面,则它的第二个数组元素的值就是我们需要的路径被点击点离路径起点的长度。试看下面的二维数组样貌,它们是排序后的结果,每一个数组元素的第一个数组元素是 py - ey 的绝对值:</p>
<pre class="code">
[
        ,
        ,
        ,
       
]
</pre>
<p>py-ey 的绝对值是我们判断被点击点是哪一点的依据,py是路径存储的 px=ex 那些点的Y坐标,px-py 值最小的那个就是我们点击的路径上的那个点。</p>
<p>效果如下:</p>

<div id="mydiv">
        <svg id="mysvg">
                <path id="sPath" d="M0 100 H80 Q-100 -60 220 100 H300" fill="none" stroke="lightgreen" stroke-width="4"></path>
                <line x1="50" y1="0" x2="50" y2="120" stroke="red" style="pointer-events: none" />
        </svg>
        <span id="cap"></span>
</div>

<script>

let len = sPath.getTotalLength();
let posAr = [];

let getMinItem = (ar) => {
        ar.sort((a,b) => a - b);
        return ar;
};

for(i = 0; i < len; i ++) {
        posAr.push(sPath.getPointAtLength(i));
}

sPath.onclick = (e) => {
        let yAr = [], xx = 0;
        let ex = Math.round(e.offsetX), ey = Math.round(e.offsetY);
        for(i = 0; i < len; i ++) {
                let px = Math.round(posAr.x),
                        py = Math.round(posAr.y);
                if(px === ex) {
                        let ar = ;
                        yAr.push(ar);
                }
        }
        let prg = getMinItem(yAr) / len * 100 + '%';
        cap.style.setProperty('--prg', prg)
};

</script>

马黑黑 发表于 2023-9-23 08:22

本帖最后由 马黑黑 于 2023-9-23 08:39 编辑

一楼示例完整代码
<style>
#mydiv {
      margin: 40px auto;
      width: 300px;
      height: 160px;
      position: relative;
}
#cap {
      position: absolute;
      width: 15px;
      height: 15px;
      background: green;
      pointer-events: none;
      offset-path: path('M0 100 H80 Q-100 -60 220 100 H300');
      offset-distance: var(--prg);
      --prg: 0%;
}
#mysvg {
      position: absolute;
      width: 100%;
      height: 100%;
      border: 1px solid gray;
}
#sPath { cursor: pointer; }
</style>

<div id="mydiv">
      <svg id="mysvg">
                <path id="sPath" d="M0 100 H80 Q-100 -60 220 100 H300" fill="none" stroke="lightgreen" stroke-width="4"></path>
                <line x1="50" y1="0" x2="50" y2="120" stroke="red" style="pointer-events: none" />
      </svg>
      <span id="cap"></span>
</div>

<script>

let len = sPath.getTotalLength();
let posAr = [];

let getMinItem = (ar) => {
      ar.sort((a,b) => a - b);
      return ar;
};

for(i = 0; i < len; i ++) {
      posAr.push(sPath.getPointAtLength(i));
}

sPath.onclick = (e) => {
      let yAr = [];
      let ex = Math.round(e.offsetX), ey = Math.round(e.offsetY);
      for(i = 0; i < len; i ++) {
                let px = Math.round(posAr.x),
                        py = Math.round(posAr.y);
                if(px === ex) {
                        let ar = ;
                        yAr.push(ar);
                }
      }
      let prg = getMinItem(yAr) / len * 100 + '%';
      cap.style.setProperty('--prg', prg)
};

</script>

马黑黑 发表于 2023-9-23 08:35

本帖解决方案不是完美的,有小瑕疵,但总体效果应该是可以接受了。只要路径自身没有交叉点、没有重合,本方案可以姑且一用,毕竟它绝对相当经济。

出现小瑕疵的地方一般是很突兀的转折处的最尖角处或类似的地方,这大约与路径的 stroke-width 有关,因为 getPointsAtLength() API 获得的XY坐标记录在现实中是基于 1px 的,而实际路径大于 1px ,这样点击才无压力,这就带来了问题:Y坐标有好几个会与点击的Y坐标值对应,这里面可能存在少量的精准度问题。

小辣椒 发表于 2023-9-23 08:59

这个有点深度了,小辣椒感觉一下子看不懂

马黑黑 发表于 2023-9-23 11:04

小辣椒 发表于 2023-9-23 08:59
这个有点深度了,小辣椒感觉一下子看不懂

这个是很难,不过没关系,将来插件的使用者要做的是制作 path 路径,我会有相应教程的

醉美水芙蓉 发表于 2023-9-23 11:55

小辣椒 发表于 2023-9-23 12:43

马黑黑 发表于 2023-9-23 11:04
这个是很难,不过没关系,将来插件的使用者要做的是制作 path 路径,我会有相应教程的

那好,我就不多看了,专门找容易一点的去完成{:4_170:}

马黑黑 发表于 2023-9-23 13:28

小辣椒 发表于 2023-9-23 12:43
那好,我就不多看了,专门找容易一点的去完成

这个,了解一下原理就好

马黑黑 发表于 2023-9-23 13:31

醉美水芙蓉 发表于 2023-9-23 11:55
看不懂,跟着老师玩玩了!

看不懂没关系的,知道个大概就好

红影 发表于 2023-9-26 10:36

只存储 px=ex 那些点的Y坐标,并做判断,这法子很聪明。非常简洁{:4_199:}

红影 发表于 2023-9-26 10:41

进度条的特点之一就是需要知道行进的长度,对非标准图形的任意点到起点的距离,真的会涉及到很多判断和计算方法的知识。黑黑很厉害,把这一过程考虑得很详细,除了路径,连线宽问题也考虑到。这个通常没人想到呢{:4_199:}

马黑黑 发表于 2023-9-26 13:10

红影 发表于 2023-9-26 10:41
进度条的特点之一就是需要知道行进的长度,对非标准图形的任意点到起点的距离,真的会涉及到很多判断和计算 ...

线宽会影响到坐标,大于1px的线宽,线性坐标点要去对应它就需要考虑到

马黑黑 发表于 2023-9-26 13:11

红影 发表于 2023-9-26 10:36
只存储 px=ex 那些点的Y坐标,并做判断,这法子很聪明。非常简洁

不过在复杂的路径,它可能会小概率出错或不准确,所以在自由路径插件中,我采用另外的算法。这里只是最初的设想。

红影 发表于 2023-9-26 22:25

马黑黑 发表于 2023-9-26 13:10
线宽会影响到坐标,大于1px的线宽,线性坐标点要去对应它就需要考虑到

这个从来没注意过呢。

红影 发表于 2023-9-26 22:26

马黑黑 发表于 2023-9-26 13:11
不过在复杂的路径,它可能会小概率出错或不准确,所以在自由路径插件中,我采用另外的算法。这里只是最初 ...

哦哦,后面的还有改进啊。

马黑黑 发表于 2023-9-26 22:36

红影 发表于 2023-9-26 22:26
哦哦,后面的还有改进啊。

测试时会碰上问题,就得解决

马黑黑 发表于 2023-9-26 22:36

红影 发表于 2023-9-26 22:25
这个从来没注意过呢。

理想状态下就是一个像素点,那样就好办了,但是,一个像素点的厚度,是不好点击到的

红影 发表于 2023-9-27 12:45

马黑黑 发表于 2023-9-26 22:36
测试时会碰上问题,就得解决

还好黑黑有本事解决{:4_187:}

红影 发表于 2023-9-27 12:46

马黑黑 发表于 2023-9-26 22:36
理想状态下就是一个像素点,那样就好办了,但是,一个像素点的厚度,是不好点击到的

是的,点击那么小的标志跟打游戏似的,太难了{:4_173:}

马黑黑 发表于 2023-9-27 12:51

红影 发表于 2023-9-27 12:46
是的,点击那么小的标志跟打游戏似的,太难了

但是太厚的路径也影响美观,以及性能开销
页: [1] 2 3
查看完整版本: 判断不规则svg路径被点击点的路径节点长度