|
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; |
|
} |
|
|