Compare commits

...

54 Commits
v2.1.0 ... main

Author SHA1 Message Date
platane
83033510f0 add test case for color optiion 2025-02-21 08:37:20 +07:00
release bot
a69d1dbca7 📦 3.3.0 2025-02-20 17:07:41 +00:00
platane
f2057e5efe disable cloudflare logging 2025-02-20 23:52:24 +07:00
platane
7fbc58b61d use cloudflare endpoint 2025-02-20 23:51:16 +07:00
platane
1f7630d984 👷 cloudflare deploy ci 2025-02-20 23:48:16 +07:00
platane
852f0ae376 deploy github user contribution endpoint to cloudflare 2025-02-20 23:43:25 +07:00
platane
10c4c3c7bd 🔧 fix manual run 2025-02-20 20:07:36 +07:00
platane
ace186c41f ⬆️ update prettier 2025-02-20 19:58:59 +07:00
platane
79c252356c ⬆️ update typescript 2025-02-20 19:57:25 +07:00
platane
3c171061b3 ⬆️ update tooling dependencies 2025-02-20 19:50:24 +07:00
platane
4783e68ce7 ⬆️ update tooling dependencies 2025-02-20 19:41:00 +07:00
Platane
85da3901f5 use bun as package manager and runner for the docke rcontainer, plus some tweak on the github action 2025-02-20 19:34:18 +07:00
platane
74bc4f0651 ⬆️ update canvas 2025-02-20 18:35:26 +07:00
platane
e55fe1f13c ⬆️ bump dependencies 2024-07-06 11:48:21 +02:00
platane
876448a004 🔨 fix cors issue (2) 2024-07-06 11:47:56 +02:00
platane
d35dc83cf2 🔨 fix cors issue 2024-07-06 11:38:34 +02:00
platane
bb7d69dde8 vercel cache 2024-02-20 16:33:48 +01:00
platane
14a003db51 cache the request from vercel for 6h 2024-02-20 16:28:37 +01:00
platane
2479713155 🚑 2024-02-20 16:23:46 +01:00
platane
debec31440 ⬆️ bump dependencies 2024-02-20 16:15:42 +01:00
platane
5332254423 👷 add manual run 2023-10-19 19:23:20 +02:00
platane
a9052b7ca2 📓 2023-10-17 17:45:17 +02:00
release bot
8b7b3e6ace 📦 3.2.0 2023-10-17 15:44:23 +00:00
platane
92f4de3970 use process.env. instead of @action/core in test and local 2023-10-17 17:30:48 +02:00
Awayume
c9644d3dfa use input instead of env to receive github token
Co-authored-by: Platane <me@platane.me>
2023-10-17 17:26:17 +02:00
platane
01fa6d7aac 🚑 vix vercel endpoint 2023-10-06 10:38:48 +02:00
release bot
b58af55b7d 📦 3.1.0 2023-09-23 18:22:23 +00:00
platane
4e5805f8af ⬆️ use node 20 2023-09-23 20:18:51 +02:00
platane
743771147d ⬆️ 2023-09-23 20:07:23 +02:00
Platane
8eddcbdbea 📓 2023-09-13 20:59:51 +02:00
Alfi Maulana
6f0ace6560 docs: fix indentation of GITHUB_TOKEN env in the README's usage section 2023-07-20 14:02:52 +02:00
platane
835fdd6b84 🚑 fix vercel function 2023-07-17 23:14:15 +02:00
platane
e6034f3972 📓 update readme 2023-07-17 23:04:14 +02:00
release bot
aebc3a9285 📦 3.0.0 2023-07-17 20:57:38 +00:00
platane
1574f65738 📓 update readme 2023-07-17 22:55:37 +02:00
platane
ebeb59fced read contribution calendar from github api 2023-07-17 22:55:37 +02:00
release bot
4489504b7a 📦 2.3.0 2023-07-17 20:37:27 +00:00
platane
027f89563f ⬆️ bump dependencies 2023-07-17 22:34:45 +02:00
platane
7233ec9e15 update contribution parser 2023-07-17 22:20:09 +02:00
platane
54dbbbf73d ♻️ run scripts with npm run vs yarn 2023-07-17 22:13:00 +02:00
Tanmoy
3eed9ce6d6 docs: remove unnecessary whitespace
there is an inconsistency in the whitespace surrounding the URL within the `srcset` attribute, hence we always get the snake in light mode
2023-07-04 01:18:04 +02:00
release bot
3acebc09eb 📦 2.2.1 2023-02-26 09:32:32 +00:00
platane
82417bf9f5 ⬆️ bump ncc 2023-02-26 10:30:34 +01:00
platane
7b6d52d221 ⬆️ bump tooling dependencies 2023-02-26 10:30:34 +01:00
platane
fd133c88c7 remove dark theme media query on default option 2023-02-26 10:21:56 +01:00
platane
229c9a9cd6 ⬆️ bump action dependencies 2023-02-26 10:15:41 +01:00
platane
3803e1ccfa 📓 use picture element to detect dark-mode in readme 2023-02-26 10:15:17 +01:00
platane
8ca289e908 🚑 fix readme lint 2023-02-26 10:00:59 +01:00
Platane
fd7cc1f05a docs(readme): syntax-highlight the darkmode snippet as html 2023-01-18 05:01:25 +01:00
Feng Kaiyu
632fcf6cb7 docs(readme): update the description of dark mode. 2023-01-18 05:01:25 +01:00
platane
e2eb91cf8f allows to select palette in demo 2023-01-06 09:19:20 +01:00
platane
38e2ed4f23 📝 add badges 2023-01-06 08:56:13 +01:00
release bot
b7a9c1e353 📦 2.2.0 2023-01-06 07:36:57 +00:00
platane
a0e08722d9 🚑 adapt the parser to the new github page markup 2023-01-06 08:25:04 +01:00
80 changed files with 29829 additions and 20269 deletions

View File

@@ -7,21 +7,21 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions/setup-node@v2 - uses: oven-sh/setup-bun@v1
with:
cache: yarn
node-version: 16
- run: yarn install --frozen-lockfile
- run: yarn type - run: bun install --frozen-lockfile
- run: yarn lint
- run: yarn test --ci - run: npm run type
- run: npm run lint
- run: bun test
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
test-action: test-action:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: update action.yml to use image from local Dockerfile - name: update action.yml to use image from local Dockerfile
run: | run: |
@@ -44,7 +44,7 @@ jobs:
test -f dist/github-contribution-grid-snake-dark.svg test -f dist/github-contribution-grid-snake-dark.svg
test -f dist/github-contribution-grid-snake.gif test -f dist/github-contribution-grid-snake.gif
- uses: crazy-max/ghaction-github-pages@v3.1.0 - uses: crazy-max/ghaction-github-pages@v4.1.0
with: with:
target_branch: output target_branch: output
build_dir: dist build_dir: dist
@@ -54,16 +54,14 @@ jobs:
test-action-svg-only: test-action-svg-only:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-node@v3 - uses: oven-sh/setup-bun@v1
with:
cache: yarn - run: bun install --frozen-lockfile
node-version: 16
- run: yarn install --frozen-lockfile
- name: build svg-only action - name: build svg-only action
run: | run: |
yarn build:action npm run build:action
rm -r svg-only/dist rm -r svg-only/dist
mv packages/action/dist svg-only/dist mv packages/action/dist svg-only/dist
@@ -75,14 +73,16 @@ jobs:
outputs: | outputs: |
dist/github-contribution-grid-snake.svg dist/github-contribution-grid-snake.svg
dist/github-contribution-grid-snake-dark.svg?palette=github-dark dist/github-contribution-grid-snake-dark.svg?palette=github-dark
dist/github-contribution-grid-snake-blue.svg?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9
- name: ensure the generated file exists - name: ensure the generated file exists
run: | run: |
ls dist ls dist
test -f dist/github-contribution-grid-snake.svg test -f dist/github-contribution-grid-snake.svg
test -f dist/github-contribution-grid-snake-dark.svg test -f dist/github-contribution-grid-snake-dark.svg
test -f dist/github-contribution-grid-snake-blue.svg
- uses: crazy-max/ghaction-github-pages@v3.1.0 - uses: crazy-max/ghaction-github-pages@v4.1.0
with: with:
target_branch: output-svg-only target_branch: output-svg-only
build_dir: dist build_dir: dist
@@ -91,22 +91,28 @@ jobs:
deploy-ghpages: deploy-ghpages:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions/setup-node@v2 - uses: oven-sh/setup-bun@v1
with:
cache: yarn
node-version: 16
- run: yarn install --frozen-lockfile
- run: yarn build:demo - run: bun install --frozen-lockfile
- run: npm run build:demo
env: env:
GITHUB_USER_CONTRIBUTION_API_ENDPOINT: https://snk-one.vercel.app/api/github-user-contribution/ GITHUB_USER_CONTRIBUTION_API_ENDPOINT: https://github-user-contribution.platane.workers.dev/github-user-contribution/
- uses: crazy-max/ghaction-github-pages@v2.6.0 - uses: actions/upload-pages-artifact@v3
with:
path: packages/demo/dist
- uses: actions/deploy-pages@v4
if: success() && github.ref == 'refs/heads/main' if: success() && github.ref == 'refs/heads/main'
with:
target_branch: gh-pages - run: bunx wrangler deploy
build_dir: packages/demo/dist if: success() && github.ref == 'refs/heads/main'
working-directory: packages/github-user-contribution-service
env: env:
GITHUB_TOKEN: ${{ secrets.MY_GITHUB_TOKEN_GH_PAGES }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

47
.github/workflows/manual-run.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: manual run
on:
workflow_dispatch:
jobs:
generate:
permissions:
contents: write
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: Platane/snk/svg-only@v3
with:
github_user_name: ${{ github.repository_owner }}
outputs: |
dist/only-svg/github-contribution-grid-snake.svg
dist/only-svg/github-contribution-grid-snake-dark.svg?palette=github-dark
dist/only-svg/github-contribution-grid-snake-blue.svg?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9
- uses: Platane/snk@v3
with:
github_user_name: ${{ github.repository_owner }}
outputs: |
dist/docker/github-contribution-grid-snake.svg
dist/docker/github-contribution-grid-snake-dark.svg?palette=github-dark
dist/docker/github-contribution-grid-snake.gif?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9
- name: ensure the generated file exists
run: |
ls dist
test -f dist/only-svg/github-contribution-grid-snake.svg
test -f dist/only-svg/github-contribution-grid-snake-dark.svg
test -f dist/only-svg/github-contribution-grid-snake-blue.svg
test -f dist/docker/github-contribution-grid-snake.svg
test -f dist/docker/github-contribution-grid-snake-dark.svg
test -f dist/docker/github-contribution-grid-snake.gif
- name: push github-contribution-grid-snake.svg to the output branch
uses: crazy-max/ghaction-github-pages@v4.1.0
with:
target_branch: manual-run-output
build_dir: dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -21,19 +21,19 @@ jobs:
permissions: permissions:
contents: write contents: write
steps: 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: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: build and publish the docker image - name: build and publish the docker image
uses: docker/build-push-action@v2 uses: docker/build-push-action@v4
id: docker-build id: docker-build
with: with:
push: true push: true
@@ -45,20 +45,18 @@ jobs:
run: | run: |
sed -i "s/image: .*/image: docker:\/\/platane\/snk@${{ steps.docker-build.outputs.digest }}/" action.yml sed -i "s/image: .*/image: docker:\/\/platane\/snk@${{ steps.docker-build.outputs.digest }}/" action.yml
- uses: actions/setup-node@v2 - uses: oven-sh/setup-bun@v1
with:
cache: yarn - run: bun install --frozen-lockfile
node-version: 16
- name: build svg-only action - name: build svg-only action
run: | run: |
yarn install --frozen-lockfile npm run build:action
yarn build:action
rm -r svg-only/dist rm -r svg-only/dist
mv packages/action/dist svg-only/dist mv packages/action/dist svg-only/dist
- name: bump package version - name: bump package version
run: yarn version --no-git-tag-version --new-version ${{ github.event.inputs.version }} run: npm version --no-git-tag-version --new-version ${{ github.event.inputs.version }}
- name: push new build, tag version and push - name: push new build, tag version and push
id: push-tags id: push-tags
@@ -77,13 +75,11 @@ jobs:
git tag v$( echo $VERSION | cut -d. -f 1-2 ) git tag v$( echo $VERSION | cut -d. -f 1-2 )
git push origin --tags --force git push origin --tags --force
echo "prerelease=false" >> $GITHUB_OUTPUT echo "prerelease=false" >> $GITHUB_OUTPUT
else else
echo "prerelease=true" >> $GITHUB_OUTPUT echo "prerelease=true" >> $GITHUB_OUTPUT
fi fi
- uses: ncipollo/release-action@v1.11.1 - uses: ncipollo/release-action@v1.15.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
tag: v${{ github.event.inputs.version }} tag: v${{ github.event.inputs.version }}
body: ${{ github.event.inputs.description }} body: ${{ github.event.inputs.description }}

6
.gitignore vendored
View File

@@ -1,6 +1,8 @@
node_modules node_modules
npm-debug.log* npm-debug.log*
yarn-error.log*
dist dist
!svg-only/dist !svg-only/dist
build build
.env
.wrangler
.dev.vars

2
.nvmrc
View File

@@ -1 +1 @@
16 20

View File

@@ -1,32 +1,27 @@
FROM node:16-slim as builder FROM oven/bun:1.2.2-slim as builder
WORKDIR /app WORKDIR /app
COPY package.json yarn.lock ./ COPY package.json bun.lock ./
COPY tsconfig.json ./ COPY tsconfig.json ./
COPY packages packages COPY packages packages
RUN export YARN_CACHE_FOLDER="$(mktemp -d)" \ RUN bun install --no-cache
&& yarn install --frozen-lockfile \
&& rm -r "$YARN_CACHE_FOLDER"
RUN yarn build:action RUN bun run build:action
FROM node:16-slim FROM oven/bun:1.2.2-slim
WORKDIR /action-release WORKDIR /action-release
RUN export YARN_CACHE_FOLDER="$(mktemp -d)" \ RUN bun add canvas@3.1.0 gifsicle@5.3.0 --no-lockfile --no-cache
&& yarn add canvas@2.10.2 gifsicle@5.3.0 --no-lockfile \
&& rm -r "$YARN_CACHE_FOLDER"
COPY --from=builder /app/packages/action/dist/ /action-release/ COPY --from=builder /app/packages/action/dist/ /action-release/
CMD ["node", "/action-release/index.js"] CMD ["bun", "/action-release/index.js"]

View File

@@ -1,5 +1,6 @@
# snk # snk
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/platane/platane/main.yml?label=action&style=flat-square)](https://github.com/Platane/Platane/actions/workflows/main.yml)
[![GitHub release](https://img.shields.io/github/release/platane/snk.svg?style=flat-square)](https://github.com/platane/snk/releases/latest) [![GitHub release](https://img.shields.io/github/release/platane/snk.svg?style=flat-square)](https://github.com/platane/snk/releases/latest)
[![GitHub marketplace](https://img.shields.io/badge/marketplace-snake-blue?logo=github&style=flat-square)](https://github.com/marketplace/actions/generate-snake-game-from-github-contribution-grid) [![GitHub marketplace](https://img.shields.io/badge/marketplace-snake-blue?logo=github&style=flat-square)](https://github.com/marketplace/actions/generate-snake-game-from-github-contribution-grid)
![type definitions](https://img.shields.io/npm/types/typescript?style=flat-square) ![type definitions](https://img.shields.io/npm/types/typescript?style=flat-square)
@@ -7,7 +8,20 @@
Generates a snake game from a github user contributions graph Generates a snake game from a github user contributions graph
![](https://github.com/Platane/snk/raw/output/github-contribution-grid-snake.svg) <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. 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. Make it a snake Game, generate a snake path where the cells get eaten in an orderly fashion.
@@ -21,7 +35,7 @@ Available as github action. It can automatically generate a new image each day.
**github action** **github action**
```yaml ```yaml
- uses: Platane/snk@v2 - uses: Platane/snk@v3
with: with:
# github user name to read the contribution graph from (**required**) # github user name to read the contribution graph from (**required**)
# using action context var `github.repository_owner` or specified user # using action context var `github.repository_owner` or specified user
@@ -42,17 +56,20 @@ Available as github action. It can automatically generate a new image each day.
dist/ocean.gif?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9 dist/ocean.gif?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9
``` ```
[example with cron job](https://github.com/Platane/Platane/blob/master/.github/workflows/main.yml#L24-L29) [example with cron job](https://github.com/Platane/Platane/blob/master/.github/workflows/main.yml#L26-L33)
If you are only interested in generating a svg, consider using this 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** **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. 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.
```md ```html
![GitHub Snake Light](github-snake.svg#gh-light-mode-only) <picture>
![GitHub Snake dark](github-snake-dark.svg#gh-dark-mode-only) <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** **interactive demo**

View File

@@ -4,15 +4,18 @@ author: "platane"
runs: runs:
using: docker using: docker
image: docker://platane/snk@sha256:89466e404c3d3ba2384e24aabad0542a643eacdc53d0c6320ce369cc1af19d56 image: docker://platane/snk@sha256:96390294299275740e5963058c9784c60c5393b3b8b16082dcf41b240db791f9
inputs: inputs:
github_user_name: github_user_name:
description: "github user name" description: "github user name"
required: true 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: outputs:
required: false required: false
default: null
description: | description: |
list of files to generate. list of files to generate.
one file per line. Each output can be customized with options as query string. one file per line. Each output can be customized with options as query string.

1489
bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +1,22 @@
{ {
"name": "snk", "name": "snk",
"description": "Generates a snake game from a github user contributions grid", "description": "Generates a snake game from a github user contributions grid",
"version": "2.1.0", "version": "3.3.0",
"private": true, "private": true,
"repository": "github:platane/snk", "repository": "github:platane/snk",
"devDependencies": { "devDependencies": {
"@sucrase/jest-plugin": "3.0.0", "@types/bun": "1.2.2",
"@types/jest": "29.2.1", "prettier": "3.5.1",
"@types/node": "16.11.7", "typescript": "5.7.3"
"jest": "29.2.2",
"prettier": "2.7.1",
"sucrase": "3.28.0",
"typescript": "4.8.4"
}, },
"workspaces": [ "workspaces": [
"packages/**" "packages/*"
], ],
"jest": {
"testEnvironment": "node",
"testMatch": [
"**/__tests__/**/?(*.)+(spec|test).ts"
],
"transform": {
"\\.(ts|tsx)$": "@sucrase/jest-plugin"
}
},
"scripts": { "scripts": {
"type": "tsc --noEmit", "type": "tsc --noEmit",
"lint": "yarn prettier -c '**/*.{ts,js,json,md,yml,yaml}' '!packages/*/dist/**' '!svg-only/dist/**'", "lint": "prettier -c '**/*.{ts,js,json,md,yml,yaml}' '!packages/*/dist/**' '!svg-only/dist/**'",
"test": "jest --verbose --passWithNoTests --no-cache", "dev:demo": "( cd packages/demo ; npm run dev )",
"dev:demo": "( cd packages/demo ; yarn dev )", "build:demo": "( cd packages/demo ; npm run build )",
"build:demo": "( cd packages/demo ; yarn build )", "build:action": "( cd packages/action ; npm run build )"
"build:action": "( cd packages/action ; yarn build )"
} }
} }

View File

@@ -1,81 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should parse /out.svg {"color_snake":"yellow"} 1`] = `
{
"animationOptions": {
"frameDuration": 100,
"step": 1,
},
"drawOptions": {
"colorDotBorder": "#1b1f230a",
"colorDots": [
"#ebedf0",
"#9be9a8",
"#40c463",
"#30a14e",
"#216e39",
],
"colorEmpty": "#ebedf0",
"colorSnake": "yellow",
"dark": {
"colorDotBorder": "#1b1f230a",
"colorDots": [
"#161b22",
"#01311f",
"#034525",
"#0f6d31",
"#00c647",
],
"colorEmpty": "#161b22",
"colorSnake": "purple",
},
"sizeCell": 16,
"sizeDot": 12,
"sizeDotBorderRadius": 2,
},
"filename": "/out.svg",
"format": "svg",
}
`;
exports[`should parse /out.svg?.gif.svg?color_snake=orange 1`] = `
{
"animationOptions": {
"frameDuration": 100,
"step": 1,
},
"drawOptions": {
"colorDotBorder": "#1b1f230a",
"colorDots": [
"#ebedf0",
"#9be9a8",
"#40c463",
"#30a14e",
"#216e39",
],
"colorEmpty": "#ebedf0",
"colorSnake": "orange",
"dark": {
"colorDotBorder": "#1b1f230a",
"colorDots": [
"#161b22",
"#01311f",
"#034525",
"#0f6d31",
"#00c647",
],
"colorEmpty": "#161b22",
"colorSnake": "purple",
},
"sizeCell": 16,
"sizeDot": 12,
"sizeDotBorderRadius": 2,
},
"filename": "/out.svg?.gif.svg",
"format": "svg",
}
`;
exports[`should parse /out.svg?{"color_snake":"yellow","color_dots":["#000","#111","#222","#333","#444"]} 1`] = ` exports[`should parse /out.svg?{"color_snake":"yellow","color_dots":["#000","#111","#222","#333","#444"]} 1`] = `
{ {
"animationOptions": { "animationOptions": {
@@ -148,6 +72,7 @@ exports[`should parse /out.svg?color_snake=orange&color_dots=#000,#111,#222,#333
"colorEmpty": "#000", "colorEmpty": "#000",
"colorSnake": "orange", "colorSnake": "orange",
"dark": { "dark": {
"colorDotBorder": "#1b1f230a",
"colorDots": [ "colorDots": [
"#a00", "#a00",
"#a11", "#a11",
@@ -156,6 +81,7 @@ exports[`should parse /out.svg?color_snake=orange&color_dots=#000,#111,#222,#333
"#a44", "#a44",
], ],
"colorEmpty": "#a00", "colorEmpty": "#a00",
"colorSnake": "orange",
}, },
"sizeCell": 16, "sizeCell": 16,
"sizeDot": 12, "sizeDot": 12,
@@ -183,18 +109,7 @@ exports[`should parse path/to/out.gif 1`] = `
], ],
"colorEmpty": "#ebedf0", "colorEmpty": "#ebedf0",
"colorSnake": "purple", "colorSnake": "purple",
"dark": { "dark": undefined,
"colorDotBorder": "#1b1f230a",
"colorDots": [
"#161b22",
"#01311f",
"#034525",
"#0f6d31",
"#00c647",
],
"colorEmpty": "#161b22",
"colorSnake": "purple",
},
"sizeCell": 16, "sizeCell": 16,
"sizeDot": 12, "sizeDot": 12,
"sizeDotBorderRadius": 2, "sizeDotBorderRadius": 2,

View File

@@ -1,10 +1,9 @@
import * as fs from "fs"; import * as fs from "fs";
import * as path from "path"; import * as path from "path";
import { it, expect } from "bun:test";
import { generateContributionSnake } from "../generateContributionSnake"; import { generateContributionSnake } from "../generateContributionSnake";
import { parseOutputsOption } from "../outputsOptions"; import { parseOutputsOption } from "../outputsOptions";
jest.setTimeout(2 * 60 * 1000);
const silent = (handler: () => void | Promise<void>) => async () => { const silent = (handler: () => void | Promise<void>) => async () => {
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
console.log = () => undefined; console.log = () => undefined;
@@ -30,7 +29,9 @@ it(
const outputs = parseOutputsOption(entries); 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[0]).toBeDefined();
expect(results[1]).toBeDefined(); expect(results[1]).toBeDefined();
@@ -39,5 +40,6 @@ it(
fs.writeFileSync(outputs[0]!.filename, results[0]!); fs.writeFileSync(outputs[0]!.filename, results[0]!);
fs.writeFileSync(outputs[1]!.filename, results[1]!); fs.writeFileSync(outputs[1]!.filename, results[1]!);
fs.writeFileSync(outputs[2]!.filename, results[2]!); fs.writeFileSync(outputs[2]!.filename, results[2]!);
}) }),
{ timeout: 2 * 60 * 1000 },
); );

View File

@@ -1,19 +1,61 @@
import { parseEntry } from "../outputsOptions"; import { parseEntry } from "../outputsOptions";
import { it, expect } from "bun:test";
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", "path/to/out.gif",
// overwrite colors (search params)
"/out.svg?color_snake=orange&color_dots=#000,#111,#222,#333,#444", "/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"]}`, `/out.svg?{"color_snake":"yellow","color_dots":["#000","#111","#222","#333","#444"]}`,
`/out.svg {"color_snake":"yellow"}`, // overwrite dark colors
"/out.svg?color_snake=orange&color_dots=#000,#111,#222,#333,#444&dark_color_dots=#a00,#a11,#a22,#a33,#a44", "/out.svg?color_snake=orange&color_dots=#000,#111,#222,#333,#444&dark_color_dots=#a00,#a11,#a22,#a33,#a44",
"/out.svg?.gif.svg?color_snake=orange",
].forEach((entry) => ].forEach((entry) =>
it(`should parse ${entry}`, () => { it(`should parse ${entry}`, () => {
expect(parseEntry(entry)).toMatchSnapshot(); expect(parseEntry(entry)).toMatchSnapshot();
}) }),
); );

View File

@@ -12,10 +12,11 @@ export const generateContributionSnake = async (
format: "svg" | "gif"; format: "svg" | "gif";
drawOptions: DrawOptions; drawOptions: DrawOptions;
animationOptions: AnimationOptions; animationOptions: AnimationOptions;
} | null)[] } | null)[],
options: { githubToken: string },
) => { ) => {
console.log("🎣 fetching github user contribution"); console.log("🎣 fetching github user contribution");
const cells = await getGithubUserContribution(userName); const cells = await getGithubUserContribution(userName, options);
const grid = userContributionToGrid(cells); const grid = userContributionToGrid(cells);
const snake = snake4; const snake = snake4;
@@ -42,10 +43,10 @@ export const generateContributionSnake = async (
cells, cells,
chain, chain,
drawOptions, drawOptions,
animationOptions animationOptions,
); );
} }
} }
}) }),
); );
}; };

View File

@@ -10,13 +10,17 @@ import { parseOutputsOption } from "./outputsOptions";
core.getMultilineInput("outputs") ?? [ core.getMultilineInput("outputs") ?? [
core.getInput("gif_out_path"), core.getInput("gif_out_path"),
core.getInput("svg_out_path"), core.getInput("svg_out_path"),
] ],
); );
const githubToken =
process.env.GITHUB_TOKEN ?? core.getInput("github_token");
const { generateContributionSnake } = await import( const { generateContributionSnake } = await import(
"./generateContributionSnake" "./generateContributionSnake"
); );
const results = await generateContributionSnake(userName, outputs); const results = await generateContributionSnake(userName, outputs, {
githubToken,
});
outputs.forEach((out, i) => { outputs.forEach((out, i) => {
const result = results[i]; const result = results[i];

View File

@@ -32,6 +32,7 @@ export const parseEntry = (entry: string) => {
sizeCell: 16, sizeCell: 16,
sizeDot: 12, sizeDot: 12,
...palettes["default"], ...palettes["default"],
dark: palettes["default"].dark && { ...palettes["default"].dark },
}; };
const animationOptions: AnimationOptions = { step: 1, frameDuration: 100 }; const animationOptions: AnimationOptions = { step: 1, frameDuration: 100 };
@@ -43,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_snake")) drawOptions.colorSnake = sp.get("color_snake")!;
if (sp.has("color_dots")) { if (sp.has("color_dots")) {
const colors = sp.get("color_dots")!.split(/[,;]/); const colors = sp.get("color_dots")!.split(/[,;]/);
@@ -56,6 +65,8 @@ export const parseEntry = (entry: string) => {
if (sp.has("dark_color_dots")) { if (sp.has("dark_color_dots")) {
const colors = sp.get("dark_color_dots")!.split(/[,;]/); const colors = sp.get("dark_color_dots")!.split(/[,;]/);
drawOptions.dark = { drawOptions.dark = {
colorDotBorder: drawOptions.colorDotBorder,
colorSnake: drawOptions.colorSnake,
...drawOptions.dark, ...drawOptions.dark,
colorDots: colors, colorDots: colors,
colorEmpty: colors[0], colorEmpty: colors[0],

View File

@@ -2,7 +2,7 @@
"name": "@snk/action", "name": "@snk/action",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@actions/core": "1.10.0", "@actions/core": "1.11.1",
"@snk/gif-creator": "1.0.0", "@snk/gif-creator": "1.0.0",
"@snk/github-user-contribution": "1.0.0", "@snk/github-user-contribution": "1.0.0",
"@snk/solver": "1.0.0", "@snk/solver": "1.0.0",
@@ -10,10 +10,9 @@
"@snk/types": "1.0.0" "@snk/types": "1.0.0"
}, },
"devDependencies": { "devDependencies": {
"@vercel/ncc": "0.34.0" "@vercel/ncc": "0.38.3"
}, },
"scripts": { "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"
} }
} }

View File

@@ -1,6 +1,6 @@
import { DrawOptions as DrawOptions } from "@snk/svg-creator"; import { DrawOptions as DrawOptions } from "@snk/svg-creator";
export const palettes: Record< export const basePalettes: Record<
string, string,
Pick< Pick<
DrawOptions, DrawOptions,
@@ -22,8 +22,6 @@ export const palettes: Record<
}; };
// aliases // aliases
palettes["github"] = { export const palettes = { ...basePalettes };
...palettes["github-light"], palettes["github"] = palettes["github-light"];
dark: { ...palettes["github-dark"] },
};
palettes["default"] = palettes["github"]; palettes["default"] = palettes["github"];

View File

@@ -76,7 +76,7 @@ export const createCanvas = ({
snake0: Snake, snake0: Snake,
snake1: Snake, snake1: Snake,
stack: Color[], stack: Color[],
k: number k: number,
) => { ) => {
ctx.clearRect(0, 0, 9999, 9999); ctx.clearRect(0, 0, 9999, 9999);
drawLerpWorld(ctx, grid, null, snake0, snake1, stack, k, drawOptions); drawLerpWorld(ctx, grid, null, snake0, snake1, stack, k, drawOptions);

View File

@@ -26,7 +26,7 @@ const tunnels = ones.map(({ x, y }) => ({
x, x,
y, y,
3 as Color, 3 as Color,
getSnakeLength(snake) getSnakeLength(snake),
), ),
})); }));

View File

@@ -8,7 +8,7 @@ const { canvas, ctx, draw, highlightCell } = createCanvas(grid);
canvas.style.pointerEvents = "auto"; canvas.style.pointerEvents = "auto";
const target = createSnakeFromCells( const target = createSnakeFromCells(
snakeToCells(snake).map((p) => ({ ...p, x: p.x - 1 })) snakeToCells(snake).map((p) => ({ ...p, x: p.x - 1 })),
); );
let chain = [snake, ...getPathToPose(snake, target)!]; let chain = [snake, ...getPathToPose(snake, target)!];

View File

@@ -14,6 +14,7 @@ import { createSvg } from "@snk/svg-creator";
import { createRpcClient } from "./worker-utils"; import { createRpcClient } from "./worker-utils";
import type { API as WorkerAPI } from "./demo.interactive.worker"; import type { API as WorkerAPI } from "./demo.interactive.worker";
import { AnimationOptions } from "@snk/gif-creator"; import { AnimationOptions } from "@snk/gif-creator";
import { basePalettes } from "@snk/action/palettes";
const createForm = ({ const createForm = ({
onSubmit, onSubmit,
@@ -119,13 +120,18 @@ const createViewer = ({
grid0, grid0,
chain, chain,
cells, cells,
drawOptions,
}: { }: {
grid0: Grid; grid0: Grid;
chain: Snake[]; chain: Snake[];
cells: Point[]; cells: Point[];
drawOptions: DrawOptions;
}) => { }) => {
const drawOptions: DrawOptions = {
sizeDotBorderRadius: 2,
sizeCell: 16,
sizeDot: 12,
...basePalettes["github-light"],
};
// //
// canvas // canvas
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
@@ -171,12 +177,12 @@ const createViewer = ({
// //
// controls // controls
const input = document.createElement("input") as any; const input = document.createElement("input");
input.type = "range"; input.type = "range";
input.value = 0; input.value = "0";
input.step = 1; input.step = "1";
input.min = 0; input.min = "0";
input.max = chain.length; input.max = "" + chain.length;
input.style.width = "calc( 100% - 20px )"; input.style.width = "calc( 100% - 20px )";
input.addEventListener("input", () => { input.addEventListener("input", () => {
spring.target = +input.value; spring.target = +input.value;
@@ -190,10 +196,49 @@ const createViewer = ({
window.addEventListener("click", onClickBackground); window.addEventListener("click", onClickBackground);
document.body.append(input); 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 // svg
const svgLink = document.createElement("a"); const svgLink = document.createElement("a");
const svgString = createSvg(grid0, cells, chain, drawOptions, { let svgString = createSvg(grid0, cells, chain, drawOptions, {
frameDuration: 100, frameDuration: 100,
} as AnimationOptions); } as AnimationOptions);
const svgImageUri = `data:image/*;charset=utf-8;base64,${btoa(svgString)}`; const svgImageUri = `data:image/*;charset=utf-8;base64,${btoa(svgString)}`;
@@ -203,9 +248,12 @@ const createViewer = ({
svgLink.addEventListener("click", (e) => { svgLink.addEventListener("click", (e) => {
const w = window.open("")!; const w = window.open("")!;
w.document.write( 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 + svgString +
"<a/>" "<a/>",
); );
e.preventDefault(); e.preventDefault();
}); });
@@ -229,35 +277,25 @@ const createViewer = ({
const onSubmit = async (userName: string) => { const onSubmit = async (userName: string) => {
const res = await fetch( const res = await fetch(
process.env.GITHUB_USER_CONTRIBUTION_API_ENDPOINT + userName process.env.GITHUB_USER_CONTRIBUTION_API_ENDPOINT + userName,
); );
const cells = (await res.json()) as Res; const cells = (await res.json()) as Res;
const drawOptions: DrawOptions = {
sizeDotBorderRadius: 2,
sizeCell: 16,
sizeDot: 12,
colorDotBorder: "#1b1f230a",
colorDots: ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"],
colorEmpty: "#ebedf0",
colorSnake: "purple",
};
const grid = userContributionToGrid(cells); const grid = userContributionToGrid(cells);
const chain = await getChain(grid); const chain = await getChain(grid);
dispose(); dispose();
createViewer({ grid0: grid, chain, cells, drawOptions }); createViewer({ grid0: grid, chain, cells });
}; };
const worker = new Worker( const worker = new Worker(
new URL( new URL(
"./demo.interactive.worker.ts", "./demo.interactive.worker.ts",
// @ts-ignore // @ts-ignore
import.meta.url import.meta.url,
) ),
); );
const { getChain } = createRpcClient<WorkerAPI>(worker); const { getChain } = createRpcClient<WorkerAPI>(worker);

View File

@@ -25,7 +25,7 @@ const onChange = () => {
const url = new URL( const url = new URL(
config.demo + ".html?" + search, config.demo + ".html?" + search,
window.location.href window.location.href,
).toString(); ).toString();
window.location.href = url; window.location.href = url;

View File

@@ -10,14 +10,15 @@
"@snk/types": "1.0.0" "@snk/types": "1.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/dat.gui": "0.7.7", "@types/dat.gui": "0.7.13",
"dat.gui": "0.7.9", "dat.gui": "0.7.9",
"html-webpack-plugin": "5.5.0", "dotenv": "16.4.7",
"ts-loader": "9.4.1", "html-webpack-plugin": "5.6.3",
"ts-node": "10.9.1", "ts-loader": "9.5.2",
"webpack": "5.74.0", "ts-node": "10.9.2",
"webpack-cli": "4.10.0", "webpack": "5.98.0",
"webpack-dev-server": "4.11.1" "webpack-cli": "6.0.1",
"webpack-dev-server": "5.2.0"
}, },
"scripts": { "scripts": {
"build": "webpack", "build": "webpack",

View File

@@ -15,7 +15,7 @@ const stepSpringOne = (
maxVelocity = Infinity, maxVelocity = Infinity,
}: { tension: number; friction: number; maxVelocity?: number }, }: { tension: number; friction: number; maxVelocity?: number },
target: number, target: number,
dt = 1 / 60 dt = 1 / 60,
) => { ) => {
const a = -tension * (s.x - target) - friction * s.v; const a = -tension * (s.x - target) - friction * s.v;
@@ -31,13 +31,13 @@ const stepSpringOne = (
export const isStable = ( export const isStable = (
s: { x: number; v: number }, s: { x: number; v: number },
target: number, target: number,
dt = 1 / 60 dt = 1 / 60,
) => Math.abs(s.x - target) < epsilon && Math.abs(s.v * dt) < epsilon; ) => Math.abs(s.x - target) < epsilon && Math.abs(s.v * dt) < epsilon;
export const isStableAndBound = ( export const isStableAndBound = (
s: { x: number; v: number }, s: { x: number; v: number },
target: number, target: number,
dt?: number dt?: number,
) => { ) => {
const stable = isStable(s, target, dt); const stable = isStable(s, target, dt);
if (stable) { if (stable) {
@@ -51,7 +51,7 @@ export const stepSpring = (
s: { x: number; v: number }, s: { x: number; v: number },
params: { tension: number; friction: number; maxVelocity?: number }, params: { tension: number; friction: number; maxVelocity?: number },
target: number, target: number,
dt = 1 / 60 dt = 1 / 60,
) => { ) => {
const interval = 1 / 60; const interval = 1 / 60;

View File

@@ -1,27 +1,40 @@
import path from "path"; import path from "path";
import HtmlWebpackPlugin from "html-webpack-plugin"; 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 webpack from "webpack";
import { getGithubUserContribution } from "@snk/github-user-contribution"; import { getGithubUserContribution } from "@snk/github-user-contribution";
import type { Configuration as WebpackConfiguration } from "webpack";
import {
ExpressRequestHandler,
type Configuration as WebpackDevServerConfiguration,
} from "webpack-dev-server";
import { config } from "dotenv";
config({ path: __dirname + "/../../.env" });
const demos: string[] = require("./demo.json"); const demos: string[] = require("./demo.json");
const webpackDevServerConfiguration: WebpackDevServerConfiguration = { const webpackDevServerConfiguration: WebpackDevServerConfiguration = {
open: { target: demos[1] + ".html" }, open: { target: demos[1] + ".html" },
onAfterSetupMiddleware: ({ app }) => { setupMiddlewares: (ms) => [
app!.get("/api/github-user-contribution/:userName", async (req, res) => { ...ms,
const userName: string = req.params.userName; (async (req, res, next) => {
res.send(await getGithubUserContribution(userName)); const userName = req.url.match(
}); /\/api\/github-user-contribution\/(\w+)/,
}, )?.[1];
if (userName)
res.send(
await getGithubUserContribution(userName, {
githubToken: process.env.GITHUB_TOKEN!,
}),
);
else next();
}) as ExpressRequestHandler,
],
}; };
const webpackConfiguration: WebpackConfiguration = { const webpackConfiguration: WebpackConfiguration = {
mode: "development", mode: "development",
entry: Object.fromEntries( entry: Object.fromEntries(
demos.map((demo: string) => [demo, `./demo.${demo}`]) demos.map((demo: string) => [demo, `./demo.${demo}`]),
), ),
target: ["web", "es2019"], target: ["web", "es2019"],
resolve: { extensions: [".ts", ".js"] }, resolve: { extensions: [".ts", ".js"] },
@@ -52,7 +65,7 @@ const webpackConfiguration: WebpackConfiguration = {
title: "snk - " + demo, title: "snk - " + demo,
filename: `${demo}.html`, filename: `${demo}.html`,
chunks: [demo], chunks: [demo],
}) }),
), ),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
title: "snk - " + demos[0], title: "snk - " + demos[0],

View File

@@ -54,6 +54,6 @@ export const createRpcClient = <API_ extends API>(worker: Worker) => {
worker.addEventListener("terminate", onTerminate); worker.addEventListener("terminate", onTerminate);
worker.postMessage({ symbol, key, methodName, args }); worker.postMessage({ symbol, key, methodName, args });
}), }),
} },
); );
}; };

View File

@@ -59,7 +59,7 @@ export const getCircleSize = (n: number) => {
export const drawCircleStack = ( export const drawCircleStack = (
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
stack: Color[], stack: Color[],
o: Options o: Options,
) => { ) => {
for (let i = stack.length; i--; ) { for (let i = stack.length; i--; ) {
const { x, y } = cellPath[i]; const { x, y } = cellPath[i];
@@ -67,7 +67,7 @@ export const drawCircleStack = (
ctx.save(); ctx.save();
ctx.translate( ctx.translate(
x * o.sizeCell + (o.sizeCell - o.sizeDot) / 2, x * o.sizeCell + (o.sizeCell - o.sizeDot) / 2,
y * o.sizeCell + (o.sizeCell - o.sizeDot) / 2 y * o.sizeCell + (o.sizeCell - o.sizeDot) / 2,
); );
//@ts-ignore //@ts-ignore

View File

@@ -16,7 +16,7 @@ export const drawGrid = (
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
grid: Grid, grid: Grid,
cells: Point[] | null, cells: Point[] | null,
o: Options o: Options,
) => { ) => {
for (let x = grid.width; x--; ) for (let x = grid.width; x--; )
for (let y = grid.height; y--; ) { for (let y = grid.height; y--; ) {
@@ -27,7 +27,7 @@ export const drawGrid = (
ctx.save(); ctx.save();
ctx.translate( ctx.translate(
x * o.sizeCell + (o.sizeCell - o.sizeDot) / 2, x * o.sizeCell + (o.sizeCell - o.sizeDot) / 2,
y * o.sizeCell + (o.sizeCell - o.sizeDot) / 2 y * o.sizeCell + (o.sizeCell - o.sizeDot) / 2,
); );
ctx.fillStyle = color; ctx.fillStyle = color;

View File

@@ -10,7 +10,7 @@ type Options = {
export const drawSnake = ( export const drawSnake = (
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
snake: Snake, snake: Snake,
o: Options o: Options,
) => { ) => {
const cells = snakeToCells(snake); const cells = snakeToCells(snake);
@@ -25,7 +25,7 @@ export const drawSnake = (
ctx, ctx,
o.sizeCell - u * 2, o.sizeCell - u * 2,
o.sizeCell - u * 2, o.sizeCell - u * 2,
(o.sizeCell - u * 2) * 0.25 (o.sizeCell - u * 2) * 0.25,
); );
ctx.fill(); ctx.fill();
ctx.restore(); ctx.restore();
@@ -40,7 +40,7 @@ export const drawSnakeLerp = (
snake0: Snake, snake0: Snake,
snake1: Snake, snake1: Snake,
k: number, k: number,
o: Options o: Options,
) => { ) => {
const m = 0.8; const m = 0.8;
const n = snake0.length / 2; const n = snake0.length / 2;
@@ -61,7 +61,7 @@ export const drawSnakeLerp = (
ctx, ctx,
o.sizeCell - u * 2, o.sizeCell - u * 2,
o.sizeCell - u * 2, o.sizeCell - u * 2,
(o.sizeCell - u * 2) * 0.25 (o.sizeCell - u * 2) * 0.25,
); );
ctx.fill(); ctx.fill();
ctx.restore(); ctx.restore();

View File

@@ -19,7 +19,7 @@ export const drawStack = (
stack: Color[], stack: Color[],
max: number, max: number,
width: number, width: number,
o: { colorDots: Record<Color, string> } o: { colorDots: Record<Color, string> },
) => { ) => {
ctx.save(); ctx.save();
@@ -39,7 +39,7 @@ export const drawWorld = (
cells: Point[] | null, cells: Point[] | null,
snake: Snake, snake: Snake,
stack: Color[], stack: Color[],
o: Options o: Options,
) => { ) => {
ctx.save(); ctx.save();
@@ -66,14 +66,14 @@ export const drawWorld = (
}; };
export const drawLerpWorld = ( export const drawLerpWorld = (
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D | CanvasRenderingContext2D,
grid: Grid, grid: Grid,
cells: Point[] | null, cells: Point[] | null,
snake0: Snake, snake0: Snake,
snake1: Snake, snake1: Snake,
stack: Color[], stack: Color[],
k: number, k: number,
o: Options o: Options,
) => { ) => {
ctx.save(); ctx.save();

View File

@@ -2,7 +2,7 @@ export const pathRoundedRect = (
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
width: number, width: number,
height: number, height: number,
borderRadius: number borderRadius: number,
) => { ) => {
ctx.moveTo(borderRadius, 0); ctx.moveTo(borderRadius, 0);
ctx.arcTo(width, 0, width, height, borderRadius); ctx.arcTo(width, 0, width, height, borderRadius);

View File

@@ -8,7 +8,7 @@ import { getPathToPose } from "@snk/solver/getPathToPose";
import type { Options as DrawOptions } from "@snk/draw/drawWorld"; import type { Options as DrawOptions } from "@snk/draw/drawWorld";
let snake = createSnakeFromCells( let snake = createSnakeFromCells(
Array.from({ length: 4 }, (_, i) => ({ x: i, y: -1 })) Array.from({ length: 4 }, (_, i) => ({ x: i, y: -1 })),
); );
// const chain = [snake]; // const chain = [snake];
@@ -45,7 +45,7 @@ const animationOptions: AnimationOptions = { frameDuration: 100, step: 1 };
) { ) {
const stats: number[] = []; const stats: number[] = [];
let buffer: Buffer; let buffer: Uint8Array;
const start = Date.now(); const start = Date.now();
const chainL = chain.slice(0, length); const chainL = chain.slice(0, length);
for (let k = 0; k < 10 && (Date.now() - start < 10 * 1000 || k < 2); k++) { for (let k = 0; k < 10 && (Date.now() - start < 10 * 1000 || k < 2); k++) {
@@ -55,7 +55,7 @@ const animationOptions: AnimationOptions = { frameDuration: 100, step: 1 };
null, null,
chainL, chainL,
drawOptions, drawOptions,
animationOptions animationOptions,
); );
stats.push(performance.now() - s); stats.push(performance.now() - s);
} }
@@ -73,12 +73,12 @@ const animationOptions: AnimationOptions = { frameDuration: 100, step: 1 };
})}ms`, })}ms`,
"", "",
].join("\n"), ].join("\n"),
stats stats,
); );
fs.writeFileSync( fs.writeFileSync(
`__tests__/__snapshots__/benchmark-output-${length}.gif`, `__tests__/__snapshots__/benchmark-output-${length}.gif`,
buffer! buffer!,
); );
} }
})(); })();

View File

@@ -1,5 +1,6 @@
import * as fs from "fs"; import * as fs from "fs";
import * as path from "path"; import * as path from "path";
import { it, expect } from "bun:test";
import { AnimationOptions, createGif } from ".."; import { AnimationOptions, createGif } from "..";
import * as grids from "@snk/types/__fixtures__/grid"; import * as grids from "@snk/types/__fixtures__/grid";
import { snake3 as snake } from "@snk/types/__fixtures__/snake"; import { snake3 as snake } from "@snk/types/__fixtures__/snake";
@@ -7,8 +8,6 @@ import { createSnakeFromCells, nextSnake } from "@snk/types/snake";
import { getBestRoute } from "@snk/solver/getBestRoute"; import { getBestRoute } from "@snk/solver/getBestRoute";
import type { Options as DrawOptions } from "@snk/draw/drawWorld"; import type { Options as DrawOptions } from "@snk/draw/drawWorld";
jest.setTimeout(20 * 1000);
const upscale = 1; const upscale = 1;
const drawOptions: DrawOptions = { const drawOptions: DrawOptions = {
sizeDotBorderRadius: 2 * upscale, sizeDotBorderRadius: 2 * upscale,
@@ -35,44 +34,58 @@ for (const key of [
"small", "small",
"smallPacked", "smallPacked",
] as const) ] as const)
it(`should generate ${key} gif`, async () => { it(
const grid = grids[key]; `should generate ${key} gif`,
async () => {
const grid = grids[key];
const chain = [snake, ...getBestRoute(grid, snake)!]; const chain = [snake, ...getBestRoute(grid, snake)!];
const gif = await createGif(
grid,
null,
chain,
drawOptions,
animationOptions,
);
expect(gif).toBeDefined();
fs.writeFileSync(path.resolve(dir, key + ".gif"), gif);
},
{ timeout: 20 * 1000 },
);
it(
`should generate swipper`,
async () => {
const grid = grids.smallFull;
let snk = createSnakeFromCells(
Array.from({ length: 6 }, (_, i) => ({ x: i, y: -1 })),
);
const chain = [snk];
for (let y = -1; y < grid.height; y++) {
snk = nextSnake(snk, 0, 1);
chain.push(snk);
for (let x = grid.width - 1; x--; ) {
snk = nextSnake(snk, (y + 100) % 2 ? 1 : -1, 0);
chain.push(snk);
}
}
const gif = await createGif( const gif = await createGif(
grid, grid,
null, null,
chain, chain,
drawOptions, drawOptions,
animationOptions animationOptions,
); );
expect(gif).toBeDefined(); expect(gif).toBeDefined();
fs.writeFileSync(path.resolve(dir, key + ".gif"), gif); fs.writeFileSync(path.resolve(dir, "swipper.gif"), gif);
}); },
{ timeout: 20 * 1000 },
it(`should generate swipper`, async () => { );
const grid = grids.smallFull;
let snk = createSnakeFromCells(
Array.from({ length: 6 }, (_, i) => ({ x: i, y: -1 }))
);
const chain = [snk];
for (let y = -1; y < grid.height; y++) {
snk = nextSnake(snk, 0, 1);
chain.push(snk);
for (let x = grid.width - 1; x--; ) {
snk = nextSnake(snk, (y + 100) % 2 ? 1 : -1, 0);
chain.push(snk);
}
}
const gif = await createGif(grid, null, chain, drawOptions, animationOptions);
expect(gif).toBeDefined();
fs.writeFileSync(path.resolve(dir, "swipper.gif"), gif);
});

View File

@@ -17,7 +17,7 @@ import gifsicle from "gifsicle";
import GIFEncoder from "gif-encoder-2"; import GIFEncoder from "gif-encoder-2";
const withTmpDir = async <T>( const withTmpDir = async <T>(
handler: (dir: string) => Promise<T> handler: (dir: string) => Promise<T>,
): Promise<T> => { ): Promise<T> => {
const { name: dir, removeCallback: cleanUp } = tmp.dirSync({ const { name: dir, removeCallback: cleanUp } = tmp.dirSync({
unsafeCleanup: true, unsafeCleanup: true,
@@ -37,13 +37,13 @@ export const createGif = async (
cells: Point[] | null, cells: Point[] | null,
chain: Snake[], chain: Snake[],
drawOptions: DrawOptions, drawOptions: DrawOptions,
animationOptions: AnimationOptions animationOptions: AnimationOptions,
) => ) =>
withTmpDir(async (dir) => { withTmpDir(async (dir) => {
const { width, height } = getCanvasWorldSize(grid0, drawOptions); const { width, height } = getCanvasWorldSize(grid0, drawOptions);
const canvas = createCanvas(width, height); const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d")!; const ctx = canvas.getContext("2d") as any as CanvasRenderingContext2D;
const grid = copyGrid(grid0); const grid = copyGrid(grid0);
const stack: Color[] = []; const stack: Color[] = [];
@@ -70,7 +70,7 @@ export const createGif = async (
snake1, snake1,
stack, stack,
k / animationOptions.step, k / animationOptions.step,
drawOptions drawOptions,
); );
encoder.addFrame(ctx); encoder.addFrame(ctx);
@@ -92,8 +92,8 @@ export const createGif = async (
"--colors=18", "--colors=18",
outFileName, outFileName,
["--output", optimizedFileName], ["--output", optimizedFileName],
].flat() ].flat(),
); );
return fs.readFileSync(optimizedFileName); return new Uint8Array(fs.readFileSync(optimizedFileName));
}); });

View File

@@ -4,17 +4,16 @@
"dependencies": { "dependencies": {
"@snk/draw": "1.0.0", "@snk/draw": "1.0.0",
"@snk/solver": "1.0.0", "@snk/solver": "1.0.0",
"canvas": "2.10.2", "canvas": "3.1.0",
"gif-encoder-2": "1.0.5", "gif-encoder-2": "1.0.5",
"gifsicle": "5.3.0", "gifsicle": "5.3.0",
"tmp": "0.2.1" "tmp": "0.2.3"
}, },
"devDependencies": { "devDependencies": {
"@types/gifsicle": "5.2.0", "@types/gifsicle": "5.2.2",
"@types/tmp": "0.2.3", "@types/tmp": "0.2.6"
"@vercel/ncc": "0.34.0"
}, },
"scripts": { "scripts": {
"benchmark": "ncc run __tests__/benchmark.ts --quiet" "benchmark": "bun __tests__/benchmark.ts"
} }
} }

View File

@@ -1,3 +1,14 @@
# @snk/github-user-contribution-service # @snk/github-user-contribution-service
Expose github-user-contribution as an endpoint, using vercel.sh Expose github-user-contribution as an endpoint. hosted on cloudflare
```sh
# deploy
bunx wrangler deploy --branch=production
# change secret
bunx wrangler secret put GITHUB_TOKEN
```

View File

@@ -1,16 +0,0 @@
import { getGithubUserContribution } from "@snk/github-user-contribution";
import { VercelRequest, VercelResponse } from "@vercel/node";
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));
} catch (err) {
console.error(err);
res.statusCode = 500;
res.end();
}
};

View File

@@ -0,0 +1,52 @@
import { getGithubUserContribution } from "@snk/github-user-contribution";
const cors =
<
Req extends { headers: Headers },
Res extends { headers: Headers },
A extends Array<any>,
>(
f: (req: Req, ...args: A) => Res | Promise<Res>,
) =>
async (req: Req, ...args: A) => {
const res = await f(req, ...args);
const origin = req.headers.get("origin");
if (origin) {
const { host, hostname } = new URL(origin);
if (hostname === "localhost" || host === "platane.github.io")
res.headers.set("Access-Control-Allow-Origin", origin);
}
res.headers.set("Access-Control-Allow-Methods", "GET, OPTIONS");
res.headers.set("Access-Control-Allow-Headers", "Content-Type");
return res;
};
export default {
fetch: cors(async (req: Request, env: { GITHUB_TOKEN: string }) => {
const url = new URL(req.url);
const [, userName] =
url.pathname.match(/^\/github-user-contribution\/([^\/]*)\/?$/) ?? [];
if (req.method === "OPTIONS") return new Response();
if (!userName || req.method !== "GET")
return new Response("unknown route", { status: 404 });
const body = await getGithubUserContribution(userName, {
githubToken: env.GITHUB_TOKEN,
});
return new Response(JSON.stringify(body), {
status: 200,
headers: {
"Cache-Control": "max-age=21600, s-maxage=21600",
"Content-Type": "application/json",
},
});
}),
};

View File

@@ -2,7 +2,13 @@
"name": "@snk/github-user-contribution-service", "name": "@snk/github-user-contribution-service",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@snk/github-user-contribution": "1.0.0", "@snk/github-user-contribution": "1.0.0"
"@vercel/node": "2.6.1" },
"devDependencies": {
"wrangler": "3.109.2",
"@cloudflare/workers-types": "4.20250214.0"
},
"scripts": {
"deploy": "wrangler deploy"
} }
} }

View File

@@ -1,5 +0,0 @@
{
"github": {
"silent": true
}
}

View File

@@ -0,0 +1,9 @@
name = "github-user-contribution"
main = "index.ts"
compatibility_date = "2024-09-02"
account_id = "56268cde636c288343cb0767952ecf2e"
workers_dev = true
# [observability]
# enabled = true

View File

@@ -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);
});

View File

@@ -1,7 +1,10 @@
import { getGithubUserContribution } from ".."; import { getGithubUserContribution } from "..";
import { describe, it, expect } from "bun:test";
describe("getGithubUserContribution", () => { describe("getGithubUserContribution", () => {
const promise = getGithubUserContribution("platane"); const promise = getGithubUserContribution("platane", {
githubToken: process.env.GITHUB_TOKEN!,
});
it("should resolve", async () => { it("should resolve", async () => {
await promise; await promise;
@@ -27,9 +30,3 @@ describe("getGithubUserContribution", () => {
expect(undefinedDays).toEqual([]); expect(undefinedDays).toEqual([]);
}); });
}); });
xit("should match snapshot for year=2019", async () => {
expect(
await getGithubUserContribution("platane", { year: 2019 })
).toMatchSnapshot();
});

View File

@@ -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("-");
};

View File

@@ -1,6 +1,3 @@
import fetch from "node-fetch";
import { formatParams, Options } from "./formatParams";
/** /**
* get the contribution grid from a github user page * get the contribution grid from a github user page
* *
@@ -19,57 +16,84 @@ import { formatParams, Options } from "./formatParams";
*/ */
export const getGithubUserContribution = async ( export const getGithubUserContribution = async (
userName: string, userName: string,
options: Options = {} o: { githubToken: string },
) => { ) => {
// either use github.com/users/xxxx/contributions for previous years const query = /* GraphQL */ `
// or github.com/xxxx ( which gives the latest update to today result ) query ($login: String!) {
const url = user(login: $login) {
"year" in options || "from" in options || "to" in options contributionsCollection {
? `https://github.com/users/${userName}/contributions?` + contributionCalendar {
formatParams(options) weeks {
: `https://github.com/${userName}`; contributionDays {
contributionCount
contributionLevel
weekday
date
}
}
}
}
}
}
`;
const variables = { login: userName };
const res = await fetch(url); const res = await fetch("https://api.github.com/graphql", {
headers: {
if (!res.ok) throw new Error(res.statusText); Authorization: `bearer ${o.githubToken}`,
"Content-Type": "application/json",
const resText = await res.text(); "User-Agent": "me@platane.me",
},
return parseUserPage(resText); method: "POST",
}; body: JSON.stringify({ variables, query }),
const parseUserPage = (content: string) => {
// take roughly the svg block
const block = content
.split(`class="js-calendar-graph-svg"`)[1]
.split("</svg>")[0];
let x = 0;
let lastYAttribute = 0;
const rects = Array.from(block.matchAll(/<rect[^>]*>/g)).map(([m]) => {
const date = m.match(/data-date="([^"]+)"/)![1];
const count = +m.match(/data-count="([^"]+)"/)![1];
const level = +m.match(/data-level="([^"]+)"/)![1];
const yAttribute = +m.match(/y="([^"]+)"/)![1];
if (lastYAttribute > yAttribute) x++;
lastYAttribute = yAttribute;
return { date, count, level, x, yAttribute };
}); });
const yAttributes = Array.from( if (!res.ok) throw new Error(await res.text().catch(() => res.statusText));
new Set(rects.map((c) => c.yAttribute)).keys()
).sort();
const cells = rects.map(({ yAttribute, ...c }) => ({ const { data, errors } = (await res.json()) as {
y: yAttributes.indexOf(yAttribute), data: GraphQLRes;
...c, errors?: { message: string }[];
})); };
return cells; 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,
})),
);
};
type GraphQLRes = {
user: {
contributionsCollection: {
contributionCalendar: {
weeks: {
contributionDays: {
contributionCount: number;
contributionLevel:
| "FOURTH_QUARTILE"
| "THIRD_QUARTILE"
| "SECOND_QUARTILE"
| "FIRST_QUARTILE"
| "NONE";
date: string;
weekday: number;
}[];
}[];
};
};
};
}; };
export type Res = Awaited<ReturnType<typeof getGithubUserContribution>>; export type Res = Awaited<ReturnType<typeof getGithubUserContribution>>;

View File

@@ -1,11 +1,4 @@
{ {
"name": "@snk/github-user-contribution", "name": "@snk/github-user-contribution",
"version": "1.0.0", "version": "1.0.0"
"dependencies": {
"cheerio": "1.0.0-rc.10",
"node-fetch": "2.6.7"
},
"devDependencies": {
"@types/node-fetch": "2.6.1"
}
} }

View File

@@ -1,3 +1,4 @@
import { it, expect } from "bun:test";
import { getBestRoute } from "../getBestRoute"; import { getBestRoute } from "../getBestRoute";
import { snake3, snake4 } from "@snk/types/__fixtures__/snake"; import { snake3, snake4 } from "@snk/types/__fixtures__/snake";
import { import {
@@ -16,7 +17,7 @@ for (const { width, height, snake } of [
{ width: 5, height: 5, snake: snake4 }, { width: 5, height: 5, snake: snake4 },
]) ])
it(`should find solution for ${n} ${width}x${height} generated grids for ${getSnakeLength( it(`should find solution for ${n} ${width}x${height} generated grids for ${getSnakeLength(
snake snake,
)} length snake`, () => { )} length snake`, () => {
const results = Array.from({ length: n }, (_, seed) => { const results = Array.from({ length: n }, (_, seed) => {
const grid = createFromSeed(seed, width, height); const grid = createFromSeed(seed, width, height);

View File

@@ -1,3 +1,4 @@
import { it, expect } from "bun:test";
import { getBestRoute } from "../getBestRoute"; import { getBestRoute } from "../getBestRoute";
import { Color, createEmptyGrid, setColor } from "@snk/types/grid"; import { Color, createEmptyGrid, setColor } from "@snk/types/grid";
import { createSnakeFromCells, snakeToCells } from "@snk/types/snake"; import { createSnakeFromCells, snakeToCells } from "@snk/types/snake";

View File

@@ -1,3 +1,4 @@
import { it, expect } from "bun:test";
import { createEmptyGrid } from "@snk/types/grid"; import { createEmptyGrid } from "@snk/types/grid";
import { getHeadX, getHeadY } from "@snk/types/snake"; import { getHeadX, getHeadY } from "@snk/types/snake";
import { snake3 } from "@snk/types/__fixtures__/snake"; import { snake3 } from "@snk/types/__fixtures__/snake";

View File

@@ -1,3 +1,4 @@
import { it, expect } from "bun:test";
import { createSnakeFromCells } from "@snk/types/snake"; import { createSnakeFromCells } from "@snk/types/snake";
import { getPathToPose } from "../getPathToPose"; import { getPathToPose } from "../getPathToPose";

View File

@@ -1,3 +1,4 @@
import { it, expect, describe } from "bun:test";
import { sortPush } from "../utils/sortPush"; import { sortPush } from "../utils/sortPush";
const sortFn = (a: number, b: number) => a - b; const sortFn = (a: number, b: number) => a - b;

View File

@@ -24,7 +24,7 @@ export const clearCleanColoredLayer = (
grid: Grid, grid: Grid,
outside: Outside, outside: Outside,
snake0: Snake, snake0: Snake,
color: Color color: Color,
) => { ) => {
const snakeN = getSnakeLength(snake0); const snakeN = getSnakeLength(snake0);
@@ -55,7 +55,7 @@ const getPathToNextPoint = (
grid: Grid, grid: Grid,
snake0: Snake, snake0: Snake,
color: Color, color: Color,
points: Point[] points: Point[],
) => { ) => {
const closeList: Snake[] = []; const closeList: Snake[] = [];
const openList: M[] = [{ snake: snake0 } as any]; const openList: M[] = [{ snake: snake0 } as any];
@@ -96,7 +96,7 @@ export const getTunnellablePoints = (
grid: Grid, grid: Grid,
outside: Outside, outside: Outside,
snakeN: number, snakeN: number,
color: Color color: Color,
) => { ) => {
const points: Point[] = []; const points: Point[] = [];

View File

@@ -20,7 +20,7 @@ export const clearResidualColoredLayer = (
grid: Grid, grid: Grid,
outside: Outside, outside: Outside,
snake0: Snake, snake0: Snake,
color: Color color: Color,
) => { ) => {
const snakeN = getSnakeLength(snake0); const snakeN = getSnakeLength(snake0);
@@ -99,7 +99,7 @@ export const getTunnellablePoints = (
grid: Grid, grid: Grid,
outside: Outside, outside: Outside,
snakeN: number, snakeN: number,
color: Color color: Color,
) => { ) => {
const points: T[] = []; const points: T[] = [];

View File

@@ -13,7 +13,7 @@ export const getBestRoute = (grid0: Grid, snake0: Snake) => {
for (const color of extractColors(grid)) { for (const color of extractColors(grid)) {
if (color > 1) if (color > 1)
chain.unshift( chain.unshift(
...clearResidualColoredLayer(grid, outside, chain[0], color) ...clearResidualColoredLayer(grid, outside, chain[0], color),
); );
chain.unshift(...clearCleanColoredLayer(grid, outside, chain[0], color)); chain.unshift(...clearCleanColoredLayer(grid, outside, chain[0], color));
} }

View File

@@ -37,7 +37,7 @@ const getSnakeEscapePath = (
grid: Grid, grid: Grid,
outside: Outside, outside: Outside,
snake0: Snake, snake0: Snake,
color: Color color: Color,
) => { ) => {
const openList: M[] = [{ snake: snake0, w: 0 } as any]; const openList: M[] = [{ snake: snake0, w: 0 } as any];
const closeList: Snake[] = []; const closeList: Snake[] = [];
@@ -79,7 +79,7 @@ export const getBestTunnel = (
x: number, x: number,
y: number, y: number,
color: Color, color: Color,
snakeN: number snakeN: number,
) => { ) => {
const c = { x, y }; const c = { x, y };
const snake0 = createSnakeFromCells(Array.from({ length: snakeN }, () => c)); const snake0 = createSnakeFromCells(Array.from({ length: snakeN }, () => c));

View File

@@ -24,7 +24,7 @@ export const createOutside = (grid: Grid, color: Color = 0 as Color) => {
export const fillOutside = ( export const fillOutside = (
outside: Outside, outside: Outside,
grid: Grid, grid: Grid,
color: Color = 0 as Color color: Color = 0 as Color,
) => { ) => {
let changed = true; let changed = true;
while (changed) { while (changed) {

View File

@@ -27,7 +27,7 @@ export const getTunnelPath = (snake0: Snake, tunnel: Point[]) => {
export const updateTunnel = ( export const updateTunnel = (
grid: Grid, grid: Grid,
tunnel: Point[], tunnel: Point[],
toDelete: Point[] toDelete: Point[],
) => { ) => {
while (tunnel.length) { while (tunnel.length) {
const { x, y } = tunnel[0]; const { x, y } = tunnel[0];

View File

@@ -1,3 +1,4 @@
import { it, expect } from "bun:test";
import * as fs from "fs"; import * as fs from "fs";
import * as path from "path"; import * as path from "path";
import { createSvg, DrawOptions as DrawOptions } from ".."; import { createSvg, DrawOptions as DrawOptions } from "..";
@@ -37,7 +38,7 @@ for (const [key, grid] of Object.entries(grids))
null, null,
chain, chain,
drawOptions, drawOptions,
animationOptions animationOptions,
); );
expect(svg).toBeDefined(); expect(svg).toBeDefined();

View File

@@ -1,3 +1,4 @@
import { it, expect } from "bun:test";
import { minifyCss } from "../css-utils"; import { minifyCss } from "../css-utils";
it("should minify css", () => { it("should minify css", () => {
@@ -6,8 +7,8 @@ it("should minify css", () => {
.c { .c {
color : red ; color : red ;
} }
`) `),
).toBe(".c{color:red}"); ).toBe(".c{color:red}");
expect( expect(
@@ -17,10 +18,10 @@ it("should minify css", () => {
color : red ; color : red ;
} }
# { # {
animation: linear 10; animation: linear 10;
} }
`) `),
).toBe(".c{top:0;color:red}#{animation:linear 10}"); ).toBe(".c{top:0;color:red}#{animation:linear 10}");
}); });

View File

@@ -16,7 +16,7 @@ const mergeKeyFrames = (keyframes: { t: number; style: string }[]) => {
*/ */
export const createAnimation = ( export const createAnimation = (
name: string, name: string,
keyframes: { t: number; style: string }[] keyframes: { t: number; style: string }[],
) => ) =>
`@keyframes ${name}{` + `@keyframes ${name}{` +
mergeKeyFrames(keyframes) mergeKeyFrames(keyframes)

View File

@@ -15,7 +15,7 @@ export type Options = {
export const createGrid = ( export const createGrid = (
cells: (Point & { t: number | null; color: Color | Empty })[], cells: (Point & { t: number | null; color: Color | Empty })[],
{ sizeDotBorderRadius, sizeDot, sizeCell }: Options, { sizeDotBorderRadius, sizeDot, sizeCell }: Options,
duration: number duration: number,
) => { ) => {
const svgElements: string[] = []; const svgElements: string[] = [];
const styles = [ const styles = [
@@ -48,7 +48,7 @@ export const createGrid = (
`.c.${id}{ `.c.${id}{
fill: var(--c${color}); fill: var(--c${color});
animation-name: ${animationName} animation-name: ${animationName}
}` }`,
); );
} }
@@ -59,7 +59,7 @@ export const createGrid = (
y: y * sizeCell + m, y: y * sizeCell + m,
rx: sizeDotBorderRadius, rx: sizeDotBorderRadius,
ry: sizeDotBorderRadius, ry: sizeDotBorderRadius,
}) }),
); );
} }

View File

@@ -34,13 +34,13 @@ export type DrawOptions = {
const getCellsFromGrid = ({ width, height }: Grid) => const getCellsFromGrid = ({ width, height }: Grid) =>
Array.from({ length: width }, (_, x) => Array.from({ length: width }, (_, x) =>
Array.from({ length: height }, (_, y) => ({ x, y })) Array.from({ length: height }, (_, y) => ({ x, y })),
).flat(); ).flat();
const createLivingCells = ( const createLivingCells = (
grid0: Grid, grid0: Grid,
chain: Snake[], chain: Snake[],
cells: Point[] | null cells: Point[] | null,
) => { ) => {
const livingCells: (Point & { const livingCells: (Point & {
t: number | null; t: number | null;
@@ -73,7 +73,7 @@ export const createSvg = (
cells: Point[] | null, cells: Point[] | null,
chain: Snake[], chain: Snake[],
drawOptions: DrawOptions, drawOptions: DrawOptions,
animationOptions: Pick<AnimationOptions, "frameDuration"> animationOptions: Pick<AnimationOptions, "frameDuration">,
) => { ) => {
const width = (grid.width + 2) * drawOptions.sizeCell; const width = (grid.width + 2) * drawOptions.sizeCell;
const height = (grid.height + 5) * drawOptions.sizeCell; const height = (grid.height + 5) * drawOptions.sizeCell;
@@ -89,7 +89,7 @@ export const createSvg = (
drawOptions, drawOptions,
grid.width * drawOptions.sizeCell, grid.width * drawOptions.sizeCell,
(grid.height + 2) * drawOptions.sizeCell, (grid.height + 2) * drawOptions.sizeCell,
duration duration,
), ),
createSnake(chain, drawOptions, duration), createSnake(chain, drawOptions, duration),
]; ];

View File

@@ -3,6 +3,5 @@
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@snk/solver": "1.0.0" "@snk/solver": "1.0.0"
}, }
"devDependencies": {}
} }

View File

@@ -15,7 +15,7 @@ const lerp = (k: number, a: number, b: number) => (1 - k) * a + k * b;
export const createSnake = ( export const createSnake = (
chain: Snake[], chain: Snake[],
{ sizeCell, sizeDot }: Options, { sizeCell, sizeDot }: Options,
duration: number duration: number,
) => { ) => {
const snakeN = chain[0] ? getSnakeLength(chain[0]) : 0; const snakeN = chain[0] ? getSnakeLength(chain[0]) : 0;
@@ -64,7 +64,7 @@ export const createSnake = (
const animationName = id; const animationName = id;
const keyframes = removeInterpolatedPositions( const keyframes = removeInterpolatedPositions(
positions.map((tr, i, { length }) => ({ ...tr, t: i / length })) positions.map((tr, i, { length }) => ({ ...tr, t: i / length })),
).map(({ t, ...p }) => ({ t, style: transform(p) })); ).map(({ t, ...p }) => ({ t, style: transform(p) }));
return [ return [

View File

@@ -11,7 +11,7 @@ export const createStack = (
{ sizeDot }: Options, { sizeDot }: Options,
width: number, width: number,
y: number, y: number,
duration: number duration: number,
) => { ) => {
const svgElements: string[] = []; const svgElements: string[] = [];
const styles = [ const styles = [
@@ -51,7 +51,7 @@ export const createStack = (
width: (ts.length * m + 0.6).toFixed(1), width: (ts.length * m + 0.6).toFixed(1),
x, x,
y, y,
}) }),
); );
styles.push( styles.push(
@@ -68,7 +68,7 @@ export const createStack = (
].map(({ scale, t }) => ({ ].map(({ scale, t }) => ({
t, t,
style: `transform:scale(${scale.toFixed(3)},1)`, style: `transform:scale(${scale.toFixed(3)},1)`,
})) })),
), ),
`.u.${id} { `.u.${id} {
@@ -76,7 +76,7 @@ export const createStack = (
animation-name: ${animationName}; animation-name: ${animationName};
transform-origin: ${x}px 0 transform-origin: ${x}px 0
} }
` `,
); );
} }

View File

@@ -1,13 +1,14 @@
import { it, expect, test } from "bun:test";
import { createEmptyGrid, setColor, getColor, isInside, Color } from "../grid"; import { createEmptyGrid, setColor, getColor, isInside, Color } from "../grid";
it("should set / get cell", () => { it("should set / get cell", () => {
const grid = createEmptyGrid(2, 3); const grid = createEmptyGrid(2, 3);
expect(getColor(grid, 0, 1)).toBe(0); expect(getColor(grid, 0, 1)).toBe(0 as any);
setColor(grid, 0, 1, 1 as Color); setColor(grid, 0, 1, 1 as Color);
expect(getColor(grid, 0, 1)).toBe(1); expect(getColor(grid, 0, 1)).toBe(1 as any);
}); });
test.each([ test.each([

View File

@@ -1,3 +1,4 @@
import { it, expect } from "bun:test";
import { import {
createSnakeFromCells, createSnakeFromCells,
nextSnake, nextSnake,
@@ -29,7 +30,7 @@ it("should return next snake", () => {
]; ];
expect(snakeToCells(nextSnake(createSnakeFromCells(snk0), 1, 0))).toEqual( expect(snakeToCells(nextSnake(createSnakeFromCells(snk0), 1, 0))).toEqual(
snk1 snk1,
); );
}); });

View File

@@ -30,7 +30,7 @@ export const setColor = (
grid: Grid, grid: Grid,
x: number, x: number,
y: number, y: number,
color: Color | Empty color: Color | Empty,
) => { ) => {
grid.data[getIndex(grid, x, y)] = color || 0; grid.data[getIndex(grid, x, y)] = color || 0;
}; };

View File

@@ -9,7 +9,7 @@ export const randomlyFillGrid = (
colors = [1, 2, 3] as Color[], colors = [1, 2, 3] as Color[],
emptyP = 2, emptyP = 2,
}: { colors?: Color[]; emptyP?: number } = {}, }: { colors?: Color[]; emptyP?: number } = {},
rand = defaultRand rand = defaultRand,
) => { ) => {
for (let x = grid.width; x--; ) for (let x = grid.width; x--; )
for (let y = grid.height; y--; ) { for (let y = grid.height; y--; ) {

View File

@@ -3,16 +3,19 @@ description: "Generates a snake game from a github user contributions grid. Outp
author: "platane" author: "platane"
runs: runs:
using: node16 using: node20
main: dist/index.js main: dist/index.js
inputs: inputs:
github_user_name: github_user_name:
description: "github user name" description: "github user name"
required: true 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: outputs:
required: false required: false
default: null
description: | description: |
list of files to generate. list of files to generate.
one file per line. Each output can be customized with options as query string. one file per line. Each output can be customized with options as query string.

2129
svg-only/dist/155.index.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,53 +1,18 @@
"use strict"; "use strict";
exports.id = 317; exports.id = 324;
exports.ids = [317]; exports.ids = [324];
exports.modules = { exports.modules = {
/***/ 5317: /***/ 324:
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
// ESM COMPAT FLAG
__webpack_require__.r(__webpack_exports__);
// EXPORTS // EXPORTS
__webpack_require__.d(__webpack_exports__, { __webpack_require__.d(__webpack_exports__, {
"generateContributionSnake": () => (/* binding */ generateContributionSnake) generateContributionSnake: () => (/* binding */ generateContributionSnake)
}); });
// EXTERNAL MODULE: ../../node_modules/node-fetch/lib/index.js
var lib = __webpack_require__(2197);
var lib_default = /*#__PURE__*/__webpack_require__.n(lib);
;// CONCATENATED MODULE: ../github-user-contribution/formatParams.ts
const formatParams = (options = {}) => {
const sp = new URLSearchParams();
const o = { ...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) => {
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("-");
};
;// CONCATENATED MODULE: ../github-user-contribution/index.ts ;// CONCATENATED MODULE: ../github-user-contribution/index.ts
/** /**
* get the contribution grid from a github user page * get the contribution grid from a github user page
* *
@@ -64,57 +29,66 @@ const formatDate = (d) => {
* getGithubUserContribution("platane", { year: 2019 }) * getGithubUserContribution("platane", { year: 2019 })
* *
*/ */
const getGithubUserContribution = async (userName, options = {}) => { const getGithubUserContribution = async (userName, o) => {
// either use github.com/users/xxxx/contributions for previous years const query = /* GraphQL */ `
// or github.com/xxxx ( which gives the latest update to today result ) query ($login: String!) {
const url = "year" in options || "from" in options || "to" in options user(login: $login) {
? `https://github.com/users/${userName}/contributions?` + contributionsCollection {
formatParams(options) contributionCalendar {
: `https://github.com/${userName}`; weeks {
const res = await lib_default()(url); contributionDays {
if (!res.ok) contributionCount
throw new Error(res.statusText); contributionLevel
const resText = await res.text(); weekday
return parseUserPage(resText); date
}; }
const parseUserPage = (content) => { }
// take roughly the svg block }
const block = content }
.split(`class="js-calendar-graph-svg"`)[1] }
.split("</svg>")[0]; }
let x = 0; `;
let lastYAttribute = 0; const variables = { login: userName };
const rects = Array.from(block.matchAll(/<rect[^>]*>/g)).map(([m]) => { const res = await fetch("https://api.github.com/graphql", {
const date = m.match(/data-date="([^"]+)"/)[1]; headers: {
const count = +m.match(/data-count="([^"]+)"/)[1]; Authorization: `bearer ${o.githubToken}`,
const level = +m.match(/data-level="([^"]+)"/)[1]; "Content-Type": "application/json",
const yAttribute = +m.match(/y="([^"]+)"/)[1]; "User-Agent": "me@platane.me",
if (lastYAttribute > yAttribute) },
x++; method: "POST",
lastYAttribute = yAttribute; body: JSON.stringify({ variables, query }),
return { date, count, level, x, yAttribute };
}); });
const yAttributes = Array.from(new Set(rects.map((c) => c.yAttribute)).keys()).sort(); if (!res.ok)
const cells = rects.map(({ yAttribute, ...c }) => ({ throw new Error(await res.text().catch(() => res.statusText));
y: yAttributes.indexOf(yAttribute), const { data, errors } = (await res.json());
...c, if (errors?.[0])
})); throw errors[0];
return cells; 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 // EXTERNAL MODULE: ../types/grid.ts
var types_grid = __webpack_require__(2881); var types_grid = __webpack_require__(105);
;// CONCATENATED MODULE: ./userContributionToGrid.ts ;// CONCATENATED MODULE: ./userContributionToGrid.ts
const userContributionToGrid = (cells) => { const userContributionToGrid = (cells) => {
const width = Math.max(0, ...cells.map((c) => c.x)) + 1; const width = Math.max(0, ...cells.map((c) => c.x)) + 1;
const height = Math.max(0, ...cells.map((c) => c.y)) + 1; const height = Math.max(0, ...cells.map((c) => c.y)) + 1;
const grid = (0,types_grid/* createEmptyGrid */.u1)(width, height); const grid = (0,types_grid/* createEmptyGrid */.Kb)(width, height);
for (const c of cells) { for (const c of cells) {
if (c.level > 0) if (c.level > 0)
(0,types_grid/* setColor */.vk)(grid, c.x, c.y, c.level); (0,types_grid/* setColor */.wW)(grid, c.x, c.y, c.level);
else else
(0,types_grid/* setColorEmpty */.Dy)(grid, c.x, c.y); (0,types_grid/* setColorEmpty */.l$)(grid, c.x, c.y);
} }
return grid; return grid;
}; };
@@ -132,10 +106,10 @@ const pointEquals = (a, b) => a.x === b.x && a.y === b.y;
const createOutside = (grid, color = 0) => { const createOutside = (grid, color = 0) => {
const outside = (0,types_grid/* createEmptyGrid */.u1)(grid.width, grid.height); const outside = (0,types_grid/* createEmptyGrid */.Kb)(grid.width, grid.height);
for (let x = outside.width; x--;) for (let x = outside.width; x--;)
for (let y = outside.height; y--;) for (let y = outside.height; y--;)
(0,types_grid/* setColor */.vk)(outside, x, y, 1); (0,types_grid/* setColor */.wW)(outside, x, y, 1);
fillOutside(outside, grid, color); fillOutside(outside, grid, color);
return outside; return outside;
}; };
@@ -145,19 +119,19 @@ const fillOutside = (outside, grid, color = 0) => {
changed = false; changed = false;
for (let x = outside.width; x--;) for (let x = outside.width; x--;)
for (let y = outside.height; y--;) for (let y = outside.height; y--;)
if ((0,types_grid/* getColor */.Lq)(grid, x, y) <= color && if ((0,types_grid/* getColor */.oU)(grid, x, y) <= color &&
!isOutside(outside, x, y) && !isOutside(outside, x, y) &&
around4.some((a) => isOutside(outside, x + a.x, y + a.y))) { around4.some((a) => isOutside(outside, x + a.x, y + a.y))) {
changed = true; changed = true;
(0,types_grid/* setColorEmpty */.Dy)(outside, x, y); (0,types_grid/* setColorEmpty */.l$)(outside, x, y);
} }
} }
return outside; 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)); const isOutside = (outside, x, y) => !(0,types_grid/* isInside */.FK)(outside, x, y) || (0,types_grid/* isEmpty */.Im)((0,types_grid/* getColor */.oU)(outside, x, y));
// EXTERNAL MODULE: ../types/snake.ts // EXTERNAL MODULE: ../types/snake.ts
var types_snake = __webpack_require__(9347); var types_snake = __webpack_require__(777);
;// CONCATENATED MODULE: ../solver/utils/sortPush.ts ;// CONCATENATED MODULE: ../solver/utils/sortPush.ts
const sortPush = (arr, x, sortFn) => { const sortPush = (arr, x, sortFn) => {
let a = 0; let a = 0;
@@ -190,9 +164,9 @@ const getTunnelPath = (snake0, tunnel) => {
const chain = []; const chain = [];
let snake = snake0; let snake = snake0;
for (let i = 1; i < tunnel.length; i++) { for (let i = 1; i < tunnel.length; i++) {
const dx = tunnel[i].x - (0,types_snake/* getHeadX */.If)(snake); const dx = tunnel[i].x - (0,types_snake/* getHeadX */.tN)(snake);
const dy = tunnel[i].y - (0,types_snake/* getHeadY */.IP)(snake); const dy = tunnel[i].y - (0,types_snake/* getHeadY */.Ap)(snake);
snake = (0,types_snake/* nextSnake */.kv)(snake, dx, dy); snake = (0,types_snake/* nextSnake */.Sc)(snake, dx, dy);
chain.unshift(snake); chain.unshift(snake);
} }
return chain; return chain;
@@ -220,7 +194,7 @@ const updateTunnel = (grid, tunnel, toDelete) => {
break; 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)); const isEmptySafe = (grid, x, y) => !(0,types_grid/* isInside */.FK)(grid, x, y) || (0,types_grid/* isEmpty */.Im)((0,types_grid/* getColor */.oU)(grid, x, y));
/** /**
* remove empty cell from start * remove empty cell from start
*/ */
@@ -255,14 +229,14 @@ const trimTunnelEnd = (grid, tunnel) => {
const getColorSafe = (grid, x, y) => (0,types_grid/* isInside */.V0)(grid, x, y) ? (0,types_grid/* getColor */.Lq)(grid, x, y) : 0; const getColorSafe = (grid, x, y) => (0,types_grid/* isInside */.FK)(grid, x, y) ? (0,types_grid/* getColor */.oU)(grid, x, y) : 0;
const setEmptySafe = (grid, x, y) => { const setEmptySafe = (grid, x, y) => {
if ((0,types_grid/* isInside */.V0)(grid, x, y)) if ((0,types_grid/* isInside */.FK)(grid, x, y))
(0,types_grid/* setColorEmpty */.Dy)(grid, x, y); (0,types_grid/* setColorEmpty */.l$)(grid, x, y);
}; };
const unwrap = (m) => !m const unwrap = (m) => !m
? [] ? []
: [...unwrap(m.parent), { x: (0,types_snake/* getHeadX */.If)(m.snake), y: (0,types_snake/* getHeadY */.IP)(m.snake) }]; : [...unwrap(m.parent), { x: (0,types_snake/* getHeadX */.tN)(m.snake), y: (0,types_snake/* getHeadY */.Ap)(m.snake) }];
/** /**
* returns the path to reach the outside which contains the least color cell * returns the path to reach the outside which contains the least color cell
*/ */
@@ -271,15 +245,15 @@ const getSnakeEscapePath = (grid, outside, snake0, color) => {
const closeList = []; const closeList = [];
while (openList[0]) { while (openList[0]) {
const o = openList.shift(); const o = openList.shift();
const x = (0,types_snake/* getHeadX */.If)(o.snake); const x = (0,types_snake/* getHeadX */.tN)(o.snake);
const y = (0,types_snake/* getHeadY */.IP)(o.snake); const y = (0,types_snake/* getHeadY */.Ap)(o.snake);
if (isOutside(outside, x, y)) if (isOutside(outside, x, y))
return unwrap(o); return unwrap(o);
for (const a of around4) { for (const a of around4) {
const c = getColorSafe(grid, x + a.x, y + a.y); const c = getColorSafe(grid, x + a.x, y + a.y);
if (c <= color && !(0,types_snake/* snakeWillSelfCollide */.nJ)(o.snake, a.x, a.y)) { if (c <= color && !(0,types_snake/* snakeWillSelfCollide */.J)(o.snake, a.x, a.y)) {
const snake = (0,types_snake/* nextSnake */.kv)(o.snake, a.x, a.y); const snake = (0,types_snake/* nextSnake */.Sc)(o.snake, a.x, a.y);
if (!closeList.some((s0) => (0,types_snake/* snakeEquals */.kE)(s0, snake))) { if (!closeList.some((s0) => (0,types_snake/* snakeEquals */.sW)(s0, snake))) {
const w = o.w + 1 + +(c === color) * 1000; const w = o.w + 1 + +(c === color) * 1000;
sortPush(openList, { snake, w, parent: o }, (a, b) => a.w - b.w); sortPush(openList, { snake, w, parent: o }, (a, b) => a.w - b.w);
closeList.push(snake); closeList.push(snake);
@@ -296,7 +270,7 @@ const getSnakeEscapePath = (grid, outside, snake0, color) => {
*/ */
const getBestTunnel = (grid, outside, x, y, color, snakeN) => { const getBestTunnel = (grid, outside, x, y, color, snakeN) => {
const c = { x, y }; const c = { x, y };
const snake0 = (0,types_snake/* createSnakeFromCells */.xG)(Array.from({ length: snakeN }, () => c)); const snake0 = (0,types_snake/* createSnakeFromCells */.yS)(Array.from({ length: snakeN }, () => c));
const one = getSnakeEscapePath(grid, outside, snake0, color); const one = getSnakeEscapePath(grid, outside, snake0, color);
if (!one) if (!one)
return null; return null;
@@ -304,9 +278,9 @@ const getBestTunnel = (grid, outside, x, y, color, snakeN) => {
const snakeICells = one.slice(0, snakeN); const snakeICells = one.slice(0, snakeN);
while (snakeICells.length < snakeN) while (snakeICells.length < snakeN)
snakeICells.push(snakeICells[snakeICells.length - 1]); snakeICells.push(snakeICells[snakeICells.length - 1]);
const snakeI = (0,types_snake/* createSnakeFromCells */.xG)(snakeICells); const snakeI = (0,types_snake/* createSnakeFromCells */.yS)(snakeICells);
// remove from the grid the colors that one eat // remove from the grid the colors that one eat
const gridI = (0,types_grid/* copyGrid */.VJ)(grid); const gridI = (0,types_grid/* copyGrid */.mi)(grid);
for (const { x, y } of one) for (const { x, y } of one)
setEmptySafe(gridI, x, y); setEmptySafe(gridI, x, y);
const two = getSnakeEscapePath(gridI, outside, snakeI, color); const two = getSnakeEscapePath(gridI, outside, snakeI, color);
@@ -334,15 +308,15 @@ const getPathTo = (grid, snake0, x, y) => {
const closeList = []; const closeList = [];
while (openList.length) { while (openList.length) {
const c = openList.shift(); const c = openList.shift();
const cx = (0,types_snake/* getHeadX */.If)(c.snake); const cx = (0,types_snake/* getHeadX */.tN)(c.snake);
const cy = (0,types_snake/* getHeadY */.IP)(c.snake); const cy = (0,types_snake/* getHeadY */.Ap)(c.snake);
for (let i = 0; i < around4.length; i++) { for (let i = 0; i < around4.length; i++) {
const { x: dx, y: dy } = around4[i]; const { x: dx, y: dy } = around4[i];
const nx = cx + dx; const nx = cx + dx;
const ny = cy + dy; const ny = cy + dy;
if (nx === x && ny === y) { if (nx === x && ny === y) {
// unwrap // unwrap
const path = [(0,types_snake/* nextSnake */.kv)(c.snake, dx, dy)]; const path = [(0,types_snake/* nextSnake */.Sc)(c.snake, dx, dy)];
let e = c; let e = c;
while (e.parent) { while (e.parent) {
path.push(e.snake); path.push(e.snake);
@@ -350,11 +324,11 @@ const getPathTo = (grid, snake0, x, y) => {
} }
return path; return path;
} }
if ((0,types_grid/* isInsideLarge */.HJ)(grid, 2, nx, ny) && if ((0,types_grid/* isInsideLarge */.Yd)(grid, 2, nx, ny) &&
!(0,types_snake/* snakeWillSelfCollide */.nJ)(c.snake, dx, dy) && !(0,types_snake/* snakeWillSelfCollide */.J)(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)))) { (!(0,types_grid/* isInside */.FK)(grid, nx, ny) || (0,types_grid/* isEmpty */.Im)((0,types_grid/* getColor */.oU)(grid, nx, ny)))) {
const nsnake = (0,types_snake/* nextSnake */.kv)(c.snake, dx, dy); const nsnake = (0,types_snake/* nextSnake */.Sc)(c.snake, dx, dy);
if (!closeList.some((s) => (0,types_snake/* snakeEquals */.kE)(nsnake, s))) { if (!closeList.some((s) => (0,types_snake/* snakeEquals */.sW)(nsnake, s))) {
const w = c.w + 1; const w = c.w + 1;
const h = Math.abs(nx - x) + Math.abs(ny - y); const h = Math.abs(nx - x) + Math.abs(ny - y);
const f = w + h; const f = w + h;
@@ -375,7 +349,7 @@ const getPathTo = (grid, snake0, x, y) => {
const clearResidualColoredLayer = (grid, outside, snake0, color) => { const clearResidualColoredLayer = (grid, outside, snake0, color) => {
const snakeN = (0,types_snake/* getSnakeLength */.JJ)(snake0); const snakeN = (0,types_snake/* getSnakeLength */.T$)(snake0);
const tunnels = getTunnellablePoints(grid, outside, snakeN, color); const tunnels = getTunnellablePoints(grid, outside, snakeN, color);
// sort // sort
tunnels.sort((a, b) => b.priority - a.priority); tunnels.sort((a, b) => b.priority - a.priority);
@@ -394,7 +368,7 @@ const clearResidualColoredLayer = (grid, outside, snake0, color) => {
fillOutside(outside, grid); fillOutside(outside, grid);
// update tunnels // update tunnels
for (let i = tunnels.length; i--;) for (let i = tunnels.length; i--;)
if ((0,types_grid/* isEmpty */.xb)((0,types_grid/* getColor */.Lq)(grid, tunnels[i].x, tunnels[i].y))) if ((0,types_grid/* isEmpty */.Im)((0,types_grid/* getColor */.oU)(grid, tunnels[i].x, tunnels[i].y)))
tunnels.splice(i, 1); tunnels.splice(i, 1);
else { else {
const t = tunnels[i]; const t = tunnels[i];
@@ -415,8 +389,8 @@ const clearResidualColoredLayer = (grid, outside, snake0, color) => {
const getNextTunnel = (ts, snake) => { const getNextTunnel = (ts, snake) => {
let minDistance = Infinity; let minDistance = Infinity;
let closestTunnel = null; let closestTunnel = null;
const x = (0,types_snake/* getHeadX */.If)(snake); const x = (0,types_snake/* getHeadX */.tN)(snake);
const y = (0,types_snake/* getHeadY */.IP)(snake); const y = (0,types_snake/* getHeadY */.Ap)(snake);
const priority = ts[0].priority; const priority = ts[0].priority;
for (let i = 0; ts[i] && ts[i].priority === priority; i++) { for (let i = 0; ts[i] && ts[i].priority === priority; i++) {
const t = ts[i].tunnel; const t = ts[i].tunnel;
@@ -435,8 +409,8 @@ const getTunnellablePoints = (grid, outside, snakeN, color) => {
const points = []; const points = [];
for (let x = grid.width; x--;) for (let x = grid.width; x--;)
for (let y = grid.height; y--;) { for (let y = grid.height; y--;) {
const c = (0,types_grid/* getColor */.Lq)(grid, x, y); const c = (0,types_grid/* getColor */.oU)(grid, x, y);
if (!(0,types_grid/* isEmpty */.xb)(c) && c < color) { if (!(0,types_grid/* isEmpty */.Im)(c) && c < color) {
const tunnel = getBestTunnel(grid, outside, x, y, color, snakeN); const tunnel = getBestTunnel(grid, outside, x, y, color, snakeN);
if (tunnel) { if (tunnel) {
const priority = getPriority(grid, color, tunnel); const priority = getPriority(grid, color, tunnel);
@@ -457,7 +431,7 @@ const getPriority = (grid, color, tunnel) => {
for (let i = 0; i < tunnel.length; i++) { for (let i = 0; i < tunnel.length; i++) {
const { x, y } = tunnel[i]; const { x, y } = tunnel[i];
const c = clearResidualColoredLayer_getColorSafe(grid, x, y); 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 (!(0,types_grid/* isEmpty */.Im)(c) && i === tunnel.findIndex((p) => p.x === x && p.y === y)) {
if (c === color) if (c === color)
nColor += 1; nColor += 1;
else else
@@ -469,10 +443,10 @@ const getPriority = (grid, color, tunnel) => {
return nLess / nColor; return nLess / nColor;
}; };
const distanceSq = (ax, ay, bx, by) => (ax - bx) ** 2 + (ay - by) ** 2; 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_getColorSafe = (grid, x, y) => (0,types_grid/* isInside */.FK)(grid, x, y) ? (0,types_grid/* getColor */.oU)(grid, x, y) : 0;
const clearResidualColoredLayer_setEmptySafe = (grid, x, y) => { const clearResidualColoredLayer_setEmptySafe = (grid, x, y) => {
if ((0,types_grid/* isInside */.V0)(grid, x, y)) if ((0,types_grid/* isInside */.FK)(grid, x, y))
(0,types_grid/* setColorEmpty */.Dy)(grid, x, y); (0,types_grid/* setColorEmpty */.l$)(grid, x, y);
}; };
;// CONCATENATED MODULE: ../solver/clearCleanColoredLayer.ts ;// CONCATENATED MODULE: ../solver/clearCleanColoredLayer.ts
@@ -482,14 +456,14 @@ const clearResidualColoredLayer_setEmptySafe = (grid, x, y) => {
const clearCleanColoredLayer = (grid, outside, snake0, color) => { const clearCleanColoredLayer = (grid, outside, snake0, color) => {
const snakeN = (0,types_snake/* getSnakeLength */.JJ)(snake0); const snakeN = (0,types_snake/* getSnakeLength */.T$)(snake0);
const points = clearCleanColoredLayer_getTunnellablePoints(grid, outside, snakeN, color); const points = clearCleanColoredLayer_getTunnellablePoints(grid, outside, snakeN, color);
const chain = [snake0]; const chain = [snake0];
while (points.length) { while (points.length) {
const path = getPathToNextPoint(grid, chain[0], color, points); const path = getPathToNextPoint(grid, chain[0], color, points);
path.pop(); path.pop();
for (const snake of path) for (const snake of path)
clearCleanColoredLayer_setEmptySafe(grid, (0,types_snake/* getHeadX */.If)(snake), (0,types_snake/* getHeadY */.IP)(snake)); clearCleanColoredLayer_setEmptySafe(grid, (0,types_snake/* getHeadX */.tN)(snake), (0,types_snake/* getHeadY */.Ap)(snake));
chain.unshift(...path); chain.unshift(...path);
} }
fillOutside(outside, grid); fillOutside(outside, grid);
@@ -502,19 +476,19 @@ const getPathToNextPoint = (grid, snake0, color, points) => {
const openList = [{ snake: snake0 }]; const openList = [{ snake: snake0 }];
while (openList.length) { while (openList.length) {
const o = openList.shift(); const o = openList.shift();
const x = (0,types_snake/* getHeadX */.If)(o.snake); const x = (0,types_snake/* getHeadX */.tN)(o.snake);
const y = (0,types_snake/* getHeadY */.IP)(o.snake); const y = (0,types_snake/* getHeadY */.Ap)(o.snake);
const i = points.findIndex((p) => p.x === x && p.y === y); const i = points.findIndex((p) => p.x === x && p.y === y);
if (i >= 0) { if (i >= 0) {
points.splice(i, 1); points.splice(i, 1);
return clearCleanColoredLayer_unwrap(o); return clearCleanColoredLayer_unwrap(o);
} }
for (const { x: dx, y: dy } of around4) { for (const { x: dx, y: dy } of around4) {
if ((0,types_grid/* isInsideLarge */.HJ)(grid, 2, x + dx, y + dy) && if ((0,types_grid/* isInsideLarge */.Yd)(grid, 2, x + dx, y + dy) &&
!(0,types_snake/* snakeWillSelfCollide */.nJ)(o.snake, dx, dy) && !(0,types_snake/* snakeWillSelfCollide */.J)(o.snake, dx, dy) &&
clearCleanColoredLayer_getColorSafe(grid, x + dx, y + dy) <= color) { clearCleanColoredLayer_getColorSafe(grid, x + dx, y + dy) <= color) {
const snake = (0,types_snake/* nextSnake */.kv)(o.snake, dx, dy); const snake = (0,types_snake/* nextSnake */.Sc)(o.snake, dx, dy);
if (!closeList.some((s0) => (0,types_snake/* snakeEquals */.kE)(s0, snake))) { if (!closeList.some((s0) => (0,types_snake/* snakeEquals */.sW)(s0, snake))) {
closeList.push(snake); closeList.push(snake);
openList.push({ snake, parent: o }); openList.push({ snake, parent: o });
} }
@@ -529,8 +503,8 @@ const clearCleanColoredLayer_getTunnellablePoints = (grid, outside, snakeN, colo
const points = []; const points = [];
for (let x = grid.width; x--;) for (let x = grid.width; x--;)
for (let y = grid.height; y--;) { for (let y = grid.height; y--;) {
const c = (0,types_grid/* getColor */.Lq)(grid, x, y); const c = (0,types_grid/* getColor */.oU)(grid, x, y);
if (!(0,types_grid/* isEmpty */.xb)(c) && if (!(0,types_grid/* isEmpty */.Im)(c) &&
c <= color && c <= color &&
!points.some((p) => p.x === x && p.y === y)) { !points.some((p) => p.x === x && p.y === y)) {
const tunnel = getBestTunnel(grid, outside, x, y, color, snakeN); const tunnel = getBestTunnel(grid, outside, x, y, color, snakeN);
@@ -542,12 +516,12 @@ const clearCleanColoredLayer_getTunnellablePoints = (grid, outside, snakeN, colo
} }
return points; 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_getColorSafe = (grid, x, y) => (0,types_grid/* isInside */.FK)(grid, x, y) ? (0,types_grid/* getColor */.oU)(grid, x, y) : 0;
const clearCleanColoredLayer_setEmptySafe = (grid, x, y) => { const clearCleanColoredLayer_setEmptySafe = (grid, x, y) => {
if ((0,types_grid/* isInside */.V0)(grid, x, y)) if ((0,types_grid/* isInside */.FK)(grid, x, y))
(0,types_grid/* setColorEmpty */.Dy)(grid, x, y); (0,types_grid/* setColorEmpty */.l$)(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)); const clearCleanColoredLayer_isEmptySafe = (grid, x, y) => !(0,types_grid/* isInside */.FK)(grid, x, y) && (0,types_grid/* isEmpty */.Im)((0,types_grid/* getColor */.oU)(grid, x, y));
;// CONCATENATED MODULE: ../solver/getBestRoute.ts ;// CONCATENATED MODULE: ../solver/getBestRoute.ts
@@ -555,7 +529,7 @@ const clearCleanColoredLayer_isEmptySafe = (grid, x, y) => !(0,types_grid/* isIn
const getBestRoute = (grid0, snake0) => { const getBestRoute = (grid0, snake0) => {
const grid = (0,types_grid/* copyGrid */.VJ)(grid0); const grid = (0,types_grid/* copyGrid */.mi)(grid0);
const outside = createOutside(grid); const outside = createOutside(grid);
const chain = [snake0]; const chain = [snake0];
for (const color of extractColors(grid)) { for (const color of extractColors(grid)) {
@@ -573,7 +547,7 @@ const extractColors = (grid) => {
;// CONCATENATED MODULE: ../types/__fixtures__/snake.ts ;// CONCATENATED MODULE: ../types/__fixtures__/snake.ts
const create = (length) => (0,types_snake/* createSnakeFromCells */.xG)(Array.from({ length }, (_, i) => ({ x: i, y: -1 }))); const create = (length) => (0,types_snake/* createSnakeFromCells */.yS)(Array.from({ length }, (_, i) => ({ x: i, y: -1 })));
const snake1 = create(1); const snake1 = create(1);
const snake3 = create(3); const snake3 = create(3);
const snake4 = create(4); const snake4 = create(4);
@@ -586,20 +560,20 @@ const snake9 = create(9);
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_isEmptySafe = (grid, x, y) => !(0,types_grid/* isInside */.FK)(grid, x, y) || (0,types_grid/* isEmpty */.Im)((0,types_grid/* getColor */.oU)(grid, x, y));
const getPathToPose = (snake0, target, grid) => { const getPathToPose = (snake0, target, grid) => {
if ((0,types_snake/* snakeEquals */.kE)(snake0, target)) if ((0,types_snake/* snakeEquals */.sW)(snake0, target))
return []; return [];
const targetCells = (0,types_snake/* snakeToCells */.Ks)(target).reverse(); const targetCells = (0,types_snake/* snakeToCells */.HU)(target).reverse();
const snakeN = (0,types_snake/* getSnakeLength */.JJ)(snake0); const snakeN = (0,types_snake/* getSnakeLength */.T$)(snake0);
const box = { const box = {
min: { min: {
x: Math.min((0,types_snake/* getHeadX */.If)(snake0), (0,types_snake/* getHeadX */.If)(target)) - snakeN - 1, x: Math.min((0,types_snake/* getHeadX */.tN)(snake0), (0,types_snake/* getHeadX */.tN)(target)) - snakeN - 1,
y: Math.min((0,types_snake/* getHeadY */.IP)(snake0), (0,types_snake/* getHeadY */.IP)(target)) - snakeN - 1, y: Math.min((0,types_snake/* getHeadY */.Ap)(snake0), (0,types_snake/* getHeadY */.Ap)(target)) - snakeN - 1,
}, },
max: { max: {
x: Math.max((0,types_snake/* getHeadX */.If)(snake0), (0,types_snake/* getHeadX */.If)(target)) + snakeN + 1, x: Math.max((0,types_snake/* getHeadX */.tN)(snake0), (0,types_snake/* getHeadX */.tN)(target)) + snakeN + 1,
y: Math.max((0,types_snake/* getHeadY */.IP)(snake0), (0,types_snake/* getHeadY */.IP)(target)) + snakeN + 1, y: Math.max((0,types_snake/* getHeadY */.Ap)(snake0), (0,types_snake/* getHeadY */.Ap)(target)) + snakeN + 1,
}, },
}; };
const [t0, ...forbidden] = targetCells; const [t0, ...forbidden] = targetCells;
@@ -608,8 +582,8 @@ const getPathToPose = (snake0, target, grid) => {
const closeList = []; const closeList = [];
while (openList.length) { while (openList.length) {
const o = openList.shift(); const o = openList.shift();
const x = (0,types_snake/* getHeadX */.If)(o.snake); const x = (0,types_snake/* getHeadX */.tN)(o.snake);
const y = (0,types_snake/* getHeadY */.IP)(o.snake); const y = (0,types_snake/* getHeadY */.Ap)(o.snake);
if (x === t0.x && y === t0.y) { if (x === t0.x && y === t0.y) {
const path = []; const path = [];
let e = o; let e = o;
@@ -626,17 +600,17 @@ const getPathToPose = (snake0, target, grid) => {
const { x: dx, y: dy } = around4[i]; const { x: dx, y: dy } = around4[i];
const nx = x + dx; const nx = x + dx;
const ny = y + dy; const ny = y + dy;
if (!(0,types_snake/* snakeWillSelfCollide */.nJ)(o.snake, dx, dy) && if (!(0,types_snake/* snakeWillSelfCollide */.J)(o.snake, dx, dy) &&
(!grid || getPathToPose_isEmptySafe(grid, nx, ny)) && (!grid || getPathToPose_isEmptySafe(grid, nx, ny)) &&
(grid (grid
? (0,types_grid/* isInsideLarge */.HJ)(grid, 2, nx, ny) ? (0,types_grid/* isInsideLarge */.Yd)(grid, 2, nx, ny)
: box.min.x <= nx && : box.min.x <= nx &&
nx <= box.max.x && nx <= box.max.x &&
box.min.y <= ny && box.min.y <= ny &&
ny <= box.max.y) && ny <= box.max.y) &&
!forbidden.some((p) => p.x === nx && p.y === ny)) { !forbidden.some((p) => p.x === nx && p.y === ny)) {
const snake = (0,types_snake/* nextSnake */.kv)(o.snake, dx, dy); const snake = (0,types_snake/* nextSnake */.Sc)(o.snake, dx, dy);
if (!closeList.some((s) => (0,types_snake/* snakeEquals */.kE)(snake, s))) { if (!closeList.some((s) => (0,types_snake/* snakeEquals */.sW)(snake, s))) {
const w = o.w + 1; const w = o.w + 1;
const h = Math.abs(nx - x) + Math.abs(ny - y); const h = Math.abs(nx - x) + Math.abs(ny - y);
const f = w + h; const f = w + h;
@@ -654,9 +628,9 @@ const getPathToPose = (snake0, target, grid) => {
const generateContributionSnake = async (userName, outputs) => { const generateContributionSnake = async (userName, outputs, options) => {
console.log("🎣 fetching github user contribution"); console.log("🎣 fetching github user contribution");
const cells = await getGithubUserContribution(userName); const cells = await getGithubUserContribution(userName, options);
const grid = userContributionToGrid(cells); const grid = userContributionToGrid(cells);
const snake = snake4; const snake = snake4;
console.log("📡 computing best route"); console.log("📡 computing best route");
@@ -669,12 +643,12 @@ const generateContributionSnake = async (userName, outputs) => {
switch (format) { switch (format) {
case "svg": { case "svg": {
console.log(`🖌 creating svg (outputs[${i}])`); console.log(`🖌 creating svg (outputs[${i}])`);
const { createSvg } = await __webpack_require__.e(/* import() */ 340).then(__webpack_require__.bind(__webpack_require__, 8340)); const { createSvg } = await __webpack_require__.e(/* import() */ 578).then(__webpack_require__.bind(__webpack_require__, 4578));
return createSvg(grid, cells, chain, drawOptions, animationOptions); return createSvg(grid, cells, chain, drawOptions, animationOptions);
} }
case "gif": { case "gif": {
console.log(`📹 creating gif (outputs[${i}])`); 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)); const { createGif } = await Promise.all(/* import() */[__webpack_require__.e(155), __webpack_require__.e(642)]).then(__webpack_require__.bind(__webpack_require__, 3642));
return await createGif(grid, cells, chain, drawOptions, animationOptions); return await createGif(grid, cells, chain, drawOptions, animationOptions);
} }
} }
@@ -684,18 +658,18 @@ const generateContributionSnake = async (userName, outputs) => {
/***/ }), /***/ }),
/***/ 2881: /***/ 105:
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "V0": () => (/* binding */ isInside), /* harmony export */ FK: () => (/* binding */ isInside),
/* harmony export */ "HJ": () => (/* binding */ isInsideLarge), /* harmony export */ Im: () => (/* binding */ isEmpty),
/* harmony export */ "VJ": () => (/* binding */ copyGrid), /* harmony export */ Kb: () => (/* binding */ createEmptyGrid),
/* harmony export */ "Lq": () => (/* binding */ getColor), /* harmony export */ Yd: () => (/* binding */ isInsideLarge),
/* harmony export */ "xb": () => (/* binding */ isEmpty), /* harmony export */ l$: () => (/* binding */ setColorEmpty),
/* harmony export */ "vk": () => (/* binding */ setColor), /* harmony export */ mi: () => (/* binding */ copyGrid),
/* harmony export */ "Dy": () => (/* binding */ setColorEmpty), /* harmony export */ oU: () => (/* binding */ getColor),
/* harmony export */ "u1": () => (/* binding */ createEmptyGrid) /* harmony export */ wW: () => (/* binding */ setColor)
/* harmony export */ }); /* harmony export */ });
/* unused harmony exports isGridEmpty, gridEquals */ /* unused harmony exports isGridEmpty, gridEquals */
const isInside = (grid, x, y) => x >= 0 && y >= 0 && x < grid.width && y < grid.height; const isInside = (grid, x, y) => x >= 0 && y >= 0 && x < grid.width && y < grid.height;
@@ -728,18 +702,18 @@ const createEmptyGrid = (width, height) => ({
/***/ }), /***/ }),
/***/ 9347: /***/ 777:
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "If": () => (/* binding */ getHeadX), /* harmony export */ Ap: () => (/* binding */ getHeadY),
/* harmony export */ "IP": () => (/* binding */ getHeadY), /* harmony export */ HU: () => (/* binding */ snakeToCells),
/* harmony export */ "JJ": () => (/* binding */ getSnakeLength), /* harmony export */ J: () => (/* binding */ snakeWillSelfCollide),
/* harmony export */ "kE": () => (/* binding */ snakeEquals), /* harmony export */ Sc: () => (/* binding */ nextSnake),
/* harmony export */ "kv": () => (/* binding */ nextSnake), /* harmony export */ T$: () => (/* binding */ getSnakeLength),
/* harmony export */ "nJ": () => (/* binding */ snakeWillSelfCollide), /* harmony export */ sW: () => (/* binding */ snakeEquals),
/* harmony export */ "Ks": () => (/* binding */ snakeToCells), /* harmony export */ tN: () => (/* binding */ getHeadX),
/* harmony export */ "xG": () => (/* binding */ createSnakeFromCells) /* harmony export */ yS: () => (/* binding */ createSnakeFromCells)
/* harmony export */ }); /* harmony export */ });
/* unused harmony export copySnake */ /* unused harmony export copySnake */
const getHeadX = (snake) => snake[0] - 2; const getHeadX = (snake) => snake[0] - 2;

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,21 @@
"use strict"; "use strict";
exports.id = 340; exports.id = 578;
exports.ids = [340]; exports.ids = [578];
exports.modules = { exports.modules = {
/***/ 8340: /***/ 4578:
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
// ESM COMPAT FLAG
__webpack_require__.r(__webpack_exports__);
// EXPORTS // EXPORTS
__webpack_require__.d(__webpack_exports__, { __webpack_require__.d(__webpack_exports__, {
"createSvg": () => (/* binding */ createSvg) createSvg: () => (/* binding */ createSvg)
}); });
// EXTERNAL MODULE: ../types/grid.ts // EXTERNAL MODULE: ../types/grid.ts
var types_grid = __webpack_require__(2881); var types_grid = __webpack_require__(105);
// EXTERNAL MODULE: ../types/snake.ts // EXTERNAL MODULE: ../types/snake.ts
var types_snake = __webpack_require__(9347); var types_snake = __webpack_require__(777);
;// CONCATENATED MODULE: ../svg-creator/xml-utils.ts ;// CONCATENATED MODULE: ../svg-creator/xml-utils.ts
const h = (element, attributes) => `<${element} ${toAttribute(attributes)}/>`; const h = (element, attributes) => `<${element} ${toAttribute(attributes)}/>`;
const toAttribute = (o) => Object.entries(o) const toAttribute = (o) => Object.entries(o)
@@ -62,10 +60,10 @@ const minifyCss = (css) => css
const lerp = (k, a, b) => (1 - k) * a + k * b; const lerp = (k, a, b) => (1 - k) * a + k * b;
const createSnake = (chain, { sizeCell, sizeDot }, duration) => { const createSnake = (chain, { sizeCell, sizeDot }, duration) => {
const snakeN = chain[0] ? (0,types_snake/* getSnakeLength */.JJ)(chain[0]) : 0; const snakeN = chain[0] ? (0,types_snake/* getSnakeLength */.T$)(chain[0]) : 0;
const snakeParts = Array.from({ length: snakeN }, () => []); const snakeParts = Array.from({ length: snakeN }, () => []);
for (const snake of chain) { for (const snake of chain) {
const cells = (0,types_snake/* snakeToCells */.Ks)(snake); const cells = (0,types_snake/* snakeToCells */.HU)(snake);
for (let i = cells.length; i--;) for (let i = cells.length; i--;)
snakeParts[i].push(cells[i]); snakeParts[i].push(cells[i]);
} }
@@ -237,15 +235,15 @@ const createLivingCells = (grid0, chain, cells) => {
x, x,
y, y,
t: null, t: null,
color: (0,types_grid/* getColor */.Lq)(grid0, x, y), color: (0,types_grid/* getColor */.oU)(grid0, x, y),
})); }));
const grid = (0,types_grid/* copyGrid */.VJ)(grid0); const grid = (0,types_grid/* copyGrid */.mi)(grid0);
for (let i = 0; i < chain.length; i++) { for (let i = 0; i < chain.length; i++) {
const snake = chain[i]; const snake = chain[i];
const x = (0,types_snake/* getHeadX */.If)(snake); const x = (0,types_snake/* getHeadX */.tN)(snake);
const y = (0,types_snake/* getHeadY */.IP)(snake); const y = (0,types_snake/* getHeadY */.Ap)(snake);
if ((0,types_grid/* isInside */.V0)(grid, x, y) && !(0,types_grid/* isEmpty */.xb)((0,types_grid/* getColor */.Lq)(grid, x, y))) { if ((0,types_grid/* isInside */.FK)(grid, x, y) && !(0,types_grid/* isEmpty */.Im)((0,types_grid/* getColor */.oU)(grid, x, y))) {
(0,types_grid/* setColorEmpty */.Dy)(grid, x, y); (0,types_grid/* setColorEmpty */.l$)(grid, x, y);
const cell = livingCells.find((c) => c.x === x && c.y === y); const cell = livingCells.find((c) => c.x === x && c.y === y);
cell.t = i / chain.length; cell.t = i / chain.length;
} }

View File

@@ -1,31 +1,29 @@
"use strict"; "use strict";
exports.id = 142; exports.id = 642;
exports.ids = [142]; exports.ids = [642];
exports.modules = { exports.modules = {
/***/ 7142: /***/ 3642:
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
// ESM COMPAT FLAG
__webpack_require__.r(__webpack_exports__);
// EXPORTS // EXPORTS
__webpack_require__.d(__webpack_exports__, { __webpack_require__.d(__webpack_exports__, {
"createGif": () => (/* binding */ createGif) createGif: () => (/* binding */ createGif)
}); });
// EXTERNAL MODULE: external "fs" // EXTERNAL MODULE: external "fs"
var external_fs_ = __webpack_require__(7147); var external_fs_ = __webpack_require__(9896);
var external_fs_default = /*#__PURE__*/__webpack_require__.n(external_fs_); var external_fs_default = /*#__PURE__*/__webpack_require__.n(external_fs_);
// EXTERNAL MODULE: external "path" // EXTERNAL MODULE: external "path"
var external_path_ = __webpack_require__(1017); var external_path_ = __webpack_require__(6928);
var external_path_default = /*#__PURE__*/__webpack_require__.n(external_path_); var external_path_default = /*#__PURE__*/__webpack_require__.n(external_path_);
// EXTERNAL MODULE: external "child_process" // EXTERNAL MODULE: external "child_process"
var external_child_process_ = __webpack_require__(2081); var external_child_process_ = __webpack_require__(5317);
// EXTERNAL MODULE: external "canvas" // EXTERNAL MODULE: external "canvas"
var external_canvas_ = __webpack_require__(1576); var external_canvas_ = __webpack_require__(9919);
// EXTERNAL MODULE: ../types/grid.ts // EXTERNAL MODULE: ../types/grid.ts
var types_grid = __webpack_require__(2881); var types_grid = __webpack_require__(105);
;// CONCATENATED MODULE: ../draw/pathRoundedRect.ts ;// CONCATENATED MODULE: ../draw/pathRoundedRect.ts
const pathRoundedRect_pathRoundedRect = (ctx, width, height, borderRadius) => { const pathRoundedRect_pathRoundedRect = (ctx, width, height, borderRadius) => {
ctx.moveTo(borderRadius, 0); ctx.moveTo(borderRadius, 0);
@@ -42,7 +40,7 @@ const drawGrid_drawGrid = (ctx, grid, cells, o) => {
for (let x = grid.width; x--;) for (let x = grid.width; x--;)
for (let y = grid.height; y--;) { for (let y = grid.height; y--;) {
if (!cells || cells.some((c) => c.x === x && c.y === y)) { if (!cells || cells.some((c) => c.x === x && c.y === y)) {
const c = (0,types_grid/* getColor */.Lq)(grid, x, y); const c = (0,types_grid/* getColor */.oU)(grid, x, y);
// @ts-ignore // @ts-ignore
const color = !c ? o.colorEmpty : o.colorDots[c]; const color = !c ? o.colorEmpty : o.colorDots[c];
ctx.save(); ctx.save();
@@ -144,27 +142,27 @@ const getCanvasWorldSize = (grid, o) => {
}; };
// EXTERNAL MODULE: ../types/snake.ts // EXTERNAL MODULE: ../types/snake.ts
var types_snake = __webpack_require__(9347); var types_snake = __webpack_require__(777);
;// CONCATENATED MODULE: ../solver/step.ts ;// CONCATENATED MODULE: ../solver/step.ts
const step = (grid, stack, snake) => { const step = (grid, stack, snake) => {
const x = (0,types_snake/* getHeadX */.If)(snake); const x = (0,types_snake/* getHeadX */.tN)(snake);
const y = (0,types_snake/* getHeadY */.IP)(snake); const y = (0,types_snake/* getHeadY */.Ap)(snake);
const color = (0,types_grid/* getColor */.Lq)(grid, x, y); const color = (0,types_grid/* getColor */.oU)(grid, x, y);
if ((0,types_grid/* isInside */.V0)(grid, x, y) && !(0,types_grid/* isEmpty */.xb)(color)) { if ((0,types_grid/* isInside */.FK)(grid, x, y) && !(0,types_grid/* isEmpty */.Im)(color)) {
stack.push(color); stack.push(color);
(0,types_grid/* setColorEmpty */.Dy)(grid, x, y); (0,types_grid/* setColorEmpty */.l$)(grid, x, y);
} }
}; };
// EXTERNAL MODULE: ../../node_modules/tmp/lib/tmp.js // EXTERNAL MODULE: ../../node_modules/tmp/lib/tmp.js
var tmp = __webpack_require__(6382); var tmp = __webpack_require__(2644);
// EXTERNAL MODULE: external "gifsicle" // EXTERNAL MODULE: external "gifsicle"
var external_gifsicle_ = __webpack_require__(542); var external_gifsicle_ = __webpack_require__(5667);
var external_gifsicle_default = /*#__PURE__*/__webpack_require__.n(external_gifsicle_); var external_gifsicle_default = /*#__PURE__*/__webpack_require__.n(external_gifsicle_);
// EXTERNAL MODULE: ../../node_modules/gif-encoder-2/index.js // EXTERNAL MODULE: ../../node_modules/gif-encoder-2/index.js
var gif_encoder_2 = __webpack_require__(3561); var gif_encoder_2 = __webpack_require__(1680);
var gif_encoder_2_default = /*#__PURE__*/__webpack_require__.n(gif_encoder_2); var gif_encoder_2_default = /*#__PURE__*/__webpack_require__.n(gif_encoder_2);
;// CONCATENATED MODULE: ../gif-creator/index.ts ;// CONCATENATED MODULE: ../gif-creator/index.ts
@@ -193,7 +191,7 @@ const createGif = async (grid0, cells, chain, drawOptions, animationOptions) =>
const { width, height } = getCanvasWorldSize(grid0, drawOptions); const { width, height } = getCanvasWorldSize(grid0, drawOptions);
const canvas = (0,external_canvas_.createCanvas)(width, height); const canvas = (0,external_canvas_.createCanvas)(width, height);
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
const grid = (0,types_grid/* copyGrid */.VJ)(grid0); const grid = (0,types_grid/* copyGrid */.mi)(grid0);
const stack = []; const stack = [];
const encoder = new (gif_encoder_2_default())(width, height, "neuquant", true); const encoder = new (gif_encoder_2_default())(width, height, "neuquant", true);
encoder.setRepeat(0); encoder.setRepeat(0);
@@ -223,7 +221,7 @@ const createGif = async (grid0, cells, chain, drawOptions, animationOptions) =>
outFileName, outFileName,
["--output", optimizedFileName], ["--output", optimizedFileName],
].flat()); ].flat());
return external_fs_default().readFileSync(optimizedFileName); return new Uint8Array(external_fs_default().readFileSync(optimizedFileName));
}); });

26146
svg-only/dist/index.js vendored

File diff suppressed because one or more lines are too long

6508
yarn.lock

File diff suppressed because it is too large Load Diff