前言
此次的 Demo 效果如下:

Demo 鏈接:https://hightopo.com/demo/comp-knob/
整體思路
- 組件參數
- 繪制旋鈕
- 繪制刻度
- 繪制指針
- 繪制標尺
- 繪制文本
- 交互效果
1.組件參數

以下是下文會使用到的部分變量,在此先貼出來
var origin, // 原點 percent, // 顯示刻度占總刻度的百分比 partAngle, // 每個刻度所占的角度 startAngle, //刻度起始的角度 calibrationPoints, // 每個刻度的信息 pointer, // 指針的信息 scaleLine, // 標尺的信息 calibrationColors // 刻度漸變色
2.繪制旋鈕
這里主要就使用了 canvas api 中的 arc() 和 createRadialGradient() 。
主要代碼:
g.beginPath(); var ringRadial = g.createRadialGradient(origin.x, origin.y, 0, origin.x, origin.y, ringRadio); ringRadial.addColorStop(0, ht.Default.brighter(ringColor, 20)); ringRadial.addColorStop(0.95, ht.Default.brighter(ringColor, 40)); ringRadial.addColorStop(1, ht.Default.darker(ringColor, 20)); var borderRadial = g.createRadialGradient(origin.x, origin.y, ringRadio - ringBorderWidth / 2, origin.x, origin.y, ringRadio + ringBorderWidth / 2); borderRadial.addColorStop(0, ht.Default.brighter(ringBorderColor, 2)); borderRadial.addColorStop(0.5, ht.Default.brighter(ringBorderColor, 4)); borderRadial.addColorStop(1, ht.Default.darker(ringBorderColor, 4)); g.fillStyle = ringRadial; g.lineWidth = ringBorderWidth; g.strokeStyle = borderRadial; g.arc(origin.x, origin.y, ringRadio, 0, 2 * Math.PI); g.closePath(); g.fill(); g.stroke();
效果圖:

3.繪制刻度
這里繪制每個刻度采用的是繪制路徑的方法,所以聲明了一個變量 calibrationPoints 用來存放每個刻度的起始點坐標,根據配置的參數去計算 calibrationPoints 的信息。
首先根據參數 calibrationPercent 計算第一個刻度的起始角度 startAngle ,然后根絕 calibrationCount 的值去計算每個刻度所占用的角度 partAngle ,最后根據三角函數和相應的角度,轉化為對應的坐標。
主要代碼:
var calibrationPoints = []; for (var i = 0; i < calibrationCount + 1; i++) { var point = { startx: origin.x + (ringRadio + ringBorderWidth + 0 * calibrationHeight) * Math.cos(startAngle - i * partAngle), starty: origin.y - (ringRadio + ringBorderWidth + 0 * calibrationHeight) * Math.sin(startAngle - i * partAngle), endx: origin.x + (ringRadio + ringBorderWidth + 1 * calibrationHeight) * Math.cos(startAngle - i * partAngle), endy: origin.y - (ringRadio + ringBorderWidth + 1 * calibrationHeight) * Math.sin(startAngle - i * partAngle) }; if (i <= (calibrationCount * percent) && percent > 0) { point.show = true; } else { point.show = false; } calibrationPoints.push(point); }
有了每個刻度的信息后,接下來就開始繪制刻度。
主要代碼:
calibrationPoints.forEach(function (i, index) { g.beginPath(); if (calibrationColorWheelShow) { calibrationBrightColor = calibrationColors[index]; } g.lineWidth = 1.2 * calibrationWidth; g.strokeStyle = i.show ? calibrationBrightColor : ht.Default.brighter(calibrationDarkColor, 10); g.moveTo(i.startx, i.starty); g.lineTo(i.endx, i.endy); g.closePath(); g.stroke(); }) calibrationPoints.forEach(function (i, index) { g.beginPath(); if (calibrationColorWheelShow) { calibrationBrightColor = calibrationColors[index]; } g.lineWidth = calibrationWidth; g.strokeStyle = i.show ? calibrationBrightColor : calibrationDarkColor; g.moveTo(i.startx, i.starty); g.lineTo(i.endx, i.endy); g.closePath(); g.stroke(); })
效果圖:

考慮到一種高亮顏色太單調,於是加了個色輪。思路:給每個刻度都添加了顏色的標識。
每個刻度的顏色計算方法:把顏色值轉換成 rgb 方式,設定多少秒改變完成,每次改變多少值,計算需要多少次,比如 rba(x,y,z) 到 rgb(a,b,c),假設需要 100 次,那么每次設定 rgb((a - x) / 100 + x, (b - y) / 100 + y, (c - z) / 100 + z) 。
主要代碼:
if (calibrationColorWheelShow) { // 顯示刻度色輪 var colors = []; calibrationColorWheel.forEach(function (i) { colors.push(ht.Default.toColorData(i)) }) // 把顏色值轉換成rgb方式,設定多少秒改變完成,每次改變多少值,計算需要多少次 // ,比如rba(x,y,z)到rgb(a,b,c),假設需要100次,那么每次設定 // rgb((a-x)/100+x,(b-y)/100+y,(c-z)/100+z) var count = Math.ceil(calibrationCount / (calibrationColorWheel.length - 1)); // 漸變次數 calibrationColors = []; for (var i = 0; i < colors.length - 1; i++) { for (var j = 1; j <= count; j++) { var item = 'rgb(' + Math.round((colors[i + 1][0] - colors[i][0]) / j + colors[i][0]) + ',' + Math.round((colors[i + 1][1] - colors[i][1]) / j + colors[i][1]) + ',' + Math.round((colors[i + 1][2] - colors[i][2]) / j + colors[i][2]) + ')'; calibrationColors.push(item) } } }
效果圖:

4.繪制指針
這個主要是根據三角函數去計算相對圓心的偏移角度,按照當前值和刻度最大值的比例來計算偏移量,然后換算成對應的坐標。
主要代碼:
pointer = { x: origin.x + (ringRadio - pointerRadio - ringBorderWidth) * Math.cos(startAngle - Math.PI * 2 * calibrationPercent * percent), y: origin.y - (ringRadio - pointerRadio - ringBorderWidth) * Math.sin(startAngle - Math.PI * 2 * calibrationPercent * percent), r: pointerRadio, color: percent > 0 ? calibrationBrightColor : calibrationDarkColor, show: true, } if (pointerShow) { g.beginPath(); g.fillStyle = pointer.color; g.arc(pointer.x, pointer.y, pointer.r, 0, Math.PI * 2); g.closePath(); g.fill(); }
效果圖:

5.繪制標尺
計算標尺角度的算法同指針。
主要代碼:
scaleLine = { startx: origin.x, starty: origin.y, endx: origin.x + (ringRadio + ringBorderWidth + 2 * calibrationHeight) * Math.cos(startAngle - Math.PI * 2 * calibrationPercent * percent), endy: origin.y - (ringRadio + ringBorderWidth + 2 * calibrationHeight) * Math.sin(startAngle - Math.PI * 2 * calibrationPercent * percent), color: percent > 0 ? calibrationBrightColor : calibrationDarkColor, show: scaleLineShow, } if (scaleLine) { g.beginPath(); g.strokeStyle = 'red'; g.setLineDash([1, 2]); g.lineWidth = 0.5 * calibrationWidth; g.moveTo(scaleLine.startx, scaleLine.starty); g.lineTo(scaleLine.endx, scaleLine.endy); g.closePath(); g.stroke(); }
效果圖:

6.繪制文本
這里主要的就是確定文本所要繪制的位置,然后根據 ht.Default.getTextSize() 來獲取文本長度,方便設置文本居中。
主要代碼:
if (labelShow) { var text = ht.Default.getTextSize(font, value); g.fillStyle = labelColor; g.font = font; g.fillText(value.toFixed(2), labelDot.x, labelDot.y); }
效果圖:

到這就完成了基本的旋鈕組件,下面繼續做一些細節上的優化。
例如加一些陰影效果,顏色漸變,配色調整等。
主要代碼:
var backgroundRadial = g.createRadialGradient(x + 0.5 * width, y + 0.2 * height, 0, x + 0.5 * width, y + 0.2 * height, Math.sqrt(Math.pow(width / 2, 2) + Math.pow(height, 2))); backgroundRadial.addColorStop(0, 'rgba(220,220,220,1)'); backgroundRadial.addColorStop(1, backgroundColor); g.fillStyle = backgroundRadial; g.fillRect(x, y, width, height); g.beginPath(); var ringRadial = g.createRadialGradient(origin.x, origin.y - ringRadio / 2, 0, origin.x, origin.y - ringRadio / 2, 1.5 * ringRadio); ringRadial.addColorStop(0, ht.Default.brighter(ringColor, 40)); // ringRadial.addColorStop(0.25, ht.Default.brighter(ringColor, 40)); ringRadial.addColorStop(1, ht.Default.darker(ringColor, 20)); var borderRadial = g.createRadialGradient(origin.x, origin.y, ringRadio - ringBorderWidth / 2, origin.x, origin.y, ringRadio + ringBorderWidth / 2); borderRadial.addColorStop(0, ht.Default.brighter(ringBorderColor, 2)); borderRadial.addColorStop(0.5, ht.Default.brighter(ringBorderColor, 4)); borderRadial.addColorStop(1, ht.Default.darker(ringBorderColor, 4)); g.fillStyle = ringRadial; g.lineWidth = ringBorderWidth; g.strokeStyle = borderRadial; g.arc(origin.x, origin.y, ringRadio, 0, 2 * Math.PI); g.closePath(); g.fill(); g.shadowBlur = 20; g.shadowColor = shadowColor; g.shadowOffsetY = ringBorderWidth; g.stroke(); g.shadowBlur = 0; g.shadowOffsetY = 0;
效果圖:

7.交互效果
以上就是繪制好了一張靜態圖,最后就只要再加上一些交互效果就可以了。
這里我采用的是 HT for Web 的矢量來實現。可參考 → 戳這
監聽 onUp 和 onDraw 事件。
onUp:
當鼠標抬起時,獲取當前旋鈕顯示的值,然后四舍五入,取其最近的刻度校准,使用 ht.Default.startAnim() 添加動畫效果。
onDraw:
根據當前鼠標停留的位置,以旋鈕原點為參照點,根據三角函數來計算指針和起始刻度的夾角 A ,計算 A 占總刻度的百分比 p ,然后設置當前值為 max * p 。
最后使用 HT 實現:
var gv = new ht.graph.GraphView(); dm = gv.dm(); dm.a('pannable', true); dm.a('zoomable', true); dm.a('rectSelectable', true); ht.Default.setCompType('knob',func); //注冊組件 ht.Default.setImage('iconKnob', data); //注冊圖片 var node = new ht.Node(); node.setImage('iconKnob'); node.s('2d.movable', false); dm.add(node); gv.fitContent(); gv.addToDOM(); window.addEventListener( 'resize', function(e) { gv.invalidate(); gv.fitContent(); }, false );
