Compare commits
6 Commits
v2.0.0-rc.
...
v2.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d078b2d231 | ||
|
|
a81c1bcc97 | ||
|
|
40b26d0110 | ||
|
|
d6e930af5b | ||
|
|
98feaa6035 | ||
|
|
8f1481341a |
11
README.md
11
README.md
@@ -21,7 +21,7 @@ Available as github action. Automatically generate a new image at the end of the
|
||||
**github action**
|
||||
|
||||
```yaml
|
||||
- uses: Platane/snk@v2.0.0-rc.1
|
||||
- uses: Platane/snk@v2.0.0-rc.3
|
||||
with:
|
||||
# github user name to read the contribution graph from (**required**)
|
||||
# using action context var `github.repository_owner` or specified user
|
||||
@@ -29,6 +29,13 @@ Available as github action. Automatically generate a new image at the end of the
|
||||
|
||||
# list of files to generate.
|
||||
# one file per line. Each output can be customized with options as query string.
|
||||
#
|
||||
# supported options:
|
||||
# - palette: A preset of color, one of [github, github-dark, github-light]
|
||||
# - color_snake: Color of the snake
|
||||
# - color_dots: Coma separated list of dots color.
|
||||
# The first one is 0 contribution, then it goes from the low contribution to the highest.
|
||||
# Exactly 5 colors are expected.
|
||||
outputs: |
|
||||
dist/github-snake.svg
|
||||
dist/github-snake.svg?palette=github-dark
|
||||
@@ -37,7 +44,7 @@ Available as github action. Automatically generate a new image at the end of the
|
||||
|
||||
[example with cron job](https://github.com/Platane/Platane/blob/master/.github/workflows/main.yml#L24-L29)
|
||||
|
||||
If you are only interested in generating a svg, you can use this other faster action: `uses: Platane/snk/svg-only@v2.0.0-rc.1`
|
||||
If you are only interested in generating a svg, you can use this other faster action: `uses: Platane/snk/svg-only@v2.0.0-rc.3`
|
||||
|
||||
**interactive demo**
|
||||
|
||||
|
||||
16
action.yml
16
action.yml
@@ -4,7 +4,7 @@ author: "platane"
|
||||
|
||||
runs:
|
||||
using: docker
|
||||
image: docker://platane/snk@sha256:e40bb02de6ed0f164eca8586b3f6c32109b2bcb426cd57c6882764825b40fe0d
|
||||
image: docker://platane/snk@sha256:3169a2e4a5b5181c203ae18e7075d0d92be88a0cdeabce6c3221288ef6a17675
|
||||
|
||||
inputs:
|
||||
github_user_name:
|
||||
@@ -15,15 +15,15 @@ inputs:
|
||||
default: null
|
||||
description: |
|
||||
list of files to generate.
|
||||
Generates one file per line. Each output can be customized with query string.
|
||||
following this pattern: path/to/file.<gif or svg>?palette=<github or github-dark>&color_snake=<color>&color_dots=<color>,<color>,<color>,<color>,<color>
|
||||
one file per line. Each output can be customized with options as query string.
|
||||
|
||||
supported query string options:
|
||||
|
||||
- palette: a preset of color, one of [github, github-dark, github-light]
|
||||
- color_snake: color of the snake
|
||||
- color_dots: coma separated list of dots color. The first one is the empty cell color, the second one is the lightest shade, the third one is the second lightest shade ect ...
|
||||
supported query string options:
|
||||
|
||||
- palette: A preset of color, one of [github, github-dark, github-light]
|
||||
- color_snake: Color of the snake
|
||||
- color_dots: Coma separated list of dots color.
|
||||
The first one is 0 contribution, then it goes from the low contribution to the highest.
|
||||
Exactly 5 colors are expected.
|
||||
example:
|
||||
outputs: |
|
||||
dark.svg?palette=github-dark&color_snake=blue
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "snk",
|
||||
"description": "Generates a snake game from a github user contributions grid",
|
||||
"version": "2.0.0-rc.3",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"repository": "github:platane/snk",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,5 +1,81 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should parse /out.svg {"color_snake":"yellow"} 1`] = `
|
||||
Object {
|
||||
"animationOptions": Object {
|
||||
"frameDuration": 100,
|
||||
"step": 1,
|
||||
},
|
||||
"drawOptions": Object {
|
||||
"colorDotBorder": "#1b1f230a",
|
||||
"colorDots": Array [
|
||||
"#ebedf0",
|
||||
"#9be9a8",
|
||||
"#40c463",
|
||||
"#30a14e",
|
||||
"#216e39",
|
||||
],
|
||||
"colorEmpty": "#ebedf0",
|
||||
"colorSnake": "yellow",
|
||||
"dark": Object {
|
||||
"colorDotBorder": "#1b1f230a",
|
||||
"colorDots": Array [
|
||||
"#161b22",
|
||||
"#01311f",
|
||||
"#034525",
|
||||
"#0f6d31",
|
||||
"#00c647",
|
||||
],
|
||||
"colorEmpty": "#161b22",
|
||||
"colorSnake": "purple",
|
||||
},
|
||||
"sizeCell": 16,
|
||||
"sizeDot": 12,
|
||||
"sizeDotBorderRadius": 2,
|
||||
},
|
||||
"filename": "/out.svg",
|
||||
"format": "svg",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`should parse /out.svg?.gif.svg?color_snake=orange 1`] = `
|
||||
Object {
|
||||
"animationOptions": Object {
|
||||
"frameDuration": 100,
|
||||
"step": 1,
|
||||
},
|
||||
"drawOptions": Object {
|
||||
"colorDotBorder": "#1b1f230a",
|
||||
"colorDots": Array [
|
||||
"#ebedf0",
|
||||
"#9be9a8",
|
||||
"#40c463",
|
||||
"#30a14e",
|
||||
"#216e39",
|
||||
],
|
||||
"colorEmpty": "#ebedf0",
|
||||
"colorSnake": "orange",
|
||||
"dark": Object {
|
||||
"colorDotBorder": "#1b1f230a",
|
||||
"colorDots": Array [
|
||||
"#161b22",
|
||||
"#01311f",
|
||||
"#034525",
|
||||
"#0f6d31",
|
||||
"#00c647",
|
||||
],
|
||||
"colorEmpty": "#161b22",
|
||||
"colorSnake": "purple",
|
||||
},
|
||||
"sizeCell": 16,
|
||||
"sizeDot": 12,
|
||||
"sizeDotBorderRadius": 2,
|
||||
},
|
||||
"filename": "/out.svg?.gif.svg",
|
||||
"format": "svg",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`should parse /out.svg?{"color_snake":"yellow","color_dots":["#000","#111","#222","#333","#444"]} 1`] = `
|
||||
Object {
|
||||
"animationOptions": Object {
|
||||
|
||||
@@ -7,7 +7,11 @@ import { parseEntry } from "../outputsOptions";
|
||||
|
||||
`/out.svg?{"color_snake":"yellow","color_dots":["#000","#111","#222","#333","#444"]}`,
|
||||
|
||||
`/out.svg {"color_snake":"yellow"}`,
|
||||
|
||||
"/out.svg?color_snake=orange&color_dots=#000,#111,#222,#333,#444&dark_color_dots=#a00,#a11,#a22,#a33,#a44",
|
||||
|
||||
"/out.svg?.gif.svg?color_snake=orange",
|
||||
].forEach((entry) =>
|
||||
it(`should parse ${entry}`, () => {
|
||||
expect(parseEntry(entry)).toMatchSnapshot();
|
||||
|
||||
@@ -15,9 +15,9 @@ export const generateContributionSnake = async (
|
||||
} | null)[]
|
||||
) => {
|
||||
console.log("🎣 fetching github user contribution");
|
||||
const { cells, colorScheme } = await getGithubUserContribution(userName);
|
||||
const cells = await getGithubUserContribution(userName);
|
||||
|
||||
const grid = userContributionToGrid(cells, colorScheme);
|
||||
const grid = userContributionToGrid(cells);
|
||||
const snake = snake4;
|
||||
|
||||
console.log("📡 computing best route");
|
||||
|
||||
@@ -5,10 +5,13 @@ import { palettes } from "./palettes";
|
||||
export const parseOutputsOption = (lines: string[]) => lines.map(parseEntry);
|
||||
|
||||
export const parseEntry = (entry: string) => {
|
||||
const m = entry.trim().match(/^(.+\.(svg|gif))(\?(.*))?$/);
|
||||
const m = entry.trim().match(/^(.+\.(svg|gif))(\?(.*)|\s*({.*}))?$/);
|
||||
|
||||
if (!m) return null;
|
||||
|
||||
const [, filename, format, , query] = m;
|
||||
const [, filename, format, _, q1, q2] = m;
|
||||
|
||||
const query = q1 ?? q2;
|
||||
|
||||
let sp = new URLSearchParams(query || "");
|
||||
|
||||
|
||||
@@ -2,17 +2,13 @@ import { setColor, createEmptyGrid, setColorEmpty } from "@snk/types/grid";
|
||||
import type { Cell } from "@snk/github-user-contribution";
|
||||
import type { Color } from "@snk/types/grid";
|
||||
|
||||
export const userContributionToGrid = (
|
||||
cells: Cell[],
|
||||
colorScheme: string[]
|
||||
) => {
|
||||
export const userContributionToGrid = (cells: Cell[]) => {
|
||||
const width = Math.max(0, ...cells.map((c) => c.x)) + 1;
|
||||
const height = Math.max(0, ...cells.map((c) => c.y)) + 1;
|
||||
|
||||
const grid = createEmptyGrid(width, height);
|
||||
for (const c of cells) {
|
||||
const k = colorScheme.indexOf(c.color);
|
||||
if (k > 0) setColor(grid, c.x, c.y, k as Color);
|
||||
if (c.level > 0) setColor(grid, c.x, c.y, c.level as Color);
|
||||
else setColorEmpty(grid, c.x, c.y);
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ const createViewer = ({
|
||||
const k = spring.x % 1;
|
||||
|
||||
ctx.clearRect(0, 0, 9999, 9999);
|
||||
drawLerpWorld(ctx, grid, null, snake0, snake1, stack, k, drawOptions);
|
||||
drawLerpWorld(ctx, grid, cells, snake0, snake1, stack, k, drawOptions);
|
||||
|
||||
if (!stable) animationFrame = requestAnimationFrame(loop);
|
||||
};
|
||||
@@ -231,19 +231,19 @@ const onSubmit = async (userName: string) => {
|
||||
const res = await fetch(
|
||||
process.env.GITHUB_USER_CONTRIBUTION_API_ENDPOINT + userName
|
||||
);
|
||||
const { cells, colorScheme } = (await res.json()) as Res;
|
||||
const cells = (await res.json()) as Res;
|
||||
|
||||
const drawOptions: DrawOptions = {
|
||||
sizeDotBorderRadius: 2,
|
||||
sizeCell: 16,
|
||||
sizeDot: 12,
|
||||
colorDotBorder: "#1b1f230a",
|
||||
colorDots: colorScheme as any,
|
||||
colorEmpty: colorScheme[0],
|
||||
colorDots: ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"],
|
||||
colorEmpty: "#ebedf0",
|
||||
colorSnake: "purple",
|
||||
};
|
||||
|
||||
const grid = userContributionToGrid(cells, colorScheme);
|
||||
const grid = userContributionToGrid(cells);
|
||||
|
||||
const chain = await getChain(grid);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,41 +7,22 @@ describe("getGithubUserContribution", () => {
|
||||
await promise;
|
||||
});
|
||||
|
||||
it("should get colorScheme", async () => {
|
||||
const { colorScheme } = await promise;
|
||||
|
||||
expect(colorScheme).toEqual([
|
||||
"#ebedf0",
|
||||
"#9be9a8",
|
||||
"#40c463",
|
||||
"#30a14e",
|
||||
"#216e39",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should get around 365 cells", async () => {
|
||||
const { cells } = await promise;
|
||||
const cells = await promise;
|
||||
|
||||
expect(cells.length).toBeGreaterThanOrEqual(365);
|
||||
expect(cells.length).toBeLessThanOrEqual(365 + 7);
|
||||
});
|
||||
|
||||
it("cells should have x / y coords representing to a 7 x (365/7) (minus unfilled last row)", async () => {
|
||||
const { cells, colorScheme } = await promise;
|
||||
const cells = await promise;
|
||||
|
||||
expect(cells.length).toBeGreaterThan(300);
|
||||
expect(colorScheme).toEqual([
|
||||
"#ebedf0",
|
||||
"#9be9a8",
|
||||
"#40c463",
|
||||
"#30a14e",
|
||||
"#216e39",
|
||||
]);
|
||||
|
||||
const undefinedDays = Array.from({ length: Math.floor(365 / 7) })
|
||||
.map((x) => Array.from({ length: 7 }).map((y) => ({ x, y })))
|
||||
.flat()
|
||||
.filter(({ x, y }) => cells.some((c) => c.x === x && c.y === y));
|
||||
.filter(({ x, y }) => cells.some((c: any) => c.x === x && c.y === y));
|
||||
|
||||
expect(undefinedDays).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import fetch from "node-fetch";
|
||||
import * as cheerio from "cheerio";
|
||||
import { formatParams, Options } from "./formatParams";
|
||||
|
||||
/**
|
||||
@@ -39,91 +38,40 @@ export const getGithubUserContribution = async (
|
||||
return parseUserPage(resText);
|
||||
};
|
||||
|
||||
const defaultColorScheme = [
|
||||
"#ebedf0",
|
||||
"#9be9a8",
|
||||
"#40c463",
|
||||
"#30a14e",
|
||||
"#216e39",
|
||||
];
|
||||
|
||||
const parseUserPage = (content: string) => {
|
||||
const $ = cheerio.load(content);
|
||||
// take roughly the svg block
|
||||
const block = content
|
||||
.split(`class="js-calendar-graph-svg"`)[1]
|
||||
.split("</svg>")[0];
|
||||
|
||||
//
|
||||
// "parse" colorScheme
|
||||
const colorScheme = [...defaultColorScheme];
|
||||
let x = 0;
|
||||
let lastYAttribute = 0;
|
||||
|
||||
//
|
||||
// parse cells
|
||||
const rawCells = $(".js-calendar-graph rect[data-count]")
|
||||
.toArray()
|
||||
.map((x) => {
|
||||
const level = +x.attribs["data-level"];
|
||||
const count = +x.attribs["data-count"];
|
||||
const date = x.attribs["data-date"];
|
||||
const rects = Array.from(block.matchAll(/<rect[^>]*>/g)).map(([m]) => {
|
||||
const date = m.match(/data-date="([^"]+)"/)![1];
|
||||
const count = +m.match(/data-count="([^"]+)"/)![1];
|
||||
const level = +m.match(/data-level="([^"]+)"/)![1];
|
||||
const yAttribute = +m.match(/y="([^"]+)"/)![1];
|
||||
|
||||
const color = colorScheme[level];
|
||||
if (lastYAttribute > yAttribute) x++;
|
||||
|
||||
if (!color) throw new Error("could not determine the color of the cell");
|
||||
lastYAttribute = yAttribute;
|
||||
|
||||
return {
|
||||
svgPosition: getSvgPosition(x),
|
||||
color,
|
||||
count,
|
||||
date,
|
||||
};
|
||||
});
|
||||
|
||||
const xMap: Record<number, true> = {};
|
||||
const yMap: Record<number, true> = {};
|
||||
rawCells.forEach(({ svgPosition: { x, y } }) => {
|
||||
xMap[x] = true;
|
||||
yMap[y] = true;
|
||||
return { date, count, level, x, yAttribute };
|
||||
});
|
||||
|
||||
const xRange = Object.keys(xMap)
|
||||
.map((x) => +x)
|
||||
.sort((a, b) => +a - +b);
|
||||
const yRange = Object.keys(yMap)
|
||||
.map((x) => +x)
|
||||
.sort((a, b) => +a - +b);
|
||||
const yAttributes = Array.from(
|
||||
new Set(rects.map((c) => c.yAttribute)).keys()
|
||||
).sort();
|
||||
|
||||
const cells = rawCells.map(({ svgPosition, ...c }) => ({
|
||||
const cells = rects.map(({ yAttribute, ...c }) => ({
|
||||
y: yAttributes.indexOf(yAttribute),
|
||||
...c,
|
||||
x: xRange.indexOf(svgPosition.x),
|
||||
y: yRange.indexOf(svgPosition.y),
|
||||
}));
|
||||
|
||||
return { cells, colorScheme };
|
||||
};
|
||||
|
||||
// returns the position of the svg elements, accounting for it's transform and it's parent transform
|
||||
// ( only accounts for translate transform )
|
||||
const getSvgPosition = (
|
||||
e: cheerio.Element | null
|
||||
): { x: number; y: number } => {
|
||||
if (!e || e.tagName === "svg") return { x: 0, y: 0 };
|
||||
|
||||
const p = getSvgPosition(e.parent as cheerio.Element);
|
||||
|
||||
if (e.attribs.x) p.x += +e.attribs.x;
|
||||
if (e.attribs.y) p.y += +e.attribs.y;
|
||||
|
||||
if (e.attribs.transform) {
|
||||
const m = e.attribs.transform.match(
|
||||
/translate\( *([\.\d]+) *, *([\.\d]+) *\)/
|
||||
);
|
||||
|
||||
if (m) {
|
||||
p.x += +m[1];
|
||||
p.y += +m[2];
|
||||
}
|
||||
}
|
||||
|
||||
return p;
|
||||
return cells;
|
||||
};
|
||||
|
||||
export type Res = Awaited<ReturnType<typeof getGithubUserContribution>>;
|
||||
|
||||
export type Cell = Res["cells"][number];
|
||||
export type Cell = Res[number];
|
||||
|
||||
@@ -15,14 +15,15 @@ inputs:
|
||||
default: null
|
||||
description: |
|
||||
list of files to generate.
|
||||
Generates one file per line. Each output can be customized with query string.
|
||||
following this pattern: path/to/file.svg?palette=<github or github-dark>&color_snake=<color>&color_dots=<color>,<color>,<color>,<color>,<color>
|
||||
one file per line. Each output can be customized with options as query string.
|
||||
|
||||
supported query string options:
|
||||
supported query string options:
|
||||
|
||||
- palette: a preset of color, one of [github, github-dark, github-light]
|
||||
- color_snake: color of the snake
|
||||
- color_dots: coma separated list of dots color. The first one is the empty cell color, the second one is the lightest shade, the third one is the second lightest shade ect ...
|
||||
- palette: A preset of color, one of [github, github-dark, github-light]
|
||||
- color_snake: Color of the snake
|
||||
- color_dots: Coma separated list of dots color.
|
||||
The first one is 0 contribution, then it goes from the low contribution to the highest.
|
||||
Exactly 5 colors are expected.
|
||||
|
||||
example:
|
||||
outputs: |
|
||||
|
||||
20168
svg-only/dist/index.js
vendored
20168
svg-only/dist/index.js
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user