founders card
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
"use strict";
|
||||
|
||||
const { parentPort, workerData } = require("node:worker_threads");
|
||||
const { writeFile } = require("node:fs/promises");
|
||||
const { createCanvas, loadImage, GlobalFonts } = require("@napi-rs/canvas");
|
||||
|
||||
function parseHexColor(hex) {
|
||||
const raw = String(hex || "").trim();
|
||||
const m = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.exec(raw);
|
||||
if (!m) return { r: 0, g: 0, b: 0, a: 1 };
|
||||
const h = m[1];
|
||||
if (h.length === 3) {
|
||||
return {
|
||||
r: parseInt(h[0] + h[0], 16),
|
||||
g: parseInt(h[1] + h[1], 16),
|
||||
b: parseInt(h[2] + h[2], 16),
|
||||
a: 1,
|
||||
};
|
||||
}
|
||||
if (h.length === 6) {
|
||||
return {
|
||||
r: parseInt(h.slice(0, 2), 16),
|
||||
g: parseInt(h.slice(2, 4), 16),
|
||||
b: parseInt(h.slice(4, 6), 16),
|
||||
a: 1,
|
||||
};
|
||||
}
|
||||
return {
|
||||
r: parseInt(h.slice(0, 2), 16),
|
||||
g: parseInt(h.slice(2, 4), 16),
|
||||
b: parseInt(h.slice(4, 6), 16),
|
||||
a: parseInt(h.slice(6, 8), 16) / 255,
|
||||
};
|
||||
}
|
||||
|
||||
function hexToRgba(hex, opacity) {
|
||||
const c = parseHexColor(hex);
|
||||
const a = Math.max(0, Math.min(1, c.a * opacity));
|
||||
return `rgba(${c.r}, ${c.g}, ${c.b}, ${a})`;
|
||||
}
|
||||
|
||||
function fontString(style) {
|
||||
const italic = style.italic ? "italic " : "";
|
||||
const weight = style.bold ? 700 : 400;
|
||||
return `${italic}${weight} ${style.fontSize}px "${style.fontFamily}"`;
|
||||
}
|
||||
|
||||
function applyShadow(ctx, style) {
|
||||
if (!style.shadowEnabled) {
|
||||
clearShadow(ctx);
|
||||
return;
|
||||
}
|
||||
ctx.shadowColor = hexToRgba(style.shadowColor, style.shadowOpacity);
|
||||
ctx.shadowBlur = style.shadowBlur;
|
||||
ctx.shadowOffsetX = style.shadowOffsetX;
|
||||
ctx.shadowOffsetY = style.shadowOffsetY;
|
||||
}
|
||||
|
||||
function clearShadow(ctx) {
|
||||
ctx.shadowColor = "rgba(0,0,0,0)";
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.shadowOffsetX = 0;
|
||||
ctx.shadowOffsetY = 0;
|
||||
}
|
||||
|
||||
function drawIdText(ctx, text, canvasWidth, canvasHeight, style) {
|
||||
ctx.font = fontString(style);
|
||||
ctx.textAlign = style.align;
|
||||
ctx.textBaseline = "alphabetic";
|
||||
const metrics = ctx.measureText(text);
|
||||
const ascent =
|
||||
Number.isFinite(metrics.actualBoundingBoxAscent) &&
|
||||
metrics.actualBoundingBoxAscent > 0
|
||||
? metrics.actualBoundingBoxAscent
|
||||
: style.fontSize * 0.8;
|
||||
const descent =
|
||||
Number.isFinite(metrics.actualBoundingBoxDescent) &&
|
||||
metrics.actualBoundingBoxDescent > 0
|
||||
? metrics.actualBoundingBoxDescent
|
||||
: style.fontSize * 0.2;
|
||||
const x = (style.xPercent / 100) * canvasWidth;
|
||||
const y = (style.yPercent / 100) * canvasHeight;
|
||||
const baselineY = y + (ascent - descent) / 2;
|
||||
|
||||
applyShadow(ctx, style);
|
||||
if (style.outlineEnabled && style.outlineWidth > 0) {
|
||||
ctx.lineJoin = "round";
|
||||
ctx.miterLimit = 2;
|
||||
ctx.lineWidth = style.outlineWidth;
|
||||
ctx.strokeStyle = hexToRgba(style.outlineColor, style.outlineOpacity);
|
||||
ctx.strokeText(text, x, baselineY);
|
||||
}
|
||||
clearShadow(ctx);
|
||||
if (!style.outlineEnabled && style.shadowEnabled) {
|
||||
applyShadow(ctx, style);
|
||||
}
|
||||
ctx.fillStyle = style.color;
|
||||
ctx.fillText(text, x, baselineY);
|
||||
clearShadow(ctx);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const { imagePath, fonts, style, tasks } = workerData;
|
||||
for (const font of fonts || []) {
|
||||
GlobalFonts.registerFromPath(font.path, font.family);
|
||||
}
|
||||
const img = await loadImage(imagePath);
|
||||
const canvas = createCanvas(img.width, img.height);
|
||||
const ctx = canvas.getContext("2d");
|
||||
for (const task of tasks) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(img, 0, 0);
|
||||
drawIdText(ctx, task.text, canvas.width, canvas.height, style);
|
||||
const png = await canvas.encode("png");
|
||||
await writeFile(task.outPath, png);
|
||||
parentPort.postMessage({ type: "progress" });
|
||||
}
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
try {
|
||||
parentPort.postMessage({ type: "error", message });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in New Issue
Block a user