From e0ace2029c2489c7d8b7b237d4c4ecd8a0f19d93 Mon Sep 17 00:00:00 2001 From: skdhg <46562212+skdhg@users.noreply.github.com> Date: Wed, 22 Nov 2023 01:54:15 +0545 Subject: [PATCH 1/7] chore: add jsdoc comments --- src/assets/AssetsFactory.ts | 3 + src/assets/Font.ts | 52 ++++++ src/assets/TemplateFactory.ts | 21 +++ src/assets/fonts/fonts.ts | 3 + src/canvas/Canvacord.ts | 2 +- src/canvas/CanvasHelper.ts | 15 ++ src/canvas/CanvasImage.ts | 31 ++++ src/canvas/Encodable.ts | 25 +++ src/canvas/ImageFilterer.ts | 57 ++++++ src/canvas/ImageGen.ts | 195 +++++++++++++++++++- src/canvas/ImageManipulator.ts | 44 ++++- src/canvas/utils.ts | 12 ++ src/components/LeaderboardBuilder.tsx | 70 ++++++++ src/components/RankCardBuilder.tsx | 239 ++++++++++++++++++++++++- src/helpers/StyleSheet.ts | 18 ++ src/helpers/image.ts | 12 +- src/helpers/jsx.ts | 48 +++++ src/helpers/loadImage.ts | 20 +++ src/helpers/utils.ts | 6 + src/templates/Builder.tsx | 81 +++++++++ src/templates/BuilderOptionsManager.ts | 24 +++ test/demo.tsx | 42 +++++ 22 files changed, 1015 insertions(+), 5 deletions(-) create mode 100644 test/demo.tsx diff --git a/src/assets/AssetsFactory.ts b/src/assets/AssetsFactory.ts index 0d9488e..c1ad361 100644 --- a/src/assets/AssetsFactory.ts +++ b/src/assets/AssetsFactory.ts @@ -5,6 +5,9 @@ export const FontFactory = new Map(); const BASE_URL = process.env.CANVACORD_ASSETS_BASE_URL || 'https://cdn.neplextech.com/canvacord'; const prepareURL = (path: string) => `${BASE_URL}/${path}`; +/** + * The image assets factory. + */ export const ImageFactory = { AFFECT: prepareURL('AFFECT.png'), BATSLAP: prepareURL('BATSLAP.png'), diff --git a/src/assets/Font.ts b/src/assets/Font.ts index 6cb961b..d1b3c00 100644 --- a/src/assets/Font.ts +++ b/src/assets/Font.ts @@ -9,15 +9,30 @@ import { Fonts } from './fonts/fonts'; const randomAlias = () => randomUUID() as string; export class Font { + /** + * Creates and registers a new Font instance for both canvas and builder apis. + * @param data The font data + * @param [alias] The font alias. If not provided, a random UUID will be used. + * @example ```typescript + * const data = await readFile('path/to/font.ttf'); + * const font = new Font(data, 'my-font'); + * ``` + */ public constructor(public data: Buffer, public alias = randomAlias()) { GlobalFonts.register(data, alias); FontFactory.set(this.alias, this); } + /** + * The alias for this font. + */ public get name() { return this.alias; } + /** + * Returns the font data that includes information such as the font name, weight, data, and style. + */ public getData(): FontData { return { data: this.data, @@ -27,28 +42,65 @@ export class Font { }; } + /** + * String representation of this font. + */ public toString() { return this.alias; } + /** + * JSON representation of this font. + */ public toJSON() { return this.getData(); } + /** + * Creates a new Font instance from a file. + * @param path The path to the font file + * @param [alias] The font alias. If not provided, a random UUID will be used. + * @example ```typescript + * const font = await Font.fromFile('path/to/font.ttf', 'my-font'); + * ``` + */ public static async fromFile(path: string, alias?: string) { const buffer = await readFile(path); return new Font(buffer, alias); } + /** + * Creates a new Font instance from a file synchronously. + * @param path The path to the font file + * @param [alias] The font alias. If not provided, a random UUID will be used. + * @example ```typescript + * const font = Font.fromFileSync('path/to/font.ttf', 'my-font'); + * ``` + */ public static fromFileSync(path: string, alias?: string) { const buffer = readFileSync(path); return new Font(buffer, alias); } + /** + * Creates a new Font instance from a buffer. + * @param buffer The buffer containing the font data + * @param [alias] The font alias. If not provided, a random UUID will be used. + * @example ```typescript + * const buffer = await readFile('path/to/font.ttf'); + * const font = Font.fromBuffer(buffer, 'my-font'); + * ``` + */ public static fromBuffer(buffer: Buffer, alias?: string) { return new Font(buffer, alias); } + /** + * Loads the default font bundled with this package. + * @example ```typescript + * const font = Font.loadDefault(); + * ``` + */ public static loadDefault() { return this.fromBuffer(Fonts.Geist, 'geist'); } diff --git a/src/assets/TemplateFactory.ts b/src/assets/TemplateFactory.ts index c84855d..554fffb 100644 --- a/src/assets/TemplateFactory.ts +++ b/src/assets/TemplateFactory.ts @@ -6,18 +6,36 @@ import type { Image, SKRSContext2D } from '@napi-rs/canvas'; export class TemplateImage { #resolved: Image | null = null; + + /** + * Creates a new TemplateImage instance. + * @param source The image source + * @example ```typescript + * const image = new TemplateImage('https://example.com/image.png'); + * ``` + */ public constructor(public source: ImageSource) {} + /** + * Whether this image has been resolved. + */ public resolved() { return this.#resolved != null; } + /** + * Resolves this image to consumable form. + */ public async resolve(): Promise { if (this.#resolved) return this.#resolved; return (this.#resolved = await createCanvasImage(this.source)); } } +/** + * Creates a new template from the provided template. + * @param template The template to create from + */ export const createTemplate = any, P extends Parameters>( cb: (...args: P) => IImageGenerationTemplate ) => { @@ -28,6 +46,9 @@ export const createTemplate = any, P extends Para }; }; +/** + * The built-in template factory. + */ export const TemplateFactory = { Affect: createTemplate((image: ImageSource) => { return { diff --git a/src/assets/fonts/fonts.ts b/src/assets/fonts/fonts.ts index b016f49..574d589 100644 --- a/src/assets/fonts/fonts.ts +++ b/src/assets/fonts/fonts.ts @@ -1,3 +1,6 @@ +/** + * The bundled fonts in this package. + */ export const Fonts = { /** * Geist sans font diff --git a/src/canvas/Canvacord.ts b/src/canvas/Canvacord.ts index cd85b25..a0c9de4 100644 --- a/src/canvas/Canvacord.ts +++ b/src/canvas/Canvacord.ts @@ -1,5 +1,5 @@ import { ImageSource } from '../helpers'; -import { ImageGen } from './ImageGen'; +import { ImageGen, ImageGenerationTemplate } from './ImageGen'; import { buffer } from 'stream/consumers'; import type { Readable } from 'stream'; import { ImageFilterer } from './ImageFilterer'; diff --git a/src/canvas/CanvasHelper.ts b/src/canvas/CanvasHelper.ts index baa729f..9c03d5b 100644 --- a/src/canvas/CanvasHelper.ts +++ b/src/canvas/CanvasHelper.ts @@ -3,12 +3,24 @@ import { Encodable } from './Encodable'; import { ContextManipulationStep } from './utils'; export abstract class CanvasHelper extends Encodable { + /** + * The steps to apply to the canvas. + */ public steps: ContextManipulationStep[] = []; private _canvas!: Canvas; + + /** + * Creates a new CanvasHelper instance. + * @param width The width of the canvas + * @param height The height of the canvas + */ public constructor(public width: number, public height: number) { super(); } + /** + * Returns the canvas instance by applying the steps. + */ public async getFinalCanvas(): Promise { this._canvas ??= createCanvas(this.width, this.height); const ctx = this._canvas.getContext('2d'); @@ -18,5 +30,8 @@ export abstract class CanvasHelper extends Encodable { return this._canvas; } + /** + * Processes the steps and applies them to the canvas. + */ public abstract process(canvas: Canvas, ctx: SKRSContext2D): Promise; } diff --git a/src/canvas/CanvasImage.ts b/src/canvas/CanvasImage.ts index 4a3c543..fd4b26f 100644 --- a/src/canvas/CanvasImage.ts +++ b/src/canvas/CanvasImage.ts @@ -6,6 +6,12 @@ import { createCanvasImage } from './utils'; export class CanvasImage extends ImageFilterer { #img: Image | null = null; + /** + * Creates a new CanvasImage instance. + * @param source The image source + * @param [width] The width of the image + * @param [height] The height of the image + */ public constructor(public source: ImageSource, width = -1, height = -1) { super(width, height); if (source instanceof Image) this.#setImg(source); @@ -18,6 +24,13 @@ export class CanvasImage extends ImageFilterer { return this.#img; } + /** + * Draws the image to the canvas. + * @param x The x position to draw the image + * @param y The y position to draw the image + * @param [width] The width of the image + * @param [height] The height of the image + */ public draw(x = 0, y = 0, width?: number, height?: number) { this.steps.push(async (ctx) => { const img = this.#img || this.#setImg(await createCanvasImage(this.source)); @@ -31,6 +44,11 @@ export class CanvasImage extends ImageFilterer { return this; } + /** + * Draws the image to the canvas with a circle clip. + * @param [width] The width of the image + * @param [height] The height of the image + */ public circle(width?: number, height?: number) { this.steps.push((ctx) => { width ??= ctx.canvas.width; @@ -46,6 +64,10 @@ export class CanvasImage extends ImageFilterer { return this; } + /** + * Draws pixelated image to the canvas. + * @param [pixels=5] The amount of pixels to use + */ public pixelate(pixels = 5) { this.steps.push((ctx) => { const pixel = pixels / 100; @@ -68,6 +90,9 @@ export class CanvasImage extends ImageFilterer { return this; } + /** + * Saves the canvas context state. + */ public save() { this.steps.push((ctx) => { ctx.save(); @@ -76,6 +101,9 @@ export class CanvasImage extends ImageFilterer { return this; } + /** + * Restores the last saved canvas context state. + */ public restore() { this.steps.push((ctx) => { ctx.restore(); @@ -84,6 +112,9 @@ export class CanvasImage extends ImageFilterer { return this; } + /** + * Returns the canvas instance by applying the steps. + */ public async getFinalCanvas(): Promise { if (this.width === -1 || this.height === -1) { if (!this.#img) this.#setImg(await createCanvasImage(this.source)); diff --git a/src/canvas/Encodable.ts b/src/canvas/Encodable.ts index 6d4d189..6cc76f3 100644 --- a/src/canvas/Encodable.ts +++ b/src/canvas/Encodable.ts @@ -3,12 +3,37 @@ import { AvifConfig, Canvas } from '@napi-rs/canvas'; export type EncodingFormat = 'png' | 'jpeg' | 'webp' | 'avif'; export abstract class Encodable { + /** + * Returns the canvas instance by applying the steps. + */ public abstract getFinalCanvas(): Promise; + /** + * Encodes the canvas to a buffer. + */ public async encode(): Promise; + /** + * Encodes the canvas to a png buffer. + * @param format The encoding format - `png` + */ public async encode(format: 'png'): Promise; + /** + * Encodes the canvas to a jpeg or webp buffer. + * @param format The encoding format - `jpeg` or `webp` + * @param [options] The quality of the image + */ public async encode(format: 'jpeg' | 'webp', options?: number): Promise; + /** + * Encodes the canvas to an avif buffer. + * @param format The encoding format - `avif` + * @param [options] The encoding options + */ public async encode(format: 'avif', options?: AvifConfig): Promise; + /** + * Encodes the canvas to a buffer. + * @param format The encoding format + * @param [options] The encoding options or quality + */ public async encode(format: EncodingFormat = 'png', options?: number | AvifConfig): Promise { const canvas = await this.getFinalCanvas(); diff --git a/src/canvas/ImageFilterer.ts b/src/canvas/ImageFilterer.ts index 84afe98..4769f36 100644 --- a/src/canvas/ImageFilterer.ts +++ b/src/canvas/ImageFilterer.ts @@ -6,6 +6,14 @@ import { createCanvasImage } from './utils'; export class ImageFilterer extends CanvasHelper { #filters = [] as string[]; + /** + * Draws the image to the canvas. + * @param image The image to draw + * @param x The x position to draw the image + * @param y The y position to draw the image + * @param [width] The width of the image + * @param [height] The height of the image + */ public drawImage(image: ImageSource, x = 0, y = 0, width = this.width, height = this.height) { this.steps.push(async (ctx) => { const img = await createCanvasImage(image); @@ -15,59 +23,103 @@ export class ImageFilterer extends CanvasHelper { return this; } + /** + * Applies invert filter to the image. + * @param value The filter intensity + */ public invert(value: number) { if (typeof value !== 'number') throw new TypeError(`Expected "value" to be a number, received ${typeof value}`); this.#filters.push(`invert(${value}%)`); return this; } + /** + * Applies grayscale filter to the image. + * @param value The filter intensity + */ public grayscale(value = 100) { this.#filters.push(`grayscale(${value}%)`); return this; } + /** + * Applies sepia filter to the image. + * @param value The filter intensity + */ public sepia(value: number) { if (typeof value !== 'number') throw new TypeError(`Expected "value" to be a number, received ${typeof value}`); this.#filters.push(`sepia(${value}%)`); return this; } + /** + * Applies opacity filter to the image. + * @param value The filter intensity + */ public opacity(value: number) { if (typeof value !== 'number') throw new TypeError(`Expected "value" to be a number, received ${typeof value}`); this.#filters.push(`opacity(${value}%)`); return this; } + /** + * Applies saturate filter to the image. + * @param value The filter intensity + */ public saturate(value: number) { if (typeof value !== 'number') throw new TypeError(`Expected "value" to be a number, received ${typeof value}`); this.#filters.push(`saturate(${value}%)`); return this; } + /** + * Applies hue-rotate filter to the image. + * @param value The degrees to rotate + */ public hueRotate(value: number) { if (typeof value !== 'number') throw new TypeError(`Expected "value" to be a number, received ${typeof value}`); this.#filters.push(`hue-rotate(${value}deg)`); return this; } + /** + * Applies contrast filter to the image. + * @param value The filter intensity + */ public contrast(value: number) { if (typeof value !== 'number') throw new TypeError(`Expected "value" to be a number, received ${typeof value}`); this.#filters.push(`contrast(${value}%)`); return this; } + /** + * Applies brightness filter to the image. + * @param value The filter intensity + */ public brightness(value: number) { if (typeof value !== 'number') throw new TypeError(`Expected "value" to be a number, received ${typeof value}`); this.#filters.push(`brightness(${value}%)`); return this; } + /** + * Applies blur filter to the image. + * @param value The filter intensity + */ public blur(value: number) { if (typeof value !== 'number') throw new TypeError(`Expected "value" to be a number, received ${typeof value}`); this.#filters.push(`blur(${value}px)`); return this; } + /** + * Applies drop-shadow filter to the image. + * @param config The drop-shadow config + * @param config.x The x offset of the shadow + * @param config.y The y offset of the shadow + * @param config.radius The blur radius of the shadow + * @param config.color The color of the shadow + */ public dropShadow(config: DropShadowConfig) { const { x, y, radius, color } = config; @@ -75,6 +127,11 @@ export class ImageFilterer extends CanvasHelper { return this; } + /** + * Renders the applied filters to the canvas. + * @param canvas The canvas to render the filters to + * @param ctx The canvas context + */ public async process(canvas: Canvas, ctx: SKRSContext2D) { if (this.#filters.length) ctx.filter = this.#filters.join(' '); diff --git a/src/canvas/ImageGen.ts b/src/canvas/ImageGen.ts index 632eeb3..f8b5793 100644 --- a/src/canvas/ImageGen.ts +++ b/src/canvas/ImageGen.ts @@ -1,110 +1,265 @@ -import { Canvas, createCanvas, Image, SKRSContext2D } from '@napi-rs/canvas'; +import { Canvas, createCanvas, SKRSContext2D } from '@napi-rs/canvas'; import { GifEncoder, EncoderOptions } from '@skyra/gifenc'; import { Encodable } from './Encodable'; import { TemplateImage } from '../assets/TemplateFactory'; export interface ImageGenerationStep { + /** + * The image to render. + */ image?: ImgenStep[]; + /** + * The text to render. + */ text?: TextGenerationStep[]; + /** + * The custom steps to apply to the canvas. + */ custom?: CustomGenerationStep[]; + /** + * The function to call before processing this step. + */ preprocess?: (canvas: Canvas, ctx: SKRSContext2D, step: ImageGenerationStep) => Awaited; + /** + * The function to call when processing this step. + */ process?: (canvas: Canvas, ctx: SKRSContext2D, step: ImageGenerationStep) => Awaited; + /** + * The function to call after processing has finished. + */ postprocess?: (canvas: Canvas, ctx: SKRSContext2D, step: ImageGenerationStep) => Awaited; } export interface CustomGenerationStep { + /** + * The function to call when processing this step. + */ process: (canvas: Canvas, ctx: SKRSContext2D, step: ImageGenerationStep) => Awaited; } export interface ImgenStep { + /** + * The image to render. + */ source: TemplateImage; + /** + * The x position of the image. + */ x: number; + /** + * The y position of the image. + */ y: number; + /** + * The width of the image. + */ width?: number; + /** + * The height of the image. + */ height?: number; + /** + * The function to call before processing this step. + */ preprocess?: (canvas: Canvas, ctx: SKRSContext2D, source: ImgenStep) => Awaited; + /** + * The function to call when processing this step. + */ process?: (canvas: Canvas, ctx: SKRSContext2D, source: ImgenStep) => Awaited; + /** + * The function to call after processing has finished. + */ postprocess?: (canvas: Canvas, ctx: SKRSContext2D, source: ImgenStep) => Awaited; } export interface TextGenerationStep { + /** + * The text to render. + */ value: string; + /** + * The font of the text. + */ font: string; + /** + * The color of the text. + */ color: string; + /** + * Whether to stroke the text. + */ stroke?: boolean; + /** + * The x position of the text. + */ x: number; + /** + * The y position of the text. + */ y: number; + /** + * The maximum width of the text. + */ maxWidth?: number; + /** + * The line height of the text. + */ lineHeight?: number; + /** + * The line width of the text. + */ lineWidth?: number; + /** + * The alignment of the text. + */ align?: 'left' | 'center' | 'right'; + /** + * The baseline of the text. + */ baseline?: 'top' | 'middle' | 'bottom'; + /** + * The directionality of the text. + */ direction?: 'inherit' | 'ltr' | 'rtl'; + /** + * The function to call before processing this step. + */ preprocess?: (canvas: Canvas, ctx: SKRSContext2D, text: TextGenerationStep) => Awaited; + /** + * The function to call when processing this step. + */ process?: (canvas: Canvas, ctx: SKRSContext2D, text: TextGenerationStep) => Awaited; + /** + * The function to call after processing has finished. + */ postprocess?: (canvas: Canvas, ctx: SKRSContext2D, text: TextGenerationStep) => Awaited; } +/** + * The template to use for image generation. + */ export interface IImageGenerationTemplate { + /** + * The width of the template. + */ width?: number; + /** + * The height of the template. + */ height?: number; + /** + * The steps to apply to the canvas. + */ steps: ImageGenerationStep[]; + /** + * The gif options. + */ gif?: EncoderOptions; } export class ImageGenerationTemplate implements IImageGenerationTemplate { + /** + * The steps to apply to the canvas. + */ public steps: ImageGenerationStep[] = []; + /** + * The gif options. + */ public gif?: EncoderOptions; + /** + * Creates a new ImageGenerationTemplate instance from a template. + * @param template The template to use + * @returns The created template + */ public static from(template: IImageGenerationTemplate) { return new ImageGenerationTemplate(template.width, template.height) .setSteps(template.steps) .setGifOptions(template.gif); } + /** + * Creates a new ImageGenerationTemplate instance. + * @param width The width of the template + * @param height The height of the template + */ public constructor(public readonly width?: number, public readonly height?: number) {} + /** + * Sets the steps. This will overwrite any existing steps. + * @param steps The steps to set + */ public setSteps(steps: ImageGenerationStep[]) { this.steps = steps; return this; } + /** + * Sets the gif options. + * @param options The gif options + */ public setGifOptions(options?: EncoderOptions) { this.gif = options; return this; } + /** + * Returns whether the template is a gif. + */ public isGif() { return this.gif != null; } + /** + * Adds a step to the template. + * @param step The step to add + */ public addStep(step: ImageGenerationStep) { this.steps.push(step); return this; } + /** + * Adds steps to the template. + * @param steps The steps to add + */ public addSteps(steps: ImageGenerationStep[]) { this.steps.push(...steps); return this; } + /** + * Clears the steps. + */ public clearSteps() { this.steps = []; return this; } + /** + * Returns whether the size is inferrable. + */ public isInferrable() { return [this.width, this.height].some((r) => r != null); } + /** + * Returns the width of the template. + */ public getWidth() { return this.width ?? this.height; } + /** + * Returns the height of the template. + */ public getHeight() { return this.height ?? this.width; } + /** + * Returns the JSON representation of the template. + */ public toJSON(): IImageGenerationTemplate { return { width: this.width, @@ -117,25 +272,44 @@ export class ImageGenerationTemplate implements IImageGenerationTemplate { export class ImageGen extends Encodable { private _canvas!: Canvas; + /** + * Creates a new ImageGen instance. + * @param template The template to use + */ public constructor(public template: ImageGenerationTemplate) { super(); } + /** + * Adds a step to the template. + * @param step The step to add + */ public addStep(step: ImageGenerationStep) { this.template.addStep(step); return this; } + /** + * Adds steps to the template. + * @param steps The steps to add + */ public addSteps(steps: ImageGenerationStep[]) { this.template.addSteps(steps); return this; } + /** + * Sets the gif options. + * @param options The gif options + */ public setGifOptions(options?: EncoderOptions) { this.template.setGifOptions(options); return this; } + /** + * Returns whether the template is a gif. + */ public isGif() { return this.template.isGif(); } @@ -152,6 +326,9 @@ export class ImageGen extends Encodable { return { width: img.width, height: img.height }; } + /** + * Generates a readable stream containing GIF data by applying the steps. + */ public async generateGif() { if (this.template.gif == null) throw new Error('Cannot generate gif on non-gif template'); const options = this.template.gif; @@ -185,6 +362,9 @@ export class ImageGen extends Encodable { return stream; } + /** + * Renders the image by applying the steps. + */ public async render() { const { width, height } = await this.#inferSize(); @@ -198,6 +378,9 @@ export class ImageGen extends Encodable { return this; } + /** + * Returns the canvas instance by applying the steps. + */ public getFinalCanvas(): Promise { if (!this._canvas) throw new Error('render() or generateGif() must be called before accessing the final canvas'); return Promise.resolve(this._canvas); @@ -272,3 +455,13 @@ export class ImageGen extends Encodable { } } } + +/** + * Creates a new image generator. + * @param template The template to use + */ +export function createImageGenerator(template: ImageGenerationTemplate) { + const gen = new ImageGen(template); + + return gen; +} diff --git a/src/canvas/ImageManipulator.ts b/src/canvas/ImageManipulator.ts index d57a3f7..f55e832 100644 --- a/src/canvas/ImageManipulator.ts +++ b/src/canvas/ImageManipulator.ts @@ -5,6 +5,9 @@ import { ContextManipulationStep } from './utils'; export class ImageManipulator extends CanvasImage { #steps: ContextManipulationStep[] = []; + /** + * Rotates the canvas. + */ public rotate(degrees: number) { this.#steps.push((ctx) => { ctx.rotate(degrees); @@ -13,6 +16,10 @@ export class ImageManipulator extends CanvasImage { return this; } + /** + * Flips the canvas. + * @param axis The axis to flip, `x` or `y`. + */ public flip(axis: 'x' | 'y') { this.#steps.push((ctx) => { switch (axis) { @@ -26,38 +33,73 @@ export class ImageManipulator extends CanvasImage { }); } + /** + * Scales the canvas. + * @param x The x scale + * @param y The y scale + */ public scale(x: number, y: number) { this.#steps.push((ctx) => { ctx.scale(x, y); }); } + /** + * Translates the canvas. + * @param x The x position to translate + * @param y The y position to translate + */ public translate(x: number, y: number) { this.#steps.push((ctx) => { ctx.translate(x, y); }); } + /** + * Erases a part of the canvas. + * @param x The x position to erase + * @param y The y position to erase + * @param width The width of the area to erase + * @param height The height of the area to erase + */ public erase(x: number, y: number, width: number, height: number) { this.#steps.push((ctx) => { ctx.clearRect(x, y, width, height); }); } + /** + * Applies a transform to the canvas. + */ public transform(a: number, b: number, c: number, d: number, e: number, f: number) { this.#steps.push((ctx) => { ctx.transform(a, b, c, d, e, f); }); } + /** + * Resets the transform of the canvas. + */ public resetTransform() { this.#steps.push((ctx) => { ctx.resetTransform(); }); } - public circularize(width: number, height: number) {} + /** + * Applies a circular clip to the image. + * @param width The width of the image + * @param height The height of the image + */ + public circularize(width: number, height: number) { + // TODO: Implement this + } + /** + * Processes the steps and applies them to the canvas. + * @param canvas The canvas to apply the steps to + * @param ctx The canvas context to apply the steps to + */ public async process(canvas: Canvas, ctx: SKRSContext2D) { for (const step of this.#steps) { await step(ctx); diff --git a/src/canvas/utils.ts b/src/canvas/utils.ts index cee219e..c38cab4 100644 --- a/src/canvas/utils.ts +++ b/src/canvas/utils.ts @@ -1,6 +1,14 @@ import { ImageSource, loadImage } from '../helpers'; import { loadImage as createImage, SKRSContext2D } from '@napi-rs/canvas'; +/** + * Creates a canvas image from the image source. + * @param img The image source + * @returns The canvas image + * @example ```typescript + * const image = await createCanvasImage('https://example.com/image.png'); + * ``` + */ export const createCanvasImage = async (img: ImageSource) => { const canvacordImg = await loadImage(img); const nativeImage = await createImage(canvacordImg.data); @@ -8,4 +16,8 @@ export const createCanvasImage = async (img: ImageSource) => { return nativeImage; }; +/** + * The steps to apply to the canvas. + * @param ctx The canvas context + */ export type ContextManipulationStep = (ctx: SKRSContext2D) => Awaited; diff --git a/src/components/LeaderboardBuilder.tsx b/src/components/LeaderboardBuilder.tsx index df2e970..eddbab4 100644 --- a/src/components/LeaderboardBuilder.tsx +++ b/src/components/LeaderboardBuilder.tsx @@ -10,19 +10,58 @@ const DefaultColors = { }; export interface LeaderboardProps { + /** + * The background image. + */ background: ImageSource | null; + /** + * The background color. + */ backgroundColor: string; + /** + * The header of this leaderboard ui. + */ header?: { + /** + * The title of this leaderboard ui. + */ title: string; + /** + * The subtitle of this leaderboard ui. + */ subtitle: string; + /** + * The image of this leaderboard ui. + */ image: ImageSource; }; + /** + * The players of this leaderboard ui. + */ players: { + /** + * The display name of this player. + */ displayName: string; + /** + * The username of this player. + */ username: string; + /** + * The level of this player. + */ level: number; + /** + * The xp of this player. + */ xp: number; + /** + * The rank of this player. + */ rank: number; + /** + * The avatar of this player. + */ avatar: ImageSource; }[]; } @@ -42,6 +81,9 @@ const MAX_RENDER_HEIGHT = 1080; const MIN_RENDER_HEIGHT = 1000; export class LeaderboardBuilder extends Builder { + /** + * Create a new leaderboard ui builder + */ public constructor() { super(500, MIN_RENDER_HEIGHT); @@ -56,21 +98,37 @@ export class LeaderboardBuilder extends Builder { }); } + /** + * Set background for this leaderboard ui + * @param background background image + */ public setBackground(background: ImageSource) { this.options.set('background', background); return this; } + /** + * Set background color for this leaderboard ui + * @param color background color + */ public setBackgroundColor(color: string) { this.options.set('backgroundColor', color); return this; } + /** + * Set header for this leaderboard ui + * @param data header data + */ public setHeader(data: LeaderboardProps['header'] & {}) { this.options.set('header', data); return this; } + /** + * Set players for this leaderboard ui + * @param players players data + */ public setPlayers(players: LeaderboardProps['players']) { const items = players.slice(0, 10); this.options.set('players', items); @@ -86,6 +144,9 @@ export class LeaderboardBuilder extends Builder { return this; } + /** + * Render this leaderboard ui on the canvas + */ public async render() { const options = this.options.getOptions(); @@ -128,10 +189,16 @@ export class LeaderboardBuilder extends Builder { ); } + /** + * Render players ui on the canvas + */ public renderPlayers(players: JSX.Element[]) { return
{players}
; } + /** + * Render top players ui on the canvas + */ public async renderTop({ avatar, displayName, level, rank, username, xp }: LeaderboardProps['players'][number]) { const image = await loadImage(avatar); const currentColor = DefaultColors[rank === 1 ? 'Yellow' : rank === 2 ? 'Blue' : 'Green']; @@ -171,6 +238,9 @@ export class LeaderboardBuilder extends Builder { ); } + /** + * Render player ui on the canvas + */ public async renderPlayer({ avatar, displayName, level, rank, username, xp }: LeaderboardProps['players'][number]) { const image = await loadImage(avatar); diff --git a/src/components/RankCardBuilder.tsx b/src/components/RankCardBuilder.tsx index 3ba8950..ec9e93a 100644 --- a/src/components/RankCardBuilder.tsx +++ b/src/components/RankCardBuilder.tsx @@ -1,67 +1,235 @@ -import { Font, FontFactory } from '../assets'; +import { FontFactory } from '../assets'; import { CSSPropertiesLike, ImageSource, JSX, loadImage, StyleSheet } from '../helpers'; import { fixed, getDefaultFont } from '../helpers/utils'; import { Builder } from '../templates/Builder'; +/** + * The user status type. + */ type StatusType = 'online' | 'idle' | 'dnd' | 'invisible'; +/** + * The rank card builder props. + */ interface RankCardBuilderProps { + /** + * The avatar for this rank card. + */ avatar: ImageSource | null; + /** + * The style for this rank card. + */ style: CSSPropertiesLike | null; + /** + * The fonts to be used for this rank card. + */ fonts: Partial<{ + /** + * The username font. + */ username: string; + /** + * The progress font. + */ progress: string; + /** + * The stats font. + */ stats: string; }>; + /** + * The status for this rank card. + */ status: StatusType; + /** + * The current xp for this rank card. + */ currentXP: number; + /** + * The required xp for this rank card. + */ requiredXP: number; + /** + * The username for this rank card. + */ username: string; + /** + * The display name for this rank card. + */ displayName: string; + /** + * The discriminator for this rank card. + */ discriminator: string; + /** + * The level of this rank card. + */ level: number; + /** + * The rank of this rank card. + */ rank: number; + /** + * The background for this rank card. + */ background: ImageSource; + /** + * The styles for each element of this rank card. + */ tw: { + /** + * The username style. + */ username: string; + /** + * The discriminator style. + */ discriminator: string; + /** + * The display name style. + */ displayName: string; + /** + * The level style. + */ level: string; + /** + * The rank style. + */ rank: string; + /** + * The xp style. + */ xp: string; + /** + * The progressbar style. + */ progress: { + /** + * The progressbar track style. + */ track: string; + /** + * The progressbar thumb style. + */ thumb: string; }; + /** + * The overlay style. + */ overlay: string; + /** + * The percentage style. + */ percentage: string; + /** + * The avatar style. + */ avatar: string; + /** + * The status style. + */ status: string; }; + /** + * The renderer configuration for this rank card. + */ renders: { + /** + * Whether to render the avatar. + */ avatar: boolean; + /** + * Whether to render the background. + */ background: boolean; + /** + * Whether to render the level. + */ level: boolean; + /** + * Whether to render the rank. + */ rank: boolean; + /** + * Whether to render the status. + */ status: boolean; + /** + * Whether to render the username. + */ username: boolean; + /** + * Whether to render the display name. + */ displayName: boolean; + /** + * Whether to render the discriminator. + */ discriminator: boolean; + /** + * Whether to render the progress. + */ progress: boolean; + /** + * Whether to render the xp. + */ xp: boolean; + /** + * Whether to render the progressbar. + */ progressbar: boolean; + /** + * The constants for this rank card. + */ constants: { + /** + * The rank constant. + */ rank: string; + /** + * The level constant. + */ level: string; + /** + * The xp constant. + */ xp: string; + /** + * The status colors constant. + */ statusColors: { + /** + * The light gray color. + */ LightGray: string; + /** + * The gray color. + */ Gray: string; + /** + * The dark gray color. + */ DarkGray: string; + /** + * The white color. + */ White: string; + /** + * The green color. + */ Green: string; + /** + * The yellow color. + */ Yellow: string; + /** + * The red color. + */ Red: string; + /** + * The blue color. + */ Blue: string; }; }; @@ -69,6 +237,24 @@ interface RankCardBuilderProps { } export class RankCardBuilder extends Builder { + /** + * Creates a new rank card builder. + * @example + * const card = new RankCardBuilder() + * .setUsername('kiki') + * .setDisplayName('Kiki') + * .setDiscriminator('1234') + * .setAvatar('...') + * .setCurrentXP(300) + * .setRequiredXP(600) + * .setLevel(2) + * .setRank(5) + * .setStatus('online'); + * + * const pngBuffer = await card.build({ + * format: 'png' + * }); + */ public constructor() { super(2000, 512); @@ -132,66 +318,117 @@ export class RankCardBuilder extends Builder { }); } + /** + * Sets the fonts to be used for this rank card. + * @param fontConfig The fonts to be used for this rank card. + */ public setFonts(fontConfig: Required) { this.options.set('fonts', fontConfig); return this; } + /** + * Sets the avatar for this rank card. + * @param image The avatar for this rank card. + */ public setAvatar(image: ImageSource) { this.options.set('avatar', image); return this; } + /** + * Sets the background for this rank card. + * @param image The background for this rank card. + */ public setBackground(image: ImageSource) { this.options.set('background', image); return this; } + /** + * Sets the status for this rank card. + * @param status The status for this rank card. + */ public setStatus(status: StatusType) { this.options.set('status', status); return this; } + /** + * Sets the username for this rank card. + * @param name The username for this rank card. + */ public setUsername(name: string) { this.options.set('username', name); return this; } + /** + * Sets the display name for this rank card. + * @param name The display name for this rank card. + */ public setDisplayName(name: string) { this.options.set('displayName', name); return this; } + /** + * Sets the discriminator for this rank card. + * @param discriminator The discriminator for this rank card. + */ public setDiscriminator(discriminator: string) { this.options.set('discriminator', discriminator); return this; } + /** + * Sets the current xp for this rank card. + * @param xp The current xp for this rank card. + */ public setCurrentXP(xp: number) { this.options.set('currentXP', xp); return this; } + /** + * Sets the required xp for this rank card. + * @param xp The required xp for this rank card. + */ public setRequiredXP(xp: number) { this.options.set('requiredXP', xp); return this; } + /** + * Sets the level of this rank card. + * @param level The level of this rank card. + */ public setLevel(level: number) { this.options.set('level', level); return this; } + /** + * Sets the rank of this rank card. + * @param rank The rank of this rank card. + */ public setRank(rank: number) { this.options.set('rank', rank); return this; } + /** + * Configures the renderer for this rank card. + * @param config The configuration for this rank card. + */ public configureRenderer(config: Partial) { this.options.merge('renders', config); return this; } + /** + * Renders this rank card into the canvas. + */ public async render() { const options = this.options.getOptions(); diff --git a/src/helpers/StyleSheet.ts b/src/helpers/StyleSheet.ts index bc98851..e66b869 100644 --- a/src/helpers/StyleSheet.ts +++ b/src/helpers/StyleSheet.ts @@ -1,8 +1,14 @@ import type { CSSProperties } from 'react'; import { twMerge, ClassNameValue } from 'tailwind-merge'; +/** + * The CSS properties like object. + */ export type CSSPropertiesLike = Record; +/** + * Performs object cleanup by deleting all undefined properties that could interfere with builder methods. + */ export const performObjectCleanup = (obj: Record, deep = false) => { for (const prop in obj) { if (obj[prop] === undefined) delete obj[prop]; @@ -13,6 +19,9 @@ export const performObjectCleanup = (obj: Record, deep = false) => export class StyleSheet extends null { private constructor() {} + /** + * Creates a new CSSPropertiesLike object. + */ public static create( styles: CSSPropertiesLike ): CSSPropertiesLike { @@ -23,6 +32,9 @@ export class StyleSheet extends null { return styles as O; } + /** + * Composes two CSSPropertiesLike objects. + */ public static compose(style1: CSSProperties, style2: CSSProperties) { performObjectCleanup(style1); performObjectCleanup(style2); @@ -30,6 +42,9 @@ export class StyleSheet extends null { return Object.assign(style1, style2); } + /** + * Flattens an array of CSSPropertiesLike objects. + */ public static flatten(style: CSSProperties[]) { return style.reduce((previous, current) => { performObjectCleanup(current); @@ -37,6 +52,9 @@ export class StyleSheet extends null { }, {} as CSSProperties); } + /** + * Merges multiple tailwind-like class names into appropriate class names. + */ public static cn(...classes: ClassNameValue[]) { return twMerge(...classes); } diff --git a/src/helpers/image.ts b/src/helpers/image.ts index ad27631..ae6eb84 100644 --- a/src/helpers/image.ts +++ b/src/helpers/image.ts @@ -2,15 +2,25 @@ import { renderAsync, type ResvgRenderOptions } from '@resvg/resvg-js'; import { EncodingFormat } from '../canvas/Encodable'; import { AvifConfig, PngEncodeOptions, Transformer } from '@napi-rs/image'; +/** + * The options for rendering the svg. + */ export type RenderSvgOptions = PngEncodeOptions | AvifConfig | number | null; +/** + * Renders the svg to the specified format. + * @param svg The svg `string` or `Buffer` to render + * @param format The format to render to + * @param [options] The options for rendering + * @param [signal] The abort signal + */ export async function renderSvg({ svg, format, options, signal }: { - svg: string; + svg: string | Buffer; format: EncodingFormat; options?: RenderSvgOptions; signal?: AbortSignal | null; diff --git a/src/helpers/jsx.ts b/src/helpers/jsx.ts index ff7da80..eae2f84 100644 --- a/src/helpers/jsx.ts +++ b/src/helpers/jsx.ts @@ -6,6 +6,9 @@ const isNode = (node: unknown): node is Node => { return typeof node === 'object' && node != null && 'toElement' in node; }; +/** + * The element initialization options. + */ export type ElementInit = { type: string; props: Record; @@ -13,12 +16,31 @@ export type ElementInit = { children?: any; }; +/** + * The JSX element. + */ export class Element { + /** + * The type of the element. + */ public type: string; + /** + * The props of the element. + */ public props: Record; + /** + * The key of the element. + */ public key: React.Key | null; + /** + * The children of the element. + */ public children?: any; + /** + * Creates a new JSX element. + * @param _init The initialization options + */ public constructor(_init: ElementInit) { this.type = _init.type; this.props = _init.props; @@ -32,8 +54,21 @@ const cleanStyles = >(properties: T): T => { return properties; }; +/** + * The JSX factory for canvacord jsx. + */ export const JSX = { + /** + * The JSX element instance. + */ Element, + /** + * Creates a new JSX element. + * @param type The type of the element + * @param props The props of the element + * @param children The children of the element + * @returns The created element + */ createElement(type: string | Element, props: Record, ...children: Element[]): Element { if (type instanceof Element) return type; @@ -57,11 +92,18 @@ export const JSX = { children }); }, + /** + * Creates a new JSX fragment. + * @param children The children of the fragment + */ Fragment({ children }: { children: Element[] | string }): Element { return new Element({ type: 'Fragment', props: { children }, children }); } }; +/** + * Renders the components. + */ export function render(components: (Node | Element | unknown)[]) { return components .map((component) => { @@ -77,6 +119,9 @@ export function render(components: (Node | Element | unknown)[]) { declare module 'react' { interface DOMAttributes { + /** + * A subset of TailwindCSS classes to apply to the element. + */ tw?: string; } } @@ -84,6 +129,9 @@ declare module 'react' { declare global { namespace JSX { interface IntrinsicAttributes { + /** + * A subset of TailwindCSS classes to apply to the element. + */ tw?: string; } } diff --git a/src/helpers/loadImage.ts b/src/helpers/loadImage.ts index fcecb3e..bcd250c 100644 --- a/src/helpers/loadImage.ts +++ b/src/helpers/loadImage.ts @@ -13,6 +13,9 @@ const MAX_REDIRECTS = 20, REDIRECT_STATUSES = new Set([301, 302]), DATA_URI = /^\s*data:/; +/** + * The supported image sources. It can be a buffer, a readable stream, a string, a URL instance or an Image instance. + */ export type ImageSource = | CanvacordImage | Buffer @@ -27,12 +30,29 @@ export type ImageSource = | URL | Image; +/** + * The options for loading an image. + */ export interface LoadImageOptions { + /** + * The headers to use when downloading the image. + */ headers?: Record; + /** + * The maximum number of redirects to follow. + */ maxRedirects?: number; + /** + * Other request options to use when downloading the image. + */ requestOptions?: import('http').RequestOptions; } +/** + * Loads an image from the specified source. + * @param source The image source + * @param [options] The options for loading the image + */ export async function loadImage(source: ImageSource, options: LoadImageOptions = {}) { // load canvacord image if (source instanceof CanvacordImage) return source; diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index 38d1026..6e1eeb2 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -1,10 +1,16 @@ import { Font, FontFactory } from '../assets'; +/** + * Abbreviates the given number. + */ export const fixed = (v: number) => { const formatter = new Intl.NumberFormat('en-US', { notation: 'compact' }); return formatter.format(v); }; +/** + * Resolves the first registered font instance if available. + */ export const getDefaultFont = () => { return (FontFactory.values().next().value ?? null) as Font | null; }; diff --git a/src/templates/Builder.tsx b/src/templates/Builder.tsx index a4e8efa..371ac57 100644 --- a/src/templates/Builder.tsx +++ b/src/templates/Builder.tsx @@ -6,39 +6,95 @@ import { renderSvg, RenderSvgOptions } from '../helpers/image'; import { JSX, Element } from '../helpers/jsx'; import { BuilderOptionsManager } from './BuilderOptionsManager'; +/** + * The builder template. + */ export interface BuilderTemplate { + /** + * The components of this template. + */ components: Array; + /** + * The width of this template. + */ width: number; + /** + * The height of this template. + */ height: number; + /** + * The style of this template. + */ style?: CSSProperties; } +/** + * The build output format. + */ export type BuildFormat = 'svg' | 'png' | 'avif' | 'jpeg' | 'webp'; +/** + * The builder build options. + */ export type BuilderBuildOptions = { + /** + * The output format. + */ format?: BuildFormat; + /** + * The options for this build. + */ options?: RenderSvgOptions; + /** + * The abort signal. + */ signal?: AbortSignal; } & SatoriOptions; +/** + * The builder node. + */ export interface Node { + /** + * Convert this node to element. + */ toElement(): Element; } export class Builder = Record> { #style: CSSPropertiesLike = {}; + /** + * The tailwind subset to apply to this builder. + */ public tw: string = ''; + /** + * The components of this builder. + */ public components = new Array(); + /** + * The options manager of this builder. + */ public options = new BuilderOptionsManager(); + /** + * Create a new builder. + * @param width the width of this builder. + * @param height the height of this builder. + */ public constructor(public width: number, public height: number) { this.adjustCanvas(); } + /** + * Bootstrap this builder with data. + */ public bootstrap(data: T) { this.options.setOptions(data); } + /** + * Adjust the canvas size. + */ public adjustCanvas() { this.#style = StyleSheet.create({ root: { @@ -49,14 +105,24 @@ export class Builder = Record> { return this; } + /** + * Get the style of this builder. + */ public get style() { return this.#style.root; } + /** + * Set the style of this builder. + */ public set style(newStyle: CSSProperties) { StyleSheet.compose(this.#style.root, newStyle); } + /** + * Add component to this builder. + * @param component the component to add. + */ public addComponent(component: T | T[]) { if (component instanceof Element && (component.type as unknown as Function) === JSX.Fragment) component = component.children; @@ -65,6 +131,10 @@ export class Builder = Record> { return this; } + /** + * Set the style of this builder. + * @param newStyle the new style. + */ public setStyle(newStyle: CSSProperties) { StyleSheet.compose(this.#style.root || {}, newStyle || {}); return this; @@ -81,10 +151,18 @@ export class Builder = Record> { .flat(1); } + /** + * Render this builder. + */ public async render(): Promise { return
{this._render()}
; } + /** + * Convert this builder into an image. + * @param options the build options. + * @returns the image buffer or svg string. + */ public async build(options: Partial = {}) { options.format ??= 'png'; @@ -109,6 +187,9 @@ export class Builder = Record> { }); } + /** + * Create a builder from builder template. + */ public static from(template: BuilderTemplate) { const builder = new this(template.width, template.height); diff --git a/src/templates/BuilderOptionsManager.ts b/src/templates/BuilderOptionsManager.ts index 920735d..bc982c4 100644 --- a/src/templates/BuilderOptionsManager.ts +++ b/src/templates/BuilderOptionsManager.ts @@ -1,22 +1,46 @@ export class BuilderOptionsManager> { + /** + * Creates a new builder options manager. + * @param options The options to use + */ constructor(private options: T = {} as T) {} + /** + * Returns the options. + */ public getOptions(): T { return this.options; } + /** + * Sets the options. This will override the previous options. + * @param options The options to use + */ public setOptions(options: T): void { this.options = options; } + /** + * Get an option by name. + * @param key The option name + * @returns The option value + */ public get(key: K): T[K] { return this.options[key]; } + /** + * Set an option by name. + * @param key The option name + * @param value The option value + */ public set(key: K, value: T[K]): void { this.options[key] = value; } + /** + * Merge new data to old data on an option by name. + */ public merge(key: K, value: Partial): void { this.options[key] = { ...this.options[key], ...value }; } diff --git a/test/demo.tsx b/test/demo.tsx new file mode 100644 index 0000000..c235d6d --- /dev/null +++ b/test/demo.tsx @@ -0,0 +1,42 @@ +import { JSX, Builder, Font } from '../dist'; +import { writeFileSync } from 'fs'; + +interface Props { + text: string; +} + +class Design extends Builder { + constructor() { + // set width and height + super(500, 500); + // initialize props + this.bootstrap({ text: '' }); + } + + setText(text: string) { + this.options.set('text', text); + return this; + } + + // this is where you have to define output ui + async render() { + return ( +
+

{this.options.get('text')}

+
+ ); + } +} + +// usage + +// load font +Font.loadDefault(); + +// create design +const design = new Design().setText('Hello World'); +const image = design.build({ format: 'png' }); + +image.then((i) => { + writeFileSync('test.png', i); +}); From 9c4e62e1ca8dc16bb1a28234a017794789af260c Mon Sep 17 00:00:00 2001 From: skdhg <46562212+skdhg@users.noreply.github.com> Date: Wed, 22 Nov 2023 01:55:02 +0545 Subject: [PATCH 2/7] chore: add jsdoc comments --- src/canvas/Canvacord.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/canvas/Canvacord.ts b/src/canvas/Canvacord.ts index a0c9de4..3b66dd4 100644 --- a/src/canvas/Canvacord.ts +++ b/src/canvas/Canvacord.ts @@ -112,4 +112,10 @@ Object.assign(CanvacordConstructor, factory); export type Canvacord = CanvacordFactory & typeof CanvacordConstructor; +/** + * Creates a new Canvacord image processor. + * @param source The image source to use + * @param options The options to use + * @returns The image processor + */ export const canvacord = CanvacordConstructor as Canvacord; From 6ddaa5fdd21b9c789f56ae845924123f232da914 Mon Sep 17 00:00:00 2001 From: skdhg <46562212+skdhg@users.noreply.github.com> Date: Wed, 22 Nov 2023 02:01:02 +0545 Subject: [PATCH 3/7] chore: update readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ebdef07..58edccd 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ Easily generate images on-the-fly with node.js using wide range of templates. - image templates _(wip)_ - image filters _(wip)_ - complex layouts _(wip)_ +- templates api _(wip)_ +- builder api _(wip)_ ## Example From c68e53d74689164f387826702f20aaaec605acf9 Mon Sep 17 00:00:00 2001 From: skdhg <46562212+skdhg@users.noreply.github.com> Date: Wed, 22 Nov 2023 02:17:03 +0545 Subject: [PATCH 4/7] chore: proper readme documentation --- README.md | 133 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 58edccd..4b424dc 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ Easily generate images on-the-fly with node.js using wide range of templates. ### Image Generation +#### Using built-in templates (New "Legacy api") + ```ts import { canvacord } from 'canvacord'; @@ -31,6 +33,10 @@ import fs from 'node:fs'; const triggered = await canvacord.triggered(image); triggered.pipe(fs.createWriteStream('triggered.gif')); +// image generation +const beautiful = await canvacord.beautiful(img); +const facepalm = await canvacord.facepalm(img); + // filters const filtered = await canvacord .filters(512, 512) @@ -54,6 +60,131 @@ const filtered = await canvacord(image, 512, 512) fs.writeFileSync('filtered.png', filtered); ``` -## XP Card Preview +## XP Card + +```ts +import { Font, RankCardBuilder } from 'canvacord'; +import { writeFile } from 'fs/promises'; + +// load default font +Font.loadDefault(); + +const card = new RankCardBuilder() + .setUsername('Lost Ctrl') + .setDisplayName('thearchaeopteryx') + .setAvatar('...') + .setCurrentXP(3800) + .setRequiredXP(2500) + .setLevel(54) + .setRank(32) + .setStatus('online'); + +const image = await card.build({ + format: 'png' +}); + +await writeFileSync('./card.png', data); +``` ![xp-card](https://raw.githubusercontent.com/neplextech/canvacord/main/test/jsx/test2.svg) + +## Creating images using custom template + +```ts +import { createTemplate, ImageFactory, TemplateImage, createImageGenerator } from 'canvacord'; + +const AffectedMeme = createTemplate((image: ImageSource) => { + return { + steps: [ + { + image: [ + { + source: new TemplateImage(ImageFactory.AFFECT), + x: 0, + y: 0 + } + ] + }, + { + image: [ + { + source: new TemplateImage(image), + x: 180, + y: 383, + width: 200, + height: 157 + } + ] + } + ] + }; +}); + +// get target photo to use on "affected" meme image +const photo = await getPhotoForMemeSomehow(); +const generator = createImageGenerator(AffectedMeme(photo)); + +// render out the image +await generator.render(); + +// get the resulting image in png format +const affectedMeme = await generator.encode('png'); +``` + +#### Result + +![output](https://raw.githubusercontent.com/neplextech/canvacord/main/test/canvas/affected.png) + +## Creating images using custom builder + +```tsx +// JSX import is required if you want to use JSX syntax +// Builder is a base class to create your own builders +// Font is a utility class to load fonts +import { JSX, Builder, Font } from 'canvacord'; +import { writeFile } from 'fs/promises'; + +// declare props types +interface Props { + text: string; +} + +class Design extends Builder { + constructor() { + // set width and height + super(500, 500); + // initialize props + this.bootstrap({ text: '' }); + } + + // define custom methods for your builder + setText(text: string) { + this.options.set('text', text); + return this; + } + + // this is where you have to define how the resulting image should look like + async render() { + return ( +
+

{this.options.get('text')}

+
+ ); + } +} + +// usage +// load font +Font.loadDefault(); + +// create design +const design = new Design().setText('Hello World'); +const image = await design.build({ format: 'png' }); + +// do something with generated image +await writeFile('./test.png', image); +``` + +#### Result + +![output](https://github.com/neplextech/canvacord/assets/46562212/c50d09d6-33c4-4b44-81c2-aed6783f503c) From 28b4633338bdf87846b331855c536a8721b81d57 Mon Sep 17 00:00:00 2001 From: skdhg <46562212+skdhg@users.noreply.github.com> Date: Wed, 22 Nov 2023 02:26:35 +0545 Subject: [PATCH 5/7] chore: proper readme documentation --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 4b424dc..211fbbe 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,29 @@ const affectedMeme = await generator.encode('png'); ## Creating images using custom builder +This is an advanced method of creating images. Canvacord builder api allows you to create your own image generator using JSX elements and a subset of tailwind class names. This is also possible without JSX, you can find an example [here](https://github.com/neplextech/canvacord/blob/7651c1aa51a844c2591cbe68a6e21eb9d1d6287a/benchmark/jsx-renderer.mjs). + +If you want to use JSX with typescript, you need to add the following options to your `tsconfig.json`: + +```jsonc +{ + "compilerOptions": { + // other options + "jsx": "react", + "jsxFactory": "JSX.createElement", + "jsxFragmentFactory": "JSX.Fragment" + } + // other options +} +``` + +You can also use pragma comments to define JSX factory and fragment factory: + +```js +/** @jsx JSX */ +/** @jsxFrag JSX */ +``` + ```tsx // JSX import is required if you want to use JSX syntax // Builder is a base class to create your own builders From 87a975c0cdb39f9ad1c1c91f8c087d0f633f386e Mon Sep 17 00:00:00 2001 From: skdhg <46562212+skdhg@users.noreply.github.com> Date: Wed, 22 Nov 2023 02:29:59 +0545 Subject: [PATCH 6/7] chore: proper readme documentation --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 211fbbe..467494e 100644 --- a/README.md +++ b/README.md @@ -156,8 +156,8 @@ If you want to use JSX with typescript, you need to add the following options to You can also use pragma comments to define JSX factory and fragment factory: ```js -/** @jsx JSX */ -/** @jsxFrag JSX */ +/** @jsx JSX.createElement */ +/** @jsxFrag JSX.Fragment */ ``` ```tsx From da0e5bd718215607f55b05ecf50a3bed3898643f Mon Sep 17 00:00:00 2001 From: skdhg <46562212+skdhg@users.noreply.github.com> Date: Wed, 22 Nov 2023 02:33:37 +0545 Subject: [PATCH 7/7] chore: add note on supported features --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 467494e..2c9895c 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,10 @@ const affectedMeme = await generator.encode('png'); ## Creating images using custom builder -This is an advanced method of creating images. Canvacord builder api allows you to create your own image generator using JSX elements and a subset of tailwind class names. This is also possible without JSX, you can find an example [here](https://github.com/neplextech/canvacord/blob/7651c1aa51a844c2591cbe68a6e21eb9d1d6287a/benchmark/jsx-renderer.mjs). +This is an advanced method of creating images. Canvacord builder api allows you to create your own image generator using JSX elements and a **subset of tailwind class names**. This is also possible without JSX, you can find an example [here](https://github.com/neplextech/canvacord/blob/7651c1aa51a844c2591cbe68a6e21eb9d1d6287a/benchmark/jsx-renderer.mjs). + +> **Note** +> It does not support many css features such as grid layout. You can use flexbox instead. If you want to use JSX with typescript, you need to add the following options to your `tsconfig.json`: