Skip to content

Commit 2d14b94

Browse files
committed
feat: rewrite typing generator, fix cursor behavior, improve langs legend
- generate-typing.js now builds SVG from scratch (no template file dependency) - Switch typing font to Consolas stack - Fix cursor offset via translateX, keep cursor visible between lines, 1.2s blink - Improve langs legend: brighter colors, 12px/500 weight, aligned dots - Add mkdirSync(dirname(outPath)) before writeFileSync in all generator scripts
1 parent 4752638 commit 2d14b94

9 files changed

Lines changed: 128 additions & 138 deletions

assets/langs.svg

Lines changed: 8 additions & 13 deletions
Loading

assets/typing.svg

Lines changed: 36 additions & 44 deletions
Loading

scripts/generate-divider.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// easy-github-profile — github.com/BerkaySevinc/easy-github-profile
22
// Copyright (c) 2025 BerkaySevinc — MIT License
33

4-
const { writeFileSync } = require('fs');
5-
const { join } = require('path');
4+
const { writeFileSync, mkdirSync } = require('fs');
5+
const { join, dirname } = require('path');
66

77
const AMPLITUDE = 10;
88
const GAP = 0;
@@ -73,6 +73,7 @@ function main() {
7373
</svg>`;
7474

7575
const outPath = join(__dirname, '..', 'assets', 'divider.svg');
76+
mkdirSync(dirname(outPath), { recursive: true });
7677
writeFileSync(outPath, svg, 'utf8');
7778
console.log('Generated assets/divider.svg');
7879
}

scripts/generate-footer.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// easy-github-profile — github.com/BerkaySevinc/easy-github-profile
22
// Copyright (c) 2025 BerkaySevinc — MIT License
33

4-
const { writeFileSync } = require('fs');
5-
const { join } = require('path');
4+
const { writeFileSync, mkdirSync } = require('fs');
5+
const { join, dirname } = require('path');
66

77
function main() {
88
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1500 160" width="100%" height="100%">
@@ -79,6 +79,7 @@ V 350 H 0 Z">
7979
</svg>`;
8080

8181
const outPath = join(__dirname, '..', 'assets', 'footer.svg');
82+
mkdirSync(dirname(outPath), { recursive: true });
8283
writeFileSync(outPath, svg, 'utf8');
8384
console.log('Generated assets/footer.svg');
8485
}

scripts/generate-header.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// easy-github-profile — github.com/BerkaySevinc/easy-github-profile
22
// Copyright (c) 2025 BerkaySevinc — MIT License
33

4-
const { writeFileSync, readFileSync } = require('fs');
5-
const { join } = require('path');
4+
const { writeFileSync, readFileSync, mkdirSync } = require('fs');
5+
const { join, dirname } = require('path');
66

77
const MAX_BIO_LENGTH = 80;
88

@@ -44,6 +44,7 @@ async function main() {
4444

4545
const svg = buildSvg(displayName, displayBio);
4646
const outPath = join(__dirname, '..', 'assets', 'header.svg');
47+
mkdirSync(dirname(outPath), { recursive: true });
4748
writeFileSync(outPath, svg, 'utf8');
4849
console.log(`Generated assets/header.svg — name: "${displayName}"`);
4950
}

scripts/generate-langs.js

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// easy-github-profile — github.com/BerkaySevinc/easy-github-profile
22
// Copyright (c) 2025 BerkaySevinc — MIT License
33

4-
const { writeFileSync } = require('fs');
5-
const { join } = require('path');
4+
const { writeFileSync, mkdirSync } = require('fs');
5+
const { join, dirname } = require('path');
66

77
const MAX_LANGS = 6;
88
const BAR_X = 20, BAR_Y = 42, BAR_W = 760, BAR_H = 22;
@@ -81,15 +81,6 @@ function buildSvg(langs) {
8181
barX += segW;
8282
}
8383

84-
// Legend items
85-
const itemW = Math.floor((W - 40) / langs.length);
86-
let legend = '';
87-
for (let i = 0; i < langs.length; i++) {
88-
const lx = 20 + i * itemW;
89-
legend += ` <circle cx="${lx + 5}" cy="88" r="5" fill="${langs[i].color}"/>\n`;
90-
legend += ` <text x="${lx + 16}" y="93" class="leg">${escapeXml(langs[i].name)} ${(withPct[i].pct * 100).toFixed(1)}%</text>\n`;
91-
}
92-
9384
// Legend circles with pulse animation (staggered per language)
9485
const itemW = Math.floor((W - 40) / langs.length);
9586
let legendAnimated = '';
@@ -99,10 +90,8 @@ function buildSvg(langs) {
9990
legendAnimated += ` <circle cx="${lx + 5}" cy="88" r="5" fill="${langs[i].color}">
10091
<animate attributeName="r" values="5;6.5;5" dur="2.5s" begin="${delay}s" repeatCount="indefinite"
10192
calcMode="spline" keyTimes="0;0.5;1" keySplines="0.4 0 0.6 1;0.4 0 0.6 1"/>
102-
<animate attributeName="opacity" values="1;0.6;1" dur="2.5s" begin="${delay}s" repeatCount="indefinite"
103-
calcMode="spline" keyTimes="0;0.5;1" keySplines="0.4 0 0.6 1;0.4 0 0.6 1"/>
10493
</circle>\n`;
105-
legendAnimated += ` <text x="${lx + 16}" y="88" class="leg" dominant-baseline="central">${escapeXml(langs[i].name)} ${(withPct[i].pct * 100).toFixed(1)}%</text>\n`;
94+
legendAnimated += ` <text x="${lx + 16}" y="92.5" class="leg">${escapeXml(langs[i].name)} ${(withPct[i].pct * 100).toFixed(1)}%</text>\n`;
10695
}
10796

10897
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
@@ -132,15 +121,15 @@ function buildSvg(langs) {
132121
@media (prefers-color-scheme: dark) {
133122
.ttl { fill: #e6edf3; }
134123
.trk { fill: #21262d; }
135-
.leg { fill: #8b949e; }
124+
.leg { fill: #b1bac4; }
136125
}
137126
@media (prefers-color-scheme: light) {
138127
.ttl { fill: #1f2328; }
139128
.trk { fill: #eaeef2; }
140-
.leg { fill: #636e7b; }
129+
.leg { fill: #424a53; }
141130
}
142131
.ttl { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif; font-size: 14px; font-weight: 600; }
143-
.leg { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif; font-size: 11px; }
132+
.leg { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif; font-size: 12px; font-weight: 500; }
144133
</style>
145134
146135
<text class="ttl" x="${W / 2}" y="24" text-anchor="middle">Top Languages</text>
@@ -178,7 +167,9 @@ async function main() {
178167
}
179168

180169
const langs = await fetchLangs(owner, process.env.GITHUB_TOKEN);
181-
writeFileSync(join(__dirname, '..', 'assets', 'langs.svg'), buildSvg(langs), 'utf8');
170+
const outPath = join(__dirname, '..', 'assets', 'langs.svg');
171+
mkdirSync(dirname(outPath), { recursive: true });
172+
writeFileSync(outPath, buildSvg(langs), 'utf8');
182173
console.log(`Generated assets/langs.svg — ${langs.map(l => l.name).join(', ')}`);
183174
}
184175

scripts/generate-sections.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ function main() {
146146
}
147147

148148
const outDir = join(__dirname, '..', 'assets', 'sections');
149-
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
149+
mkdirSync(outDir, { recursive: true });
150150

151151
for (const section of sections) {
152152
if (!section.label || !section.badges?.length) {

scripts/generate-stats.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// easy-github-profile — github.com/BerkaySevinc/easy-github-profile
22
// Copyright (c) 2025 BerkaySevinc — MIT License
33

4-
const { writeFileSync } = require('fs');
5-
const { join } = require('path');
4+
const { writeFileSync, mkdirSync } = require('fs');
5+
const { join, dirname } = require('path');
66

77
async function fetchStats(owner, token) {
88
const headers = { 'User-Agent': 'github-profile-generator', 'Content-Type': 'application/json' };
@@ -111,7 +111,9 @@ async function main() {
111111
}
112112

113113
const stats = await fetchStats(owner, process.env.GITHUB_TOKEN);
114-
writeFileSync(join(__dirname, '..', 'assets', 'stats.svg'), buildSvg(stats), 'utf8');
114+
const outPath = join(__dirname, '..', 'assets', 'stats.svg');
115+
mkdirSync(dirname(outPath), { recursive: true });
116+
writeFileSync(outPath, buildSvg(stats), 'utf8');
115117
console.log(`Generated assets/stats.svg — commits: ${stats.commits}, stars: ${stats.stars}, repos: ${stats.repos}`);
116118
}
117119

scripts/generate-typing.js

Lines changed: 60 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// easy-github-profile — github.com/BerkaySevinc/easy-github-profile
22
// Copyright (c) 2025 BerkaySevinc — MIT License
33

4-
const { writeFileSync, readFileSync } = require('fs');
5-
const { join } = require('path');
4+
const { writeFileSync, readFileSync, mkdirSync } = require('fs');
5+
const { join, dirname } = require('path');
66

7-
const CHAR_WIDTH = 13.2;
8-
const SVG_WIDTH = 800;
9-
const CYCLE_SECS = 20;
10-
const FONT_MARKER = '/* Clip rect animations */';
7+
const CHAR_WIDTH = 13.2;
8+
const SVG_WIDTH = 800;
9+
const CYCLE_SECS = 20;
10+
const CURSOR_OFFSET = 2; // px gap between last typed char and cursor
1111

1212

1313
function loadConfig() {
@@ -32,10 +32,10 @@ function r(n) {
3232
return parseFloat(n.toFixed(2));
3333
}
3434

35-
function buildDynamicCss(lines) {
35+
function buildCss(lines) {
3636
const N = lines.length;
3737
const window = 100 / N;
38-
let css = FONT_MARKER + '\n';
38+
let css = '';
3939

4040
// Clip rect animation class references
4141
for (let i = 0; i < N; i++) {
@@ -44,13 +44,13 @@ function buildDynamicCss(lines) {
4444

4545
// Clip rect keyframes
4646
for (let i = 0; i < N; i++) {
47-
const n = i + 1;
48-
const chars = lines[i].length;
49-
const width = r(chars * CHAR_WIDTH);
50-
const start = r(i * window);
51-
const typeEnd = r(start + window * 0.35);
52-
const pauseEnd = r(start + window * 0.65);
53-
const deleteEnd = r(start + window * 0.85);
47+
const n = i + 1;
48+
const chars = lines[i].length;
49+
const width = r(chars * CHAR_WIDTH);
50+
const start = r(i * window);
51+
const typeEnd = r(start + window * 0.35);
52+
const pauseEnd = r(start + window * 0.65);
53+
const deleteEnd = r(start + window * 0.85);
5454

5555
css += `\n /* Line ${n}: ${escapeXml(lines[i])}${chars} chars — ${width}px */\n`;
5656
css += ` @keyframes cl${n} {\n`;
@@ -70,27 +70,40 @@ function buildDynamicCss(lines) {
7070

7171
// Cursor wrapper keyframes
7272
for (let i = 0; i < N; i++) {
73-
const n = i + 1;
74-
const chars = lines[i].length;
75-
const width = r(chars * CHAR_WIDTH);
76-
const start = r(i * window);
77-
const typeEnd = r(start + window * 0.35);
78-
const pauseEnd = r(start + window * 0.65);
79-
const deleteEnd = r(start + window * 0.85);
73+
const n = i + 1;
74+
const chars = lines[i].length;
75+
const width = r(chars * CHAR_WIDTH);
76+
const start = r(i * window);
77+
const typeEnd = r(start + window * 0.35);
78+
const pauseEnd = r(start + window * 0.65);
79+
const deleteEnd = r(start + window * 0.85);
80+
const isLast = i === N - 1;
81+
const nextStart = isLast ? null : r((i + 1) * window);
8082

8183
css += `\n @keyframes cw${n} {\n`;
8284
if (i > 0) {
83-
css += ` 0%, ${r(start - 0.1)}% { visibility: hidden; transform: translateX(0px); }\n`;
85+
css += ` 0%, ${r(start - 0.1)}% { visibility: hidden; transform: translateX(${CURSOR_OFFSET}px); }\n`;
86+
}
87+
css += ` ${start}% { transform: translateX(${CURSOR_OFFSET}px); visibility: visible; animation-timing-function: steps(${chars}, end); }\n`;
88+
css += ` ${typeEnd}% { transform: translateX(${width + CURSOR_OFFSET}px); visibility: visible; animation-timing-function: step-end; }\n`;
89+
css += ` ${pauseEnd}% { transform: translateX(${width + CURSOR_OFFSET}px); visibility: visible; animation-timing-function: steps(${chars}, end); }\n`;
90+
css += ` ${deleteEnd}% { transform: translateX(${CURSOR_OFFSET}px); visibility: visible; }\n`;
91+
if (!isLast) {
92+
css += ` ${r(nextStart - 0.1)}%, 100% { visibility: hidden; transform: translateX(${CURSOR_OFFSET}px); }\n`;
93+
} else {
94+
css += ` 100% { visibility: visible; transform: translateX(${CURSOR_OFFSET}px); }\n`;
8495
}
85-
css += ` ${start}% { transform: translateX(0px); visibility: visible; animation-timing-function: steps(${chars}, end); }\n`;
86-
css += ` ${typeEnd}% { transform: translateX(${width}px); visibility: visible; animation-timing-function: step-end; }\n`;
87-
css += ` ${pauseEnd}% { transform: translateX(${width}px); visibility: visible; animation-timing-function: steps(${chars}, end); }\n`;
88-
css += ` ${deleteEnd}% { transform: translateX(0px); visibility: visible; }\n`;
89-
css += ` ${r(deleteEnd + 0.01)}% { visibility: hidden; transform: translateX(0px); }\n`;
90-
css += ` 100% { visibility: hidden; transform: translateX(0px); }\n`;
9196
css += ` }\n`;
9297
}
9398

99+
// Cursor blink
100+
css += `\n /* Cursor blink */\n`;
101+
css += ` .cur { fill: #A78BFA; animation: blink 1.2s step-end infinite; }\n`;
102+
css += ` @keyframes blink {\n`;
103+
css += ` 0%, 100% { opacity: 1; }\n`;
104+
css += ` 50% { opacity: 0; }\n`;
105+
css += ` }\n`;
106+
94107
return css;
95108
}
96109

@@ -119,12 +132,27 @@ function buildSvgBody(lines) {
119132
// cursor elements
120133
out += '\n<!-- Cursors -->\n';
121134
for (let i = 0; i < lines.length; i++) {
122-
out += `<g class="cw${i + 1}"><rect class="cur" x="${metrics[i].x}" y="16" width="2.5" height="22"/></g>\n`;
135+
out += `<g class="cw${i + 1}"><rect class="cur" x="${metrics[i].x}" y="16" width="3.5" height="22"/></g>\n`;
123136
}
124137

125138
return out;
126139
}
127140

141+
function buildSvg(lines) {
142+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${SVG_WIDTH}" height="54" viewBox="0 0 ${SVG_WIDTH} 54">
143+
<style>
144+
.t {
145+
font-family: 'Consolas', 'Monaco', 'Lucida Console', 'Courier New', monospace;
146+
font-size: 22px;
147+
font-weight: 700;
148+
fill: #A78BFA;
149+
}
150+
${buildCss(lines)}</style>
151+
152+
${buildSvgBody(lines)}
153+
</svg>`;
154+
}
155+
128156
async function fetchProfileLines(owner) {
129157
const headers = { 'User-Agent': 'github-profile-generator' };
130158
if (process.env.GITHUB_TOKEN) headers['Authorization'] = `Bearer ${process.env.GITHUB_TOKEN}`;
@@ -155,30 +183,9 @@ async function main() {
155183
lines = await fetchProfileLines(owner);
156184
}
157185

158-
const existing = readFileSync(join(__dirname, '..', 'assets', 'typing.svg'), 'utf8');
159-
const markerIdx = existing.indexOf(FONT_MARKER);
160-
if (markerIdx === -1) {
161-
console.error('Error: Font marker not found in assets/typing.svg');
162-
process.exit(1);
163-
}
164-
165-
const fontPrefix = existing.slice(0, markerIdx);
166-
167-
const fixedCssTail = `
168-
/* Cursor blink */
169-
.cur { fill: #A78BFA; animation: blink 0.8s step-end infinite; }
170-
@keyframes blink {
171-
0%, 100% { opacity: 1; }
172-
50% { opacity: 0; }
173-
}
174-
</style>
175-
176-
`;
177-
178-
const svg = fontPrefix + buildDynamicCss(lines) + fixedCssTail + buildSvgBody(lines) + '\n</svg>';
179-
180186
const outPath = join(__dirname, '..', 'assets', 'typing.svg');
181-
writeFileSync(outPath, svg, 'utf8');
187+
mkdirSync(dirname(outPath), { recursive: true });
188+
writeFileSync(outPath, buildSvg(lines), 'utf8');
182189
console.log(`Generated assets/typing.svg — ${lines.length} lines`);
183190
}
184191

0 commit comments

Comments
 (0)