pixi.js 影像資源(svg)轉紋理

傅小灰發表於2021-01-03

當Pixi使用WebGL去呼叫GPU渲染影像時,需要先將影像轉化為GPU可以處理的版本。而能夠被GPU處理的影像就叫做紋理,在pixi中使用紋理快取來儲存和引用所有紋理。通過將紋理分配給精靈,再將精靈新增到舞臺上,從而顯示影像。

影像轉化為紋理的方式

1. app的loader物件

Pixi強大的loader物件可以載入任何種類的影像資源,並儲存在紋理快取中。後續如果需要繼續獲取紋理,就不用再重複載入影像,直接從快取中獲取即可,減輕GPU記憶體佔用。

app.loader
  .add("imgs/1.jpg")
  .load(setup);

function setup() {
  //This code will run when the loader has finished loading the image
  let sprite = new PIXI.Sprite(app.loader.resources["imgs/1.jpg"].texture);
  app.stage.add(spirte)
}
2. Pixi的Texture型別

Pixi的Texture型別,實現了載入影像的靜態方法。
static from(source: string | HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | PIXI.BaseTexture, options?: any, strict?: boolean): PIXI.Texture;

從允許的引數型別可以看出,我們可以通過直接傳入影像地址、img標籤、canvas標籤、video標籤,將影像轉化為紋理。

  • 通過影像連結載入
var texture = PIXI.Texture.from("imgs/1.jpg");
var sprite = new PIXI.Sprite(PIXI.utils.TextureCache["imgs/1.jpg"]);
app.stage.addChild(sprite);

根據連結載入影像,更推薦這種方式,可以捕獲到影像載入失敗

ps:其實fromURL(),內部呼叫的還是from()

 PIXI.Texture.fromURL(data)
.then((texture) => {
    const spirte = new PIXI.Sprite(texture);
    app.stage.addChild(spirte);
})
.catch((e) => {
    console.log("load error", e);
});
  • 通過標籤載入
var img = document.getElementById("img");
var texture = PIXI.Texture.from(img);
var sprite = new PIXI.Sprite(texture)
app.stage.addChild(sprite)

將SVG Dom節點轉為紋理

如果只是單純的把svg作為一個單獨的外部檔案,其實只要按照上面兩種轉換紋理的方式,傳入svg影像連結就可以實現了。但是如果這個svg是在同一個html頁上的dom節點呢?這時候還能將其轉為紋理嗎?答案是可以的。

注意觀察Texture.from()的引數,可以傳入影像的連結。那麼base64編碼後的影像地址,按理來說也可以。所以只要將頁面上的svg節點,轉化為base64編碼即可。

function getSvgBase64(id) {
    var svg = document.getElementById(id)
    return "data:image/svg+xml;base64," +  window.btoa(svg.outerHTML);
}

關鍵程式碼:window.btoa() 建立一個base64編碼的字串,解碼方法 window.atob()

原始碼解析

首先,從Texture.from()開始入手,我們具體看看pixi是如何載入影像紋理的。

在from方法中有這麼一句話texture = new Texture(new BaseTexture(source, options));。所有的Texture對應的還有一個BaseTexture,他們之間的關係可以這麼解釋

BaseTexture : The base texture source to create the texture from

接下來看一下 BaseTexture類的建構函式,其中呼叫了autoDetectResource()方法,在這個方法中真正的對資源進行了檢測分類,並根據不同型別的資源呼叫不同的資源外掛(ResourcePlugin)。

function autoDetectResource(source: unknown, options?: IAutoDetectOptions): Resource
{
    if (!source)
    {
        return null;
    }

    let extension = '';
    if (typeof source === 'string')
    {
        // search for file extension: period, 3-4 chars, then ?, # or EOL
        const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source);
        if (result)
        {
            extension = result[1].toLowerCase();
        }
    }

    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);
        }
    }
    throw new Error('Unrecognized source type to auto-detect Resource');
}

INSTALLED在index.ts中已經初始化注入所有的ResourcePlugin

INSTALLED.push(
    ImageResource,
    ImageBitmapResource,
    CanvasResource,
    VideoResource,
    SVGResource,
    BufferResource,
    CubeResource,
    ArrayResource
);

在這裡可以看到,pixi中有一個SVGResource,我們就以這個為例繼續深入看下內部的處理機制。

簡化版SVGResource類:

export class SVGResource extends BaseImageResource
{
    constructor(sourceBase64: string, options?: ISVGResourceOptions)
    {
        //...
        super(document.createElement('canvas'));
        if (options.autoLoad !== false)
        {
            this.load();
        }
    }

    load(): Promise<SVGResource>
    {
        // Convert SVG inline string to data-uri
        if ((/^\<svg/).test(this.svg.trim()))
        {
            if (!btoa)
            {
                throw new Error('Your browser doesn\'t support base64 conversions.');
            }
            (this as any).svg = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(this.svg)))}`;
        }

        this._loadSvg();
        return this._load;
    }

    /**
     * Loads an SVG image from `imageUrl` or `data URL`.
     *
     * @private
     */
    private _loadSvg(): void
    {
        const tempImage = new Image();

        BaseImageResource.crossOrigin(tempImage, this.svg, this._crossorigin);
        tempImage.src = this.svg;//將base64編碼的Svg字串,建立為Image物件

        tempImage.onload = (): void =>
        {
            // Draw the Svg to the canvas
            canvas
                .getContext('2d')
                .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, width, height);

        };
    }

    static test(source: unknown, extension?: string): boolean
    {
        // url file extension is SVG
        return extension === 'svg'
            // source is SVG data-uri
            || (typeof source === 'string' && (/^data:image\/svg\+xml(;(charset=utf8|utf8))?;base64/).test(source))
            // source is SVG inline
            || (typeof source === 'string' && source.indexOf('<svg') === 0);
    }

}

看完這裡就差不多明白了,對於傳入的Source來說,當在autoDetectResource()中通過test()方法檢測到資源為SVG格式後,將其轉換為Base64字串(也就是說直接傳入拼接好的svg字串也是可以被解析的~),然後再load為Image物件,載入到臨時canvas中。最後再通過BaseImageResourceupload()呼叫GPU輸出影像資源。

總結

一步步瞭解一個東西過程還是很有意思的,每走一步都會有新的發現。事實上還是有很多東西沒有搞懂,就留著以後去發現了。

相關文章