pixi.js
Version:
<p align="center"> <a href="https://pixijs.com" target="_blank" rel="noopener noreferrer"> <img height="150" src="https://files.pixijs.download/branding/pixijs-logo-transparent-dark.svg?v=1" alt="PixiJS logo"> </a> </p> <br/> <p align="center">
258 lines (255 loc) • 8.55 kB
JavaScript
import { ExtensionType, extensions } from '../../../extensions/Extensions.mjs';
import { Bounds } from '../../../scene/container/bounds/Bounds.mjs';
import { getGlobalRenderableBounds } from '../../../scene/container/bounds/getRenderableBounds.mjs';
import { getPo2TextureFromSource } from '../../../scene/text/utils/getPo2TextureFromSource.mjs';
import { CanvasPool } from '../shared/texture/CanvasPool.mjs';
import { canvasUtils } from './utils/canvasUtils.mjs';
"use strict";
function isCanvasFilterCapable(filter) {
return typeof filter.getCanvasFilterString === "function";
}
class CanvasFilterFrame {
constructor() {
this.skip = false;
this.useClip = false;
this.filters = null;
this.container = null;
this.bounds = new Bounds();
this.cssFilterString = "";
}
}
class CanvasFilterSystem {
/**
* @param renderer - The Canvas renderer
* @param renderer.canvasContext
* @param renderer.canvasContext.activeContext
* @param renderer.canvasContext.activeResolution
*/
constructor(renderer) {
this._filterStack = [];
this._filterStackIndex = 0;
this._savedStates = [];
this._alphaMultiplier = 1;
this._warnedFilterTypes = /* @__PURE__ */ new Set();
this.renderer = renderer;
}
/**
* Push a filter instruction onto the stack.
* Called when entering a filtered container.
* @param instruction - The filter instruction from FilterPipe
*/
push(instruction) {
const filterFrame = this._pushFilterFrame();
const filters = instruction.filterEffect.filters;
filterFrame.skip = false;
filterFrame.useClip = false;
filterFrame.filters = filters;
filterFrame.container = instruction.container;
filterFrame.cssFilterString = "";
if (filters.every((filter) => !filter.enabled)) {
filterFrame.skip = true;
return;
}
const cssFilters = [];
const alphaMultiplier = 1;
for (const filter of filters) {
if (!filter.enabled) continue;
if (!isCanvasFilterCapable(filter)) {
this._warnUnsupportedFilter(filter);
continue;
}
const cssString = filter.getCanvasFilterString();
if (cssString === null) {
this._warnUnsupportedFilter(filter);
continue;
}
if (cssString) {
cssFilters.push(cssString);
}
}
if (cssFilters.length === 0 && alphaMultiplier === 1) {
filterFrame.skip = true;
return;
}
filterFrame.cssFilterString = cssFilters.join(" ");
this._calculateFilterArea(instruction, filterFrame.bounds);
filterFrame.useClip = !!instruction.filterEffect.filterArea;
const context = this.renderer.canvasContext.activeContext;
const previousFilter = context.filter || "none";
this._savedStates.push({ filter: previousFilter, alphaMultiplier: this._alphaMultiplier });
if (filterFrame.useClip && Number.isFinite(filterFrame.bounds.width) && Number.isFinite(filterFrame.bounds.height) && filterFrame.bounds.width > 0 && filterFrame.bounds.height > 0) {
const resolution = this.renderer.canvasContext.activeResolution || 1;
context.save();
context.setTransform(1, 0, 0, 1, 0, 0);
context.beginPath();
context.rect(
filterFrame.bounds.x * resolution,
filterFrame.bounds.y * resolution,
filterFrame.bounds.width * resolution,
filterFrame.bounds.height * resolution
);
context.clip();
} else {
filterFrame.useClip = false;
}
if (alphaMultiplier !== 1) {
this._alphaMultiplier *= alphaMultiplier;
}
if (filterFrame.cssFilterString) {
context.filter = previousFilter !== "none" ? `${previousFilter} ${filterFrame.cssFilterString}` : filterFrame.cssFilterString;
}
}
/** Pop a filter from the stack. Called when exiting a filtered container. */
pop() {
const filterFrame = this._popFilterFrame();
if (filterFrame.skip) {
return;
}
const savedState = this._savedStates.pop();
if (!savedState) {
return;
}
const context = this.renderer.canvasContext.activeContext;
if (filterFrame.useClip) {
context.restore();
} else {
context.filter = savedState.filter;
}
this._alphaMultiplier = savedState.alphaMultiplier;
}
/**
* Applies supported filters to a texture and returns a new texture.
* Unsupported filters are skipped with a warn-once message.
* @param params - The parameters for applying filters.
* @param params.texture
* @param params.filters
* @returns The resulting texture after filters are applied.
*/
generateFilteredTexture({ texture, filters }) {
if (!filters?.length || filters.every((filter) => !filter.enabled)) {
return texture;
}
const cssFilters = [];
const alphaMultiplier = 1;
for (const filter of filters) {
if (!filter.enabled) continue;
if (!isCanvasFilterCapable(filter)) {
this._warnUnsupportedFilter(filter);
continue;
}
const cssString = filter.getCanvasFilterString();
if (cssString === null) {
this._warnUnsupportedFilter(filter);
continue;
}
if (cssString) {
cssFilters.push(cssString);
}
}
if (cssFilters.length === 0 && alphaMultiplier === 1) {
return texture;
}
const source = canvasUtils.getCanvasSource(texture);
if (!source) {
return texture;
}
const frame = texture.frame;
const resolution = texture.source._resolution ?? texture.source.resolution ?? 1;
const width = frame.width;
const height = frame.height;
const canvasAndContext = CanvasPool.getOptimalCanvasAndContext(width, height, resolution);
const { canvas, context } = canvasAndContext;
context.setTransform(1, 0, 0, 1, 0, 0);
context.clearRect(0, 0, canvas.width, canvas.height);
if (cssFilters.length) {
context.filter = cssFilters.join(" ");
}
if (alphaMultiplier !== 1) {
context.globalAlpha = alphaMultiplier;
}
const sx = frame.x * resolution;
const sy = frame.y * resolution;
const sw = width * resolution;
const sh = height * resolution;
context.drawImage(
source,
sx,
sy,
sw,
sh,
0,
0,
sw,
sh
);
context.filter = "none";
context.globalAlpha = 1;
return getPo2TextureFromSource(canvas, width, height, resolution);
}
/**
* Calculate the filter area bounds.
* @param instruction - Filter instruction
* @param bounds - Bounds object to populate
*/
_calculateFilterArea(instruction, bounds) {
if (instruction.renderables) {
getGlobalRenderableBounds(instruction.renderables, bounds);
} else if (instruction.filterEffect.filterArea) {
bounds.clear();
bounds.addRect(instruction.filterEffect.filterArea);
bounds.applyMatrix(instruction.container.worldTransform);
} else {
instruction.container.getFastGlobalBounds(true, bounds);
}
if (instruction.container) {
const renderGroup = instruction.container.renderGroup || instruction.container.parentRenderGroup;
const filterFrameTransform = renderGroup?.cacheToLocalTransform;
if (filterFrameTransform) {
bounds.applyMatrix(filterFrameTransform);
}
}
}
_warnUnsupportedFilter(filter) {
const filterName = filter?.constructor?.name || "Filter";
if (this._warnedFilterTypes.has(filterName)) {
return;
}
this._warnedFilterTypes.add(filterName);
console.warn(
`CanvasRenderer: filter "${filterName}" is not supported in Canvas2D and will be skipped.`
);
}
get alphaMultiplier() {
return this._alphaMultiplier;
}
_pushFilterFrame() {
let filterFrame = this._filterStack[this._filterStackIndex];
if (!filterFrame) {
filterFrame = this._filterStack[this._filterStackIndex] = new CanvasFilterFrame();
}
this._filterStackIndex++;
return filterFrame;
}
_popFilterFrame() {
if (this._filterStackIndex <= 0) {
return this._filterStack[0];
}
this._filterStackIndex--;
return this._filterStack[this._filterStackIndex];
}
/** Destroys the system */
destroy() {
this._filterStack = null;
this._savedStates = null;
this._warnedFilterTypes = null;
this._alphaMultiplier = 1;
}
}
/** @ignore */
CanvasFilterSystem.extension = {
type: [ExtensionType.CanvasSystem],
name: "filter"
};
extensions.add(CanvasFilterSystem);
export { CanvasFilterSystem, isCanvasFilterCapable };
//# sourceMappingURL=CanvasFilterSystem.mjs.map