Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
283 changes: 226 additions & 57 deletions app/api/generate-poster/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { createCanvas, loadImage } from "canvas";
import path from "path";
import { createClerkClient } from "@clerk/backend";
import { v4 as uuidv4 } from 'uuid';
import { v4 as uuidv4 } from "uuid";

// Initialize S3 Client
const s3Client = new S3Client({
Expand Down Expand Up @@ -70,52 +70,157 @@ export async function POST(req: Request) {
const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d");

// Set background to white
// Set background to white with a slight border effect
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, width, height);

// Header section (top 250px)
const redditLogo = await loadImage(path.join(process.cwd(), "public", "reddit-logo.png"));
const logoSize = 150; // Slightly smaller logo
const headerPadding = 50;
ctx.drawImage(redditLogo, headerPadding, headerPadding, logoSize, logoSize);

// Add username with verification badge style
ctx.font = "bold 70px Arial";

// Add a subtle rounded rectangle container for the post
const padding = 40;
const cornerRadius = 20;
const containerWidth = width - padding * 2;
const containerHeight = height - padding * 2;

// Draw rounded rectangle for post container
ctx.beginPath();
ctx.moveTo(padding + cornerRadius, padding);
ctx.lineTo(padding + containerWidth - cornerRadius, padding);
ctx.arcTo(
padding + containerWidth,
padding,
padding + containerWidth,
padding + cornerRadius,
cornerRadius
);
ctx.lineTo(
padding + containerWidth,
padding + containerHeight - cornerRadius
);
ctx.arcTo(
padding + containerWidth,
padding + containerHeight,
padding + containerWidth - cornerRadius,
padding + containerHeight,
cornerRadius
);
ctx.lineTo(padding + cornerRadius, padding + containerHeight);
ctx.arcTo(
padding,
padding + containerHeight,
padding,
padding + containerHeight - cornerRadius,
cornerRadius
);
ctx.lineTo(padding, padding + cornerRadius);
ctx.arcTo(padding, padding, padding + cornerRadius, padding, cornerRadius);
ctx.closePath();

// Add subtle shadow effect
ctx.shadowColor = "rgba(0, 0, 0, 0.1)";
ctx.shadowBlur = 15;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 5;
ctx.fillStyle = "#ffffff";
ctx.fill();

// Reset shadow
ctx.shadowColor = "transparent";
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;

// Header section with Reddit styling
const headerPadding = 60;
const headerY = padding + headerPadding;

// Load and draw Reddit logo (smaller, like a profile picture)
const redditLogo = await loadImage(
path.join(process.cwd(), "public", "reddit-logo.png")
);
const logoSize = 80;
ctx.drawImage(
redditLogo,
padding + headerPadding,
headerY,
logoSize,
logoSize
);

// Add username in Reddit style
ctx.font = "bold 40px Arial";
ctx.fillStyle = "#000000";
ctx.textAlign = "left";
const usernameX = logoSize + headerPadding + 50;
const usernameY = headerPadding + (logoSize / 2) + 20;
ctx.fillText(`@${subreddit}Book`, usernameX, usernameY);
// Add verification badge (blue circle)
const usernameWidth = ctx.measureText(`@${subreddit}Book`).width;
const badgeX = usernameX + usernameWidth + 30;
const badgeY = usernameY - 25;
const usernameX = padding + headerPadding + logoSize + 20;
const usernameY = headerY + logoSize / 2;
ctx.fillText(`${subreddit}`, usernameX, usernameY);

// Add verified badge
const usernameWidth = ctx.measureText(`${subreddit}`).width;
const badgeX = usernameX + usernameWidth + 15;
const badgeY = usernameY - 15;
ctx.beginPath();
ctx.arc(badgeX, badgeY, 30, 0, 2 * Math.PI);
ctx.arc(badgeX, badgeY, 15, 0, 2 * Math.PI);
ctx.fillStyle = "#1DA1F2";
ctx.fill();

// Add checkmark in verification badge
ctx.font = "bold 40px Arial";
ctx.font = "bold 20px Arial";
ctx.fillStyle = "#ffffff";
ctx.textAlign = "center";
ctx.fillText("✓", badgeX, badgeY + 15);

// Title section (centered in remaining space)
ctx.font = "bold 100px Arial"; // Slightly smaller font
ctx.fillText("✓", badgeX, badgeY + 7);

// Add view count
ctx.font = "28px Arial";
ctx.fillStyle = "#6e767d";
ctx.textAlign = "left";
ctx.fillText("999,999", usernameX, usernameY + 30);

// Add audio control visual element (red pause button with waveform)
const audioControlX = width - padding - headerPadding - 200;
const audioControlY = headerY + logoSize / 2 - 20;

// Pause button (red circle)
ctx.beginPath();
ctx.arc(audioControlX, audioControlY, 20, 0, 2 * Math.PI);
ctx.fillStyle = "#FF4500"; // Reddit orange-red
ctx.fill();

// Pause icon (white)
ctx.fillStyle = "#ffffff";
ctx.fillRect(audioControlX - 7, audioControlY - 10, 5, 20);
ctx.fillRect(audioControlX + 2, audioControlY - 10, 5, 20);

// Audio waveform visualization
const waveformWidth = 150;
const waveformHeight = 40;
const waveformX = audioControlX + 40;
const waveformY = audioControlY - waveformHeight / 2;

// Draw waveform bars
ctx.fillStyle = "#FF4500"; // Reddit orange-red
for (let i = 0; i < 15; i++) {
const barHeight = Math.random() * waveformHeight;
const barWidth = 6;
const barX = waveformX + i * 10;
const barY = waveformY + (waveformHeight - barHeight) / 2;
ctx.fillRect(barX, barY, barWidth, barHeight);
}

// Title section
const titlePadding = 60;
const titleX = padding + titlePadding;
const titleY = headerY + logoSize + 80;

// Draw title in Reddit style
ctx.font = "bold 60px Arial";
ctx.fillStyle = "#000000";
ctx.textAlign = "center";
ctx.textAlign = "left";

// Word wrap the title
const words = title.split(" ");
let line = "";
let lines = [];
const maxWidth = width - 200;
const titleX = width / 2; // Center horizontally
const titleStartY = 300; // Start below header

const maxWidth = containerWidth - titlePadding * 2;

for (let word of words) {
const testLine = line + word + " ";
const metrics = ctx.measureText(testLine);
Expand All @@ -128,48 +233,113 @@ export async function POST(req: Request) {
}
lines.push(line);

// Calculate total height of title to center vertically in remaining space
const lineHeight = 120;
const totalTitleHeight = lines.length * lineHeight;
const availableSpace = height - titleStartY - 200; // Space between header and metrics
const titleY = titleStartY + (availableSpace - totalTitleHeight) / 2;

// Draw wrapped title
const lineHeight = 85;
lines.forEach((line, i) => {
ctx.fillText(line.trim(), titleX, titleY + (i * lineHeight));
ctx.fillText(line.trim(), titleX, titleY + i * lineHeight);
});

// Metrics section (bottom)
const metricY = height - 100;
// Heart icon
const metricY = height - padding - headerPadding;

// Heart icon and count
ctx.beginPath();
ctx.arc(120, metricY, 35, 0, 2 * Math.PI);
ctx.arc(padding + headerPadding + 30, metricY, 20, 0, 2 * Math.PI);
ctx.fillStyle = "#ffffff";
ctx.fill();
ctx.strokeStyle = "#6e767d";
ctx.lineWidth = 3;
ctx.lineWidth = 2;
ctx.stroke();


// Heart shape
const heartX = padding + headerPadding + 30;
const heartY = metricY;
const heartSize = 12;

ctx.beginPath();
ctx.moveTo(heartX, heartY + heartSize * 0.3);
ctx.bezierCurveTo(
heartX,
heartY,
heartX - heartSize,
heartY,
heartX - heartSize,
heartY - heartSize * 0.5
);
ctx.bezierCurveTo(
heartX - heartSize,
heartY - heartSize * 1.1,
heartX,
heartY - heartSize * 1.1,
heartX,
heartY - heartSize * 0.6
);
ctx.bezierCurveTo(
heartX,
heartY - heartSize * 1.1,
heartX + heartSize,
heartY - heartSize * 1.1,
heartX + heartSize,
heartY - heartSize * 0.5
);
ctx.bezierCurveTo(
heartX + heartSize,
heartY,
heartX,
heartY,
heartX,
heartY + heartSize * 0.3
);
ctx.closePath();

ctx.fillStyle = "#6e767d";
ctx.fill();

// Like count
ctx.font = "bold 50px Arial";
ctx.font = "bold 32px Arial";
ctx.fillStyle = "#6e767d";
ctx.textAlign = "left";
ctx.fillText("99+", 180, metricY + 15);
ctx.fillText("999+", padding + headerPadding + 70, metricY + 10);

// Comment icon
const commentX = padding + headerPadding + 170;
const commentY = metricY;

ctx.beginPath();
ctx.arc(320, metricY, 35, 0, 2 * Math.PI);
ctx.arc(commentX, commentY, 20, 0, 2 * Math.PI);
ctx.fillStyle = "#ffffff";
ctx.fill();
ctx.strokeStyle = "#6e767d";
ctx.lineWidth = 3;
ctx.lineWidth = 2;
ctx.stroke();


// Comment bubble shape
const bubbleSize = 12;
ctx.beginPath();
ctx.moveTo(commentX - bubbleSize, commentY - bubbleSize);
ctx.lineTo(commentX + bubbleSize, commentY - bubbleSize);
ctx.lineTo(commentX + bubbleSize, commentY + bubbleSize * 0.5);
ctx.lineTo(commentX + bubbleSize * 0.5, commentY + bubbleSize * 0.5);
ctx.lineTo(commentX, commentY + bubbleSize);
ctx.lineTo(commentX, commentY + bubbleSize * 0.5);
ctx.lineTo(commentX - bubbleSize, commentY + bubbleSize * 0.5);
ctx.closePath();

ctx.fillStyle = "#6e767d";
ctx.fill();

// Comment count
ctx.fillText("99+", 380, metricY + 15);
ctx.fillText("999+", commentX + 40, metricY + 10);

// Author attribution
ctx.font = "28px Arial";
ctx.fillStyle = "#6e767d";
ctx.textAlign = "right";
ctx.fillText(`Posted by u/${author}`, width - headerPadding, height - headerPadding);
ctx.fillText(
`Posted by u/${author}`,
width - padding - headerPadding,
metricY + 10
);

// Convert canvas to buffer
const buffer = canvas.toBuffer("image/png");
Expand Down Expand Up @@ -198,12 +368,11 @@ export async function POST(req: Request) {
success: true,
poster_file_name,
});

} catch (error) {
console.error("Error generating poster:", error);
return NextResponse.json(
{ error: "Failed to generate poster" },
{ status: 500 }
);
}
}
}