Compare commits
41 Commits
v1.0.1
...
v2.0.0-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e275adbb6 | ||
|
|
66fef03781 | ||
|
|
5841a21a09 | ||
|
|
cce5c4514d | ||
|
|
fb82d42d53 | ||
|
|
e3ad8b2caf | ||
|
|
c21e390ca9 | ||
|
|
7077112ba4 | ||
|
|
e7aa7b7289 | ||
|
|
6b320a1ac4 | ||
|
|
579bcf1afe | ||
|
|
1018f7a937 | ||
|
|
4edf90f41b | ||
|
|
faf76e6eb6 | ||
|
|
5bede02e06 | ||
|
|
4f7ff9bc90 | ||
|
|
b0d592375a | ||
|
|
672fe6bf0e | ||
|
|
829a59da98 | ||
|
|
58176f658e | ||
|
|
9c881735b7 | ||
|
|
3c697c687e | ||
|
|
825e58e5fd | ||
|
|
9232c14971 | ||
|
|
cd3320efff | ||
|
|
553d8d8efa | ||
|
|
e80a44ca5f | ||
|
|
4ced502e11 | ||
|
|
0374e20a50 | ||
|
|
7ba88d1fbd | ||
|
|
909a9c7fce | ||
|
|
e1dcae75b9 | ||
|
|
5df41911e6 | ||
|
|
c9b130d9da | ||
|
|
05df7cb642 | ||
|
|
309795a2a5 | ||
|
|
e79b3bb634 | ||
|
|
7c0522bfa8 | ||
|
|
be91c43c71 | ||
|
|
67c66ac8ae | ||
|
|
c97378f175 |
74
.github/workflows/main.yml
vendored
74
.github/workflows/main.yml
vendored
@@ -18,43 +18,38 @@ 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
|
||||
needs: build-docker-image
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: update action.yml to use image from local Dockerfile
|
||||
run: |
|
||||
sed -i "s/image: .*/image: Dockerfile/" action.yml
|
||||
|
||||
- name: generate-snake-game-from-github-contribution-grid
|
||||
id: snake-gif
|
||||
uses: Platane/snk@master
|
||||
id: generate-snake
|
||||
uses: ./
|
||||
with:
|
||||
github_user_name: platane
|
||||
gif_out_path: dist/github-contribution-grid-snake.gif
|
||||
svg_out_path: dist/github-contribution-grid-snake.svg
|
||||
outputs: |
|
||||
dist/github-contribution-grid-snake.svg
|
||||
dist/github-contribution-grid-snake-dark.svg?palette=github-dark
|
||||
dist/github-contribution-grid-snake.gif?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9
|
||||
|
||||
- name: ensure the generated file exists
|
||||
run: |
|
||||
ls -l ${{ steps.snake-gif.outputs.gif_out_path }}
|
||||
test -f ${{ steps.snake-gif.outputs.gif_out_path }}
|
||||
ls dist
|
||||
test -f dist/github-contribution-grid-snake.svg
|
||||
test -f dist/github-contribution-grid-snake-dark.svg
|
||||
test -f dist/github-contribution-grid-snake.gif
|
||||
|
||||
- uses: crazy-max/ghaction-github-pages@v2.5.0
|
||||
with:
|
||||
target_branch: output
|
||||
build_dir: dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.MY_GITHUB_TOKEN_GH_PAGES }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
deploy-ghpages:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -70,41 +65,10 @@ jobs:
|
||||
env:
|
||||
GITHUB_USER_CONTRIBUTION_API_ENDPOINT: https://snk-one.vercel.app/api/github-user-contribution/
|
||||
|
||||
- uses: crazy-max/ghaction-github-pages@v2.5.0
|
||||
if: success() && github.ref == 'refs/heads/master'
|
||||
- uses: crazy-max/ghaction-github-pages@v2.6.0
|
||||
if: success() && github.ref == 'refs/heads/main'
|
||||
with:
|
||||
target_branch: gh-pages
|
||||
build_dir: packages/demo/dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.MY_GITHUB_TOKEN_GH_PAGES }}
|
||||
|
||||
build-docker-image:
|
||||
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: yarn build:action
|
||||
|
||||
- uses: docker/setup-qemu-action@v1
|
||||
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
|
||||
- uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- uses: docker/build-push-action@v2
|
||||
id: docker_build
|
||||
with:
|
||||
push: ${{ github.ref == 'refs/heads/master' }}
|
||||
tags: |
|
||||
platane/snk:latest
|
||||
platane/snk:${{ github.sha }}
|
||||
file: packages/action/Dockerfile
|
||||
context: packages/action
|
||||
|
||||
85
.github/workflows/release.yml
vendored
Normal file
85
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
name: release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version"
|
||||
default: "0.0.1"
|
||||
required: true
|
||||
type: string
|
||||
description:
|
||||
description: "Version description"
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: docker/setup-qemu-action@v1
|
||||
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
|
||||
- uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: build and publish the docker image
|
||||
uses: docker/build-push-action@v2
|
||||
id: docker-build
|
||||
with:
|
||||
push: true
|
||||
tags: |
|
||||
platane/snk:${{ github.sha }}
|
||||
platane/snk:${{ github.event.inputs.version }}
|
||||
|
||||
- name: update action.yml to point to the newly created docker image
|
||||
run: |
|
||||
sed -i "s/image: .*/image: docker:\/\/platane\/snk@${{ steps.docker-build.outputs.digest }}/" action.yml
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
cache: yarn
|
||||
node-version: 16
|
||||
|
||||
- name: build svg-only action
|
||||
run: |
|
||||
yarn install --frozen-lockfile
|
||||
yarn build:action
|
||||
rm -r svg-only/dist
|
||||
mv packages/action/dist svg-only/dist
|
||||
|
||||
- name: bump package version
|
||||
run: yarn version --no-git-tag-version --new-version ${{ github.event.inputs.version }}
|
||||
|
||||
- name: push new build, tag version and push
|
||||
id: push-tags
|
||||
run: |
|
||||
VERSION=${{ github.event.inputs.version }}
|
||||
|
||||
git config --global user.email "bot@platane.me"
|
||||
git config --global user.name "release bot"
|
||||
git add package.json svg-only/dist action.yml
|
||||
git commit -m "📦 $VERSION"
|
||||
git tag v$VERSION
|
||||
git push origin main --tags
|
||||
|
||||
if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
git tag v$( echo $VERSION | cut -d. -f 1-1 )
|
||||
git tag v$( echo $VERSION | cut -d. -f 1-2 )
|
||||
git push origin --tags --force
|
||||
echo ::set-output name=prerelease::false
|
||||
else
|
||||
echo ::set-output name=prerelease::true
|
||||
fi
|
||||
|
||||
- uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: v${{ github.event.inputs.version }}
|
||||
body: ${{ github.event.inputs.description }}
|
||||
prerelease: ${{ steps.push-tags.outputs.prerelease }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,4 +2,5 @@ node_modules
|
||||
npm-debug.log*
|
||||
yarn-error.log*
|
||||
dist
|
||||
!svg-only/dist
|
||||
build
|
||||
32
Dockerfile
Normal file
32
Dockerfile
Normal file
@@ -0,0 +1,32 @@
|
||||
FROM node:16-slim as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
COPY tsconfig.json ./
|
||||
|
||||
COPY packages packages
|
||||
|
||||
RUN export YARN_CACHE_FOLDER="$(mktemp -d)" \
|
||||
&& yarn install --frozen-lockfile \
|
||||
&& rm -r "$YARN_CACHE_FOLDER"
|
||||
|
||||
RUN yarn build:action
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
FROM node:16-slim
|
||||
|
||||
WORKDIR /action-release
|
||||
|
||||
RUN export YARN_CACHE_FOLDER="$(mktemp -d)" \
|
||||
&& yarn add canvas@2.9.1 gifsicle@5.3.0 --no-lockfile \
|
||||
&& rm -r "$YARN_CACHE_FOLDER"
|
||||
|
||||
COPY --from=builder /app/packages/action/dist/ /action-release/
|
||||
|
||||
CMD ["node", "/action-release/index.js"]
|
||||
|
||||
20
README.md
20
README.md
@@ -1,5 +1,6 @@
|
||||
# snk
|
||||
|
||||
[](https://github.com/platane/snk/releases/latest)
|
||||
[](https://github.com/marketplace/actions/generate-snake-game-from-github-contribution-grid)
|
||||

|
||||

|
||||
@@ -20,22 +21,23 @@ Available as github action. Automatically generate a new image at the end of the
|
||||
**github action**
|
||||
|
||||
```yaml
|
||||
- uses: Platane/snk@master
|
||||
- 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
|
||||
github_user_name: ${{ github.repository_owner }}
|
||||
|
||||
# path of the generated gif file
|
||||
# If left empty, the gif file will not be generated
|
||||
gif_out_path: dist/github-snake.gif
|
||||
|
||||
# path of the generated svg file
|
||||
# If left empty, the svg file will not be generated
|
||||
svg_out_path: dist/github-snake.svg
|
||||
# list of files to generate.
|
||||
# one file per line. Each output can be customized with options as query string.
|
||||
outputs: |
|
||||
dist/github-snake.svg
|
||||
dist/github-snake.svg?palette=github-dark
|
||||
dist/ocean.gif?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9
|
||||
```
|
||||
|
||||
> [example with cron job](https://github.com/Platane/Platane/blob/master/.github/workflows/main.yml#L24-L29)
|
||||
[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`
|
||||
|
||||
**interactive demo**
|
||||
|
||||
|
||||
31
action.yml
31
action.yml
@@ -3,24 +3,29 @@ description: "Generates a snake game from a github user contributions grid. Outp
|
||||
author: "platane"
|
||||
|
||||
runs:
|
||||
using: "docker"
|
||||
image: "docker://platane/snk:latest"
|
||||
using: docker
|
||||
image: docker://platane/snk@sha256:e40bb02de6ed0f164eca8586b3f6c32109b2bcb426cd57c6882764825b40fe0d
|
||||
|
||||
inputs:
|
||||
github_user_name:
|
||||
description: "github user name"
|
||||
required: true
|
||||
gif_out_path:
|
||||
description: "path of the generated gif file. If left empty, the gif file will not be generated."
|
||||
required: false
|
||||
default: null
|
||||
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.<gif or svg>?palette=<github or github-dark>&color_snake=<color>&color_dots=<color>,<color>,<color>,<color>,<color>
|
||||
|
||||
outputs:
|
||||
gif_out_path:
|
||||
description: "path of the generated gif"
|
||||
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.gif?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9
|
||||
|
||||
10
package.json
10
package.json
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"name": "snk",
|
||||
"description": "Generates a snake game from a github user contributions grid",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0-rc.3",
|
||||
"private": true,
|
||||
"repository": "github:platane/snk",
|
||||
"devDependencies": {
|
||||
"@types/jest": "27.4.1",
|
||||
"@types/node": "16.11.7",
|
||||
"jest": "27.5.1",
|
||||
"prettier": "2.6.0",
|
||||
"ts-jest": "27.1.3",
|
||||
"typescript": "4.6.2"
|
||||
"prettier": "2.6.2",
|
||||
"ts-jest": "27.1.4",
|
||||
"typescript": "4.6.3"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/**"
|
||||
],
|
||||
"scripts": {
|
||||
"type": "tsc --noEmit",
|
||||
"lint": "yarn prettier -c '**/*.{ts,js,json,md,yml,yaml}' '!packages/*/dist/**'",
|
||||
"lint": "yarn prettier -c '**/*.{ts,js,json,md,yml,yaml}' '!packages/*/dist/**' '!svg-only/dist/**'",
|
||||
"test": "jest --verbose --passWithNoTests --no-cache",
|
||||
"dev:demo": "( cd packages/demo ; yarn dev )",
|
||||
"build:demo": "( cd packages/demo ; yarn build )",
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
FROM node:16-slim
|
||||
|
||||
WORKDIR /github/snk
|
||||
|
||||
RUN npm install canvas@2.8.0 gifsicle@5.2.0 --no-save --no-package-lock
|
||||
|
||||
COPY dist /github/snk/
|
||||
|
||||
CMD ["node", "/github/snk/index.js"]
|
||||
@@ -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.
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
*
|
||||
!.gitignore
|
||||
!.gitignore
|
||||
!*.snap
|
||||
@@ -0,0 +1,129 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
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 [
|
||||
"#000",
|
||||
"#111",
|
||||
"#222",
|
||||
"#333",
|
||||
"#444",
|
||||
],
|
||||
"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,
|
||||
"sizeDot": 12,
|
||||
"sizeDotBorderRadius": 2,
|
||||
},
|
||||
"filename": "/out.svg",
|
||||
"format": "svg",
|
||||
}
|
||||
`;
|
||||
|
||||
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 [
|
||||
"#ebedf0",
|
||||
"#9be9a8",
|
||||
"#40c463",
|
||||
"#30a14e",
|
||||
"#216e39",
|
||||
],
|
||||
"colorEmpty": "#ebedf0",
|
||||
"colorSnake": "purple",
|
||||
"dark": Object {
|
||||
"colorDotBorder": "#1b1f230a",
|
||||
"colorDots": Array [
|
||||
"#161b22",
|
||||
"#01311f",
|
||||
"#034525",
|
||||
"#0f6d31",
|
||||
"#00c647",
|
||||
],
|
||||
"colorEmpty": "#161b22",
|
||||
"colorSnake": "purple",
|
||||
},
|
||||
"sizeCell": 16,
|
||||
"sizeDot": 12,
|
||||
"sizeDotBorderRadius": 2,
|
||||
},
|
||||
"filename": "path/to/out.gif",
|
||||
"format": "gif",
|
||||
}
|
||||
`;
|
||||
@@ -1,19 +0,0 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { generateContributionSnake } from "../generateContributionSnake";
|
||||
|
||||
(async () => {
|
||||
const outputSvg = path.join(__dirname, "__snapshots__/out.svg");
|
||||
const outputGif = path.join(__dirname, "__snapshots__/out.gif");
|
||||
|
||||
const buffer = await generateContributionSnake("platane", {
|
||||
svg: true,
|
||||
gif: true,
|
||||
});
|
||||
|
||||
console.log("💾 writing to", outputSvg);
|
||||
fs.writeFileSync(outputSvg, buffer.svg);
|
||||
|
||||
console.log("💾 writing to", outputGif);
|
||||
fs.writeFileSync(outputGif, buffer.gif);
|
||||
})();
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { generateContributionSnake } from "../generateContributionSnake";
|
||||
import { parseOutputsOption } from "../outputsOptions";
|
||||
|
||||
jest.setTimeout(2 * 60 * 1000);
|
||||
|
||||
@@ -17,22 +18,26 @@ const silent = (handler: () => void | Promise<void>) => async () => {
|
||||
it(
|
||||
"should generate contribution snake",
|
||||
silent(async () => {
|
||||
const outputSvg = path.join(__dirname, "__snapshots__/out.svg");
|
||||
const outputGif = path.join(__dirname, "__snapshots__/out.gif");
|
||||
const entries = [
|
||||
path.join(__dirname, "__snapshots__/out.svg"),
|
||||
|
||||
console.log = () => undefined;
|
||||
const buffer = await generateContributionSnake("platane", {
|
||||
svg: true,
|
||||
gif: true,
|
||||
});
|
||||
path.join(__dirname, "__snapshots__/out-dark.svg") +
|
||||
"?palette=github-dark&color_snake=orange",
|
||||
|
||||
expect(buffer.svg).toBeDefined();
|
||||
expect(buffer.gif).toBeDefined();
|
||||
path.join(__dirname, "__snapshots__/out.gif") +
|
||||
"?color_snake=orange&color_dots=#d4e0f0,#8dbdff,#64a1f4,#4b91f1,#3c7dd9",
|
||||
];
|
||||
|
||||
console.log("💾 writing to", outputSvg);
|
||||
fs.writeFileSync(outputSvg, buffer.svg);
|
||||
const outputs = parseOutputsOption(entries);
|
||||
|
||||
console.log("💾 writing to", outputGif);
|
||||
fs.writeFileSync(outputGif, buffer.gif);
|
||||
const results = await generateContributionSnake("platane", outputs);
|
||||
|
||||
expect(results[0]).toBeDefined();
|
||||
expect(results[1]).toBeDefined();
|
||||
expect(results[2]).toBeDefined();
|
||||
|
||||
fs.writeFileSync(outputs[0]!.filename, results[0]!);
|
||||
fs.writeFileSync(outputs[1]!.filename, results[1]!);
|
||||
fs.writeFileSync(outputs[2]!.filename, results[2]!);
|
||||
})
|
||||
);
|
||||
|
||||
15
packages/action/__tests__/outputsOptions.spec.ts
Normal file
15
packages/action/__tests__/outputsOptions.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { parseEntry } from "../outputsOptions";
|
||||
|
||||
[
|
||||
"path/to/out.gif",
|
||||
|
||||
"/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();
|
||||
})
|
||||
);
|
||||
@@ -1,14 +1,18 @@
|
||||
import { getGithubUserContribution } from "@snk/github-user-contribution";
|
||||
import { userContributionToGrid } from "./userContributionToGrid";
|
||||
import { getBestRoute } from "@snk/solver/getBestRoute";
|
||||
import { createGif } from "@snk/gif-creator";
|
||||
import { createSvg } from "../svg-creator";
|
||||
import { snake4 } from "@snk/types/__fixtures__/snake";
|
||||
import { getPathToPose } from "@snk/solver/getPathToPose";
|
||||
import type { DrawOptions as DrawOptions } from "@snk/svg-creator";
|
||||
import type { AnimationOptions } from "@snk/gif-creator";
|
||||
|
||||
export const generateContributionSnake = async (
|
||||
userName: string,
|
||||
format: { svg?: boolean; gif?: boolean }
|
||||
outputs: ({
|
||||
format: "svg" | "gif";
|
||||
drawOptions: DrawOptions;
|
||||
animationOptions: AnimationOptions;
|
||||
} | null)[]
|
||||
) => {
|
||||
console.log("🎣 fetching github user contribution");
|
||||
const { cells, colorScheme } = await getGithubUserContribution(userName);
|
||||
@@ -16,38 +20,32 @@ export const generateContributionSnake = async (
|
||||
const grid = userContributionToGrid(cells, colorScheme);
|
||||
const snake = snake4;
|
||||
|
||||
const drawOptions = {
|
||||
sizeBorderRadius: 2,
|
||||
sizeCell: 16,
|
||||
sizeDot: 12,
|
||||
colorBorder: "#1b1f230a",
|
||||
colorDots: colorScheme as any,
|
||||
colorEmpty: colorScheme[0],
|
||||
colorSnake: "purple",
|
||||
cells,
|
||||
dark: {
|
||||
colorEmpty: "#161b22",
|
||||
colorDots: { 1: "#01311f", 2: "#034525", 3: "#0f6d31", 4: "#00c647" },
|
||||
},
|
||||
};
|
||||
|
||||
const gifOptions = { frameDuration: 100, step: 1 };
|
||||
|
||||
console.log("📡 computing best route");
|
||||
const chain = getBestRoute(grid, snake)!;
|
||||
chain.push(...getPathToPose(chain.slice(-1)[0], snake)!);
|
||||
|
||||
const output: Record<string, Buffer | string> = {};
|
||||
|
||||
if (format.gif) {
|
||||
console.log("📹 creating gif");
|
||||
output.gif = await createGif(grid, chain, drawOptions, gifOptions);
|
||||
}
|
||||
|
||||
if (format.svg) {
|
||||
console.log("🖌 creating svg");
|
||||
output.svg = createSvg(grid, chain, drawOptions, gifOptions);
|
||||
}
|
||||
|
||||
return output;
|
||||
return Promise.all(
|
||||
outputs.map(async (out, i) => {
|
||||
if (!out) return;
|
||||
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, cells, chain, drawOptions, animationOptions);
|
||||
}
|
||||
case "gif": {
|
||||
console.log(`📹 creating gif (outputs[${i}])`);
|
||||
const { createGif } = await import("@snk/gif-creator");
|
||||
return await createGif(
|
||||
grid,
|
||||
cells,
|
||||
chain,
|
||||
drawOptions,
|
||||
animationOptions
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,30 +2,28 @@ import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as core from "@actions/core";
|
||||
import { generateContributionSnake } from "./generateContributionSnake";
|
||||
import { parseOutputsOption } from "./outputsOptions";
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const userName = core.getInput("github_user_name");
|
||||
const format = {
|
||||
svg: core.getInput("svg_out_path"),
|
||||
gif: core.getInput("gif_out_path"),
|
||||
};
|
||||
|
||||
const { svg, gif } = await generateContributionSnake(
|
||||
userName,
|
||||
format as any
|
||||
const outputs = parseOutputsOption(
|
||||
core.getMultilineInput("outputs") ?? [
|
||||
core.getInput("gif_out_path"),
|
||||
core.getInput("svg_out_path"),
|
||||
]
|
||||
);
|
||||
|
||||
if (svg) {
|
||||
fs.mkdirSync(path.dirname(format.svg), { recursive: true });
|
||||
fs.writeFileSync(format.svg, svg);
|
||||
core.setOutput("svg_out_path", format.svg);
|
||||
}
|
||||
if (gif) {
|
||||
fs.mkdirSync(path.dirname(format.gif), { recursive: true });
|
||||
fs.writeFileSync(format.gif, gif);
|
||||
core.setOutput("gif_out_path", format.gif);
|
||||
}
|
||||
const results = await generateContributionSnake(userName, outputs);
|
||||
|
||||
outputs.forEach((out, i) => {
|
||||
const result = results[i];
|
||||
if (out?.filename && result) {
|
||||
console.log(`💾 writing to ${out?.filename}`);
|
||||
fs.mkdirSync(path.dirname(out?.filename), { recursive: true });
|
||||
fs.writeFileSync(out?.filename, result);
|
||||
}
|
||||
});
|
||||
} catch (e: any) {
|
||||
core.setFailed(`Action failed with "${e.message}"`);
|
||||
}
|
||||
|
||||
72
packages/action/outputsOptions.ts
Normal file
72
packages/action/outputsOptions.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
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))(\?(.*))?$/);
|
||||
if (!m) return null;
|
||||
|
||||
const [, filename, format, , query] = m;
|
||||
|
||||
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,
|
||||
sizeCell: 16,
|
||||
sizeDot: 12,
|
||||
...palettes["default"],
|
||||
};
|
||||
const animationOptions: AnimationOptions = { step: 1, frameDuration: 100 };
|
||||
|
||||
{
|
||||
const palette = palettes[sp.get("palette")!];
|
||||
if (palette) {
|
||||
Object.assign(drawOptions, palette);
|
||||
drawOptions.dark = palette.dark && { ...palette.dark };
|
||||
}
|
||||
}
|
||||
|
||||
if (sp.has("color_snake")) drawOptions.colorSnake = sp.get("color_snake")!;
|
||||
if (sp.has("color_dots")) {
|
||||
const colors = sp.get("color_dots")!.split(/[,;]/);
|
||||
drawOptions.colorDots = colors;
|
||||
drawOptions.colorEmpty = colors[0];
|
||||
drawOptions.dark = undefined;
|
||||
}
|
||||
if (sp.has("color_dot_border"))
|
||||
drawOptions.colorDotBorder = sp.get("color_dot_border")!;
|
||||
|
||||
if (sp.has("dark_color_dots")) {
|
||||
const colors = sp.get("dark_color_dots")!.split(/[,;]/);
|
||||
drawOptions.dark = {
|
||||
...drawOptions.dark,
|
||||
colorDots: colors,
|
||||
colorEmpty: colors[0],
|
||||
};
|
||||
}
|
||||
if (sp.has("dark_color_dot_border") && drawOptions.dark)
|
||||
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,
|
||||
format: format as "svg" | "gif",
|
||||
drawOptions,
|
||||
animationOptions,
|
||||
};
|
||||
};
|
||||
@@ -4,14 +4,15 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "1.6.0",
|
||||
"@snk/gif-creator": "1.0.0",
|
||||
"@snk/github-user-contribution": "1.0.0"
|
||||
"@snk/github-user-contribution": "1.0.0",
|
||||
"@snk/solver": "1.0.0",
|
||||
"@snk/svg-creator": "1.0.0",
|
||||
"@snk/types": "1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@zeit/ncc": "0.22.3",
|
||||
"ts-node": "10.7.0"
|
||||
"@vercel/ncc": "0.24.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "ncc build --external canvas --external gifsicle --out dist ./index.ts",
|
||||
"dev": "ts-node __tests__/dev.ts"
|
||||
"build": "ncc build --external canvas --external gifsicle --out dist ./index.ts"
|
||||
}
|
||||
}
|
||||
|
||||
29
packages/action/palettes.ts
Normal file
29
packages/action/palettes.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { DrawOptions as DrawOptions } from "@snk/svg-creator";
|
||||
|
||||
export const palettes: Record<
|
||||
string,
|
||||
Pick<
|
||||
DrawOptions,
|
||||
"colorDotBorder" | "colorEmpty" | "colorSnake" | "colorDots" | "dark"
|
||||
>
|
||||
> = {
|
||||
"github-light": {
|
||||
colorDotBorder: "#1b1f230a",
|
||||
colorDots: ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"],
|
||||
colorEmpty: "#ebedf0",
|
||||
colorSnake: "purple",
|
||||
},
|
||||
"github-dark": {
|
||||
colorDotBorder: "#1b1f230a",
|
||||
colorEmpty: "#161b22",
|
||||
colorDots: ["#161b22", "#01311f", "#034525", "#0f6d31", "#00c647"],
|
||||
colorSnake: "purple",
|
||||
},
|
||||
};
|
||||
|
||||
// aliases
|
||||
palettes["github"] = {
|
||||
...palettes["github-light"],
|
||||
dark: { ...palettes["github-dark"] },
|
||||
};
|
||||
palettes["default"] = palettes["github"];
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Color, Grid } from "@snk/types/grid";
|
||||
import { drawLerpWorld, drawWorld } from "@snk/draw/drawWorld";
|
||||
import { Snake } from "@snk/types/snake";
|
||||
import type { DrawOptions as DrawOptions } from "@snk/svg-creator";
|
||||
|
||||
export const drawOptions = {
|
||||
sizeBorderRadius: 2,
|
||||
export const drawOptions: DrawOptions = {
|
||||
sizeDotBorderRadius: 2,
|
||||
sizeCell: 16,
|
||||
sizeDot: 12,
|
||||
colorBorder: "#1b1f230a",
|
||||
colorDotBorder: "#1b1f230a",
|
||||
colorDots: {
|
||||
1: "#9be9a8",
|
||||
2: "#40c463",
|
||||
@@ -67,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 = (
|
||||
@@ -78,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") => {
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { getBestRoute } from "@snk/solver/getBestRoute";
|
||||
import { Color, copyGrid, Grid } from "@snk/types/grid";
|
||||
import { step } from "@snk/solver/step";
|
||||
import { isStableAndBound, stepSpring } from "./springUtils";
|
||||
import { Res } from "@snk/github-user-contribution";
|
||||
import { Snake } from "@snk/types/snake";
|
||||
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,
|
||||
Options,
|
||||
Options as DrawOptions,
|
||||
} from "@snk/draw/drawWorld";
|
||||
import { userContributionToGrid } from "../action/userContributionToGrid";
|
||||
import { snake4 as snake } from "@snk/types/__fixtures__/snake";
|
||||
import { getPathToPose } from "@snk/solver/getPathToPose";
|
||||
import { createSvg } from "../svg-creator";
|
||||
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,
|
||||
@@ -47,15 +48,24 @@ const createForm = ({
|
||||
|
||||
form.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
onSubmit(input.value).catch((err) => {
|
||||
label.innerText = "error :(";
|
||||
throw err;
|
||||
});
|
||||
|
||||
onSubmit(input.value)
|
||||
.finally(() => {
|
||||
clearTimeout(timeout);
|
||||
})
|
||||
.catch((err) => {
|
||||
label.innerText = "error :(";
|
||||
throw err;
|
||||
});
|
||||
|
||||
input.disabled = true;
|
||||
submit.disabled = true;
|
||||
form.appendChild(label);
|
||||
label.innerText = "loading ...";
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
label.innerText = "loading ( it might take a while ) ... ";
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
//
|
||||
@@ -75,6 +85,7 @@ const createGithubProfile = () => {
|
||||
container.style.opacity = "0";
|
||||
container.style.display = "flex";
|
||||
container.style.flexDirection = "column";
|
||||
container.style.height = "120px";
|
||||
container.style.alignItems = "flex-start";
|
||||
const image = document.createElement("img");
|
||||
image.style.width = "100px";
|
||||
@@ -107,11 +118,13 @@ const createGithubProfile = () => {
|
||||
const createViewer = ({
|
||||
grid0,
|
||||
chain,
|
||||
cells,
|
||||
drawOptions,
|
||||
}: {
|
||||
grid0: Grid;
|
||||
chain: Snake[];
|
||||
drawOptions: Options;
|
||||
cells: Point[];
|
||||
drawOptions: DrawOptions;
|
||||
}) => {
|
||||
//
|
||||
// canvas
|
||||
@@ -150,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);
|
||||
};
|
||||
@@ -180,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";
|
||||
@@ -220,25 +233,35 @@ const onSubmit = async (userName: string) => {
|
||||
);
|
||||
const { cells, colorScheme } = (await res.json()) as Res;
|
||||
|
||||
const drawOptions = {
|
||||
sizeBorderRadius: 2,
|
||||
const drawOptions: DrawOptions = {
|
||||
sizeDotBorderRadius: 2,
|
||||
sizeCell: 16,
|
||||
sizeDot: 12,
|
||||
colorBorder: "#1b1f230a",
|
||||
colorDotBorder: "#1b1f230a",
|
||||
colorDots: colorScheme as any,
|
||||
colorEmpty: colorScheme[0],
|
||||
colorSnake: "purple",
|
||||
cells,
|
||||
};
|
||||
|
||||
const grid = userContributionToGrid(cells, colorScheme);
|
||||
const chain = getBestRoute(grid, snake)!;
|
||||
chain.push(...getPathToPose(chain.slice(-1)[0], snake)!);
|
||||
|
||||
const chain = await getChain(grid);
|
||||
|
||||
dispose();
|
||||
|
||||
createViewer({ grid0: grid, chain, drawOptions });
|
||||
createViewer({ grid0: grid, chain, cells, drawOptions });
|
||||
};
|
||||
|
||||
const worker = new Worker(
|
||||
new URL(
|
||||
"./demo.interactive.worker.ts",
|
||||
// @ts-ignore
|
||||
import.meta.url
|
||||
)
|
||||
);
|
||||
|
||||
const { getChain } = createRpcClient<WorkerAPI>(worker);
|
||||
|
||||
const profile = createGithubProfile();
|
||||
const { dispose } = createForm({
|
||||
onSubmit,
|
||||
|
||||
17
packages/demo/demo.interactive.worker.ts
Normal file
17
packages/demo/demo.interactive.worker.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { getBestRoute } from "@snk/solver/getBestRoute";
|
||||
import { getPathToPose } from "@snk/solver/getPathToPose";
|
||||
import { snake4 as snake } from "@snk/types/__fixtures__/snake";
|
||||
import type { Grid } from "@snk/types/grid";
|
||||
import { createRpcServer } from "./worker-utils";
|
||||
|
||||
const getChain = (grid: Grid) => {
|
||||
const chain = getBestRoute(grid, snake)!;
|
||||
chain.push(...getPathToPose(chain.slice(-1)[0], snake)!);
|
||||
|
||||
return chain;
|
||||
};
|
||||
|
||||
const api = { getChain };
|
||||
export type API = typeof api;
|
||||
|
||||
createRpcServer(api);
|
||||
@@ -1,15 +1,18 @@
|
||||
import "./menu";
|
||||
import { getBestRoute } from "@snk/solver/getBestRoute";
|
||||
import { createSvg } from "../svg-creator";
|
||||
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;
|
||||
|
||||
@@ -2,20 +2,22 @@
|
||||
"name": "@snk/demo",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@snk/action": "1.0.0",
|
||||
"@snk/draw": "1.0.0",
|
||||
"@snk/github-user-contribution": "1.0.0",
|
||||
"@snk/solver": "1.0.0",
|
||||
"canvas": "2.9.1",
|
||||
"gifsicle": "5.3.0"
|
||||
"@snk/svg-creator": "1.0.0",
|
||||
"@snk/types": "1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dat.gui": "0.7.7",
|
||||
"dat.gui": "0.7.7",
|
||||
"dat.gui": "0.7.9",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"ts-loader": "9.2.6",
|
||||
"ts-loader": "9.2.8",
|
||||
"ts-node": "10.7.0",
|
||||
"webpack": "5.70.0",
|
||||
"webpack": "5.72.0",
|
||||
"webpack-cli": "4.9.2",
|
||||
"webpack-dev-server": "4.7.4"
|
||||
"webpack-dev-server": "4.8.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
|
||||
59
packages/demo/worker-utils.ts
Normal file
59
packages/demo/worker-utils.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
type API = Record<string, (...args: any[]) => any>;
|
||||
|
||||
const symbol = "worker-rpc__";
|
||||
|
||||
export const createRpcServer = (api: API) =>
|
||||
self.addEventListener("message", async (event) => {
|
||||
if (event.data?.symbol === symbol) {
|
||||
try {
|
||||
const res = await api[event.data.methodName](...event.data.args);
|
||||
self.postMessage({ symbol, key: event.data.key, res });
|
||||
} catch (error: any) {
|
||||
postMessage({ symbol, key: event.data.key, error: error.message });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const createRpcClient = <API_ extends API>(worker: Worker) => {
|
||||
const originalTerminate = worker.terminate;
|
||||
worker.terminate = () => {
|
||||
worker.dispatchEvent(new Event("terminate"));
|
||||
originalTerminate.call(worker);
|
||||
};
|
||||
|
||||
return new Proxy(
|
||||
{} as {
|
||||
[K in keyof API_]: (
|
||||
...args: Parameters<API_[K]>
|
||||
) => Promise<Awaited<ReturnType<API_[K]>>>;
|
||||
},
|
||||
{
|
||||
get:
|
||||
(_, methodName) =>
|
||||
(...args: any[]) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const key = Math.random().toString();
|
||||
|
||||
const onTerminate = () => {
|
||||
worker.removeEventListener("terminate", onTerminate);
|
||||
worker.removeEventListener("message", onMessageHandler);
|
||||
reject(new Error("worker terminated"));
|
||||
};
|
||||
|
||||
const onMessageHandler = (event: MessageEvent) => {
|
||||
if (event.data?.symbol === symbol && event.data.key === key) {
|
||||
if (event.data.error) reject(event.data.error);
|
||||
else if (event.data.res) resolve(event.data.res);
|
||||
|
||||
worker.removeEventListener("terminate", onTerminate);
|
||||
worker.removeEventListener("message", onMessageHandler);
|
||||
}
|
||||
};
|
||||
|
||||
worker.addEventListener("message", onMessageHandler);
|
||||
worker.addEventListener("terminate", onTerminate);
|
||||
worker.postMessage({ symbol, key, methodName, args });
|
||||
}),
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -6,21 +6,21 @@ import type { Point } from "@snk/types/point";
|
||||
type Options = {
|
||||
colorDots: Record<Color, string>;
|
||||
colorEmpty: string;
|
||||
colorBorder: string;
|
||||
colorDotBorder: string;
|
||||
sizeCell: number;
|
||||
sizeDot: number;
|
||||
sizeBorderRadius: number;
|
||||
cells?: Point[];
|
||||
sizeDotBorderRadius: number;
|
||||
};
|
||||
|
||||
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];
|
||||
@@ -31,11 +31,11 @@ export const drawGrid = (
|
||||
);
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.strokeStyle = o.colorBorder;
|
||||
ctx.strokeStyle = o.colorDotBorder;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
|
||||
pathRoundedRect(ctx, o.sizeDot, o.sizeDot, o.sizeBorderRadius);
|
||||
pathRoundedRect(ctx, o.sizeDot, o.sizeDot, o.sizeDotBorderRadius);
|
||||
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
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>;
|
||||
colorEmpty: string;
|
||||
colorBorder: string;
|
||||
colorDotBorder: string;
|
||||
colorSnake: string;
|
||||
sizeCell: number;
|
||||
sizeDot: number;
|
||||
sizeBorderRadius: number;
|
||||
cells?: Point[];
|
||||
sizeDotBorderRadius: number;
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
@@ -2,9 +2,10 @@ 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";
|
||||
|
||||
let snake = createSnakeFromCells(
|
||||
Array.from({ length: 4 }, (_, i) => ({ x: i, y: -1 }))
|
||||
@@ -24,17 +25,17 @@ let snake = createSnakeFromCells(
|
||||
const chain = getBestRoute(grid, snake)!;
|
||||
chain.push(...getPathToPose(chain.slice(-1)[0], snake)!);
|
||||
|
||||
const drawOptions = {
|
||||
sizeBorderRadius: 2,
|
||||
const drawOptions: DrawOptions = {
|
||||
sizeDotBorderRadius: 2,
|
||||
sizeCell: 16,
|
||||
sizeDot: 12,
|
||||
colorBorder: "#1b1f230a",
|
||||
colorDotBorder: "#1b1f230a",
|
||||
colorDots: { 1: "#9be9a8", 2: "#40c463", 3: "#30a14e", 4: "#216e39" },
|
||||
colorEmpty: "#ebedf0",
|
||||
colorSnake: "purple",
|
||||
};
|
||||
|
||||
const gifOptions = { frameDuration: 100, step: 1 };
|
||||
const animationOptions: AnimationOptions = { frameDuration: 100, step: 1 };
|
||||
|
||||
(async () => {
|
||||
for (
|
||||
@@ -49,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
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";
|
||||
import { getBestRoute } from "@snk/solver/getBestRoute";
|
||||
import type { Options as DrawOptions } from "@snk/draw/drawWorld";
|
||||
|
||||
jest.setTimeout(20 * 1000);
|
||||
|
||||
const upscale = 1;
|
||||
const drawOptions = {
|
||||
sizeBorderRadius: 2 * upscale,
|
||||
const drawOptions: DrawOptions = {
|
||||
sizeDotBorderRadius: 2 * upscale,
|
||||
sizeCell: 16 * upscale,
|
||||
sizeDot: 12 * upscale,
|
||||
colorBorder: "#1b1f230a",
|
||||
colorDotBorder: "#1b1f230a",
|
||||
colorDots: { 1: "#9be9a8", 2: "#40c463", 3: "#30a14e", 4: "#216e39" },
|
||||
colorEmpty: "#ebedf0",
|
||||
colorSnake: "purple",
|
||||
};
|
||||
|
||||
const gifOptions = { frameDuration: 200, step: 1 };
|
||||
const animationOptions: AnimationOptions = { frameDuration: 200, step: 1 };
|
||||
|
||||
const dir = path.resolve(__dirname, "__snapshots__");
|
||||
|
||||
@@ -39,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();
|
||||
|
||||
@@ -63,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();
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"devDependencies": {
|
||||
"@types/gifsicle": "5.2.0",
|
||||
"@types/tmp": "0.2.3",
|
||||
"@zeit/ncc": "0.22.3"
|
||||
"@vercel/ncc": "0.24.1"
|
||||
},
|
||||
"scripts": {
|
||||
"benchmark": "ncc run __tests__/benchmark.ts --quiet"
|
||||
|
||||
@@ -124,8 +124,6 @@ const getSvgPosition = (
|
||||
return p;
|
||||
};
|
||||
|
||||
type ThenArg<T> = T extends PromiseLike<infer U> ? U : T;
|
||||
|
||||
export type Res = ThenArg<ReturnType<typeof getGithubUserContribution>>;
|
||||
export type Res = Awaited<ReturnType<typeof getGithubUserContribution>>;
|
||||
|
||||
export type Cell = Res["cells"][number];
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"cheerio": "1.0.0-rc.10",
|
||||
"node-fetch": "2.6.1"
|
||||
"node-fetch": "2.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node-fetch": "2.6.1"
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { createSvg } 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 = {
|
||||
sizeBorderRadius: 2,
|
||||
const drawOptions: DrawOptions = {
|
||||
sizeDotBorderRadius: 2,
|
||||
sizeCell: 16,
|
||||
sizeDot: 12,
|
||||
colorBorder: "#1b1f230a",
|
||||
colorDotBorder: "#1b1f230a",
|
||||
colorDots: { 1: "#9be9a8", 2: "#40c463", 3: "#30a14e", 4: "#216e39" },
|
||||
colorEmpty: "#ebedf0",
|
||||
colorSnake: "purple",
|
||||
@@ -19,7 +20,7 @@ const drawOptions = {
|
||||
},
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
|
||||
@@ -5,25 +5,23 @@ import { h } from "./utils";
|
||||
export type Options = {
|
||||
colorDots: Record<Color, string>;
|
||||
colorEmpty: string;
|
||||
colorBorder: string;
|
||||
colorDotBorder: string;
|
||||
sizeCell: number;
|
||||
sizeDot: number;
|
||||
sizeBorderRadius: number;
|
||||
sizeDotBorderRadius: number;
|
||||
};
|
||||
|
||||
const percent = (x: number) => (x * 100).toFixed(2);
|
||||
|
||||
export const createGrid = (
|
||||
cells: (Point & { t: number | null; color: Color | Empty })[],
|
||||
{ sizeBorderRadius, sizeDot, sizeCell }: Options,
|
||||
{ sizeDotBorderRadius, sizeDot, sizeCell }: Options,
|
||||
duration: number
|
||||
) => {
|
||||
const svgElements: string[] = [];
|
||||
const styles = [
|
||||
`.c{
|
||||
shape-rendering: geometricPrecision;
|
||||
rx: ${sizeBorderRadius};
|
||||
ry: ${sizeBorderRadius};
|
||||
fill: var(--ce);
|
||||
stroke-width: 1px;
|
||||
stroke: var(--cb);
|
||||
@@ -56,6 +54,8 @@ export const createGrid = (
|
||||
class: ["c", id].filter(Boolean).join(" "),
|
||||
x: x * s + m,
|
||||
y: y * s + m,
|
||||
rx: sizeDotBorderRadius,
|
||||
ry: sizeDotBorderRadius,
|
||||
width: d,
|
||||
height: d,
|
||||
})
|
||||
|
||||
@@ -14,20 +14,20 @@ 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;
|
||||
colorBorder: string;
|
||||
colorDotBorder: string;
|
||||
colorSnake: string;
|
||||
sizeCell: number;
|
||||
sizeDot: number;
|
||||
sizeBorderRadius: number;
|
||||
cells?: Point[];
|
||||
sizeDotBorderRadius: number;
|
||||
dark?: {
|
||||
colorDots: Record<Color, string>;
|
||||
colorEmpty: string;
|
||||
colorBorder?: string;
|
||||
colorDotBorder?: string;
|
||||
colorSnake?: 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,
|
||||
@@ -115,6 +116,10 @@ export const createSvg = (
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
}).replace("/>", ">"),
|
||||
|
||||
"<desc>",
|
||||
"Generated with https://github.com/Platane/snk",
|
||||
"</desc>",
|
||||
|
||||
"<style>",
|
||||
optimizeCss(style),
|
||||
"</style>",
|
||||
@@ -130,10 +135,10 @@ 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.colorBorder};
|
||||
--cb: ${drawOptions.colorDotBorder};
|
||||
--cs: ${drawOptions.colorSnake};
|
||||
--ce: ${drawOptions.colorEmpty};
|
||||
${Object.entries(drawOptions.colorDots)
|
||||
@@ -145,7 +150,7 @@ const generateColorVar = (drawOptions: Options) =>
|
||||
? `
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--cb: ${drawOptions.dark.colorBorder || drawOptions.colorBorder};
|
||||
--cb: ${drawOptions.dark.colorDotBorder || drawOptions.colorDotBorder};
|
||||
--cs: ${drawOptions.dark.colorSnake || drawOptions.colorSnake};
|
||||
--ce: ${drawOptions.dark.colorEmpty};
|
||||
${Object.entries(drawOptions.dark.colorDots)
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { getSnakeLength, snakeToCells } from "@snk/types/snake";
|
||||
import type { Snake } from "@snk/types/snake";
|
||||
import type { Color } from "@snk/types/grid";
|
||||
import type { Point } from "@snk/types/point";
|
||||
import { h } from "./utils";
|
||||
|
||||
export type Options = {
|
||||
colorDots: Record<Color, string>;
|
||||
colorEmpty: string;
|
||||
colorBorder: string;
|
||||
colorSnake: string;
|
||||
sizeCell: number;
|
||||
sizeDot: number;
|
||||
sizeBorderRadius: number;
|
||||
};
|
||||
|
||||
const percent = (x: number) => (x * 100).toFixed(2);
|
||||
|
||||
9
svg-only/README.md
Normal file
9
svg-only/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# svg-only
|
||||
|
||||
Another action running purely on js (without Docker).
|
||||
|
||||
As a drawback, it can not generate gif image.
|
||||
|
||||
## Build process
|
||||
|
||||
dist file are built and push on release, by the release action.
|
||||
31
svg-only/action.yml
Normal file
31
svg-only/action.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
name: "generate-snake-game-from-github-contribution-grid"
|
||||
description: "Generates a snake game from a github user contributions grid. Output the animation as svg"
|
||||
author: "platane"
|
||||
|
||||
runs:
|
||||
using: node16
|
||||
main: dist/index.js
|
||||
|
||||
inputs:
|
||||
github_user_name:
|
||||
description: "github user name"
|
||||
required: true
|
||||
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>
|
||||
|
||||
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
|
||||
50757
svg-only/dist/index.js
vendored
Normal file
50757
svg-only/dist/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -6,7 +6,8 @@
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"esModuleInterop": true
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user