Compare commits

...

8 Commits

Author SHA1 Message Date
release bot
2e275adbb6 📦 2.0.0-rc.3 2022-04-12 21:06:46 +00:00
platane
66fef03781 👷 2022-04-12 22:04:20 +02:00
platane
5841a21a09 👷 remove benchmark test 2022-04-12 22:01:29 +02:00
platane
cce5c4514d ♻️ refacto: rename options 2022-04-12 22:01:29 +02:00
platane
fb82d42d53 🚀 Allow to pass option as Json 2022-04-12 21:34:25 +02:00
release bot
e3ad8b2caf 📦 2.0.0-rc.2 2022-04-11 22:00:35 +00:00
platane
c21e390ca9 🐛 fix svg-only action.yaml 2022-04-11 23:57:57 +02:00
platane
7077112ba4 📓 2022-04-11 23:48:03 +02:00
22 changed files with 261 additions and 128 deletions

View File

@@ -18,19 +18,6 @@ jobs:
- run: yarn lint
- run: yarn test --ci
test-benchmark:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
cache: yarn
node-version: 16
- run: yarn install --frozen-lockfile
- run: ( cd packages/gif-creator ; yarn benchmark )
test-action:
runs-on: ubuntu-latest
steps:
@@ -79,7 +66,7 @@ jobs:
GITHUB_USER_CONTRIBUTION_API_ENDPOINT: https://snk-one.vercel.app/api/github-user-contribution/
- uses: crazy-max/ghaction-github-pages@v2.6.0
if: success() && github.ref.name == 'main'
if: success() && github.ref == 'refs/heads/main'
with:
target_branch: gh-pages
build_dir: packages/demo/dist

View File

@@ -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
- uses: Platane/snk@v2.0.0-rc.1
with:
# github user name to read the contribution graph from (**required**)
# using action context var `github.repository_owner` or specified user
@@ -37,7 +37,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`
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`
**interactive demo**

View File

@@ -4,7 +4,7 @@ author: "platane"
runs:
using: docker
image: docker://platane/snk@sha256:300fb94d3b1214e6c229990b458286a8f1c4c68a178b1b59b670c9fcac7c80d1
image: docker://platane/snk@sha256:e40bb02de6ed0f164eca8586b3f6c32109b2bcb426cd57c6882764825b40fe0d
inputs:
github_user_name:

View File

@@ -1,7 +1,7 @@
{
"name": "snk",
"description": "Generates a snake game from a github user contributions grid",
"version": "2.0.0-rc.1",
"version": "2.0.0-rc.3",
"private": true,
"repository": "github:platane/snk",
"devDependencies": {

View File

@@ -8,8 +8,6 @@ Contains the github action code.
Because the gif generation requires some native libs, we cannot use a node.js action.
Use a docker action instead, the image is created from the [Dockerfile](./Dockerfile).
Use a docker action instead, the image is created from the [Dockerfile](../../Dockerfile).
It's published to [dockerhub](https://hub.docker.com/r/platane/snk) which makes for faster build ( compare to building the image when the action runs )
Notice that the [action.yml](../../action.yml) point to the latest version of the image. Which makes releasing sematic versioning of the action pointless. Which is probably fine for a wacky project like this one.

View File

@@ -1,17 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should parse /out.svg?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9 1`] = `
exports[`should parse /out.svg?{"color_snake":"yellow","color_dots":["#000","#111","#222","#333","#444"]} 1`] = `
Object {
"animationOptions": Object {
"frameDuration": 100,
"step": 1,
},
"drawOptions": Object {
"colorDotBorder": "#1b1f230a",
"colorDots": Array [
"#bfd6f6",
"#8dbdff",
"#64a1f4",
"#4b91f1",
"#3c7dd9",
"#000",
"#111",
"#222",
"#333",
"#444",
],
"colorEmpty": "#bfd6f6",
"colorEmpty": "#000",
"colorSnake": "yellow",
"dark": undefined,
"sizeCell": 16,
"sizeDot": 12,
"sizeDotBorderRadius": 2,
},
"filename": "/out.svg",
"format": "svg",
}
`;
exports[`should parse /out.svg?color_snake=orange&color_dots=#000,#111,#222,#333,#444 1`] = `
Object {
"animationOptions": Object {
"frameDuration": 100,
"step": 1,
},
"drawOptions": Object {
"colorDotBorder": "#1b1f230a",
"colorDots": Array [
"#000",
"#111",
"#222",
"#333",
"#444",
],
"colorEmpty": "#000",
"colorSnake": "orange",
"dark": undefined,
"sizeCell": 16,
@@ -20,15 +51,51 @@ Object {
},
"filename": "/out.svg",
"format": "svg",
"gifOptions": Object {
}
`;
exports[`should parse /out.svg?color_snake=orange&color_dots=#000,#111,#222,#333,#444&dark_color_dots=#a00,#a11,#a22,#a33,#a44 1`] = `
Object {
"animationOptions": Object {
"frameDuration": 100,
"step": 1,
},
"drawOptions": Object {
"colorDotBorder": "#1b1f230a",
"colorDots": Array [
"#000",
"#111",
"#222",
"#333",
"#444",
],
"colorEmpty": "#000",
"colorSnake": "orange",
"dark": Object {
"colorDots": Array [
"#a00",
"#a11",
"#a22",
"#a33",
"#a44",
],
"colorEmpty": "#a00",
},
"sizeCell": 16,
"sizeDot": 12,
"sizeDotBorderRadius": 2,
},
"filename": "/out.svg",
"format": "svg",
}
`;
exports[`should parse path/to/out.gif 1`] = `
Object {
"animationOptions": Object {
"frameDuration": 100,
"step": 1,
},
"drawOptions": Object {
"colorDotBorder": "#1b1f230a",
"colorDots": Array [
@@ -58,9 +125,5 @@ Object {
},
"filename": "path/to/out.gif",
"format": "gif",
"gifOptions": Object {
"frameDuration": 100,
"step": 1,
},
}
`;

View File

@@ -3,7 +3,11 @@ import { parseEntry } from "../outputsOptions";
[
"path/to/out.gif",
"/out.svg?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9",
"/out.svg?color_snake=orange&color_dots=#000,#111,#222,#333,#444",
`/out.svg?{"color_snake":"yellow","color_dots":["#000","#111","#222","#333","#444"]}`,
"/out.svg?color_snake=orange&color_dots=#000,#111,#222,#333,#444&dark_color_dots=#a00,#a11,#a22,#a33,#a44",
].forEach((entry) =>
it(`should parse ${entry}`, () => {
expect(parseEntry(entry)).toMatchSnapshot();

View File

@@ -3,17 +3,15 @@ import { userContributionToGrid } from "./userContributionToGrid";
import { getBestRoute } from "@snk/solver/getBestRoute";
import { snake4 } from "@snk/types/__fixtures__/snake";
import { getPathToPose } from "@snk/solver/getPathToPose";
import { Options as DrawOptions } from "@snk/svg-creator";
import type { DrawOptions as DrawOptions } from "@snk/svg-creator";
import type { AnimationOptions } from "@snk/gif-creator";
export const generateContributionSnake = async (
userName: string,
outputs: ({
format: "svg" | "gif";
drawOptions: DrawOptions;
gifOptions: {
frameDuration: number;
step: number;
};
animationOptions: AnimationOptions;
} | null)[]
) => {
console.log("🎣 fetching github user contribution");
@@ -29,17 +27,23 @@ export const generateContributionSnake = async (
return Promise.all(
outputs.map(async (out, i) => {
if (!out) return;
const { format, drawOptions, gifOptions } = out;
const { format, drawOptions, animationOptions } = out;
switch (format) {
case "svg": {
console.log(`🖌 creating svg (outputs[${i}])`);
const { createSvg } = await import("@snk/svg-creator");
return createSvg(grid, chain, drawOptions, gifOptions);
return createSvg(grid, cells, chain, drawOptions, animationOptions);
}
case "gif": {
console.log(`📹 creating gif (outputs[${i}])`);
const { createGif } = await import("@snk/gif-creator");
return await createGif(grid, chain, drawOptions, gifOptions);
return await createGif(
grid,
cells,
chain,
drawOptions,
animationOptions
);
}
}
})

View File

@@ -1,15 +1,28 @@
import { Options as DrawOptions } from "@snk/svg-creator";
import type { AnimationOptions } from "@snk/gif-creator";
import type { DrawOptions as DrawOptions } from "@snk/svg-creator";
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))(\?(.*))?$/);
if (!m) return null;
const [_, filename, format, query] = m;
const [, filename, format, , query] = m;
const sp = new URLSearchParams(query || "");
let sp = new URLSearchParams(query || "");
try {
const o = JSON.parse(query);
if (Array.isArray(o.color_dots)) o.color_dots = o.color_dots.join(",");
if (Array.isArray(o.dark_color_dots))
o.dark_color_dots = o.dark_color_dots.join(",");
sp = new URLSearchParams(o);
} catch (err) {
if (!(err instanceof SyntaxError)) throw err;
}
const drawOptions: DrawOptions = {
sizeDotBorderRadius: 2,
@@ -17,7 +30,7 @@ export const parseEntry = (entry: string) => {
sizeDot: 12,
...palettes["default"],
};
const gifOptions = { step: 1, frameDuration: 100 };
const animationOptions: AnimationOptions = { step: 1, frameDuration: 100 };
{
const palette = palettes[sp.get("palette")!];
@@ -50,5 +63,10 @@ export const parseEntry = (entry: string) => {
if (sp.has("dark_color_snake") && drawOptions.dark)
drawOptions.dark.colorSnake = sp.get("color_snake")!;
return { filename, format: format as "svg" | "gif", drawOptions, gifOptions };
return {
filename,
format: format as "svg" | "gif",
drawOptions,
animationOptions,
};
};

View File

@@ -1,4 +1,4 @@
import { Options as DrawOptions } from "@snk/svg-creator";
import { DrawOptions as DrawOptions } from "@snk/svg-creator";
export const palettes: Record<
string,

View File

@@ -1,7 +1,7 @@
import { Color, Grid } from "@snk/types/grid";
import { drawLerpWorld, drawWorld } from "@snk/draw/drawWorld";
import { Snake } from "@snk/types/snake";
import type { Options as DrawOptions } from "@snk/svg-creator";
import type { DrawOptions as DrawOptions } from "@snk/svg-creator";
export const drawOptions: DrawOptions = {
sizeDotBorderRadius: 2,
@@ -68,7 +68,7 @@ export const createCanvas = ({
const draw = (grid: Grid, snake: Snake, stack: Color[]) => {
ctx.clearRect(0, 0, 9999, 9999);
drawWorld(ctx, grid, snake, stack, drawOptions);
drawWorld(ctx, grid, null, snake, stack, drawOptions);
};
const drawLerp = (
@@ -79,7 +79,7 @@ export const createCanvas = ({
k: number
) => {
ctx.clearRect(0, 0, 9999, 9999);
drawLerpWorld(ctx, grid, snake0, snake1, stack, k, drawOptions);
drawLerpWorld(ctx, grid, null, snake0, snake1, stack, k, drawOptions);
};
const highlightCell = (x: number, y: number, color = "orange") => {

View File

@@ -3,6 +3,7 @@ import { step } from "@snk/solver/step";
import { isStableAndBound, stepSpring } from "./springUtils";
import type { Res } from "@snk/github-user-contribution";
import type { Snake } from "@snk/types/snake";
import type { Point } from "@snk/types/point";
import {
drawLerpWorld,
getCanvasWorldSize,
@@ -12,6 +13,7 @@ import { userContributionToGrid } from "@snk/action/userContributionToGrid";
import { createSvg } from "@snk/svg-creator";
import { createRpcClient } from "./worker-utils";
import type { API as WorkerAPI } from "./demo.interactive.worker";
import { AnimationOptions } from "@snk/gif-creator";
const createForm = ({
onSubmit,
@@ -116,10 +118,12 @@ const createGithubProfile = () => {
const createViewer = ({
grid0,
chain,
cells,
drawOptions,
}: {
grid0: Grid;
chain: Snake[];
cells: Point[];
drawOptions: DrawOptions;
}) => {
//
@@ -159,7 +163,7 @@ const createViewer = ({
const k = spring.x % 1;
ctx.clearRect(0, 0, 9999, 9999);
drawLerpWorld(ctx, grid, snake0, snake1, stack, k, drawOptions);
drawLerpWorld(ctx, grid, null, snake0, snake1, stack, k, drawOptions);
if (!stable) animationFrame = requestAnimationFrame(loop);
};
@@ -189,9 +193,9 @@ const createViewer = ({
//
// svg
const svgLink = document.createElement("a");
const svgString = createSvg(grid0, chain, drawOptions, {
const svgString = createSvg(grid0, cells, chain, drawOptions, {
frameDuration: 100,
});
} as AnimationOptions);
const svgImageUri = `data:image/*;charset=utf-8;base64,${btoa(svgString)}`;
svgLink.href = svgImageUri;
svgLink.innerText = "github-user-contribution.svg";
@@ -237,7 +241,6 @@ const onSubmit = async (userName: string) => {
colorDots: colorScheme as any,
colorEmpty: colorScheme[0],
colorSnake: "purple",
cells,
};
const grid = userContributionToGrid(cells, colorScheme);
@@ -246,7 +249,7 @@ const onSubmit = async (userName: string) => {
dispose();
createViewer({ grid0: grid, chain, drawOptions });
createViewer({ grid0: grid, chain, cells, drawOptions });
};
const worker = new Worker(

View File

@@ -4,12 +4,15 @@ import { createSvg } from "@snk/svg-creator";
import { grid, snake } from "./sample";
import { drawOptions } from "./canvas";
import { getPathToPose } from "@snk/solver/getPathToPose";
import type { AnimationOptions } from "@snk/gif-creator";
const chain = getBestRoute(grid, snake);
chain.push(...getPathToPose(chain.slice(-1)[0], snake)!);
(async () => {
const svg = await createSvg(grid, chain, drawOptions, { frameDuration: 200 });
const svg = await createSvg(grid, null, chain, drawOptions, {
frameDuration: 200,
} as AnimationOptions);
const container = document.createElement("div");
container.innerHTML = svg;

View File

@@ -10,17 +10,17 @@ type Options = {
sizeCell: number;
sizeDot: number;
sizeDotBorderRadius: number;
cells?: Point[];
};
export const drawGrid = (
ctx: CanvasRenderingContext2D,
grid: Grid,
cells: Point[] | null,
o: Options
) => {
for (let x = grid.width; x--; )
for (let y = grid.height; y--; ) {
if (!o.cells || o.cells.some((c) => c.x === x && c.y === y)) {
if (!cells || cells.some((c) => c.x === x && c.y === y)) {
const c = getColor(grid, x, y);
// @ts-ignore
const color = !c ? o.colorEmpty : o.colorDots[c];

View File

@@ -1,8 +1,8 @@
import { drawGrid } from "./drawGrid";
import { drawSnake, drawSnakeLerp } from "./drawSnake";
import type { Grid, Color } from "@snk/types/grid";
import type { Point } from "@snk/types/point";
import type { Snake } from "@snk/types/snake";
import type { Point } from "@snk/types/point";
export type Options = {
colorDots: Record<Color, string>;
@@ -12,7 +12,6 @@ export type Options = {
sizeCell: number;
sizeDot: number;
sizeDotBorderRadius: number;
cells?: Point[];
};
export const drawStack = (
@@ -37,6 +36,7 @@ export const drawStack = (
export const drawWorld = (
ctx: CanvasRenderingContext2D,
grid: Grid,
cells: Point[] | null,
snake: Snake,
stack: Color[],
o: Options
@@ -44,7 +44,7 @@ export const drawWorld = (
ctx.save();
ctx.translate(1 * o.sizeCell, 2 * o.sizeCell);
drawGrid(ctx, grid, o);
drawGrid(ctx, grid, cells, o);
drawSnake(ctx, snake, o);
ctx.restore();
@@ -68,6 +68,7 @@ export const drawWorld = (
export const drawLerpWorld = (
ctx: CanvasRenderingContext2D,
grid: Grid,
cells: Point[] | null,
snake0: Snake,
snake1: Snake,
stack: Color[],
@@ -77,7 +78,7 @@ export const drawLerpWorld = (
ctx.save();
ctx.translate(1 * o.sizeCell, 2 * o.sizeCell);
drawGrid(ctx, grid, o);
drawGrid(ctx, grid, cells, o);
drawSnakeLerp(ctx, snake0, snake1, k, o);
ctx.translate(0, (grid.height + 2) * o.sizeCell);

View File

@@ -2,7 +2,7 @@ import * as fs from "fs";
import { performance } from "perf_hooks";
import { createSnakeFromCells } from "@snk/types/snake";
import { realistic as grid } from "@snk/types/__fixtures__/grid";
import { createGif } from "..";
import { AnimationOptions, createGif } from "..";
import { getBestRoute } from "@snk/solver/getBestRoute";
import { getPathToPose } from "@snk/solver/getPathToPose";
import type { Options as DrawOptions } from "@snk/draw/drawWorld";
@@ -35,7 +35,7 @@ const drawOptions: DrawOptions = {
colorSnake: "purple",
};
const gifOptions = { frameDuration: 100, step: 1 };
const animationOptions: AnimationOptions = { frameDuration: 100, step: 1 };
(async () => {
for (
@@ -50,7 +50,13 @@ const gifOptions = { frameDuration: 100, step: 1 };
const chainL = chain.slice(0, length);
for (let k = 0; k < 10 && (Date.now() - start < 10 * 1000 || k < 2); k++) {
const s = performance.now();
buffer = await createGif(grid, chainL, drawOptions, gifOptions);
buffer = await createGif(
grid,
null,
chainL,
drawOptions,
animationOptions
);
stats.push(performance.now() - s);
}

View File

@@ -1,6 +1,6 @@
import * as fs from "fs";
import * as path from "path";
import { createGif } from "..";
import { AnimationOptions, createGif } from "..";
import * as grids from "@snk/types/__fixtures__/grid";
import { snake3 as snake } from "@snk/types/__fixtures__/snake";
import { createSnakeFromCells, nextSnake } from "@snk/types/snake";
@@ -20,7 +20,7 @@ const drawOptions: DrawOptions = {
colorSnake: "purple",
};
const gifOptions = { frameDuration: 200, step: 1 };
const animationOptions: AnimationOptions = { frameDuration: 200, step: 1 };
const dir = path.resolve(__dirname, "__snapshots__");
@@ -40,7 +40,13 @@ for (const key of [
const chain = [snake, ...getBestRoute(grid, snake)!];
const gif = await createGif(grid, chain, drawOptions, gifOptions);
const gif = await createGif(
grid,
null,
chain,
drawOptions,
animationOptions
);
expect(gif).toBeDefined();
@@ -64,7 +70,7 @@ it(`should generate swipper`, async () => {
}
}
const gif = await createGif(grid, chain, drawOptions, gifOptions);
const gif = await createGif(grid, null, chain, drawOptions, animationOptions);
expect(gif).toBeDefined();

View File

@@ -5,10 +5,11 @@ import { createCanvas } from "canvas";
import { Grid, copyGrid, Color } from "@snk/types/grid";
import { Snake } from "@snk/types/snake";
import {
Options,
Options as DrawOptions,
drawLerpWorld,
getCanvasWorldSize,
} from "@snk/draw/drawWorld";
import type { Point } from "@snk/types/point";
import { step } from "@snk/solver/step";
import tmp from "tmp";
import gifsicle from "gifsicle";
@@ -29,11 +30,14 @@ const withTmpDir = async <T>(
}
};
export type AnimationOptions = { frameDuration: number; step: number };
export const createGif = async (
grid0: Grid,
cells: Point[] | null,
chain: Snake[],
drawOptions: Options,
gifOptions: { frameDuration: number; step: number }
drawOptions: DrawOptions,
animationOptions: AnimationOptions
) =>
withTmpDir(async (dir) => {
const { width, height } = getCanvasWorldSize(grid0, drawOptions);
@@ -46,7 +50,7 @@ export const createGif = async (
const encoder = new GIFEncoder(width, height, "neuquant", true);
encoder.setRepeat(0);
encoder.setDelay(gifOptions.frameDuration);
encoder.setDelay(animationOptions.frameDuration);
encoder.start();
for (let i = 0; i < chain.length; i += 1) {
@@ -54,17 +58,18 @@ export const createGif = async (
const snake1 = chain[Math.min(chain.length - 1, i + 1)];
step(grid, stack, snake0);
for (let k = 0; k < gifOptions.step; k++) {
for (let k = 0; k < animationOptions.step; k++) {
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, width, height);
drawLerpWorld(
ctx,
grid,
cells,
snake0,
snake1,
stack,
k / gifOptions.step,
k / animationOptions.step,
drawOptions
);

View File

@@ -1,11 +1,12 @@
import * as fs from "fs";
import * as path from "path";
import { createSvg, Options } from "..";
import { createSvg, DrawOptions as DrawOptions } from "..";
import * as grids from "@snk/types/__fixtures__/grid";
import { snake3 as snake } from "@snk/types/__fixtures__/snake";
import { getBestRoute } from "@snk/solver/getBestRoute";
import { AnimationOptions } from "@snk/gif-creator";
const drawOptions: Options = {
const drawOptions: DrawOptions = {
sizeDotBorderRadius: 2,
sizeCell: 16,
sizeDot: 12,
@@ -19,7 +20,7 @@ const drawOptions: Options = {
},
};
const gifOptions = { frameDuration: 100, step: 1 };
const animationOptions: AnimationOptions = { frameDuration: 100, step: 1 };
const dir = path.resolve(__dirname, "__snapshots__");
@@ -31,7 +32,13 @@ for (const [key, grid] of Object.entries(grids))
it(`should generate ${key} svg`, async () => {
const chain = [snake, ...getBestRoute(grid, snake)!];
const svg = await createSvg(grid, chain, drawOptions, gifOptions);
const svg = await createSvg(
grid,
null,
chain,
drawOptions,
animationOptions
);
expect(svg).toBeDefined();

View File

@@ -14,8 +14,9 @@ import { createGrid } from "./grid";
import { createStack } from "./stack";
import { h } from "./utils";
import * as csso from "csso";
import { AnimationOptions } from "@snk/gif-creator";
export type Options = {
export type DrawOptions = {
colorDots: Record<Color, string>;
colorEmpty: string;
colorDotBorder: string;
@@ -23,7 +24,6 @@ export type Options = {
sizeCell: number;
sizeDot: number;
sizeDotBorderRadius: number;
cells?: Point[];
dark?: {
colorDots: Record<Color, string>;
colorEmpty: string;
@@ -40,12 +40,12 @@ const getCellsFromGrid = ({ width, height }: Grid) =>
const createLivingCells = (
grid0: Grid,
chain: Snake[],
drawOptions: Options
cells: Point[] | null
) => {
const cells: (Point & {
const livingCells: (Point & {
t: number | null;
color: Color | Empty;
})[] = (drawOptions.cells ?? getCellsFromGrid(grid0)).map(({ x, y }) => ({
})[] = (cells ?? getCellsFromGrid(grid0)).map(({ x, y }) => ({
x,
y,
t: null,
@@ -60,31 +60,32 @@ const createLivingCells = (
if (isInside(grid, x, y) && !isEmpty(getColor(grid, x, y))) {
setColorEmpty(grid, x, y);
const cell = cells.find((c) => c.x === x && c.y === y)!;
const cell = livingCells.find((c) => c.x === x && c.y === y)!;
cell.t = i / chain.length;
}
}
return cells;
return livingCells;
};
export const createSvg = (
grid: Grid,
cells: Point[] | null,
chain: Snake[],
drawOptions: Options,
gifOptions: { frameDuration: number }
drawOptions: DrawOptions,
animationOptions: Pick<AnimationOptions, "frameDuration">
) => {
const width = (grid.width + 2) * drawOptions.sizeCell;
const height = (grid.height + 5) * drawOptions.sizeCell;
const duration = gifOptions.frameDuration * chain.length;
const duration = animationOptions.frameDuration * chain.length;
const cells = createLivingCells(grid, chain, drawOptions);
const livingCells = createLivingCells(grid, chain, cells);
const elements = [
createGrid(cells, drawOptions, duration),
createGrid(livingCells, drawOptions, duration),
createStack(
cells,
livingCells,
drawOptions,
grid.width * drawOptions.sizeCell,
(grid.height + 2) * drawOptions.sizeCell,
@@ -134,7 +135,7 @@ export const createSvg = (
const optimizeCss = (css: string) => csso.minify(css).css;
const optimizeSvg = (svg: string) => svg;
const generateColorVar = (drawOptions: Options) =>
const generateColorVar = (drawOptions: DrawOptions) =>
`
:root {
--cb: ${drawOptions.colorDotBorder};

View File

@@ -10,11 +10,22 @@ inputs:
github_user_name:
description: "github user name"
required: true
svg_out_path:
description: "path of the generated svg file. If left empty, the svg file will not be generated."
outputs:
required: false
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>
outputs:
svg_out_path:
description: "path of the generated svg"
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 ...
example:
outputs: |
dark.svg?palette=github-dark&color_snake=blue
light.svg?color_snake=#7845ab
ocean.svg?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9

View File

@@ -32796,13 +32796,13 @@ var generateContributionSnake = function (userName, outputs) { return __awaiter(
chain = (0, getBestRoute_1.getBestRoute)(grid, snake);
chain.push.apply(chain, (0, getPathToPose_1.getPathToPose)(chain.slice(-1)[0], snake));
return [2 /*return*/, Promise.all(outputs.map(function (out, i) { return __awaiter(void 0, void 0, void 0, function () {
var format, drawOptions, gifOptions, _a, createSvg, createGif;
var format, drawOptions, animationOptions, _a, createSvg, createGif;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
if (!out)
return [2 /*return*/];
format = out.format, drawOptions = out.drawOptions, gifOptions = out.gifOptions;
format = out.format, drawOptions = out.drawOptions, animationOptions = out.animationOptions;
_a = format;
switch (_a) {
case "svg": return [3 /*break*/, 1];
@@ -32814,13 +32814,13 @@ var generateContributionSnake = function (userName, outputs) { return __awaiter(
return [4 /*yield*/, Promise.resolve().then(function () { return __importStar(__webpack_require__(7415)); })];
case 2:
createSvg = (_b.sent()).createSvg;
return [2 /*return*/, createSvg(grid, chain, drawOptions, gifOptions)];
return [2 /*return*/, createSvg(grid, cells, chain, drawOptions, animationOptions)];
case 3:
console.log("\uD83D\uDCF9 creating gif (outputs[".concat(i, "])"));
return [4 /*yield*/, Promise.resolve().then(function () { return __importStar(__webpack_require__(340)); })];
case 4:
createGif = (_b.sent()).createGif;
return [4 /*yield*/, createGif(grid, chain, drawOptions, gifOptions)];
return [4 /*yield*/, createGif(grid, cells, chain, drawOptions, animationOptions)];
case 5: return [2 /*return*/, _b.sent()];
case 6: return [2 /*return*/];
}
@@ -32962,13 +32962,25 @@ var palettes_1 = __webpack_require__(3848);
var parseOutputsOption = function (lines) { return lines.map(exports.parseEntry); };
exports.parseOutputsOption = parseOutputsOption;
var parseEntry = function (entry) {
var m = entry.trim().match(/^(.+\.(svg|gif))(\?.*)?$/);
var m = entry.trim().match(/^(.+\.(svg|gif))(\?(.*))?$/);
if (!m)
return null;
var _ = m[0], filename = m[1], format = m[2], query = m[3];
var filename = m[1], format = m[2], query = m[4];
var sp = new URLSearchParams(query || "");
try {
var o = JSON.parse(query);
if (Array.isArray(o.color_dots))
o.color_dots = o.color_dots.join(",");
if (Array.isArray(o.dark_color_dots))
o.dark_color_dots = o.dark_color_dots.join(",");
sp = new URLSearchParams(o);
}
catch (err) {
if (!(err instanceof SyntaxError))
throw err;
}
var drawOptions = __assign({ sizeDotBorderRadius: 2, sizeCell: 16, sizeDot: 12 }, palettes_1.palettes["default"]);
var gifOptions = { step: 1, frameDuration: 100 };
var animationOptions = { step: 1, frameDuration: 100 };
{
var palette = palettes_1.palettes[sp.get("palette")];
if (palette) {
@@ -32994,7 +33006,12 @@ var parseEntry = function (entry) {
drawOptions.dark.colorDotBorder = sp.get("color_dot_border");
if (sp.has("dark_color_snake") && drawOptions.dark)
drawOptions.dark.colorSnake = sp.get("color_snake");
return { filename: filename, format: format, drawOptions: drawOptions, gifOptions: gifOptions };
return {
filename: filename,
format: format,
drawOptions: drawOptions,
animationOptions: animationOptions
};
};
exports.parseEntry = parseEntry;
@@ -33085,10 +33102,10 @@ exports.__esModule = true;
exports.drawGrid = void 0;
var grid_1 = __webpack_require__(2881);
var pathRoundedRect_1 = __webpack_require__(2356);
var drawGrid = function (ctx, grid, o) {
var drawGrid = function (ctx, grid, cells, o) {
var _loop_1 = function (x) {
var _loop_2 = function (y) {
if (!o.cells || o.cells.some(function (c) { return c.x === x && c.y === y; })) {
if (!cells || cells.some(function (c) { return c.x === x && c.y === y; })) {
var c = (0, grid_1.getColor)(grid, x, y);
// @ts-ignore
var color = !c ? o.colorEmpty : o.colorDots[c];
@@ -33186,10 +33203,10 @@ var drawStack = function (ctx, stack, max, width, o) {
ctx.restore();
};
exports.drawStack = drawStack;
var drawWorld = function (ctx, grid, snake, stack, o) {
var drawWorld = function (ctx, grid, cells, snake, stack, o) {
ctx.save();
ctx.translate(1 * o.sizeCell, 2 * o.sizeCell);
(0, drawGrid_1.drawGrid)(ctx, grid, o);
(0, drawGrid_1.drawGrid)(ctx, grid, cells, o);
(0, drawSnake_1.drawSnake)(ctx, snake, o);
ctx.restore();
ctx.save();
@@ -33204,10 +33221,10 @@ var drawWorld = function (ctx, grid, snake, stack, o) {
// ctx.restore();
};
exports.drawWorld = drawWorld;
var drawLerpWorld = function (ctx, grid, snake0, snake1, stack, k, o) {
var drawLerpWorld = function (ctx, grid, cells, snake0, snake1, stack, k, o) {
ctx.save();
ctx.translate(1 * o.sizeCell, 2 * o.sizeCell);
(0, drawGrid_1.drawGrid)(ctx, grid, o);
(0, drawGrid_1.drawGrid)(ctx, grid, cells, o);
(0, drawSnake_1.drawSnakeLerp)(ctx, snake0, snake1, k, o);
ctx.translate(0, (grid.height + 2) * o.sizeCell);
var max = grid.data.reduce(function (sum, x) { return sum + +!!x; }, stack.length);
@@ -33321,7 +33338,7 @@ var withTmpDir = function (handler) { return __awaiter(void 0, void 0, void 0, f
}
});
}); };
var createGif = function (grid0, chain, drawOptions, gifOptions) { return __awaiter(void 0, void 0, void 0, function () {
var createGif = function (grid0, cells, chain, drawOptions, animationOptions) { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
return [2 /*return*/, withTmpDir(function (dir) { return __awaiter(void 0, void 0, void 0, function () {
var _a, width, height, canvas, ctx, grid, stack, encoder, i, snake0, snake1, k, outFileName, optimizedFileName;
@@ -33333,17 +33350,17 @@ var createGif = function (grid0, chain, drawOptions, gifOptions) { return __awai
stack = [];
encoder = new gif_encoder_2_1["default"](width, height, "neuquant", true);
encoder.setRepeat(0);
encoder.setDelay(gifOptions.frameDuration);
encoder.setDelay(animationOptions.frameDuration);
encoder.start();
for (i = 0; i < chain.length; i += 1) {
snake0 = chain[i];
snake1 = chain[Math.min(chain.length - 1, i + 1)];
(0, step_1.step)(grid, stack, snake0);
for (k = 0; k < gifOptions.step; k++) {
for (k = 0; k < animationOptions.step; k++) {
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, width, height);
(0, drawWorld_1.drawLerpWorld)(ctx, grid, snake0, snake1, stack, k / gifOptions.step, drawOptions);
(0, drawWorld_1.drawLerpWorld)(ctx, grid, cells, snake0, snake1, stack, k / animationOptions.step, drawOptions);
encoder.addFrame(ctx);
}
}
@@ -34468,9 +34485,8 @@ var getCellsFromGrid = function (_a) {
return Array.from({ length: height }, function (_, y) { return ({ x: x, y: y }); });
}).flat();
};
var createLivingCells = function (grid0, chain, drawOptions) {
var _a;
var cells = ((_a = drawOptions.cells) !== null && _a !== void 0 ? _a : getCellsFromGrid(grid0)).map(function (_a) {
var createLivingCells = function (grid0, chain, cells) {
var livingCells = (cells !== null && cells !== void 0 ? cells : getCellsFromGrid(grid0)).map(function (_a) {
var x = _a.x, y = _a.y;
return ({
x: x,
@@ -34486,23 +34502,23 @@ var createLivingCells = function (grid0, chain, drawOptions) {
var y = (0, snake_1.getHeadY)(snake);
if ((0, grid_1.isInside)(grid, x, y) && !(0, grid_1.isEmpty)((0, grid_1.getColor)(grid, x, y))) {
(0, grid_1.setColorEmpty)(grid, x, y);
var cell = cells.find(function (c) { return c.x === x && c.y === y; });
var cell = livingCells.find(function (c) { return c.x === x && c.y === y; });
cell.t = i / chain.length;
}
};
for (var i = 0; i < chain.length; i++) {
_loop_1(i);
}
return cells;
return livingCells;
};
var createSvg = function (grid, chain, drawOptions, gifOptions) {
var createSvg = function (grid, cells, chain, drawOptions, animationOptions) {
var width = (grid.width + 2) * drawOptions.sizeCell;
var height = (grid.height + 5) * drawOptions.sizeCell;
var duration = gifOptions.frameDuration * chain.length;
var cells = createLivingCells(grid, chain, drawOptions);
var duration = animationOptions.frameDuration * chain.length;
var livingCells = createLivingCells(grid, chain, cells);
var elements = [
(0, grid_2.createGrid)(cells, drawOptions, duration),
(0, stack_1.createStack)(cells, drawOptions, grid.width * drawOptions.sizeCell, (grid.height + 2) * drawOptions.sizeCell, duration),
(0, grid_2.createGrid)(livingCells, drawOptions, duration),
(0, stack_1.createStack)(livingCells, drawOptions, grid.width * drawOptions.sizeCell, (grid.height + 2) * drawOptions.sizeCell, duration),
(0, snake_2.createSnake)(chain, drawOptions, duration),
];
var viewBox = [