fabric和konva主要是用于实现编辑器的场景,而Pixi则是一个高性能2D动画渲染库,一般用于一些H5的小游戏或可交互页面。git
本次经过如下几个方面来对其进行分析:github
系列目录canvas
Pixi是一个基于WebGL Renderer的高性能跨平台渲染库。其中默认使用WebGL相关插件(回退使用CanvasRenderer)去渲染2D图形,而且在资源加载和动画处理方面也有比较好的设计和优化。数组
本文所用的Pixi版本为5.2.0。浏览器
在使用Pixi前,须要建立一个Application对象,做为最外层的应用对象。markdown
Application是Pixi中统领全局的对象,其中包含了使用的渲染器(render)、舞台(stage)、安装的插件等主要属性及操做器。app
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图形绘制(贝塞尔曲线、基础图形等)与辅助方法(碰撞检测)的代码,而且针对动画与资源加载也作了许多优化和额外的功能,不失为一个优秀的框架。