Name card almost done

This commit is contained in:
2025-11-11 00:00:01 +01:00
parent 57f8515491
commit d5071dbf69
2 changed files with 46 additions and 38 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -29,13 +29,19 @@ export class NameCardCreator {
const template = this.loadTemplate(); const template = this.loadTemplate();
const avatar = await this.handleAvatar(avatarPath); const avatarSize = 450;
const messageBuffer = await this.getMessageBuffer(`Hello ${name}!`); const avatar = await this.handleAvatar(avatarPath, avatarSize);
const avatarPos = { x : 1500-(avatarSize/2), y : 100}
const messageWidth = 1500;
const messageHeight = 200;
const messageBuffer = await this.getMessageBuffer(`Hello ${name}!`, messageWidth, messageHeight);
const messagePos = { x : 1500-(messageWidth/2), y : avatarPos.y + 550}
const result = await template const result = await template
.composite([ .composite([
{ input: avatar, top: 215, left: 275 }, { input: avatar, top: avatarPos.y, left: avatarPos.x },
{ input: messageBuffer, top: 400, left: 1300 } { input: messageBuffer, top: messagePos.y, left: messagePos.x }
]) ])
.toFile("namecard.png") .toFile("namecard.png")
@@ -50,69 +56,71 @@ export class NameCardCreator {
/** /**
* *
* @param avatarPath * @param avatarPath
* @param outputSize
* @returns {Promise<Buffer<ArrayBufferLike>>} * @returns {Promise<Buffer<ArrayBufferLike>>}
*/ */
async handleAvatar(avatarPath) { async handleAvatar(avatarPath, outputSize) {
const avatarSize = 670; const avatarSize = outputSize;
const borderSize = 8; const borderSize = 8;
const radius = avatarSize / 2;
const totalSize = avatarSize + borderSize * 2; const totalSize = avatarSize + borderSize * 2;
const avatarRadius = avatarSize / 2;
const maskBuffer = Buffer.from(`
<svg width="${avatarSize}" height="${avatarSize}" xmlns="http://www.w3.org/2000/svg">
<circle cx="${avatarRadius}" cy="${avatarRadius}" r="${avatarRadius}" fill="white"/>
</svg>
`);
const avatarBuffer = await sharp(avatarPath) const avatarBuffer = await sharp(avatarPath)
.resize(avatarSize, avatarSize, { fit: "cover" }) .resize(avatarSize, avatarSize, { fit: "cover" })
.png() .png()
.toBuffer(); .toBuffer();
const maskSvg = Buffer.from(` const roundedAvatarBuffer = await sharp(avatarBuffer)
<svg width="${avatarSize}" height="${avatarSize}" xmlns="http://www.w3.org/2000/svg"> .composite([{ input: maskBuffer, blend: "dest-in" }])
<circle cx="${radius}" cy="${radius}" r="${radius}" fill="white"/>
</svg>
`);
const roundedAvatar = await sharp(avatarBuffer)
.composite([{ input: maskSvg, blend: "dest-in" }])
.png() .png()
.toBuffer(); .toBuffer();
const roundedBorder = Buffer.from(` const backgroundBuffer = Buffer.from(`
<svg width="${totalSize}" height="${totalSize}" xmlns="http://www.w3.org/2000/svg"> <svg width="${totalSize}" height="${totalSize}" xmlns="http://www.w3.org/2000/svg">
<circle <circle
cx="${totalSize / 2}" cx="${totalSize / 2}"
cy="${totalSize / 2}" cy="${totalSize / 2}"
r="${radius + borderSize / 2}" r="${avatarRadius}"
stroke="#ffffff" stroke="#ffffff"
stroke-width="${borderSize}" stroke-width="${borderSize}"
fill="none" fill="white"/>
/>
</svg> </svg>
`) `);
return await sharp({ return await sharp({
create: { create: {
width: totalSize, width: totalSize,
height: totalSize, height: totalSize,
channels: 4, channels: 4,
background: "#0000" background: "#0000",
} }})
}).composite([ .composite([
{ input: roundedAvatar, top: 0, left: 0 }, { input: backgroundBuffer, top: 0, left: 0 },
{ input: roundedBorder, top: borderSize, left: borderSize } { input: roundedAvatarBuffer, top: borderSize, left: borderSize }
]) ])
.png() .png()
.toBuffer(); .toBuffer();
} }
async getMessageBuffer(message){ async getMessageBuffer(message, outputWidth, outputHeight){
const messageSvg = this.getMessageSvg(message); const messageSvg = this.getMessageSvg(message, outputWidth, outputHeight);
return Buffer.from(messageSvg, "utf-8"); return Buffer.from(messageSvg, "utf-8");
} }
/** /**
* *
* @param message {string} * @param message {string}
* @param outputWidth
* @param outputHeight
* @returns {string} * @returns {string}
*/ */
getMessageSvg(message) { getMessageSvg(message, outputWidth, outputHeight) {
const safeMessage = const safeMessage =
message message
.replace(/&/g, "&amp;") .replace(/&/g, "&amp;")
@@ -120,7 +128,7 @@ export class NameCardCreator {
.replace(/>/g, "&gt;"); .replace(/>/g, "&gt;");
return ` return `
<svg width="1500" height="200" xmlns="http://www.w3.org/2000/svg"> <svg width="${outputWidth}" height="${outputHeight}" xmlns="http://www.w3.org/2000/svg">
<style> <style>
@font-face { @font-face {
font-family: 'Fredoka'; font-family: 'Fredoka';
@@ -129,7 +137,7 @@ export class NameCardCreator {
.title { .title {
font-family: 'Fredoka', sans-serif; font-family: 'Fredoka', sans-serif;
fill: #ede6e6; fill: #ede6e6;
font-size: 80px; font-size: ${outputHeight/2}px;
font-weight: bold; font-weight: bold;
dominant-baseline: middle; dominant-baseline: middle;
} }