轉盤效果
本文章講解怎麼實現這樣一個螺旋轉盤動態效果。不停旋轉,箭頭指向的扇形會變成高亮,整個轉盤有個漸變效果,中間鏤空。
利用圖片填充顏色
首先準備如下三張圖
三張圖怎麼利用? 思路大概下面所標示的。第一張和第三種是蓋到第二張上的,第二張圖作為填充圓形的顏色。
因為用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 }
建立文字
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 }
旋轉重新整理
定時重新整理畫面,將角度每次變化: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。
上面的程式碼不是很全面,如果有需要可以私信找我要哈。