mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-24 05:05:20 -04:00
151 lines
6.0 KiB
TypeScript
151 lines
6.0 KiB
TypeScript
import { createCanvas, loadImage } from 'canvas';
|
|
import type { CanvasRenderingContext2D, Image } from 'canvas';
|
|
import { Router } from 'express';
|
|
import SQL from 'sql-template-strings';
|
|
|
|
import type { Translations } from '../../locale/translations.ts';
|
|
import { buildPronounUsage, parsePronounGroups, parsePronouns } from '../../src/buildPronoun.ts';
|
|
import { PronounLibrary } from '../../src/classes.ts';
|
|
import type { PronounUsage } from '../../src/classes.ts';
|
|
import { handleErrorAsync } from '../../src/helpers.ts';
|
|
import { Translator } from '../../src/translator.ts';
|
|
import { loadTsv } from '../../src/tsv.ts';
|
|
import type { User } from '../../src/user.ts';
|
|
import avatar from '../avatar.ts';
|
|
import { loadSuml, loadSumlFromBase } from '../loader.ts';
|
|
import { registerLocaleFont } from '../localeFont.ts';
|
|
import { rootDir } from '../paths.ts';
|
|
|
|
import type { Database } from '~/server/db.ts';
|
|
|
|
const translations = loadSuml('translations') as Translations;
|
|
const baseTranslations = loadSumlFromBase('locale/_base/translations') as Translations;
|
|
|
|
const translator = new Translator(translations, baseTranslations, global.config);
|
|
|
|
const pronouns = parsePronouns(global.config, loadTsv(`${rootDir}/data/pronouns/pronouns.tsv`));
|
|
const pronounGroups = parsePronounGroups(loadTsv(`${rootDir}/data/pronouns/pronounGroups.tsv`));
|
|
const pronounLibrary = new PronounLibrary(global.config, pronounGroups, pronouns);
|
|
|
|
const drawCircle = (context: CanvasRenderingContext2D, image: Image, x: number, y: number, size: number): void => {
|
|
context.save();
|
|
context.beginPath();
|
|
context.arc(x + size / 2, y + size / 2, size / 2, 0, Math.PI * 2, true);
|
|
context.closePath();
|
|
context.clip();
|
|
|
|
context.drawImage(image, x, y, size, size);
|
|
|
|
context.beginPath();
|
|
context.arc(x, y, size / 2, 0, Math.PI * 2, true);
|
|
context.clip();
|
|
context.closePath();
|
|
context.restore();
|
|
};
|
|
|
|
const router = Router();
|
|
|
|
const width = 1200;
|
|
const height = 600;
|
|
const mime = 'image/png';
|
|
const imageSize = 200;
|
|
const leftRatio = 4;
|
|
|
|
const getBannerKey = defineCachedFunction(async (path: string, db: Database) => {
|
|
const fontName = registerLocaleFont('fontHeadings', ['regular', 'bold']);
|
|
|
|
const canvas = createCanvas(width, height);
|
|
const context = canvas.getContext('2d');
|
|
|
|
const bg = await loadImage('public/bg.png');
|
|
context.drawImage(bg, 0, 0, width, height);
|
|
|
|
let key = undefined;
|
|
if (path === 'zaimki') {
|
|
key = await drawDefault(context, fontName);
|
|
}
|
|
if (!key && path.startsWith('@')) {
|
|
const user = await db.get<Pick<User, 'id' | 'username' | 'email' | 'avatarSource'>>(SQL`SELECT id, username, email, avatarSource FROM users WHERE username=${path.substring(1)}`);
|
|
if (user) {
|
|
key = await drawProfileBanner(context, fontName, user, db);
|
|
}
|
|
}
|
|
if (!key) {
|
|
const usage = buildPronounUsage(pronounLibrary, path, config, translator);
|
|
if (usage !== null) {
|
|
key = await drawPronounUsage(context, fontName, usage);
|
|
}
|
|
}
|
|
if (!key) {
|
|
key = await drawDefault(context, fontName);
|
|
}
|
|
|
|
await useStorage('data').setItemRaw<Buffer>(`banner:${key}`, canvas.toBuffer(mime));
|
|
return key;
|
|
}, {
|
|
name: 'banner',
|
|
getKey: (path) => path,
|
|
maxAge: 24 * 60 * 60,
|
|
});
|
|
|
|
const drawDefault = async (context: CanvasRenderingContext2D, fontName: string) => {
|
|
const leftRatio = 5;
|
|
const logo = await loadImage('public/logo/logo.svg');
|
|
|
|
context.drawImage(logo, width / leftRatio - imageSize / 2, height / 2 - imageSize / 2, imageSize, imageSize);
|
|
context.font = `regular ${translations.title.length < 10 ? 120 : translations.title.length < 14 ? 80 : 72}pt '${fontName}'`;
|
|
context.fillText(translations.title, width / leftRatio + imageSize / 1.5, height / 2 + (translations.title.length < 10 ? 48 : translations.title.length < 14 ? 36 : 24));
|
|
return 'default.png';
|
|
};
|
|
|
|
const drawPronounUsage = async (context: CanvasRenderingContext2D, fontName: string, usage: PronounUsage) => {
|
|
const logo = await loadImage('public/logo/logo.svg');
|
|
|
|
context.drawImage(logo, width / leftRatio - imageSize / 2, height / 2 - imageSize / 2, imageSize, imageSize);
|
|
context.font = `regular 48pt '${fontName}'`;
|
|
context.fillText(`${translations.pronouns.intro}:`, width / leftRatio + imageSize / 1.5, height / 2 - 36);
|
|
|
|
context.font = `bold ${usage.short.options.length <= 2 ? '70' : '36'}pt '${fontName}'`;
|
|
context.fillText(
|
|
usage.short.options.map((o) => o.replace(/ ?\[[^\]]+] ?/g, '').trim()).join('\n'),
|
|
width / leftRatio + imageSize / 1.5,
|
|
height / 2 + (usage.short.options.length <= 2 ? 72 : 24),
|
|
);
|
|
return `pronoun:${usage.short.options.join('&').replaceAll(/[/:]/g, '-')}.png`;
|
|
};
|
|
|
|
const drawProfileBanner = async (
|
|
context: CanvasRenderingContext2D,
|
|
fontName: string,
|
|
user: Pick<User, 'id' | 'username' | 'email' | 'avatarSource'>,
|
|
db: Database,
|
|
) => {
|
|
const logoPrimary = await loadImage('public/logo/logo-primary.svg');
|
|
|
|
try {
|
|
const avatarImage = await loadImage(await avatar(db, user));
|
|
drawCircle(context, avatarImage, width / leftRatio - imageSize / 2, height / 2 - imageSize / 2, imageSize);
|
|
} catch {}
|
|
|
|
context.font = `regular 48pt '${fontName}'`;
|
|
context.fillText(`@${user.username}`, width / leftRatio + imageSize, height / 2);
|
|
|
|
context.font = `regular 24pt '${fontName}'`;
|
|
context.fillStyle = '#C71585';
|
|
const logoSize = 24 * 1.25;
|
|
context.drawImage(logoPrimary, width / leftRatio + imageSize, height / 2 + logoSize - 8, logoSize, logoSize);
|
|
context.fillText(translations.title, width / leftRatio + imageSize + 36, height / 2 + 48);
|
|
|
|
return `user:${user.username}.png`;
|
|
};
|
|
|
|
router.get('/banner/:pronounName*.png', handleErrorAsync(async (req, res) => {
|
|
const path = (req.params.pronounName + req.params[0]).replace(/(&)+$/, '');
|
|
|
|
const key = await getBannerKey(path, req.db);
|
|
const banner = await useStorage('data').getItemRaw<Buffer>(`banner:${key}`);
|
|
return res.set('content-type', mime).send(banner);
|
|
}));
|
|
|
|
export default router;
|