From 31fef0b916518be573ef39f335ecfb2949c8fb9c Mon Sep 17 00:00:00 2001 From: twlite <46562212+twlite@users.noreply.github.com> Date: Fri, 5 Jan 2024 21:43:39 +0545 Subject: [PATCH] fix: leaderboard height adjustment issue --- packages/canvacord/src/assets/Font.ts | 2 +- .../canvacord/src/assets/TemplateFactory.ts | 7 +- packages/canvacord/src/canvas/Canvacord.ts | 3 +- packages/canvacord/src/canvas/CanvasImage.ts | 4 + .../canvacord/src/canvas/ImageFilterer.ts | 1 + packages/canvacord/src/canvas/ImageGen.ts | 10 +- packages/canvacord/src/canvas/utils.ts | 1 - .../src/components/LeaderboardBuilder.tsx | 113 ++++++++++++++---- .../src/components/RankCardBuilder.tsx | 1 + .../components/rank-card/NeoClassicalCard.tsx | 7 +- packages/canvacord/src/helpers/StyleSheet.ts | 6 +- packages/canvacord/src/helpers/jsx.ts | 23 ++-- packages/canvacord/src/helpers/loadImage.ts | 69 ++++++----- packages/canvacord/src/templates/Builder.tsx | 29 +++-- .../src/templates/BuilderOptionsManager.ts | 1 + 15 files changed, 193 insertions(+), 84 deletions(-) diff --git a/packages/canvacord/src/assets/Font.ts b/packages/canvacord/src/assets/Font.ts index 122d220..0132ba9 100644 --- a/packages/canvacord/src/assets/Font.ts +++ b/packages/canvacord/src/assets/Font.ts @@ -101,6 +101,6 @@ export class Font { */ public static loadDefault() { - return this.fromBuffer(Fonts.Geist, "geist"); + return Font.fromBuffer(Fonts.Geist, "geist"); } } diff --git a/packages/canvacord/src/assets/TemplateFactory.ts b/packages/canvacord/src/assets/TemplateFactory.ts index f10fb5a..8dd1147 100644 --- a/packages/canvacord/src/assets/TemplateFactory.ts +++ b/packages/canvacord/src/assets/TemplateFactory.ts @@ -28,6 +28,7 @@ export class TemplateImage { */ public async resolve(): Promise { if (this.#resolved) return this.#resolved; + // biome-ignore lint: assignment should not be an expression return (this.#resolved = await createCanvasImage(this.source)); } } @@ -36,7 +37,11 @@ export class TemplateImage { * Creates a new template from the provided template. * @param template The template to create from */ -export const createTemplate = any, P extends Parameters>( +export const createTemplate = < + // biome-ignore lint: any is tolerable here + F extends (...args: any[]) => any, + P extends Parameters, +>( cb: (...args: P) => IImageGenerationTemplate, ) => { return (...args: Parameters) => { diff --git a/packages/canvacord/src/canvas/Canvacord.ts b/packages/canvacord/src/canvas/Canvacord.ts index 5f98fca..2079301 100644 --- a/packages/canvacord/src/canvas/Canvacord.ts +++ b/packages/canvacord/src/canvas/Canvacord.ts @@ -70,7 +70,7 @@ for (const key in TemplateFactory) { const method = key.toLowerCase() as Lowercase; if (method === "triggered") continue; - factory[method] = async function (...args: Parameters) { + factory[method] = async (...args: Parameters) => { // @ts-expect-error const template = TemplateFactory[capitalize(method)](...args); const generator = new ImageGen(template); @@ -100,6 +100,7 @@ export interface CanvacordInit { * @returns The image processor */ function CanvacordConstructor(source: ImageSource, options?: CanvacordInit) { + // biome-ignore lint: reassignment is tolerable here options ??= {}; const img = new CanvasImage(source, options?.width ?? -1, options?.height ?? -1); diff --git a/packages/canvacord/src/canvas/CanvasImage.ts b/packages/canvacord/src/canvas/CanvasImage.ts index 8a7633e..d793936 100644 --- a/packages/canvacord/src/canvas/CanvasImage.ts +++ b/packages/canvacord/src/canvas/CanvasImage.ts @@ -35,7 +35,9 @@ export class CanvasImage extends ImageFilterer { this.steps.push(async (ctx) => { const img = this.#img || this.#setImg(await createCanvasImage(this.source)); + // biome-ignore lint: reassignment width ??= this.width; + // biome-ignore lint: reassignment height ??= this.height; ctx.drawImage(img, x, y, width, height); @@ -51,7 +53,9 @@ export class CanvasImage extends ImageFilterer { */ public circle(width?: number, height?: number) { this.steps.push((ctx) => { + // biome-ignore lint: reassignment width ??= ctx.canvas.width; + // biome-ignore lint: reassignment height ??= ctx.canvas.height; ctx.globalCompositeOperation = "destination-in"; diff --git a/packages/canvacord/src/canvas/ImageFilterer.ts b/packages/canvacord/src/canvas/ImageFilterer.ts index 8ac67ba..44758f0 100644 --- a/packages/canvacord/src/canvas/ImageFilterer.ts +++ b/packages/canvacord/src/canvas/ImageFilterer.ts @@ -136,6 +136,7 @@ export class ImageFilterer extends CanvasHelper { if (this.#filters.length) ctx.filter = this.#filters.join(" "); while (this.steps.length > 0) { + // biome-ignore lint: non-null assertion await this.steps.shift()!(ctx); } } diff --git a/packages/canvacord/src/canvas/ImageGen.ts b/packages/canvacord/src/canvas/ImageGen.ts index ea2f76c..1ef8ab7 100644 --- a/packages/canvacord/src/canvas/ImageGen.ts +++ b/packages/canvacord/src/canvas/ImageGen.ts @@ -315,7 +315,13 @@ export class ImageGen extends Encodable { } async #inferSize() { - if (this.template.isInferrable()) return { width: this.template.getWidth()!, height: this.template.getHeight()! }; + if (this.template.isInferrable()) + return { + // biome-ignore lint: non-null assertion + width: this.template.getWidth()!, + // biome-ignore lint: non-null assertion + height: this.template.getHeight()!, + }; if (!this.template.steps.length) throw new Error("Cannot infer size from empty template"); const firstImg = this.template.steps.find((s) => s.image?.length)?.image?.[0]; @@ -344,6 +350,7 @@ export class ImageGen extends Encodable { if (options.framerate != null) encoder.setFramerate(options.framerate); if (options.transparent != null) encoder.setTransparent(options.transparent); + // biome-ignore lint: assignment should not be an expression const canvas = (this._canvas = createCanvas(width, height)); const ctx = canvas.getContext("2d"); @@ -368,6 +375,7 @@ export class ImageGen extends Encodable { public async render() { const { width, height } = await this.#inferSize(); + // biome-ignore lint: assignment should not be an expression const canvas = (this._canvas = createCanvas(width, height)); const ctx = canvas.getContext("2d"); diff --git a/packages/canvacord/src/canvas/utils.ts b/packages/canvacord/src/canvas/utils.ts index 0f29305..f3c89ad 100644 --- a/packages/canvacord/src/canvas/utils.ts +++ b/packages/canvacord/src/canvas/utils.ts @@ -7,7 +7,6 @@ import { loadImage as createImage, SKRSContext2D } from "@napi-rs/canvas"; * @returns The canvas image * * const image = await createCanvasImage('https://example.com/image.png'); - */ export const createCanvasImage = async (img: ImageSource) => { const canvacordImg = await loadImage(img); diff --git a/packages/canvacord/src/components/LeaderboardBuilder.tsx b/packages/canvacord/src/components/LeaderboardBuilder.tsx index 21849e0..6fa8b0f 100644 --- a/packages/canvacord/src/components/LeaderboardBuilder.tsx +++ b/packages/canvacord/src/components/LeaderboardBuilder.tsx @@ -80,7 +80,14 @@ export interface LeaderboardProps { const Crown = () => { return ( - + // biome-ignore lint: alternative text title + { }; const MIN_RENDER_HEIGHT = 420; +const HEIGHT_INTERVAL = [394, 498, 594, 690, 786, 882, 978, 1074] as const; export class LeaderboardBuilder extends Builder { /** @@ -173,14 +181,13 @@ export class LeaderboardBuilder extends Builder { throw new RangeError("Number of players must be greater than 0"); } - const minh = options.header ? MIN_RENDER_HEIGHT - 130 : MIN_RENDER_HEIGHT; - const calculatedHeight = minh + (total - 3) * 90; - const diff = total >= 7 ? total - 7 : 0; - const incremented = 10 * diff; - this.height = Math.max(calculatedHeight + incremented, minh); + const adjustedHeight = HEIGHT_INTERVAL[total - 3] ?? MIN_RENDER_HEIGHT; + + this.height = adjustedHeight; this.adjustCanvas(); + // biome-ignore lint: declare variables separately let background, headerImg; if (options.background) { @@ -191,29 +198,51 @@ export class LeaderboardBuilder extends Builder { headerImg = await loadImage(options.header.image); } - const winners = [options.players[1], options.players[0], options.players[2]].filter(Boolean); + const winners = [ + options.players[1], + options.players[0], + options.players[2], + ].filter(Boolean); return (
- {background && } + {background && ( + background + )}
{options.header && headerImg ? (
- -

{options.header.title}

-

{options.header.subtitle}

+ header +

+ {options.header.title} +

+

+ {options.header.subtitle} +

) : null}
{await Promise.all(winners.map((winner) => this.renderTop(winner)))}
{this.renderPlayers( - await Promise.all(options.players.filter((f) => !winners.includes(f)).map((m) => this.renderPlayer(m))), + await Promise.all( + options.players + .filter((f) => !winners.includes(f)) + .map((m) => this.renderPlayer(m)) + ) )}
@@ -224,15 +253,27 @@ export class LeaderboardBuilder extends Builder { * Render players ui on the canvas */ public renderPlayers(players: JSX.Element[]) { - return
{players}
; + return ( +
+ {players} +
+ ); } /** * Render top players ui on the canvas */ - public async renderTop({ avatar, displayName, level, rank, username, xp }: LeaderboardProps["players"][number]) { + 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"]; + const currentColor = + DefaultColors[rank === 1 ? "Yellow" : rank === 2 ? "Blue" : "Green"]; const crown = rank === 1; return ( @@ -240,7 +281,7 @@ export class LeaderboardBuilder extends Builder { className={StyleSheet.cn( "relative flex flex-col items-center justify-center p-4 bg-[#1E2237CC] w-[35%] rounded-md", crown ? "-mt-4 bg-[#252A40CC] rounded-b-none h-[113%]" : "", - rank === 2 ? "rounded-br-none" : rank === 3 ? "rounded-bl-none" : "", + rank === 2 ? "rounded-br-none" : rank === 3 ? "rounded-bl-none" : "" )} > {crown && ( @@ -251,7 +292,10 @@ export class LeaderboardBuilder extends Builder {
avatar
{
-

{displayName}

+

+ {displayName} +

@{username}

{this.options.get("text").level} {level}

- {fixed(xp, this.options.get("abbreviate"))} {this.options.get("text").xp} + {fixed(xp, this.options.get("abbreviate"))}{" "} + {this.options.get("text").xp}

@@ -276,7 +323,14 @@ export class LeaderboardBuilder extends Builder { /** * Render player ui on the canvas */ - public async renderPlayer({ avatar, displayName, level, rank, username, xp }: LeaderboardProps["players"][number]) { + public async renderPlayer({ + avatar, + displayName, + level, + rank, + username, + xp, + }: LeaderboardProps["players"][number]) { const image = await loadImage(avatar); return ( @@ -284,11 +338,19 @@ export class LeaderboardBuilder extends Builder {

{rank}

-

{this.options.get("text").rank}

+

+ {this.options.get("text").rank} +

- + avatar
-

{displayName}

+

+ {displayName} +

@{username}

@@ -297,7 +359,8 @@ export class LeaderboardBuilder extends Builder { {this.options.get("text").level} {level}

- {fixed(xp, this.options.get("abbreviate"))} {this.options.get("text").xp} + {fixed(xp, this.options.get("abbreviate"))}{" "} + {this.options.get("text").xp}

diff --git a/packages/canvacord/src/components/RankCardBuilder.tsx b/packages/canvacord/src/components/RankCardBuilder.tsx index 880734b..53384ef 100644 --- a/packages/canvacord/src/components/RankCardBuilder.tsx +++ b/packages/canvacord/src/components/RankCardBuilder.tsx @@ -274,6 +274,7 @@ export class RankCardBuilder extends Builder { {...{ ...options, avatar: avatar.toDataURL(), + // biome-ignore lint: forbidden non-null assertion backgroundColor: background!, }} /> diff --git a/packages/canvacord/src/components/rank-card/NeoClassicalCard.tsx b/packages/canvacord/src/components/rank-card/NeoClassicalCard.tsx index 3906da8..b4770a6 100644 --- a/packages/canvacord/src/components/rank-card/NeoClassicalCard.tsx +++ b/packages/canvacord/src/components/rank-card/NeoClassicalCard.tsx @@ -100,7 +100,7 @@ export function NeoClassicalCard(props: RankCardProps) { const shouldSkipStats = currentXP == null && requiredXP == null; const progress = calculateProgress(currentXP ?? 0, requiredXP ?? 0); - const progressWidth = typeof progress !== "number" || isNaN(progress) ? 0 : clamp(progress); + const progressWidth = typeof progress !== "number" || Number.isNaN(progress) ? 0 : clamp(progress); return (
avatar = Rec /** * Performs object cleanup by deleting all undefined properties that could interfere with builder methods. */ -export const performObjectCleanup = (obj: Record, deep = false) => { +export const performObjectCleanup = ( + // biome-ignore lint: any is tolerated here + obj: Record, + deep = false, +) => { for (const prop in obj) { if (obj[prop] === undefined) delete obj[prop]; if (typeof obj[prop] === "object" && deep) performObjectCleanup(obj[prop], deep); diff --git a/packages/canvacord/src/helpers/jsx.ts b/packages/canvacord/src/helpers/jsx.ts index 07ab3c4..4359de8 100644 --- a/packages/canvacord/src/helpers/jsx.ts +++ b/packages/canvacord/src/helpers/jsx.ts @@ -13,6 +13,7 @@ export type ElementInit = { type: string; props: Record; key?: React.Key | null; + // biome-ignore lint: any is tolerated here children?: any; }; @@ -35,6 +36,7 @@ export class Element { /** * The children of the element. */ + // biome-ignore lint: any is tolerated here public children?: any; /** @@ -72,8 +74,10 @@ export const JSX = { createElement(type: string | Element, props: Record, ...children: Element[]): Element { if (type instanceof Element) return type; + // biome-ignore lint: reassigning function parameter props ??= {}; + // biome-ignore lint: delete is necessary here if (isObjectEmpty(props.style)) delete props.style; // monkey-patch layout issues @@ -107,18 +111,17 @@ export const JSX = { * Renders the components. */ export function render(components: (Node | Element | unknown)[]) { - return components - .map((component) => { - if (component == null) return []; - if (component instanceof Element) return component; - if (isNode(component)) return component.toElement(); + return components.flatMap((component) => { + if (component == null) return []; + if (component instanceof Element) return component; + if (isNode(component)) return component.toElement(); - const child = String(component) as unknown as Element; - return JSX.createElement("span", { children: child }, child); - }) - .flat(1); + const child = String(component) as unknown as Element; + return JSX.createElement("span", { children: child }, child); + }); } +// biome-ignore lint: any is tolerated here function isObjectEmpty(obj: any) { - return obj ? Object.keys(obj).length === 0 : false; + return typeof obj === "object" && obj != null ? Object.keys(obj).length === 0 : false; } diff --git a/packages/canvacord/src/helpers/loadImage.ts b/packages/canvacord/src/helpers/loadImage.ts index 260ed93..18bbcba 100644 --- a/packages/canvacord/src/helpers/loadImage.ts +++ b/packages/canvacord/src/helpers/loadImage.ts @@ -8,8 +8,10 @@ import { Image } from "@napi-rs/canvas"; import { buffer } from "stream/consumers"; import { Transformer } from "@napi-rs/image"; +// biome-ignore lint: declare variables separately let http: typeof import("http"), https: typeof import("https"); +// biome-ignore lint: declare variables separately const MAX_REDIRECTS = 20, REDIRECT_STATUSES = new Set([301, 302]), DATA_URI = /^\s*data:/; @@ -80,31 +82,31 @@ export async function loadImage(source: ImageSource, options: LoadImageOptions = // if the source exists as a file, construct image from that file if (await exists(source)) { return createImage(await fs.promises.readFile(source)); - } else { - if (typeof fetch !== "undefined") { - return fetch(source, { - redirect: "follow", - // @ts-expect-error - headers: options.requestOptions?.headers, - }).then(async (res) => { - if (!res.ok) throw new Error(`remote source rejected with status code ${res.status}`); - return await createImage(Buffer.from(await res.arrayBuffer())); - }); - } - // the source is a remote url here - source = source instanceof URL ? source : new URL(source); - // attempt to download the remote source and construct image - const data = await new Promise((resolve, reject) => - makeRequest( - source as URL, - resolve, - reject, - typeof options.maxRedirects === "number" && options.maxRedirects >= 0 ? options.maxRedirects : MAX_REDIRECTS, - options.requestOptions || {}, - ), - ); - return createImage(data); } + if (typeof fetch !== "undefined") { + return fetch(source, { + redirect: "follow", + // @ts-expect-error + headers: options.requestOptions?.headers, + }).then(async (res) => { + if (!res.ok) throw new Error(`remote source rejected with status code ${res.status}`); + return await createImage(Buffer.from(await res.arrayBuffer())); + }); + } + // the source is a remote url here + // biome-ignore lint: any is tolerated here + source = source instanceof URL ? source : new URL(source); + // attempt to download the remote source and construct image + const data = await new Promise((resolve, reject) => + makeRequest( + source as URL, + resolve, + reject, + typeof options.maxRedirects === "number" && options.maxRedirects >= 0 ? options.maxRedirects : MAX_REDIRECTS, + options.requestOptions || {}, + ), + ); + return createImage(data); } // throw error as don't support that source @@ -122,17 +124,28 @@ function makeRequest( // lazy load the lib const lib: typeof import("http") = isHttps ? !https - ? (https = require("https")) + ? // biome-ignore lint: assignment should not be an expression + (https = require("https")) : https : !http - ? (http = require("http")) + ? // biome-ignore lint: assignment should not be an expression + (http = require("http")) : http; lib .get(url.toString(), requestOptions || {}, (res) => { - const shouldRedirect = REDIRECT_STATUSES.has(res.statusCode!) && typeof res.headers.location === "string"; + const shouldRedirect = + // biome-ignore lint: forbidden non-null-assertion + REDIRECT_STATUSES.has(res.statusCode!) && typeof res.headers.location === "string"; if (shouldRedirect && redirectCount > 0) - return makeRequest(new URL(res.headers.location!), resolve, reject, redirectCount - 1, requestOptions); + return makeRequest( + // biome-ignore lint: forbidden non-null-assertion + new URL(res.headers.location!), + resolve, + reject, + redirectCount - 1, + requestOptions, + ); if (typeof res.statusCode === "number" && (res.statusCode < 200 || res.statusCode >= 300)) { return reject(new Error(`remote source rejected with status code ${res.statusCode}`)); } diff --git a/packages/canvacord/src/templates/Builder.tsx b/packages/canvacord/src/templates/Builder.tsx index 805ddc6..e622530 100644 --- a/packages/canvacord/src/templates/Builder.tsx +++ b/packages/canvacord/src/templates/Builder.tsx @@ -15,7 +15,7 @@ const isEmoji = (str: string) => { function emojiToUnicode(emoji: string): string { if (emoji.length === 1) return emoji.charCodeAt(0).toString(16); - let comp = (emoji.charCodeAt(0) - 0xd800) * 0x400 + (emoji.charCodeAt(1) - 0xdc00) + 0x10000; + const comp = (emoji.charCodeAt(0) - 0xd800) * 0x400 + (emoji.charCodeAt(1) - 0xdc00) + 0x10000; if (comp < 0) return emoji.charCodeAt(0).toString(16); return comp.toString(16).toLowerCase(); } @@ -120,12 +120,13 @@ export interface Node { toElement(): Element; } +// biome-ignore lint: we do not know the type of the component export class Builder = Record> { #style: CSSPropertiesLike = {}; /** * The tailwind subset to apply to this builder. */ - public tw: string = ""; + public tw = ""; /** * The components of this builder. */ @@ -188,8 +189,14 @@ export class Builder = Record> { * @param component the component to add. */ public addComponent(component: T | T[]) { - if (component instanceof Element && (component.type as unknown as Function) === JSX.Fragment) + if ( + component instanceof Element && + // biome-ignore lint: Function could be a component + (component.type as unknown as Function) === JSX.Fragment + ) + // biome-ignore lint: Reassigning is considered component = component.children; + // biome-ignore lint: Reassigning is considered if (!Array.isArray(component)) component = [component]; this.components.push(...component); return this; @@ -213,14 +220,12 @@ export class Builder = Record> { } private _render() { - return this.components - .map((component) => { - if (component == null) return []; - if (component instanceof Element) return component; - if (component.toElement) return component.toElement(); - return {String(component)}; - }) - .flat(1) as React.ReactNode[]; + return this.components.flatMap((component) => { + if (component == null) return []; + if (component instanceof Element) return component; + if (component.toElement) return component.toElement(); + return {String(component)}; + }) as React.ReactNode[]; } /** @@ -286,7 +291,7 @@ export class Builder = Record> { * Create a builder from builder template. */ public static from(template: BuilderTemplate) { - const builder = new this(template.width, template.height); + const builder = new Builder(template.width, template.height); if (template.style) builder.style = template.style; builder.components = template.components; diff --git a/packages/canvacord/src/templates/BuilderOptionsManager.ts b/packages/canvacord/src/templates/BuilderOptionsManager.ts index bc982c4..34ffc76 100644 --- a/packages/canvacord/src/templates/BuilderOptionsManager.ts +++ b/packages/canvacord/src/templates/BuilderOptionsManager.ts @@ -1,3 +1,4 @@ +// biome-ignore lint: any is tolerated here export class BuilderOptionsManager> { /** * Creates a new builder options manager.