上一次隨筆大概的講了下構建一個矢量繪圖渲染器的基本架構。下面我們來繼續深入的完善我們的渲染器。
本次隨筆目標:實現定點的放大縮小功能、漫游,先上Demo。(大家可以添加多個點和圓,在放大、縮小的時候兩者有什么不同?—— 點的大小貌似沒有變化,而圓的呢。。想想為什么)
1.渲染器類的實現
上一節我們已經實現了點的繪制,但是並沒有給大家介紹渲染器類。如果有關注的同學,請打開上一節最后一部分看看我們在圖層類中調用渲染器的地方。
this.renderer.drawGeometry(vector.geometry, style);
沒錯我們通過drawGeometry方法來繪制,這樣我們降低了渲染器類與圖層類的耦合。渲染器只關心圖形和樣式兩個參數,其他的信息對於渲染器來說是無用的。
1.首先看構造函數
//渲染器類的構造函數。 function Canvas (layer) { this.canvas = document.createElement("canvas"); this.context = this.canvas.getContext("2d"); //只有當lock為false的時候才會執行繪制。 this.lock = true; this.layer = layer; this.setSize(layer.size); this.geometrys = {}; layer.div.appendChild(this.canvas); }
我們在構造函數中創建了一個canvas元素,並添加為layer.div的子元素。
2.設置渲染器寬高的方法——setSize
//設置canvas元素的大小。 Canvas.prototype.setSize = function(size){ this.canvas.width = size.w; this.canvas.height = size.h; this.canvas.style.width = size.w + "px"; this.canvas.style.height = size.h + "px"; }
3.目前唯一對外的接口——drawGeometry
//這個方法用於收集所有需要繪制的矢量元素。 Canvas.prototype.drawGeometry = function(geometry, style){ this.geometrys[geometry.id] = [geometry, style]; //如果渲染器沒有被鎖定則可以進行重繪。 if(!this.lock){ this.redraw(); } }
在渲染器類中有一個geometrys的對象,里面存儲着所有需要被繪制幾何形狀和其對應的樣式。
每次我們判斷lock這個屬性,他代表了當前是否鎖定這個渲染器,如果不鎖定,就把剛剛存在geometrys對象里面的所有矢量圖形繪制一遍。(因為渲染器本身並不知道有多少個矢量元素需要繪制,所以我們要通過lock這個屬性來控制)。
4.全部重繪——redraw方法。
//每次繪制都是全部清除,全部重繪。 //todo加入快照后可以大大提高性能。 Canvas.prototype.redraw = function(){ this.context.clearRect(0, 0, this.layer.size.w, this.layer.size.h); var geometry; if(!this.lock){ for(var id in this.geometrys){ if(this.geometrys.hasOwnProperty(id)){ geometry = this.geometrys[id][0]; style = this.geometrys[id][1]; this.draw(geometry, style, geometry.id); } } } }
我們在redraw這個方法中執行了對geometrys的遍歷繪制,但是不要忘記首先需要調用clearRect做一次全部清除。
5.每一個矢量圖形的繪制——draw方法。
//每一個矢量元素的繪制,這里我們在以后會添加更多的矢量圖形。 Canvas.prototype.draw = function(geometry, style, id){ if(geometry instanceof Point){ this.drawPoint(geometry, style, id); } //{todo} 我們在這里判斷各種矢量要素的繪制。 }
draw方法里面首先需要判斷我們當前的這個矢量圖形到底是什么,在通過對應的方法進行繪制。
6.繪制點和設置樣式 —— drawPoint和setCanvasStyle方法。
//針對點的繪制方法。 Canvas.prototype.drawPoint = function(geometry, style, id){ var radius = style.pointRadius; var twoPi = Math.PI*2; var pt = this.getLocalXY(geometry); //填充 if(style.fill) { this.setCanvasStyle("fill", style) this.context.beginPath(); this.context.arc(pt.x, pt.y, radius, 0, twoPi, true); this.context.fill(); } //描邊 if(style.stroke) { this.setCanvasStyle("stroke", style) this.context.beginPath(); this.context.arc(pt.x, pt.y, radius, 0, twoPi, true); this.context.stroke(); } this.setCanvasStyle("reset"); } //設置canvas的樣式。 Canvas.prototype.setCanvasStyle = function(type, style) { if (type === "fill") { this.context.globalAlpha = style['fillOpacity']; this.context.fillStyle = style['fillColor']; } else if (type === "stroke") { this.context.globalAlpha = style['strokeOpacity']; this.context.strokeStyle = style['strokeColor']; this.context.lineWidth = style['strokeWidth']; } else { this.context.globalAlpha = 0; this.context.lineWidth = 1; } }
我們的點的位置信息是基於單位坐標的。比如說我們的canvas大小是400 * 400,則世界坐標系的原點位置(0,0)對應在屏幕上則應該是(200,200)。
所以一個定義在原點位置的點,則需要一個函數對我們的世界坐標系進行轉換,變成一個屏幕可顯示的坐標。這樣我們需要一個函數getLocalXY。
7.世界坐標與屏幕坐標的轉換——getLocalXY。
//獲得一個點的屏幕顯示位置。 Canvas.prototype.getLocalXY = function(point) { var resolution = this.layer.getRes(); var extent = this.layer.bounds; var x = (point.x / resolution + (-extent.left / resolution)); var y = ((extent.top / resolution) - point.y / resolution); return {x: x, y: y}; }
這里我們先不用管resolution這個函數到底是干什么的,在后面一點我會給大家解釋的。
這里我們只需要知道這樣就可以把我們在世界坐標系中定義的點轉換為屏幕坐標。
8.總結下渲染器
可能大家看到這里覺得很郁悶,簡簡單單的畫圓方法通過我們的渲染器一封裝咋就實現的這么復雜哩?
1.通過這樣的一個結構我們可以很好的擴展所支持的矢量圖形種類(雖然到目前為止只有點)。
2.我們結合幾何信息和樣式兩個方面來繪制一個矢量圖形。幾何信息只表示位置,大小等;樣式控制線寬,點大小,透明度,顏色等。這樣一來我們在設計的過程中可以更好的專注一方面的內容。
3.使用這樣的結構最最重要的是:我們需要實現矢量圖形,而不是普通的像素圖片。
2.關於zoom,resolution和當前視圖范圍的概念
1.zoom 和 resolution
之前我們在Layer這個類里面定義了一個方法,叫做getRes:
//這個res代表當前zoom下每像素代表的單位長度。 //比如當前縮放比率為 200% 則通過計算得到 res為0.5,說明當前zoom下每個像素只表示0.5個單位長度。 Layer.prototype.getRes = function() { this.res = 1 / (this.zoom / 100); return this.res; }
為了明白這個函數的意思,首先我們得明確一個幾個事情:
1.當我們為一個div創建圖層,假設div的widht是400px、height是400px,其默認的zoom為100%。我們其實是為這個div創建了一個世界坐標系,坐標系的原點(0,0)在div的中心也就是(200,200)這個位置,整個世界坐標系的范圍也就是:左下(-200,-200);右上(200,200)。
2.在我們的世界坐標系中的長度單位和實際的像素長度單位有一個對應關系,當zoom為100%時,一個像素單位對應這一個世界坐標系中的單位。通過getRes方法我們在對應的zoom下面可以計算出當前的resolution。假設當前的zoom為200%。則通過計算:
“1 / (200 / 100)” 得到0.5這個值,也就是說在當前的zoom下,1個像素單位只能表示0.5個世界坐標系中的單位,通過這一關系我們就實現了矢量縮放。
2.當前視圖范圍
通過layer.moveto我們來尋找當前范圍,並對當前范圍的矢量圖形進行顯示。
Layer.prototype.moveTo = function (zoom, center) { this.zoom = zoom; if(!center) { center = this.center; } var res = this.getRes(); var width = this.size.w * res; var height = this.size.h * res; //獲取新的視圖范圍。 var bounds = new CanvasSketch.Bounds(center.x - width/2, center.y - height/2, center.x + width/2, center.y + height/2); this.bounds = bounds; //記錄已經繪制vector的個數 var index = 0; this.renderer.lock = true; for(var id in this.vectors){ index++; if(index == this.vectorsCount) { this.renderer.lock = false; } this.drawVector(this.vectors[id]); } }
這段代碼當中,我們首先用width、height兩個局部變量表示了在當前的縮放級別下,整個div所能表示的世界坐標系中的長度和寬度。並通過中心點確定了當前的視圖范圍,循環所有的矢量圖形,在當前視圖范圍外的就一定不會被繪制。
下面這個Demo便是通過此理論制作的矢量圖形漫游。
3.創建新的圖形——圓
圓和點最大的區別在於圓有半徑而點沒有,圓用擁有一個世界坐標系下的長度半徑,所以圓的大小會根據zoom的不同而改變,圓繼承自點,所以代碼非常easy
function Circle(x, y, radius) { Point.apply(this, arguments); this.radius = radius; } Circle.prototype = new Point(); Circle.prototype.getBounds = function () { if(!this.bounds) { this.bounds = new CanvasSketch.Bounds(this.x - this.radius, this.y - this.radius, this.x + this.radius, this.y + this.radius); return this.bounds; } else { return this.bounds; } } Circle.prototype.geoType = "Circle";
有了圓我們就需要在渲染器類中加入渲染圓的方法:
//針對圓的繪制方法。 Canvas.prototype.drawCircle = function(geometry, style, id){ var radius = geometry.radius var twoPi = Math.PI*2; var pt = this.getLocalXY(geometry); //填充 if(style.fill) { this.setCanvasStyle("fill", style) this.context.beginPath(); this.context.arc(pt.x, pt.y, radius / this.layer.res, 0, twoPi, true); this.context.fill(); } //描邊 if(style.stroke) { this.setCanvasStyle("stroke", style) this.context.beginPath(); this.context.arc(pt.x, pt.y, radius / this.layer.res, 0, twoPi, true); this.context.stroke(); } this.setCanvasStyle("reset"); }
這樣一來我們的渲染器就新增了一個幾何形狀,慢慢的大家就會體會到這樣架構的優勢所在:每次增加新的幾何元素都非常的方便,擴展性強~
現在大家在看看本次隨筆一開始的demo,添加點和圓並進行縮放,點的屏幕顯示大小永遠不變,而圓會隨着zoom值得變換而發生改變。
嘗試一下:下載后面提供的本次隨筆的源碼,並修改兩個demo的,可能會發現很有趣~
下次隨筆預告:1.用世界坐標系控制縮放中心點實在困難,下次我們來解決用屏幕坐標來控制縮放。
2.增加鼠標事件,鼠標可以滾輪可以縮放,拖動可以平移。
3.增加更多的圖形支持。
本次隨筆的所有源碼+demo,請點擊下載。
謝謝關注!