Skip to content

Commit 93c1463

Browse files
committed
redesigned card to light theme and added official logo
1 parent faff487 commit 93c1463

1 file changed

Lines changed: 88 additions & 64 deletions

File tree

src/utils/cardGenerator.ts

Lines changed: 88 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -76,17 +76,48 @@ export async function generateShareCard(data: ShareCardData): Promise<Blob> {
7676
throw new Error("Could not get 2d context from canvas");
7777
}
7878

79-
// 1. Draw Base Dark Gradient Background
79+
// 0. Pre-load all assets concurrently (Logo, Avatar, and Badges)
80+
let logoImg: HTMLImageElement | null = null;
81+
let avatarImg: HTMLImageElement | null = null;
82+
83+
const logoPromise = loadImage("/img/logo.png")
84+
.then((img) => {
85+
logoImg = img;
86+
})
87+
.catch((err) => console.warn("Could not load logo: ", err));
88+
89+
const avatarPromise = loadImage(data.avatarUrl)
90+
.then((img) => {
91+
avatarImg = img;
92+
})
93+
.catch((err) => console.warn("Could not load avatar: ", err));
94+
95+
const badgePromises = data.allBadges.map(async (badge) => {
96+
try {
97+
const img = await loadImage(badge.image);
98+
return { image: badge.image, img, loaded: true };
99+
} catch {
100+
return { image: badge.image, img: null, loaded: false };
101+
}
102+
});
103+
104+
const [, , loadedBadges] = await Promise.all([
105+
logoPromise,
106+
avatarPromise,
107+
Promise.all(badgePromises),
108+
]);
109+
110+
// 1. Draw Base Light Radial Gradient Background
80111
const bgGrad = ctx.createRadialGradient(600, 315, 100, 600, 315, 700);
81-
bgGrad.addColorStop(0, "#1e1b4b"); // Dark Indigo center
82-
bgGrad.addColorStop(1, "#09090e"); // Near-black edges
112+
bgGrad.addColorStop(0, "#ffffff"); // Pure white center
113+
bgGrad.addColorStop(1, "#f1f5f9"); // Soft slate-grey-blue edges
83114
ctx.fillStyle = bgGrad;
84115
ctx.fillRect(0, 0, 1200, 630);
85116

86-
// 2. Draw Decorative Ambient Glow Circles
117+
// 2. Draw Decorative Subtle Ambient Glow Circles
87118
// Left ambient glow (indigo)
88119
const leftGlow = ctx.createRadialGradient(250, 250, 50, 250, 250, 350);
89-
leftGlow.addColorStop(0, "rgba(99, 102, 241, 0.12)");
120+
leftGlow.addColorStop(0, "rgba(99, 102, 241, 0.05)");
90121
leftGlow.addColorStop(1, "rgba(99, 102, 241, 0)");
91122
ctx.fillStyle = leftGlow;
92123
ctx.beginPath();
@@ -95,80 +126,86 @@ export async function generateShareCard(data: ShareCardData): Promise<Blob> {
95126

96127
// Right ambient glow (violet/purple)
97128
const rightGlow = ctx.createRadialGradient(950, 380, 50, 950, 380, 350);
98-
rightGlow.addColorStop(0, "rgba(139, 92, 246, 0.12)");
99-
rightGlow.addColorStop(1, "rgba(139, 92, 246, 0)");
129+
rightGlow.addColorStop(0, "rgba(168, 85, 247, 0.05)");
130+
rightGlow.addColorStop(1, "rgba(168, 85, 247, 0)");
100131
ctx.fillStyle = rightGlow;
101132
ctx.beginPath();
102133
ctx.arc(950, 380, 350, 0, Math.PI * 2);
103134
ctx.fill();
104135

105-
// 3. Draw Glassmorphic Card Container
136+
// 3. Draw Premium Light Glassmorphic Card Container
106137
ctx.save();
138+
// Premium soft drop shadow
139+
ctx.shadowColor = "rgba(15, 23, 42, 0.06)";
140+
ctx.shadowBlur = 30;
141+
ctx.shadowOffsetY = 12;
142+
ctx.shadowOffsetX = 0;
143+
107144
drawRoundedRect(ctx, 50, 50, 1100, 530, 24);
108145
// Fill gradient
109146
const cardGrad = ctx.createLinearGradient(50, 50, 1150, 580);
110-
cardGrad.addColorStop(0, "rgba(30, 41, 59, 0.45)");
111-
cardGrad.addColorStop(1, "rgba(15, 23, 42, 0.65)");
147+
cardGrad.addColorStop(0, "rgba(255, 255, 255, 0.92)");
148+
cardGrad.addColorStop(1, "rgba(248, 250, 252, 0.95)");
112149
ctx.fillStyle = cardGrad;
113150
ctx.fill();
151+
152+
// Disable shadow for border/stroke
153+
ctx.shadowBlur = 0;
154+
ctx.shadowOffsetY = 0;
155+
114156
// Stroke border
115157
ctx.lineWidth = 1.5;
116158
const strokeGrad = ctx.createLinearGradient(50, 50, 1150, 580);
117-
strokeGrad.addColorStop(0, "rgba(255, 255, 255, 0.12)");
118-
strokeGrad.addColorStop(0.5, "rgba(255, 255, 255, 0.04)");
119-
strokeGrad.addColorStop(1, "rgba(99, 102, 241, 0.25)");
159+
strokeGrad.addColorStop(0, "rgba(255, 255, 255, 0.9)");
160+
strokeGrad.addColorStop(0.5, "rgba(226, 232, 240, 0.8)");
161+
strokeGrad.addColorStop(1, "rgba(99, 102, 241, 0.15)");
120162
ctx.strokeStyle = strokeGrad;
121163
ctx.stroke();
122164
ctx.restore();
123165

124166
// 4. Draw Recode Hive Logo (Top-Left)
125-
// Glowing hexagon logo mark
126-
ctx.save();
127-
drawHexagon(ctx, 110, 115, 22);
128-
const logoGrad = ctx.createLinearGradient(90, 95, 130, 135);
129-
logoGrad.addColorStop(0, "#6366f1"); // Indigo
130-
logoGrad.addColorStop(1, "#a855f7"); // Purple
131-
ctx.fillStyle = logoGrad;
132-
ctx.fill();
133-
ctx.restore();
167+
if (logoImg) {
168+
ctx.drawImage(logoImg, 86, 91, 48, 48);
169+
} else {
170+
// Fallback: Glowing hexagon logo mark
171+
ctx.save();
172+
drawHexagon(ctx, 110, 115, 22);
173+
const logoGrad = ctx.createLinearGradient(90, 95, 130, 135);
174+
logoGrad.addColorStop(0, "#6366f1"); // Indigo
175+
logoGrad.addColorStop(1, "#a855f7"); // Purple
176+
ctx.fillStyle = logoGrad;
177+
ctx.fill();
178+
ctx.restore();
179+
}
134180

135181
// Logo text
136182
ctx.font = "bold 38px Inter, system-ui, -apple-system, sans-serif";
137-
ctx.fillStyle = "#ffffff";
183+
ctx.fillStyle = "#0f172a"; // Deep slate
138184
ctx.fillText("recode", 155, 120);
139185

140-
ctx.fillStyle = "#f59e0b"; // Golden yellow
186+
ctx.fillStyle = "#d97706"; // Amber-gold (highly readable on light theme)
141187
ctx.fillText("hive", 280, 120);
142188

143189
// Subtitle / Tagline
144190
ctx.font = "500 16px Inter, system-ui, -apple-system, sans-serif";
145-
ctx.fillStyle = "#64748b"; // Cool slate gray
191+
ctx.fillStyle = "#475569"; // Slate gray (darker for readability)
146192
ctx.fillText("Learn, Build & Grow with Open Source", 155, 146);
147193

148-
// 5. Load and Draw Contributor Avatar
194+
// 5. Draw Contributor Avatar
149195
const avatarX = 160;
150196
const avatarY = 285;
151197
const avatarRadius = 70;
152198

153-
let avatarLoaded = false;
154-
let avatarImg: HTMLImageElement | null = null;
155-
try {
156-
avatarImg = await loadImage(data.avatarUrl);
157-
avatarLoaded = true;
158-
} catch (err) {
159-
console.warn("Could not load contributor avatar: ", err);
160-
}
161-
162199
// Draw Avatar ring glow
163200
ctx.save();
164201
ctx.beginPath();
165202
ctx.arc(avatarX, avatarY, avatarRadius + 5, 0, Math.PI * 2);
166-
ctx.strokeStyle = "rgba(99, 102, 241, 0.5)";
203+
ctx.strokeStyle = "rgba(99, 102, 241, 0.15)";
167204
ctx.lineWidth = 4;
168205
ctx.stroke();
169206
ctx.restore();
170207

171-
if (avatarLoaded && avatarImg) {
208+
if (avatarImg) {
172209
ctx.save();
173210
ctx.beginPath();
174211
ctx.arc(avatarX, avatarY, avatarRadius, 0, Math.PI * 2);
@@ -213,33 +250,33 @@ export async function generateShareCard(data: ShareCardData): Promise<Blob> {
213250

214251
// Username
215252
ctx.font = "bold 38px Inter, system-ui, -apple-system, sans-serif";
216-
ctx.fillStyle = "#ffffff";
253+
ctx.fillStyle = "#0f172a"; // Deep slate
217254
ctx.fillText(`@${data.username}`, 260, 275);
218255

219256
// Rank Pill
220257
const pillX = 260;
221258
const pillY = 295;
222-
const pillW = 145;
259+
const pillW = 165;
223260
const pillH = 34;
224261
const pillR = 17;
225262

226263
ctx.save();
227264
drawRoundedRect(ctx, pillX, pillY, pillW, pillH, pillR);
228-
ctx.fillStyle = "rgba(99, 102, 241, 0.15)";
265+
ctx.fillStyle = "rgba(99, 102, 241, 0.08)";
229266
ctx.fill();
230-
ctx.strokeStyle = "rgba(99, 102, 241, 0.4)";
267+
ctx.strokeStyle = "rgba(99, 102, 241, 0.25)";
231268
ctx.lineWidth = 1.2;
232269
ctx.stroke();
233270

234271
ctx.font = "bold 15px Inter, system-ui, -apple-system, sans-serif";
235-
ctx.fillStyle = "#93c5fd"; // Sky blue text
272+
ctx.fillStyle = "#4f46e5"; // Indigo/violet rank text
236273
ctx.textAlign = "center";
237274
ctx.fillText(`Leaderboard Rank #${data.rank}`, pillX + pillW / 2, pillY + 22);
238275
ctx.restore();
239276

240277
// 7. Draw Divider & Stats Section
241278
// Divider
242-
ctx.strokeStyle = "rgba(255, 255, 255, 0.08)";
279+
ctx.strokeStyle = "rgba(226, 232, 240, 0.8)";
243280
ctx.lineWidth = 1.5;
244281
ctx.beginPath();
245282
ctx.moveTo(760, 220);
@@ -249,24 +286,24 @@ export async function generateShareCard(data: ShareCardData): Promise<Blob> {
249286
// Merged PRs Column
250287
ctx.textAlign = "center";
251288
ctx.font = "bold 68px Inter, system-ui, -apple-system, sans-serif";
252-
ctx.fillStyle = "#ffffff";
289+
ctx.fillStyle = "#0f172a"; // Deep slate
253290
ctx.fillText(data.prs.toString(), 640, 280);
254291

255292
ctx.font = "bold 14px Inter, system-ui, -apple-system, sans-serif";
256-
ctx.fillStyle = "#64748b";
293+
ctx.fillStyle = "#64748b"; // Slate grey label
257294
ctx.fillText("MERGED PRS", 640, 310);
258295

259296
// Total Points Column
260297
ctx.font = "bold 68px Inter, system-ui, -apple-system, sans-serif";
261-
ctx.fillStyle = "#8b5cf6"; // Highlight purple
298+
ctx.fillStyle = "#7c3aed"; // Violet highlight
262299
ctx.fillText(data.points.toString(), 880, 280);
263300

264301
ctx.font = "bold 14px Inter, system-ui, -apple-system, sans-serif";
265-
ctx.fillStyle = "#64748b";
302+
ctx.fillStyle = "#64748b"; // Slate grey label
266303
ctx.fillText("TOTAL POINTS", 880, 310);
267304

268305
// 8. Draw Lower Divider & Badges Section
269-
ctx.strokeStyle = "rgba(255, 255, 255, 0.08)";
306+
ctx.strokeStyle = "rgba(226, 232, 240, 0.8)";
270307
ctx.beginPath();
271308
ctx.moveTo(100, 395);
272309
ctx.lineTo(1100, 395);
@@ -278,19 +315,6 @@ export async function generateShareCard(data: ShareCardData): Promise<Blob> {
278315
ctx.fillStyle = "#64748b";
279316
ctx.fillText("UNLOCKED CONTRIBUTOR ACHIEVEMENTS", 100, 430);
280317

281-
// Load and Draw all Badges
282-
// Pre-load all images to draw them inline
283-
const badgePromises = data.allBadges.map(async (badge) => {
284-
try {
285-
const img = await loadImage(badge.image);
286-
return { image: badge.image, img, loaded: true };
287-
} catch {
288-
return { image: badge.image, img: null, loaded: false };
289-
}
290-
});
291-
292-
const loadedBadges = await Promise.all(badgePromises);
293-
294318
// Position and draw badges
295319
const badgeWidth = 64;
296320
const badgeHeight = 64;
@@ -305,8 +329,8 @@ export async function generateShareCard(data: ShareCardData): Promise<Blob> {
305329
ctx.save();
306330
if (isEarned) {
307331
ctx.globalAlpha = 1.0;
308-
// Add subtle drop shadow under earned badges
309-
ctx.shadowColor = "rgba(139, 92, 246, 0.4)";
332+
// Soft drop shadow under earned badges for light theme
333+
ctx.shadowColor = "rgba(99, 102, 241, 0.2)";
310334
ctx.shadowBlur = 12;
311335
ctx.shadowOffsetY = 4;
312336
} else {
@@ -326,7 +350,7 @@ export async function generateShareCard(data: ShareCardData): Promise<Blob> {
326350
0,
327351
Math.PI * 2
328352
);
329-
ctx.fillStyle = isEarned ? "#8b5cf6" : "#334155";
353+
ctx.fillStyle = isEarned ? "#7c3aed" : "#cbd5e1";
330354
ctx.fill();
331355
}
332356
ctx.restore();

0 commit comments

Comments
 (0)