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
1313function 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+
128156async 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