PageSourceSearch

https://aymuos15.github.io/js/contrib-graph.js

js aymuos15.github.io collected 2026-10-03 08:40:56 UTC 6,061 bytes, 221 lines download raw bytes

1// ── 3D isometric contribution graph ──────────────────────────────────────────
2let _weeks = [],
3  _graphReady = false;
4
5function _colors() {
6  const dark = document.documentElement.getAttribute("data-theme") === "dark";
7  return dark
8    ? ["#1e1c18", "#4a3520", "#8a6530", "#c4a060", "#f0d090"]
9    : ["#e8e3da", "#c4a070", "#a07840", "#7a5520", "#4a3010"];
10}
11
12function _shade(hex, f) {
13  // hex is "#rrggbb"; each channel is read straight off the string.
14  const channel = (at) =>
15    Math.min(255, Math.trunc(Number.parseInt(hex.slice(at, at + 2), 16) * f));
16  return `rgb(${channel(1)},${channel(3)},${channel(5)})`;
17}
18
19function _renderCanvas(animate) {
20  if (!_weeks.length) {
21    return;
22  }
23  const pal = _colors();
24  const CW = 9,
25    GAP = 2,
26    PX = CW + GAP,
27    PY = CW + GAP;
28  const MAX_H = 18,
29    MIN_H = 1,
30    DX = 3,
31    DY = 3;
32  const nC = _weeks.length,
33    nR = 7;
34  const maxN = Math.max(
35    1,
36    ..._weeks
37      .flat()
38      .filter(Boolean)
39      .map((d) => d.count)
40  );
41
42  const OX = 4,
43    OY = MAX_H + DY + 4;
44  const W = OX + nC * PX + DX + 4;
45  const H = OY + nR * PY + 4;
46
47  const dpr = window.devicePixelRatio || 1;
48  contribCanvas.width = Math.ceil(W * dpr);
49  contribCanvas.height = Math.ceil(H * dpr);
50  contribCanvas.style.width = `${W}px`;
51  contribCanvas.style.height = `${H}px`;
52
53  // precompute target heights
54  const targets = [];
55  for (let row = 0; row < nR; row += 1) {
56    targets[row] = [];
57    for (let col = 0; col < nC; col += 1) {
58      const day = _weeks[col]?.[row];
59      const count = day?.count ?? 0;
60      targets[row][col] =
61        count > 0 ? MIN_H + (count / maxN) * (MAX_H - MIN_H) : MIN_H;
62    }
63  }
64
65  function drawFrame(progress) {
66    const ctx = contribCanvas.getContext("2d");
67    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
68    ctx.clearRect(0, 0, W, H);
69
70    const hits = [];
71    for (let row = 0; row < nR; row += 1) {
72      for (let col = 0; col < nC; col += 1) {
73        const day = _weeks[col]?.[row];
74        const level = day?.level ?? 0;
75        const targetH = targets[row][col];
76
77        // stagger: each column starts slightly later
78        const delay = (col / nC) * 0.5;
79        const local = Math.max(0, Math.min(1, (progress - delay) / (1 - 0.5)));
80        // ease out cubic
81        const ease = 1 - (1 - local) ** 3;
82        const h = MIN_H + (targetH - MIN_H) * ease;
83
84        const sx = OX + col * PX;
85        const by = OY + row * PY + CW;
86        const ty = by - h;
87        const c = pal[level];
88
89        // front face
90        ctx.fillStyle = c;
91        ctx.fillRect(sx, ty, CW, h);
92        // top face
93        ctx.beginPath();
94        ctx.moveTo(sx, ty);
95        ctx.lineTo(sx + CW, ty);
96        ctx.lineTo(sx + CW + DX, ty - DY);
97        ctx.lineTo(sx + DX, ty - DY);
98        ctx.closePath();
99        ctx.fillStyle = _shade(c, 1.18);
100        ctx.fill();
101        // right face
102        ctx.beginPath();
103        ctx.moveTo(sx + CW, ty);
104        ctx.lineTo(sx + CW + DX, ty - DY);
105        ctx.lineTo(sx + CW + DX, by - DY);
106        ctx.lineTo(sx + CW, by);
107        ctx.closePath();
108        ctx.fillStyle = _shade(c, 0.65);
109        ctx.fill();
110
111        hits.push({ day, h, sx, ty });
112      }
113    }
114    contribCanvas._hits = hits;
115    contribCanvas._CW = CW;
116  }
117
118  if (!animate) {
119    drawFrame(1);
120    return;
121  }
122
123  const duration = 800;
124  const start = performance.now();
125  function tick(now) {
126    const progress = Math.min(1, (now - start) / duration);
127    drawFrame(progress);
128    if (progress < 1) {
129      requestAnimationFrame(tick);
130    }
131  }
132  requestAnimationFrame(tick);
133}
134
135contribCanvas.addEventListener("mousemove", (e) => {
136  const hits = contribCanvas._hits;
137  if (!hits) {
138    return;
139  }
140  const CW = contribCanvas._CW;
141  const rect = contribCanvas.getBoundingClientRect();
142  const sc =
143    (Number.parseFloat(contribCanvas.style.width) || rect.width) / rect.width;
144  const mx = (e.clientX - rect.left) * sc;
145  const my = (e.clientY - rect.top) * sc;
146
147  let hit = null;
148  for (let i = hits.length - 1; i >= 0; i -= 1) {
149    const { sx, ty, h } = hits[i];
150    if (mx >= sx && mx < sx + CW && my >= ty && my < ty + h) {
151      hit = hits[i];
152      break;
153    }
154  }
155
156  if (hit?.day) {
157    const { count, date } = hit.day;
158    const label = new Date(`${date}T12:00:00`).toLocaleDateString("en-US", {
159      day: "numeric",
160      month: "short",
161      weekday: "short",
162    });
163    contribTip.textContent = `${count || "no"} contribution${count === 1 ? "" : "s"} · ${label}`;
164    contribTip.style.display = "block";
165    contribTip.style.left = `${e.clientX + 14}px`;
166    contribTip.style.top = `${e.clientY - 38}px`;
167  } else {
168    contribTip.style.display = "none";
169  }
170});
171contribCanvas.addEventListener("mouseleave", () => {
172  contribTip.style.display = "none";
173});
174
175async function _loadContribs() {
176  const res = await window.fetch(
177    "https://github-contributions-api.jogruber.de/v4/aymuos15?y=last"
178  );
179  const days = (await res.json()).contributions || [];
180  const weeks = [];
181  let week = new Array(7).fill(null);
182  for (const day of days) {
183    const dow = new Date(`${day.date}T12:00:00`).getDay();
184    if (dow === 0 && week.some(Boolean)) {
185      weeks.push([...week]);
186      week = new Array(7).fill(null);
187    }
188    week[dow] = day;
189  }
190  if (week.some(Boolean)) {
191    weeks.push(week);
192  }
193  return weeks;
194}
195
196// Prefetch contrib data at load time so it's ready when the PRs tab is opened
197_loadContribs()
198  .then((w) => {
199    _weeks = w;
200    _graphReady = true;
201  })
202  .catch((err) => console.warn("Contrib graph prefetch:", err));
203
204function showContribGraph() {
205  contribGraph.classList.add("visible");
206  if (_graphReady) {
207    _renderCanvas(true);
208    return;
209  }
210  _loadContribs()
211    .then((w) => {
212      _weeks = w;
213      _graphReady = true;
214      _renderCanvas(true);
215    })
216    .catch((err) => console.warn("Contrib graph:", err));
217}
218function hideContribGraph() {
219  contribGraph.classList.remove("visible");
220  contribTip.style.display = "none";
221}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.