|
| 1 | +#!/usr/bin/env node |
| 2 | + |
| 3 | +import { createCanvas, loadImage } from 'canvas'; |
| 4 | +import { readFile, writeFile } from 'fs/promises'; |
| 5 | +import { fileURLToPath } from 'url'; |
| 6 | +import { dirname, join } from 'path'; |
| 7 | + |
| 8 | +const __filename = fileURLToPath(import.meta.url); |
| 9 | +const __dirname = dirname(__filename); |
| 10 | + |
| 11 | +// Open Graph standard dimensions |
| 12 | +const WIDTH = 1200; |
| 13 | +const HEIGHT = 630; |
| 14 | + |
| 15 | +async function generateSocialCard() { |
| 16 | + console.log('Generating social card...'); |
| 17 | + |
| 18 | + // Create canvas |
| 19 | + const canvas = createCanvas(WIDTH, HEIGHT); |
| 20 | + const ctx = canvas.getContext('2d'); |
| 21 | + |
| 22 | + // Create gradient background (purple to pink, matching the original) |
| 23 | + const gradient = ctx.createLinearGradient(0, 0, 0, HEIGHT); |
| 24 | + gradient.addColorStop(0, '#7c3aed'); // Purple |
| 25 | + gradient.addColorStop(1, '#ec4899'); // Fuchsia/Pink |
| 26 | + |
| 27 | + ctx.fillStyle = gradient; |
| 28 | + ctx.fillRect(0, 0, WIDTH, HEIGHT); |
| 29 | + |
| 30 | + // Add a white/cream box in the center for the logo |
| 31 | + const boxSize = 200; |
| 32 | + const boxX = (WIDTH - boxSize) / 2; |
| 33 | + const boxY = 135; |
| 34 | + |
| 35 | + ctx.fillStyle = '#f5f5f0'; |
| 36 | + ctx.fillRect(boxX, boxY, boxSize, boxSize); |
| 37 | + |
| 38 | + // Load and draw the logo SVG |
| 39 | + // For SVG, we need to use a workaround - load it as an image |
| 40 | + // The logo SVG needs to be converted first, or we can embed a simplified version |
| 41 | + |
| 42 | + // Draw a simplified version of the logo directly |
| 43 | + // Purple loop with fuchsia node |
| 44 | + const centerX = WIDTH / 2; |
| 45 | + const centerY = boxY + boxSize / 2; |
| 46 | + const radius = 50; |
| 47 | + |
| 48 | + // Draw the gapped circle (loop) |
| 49 | + ctx.strokeStyle = '#7c3aed'; |
| 50 | + ctx.lineWidth = 16; |
| 51 | + ctx.lineCap = 'round'; |
| 52 | + |
| 53 | + const startAngle = (Math.PI / 180) * 10; |
| 54 | + const endAngle = (Math.PI / 180) * 280; |
| 55 | + |
| 56 | + ctx.beginPath(); |
| 57 | + ctx.arc(centerX, centerY, radius, startAngle, endAngle); |
| 58 | + ctx.stroke(); |
| 59 | + |
| 60 | + // Draw the node (small square at top-right) |
| 61 | + const nodeSize = 22; |
| 62 | + const nodeX = centerX + Math.cos((Math.PI / 180) * 45) * radius - nodeSize / 2; |
| 63 | + const nodeY = centerY - Math.sin((Math.PI / 180) * 45) * radius - nodeSize / 2; |
| 64 | + |
| 65 | + ctx.fillStyle = '#ec4899'; |
| 66 | + ctx.beginPath(); |
| 67 | + ctx.roundRect(nodeX, nodeY, nodeSize, nodeSize, 5); |
| 68 | + ctx.fill(); |
| 69 | + |
| 70 | + // Add title text |
| 71 | + ctx.fillStyle = '#ffffff'; |
| 72 | + ctx.font = 'bold 90px sans-serif'; |
| 73 | + ctx.textAlign = 'center'; |
| 74 | + ctx.textBaseline = 'middle'; |
| 75 | + |
| 76 | + const titleY = boxY + boxSize + 100; |
| 77 | + ctx.fillText('AI Coding Course', centerX, titleY); |
| 78 | + |
| 79 | + // Add subtitle text |
| 80 | + ctx.font = '32px sans-serif'; |
| 81 | + const subtitleY = titleY + 70; |
| 82 | + ctx.fillText('Master AI-assisted software engineering for experienced developers', centerX, subtitleY); |
| 83 | + |
| 84 | + // Convert to buffer and save |
| 85 | + const buffer = canvas.toBuffer('image/png'); |
| 86 | + const outputPath = join(__dirname, '..', 'website', 'static', 'img', 'social-card.png'); |
| 87 | + |
| 88 | + await writeFile(outputPath, buffer); |
| 89 | + |
| 90 | + console.log(`✓ Social card generated: ${outputPath}`); |
| 91 | + console.log(` Dimensions: ${WIDTH}x${HEIGHT}px`); |
| 92 | + console.log(` File size: ${(buffer.length / 1024).toFixed(1)}KB`); |
| 93 | +} |
| 94 | + |
| 95 | +generateSocialCard().catch(console.error); |
0 commit comments