三个文件,都比较长:

  • Container:绘制对象的容器,不代表具体的物体,但是可以对其做一些蒙版和滤镜操作,类似于PS里的图层
  • DisplayObject:可绘制对象的基类,包括Container,Graphics,Sprites等等
  • Bounds:AABB的bounding box,一般图形库应该有的数据结构

所以Display包本质上定义了一些图形的基类和容器,相当于抽象的图形,大概率会有一系列图形操作和变换的方法。


Bounds

从最简单的Bounds入手。由于在three.js源码阅读中已经对渲染库的数学知识有一定了解了,这里直接忽略一些基本的数学操作,关注有特点的东西。
于是发现,没了。。。
Bounds本身就只有minX,minY,maxX,maxY等属性,再加上一个rectangle对象。
其余的就是各种各样的变换,以及和其它对象的运算求运算后bound的操作,基本都比较清晰。

DisplayObject

DisplayObject是其它所有可绘制对象的抽象类,具体见文档:https://pixijs.download/release/docs/PIXI.DisplayObject.html
DisplayObject的很大一部分便是其中包含的属性,以及在创建的时候constructor中为这些属性赋值。

一些属性比较直观:

  • transform: 设置变换参数,包括位置,旋转,缩放等等,有一系列的getter/setter以及setTransform函数来设置与获取
  • alpha:opacity设置
  • worldAlpha:环境的alpha
  • parent: Container,对象的父对象,有对应的setParent函数设置
  • filterArea,filter,_enableFilters:为物体添加滤镜效果,设置滤镜的影响区域,滤镜就是WebGL的那些颜色混合属性
  • isSprite: 判断是否是sprite,(有点奇怪,专门有个属性判断

一些(抽象)方法也比较直观:

  • setParent:设置对象的parent,parent对象直接添加一个child就好了
  • removeChild:抽象方法,字面意思;
  • render:抽象方法,字面意思,具体的对象应该有具体的实现

除此之外,DisplayObject还有一些事件,在进行一些行为的时候触发:

  • added
  • removed
  • destroyed

destroy

destroy函数处理了与parent和listener的关系,同时释放自己的资源,并emit自己destroy的信息,同时,内部的_destroyed属性用来标记object是否被销毁。这些步骤可以学习下:

  1. destroy(_options?: IDestroyOptions|boolean): void
  2. {
  3. if (this.parent)
  4. {
  5. this.parent.removeChild(this);
  6. }
  7. this.emit('destroyed');
  8. this.removeAllListeners();
  9. this.transform = null;
  10. this.parent = null;
  11. // ...
  12. this._destroyed = true;
  13. }

可见性

Pixi用visible,renderable两个属性控制可见性。
两者有一定区别,visible: false类似于css中的display:none,renderable: false类似css中的visible: hidden。
另外,判断一个object在整个世界中可见的,需要其所有上层对象都是可见的,因此:

get worldVisible(): boolean
{
    let item = this as DisplayObject;

    do
    {
        if (!item.visible)
        {
            return false;
        }

        item = item.parent;
    } while (item);

    return true;
}

遮挡顺序

Pixi需要二维平面内物体的遮挡顺序,因此需要引入额外的信息,zIndex用以控制顺序,如果两个的zIndex相同,按照原始顺序排列。
控制遮挡顺序的属性包括:

  • _zIndex: 字面意思,用来控制遮挡顺序
  • _lastSortedIndex: 原始排序,用于在_zIndex相同的情况下进行排序
  • sortDirty: children的排列顺序是否正确(在children更新index之后,顺序可能被打乱,后续需要重新排列)

Mask

Pixi支持用一个Object作为另一个物体的遮罩,也就是设置该物体的mask属性。而作为mask的对象,不会被渲染。Pixi内部在mask的setter中,配合_maskRefCount来实现这一功能。

set mask(value: Container|MaskData|null)
{
      // ...
    if (this._mask)
    {
        const maskObject = ((this._mask as MaskData).maskObject || this._mask) as Container;

        maskObject._maskRefCount--;

        if (maskObject._maskRefCount === 0)
        {
            maskObject.renderable = true;
            maskObject.isMask = false;
        }
    }

    this._mask = value;

    if (this._mask)
    {
        const maskObject = ((this._mask as MaskData).maskObject || this._mask) as Container;

        if (maskObject._maskRefCount === 0)
        {
            maskObject.renderable = false;
            maskObject.isMask = true;
        }

        maskObject._maskRefCount++;
    }
}

_maskRefCount记录了当前对象是否被用作mask,如果被用作mask(_maskRefCount != 0)则不会被渲染,标记为是mask。而当取消作为mask之后,则更新_maskRefCount

tempDisplayObject

Pixi提供了一个默认的DisplayObject对象:tempDisplayObject

export class TemporaryDisplayObject extends DisplayObject
{
    calculateBounds: () => null;
    removeChild: (child: DisplayObject) => null;
    render: (renderer: Renderer) => null;
    sortDirty: boolean = null;
}

也就是把DisplayObject中abstract的属性补全了下,最小的实例对象。
这个对象一般作为对象的临时parent,用来进行一些以root为基准transform时使用。

有控制是否使用其作为parent的方法:

enableTempParent(): Container
{
    const myParent = this.parent;

    this.parent = this._tempDisplayObjectParent as Container;

    return myParent;
}

disableTempParent(cacheParent: Container): void
{
    this.parent = cacheParent;
}


mixin

把一些对象上的属性和方法附加到DisplayObject上,这里可以学习一下API,用Object.getOwnPropertyDescriptorObject.defineProperty可以把各种配置直接复制过来。

    static mixin(source: Dict<any>): void
    {
        // get all the enumerable property keys
        const keys = Object.keys(source);

        // loop through properties
        for (let i = 0; i < keys.length; ++i)
        {
            const propertyName = keys[i];

            // Set the property using the property descriptor - this works for accessors and normal value properties
            Object.defineProperty(
                DisplayObject.prototype,
                propertyName,
                Object.getOwnPropertyDescriptor(source, propertyName)
            );
        }
    }

Transform

DisplayObject里有transform对象,同时暴露出许多相关的getter和setter可以设置transform中的位置,缩放等等,如setTransform, updateTransform等方法。

另外值得注意的是,在updateTransform中,更新了_boundsID,并重新计算了对象的worldAlpha

displayObjectUpdateTransform

关于displayObjectUpdateTransform这个函数,相当于是为DisplayObject保留了自己的updateTransform操作?子类overrideupdateTransform函数的时候,依然在toLocal, toGlobal函数中可以使用DisplayObject中的updateTransform吧。
只能这样解释了,不然说不通为啥毕竟只用updateTransform就可以了。
TODO:不理解,需要后续看看

/**
 * DisplayObject default updateTransform, does not update children of container.
 * Will crash if there's no parent element.
 *
 * @memberof PIXI.DisplayObject#
 * @method displayObjectUpdateTransform
 */
DisplayObject.prototype.displayObjectUpdateTransform = DisplayObject.prototype.updateTransform;

toLocal/toGlobal

功能比较好理解:

  • toGlobal将当前object的局部坐标系下的点转化为全局的坐标
  • toLocal将全局的点,或者另一个object局部坐标系下的点转化为当前object局部坐标系下的坐标

toGlobal为例:

toGlobal<P extends IPointData = Point>(position: IPointData, point?: P, skipUpdate = false): P
{
    if (!skipUpdate)
    {
        this._recursivePostUpdateTransform();

        if (!this.parent)
        {
            this.parent = this._tempDisplayObjectParent as Container;
            this.displayObjectUpdateTransform();
            this.parent = null;
        }
        else
        {
            this.displayObjectUpdateTransform();
        }
    }

    // don't need to update the lot
    return this.worldTransform.apply<P>(position, point);
}

如果不要求更新object的transform,只要应用当前的worldTransform即可。
如果要更新object的transform,则需要进行如下两步操作:

this._recursivePostUpdateTransform();
this.displayObjectUpdateTransform();

基本的功能就是递归更新transform,不过为什么最后还要执行displayObjectUpdateTransform还是有点不理解。TODO!!

toLocal的话,与toGlobal刚好相反,应用逆矩阵即可。对于相对object的处理也比较简单,先转到global再到另一个的local即可:

if (from)
{
    position = from.toGlobal(position, point, skipUpdate);
}

// ...

return this.worldTransform.applyInverse<P>(position, point);

Bounds

一些bounds相关的属性:

  • _bounds: 包围盒,相对于世界坐标系来说
  • _localBounds: local坐标系下的包围盒,相对于父元素的坐标系来说
  • _boundsID: 控制bounds是否dirty,bounds可能会有计算和cache的过程(TODO)
  • _boundsRect: _bounds对应的rectangle,也是一个cache,防止重新计算rect
  • _localBoundsRect: _localBounds对应的rectangle

涉及的三个方法:

calculateBounds

抽象方法,每种object有自己计算bound的方式

getBounds

基本比较常规,主要做三件事情:

  1. 更新transform(如果有必要)
  2. 调用calculateBounds计算bounds(如果有必要)
  3. 更新_boundsRect,并返回rect

getLocalBounds

getLocalBounds(rect?: Rectangle): Rectangle
{
        // ...

    const transformRef = this.transform;
    const parentRef = this.parent;

    this.parent = null;
    this.transform = this._tempDisplayObjectParent.transform;

    const worldBounds = this._bounds;
    const worldBoundsID = this._boundsID;

    this._bounds = this._localBounds;

    const bounds = this.getBounds(false, rect);

    this.parent = parentRef;
    this.transform = transformRef;

    this._bounds = worldBounds;
    this._bounds.updateID += this._boundsID - worldBoundsID;// reflect side-effects

    return bounds;
}

好家伙,getLocalBounds的实现就太暴力了吧。
把object的parent替换掉,以它自己作为root,重新执行calculateBounds,这样就得到了local的结果
难怪:

Do know that usage of getLocalBounds can corrupt the _bounds of children (the whole subtree, actually). This is a known issue that has not been solved. See [getLocalBounds] for more details.

不过最后updateID的操作还有点不太明白,为何如此设置,虽然是很细节的东西啦,就是为了标记children都已经是dirty的了。

Container

容器对象,可以用来整合对象,统一进行操作。
Container继承自DisplayObject,不过并非抽象类,可以直接创建一个Container。

看属性,没几个,主要是children相关的属性和操作。

属性

children

DisplayObject数组类型,用来存放children

parent

container自己的parent,也是Container类型

sortableChildren

children是不是要根据zIndex进行排序

sortDirty

当前顺序是否dirty,是否需要再下一次更新的时候sort

很简单啦,然后方法大概率也是操作相关的属性吧。

width/height

一个container有啥width/height呢?这里的设定还是比较有趣的

get width(): number
{
    return this.scale.x * this.getLocalBounds().width;
}

set width(value: number)
{
    const width = this.getLocalBounds().width;

    if (width !== 0)
    {
        this.scale.x = value / width;
    }
    else
    {
        this.scale.x = 1;
    }

    this._width = value;
}

相当于是自己所有children的localBounds,乘以自己的scale,很合理
set的时候,自然不能挨个改children的size呀,所以只好改自己的scale了

另外,_width_height属性好像没啥用吧,可能就是调试用的

方法

onChildrenChange

暴露出一个接口,可以让子类去override,定义当children change时候的行为。

addChild

可以支持多个参数,不过只看添加一个的部分:

// if the child has a parent then lets remove it as PixiJS objects can only exist in one place
if (child.parent)
{
    child.parent.removeChild(child);
}

child.parent = this;
this.sortDirty = true;

// ensure child transform will be recalculated
child.transform._parentID = -1;

this.children.push(child);

// ensure bounds will be recalculated
this._boundsID++;

// TODO - lets either do all callbacks or all events.. not both!
this.onChildrenChange(this.children.length - 1);
this.emit('childAdded', child, this, this.children.length - 1);
child.emit('added', this);

基本就是,原parent的children里删掉,新parent里的增加,并添加child的parent关系
同时,通过对应counter标记transform和bounds为dirty
最后触发回调函数或添加child的事件

destroy

也没有什么特别好说的,基本上就是处理内部存储的信息,以及触发事件什么的
可以选择要不要destroy children

updateTransform

从上到下,更新自己的以及自己children的transform
同时谁当自己的worldAlpha,其实在这里设置这个属性蛮尴尬的

calculateBounds

calculateBounds(): void
{
    this._bounds.clear();

    this._calculateBounds();

    for (let i = 0; i < this.children.length; i++)
    {
        const child = this.children[i];

        if (!child.visible || !child.renderable)
        {
            continue;
        }

        child.calculateBounds();

        // TODO: filter+mask, need to mask both somehow
        if (child._mask)
        {
            const maskObject = ((child._mask as MaskData).maskObject || child._mask) as Container;

            maskObject.calculateBounds();
            this._bounds.addBoundsMask(child._bounds, maskObject._bounds);
        }
        else if (child.filterArea)
        {
            this._bounds.addBoundsArea(child._bounds, child.filterArea);
        }
        else
        {
            this._bounds.addBounds(child._bounds);
        }
    }

    this._bounds.updateID = this._boundsID;
}

自己的_calculateBounds是空的,就不讨论了
依次计算children的bounds
不过这里考虑了mask和filter的存在,只计算child的有效区域

getLocalBounds

public getLocalBounds(rect?: Rectangle, skipChildrenUpdate = false): Rectangle
{
    const result = super.getLocalBounds(rect);

    if (!skipChildrenUpdate)
    {
        for (let i = 0, j = this.children.length; i < j; ++i)
        {
            const child = this.children[i];

            if (child.visible)
            {
                child.updateTransform();
            }
        }
    }

    return result;
}

基本是调用DisplayObject的getLocalBounds,不过这里要对所有的children执行updateTransform,应该是在计算localBounds的时候破坏了children的transform

render

    render(renderer: Renderer): void
    {
        // if the object is not visible or the alpha is 0 then no need to render this element
        if (!this.visible || this.worldAlpha <= 0 || !this.renderable)
        {
            return;
        }

        // do a quick check to see if this element has a mask or a filter.
        if (this._mask || (this.filters && this.filters.length))
        {
            this.renderAdvanced(renderer);
        }
        else
        {
            this._render(renderer);

            // simple render children!
            for (let i = 0, j = this.children.length; i < j; ++i)
            {
                this.children[i].render(renderer);
            }
        }
    }

有必要好好看看,算是比较重要的方法了。
对于visible, alpharenderable进行一个初步的判断。
如果没有mask或者filter,直接调用自身的_render函数和children的render
Container的_render是空的,因为本身不需要画出什么东西,如果需要自定义的container,可能会加入一些东西。

如果有mask或filter,则需要更复杂的render:renderAdvanced

protected renderAdvanced(renderer: Renderer): void
{
    const filters = this.filters;
    const mask = this._mask as MaskData;

    // push filter first as we need to ensure the stencil buffer is correct for any masking
    if (filters)
    {
        if (!this._enabledFilters)
        {
            this._enabledFilters = [];
        }

        this._enabledFilters.length = 0;

        for (let i = 0; i < filters.length; i++)
        {
            if (filters[i].enabled)
            {
                this._enabledFilters.push(filters[i]);
            }
        }
    }

    const flush = (filters && this._enabledFilters && this._enabledFilters.length)
        || (mask && (!mask.isMaskData
            || (mask.enabled && (mask.autoDetect || mask.type !== MASK_TYPES.NONE))));

    if (flush)
    {
        renderer.batch.flush();
    }

    if (filters && this._enabledFilters && this._enabledFilters.length)
    {
        renderer.filter.push(this, this._enabledFilters);
    }

    if (mask)
    {
        renderer.mask.push(this, this._mask);
    }

    // add this object to the batch, only rendered if it has a texture.
    this._render(renderer);

    // now loop through the children and make sure they get rendered
    for (let i = 0, j = this.children.length; i < j; i++)
    {
        this.children[i].render(renderer);
    }

    if (flush)
    {
        renderer.batch.flush();
    }

    if (mask)
    {
        renderer.mask.pop(this);
    }

    if (filters && this._enabledFilters && this._enabledFilters.length)
    {
        renderer.filter.pop();
    }
}

这函数有亿点点麻烦呀。
其实仔细拆分下来也还好,首先整理下filter和mask,并判断两者是否至少存在一个,如果存在,没办法使用batch render。
紧接着就是给renderer设置mask和filter,交给renderer去做了,这里并不涉及细节。
绘制完之后,将添加给renderer的mask和filter再删掉。

TODO:不过有一点不明白,没啥render完之后,还要flush呢?此时不应该有一个flush的逆操作么?
这个可能要之后看core的时候再研究了。

其它的一些关于children操作的方法都比较常规,无非是获取,设置,交换位置等等
最后,Container的updateTransform也是默认的,就搞不懂了

Container.prototype.containerUpdateTransform = Container.prototype.updateTransform;

TODO

  • displayObjectUpdateTransform
  • isSprite属性
  • getLocalBoundsupdateID的更新
  • renderAdvanced中的第二个flush操作