實現一個螺旋轉盤

方帥發表於2021-05-07

轉盤效果

本文章講解怎麼實現這樣一個螺旋轉盤動態效果。不停旋轉,箭頭指向的扇形會變成高亮,整個轉盤有個漸變效果,中間鏤空。

利用圖片填充顏色

首先準備如下三張圖

三張圖怎麼利用? 思路大概下面所標示的。第一張和第三種是蓋到第二張上的,第二張圖作為填充圓形的顏色。

 

   因為用canvas本身的shadow並不好實現這個效果,另外效能也不高。所以就直接利用了上面左側的這張圖。來呈現高亮選中的那個轉盤扇形。

初始化轉盤

1. 載入圖片並儲存填充顏色

為了可以繪製出扇形上顏色漸變的效果,保證整體上是一張過渡性很好的漸變色盤。需要載入這三張圖片,並繪製到一個離屏canvas上,藉助createPattern,用canvas作為重複內容填充繪製路徑,並儲存下來用於後面繪製扇形填充使用。

 1     initWheelImage() {
 2         var intoCanvas = document.createElement("canvas");//建立canvas
 3         var ctx = intoCanvas.getContext('2d');
 4         intoCanvas.width = this.width + this.position.x;//設定到正確的寬高
 5         intoCanvas.height = this.height + this.position.y;
 6 
 7         //將三張圖片載入
 8         let wheelSurfColor = new Image();
 9         wheelSurfColor.src = wheelSurfColorUrl;
10         let wheelSurfCoverImg = new Image();
11         wheelSurfCoverImg.src = wheelSurfCoverUrl;
12         let innerGlowSector = new Image();
13         innerGlowSector.src = innerGlowSectorUrl;
14         //使用promise將圖片載入成功後執行繪製操作
15         let that = this;
16         let arrPromise = [];
17         arrPromise.push(Util.getImgLoadPromise(wheelSurfColor));
18         arrPromise.push(Util.getImgLoadPromise(innerGlowSector));
19         arrPromise.push(Util.getImgLoadPromise(wheelSurfCoverImg));
20         Promise.all(arrPromise).then(
21             function (imgArr) {
22                 let r = that.innerRadius + that.maxOuterR;
23                 //將圖片繪製到離屏canvas上
24                 ctx.drawImage(wheelSurfColor, that.position.x + that.width / 2 - r, that.position.y + that.height / 2 - r, r * 2, r * 2);
25                 //指定重複元素用來填充路徑
26                 //或者  ctx.createPattern(intoCanvas, 'no-repeat')  使用哪個canvas都行
27                 that.wheelSurfColorPattern = that.layer.canvasContext.createPattern(intoCanvas, 'no-repeat');
28                 ctx.clearRect(0, 0, intoCanvas.width, intoCanvas.height)
29 
30                 //同上面幾句程式碼作用一致
31                 ctx.drawImage(wheelSurfCoverImg, that.position.x + that.width / 2 - r, that.position.y + that.height / 2 - r, r * 2, r * 2);
32                 that.wheelSurfCoverPattern = that.layer.canvasContext.createPattern(intoCanvas, 'no-repeat');
33                 that.initWheel.call(that, innerGlowSector);
34                 that.update.call(that)
35             }
36         );
37     }

圖片載入完成後,呼叫初始化轉盤其他元素的方法。 

2. 初始化轉盤其他展示內容

 展示內容包括:31個扇形塊,31個文字,頂部扇形,中間圓環,中間文字,箭頭,刻度線等。這裡用group劃分組利於管理控制。

繪製文字有效能問題,所以用到了離屏canvas。

  1     initWheel(innerGlowSector: HTMLImageElement) {
  2         let circlePerDis = 73 * this.scale;
  3         let font18 = 18 * this.scale;
  4         let font20 = 20 * this.scale;
  5         //繪製30個扇形塊
  6         let i;
  7         let renderStyle = new RenderStyle(true, false, this.wheelSurfColorPattern);
  8         //30個扇形組成group 並且設定顏色漸變的效果 需要初始化31個這樣轉的時候才不會有空缺
  9         this.sectorGroup = new Group(renderStyle, new RotateConfig());
 10         for (i = 1; i <= 36; i++) {
 11             if (i >= 32) {//不需要做什麼了
 12 
 13             } else {//加31個扇形
 14                 this.addNewSector(i);
 15             }
 16         }
 17 
 18         this.sectorGroup.setRotateCenter(this.position.x + this.width / 2, this.position.y + this.height / 2);
 19         this.sectorGroup.rotateBy(-60);
 20         this.group.add(this.sectorGroup);
 21 
 22         //繪製扇形上面的文字
 23         this.textGroup = new Group(new RenderStyle(), new RotateConfig());
 24         for (i = 1; i <= 36; i++) {
 25             if (i >= 32) {
 26 
 27             } else {
 28                 this.addNewText(i, i >= 17 ? "left" : "right");
 29             }
 30         }
 31         this.textGroup.setRotateCenter(this.position.x + this.width / 2, this.position.y + this.height / 2);
 32         //優化text繪製方式 提高效能 begin
 33         this.redrawTextCanvas();
 34         this.textCanvasImage = new CanvasImage(this.textCanvas, new RenderStyle(), new RotateConfig(), new RegionConfig());
 35         this.textCanvasImage.setRotateCenter(this.position.x + this.width / 2, this.position.y + this.height / 2);
 36         this.textCanvasImage.rotateBy(-60);
 37         this.group.add(this.textCanvasImage);
 38         //優化text繪製方式 提高效能 end
 39 
 40         //繪製頂部的扇形  鏤空效果
 41         let renderStyle2 = new RenderStyle(true, false, this.wheelSurfCoverPattern);
 42         renderStyle2.setGlobalCompositeOperation("destination-out");
 43         let sector2 = new Circle(new CircleConfig(this.innerRadius + this.maxOuterR + this.radiusDiff, new Point(this.position.x + this.width / 2, this.position.y + this.height / 2), 0, 360), renderStyle2, new RotateConfig());
 44         sector2.setRotateCenter(this.position.x + this.width / 2, this.position.y + this.height / 2);
 45         this.group.add(sector2);
 46 
 47         //繪製頂部半個扇形,藍顏色有加深部分
 48         let sectorColorDeeprenderStyle = new RenderStyle(true, false);
 49         let percentColorArr = new Array<PercentColor>(new PercentColor(0, "rgba(0,30,255,0)"), new PercentColor(0.5, "rgba(0,30,255,0.2)"), new PercentColor(1, "rgba(0,30,255,1)"));
 50         sectorColorDeeprenderStyle.setLinearGradient(new LinearGradientConfig(this.position.x + this.width / 2, this.position.y + this.height / 2, this.position.x + this.width / 2 + this.innerRadius + this.maxOuterR / 2, this.position.y + this.height / 2, percentColorArr));
 51         let sectorColorDeep = new Sector(new SectorConfig(this.innerRadius + circlePerDis * 4, new Point(this.position.x + this.width / 2, this.position.y + this.height / 2), 270, 315), sectorColorDeeprenderStyle);
 52         this.group.add(sectorColorDeep);
 53 
 54         //繪製中間的圓  鏤空效果
 55         let renderStyle1 = new RenderStyle(true, false, "green");//隨便什麼顏色
 56         renderStyle1.setGlobalCompositeOperation("destination-out");
 57         let circle = new Circle(new CircleConfig(this.innerRadius, new Point(this.position.x + this.width / 2, this.position.y + this.height / 2)), renderStyle1);
 58         this.group.add(circle);
 59 
 60         //環繞文字內部的圓 緊貼扇形
 61         let circleLineRenderStyle = new RenderStyle(false, true, "", "", 1, false);
 62         circleLineRenderStyle.setLinearGradient(new LinearGradientConfig(this.position.x, this.position.y, this.position.x + this.width, this.position.y, this.percentColorArr))
 63         let circleLine = new Circle(new CircleConfig(this.innerRadius, new Point(this.position.x + this.width / 2, this.position.y + this.height / 2), -30, 300, true), circleLineRenderStyle, new RotateConfig());
 64         circleLine.setRotateCenter(this.position.x + this.width / 2, this.position.y + this.height / 2);
 65         circleLine.rotateBy(-60);
 66         this.group.add(circleLine);
 67         //繪製環繞文字的兩個線條
 68         let circleLineTop = new Circle(new CircleConfig(this.innerRadius / 3 * 2, new Point(this.position.x + this.width / 2, this.position.y + this.height / 2), 220, 320), new RenderStyle(false, true, "", "rgba(38,110,225,1)", 1, false));
 69         this.group.add(circleLineTop);
 70         let circleLineBottom = new Circle(new CircleConfig(this.innerRadius / 3 * 2, new Point(this.position.x + this.width / 2, this.position.y + this.height / 2), 20, 160), new RenderStyle(false, true, "", "rgba(38,110,225,1)", 1, false));
 71         this.group.add(circleLineBottom);
 72 
 73         //繪製內部文字
 74         this.className = new Text(this.dataList.get(1).gradeName + "-" + this.dataList.get(1).subjectName, new Point(this.position.x + this.width / 2, this.position.y + this.height / 2 - 20), new TextConfig("Arial", font18, "normal", "normal"), new RenderStyle(true, true, "#00f6ff"), new RotateConfig());
 75         this.className.moveBy(-this.className.getWidth(this.layer.canvasContext) / 2, 0);
 76         this.group.add(this.className);
 77         this.articleName = new Text(Util.subString1(this.dataList.get(1).unitName, 28, "..."), new Point(this.position.x + this.width / 2, this.position.y + this.height / 2 + 20), new TextConfig("Arial", font20, "normal", "normal"), new RenderStyle(true, true, "#00f6ff"), new RotateConfig());
 78         this.articleName.moveBy(-this.articleName.getWidth(this.layer.canvasContext) / 2, 0);
 79         this.group.add(this.articleName);
 80 
 81 
 82         //依次繪製4個圓圈
 83         let circle1 = new Circle(new CircleConfig(this.innerRadius + circlePerDis, new Point(this.position.x + this.width / 2, this.position.y + this.height / 2)), new RenderStyle(false, true, "", "rgba(255,255,255,0.15)", 1, false));
 84         this.group.add(circle1);
 85         let circle2 = new Circle(new CircleConfig(this.innerRadius + circlePerDis * 2, new Point(this.position.x + this.width / 2, this.position.y + this.height / 2)), new RenderStyle(false, true, "", "rgba(255,255,255,0.15)", 1, false));
 86         this.group.add(circle2);
 87         let circle3 = new Circle(new CircleConfig(this.innerRadius + circlePerDis * 3, new Point(this.position.x + this.width / 2, this.position.y + this.height / 2)), new RenderStyle(false, true, "", "rgba(60,126,229,0.5)", 1, false));
 88         this.group.add(circle3);
 89         //最外層的圓圈有內發光效果
 90         let circle4RenderStyle = new RenderStyle(true, true, "", "rgba(60,126,229,0.5)", 1);
 91         circle4RenderStyle.setRadialGradient(new RadialGradientConfig(this.position.x + this.width / 2, this.position.y + this.height / 2, this.innerRadius, this.position.x + this.width / 2, this.position.y + this.height / 2, this.innerRadius + 73 * 4,
 92             [new PercentColor(0, "rgba(0,97,242,0)"), new PercentColor(0.7, "rgba(0,97,242,0)"), new PercentColor(0.9, "rgba(3,44,253,0.1)"), new PercentColor(1, "rgba(0,97,242,0.1)")], true, false))
 93         let circle4 = new Circle(new CircleConfig(this.innerRadius + circlePerDis * 4, new Point(this.position.x + this.width / 2, this.position.y + this.height / 2)), circle4RenderStyle);
 94         this.group.add(circle4);
 95 
 96         //繪製縱向座標尺
 97         let lineGroup = new Group();
 98         let lineY = new Line(new Point(this.position.x + this.width / 2, this.position.y + this.height / 2 - this.innerRadius), new Point(this.position.x + this.width / 2, this.position.y + this.height / 2 - this.innerRadius - this.maxOuterR), new RenderStyle(false, true, "", "rgba(255,255,255,0.1)"))
 99         lineGroup.add(lineY);
100         for (let i = 0; i <= 12; i++) {
101             let lineX = new Line(new Point(this.position.x + this.width / 2 - 6, this.position.y + this.height / 2 - this.innerRadius - circlePerDis * 4 / 12 * i), new Point(this.position.x + this.width / 2, this.position.y + this.height / 2 - this.innerRadius - circlePerDis * 4 / 12 * i), new RenderStyle(false, true, "", "rgba(255,255,255,0.2)"))
102             lineGroup.add(lineX);
103         }
104 
105         //繪製內發光啟用扇形及上面的文字
106         let innerGlowSectorImg = new CanvasImage(innerGlowSector, new RenderStyle(), new RotateConfig(), new RegionConfig(this.position.x + this.width / 2, this.position.y + this.height / 2 - 61 * this.scale, this.innerRadius + this.minOuterR + this.radiusDiff * 30, innerGlowSector.height * this.scale));
107         this.innerGlowSectorGroup.add(innerGlowSectorImg);
108         let activeSectorText = new Text(this.textGroup.children[1].text, new Point(this.position.x + this.width / 2 + this.innerRadius + 60 * this.scale, this.position.y + this.height / 2 + 7 * this.scale), new TextConfig("Arial", font18, "normal", "normal"), new RenderStyle(true, true, "#FFF"), new RotateConfig());
109         this.innerGlowSectorGroup.add(activeSectorText);
110         this.innerGlowSectorGroup.setRotateCenter(this.position.x + this.width / 2, this.position.y + this.height / 2);
111         this.innerGlowSectorGroup.rotateBy(-45);
112         this.group.add(this.innerGlowSectorGroup);
113 
114         //繪製發光箭頭
115         let arrowRenderStyle = new RenderStyle(true, false, "#FFF", "#FFF", 1, true);
116         arrowRenderStyle.setShadowStyle(new ShadowStyle(0, 0, 20, "#FFF"))
117         let arrowPath = new Path(arrowRenderStyle);
118         let p0 = Util.computePointAfterRotate(this.position.x + this.width / 2 + this.innerRadius / 3 * 2, this.position.y + this.height / 2 - 4, this.position.x + this.width / 2, this.position.y + this.height / 2, -45);
119         let p1 = Util.computePointAfterRotate(this.position.x + this.width / 2 + this.innerRadius / 3 * 2, this.position.y + this.height / 2 + 4, this.position.x + this.width / 2, this.position.y + this.height / 2, -45);
120         let p2 = Util.computePointAfterRotate(this.position.x + this.width / 2 + this.innerRadius / 3 * 2 + 90, this.position.y + this.height / 2, this.position.x + this.width / 2, this.position.y + this.height / 2, -45);
121         arrowPath.addPoint(new Point(p0.x, p0.y));
122         arrowPath.addPoint(new Point(p1.x, p1.y));
123         arrowPath.addPoint(new Point(p2.x, p2.y));
124         this.group.add(arrowPath);
125 
126         //繪製左右兩部分關聯的折線
127         let connectLinePath = new Path(new RenderStyle(false, true, "", "#00c6ff", 1, false));
128         let point0 = Util.computePointAfterRotate(this.position.x + this.width / 2 + this.innerRadius + this.maxOuterR + 5 * this.scale, this.position.y + this.height / 2, this.position.x + this.width / 2, this.position.y + this.height / 2, -45);
129         let point1 = Util.computePointAfterRotate(this.position.x + this.width / 2 + this.innerRadius + this.maxOuterR + 50 * this.scale, this.position.y + this.height / 2, this.position.x + this.width / 2, this.position.y + this.height / 2, -45);
130         let point2 = new Point(this.width, point1.y);
131         connectLinePath.addPoint(new Point(point0.x, point0.y));
132         connectLinePath.addPoint(new Point(point1.x, point1.y));
133         connectLinePath.addPoint(new Point(point2.x, point2.y));
134         this.group.add(connectLinePath);
135         //繪製左右兩部分關聯的折線起點小矩形
136         let connectLineRect = new Rect(new RenderStyle(true, false, "#00c6ff", ""), new RotateConfig(), new RegionConfig(point0.x - 5, point0.y - 5, 10, 10));
137         this.group.add(connectLineRect);
138 
139         this.group.add(lineGroup)
140         this.layer.add(this.group)
141     };

建立扇形

實現一個螺旋轉盤
1     addNewSector(index: number) {
2         let sector = new Sector(new SectorConfig(this.maxOuterR + this.innerRadius - index * this.radiusDiff, new Point(this.position.x + this.width / 2, this.position.y + this.height / 2), (index - 1) * this.degreeDiff, index * this.degreeDiff - 0.1), new RenderStyle(true, false, "", "", 1, false));
3         this.sectorGroup.add(sector);
4     }
addNewSector

建立文字

實現一個螺旋轉盤
 1     addNewText(index: number, textAlign: string) {
 2         let font14 = 14 * this.scale;
 3         let text;
 4         let unitName = Util.subString1(this.dataList.get(this.textIndex).unitName, 28, "...");
 5         if (textAlign == "left") {
 6             text = new Text(unitName, new Point(this.position.x + this.width / 2 - this.innerRadius - this.textHeadDis, this.position.y + this.height / 2 + 5 * this.scale), new TextConfig("Arial", font14, "normal", "normal", "right"), new RenderStyle(true, true, "#FFF"), new RotateConfig());
 7             //獲知text的寬度後 將其移動到合適的位置
 8             let width = text.width;
 9             text.moveBy(width, 0);
10             text.setRotateCenter(this.position.x + this.width / 2, this.position.y + this.height / 2);
11             text.rotateBy((index - 18) * 10 - 5);
12         } else {
13             text = new Text(unitName, new Point(this.position.x + this.width / 2 + this.innerRadius + this.textHeadDis, this.position.y + this.height / 2 + 5 * this.scale), new TextConfig("Arial", font14, "normal", "normal", "left"), new RenderStyle(true, true, "#FFF"), new RotateConfig());
14             text.setRotateCenter(this.position.x + this.width / 2, this.position.y + this.height / 2);
15             text.rotateBy(index * 10 - 5);
16         }
17 
18         this.textGroup.add(text);
19         this.textIndex++;
20         this.textIndex = this.textIndex % this.dataList.length();
21     }
addNewText

旋轉重新整理

 定時重新整理畫面,將角度每次變化:degreeDiff = 0.05。因為有組group的概念,所以旋轉的時候整個組旋轉比單個元素旋轉要方便。效能也會提高,因為減少了context的API呼叫。

 1     update() {
 2         if (this.sectorGroup && this.textGroup) {
 3             let that = this;
 4             that.clearLayer();
 5             let degreeDiff = 0.05; 
 6             // 沒旋轉10度 需要新生成一個小扇形,最大扇形去除。所有扇形長度慢慢增加
 7             let allSectors = this.sectorGroup.getChildren();
 8             let allTexts = this.textGroup.getChildren();
 9             allSectors.forEach((sector: Sector, index: number) => {
10                 sector.sectorConfig.radius += (degreeDiff / that.degreeDiff) * that.radiusDiff;
11                 sector.changeStartDegree(sector.sectorConfig.startDegree - degreeDiff);
12                 sector.changeEndDegree(sector.sectorConfig.endDegree - degreeDiff);
13             });
14             //優化text繪製方式 提高效能 begin
15             this.textCanvasImage.rotateBy(-degreeDiff);
16             //優化text繪製方式 提高效能 end
17 
18             //選中高亮扇形不斷旋轉
19             this.innerGlowSectorGroup.rotateBy(-degreeDiff);
20 
21             this.updateDegree = Math.round((this.updateDegree + degreeDiff) * 1000) / 1000;//防止出現小數位太多的情況
22             if ((this.updateDegree + 5) % 10 == 0) {//選中高亮扇形需要切換到下一個位置
23                 this.currentIndex++;
24                 this.currentIndex = this.currentIndex % this.dataList.length();
25                 this.innerGlowSectorGroup.rotateBy(10);
26                 let unitName = Util.subString1(this.dataList.get(this.currentIndex).unitName, 28, "...")
27                 this.innerGlowSectorGroup.children[1].text = unitName;
28                 this.articleName.text = unitName;
29                 this.articleName.position.x = this.position.x + this.width / 2 - this.articleName.getWidth(this.layer.canvasContext) / 2;
30 
31                 this.className.text = this.dataList.get(this.currentIndex).gradeName + "-" + this.dataList.get(this.currentIndex).subjectName;
32                 this.className.position.x = this.position.x + this.width / 2 - this.className.getWidth(this.layer.canvasContext) / 2;
33                 this.nextCallBack && this.nextCallBack();
34             }
35             if (this.updateDegree % 10 == 0) {
36                 (<Sector>allSectors[0]).removeSelf();
37                 this.addNewSector(31);
38                 //優化text繪製方式 提高效能 begin
39                 allTexts.forEach((text: Text, index: number) => {
40                     text.rotateBy(-10);
41                 });
42                 this.textCanvasImage.rotateBy(10);
43                 //優化text繪製方式 提高效能 end
44 
45                 (<Text>allTexts[0]).removeSelf();
46                 this.addNewText(31, "left");
47 
48                 //修改最底部扇形條文字的方向
49                 (<Text>allTexts[15]).textConfig.align = "left";
50                 (<Text>allTexts[15]).moveBy(this.position.x + this.width / 2 + this.innerRadius + this.textHeadDis - (<Text>allTexts[15]).position.x, this.position.y + this.height / 2 - (<Text>allTexts[15]).position.y);
51                 (<Text>allTexts[15]).rotateBy((16 * 10 - 4) - (<Text>allTexts[15]).rotateConfig.rotateDegree);
52                 this.redrawTextCanvas();
53             }
54             this.group.draw(this.layer.canvasContext, null);
55         }
56     }

底層有個繪相簿,定義了很多基本形狀,像程式碼中sector扇形,group組,text文字類等。

底層庫github地址:https://github.com/fangsmile/Canvas-GraphLib。

上面的程式碼不是很全面,如果有需要可以私信找我要哈。

相關文章