|
import {
|
|
Application,
|
|
Container,
|
|
Graphics,
|
|
Sprite,
|
|
Rectangle,
|
|
RenderTexture,
|
|
type IRenderer,
|
|
type DisplayObject,
|
|
type ICanvas
|
|
} from "pixi.js";
|
|
|
|
import { type LayerScene } from "../layers/utils";
|
|
|
|
|
|
|
|
|
|
export interface PixiApp {
|
|
|
|
|
|
|
|
layer_container: Container;
|
|
|
|
|
|
|
|
background_container: Container;
|
|
|
|
|
|
|
|
renderer: IRenderer;
|
|
|
|
|
|
|
|
view: HTMLCanvasElement & ICanvas;
|
|
|
|
|
|
|
|
mask_container: Container;
|
|
destroy(): void;
|
|
|
|
|
|
|
|
|
|
|
|
resize(width: number, height: number): void;
|
|
|
|
|
|
|
|
|
|
|
|
get_blobs(
|
|
layers: LayerScene[],
|
|
bounds: Rectangle,
|
|
dimensions: [number, number]
|
|
): Promise<ImageBlobs>;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get_layers?: () => LayerScene[];
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function create_pixi_app({
|
|
target,
|
|
dimensions: [width, height],
|
|
antialias
|
|
}: {
|
|
target: HTMLElement;
|
|
dimensions: [number, number];
|
|
antialias: boolean;
|
|
}): PixiApp {
|
|
const ratio = window.devicePixelRatio || 1;
|
|
const app = new Application({
|
|
width,
|
|
height,
|
|
antialias: antialias,
|
|
backgroundAlpha: 0,
|
|
eventMode: "static"
|
|
});
|
|
const view = app.view as HTMLCanvasElement;
|
|
|
|
app.stage.sortableChildren = true;
|
|
view.style.maxWidth = `${width / ratio}px`;
|
|
view.style.maxHeight = `${height / ratio}px`;
|
|
view.style.width = "100%";
|
|
view.style.height = "100%";
|
|
|
|
target.appendChild(app.view as HTMLCanvasElement);
|
|
|
|
|
|
|
|
const background_container = new Container() as Container & DisplayObject;
|
|
background_container.zIndex = 0;
|
|
const layer_container = new Container() as Container & DisplayObject;
|
|
layer_container.zIndex = 1;
|
|
|
|
|
|
layer_container.sortableChildren = true;
|
|
|
|
const mask_container = new Container() as Container & DisplayObject;
|
|
mask_container.zIndex = 1;
|
|
const composite_container = new Container() as Container & DisplayObject;
|
|
composite_container.zIndex = 0;
|
|
|
|
mask_container.addChild(background_container);
|
|
mask_container.addChild(layer_container);
|
|
|
|
app.stage.addChild(mask_container);
|
|
app.stage.addChild(composite_container);
|
|
const mask = new Graphics();
|
|
let text = RenderTexture.create({
|
|
width,
|
|
height
|
|
});
|
|
const sprite = new Sprite(text);
|
|
|
|
mask_container.mask = sprite;
|
|
|
|
app.render();
|
|
|
|
function reset_mask(width: number, height: number): void {
|
|
background_container.removeChildren();
|
|
mask.beginFill(0xffffff, 1);
|
|
mask.drawRect(0, 0, width, height);
|
|
mask.endFill();
|
|
text = RenderTexture.create({
|
|
width,
|
|
height
|
|
});
|
|
app.renderer.render(mask, {
|
|
renderTexture: text
|
|
});
|
|
|
|
const sprite = new Sprite(text);
|
|
|
|
mask_container.mask = sprite;
|
|
}
|
|
|
|
function resize(width: number, height: number): void {
|
|
app.renderer.resize(width, height);
|
|
view.style.maxWidth = `${width / ratio}px`;
|
|
view.style.maxHeight = `${height / ratio}px`;
|
|
reset_mask(width, height);
|
|
}
|
|
|
|
async function get_blobs(
|
|
_layers: LayerScene[],
|
|
bounds: Rectangle,
|
|
[w, h]: [number, number]
|
|
): Promise<ImageBlobs> {
|
|
const background = await get_canvas_blob(
|
|
app.renderer,
|
|
background_container,
|
|
bounds,
|
|
w,
|
|
h
|
|
);
|
|
|
|
const layers = await Promise.all(
|
|
_layers.map((layer) =>
|
|
get_canvas_blob(
|
|
app.renderer,
|
|
layer.composite as DisplayObject,
|
|
bounds,
|
|
w,
|
|
h
|
|
)
|
|
)
|
|
);
|
|
|
|
const composite = await get_canvas_blob(
|
|
app.renderer,
|
|
mask_container,
|
|
bounds,
|
|
w,
|
|
h
|
|
);
|
|
|
|
return {
|
|
background,
|
|
layers,
|
|
composite
|
|
};
|
|
}
|
|
|
|
return {
|
|
layer_container,
|
|
renderer: app.renderer,
|
|
destroy: () => app.destroy(true),
|
|
view: app.view as HTMLCanvasElement & ICanvas,
|
|
background_container,
|
|
mask_container,
|
|
resize,
|
|
get_blobs
|
|
};
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function make_graphics(z_index: number): Graphics {
|
|
const graphics = new Graphics();
|
|
graphics.eventMode = "none";
|
|
graphics.zIndex = z_index;
|
|
|
|
return graphics;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function clamp(n: number, min: number, max: number): number {
|
|
return n < min ? min : n > max ? max : n;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function get_canvas_blob(
|
|
renderer: IRenderer,
|
|
obj: DisplayObject,
|
|
bounds: Rectangle,
|
|
width: number,
|
|
height: number
|
|
): Promise<Blob | null> {
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
const src_canvas = renderer.extract.canvas(
|
|
obj,
|
|
new Rectangle(0, 0, width, height)
|
|
);
|
|
|
|
|
|
let dest_canvas = document.createElement("canvas");
|
|
dest_canvas.width = bounds.width;
|
|
dest_canvas.height = bounds.height;
|
|
let dest_ctx = dest_canvas.getContext("2d");
|
|
|
|
if (!dest_ctx) {
|
|
resolve(null);
|
|
throw new Error("Could not create canvas context");
|
|
}
|
|
|
|
|
|
dest_ctx.drawImage(
|
|
src_canvas as HTMLCanvasElement,
|
|
|
|
bounds.x,
|
|
bounds.y,
|
|
bounds.width,
|
|
bounds.height,
|
|
|
|
0,
|
|
0,
|
|
bounds.width,
|
|
bounds.height
|
|
);
|
|
|
|
|
|
dest_canvas.toBlob?.((blob) => {
|
|
if (!blob) {
|
|
resolve(null);
|
|
}
|
|
resolve(blob);
|
|
});
|
|
});
|
|
}
|
|
|
|
export interface ImageBlobs {
|
|
background: Blob | null;
|
|
layers: (Blob | null)[];
|
|
composite: Blob | null;
|
|
}
|
|
|