Skip to content

Commit 7168464

Browse files
govindup63sxivansx
andauthored
Add Point Blank theme: a terminal-style contribution wallpaper (#25)
Point Blank (pointblank.club) is a student-run competitive-programming and FOSS community. Their mark is `<.` ... a blank ... `>` in emerald on black, the empty space puns on the name. The theme renders the calendar as one terminal screen (src/lib/pbScene.ts): the `<. >` mark is the hero above the grid, drawn to the official logo's measured proportions (open chevrons, a low baseline dot hard after `<`, then the wide blank to `>`). The contribution grid is the glowing emerald output, kept at the exact standard tile size and position of every other theme, only the background and cell style change. Faint CRT scanlines and a centre vignette give it depth; the two-tone "Point Blank" wordmark sits below. It is a single full-scene theme (style: "pointblank", no variants), grouped under Special in the picker with the shape selector hidden. Bundles JetBrains Mono (OFL) for the terminal wordmark. Signed-off-by: Govind Pandey <govindup63@gmail.com> Co-authored-by: Shivansh Pandey <shivanshp987@gmail.com>
1 parent b958406 commit 7168464

6 files changed

Lines changed: 264 additions & 3 deletions

File tree

fonts/JetBrainsMono-Bold.ttf

268 KB
Binary file not shown.

fonts/JetBrainsMono-Regular.ttf

264 KB
Binary file not shown.

src/app/page.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ const MINECRAFT_DEFAULT = "minecraft-slime";
1414
const ONEPIECE_DEFAULT = "onepiece-jollyroger";
1515
const AOT_DEFAULT = "aot-wingsoffreedom";
1616
const GOT_DEFAULT = "got-targaryen";
17+
const POINTBLANK_ID = "pointblank";
1718
const POKEMON_DEFAULT = "pokemon-pikachu";
1819
const isMinecraftId = (id: string) => id.startsWith("minecraft-");
1920
const isOnePieceId = (id: string) => id.startsWith("onepiece-");
2021
const isAotId = (id: string) => id.startsWith("aot-");
2122
const isGotId = (id: string) => id.startsWith("got-");
23+
const isPointBlankId = (id: string) => id === POINTBLANK_ID;
2224
const isPokemonId = (id: string) => id.startsWith("pokemon-");
2325

2426
type Theme = { id: string; name: string; colors: string[]; background: string };
@@ -362,7 +364,7 @@ export default function Home() {
362364
// single picker tiles that expand to show their variants; everything else stays
363365
// in the flat theme grid.
364366
const gridThemes = themes.filter(
365-
(t) => !isMinecraftId(t.id) && !isOnePieceId(t.id) && !isAotId(t.id) && !isGotId(t.id) && !isPokemonId(t.id)
367+
(t) => !isMinecraftId(t.id) && !isOnePieceId(t.id) && !isAotId(t.id) && !isGotId(t.id) && !isPointBlankId(t.id) && !isPokemonId(t.id)
366368
);
367369
const minecraftThemes = themes.filter((t) => isMinecraftId(t.id));
368370
const onepieceThemes = themes.filter((t) => isOnePieceId(t.id));
@@ -372,10 +374,12 @@ export default function Home() {
372374
const onepieceSelected = isOnePieceId(selectedTheme);
373375
const aotSelected = isAotId(selectedTheme);
374376
const gotSelected = isGotId(selectedTheme);
377+
const pointblankSelected = isPointBlankId(selectedTheme);
375378
const aotGroup = aotThemes.find((t) => t.id === AOT_DEFAULT) ?? aotThemes[0];
376379
const minecraftGroup = minecraftThemes.find((t) => t.id === MINECRAFT_DEFAULT) ?? minecraftThemes[0];
377380
const onepieceGroup = onepieceThemes.find((t) => t.id === ONEPIECE_DEFAULT) ?? onepieceThemes[0];
378381
const gotGroup = gotThemes.find((t) => t.id === GOT_DEFAULT) ?? gotThemes[0];
382+
const pointblankTheme = themes.find((t) => isPointBlankId(t.id));
379383
const pokemonThemes = themes.filter((t) => isPokemonId(t.id));
380384
const pokemonSelected = isPokemonId(selectedTheme);
381385
const pokemonGroup = pokemonThemes.find((t) => t.id === POKEMON_DEFAULT) ?? pokemonThemes[0];
@@ -677,7 +681,7 @@ export default function Home() {
677681

678682
{/* Shape lives with General — it only changes how these solid cells
679683
are drawn (box vs circle); pixel-art/full-scene themes ignore it. */}
680-
{!minecraftSelected && !onepieceSelected && !aotSelected && !gotSelected && !pokemonSelected && (
684+
{!minecraftSelected && !onepieceSelected && !aotSelected && !gotSelected && !pointblankSelected && !pokemonSelected && (
681685
<div className="mt-3 flex items-center gap-2.5">
682686
<span className="text-[11px] font-medium text-white/30">Shape</span>
683687
<div className="inline-flex gap-1 p-1 bg-white/[0.04] border border-white/[0.08] rounded-lg">
@@ -805,6 +809,30 @@ export default function Home() {
805809
</button>
806810
)}
807811

812+
{/* Point Blank — a single full-scene theme (no variants) */}
813+
{pointblankTheme && (
814+
<button
815+
onClick={() => setSelectedTheme(POINTBLANK_ID)}
816+
aria-pressed={pointblankSelected}
817+
aria-label="Point Blank theme"
818+
className={`px-3.5 py-2.5 rounded-lg border transition-all cursor-pointer ${
819+
pointblankSelected
820+
? "border-white/50 ring-1 ring-white/10"
821+
: "border-white/[0.07] hover:border-white/20"
822+
}`}
823+
style={{ background: pointblankTheme.background }}
824+
>
825+
<div className="flex gap-1 justify-center mb-1.5">
826+
{pointblankTheme.colors.map((c, i) => (
827+
<span key={i} className="w-2 h-2 rounded-full" style={{ background: c }} />
828+
))}
829+
</div>
830+
<span className="text-[10px] font-semibold uppercase tracking-wider block text-center text-white/60">
831+
Point Blank
832+
</span>
833+
</button>
834+
)}
835+
808836
{/* Pokémon group */}
809837
{pokemonGroup && (
810838
<button

src/lib/pbScene.ts

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { type CanvasRenderingContext2D } from "canvas";
2+
3+
// Full-canvas Point Blank wallpaper, kept minimal. Point Blank (pointblank.club)
4+
// is a competitive-programming / FOSS community whose mark is `<.` ... a blank ...
5+
// `>` in emerald on black — the dot is a low period hard after the opening
6+
// bracket, then a wide blank, then the closing bracket. The mark sits as the hero
7+
// above the contribution grid, which keeps the standard tile size/position of
8+
// every other theme; only the background and cell style change.
9+
10+
export interface PbSceneArgs {
11+
width: number;
12+
height: number;
13+
gridLeft: number;
14+
gridTop: number;
15+
numCols: number;
16+
numRows: number;
17+
cellSize: number;
18+
cellStep: number;
19+
cornerRadius: number;
20+
levels: number[];
21+
}
22+
23+
// Brand emerald (#00c853) on near-black, high-contrast dim→bright ramp so busy
24+
// days read as sharp points of light. The mark is brighter than any tile.
25+
const BG = "#050807";
26+
const EMPTY = "rgba(18,34,24,0.6)"; // a day with no commits
27+
const RAMP = ["#0c4a2a", "#0e8a44", "#00c853", "#5dff9e"]; // levels 0..3
28+
const RAMP_RGB = [
29+
[12, 74, 42],
30+
[14, 138, 68],
31+
[0, 200, 83],
32+
[93, 255, 158],
33+
];
34+
const MARK = "#1bff8c";
35+
const MARK_GLOW = "rgba(40,255,150,0.55)";
36+
37+
function roundRectPath(
38+
ctx: CanvasRenderingContext2D,
39+
x: number,
40+
y: number,
41+
w: number,
42+
h: number,
43+
r: number
44+
) {
45+
ctx.beginPath();
46+
ctx.roundRect(x, y, w, h, r);
47+
}
48+
49+
// The `<. >` mark, drawn to the official logo's proportions (measured from the
50+
// brand asset): chevron width ≈ 0.78× its height; the dot is ≈0.5 a chevron wide,
51+
// a low baseline period just right of `<`; then a wide blank (~1.9 chevrons) to
52+
// `>`. Total width ≈ 4.96 chevrons. Soft glow pass then a crisp pass.
53+
function drawMark(ctx: CanvasRenderingContext2D, cx: number, cy: number, targetW: number) {
54+
const cw = targetW / 4.96; // chevron width
55+
const gh = cw / 0.776; // chevron glyph height
56+
const armH = gh / 2;
57+
const dotR = cw * 0.25;
58+
const gap1 = cw * 0.58; // `<` to `.`
59+
const gap2 = cw * 1.88; // `.` to `>` — the blank
60+
const dotCy = cy + gh * 0.30; // low, like a period on the baseline
61+
62+
let x = cx - targetW / 2;
63+
const lVertX = x;
64+
const lArmX = x + cw;
65+
x += cw + gap1;
66+
const dotCx = x + dotR;
67+
x += dotR * 2 + gap2;
68+
const rArmX = x;
69+
const rVertX = x + cw;
70+
71+
const paint = (lineWidth: number, color: string, blur: number) => {
72+
ctx.save();
73+
if (blur > 0) {
74+
ctx.shadowColor = MARK_GLOW;
75+
ctx.shadowBlur = blur;
76+
}
77+
ctx.lineWidth = lineWidth;
78+
ctx.lineJoin = "round";
79+
ctx.lineCap = "round";
80+
ctx.strokeStyle = color;
81+
ctx.fillStyle = color;
82+
ctx.beginPath(); // `<`
83+
ctx.moveTo(lArmX, cy - armH);
84+
ctx.lineTo(lVertX, cy);
85+
ctx.lineTo(lArmX, cy + armH);
86+
ctx.stroke();
87+
ctx.beginPath(); // `.`
88+
ctx.arc(dotCx, dotCy, dotR, 0, Math.PI * 2);
89+
ctx.fill();
90+
ctx.beginPath(); // `>`
91+
ctx.moveTo(rArmX, cy - armH);
92+
ctx.lineTo(rVertX, cy);
93+
ctx.lineTo(rArmX, cy + armH);
94+
ctx.stroke();
95+
ctx.restore();
96+
};
97+
98+
ctx.save();
99+
ctx.globalCompositeOperation = "lighter";
100+
paint(gh * 0.30, "rgba(27,255,140,0.16)", 0); // soft halo
101+
ctx.restore();
102+
paint(gh * 0.17, MARK, 20); // crisp
103+
}
104+
105+
export function renderPbScene(ctx: CanvasRenderingContext2D, a: PbSceneArgs): void {
106+
const { width, height, gridLeft, gridTop, numCols, numRows, cellSize, cellStep, cornerRadius, levels } = a;
107+
const scale = width / 393;
108+
109+
// Standard grid geometry — identical to every other theme. Never resized.
110+
const gridW = numCols * cellStep - (cellStep - cellSize);
111+
const gridH = numRows * cellStep - (cellStep - cellSize);
112+
const midY = gridTop + gridH / 2;
113+
114+
// 1) Terminal black background.
115+
ctx.fillStyle = BG;
116+
ctx.fillRect(0, 0, width, height);
117+
118+
// 2) Ambient screen glow pooling behind the grid.
119+
ctx.save();
120+
ctx.globalCompositeOperation = "lighter";
121+
const ambient = ctx.createRadialGradient(width / 2, midY, 0, width / 2, midY, gridW * 0.8);
122+
ambient.addColorStop(0, "rgba(0,200,83,0.10)");
123+
ambient.addColorStop(0.6, "rgba(0,160,70,0.04)");
124+
ambient.addColorStop(1, "rgba(0,0,0,0)");
125+
ctx.fillStyle = ambient;
126+
ctx.fillRect(0, gridTop - gridH * 0.4, width, gridH * 1.8);
127+
ctx.restore();
128+
129+
// 3) CRT scanlines, very faint, additive emerald.
130+
ctx.save();
131+
ctx.globalCompositeOperation = "lighter";
132+
ctx.fillStyle = "rgba(0,200,83,0.022)";
133+
const lineGap = Math.max(3, Math.round(4 * scale));
134+
for (let y = 0; y < height; y += lineGap) {
135+
ctx.fillRect(0, y, width, Math.max(1, Math.round(scale)));
136+
}
137+
ctx.restore();
138+
139+
// 4) The hero mark, centred above the grid, clear of the lock-screen clock.
140+
drawMark(ctx, width / 2, height * 0.25, width * 0.46);
141+
142+
// 5) The grid — your year, the glowing output. Standard size.
143+
for (let i = 0; i < levels.length; i++) {
144+
const col = i % numCols;
145+
const row = Math.floor(i / numCols);
146+
const x = gridLeft + col * cellStep;
147+
const y = gridTop + row * cellStep;
148+
const lv = levels[i];
149+
150+
if (lv < 0) {
151+
roundRectPath(ctx, x, y, cellSize, cellSize, cornerRadius);
152+
ctx.fillStyle = EMPTY;
153+
ctx.fill();
154+
continue;
155+
}
156+
157+
ctx.save();
158+
ctx.shadowColor = `rgba(${RAMP_RGB[lv][0]},${RAMP_RGB[lv][1]},${RAMP_RGB[lv][2]},${0.4 + lv * 0.12})`;
159+
ctx.shadowBlur = (2 + lv * 2) * scale;
160+
roundRectPath(ctx, x, y, cellSize, cellSize, cornerRadius);
161+
ctx.fillStyle = RAMP[lv];
162+
ctx.fill();
163+
ctx.restore();
164+
165+
if (lv >= 2) {
166+
ctx.save();
167+
ctx.globalCompositeOperation = "lighter";
168+
ctx.globalAlpha = 0.16 * (lv - 1);
169+
roundRectPath(ctx, x, y, cellSize, cellSize * 0.42, cornerRadius);
170+
ctx.fillStyle = "#cfffe4";
171+
ctx.fill();
172+
ctx.restore();
173+
}
174+
}
175+
176+
// 6) Vignette — center-weighted, so the screen glows from the middle.
177+
ctx.save();
178+
ctx.globalCompositeOperation = "multiply";
179+
const vig = ctx.createRadialGradient(
180+
width / 2, height * 0.42, gridW * 0.55,
181+
width / 2, height * 0.42, Math.max(width, height) * 0.72
182+
);
183+
vig.addColorStop(0, "rgba(255,255,255,1)");
184+
vig.addColorStop(1, "rgba(10,18,12,1)");
185+
ctx.fillStyle = vig;
186+
ctx.fillRect(0, 0, width, height);
187+
ctx.restore();
188+
}

src/render.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { AttackOnTitanVariant } from "./lib/attackontitan";
1010
import { renderAotScene } from "./lib/aotScene";
1111
import { renderGotScene } from "./lib/gotScene";
1212
import { GAMEOFTHRONES_WORDS, type GameOfThronesVariant } from "./lib/gameofthrones";
13+
import { renderPbScene } from "./lib/pbScene";
1314
import { renderPokemonScene } from "./lib/pokemonScene";
1415
import { type PokemonVariant } from "./lib/pokemon";
1516
import type { ContributionCalendar } from "./github";
@@ -23,6 +24,9 @@ registerFont(path.join(fontsDir, "Inter-Bold.ttf"), { family: "Inter", weight: "
2324
// plain `Cinzel` request both resolve to this TTF.
2425
registerFont(path.join(fontsDir, "Cinzel.ttf"), { family: "Cinzel", weight: "bold" });
2526
registerFont(path.join(fontsDir, "Cinzel.ttf"), { family: "Cinzel" });
27+
// JetBrains Mono: the terminal typeface for the Point Blank theme's shell prompt.
28+
registerFont(path.join(fontsDir, "JetBrainsMono-Regular.ttf"), { family: "JetBrains Mono" });
29+
registerFont(path.join(fontsDir, "JetBrainsMono-Bold.ttf"), { family: "JetBrains Mono", weight: "bold" });
2630

2731
// The product's own domain, baked into every wallpaper as a watermark.
2832
export const WATERMARK = "gitwall.space";
@@ -118,6 +122,7 @@ export function renderWallpaper(
118122
// block edges stay crisp — it's restored before the text below.
119123
const isAttackOnTitan = theme.style === "attackontitan";
120124
const isGot = theme.style === "gameofthrones";
125+
const isPointBlank = theme.style === "pointblank";
121126
const isPokemon = theme.style === "pokemon";
122127

123128
if (isAttackOnTitan) {
@@ -135,6 +140,12 @@ export function renderWallpaper(
135140
width, height, gridLeft, gridTop, numCols, numRows, cellSize, cellStep,
136141
cornerRadius, levels, variant: theme.variant as GameOfThronesVariant,
137142
});
143+
} else if (isPointBlank) {
144+
const levels = recentDays.map((d) => getContributionLevel(d.contributionCount));
145+
renderPbScene(ctx, {
146+
width, height, gridLeft, gridTop, numCols, numRows, cellSize, cellStep,
147+
cornerRadius, levels,
148+
});
138149
} else if (isPokemon) {
139150
const levels = recentDays.map((d) => getContributionLevel(d.contributionCount));
140151
renderPokemonScene(ctx, {
@@ -194,6 +205,26 @@ export function renderWallpaper(
194205
}
195206
}
196207

208+
// Point Blank's wordmark, two-toned like the official lockup — "Point" in the
209+
// brand emerald, "Blank" muted, in the terminal typeface.
210+
if (isPointBlank) {
211+
ctx.save();
212+
ctx.font = `bold ${Math.round(16 * scale)}px "JetBrains Mono"`;
213+
ctx.textAlign = "left";
214+
const p1 = "Point ";
215+
const p2 = "Blank";
216+
const w1 = ctx.measureText(p1).width;
217+
const w2 = ctx.measureText(p2).width;
218+
const startX = width / 2 - (w1 + w2) / 2;
219+
const wy = bottomMid - Math.round(40 * scale);
220+
ctx.fillStyle = theme.levels[2];
221+
ctx.fillText(p1, startX, wy);
222+
ctx.fillStyle = "#7f8f84";
223+
ctx.fillText(p2, startX + w1, wy);
224+
ctx.restore();
225+
ctx.textAlign = "center";
226+
}
227+
197228
if (user) {
198229
ctx.fillStyle = theme.text;
199230
ctx.font = `bold ${Math.round(13 * scale)}px Inter`;

src/themes.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export interface Theme {
1414
// When set, cells are drawn as pixel-art icons instead of plain boxes/circles,
1515
// or (attackontitan/gameofthrones) the whole calendar is rendered as one scene.
1616
// `empty`/`levels` only feed the theme-picker swatch in these modes.
17-
style?: "minecraft" | "onepiece" | "attackontitan" | "gameofthrones" | "pokemon";
17+
style?: "minecraft" | "onepiece" | "attackontitan" | "gameofthrones" | "pokemon" | "pointblank";
1818
variant?: MinecraftVariant | OnePieceVariant | AttackOnTitanVariant | GameOfThronesVariant | PokemonVariant;
1919
}
2020

@@ -258,6 +258,20 @@ export const THEMES: Record<string, Theme> = {
258258
subtext: "#88a4c0",
259259
},
260260

261+
// ── Point Blank ───────────────────────────────────────────────────────────
262+
// The whole calendar renders as one terminal screen (see src/lib/pbScene.ts):
263+
// your year is the code, clasped by the giant `<.>` brackets of the logo, with
264+
// a cursor block on today. `empty`/`levels` only feed the theme-picker swatch.
265+
pointblank: {
266+
name: "Point Blank",
267+
style: "pointblank",
268+
background: "#04070a",
269+
empty: "#1a3022",
270+
levels: ["#0d5230", "#10914c", "#00c853", "#5dffa0"],
271+
text: "#d6ffe6",
272+
subtext: "#6fbf90",
273+
},
274+
261275
// Pokémon: a retro Pokédex handheld with a Game Boy-style screen (see
262276
// src/lib/pokemonScene.ts). `background` is the device colour and `levels`
263277
// the screen palette — these only feed the picker swatch; the scene renderer

0 commit comments

Comments
 (0)