@nmmty/lazycanvas
Version:
A simple way to interact with @napi-rs/canvas in an advanced way!
184 lines (183 loc) • 8.03 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RenderManager = void 0;
const types_1 = require("../../types");
const LazyUtil_1 = require("../../utils/LazyUtil");
// @ts-ignore
const gifenc_1 = require("gifenc");
/**
* Class responsible for managing rendering operations, including static and animated exports.
*/
class RenderManager {
/**
* The LazyCanvas instance used for rendering.
*/
lazyCanvas;
/**
* Whether debugging is enabled.
*/
debug;
/**
* Constructs a new RenderManager instance.
* @param {LazyCanvas} [lazyCanvas] - The LazyCanvas instance to use for rendering.
* @param {Object} [opts] - Optional settings for the RenderManager.
* @param {boolean} [opts.debug] - Whether debugging is enabled.
*/
constructor(lazyCanvas, opts) {
this.lazyCanvas = lazyCanvas;
this.debug = opts?.debug || false;
}
/**
* Merges multiple ImageData objects into a single ImageData object.
* @param {SKRSContext2D} [ctx] - The canvas rendering context.
* @param {ImageData[]} [imageDataList] - The list of ImageData objects to merge.
* @param {number} [width] - The width of the resulting ImageData.
* @param {number} [height] - The height of the resulting ImageData.
* @returns {ImageData} The merged ImageData object.
*/
mergeImageData(ctx, imageDataList, width, height) {
const mergedData = ctx.createImageData(width, height);
const mergedPixels = mergedData.data;
for (const imageData of imageDataList) {
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
const r = pixels[i];
const g = pixels[i + 1];
const b = pixels[i + 2];
const a = pixels[i + 3] / 255;
const existingAlpha = mergedPixels[i + 3] / 255;
const newAlpha = a + existingAlpha * (1 - a);
if (newAlpha > 0) {
mergedPixels[i] = (r * a + mergedPixels[i] * existingAlpha * (1 - a)) / newAlpha;
mergedPixels[i + 1] = (g * a + mergedPixels[i + 1] * existingAlpha * (1 - a)) / newAlpha;
mergedPixels[i + 2] = (b * a + mergedPixels[i + 2] * existingAlpha * (1 - a)) / newAlpha;
mergedPixels[i + 3] = newAlpha * 255;
}
}
}
return mergedData;
}
/**
* Renders a single layer or group of layers.
* @param {AnyLayer | Group} [layer] - The layer or group to render.
* @returns {Promise<SKRSContext2D>} The canvas rendering context after rendering.
*/
async renderLayer(layer) {
if (this.debug)
LazyUtil_1.LazyLog.log('info', `Rendering ${layer.id}...\nData:`, layer.toJSON());
if (layer.visible) {
this.lazyCanvas.ctx.globalCompositeOperation = layer.props?.globalComposite || 'source-over';
await layer.draw(this.lazyCanvas.ctx, this.lazyCanvas.canvas, this.lazyCanvas.manager.layers, this.debug);
this.lazyCanvas.ctx.shadowColor = 'transparent';
}
return this.lazyCanvas.ctx;
}
/**
* Renders all layers statically and exports the result in the specified format.
* @param {AnyExport} [exportType] - The export format (e.g., buffer, SVG, or context).
* @returns {Promise<Buffer | SKRSContext2D | string>} The rendered output in the specified format.
*/
async renderStatic(exportType) {
if (this.debug)
LazyUtil_1.LazyLog.log('info', `Rendering static...`);
for (const layer of this.lazyCanvas.manager.layers.toArray()) {
await this.renderLayer(layer);
}
switch (exportType) {
case types_1.Export.BUFFER:
case "buffer":
case types_1.Export.SVG:
case "svg":
if ('getContent' in this.lazyCanvas.canvas) {
return this.lazyCanvas.canvas.getContent().toString('utf8');
}
return this.lazyCanvas.canvas.toBuffer('image/png');
case types_1.Export.CTX:
case "ctx":
return this.lazyCanvas.ctx;
default:
if ('getContent' in this.lazyCanvas.canvas) {
return this.lazyCanvas.canvas.getContent().toString('utf8');
}
return this.lazyCanvas.canvas.toBuffer('image/png');
}
}
/**
* Renders an animated sequence of layers and exports it as a GIF.
* @returns {Promise<Buffer>} The rendered animation as a Buffer.
*/
async renderAnimation() {
const encoder = new gifenc_1.GIFEncoder();
if (this.debug)
LazyUtil_1.LazyLog.log('info', `Rendering animation...\nData:`, this.lazyCanvas.manager.animation.options);
const frameBuffer = [];
const { width, height } = this.lazyCanvas.options;
const delay = 1000 / this.lazyCanvas.manager.animation.options.frameRate;
const { loop, colorSpace, maxColors, transparency, utils } = this.lazyCanvas.manager.animation.options;
let frameNumber = 0;
for (const layer of this.lazyCanvas.manager.layers.toArray()) {
// onAnimationFrame hook
this.lazyCanvas.manager.plugins.executeHook('onAnimationFrame', this.lazyCanvas, frameNumber);
const ctx = await this.renderLayer(layer);
frameBuffer.push(ctx.getImageData(0, 0, width, height));
if (frameBuffer.length > utils.buffer.size) {
frameBuffer.shift();
}
const mergeData = this.mergeImageData(ctx, frameBuffer, width, height);
const palette = (0, gifenc_1.quantize)(mergeData.data, maxColors, { format: colorSpace });
const index = (0, gifenc_1.applyPalette)(mergeData.data, palette, colorSpace);
encoder.writeFrame(index, width, height, {
palette,
transparent: transparency,
delay,
loop
});
if (utils.clear)
ctx.clearRect(0, 0, width, height);
frameNumber++;
}
encoder.finish();
return encoder.bytesView();
}
/**
* Renders all layers and exports the result in the specified format.
* @param {AnyExport} [format] - The export format (e.g., buffer, context, SVG, or canvas).
* @returns {Promise<Buffer | SKRSContext2D | Canvas | SvgCanvas | string>} The rendered output in the specified format.
*/
async render(format) {
let result;
// beforeRender hook
this.lazyCanvas.manager.plugins.executeHook('beforeRender', this.lazyCanvas);
switch (format) {
case types_1.Export.BUFFER:
case "buffer":
if (this.lazyCanvas.options.animated) {
result = await this.renderAnimation();
}
else {
result = await this.renderStatic(types_1.Export.BUFFER);
}
break;
case types_1.Export.CTX:
case "ctx":
result = this.lazyCanvas.ctx;
break;
case types_1.Export.SVG:
case "svg":
result = await this.renderStatic(types_1.Export.SVG);
break;
case types_1.Export.CANVAS:
case "canvas":
await this.renderStatic(this.lazyCanvas.options.exportType === 'svg' ? types_1.Export.SVG : types_1.Export.BUFFER);
result = this.lazyCanvas.canvas;
break;
default:
result = await this.renderStatic(types_1.Export.BUFFER);
break;
}
// afterRender hook
this.lazyCanvas.manager.plugins.executeHook('afterRender', this.lazyCanvas);
return result;
}
}
exports.RenderManager = RenderManager;