Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7ef7da9fb | ||
|
|
db283098a9 | ||
|
|
621d78be60 | ||
|
|
42f5b68655 | ||
|
|
c135277bdf | ||
|
|
34d5617f54 | ||
|
|
be9fca7f10 | ||
|
|
c0a042d6b4 | ||
|
|
85e229a04d | ||
|
|
da5e045399 | ||
|
|
c22b80d02b | ||
|
|
3db2b4069e | ||
|
|
83033510f0 | ||
|
|
a69d1dbca7 | ||
|
|
f2057e5efe | ||
|
|
7fbc58b61d | ||
|
|
1f7630d984 | ||
|
|
852f0ae376 | ||
|
|
10c4c3c7bd | ||
|
|
ace186c41f | ||
|
|
79c252356c | ||
|
|
3c171061b3 | ||
|
|
4783e68ce7 | ||
|
|
85da3901f5 | ||
|
|
74bc4f0651 | ||
|
|
e55fe1f13c | ||
|
|
876448a004 | ||
|
|
d35dc83cf2 | ||
|
|
bb7d69dde8 | ||
|
|
14a003db51 | ||
|
|
2479713155 | ||
|
|
debec31440 | ||
|
|
5332254423 | ||
|
|
a9052b7ca2 | ||
|
|
8b7b3e6ace | ||
|
|
92f4de3970 | ||
|
|
c9644d3dfa | ||
|
|
01fa6d7aac | ||
|
|
b58af55b7d | ||
|
|
4e5805f8af | ||
|
|
743771147d | ||
|
|
8eddcbdbea | ||
|
|
6f0ace6560 | ||
|
|
835fdd6b84 | ||
|
|
e6034f3972 | ||
|
|
aebc3a9285 | ||
|
|
1574f65738 | ||
|
|
ebeb59fced | ||
|
|
4489504b7a | ||
|
|
027f89563f | ||
|
|
7233ec9e15 | ||
|
|
54dbbbf73d | ||
|
|
3eed9ce6d6 | ||
|
|
3acebc09eb | ||
|
|
82417bf9f5 | ||
|
|
7b6d52d221 | ||
|
|
fd133c88c7 | ||
|
|
229c9a9cd6 | ||
|
|
3803e1ccfa | ||
|
|
8ca289e908 | ||
|
|
fd7cc1f05a | ||
|
|
632fcf6cb7 | ||
|
|
e2eb91cf8f | ||
|
|
38e2ed4f23 | ||
|
|
b7a9c1e353 | ||
|
|
a0e08722d9 |
78
.github/workflows/main.yml
vendored
78
.github/workflows/main.yml
vendored
@@ -7,21 +7,29 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
cache: yarn
|
||||
node-version: 16
|
||||
- run: yarn install --frozen-lockfile
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- run: yarn type
|
||||
- run: yarn lint
|
||||
- run: yarn test --ci
|
||||
- run: bun install --frozen-lockfile
|
||||
|
||||
- run: bun run build
|
||||
working-directory: packages/solver-r
|
||||
|
||||
- run: npm run type
|
||||
|
||||
- run: bun test
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- run: cargo test
|
||||
working-directory: packages/solver-r
|
||||
|
||||
- run: npm run lint
|
||||
|
||||
test-action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: update action.yml to use image from local Dockerfile
|
||||
run: |
|
||||
@@ -44,7 +52,7 @@ jobs:
|
||||
test -f dist/github-contribution-grid-snake-dark.svg
|
||||
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:
|
||||
target_branch: output
|
||||
build_dir: dist
|
||||
@@ -54,16 +62,14 @@ jobs:
|
||||
test-action-svg-only:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: yarn
|
||||
node-version: 16
|
||||
- run: yarn install --frozen-lockfile
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v1
|
||||
|
||||
- run: bun install --frozen-lockfile
|
||||
|
||||
- name: build svg-only action
|
||||
run: |
|
||||
yarn build:action
|
||||
npm run build:action
|
||||
rm -r svg-only/dist
|
||||
mv packages/action/dist svg-only/dist
|
||||
|
||||
@@ -75,14 +81,16 @@ jobs:
|
||||
outputs: |
|
||||
dist/github-contribution-grid-snake.svg
|
||||
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
|
||||
run: |
|
||||
ls dist
|
||||
test -f dist/github-contribution-grid-snake.svg
|
||||
test -f dist/github-contribution-grid-snake-dark.svg
|
||||
test -f dist/github-contribution-grid-snake-blue.svg
|
||||
|
||||
- uses: crazy-max/ghaction-github-pages@v3.1.0
|
||||
- uses: crazy-max/ghaction-github-pages@v4.1.0
|
||||
with:
|
||||
target_branch: output-svg-only
|
||||
build_dir: dist
|
||||
@@ -91,22 +99,28 @@ jobs:
|
||||
|
||||
deploy-ghpages:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
cache: yarn
|
||||
node-version: 16
|
||||
- run: yarn install --frozen-lockfile
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v1
|
||||
|
||||
- run: yarn build:demo
|
||||
- run: bun install --frozen-lockfile
|
||||
|
||||
- run: npm run build:demo
|
||||
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'
|
||||
with:
|
||||
target_branch: gh-pages
|
||||
build_dir: packages/demo/dist
|
||||
|
||||
- run: bunx wrangler deploy
|
||||
if: success() && github.ref == 'refs/heads/main'
|
||||
working-directory: packages/github-user-contribution-service
|
||||
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
47
.github/workflows/manual-run.yml
vendored
Normal 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 }}
|
||||
28
.github/workflows/release.yml
vendored
28
.github/workflows/release.yml
vendored
@@ -21,19 +21,19 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: docker/setup-qemu-action@v1
|
||||
- uses: docker/setup-qemu-action@v2
|
||||
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
|
||||
- uses: docker/login-action@v1
|
||||
- uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: build and publish the docker image
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v4
|
||||
id: docker-build
|
||||
with:
|
||||
push: true
|
||||
@@ -45,20 +45,18 @@ jobs:
|
||||
run: |
|
||||
sed -i "s/image: .*/image: docker:\/\/platane\/snk@${{ steps.docker-build.outputs.digest }}/" action.yml
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
cache: yarn
|
||||
node-version: 16
|
||||
- uses: oven-sh/setup-bun@v1
|
||||
|
||||
- run: bun install --frozen-lockfile
|
||||
|
||||
- name: build svg-only action
|
||||
run: |
|
||||
yarn install --frozen-lockfile
|
||||
yarn build:action
|
||||
npm run build:action
|
||||
rm -r svg-only/dist
|
||||
mv packages/action/dist svg-only/dist
|
||||
|
||||
- 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
|
||||
id: push-tags
|
||||
@@ -77,13 +75,11 @@ jobs:
|
||||
git tag v$( echo $VERSION | cut -d. -f 1-2 )
|
||||
git push origin --tags --force
|
||||
echo "prerelease=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
else
|
||||
echo "prerelease=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- uses: ncipollo/release-action@v1.11.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: ncipollo/release-action@v1.15.0
|
||||
with:
|
||||
tag: v${{ github.event.inputs.version }}
|
||||
body: ${{ github.event.inputs.description }}
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,6 +1,9 @@
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
yarn-error.log*
|
||||
dist
|
||||
!svg-only/dist
|
||||
build
|
||||
build
|
||||
.env
|
||||
.wrangler
|
||||
.dev.vars
|
||||
target
|
||||
|
||||
19
Dockerfile
19
Dockerfile
@@ -1,32 +1,27 @@
|
||||
FROM node:16-slim as builder
|
||||
FROM oven/bun:1.2.2-slim as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
COPY package.json bun.lock ./
|
||||
|
||||
COPY tsconfig.json ./
|
||||
|
||||
COPY packages packages
|
||||
|
||||
RUN export YARN_CACHE_FOLDER="$(mktemp -d)" \
|
||||
&& yarn install --frozen-lockfile \
|
||||
&& rm -r "$YARN_CACHE_FOLDER"
|
||||
RUN bun install --no-cache
|
||||
|
||||
RUN yarn build:action
|
||||
RUN bun run build:action
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
FROM node:16-slim
|
||||
FROM oven/bun:1.2.2-slim
|
||||
|
||||
WORKDIR /action-release
|
||||
|
||||
RUN export YARN_CACHE_FOLDER="$(mktemp -d)" \
|
||||
&& yarn add canvas@2.10.2 gifsicle@5.3.0 --no-lockfile \
|
||||
&& rm -r "$YARN_CACHE_FOLDER"
|
||||
RUN bun add canvas@3.1.0 gifsicle@5.3.0 --no-lockfile --no-cache
|
||||
|
||||
COPY --from=builder /app/packages/action/dist/ /action-release/
|
||||
|
||||
CMD ["node", "/action-release/index.js"]
|
||||
|
||||
CMD ["bun", "/action-release/index.js"]
|
||||
|
||||
33
README.md
33
README.md
@@ -1,5 +1,6 @@
|
||||
# snk
|
||||
|
||||
[](https://github.com/Platane/Platane/actions/workflows/main.yml)
|
||||
[](https://github.com/platane/snk/releases/latest)
|
||||
[](https://github.com/marketplace/actions/generate-snake-game-from-github-contribution-grid)
|
||||

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

|
||||
<picture>
|
||||
<source
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="https://raw.githubusercontent.com/platane/snk/output/github-contribution-grid-snake-dark.svg"
|
||||
/>
|
||||
<source
|
||||
media="(prefers-color-scheme: light)"
|
||||
srcset="https://raw.githubusercontent.com/platane/snk/output/github-contribution-grid-snake.svg"
|
||||
/>
|
||||
<img
|
||||
alt="github contribution grid snake animation"
|
||||
src="https://raw.githubusercontent.com/platane/snk/output/github-contribution-grid-snake.svg"
|
||||
/>
|
||||
</picture>
|
||||
|
||||
Pull a github user's contribution graph.
|
||||
Make it a snake Game, generate a snake path where the cells get eaten in an orderly fashion.
|
||||
@@ -21,7 +35,7 @@ Available as github action. It can automatically generate a new image each day.
|
||||
**github action**
|
||||
|
||||
```yaml
|
||||
- uses: Platane/snk@v2
|
||||
- uses: Platane/snk@v3
|
||||
with:
|
||||
# github user name to read the contribution graph from (**required**)
|
||||
# using action context var `github.repository_owner` or specified user
|
||||
@@ -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
|
||||
```
|
||||
|
||||
[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**
|
||||
|
||||
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
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="github-snake-dark.svg" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="github-snake.svg" />
|
||||
<img alt="github-snake" src="github-snake.svg" />
|
||||
</picture>
|
||||
```
|
||||
|
||||
**interactive demo**
|
||||
|
||||
@@ -4,15 +4,18 @@ author: "platane"
|
||||
|
||||
runs:
|
||||
using: docker
|
||||
image: docker://platane/snk@sha256:89466e404c3d3ba2384e24aabad0542a643eacdc53d0c6320ce369cc1af19d56
|
||||
image: docker://platane/snk@sha256:96390294299275740e5963058c9784c60c5393b3b8b16082dcf41b240db791f9
|
||||
|
||||
inputs:
|
||||
github_user_name:
|
||||
description: "github user name"
|
||||
required: true
|
||||
github_token:
|
||||
description: "github token used to fetch the contribution calendar. Default to the action token if empty."
|
||||
required: false
|
||||
default: ${{ github.token }}
|
||||
outputs:
|
||||
required: false
|
||||
default: null
|
||||
description: |
|
||||
list of files to generate.
|
||||
one file per line. Each output can be customized with options as query string.
|
||||
|
||||
37
package.json
37
package.json
@@ -1,36 +1,25 @@
|
||||
{
|
||||
"name": "snk",
|
||||
"description": "Generates a snake game from a github user contributions grid",
|
||||
"version": "2.1.0",
|
||||
"version": "3.3.0",
|
||||
"private": true,
|
||||
"repository": "github:platane/snk",
|
||||
"devDependencies": {
|
||||
"@sucrase/jest-plugin": "3.0.0",
|
||||
"@types/jest": "29.2.1",
|
||||
"@types/node": "16.11.7",
|
||||
"jest": "29.2.2",
|
||||
"prettier": "2.7.1",
|
||||
"sucrase": "3.28.0",
|
||||
"typescript": "4.8.4"
|
||||
"@types/bun": "1.2.2",
|
||||
"prettier": "3.5.1",
|
||||
"typescript": "5.7.3"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/**"
|
||||
"packages/*"
|
||||
],
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
"testMatch": [
|
||||
"**/__tests__/**/?(*.)+(spec|test).ts"
|
||||
],
|
||||
"transform": {
|
||||
"\\.(ts|tsx)$": "@sucrase/jest-plugin"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"type": "tsc --noEmit",
|
||||
"lint": "yarn prettier -c '**/*.{ts,js,json,md,yml,yaml}' '!packages/*/dist/**' '!svg-only/dist/**'",
|
||||
"test": "jest --verbose --passWithNoTests --no-cache",
|
||||
"dev:demo": "( cd packages/demo ; yarn dev )",
|
||||
"build:demo": "( cd packages/demo ; yarn build )",
|
||||
"build:action": "( cd packages/action ; yarn build )"
|
||||
}
|
||||
"lint": "prettier -c '**/*.{ts,js,json,md,yml,yaml}' '!packages/*/dist/**' '!svg-only/dist/**'",
|
||||
"dev:demo": "( cd packages/demo ; npm run dev )",
|
||||
"build:demo": "( cd packages/demo ; npm run build )",
|
||||
"build:action": "( cd packages/action ; npm run build )"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"wasm-pack"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,81 +1,5 @@
|
||||
// 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`] = `
|
||||
{
|
||||
"animationOptions": {
|
||||
@@ -148,6 +72,7 @@ exports[`should parse /out.svg?color_snake=orange&color_dots=#000,#111,#222,#333
|
||||
"colorEmpty": "#000",
|
||||
"colorSnake": "orange",
|
||||
"dark": {
|
||||
"colorDotBorder": "#1b1f230a",
|
||||
"colorDots": [
|
||||
"#a00",
|
||||
"#a11",
|
||||
@@ -156,6 +81,7 @@ exports[`should parse /out.svg?color_snake=orange&color_dots=#000,#111,#222,#333
|
||||
"#a44",
|
||||
],
|
||||
"colorEmpty": "#a00",
|
||||
"colorSnake": "orange",
|
||||
},
|
||||
"sizeCell": 16,
|
||||
"sizeDot": 12,
|
||||
@@ -183,18 +109,7 @@ exports[`should parse path/to/out.gif 1`] = `
|
||||
],
|
||||
"colorEmpty": "#ebedf0",
|
||||
"colorSnake": "purple",
|
||||
"dark": {
|
||||
"colorDotBorder": "#1b1f230a",
|
||||
"colorDots": [
|
||||
"#161b22",
|
||||
"#01311f",
|
||||
"#034525",
|
||||
"#0f6d31",
|
||||
"#00c647",
|
||||
],
|
||||
"colorEmpty": "#161b22",
|
||||
"colorSnake": "purple",
|
||||
},
|
||||
"dark": undefined,
|
||||
"sizeCell": 16,
|
||||
"sizeDot": 12,
|
||||
"sizeDotBorderRadius": 2,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { it, expect } from "bun:test";
|
||||
import { generateContributionSnake } from "../generateContributionSnake";
|
||||
import { parseOutputsOption } from "../outputsOptions";
|
||||
|
||||
jest.setTimeout(2 * 60 * 1000);
|
||||
|
||||
const silent = (handler: () => void | Promise<void>) => async () => {
|
||||
const originalConsoleLog = console.log;
|
||||
console.log = () => undefined;
|
||||
@@ -30,7 +29,9 @@ it(
|
||||
|
||||
const outputs = parseOutputsOption(entries);
|
||||
|
||||
const results = await generateContributionSnake("platane", outputs);
|
||||
const results = await generateContributionSnake("platane", outputs, {
|
||||
githubToken: process.env.GITHUB_TOKEN!,
|
||||
});
|
||||
|
||||
expect(results[0]).toBeDefined();
|
||||
expect(results[1]).toBeDefined();
|
||||
@@ -39,5 +40,6 @@ it(
|
||||
fs.writeFileSync(outputs[0]!.filename, results[0]!);
|
||||
fs.writeFileSync(outputs[1]!.filename, results[1]!);
|
||||
fs.writeFileSync(outputs[2]!.filename, results[2]!);
|
||||
})
|
||||
}),
|
||||
{ timeout: 2 * 60 * 1000 },
|
||||
);
|
||||
|
||||
@@ -1,19 +1,61 @@
|
||||
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",
|
||||
|
||||
// overwrite colors (search params)
|
||||
"/out.svg?color_snake=orange&color_dots=#000,#111,#222,#333,#444",
|
||||
|
||||
// overwrite colors (json)
|
||||
`/out.svg?{"color_snake":"yellow","color_dots":["#000","#111","#222","#333","#444"]}`,
|
||||
|
||||
`/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?.gif.svg?color_snake=orange",
|
||||
].forEach((entry) =>
|
||||
it(`should parse ${entry}`, () => {
|
||||
expect(parseEntry(entry)).toMatchSnapshot();
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -12,10 +12,11 @@ export const generateContributionSnake = async (
|
||||
format: "svg" | "gif";
|
||||
drawOptions: DrawOptions;
|
||||
animationOptions: AnimationOptions;
|
||||
} | null)[]
|
||||
} | null)[],
|
||||
options: { githubToken: string },
|
||||
) => {
|
||||
console.log("🎣 fetching github user contribution");
|
||||
const cells = await getGithubUserContribution(userName);
|
||||
const cells = await getGithubUserContribution(userName, options);
|
||||
|
||||
const grid = userContributionToGrid(cells);
|
||||
const snake = snake4;
|
||||
@@ -42,10 +43,10 @@ export const generateContributionSnake = async (
|
||||
cells,
|
||||
chain,
|
||||
drawOptions,
|
||||
animationOptions
|
||||
animationOptions,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,13 +10,17 @@ import { parseOutputsOption } from "./outputsOptions";
|
||||
core.getMultilineInput("outputs") ?? [
|
||||
core.getInput("gif_out_path"),
|
||||
core.getInput("svg_out_path"),
|
||||
]
|
||||
],
|
||||
);
|
||||
const githubToken =
|
||||
process.env.GITHUB_TOKEN ?? core.getInput("github_token");
|
||||
|
||||
const { generateContributionSnake } = await import(
|
||||
"./generateContributionSnake"
|
||||
);
|
||||
const results = await generateContributionSnake(userName, outputs);
|
||||
const results = await generateContributionSnake(userName, outputs, {
|
||||
githubToken,
|
||||
});
|
||||
|
||||
outputs.forEach((out, i) => {
|
||||
const result = results[i];
|
||||
|
||||
@@ -32,6 +32,7 @@ export const parseEntry = (entry: string) => {
|
||||
sizeCell: 16,
|
||||
sizeDot: 12,
|
||||
...palettes["default"],
|
||||
dark: palettes["default"].dark && { ...palettes["default"].dark },
|
||||
};
|
||||
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_dots")) {
|
||||
const colors = sp.get("color_dots")!.split(/[,;]/);
|
||||
@@ -56,6 +65,8 @@ export const parseEntry = (entry: string) => {
|
||||
if (sp.has("dark_color_dots")) {
|
||||
const colors = sp.get("dark_color_dots")!.split(/[,;]/);
|
||||
drawOptions.dark = {
|
||||
colorDotBorder: drawOptions.colorDotBorder,
|
||||
colorSnake: drawOptions.colorSnake,
|
||||
...drawOptions.dark,
|
||||
colorDots: colors,
|
||||
colorEmpty: colors[0],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@snk/action",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@actions/core": "1.10.0",
|
||||
"@actions/core": "1.11.1",
|
||||
"@snk/gif-creator": "1.0.0",
|
||||
"@snk/github-user-contribution": "1.0.0",
|
||||
"@snk/solver": "1.0.0",
|
||||
@@ -10,10 +10,9 @@
|
||||
"@snk/types": "1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vercel/ncc": "0.34.0"
|
||||
"@vercel/ncc": "0.38.3"
|
||||
},
|
||||
"scripts": {
|
||||
"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"
|
||||
"build": "ncc build --external canvas --external gifsicle --out dist ./index.ts"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DrawOptions as DrawOptions } from "@snk/svg-creator";
|
||||
|
||||
export const palettes: Record<
|
||||
export const basePalettes: Record<
|
||||
string,
|
||||
Pick<
|
||||
DrawOptions,
|
||||
@@ -22,8 +22,6 @@ export const palettes: Record<
|
||||
};
|
||||
|
||||
// aliases
|
||||
palettes["github"] = {
|
||||
...palettes["github-light"],
|
||||
dark: { ...palettes["github-dark"] },
|
||||
};
|
||||
export const palettes = { ...basePalettes };
|
||||
palettes["github"] = palettes["github-light"];
|
||||
palettes["default"] = palettes["github"];
|
||||
|
||||
@@ -76,7 +76,7 @@ export const createCanvas = ({
|
||||
snake0: Snake,
|
||||
snake1: Snake,
|
||||
stack: Color[],
|
||||
k: number
|
||||
k: number,
|
||||
) => {
|
||||
ctx.clearRect(0, 0, 9999, 9999);
|
||||
drawLerpWorld(ctx, grid, null, snake0, snake1, stack, k, drawOptions);
|
||||
|
||||
@@ -26,7 +26,7 @@ const tunnels = ones.map(({ x, y }) => ({
|
||||
x,
|
||||
y,
|
||||
3 as Color,
|
||||
getSnakeLength(snake)
|
||||
getSnakeLength(snake),
|
||||
),
|
||||
}));
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ const { canvas, ctx, draw, highlightCell } = createCanvas(grid);
|
||||
canvas.style.pointerEvents = "auto";
|
||||
|
||||
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)!];
|
||||
|
||||
@@ -14,6 +14,7 @@ import { createSvg } from "@snk/svg-creator";
|
||||
import { createRpcClient } from "./worker-utils";
|
||||
import type { API as WorkerAPI } from "./demo.interactive.worker";
|
||||
import { AnimationOptions } from "@snk/gif-creator";
|
||||
import { basePalettes } from "@snk/action/palettes";
|
||||
|
||||
const createForm = ({
|
||||
onSubmit,
|
||||
@@ -119,13 +120,18 @@ const createViewer = ({
|
||||
grid0,
|
||||
chain,
|
||||
cells,
|
||||
drawOptions,
|
||||
}: {
|
||||
grid0: Grid;
|
||||
chain: Snake[];
|
||||
cells: Point[];
|
||||
drawOptions: DrawOptions;
|
||||
}) => {
|
||||
const drawOptions: DrawOptions = {
|
||||
sizeDotBorderRadius: 2,
|
||||
sizeCell: 16,
|
||||
sizeDot: 12,
|
||||
...basePalettes["github-light"],
|
||||
};
|
||||
|
||||
//
|
||||
// canvas
|
||||
const canvas = document.createElement("canvas");
|
||||
@@ -171,12 +177,12 @@ const createViewer = ({
|
||||
|
||||
//
|
||||
// controls
|
||||
const input = document.createElement("input") as any;
|
||||
const input = document.createElement("input");
|
||||
input.type = "range";
|
||||
input.value = 0;
|
||||
input.step = 1;
|
||||
input.min = 0;
|
||||
input.max = chain.length;
|
||||
input.value = "0";
|
||||
input.step = "1";
|
||||
input.min = "0";
|
||||
input.max = "" + chain.length;
|
||||
input.style.width = "calc( 100% - 20px )";
|
||||
input.addEventListener("input", () => {
|
||||
spring.target = +input.value;
|
||||
@@ -190,10 +196,49 @@ const createViewer = ({
|
||||
window.addEventListener("click", onClickBackground);
|
||||
document.body.append(input);
|
||||
|
||||
//
|
||||
const schemaSelect = document.createElement("select");
|
||||
schemaSelect.style.margin = "10px";
|
||||
schemaSelect.style.alignSelf = "flex-start";
|
||||
schemaSelect.value = "github-light";
|
||||
schemaSelect.addEventListener("change", () => {
|
||||
Object.assign(drawOptions, basePalettes[schemaSelect.value]);
|
||||
|
||||
svgString = createSvg(grid0, cells, chain, drawOptions, {
|
||||
frameDuration: 100,
|
||||
} as AnimationOptions);
|
||||
const svgImageUri = `data:image/*;charset=utf-8;base64,${btoa(svgString)}`;
|
||||
svgLink.href = svgImageUri;
|
||||
|
||||
if (schemaSelect.value.includes("dark"))
|
||||
document.body.parentElement?.classList.add("dark-mode");
|
||||
else document.body.parentElement?.classList.remove("dark-mode");
|
||||
|
||||
loop();
|
||||
});
|
||||
for (const name of Object.keys(basePalettes)) {
|
||||
const option = document.createElement("option");
|
||||
option.value = name;
|
||||
option.innerText = name;
|
||||
schemaSelect.appendChild(option);
|
||||
}
|
||||
document.body.append(schemaSelect);
|
||||
|
||||
//
|
||||
// dark mode
|
||||
const style = document.createElement("style");
|
||||
style.innerText = `
|
||||
html { transition:background-color 180ms }
|
||||
a { transition:color 180ms }
|
||||
html.dark-mode{ background-color:#0d1117 }
|
||||
html.dark-mode a{ color:rgb(201, 209, 217) }
|
||||
`;
|
||||
document.head.append(style);
|
||||
|
||||
//
|
||||
// svg
|
||||
const svgLink = document.createElement("a");
|
||||
const svgString = createSvg(grid0, cells, chain, drawOptions, {
|
||||
let svgString = createSvg(grid0, cells, chain, drawOptions, {
|
||||
frameDuration: 100,
|
||||
} as AnimationOptions);
|
||||
const svgImageUri = `data:image/*;charset=utf-8;base64,${btoa(svgString)}`;
|
||||
@@ -203,9 +248,12 @@ const createViewer = ({
|
||||
svgLink.addEventListener("click", (e) => {
|
||||
const w = window.open("")!;
|
||||
w.document.write(
|
||||
`<a href="${svgImageUri}" download="github-user-contribution.svg">` +
|
||||
(document.body.parentElement?.classList.contains("dark-mode")
|
||||
? "<style>html{ background-color:#0d1117 }</style>"
|
||||
: "") +
|
||||
`<a href="${svgLink.href}" download="github-user-contribution.svg">` +
|
||||
svgString +
|
||||
"<a/>"
|
||||
"<a/>",
|
||||
);
|
||||
e.preventDefault();
|
||||
});
|
||||
@@ -229,35 +277,25 @@ const createViewer = ({
|
||||
|
||||
const onSubmit = async (userName: string) => {
|
||||
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 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 chain = await getChain(grid);
|
||||
|
||||
dispose();
|
||||
|
||||
createViewer({ grid0: grid, chain, cells, drawOptions });
|
||||
createViewer({ grid0: grid, chain, cells });
|
||||
};
|
||||
|
||||
const worker = new Worker(
|
||||
new URL(
|
||||
"./demo.interactive.worker.ts",
|
||||
// @ts-ignore
|
||||
import.meta.url
|
||||
)
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
|
||||
const { getChain } = createRpcClient<WorkerAPI>(worker);
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
"outside",
|
||||
"getPathToPose",
|
||||
"getPathTo",
|
||||
"svg"
|
||||
"svg",
|
||||
"rust"
|
||||
]
|
||||
|
||||
24
packages/demo/demo.rust.ts
Normal file
24
packages/demo/demo.rust.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { createCanvas } from "./canvas";
|
||||
import "./menu";
|
||||
import { grid } from "./sample";
|
||||
|
||||
(async () => {
|
||||
const api = await import("@snk/solver-r");
|
||||
|
||||
const g = api.IGrid.create(grid.width, grid.height, grid.data);
|
||||
|
||||
const freeCells = api.iget_free_cell(g);
|
||||
|
||||
{
|
||||
const { canvas, draw, highlightCell } = createCanvas(g);
|
||||
document.body.appendChild(canvas);
|
||||
|
||||
draw({ width: g.width, height: g.height, data: g.data }, [] as any, []);
|
||||
|
||||
for (let i = freeCells.length / 2; i--; ) {
|
||||
const x = freeCells[i * 2 + 0];
|
||||
const y = freeCells[i * 2 + 1];
|
||||
highlightCell(x, y);
|
||||
}
|
||||
}
|
||||
})();
|
||||
@@ -25,7 +25,7 @@ const onChange = () => {
|
||||
|
||||
const url = new URL(
|
||||
config.demo + ".html?" + search,
|
||||
window.location.href
|
||||
window.location.href,
|
||||
).toString();
|
||||
|
||||
window.location.href = url;
|
||||
|
||||
@@ -10,14 +10,15 @@
|
||||
"@snk/types": "1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dat.gui": "0.7.7",
|
||||
"@types/dat.gui": "0.7.13",
|
||||
"dat.gui": "0.7.9",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"ts-loader": "9.4.1",
|
||||
"ts-node": "10.9.1",
|
||||
"webpack": "5.74.0",
|
||||
"webpack-cli": "4.10.0",
|
||||
"webpack-dev-server": "4.11.1"
|
||||
"dotenv": "16.4.7",
|
||||
"html-webpack-plugin": "5.6.3",
|
||||
"ts-loader": "9.5.2",
|
||||
"ts-node": "10.9.2",
|
||||
"webpack": "5.98.0",
|
||||
"webpack-cli": "6.0.1",
|
||||
"webpack-dev-server": "5.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
|
||||
@@ -15,7 +15,7 @@ const stepSpringOne = (
|
||||
maxVelocity = Infinity,
|
||||
}: { tension: number; friction: number; maxVelocity?: number },
|
||||
target: number,
|
||||
dt = 1 / 60
|
||||
dt = 1 / 60,
|
||||
) => {
|
||||
const a = -tension * (s.x - target) - friction * s.v;
|
||||
|
||||
@@ -31,13 +31,13 @@ const stepSpringOne = (
|
||||
export const isStable = (
|
||||
s: { x: number; v: number },
|
||||
target: number,
|
||||
dt = 1 / 60
|
||||
dt = 1 / 60,
|
||||
) => Math.abs(s.x - target) < epsilon && Math.abs(s.v * dt) < epsilon;
|
||||
|
||||
export const isStableAndBound = (
|
||||
s: { x: number; v: number },
|
||||
target: number,
|
||||
dt?: number
|
||||
dt?: number,
|
||||
) => {
|
||||
const stable = isStable(s, target, dt);
|
||||
if (stable) {
|
||||
@@ -51,7 +51,7 @@ export const stepSpring = (
|
||||
s: { x: number; v: number },
|
||||
params: { tension: number; friction: number; maxVelocity?: number },
|
||||
target: number,
|
||||
dt = 1 / 60
|
||||
dt = 1 / 60,
|
||||
) => {
|
||||
const interval = 1 / 60;
|
||||
|
||||
|
||||
@@ -1,27 +1,40 @@
|
||||
import path from "path";
|
||||
import HtmlWebpackPlugin from "html-webpack-plugin";
|
||||
|
||||
import type { Configuration as WebpackConfiguration } from "webpack";
|
||||
import type { Configuration as WebpackDevServerConfiguration } from "webpack-dev-server";
|
||||
import webpack from "webpack";
|
||||
import { getGithubUserContribution } from "@snk/github-user-contribution";
|
||||
import 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 webpackDevServerConfiguration: WebpackDevServerConfiguration = {
|
||||
open: { target: demos[1] + ".html" },
|
||||
onAfterSetupMiddleware: ({ app }) => {
|
||||
app!.get("/api/github-user-contribution/:userName", async (req, res) => {
|
||||
const userName: string = req.params.userName;
|
||||
res.send(await getGithubUserContribution(userName));
|
||||
});
|
||||
},
|
||||
setupMiddlewares: (ms) => [
|
||||
...ms,
|
||||
(async (req, res, next) => {
|
||||
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 = {
|
||||
mode: "development",
|
||||
entry: Object.fromEntries(
|
||||
demos.map((demo: string) => [demo, `./demo.${demo}`])
|
||||
demos.map((demo: string) => [demo, `./demo.${demo}`]),
|
||||
),
|
||||
target: ["web", "es2019"],
|
||||
resolve: { extensions: [".ts", ".js"] },
|
||||
@@ -29,6 +42,9 @@ const webpackConfiguration: WebpackConfiguration = {
|
||||
path: path.join(__dirname, "dist"),
|
||||
filename: "[contenthash].js",
|
||||
},
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
@@ -52,7 +68,7 @@ const webpackConfiguration: WebpackConfiguration = {
|
||||
title: "snk - " + demo,
|
||||
filename: `${demo}.html`,
|
||||
chunks: [demo],
|
||||
})
|
||||
}),
|
||||
),
|
||||
new HtmlWebpackPlugin({
|
||||
title: "snk - " + demos[0],
|
||||
|
||||
@@ -54,6 +54,6 @@ export const createRpcClient = <API_ extends API>(worker: Worker) => {
|
||||
worker.addEventListener("terminate", onTerminate);
|
||||
worker.postMessage({ symbol, key, methodName, args });
|
||||
}),
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -59,7 +59,7 @@ export const getCircleSize = (n: number) => {
|
||||
export const drawCircleStack = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
stack: Color[],
|
||||
o: Options
|
||||
o: Options,
|
||||
) => {
|
||||
for (let i = stack.length; i--; ) {
|
||||
const { x, y } = cellPath[i];
|
||||
@@ -67,7 +67,7 @@ export const drawCircleStack = (
|
||||
ctx.save();
|
||||
ctx.translate(
|
||||
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
|
||||
|
||||
@@ -16,7 +16,7 @@ export const drawGrid = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
grid: Grid,
|
||||
cells: Point[] | null,
|
||||
o: Options
|
||||
o: Options,
|
||||
) => {
|
||||
for (let x = grid.width; x--; )
|
||||
for (let y = grid.height; y--; ) {
|
||||
@@ -27,7 +27,7 @@ export const drawGrid = (
|
||||
ctx.save();
|
||||
ctx.translate(
|
||||
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;
|
||||
|
||||
@@ -10,7 +10,7 @@ type Options = {
|
||||
export const drawSnake = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
snake: Snake,
|
||||
o: Options
|
||||
o: Options,
|
||||
) => {
|
||||
const cells = snakeToCells(snake);
|
||||
|
||||
@@ -25,7 +25,7 @@ export const drawSnake = (
|
||||
ctx,
|
||||
o.sizeCell - u * 2,
|
||||
o.sizeCell - u * 2,
|
||||
(o.sizeCell - u * 2) * 0.25
|
||||
(o.sizeCell - u * 2) * 0.25,
|
||||
);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
@@ -40,7 +40,7 @@ export const drawSnakeLerp = (
|
||||
snake0: Snake,
|
||||
snake1: Snake,
|
||||
k: number,
|
||||
o: Options
|
||||
o: Options,
|
||||
) => {
|
||||
const m = 0.8;
|
||||
const n = snake0.length / 2;
|
||||
@@ -61,7 +61,7 @@ export const drawSnakeLerp = (
|
||||
ctx,
|
||||
o.sizeCell - u * 2,
|
||||
o.sizeCell - u * 2,
|
||||
(o.sizeCell - u * 2) * 0.25
|
||||
(o.sizeCell - u * 2) * 0.25,
|
||||
);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
|
||||
@@ -19,7 +19,7 @@ export const drawStack = (
|
||||
stack: Color[],
|
||||
max: number,
|
||||
width: number,
|
||||
o: { colorDots: Record<Color, string> }
|
||||
o: { colorDots: Record<Color, string> },
|
||||
) => {
|
||||
ctx.save();
|
||||
|
||||
@@ -39,7 +39,7 @@ export const drawWorld = (
|
||||
cells: Point[] | null,
|
||||
snake: Snake,
|
||||
stack: Color[],
|
||||
o: Options
|
||||
o: Options,
|
||||
) => {
|
||||
ctx.save();
|
||||
|
||||
@@ -66,14 +66,14 @@ export const drawWorld = (
|
||||
};
|
||||
|
||||
export const drawLerpWorld = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
ctx: CanvasRenderingContext2D | CanvasRenderingContext2D,
|
||||
grid: Grid,
|
||||
cells: Point[] | null,
|
||||
snake0: Snake,
|
||||
snake1: Snake,
|
||||
stack: Color[],
|
||||
k: number,
|
||||
o: Options
|
||||
o: Options,
|
||||
) => {
|
||||
ctx.save();
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ export const pathRoundedRect = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number,
|
||||
borderRadius: number
|
||||
borderRadius: number,
|
||||
) => {
|
||||
ctx.moveTo(borderRadius, 0);
|
||||
ctx.arcTo(width, 0, width, height, borderRadius);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getPathToPose } from "@snk/solver/getPathToPose";
|
||||
import type { Options as DrawOptions } from "@snk/draw/drawWorld";
|
||||
|
||||
let snake = createSnakeFromCells(
|
||||
Array.from({ length: 4 }, (_, i) => ({ x: i, y: -1 }))
|
||||
Array.from({ length: 4 }, (_, i) => ({ x: i, y: -1 })),
|
||||
);
|
||||
|
||||
// const chain = [snake];
|
||||
@@ -45,7 +45,7 @@ const animationOptions: AnimationOptions = { frameDuration: 100, step: 1 };
|
||||
) {
|
||||
const stats: number[] = [];
|
||||
|
||||
let buffer: Buffer;
|
||||
let buffer: Uint8Array;
|
||||
const start = Date.now();
|
||||
const chainL = chain.slice(0, length);
|
||||
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,
|
||||
chainL,
|
||||
drawOptions,
|
||||
animationOptions
|
||||
animationOptions,
|
||||
);
|
||||
stats.push(performance.now() - s);
|
||||
}
|
||||
@@ -73,12 +73,12 @@ const animationOptions: AnimationOptions = { frameDuration: 100, step: 1 };
|
||||
})}ms`,
|
||||
"",
|
||||
].join("\n"),
|
||||
stats
|
||||
stats,
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
`__tests__/__snapshots__/benchmark-output-${length}.gif`,
|
||||
buffer!
|
||||
buffer!,
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { it, expect } from "bun:test";
|
||||
import { AnimationOptions, createGif } from "..";
|
||||
import * as grids from "@snk/types/__fixtures__/grid";
|
||||
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 type { Options as DrawOptions } from "@snk/draw/drawWorld";
|
||||
|
||||
jest.setTimeout(20 * 1000);
|
||||
|
||||
const upscale = 1;
|
||||
const drawOptions: DrawOptions = {
|
||||
sizeDotBorderRadius: 2 * upscale,
|
||||
@@ -35,44 +34,58 @@ for (const key of [
|
||||
"small",
|
||||
"smallPacked",
|
||||
] as const)
|
||||
it(`should generate ${key} gif`, async () => {
|
||||
const grid = grids[key];
|
||||
it(
|
||||
`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(
|
||||
grid,
|
||||
null,
|
||||
chain,
|
||||
drawOptions,
|
||||
animationOptions
|
||||
animationOptions,
|
||||
);
|
||||
|
||||
expect(gif).toBeDefined();
|
||||
|
||||
fs.writeFileSync(path.resolve(dir, key + ".gif"), gif);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
fs.writeFileSync(path.resolve(dir, "swipper.gif"), gif);
|
||||
},
|
||||
{ timeout: 20 * 1000 },
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ import gifsicle from "gifsicle";
|
||||
import GIFEncoder from "gif-encoder-2";
|
||||
|
||||
const withTmpDir = async <T>(
|
||||
handler: (dir: string) => Promise<T>
|
||||
handler: (dir: string) => Promise<T>,
|
||||
): Promise<T> => {
|
||||
const { name: dir, removeCallback: cleanUp } = tmp.dirSync({
|
||||
unsafeCleanup: true,
|
||||
@@ -37,13 +37,13 @@ export const createGif = async (
|
||||
cells: Point[] | null,
|
||||
chain: Snake[],
|
||||
drawOptions: DrawOptions,
|
||||
animationOptions: AnimationOptions
|
||||
animationOptions: AnimationOptions,
|
||||
) =>
|
||||
withTmpDir(async (dir) => {
|
||||
const { width, height } = getCanvasWorldSize(grid0, drawOptions);
|
||||
|
||||
const canvas = createCanvas(width, height);
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
const ctx = canvas.getContext("2d") as any as CanvasRenderingContext2D;
|
||||
|
||||
const grid = copyGrid(grid0);
|
||||
const stack: Color[] = [];
|
||||
@@ -70,7 +70,7 @@ export const createGif = async (
|
||||
snake1,
|
||||
stack,
|
||||
k / animationOptions.step,
|
||||
drawOptions
|
||||
drawOptions,
|
||||
);
|
||||
|
||||
encoder.addFrame(ctx);
|
||||
@@ -92,8 +92,8 @@ export const createGif = async (
|
||||
"--colors=18",
|
||||
outFileName,
|
||||
["--output", optimizedFileName],
|
||||
].flat()
|
||||
].flat(),
|
||||
);
|
||||
|
||||
return fs.readFileSync(optimizedFileName);
|
||||
return new Uint8Array(fs.readFileSync(optimizedFileName));
|
||||
});
|
||||
|
||||
@@ -4,17 +4,16 @@
|
||||
"dependencies": {
|
||||
"@snk/draw": "1.0.0",
|
||||
"@snk/solver": "1.0.0",
|
||||
"canvas": "2.10.2",
|
||||
"canvas": "3.1.0",
|
||||
"gif-encoder-2": "1.0.5",
|
||||
"gifsicle": "5.3.0",
|
||||
"tmp": "0.2.1"
|
||||
"tmp": "0.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/gifsicle": "5.2.0",
|
||||
"@types/tmp": "0.2.3",
|
||||
"@vercel/ncc": "0.34.0"
|
||||
"@types/gifsicle": "5.2.2",
|
||||
"@types/tmp": "0.2.6"
|
||||
},
|
||||
"scripts": {
|
||||
"benchmark": "ncc run __tests__/benchmark.ts --quiet"
|
||||
"benchmark": "bun __tests__/benchmark.ts"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
# @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
|
||||
|
||||
```
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
52
packages/github-user-contribution-service/index.ts
Normal file
52
packages/github-user-contribution-service/index.ts
Normal 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",
|
||||
},
|
||||
});
|
||||
}),
|
||||
};
|
||||
@@ -2,7 +2,13 @@
|
||||
"name": "@snk/github-user-contribution-service",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@snk/github-user-contribution": "1.0.0",
|
||||
"@vercel/node": "2.6.1"
|
||||
"@snk/github-user-contribution": "1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"wrangler": "3.109.2",
|
||||
"@cloudflare/workers-types": "4.20250214.0"
|
||||
},
|
||||
"scripts": {
|
||||
"deploy": "wrangler deploy"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"github": {
|
||||
"silent": true
|
||||
}
|
||||
}
|
||||
9
packages/github-user-contribution-service/wrangler.toml
Normal file
9
packages/github-user-contribution-service/wrangler.toml
Normal 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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +0,0 @@
|
||||
import { formatParams } from "../formatParams";
|
||||
|
||||
const params = [
|
||||
//
|
||||
[{}, ""],
|
||||
[{ year: 2017 }, "from=2017-01-01&to=2017-12-31"],
|
||||
[{ from: "2017-12-03" }, "from=2017-12-03"],
|
||||
[{ to: "2017-12-03" }, "to=2017-12-03"],
|
||||
] as const;
|
||||
|
||||
params.forEach(([params, res]) =>
|
||||
it(`should format ${JSON.stringify(params)}`, () => {
|
||||
expect(formatParams(params)).toBe(res);
|
||||
})
|
||||
);
|
||||
|
||||
it("should fail if the date is in the future", () => {
|
||||
expect(() => formatParams({ to: "9999-01-01" })).toThrow(Error);
|
||||
});
|
||||
@@ -1,7 +1,10 @@
|
||||
import { getGithubUserContribution } from "..";
|
||||
import { describe, it, expect } from "bun:test";
|
||||
|
||||
describe("getGithubUserContribution", () => {
|
||||
const promise = getGithubUserContribution("platane");
|
||||
const promise = getGithubUserContribution("platane", {
|
||||
githubToken: process.env.GITHUB_TOKEN!,
|
||||
});
|
||||
|
||||
it("should resolve", async () => {
|
||||
await promise;
|
||||
@@ -27,9 +30,3 @@ describe("getGithubUserContribution", () => {
|
||||
expect(undefinedDays).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
xit("should match snapshot for year=2019", async () => {
|
||||
expect(
|
||||
await getGithubUserContribution("platane", { year: 2019 })
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
export type Options = { from?: string; to?: string } | { year: number };
|
||||
|
||||
export const formatParams = (options: Options = {}) => {
|
||||
const sp = new URLSearchParams();
|
||||
|
||||
const o: any = { ...options };
|
||||
|
||||
if ("year" in options) {
|
||||
o.from = `${options.year}-01-01`;
|
||||
o.to = `${options.year}-12-31`;
|
||||
}
|
||||
|
||||
for (const s of ["from", "to"])
|
||||
if (o[s]) {
|
||||
const value = o[s];
|
||||
|
||||
if (value >= formatDate(new Date()))
|
||||
throw new Error(
|
||||
"Cannot get contribution for a date in the future.\nPlease limit your range to the current UTC day."
|
||||
);
|
||||
|
||||
sp.set(s, value);
|
||||
}
|
||||
|
||||
return sp.toString();
|
||||
};
|
||||
|
||||
const formatDate = (d: Date) => {
|
||||
const year = d.getUTCFullYear();
|
||||
const month = d.getUTCMonth() + 1;
|
||||
const date = d.getUTCDate();
|
||||
|
||||
return [
|
||||
year,
|
||||
month.toString().padStart(2, "0"),
|
||||
date.toString().padStart(2, "0"),
|
||||
].join("-");
|
||||
};
|
||||
@@ -1,6 +1,3 @@
|
||||
import fetch from "node-fetch";
|
||||
import { formatParams, Options } from "./formatParams";
|
||||
|
||||
/**
|
||||
* get the contribution grid from a github user page
|
||||
*
|
||||
@@ -19,57 +16,84 @@ import { formatParams, Options } from "./formatParams";
|
||||
*/
|
||||
export const getGithubUserContribution = async (
|
||||
userName: string,
|
||||
options: Options = {}
|
||||
o: { githubToken: string },
|
||||
) => {
|
||||
// either use github.com/users/xxxx/contributions for previous years
|
||||
// or github.com/xxxx ( which gives the latest update to today result )
|
||||
const url =
|
||||
"year" in options || "from" in options || "to" in options
|
||||
? `https://github.com/users/${userName}/contributions?` +
|
||||
formatParams(options)
|
||||
: `https://github.com/${userName}`;
|
||||
const query = /* GraphQL */ `
|
||||
query ($login: String!) {
|
||||
user(login: $login) {
|
||||
contributionsCollection {
|
||||
contributionCalendar {
|
||||
weeks {
|
||||
contributionDays {
|
||||
contributionCount
|
||||
contributionLevel
|
||||
weekday
|
||||
date
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const variables = { login: userName };
|
||||
|
||||
const res = await fetch(url);
|
||||
|
||||
if (!res.ok) throw new Error(res.statusText);
|
||||
|
||||
const resText = await res.text();
|
||||
|
||||
return parseUserPage(resText);
|
||||
};
|
||||
|
||||
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 res = await fetch("https://api.github.com/graphql", {
|
||||
headers: {
|
||||
Authorization: `bearer ${o.githubToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "me@platane.me",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({ variables, query }),
|
||||
});
|
||||
|
||||
const yAttributes = Array.from(
|
||||
new Set(rects.map((c) => c.yAttribute)).keys()
|
||||
).sort();
|
||||
if (!res.ok) throw new Error(await res.text().catch(() => res.statusText));
|
||||
|
||||
const cells = rects.map(({ yAttribute, ...c }) => ({
|
||||
y: yAttributes.indexOf(yAttribute),
|
||||
...c,
|
||||
}));
|
||||
const { data, errors } = (await res.json()) as {
|
||||
data: GraphQLRes;
|
||||
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>>;
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
{
|
||||
"name": "@snk/github-user-contribution",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"cheerio": "1.0.0-rc.10",
|
||||
"node-fetch": "2.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node-fetch": "2.6.1"
|
||||
}
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
||||
2
packages/solver-r/.gitignore
vendored
Normal file
2
packages/solver-r/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
target
|
||||
pkg
|
||||
341
packages/solver-r/Cargo.lock
generated
Normal file
341
packages/solver-r/Cargo.lock
generated
Normal file
@@ -0,0 +1,341 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "console_error_panic_hook"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console_log"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f"
|
||||
dependencies = [
|
||||
"log",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
|
||||
|
||||
[[package]]
|
||||
name = "minicov"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f27fe9f1cc3c22e1687f9446c2083c4c5fc7f0bcf1c7a86bdbded14985895b4b"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "snk-solver-rust"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test"
|
||||
version = "0.3.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"minicov",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-bindgen-test-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test-macro"
|
||||
version = "0.3.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
26
packages/solver-r/Cargo.toml
Normal file
26
packages/solver-r/Cargo.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "snk-solver-rust"
|
||||
version = "1.0.0"
|
||||
authors = ["platane"]
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = ["console_error_panic_hook"]
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2.100"
|
||||
js-sys = "0.3.77"
|
||||
console_log = "1.0.0"
|
||||
log = "0.4"
|
||||
|
||||
# The `console_error_panic_hook` crate provides better debugging of panics by
|
||||
# logging them with `console.error`. This is great for development, but requires
|
||||
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
|
||||
# code size when deploying.
|
||||
console_error_panic_hook = { version = "0.1.7", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.34"
|
||||
11
packages/solver-r/package.json
Normal file
11
packages/solver-r/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@snk/solver-r",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"wasm-pack": "0.13.1"
|
||||
},
|
||||
"main": "./pkg/snk_solver_rust.js",
|
||||
"scripts": {
|
||||
"build": "wasm-pack build"
|
||||
}
|
||||
}
|
||||
84
packages/solver-r/src/grid.rs
Normal file
84
packages/solver-r/src/grid.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)]
|
||||
pub struct Point {
|
||||
pub x: i8,
|
||||
pub y: i8,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
|
||||
#[repr(u8)]
|
||||
pub enum Cell {
|
||||
Empty = 0,
|
||||
Color1 = 1,
|
||||
Color2 = 2,
|
||||
Color3 = 3,
|
||||
Color4 = 4,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Grid {
|
||||
pub width: u8,
|
||||
pub height: u8,
|
||||
pub cells: Vec<Cell>,
|
||||
}
|
||||
impl Grid {
|
||||
pub fn create_empty(width: u8, height: u8) -> Grid {
|
||||
let n = (width as usize) * (height as usize);
|
||||
let cells = (0..n).map(|_| Cell::Empty).collect();
|
||||
|
||||
Grid {
|
||||
width,
|
||||
height,
|
||||
cells,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_index(&self, x: i8, y: i8) -> usize {
|
||||
return (x as usize) * (self.height as usize) + (y as usize);
|
||||
}
|
||||
pub fn get_cell(&self, p: &Point) -> Cell {
|
||||
let i = self.get_index(p.x, p.y);
|
||||
return self.cells[i];
|
||||
}
|
||||
pub fn set_cell(&mut self, p: &Point, value: Cell) -> () {
|
||||
let i = self.get_index(p.x, p.y);
|
||||
self.cells[i] = value;
|
||||
}
|
||||
pub fn is_inside(&self, p: &Point) -> bool {
|
||||
p.x >= 0 && p.x < (self.width as i8) && p.y >= 0 && p.y < (self.height as i8)
|
||||
}
|
||||
}
|
||||
|
||||
pub const DIRECTION_RIGHT: Point = Point { x: 1, y: 0 };
|
||||
pub const DIRECTION_LEFT: Point = Point { x: -1, y: 0 };
|
||||
pub const DIRECTION_UP: Point = Point { x: 0, y: 1 };
|
||||
pub const DIRECTION_DOWN: Point = Point { x: 0, y: -1 };
|
||||
pub const DIRECTIONS: [Point; 4] = [
|
||||
DIRECTION_RIGHT,
|
||||
DIRECTION_LEFT,
|
||||
DIRECTION_UP,
|
||||
DIRECTION_DOWN,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn it_should_sort_cell() {
|
||||
assert_eq!(Cell::Empty < Cell::Color1, true);
|
||||
assert_eq!(Cell::Color1 < Cell::Color2, true);
|
||||
assert_eq!(Cell::Color2 < Cell::Color3, true);
|
||||
assert_eq!(Cell::Color3 < Cell::Color4, true);
|
||||
}
|
||||
#[test]
|
||||
fn it_should_grid_create() {
|
||||
let grid = Grid::create_empty(30, 10);
|
||||
|
||||
assert_eq!(grid.width, 30);
|
||||
assert_eq!(grid.height, 10);
|
||||
assert_eq!(grid.get_cell(&Point { x: 2, y: 3 }), Cell::Empty);
|
||||
}
|
||||
#[test]
|
||||
fn it_should_grid_setter() {
|
||||
let mut grid = Grid::create_empty(20, 10);
|
||||
|
||||
grid.set_cell(&Point { x: 12, y: 3 }, Cell::Color1);
|
||||
|
||||
assert_eq!(grid.get_cell(&Point { x: 12, y: 3 }), Cell::Color1);
|
||||
}
|
||||
79
packages/solver-r/src/lib.rs
Normal file
79
packages/solver-r/src/lib.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
mod grid;
|
||||
mod snake;
|
||||
mod snake_compact;
|
||||
mod snake_walk;
|
||||
mod solver;
|
||||
|
||||
use grid::{Cell, Grid};
|
||||
use js_sys;
|
||||
use solver::get_free_cell;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
fn alert(s: &str);
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn greet() {
|
||||
alert("Hello, wasm-game-of-life!");
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone)]
|
||||
pub struct IGrid {
|
||||
pub width: u8,
|
||||
pub height: u8,
|
||||
cells: Vec<Cell>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl IGrid {
|
||||
pub fn create(width: u8, height: u8, data: js_sys::Uint8Array) -> IGrid {
|
||||
let cells = data
|
||||
.to_vec()
|
||||
.iter()
|
||||
.map(|u| match u {
|
||||
0 => Cell::Empty,
|
||||
1 => Cell::Color1,
|
||||
2 => Cell::Color2,
|
||||
3 => Cell::Color3,
|
||||
4 => Cell::Color4,
|
||||
_ => panic!("unknown cell"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
IGrid {
|
||||
width,
|
||||
height,
|
||||
cells,
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn data(&self) -> js_sys::Uint8Array {
|
||||
let o: Vec<u8> = self.cells.iter().map(|u| *u as u8).collect();
|
||||
js_sys::Uint8Array::from(&o[..])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IGrid> for Grid {
|
||||
fn from(value: IGrid) -> Self {
|
||||
Self {
|
||||
width: value.width,
|
||||
height: value.height,
|
||||
cells: value.cells,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn iget_free_cell(grid: &IGrid) -> js_sys::Uint8Array {
|
||||
let g = Grid::from(grid.clone());
|
||||
|
||||
let (_, out) = get_free_cell(&g, Cell::Color1);
|
||||
|
||||
let o: Vec<u8> = out.iter().flat_map(|p| [p.x as u8, p.y as u8]).collect();
|
||||
|
||||
js_sys::Uint8Array::from(&o[..])
|
||||
}
|
||||
95
packages/solver-r/src/snake.rs
Normal file
95
packages/solver-r/src/snake.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use crate::grid::{Point, DIRECTIONS, DIRECTION_DOWN, DIRECTION_LEFT, DIRECTION_UP};
|
||||
|
||||
/**
|
||||
* head is at 0
|
||||
*/
|
||||
pub type Snake = Vec<Point>;
|
||||
|
||||
pub fn move_snake(s: &mut Snake, dir: &Point) -> () {
|
||||
let mut e = s.pop().unwrap();
|
||||
e.x = s[0].x + dir.x;
|
||||
e.y = s[0].y + dir.y;
|
||||
s.insert(0, e);
|
||||
}
|
||||
pub fn snake_will_self_collide(s: &Snake, dir: &Point) -> bool {
|
||||
let next_head = Point {
|
||||
x: s[0].x + dir.x,
|
||||
y: s[0].y + dir.y,
|
||||
};
|
||||
|
||||
(&s[0..(s.len() - 1)]).contains(&next_head)
|
||||
}
|
||||
pub fn get_snake_head(s: &Snake) -> Point {
|
||||
s[0]
|
||||
}
|
||||
pub fn get_next_snake_head(s: &Snake, dir: &Point) -> Point {
|
||||
Point {
|
||||
x: s[0].x + dir.x,
|
||||
y: s[0].y + dir.y,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_return_head() {
|
||||
let s = vec![
|
||||
//
|
||||
Point { x: 3, y: 0 },
|
||||
Point { x: 2, y: 0 },
|
||||
Point { x: 1, y: 0 },
|
||||
];
|
||||
|
||||
assert_eq!(get_snake_head(&s), Point { x: 3, y: 0 });
|
||||
}
|
||||
#[test]
|
||||
fn it_should_detect_self_collide() {
|
||||
let mut s = vec![
|
||||
//
|
||||
Point { x: 6, y: 0 },
|
||||
Point { x: 5, y: 0 },
|
||||
Point { x: 4, y: 0 },
|
||||
Point { x: 3, y: 0 },
|
||||
Point { x: 2, y: 0 },
|
||||
Point { x: 1, y: 0 },
|
||||
];
|
||||
|
||||
move_snake(&mut s, &DIRECTION_UP);
|
||||
move_snake(&mut s, &DIRECTION_LEFT);
|
||||
|
||||
assert_eq!(snake_will_self_collide(&s, &DIRECTION_DOWN), true);
|
||||
|
||||
move_snake(&mut s, &DIRECTION_LEFT);
|
||||
|
||||
assert_eq!(snake_will_self_collide(&s, &DIRECTION_DOWN), false);
|
||||
}
|
||||
#[test]
|
||||
fn it_should_detect_self_collide_2() {
|
||||
let s = vec![
|
||||
//
|
||||
Point { x: 3, y: 0 },
|
||||
Point { x: 2, y: 0 },
|
||||
Point { x: 1, y: 0 },
|
||||
];
|
||||
|
||||
assert_eq!(snake_will_self_collide(&s, &DIRECTION_LEFT), true);
|
||||
}
|
||||
#[test]
|
||||
fn it_should_move_snake() {
|
||||
let mut s = vec![
|
||||
//
|
||||
Point { x: 3, y: 0 },
|
||||
Point { x: 2, y: 0 },
|
||||
Point { x: 1, y: 0 },
|
||||
];
|
||||
|
||||
move_snake(&mut s, &DIRECTION_UP);
|
||||
|
||||
assert_eq!(
|
||||
s,
|
||||
vec![
|
||||
//
|
||||
Point { x: 3, y: 1 },
|
||||
Point { x: 3, y: 0 },
|
||||
Point { x: 2, y: 0 },
|
||||
]
|
||||
);
|
||||
}
|
||||
176
packages/solver-r/src/snake_compact.rs
Normal file
176
packages/solver-r/src/snake_compact.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
use crate::grid::Point;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
|
||||
pub enum Direction {
|
||||
Left = 0,
|
||||
Right = 1,
|
||||
Up = 2,
|
||||
Down = 3,
|
||||
}
|
||||
|
||||
fn get_direction_vector(dir: Direction) -> Point {
|
||||
match dir {
|
||||
Direction::Down => Point { x: 0, y: -1 },
|
||||
Direction::Up => Point { x: 0, y: 1 },
|
||||
Direction::Left => Point { x: -1, y: 0 },
|
||||
Direction::Right => Point { x: 1, y: 0 },
|
||||
}
|
||||
}
|
||||
fn get_direction_from_vector(v: &Point) -> Direction {
|
||||
match v {
|
||||
Point { x: 0, y: -1 } => Direction::Down,
|
||||
Point { x: 0, y: 1 } => Direction::Up,
|
||||
Point { x: -1, y: 0 } => Direction::Left,
|
||||
Point { x: 1, y: 0 } => Direction::Right,
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SnakeC {
|
||||
pub head: Point,
|
||||
pub body: Vec<Direction>,
|
||||
}
|
||||
impl SnakeC {
|
||||
pub fn get_cells(&self) -> Vec<Point> {
|
||||
let mut e = self.head.clone();
|
||||
let mut out = Vec::new();
|
||||
|
||||
out.push(e.clone());
|
||||
for dir in self.body.iter() {
|
||||
let v = get_direction_vector(*dir);
|
||||
e.x -= v.x;
|
||||
e.y -= v.y;
|
||||
out.push(e.clone());
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
pub fn is_head_self_colliding(&self) -> bool {
|
||||
self.get_cells()[1..].contains(&self.head)
|
||||
}
|
||||
|
||||
pub fn advance(&mut self, dir: Direction) -> () {
|
||||
let v = get_direction_vector(dir);
|
||||
|
||||
self.head.x += v.x;
|
||||
self.head.y += v.y;
|
||||
|
||||
self.body.pop();
|
||||
self.body.insert(0, dir);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Point>> for SnakeC {
|
||||
fn from(value: Vec<Point>) -> Self {
|
||||
let head = value.get(0).unwrap().clone();
|
||||
let body = value
|
||||
.windows(2)
|
||||
.map(|w| {
|
||||
let v = Point {
|
||||
x: w[0].x - w[1].x,
|
||||
y: w[0].y - w[1].y,
|
||||
};
|
||||
get_direction_from_vector(&v)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self { head, body }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_get_the_snake_cell() {
|
||||
let s = SnakeC {
|
||||
head: Point { x: 10, y: 5 },
|
||||
body: vec![Direction::Up, Direction::Up, Direction::Left],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
s.get_cells(),
|
||||
vec![
|
||||
//
|
||||
Point { x: 10, y: 5 },
|
||||
Point { x: 10, y: 4 },
|
||||
Point { x: 10, y: 3 },
|
||||
Point { x: 11, y: 3 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_get_snake_from_point_list() {
|
||||
let s = SnakeC::from(vec![
|
||||
//
|
||||
Point { x: 10, y: 5 },
|
||||
Point { x: 10, y: 4 },
|
||||
Point { x: 10, y: 3 },
|
||||
Point { x: 11, y: 3 },
|
||||
Point { x: 12, y: 3 },
|
||||
Point { x: 12, y: 2 },
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
s.get_cells(),
|
||||
vec![
|
||||
//
|
||||
Point { x: 10, y: 5 },
|
||||
Point { x: 10, y: 4 },
|
||||
Point { x: 10, y: 3 },
|
||||
Point { x: 11, y: 3 },
|
||||
Point { x: 12, y: 3 },
|
||||
Point { x: 12, y: 2 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_advance_snake() {
|
||||
let mut s = SnakeC::from(vec![
|
||||
//
|
||||
Point { x: 10, y: 3 },
|
||||
Point { x: 11, y: 3 },
|
||||
Point { x: 12, y: 3 },
|
||||
Point { x: 12, y: 2 },
|
||||
]);
|
||||
|
||||
s.advance(Direction::Up);
|
||||
|
||||
assert_eq!(
|
||||
s.get_cells(),
|
||||
vec![
|
||||
//
|
||||
Point { x: 10, y: 4 },
|
||||
Point { x: 10, y: 3 },
|
||||
Point { x: 11, y: 3 },
|
||||
Point { x: 12, y: 3 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_detect_self_collision() {
|
||||
let mut s = SnakeC::from(vec![
|
||||
//
|
||||
Point { x: 0, y: 0 },
|
||||
Point { x: 0, y: 1 },
|
||||
Point { x: 0, y: 2 },
|
||||
Point { x: 0, y: 3 },
|
||||
Point { x: 0, y: 4 },
|
||||
Point { x: 0, y: 5 },
|
||||
Point { x: 0, y: 6 },
|
||||
]);
|
||||
|
||||
assert_eq!(s.is_head_self_colliding(), false);
|
||||
|
||||
s.advance(Direction::Right);
|
||||
s.advance(Direction::Up);
|
||||
s.advance(Direction::Up);
|
||||
|
||||
assert_eq!(s.is_head_self_colliding(), false);
|
||||
|
||||
s.advance(Direction::Left);
|
||||
|
||||
assert_eq!(s.is_head_self_colliding(), true);
|
||||
}
|
||||
58
packages/solver-r/src/snake_walk.rs
Normal file
58
packages/solver-r/src/snake_walk.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::grid::{Cell, Grid, Point, DIRECTIONS};
|
||||
use crate::snake::{
|
||||
get_next_snake_head, get_snake_head, move_snake, snake_will_self_collide, Snake,
|
||||
};
|
||||
|
||||
pub fn get_route_to_eat_all(
|
||||
grid: &Grid,
|
||||
walkable: Cell,
|
||||
initial_snake: &Snake,
|
||||
cells_to_eat: HashSet<Point>,
|
||||
) -> Vec<Point> {
|
||||
// let mut targets: Vec<Point> = cells_to_eat.iter().map(|p| p.clone()).collect();
|
||||
|
||||
let mut targets: Vec<&Point> = cells_to_eat.iter().collect();
|
||||
|
||||
let mut path: Vec<Point> = Vec::new();
|
||||
|
||||
let mut initial_snake = initial_snake.clone();
|
||||
|
||||
while let Some(target) = targets.pop() {
|
||||
// prepare
|
||||
let mut open_list: HashSet<(Snake, Vec<Point>)> = HashSet::new();
|
||||
open_list.insert((initial_snake.clone(), Vec::new()));
|
||||
|
||||
while let Some(x) = open_list.iter().next().cloned() {
|
||||
open_list.remove(&x);
|
||||
|
||||
let snake = x.0;
|
||||
let mut sub_path = x.1;
|
||||
|
||||
if get_snake_head(&snake) == *target {
|
||||
path.append(&mut sub_path);
|
||||
initial_snake = snake;
|
||||
break;
|
||||
}
|
||||
|
||||
for dir in DIRECTIONS {
|
||||
if {
|
||||
let h = get_next_snake_head(&snake, &dir);
|
||||
grid.get_cell(&h) <= walkable
|
||||
} && !snake_will_self_collide(&snake, &dir)
|
||||
{
|
||||
let mut next_snake = snake.clone();
|
||||
move_snake(&mut next_snake, &dir);
|
||||
|
||||
let mut next_sub_path = sub_path.clone();
|
||||
next_sub_path.push(dir.clone());
|
||||
|
||||
open_list.insert((next_snake, next_sub_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
127
packages/solver-r/src/solver.rs
Normal file
127
packages/solver-r/src/solver.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::grid::{Cell, Grid, Point};
|
||||
|
||||
pub fn get_free_cell(grid: &Grid, walkable: Cell) -> (HashSet<Point>, HashSet<Point>) {
|
||||
let mut free_cells: HashSet<Point> = HashSet::new();
|
||||
let mut one_way_cells: HashSet<Point> = HashSet::new();
|
||||
let mut open_list: HashSet<Point> = HashSet::new();
|
||||
|
||||
for x in 0..(grid.width as i8) {
|
||||
open_list.insert(Point { x, y: 0 });
|
||||
open_list.insert(Point {
|
||||
x,
|
||||
y: (grid.height as i8) - 1,
|
||||
});
|
||||
}
|
||||
for y in 0..(grid.height as i8) {
|
||||
open_list.insert(Point { x: 0, y });
|
||||
open_list.insert(Point {
|
||||
x: (grid.width as i8) - 1,
|
||||
y,
|
||||
});
|
||||
}
|
||||
open_list.retain(|p| grid.get_cell(&p) <= walkable);
|
||||
|
||||
let directions = [
|
||||
Point { x: 1, y: 0 },
|
||||
Point { x: -1, y: 0 },
|
||||
Point { x: 0, y: 1 },
|
||||
Point { x: 0, y: -1 },
|
||||
];
|
||||
|
||||
while let Some(p) = open_list.iter().next().cloned() {
|
||||
open_list.remove(&p);
|
||||
|
||||
let has_enough_free_exits = {
|
||||
let mut exit_count = 0;
|
||||
let mut visited: HashSet<Point> = HashSet::new();
|
||||
|
||||
for dir in directions {
|
||||
let neighbour = Point {
|
||||
x: p.x + dir.x,
|
||||
y: p.y + dir.y,
|
||||
};
|
||||
|
||||
if !visited.contains(&neighbour)
|
||||
&& (free_cells.contains(&neighbour) || !grid.is_inside(&neighbour))
|
||||
{
|
||||
visited.insert(neighbour);
|
||||
exit_count += 1;
|
||||
}
|
||||
|
||||
if grid.is_inside(&neighbour) && grid.get_cell(&neighbour) <= walkable {
|
||||
for alt in [-1, 1] {
|
||||
let corner = {
|
||||
if neighbour.x != 0 {
|
||||
Point {
|
||||
x: neighbour.x,
|
||||
y: neighbour.y + alt,
|
||||
}
|
||||
} else {
|
||||
Point {
|
||||
x: neighbour.x + alt,
|
||||
y: neighbour.y,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !visited.contains(&neighbour)
|
||||
&& !visited.contains(&corner)
|
||||
&& (free_cells.contains(&corner) || !grid.is_inside(&corner))
|
||||
{
|
||||
visited.insert(neighbour);
|
||||
visited.insert(corner);
|
||||
exit_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exit_count >= 2
|
||||
};
|
||||
|
||||
if has_enough_free_exits {
|
||||
free_cells.insert(p);
|
||||
|
||||
for dir in directions {
|
||||
let neighbour = Point {
|
||||
x: p.x + dir.x,
|
||||
y: p.y + dir.y,
|
||||
};
|
||||
|
||||
if !free_cells.contains(&neighbour)
|
||||
&& grid.is_inside(&neighbour)
|
||||
&& grid.get_cell(&neighbour) <= walkable
|
||||
{
|
||||
open_list.insert(neighbour);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
one_way_cells.insert(p);
|
||||
}
|
||||
}
|
||||
|
||||
one_way_cells.retain(|p| !free_cells.contains(&p));
|
||||
|
||||
(free_cells, one_way_cells)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_collect_free_cell() {
|
||||
let mut grid = Grid::create_empty(2, 2);
|
||||
|
||||
grid.set_cell(&Point { x: 1, y: 1 }, Cell::Color2);
|
||||
|
||||
let (free_cells, _) = get_free_cell(&grid, Cell::Color1);
|
||||
|
||||
assert_eq!(
|
||||
free_cells,
|
||||
HashSet::from([
|
||||
//
|
||||
Point { x: 0, y: 0 },
|
||||
Point { x: 0, y: 1 },
|
||||
Point { x: 1, y: 0 },
|
||||
])
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { it, expect } from "bun:test";
|
||||
import { getBestRoute } from "../getBestRoute";
|
||||
import { snake3, snake4 } from "@snk/types/__fixtures__/snake";
|
||||
import {
|
||||
@@ -16,7 +17,7 @@ for (const { width, height, snake } of [
|
||||
{ width: 5, height: 5, snake: snake4 },
|
||||
])
|
||||
it(`should find solution for ${n} ${width}x${height} generated grids for ${getSnakeLength(
|
||||
snake
|
||||
snake,
|
||||
)} length snake`, () => {
|
||||
const results = Array.from({ length: n }, (_, seed) => {
|
||||
const grid = createFromSeed(seed, width, height);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { it, expect } from "bun:test";
|
||||
import { getBestRoute } from "../getBestRoute";
|
||||
import { Color, createEmptyGrid, setColor } from "@snk/types/grid";
|
||||
import { createSnakeFromCells, snakeToCells } from "@snk/types/snake";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { it, expect } from "bun:test";
|
||||
import { createEmptyGrid } from "@snk/types/grid";
|
||||
import { getHeadX, getHeadY } from "@snk/types/snake";
|
||||
import { snake3 } from "@snk/types/__fixtures__/snake";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { it, expect } from "bun:test";
|
||||
import { createSnakeFromCells } from "@snk/types/snake";
|
||||
import { getPathToPose } from "../getPathToPose";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { it, expect, describe } from "bun:test";
|
||||
import { sortPush } from "../utils/sortPush";
|
||||
|
||||
const sortFn = (a: number, b: number) => a - b;
|
||||
|
||||
@@ -24,7 +24,7 @@ export const clearCleanColoredLayer = (
|
||||
grid: Grid,
|
||||
outside: Outside,
|
||||
snake0: Snake,
|
||||
color: Color
|
||||
color: Color,
|
||||
) => {
|
||||
const snakeN = getSnakeLength(snake0);
|
||||
|
||||
@@ -55,7 +55,7 @@ const getPathToNextPoint = (
|
||||
grid: Grid,
|
||||
snake0: Snake,
|
||||
color: Color,
|
||||
points: Point[]
|
||||
points: Point[],
|
||||
) => {
|
||||
const closeList: Snake[] = [];
|
||||
const openList: M[] = [{ snake: snake0 } as any];
|
||||
@@ -96,7 +96,7 @@ export const getTunnellablePoints = (
|
||||
grid: Grid,
|
||||
outside: Outside,
|
||||
snakeN: number,
|
||||
color: Color
|
||||
color: Color,
|
||||
) => {
|
||||
const points: Point[] = [];
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export const clearResidualColoredLayer = (
|
||||
grid: Grid,
|
||||
outside: Outside,
|
||||
snake0: Snake,
|
||||
color: Color
|
||||
color: Color,
|
||||
) => {
|
||||
const snakeN = getSnakeLength(snake0);
|
||||
|
||||
@@ -99,7 +99,7 @@ export const getTunnellablePoints = (
|
||||
grid: Grid,
|
||||
outside: Outside,
|
||||
snakeN: number,
|
||||
color: Color
|
||||
color: Color,
|
||||
) => {
|
||||
const points: T[] = [];
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export const getBestRoute = (grid0: Grid, snake0: Snake) => {
|
||||
for (const color of extractColors(grid)) {
|
||||
if (color > 1)
|
||||
chain.unshift(
|
||||
...clearResidualColoredLayer(grid, outside, chain[0], color)
|
||||
...clearResidualColoredLayer(grid, outside, chain[0], color),
|
||||
);
|
||||
chain.unshift(...clearCleanColoredLayer(grid, outside, chain[0], color));
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ const getSnakeEscapePath = (
|
||||
grid: Grid,
|
||||
outside: Outside,
|
||||
snake0: Snake,
|
||||
color: Color
|
||||
color: Color,
|
||||
) => {
|
||||
const openList: M[] = [{ snake: snake0, w: 0 } as any];
|
||||
const closeList: Snake[] = [];
|
||||
@@ -79,7 +79,7 @@ export const getBestTunnel = (
|
||||
x: number,
|
||||
y: number,
|
||||
color: Color,
|
||||
snakeN: number
|
||||
snakeN: number,
|
||||
) => {
|
||||
const c = { x, y };
|
||||
const snake0 = createSnakeFromCells(Array.from({ length: snakeN }, () => c));
|
||||
|
||||
@@ -24,7 +24,7 @@ export const createOutside = (grid: Grid, color: Color = 0 as Color) => {
|
||||
export const fillOutside = (
|
||||
outside: Outside,
|
||||
grid: Grid,
|
||||
color: Color = 0 as Color
|
||||
color: Color = 0 as Color,
|
||||
) => {
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
|
||||
@@ -27,7 +27,7 @@ export const getTunnelPath = (snake0: Snake, tunnel: Point[]) => {
|
||||
export const updateTunnel = (
|
||||
grid: Grid,
|
||||
tunnel: Point[],
|
||||
toDelete: Point[]
|
||||
toDelete: Point[],
|
||||
) => {
|
||||
while (tunnel.length) {
|
||||
const { x, y } = tunnel[0];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { it, expect } from "bun:test";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { createSvg, DrawOptions as DrawOptions } from "..";
|
||||
@@ -37,7 +38,7 @@ for (const [key, grid] of Object.entries(grids))
|
||||
null,
|
||||
chain,
|
||||
drawOptions,
|
||||
animationOptions
|
||||
animationOptions,
|
||||
);
|
||||
|
||||
expect(svg).toBeDefined();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { it, expect } from "bun:test";
|
||||
import { minifyCss } from "../css-utils";
|
||||
|
||||
it("should minify css", () => {
|
||||
@@ -6,8 +7,8 @@ it("should minify css", () => {
|
||||
.c {
|
||||
color : red ;
|
||||
}
|
||||
|
||||
`)
|
||||
|
||||
`),
|
||||
).toBe(".c{color:red}");
|
||||
|
||||
expect(
|
||||
@@ -17,10 +18,10 @@ it("should minify css", () => {
|
||||
color : red ;
|
||||
}
|
||||
|
||||
# {
|
||||
# {
|
||||
animation: linear 10;
|
||||
}
|
||||
|
||||
`)
|
||||
|
||||
`),
|
||||
).toBe(".c{top:0;color:red}#{animation:linear 10}");
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ const mergeKeyFrames = (keyframes: { t: number; style: string }[]) => {
|
||||
*/
|
||||
export const createAnimation = (
|
||||
name: string,
|
||||
keyframes: { t: number; style: string }[]
|
||||
keyframes: { t: number; style: string }[],
|
||||
) =>
|
||||
`@keyframes ${name}{` +
|
||||
mergeKeyFrames(keyframes)
|
||||
|
||||
@@ -15,7 +15,7 @@ export type Options = {
|
||||
export const createGrid = (
|
||||
cells: (Point & { t: number | null; color: Color | Empty })[],
|
||||
{ sizeDotBorderRadius, sizeDot, sizeCell }: Options,
|
||||
duration: number
|
||||
duration: number,
|
||||
) => {
|
||||
const svgElements: string[] = [];
|
||||
const styles = [
|
||||
@@ -48,7 +48,7 @@ export const createGrid = (
|
||||
`.c.${id}{
|
||||
fill: var(--c${color});
|
||||
animation-name: ${animationName}
|
||||
}`
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export const createGrid = (
|
||||
y: y * sizeCell + m,
|
||||
rx: sizeDotBorderRadius,
|
||||
ry: sizeDotBorderRadius,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,13 +34,13 @@ export type DrawOptions = {
|
||||
|
||||
const getCellsFromGrid = ({ width, height }: Grid) =>
|
||||
Array.from({ length: width }, (_, x) =>
|
||||
Array.from({ length: height }, (_, y) => ({ x, y }))
|
||||
Array.from({ length: height }, (_, y) => ({ x, y })),
|
||||
).flat();
|
||||
|
||||
const createLivingCells = (
|
||||
grid0: Grid,
|
||||
chain: Snake[],
|
||||
cells: Point[] | null
|
||||
cells: Point[] | null,
|
||||
) => {
|
||||
const livingCells: (Point & {
|
||||
t: number | null;
|
||||
@@ -73,7 +73,7 @@ export const createSvg = (
|
||||
cells: Point[] | null,
|
||||
chain: Snake[],
|
||||
drawOptions: DrawOptions,
|
||||
animationOptions: Pick<AnimationOptions, "frameDuration">
|
||||
animationOptions: Pick<AnimationOptions, "frameDuration">,
|
||||
) => {
|
||||
const width = (grid.width + 2) * drawOptions.sizeCell;
|
||||
const height = (grid.height + 5) * drawOptions.sizeCell;
|
||||
@@ -89,7 +89,7 @@ export const createSvg = (
|
||||
drawOptions,
|
||||
grid.width * drawOptions.sizeCell,
|
||||
(grid.height + 2) * drawOptions.sizeCell,
|
||||
duration
|
||||
duration,
|
||||
),
|
||||
createSnake(chain, drawOptions, duration),
|
||||
];
|
||||
|
||||
@@ -3,6 +3,5 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@snk/solver": "1.0.0"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const lerp = (k: number, a: number, b: number) => (1 - k) * a + k * b;
|
||||
export const createSnake = (
|
||||
chain: Snake[],
|
||||
{ sizeCell, sizeDot }: Options,
|
||||
duration: number
|
||||
duration: number,
|
||||
) => {
|
||||
const snakeN = chain[0] ? getSnakeLength(chain[0]) : 0;
|
||||
|
||||
@@ -64,7 +64,7 @@ export const createSnake = (
|
||||
const animationName = id;
|
||||
|
||||
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) }));
|
||||
|
||||
return [
|
||||
|
||||
@@ -11,7 +11,7 @@ export const createStack = (
|
||||
{ sizeDot }: Options,
|
||||
width: number,
|
||||
y: number,
|
||||
duration: number
|
||||
duration: number,
|
||||
) => {
|
||||
const svgElements: string[] = [];
|
||||
const styles = [
|
||||
@@ -51,7 +51,7 @@ export const createStack = (
|
||||
width: (ts.length * m + 0.6).toFixed(1),
|
||||
x,
|
||||
y,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
styles.push(
|
||||
@@ -68,7 +68,7 @@ export const createStack = (
|
||||
].map(({ scale, t }) => ({
|
||||
t,
|
||||
style: `transform:scale(${scale.toFixed(3)},1)`,
|
||||
}))
|
||||
})),
|
||||
),
|
||||
|
||||
`.u.${id} {
|
||||
@@ -76,7 +76,7 @@ export const createStack = (
|
||||
animation-name: ${animationName};
|
||||
transform-origin: ${x}px 0
|
||||
}
|
||||
`
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,12 @@ export const tunnels = createFromAscii(`
|
||||
#.# #.# #.#
|
||||
#.# ### # #
|
||||
`);
|
||||
export const line = createFromAscii(`
|
||||
|
||||
#######
|
||||
.. #
|
||||
##### #
|
||||
`);
|
||||
|
||||
const createRandom = (width: number, height: number, emptyP: number) => {
|
||||
const grid = createEmptyGrid(width, height);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { it, expect, test } from "bun:test";
|
||||
import { createEmptyGrid, setColor, getColor, isInside, Color } from "../grid";
|
||||
|
||||
it("should set / get cell", () => {
|
||||
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);
|
||||
|
||||
expect(getColor(grid, 0, 1)).toBe(1);
|
||||
expect(getColor(grid, 0, 1)).toBe(1 as any);
|
||||
});
|
||||
|
||||
test.each([
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { it, expect } from "bun:test";
|
||||
import {
|
||||
createSnakeFromCells,
|
||||
nextSnake,
|
||||
@@ -29,7 +30,7 @@ it("should return next snake", () => {
|
||||
];
|
||||
|
||||
expect(snakeToCells(nextSnake(createSnakeFromCells(snk0), 1, 0))).toEqual(
|
||||
snk1
|
||||
snk1,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export const setColor = (
|
||||
grid: Grid,
|
||||
x: number,
|
||||
y: number,
|
||||
color: Color | Empty
|
||||
color: Color | Empty,
|
||||
) => {
|
||||
grid.data[getIndex(grid, x, y)] = color || 0;
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ export const randomlyFillGrid = (
|
||||
colors = [1, 2, 3] as Color[],
|
||||
emptyP = 2,
|
||||
}: { colors?: Color[]; emptyP?: number } = {},
|
||||
rand = defaultRand
|
||||
rand = defaultRand,
|
||||
) => {
|
||||
for (let x = grid.width; x--; )
|
||||
for (let y = grid.height; y--; ) {
|
||||
|
||||
@@ -3,16 +3,19 @@ description: "Generates a snake game from a github user contributions grid. Outp
|
||||
author: "platane"
|
||||
|
||||
runs:
|
||||
using: node16
|
||||
using: node20
|
||||
main: dist/index.js
|
||||
|
||||
inputs:
|
||||
github_user_name:
|
||||
description: "github user name"
|
||||
required: true
|
||||
github_token:
|
||||
description: "github token used to fetch the contribution calendar. Default to the action token if empty."
|
||||
required: false
|
||||
default: ${{ github.token }}
|
||||
outputs:
|
||||
required: false
|
||||
default: null
|
||||
description: |
|
||||
list of files to generate.
|
||||
one file per line. Each output can be customized with options as query string.
|
||||
|
||||
2129
svg-only/dist/155.index.js
vendored
Normal file
2129
svg-only/dist/155.index.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3889
svg-only/dist/197.index.js
vendored
3889
svg-only/dist/197.index.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,53 +1,18 @@
|
||||
"use strict";
|
||||
exports.id = 317;
|
||||
exports.ids = [317];
|
||||
exports.id = 324;
|
||||
exports.ids = [324];
|
||||
exports.modules = {
|
||||
|
||||
/***/ 5317:
|
||||
/***/ 324:
|
||||
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
// ESM COMPAT FLAG
|
||||
__webpack_require__.r(__webpack_exports__);
|
||||
|
||||
// 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
|
||||
|
||||
|
||||
/**
|
||||
* get the contribution grid from a github user page
|
||||
*
|
||||
@@ -64,57 +29,66 @@ const formatDate = (d) => {
|
||||
* getGithubUserContribution("platane", { year: 2019 })
|
||||
*
|
||||
*/
|
||||
const getGithubUserContribution = async (userName, options = {}) => {
|
||||
// either use github.com/users/xxxx/contributions for previous years
|
||||
// or github.com/xxxx ( which gives the latest update to today result )
|
||||
const url = "year" in options || "from" in options || "to" in options
|
||||
? `https://github.com/users/${userName}/contributions?` +
|
||||
formatParams(options)
|
||||
: `https://github.com/${userName}`;
|
||||
const res = await lib_default()(url);
|
||||
if (!res.ok)
|
||||
throw new Error(res.statusText);
|
||||
const resText = await res.text();
|
||||
return parseUserPage(resText);
|
||||
};
|
||||
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 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 getGithubUserContribution = async (userName, o) => {
|
||||
const query = /* GraphQL */ `
|
||||
query ($login: String!) {
|
||||
user(login: $login) {
|
||||
contributionsCollection {
|
||||
contributionCalendar {
|
||||
weeks {
|
||||
contributionDays {
|
||||
contributionCount
|
||||
contributionLevel
|
||||
weekday
|
||||
date
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const variables = { login: userName };
|
||||
const res = await fetch("https://api.github.com/graphql", {
|
||||
headers: {
|
||||
Authorization: `bearer ${o.githubToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "me@platane.me",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({ variables, query }),
|
||||
});
|
||||
const yAttributes = Array.from(new Set(rects.map((c) => c.yAttribute)).keys()).sort();
|
||||
const cells = rects.map(({ yAttribute, ...c }) => ({
|
||||
y: yAttributes.indexOf(yAttribute),
|
||||
...c,
|
||||
}));
|
||||
return cells;
|
||||
if (!res.ok)
|
||||
throw new Error(await res.text().catch(() => res.statusText));
|
||||
const { data, errors } = (await res.json());
|
||||
if (errors?.[0])
|
||||
throw errors[0];
|
||||
return data.user.contributionsCollection.contributionCalendar.weeks.flatMap(({ contributionDays }, x) => contributionDays.map((d) => ({
|
||||
x,
|
||||
y: d.weekday,
|
||||
date: d.date,
|
||||
count: d.contributionCount,
|
||||
level: (d.contributionLevel === "FOURTH_QUARTILE" && 4) ||
|
||||
(d.contributionLevel === "THIRD_QUARTILE" && 3) ||
|
||||
(d.contributionLevel === "SECOND_QUARTILE" && 2) ||
|
||||
(d.contributionLevel === "FIRST_QUARTILE" && 1) ||
|
||||
0,
|
||||
})));
|
||||
};
|
||||
|
||||
// EXTERNAL MODULE: ../types/grid.ts
|
||||
var types_grid = __webpack_require__(2881);
|
||||
var types_grid = __webpack_require__(105);
|
||||
;// CONCATENATED MODULE: ./userContributionToGrid.ts
|
||||
|
||||
const userContributionToGrid = (cells) => {
|
||||
const width = Math.max(0, ...cells.map((c) => c.x)) + 1;
|
||||
const height = Math.max(0, ...cells.map((c) => c.y)) + 1;
|
||||
const grid = (0,types_grid/* createEmptyGrid */.u1)(width, height);
|
||||
const grid = (0,types_grid/* createEmptyGrid */.Kb)(width, height);
|
||||
for (const c of cells) {
|
||||
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
|
||||
(0,types_grid/* setColorEmpty */.Dy)(grid, c.x, c.y);
|
||||
(0,types_grid/* setColorEmpty */.l$)(grid, c.x, c.y);
|
||||
}
|
||||
return grid;
|
||||
};
|
||||
@@ -132,10 +106,10 @@ const pointEquals = (a, b) => a.x === b.x && a.y === b.y;
|
||||
|
||||
|
||||
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 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);
|
||||
return outside;
|
||||
};
|
||||
@@ -145,19 +119,19 @@ const fillOutside = (outside, grid, color = 0) => {
|
||||
changed = false;
|
||||
for (let x = outside.width; x--;)
|
||||
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) &&
|
||||
around4.some((a) => isOutside(outside, x + a.x, y + a.y))) {
|
||||
changed = true;
|
||||
(0,types_grid/* setColorEmpty */.Dy)(outside, x, y);
|
||||
(0,types_grid/* setColorEmpty */.l$)(outside, x, y);
|
||||
}
|
||||
}
|
||||
return outside;
|
||||
};
|
||||
const isOutside = (outside, x, y) => !(0,types_grid/* isInside */.V0)(outside, x, y) || (0,types_grid/* isEmpty */.xb)((0,types_grid/* getColor */.Lq)(outside, x, y));
|
||||
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
|
||||
var types_snake = __webpack_require__(9347);
|
||||
var types_snake = __webpack_require__(777);
|
||||
;// CONCATENATED MODULE: ../solver/utils/sortPush.ts
|
||||
const sortPush = (arr, x, sortFn) => {
|
||||
let a = 0;
|
||||
@@ -190,9 +164,9 @@ const getTunnelPath = (snake0, tunnel) => {
|
||||
const chain = [];
|
||||
let snake = snake0;
|
||||
for (let i = 1; i < tunnel.length; i++) {
|
||||
const dx = tunnel[i].x - (0,types_snake/* getHeadX */.If)(snake);
|
||||
const dy = tunnel[i].y - (0,types_snake/* getHeadY */.IP)(snake);
|
||||
snake = (0,types_snake/* nextSnake */.kv)(snake, dx, dy);
|
||||
const dx = tunnel[i].x - (0,types_snake/* getHeadX */.tN)(snake);
|
||||
const dy = tunnel[i].y - (0,types_snake/* getHeadY */.Ap)(snake);
|
||||
snake = (0,types_snake/* nextSnake */.Sc)(snake, dx, dy);
|
||||
chain.unshift(snake);
|
||||
}
|
||||
return chain;
|
||||
@@ -220,7 +194,7 @@ const updateTunnel = (grid, tunnel, toDelete) => {
|
||||
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
|
||||
*/
|
||||
@@ -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) => {
|
||||
if ((0,types_grid/* isInside */.V0)(grid, x, y))
|
||||
(0,types_grid/* setColorEmpty */.Dy)(grid, x, y);
|
||||
if ((0,types_grid/* isInside */.FK)(grid, x, y))
|
||||
(0,types_grid/* setColorEmpty */.l$)(grid, x, y);
|
||||
};
|
||||
const unwrap = (m) => !m
|
||||
? []
|
||||
: [...unwrap(m.parent), { x: (0,types_snake/* getHeadX */.If)(m.snake), y: (0,types_snake/* getHeadY */.IP)(m.snake) }];
|
||||
: [...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
|
||||
*/
|
||||
@@ -271,15 +245,15 @@ const getSnakeEscapePath = (grid, outside, snake0, color) => {
|
||||
const closeList = [];
|
||||
while (openList[0]) {
|
||||
const o = openList.shift();
|
||||
const x = (0,types_snake/* getHeadX */.If)(o.snake);
|
||||
const y = (0,types_snake/* getHeadY */.IP)(o.snake);
|
||||
const x = (0,types_snake/* getHeadX */.tN)(o.snake);
|
||||
const y = (0,types_snake/* getHeadY */.Ap)(o.snake);
|
||||
if (isOutside(outside, x, y))
|
||||
return unwrap(o);
|
||||
for (const a of around4) {
|
||||
const c = getColorSafe(grid, x + a.x, y + a.y);
|
||||
if (c <= color && !(0,types_snake/* snakeWillSelfCollide */.nJ)(o.snake, a.x, a.y)) {
|
||||
const snake = (0,types_snake/* nextSnake */.kv)(o.snake, a.x, a.y);
|
||||
if (!closeList.some((s0) => (0,types_snake/* snakeEquals */.kE)(s0, snake))) {
|
||||
if (c <= color && !(0,types_snake/* snakeWillSelfCollide */.J)(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 */.sW)(s0, snake))) {
|
||||
const w = o.w + 1 + +(c === color) * 1000;
|
||||
sortPush(openList, { snake, w, parent: o }, (a, b) => a.w - b.w);
|
||||
closeList.push(snake);
|
||||
@@ -296,7 +270,7 @@ const getSnakeEscapePath = (grid, outside, snake0, color) => {
|
||||
*/
|
||||
const getBestTunnel = (grid, outside, x, y, color, snakeN) => {
|
||||
const c = { x, y };
|
||||
const snake0 = (0,types_snake/* createSnakeFromCells */.xG)(Array.from({ length: snakeN }, () => c));
|
||||
const snake0 = (0,types_snake/* createSnakeFromCells */.yS)(Array.from({ length: snakeN }, () => c));
|
||||
const one = getSnakeEscapePath(grid, outside, snake0, color);
|
||||
if (!one)
|
||||
return null;
|
||||
@@ -304,9 +278,9 @@ const getBestTunnel = (grid, outside, x, y, color, snakeN) => {
|
||||
const snakeICells = one.slice(0, snakeN);
|
||||
while (snakeICells.length < snakeN)
|
||||
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
|
||||
const gridI = (0,types_grid/* copyGrid */.VJ)(grid);
|
||||
const gridI = (0,types_grid/* copyGrid */.mi)(grid);
|
||||
for (const { x, y } of one)
|
||||
setEmptySafe(gridI, x, y);
|
||||
const two = getSnakeEscapePath(gridI, outside, snakeI, color);
|
||||
@@ -334,15 +308,15 @@ const getPathTo = (grid, snake0, x, y) => {
|
||||
const closeList = [];
|
||||
while (openList.length) {
|
||||
const c = openList.shift();
|
||||
const cx = (0,types_snake/* getHeadX */.If)(c.snake);
|
||||
const cy = (0,types_snake/* getHeadY */.IP)(c.snake);
|
||||
const cx = (0,types_snake/* getHeadX */.tN)(c.snake);
|
||||
const cy = (0,types_snake/* getHeadY */.Ap)(c.snake);
|
||||
for (let i = 0; i < around4.length; i++) {
|
||||
const { x: dx, y: dy } = around4[i];
|
||||
const nx = cx + dx;
|
||||
const ny = cy + dy;
|
||||
if (nx === x && ny === y) {
|
||||
// unwrap
|
||||
const path = [(0,types_snake/* nextSnake */.kv)(c.snake, dx, dy)];
|
||||
const path = [(0,types_snake/* nextSnake */.Sc)(c.snake, dx, dy)];
|
||||
let e = c;
|
||||
while (e.parent) {
|
||||
path.push(e.snake);
|
||||
@@ -350,11 +324,11 @@ const getPathTo = (grid, snake0, x, y) => {
|
||||
}
|
||||
return path;
|
||||
}
|
||||
if ((0,types_grid/* isInsideLarge */.HJ)(grid, 2, nx, ny) &&
|
||||
!(0,types_snake/* snakeWillSelfCollide */.nJ)(c.snake, dx, dy) &&
|
||||
(!(0,types_grid/* isInside */.V0)(grid, nx, ny) || (0,types_grid/* isEmpty */.xb)((0,types_grid/* getColor */.Lq)(grid, nx, ny)))) {
|
||||
const nsnake = (0,types_snake/* nextSnake */.kv)(c.snake, dx, dy);
|
||||
if (!closeList.some((s) => (0,types_snake/* snakeEquals */.kE)(nsnake, s))) {
|
||||
if ((0,types_grid/* isInsideLarge */.Yd)(grid, 2, nx, ny) &&
|
||||
!(0,types_snake/* snakeWillSelfCollide */.J)(c.snake, dx, dy) &&
|
||||
(!(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 */.Sc)(c.snake, dx, dy);
|
||||
if (!closeList.some((s) => (0,types_snake/* snakeEquals */.sW)(nsnake, s))) {
|
||||
const w = c.w + 1;
|
||||
const h = Math.abs(nx - x) + Math.abs(ny - y);
|
||||
const f = w + h;
|
||||
@@ -375,7 +349,7 @@ const getPathTo = (grid, snake0, x, y) => {
|
||||
|
||||
|
||||
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);
|
||||
// sort
|
||||
tunnels.sort((a, b) => b.priority - a.priority);
|
||||
@@ -394,7 +368,7 @@ const clearResidualColoredLayer = (grid, outside, snake0, color) => {
|
||||
fillOutside(outside, grid);
|
||||
// update tunnels
|
||||
for (let i = tunnels.length; i--;)
|
||||
if ((0,types_grid/* isEmpty */.xb)((0,types_grid/* getColor */.Lq)(grid, tunnels[i].x, tunnels[i].y)))
|
||||
if ((0,types_grid/* isEmpty */.Im)((0,types_grid/* getColor */.oU)(grid, tunnels[i].x, tunnels[i].y)))
|
||||
tunnels.splice(i, 1);
|
||||
else {
|
||||
const t = tunnels[i];
|
||||
@@ -415,8 +389,8 @@ const clearResidualColoredLayer = (grid, outside, snake0, color) => {
|
||||
const getNextTunnel = (ts, snake) => {
|
||||
let minDistance = Infinity;
|
||||
let closestTunnel = null;
|
||||
const x = (0,types_snake/* getHeadX */.If)(snake);
|
||||
const y = (0,types_snake/* getHeadY */.IP)(snake);
|
||||
const x = (0,types_snake/* getHeadX */.tN)(snake);
|
||||
const y = (0,types_snake/* getHeadY */.Ap)(snake);
|
||||
const priority = ts[0].priority;
|
||||
for (let i = 0; ts[i] && ts[i].priority === priority; i++) {
|
||||
const t = ts[i].tunnel;
|
||||
@@ -435,8 +409,8 @@ const getTunnellablePoints = (grid, outside, snakeN, color) => {
|
||||
const points = [];
|
||||
for (let x = grid.width; x--;)
|
||||
for (let y = grid.height; y--;) {
|
||||
const c = (0,types_grid/* getColor */.Lq)(grid, x, y);
|
||||
if (!(0,types_grid/* isEmpty */.xb)(c) && c < color) {
|
||||
const c = (0,types_grid/* getColor */.oU)(grid, x, y);
|
||||
if (!(0,types_grid/* isEmpty */.Im)(c) && c < color) {
|
||||
const tunnel = getBestTunnel(grid, outside, x, y, color, snakeN);
|
||||
if (tunnel) {
|
||||
const priority = getPriority(grid, color, tunnel);
|
||||
@@ -457,7 +431,7 @@ const getPriority = (grid, color, tunnel) => {
|
||||
for (let i = 0; i < tunnel.length; i++) {
|
||||
const { x, y } = tunnel[i];
|
||||
const c = clearResidualColoredLayer_getColorSafe(grid, x, y);
|
||||
if (!(0,types_grid/* isEmpty */.xb)(c) && i === tunnel.findIndex((p) => p.x === x && p.y === y)) {
|
||||
if (!(0,types_grid/* isEmpty */.Im)(c) && i === tunnel.findIndex((p) => p.x === x && p.y === y)) {
|
||||
if (c === color)
|
||||
nColor += 1;
|
||||
else
|
||||
@@ -469,10 +443,10 @@ const getPriority = (grid, color, tunnel) => {
|
||||
return nLess / nColor;
|
||||
};
|
||||
const distanceSq = (ax, ay, bx, by) => (ax - bx) ** 2 + (ay - by) ** 2;
|
||||
const clearResidualColoredLayer_getColorSafe = (grid, x, y) => (0,types_grid/* isInside */.V0)(grid, x, y) ? (0,types_grid/* getColor */.Lq)(grid, x, y) : 0;
|
||||
const clearResidualColoredLayer_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) => {
|
||||
if ((0,types_grid/* isInside */.V0)(grid, x, y))
|
||||
(0,types_grid/* setColorEmpty */.Dy)(grid, x, y);
|
||||
if ((0,types_grid/* isInside */.FK)(grid, x, y))
|
||||
(0,types_grid/* setColorEmpty */.l$)(grid, x, y);
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../solver/clearCleanColoredLayer.ts
|
||||
@@ -482,14 +456,14 @@ const clearResidualColoredLayer_setEmptySafe = (grid, x, y) => {
|
||||
|
||||
|
||||
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 chain = [snake0];
|
||||
while (points.length) {
|
||||
const path = getPathToNextPoint(grid, chain[0], color, points);
|
||||
path.pop();
|
||||
for (const snake of path)
|
||||
clearCleanColoredLayer_setEmptySafe(grid, (0,types_snake/* getHeadX */.If)(snake), (0,types_snake/* getHeadY */.IP)(snake));
|
||||
clearCleanColoredLayer_setEmptySafe(grid, (0,types_snake/* getHeadX */.tN)(snake), (0,types_snake/* getHeadY */.Ap)(snake));
|
||||
chain.unshift(...path);
|
||||
}
|
||||
fillOutside(outside, grid);
|
||||
@@ -502,19 +476,19 @@ const getPathToNextPoint = (grid, snake0, color, points) => {
|
||||
const openList = [{ snake: snake0 }];
|
||||
while (openList.length) {
|
||||
const o = openList.shift();
|
||||
const x = (0,types_snake/* getHeadX */.If)(o.snake);
|
||||
const y = (0,types_snake/* getHeadY */.IP)(o.snake);
|
||||
const x = (0,types_snake/* getHeadX */.tN)(o.snake);
|
||||
const y = (0,types_snake/* getHeadY */.Ap)(o.snake);
|
||||
const i = points.findIndex((p) => p.x === x && p.y === y);
|
||||
if (i >= 0) {
|
||||
points.splice(i, 1);
|
||||
return clearCleanColoredLayer_unwrap(o);
|
||||
}
|
||||
for (const { x: dx, y: dy } of around4) {
|
||||
if ((0,types_grid/* isInsideLarge */.HJ)(grid, 2, x + dx, y + dy) &&
|
||||
!(0,types_snake/* snakeWillSelfCollide */.nJ)(o.snake, dx, dy) &&
|
||||
if ((0,types_grid/* isInsideLarge */.Yd)(grid, 2, x + dx, y + dy) &&
|
||||
!(0,types_snake/* snakeWillSelfCollide */.J)(o.snake, dx, dy) &&
|
||||
clearCleanColoredLayer_getColorSafe(grid, x + dx, y + dy) <= color) {
|
||||
const snake = (0,types_snake/* nextSnake */.kv)(o.snake, dx, dy);
|
||||
if (!closeList.some((s0) => (0,types_snake/* snakeEquals */.kE)(s0, snake))) {
|
||||
const snake = (0,types_snake/* nextSnake */.Sc)(o.snake, dx, dy);
|
||||
if (!closeList.some((s0) => (0,types_snake/* snakeEquals */.sW)(s0, snake))) {
|
||||
closeList.push(snake);
|
||||
openList.push({ snake, parent: o });
|
||||
}
|
||||
@@ -529,8 +503,8 @@ const clearCleanColoredLayer_getTunnellablePoints = (grid, outside, snakeN, colo
|
||||
const points = [];
|
||||
for (let x = grid.width; x--;)
|
||||
for (let y = grid.height; y--;) {
|
||||
const c = (0,types_grid/* getColor */.Lq)(grid, x, y);
|
||||
if (!(0,types_grid/* isEmpty */.xb)(c) &&
|
||||
const c = (0,types_grid/* getColor */.oU)(grid, x, y);
|
||||
if (!(0,types_grid/* isEmpty */.Im)(c) &&
|
||||
c <= color &&
|
||||
!points.some((p) => p.x === x && p.y === y)) {
|
||||
const tunnel = getBestTunnel(grid, outside, x, y, color, snakeN);
|
||||
@@ -542,12 +516,12 @@ const clearCleanColoredLayer_getTunnellablePoints = (grid, outside, snakeN, colo
|
||||
}
|
||||
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) => {
|
||||
if ((0,types_grid/* isInside */.V0)(grid, x, y))
|
||||
(0,types_grid/* setColorEmpty */.Dy)(grid, x, y);
|
||||
if ((0,types_grid/* isInside */.FK)(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
|
||||
|
||||
@@ -555,7 +529,7 @@ const clearCleanColoredLayer_isEmptySafe = (grid, x, y) => !(0,types_grid/* isIn
|
||||
|
||||
|
||||
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 chain = [snake0];
|
||||
for (const color of extractColors(grid)) {
|
||||
@@ -573,7 +547,7 @@ const extractColors = (grid) => {
|
||||
|
||||
;// 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 snake3 = create(3);
|
||||
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) => {
|
||||
if ((0,types_snake/* snakeEquals */.kE)(snake0, target))
|
||||
if ((0,types_snake/* snakeEquals */.sW)(snake0, target))
|
||||
return [];
|
||||
const targetCells = (0,types_snake/* snakeToCells */.Ks)(target).reverse();
|
||||
const snakeN = (0,types_snake/* getSnakeLength */.JJ)(snake0);
|
||||
const targetCells = (0,types_snake/* snakeToCells */.HU)(target).reverse();
|
||||
const snakeN = (0,types_snake/* getSnakeLength */.T$)(snake0);
|
||||
const box = {
|
||||
min: {
|
||||
x: Math.min((0,types_snake/* getHeadX */.If)(snake0), (0,types_snake/* getHeadX */.If)(target)) - snakeN - 1,
|
||||
y: Math.min((0,types_snake/* getHeadY */.IP)(snake0), (0,types_snake/* getHeadY */.IP)(target)) - snakeN - 1,
|
||||
x: Math.min((0,types_snake/* getHeadX */.tN)(snake0), (0,types_snake/* getHeadX */.tN)(target)) - snakeN - 1,
|
||||
y: Math.min((0,types_snake/* getHeadY */.Ap)(snake0), (0,types_snake/* getHeadY */.Ap)(target)) - snakeN - 1,
|
||||
},
|
||||
max: {
|
||||
x: Math.max((0,types_snake/* getHeadX */.If)(snake0), (0,types_snake/* getHeadX */.If)(target)) + snakeN + 1,
|
||||
y: Math.max((0,types_snake/* getHeadY */.IP)(snake0), (0,types_snake/* getHeadY */.IP)(target)) + snakeN + 1,
|
||||
x: Math.max((0,types_snake/* getHeadX */.tN)(snake0), (0,types_snake/* getHeadX */.tN)(target)) + snakeN + 1,
|
||||
y: Math.max((0,types_snake/* getHeadY */.Ap)(snake0), (0,types_snake/* getHeadY */.Ap)(target)) + snakeN + 1,
|
||||
},
|
||||
};
|
||||
const [t0, ...forbidden] = targetCells;
|
||||
@@ -608,8 +582,8 @@ const getPathToPose = (snake0, target, grid) => {
|
||||
const closeList = [];
|
||||
while (openList.length) {
|
||||
const o = openList.shift();
|
||||
const x = (0,types_snake/* getHeadX */.If)(o.snake);
|
||||
const y = (0,types_snake/* getHeadY */.IP)(o.snake);
|
||||
const x = (0,types_snake/* getHeadX */.tN)(o.snake);
|
||||
const y = (0,types_snake/* getHeadY */.Ap)(o.snake);
|
||||
if (x === t0.x && y === t0.y) {
|
||||
const path = [];
|
||||
let e = o;
|
||||
@@ -626,17 +600,17 @@ const getPathToPose = (snake0, target, grid) => {
|
||||
const { x: dx, y: dy } = around4[i];
|
||||
const nx = x + dx;
|
||||
const ny = y + dy;
|
||||
if (!(0,types_snake/* snakeWillSelfCollide */.nJ)(o.snake, dx, dy) &&
|
||||
if (!(0,types_snake/* snakeWillSelfCollide */.J)(o.snake, dx, dy) &&
|
||||
(!grid || getPathToPose_isEmptySafe(grid, nx, ny)) &&
|
||||
(grid
|
||||
? (0,types_grid/* isInsideLarge */.HJ)(grid, 2, nx, ny)
|
||||
? (0,types_grid/* isInsideLarge */.Yd)(grid, 2, nx, ny)
|
||||
: box.min.x <= nx &&
|
||||
nx <= box.max.x &&
|
||||
box.min.y <= ny &&
|
||||
ny <= box.max.y) &&
|
||||
!forbidden.some((p) => p.x === nx && p.y === ny)) {
|
||||
const snake = (0,types_snake/* nextSnake */.kv)(o.snake, dx, dy);
|
||||
if (!closeList.some((s) => (0,types_snake/* snakeEquals */.kE)(snake, s))) {
|
||||
const snake = (0,types_snake/* nextSnake */.Sc)(o.snake, dx, dy);
|
||||
if (!closeList.some((s) => (0,types_snake/* snakeEquals */.sW)(snake, s))) {
|
||||
const w = o.w + 1;
|
||||
const h = Math.abs(nx - x) + Math.abs(ny - y);
|
||||
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");
|
||||
const cells = await getGithubUserContribution(userName);
|
||||
const cells = await getGithubUserContribution(userName, options);
|
||||
const grid = userContributionToGrid(cells);
|
||||
const snake = snake4;
|
||||
console.log("📡 computing best route");
|
||||
@@ -669,12 +643,12 @@ const generateContributionSnake = async (userName, outputs) => {
|
||||
switch (format) {
|
||||
case "svg": {
|
||||
console.log(`🖌 creating svg (outputs[${i}])`);
|
||||
const { createSvg } = await __webpack_require__.e(/* import() */ 340).then(__webpack_require__.bind(__webpack_require__, 8340));
|
||||
const { createSvg } = await __webpack_require__.e(/* import() */ 578).then(__webpack_require__.bind(__webpack_require__, 4578));
|
||||
return createSvg(grid, cells, chain, drawOptions, animationOptions);
|
||||
}
|
||||
case "gif": {
|
||||
console.log(`📹 creating gif (outputs[${i}])`);
|
||||
const { createGif } = await Promise.all(/* import() */[__webpack_require__.e(371), __webpack_require__.e(142)]).then(__webpack_require__.bind(__webpack_require__, 7142));
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -684,18 +658,18 @@ const generateContributionSnake = async (userName, outputs) => {
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 2881:
|
||||
/***/ 105:
|
||||
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
||||
/* harmony export */ "V0": () => (/* binding */ isInside),
|
||||
/* harmony export */ "HJ": () => (/* binding */ isInsideLarge),
|
||||
/* harmony export */ "VJ": () => (/* binding */ copyGrid),
|
||||
/* harmony export */ "Lq": () => (/* binding */ getColor),
|
||||
/* harmony export */ "xb": () => (/* binding */ isEmpty),
|
||||
/* harmony export */ "vk": () => (/* binding */ setColor),
|
||||
/* harmony export */ "Dy": () => (/* binding */ setColorEmpty),
|
||||
/* harmony export */ "u1": () => (/* binding */ createEmptyGrid)
|
||||
/* harmony export */ FK: () => (/* binding */ isInside),
|
||||
/* harmony export */ Im: () => (/* binding */ isEmpty),
|
||||
/* harmony export */ Kb: () => (/* binding */ createEmptyGrid),
|
||||
/* harmony export */ Yd: () => (/* binding */ isInsideLarge),
|
||||
/* harmony export */ l$: () => (/* binding */ setColorEmpty),
|
||||
/* harmony export */ mi: () => (/* binding */ copyGrid),
|
||||
/* harmony export */ oU: () => (/* binding */ getColor),
|
||||
/* harmony export */ wW: () => (/* binding */ setColor)
|
||||
/* harmony export */ });
|
||||
/* unused harmony exports isGridEmpty, gridEquals */
|
||||
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__) => {
|
||||
|
||||
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
||||
/* harmony export */ "If": () => (/* binding */ getHeadX),
|
||||
/* harmony export */ "IP": () => (/* binding */ getHeadY),
|
||||
/* harmony export */ "JJ": () => (/* binding */ getSnakeLength),
|
||||
/* harmony export */ "kE": () => (/* binding */ snakeEquals),
|
||||
/* harmony export */ "kv": () => (/* binding */ nextSnake),
|
||||
/* harmony export */ "nJ": () => (/* binding */ snakeWillSelfCollide),
|
||||
/* harmony export */ "Ks": () => (/* binding */ snakeToCells),
|
||||
/* harmony export */ "xG": () => (/* binding */ createSnakeFromCells)
|
||||
/* harmony export */ Ap: () => (/* binding */ getHeadY),
|
||||
/* harmony export */ HU: () => (/* binding */ snakeToCells),
|
||||
/* harmony export */ J: () => (/* binding */ snakeWillSelfCollide),
|
||||
/* harmony export */ Sc: () => (/* binding */ nextSnake),
|
||||
/* harmony export */ T$: () => (/* binding */ getSnakeLength),
|
||||
/* harmony export */ sW: () => (/* binding */ snakeEquals),
|
||||
/* harmony export */ tN: () => (/* binding */ getHeadX),
|
||||
/* harmony export */ yS: () => (/* binding */ createSnakeFromCells)
|
||||
/* harmony export */ });
|
||||
/* unused harmony export copySnake */
|
||||
const getHeadX = (snake) => snake[0] - 2;
|
||||
5865
svg-only/dist/371.index.js
vendored
5865
svg-only/dist/371.index.js
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,21 @@
|
||||
"use strict";
|
||||
exports.id = 340;
|
||||
exports.ids = [340];
|
||||
exports.id = 578;
|
||||
exports.ids = [578];
|
||||
exports.modules = {
|
||||
|
||||
/***/ 8340:
|
||||
/***/ 4578:
|
||||
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
// ESM COMPAT FLAG
|
||||
__webpack_require__.r(__webpack_exports__);
|
||||
|
||||
// EXPORTS
|
||||
__webpack_require__.d(__webpack_exports__, {
|
||||
"createSvg": () => (/* binding */ createSvg)
|
||||
createSvg: () => (/* binding */ createSvg)
|
||||
});
|
||||
|
||||
// EXTERNAL MODULE: ../types/grid.ts
|
||||
var types_grid = __webpack_require__(2881);
|
||||
var types_grid = __webpack_require__(105);
|
||||
// EXTERNAL MODULE: ../types/snake.ts
|
||||
var types_snake = __webpack_require__(9347);
|
||||
var types_snake = __webpack_require__(777);
|
||||
;// CONCATENATED MODULE: ../svg-creator/xml-utils.ts
|
||||
const h = (element, attributes) => `<${element} ${toAttribute(attributes)}/>`;
|
||||
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 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 }, () => []);
|
||||
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--;)
|
||||
snakeParts[i].push(cells[i]);
|
||||
}
|
||||
@@ -237,15 +235,15 @@ const createLivingCells = (grid0, chain, cells) => {
|
||||
x,
|
||||
y,
|
||||
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++) {
|
||||
const snake = chain[i];
|
||||
const x = (0,types_snake/* getHeadX */.If)(snake);
|
||||
const y = (0,types_snake/* getHeadY */.IP)(snake);
|
||||
if ((0,types_grid/* isInside */.V0)(grid, x, y) && !(0,types_grid/* isEmpty */.xb)((0,types_grid/* getColor */.Lq)(grid, x, y))) {
|
||||
(0,types_grid/* setColorEmpty */.Dy)(grid, x, y);
|
||||
const x = (0,types_snake/* getHeadX */.tN)(snake);
|
||||
const y = (0,types_snake/* getHeadY */.Ap)(snake);
|
||||
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 */.l$)(grid, x, y);
|
||||
const cell = livingCells.find((c) => c.x === x && c.y === y);
|
||||
cell.t = i / chain.length;
|
||||
}
|
||||
@@ -1,31 +1,29 @@
|
||||
"use strict";
|
||||
exports.id = 142;
|
||||
exports.ids = [142];
|
||||
exports.id = 642;
|
||||
exports.ids = [642];
|
||||
exports.modules = {
|
||||
|
||||
/***/ 7142:
|
||||
/***/ 3642:
|
||||
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
// ESM COMPAT FLAG
|
||||
__webpack_require__.r(__webpack_exports__);
|
||||
|
||||
// EXPORTS
|
||||
__webpack_require__.d(__webpack_exports__, {
|
||||
"createGif": () => (/* binding */ createGif)
|
||||
createGif: () => (/* binding */ createGif)
|
||||
});
|
||||
|
||||
// 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_);
|
||||
// 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_);
|
||||
// EXTERNAL MODULE: external "child_process"
|
||||
var external_child_process_ = __webpack_require__(2081);
|
||||
var external_child_process_ = __webpack_require__(5317);
|
||||
// EXTERNAL MODULE: external "canvas"
|
||||
var external_canvas_ = __webpack_require__(1576);
|
||||
var external_canvas_ = __webpack_require__(9919);
|
||||
// EXTERNAL MODULE: ../types/grid.ts
|
||||
var types_grid = __webpack_require__(2881);
|
||||
var types_grid = __webpack_require__(105);
|
||||
;// CONCATENATED MODULE: ../draw/pathRoundedRect.ts
|
||||
const pathRoundedRect_pathRoundedRect = (ctx, width, height, borderRadius) => {
|
||||
ctx.moveTo(borderRadius, 0);
|
||||
@@ -42,7 +40,7 @@ const drawGrid_drawGrid = (ctx, grid, cells, o) => {
|
||||
for (let x = grid.width; x--;)
|
||||
for (let y = grid.height; y--;) {
|
||||
if (!cells || cells.some((c) => c.x === x && c.y === y)) {
|
||||
const c = (0,types_grid/* getColor */.Lq)(grid, x, y);
|
||||
const c = (0,types_grid/* getColor */.oU)(grid, x, y);
|
||||
// @ts-ignore
|
||||
const color = !c ? o.colorEmpty : o.colorDots[c];
|
||||
ctx.save();
|
||||
@@ -144,27 +142,27 @@ const getCanvasWorldSize = (grid, o) => {
|
||||
};
|
||||
|
||||
// EXTERNAL MODULE: ../types/snake.ts
|
||||
var types_snake = __webpack_require__(9347);
|
||||
var types_snake = __webpack_require__(777);
|
||||
;// CONCATENATED MODULE: ../solver/step.ts
|
||||
|
||||
|
||||
const step = (grid, stack, snake) => {
|
||||
const x = (0,types_snake/* getHeadX */.If)(snake);
|
||||
const y = (0,types_snake/* getHeadY */.IP)(snake);
|
||||
const color = (0,types_grid/* getColor */.Lq)(grid, x, y);
|
||||
if ((0,types_grid/* isInside */.V0)(grid, x, y) && !(0,types_grid/* isEmpty */.xb)(color)) {
|
||||
const x = (0,types_snake/* getHeadX */.tN)(snake);
|
||||
const y = (0,types_snake/* getHeadY */.Ap)(snake);
|
||||
const color = (0,types_grid/* getColor */.oU)(grid, x, y);
|
||||
if ((0,types_grid/* isInside */.FK)(grid, x, y) && !(0,types_grid/* isEmpty */.Im)(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
|
||||
var tmp = __webpack_require__(6382);
|
||||
var tmp = __webpack_require__(2644);
|
||||
// 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_);
|
||||
// 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);
|
||||
;// 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 canvas = (0,external_canvas_.createCanvas)(width, height);
|
||||
const ctx = canvas.getContext("2d");
|
||||
const grid = (0,types_grid/* copyGrid */.VJ)(grid0);
|
||||
const grid = (0,types_grid/* copyGrid */.mi)(grid0);
|
||||
const stack = [];
|
||||
const encoder = new (gif_encoder_2_default())(width, height, "neuquant", true);
|
||||
encoder.setRepeat(0);
|
||||
@@ -223,7 +221,7 @@ const createGif = async (grid0, cells, chain, drawOptions, animationOptions) =>
|
||||
outFileName,
|
||||
["--output", optimizedFileName],
|
||||
].flat());
|
||||
return external_fs_default().readFileSync(optimizedFileName);
|
||||
return new Uint8Array(external_fs_default().readFileSync(optimizedFileName));
|
||||
});
|
||||
|
||||
|
||||
26146
svg-only/dist/index.js
vendored
26146
svg-only/dist/index.js
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user