control/FramebufferPreview.tstypescript
import { Dialog } from "../ui/Dialog";
import { Framebuffer } from "../webgl/Framebuffer";
import { Control, IControlParams } from "./Control";
import { ShaderProgram } from "../webgl/ShaderProgram";

function creteCanvas(width: number, height: number) {
    let canvas = document.createElement("canvas") as HTMLCanvasElement;
    canvas.width = width;
    canvas.height = height;
    canvas.style.position = "absolute";
    canvas.style.width = "100%";
    canvas.style.height = "100%";
    return canvas;
}

export interface IFramebufferDialogParams extends IControlParams {
    framebuffer?: Framebuffer;
    arrayTexture?: WebGLTexture;
    arrayLayer?: number;
    width?: number;
    height?: number;
    left?: number;
    top?: number;
    title?: string;
    common?: string;
    image?: string;
    flippedY?: boolean;
}

export class FramebufferPreview extends Control {
    protected _dialog: Dialog<null>;

    protected _framebuffer: Framebuffer | null;
    protected _arrayTexture: WebGLTexture | null;
    protected _arrayLayer: number;
    protected _width: number;
    protected _height: number;
    protected _isArrayMode: boolean;
    protected _screenFramebuffer: Framebuffer | null;
    protected _program: ShaderProgram;

    public $canvas: HTMLCanvasElement;
    public framebufferCurrentTexture: number;

    constructor(params: IFramebufferDialogParams) {
        super({
            autoActivate: true,
            ...params
        });

        this._dialog = new Dialog<null>({
            title: params.title || "",
            width: params.width ?? 580,
            height: params.height ?? 340,
            left: params.left || 100,
            top: params.top || 100,
            useHide: true
        });
        this._dialog.events.on("visibility", this._onDialogVisibilityChange);
        this.$canvas = creteCanvas(this._dialog.width, this._dialog.height);

        this._arrayTexture = params.arrayTexture || null;
        this._arrayLayer = params.arrayLayer ?? 0;
        this._isArrayMode = !!this._arrayTexture;

        this._framebuffer = params.framebuffer || null;
        this._screenFramebuffer = null;
        this.framebufferCurrentTexture = 0;

        this._width = params.width ?? this._framebuffer?.width ?? 512;
        this._height = params.height ?? this._framebuffer?.height ?? 512;

        this._program = framebuffer_dialog_screen(
            this.__id,
            params.common,
            params.image,
            params.flippedY,
            this._isArrayMode
        );
    }

    public bindFramebuffer(framebuffer: Framebuffer): void {
        if (!framebuffer.isEqual(this._framebuffer)) {
            this._framebuffer = framebuffer;
        }
    }

    public bindArrayTexture(texture: WebGLTexture, layer: number): void {
        this._arrayTexture = texture;
        this._arrayLayer = layer;
        this._isArrayMode = true;
    }

    public override oninit() {
        super.oninit();
        if (this.renderer) {
            this.renderer.handler.addProgram(this._program);

            this._screenFramebuffer = new Framebuffer(this.renderer.handler, {
                width: this._width,
                height: this._height,
                useDepth: false,
                targets: [
                    {
                        internalFormat: "RGBA8",
                        attachment: "COLOR_ATTACHMENT",
                        readAsync: true
                    }
                ]
            });
            this._screenFramebuffer.init();
        }
    }

    public override activate() {
        super.activate();
        this._dialog.appendTo(document.body);
        this._dialog.container?.appendChild(this.$canvas);
        this._dialog.show();
        this._subscribeDraw();
    }

    public override deactivate() {
        super.deactivate();
        this._unsubscribeDraw();
        this._dialog.remove();
    }

    protected _isDrawSubscribed: boolean = false;

    protected _subscribeDraw() {
        if (!this.renderer || this._isDrawSubscribed) return;
        this.renderer.events.on("predraw", this._onDraw);
        this._isDrawSubscribed = true;
    }

    protected _unsubscribeDraw() {
        if (!this.renderer || !this._isDrawSubscribed) return;
        this.renderer.events.off("predraw", this._onDraw);
        this._isDrawSubscribed = false;
    }

    protected _onDialogVisibilityChange = (isVisible: boolean) => {
        if (isVisible) {
            this._subscribeDraw();
        } else {
            this._unsubscribeDraw();
        }
    };

    protected _onDraw = () => {
        if (!this._dialog.getVisibility()) return;

        const hasSource = this._isArrayMode ? !!this._arrayTexture : !!this._framebuffer;

        if (!hasSource || !this._screenFramebuffer) return;

        let r = this.renderer!;
        let h = r.handler;
        let gl = h.gl!;

        gl.disable(gl.BLEND);
        this._screenFramebuffer.activate();
        let p = this._program;

        gl.bindBuffer(gl.ARRAY_BUFFER, r.screenFramePositionBuffer!);
        gl.vertexAttribPointer(p.corners, 2, gl.FLOAT, false, 0, 0);

        p.activate();

        gl.activeTexture(gl.TEXTURE0);
        if (this._isArrayMode) {
            gl.bindTexture(gl.TEXTURE_2D_ARRAY, this._arrayTexture);
            gl.uniform1i(p.uniforms.inputTextureArray, 0);
            gl.uniform1i(p.uniforms.u_arrayLayer, this._arrayLayer);
        } else {
            gl.bindTexture(gl.TEXTURE_2D, this._framebuffer!.textures[this.framebufferCurrentTexture]);
            gl.uniform1i(p.uniforms.inputTexture, 0);
        }

        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

        this._screenFramebuffer.deactivate();
        gl.enable(gl.BLEND);

        this._screenFramebuffer.readPixelBuffersAsync((f) => {
            let pixels = f.getPixelBufferData();
            if (pixels) {
                let width = f.width;
                let height = f.height;
                let ctx = this.$canvas.getContext("2d")!;

                let clamped = new Uint8ClampedArray(pixels.buffer as ArrayBuffer, pixels.byteOffset, pixels.byteLength);
                let imageData = new ImageData(clamped, width, height);

                createImageBitmap(imageData).then((bitmap) => {
                    ctx.clearRect(0, 0, this.$canvas.width, this.$canvas.height);
                    ctx.drawImage(bitmap, 0, 0, this.$canvas.width, this.$canvas.height);
                });
            }
        });
    };
}

function framebuffer_dialog_screen(
    id: number = 0,
    common?: string | null,
    mainImage?: string | null,
    flippedY?: boolean,
    isArrayMode: boolean = false
): ShaderProgram {
    const uniforms: Record<string, string> = isArrayMode
        ? { inputTextureArray: "sampler2DArray", u_arrayLayer: "int" }
        : { inputTexture: "sampler2D" };

    const samplerDecl = isArrayMode
        ? `precision highp sampler2DArray;\nuniform sampler2DArray inputTextureArray;\nuniform int u_arrayLayer;`
        : `uniform sampler2D inputTexture;`;

    const defaultMainImage = isArrayMode
        ? `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
                fragColor = texture(inputTextureArray, vec3(fragCoord, float(u_arrayLayer)));
            }`
        : `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
                fragColor = texture(inputTexture, fragCoord);
            }`;

    return new ShaderProgram(`framebuffer_dialog_screen:${id.toString()}`, {
        uniforms,
        attributes: {
            corners: "vec2"
        },
        vertexShader: `#version 300 es

            in vec2 corners;

            out vec2 tc;

            void main(void) {
                gl_Position = vec4(corners, 0.0, 1.0);
                tc = corners * 0.5 + 0.5;
                ${flippedY ? `tc.y = 1.0 - tc.y;` : ``}
            }`,
        fragmentShader: `#version 300 es

            precision highp float;

            ${samplerDecl}

            in vec2 tc;

            layout(location = 0) out vec4 fragColor;

            ${common || ""}

            ${mainImage || defaultMainImage}

            void main(void) {
               mainImage(fragColor, tc);
            }`
    });
}