Source: rendering/renderers/shared/blendModes/BlendModePipe.ts

import { extensions, ExtensionType } from '../../../../extensions/Extensions';
import { FilterEffect } from '../../../../filters/FilterEffect';
import { warn } from '../../../../utils/logging/warn';

import type { BlendModeFilter } from '../../../../filters/blend-modes/BlendModeFilter';
import type { FilterInstruction } from '../../../../filters/FilterSystem';
import type { Renderer } from '../../types';
import type { Instruction } from '../instructions/Instruction';
import type { InstructionSet } from '../instructions/InstructionSet';
import type { InstructionPipe } from '../instructions/RenderPipe';
import type { Renderable } from '../Renderable';
import type { BLEND_MODES } from '../state/const';

interface AdvancedBlendInstruction extends Instruction
{
    renderPipeId: 'blendMode',
    blendMode: BLEND_MODES,
    activeBlend: Renderable[],
}

// class map
const BLEND_MODE_FILTERS: Partial<Record<BLEND_MODES, new () => BlendModeFilter>> = {} as const;

extensions.handle(ExtensionType.BlendMode, (value) =>
{
    if (!value.name)
    {
        throw new Error('BlendMode extension must have a name property');
    }
    BLEND_MODE_FILTERS[value.name as BLEND_MODES] = value.ref;
}, (value) =>
{
    delete BLEND_MODE_FILTERS[value.name as BLEND_MODES];
});

/**
 * This Pipe handles the blend mode switching of the renderer.
 * It will insert instructions into the renderers.InstructionSet to switch the blend mode according to the
 * blend modes of the scene graph.
 *
 * This pipe is were wwe handle Advanced blend modes. Advanced blend modes essentially wrap the renderables
 * in a filter that applies the blend mode.
 *
 * You only need to use this class if you are building your own render instruction set rather than letting PixiJS build
 * the instruction set for you by traversing the scene graph
 * @memberof rendering
 */
export class BlendModePipe implements InstructionPipe<AdvancedBlendInstruction>
{
    /** @ignore */
    public static extension = {
        type: [
            ExtensionType.WebGLPipes,
            ExtensionType.WebGPUPipes,
            ExtensionType.CanvasPipes,
        ],
        name: 'blendMode',
    } as const;

    private _renderer: Renderer;

    private _renderableList: Renderable[];
    private _activeBlendMode: BLEND_MODES;

    private _isAdvanced = false;

    private _filterHash: Partial<Record<BLEND_MODES, FilterEffect>> = Object.create(null);

    constructor(renderer: Renderer)
    {
        this._renderer = renderer;
    }

    /**
     * This ensures that a blendMode switch is added to the instruction set if the blend mode has changed.
     * @param renderable - The renderable we are adding to the instruction set
     * @param blendMode - The blend mode of the renderable
     * @param instructionSet - The instruction set we are adding to
     */
    public setBlendMode(renderable: Renderable, blendMode: BLEND_MODES, instructionSet: InstructionSet)
    {
        if (this._activeBlendMode === blendMode)
        {
            if (this._isAdvanced) this._renderableList.push(renderable);

            return;
        }

        this._activeBlendMode = blendMode;

        if (this._isAdvanced)
        {
            this._endAdvancedBlendMode(instructionSet);
        }

        this._isAdvanced = !!BLEND_MODE_FILTERS[blendMode];

        if (this._isAdvanced)
        {
            this._beginAdvancedBlendMode(instructionSet);

            this._renderableList.push(renderable);
        }
    }

    private _beginAdvancedBlendMode(instructionSet: InstructionSet)
    {
        this._renderer.renderPipes.batch.break(instructionSet);

        const blendMode = this._activeBlendMode;

        if (!BLEND_MODE_FILTERS[blendMode as keyof typeof BLEND_MODE_FILTERS])
        {
            // #if _DEBUG
            warn(`Unable to assign BlendMode: '${blendMode}'. `
            + `You may want to include: import 'pixi.js/advanced-blend-modes'`);
            // #endif

            return;
        }

        let filterEffect = this._filterHash[blendMode];

        // this does need an execute?
        if (!filterEffect)
        {
            filterEffect = this._filterHash[blendMode] = new FilterEffect();

            filterEffect.filters = [new BLEND_MODE_FILTERS[blendMode as keyof typeof BLEND_MODE_FILTERS]()];
        }

        const instruction: FilterInstruction = {
            renderPipeId: 'filter',
            action: 'pushFilter',
            renderables: [],
            filterEffect,
            canBundle: false,
        };

        this._renderableList = instruction.renderables;
        instructionSet.add(instruction);
    }

    private _endAdvancedBlendMode(instructionSet: InstructionSet)
    {
        this._renderableList = null;
        this._renderer.renderPipes.batch.break(instructionSet);

        instructionSet.add({
            renderPipeId: 'filter',
            action: 'popFilter',
            canBundle: false,
        });
    }

    /**
     * called when the instruction build process is starting this will reset internally to the default blend mode
     * @internal
     * @ignore
     */
    public buildStart()
    {
        this._isAdvanced = false;
    }

    /**
     * called when the instruction build process is finished, ensuring that if there is an advanced blend mode
     * active, we add the final render instructions added to the instruction set
     * @param instructionSet - The instruction set we are adding to
     * @internal
     * @ignore
     */
    public buildEnd(instructionSet: InstructionSet)
    {
        if (this._isAdvanced)
        {
            this._endAdvancedBlendMode(instructionSet);
        }
    }

    /**
     * @internal
     * @ignore
     */
    public destroy()
    {
        this._renderer = null;
        this._renderableList = null;

        for (const i in this._filterHash)
        {
            this._filterHash[i as BLEND_MODES].destroy();
        }

        this._filterHash = null;
    }
}