feat: bootstrap lunch picker miniapp with backend, docs, and branding assets
165
template/assets/branding/generate-logos-v2.js
Normal file
@@ -0,0 +1,165 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { createCanvas } = require('@napi-rs/canvas');
|
||||
|
||||
const SIZE = 600;
|
||||
const outDir = __dirname;
|
||||
|
||||
function roundedRect(ctx, x, y, w, h, r) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.arcTo(x + w, y, x + w, y + h, r);
|
||||
ctx.arcTo(x + w, y + h, x, y + h, r);
|
||||
ctx.arcTo(x, y + h, x, y, r);
|
||||
ctx.arcTo(x, y, x + w, y, r);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function drawSteam(ctx, x, y, color) {
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 10;
|
||||
ctx.lineCap = 'round';
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y + 32);
|
||||
ctx.bezierCurveTo(x - 14, y + 6, x + 22, y - 6, x + 8, y - 34);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawChopsticks(ctx, color) {
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 12;
|
||||
ctx.lineCap = 'round';
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(380, 170);
|
||||
ctx.lineTo(490, 280);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(415, 150);
|
||||
ctx.lineTo(525, 260);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawLogo({
|
||||
filename,
|
||||
bgGradientFrom,
|
||||
bgGradientTo,
|
||||
cardColor,
|
||||
cardShadow,
|
||||
bowlTop,
|
||||
bowlBottom,
|
||||
accent,
|
||||
textColor,
|
||||
steamColor,
|
||||
chopColor,
|
||||
}) {
|
||||
const canvas = createCanvas(SIZE, SIZE);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// background
|
||||
const bg = ctx.createLinearGradient(0, 0, SIZE, SIZE);
|
||||
bg.addColorStop(0, bgGradientFrom);
|
||||
bg.addColorStop(1, bgGradientTo);
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(0, 0, SIZE, SIZE);
|
||||
|
||||
// icon card
|
||||
ctx.save();
|
||||
ctx.shadowColor = cardShadow;
|
||||
ctx.shadowBlur = 28;
|
||||
ctx.shadowOffsetY = 14;
|
||||
roundedRect(ctx, 72, 72, 456, 456, 120);
|
||||
ctx.fillStyle = cardColor;
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
|
||||
// bowl gradient
|
||||
const bowlGrad = ctx.createLinearGradient(200, 270, 400, 420);
|
||||
bowlGrad.addColorStop(0, bowlTop);
|
||||
bowlGrad.addColorStop(1, bowlBottom);
|
||||
|
||||
// bowl body
|
||||
ctx.fillStyle = bowlGrad;
|
||||
roundedRect(ctx, 170, 300, 260, 130, 54);
|
||||
ctx.fill();
|
||||
|
||||
// bowl lip
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.86)';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(300, 298, 144, 36, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// inner bowl
|
||||
ctx.fillStyle = 'rgba(13,31,60,0.32)';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(300, 300, 76, 18, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// noodles
|
||||
ctx.strokeStyle = accent;
|
||||
ctx.lineWidth = 9;
|
||||
ctx.lineCap = 'round';
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const y = 282 + i * 8;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(220, y);
|
||||
ctx.bezierCurveTo(250, y - 14, 350, y - 14, 380, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// steam
|
||||
drawSteam(ctx, 245, 215, steamColor);
|
||||
drawSteam(ctx, 300, 190, steamColor);
|
||||
drawSteam(ctx, 355, 215, steamColor);
|
||||
|
||||
// chopsticks
|
||||
drawChopsticks(ctx, chopColor);
|
||||
|
||||
// JM monogram chip
|
||||
roundedRect(ctx, 220, 448, 160, 56, 28);
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.22)';
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.font = 'bold 34px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('점메추', 300, 476);
|
||||
|
||||
const output = path.join(outDir, filename);
|
||||
fs.writeFileSync(output, canvas.toBuffer('image/png'));
|
||||
return output;
|
||||
}
|
||||
|
||||
const light = drawLogo({
|
||||
filename: 'lunch-pick-logo-light-600-v2.png',
|
||||
bgGradientFrom: '#F7FBFF',
|
||||
bgGradientTo: '#E8F3FF',
|
||||
cardColor: 'rgba(255,255,255,0.94)',
|
||||
cardShadow: 'rgba(30,102,180,0.20)',
|
||||
bowlTop: '#29B2F4',
|
||||
bowlBottom: '#0A7FE8',
|
||||
accent: '#0876D7',
|
||||
textColor: '#0F3F7C',
|
||||
steamColor: '#5AAAF6',
|
||||
chopColor: '#1D4D87',
|
||||
});
|
||||
|
||||
const dark = drawLogo({
|
||||
filename: 'lunch-pick-logo-dark-600-v2.png',
|
||||
bgGradientFrom: '#061226',
|
||||
bgGradientTo: '#0D203F',
|
||||
cardColor: 'rgba(17,39,72,0.95)',
|
||||
cardShadow: 'rgba(0,0,0,0.45)',
|
||||
bowlTop: '#56C8FF',
|
||||
bowlBottom: '#1997F0',
|
||||
accent: '#9ED9FF',
|
||||
textColor: '#EAF4FF',
|
||||
steamColor: '#BFE4FF',
|
||||
chopColor: '#E6F3FF',
|
||||
});
|
||||
|
||||
console.log(light);
|
||||
console.log(dark);
|
||||
150
template/assets/branding/generate-logos-v3.js
Normal file
@@ -0,0 +1,150 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { createCanvas } = require('@napi-rs/canvas');
|
||||
|
||||
const SIZE = 600;
|
||||
const outDir = __dirname;
|
||||
|
||||
function roundedRect(ctx, x, y, w, h, r) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.arcTo(x + w, y, x + w, y + h, r);
|
||||
ctx.arcTo(x + w, y + h, x, y + h, r);
|
||||
ctx.arcTo(x, y + h, x, y, r);
|
||||
ctx.arcTo(x, y, x + w, y, r);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function drawSteam(ctx, x, y, color) {
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 10;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y + 36);
|
||||
ctx.bezierCurveTo(x - 14, y + 10, x + 20, y, x + 6, y - 30);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawRice(ctx, cx, cy, colorA, colorB) {
|
||||
// Rice mound
|
||||
const grad = ctx.createLinearGradient(cx - 100, cy - 30, cx + 100, cy + 20);
|
||||
grad.addColorStop(0, colorA);
|
||||
grad.addColorStop(1, colorB);
|
||||
ctx.fillStyle = grad;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, cy, 128, 56, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Rice grains highlights (subtle)
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.55)';
|
||||
for (let i = 0; i < 14; i++) {
|
||||
const x = cx - 95 + i * 14;
|
||||
const y = cy - 6 + ((i % 2) * 6 - 3);
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x, y, 4, 2.6, -0.3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
function drawLogo(theme) {
|
||||
const canvas = createCanvas(SIZE, SIZE);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Background gradient
|
||||
const bg = ctx.createLinearGradient(0, 0, SIZE, SIZE);
|
||||
bg.addColorStop(0, theme.bgFrom);
|
||||
bg.addColorStop(1, theme.bgTo);
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(0, 0, SIZE, SIZE);
|
||||
|
||||
// Main card
|
||||
ctx.save();
|
||||
ctx.shadowColor = theme.shadow;
|
||||
ctx.shadowBlur = 28;
|
||||
ctx.shadowOffsetY = 10;
|
||||
roundedRect(ctx, 74, 74, 452, 452, 122);
|
||||
ctx.fillStyle = theme.card;
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
|
||||
// Bowl body
|
||||
const bowlGrad = ctx.createLinearGradient(180, 280, 420, 430);
|
||||
bowlGrad.addColorStop(0, theme.bowlTop);
|
||||
bowlGrad.addColorStop(1, theme.bowlBottom);
|
||||
|
||||
ctx.fillStyle = bowlGrad;
|
||||
roundedRect(ctx, 168, 302, 264, 144, 64);
|
||||
ctx.fill();
|
||||
|
||||
// Bowl lip outer
|
||||
ctx.fillStyle = theme.lipOuter;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(300, 300, 154, 44, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Bowl lip inner
|
||||
ctx.fillStyle = theme.lipInner;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(300, 304, 104, 26, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Rice
|
||||
drawRice(ctx, 300, 278, theme.riceA, theme.riceB);
|
||||
|
||||
// Chopsticks (to imply meal)
|
||||
ctx.strokeStyle = theme.chop;
|
||||
ctx.lineWidth = 12;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(380, 178);
|
||||
ctx.lineTo(492, 290);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(416, 158);
|
||||
ctx.lineTo(528, 270);
|
||||
ctx.stroke();
|
||||
|
||||
// Steam
|
||||
drawSteam(ctx, 240, 196, theme.steam);
|
||||
drawSteam(ctx, 300, 172, theme.steam);
|
||||
drawSteam(ctx, 360, 196, theme.steam);
|
||||
|
||||
const output = path.join(outDir, theme.filename);
|
||||
fs.writeFileSync(output, canvas.toBuffer('image/png'));
|
||||
return output;
|
||||
}
|
||||
|
||||
const light = drawLogo({
|
||||
filename: 'lunch-pick-logo-light-600-v3.png',
|
||||
bgFrom: '#F6FAFF',
|
||||
bgTo: '#EAF3FF',
|
||||
card: 'rgba(255,255,255,0.96)',
|
||||
shadow: 'rgba(16,78,147,0.18)',
|
||||
bowlTop: '#31B4F4',
|
||||
bowlBottom: '#0A85E8',
|
||||
lipOuter: '#DDE9F8',
|
||||
lipInner: '#8EA6C5',
|
||||
riceA: '#FFFFFF',
|
||||
riceB: '#EFF6FF',
|
||||
chop: '#1E4F8A',
|
||||
steam: '#5EA8EE',
|
||||
});
|
||||
|
||||
const dark = drawLogo({
|
||||
filename: 'lunch-pick-logo-dark-600-v3.png',
|
||||
bgFrom: '#07142A',
|
||||
bgTo: '#0E2243',
|
||||
card: 'rgba(17,39,72,0.96)',
|
||||
shadow: 'rgba(0,0,0,0.46)',
|
||||
bowlTop: '#66CBFF',
|
||||
bowlBottom: '#1A9EF4',
|
||||
lipOuter: '#DDE6F2',
|
||||
lipInner: '#8CA2BC',
|
||||
riceA: '#FFFFFF',
|
||||
riceB: '#F2F7FF',
|
||||
chop: '#E4F0FF',
|
||||
steam: '#B8DCFF',
|
||||
});
|
||||
|
||||
console.log(light);
|
||||
console.log(dark);
|
||||
166
template/assets/branding/generate-logos-v4.js
Normal file
@@ -0,0 +1,166 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { createCanvas } = require('@napi-rs/canvas');
|
||||
|
||||
const SIZE = 600;
|
||||
const outDir = __dirname;
|
||||
|
||||
function roundedRect(ctx, x, y, w, h, r) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.arcTo(x + w, y, x + w, y + h, r);
|
||||
ctx.arcTo(x + w, y + h, x, y + h, r);
|
||||
ctx.arcTo(x, y + h, x, y, r);
|
||||
ctx.arcTo(x, y, x + w, y, r);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function drawSteam(ctx, x, y, color) {
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 10;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y + 34);
|
||||
ctx.bezierCurveTo(x - 14, y + 8, x + 18, y - 2, x + 6, y - 30);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawRice(ctx, cx, cy, colorA, colorB) {
|
||||
const grad = ctx.createLinearGradient(cx - 100, cy - 30, cx + 100, cy + 20);
|
||||
grad.addColorStop(0, colorA);
|
||||
grad.addColorStop(1, colorB);
|
||||
ctx.fillStyle = grad;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, cy, 128, 56, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.65)';
|
||||
for (let i = 0; i < 14; i++) {
|
||||
const x = cx - 95 + i * 14;
|
||||
const y = cy - 8 + ((i % 2) * 6 - 3);
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x, y, 4.3, 2.7, -0.3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
function drawSideDish(ctx, x, y, colorOuter, colorInner) {
|
||||
// small plate
|
||||
ctx.fillStyle = colorOuter;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x, y, 38, 16, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// food on plate
|
||||
ctx.fillStyle = colorInner;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x, y - 2, 24, 9, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function drawLogo(theme) {
|
||||
const canvas = createCanvas(SIZE, SIZE);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const bg = ctx.createLinearGradient(0, 0, SIZE, SIZE);
|
||||
bg.addColorStop(0, theme.bgFrom);
|
||||
bg.addColorStop(1, theme.bgTo);
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(0, 0, SIZE, SIZE);
|
||||
|
||||
ctx.save();
|
||||
ctx.shadowColor = theme.shadow;
|
||||
ctx.shadowBlur = 28;
|
||||
ctx.shadowOffsetY = 10;
|
||||
roundedRect(ctx, 74, 74, 452, 452, 122);
|
||||
ctx.fillStyle = theme.card;
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
|
||||
// main bowl
|
||||
const bowlGrad = ctx.createLinearGradient(180, 280, 420, 430);
|
||||
bowlGrad.addColorStop(0, theme.bowlTop);
|
||||
bowlGrad.addColorStop(1, theme.bowlBottom);
|
||||
|
||||
ctx.fillStyle = bowlGrad;
|
||||
roundedRect(ctx, 168, 306, 264, 140, 62);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = theme.lipOuter;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(300, 305, 154, 44, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = theme.lipInner;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(300, 310, 102, 25, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
drawRice(ctx, 300, 283, theme.riceA, theme.riceB);
|
||||
|
||||
// side dishes (to emphasize food/meal)
|
||||
drawSideDish(ctx, 192, 352, theme.sidePlate, theme.sideFoodA);
|
||||
drawSideDish(ctx, 408, 352, theme.sidePlate, theme.sideFoodB);
|
||||
|
||||
// spoon silhouette (meal cue, no chopsticks)
|
||||
ctx.strokeStyle = theme.spoon;
|
||||
ctx.lineWidth = 10;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(445, 210);
|
||||
ctx.lineTo(490, 300);
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = theme.spoon;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(430, 196, 20, 14, -0.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
drawSteam(ctx, 245, 196, theme.steam);
|
||||
drawSteam(ctx, 300, 172, theme.steam);
|
||||
drawSteam(ctx, 355, 196, theme.steam);
|
||||
|
||||
const output = path.join(outDir, theme.filename);
|
||||
fs.writeFileSync(output, canvas.toBuffer('image/png'));
|
||||
return output;
|
||||
}
|
||||
|
||||
const light = drawLogo({
|
||||
filename: 'lunch-pick-logo-light-600-v4.png',
|
||||
bgFrom: '#F6FAFF',
|
||||
bgTo: '#EAF3FF',
|
||||
card: 'rgba(255,255,255,0.97)',
|
||||
shadow: 'rgba(16,78,147,0.18)',
|
||||
bowlTop: '#31B4F4',
|
||||
bowlBottom: '#0A85E8',
|
||||
lipOuter: '#DDE9F8',
|
||||
lipInner: '#8EA6C5',
|
||||
riceA: '#FFFFFF',
|
||||
riceB: '#EFF6FF',
|
||||
sidePlate: '#D8E4F5',
|
||||
sideFoodA: '#EF7C42',
|
||||
sideFoodB: '#8BC34A',
|
||||
spoon: '#1E4F8A',
|
||||
steam: '#5EA8EE',
|
||||
});
|
||||
|
||||
const dark = drawLogo({
|
||||
filename: 'lunch-pick-logo-dark-600-v4.png',
|
||||
bgFrom: '#07142A',
|
||||
bgTo: '#0E2243',
|
||||
card: 'rgba(17,39,72,0.96)',
|
||||
shadow: 'rgba(0,0,0,0.46)',
|
||||
bowlTop: '#66CBFF',
|
||||
bowlBottom: '#1A9EF4',
|
||||
lipOuter: '#DDE6F2',
|
||||
lipInner: '#8CA2BC',
|
||||
riceA: '#FFFFFF',
|
||||
riceB: '#F2F7FF',
|
||||
sidePlate: '#6E86A6',
|
||||
sideFoodA: '#FF9F5A',
|
||||
sideFoodB: '#A0D66A',
|
||||
spoon: '#E6F1FF',
|
||||
steam: '#B8DCFF',
|
||||
});
|
||||
|
||||
console.log(light);
|
||||
console.log(dark);
|
||||
126
template/assets/branding/generate-logos-v5.js
Normal file
@@ -0,0 +1,126 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { createCanvas } = require('@napi-rs/canvas');
|
||||
|
||||
const SIZE = 600;
|
||||
const outDir = __dirname;
|
||||
|
||||
function roundedRect(ctx, x, y, w, h, r) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.arcTo(x + w, y, x + w, y + h, r);
|
||||
ctx.arcTo(x + w, y + h, x, y + h, r);
|
||||
ctx.arcTo(x, y + h, x, y, r);
|
||||
ctx.arcTo(x, y, x + w, y, r);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function drawSteam(ctx, x, y, color) {
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 10;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y + 34);
|
||||
ctx.bezierCurveTo(x - 14, y + 8, x + 18, y - 2, x + 6, y - 30);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawRice(ctx, cx, cy, colorA, colorB) {
|
||||
const grad = ctx.createLinearGradient(cx - 100, cy - 30, cx + 100, cy + 20);
|
||||
grad.addColorStop(0, colorA);
|
||||
grad.addColorStop(1, colorB);
|
||||
ctx.fillStyle = grad;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, cy, 128, 56, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.65)';
|
||||
for (let i = 0; i < 14; i++) {
|
||||
const x = cx - 95 + i * 14;
|
||||
const y = cy - 8 + ((i % 2) * 6 - 3);
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x, y, 4.3, 2.7, -0.3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
function drawLogo(theme) {
|
||||
const canvas = createCanvas(SIZE, SIZE);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const bg = ctx.createLinearGradient(0, 0, SIZE, SIZE);
|
||||
bg.addColorStop(0, theme.bgFrom);
|
||||
bg.addColorStop(1, theme.bgTo);
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(0, 0, SIZE, SIZE);
|
||||
|
||||
ctx.save();
|
||||
ctx.shadowColor = theme.shadow;
|
||||
ctx.shadowBlur = 28;
|
||||
ctx.shadowOffsetY = 10;
|
||||
roundedRect(ctx, 74, 74, 452, 452, 122);
|
||||
ctx.fillStyle = theme.card;
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
|
||||
const bowlGrad = ctx.createLinearGradient(180, 280, 420, 430);
|
||||
bowlGrad.addColorStop(0, theme.bowlTop);
|
||||
bowlGrad.addColorStop(1, theme.bowlBottom);
|
||||
|
||||
ctx.fillStyle = bowlGrad;
|
||||
roundedRect(ctx, 168, 306, 264, 140, 62);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = theme.lipOuter;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(300, 305, 154, 44, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = theme.lipInner;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(300, 310, 102, 25, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
drawRice(ctx, 300, 283, theme.riceA, theme.riceB);
|
||||
|
||||
drawSteam(ctx, 245, 196, theme.steam);
|
||||
drawSteam(ctx, 300, 172, theme.steam);
|
||||
drawSteam(ctx, 355, 196, theme.steam);
|
||||
|
||||
const output = path.join(outDir, theme.filename);
|
||||
fs.writeFileSync(output, canvas.toBuffer('image/png'));
|
||||
return output;
|
||||
}
|
||||
|
||||
const light = drawLogo({
|
||||
filename: 'lunch-pick-logo-light-600-v5.png',
|
||||
bgFrom: '#F6FAFF',
|
||||
bgTo: '#EAF3FF',
|
||||
card: 'rgba(255,255,255,0.97)',
|
||||
shadow: 'rgba(16,78,147,0.18)',
|
||||
bowlTop: '#31B4F4',
|
||||
bowlBottom: '#0A85E8',
|
||||
lipOuter: '#DDE9F8',
|
||||
lipInner: '#8EA6C5',
|
||||
riceA: '#FFFFFF',
|
||||
riceB: '#EFF6FF',
|
||||
steam: '#5EA8EE',
|
||||
});
|
||||
|
||||
const dark = drawLogo({
|
||||
filename: 'lunch-pick-logo-dark-600-v5.png',
|
||||
bgFrom: '#07142A',
|
||||
bgTo: '#0E2243',
|
||||
card: 'rgba(17,39,72,0.96)',
|
||||
shadow: 'rgba(0,0,0,0.46)',
|
||||
bowlTop: '#66CBFF',
|
||||
bowlBottom: '#1A9EF4',
|
||||
lipOuter: '#DDE6F2',
|
||||
lipInner: '#8CA2BC',
|
||||
riceA: '#FFFFFF',
|
||||
riceB: '#F2F7FF',
|
||||
steam: '#B8DCFF',
|
||||
});
|
||||
|
||||
console.log(light);
|
||||
console.log(dark);
|
||||
136
template/assets/branding/generate-logos.js
Normal file
@@ -0,0 +1,136 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { PNG } = require('pngjs');
|
||||
|
||||
const SIZE = 600;
|
||||
|
||||
function hexToRgb(hex) {
|
||||
const clean = hex.replace('#', '');
|
||||
return {
|
||||
r: parseInt(clean.substring(0, 2), 16),
|
||||
g: parseInt(clean.substring(2, 4), 16),
|
||||
b: parseInt(clean.substring(4, 6), 16),
|
||||
};
|
||||
}
|
||||
|
||||
function setPixel(png, x, y, color) {
|
||||
if (x < 0 || y < 0 || x >= png.width || y >= png.height) return;
|
||||
const idx = (png.width * y + x) << 2;
|
||||
png.data[idx] = color.r;
|
||||
png.data[idx + 1] = color.g;
|
||||
png.data[idx + 2] = color.b;
|
||||
png.data[idx + 3] = 255;
|
||||
}
|
||||
|
||||
function fillRect(png, x0, y0, x1, y1, color) {
|
||||
const sx = Math.max(0, Math.floor(x0));
|
||||
const sy = Math.max(0, Math.floor(y0));
|
||||
const ex = Math.min(png.width - 1, Math.ceil(x1));
|
||||
const ey = Math.min(png.height - 1, Math.ceil(y1));
|
||||
for (let y = sy; y <= ey; y++) {
|
||||
for (let x = sx; x <= ex; x++) setPixel(png, x, y, color);
|
||||
}
|
||||
}
|
||||
|
||||
function fillCircle(png, cx, cy, r, color) {
|
||||
const rr = r * r;
|
||||
const x0 = Math.floor(cx - r);
|
||||
const x1 = Math.ceil(cx + r);
|
||||
const y0 = Math.floor(cy - r);
|
||||
const y1 = Math.ceil(cy + r);
|
||||
for (let y = y0; y <= y1; y++) {
|
||||
for (let x = x0; x <= x1; x++) {
|
||||
const dx = x - cx;
|
||||
const dy = y - cy;
|
||||
if (dx * dx + dy * dy <= rr) setPixel(png, x, y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fillEllipse(png, cx, cy, rx, ry, color) {
|
||||
const rx2 = rx * rx;
|
||||
const ry2 = ry * ry;
|
||||
const x0 = Math.floor(cx - rx);
|
||||
const x1 = Math.ceil(cx + rx);
|
||||
const y0 = Math.floor(cy - ry);
|
||||
const y1 = Math.ceil(cy + ry);
|
||||
for (let y = y0; y <= y1; y++) {
|
||||
for (let x = x0; x <= x1; x++) {
|
||||
const dx = x - cx;
|
||||
const dy = y - cy;
|
||||
if ((dx * dx) / rx2 + (dy * dy) / ry2 <= 1) setPixel(png, x, y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawThickLine(png, x0, y0, x1, y1, thickness, color) {
|
||||
const steps = Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0));
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const t = i / (steps || 1);
|
||||
const x = Math.round(x0 + (x1 - x0) * t);
|
||||
const y = Math.round(y0 + (y1 - y0) * t);
|
||||
fillCircle(png, x, y, thickness / 2, color);
|
||||
}
|
||||
}
|
||||
|
||||
function drawLogo(filename, theme) {
|
||||
const png = new PNG({ width: SIZE, height: SIZE });
|
||||
|
||||
const bg = hexToRgb(theme.bg);
|
||||
const card = hexToRgb(theme.card);
|
||||
const bowl = hexToRgb(theme.bowl);
|
||||
const accent = hexToRgb(theme.accent);
|
||||
|
||||
fillRect(png, 0, 0, SIZE - 1, SIZE - 1, bg);
|
||||
|
||||
// Card background shape
|
||||
fillRect(png, 80, 80, 520, 520, card);
|
||||
fillCircle(png, 80, 80, 70, card);
|
||||
fillCircle(png, 520, 80, 70, card);
|
||||
fillCircle(png, 80, 520, 70, card);
|
||||
fillCircle(png, 520, 520, 70, card);
|
||||
|
||||
// Bowl
|
||||
fillEllipse(png, 300, 350, 165, 90, bowl);
|
||||
fillRect(png, 135, 350, 465, 430, bowl);
|
||||
fillEllipse(png, 300, 430, 165, 45, bowl);
|
||||
|
||||
// Bowl top cut
|
||||
fillEllipse(png, 300, 340, 175, 68, card);
|
||||
fillEllipse(png, 300, 346, 175, 4, accent);
|
||||
|
||||
// Noodles
|
||||
fillEllipse(png, 250, 372, 50, 12, accent);
|
||||
fillEllipse(png, 300, 374, 55, 13, accent);
|
||||
fillEllipse(png, 350, 372, 50, 12, accent);
|
||||
|
||||
// Steam
|
||||
fillEllipse(png, 245, 255, 14, 32, accent);
|
||||
fillEllipse(png, 300, 235, 16, 38, accent);
|
||||
fillEllipse(png, 355, 255, 14, 32, accent);
|
||||
|
||||
// Chopsticks
|
||||
drawThickLine(png, 360, 205, 485, 330, 11, accent);
|
||||
drawThickLine(png, 390, 188, 515, 313, 11, accent);
|
||||
|
||||
const outPath = path.join(__dirname, filename);
|
||||
fs.writeFileSync(outPath, PNG.sync.write(png));
|
||||
return outPath;
|
||||
}
|
||||
|
||||
const light = drawLogo('lunch-pick-logo-light-600.png', {
|
||||
bg: '#F8FAFC',
|
||||
card: '#E0F2FE',
|
||||
bowl: '#0EA5E9',
|
||||
accent: '#0C4A6E',
|
||||
});
|
||||
|
||||
const dark = drawLogo('lunch-pick-logo-dark-600.png', {
|
||||
bg: '#0B1220',
|
||||
card: '#13233C',
|
||||
bowl: '#38BDF8',
|
||||
accent: '#E2E8F0',
|
||||
});
|
||||
|
||||
console.log(light);
|
||||
console.log(dark);
|
||||
198
template/assets/branding/generate-thumbnail-1932x828-v5.js
Normal file
@@ -0,0 +1,198 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { createCanvas, GlobalFonts } = require('@napi-rs/canvas');
|
||||
|
||||
const W = 1932;
|
||||
const H = 828;
|
||||
const outDir = __dirname;
|
||||
|
||||
const fontCandidates = [
|
||||
'/usr/share/fonts/truetype/nanum/NanumGothic.ttf',
|
||||
'/usr/share/fonts/truetype/nanum/NanumGothicBold.ttf',
|
||||
'/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc',
|
||||
'/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc',
|
||||
];
|
||||
|
||||
for (const fp of fontCandidates) {
|
||||
if (fs.existsSync(fp)) {
|
||||
GlobalFonts.registerFromPath(fp, path.basename(fp));
|
||||
}
|
||||
}
|
||||
|
||||
function pickFamily(weight = 'regular') {
|
||||
if (weight === 'bold' && fs.existsSync('/usr/share/fonts/truetype/nanum/NanumGothicBold.ttf')) {
|
||||
return 'NanumGothicBold.ttf';
|
||||
}
|
||||
if (fs.existsSync('/usr/share/fonts/truetype/nanum/NanumGothic.ttf')) {
|
||||
return 'NanumGothic.ttf';
|
||||
}
|
||||
if (weight === 'bold' && fs.existsSync('/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc')) {
|
||||
return 'NotoSansCJK-Bold.ttc';
|
||||
}
|
||||
if (fs.existsSync('/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc')) {
|
||||
return 'NotoSansCJK-Regular.ttc';
|
||||
}
|
||||
return 'sans-serif';
|
||||
}
|
||||
|
||||
function roundedRect(ctx, x, y, w, h, r) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.arcTo(x + w, y, x + w, y + h, r);
|
||||
ctx.arcTo(x + w, y + h, x, y + h, r);
|
||||
ctx.arcTo(x, y + h, x, y, r);
|
||||
ctx.arcTo(x, y, x + w, y, r);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function drawSteam(ctx, x, y, color, width = 18) {
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = width;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y + 56);
|
||||
ctx.bezierCurveTo(x - 22, y + 16, x + 28, y - 6, x + 10, y - 54);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawV5Bowl(ctx, x, y, theme) {
|
||||
const bowlGrad = ctx.createLinearGradient(x - 220, y + 80, x + 220, y + 300);
|
||||
bowlGrad.addColorStop(0, theme.bowlTop);
|
||||
bowlGrad.addColorStop(1, theme.bowlBottom);
|
||||
ctx.fillStyle = bowlGrad;
|
||||
roundedRect(ctx, x - 250, y + 120, 500, 260, 120);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = theme.lipOuter;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x, y + 120, 280, 76, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = theme.lipInner;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x, y + 130, 185, 44, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
const riceGrad = ctx.createLinearGradient(x - 150, y + 20, x + 150, y + 150);
|
||||
riceGrad.addColorStop(0, theme.riceA);
|
||||
riceGrad.addColorStop(1, theme.riceB);
|
||||
ctx.fillStyle = riceGrad;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x, y + 74, 230, 96, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.65)';
|
||||
for (let i = 0; i < 18; i++) {
|
||||
const gx = x - 130 + i * 15;
|
||||
const gy = y + 54 + ((i % 2) * 7 - 3);
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(gx, gy, 6, 3, -0.2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
drawSteam(ctx, x - 110, y - 120, theme.steam, 16);
|
||||
drawSteam(ctx, x, y - 145, theme.steam, 18);
|
||||
drawSteam(ctx, x + 110, y - 120, theme.steam, 16);
|
||||
}
|
||||
|
||||
function drawThumbnail({ filename, theme }) {
|
||||
const canvas = createCanvas(W, H);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const bg = ctx.createLinearGradient(0, 0, W, H);
|
||||
bg.addColorStop(0, theme.bgFrom);
|
||||
bg.addColorStop(1, theme.bgTo);
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
ctx.fillStyle = theme.blob1;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(240, 140, 290, 190, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = theme.blob2;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(1680, 710, 380, 210, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
roundedRect(ctx, 72, 62, W - 144, H - 124, 64);
|
||||
ctx.fillStyle = theme.card;
|
||||
ctx.fill();
|
||||
|
||||
drawV5Bowl(ctx, 520, 300, theme);
|
||||
|
||||
ctx.fillStyle = theme.title;
|
||||
ctx.font = `700 112px "${pickFamily('bold')}"`;
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText('점메추', 910, 220);
|
||||
|
||||
ctx.fillStyle = theme.subtitle;
|
||||
ctx.font = `700 54px "${pickFamily('bold')}"`;
|
||||
ctx.fillText('직장인 점심 추천', 915, 364);
|
||||
|
||||
ctx.fillStyle = theme.desc;
|
||||
ctx.font = `600 36px "${pickFamily('regular')}"`;
|
||||
ctx.fillText('가까운 식당 찾기 · 카테고리별 탐색 · 리뷰/별점', 915, 448);
|
||||
|
||||
roundedRect(ctx, 915, 520, 540, 94, 46);
|
||||
ctx.fillStyle = theme.badge;
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = theme.badgeText;
|
||||
ctx.font = `700 42px "${pickFamily('bold')}"`;
|
||||
ctx.fillText('오늘 점심, 더 빠르게 결정', 965, 545);
|
||||
|
||||
const output = path.join(outDir, filename);
|
||||
fs.writeFileSync(output, canvas.toBuffer('image/png'));
|
||||
return output;
|
||||
}
|
||||
|
||||
const light = drawThumbnail({
|
||||
filename: 'thumbnail-1932x828-light-v5.png',
|
||||
theme: {
|
||||
bgFrom: '#F6FAFF',
|
||||
bgTo: '#E5F1FF',
|
||||
blob1: 'rgba(108,174,255,0.16)',
|
||||
blob2: 'rgba(41,130,255,0.12)',
|
||||
card: 'rgba(255,255,255,0.9)',
|
||||
bowlTop: '#31B4F4',
|
||||
bowlBottom: '#0A85E8',
|
||||
lipOuter: '#DDE9F8',
|
||||
lipInner: '#8EA6C5',
|
||||
riceA: '#FFFFFF',
|
||||
riceB: '#EFF6FF',
|
||||
steam: '#5EA8EE',
|
||||
title: '#114B95',
|
||||
subtitle: '#1D5FB0',
|
||||
desc: '#3A5A84',
|
||||
badge: '#0B86E9',
|
||||
badgeText: '#FFFFFF',
|
||||
},
|
||||
});
|
||||
|
||||
const dark = drawThumbnail({
|
||||
filename: 'thumbnail-1932x828-dark-v5.png',
|
||||
theme: {
|
||||
bgFrom: '#07142A',
|
||||
bgTo: '#0F284C',
|
||||
blob1: 'rgba(102,180,255,0.12)',
|
||||
blob2: 'rgba(59,148,255,0.1)',
|
||||
card: 'rgba(18,42,78,0.84)',
|
||||
bowlTop: '#66CBFF',
|
||||
bowlBottom: '#1A9EF4',
|
||||
lipOuter: '#DDE6F2',
|
||||
lipInner: '#8CA2BC',
|
||||
riceA: '#FFFFFF',
|
||||
riceB: '#F2F7FF',
|
||||
steam: '#B8DCFF',
|
||||
title: '#EAF4FF',
|
||||
subtitle: '#B8D9FF',
|
||||
desc: '#9EC2E8',
|
||||
badge: '#2A9DF8',
|
||||
badgeText: '#FFFFFF',
|
||||
},
|
||||
});
|
||||
|
||||
console.log(light);
|
||||
console.log(dark);
|
||||
180
template/assets/branding/generate-thumbnail-1932x828.js
Normal file
@@ -0,0 +1,180 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { createCanvas } = require('@napi-rs/canvas');
|
||||
|
||||
const W = 1932;
|
||||
const H = 828;
|
||||
const outDir = __dirname;
|
||||
|
||||
function roundedRect(ctx, x, y, w, h, r) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.arcTo(x + w, y, x + w, y + h, r);
|
||||
ctx.arcTo(x + w, y + h, x, y + h, r);
|
||||
ctx.arcTo(x, y + h, x, y, r);
|
||||
ctx.arcTo(x, y, x + w, y, r);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function drawSteam(ctx, x, y, color, width = 18) {
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = width;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y + 56);
|
||||
ctx.bezierCurveTo(x - 22, y + 16, x + 28, y - 6, x + 10, y - 54);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawHeroBowl(ctx, x, y, theme) {
|
||||
// bowl body
|
||||
const grad = ctx.createLinearGradient(x - 220, y + 80, x + 220, y + 300);
|
||||
grad.addColorStop(0, theme.bowlTop);
|
||||
grad.addColorStop(1, theme.bowlBottom);
|
||||
ctx.fillStyle = grad;
|
||||
roundedRect(ctx, x - 250, y + 120, 500, 260, 120);
|
||||
ctx.fill();
|
||||
|
||||
// lip
|
||||
ctx.fillStyle = theme.lipOuter;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x, y + 120, 280, 76, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = theme.lipInner;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x, y + 130, 185, 44, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// rice
|
||||
const riceGrad = ctx.createLinearGradient(x - 150, y + 20, x + 150, y + 150);
|
||||
riceGrad.addColorStop(0, theme.riceA);
|
||||
riceGrad.addColorStop(1, theme.riceB);
|
||||
ctx.fillStyle = riceGrad;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x, y + 74, 230, 96, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// grains
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.65)';
|
||||
for (let i = 0; i < 18; i++) {
|
||||
const gx = x - 130 + i * 15;
|
||||
const gy = y + 54 + ((i % 2) * 7 - 3);
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(gx, gy, 6, 3, -0.2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// steam
|
||||
drawSteam(ctx, x - 110, y - 120, theme.steam, 16);
|
||||
drawSteam(ctx, x, y - 145, theme.steam, 18);
|
||||
drawSteam(ctx, x + 110, y - 120, theme.steam, 16);
|
||||
}
|
||||
|
||||
function drawThumbnail({ filename, theme }) {
|
||||
const canvas = createCanvas(W, H);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// background
|
||||
const bg = ctx.createLinearGradient(0, 0, W, H);
|
||||
bg.addColorStop(0, theme.bgFrom);
|
||||
bg.addColorStop(1, theme.bgTo);
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
// soft circles
|
||||
ctx.fillStyle = theme.blob1;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(260, 120, 280, 180, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = theme.blob2;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(1700, 720, 360, 220, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// glass card
|
||||
roundedRect(ctx, 72, 62, W - 144, H - 124, 64);
|
||||
ctx.fillStyle = theme.card;
|
||||
ctx.fill();
|
||||
|
||||
// left visual
|
||||
drawHeroBowl(ctx, 520, 300, theme);
|
||||
|
||||
// right text area
|
||||
ctx.fillStyle = theme.title;
|
||||
ctx.font = 'bold 112px sans-serif';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText('점메추', 910, 220);
|
||||
|
||||
ctx.fillStyle = theme.subtitle;
|
||||
ctx.font = '600 52px sans-serif';
|
||||
ctx.fillText('직장인 점심 추천', 915, 360);
|
||||
|
||||
ctx.fillStyle = theme.desc;
|
||||
ctx.font = '500 34px sans-serif';
|
||||
ctx.fillText('가까운 식당 찾기 · 카테고리별 탐색 · 리뷰/별점', 915, 446);
|
||||
|
||||
// badge
|
||||
roundedRect(ctx, 915, 518, 520, 92, 44);
|
||||
ctx.fillStyle = theme.badge;
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = theme.badgeText;
|
||||
ctx.font = '700 40px sans-serif';
|
||||
ctx.fillText('오늘 점심, 더 빠르게 결정', 962, 542);
|
||||
|
||||
const output = path.join(outDir, filename);
|
||||
fs.writeFileSync(output, canvas.toBuffer('image/png'));
|
||||
return output;
|
||||
}
|
||||
|
||||
const light = drawThumbnail({
|
||||
filename: 'thumbnail-1932x828-light.png',
|
||||
theme: {
|
||||
bgFrom: '#F6FAFF',
|
||||
bgTo: '#E5F1FF',
|
||||
blob1: 'rgba(108,174,255,0.16)',
|
||||
blob2: 'rgba(41,130,255,0.12)',
|
||||
card: 'rgba(255,255,255,0.9)',
|
||||
bowlTop: '#31B4F4',
|
||||
bowlBottom: '#0A85E8',
|
||||
lipOuter: '#DDE9F8',
|
||||
lipInner: '#8EA6C5',
|
||||
riceA: '#FFFFFF',
|
||||
riceB: '#EFF6FF',
|
||||
steam: '#5EA8EE',
|
||||
title: '#114B95',
|
||||
subtitle: '#1D5FB0',
|
||||
desc: '#3A5A84',
|
||||
badge: '#0B86E9',
|
||||
badgeText: '#FFFFFF',
|
||||
},
|
||||
});
|
||||
|
||||
const dark = drawThumbnail({
|
||||
filename: 'thumbnail-1932x828-dark.png',
|
||||
theme: {
|
||||
bgFrom: '#07142A',
|
||||
bgTo: '#0F284C',
|
||||
blob1: 'rgba(102,180,255,0.12)',
|
||||
blob2: 'rgba(59,148,255,0.1)',
|
||||
card: 'rgba(18,42,78,0.84)',
|
||||
bowlTop: '#66CBFF',
|
||||
bowlBottom: '#1A9EF4',
|
||||
lipOuter: '#DDE6F2',
|
||||
lipInner: '#8CA2BC',
|
||||
riceA: '#FFFFFF',
|
||||
riceB: '#F2F7FF',
|
||||
steam: '#B8DCFF',
|
||||
title: '#EAF4FF',
|
||||
subtitle: '#B8D9FF',
|
||||
desc: '#9EC2E8',
|
||||
badge: '#2A9DF8',
|
||||
badgeText: '#FFFFFF',
|
||||
},
|
||||
});
|
||||
|
||||
console.log(light);
|
||||
console.log(dark);
|
||||
BIN
template/assets/branding/lunch-pick-logo-dark-600-v2.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
template/assets/branding/lunch-pick-logo-dark-600-v3.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
template/assets/branding/lunch-pick-logo-dark-600-v4.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
template/assets/branding/lunch-pick-logo-dark-600-v5.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
template/assets/branding/lunch-pick-logo-dark-600.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
template/assets/branding/lunch-pick-logo-light-600-v2.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
template/assets/branding/lunch-pick-logo-light-600-v3.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
template/assets/branding/lunch-pick-logo-light-600-v4.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
template/assets/branding/lunch-pick-logo-light-600-v5.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
template/assets/branding/lunch-pick-logo-light-600.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
template/assets/branding/thumbnail-1932x828-dark-v5.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
template/assets/branding/thumbnail-1932x828-dark.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
template/assets/branding/thumbnail-1932x828-light-v5.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
template/assets/branding/thumbnail-1932x828-light.png
Normal file
|
After Width: | Height: | Size: 54 KiB |