Compare commits
57 Commits
v2.0.0-rc.
...
v3.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b7b3e6ace | ||
|
|
92f4de3970 | ||
|
|
c9644d3dfa | ||
|
|
01fa6d7aac | ||
|
|
b58af55b7d | ||
|
|
4e5805f8af | ||
|
|
743771147d | ||
|
|
8eddcbdbea | ||
|
|
6f0ace6560 | ||
|
|
835fdd6b84 | ||
|
|
e6034f3972 | ||
|
|
aebc3a9285 | ||
|
|
1574f65738 | ||
|
|
ebeb59fced | ||
|
|
4489504b7a | ||
|
|
027f89563f | ||
|
|
7233ec9e15 | ||
|
|
54dbbbf73d | ||
|
|
3eed9ce6d6 | ||
|
|
3acebc09eb | ||
|
|
82417bf9f5 | ||
|
|
7b6d52d221 | ||
|
|
fd133c88c7 | ||
|
|
229c9a9cd6 | ||
|
|
3803e1ccfa | ||
|
|
8ca289e908 | ||
|
|
fd7cc1f05a | ||
|
|
632fcf6cb7 | ||
|
|
e2eb91cf8f | ||
|
|
38e2ed4f23 | ||
|
|
b7a9c1e353 | ||
|
|
a0e08722d9 | ||
|
|
29c7ee48ec | ||
|
|
21655d1bda | ||
|
|
b895ed2e0f | ||
|
|
96773d2b2e | ||
|
|
79ae29668c | ||
|
|
62f6ff3091 | ||
|
|
4a03759871 | ||
|
|
463b90d43c | ||
|
|
b40f17a02e | ||
|
|
f83b9ab0c3 | ||
|
|
fb80d60b23 | ||
|
|
d078b2d231 | ||
|
|
a81c1bcc97 | ||
|
|
40b26d0110 | ||
|
|
d6e930af5b | ||
|
|
98feaa6035 | ||
|
|
8f1481341a | ||
|
|
2e275adbb6 | ||
|
|
66fef03781 | ||
|
|
5841a21a09 | ||
|
|
cce5c4514d | ||
|
|
fb82d42d53 | ||
|
|
e3ad8b2caf | ||
|
|
c21e390ca9 | ||
|
|
7077112ba4 |
81
.github/workflows/main.yml
vendored
81
.github/workflows/main.yml
vendored
@@ -7,34 +7,23 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: yarn
|
||||
node-version: 16
|
||||
node-version: 20
|
||||
- run: yarn install --frozen-lockfile
|
||||
|
||||
- run: yarn type
|
||||
- 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 )
|
||||
- run: npm run type
|
||||
- run: npm run lint
|
||||
- run: npm run test --ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
test-action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: update action.yml to use image from local Dockerfile
|
||||
run: |
|
||||
@@ -57,29 +46,67 @@ jobs:
|
||||
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
|
||||
- uses: crazy-max/ghaction-github-pages@v3.1.0
|
||||
with:
|
||||
target_branch: output
|
||||
build_dir: dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
test-action-svg-only:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: yarn
|
||||
node-version: 20
|
||||
- run: yarn install --frozen-lockfile
|
||||
|
||||
- name: build svg-only action
|
||||
run: |
|
||||
npm run build:action
|
||||
rm -r svg-only/dist
|
||||
mv packages/action/dist svg-only/dist
|
||||
|
||||
- name: generate-snake-game-from-github-contribution-grid
|
||||
id: generate-snake
|
||||
uses: ./svg-only
|
||||
with:
|
||||
github_user_name: platane
|
||||
outputs: |
|
||||
dist/github-contribution-grid-snake.svg
|
||||
dist/github-contribution-grid-snake-dark.svg?palette=github-dark
|
||||
|
||||
- name: ensure the generated file exists
|
||||
run: |
|
||||
ls dist
|
||||
test -f dist/github-contribution-grid-snake.svg
|
||||
test -f dist/github-contribution-grid-snake-dark.svg
|
||||
|
||||
- uses: crazy-max/ghaction-github-pages@v3.1.0
|
||||
with:
|
||||
target_branch: output-svg-only
|
||||
build_dir: dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
deploy-ghpages:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: yarn
|
||||
node-version: 16
|
||||
node-version: 20
|
||||
- run: yarn install --frozen-lockfile
|
||||
|
||||
- run: yarn build:demo
|
||||
- run: npm run build:demo
|
||||
env:
|
||||
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'
|
||||
- uses: crazy-max/ghaction-github-pages@v3.1.0
|
||||
if: success() && github.ref == 'refs/heads/main'
|
||||
with:
|
||||
target_branch: gh-pages
|
||||
build_dir: packages/demo/dist
|
||||
|
||||
31
.github/workflows/release.yml
vendored
31
.github/workflows/release.yml
vendored
@@ -4,7 +4,10 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version"
|
||||
description: |
|
||||
New version for the release
|
||||
If the version is in format <major>.<minor>.<patch> a new release is emitted.
|
||||
Otherwise for other format ( for example <major>.<minor>.<patch>-beta.1 ), a prerelease is emitted.
|
||||
default: "0.0.1"
|
||||
required: true
|
||||
type: string
|
||||
@@ -15,20 +18,22 @@ on:
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: docker/setup-qemu-action@v1
|
||||
- uses: docker/setup-qemu-action@v2
|
||||
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
|
||||
- uses: docker/login-action@v1
|
||||
- uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: build and publish the docker image
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v4
|
||||
id: docker-build
|
||||
with:
|
||||
push: true
|
||||
@@ -40,15 +45,15 @@ jobs:
|
||||
run: |
|
||||
sed -i "s/image: .*/image: docker:\/\/platane\/snk@${{ steps.docker-build.outputs.digest }}/" action.yml
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: yarn
|
||||
node-version: 16
|
||||
node-version: 20
|
||||
|
||||
- name: build svg-only action
|
||||
run: |
|
||||
yarn install --frozen-lockfile
|
||||
yarn build:action
|
||||
npm run build:action
|
||||
rm -r svg-only/dist
|
||||
mv packages/action/dist svg-only/dist
|
||||
|
||||
@@ -71,15 +76,15 @@ jobs:
|
||||
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
|
||||
echo "prerelease=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo ::set-output name=prerelease::true
|
||||
echo "prerelease=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- uses: actions/create-release@v1
|
||||
- uses: ncipollo/release-action@v1.12.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: v${{ github.event.inputs.version }}
|
||||
tag: v${{ github.event.inputs.version }}
|
||||
body: ${{ github.event.inputs.description }}
|
||||
prerelease: ${{ steps.push-tags.outputs.prerelease }}
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,4 +3,5 @@ npm-debug.log*
|
||||
yarn-error.log*
|
||||
dist
|
||||
!svg-only/dist
|
||||
build
|
||||
build
|
||||
.env
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:16-slim as builder
|
||||
FROM node:20-slim as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -18,12 +18,12 @@ RUN yarn build:action
|
||||
|
||||
|
||||
|
||||
FROM node:16-slim
|
||||
FROM node:20-slim
|
||||
|
||||
WORKDIR /action-release
|
||||
|
||||
RUN export YARN_CACHE_FOLDER="$(mktemp -d)" \
|
||||
&& yarn add canvas@2.9.1 gifsicle@5.3.0 --no-lockfile \
|
||||
&& yarn add canvas@2.11.2 gifsicle@5.3.0 --no-lockfile \
|
||||
&& rm -r "$YARN_CACHE_FOLDER"
|
||||
|
||||
COPY --from=builder /app/packages/action/dist/ /action-release/
|
||||
|
||||
49
README.md
49
README.md
@@ -1,5 +1,6 @@
|
||||
# snk
|
||||
|
||||
[](https://github.com/Platane/Platane/actions/workflows/main.yml)
|
||||
[](https://github.com/platane/snk/releases/latest)
|
||||
[](https://github.com/marketplace/actions/generate-snake-game-from-github-contribution-grid)
|
||||

|
||||
@@ -7,21 +8,34 @@
|
||||
|
||||
Generates a snake game from a github user contributions graph
|
||||
|
||||

|
||||
<picture>
|
||||
<source
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="https://raw.githubusercontent.com/platane/snk/output/github-contribution-grid-snake-dark.svg"
|
||||
/>
|
||||
<source
|
||||
media="(prefers-color-scheme: light)"
|
||||
srcset="https://raw.githubusercontent.com/platane/snk/output/github-contribution-grid-snake.svg"
|
||||
/>
|
||||
<img
|
||||
alt="github contribution grid snake animation"
|
||||
src="https://raw.githubusercontent.com/platane/snk/output/github-contribution-grid-snake.svg"
|
||||
/>
|
||||
</picture>
|
||||
|
||||
Pull a github user's contribution graph.
|
||||
Make it a snake Game, generate a snake path where the cells get eaten in an orderly fashion.
|
||||
|
||||
Generate a [gif](https://github.com/Platane/snk/raw/output/github-contribution-grid-snake.gif) or [svg](https://github.com/Platane/snk/raw/output/github-contribution-grid-snake.svg) image.
|
||||
|
||||
Available as github action. Automatically generate a new image at the end of the day. Which makes for great [github profile readme](https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-your-github-profile/managing-your-profile-readme)
|
||||
Available as github action. It can automatically generate a new image each day. Which makes for great [github profile readme](https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-your-github-profile/managing-your-profile-readme)
|
||||
|
||||
## Usage
|
||||
|
||||
**github action**
|
||||
|
||||
```yaml
|
||||
- uses: Platane/snk@v2
|
||||
- uses: Platane/snk@v3
|
||||
with:
|
||||
# github user name to read the contribution graph from (**required**)
|
||||
# using action context var `github.repository_owner` or specified user
|
||||
@@ -29,15 +43,38 @@ 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
|
||||
dist/github-snake-dark.svg?palette=github-dark
|
||||
dist/ocean.gif?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9
|
||||
|
||||
env:
|
||||
# a github token is required to fetch the contribution calendar from github API
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
[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#L26-L35)
|
||||
|
||||
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, consider using this faster action: `uses: Platane/snk/svg-only@v3`
|
||||
|
||||
**dark mode**
|
||||
|
||||
For **dark mode** support on github, use this [special syntax](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#specifying-the-theme-an-image-is-shown-to) in your readme.
|
||||
|
||||
```html
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="github-snake-dark.svg" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="github-snake.svg" />
|
||||
<img alt="github-snake" src="github-snake.svg" />
|
||||
</picture>
|
||||
```
|
||||
|
||||
**interactive demo**
|
||||
|
||||
|
||||
20
action.yml
20
action.yml
@@ -4,26 +4,30 @@ author: "platane"
|
||||
|
||||
runs:
|
||||
using: docker
|
||||
image: docker://platane/snk@sha256:300fb94d3b1214e6c229990b458286a8f1c4c68a178b1b59b670c9fcac7c80d1
|
||||
image: docker://platane/snk@sha256:1c8a0b51a75ad8cf36b7defddd2187bdbb92bbbb5521a9e6cc5df795b00fc590
|
||||
|
||||
inputs:
|
||||
github_user_name:
|
||||
description: "github user name"
|
||||
required: true
|
||||
github_token:
|
||||
description: "github token used to fetch the contribution calendar. Default to the action token if empty."
|
||||
required: false
|
||||
default: ${{ github.token }}
|
||||
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>
|
||||
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,5 +0,0 @@
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
testMatch: ["**/__tests__/**/?(*.)+(spec|test).ts"],
|
||||
};
|
||||
34
package.json
34
package.json
@@ -1,26 +1,36 @@
|
||||
{
|
||||
"name": "snk",
|
||||
"description": "Generates a snake game from a github user contributions grid",
|
||||
"version": "2.0.0-rc.1",
|
||||
"version": "3.2.0",
|
||||
"private": true,
|
||||
"repository": "github:platane/snk",
|
||||
"devDependencies": {
|
||||
"@types/jest": "27.4.1",
|
||||
"@types/node": "16.11.7",
|
||||
"jest": "27.5.1",
|
||||
"prettier": "2.6.2",
|
||||
"ts-jest": "27.1.4",
|
||||
"typescript": "4.6.3"
|
||||
"@sucrase/jest-plugin": "3.0.0",
|
||||
"@types/jest": "29.5.5",
|
||||
"@types/node": "20.6.3",
|
||||
"jest": "29.7.0",
|
||||
"prettier": "2.8.8",
|
||||
"sucrase": "3.34.0",
|
||||
"typescript": "5.2.2"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/**"
|
||||
],
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
"testMatch": [
|
||||
"**/__tests__/**/?(*.)+(spec|test).ts"
|
||||
],
|
||||
"transform": {
|
||||
"\\.(ts|tsx)$": "@sucrase/jest-plugin"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"type": "tsc --noEmit",
|
||||
"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 )",
|
||||
"build:action": "( cd packages/action ; yarn build )"
|
||||
"lint": "prettier -c '**/*.{ts,js,json,md,yml,yaml}' '!packages/*/dist/**' '!svg-only/dist/**'",
|
||||
"test": "jest --verbose --no-cache",
|
||||
"dev:demo": "( cd packages/demo ; npm run dev )",
|
||||
"build:demo": "( cd packages/demo ; npm run build )",
|
||||
"build:action": "( cd packages/action ; npm run build )"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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`] = `
|
||||
Object {
|
||||
"drawOptions": Object {
|
||||
exports[`should parse /out.svg?{"color_snake":"yellow","color_dots":["#000","#111","#222","#333","#444"]} 1`] = `
|
||||
{
|
||||
"animationOptions": {
|
||||
"frameDuration": 100,
|
||||
"step": 1,
|
||||
},
|
||||
"drawOptions": {
|
||||
"colorDotBorder": "#1b1f230a",
|
||||
"colorDots": Array [
|
||||
"#bfd6f6",
|
||||
"#8dbdff",
|
||||
"#64a1f4",
|
||||
"#4b91f1",
|
||||
"#3c7dd9",
|
||||
"colorDots": [
|
||||
"#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`] = `
|
||||
{
|
||||
"animationOptions": {
|
||||
"frameDuration": 100,
|
||||
"step": 1,
|
||||
},
|
||||
"drawOptions": {
|
||||
"colorDotBorder": "#1b1f230a",
|
||||
"colorDots": [
|
||||
"#000",
|
||||
"#111",
|
||||
"#222",
|
||||
"#333",
|
||||
"#444",
|
||||
],
|
||||
"colorEmpty": "#000",
|
||||
"colorSnake": "orange",
|
||||
"dark": undefined,
|
||||
"sizeCell": 16,
|
||||
@@ -20,18 +51,56 @@ 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`] = `
|
||||
{
|
||||
"animationOptions": {
|
||||
"frameDuration": 100,
|
||||
"step": 1,
|
||||
},
|
||||
"drawOptions": {
|
||||
"colorDotBorder": "#1b1f230a",
|
||||
"colorDots": [
|
||||
"#000",
|
||||
"#111",
|
||||
"#222",
|
||||
"#333",
|
||||
"#444",
|
||||
],
|
||||
"colorEmpty": "#000",
|
||||
"colorSnake": "orange",
|
||||
"dark": {
|
||||
"colorDotBorder": "#1b1f230a",
|
||||
"colorDots": [
|
||||
"#a00",
|
||||
"#a11",
|
||||
"#a22",
|
||||
"#a33",
|
||||
"#a44",
|
||||
],
|
||||
"colorEmpty": "#a00",
|
||||
"colorSnake": "orange",
|
||||
},
|
||||
"sizeCell": 16,
|
||||
"sizeDot": 12,
|
||||
"sizeDotBorderRadius": 2,
|
||||
},
|
||||
"filename": "/out.svg",
|
||||
"format": "svg",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`should parse path/to/out.gif 1`] = `
|
||||
Object {
|
||||
"drawOptions": Object {
|
||||
{
|
||||
"animationOptions": {
|
||||
"frameDuration": 100,
|
||||
"step": 1,
|
||||
},
|
||||
"drawOptions": {
|
||||
"colorDotBorder": "#1b1f230a",
|
||||
"colorDots": Array [
|
||||
"colorDots": [
|
||||
"#ebedf0",
|
||||
"#9be9a8",
|
||||
"#40c463",
|
||||
@@ -40,27 +109,12 @@ Object {
|
||||
],
|
||||
"colorEmpty": "#ebedf0",
|
||||
"colorSnake": "purple",
|
||||
"dark": Object {
|
||||
"colorDotBorder": "#1b1f230a",
|
||||
"colorDots": Array [
|
||||
"#161b22",
|
||||
"#01311f",
|
||||
"#034525",
|
||||
"#0f6d31",
|
||||
"#00c647",
|
||||
],
|
||||
"colorEmpty": "#161b22",
|
||||
"colorSnake": "purple",
|
||||
},
|
||||
"dark": undefined,
|
||||
"sizeCell": 16,
|
||||
"sizeDot": 12,
|
||||
"sizeDotBorderRadius": 2,
|
||||
},
|
||||
"filename": "path/to/out.gif",
|
||||
"format": "gif",
|
||||
"gifOptions": Object {
|
||||
"frameDuration": 100,
|
||||
"step": 1,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -2,6 +2,8 @@ import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { generateContributionSnake } from "../generateContributionSnake";
|
||||
import { parseOutputsOption } from "../outputsOptions";
|
||||
import { config } from "dotenv";
|
||||
config({ path: __dirname + "/../../../.env" });
|
||||
|
||||
jest.setTimeout(2 * 60 * 1000);
|
||||
|
||||
@@ -30,7 +32,9 @@ it(
|
||||
|
||||
const outputs = parseOutputsOption(entries);
|
||||
|
||||
const results = await generateContributionSnake("platane", outputs);
|
||||
const results = await generateContributionSnake("platane", outputs, {
|
||||
githubToken: process.env.GITHUB_TOKEN!,
|
||||
});
|
||||
|
||||
expect(results[0]).toBeDefined();
|
||||
expect(results[1]).toBeDefined();
|
||||
|
||||
@@ -1,9 +1,58 @@
|
||||
import { parseEntry } from "../outputsOptions";
|
||||
|
||||
it("should parse options as json", () => {
|
||||
expect(
|
||||
parseEntry(`/out.svg {"color_snake":"yellow"}`)?.drawOptions
|
||||
).toHaveProperty("colorSnake", "yellow");
|
||||
|
||||
expect(
|
||||
parseEntry(`/out.svg?{"color_snake":"yellow"}`)?.drawOptions
|
||||
).toHaveProperty("colorSnake", "yellow");
|
||||
|
||||
expect(
|
||||
parseEntry(`/out.svg?{"color_dots":["#000","#111","#222","#333","#444"]}`)
|
||||
?.drawOptions.colorDots
|
||||
).toEqual(["#000", "#111", "#222", "#333", "#444"]);
|
||||
});
|
||||
|
||||
it("should parse options as searchparams", () => {
|
||||
expect(parseEntry(`/out.svg?color_snake=yellow`)?.drawOptions).toHaveProperty(
|
||||
"colorSnake",
|
||||
"yellow"
|
||||
);
|
||||
|
||||
expect(
|
||||
parseEntry(`/out.svg?color_dots=#000,#111,#222,#333,#444`)?.drawOptions
|
||||
.colorDots
|
||||
).toEqual(["#000", "#111", "#222", "#333", "#444"]);
|
||||
});
|
||||
|
||||
it("should parse filename", () => {
|
||||
expect(parseEntry(`/a/b/c.svg?{"color_snake":"yellow"}`)).toHaveProperty(
|
||||
"filename",
|
||||
"/a/b/c.svg"
|
||||
);
|
||||
expect(
|
||||
parseEntry(`/a/b/out.svg?.gif.svg?{"color_snake":"yellow"}`)
|
||||
).toHaveProperty("filename", "/a/b/out.svg?.gif.svg");
|
||||
|
||||
expect(
|
||||
parseEntry(`/a/b/{[-1].svg?.gif.svg?{"color_snake":"yellow"}`)
|
||||
).toHaveProperty("filename", "/a/b/{[-1].svg?.gif.svg");
|
||||
});
|
||||
|
||||
[
|
||||
// default
|
||||
"path/to/out.gif",
|
||||
|
||||
"/out.svg?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9",
|
||||
// overwrite colors (search params)
|
||||
"/out.svg?color_snake=orange&color_dots=#000,#111,#222,#333,#444",
|
||||
|
||||
// overwrite colors (json)
|
||||
`/out.svg?{"color_snake":"yellow","color_dots":["#000","#111","#222","#333","#444"]}`,
|
||||
|
||||
// overwrite dark colors
|
||||
"/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();
|
||||
|
||||
@@ -3,23 +3,22 @@ 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;
|
||||
};
|
||||
} | null)[]
|
||||
animationOptions: AnimationOptions;
|
||||
} | null)[],
|
||||
options: { githubToken: string }
|
||||
) => {
|
||||
console.log("🎣 fetching github user contribution");
|
||||
const { cells, colorScheme } = await getGithubUserContribution(userName);
|
||||
const cells = await getGithubUserContribution(userName, options);
|
||||
|
||||
const grid = userContributionToGrid(cells, colorScheme);
|
||||
const grid = userContributionToGrid(cells);
|
||||
const snake = snake4;
|
||||
|
||||
console.log("📡 computing best route");
|
||||
@@ -29,17 +28,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
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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 () => {
|
||||
@@ -13,8 +12,15 @@ import { parseOutputsOption } from "./outputsOptions";
|
||||
core.getInput("svg_out_path"),
|
||||
]
|
||||
);
|
||||
const githubToken =
|
||||
process.env.GITHUB_TOKEN ?? core.getInput("github_token");
|
||||
|
||||
const results = await generateContributionSnake(userName, outputs);
|
||||
const { generateContributionSnake } = await import(
|
||||
"./generateContributionSnake"
|
||||
);
|
||||
const results = await generateContributionSnake(userName, outputs, {
|
||||
githubToken,
|
||||
});
|
||||
|
||||
outputs.forEach((out, i) => {
|
||||
const result = results[i];
|
||||
|
||||
@@ -1,23 +1,40 @@
|
||||
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))(\?(.*)|\s*({.*}))?$/);
|
||||
|
||||
if (!m) return null;
|
||||
|
||||
const [_, filename, format, query] = m;
|
||||
const [, filename, format, _, q1, q2] = m;
|
||||
|
||||
const sp = new URLSearchParams(query || "");
|
||||
const query = q1 ?? q2;
|
||||
|
||||
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"],
|
||||
dark: palettes["default"].dark && { ...palettes["default"].dark },
|
||||
};
|
||||
const gifOptions = { step: 1, frameDuration: 100 };
|
||||
const animationOptions: AnimationOptions = { step: 1, frameDuration: 100 };
|
||||
|
||||
{
|
||||
const palette = palettes[sp.get("palette")!];
|
||||
@@ -27,6 +44,14 @@ export const parseEntry = (entry: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const dark_palette = palettes[sp.get("dark_palette")!];
|
||||
if (dark_palette) {
|
||||
const clone = { ...dark_palette, dark: undefined };
|
||||
drawOptions.dark = clone;
|
||||
}
|
||||
}
|
||||
|
||||
if (sp.has("color_snake")) drawOptions.colorSnake = sp.get("color_snake")!;
|
||||
if (sp.has("color_dots")) {
|
||||
const colors = sp.get("color_dots")!.split(/[,;]/);
|
||||
@@ -40,6 +65,8 @@ export const parseEntry = (entry: string) => {
|
||||
if (sp.has("dark_color_dots")) {
|
||||
const colors = sp.get("dark_color_dots")!.split(/[,;]/);
|
||||
drawOptions.dark = {
|
||||
colorDotBorder: drawOptions.colorDotBorder,
|
||||
colorSnake: drawOptions.colorSnake,
|
||||
...drawOptions.dark,
|
||||
colorDots: colors,
|
||||
colorEmpty: colors[0],
|
||||
@@ -50,5 +77,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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@snk/action",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@actions/core": "1.6.0",
|
||||
"@actions/core": "1.10.1",
|
||||
"@snk/gif-creator": "1.0.0",
|
||||
"@snk/github-user-contribution": "1.0.0",
|
||||
"@snk/solver": "1.0.0",
|
||||
@@ -10,9 +10,11 @@
|
||||
"@snk/types": "1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vercel/ncc": "0.24.1"
|
||||
"@vercel/ncc": "0.38.0",
|
||||
"dotenv": "16.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "ncc build --external canvas --external gifsicle --out dist ./index.ts"
|
||||
"build": "ncc build --external canvas --external gifsicle --out dist ./index.ts",
|
||||
"run:build": "INPUT_GITHUB_USER_NAME=platane INPUT_OUTPUTS='dist/out.svg' node dist/index.js"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Options as DrawOptions } from "@snk/svg-creator";
|
||||
import { DrawOptions as DrawOptions } from "@snk/svg-creator";
|
||||
|
||||
export const palettes: Record<
|
||||
export const basePalettes: Record<
|
||||
string,
|
||||
Pick<
|
||||
DrawOptions,
|
||||
@@ -22,8 +22,6 @@ export const palettes: Record<
|
||||
};
|
||||
|
||||
// aliases
|
||||
palettes["github"] = {
|
||||
...palettes["github-light"],
|
||||
dark: { ...palettes["github-dark"] },
|
||||
};
|
||||
export const palettes = { ...basePalettes };
|
||||
palettes["github"] = palettes["github-light"];
|
||||
palettes["default"] = palettes["github"];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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") => {
|
||||
|
||||
@@ -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,8 @@ 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";
|
||||
import { basePalettes } from "@snk/action/palettes";
|
||||
|
||||
const createForm = ({
|
||||
onSubmit,
|
||||
@@ -116,12 +119,19 @@ const createGithubProfile = () => {
|
||||
const createViewer = ({
|
||||
grid0,
|
||||
chain,
|
||||
drawOptions,
|
||||
cells,
|
||||
}: {
|
||||
grid0: Grid;
|
||||
chain: Snake[];
|
||||
drawOptions: DrawOptions;
|
||||
cells: Point[];
|
||||
}) => {
|
||||
const drawOptions: DrawOptions = {
|
||||
sizeDotBorderRadius: 2,
|
||||
sizeCell: 16,
|
||||
sizeDot: 12,
|
||||
...basePalettes["github-light"],
|
||||
};
|
||||
|
||||
//
|
||||
// canvas
|
||||
const canvas = document.createElement("canvas");
|
||||
@@ -159,7 +169,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, cells, snake0, snake1, stack, k, drawOptions);
|
||||
|
||||
if (!stable) animationFrame = requestAnimationFrame(loop);
|
||||
};
|
||||
@@ -167,12 +177,12 @@ const createViewer = ({
|
||||
|
||||
//
|
||||
// controls
|
||||
const input = document.createElement("input") as any;
|
||||
const input = document.createElement("input");
|
||||
input.type = "range";
|
||||
input.value = 0;
|
||||
input.step = 1;
|
||||
input.min = 0;
|
||||
input.max = chain.length;
|
||||
input.value = "0";
|
||||
input.step = "1";
|
||||
input.min = "0";
|
||||
input.max = "" + chain.length;
|
||||
input.style.width = "calc( 100% - 20px )";
|
||||
input.addEventListener("input", () => {
|
||||
spring.target = +input.value;
|
||||
@@ -186,12 +196,51 @@ const createViewer = ({
|
||||
window.addEventListener("click", onClickBackground);
|
||||
document.body.append(input);
|
||||
|
||||
//
|
||||
const schemaSelect = document.createElement("select");
|
||||
schemaSelect.style.margin = "10px";
|
||||
schemaSelect.style.alignSelf = "flex-start";
|
||||
schemaSelect.value = "github-light";
|
||||
schemaSelect.addEventListener("change", () => {
|
||||
Object.assign(drawOptions, basePalettes[schemaSelect.value]);
|
||||
|
||||
svgString = createSvg(grid0, cells, chain, drawOptions, {
|
||||
frameDuration: 100,
|
||||
} as AnimationOptions);
|
||||
const svgImageUri = `data:image/*;charset=utf-8;base64,${btoa(svgString)}`;
|
||||
svgLink.href = svgImageUri;
|
||||
|
||||
if (schemaSelect.value.includes("dark"))
|
||||
document.body.parentElement?.classList.add("dark-mode");
|
||||
else document.body.parentElement?.classList.remove("dark-mode");
|
||||
|
||||
loop();
|
||||
});
|
||||
for (const name of Object.keys(basePalettes)) {
|
||||
const option = document.createElement("option");
|
||||
option.value = name;
|
||||
option.innerText = name;
|
||||
schemaSelect.appendChild(option);
|
||||
}
|
||||
document.body.append(schemaSelect);
|
||||
|
||||
//
|
||||
// dark mode
|
||||
const style = document.createElement("style");
|
||||
style.innerText = `
|
||||
html { transition:background-color 180ms }
|
||||
a { transition:color 180ms }
|
||||
html.dark-mode{ background-color:#0d1117 }
|
||||
html.dark-mode a{ color:rgb(201, 209, 217) }
|
||||
`;
|
||||
document.head.append(style);
|
||||
|
||||
//
|
||||
// svg
|
||||
const svgLink = document.createElement("a");
|
||||
const svgString = createSvg(grid0, chain, drawOptions, {
|
||||
let 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";
|
||||
@@ -199,7 +248,10 @@ const createViewer = ({
|
||||
svgLink.addEventListener("click", (e) => {
|
||||
const w = window.open("")!;
|
||||
w.document.write(
|
||||
`<a href="${svgImageUri}" download="github-user-contribution.svg">` +
|
||||
(document.body.parentElement?.classList.contains("dark-mode")
|
||||
? "<style>html{ background-color:#0d1117 }</style>"
|
||||
: "") +
|
||||
`<a href="${svgLink.href}" download="github-user-contribution.svg">` +
|
||||
svgString +
|
||||
"<a/>"
|
||||
);
|
||||
@@ -227,26 +279,15 @@ 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],
|
||||
colorSnake: "purple",
|
||||
cells,
|
||||
};
|
||||
|
||||
const grid = userContributionToGrid(cells, colorScheme);
|
||||
const grid = userContributionToGrid(cells);
|
||||
|
||||
const chain = await getChain(grid);
|
||||
|
||||
dispose();
|
||||
|
||||
createViewer({ grid0: grid, chain, drawOptions });
|
||||
createViewer({ grid0: grid, chain, cells });
|
||||
};
|
||||
|
||||
const worker = new Worker(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -10,14 +10,15 @@
|
||||
"@snk/types": "1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dat.gui": "0.7.7",
|
||||
"dotenv": "16.3.1",
|
||||
"@types/dat.gui": "0.7.10",
|
||||
"dat.gui": "0.7.9",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"ts-loader": "9.2.8",
|
||||
"ts-node": "10.7.0",
|
||||
"webpack": "5.72.0",
|
||||
"webpack-cli": "4.9.2",
|
||||
"webpack-dev-server": "4.8.1"
|
||||
"html-webpack-plugin": "5.5.3",
|
||||
"ts-loader": "9.4.4",
|
||||
"ts-node": "10.9.1",
|
||||
"webpack": "5.88.2",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "4.15.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import path from "path";
|
||||
import HtmlWebpackPlugin from "html-webpack-plugin";
|
||||
|
||||
import type { Configuration as WebpackConfiguration } from "webpack";
|
||||
import type { Configuration as WebpackDevServerConfiguration } from "webpack-dev-server";
|
||||
import webpack from "webpack";
|
||||
import { getGithubUserContribution } from "@snk/github-user-contribution";
|
||||
import { config } from "dotenv";
|
||||
import type { Configuration as WebpackConfiguration } from "webpack";
|
||||
import type { Configuration as WebpackDevServerConfiguration } from "webpack-dev-server";
|
||||
config({ path: __dirname + "/../../.env" });
|
||||
|
||||
const demos: string[] = require("./demo.json");
|
||||
|
||||
@@ -13,7 +14,11 @@ const webpackDevServerConfiguration: WebpackDevServerConfiguration = {
|
||||
onAfterSetupMiddleware: ({ app }) => {
|
||||
app!.get("/api/github-user-contribution/:userName", async (req, res) => {
|
||||
const userName: string = req.params.userName;
|
||||
res.send(await getGithubUserContribution(userName));
|
||||
res.send(
|
||||
await getGithubUserContribution(userName, {
|
||||
githubToken: process.env.GITHUB_TOKEN!,
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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();
|
||||
@@ -66,8 +66,9 @@ export const drawWorld = (
|
||||
};
|
||||
|
||||
export const drawLerpWorld = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
ctx: CanvasRenderingContext2D | 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,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,24 +30,27 @@ 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);
|
||||
|
||||
const canvas = createCanvas(width, height);
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
const ctx = canvas.getContext("2d") as any as CanvasRenderingContext2D;
|
||||
|
||||
const grid = copyGrid(grid0);
|
||||
const stack: Color[] = [];
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
"dependencies": {
|
||||
"@snk/draw": "1.0.0",
|
||||
"@snk/solver": "1.0.0",
|
||||
"canvas": "2.9.1",
|
||||
"canvas": "2.11.2",
|
||||
"gif-encoder-2": "1.0.5",
|
||||
"gifsicle": "5.3.0",
|
||||
"tmp": "0.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/gifsicle": "5.2.0",
|
||||
"@types/tmp": "0.2.3",
|
||||
"@vercel/ncc": "0.24.1"
|
||||
"@types/tmp": "0.2.4",
|
||||
"@vercel/ncc": "0.38.0"
|
||||
},
|
||||
"scripts": {
|
||||
"benchmark": "ncc run __tests__/benchmark.ts --quiet"
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { getGithubUserContribution } from "@snk/github-user-contribution";
|
||||
import { NowRequest, NowResponse } from "@vercel/node";
|
||||
import { VercelRequest, VercelResponse } from "@vercel/node";
|
||||
import nodeFetch from "node-fetch";
|
||||
|
||||
export default async (req: NowRequest, res: NowResponse) => {
|
||||
(global as any).fetch = nodeFetch;
|
||||
|
||||
export default async (req: VercelRequest, res: VercelResponse) => {
|
||||
const { userName } = req.query;
|
||||
|
||||
try {
|
||||
res.setHeader("Access-Control-Allow-Origin", "https://platane.github.io");
|
||||
res.statusCode = 200;
|
||||
res.json(await getGithubUserContribution(userName as string));
|
||||
res.json(
|
||||
await getGithubUserContribution(userName as string, {
|
||||
githubToken: process.env.GITHUB_TOKEN!,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.statusCode = 500;
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
"name": "@snk/github-user-contribution-service",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"node-fetch": "2.7.0",
|
||||
"@snk/github-user-contribution": "1.0.0",
|
||||
"@vercel/node": "1.14.0"
|
||||
"@vercel/node": "3.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node-fetch": "2.6.6"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +0,0 @@
|
||||
import { formatParams } from "../formatParams";
|
||||
|
||||
const params = [
|
||||
//
|
||||
[{}, ""],
|
||||
[{ year: 2017 }, "from=2017-01-01&to=2017-12-31"],
|
||||
[{ from: "2017-12-03" }, "from=2017-12-03"],
|
||||
[{ to: "2017-12-03" }, "to=2017-12-03"],
|
||||
] as const;
|
||||
|
||||
params.forEach(([params, res]) =>
|
||||
it(`should format ${JSON.stringify(params)}`, () => {
|
||||
expect(formatParams(params)).toBe(res);
|
||||
})
|
||||
);
|
||||
|
||||
it("should fail if the date is in the future", () => {
|
||||
expect(() => formatParams({ to: "9999-01-01" })).toThrow(Error);
|
||||
});
|
||||
@@ -1,54 +1,33 @@
|
||||
import { getGithubUserContribution } from "..";
|
||||
import { config } from "dotenv";
|
||||
config({ path: __dirname + "/../../../.env" });
|
||||
|
||||
describe("getGithubUserContribution", () => {
|
||||
const promise = getGithubUserContribution("platane");
|
||||
const promise = getGithubUserContribution("platane", {
|
||||
githubToken: process.env.GITHUB_TOKEN!,
|
||||
});
|
||||
|
||||
it("should resolve", async () => {
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should match snapshot for year=2019", async () => {
|
||||
expect(
|
||||
await getGithubUserContribution("platane", { year: 2019 })
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
export type Options = { from?: string; to?: string } | { year: number };
|
||||
|
||||
export const formatParams = (options: Options = {}) => {
|
||||
const sp = new URLSearchParams();
|
||||
|
||||
const o: any = { ...options };
|
||||
|
||||
if ("year" in options) {
|
||||
o.from = `${options.year}-01-01`;
|
||||
o.to = `${options.year}-12-31`;
|
||||
}
|
||||
|
||||
for (const s of ["from", "to"])
|
||||
if (o[s]) {
|
||||
const value = o[s];
|
||||
|
||||
if (value >= formatDate(new Date()))
|
||||
throw new Error(
|
||||
"Cannot get contribution for a date in the future.\nPlease limit your range to the current UTC day."
|
||||
);
|
||||
|
||||
sp.set(s, value);
|
||||
}
|
||||
|
||||
return sp.toString();
|
||||
};
|
||||
|
||||
const formatDate = (d: Date) => {
|
||||
const year = d.getUTCFullYear();
|
||||
const month = d.getUTCMonth() + 1;
|
||||
const date = d.getUTCDate();
|
||||
|
||||
return [
|
||||
year,
|
||||
month.toString().padStart(2, "0"),
|
||||
date.toString().padStart(2, "0"),
|
||||
].join("-");
|
||||
};
|
||||
@@ -1,7 +1,3 @@
|
||||
import fetch from "node-fetch";
|
||||
import * as cheerio from "cheerio";
|
||||
import { formatParams, Options } from "./formatParams";
|
||||
|
||||
/**
|
||||
* get the contribution grid from a github user page
|
||||
*
|
||||
@@ -20,110 +16,85 @@ import { formatParams, Options } from "./formatParams";
|
||||
*/
|
||||
export const getGithubUserContribution = async (
|
||||
userName: string,
|
||||
options: Options = {}
|
||||
o: { githubToken: string }
|
||||
) => {
|
||||
// either use github.com/users/xxxx/contributions for previous years
|
||||
// or github.com/xxxx ( which gives the latest update to today result )
|
||||
const url =
|
||||
"year" in options || "from" in options || "to" in options
|
||||
? `https://github.com/users/${userName}/contributions?` +
|
||||
formatParams(options)
|
||||
: `https://github.com/${userName}`;
|
||||
const query = /* GraphQL */ `
|
||||
query ($login: String!) {
|
||||
user(login: $login) {
|
||||
contributionsCollection {
|
||||
contributionCalendar {
|
||||
weeks {
|
||||
contributionDays {
|
||||
contributionCount
|
||||
contributionLevel
|
||||
weekday
|
||||
date
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const variables = { login: userName };
|
||||
|
||||
const res = await fetch(url);
|
||||
const res = await fetch("https://api.github.com/graphql", {
|
||||
headers: {
|
||||
Authorization: `bearer ${o.githubToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({ variables, query }),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(res.statusText);
|
||||
|
||||
const resText = await res.text();
|
||||
const { data, errors } = (await res.json()) as {
|
||||
data: GraphQLRes;
|
||||
errors?: { message: string }[];
|
||||
};
|
||||
|
||||
return parseUserPage(resText);
|
||||
if (errors?.[0]) throw errors[0];
|
||||
|
||||
return data.user.contributionsCollection.contributionCalendar.weeks.flatMap(
|
||||
({ contributionDays }, x) =>
|
||||
contributionDays.map((d) => ({
|
||||
x,
|
||||
y: d.weekday,
|
||||
date: d.date,
|
||||
count: d.contributionCount,
|
||||
level:
|
||||
(d.contributionLevel === "FOURTH_QUARTILE" && 4) ||
|
||||
(d.contributionLevel === "THIRD_QUARTILE" && 3) ||
|
||||
(d.contributionLevel === "SECOND_QUARTILE" && 2) ||
|
||||
(d.contributionLevel === "FIRST_QUARTILE" && 1) ||
|
||||
0,
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const defaultColorScheme = [
|
||||
"#ebedf0",
|
||||
"#9be9a8",
|
||||
"#40c463",
|
||||
"#30a14e",
|
||||
"#216e39",
|
||||
];
|
||||
|
||||
const parseUserPage = (content: string) => {
|
||||
const $ = cheerio.load(content);
|
||||
|
||||
//
|
||||
// "parse" colorScheme
|
||||
const colorScheme = [...defaultColorScheme];
|
||||
|
||||
//
|
||||
// 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 color = colorScheme[level];
|
||||
|
||||
if (!color) throw new Error("could not determine the color of the cell");
|
||||
|
||||
return {
|
||||
svgPosition: getSvgPosition(x),
|
||||
color,
|
||||
count,
|
||||
date,
|
||||
type GraphQLRes = {
|
||||
user: {
|
||||
contributionsCollection: {
|
||||
contributionCalendar: {
|
||||
weeks: {
|
||||
contributionDays: {
|
||||
contributionCount: number;
|
||||
contributionLevel:
|
||||
| "FOURTH_QUARTILE"
|
||||
| "THIRD_QUARTILE"
|
||||
| "SECOND_QUARTILE"
|
||||
| "FIRST_QUARTILE"
|
||||
| "NONE";
|
||||
date: string;
|
||||
weekday: number;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
});
|
||||
|
||||
const xMap: Record<number, true> = {};
|
||||
const yMap: Record<number, true> = {};
|
||||
rawCells.forEach(({ svgPosition: { x, y } }) => {
|
||||
xMap[x] = true;
|
||||
yMap[y] = true;
|
||||
});
|
||||
|
||||
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 cells = rawCells.map(({ svgPosition, ...c }) => ({
|
||||
...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;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type Res = Awaited<ReturnType<typeof getGithubUserContribution>>;
|
||||
|
||||
export type Cell = Res["cells"][number];
|
||||
export type Cell = Res[number];
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"name": "@snk/github-user-contribution",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"cheerio": "1.0.0-rc.10",
|
||||
"node-fetch": "2.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node-fetch": "2.6.1"
|
||||
"dotenv": "16.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
26
packages/svg-creator/__tests__/minifyCss.spec.ts
Normal file
26
packages/svg-creator/__tests__/minifyCss.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { minifyCss } from "../css-utils";
|
||||
|
||||
it("should minify css", () => {
|
||||
expect(
|
||||
minifyCss(`
|
||||
.c {
|
||||
color : red ;
|
||||
}
|
||||
|
||||
`)
|
||||
).toBe(".c{color:red}");
|
||||
|
||||
expect(
|
||||
minifyCss(`
|
||||
.c {
|
||||
top : 0;
|
||||
color : red ;
|
||||
}
|
||||
|
||||
# {
|
||||
animation: linear 10;
|
||||
}
|
||||
|
||||
`)
|
||||
).toBe(".c{top:0;color:red}#{animation:linear 10}");
|
||||
});
|
||||
38
packages/svg-creator/css-utils.ts
Normal file
38
packages/svg-creator/css-utils.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
const percent = (x: number) =>
|
||||
parseFloat((x * 100).toFixed(2)).toString() + "%";
|
||||
|
||||
const mergeKeyFrames = (keyframes: { t: number; style: string }[]) => {
|
||||
const s = new Map<string, number[]>();
|
||||
for (const { t, style } of keyframes) {
|
||||
s.set(style, [...(s.get(style) ?? []), t]);
|
||||
}
|
||||
return Array.from(s.entries())
|
||||
.map(([style, ts]) => ({ style, ts }))
|
||||
.sort((a, b) => a.ts[0] - b.ts[0]);
|
||||
};
|
||||
|
||||
/**
|
||||
* generate the keyframe animation from a list of keyframe
|
||||
*/
|
||||
export const createAnimation = (
|
||||
name: string,
|
||||
keyframes: { t: number; style: string }[]
|
||||
) =>
|
||||
`@keyframes ${name}{` +
|
||||
mergeKeyFrames(keyframes)
|
||||
.map(({ style, ts }) => ts.map(percent).join(",") + `{${style}}`)
|
||||
.join("") +
|
||||
"}";
|
||||
|
||||
/**
|
||||
* remove white spaces
|
||||
*/
|
||||
export const minifyCss = (css: string) =>
|
||||
css
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/.\s+[,;:{}()]/g, (a) => a.replace(/\s+/g, ""))
|
||||
.replace(/[,;:{}()]\s+./g, (a) => a.replace(/\s+/g, ""))
|
||||
.replace(/.\s+[,;:{}()]/g, (a) => a.replace(/\s+/g, ""))
|
||||
.replace(/[,;:{}()]\s+./g, (a) => a.replace(/\s+/g, ""))
|
||||
.replace(/\;\s*\}/g, "}")
|
||||
.trim();
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Color, Empty } from "@snk/types/grid";
|
||||
import type { Point } from "@snk/types/point";
|
||||
import { h } from "./utils";
|
||||
import { createAnimation } from "./css-utils";
|
||||
import { h } from "./xml-utils";
|
||||
|
||||
export type Options = {
|
||||
colorDots: Record<Color, string>;
|
||||
@@ -11,8 +12,6 @@ export type Options = {
|
||||
sizeDotBorderRadius: number;
|
||||
};
|
||||
|
||||
const percent = (x: number) => (x * 100).toFixed(2);
|
||||
|
||||
export const createGrid = (
|
||||
cells: (Point & { t: number | null; color: Color | Empty })[],
|
||||
{ sizeDotBorderRadius, sizeDot, sizeCell }: Options,
|
||||
@@ -26,38 +25,40 @@ export const createGrid = (
|
||||
stroke-width: 1px;
|
||||
stroke: var(--cb);
|
||||
animation: none ${duration}ms linear infinite;
|
||||
width: ${sizeDot}px;
|
||||
height: ${sizeDot}px;
|
||||
}`,
|
||||
];
|
||||
|
||||
let i = 0;
|
||||
for (const { x, y, color, t } of cells) {
|
||||
const id = t && "c" + (i++).toString(36);
|
||||
const s = sizeCell;
|
||||
const d = sizeDot;
|
||||
const m = (s - d) / 2;
|
||||
const m = (sizeCell - sizeDot) / 2;
|
||||
|
||||
if (t !== null) {
|
||||
if (t !== null && id) {
|
||||
const animationName = id;
|
||||
|
||||
styles.push(
|
||||
`@keyframes ${animationName} {` +
|
||||
`${percent(t - 0.0001)}%{fill:var(--c${color})}` +
|
||||
`${percent(t + 0.0001)}%,100%{fill:var(--ce)}` +
|
||||
"}",
|
||||
createAnimation(animationName, [
|
||||
{ t: t - 0.0001, style: `fill:var(--c${color})` },
|
||||
{ t: t + 0.0001, style: `fill:var(--ce)` },
|
||||
{ t: 1, style: `fill:var(--ce)` },
|
||||
]),
|
||||
|
||||
`.c.${id}{fill: var(--c${color}); animation-name: ${animationName}}`
|
||||
`.c.${id}{
|
||||
fill: var(--c${color});
|
||||
animation-name: ${animationName}
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
svgElements.push(
|
||||
h("rect", {
|
||||
class: ["c", id].filter(Boolean).join(" "),
|
||||
x: x * s + m,
|
||||
y: y * s + m,
|
||||
x: x * sizeCell + m,
|
||||
y: y * sizeCell + m,
|
||||
rx: sizeDotBorderRadius,
|
||||
ry: sizeDotBorderRadius,
|
||||
width: d,
|
||||
height: d,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,13 +9,14 @@ import { getHeadX, getHeadY } from "@snk/types/snake";
|
||||
import type { Snake } from "@snk/types/snake";
|
||||
import type { Grid, Color, Empty } from "@snk/types/grid";
|
||||
import type { Point } from "@snk/types/point";
|
||||
import type { AnimationOptions } from "@snk/gif-creator";
|
||||
import { createSnake } from "./snake";
|
||||
import { createGrid } from "./grid";
|
||||
import { createStack } from "./stack";
|
||||
import { h } from "./utils";
|
||||
import * as csso from "csso";
|
||||
import { h } from "./xml-utils";
|
||||
import { minifyCss } from "./css-utils";
|
||||
|
||||
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,
|
||||
@@ -131,10 +132,10 @@ export const createSvg = (
|
||||
return optimizeSvg(svg);
|
||||
};
|
||||
|
||||
const optimizeCss = (css: string) => csso.minify(css).css;
|
||||
const optimizeCss = (css: string) => minifyCss(css);
|
||||
const optimizeSvg = (svg: string) => svg;
|
||||
|
||||
const generateColorVar = (drawOptions: Options) =>
|
||||
const generateColorVar = (drawOptions: DrawOptions) =>
|
||||
`
|
||||
:root {
|
||||
--cb: ${drawOptions.colorDotBorder};
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
"name": "@snk/svg-creator",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@snk/solver": "1.0.0",
|
||||
"csso": "5.0.3"
|
||||
"@snk/solver": "1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/csso": "5.0.0"
|
||||
}
|
||||
"devDependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { getSnakeLength, snakeToCells } from "@snk/types/snake";
|
||||
import type { Snake } from "@snk/types/snake";
|
||||
import type { Point } from "@snk/types/point";
|
||||
import { h } from "./utils";
|
||||
import { h } from "./xml-utils";
|
||||
import { createAnimation } from "./css-utils";
|
||||
|
||||
export type Options = {
|
||||
colorSnake: string;
|
||||
@@ -9,8 +10,6 @@ export type Options = {
|
||||
sizeDot: number;
|
||||
};
|
||||
|
||||
const percent = (x: number) => (x * 100).toFixed(2);
|
||||
|
||||
const lerp = (k: number, a: number, b: number) => (1 - k) * a + k * b;
|
||||
|
||||
export const createSnake = (
|
||||
@@ -55,8 +54,8 @@ export const createSnake = (
|
||||
|
||||
const styles = [
|
||||
`.s{
|
||||
shape-rendering:geometricPrecision;
|
||||
fill:var(--cs);
|
||||
shape-rendering: geometricPrecision;
|
||||
fill: var(--cs);
|
||||
animation: none linear ${duration}ms infinite
|
||||
}`,
|
||||
|
||||
@@ -64,16 +63,17 @@ export const createSnake = (
|
||||
const id = `s${i}`;
|
||||
const animationName = id;
|
||||
|
||||
return [
|
||||
`@keyframes ${animationName} {` +
|
||||
removeInterpolatedPositions(
|
||||
positions.map((tr, i, { length }) => ({ ...tr, t: i / length }))
|
||||
)
|
||||
.map((p) => `${percent(p.t)}%{${transform(p)}}`)
|
||||
.join("") +
|
||||
"}",
|
||||
const keyframes = removeInterpolatedPositions(
|
||||
positions.map((tr, i, { length }) => ({ ...tr, t: i / length }))
|
||||
).map(({ t, ...p }) => ({ t, style: transform(p) }));
|
||||
|
||||
`.s.${id}{${transform(positions[0])};animation-name: ${animationName}}`,
|
||||
return [
|
||||
createAnimation(animationName, keyframes),
|
||||
|
||||
`.s.${id}{
|
||||
${transform(positions[0])};
|
||||
animation-name: ${animationName}
|
||||
}`,
|
||||
];
|
||||
}),
|
||||
].flat();
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { Color, Empty } from "@snk/types/grid";
|
||||
import { h } from "./utils";
|
||||
import { createAnimation } from "./css-utils";
|
||||
import { h } from "./xml-utils";
|
||||
|
||||
export type Options = {
|
||||
sizeDot: number;
|
||||
};
|
||||
|
||||
const percent = (x: number) => (x * 100).toFixed(2);
|
||||
|
||||
export const createStack = (
|
||||
cells: { t: number | null; color: Color | Empty }[],
|
||||
{ sizeDot }: Options,
|
||||
@@ -56,23 +55,28 @@ export const createStack = (
|
||||
);
|
||||
|
||||
styles.push(
|
||||
`@keyframes ${animationName} {` +
|
||||
createAnimation(
|
||||
animationName,
|
||||
[
|
||||
...ts.map((t, i, { length }) => [
|
||||
{ scale: i / length, t: t - 0.0001 },
|
||||
{ scale: (i + 1) / length, t: t + 0.0001 },
|
||||
]),
|
||||
[{ scale: 1, t: 1 }],
|
||||
]
|
||||
.flat()
|
||||
.map(
|
||||
({ scale, t }) =>
|
||||
`${percent(t)}%{transform:scale(${scale.toFixed(2)},1)}`
|
||||
)
|
||||
.join("\n") +
|
||||
"}",
|
||||
...ts
|
||||
.map((t, i, { length }) => [
|
||||
{ scale: i / length, t: t - 0.0001 },
|
||||
{ scale: (i + 1) / length, t: t + 0.0001 },
|
||||
])
|
||||
.flat(),
|
||||
{ scale: 1, t: 1 },
|
||||
].map(({ scale, t }) => ({
|
||||
t,
|
||||
style: `transform:scale(${scale.toFixed(3)},1)`,
|
||||
}))
|
||||
),
|
||||
|
||||
`.u.${id}{fill:var(--c${color});animation-name:${animationName};transform-origin:${x}px 0}`
|
||||
`.u.${id} {
|
||||
fill: var(--c${color});
|
||||
animation-name: ${animationName};
|
||||
transform-origin: ${x}px 0
|
||||
}
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,18 +3,34 @@ description: "Generates a snake game from a github user contributions grid. Outp
|
||||
author: "platane"
|
||||
|
||||
runs:
|
||||
using: node16
|
||||
using: node20
|
||||
main: dist/index.js
|
||||
|
||||
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."
|
||||
github_token:
|
||||
description: "github token used to fetch the contribution calendar. Default to the action token if empty."
|
||||
required: false
|
||||
default: ${{ github.token }}
|
||||
outputs:
|
||||
required: false
|
||||
default: null
|
||||
description: |
|
||||
list of files to generate.
|
||||
one file per line. Each output can be customized with options as query string.
|
||||
|
||||
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 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
|
||||
light.svg?color_snake=#7845ab
|
||||
ocean.svg?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9
|
||||
|
||||
233
svg-only/dist/142.index.js
vendored
Normal file
233
svg-only/dist/142.index.js
vendored
Normal file
@@ -0,0 +1,233 @@
|
||||
"use strict";
|
||||
exports.id = 142;
|
||||
exports.ids = [142];
|
||||
exports.modules = {
|
||||
|
||||
/***/ 7142:
|
||||
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
// ESM COMPAT FLAG
|
||||
__webpack_require__.r(__webpack_exports__);
|
||||
|
||||
// EXPORTS
|
||||
__webpack_require__.d(__webpack_exports__, {
|
||||
"createGif": () => (/* binding */ createGif)
|
||||
});
|
||||
|
||||
// EXTERNAL MODULE: external "fs"
|
||||
var external_fs_ = __webpack_require__(7147);
|
||||
var external_fs_default = /*#__PURE__*/__webpack_require__.n(external_fs_);
|
||||
// EXTERNAL MODULE: external "path"
|
||||
var external_path_ = __webpack_require__(1017);
|
||||
var external_path_default = /*#__PURE__*/__webpack_require__.n(external_path_);
|
||||
// EXTERNAL MODULE: external "child_process"
|
||||
var external_child_process_ = __webpack_require__(2081);
|
||||
// EXTERNAL MODULE: external "canvas"
|
||||
var external_canvas_ = __webpack_require__(1576);
|
||||
// EXTERNAL MODULE: ../types/grid.ts
|
||||
var types_grid = __webpack_require__(2881);
|
||||
;// CONCATENATED MODULE: ../draw/pathRoundedRect.ts
|
||||
const pathRoundedRect_pathRoundedRect = (ctx, width, height, borderRadius) => {
|
||||
ctx.moveTo(borderRadius, 0);
|
||||
ctx.arcTo(width, 0, width, height, borderRadius);
|
||||
ctx.arcTo(width, height, 0, height, borderRadius);
|
||||
ctx.arcTo(0, height, 0, 0, borderRadius);
|
||||
ctx.arcTo(0, 0, width, 0, borderRadius);
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../draw/drawGrid.ts
|
||||
|
||||
|
||||
const drawGrid_drawGrid = (ctx, grid, cells, o) => {
|
||||
for (let x = grid.width; x--;)
|
||||
for (let y = grid.height; y--;) {
|
||||
if (!cells || cells.some((c) => c.x === x && c.y === y)) {
|
||||
const c = (0,types_grid/* getColor */.Lq)(grid, x, y);
|
||||
// @ts-ignore
|
||||
const color = !c ? o.colorEmpty : o.colorDots[c];
|
||||
ctx.save();
|
||||
ctx.translate(x * o.sizeCell + (o.sizeCell - o.sizeDot) / 2, y * o.sizeCell + (o.sizeCell - o.sizeDot) / 2);
|
||||
ctx.fillStyle = color;
|
||||
ctx.strokeStyle = o.colorDotBorder;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
pathRoundedRect_pathRoundedRect(ctx, o.sizeDot, o.sizeDot, o.sizeDotBorderRadius);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../draw/drawSnake.ts
|
||||
|
||||
|
||||
const drawSnake_drawSnake = (ctx, snake, o) => {
|
||||
const cells = snakeToCells(snake);
|
||||
for (let i = 0; i < cells.length; i++) {
|
||||
const u = (i + 1) * 0.6;
|
||||
ctx.save();
|
||||
ctx.fillStyle = o.colorSnake;
|
||||
ctx.translate(cells[i].x * o.sizeCell + u, cells[i].y * o.sizeCell + u);
|
||||
ctx.beginPath();
|
||||
pathRoundedRect(ctx, o.sizeCell - u * 2, o.sizeCell - u * 2, (o.sizeCell - u * 2) * 0.25);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
const lerp = (k, a, b) => (1 - k) * a + k * b;
|
||||
const clamp = (x, a, b) => Math.max(a, Math.min(b, x));
|
||||
const drawSnakeLerp = (ctx, snake0, snake1, k, o) => {
|
||||
const m = 0.8;
|
||||
const n = snake0.length / 2;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const u = (i + 1) * 0.6 * (o.sizeCell / 16);
|
||||
const a = (1 - m) * (i / Math.max(n - 1, 1));
|
||||
const ki = clamp((k - a) / m, 0, 1);
|
||||
const x = lerp(ki, snake0[i * 2 + 0], snake1[i * 2 + 0]) - 2;
|
||||
const y = lerp(ki, snake0[i * 2 + 1], snake1[i * 2 + 1]) - 2;
|
||||
ctx.save();
|
||||
ctx.fillStyle = o.colorSnake;
|
||||
ctx.translate(x * o.sizeCell + u, y * o.sizeCell + u);
|
||||
ctx.beginPath();
|
||||
pathRoundedRect_pathRoundedRect(ctx, o.sizeCell - u * 2, o.sizeCell - u * 2, (o.sizeCell - u * 2) * 0.25);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../draw/drawWorld.ts
|
||||
|
||||
|
||||
const drawStack = (ctx, stack, max, width, o) => {
|
||||
ctx.save();
|
||||
const m = width / max;
|
||||
for (let i = 0; i < stack.length; i++) {
|
||||
// @ts-ignore
|
||||
ctx.fillStyle = o.colorDots[stack[i]];
|
||||
ctx.fillRect(i * m, 0, m + width * 0.005, 10);
|
||||
}
|
||||
ctx.restore();
|
||||
};
|
||||
const drawWorld = (ctx, grid, cells, snake, stack, o) => {
|
||||
ctx.save();
|
||||
ctx.translate(1 * o.sizeCell, 2 * o.sizeCell);
|
||||
drawGrid(ctx, grid, cells, o);
|
||||
drawSnake(ctx, snake, o);
|
||||
ctx.restore();
|
||||
ctx.save();
|
||||
ctx.translate(o.sizeCell, (grid.height + 4) * o.sizeCell);
|
||||
const max = grid.data.reduce((sum, x) => sum + +!!x, stack.length);
|
||||
drawStack(ctx, stack, max, grid.width * o.sizeCell, o);
|
||||
ctx.restore();
|
||||
// ctx.save();
|
||||
// ctx.translate(o.sizeCell + 100, (grid.height + 4) * o.sizeCell + 100);
|
||||
// ctx.scale(0.6, 0.6);
|
||||
// drawCircleStack(ctx, stack, o);
|
||||
// ctx.restore();
|
||||
};
|
||||
const drawLerpWorld = (ctx, grid, cells, snake0, snake1, stack, k, o) => {
|
||||
ctx.save();
|
||||
ctx.translate(1 * o.sizeCell, 2 * o.sizeCell);
|
||||
drawGrid_drawGrid(ctx, grid, cells, o);
|
||||
drawSnakeLerp(ctx, snake0, snake1, k, o);
|
||||
ctx.translate(0, (grid.height + 2) * o.sizeCell);
|
||||
const max = grid.data.reduce((sum, x) => sum + +!!x, stack.length);
|
||||
drawStack(ctx, stack, max, grid.width * o.sizeCell, o);
|
||||
ctx.restore();
|
||||
};
|
||||
const getCanvasWorldSize = (grid, o) => {
|
||||
const width = o.sizeCell * (grid.width + 2);
|
||||
const height = o.sizeCell * (grid.height + 4) + 30;
|
||||
return { width, height };
|
||||
};
|
||||
|
||||
// EXTERNAL MODULE: ../types/snake.ts
|
||||
var types_snake = __webpack_require__(9347);
|
||||
;// CONCATENATED MODULE: ../solver/step.ts
|
||||
|
||||
|
||||
const step = (grid, stack, snake) => {
|
||||
const x = (0,types_snake/* getHeadX */.If)(snake);
|
||||
const y = (0,types_snake/* getHeadY */.IP)(snake);
|
||||
const color = (0,types_grid/* getColor */.Lq)(grid, x, y);
|
||||
if ((0,types_grid/* isInside */.V0)(grid, x, y) && !(0,types_grid/* isEmpty */.xb)(color)) {
|
||||
stack.push(color);
|
||||
(0,types_grid/* setColorEmpty */.Dy)(grid, x, y);
|
||||
}
|
||||
};
|
||||
|
||||
// EXTERNAL MODULE: ../../node_modules/tmp/lib/tmp.js
|
||||
var tmp = __webpack_require__(6382);
|
||||
// EXTERNAL MODULE: external "gifsicle"
|
||||
var external_gifsicle_ = __webpack_require__(542);
|
||||
var external_gifsicle_default = /*#__PURE__*/__webpack_require__.n(external_gifsicle_);
|
||||
// EXTERNAL MODULE: ../../node_modules/gif-encoder-2/index.js
|
||||
var gif_encoder_2 = __webpack_require__(3561);
|
||||
var gif_encoder_2_default = /*#__PURE__*/__webpack_require__.n(gif_encoder_2);
|
||||
;// CONCATENATED MODULE: ../gif-creator/index.ts
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
|
||||
const withTmpDir = async (handler) => {
|
||||
const { name: dir, removeCallback: cleanUp } = tmp.dirSync({
|
||||
unsafeCleanup: true,
|
||||
});
|
||||
try {
|
||||
return await handler(dir);
|
||||
}
|
||||
finally {
|
||||
cleanUp();
|
||||
}
|
||||
};
|
||||
const createGif = async (grid0, cells, chain, drawOptions, animationOptions) => withTmpDir(async (dir) => {
|
||||
const { width, height } = getCanvasWorldSize(grid0, drawOptions);
|
||||
const canvas = (0,external_canvas_.createCanvas)(width, height);
|
||||
const ctx = canvas.getContext("2d");
|
||||
const grid = (0,types_grid/* copyGrid */.VJ)(grid0);
|
||||
const stack = [];
|
||||
const encoder = new (gif_encoder_2_default())(width, height, "neuquant", true);
|
||||
encoder.setRepeat(0);
|
||||
encoder.setDelay(animationOptions.frameDuration);
|
||||
encoder.start();
|
||||
for (let i = 0; i < chain.length; i += 1) {
|
||||
const snake0 = chain[i];
|
||||
const snake1 = chain[Math.min(chain.length - 1, i + 1)];
|
||||
step(grid, stack, snake0);
|
||||
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 / animationOptions.step, drawOptions);
|
||||
encoder.addFrame(ctx);
|
||||
}
|
||||
}
|
||||
const outFileName = external_path_default().join(dir, "out.gif");
|
||||
const optimizedFileName = external_path_default().join(dir, "out.optimized.gif");
|
||||
encoder.finish();
|
||||
external_fs_default().writeFileSync(outFileName, encoder.out.getData());
|
||||
(0,external_child_process_.execFileSync)((external_gifsicle_default()), [
|
||||
//
|
||||
"--optimize=3",
|
||||
"--color-method=diversity",
|
||||
"--colors=18",
|
||||
outFileName,
|
||||
["--output", optimizedFileName],
|
||||
].flat());
|
||||
return external_fs_default().readFileSync(optimizedFileName);
|
||||
});
|
||||
|
||||
|
||||
/***/ })
|
||||
|
||||
};
|
||||
;
|
||||
325
svg-only/dist/340.index.js
vendored
Normal file
325
svg-only/dist/340.index.js
vendored
Normal file
@@ -0,0 +1,325 @@
|
||||
"use strict";
|
||||
exports.id = 340;
|
||||
exports.ids = [340];
|
||||
exports.modules = {
|
||||
|
||||
/***/ 8340:
|
||||
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
// ESM COMPAT FLAG
|
||||
__webpack_require__.r(__webpack_exports__);
|
||||
|
||||
// EXPORTS
|
||||
__webpack_require__.d(__webpack_exports__, {
|
||||
"createSvg": () => (/* binding */ createSvg)
|
||||
});
|
||||
|
||||
// EXTERNAL MODULE: ../types/grid.ts
|
||||
var types_grid = __webpack_require__(2881);
|
||||
// EXTERNAL MODULE: ../types/snake.ts
|
||||
var types_snake = __webpack_require__(9347);
|
||||
;// CONCATENATED MODULE: ../svg-creator/xml-utils.ts
|
||||
const h = (element, attributes) => `<${element} ${toAttribute(attributes)}/>`;
|
||||
const toAttribute = (o) => Object.entries(o)
|
||||
.filter(([, value]) => value !== null)
|
||||
.map(([name, value]) => `${name}="${value}"`)
|
||||
.join(" ");
|
||||
|
||||
;// CONCATENATED MODULE: ../svg-creator/css-utils.ts
|
||||
const percent = (x) => parseFloat((x * 100).toFixed(2)).toString() + "%";
|
||||
const mergeKeyFrames = (keyframes) => {
|
||||
const s = new Map();
|
||||
for (const { t, style } of keyframes) {
|
||||
s.set(style, [...(s.get(style) ?? []), t]);
|
||||
}
|
||||
return Array.from(s.entries())
|
||||
.map(([style, ts]) => ({ style, ts }))
|
||||
.sort((a, b) => a.ts[0] - b.ts[0]);
|
||||
};
|
||||
/**
|
||||
* generate the keyframe animation from a list of keyframe
|
||||
*/
|
||||
const createAnimation = (name, keyframes) => `@keyframes ${name}{` +
|
||||
mergeKeyFrames(keyframes)
|
||||
.map(({ style, ts }) => ts.map(percent).join(",") + `{${style}}`)
|
||||
.join("") +
|
||||
"}";
|
||||
/**
|
||||
* remove white spaces
|
||||
*/
|
||||
const minifyCss = (css) => css
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/.\s+[,;:{}()]/g, (a) => a.replace(/\s+/g, ""))
|
||||
.replace(/[,;:{}()]\s+./g, (a) => a.replace(/\s+/g, ""))
|
||||
.replace(/.\s+[,;:{}()]/g, (a) => a.replace(/\s+/g, ""))
|
||||
.replace(/[,;:{}()]\s+./g, (a) => a.replace(/\s+/g, ""))
|
||||
.replace(/\;\s*\}/g, "}")
|
||||
.trim();
|
||||
|
||||
;// CONCATENATED MODULE: ../svg-creator/snake.ts
|
||||
|
||||
|
||||
|
||||
const lerp = (k, a, b) => (1 - k) * a + k * b;
|
||||
const createSnake = (chain, { sizeCell, sizeDot }, duration) => {
|
||||
const snakeN = chain[0] ? (0,types_snake/* getSnakeLength */.JJ)(chain[0]) : 0;
|
||||
const snakeParts = Array.from({ length: snakeN }, () => []);
|
||||
for (const snake of chain) {
|
||||
const cells = (0,types_snake/* snakeToCells */.Ks)(snake);
|
||||
for (let i = cells.length; i--;)
|
||||
snakeParts[i].push(cells[i]);
|
||||
}
|
||||
const svgElements = snakeParts.map((_, i, { length }) => {
|
||||
// compute snake part size
|
||||
const dMin = sizeDot * 0.8;
|
||||
const dMax = sizeCell * 0.9;
|
||||
const iMax = Math.min(4, length);
|
||||
const u = (1 - Math.min(i, iMax) / iMax) ** 2;
|
||||
const s = lerp(u, dMin, dMax);
|
||||
const m = (sizeCell - s) / 2;
|
||||
const r = Math.min(4.5, (4 * s) / sizeDot);
|
||||
return h("rect", {
|
||||
class: `s s${i}`,
|
||||
x: m.toFixed(1),
|
||||
y: m.toFixed(1),
|
||||
width: s.toFixed(1),
|
||||
height: s.toFixed(1),
|
||||
rx: r.toFixed(1),
|
||||
ry: r.toFixed(1),
|
||||
});
|
||||
});
|
||||
const transform = ({ x, y }) => `transform:translate(${x * sizeCell}px,${y * sizeCell}px)`;
|
||||
const styles = [
|
||||
`.s{
|
||||
shape-rendering: geometricPrecision;
|
||||
fill: var(--cs);
|
||||
animation: none linear ${duration}ms infinite
|
||||
}`,
|
||||
...snakeParts.map((positions, i) => {
|
||||
const id = `s${i}`;
|
||||
const animationName = id;
|
||||
const keyframes = removeInterpolatedPositions(positions.map((tr, i, { length }) => ({ ...tr, t: i / length }))).map(({ t, ...p }) => ({ t, style: transform(p) }));
|
||||
return [
|
||||
createAnimation(animationName, keyframes),
|
||||
`.s.${id}{
|
||||
${transform(positions[0])};
|
||||
animation-name: ${animationName}
|
||||
}`,
|
||||
];
|
||||
}),
|
||||
].flat();
|
||||
return { svgElements, styles };
|
||||
};
|
||||
const removeInterpolatedPositions = (arr) => arr.filter((u, i, arr) => {
|
||||
if (i - 1 < 0 || i + 1 >= arr.length)
|
||||
return true;
|
||||
const a = arr[i - 1];
|
||||
const b = arr[i + 1];
|
||||
const ex = (a.x + b.x) / 2;
|
||||
const ey = (a.y + b.y) / 2;
|
||||
// return true;
|
||||
return !(Math.abs(ex - u.x) < 0.01 && Math.abs(ey - u.y) < 0.01);
|
||||
});
|
||||
|
||||
;// CONCATENATED MODULE: ../svg-creator/grid.ts
|
||||
|
||||
|
||||
const createGrid = (cells, { sizeDotBorderRadius, sizeDot, sizeCell }, duration) => {
|
||||
const svgElements = [];
|
||||
const styles = [
|
||||
`.c{
|
||||
shape-rendering: geometricPrecision;
|
||||
fill: var(--ce);
|
||||
stroke-width: 1px;
|
||||
stroke: var(--cb);
|
||||
animation: none ${duration}ms linear infinite;
|
||||
width: ${sizeDot}px;
|
||||
height: ${sizeDot}px;
|
||||
}`,
|
||||
];
|
||||
let i = 0;
|
||||
for (const { x, y, color, t } of cells) {
|
||||
const id = t && "c" + (i++).toString(36);
|
||||
const m = (sizeCell - sizeDot) / 2;
|
||||
if (t !== null && id) {
|
||||
const animationName = id;
|
||||
styles.push(createAnimation(animationName, [
|
||||
{ t: t - 0.0001, style: `fill:var(--c${color})` },
|
||||
{ t: t + 0.0001, style: `fill:var(--ce)` },
|
||||
{ t: 1, style: `fill:var(--ce)` },
|
||||
]), `.c.${id}{
|
||||
fill: var(--c${color});
|
||||
animation-name: ${animationName}
|
||||
}`);
|
||||
}
|
||||
svgElements.push(h("rect", {
|
||||
class: ["c", id].filter(Boolean).join(" "),
|
||||
x: x * sizeCell + m,
|
||||
y: y * sizeCell + m,
|
||||
rx: sizeDotBorderRadius,
|
||||
ry: sizeDotBorderRadius,
|
||||
}));
|
||||
}
|
||||
return { svgElements, styles };
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../svg-creator/stack.ts
|
||||
|
||||
|
||||
const createStack = (cells, { sizeDot }, width, y, duration) => {
|
||||
const svgElements = [];
|
||||
const styles = [
|
||||
`.u{
|
||||
transform-origin: 0 0;
|
||||
transform: scale(0,1);
|
||||
animation: none linear ${duration}ms infinite;
|
||||
}`,
|
||||
];
|
||||
const stack = cells
|
||||
.slice()
|
||||
.filter((a) => a.t !== null)
|
||||
.sort((a, b) => a.t - b.t);
|
||||
const blocks = [];
|
||||
stack.forEach(({ color, t }) => {
|
||||
const latest = blocks[blocks.length - 1];
|
||||
if (latest?.color === color)
|
||||
latest.ts.push(t);
|
||||
else
|
||||
blocks.push({ color, ts: [t] });
|
||||
});
|
||||
const m = width / stack.length;
|
||||
let i = 0;
|
||||
let nx = 0;
|
||||
for (const { color, ts } of blocks) {
|
||||
const id = "u" + (i++).toString(36);
|
||||
const animationName = id;
|
||||
const x = (nx * m).toFixed(1);
|
||||
nx += ts.length;
|
||||
svgElements.push(h("rect", {
|
||||
class: `u ${id}`,
|
||||
height: sizeDot,
|
||||
width: (ts.length * m + 0.6).toFixed(1),
|
||||
x,
|
||||
y,
|
||||
}));
|
||||
styles.push(createAnimation(animationName, [
|
||||
...ts
|
||||
.map((t, i, { length }) => [
|
||||
{ scale: i / length, t: t - 0.0001 },
|
||||
{ scale: (i + 1) / length, t: t + 0.0001 },
|
||||
])
|
||||
.flat(),
|
||||
{ scale: 1, t: 1 },
|
||||
].map(({ scale, t }) => ({
|
||||
t,
|
||||
style: `transform:scale(${scale.toFixed(3)},1)`,
|
||||
}))), `.u.${id} {
|
||||
fill: var(--c${color});
|
||||
animation-name: ${animationName};
|
||||
transform-origin: ${x}px 0
|
||||
}
|
||||
`);
|
||||
}
|
||||
return { svgElements, styles };
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../svg-creator/index.ts
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const getCellsFromGrid = ({ width, height }) => Array.from({ length: width }, (_, x) => Array.from({ length: height }, (_, y) => ({ x, y }))).flat();
|
||||
const createLivingCells = (grid0, chain, cells) => {
|
||||
const livingCells = (cells ?? getCellsFromGrid(grid0)).map(({ x, y }) => ({
|
||||
x,
|
||||
y,
|
||||
t: null,
|
||||
color: (0,types_grid/* getColor */.Lq)(grid0, x, y),
|
||||
}));
|
||||
const grid = (0,types_grid/* copyGrid */.VJ)(grid0);
|
||||
for (let i = 0; i < chain.length; i++) {
|
||||
const snake = chain[i];
|
||||
const x = (0,types_snake/* getHeadX */.If)(snake);
|
||||
const y = (0,types_snake/* getHeadY */.IP)(snake);
|
||||
if ((0,types_grid/* isInside */.V0)(grid, x, y) && !(0,types_grid/* isEmpty */.xb)((0,types_grid/* getColor */.Lq)(grid, x, y))) {
|
||||
(0,types_grid/* setColorEmpty */.Dy)(grid, x, y);
|
||||
const cell = livingCells.find((c) => c.x === x && c.y === y);
|
||||
cell.t = i / chain.length;
|
||||
}
|
||||
}
|
||||
return livingCells;
|
||||
};
|
||||
const createSvg = (grid, cells, chain, drawOptions, animationOptions) => {
|
||||
const width = (grid.width + 2) * drawOptions.sizeCell;
|
||||
const height = (grid.height + 5) * drawOptions.sizeCell;
|
||||
const duration = animationOptions.frameDuration * chain.length;
|
||||
const livingCells = createLivingCells(grid, chain, cells);
|
||||
const elements = [
|
||||
createGrid(livingCells, drawOptions, duration),
|
||||
createStack(livingCells, drawOptions, grid.width * drawOptions.sizeCell, (grid.height + 2) * drawOptions.sizeCell, duration),
|
||||
createSnake(chain, drawOptions, duration),
|
||||
];
|
||||
const viewBox = [
|
||||
-drawOptions.sizeCell,
|
||||
-drawOptions.sizeCell * 2,
|
||||
width,
|
||||
height,
|
||||
].join(" ");
|
||||
const style = generateColorVar(drawOptions) +
|
||||
elements
|
||||
.map((e) => e.styles)
|
||||
.flat()
|
||||
.join("\n");
|
||||
const svg = [
|
||||
h("svg", {
|
||||
viewBox,
|
||||
width,
|
||||
height,
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
}).replace("/>", ">"),
|
||||
"<desc>",
|
||||
"Generated with https://github.com/Platane/snk",
|
||||
"</desc>",
|
||||
"<style>",
|
||||
optimizeCss(style),
|
||||
"</style>",
|
||||
...elements.map((e) => e.svgElements).flat(),
|
||||
"</svg>",
|
||||
].join("");
|
||||
return optimizeSvg(svg);
|
||||
};
|
||||
const optimizeCss = (css) => minifyCss(css);
|
||||
const optimizeSvg = (svg) => svg;
|
||||
const generateColorVar = (drawOptions) => `
|
||||
:root {
|
||||
--cb: ${drawOptions.colorDotBorder};
|
||||
--cs: ${drawOptions.colorSnake};
|
||||
--ce: ${drawOptions.colorEmpty};
|
||||
${Object.entries(drawOptions.colorDots)
|
||||
.map(([i, color]) => `--c${i}:${color};`)
|
||||
.join("")}
|
||||
}
|
||||
` +
|
||||
(drawOptions.dark
|
||||
? `
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--cb: ${drawOptions.dark.colorDotBorder || drawOptions.colorDotBorder};
|
||||
--cs: ${drawOptions.dark.colorSnake || drawOptions.colorSnake};
|
||||
--ce: ${drawOptions.dark.colorEmpty};
|
||||
${Object.entries(drawOptions.dark.colorDots)
|
||||
.map(([i, color]) => `--c${i}:${color};`)
|
||||
.join("")}
|
||||
}
|
||||
}
|
||||
`
|
||||
: "");
|
||||
|
||||
|
||||
/***/ })
|
||||
|
||||
};
|
||||
;
|
||||
5865
svg-only/dist/371.index.js
vendored
Normal file
5865
svg-only/dist/371.index.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
769
svg-only/dist/407.index.js
vendored
Normal file
769
svg-only/dist/407.index.js
vendored
Normal file
@@ -0,0 +1,769 @@
|
||||
"use strict";
|
||||
exports.id = 407;
|
||||
exports.ids = [407];
|
||||
exports.modules = {
|
||||
|
||||
/***/ 407:
|
||||
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
// ESM COMPAT FLAG
|
||||
__webpack_require__.r(__webpack_exports__);
|
||||
|
||||
// EXPORTS
|
||||
__webpack_require__.d(__webpack_exports__, {
|
||||
"generateContributionSnake": () => (/* binding */ generateContributionSnake)
|
||||
});
|
||||
|
||||
;// CONCATENATED MODULE: ../github-user-contribution/index.ts
|
||||
/**
|
||||
* get the contribution grid from a github user page
|
||||
*
|
||||
* use options.from=YYYY-MM-DD options.to=YYYY-MM-DD to get the contribution grid for a specific time range
|
||||
* or year=2019 as an alias for from=2019-01-01 to=2019-12-31
|
||||
*
|
||||
* otherwise return use the time range from today minus one year to today ( as seen in github profile page )
|
||||
*
|
||||
* @param userName github user name
|
||||
* @param options
|
||||
*
|
||||
* @example
|
||||
* getGithubUserContribution("platane", { from: "2019-01-01", to: "2019-12-31" })
|
||||
* getGithubUserContribution("platane", { year: 2019 })
|
||||
*
|
||||
*/
|
||||
const getGithubUserContribution = async (userName, o) => {
|
||||
const query = /* GraphQL */ `
|
||||
query ($login: String!) {
|
||||
user(login: $login) {
|
||||
contributionsCollection {
|
||||
contributionCalendar {
|
||||
weeks {
|
||||
contributionDays {
|
||||
contributionCount
|
||||
contributionLevel
|
||||
weekday
|
||||
date
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const variables = { login: userName };
|
||||
const res = await fetch("https://api.github.com/graphql", {
|
||||
headers: {
|
||||
Authorization: `bearer ${o.githubToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({ variables, query }),
|
||||
});
|
||||
if (!res.ok)
|
||||
throw new Error(res.statusText);
|
||||
const { data, errors } = (await res.json());
|
||||
if (errors?.[0])
|
||||
throw errors[0];
|
||||
return data.user.contributionsCollection.contributionCalendar.weeks.flatMap(({ contributionDays }, x) => contributionDays.map((d) => ({
|
||||
x,
|
||||
y: d.weekday,
|
||||
date: d.date,
|
||||
count: d.contributionCount,
|
||||
level: (d.contributionLevel === "FOURTH_QUARTILE" && 4) ||
|
||||
(d.contributionLevel === "THIRD_QUARTILE" && 3) ||
|
||||
(d.contributionLevel === "SECOND_QUARTILE" && 2) ||
|
||||
(d.contributionLevel === "FIRST_QUARTILE" && 1) ||
|
||||
0,
|
||||
})));
|
||||
};
|
||||
|
||||
// EXTERNAL MODULE: ../types/grid.ts
|
||||
var types_grid = __webpack_require__(2881);
|
||||
;// CONCATENATED MODULE: ./userContributionToGrid.ts
|
||||
|
||||
const userContributionToGrid = (cells) => {
|
||||
const width = Math.max(0, ...cells.map((c) => c.x)) + 1;
|
||||
const height = Math.max(0, ...cells.map((c) => c.y)) + 1;
|
||||
const grid = (0,types_grid/* createEmptyGrid */.u1)(width, height);
|
||||
for (const c of cells) {
|
||||
if (c.level > 0)
|
||||
(0,types_grid/* setColor */.vk)(grid, c.x, c.y, c.level);
|
||||
else
|
||||
(0,types_grid/* setColorEmpty */.Dy)(grid, c.x, c.y);
|
||||
}
|
||||
return grid;
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../types/point.ts
|
||||
const around4 = [
|
||||
{ x: 1, y: 0 },
|
||||
{ x: 0, y: -1 },
|
||||
{ x: -1, y: 0 },
|
||||
{ x: 0, y: 1 },
|
||||
];
|
||||
const pointEquals = (a, b) => a.x === b.x && a.y === b.y;
|
||||
|
||||
;// CONCATENATED MODULE: ../solver/outside.ts
|
||||
|
||||
|
||||
const createOutside = (grid, color = 0) => {
|
||||
const outside = (0,types_grid/* createEmptyGrid */.u1)(grid.width, grid.height);
|
||||
for (let x = outside.width; x--;)
|
||||
for (let y = outside.height; y--;)
|
||||
(0,types_grid/* setColor */.vk)(outside, x, y, 1);
|
||||
fillOutside(outside, grid, color);
|
||||
return outside;
|
||||
};
|
||||
const fillOutside = (outside, grid, color = 0) => {
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (let x = outside.width; x--;)
|
||||
for (let y = outside.height; y--;)
|
||||
if ((0,types_grid/* getColor */.Lq)(grid, x, y) <= color &&
|
||||
!isOutside(outside, x, y) &&
|
||||
around4.some((a) => isOutside(outside, x + a.x, y + a.y))) {
|
||||
changed = true;
|
||||
(0,types_grid/* setColorEmpty */.Dy)(outside, x, y);
|
||||
}
|
||||
}
|
||||
return outside;
|
||||
};
|
||||
const isOutside = (outside, x, y) => !(0,types_grid/* isInside */.V0)(outside, x, y) || (0,types_grid/* isEmpty */.xb)((0,types_grid/* getColor */.Lq)(outside, x, y));
|
||||
|
||||
// EXTERNAL MODULE: ../types/snake.ts
|
||||
var types_snake = __webpack_require__(9347);
|
||||
;// CONCATENATED MODULE: ../solver/utils/sortPush.ts
|
||||
const sortPush = (arr, x, sortFn) => {
|
||||
let a = 0;
|
||||
let b = arr.length;
|
||||
if (arr.length === 0 || sortFn(x, arr[a]) <= 0) {
|
||||
arr.unshift(x);
|
||||
return;
|
||||
}
|
||||
while (b - a > 1) {
|
||||
const e = Math.ceil((a + b) / 2);
|
||||
const s = sortFn(x, arr[e]);
|
||||
if (s === 0)
|
||||
a = b = e;
|
||||
else if (s > 0)
|
||||
a = e;
|
||||
else
|
||||
b = e;
|
||||
}
|
||||
const e = Math.ceil((a + b) / 2);
|
||||
arr.splice(e, 0, x);
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../solver/tunnel.ts
|
||||
|
||||
|
||||
/**
|
||||
* get the sequence of snake to cross the tunnel
|
||||
*/
|
||||
const getTunnelPath = (snake0, tunnel) => {
|
||||
const chain = [];
|
||||
let snake = snake0;
|
||||
for (let i = 1; i < tunnel.length; i++) {
|
||||
const dx = tunnel[i].x - (0,types_snake/* getHeadX */.If)(snake);
|
||||
const dy = tunnel[i].y - (0,types_snake/* getHeadY */.IP)(snake);
|
||||
snake = (0,types_snake/* nextSnake */.kv)(snake, dx, dy);
|
||||
chain.unshift(snake);
|
||||
}
|
||||
return chain;
|
||||
};
|
||||
/**
|
||||
* assuming the grid change and the colors got deleted, update the tunnel
|
||||
*/
|
||||
const updateTunnel = (grid, tunnel, toDelete) => {
|
||||
while (tunnel.length) {
|
||||
const { x, y } = tunnel[0];
|
||||
if (isEmptySafe(grid, x, y) ||
|
||||
toDelete.some((p) => p.x === x && p.y === y)) {
|
||||
tunnel.shift();
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
while (tunnel.length) {
|
||||
const { x, y } = tunnel[tunnel.length - 1];
|
||||
if (isEmptySafe(grid, x, y) ||
|
||||
toDelete.some((p) => p.x === x && p.y === y)) {
|
||||
tunnel.pop();
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
};
|
||||
const isEmptySafe = (grid, x, y) => !(0,types_grid/* isInside */.V0)(grid, x, y) || (0,types_grid/* isEmpty */.xb)((0,types_grid/* getColor */.Lq)(grid, x, y));
|
||||
/**
|
||||
* remove empty cell from start
|
||||
*/
|
||||
const trimTunnelStart = (grid, tunnel) => {
|
||||
while (tunnel.length) {
|
||||
const { x, y } = tunnel[0];
|
||||
if (isEmptySafe(grid, x, y))
|
||||
tunnel.shift();
|
||||
else
|
||||
break;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* remove empty cell from end
|
||||
*/
|
||||
const trimTunnelEnd = (grid, tunnel) => {
|
||||
while (tunnel.length) {
|
||||
const i = tunnel.length - 1;
|
||||
const { x, y } = tunnel[i];
|
||||
if (isEmptySafe(grid, x, y) ||
|
||||
tunnel.findIndex((p) => p.x === x && p.y === y) < i)
|
||||
tunnel.pop();
|
||||
else
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../solver/getBestTunnel.ts
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const getColorSafe = (grid, x, y) => (0,types_grid/* isInside */.V0)(grid, x, y) ? (0,types_grid/* getColor */.Lq)(grid, x, y) : 0;
|
||||
const setEmptySafe = (grid, x, y) => {
|
||||
if ((0,types_grid/* isInside */.V0)(grid, x, y))
|
||||
(0,types_grid/* setColorEmpty */.Dy)(grid, x, y);
|
||||
};
|
||||
const unwrap = (m) => !m
|
||||
? []
|
||||
: [...unwrap(m.parent), { x: (0,types_snake/* getHeadX */.If)(m.snake), y: (0,types_snake/* getHeadY */.IP)(m.snake) }];
|
||||
/**
|
||||
* returns the path to reach the outside which contains the least color cell
|
||||
*/
|
||||
const getSnakeEscapePath = (grid, outside, snake0, color) => {
|
||||
const openList = [{ snake: snake0, w: 0 }];
|
||||
const closeList = [];
|
||||
while (openList[0]) {
|
||||
const o = openList.shift();
|
||||
const x = (0,types_snake/* getHeadX */.If)(o.snake);
|
||||
const y = (0,types_snake/* getHeadY */.IP)(o.snake);
|
||||
if (isOutside(outside, x, y))
|
||||
return unwrap(o);
|
||||
for (const a of around4) {
|
||||
const c = getColorSafe(grid, x + a.x, y + a.y);
|
||||
if (c <= color && !(0,types_snake/* snakeWillSelfCollide */.nJ)(o.snake, a.x, a.y)) {
|
||||
const snake = (0,types_snake/* nextSnake */.kv)(o.snake, a.x, a.y);
|
||||
if (!closeList.some((s0) => (0,types_snake/* snakeEquals */.kE)(s0, snake))) {
|
||||
const w = o.w + 1 + +(c === color) * 1000;
|
||||
sortPush(openList, { snake, w, parent: o }, (a, b) => a.w - b.w);
|
||||
closeList.push(snake);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
/**
|
||||
* compute the best tunnel to get to the cell and back to the outside ( best = less usage of <color> )
|
||||
*
|
||||
* notice that it's one of the best tunnels, more with the same score could exist
|
||||
*/
|
||||
const getBestTunnel = (grid, outside, x, y, color, snakeN) => {
|
||||
const c = { x, y };
|
||||
const snake0 = (0,types_snake/* createSnakeFromCells */.xG)(Array.from({ length: snakeN }, () => c));
|
||||
const one = getSnakeEscapePath(grid, outside, snake0, color);
|
||||
if (!one)
|
||||
return null;
|
||||
// get the position of the snake if it was going to leave the x,y cell
|
||||
const snakeICells = one.slice(0, snakeN);
|
||||
while (snakeICells.length < snakeN)
|
||||
snakeICells.push(snakeICells[snakeICells.length - 1]);
|
||||
const snakeI = (0,types_snake/* createSnakeFromCells */.xG)(snakeICells);
|
||||
// remove from the grid the colors that one eat
|
||||
const gridI = (0,types_grid/* copyGrid */.VJ)(grid);
|
||||
for (const { x, y } of one)
|
||||
setEmptySafe(gridI, x, y);
|
||||
const two = getSnakeEscapePath(gridI, outside, snakeI, color);
|
||||
if (!two)
|
||||
return null;
|
||||
one.shift();
|
||||
one.reverse();
|
||||
one.push(...two);
|
||||
trimTunnelStart(grid, one);
|
||||
trimTunnelEnd(grid, one);
|
||||
return one;
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../solver/getPathTo.ts
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* starting from snake0, get to the cell x,y
|
||||
* return the snake chain (reversed)
|
||||
*/
|
||||
const getPathTo = (grid, snake0, x, y) => {
|
||||
const openList = [{ snake: snake0, w: 0 }];
|
||||
const closeList = [];
|
||||
while (openList.length) {
|
||||
const c = openList.shift();
|
||||
const cx = (0,types_snake/* getHeadX */.If)(c.snake);
|
||||
const cy = (0,types_snake/* getHeadY */.IP)(c.snake);
|
||||
for (let i = 0; i < around4.length; i++) {
|
||||
const { x: dx, y: dy } = around4[i];
|
||||
const nx = cx + dx;
|
||||
const ny = cy + dy;
|
||||
if (nx === x && ny === y) {
|
||||
// unwrap
|
||||
const path = [(0,types_snake/* nextSnake */.kv)(c.snake, dx, dy)];
|
||||
let e = c;
|
||||
while (e.parent) {
|
||||
path.push(e.snake);
|
||||
e = e.parent;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
if ((0,types_grid/* isInsideLarge */.HJ)(grid, 2, nx, ny) &&
|
||||
!(0,types_snake/* snakeWillSelfCollide */.nJ)(c.snake, dx, dy) &&
|
||||
(!(0,types_grid/* isInside */.V0)(grid, nx, ny) || (0,types_grid/* isEmpty */.xb)((0,types_grid/* getColor */.Lq)(grid, nx, ny)))) {
|
||||
const nsnake = (0,types_snake/* nextSnake */.kv)(c.snake, dx, dy);
|
||||
if (!closeList.some((s) => (0,types_snake/* snakeEquals */.kE)(nsnake, s))) {
|
||||
const w = c.w + 1;
|
||||
const h = Math.abs(nx - x) + Math.abs(ny - y);
|
||||
const f = w + h;
|
||||
const o = { snake: nsnake, parent: c, w, h, f };
|
||||
sortPush(openList, o, (a, b) => a.f - b.f);
|
||||
closeList.push(nsnake);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../solver/clearResidualColoredLayer.ts
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const clearResidualColoredLayer = (grid, outside, snake0, color) => {
|
||||
const snakeN = (0,types_snake/* getSnakeLength */.JJ)(snake0);
|
||||
const tunnels = getTunnellablePoints(grid, outside, snakeN, color);
|
||||
// sort
|
||||
tunnels.sort((a, b) => b.priority - a.priority);
|
||||
const chain = [snake0];
|
||||
while (tunnels.length) {
|
||||
// get the best next tunnel
|
||||
let t = getNextTunnel(tunnels, chain[0]);
|
||||
// goes to the start of the tunnel
|
||||
chain.unshift(...getPathTo(grid, chain[0], t[0].x, t[0].y));
|
||||
// goes to the end of the tunnel
|
||||
chain.unshift(...getTunnelPath(chain[0], t));
|
||||
// update grid
|
||||
for (const { x, y } of t)
|
||||
clearResidualColoredLayer_setEmptySafe(grid, x, y);
|
||||
// update outside
|
||||
fillOutside(outside, grid);
|
||||
// update tunnels
|
||||
for (let i = tunnels.length; i--;)
|
||||
if ((0,types_grid/* isEmpty */.xb)((0,types_grid/* getColor */.Lq)(grid, tunnels[i].x, tunnels[i].y)))
|
||||
tunnels.splice(i, 1);
|
||||
else {
|
||||
const t = tunnels[i];
|
||||
const tunnel = getBestTunnel(grid, outside, t.x, t.y, color, snakeN);
|
||||
if (!tunnel)
|
||||
tunnels.splice(i, 1);
|
||||
else {
|
||||
t.tunnel = tunnel;
|
||||
t.priority = getPriority(grid, color, tunnel);
|
||||
}
|
||||
}
|
||||
// re-sort
|
||||
tunnels.sort((a, b) => b.priority - a.priority);
|
||||
}
|
||||
chain.pop();
|
||||
return chain;
|
||||
};
|
||||
const getNextTunnel = (ts, snake) => {
|
||||
let minDistance = Infinity;
|
||||
let closestTunnel = null;
|
||||
const x = (0,types_snake/* getHeadX */.If)(snake);
|
||||
const y = (0,types_snake/* getHeadY */.IP)(snake);
|
||||
const priority = ts[0].priority;
|
||||
for (let i = 0; ts[i] && ts[i].priority === priority; i++) {
|
||||
const t = ts[i].tunnel;
|
||||
const d = distanceSq(t[0].x, t[0].y, x, y);
|
||||
if (d < minDistance) {
|
||||
minDistance = d;
|
||||
closestTunnel = t;
|
||||
}
|
||||
}
|
||||
return closestTunnel;
|
||||
};
|
||||
/**
|
||||
* get all the tunnels for all the cells accessible
|
||||
*/
|
||||
const getTunnellablePoints = (grid, outside, snakeN, color) => {
|
||||
const points = [];
|
||||
for (let x = grid.width; x--;)
|
||||
for (let y = grid.height; y--;) {
|
||||
const c = (0,types_grid/* getColor */.Lq)(grid, x, y);
|
||||
if (!(0,types_grid/* isEmpty */.xb)(c) && c < color) {
|
||||
const tunnel = getBestTunnel(grid, outside, x, y, color, snakeN);
|
||||
if (tunnel) {
|
||||
const priority = getPriority(grid, color, tunnel);
|
||||
points.push({ x, y, priority, tunnel });
|
||||
}
|
||||
}
|
||||
}
|
||||
return points;
|
||||
};
|
||||
/**
|
||||
* get the score of the tunnel
|
||||
* prioritize tunnel with maximum color smaller than <color> and with minimum <color>
|
||||
* with some tweaks
|
||||
*/
|
||||
const getPriority = (grid, color, tunnel) => {
|
||||
let nColor = 0;
|
||||
let nLess = 0;
|
||||
for (let i = 0; i < tunnel.length; i++) {
|
||||
const { x, y } = tunnel[i];
|
||||
const c = clearResidualColoredLayer_getColorSafe(grid, x, y);
|
||||
if (!(0,types_grid/* isEmpty */.xb)(c) && i === tunnel.findIndex((p) => p.x === x && p.y === y)) {
|
||||
if (c === color)
|
||||
nColor += 1;
|
||||
else
|
||||
nLess += color - c;
|
||||
}
|
||||
}
|
||||
if (nColor === 0)
|
||||
return 99999;
|
||||
return nLess / nColor;
|
||||
};
|
||||
const distanceSq = (ax, ay, bx, by) => (ax - bx) ** 2 + (ay - by) ** 2;
|
||||
const clearResidualColoredLayer_getColorSafe = (grid, x, y) => (0,types_grid/* isInside */.V0)(grid, x, y) ? (0,types_grid/* getColor */.Lq)(grid, x, y) : 0;
|
||||
const clearResidualColoredLayer_setEmptySafe = (grid, x, y) => {
|
||||
if ((0,types_grid/* isInside */.V0)(grid, x, y))
|
||||
(0,types_grid/* setColorEmpty */.Dy)(grid, x, y);
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../solver/clearCleanColoredLayer.ts
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const clearCleanColoredLayer = (grid, outside, snake0, color) => {
|
||||
const snakeN = (0,types_snake/* getSnakeLength */.JJ)(snake0);
|
||||
const points = clearCleanColoredLayer_getTunnellablePoints(grid, outside, snakeN, color);
|
||||
const chain = [snake0];
|
||||
while (points.length) {
|
||||
const path = getPathToNextPoint(grid, chain[0], color, points);
|
||||
path.pop();
|
||||
for (const snake of path)
|
||||
clearCleanColoredLayer_setEmptySafe(grid, (0,types_snake/* getHeadX */.If)(snake), (0,types_snake/* getHeadY */.IP)(snake));
|
||||
chain.unshift(...path);
|
||||
}
|
||||
fillOutside(outside, grid);
|
||||
chain.pop();
|
||||
return chain;
|
||||
};
|
||||
const clearCleanColoredLayer_unwrap = (m) => !m ? [] : [m.snake, ...clearCleanColoredLayer_unwrap(m.parent)];
|
||||
const getPathToNextPoint = (grid, snake0, color, points) => {
|
||||
const closeList = [];
|
||||
const openList = [{ snake: snake0 }];
|
||||
while (openList.length) {
|
||||
const o = openList.shift();
|
||||
const x = (0,types_snake/* getHeadX */.If)(o.snake);
|
||||
const y = (0,types_snake/* getHeadY */.IP)(o.snake);
|
||||
const i = points.findIndex((p) => p.x === x && p.y === y);
|
||||
if (i >= 0) {
|
||||
points.splice(i, 1);
|
||||
return clearCleanColoredLayer_unwrap(o);
|
||||
}
|
||||
for (const { x: dx, y: dy } of around4) {
|
||||
if ((0,types_grid/* isInsideLarge */.HJ)(grid, 2, x + dx, y + dy) &&
|
||||
!(0,types_snake/* snakeWillSelfCollide */.nJ)(o.snake, dx, dy) &&
|
||||
clearCleanColoredLayer_getColorSafe(grid, x + dx, y + dy) <= color) {
|
||||
const snake = (0,types_snake/* nextSnake */.kv)(o.snake, dx, dy);
|
||||
if (!closeList.some((s0) => (0,types_snake/* snakeEquals */.kE)(s0, snake))) {
|
||||
closeList.push(snake);
|
||||
openList.push({ snake, parent: o });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
/**
|
||||
* get all cells that are tunnellable
|
||||
*/
|
||||
const clearCleanColoredLayer_getTunnellablePoints = (grid, outside, snakeN, color) => {
|
||||
const points = [];
|
||||
for (let x = grid.width; x--;)
|
||||
for (let y = grid.height; y--;) {
|
||||
const c = (0,types_grid/* getColor */.Lq)(grid, x, y);
|
||||
if (!(0,types_grid/* isEmpty */.xb)(c) &&
|
||||
c <= color &&
|
||||
!points.some((p) => p.x === x && p.y === y)) {
|
||||
const tunnel = getBestTunnel(grid, outside, x, y, color, snakeN);
|
||||
if (tunnel)
|
||||
for (const p of tunnel)
|
||||
if (!clearCleanColoredLayer_isEmptySafe(grid, p.x, p.y))
|
||||
points.push(p);
|
||||
}
|
||||
}
|
||||
return points;
|
||||
};
|
||||
const clearCleanColoredLayer_getColorSafe = (grid, x, y) => (0,types_grid/* isInside */.V0)(grid, x, y) ? (0,types_grid/* getColor */.Lq)(grid, x, y) : 0;
|
||||
const clearCleanColoredLayer_setEmptySafe = (grid, x, y) => {
|
||||
if ((0,types_grid/* isInside */.V0)(grid, x, y))
|
||||
(0,types_grid/* setColorEmpty */.Dy)(grid, x, y);
|
||||
};
|
||||
const clearCleanColoredLayer_isEmptySafe = (grid, x, y) => !(0,types_grid/* isInside */.V0)(grid, x, y) && (0,types_grid/* isEmpty */.xb)((0,types_grid/* getColor */.Lq)(grid, x, y));
|
||||
|
||||
;// CONCATENATED MODULE: ../solver/getBestRoute.ts
|
||||
|
||||
|
||||
|
||||
|
||||
const getBestRoute = (grid0, snake0) => {
|
||||
const grid = (0,types_grid/* copyGrid */.VJ)(grid0);
|
||||
const outside = createOutside(grid);
|
||||
const chain = [snake0];
|
||||
for (const color of extractColors(grid)) {
|
||||
if (color > 1)
|
||||
chain.unshift(...clearResidualColoredLayer(grid, outside, chain[0], color));
|
||||
chain.unshift(...clearCleanColoredLayer(grid, outside, chain[0], color));
|
||||
}
|
||||
return chain.reverse();
|
||||
};
|
||||
const extractColors = (grid) => {
|
||||
// @ts-ignore
|
||||
let maxColor = Math.max(...grid.data);
|
||||
return Array.from({ length: maxColor }, (_, i) => (i + 1));
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../types/__fixtures__/snake.ts
|
||||
|
||||
const create = (length) => (0,types_snake/* createSnakeFromCells */.xG)(Array.from({ length }, (_, i) => ({ x: i, y: -1 })));
|
||||
const snake1 = create(1);
|
||||
const snake3 = create(3);
|
||||
const snake4 = create(4);
|
||||
const snake5 = create(5);
|
||||
const snake9 = create(9);
|
||||
|
||||
;// CONCATENATED MODULE: ../solver/getPathToPose.ts
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const getPathToPose_isEmptySafe = (grid, x, y) => !(0,types_grid/* isInside */.V0)(grid, x, y) || (0,types_grid/* isEmpty */.xb)((0,types_grid/* getColor */.Lq)(grid, x, y));
|
||||
const getPathToPose = (snake0, target, grid) => {
|
||||
if ((0,types_snake/* snakeEquals */.kE)(snake0, target))
|
||||
return [];
|
||||
const targetCells = (0,types_snake/* snakeToCells */.Ks)(target).reverse();
|
||||
const snakeN = (0,types_snake/* getSnakeLength */.JJ)(snake0);
|
||||
const box = {
|
||||
min: {
|
||||
x: Math.min((0,types_snake/* getHeadX */.If)(snake0), (0,types_snake/* getHeadX */.If)(target)) - snakeN - 1,
|
||||
y: Math.min((0,types_snake/* getHeadY */.IP)(snake0), (0,types_snake/* getHeadY */.IP)(target)) - snakeN - 1,
|
||||
},
|
||||
max: {
|
||||
x: Math.max((0,types_snake/* getHeadX */.If)(snake0), (0,types_snake/* getHeadX */.If)(target)) + snakeN + 1,
|
||||
y: Math.max((0,types_snake/* getHeadY */.IP)(snake0), (0,types_snake/* getHeadY */.IP)(target)) + snakeN + 1,
|
||||
},
|
||||
};
|
||||
const [t0, ...forbidden] = targetCells;
|
||||
forbidden.slice(0, 3);
|
||||
const openList = [{ snake: snake0, w: 0 }];
|
||||
const closeList = [];
|
||||
while (openList.length) {
|
||||
const o = openList.shift();
|
||||
const x = (0,types_snake/* getHeadX */.If)(o.snake);
|
||||
const y = (0,types_snake/* getHeadY */.IP)(o.snake);
|
||||
if (x === t0.x && y === t0.y) {
|
||||
const path = [];
|
||||
let e = o;
|
||||
while (e) {
|
||||
path.push(e.snake);
|
||||
e = e.parent;
|
||||
}
|
||||
path.unshift(...getTunnelPath(path[0], targetCells));
|
||||
path.pop();
|
||||
path.reverse();
|
||||
return path;
|
||||
}
|
||||
for (let i = 0; i < around4.length; i++) {
|
||||
const { x: dx, y: dy } = around4[i];
|
||||
const nx = x + dx;
|
||||
const ny = y + dy;
|
||||
if (!(0,types_snake/* snakeWillSelfCollide */.nJ)(o.snake, dx, dy) &&
|
||||
(!grid || getPathToPose_isEmptySafe(grid, nx, ny)) &&
|
||||
(grid
|
||||
? (0,types_grid/* isInsideLarge */.HJ)(grid, 2, nx, ny)
|
||||
: box.min.x <= nx &&
|
||||
nx <= box.max.x &&
|
||||
box.min.y <= ny &&
|
||||
ny <= box.max.y) &&
|
||||
!forbidden.some((p) => p.x === nx && p.y === ny)) {
|
||||
const snake = (0,types_snake/* nextSnake */.kv)(o.snake, dx, dy);
|
||||
if (!closeList.some((s) => (0,types_snake/* snakeEquals */.kE)(snake, s))) {
|
||||
const w = o.w + 1;
|
||||
const h = Math.abs(nx - x) + Math.abs(ny - y);
|
||||
const f = w + h;
|
||||
sortPush(openList, { f, w, snake, parent: o }, (a, b) => a.f - b.f);
|
||||
closeList.push(snake);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ./generateContributionSnake.ts
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const generateContributionSnake = async (userName, outputs, options) => {
|
||||
console.log("🎣 fetching github user contribution");
|
||||
const cells = await getGithubUserContribution(userName, options);
|
||||
const grid = userContributionToGrid(cells);
|
||||
const snake = snake4;
|
||||
console.log("📡 computing best route");
|
||||
const chain = getBestRoute(grid, snake);
|
||||
chain.push(...getPathToPose(chain.slice(-1)[0], snake));
|
||||
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 __webpack_require__.e(/* import() */ 340).then(__webpack_require__.bind(__webpack_require__, 8340));
|
||||
return createSvg(grid, cells, chain, drawOptions, animationOptions);
|
||||
}
|
||||
case "gif": {
|
||||
console.log(`📹 creating gif (outputs[${i}])`);
|
||||
const { createGif } = await Promise.all(/* import() */[__webpack_require__.e(371), __webpack_require__.e(142)]).then(__webpack_require__.bind(__webpack_require__, 7142));
|
||||
return await createGif(grid, cells, chain, drawOptions, animationOptions);
|
||||
}
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 2881:
|
||||
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
||||
/* harmony export */ "Dy": () => (/* binding */ setColorEmpty),
|
||||
/* harmony export */ "HJ": () => (/* binding */ isInsideLarge),
|
||||
/* harmony export */ "Lq": () => (/* binding */ getColor),
|
||||
/* harmony export */ "V0": () => (/* binding */ isInside),
|
||||
/* harmony export */ "VJ": () => (/* binding */ copyGrid),
|
||||
/* harmony export */ "u1": () => (/* binding */ createEmptyGrid),
|
||||
/* harmony export */ "vk": () => (/* binding */ setColor),
|
||||
/* harmony export */ "xb": () => (/* binding */ isEmpty)
|
||||
/* harmony export */ });
|
||||
/* unused harmony exports isGridEmpty, gridEquals */
|
||||
const isInside = (grid, x, y) => x >= 0 && y >= 0 && x < grid.width && y < grid.height;
|
||||
const isInsideLarge = (grid, m, x, y) => x >= -m && y >= -m && x < grid.width + m && y < grid.height + m;
|
||||
const copyGrid = ({ width, height, data }) => ({
|
||||
width,
|
||||
height,
|
||||
data: Uint8Array.from(data),
|
||||
});
|
||||
const getIndex = (grid, x, y) => x * grid.height + y;
|
||||
const getColor = (grid, x, y) => grid.data[getIndex(grid, x, y)];
|
||||
const isEmpty = (color) => color === 0;
|
||||
const setColor = (grid, x, y, color) => {
|
||||
grid.data[getIndex(grid, x, y)] = color || 0;
|
||||
};
|
||||
const setColorEmpty = (grid, x, y) => {
|
||||
setColor(grid, x, y, 0);
|
||||
};
|
||||
/**
|
||||
* return true if the grid is empty
|
||||
*/
|
||||
const isGridEmpty = (grid) => grid.data.every((x) => x === 0);
|
||||
const gridEquals = (a, b) => a.data.every((_, i) => a.data[i] === b.data[i]);
|
||||
const createEmptyGrid = (width, height) => ({
|
||||
width,
|
||||
height,
|
||||
data: new Uint8Array(width * height),
|
||||
});
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 9347:
|
||||
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
||||
/* harmony export */ "IP": () => (/* binding */ getHeadY),
|
||||
/* harmony export */ "If": () => (/* binding */ getHeadX),
|
||||
/* harmony export */ "JJ": () => (/* binding */ getSnakeLength),
|
||||
/* harmony export */ "Ks": () => (/* binding */ snakeToCells),
|
||||
/* harmony export */ "kE": () => (/* binding */ snakeEquals),
|
||||
/* harmony export */ "kv": () => (/* binding */ nextSnake),
|
||||
/* harmony export */ "nJ": () => (/* binding */ snakeWillSelfCollide),
|
||||
/* harmony export */ "xG": () => (/* binding */ createSnakeFromCells)
|
||||
/* harmony export */ });
|
||||
/* unused harmony export copySnake */
|
||||
const getHeadX = (snake) => snake[0] - 2;
|
||||
const getHeadY = (snake) => snake[1] - 2;
|
||||
const getSnakeLength = (snake) => snake.length / 2;
|
||||
const copySnake = (snake) => snake.slice();
|
||||
const snakeEquals = (a, b) => {
|
||||
for (let i = 0; i < a.length; i++)
|
||||
if (a[i] !== b[i])
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
/**
|
||||
* return a copy of the next snake, considering that dx, dy is the direction
|
||||
*/
|
||||
const nextSnake = (snake, dx, dy) => {
|
||||
const copy = new Uint8Array(snake.length);
|
||||
for (let i = 2; i < snake.length; i++)
|
||||
copy[i] = snake[i - 2];
|
||||
copy[0] = snake[0] + dx;
|
||||
copy[1] = snake[1] + dy;
|
||||
return copy;
|
||||
};
|
||||
/**
|
||||
* return true if the next snake will collide with itself
|
||||
*/
|
||||
const snakeWillSelfCollide = (snake, dx, dy) => {
|
||||
const nx = snake[0] + dx;
|
||||
const ny = snake[1] + dy;
|
||||
for (let i = 2; i < snake.length - 2; i += 2)
|
||||
if (snake[i + 0] === nx && snake[i + 1] === ny)
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
const snakeToCells = (snake) => Array.from({ length: snake.length / 2 }, (_, i) => ({
|
||||
x: snake[i * 2 + 0] - 2,
|
||||
y: snake[i * 2 + 1] - 2,
|
||||
}));
|
||||
const createSnakeFromCells = (points) => {
|
||||
const snake = new Uint8Array(points.length * 2);
|
||||
for (let i = points.length; i--;) {
|
||||
snake[i * 2 + 0] = points[i].x + 2;
|
||||
snake[i * 2 + 1] = points[i].y + 2;
|
||||
}
|
||||
return snake;
|
||||
};
|
||||
|
||||
|
||||
/***/ })
|
||||
|
||||
};
|
||||
;
|
||||
50701
svg-only/dist/index.js
vendored
50701
svg-only/dist/index.js
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user