headreel 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/assets/fonts/JetBrainsMono-Regular.ttf +0 -0
- package/assets/fonts/OFL-JetBrainsMono.txt +93 -0
- package/assets/fonts/OFL-SpaceGrotesk.txt +93 -0
- package/assets/fonts/SpaceGrotesk-Bold.ttf +0 -0
- package/assets/fonts/SpaceGrotesk-Medium.ttf +0 -0
- package/dist/cli/args.js +133 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/run.js +83 -0
- package/dist/core/canvas.js +2 -0
- package/dist/core/data/contributions.js +63 -0
- package/dist/core/data/errors.js +12 -0
- package/dist/core/data/fixture.js +18 -0
- package/dist/core/data/graphql.js +65 -0
- package/dist/core/data/profile.js +13 -0
- package/dist/core/data/token.js +26 -0
- package/dist/core/encode/gif.js +92 -0
- package/dist/core/fonts.js +22 -0
- package/dist/core/pipeline.js +22 -0
- package/dist/core/prng.js +19 -0
- package/dist/core/render/dom.js +66 -0
- package/dist/core/render/render.js +46 -0
- package/dist/styles/contribution-city/city.js +123 -0
- package/dist/styles/contribution-city/index.js +14 -0
- package/dist/styles/contribution-city/options.js +7 -0
- package/dist/styles/contribution-city/sketch.js +289 -0
- package/dist/styles/index.js +5 -0
- package/dist/styles/types.js +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { cityOrigin, LAYOUT, tileAt } from './city.js';
|
|
2
|
+
const PALETTE = {
|
|
3
|
+
skyTop: '#07090c',
|
|
4
|
+
skyBottom: '#1c1917',
|
|
5
|
+
skyFloor: '#0c0a09',
|
|
6
|
+
front: [20, 28, 34],
|
|
7
|
+
side: [12, 18, 23],
|
|
8
|
+
top: [8, 145, 178],
|
|
9
|
+
window: [165, 243, 252],
|
|
10
|
+
cyan: '#22d3ee',
|
|
11
|
+
ink: '#f5f5f4',
|
|
12
|
+
muted: '#a8a29e',
|
|
13
|
+
};
|
|
14
|
+
const SANS = 'Space Grotesk';
|
|
15
|
+
const MONO = 'JetBrains Mono';
|
|
16
|
+
const TEXT_X = 48;
|
|
17
|
+
const PROMPT = '~/github';
|
|
18
|
+
const { width: W, height: H, depthX, depthY, baseY } = LAYOUT;
|
|
19
|
+
/** 0..1, periodic in `phase`, so every animated value loops seamlessly. */
|
|
20
|
+
function wave(phase, cycles, off) {
|
|
21
|
+
return 0.5 + 0.5 * Math.sin(Math.PI * 2 * (phase * cycles + off));
|
|
22
|
+
}
|
|
23
|
+
function easeInOut(t) {
|
|
24
|
+
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
25
|
+
}
|
|
26
|
+
/** "https://www.example.com/" -> "www.example.com" */
|
|
27
|
+
function displayUrl(url) {
|
|
28
|
+
return url.replace(/^[a-z]+:\/\//i, '').replace(/\/+$/, '');
|
|
29
|
+
}
|
|
30
|
+
export function createCitySketch(city, identity, rng) {
|
|
31
|
+
const origin = cityOrigin(city.weeks);
|
|
32
|
+
const noiseSeed = Math.floor(rng() * 2 ** 31);
|
|
33
|
+
const grainSeed = Math.floor(rng() * 2 ** 31);
|
|
34
|
+
let sky;
|
|
35
|
+
let grain;
|
|
36
|
+
const scanPosition = (phase) => {
|
|
37
|
+
const start = origin.x - 100;
|
|
38
|
+
return start + (W + 100 - start) * phase;
|
|
39
|
+
};
|
|
40
|
+
function renderSky(p) {
|
|
41
|
+
const g = p.createGraphics(W, H);
|
|
42
|
+
g.pixelDensity(1);
|
|
43
|
+
const ctx = g.drawingContext;
|
|
44
|
+
const grad = ctx.createLinearGradient(0, 0, 0, H);
|
|
45
|
+
grad.addColorStop(0, PALETTE.skyTop);
|
|
46
|
+
grad.addColorStop(0.75, PALETTE.skyBottom);
|
|
47
|
+
grad.addColorStop(1, PALETTE.skyFloor);
|
|
48
|
+
ctx.fillStyle = grad;
|
|
49
|
+
ctx.fillRect(0, 0, W, H);
|
|
50
|
+
// Horizon glow behind the skyline.
|
|
51
|
+
const gx = origin.x + 520;
|
|
52
|
+
const gy = baseY - 40;
|
|
53
|
+
const glow = ctx.createRadialGradient(gx, gy, 10, gx, gy, 620);
|
|
54
|
+
glow.addColorStop(0, 'rgba(8,145,178,0.22)');
|
|
55
|
+
glow.addColorStop(0.5, 'rgba(8,145,178,0.06)');
|
|
56
|
+
glow.addColorStop(1, 'rgba(8,145,178,0)');
|
|
57
|
+
ctx.fillStyle = glow;
|
|
58
|
+
ctx.fillRect(0, 0, W, H);
|
|
59
|
+
// Faint contour lines from warped noise.
|
|
60
|
+
p.noiseSeed(noiseSeed);
|
|
61
|
+
g.noFill();
|
|
62
|
+
g.strokeWeight(0.6);
|
|
63
|
+
for (let k = 0; k < 7; k++) {
|
|
64
|
+
g.stroke(34, 211, 238, 10 + k * 1.5);
|
|
65
|
+
g.beginShape();
|
|
66
|
+
for (let x = 0; x <= W; x += 8) {
|
|
67
|
+
const n = p.noise(x * 0.0025, k * 0.4);
|
|
68
|
+
g.vertex(x, 80 + k * 26 + (p.noise(x * 0.004 + n * 2, k) - 0.5) * 70);
|
|
69
|
+
}
|
|
70
|
+
g.endShape();
|
|
71
|
+
}
|
|
72
|
+
return g;
|
|
73
|
+
}
|
|
74
|
+
function renderGrain(p) {
|
|
75
|
+
const g = p.createGraphics(W, H);
|
|
76
|
+
g.pixelDensity(1);
|
|
77
|
+
g.noStroke();
|
|
78
|
+
let s = grainSeed;
|
|
79
|
+
const next = () => {
|
|
80
|
+
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
|
|
81
|
+
return s / 4294967296;
|
|
82
|
+
};
|
|
83
|
+
for (let i = 0; i < LAYOUT.grain; i++) {
|
|
84
|
+
g.fill(255, 4 + next() * 8);
|
|
85
|
+
g.rect(next() * W, next() * H, 1, 1);
|
|
86
|
+
}
|
|
87
|
+
return g;
|
|
88
|
+
}
|
|
89
|
+
function drawStars(p, phase) {
|
|
90
|
+
p.noStroke();
|
|
91
|
+
for (const s of city.stars) {
|
|
92
|
+
p.fill(226, 244, 250, s.a * (0.35 + 0.65 * wave(phase, s.cycles, s.off)));
|
|
93
|
+
p.rect(s.x, s.y, s.s, s.s);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function drawGround(p) {
|
|
97
|
+
const back = tileAt(city.weeks, 0, 0);
|
|
98
|
+
const frontRight = tileAt(city.weeks, city.weeks, 6);
|
|
99
|
+
p.noStroke();
|
|
100
|
+
p.fill(14, 18, 22);
|
|
101
|
+
p.quad(origin.x - 14, origin.y + 6, frontRight.x + 14, origin.y + 6, frontRight.x + 14 + 7 * depthX, back.y - depthY - 4, back.x - 14 + depthX, back.y - depthY - 4);
|
|
102
|
+
// Street grid, one line per weekday row.
|
|
103
|
+
p.stroke(34, 211, 238, 18);
|
|
104
|
+
p.strokeWeight(0.6);
|
|
105
|
+
for (let d = 0; d <= 7; d++) {
|
|
106
|
+
const y = origin.y - d * depthY + 2;
|
|
107
|
+
p.line(origin.x + d * depthX - 8, y, frontRight.x + d * depthX + 8, y);
|
|
108
|
+
}
|
|
109
|
+
p.stroke(34, 211, 238, 60);
|
|
110
|
+
p.strokeWeight(1);
|
|
111
|
+
p.line(origin.x - 14, origin.y + 6, frontRight.x + 14, origin.y + 6);
|
|
112
|
+
}
|
|
113
|
+
function drawBuilding(p, b, phase, scanX) {
|
|
114
|
+
const { x, y, bw: w, h } = b;
|
|
115
|
+
const dx = depthX * 0.8;
|
|
116
|
+
const dy = depthY * 0.8;
|
|
117
|
+
const hit = Math.max(0, 1 - Math.abs(x + w / 2 - scanX) / 70);
|
|
118
|
+
if (h === 0) {
|
|
119
|
+
p.noStroke();
|
|
120
|
+
p.fill(34, 211, 238, 10 + hit * 40);
|
|
121
|
+
p.quad(x, y, x + w, y, x + w + dx, y - dy, x + dx, y - dy);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const lum = 0.55 + b.t * 0.45;
|
|
125
|
+
const [fr, fg, fb] = PALETTE.front;
|
|
126
|
+
const [sr, sg, sb] = PALETTE.side;
|
|
127
|
+
const [tr, tg, tb] = PALETTE.top;
|
|
128
|
+
p.noStroke();
|
|
129
|
+
p.fill(sr + hit * 10, sg + hit * 30, sb + hit * 38);
|
|
130
|
+
p.quad(x + w, y, x + w + dx, y - dy, x + w + dx, y - dy - h, x + w, y - h);
|
|
131
|
+
p.fill(fr * lum + hit * 12, fg * lum + hit * 40, fb * lum + hit * 50);
|
|
132
|
+
p.rect(x, y - h, w, h);
|
|
133
|
+
p.fill(tr + hit * 60, tg + hit * 80, tb + hit * 60, Math.min(255, 90 + b.t * 165 + hit * 80));
|
|
134
|
+
p.quad(x, y - h, x + w, y - h, x + w + dx, y - h - dy, x + dx, y - h - dy);
|
|
135
|
+
p.stroke(165, 243, 252, 40 + b.t * 120 + hit * 90);
|
|
136
|
+
p.strokeWeight(0.8);
|
|
137
|
+
p.line(x, y - h, x + w, y - h);
|
|
138
|
+
p.noStroke();
|
|
139
|
+
const [wr, wg, wb] = PALETTE.window;
|
|
140
|
+
for (const win of b.windows) {
|
|
141
|
+
const on = win.cycles ? wave(phase, win.cycles, win.off) > 0.5 : win.lit;
|
|
142
|
+
const a = on ? 150 + b.t * 80 : 18;
|
|
143
|
+
p.fill(wr, wg, wb, Math.min(255, a + hit * 120));
|
|
144
|
+
p.rect(x + win.x, y - h + win.y, 3, 2);
|
|
145
|
+
}
|
|
146
|
+
if (b.beacon) {
|
|
147
|
+
const pulse = wave(phase, 2, b.beaconOffset);
|
|
148
|
+
const cx = x + w / 2 + dx / 2;
|
|
149
|
+
const cy = y - h - dy / 2 - 5;
|
|
150
|
+
p.stroke(245, 158, 11, 120);
|
|
151
|
+
p.strokeWeight(1);
|
|
152
|
+
p.line(cx, cy + 4, cx, cy);
|
|
153
|
+
p.noStroke();
|
|
154
|
+
p.fill(245, 158, 11, 30 + pulse * 60);
|
|
155
|
+
p.circle(cx, cy, 8 + pulse * 8);
|
|
156
|
+
p.fill(253, 230, 138, 180 + pulse * 75);
|
|
157
|
+
p.circle(cx, cy, 3);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function roofPoint(b) {
|
|
161
|
+
return { x: b.x + b.bw / 2 + depthX * 0.4, y: b.y - b.h - depthY * 0.4 };
|
|
162
|
+
}
|
|
163
|
+
function drawArc(p, arc, phase) {
|
|
164
|
+
const p0 = roofPoint(arc.a);
|
|
165
|
+
const p3 = roofPoint(arc.b);
|
|
166
|
+
const top = Math.min(p0.y, p3.y) - arc.lift;
|
|
167
|
+
const c1x = p0.x + (p3.x - p0.x) * 0.25;
|
|
168
|
+
const c2x = p0.x + (p3.x - p0.x) * 0.75;
|
|
169
|
+
p.noFill();
|
|
170
|
+
p.stroke(34, 211, 238, 34);
|
|
171
|
+
p.strokeWeight(0.8);
|
|
172
|
+
p.bezier(p0.x, p0.y, c1x, top, c2x, top, p3.x, p3.y);
|
|
173
|
+
// A packet travels the arc once per loop, trailed by fading dots.
|
|
174
|
+
const t = (phase + arc.off) % 1;
|
|
175
|
+
const eased = easeInOut(t);
|
|
176
|
+
const fade = Math.sin(Math.PI * t);
|
|
177
|
+
p.noStroke();
|
|
178
|
+
for (let k = 6; k >= 0; k--) {
|
|
179
|
+
const s = Math.max(0, eased - k * 0.012);
|
|
180
|
+
const px = p.bezierPoint(p0.x, c1x, c2x, p3.x, s);
|
|
181
|
+
const py = p.bezierPoint(p0.y, top, top, p3.y, s);
|
|
182
|
+
p.fill(165, 243, 252, (k === 0 ? 255 : 110 - k * 14) * fade);
|
|
183
|
+
p.circle(px, py, k === 0 ? 4 : 3 - k * 0.3);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function drawScanBeam(p, scanX, phase) {
|
|
187
|
+
// Fade at both ends of the sweep so the loop seam is invisible.
|
|
188
|
+
const k = Math.min(1, phase / 0.06, (1 - phase) / 0.06);
|
|
189
|
+
const ctx = p.drawingContext;
|
|
190
|
+
ctx.globalAlpha = k;
|
|
191
|
+
const g = ctx.createLinearGradient(scanX - 60, 0, scanX + 60, 0);
|
|
192
|
+
g.addColorStop(0, 'rgba(34,211,238,0)');
|
|
193
|
+
g.addColorStop(0.5, 'rgba(34,211,238,0.07)');
|
|
194
|
+
g.addColorStop(1, 'rgba(34,211,238,0)');
|
|
195
|
+
ctx.fillStyle = g;
|
|
196
|
+
ctx.fillRect(scanX - 60, 150, 120, baseY - 140);
|
|
197
|
+
p.stroke(165, 243, 252, 70);
|
|
198
|
+
p.strokeWeight(1);
|
|
199
|
+
p.line(scanX, baseY + 8, scanX, baseY + 16);
|
|
200
|
+
ctx.globalAlpha = 1;
|
|
201
|
+
}
|
|
202
|
+
function drawMonths(p, scanX) {
|
|
203
|
+
p.textFont(MONO);
|
|
204
|
+
p.textStyle(p.NORMAL);
|
|
205
|
+
p.textSize(10);
|
|
206
|
+
p.textAlign(p.LEFT, p.TOP);
|
|
207
|
+
p.noStroke();
|
|
208
|
+
for (const m of city.months) {
|
|
209
|
+
const t = tileAt(city.weeks, m.w, 6);
|
|
210
|
+
const hit = Math.max(0, 1 - Math.abs(t.x - scanX) / 80);
|
|
211
|
+
p.fill(168, 162, 158, 110 + hit * 145);
|
|
212
|
+
p.text(m.label, t.x, baseY + 14);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function drawText(p, phase) {
|
|
216
|
+
const x = TEXT_X;
|
|
217
|
+
const ctx = p.drawingContext;
|
|
218
|
+
p.noStroke();
|
|
219
|
+
p.textAlign(p.LEFT, p.BASELINE);
|
|
220
|
+
// Prompt line with a blinking block cursor.
|
|
221
|
+
p.textFont(MONO);
|
|
222
|
+
p.textStyle(p.NORMAL);
|
|
223
|
+
p.textSize(13);
|
|
224
|
+
p.fill(PALETTE.cyan);
|
|
225
|
+
p.text(PROMPT, x, 58);
|
|
226
|
+
if (wave(phase, 3, 0) > 0.5) {
|
|
227
|
+
p.rect(x + p.textWidth(PROMPT) + 4, 47, 7, 13);
|
|
228
|
+
}
|
|
229
|
+
p.textFont(SANS);
|
|
230
|
+
p.textStyle(p.BOLD);
|
|
231
|
+
p.textSize(50);
|
|
232
|
+
ctx.letterSpacing = '3px';
|
|
233
|
+
p.fill(PALETTE.ink);
|
|
234
|
+
p.text(identity.name.toUpperCase(), x, 116);
|
|
235
|
+
ctx.letterSpacing = '0px';
|
|
236
|
+
if (identity.tagline) {
|
|
237
|
+
p.textStyle(p.NORMAL);
|
|
238
|
+
p.textSize(17);
|
|
239
|
+
p.fill(PALETTE.cyan);
|
|
240
|
+
p.text(identity.tagline, x, 146);
|
|
241
|
+
}
|
|
242
|
+
p.textFont(MONO);
|
|
243
|
+
p.textStyle(p.NORMAL);
|
|
244
|
+
p.textSize(11);
|
|
245
|
+
p.fill(PALETTE.muted);
|
|
246
|
+
p.text('LAST 12 MONTHS', x, 206);
|
|
247
|
+
p.textFont(SANS);
|
|
248
|
+
p.textStyle(p.BOLD);
|
|
249
|
+
p.textSize(34);
|
|
250
|
+
p.fill(PALETTE.ink);
|
|
251
|
+
p.text(city.total.toLocaleString('en-US'), x, 244);
|
|
252
|
+
p.textFont(MONO);
|
|
253
|
+
p.textStyle(p.NORMAL);
|
|
254
|
+
p.textSize(11);
|
|
255
|
+
p.fill(PALETTE.muted);
|
|
256
|
+
p.text('contributions', x, 262);
|
|
257
|
+
if (city.maxCount > 0) {
|
|
258
|
+
p.fill(245, 158, 11);
|
|
259
|
+
p.circle(x + 4, 292, 5);
|
|
260
|
+
p.fill(PALETTE.muted);
|
|
261
|
+
p.text('busiest days', x + 14, 296);
|
|
262
|
+
}
|
|
263
|
+
if (identity.website) {
|
|
264
|
+
p.textSize(12);
|
|
265
|
+
p.fill(PALETTE.cyan);
|
|
266
|
+
p.text(`↗ ${displayUrl(identity.website)}`, x, 350);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
setup(p) {
|
|
271
|
+
sky = renderSky(p);
|
|
272
|
+
grain = renderGrain(p);
|
|
273
|
+
},
|
|
274
|
+
draw(p, _frame, phase) {
|
|
275
|
+
const scanX = scanPosition(phase);
|
|
276
|
+
p.image(sky, 0, 0);
|
|
277
|
+
drawStars(p, phase);
|
|
278
|
+
drawGround(p);
|
|
279
|
+
for (const b of city.buildings)
|
|
280
|
+
drawBuilding(p, b, phase, scanX);
|
|
281
|
+
for (const arc of city.arcs)
|
|
282
|
+
drawArc(p, arc, phase);
|
|
283
|
+
drawScanBeam(p, scanX, phase);
|
|
284
|
+
drawMonths(p, scanX);
|
|
285
|
+
drawText(p, phase);
|
|
286
|
+
p.image(grain, 0, 0);
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "headreel",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Animated GitHub profile banner, rendered from your GitHub data. GitHub Action and CLI.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"github",
|
|
7
|
+
"profile",
|
|
8
|
+
"readme",
|
|
9
|
+
"banner",
|
|
10
|
+
"gif",
|
|
11
|
+
"github-action",
|
|
12
|
+
"contributions"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/arifszn/headreel#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/arifszn/headreel/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/arifszn/headreel.git"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "Ariful Alam",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"headreel": "dist/cli/index.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"assets"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
|
34
|
+
},
|
|
35
|
+
"packageManager": "pnpm@11.1.3",
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -p tsconfig.build.json",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"lint": "oxlint",
|
|
40
|
+
"format": "prettier --write .",
|
|
41
|
+
"format:check": "prettier --check .",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"dev": "tsx src/cli/index.ts",
|
|
44
|
+
"prepack": "npm run build"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@napi-rs/canvas": "1.0.9",
|
|
48
|
+
"gifenc": "1.0.3",
|
|
49
|
+
"jsdom": "30.1.0",
|
|
50
|
+
"p5": "2.3.2",
|
|
51
|
+
"zod": "4.6.5"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/jsdom": "30.0.0",
|
|
55
|
+
"@types/node": "22.20.3",
|
|
56
|
+
"oxlint": "1.83.0",
|
|
57
|
+
"prettier": "3.9.7",
|
|
58
|
+
"tsx": "4.23.13",
|
|
59
|
+
"typescript": "7.0.2",
|
|
60
|
+
"vitest": "5.0.1"
|
|
61
|
+
}
|
|
62
|
+
}
|