今天心血来潮,想做一个环状调节旋钮。然鹅没有现成可用的标签,所以自己用css和js打造一个。

第一个版本

第一个版本是这样的

这个版本利用了 css3 的 3d 样式。思路很简单粗暴,就是将原点放置在大圆的中心,然后大圆边缘做一个小圆,当我们拖动 range 时候,js 监听 oninput 事件,让小圆绕Z轴旋转相同的角度。

想法和实现都是简单的,但时有一个缺点——就是我们调节旋转角度时,只能通过下方的拖条,不能用鼠标直接拖动。
这样自然而然会产生一个需求:怎么做一个效果,让旋钮随着鼠标拖动?

问题分析

这个问题看似简单,实际却花了我很长时间。记事本是我代码辅助的神器。让我们分析这个过程:

首先,打开记事本,将卡住的问题用一句话描述:
怎么做一个旋钮拖动效果?

盯着这行字细想了想,我得出这样一个结论:

鼠标位置如何决定旋钮位置?我的方案是这样:

怎样找到离鼠标最近的圆上一个点呢?不难知道,答案就是连接圆心与鼠标坐标,求出这条线段在圆上的交点。

将思路转化为执行步骤:

然后发现第一版的调节旋钮完全就是多余的,不如说不用3d样式,就可以达成目的。

现在,问题转化为怎样写出getClosestPoint这个函数

问题解决

这显然是个数学问题。。。
已知:圆心(centerX,centerY),点坐标(x,y),半径r,求:圆外一点与圆心连线与圆交点坐标。

一个经典方法是:联立圆与直线方程,求解交点,然后将它的坐标用一条长表达式返回。这个方案够直接,但不够简单,可能需要写上很长代码。

真的有必要这么麻烦吗?让我们换个角度想一下这个问题,很多时候我们觉得一个数学类问题很难,只是因为它太抽象,没有条件/已知条件不够简明,现在我们给这个问题一些具体且足够简单的数字试试看:

假设这个圆是以原点为圆心,半径为1的一个圆(单位圆),假设圆心为O,鼠标点为A,交于点B,可将它们画出来:

可以发现,因为圆O是单位圆,所以OA连线在圆上的交点坐标,恰好就是向量OA的方向余弦。

已知一个点(x,y),其与原点所成向量的方向余弦为

                                             cosα = x / sqrt(x*x + y*y) 
                                             cosβ = y / sqrt(x*x + y*y)

所以所求的B坐标,就是(cosα,cosβ)

如果你对上面的解法一时难以理解,那我们换个更简单的解释好了。
以原点为圆心,画出在A点所在的大圆,那么点B是什么?——点B恰好就是,大圆固定圆心,按比例缩放后,A点所处的位置。如图

设小圆半径为r,大圆半径为R,那么点B的坐标(x2,y2)就是点A的r/R等比缩放:

                                                      x2 = x / R * r 
                                                      y2 = y  / R * r

R怎么算?R就是sqrt(x*x + y*y),所以这个式子跟上面是一样的,都是特殊情形的解,现在我们将视角拉回一般情形:

求任意一圆O与圆外一点A所连交点,只需先将坐标系平移到该圆中心,将平移后A点坐标按比缩放计算出B点。然后坐标系变换回去,变换后B点的坐标,就是所求交点。

将思路整理成伪代码

再整理成真代码

将代码抄到项目,测试一下定点是否准确

然后在这里发现要操作的对象是旋钮,而不是外部(因为我们平时转旋钮的操作对象可不是空气),所以交互应该是拖动圆内的鼠标而不是圆外,再调整一下代码

模型ok,再将其余的细节实现一下,可随鼠标拖动调节的旋钮就完成啦。

值得一提的是,在这过程中我发现了一个bug,就是点按鼠标快速拖动+松手的时候,元素下角会出现类似打开新页面的图标,然后左键松开事件不会触发,导致松手了,元素还一直跟随鼠标转。

原理不明,我只知道给点击事件阻止默认动作后它就恢复正常了,如下

最后附上效果和笔记,欢迎大家批评指正~

总结

项目虽小,但我写这篇文章的目的,不在技术,而在分享自己解决问题的过程。

总结一下,我今天解决问题的套路是:

  • 理不清思路时,在记事本描述清楚问题本质,一步一步写下思路,从伪代码过渡到真代码
  • 解决“一般问题”时,可先通过某种处理,将它转化为简明的“特殊情况”,解之,然后逆处理