fabric和konva主要是用於實現編輯器的場景,而Pixi則是一個高性能2D動畫渲染庫,一般用於一些H5的小遊戲或可交互頁面。git
本次經過如下幾個方面來對其進行分析:github
系列目錄canvas
Pixi是一個基於WebGL Renderer的高性能跨平臺渲染庫。其中默認使用WebGL相關插件(回退使用CanvasRenderer)去渲染2D圖形,而且在資源加載和動畫處理方面也有比較好的設計和優化。數組
本文所用的Pixi版本爲5.2.0。瀏覽器
在使用Pixi前,須要建立一個Application對象,做爲最外層的應用對象。app
Application是Pixi中統領全局的對象,其中包含了使用的渲染器(render)、舞臺(stage)、安裝的插件等主要屬性及操做器。框架
export class Application {
constructor(options)
{
// 處理配置
options = Object.assign({
forceCanvas: false,
}, options);
// 初始化渲染器
this.renderer = autoDetectRenderer(options);
// 初始化舞臺容器
this.stage = new Container();
// 安裝插件
Application._plugins.forEach((plugin) =>
{
plugin.init.call(this, options);
});
}
// ...
}
複製代碼
提供的方法也是從stage和renderer對象中取得的屬性或其餘操做,如view(), screen()等。編輯器
能夠看到在App的建立過程當中,會根據當前環境選擇可用的渲染器。函數
默認採用WebGLRenderer,若當前瀏覽器環境不支持WebGL則使用Canvas。根據渲染方式初始化對應的renderer工具
這兩種渲染器均實現自AbstractRenderer類,在這個類中保存了渲染器所的綁定的canvas元素、設置透明度與分辨率等屬性。
packages/core/src/Renderer
在WebGLRenderer的初始化過程當中,會在Renderer類上註冊不一樣類型的系統插件(均繼承自System類),如上下文插件(ContextSystem)、着色器插件(ShaderSystem)、紋理插件(TextureSystem)等等,而且在註冊系統插件時會插入表明不一樣階段的生命週期鉤子(runner: prerender | postrender | resize | update | contextChange),
來看看System這個類,其實很簡單,就是用一個於在renderer類上擴展相關屬性與方法的類。
export class System {
constructor(renderer) {
this.renderer = renderer;
}
destroy() {
this.renderer = null;
}
}
複製代碼
這些System插件主要有:
做爲一個renderer,最重要的方法便是它的render()方法,它的執行過程(省去了生命週期函數)以下:
render(displayObject, renderTexture, clear, transform, skipUpdateTransform) {
// 1. 應用變換(GPU級別)
this.projection.transform = transform;
// 2. 渲染紋理綁定與BatchRendering處理
this.renderTexture.bind(renderTexture);
this.batch.currentRenderer.start();
// 3. 執行元素渲染,將頂點、索引和紋理等數據添加到BatchRendering中
displayObject.render();
// 4. 執行renderer的繪製方法
this.batch.currentRenderer.flush();
// 根據傳入的clear與renderTexture參數對紋理的處理...
// 5. 清空變換
this.projection.transform = null;
}
複製代碼
有關渲染的工做主要由BatchSystem插件負責執行,BatchRenderer
packages/canvas/canvas-renderer/src/CanvasRenderer
較WebGLRenderer的實現比較簡單,在構建函數中並無加載其餘插件,僅初始化了一些屬性,如mask與blendMode等,
CanvasRenderer的render()執行流程以下:
render(displayObject, renderTexture, clear, transform, skipUpdateTransform) {
const context = this.context;
// 1. 當前狀態壓入狀態棧
context.save();
// 2. 初始化變換及樣式屬性
context.setTransform(1, 0, 0, 1, 0, 0);
context.globalAlpha = 1;
this._activeBlendMode = BLEND_MODES.NORMAL;
this._outerBlend = false;
context.globalCompositeOperation = this.blendModes[BLEND_MODES.NORMAL];
// 3.執行元素渲染
const tempContext = this.context;
this.context = context;
displayObject.renderCanvas(this);
this.context = tempContext;
// 4. 從狀態棧恢復以前狀態
context.restore();
}
複製代碼
Stage本質是一個Container對象,與Konva中的概念相似。
Pixi的Container是一種DisplayObject容器,負責children的管理、變換的應用及包圍盒(bounds)計算。Container中能夠包含精靈(Sprite)或圖形(Graphic)對象,實現分組的效果,須要注意的是在Container應用的變換會做用到全部子元素上。
DisplayObject是顯示的基礎元素,其中包含元素的變換矩陣、alpha係數和層級係數等屬性及相關數據操做的方法,每一個繼承它的類的對象要想渲染出來必須實現它的_render方法。
Pixi中的精靈(Sprite)爲一種可交互的紋理對象,繼承自Container類,所以也能夠嵌套其餘DisplayObject對象,造成圖形樹。
Sprite類中包含用於頂點計算和目標檢測等方法,用於爲渲染提供關鍵數據及爲交互事件的處理提供輔助方法等。
vertex的計算
calculateVertices() {
const texture = this._texture;
// 1. 解析變換矩陣
const wt = this.transform.worldTransform;
const tx = wt.tx;
// ...
// 2. 計算當前區域
const vertexData = this.vertexData;
const anchor = this._anchor;
let w1 = -anchor._x * orig.width;
let w0 = w1 + orig.width;
let h1 = -anchor._y * orig.height;
let h0 = h1 + orig.height;
// 3. 計算經過世界變換後的四個頂點座標
vertexData[0] = (a * w1) + (c * h1) + tx;
vertexData[1] = (d * h1) + (b * w1) + ty;
// ...
}
複製代碼
判斷點是否在該精靈的區域中
containsPoint(point) {
// 1. 在世界空間上應用逆變換獲得模型空間座標
this.worldTransform.applyInverse(point, tempPoint);
// 2. 經過紋理與錨點計算精靈幾何屬性
const width = this._texture.orig.width;
const height = this._texture.orig.height;
const x1 = -width * this.anchor.x;
let y1 = 0;
// 3. 判斷是否位於對象區域
if (tempPoint.x >= x1 && tempPoint.x < x1 + width) {
y1 = -height * this.anchor.y;
if (tempPoint.y >= y1 && tempPoint.y < y1 + height) {
return true;
}
}
return false;
}
複製代碼
在Sprite類中默認使用BatchRenderer對精靈進行渲染,BatchRenderer爲WebGLRenderer中的一個插件,用於記錄相關數據,統一執行繪製(flush)。
// 經過修改該pluginName屬性設置負責渲染該精靈的插件
this.pluginName = 'batch';
_render(renderer) {
this.calculateVertices();
renderer.batch.setObjectRenderer(renderer.plugins[this.pluginName]);
renderer.plugins[this.pluginName].render(this);
}
複製代碼
在場景中除了加載紋理圖像生成的精靈外,還能夠經過常規或自定義的幾何圖形來添加圖形對象,
Graphic中提供相似CanvasContext上的繪圖API,好比drawRect、drawCircle等,將這些基礎圖形的數據通過處理後(如三角化),再使用WebGL的API進行繪製。Graphic一樣繼承自Container類。
// packages/graphics/src/Graphics.js
drawRect(x, y, width, height) {
return this.drawShape(new Rectangle(x, y, width, height));
}
複製代碼
對於每種圖形,除了保存關鍵屬性外,還實現一些輔助方法,如點與圖形的碰撞檢測函數等:
// packages/math/src/shapes/Rectangle.ts
contains(x: number, y: number): boolean {
if (this.width <= 0 || this.height <= 0) { return false; }
if (x >= this.x && x < this.x + this.width) {
if (y >= this.y && y < this.y + this.height) { return true; }
}
return false;
}
複製代碼
Pixi對於曲線圖形並無提供碰撞檢測的方法,若須要實現吸附點操做之類的功能只能自定義一些hitDetect的方法,或在外面使用isPointInStroke這類API。
在Graphics對象的geometry屬性中存儲緩衝區中使用的幾何數據,在drawShape時會將圖形數據及樣式屬性打包成GraphicsData對象添加到當前的圖形數組中,用於以後的實際繪製。
// packages/graphics/src/GraphicsGeometry.js
drawShape(shape, fillStyle, lineStyle, matrix)
{
const data = new GraphicsData(shape, fillStyle, lineStyle, matrix);
this.graphicsData.push(data);
this.dirty++;
return this;
}
複製代碼
在繪製(更新batch指令、執行填充)時,會計算圖形的頂點位置並將三角化後的頂點數據及索引添加到Geometry對象的頂點數組中。
// packages/graphics/src/utils/buildRectangle
// 1. 頂點座標計算
build() {
points.push(x, y,
x + width, y,
x + width, y + height,
x, y + height);
}
// 2. 圖形三角化,插入頂點數據及三角形頂點索引,用於以後繪製
triangulate() {
const vertPos = verts.length / 2;
verts.push(points[0], points[1],
points[2], points[3],
points[6], points[7],
points[4], points[5]);
graphicsGeometry.indices.push(vertPos, vertPos + 1, vertPos + 2,
vertPos + 1, vertPos + 2, vertPos + 3);
}
複製代碼
Graphic在執行渲染時會經過圖形的batchable屬性來決定是使用BatchRender仍是DirectRender的方式:
_render(renderer) {
// 多邊形對象繪製(本質是PathDrawing)
this.finishPoly();
// 讀取geometry,生成batch數據
const geometry = this.geometry;
geometry.updateBatches();
// 執行渲染
if (geometry.batchable) {
// 判斷batch數據是否須要更新
if (this.batchDirty !== geometry.batchDirty) {
this._populateBatches();
}
// 執行BatchRender
this._renderBatched(renderer);
} else {
renderer.batch.flush();
// 執行DirectRender
this._renderDirect(renderer);
}
}
複製代碼
其中BatchRender與精靈中渲染的方式相似,均爲調用BatchSystem執行繪製,在以前須要一些頂點與索引計算等工做。DirectRender中也比較簡單,設置了渲染着色器,執行geometry中存儲的drawCalls渲染指令。
_renderDirect(renderer) {
// 設置uniform
uniforms.translationMatrix = this.transform.worldTransform;
uniforms.tint[0] = (((tint >> 16) & 0xFF) / 255) * worldAlpha;
uniforms.tint[1] = (((tint >> 8) & 0xFF) / 255) * worldAlpha;
uniforms.tint[2] = ((tint & 0xFF) / 255) * worldAlpha;
uniforms.tint[3] = worldAlpha;
// 設置着色器及狀態
renderer.shader.bind(shader);
renderer.geometry.bind(geometry, shader);
renderer.state.set(this.state);
// 解析存儲的繪製指令,執行渲染
for (let i = 0, l = drawCalls.length; i < l; i++) {
this._renderDrawCallDirect(renderer, geometry.drawCalls[i]);
}
}
複製代碼
Pixi的應用場景中多數都須要加載圖像或音頻資源,如其餘遊戲框架同樣,所以具備專門的Loader工具對資源進行處理。
Pixi中使用了resource-loader這個庫來在內部處理資源加載,將其封裝爲通用的資源加載類Loader及紋理加載類TextureLoader。
在TextureLoader中只作了一件事,在加載完成的回調中判斷若資源爲Image類型,則經過resource生成Texture對象並添加到texture屬性
export class TextureLoader {
static use(resource, next) {
if (resource.data && resource.type === Resource.TYPE.IMAGE) {
resource.texture = Texture.fromLoader(
resource.data,
resource.url,
resource.name
);
}
next();
}
}
複製代碼
接下來看看其中重要的表示所展現圖像的Texture對象是什麼。
紋理爲精靈對象提供渲染的圖像數據,支持多種圖像數據類型。
當經過以下方法建立精靈時:
const bunny = PIXI.Sprite.from('examples/assets/bunny.png');
複製代碼
在內部執行了:
// packages/sprite/src/Sprite
from(source, options) {
const texture = (source instanceof Texture)
? source
: Texture.from(source, options);
return new Sprite(texture);
}
// packages/core/src/textures/Texture
from(source, options = {}, strict = settings.STRICT_TEXTURE_CACHE) {
texture = new Texture(new BaseTexture(source, options));
texture.baseTexture.cacheId = cacheId;
BaseTexture.addToCache(texture.baseTexture, cacheId);
Texture.addToCache(texture, cacheId);
}
複製代碼
能夠看出在精靈的from中實際調用了Texture的from方法用來解析與生成紋理。
在BaseTexture中會根據傳入的source自動判斷該資源的類型(autoDetectResource),判斷是否爲SVG、Canvas、Buffer等資源類型,若通過test後該source的特徵均不知足這些類型,則做爲Image類型加載,關鍵部分以下:
autoDetectResource(source, options) {
for (let i = INSTALLED.length - 1; i >= 0; --i) {
const ResourcePlugin = INSTALLED[i];
if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) {
return new ResourcePlugin(source, options);
}
}
return new ImageResource(source, options);
}
複製代碼
ImageResource中會使用ImageElement對象來加載圖片。
外層的Texture類中則
說完基礎元素及資源處理,就到了與實際展現或操做有關的變換、交互及動畫部分了。
packages/interaction/Matrix & Transform
爲了高效,採用一維數組的格式保存變換矩陣,使用math庫中的Matrix和Transform的組合實現變換數據的相關操做。
Pixi並無爲精靈提供顯式調用的變換相關方法(rotate, translate, scale),僅能經過直接改變變換屬性來實現變換,這些變換屬性位於DisplayObject類中,即Container和Sprite的父類。
能夠看看這個例子,經過改變精靈的rotation屬性來控制旋轉
app.ticker.add((delta) => {
bunny.rotation += 0.1 * delta;
});
複製代碼
改變屬性後執行的流程
Sprite
set rotation(value) {
this.transform.rotation = value;
}
複製代碼
Transform
set rotation(value) {
if (this._rotation !== value)
{
this._rotation = value;
this.updateSkew();
}
}
protected updateSkew(): void {
// 計算變換矩陣中scale與skew參數
this._cx = Math.cos(this._rotation + this.skew.y);
this._sx = Math.sin(this._rotation + this.skew.y);
this._cy = -Math.sin(this._rotation - this.skew.x); // cos, added PI/2
this._sy = Math.cos(this._rotation - this.skew.x); // sin, added PI/2
}
複製代碼
packages/interaction/src/InteractionManager
默認狀況下,負責交互事件的InteractionManager(如下簡稱IManager)是做爲一個插件加載到renderer上。
Manager在初始化時在renderer的view屬性對應的元素上一股腦的綁定了相關事件的事件監聽函數:
var element = this.renderer.view;
this.interactionDOMElement = element;
// ...
if (this.supportsPointerEvents) {
window.document.addEventListener('pointermove', this.onPointerMove, true);
this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true);
this.interactionDOMElement.addEventListener('pointerleave', this.onPointerOut, true);
this.interactionDOMElement.addEventListener('pointerover', this.onPointerOver, true);
window.addEventListener('pointercancel', this.onPointerCancel, true);
window.addEventListener('pointerup', this.onPointerUp, true);
} else {
// ...
複製代碼
這裏相比較的話仍是Konva的綁定事件監聽的方式較爲科學,Konva考慮到了不一樣事件觸發的次序來對事件與監聽函數進行綁定,而不是單純在某一時間點統一的綁定與移除。
IManager在監聽交互事件時除了觸發相關事件外,還會在內部的DisplayObject上執行目標檢測與事件分發:
processInteractive(interactionEvent, displayObject, func, hitTest) {
// 目標檢測,並向內部的interactive DisplayObject分發事件
const hit = this.search.findHit(interactionEvent, displayObject, func, hitTest);
// 處理延遲事件,當多個mouse/pointer事件觸發時
const delayedEvents = this.delayedEvents;
if (!delayedEvents.length) { return hit; }
// 重置hint,爲了在tree中繼續搜索
interactionEvent.stopPropagationHint = false;
const delayedLen = delayedEvents.length;
this.delayedEvents = [];
// 向DisplayObjects分發事件
for (let i = 0; i < delayedLen; i++) {
const { displayObject, eventString, eventData } = delayedEvents[i];
// 當到達須要中止的地方設置
if (eventData.stopsPropagatingAt === displayObject) {
eventData.stopPropagationHint = true;
}
this.dispatchEvent(displayObject, eventString, eventData);
}
return hit;
}
複製代碼
其中findHit爲TreeSearch的對象方法,用於執行實際的目標檢測與事件分發行爲。
packages/interaction/src/TreeSearch
TreeSearch使用recursiveFindHit
這個遞歸函數來在DisplayObject上執行目標檢測
findHit(interactionEvent, displayObject, func, hitTest) {
this.recursiveFindHit(interactionEvent, displayObject, func, hitTest, false);
}
// ...
recursiveFindHit(interactionEvent, displayObject, func, hitTest, interactive) {
// 1. hitArea與mask判斷
if (displayObject.hitArea) {
// 若存在hitArea,經過contains判斷該點是否在模型空間的目標區域內
if (hitTest) {
displayObject.worldTransform.applyInverse(point, this._tempPoint);
if (!displayObject.hitArea.contains(this._tempPoint.x, this._tempPoint.y)) {
hitTest = false;
hitTestChildren = false;
} else {
hit = true;
}
}
interactiveParent = false;
// 若存在
} else if (displayObject._mask) {
// 若存在mask,經過contains判斷該點是否在mask區域內
if (hitTest) {
if (!(displayObject._mask.containsPoint && displayObject._mask.containsPoint(point))) {
hitTest = false;
}
}
}
// 2. 執行遞歸函數檢測子元素的碰撞狀況
if (hitTestChildren && displayObject.interactiveChildren && displayObject.children) {
const children = displayObject.children;
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];
// 遞歸調用,若爲true說明檢測到碰撞對象
const childHit = this.recursiveFindHit(interactionEvent, child, func, hitTest, interactiveParent);
if (childHit)
{
// 若當前子元素的父輩被移除,則跳過檢測
if (!child.parent) { continue; }
interactiveParent = false;
// PS: 這裏的if(childHit)檢測是多餘的?
if (childHit) {
if (interactionEvent.target) {
hitTest = false;
}
hit = true;
}
}
}
}
// 3. 執行目標檢測
if (interactive) {
if (hitTest && !interactionEvent.target) {
// 以前檢測過hitArea,這裏再也不處理
if (!displayObject.hitArea && displayObject.containsPoint) {
if (displayObject.containsPoint(point))
{
hit = true;
}
}
}
// 若該元素interactive爲true,則設置爲當前事件的target,並執行傳入的回調函數
if (displayObject.interactive) {
if (hit && !interactionEvent.target) {
interactionEvent.target = displayObject;
}
if (func) {
func(interactionEvent, displayObject, !!hit);
}
}
}
return hit;
}
複製代碼
packages/ticker
動畫是Pixi中比較重要的一個模塊,它將rAF動畫封裝成了一個Ticker類,主要有以下三個特性:
一般咱們執行rAF動畫時都是簡單的遞歸調用,以下:
function render() {
work();
requestAnimationFrame(render);
}
複製代碼
使用Ticker操做幀動畫的執行函數:
let numA = 0;
let numB = 0;
const renderTaskInit = () => { initWork() }
const renderTaskA = () => { renderWork() }
const renderTaskB = () => { renderWork() }
app.ticker.addOnce() // 僅執行一次的任務
app.ticker.add(renderTaskA); // 循環執行的任務
app.ticker.add(renderTaskB, this); // 循環執行的任務,可傳入context對象
app.ticker.remove(renderTaskA) // 移除任務
複製代碼
Ticker的原理
內部實現主要由Ticker與TickerListener這兩個類組成。
1.動畫開始與中止的控制
start(): void {
if (!this.started) {
this.started = true;
this._requestIfNeeded();
}
}
private _requestIfNeeded(): void {
if (this._requestId === null && this._head.next) {
this.lastTime = performance.now();
this._lastFrame = this.lastTime;
this._requestId = requestAnimationFrame(this._tick);
}
}
stop(): void {
if (this.started) {
this.started = false;
this._cancelIfNeeded();
}
}
private _cancelIfNeeded(): void {
if (this._requestId !== null) {
cancelAnimationFrame(this._requestId);
this._requestId = null;
}
}
複製代碼
2.MainLoop中的任務管理
Ticker類的對象在初始化時會建立_ticker來執行rAF的遞歸:
this._tick = (time: number): void =>{
this._requestId = null;
if (this.started) {
// 調用事件監聽器
this.update(time);
// 當執行
if (this.started && this._requestId === null && this._head.next)
{
this._requestId = requestAnimationFrame(this._tick);
}
}
};
複製代碼
在update方法中會遍歷一個監聽器鏈表
update(currentTime = performance.now()): void {
// ...
const head = this._head;
let listener = head.next;
while (listener) {
listener = listener.emit(this.deltaTime);
}
if (!head.next) {
this._cancelIfNeeded();
}
// ...
}
複製代碼
其中的listener爲一個TickerListener對象,在這個對象中以鏈表的結構存儲多個監聽事件的處理函數,每次emit時執行當前函數,並返回next值對應的下一個listener,若listener爲空則表示執行完畢。
emit(deltaTime: number): TickerListener {
if (this.fn) {
if (this.context) {
this.fn.call(this.context, deltaTime);
} else {
(this as TickerListener<any>).fn(deltaTime);
}
}
const redirect = this.next;
// ...
return redirect;
}
複製代碼
3. 控制任務執行頻率
當設置最大FPS時,會計算每秒內幀之間的最短間隔:
set maxFPS(fps) {
if (fps === 0){
this._minElapsedMS = 0;
} else {
const maxFPS = Math.max(this.minFPS, fps);
this._minElapsedMS = 1 / (maxFPS / 1000);
}
}
複製代碼
則在update()方法中會根據這個時間判斷是否在這一幀內執行後續任務:
update(currentTime = performance.now()): void {
// ...
if (this._minElapsedMS) {
const delta = currentTime - this._lastFrame | 0;
if (delta < this._minElapsedMS) {
return;
}
this._lastFrame = currentTime - (delta % this._minElapsedMS);
}
// ...
}
複製代碼
能夠看出,Pixi實現了高性能2D渲染的目標,背後的付出則是大量額外實現的WebGL圖形繪製(貝塞爾曲線、基礎圖形等)與輔助方法(碰撞檢測)的代碼,而且針對動畫與資源加載也作了許多優化和額外的功能,不失爲一個優秀的框架。