Compare commits
93 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 | ||
|
|
29c7ee48ec | ||
|
|
21655d1bda | ||
|
|
b895ed2e0f | ||
|
|
96773d2b2e | ||
|
|
79ae29668c | ||
|
|
62f6ff3091 | ||
|
|
4a03759871 | ||
|
|
463b90d43c | ||
|
|
b40f17a02e | ||
|
|
f83b9ab0c3 | ||
|
|
fb80d60b23 | ||
|
|
d078b2d231 | ||
|
|
a81c1bcc97 | ||
|
|
40b26d0110 | ||
|
|
d6e930af5b | ||
|
|
98feaa6035 | ||
|
|
8f1481341a | ||
|
|
2e275adbb6 | ||
|
|
66fef03781 | ||
|
|
5841a21a09 | ||
|
|
cce5c4514d | ||
|
|
fb82d42d53 | ||
|
|
e3ad8b2caf | ||
|
|
c21e390ca9 | ||
|
|
7077112ba4 | ||
|
|
e7aa7b7289 | ||
|
|
6b320a1ac4 |
122
.github/workflows/main.yml
vendored
122
.github/workflows/main.yml
vendored
@@ -7,34 +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
|
||||
|
||||
test-benchmark:
|
||||
runs-on: ubuntu-latest
|
||||
- run: bun run build
|
||||
working-directory: packages/solver-r
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
cache: yarn
|
||||
node-version: 16
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: npm run type
|
||||
|
||||
- run: ( cd packages/gif-creator ; yarn benchmark )
|
||||
- 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: |
|
||||
@@ -45,40 +40,87 @@ jobs:
|
||||
uses: ./
|
||||
with:
|
||||
github_user_name: platane
|
||||
gif_out_path: dist/github-contribution-grid-snake.gif
|
||||
svg_out_path: dist/github-contribution-grid-snake.svg
|
||||
outputs: |
|
||||
dist/github-contribution-grid-snake.svg
|
||||
dist/github-contribution-grid-snake-dark.svg?palette=github-dark
|
||||
dist/github-contribution-grid-snake.gif?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9
|
||||
|
||||
- name: ensure the generated file exists
|
||||
run: |
|
||||
ls dist
|
||||
test -f ${{ steps.generate-snake.outputs.gif_out_path }}
|
||||
test -f ${{ steps.generate-snake.outputs.svg_out_path }}
|
||||
test -f dist/github-contribution-grid-snake.svg
|
||||
test -f dist/github-contribution-grid-snake-dark.svg
|
||||
test -f dist/github-contribution-grid-snake.gif
|
||||
|
||||
- uses: crazy-max/ghaction-github-pages@v2.5.0
|
||||
- uses: crazy-max/ghaction-github-pages@v4.1.0
|
||||
with:
|
||||
target_branch: output
|
||||
build_dir: dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
deploy-ghpages:
|
||||
test-action-svg-only:
|
||||
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
|
||||
|
||||
- run: yarn build:demo
|
||||
env:
|
||||
GITHUB_USER_CONTRIBUTION_API_ENDPOINT: https://snk-one.vercel.app/api/github-user-contribution/
|
||||
- run: bun install --frozen-lockfile
|
||||
|
||||
- uses: crazy-max/ghaction-github-pages@v2.6.0
|
||||
if: success() && github.ref == 'refs/heads/master'
|
||||
- name: build svg-only action
|
||||
run: |
|
||||
npm run build:action
|
||||
rm -r svg-only/dist
|
||||
mv packages/action/dist svg-only/dist
|
||||
|
||||
- name: generate-snake-game-from-github-contribution-grid
|
||||
id: generate-snake
|
||||
uses: ./svg-only
|
||||
with:
|
||||
target_branch: gh-pages
|
||||
build_dir: packages/demo/dist
|
||||
github_user_name: platane
|
||||
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@v4.1.0
|
||||
with:
|
||||
target_branch: output-svg-only
|
||||
build_dir: dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.MY_GITHUB_TOKEN_GH_PAGES }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
deploy-ghpages:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v1
|
||||
|
||||
- run: bun install --frozen-lockfile
|
||||
|
||||
- run: npm run build:demo
|
||||
env:
|
||||
GITHUB_USER_CONTRIBUTION_API_ENDPOINT: https://github-user-contribution.platane.workers.dev/github-user-contribution/
|
||||
|
||||
- uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: packages/demo/dist
|
||||
|
||||
- uses: actions/deploy-pages@v4
|
||||
if: success() && github.ref == 'refs/heads/main'
|
||||
|
||||
- run: bunx wrangler deploy
|
||||
if: success() && github.ref == 'refs/heads/main'
|
||||
working-directory: packages/github-user-contribution-service
|
||||
env:
|
||||
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 }}
|
||||
43
.github/workflows/release.yml
vendored
43
.github/workflows/release.yml
vendored
@@ -4,7 +4,10 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version"
|
||||
description: |
|
||||
New version for the release
|
||||
If the version is in format <major>.<minor>.<patch> a new release is emitted.
|
||||
Otherwise for other format ( for example <major>.<minor>.<patch>-beta.1 ), a prerelease is emitted.
|
||||
default: "0.0.1"
|
||||
required: true
|
||||
type: string
|
||||
@@ -15,20 +18,22 @@ on:
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: docker/setup-qemu-action@v1
|
||||
- uses: docker/setup-qemu-action@v2
|
||||
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
|
||||
- uses: docker/login-action@v1
|
||||
- uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: build and publish the docker image
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v4
|
||||
id: docker-build
|
||||
with:
|
||||
push: true
|
||||
@@ -40,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
|
||||
@@ -65,21 +68,19 @@ jobs:
|
||||
git add package.json svg-only/dist action.yml
|
||||
git commit -m "📦 $VERSION"
|
||||
git tag v$VERSION
|
||||
git push origin master --tags
|
||||
git push origin main --tags
|
||||
|
||||
if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
git tag v$( echo $VERSION | cut -d. -f 1-1 )
|
||||
git tag v$( echo $VERSION | cut -d. -f 1-2 )
|
||||
git push origin --tags --force
|
||||
echo ::set-output name=prerelease::false
|
||||
else
|
||||
echo ::set-output name=prerelease::true
|
||||
echo "prerelease=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "prerelease=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: ncipollo/release-action@v1.15.0
|
||||
with:
|
||||
tag_name: v${{ github.event.inputs.version }}
|
||||
tag: v${{ github.event.inputs.version }}
|
||||
body: ${{ github.event.inputs.description }}
|
||||
prerelease: ${{ steps.push-tags.outputs.prerelease }}
|
||||
|
||||
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.9.1 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"]
|
||||
|
||||
56
README.md
56
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,38 +8,69 @@
|
||||
|
||||
Generates a snake game from a github user contributions graph
|
||||
|
||||

|
||||
<picture>
|
||||
<source
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="https://raw.githubusercontent.com/platane/snk/output/github-contribution-grid-snake-dark.svg"
|
||||
/>
|
||||
<source
|
||||
media="(prefers-color-scheme: light)"
|
||||
srcset="https://raw.githubusercontent.com/platane/snk/output/github-contribution-grid-snake.svg"
|
||||
/>
|
||||
<img
|
||||
alt="github contribution grid snake animation"
|
||||
src="https://raw.githubusercontent.com/platane/snk/output/github-contribution-grid-snake.svg"
|
||||
/>
|
||||
</picture>
|
||||
|
||||
Pull a github user's contribution graph.
|
||||
Make it a snake Game, generate a snake path where the cells get eaten in an orderly fashion.
|
||||
|
||||
Generate a [gif](https://github.com/Platane/snk/raw/output/github-contribution-grid-snake.gif) or [svg](https://github.com/Platane/snk/raw/output/github-contribution-grid-snake.svg) image.
|
||||
|
||||
Available as github action. Automatically generate a new image at the end of the day. Which makes for great [github profile readme](https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-your-github-profile/managing-your-profile-readme)
|
||||
Available as github action. It can automatically generate a new image each day. Which makes for great [github profile readme](https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-your-github-profile/managing-your-profile-readme)
|
||||
|
||||
## Usage
|
||||
|
||||
**github action**
|
||||
|
||||
```yaml
|
||||
- uses: Platane/snk@v1
|
||||
- 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
|
||||
github_user_name: ${{ github.repository_owner }}
|
||||
|
||||
# path of the generated gif file
|
||||
# If left empty, the gif file will not be generated
|
||||
gif_out_path: dist/github-snake.gif
|
||||
|
||||
# path of the generated svg file
|
||||
# If left empty, the svg file will not be generated
|
||||
svg_out_path: dist/github-snake.svg
|
||||
# list of files to generate.
|
||||
# one file per line. Each output can be customized with options as query string.
|
||||
#
|
||||
# supported options:
|
||||
# - palette: A preset of color, one of [github, github-dark, github-light]
|
||||
# - color_snake: Color of the snake
|
||||
# - color_dots: Coma separated list of dots color.
|
||||
# The first one is 0 contribution, then it goes from the low contribution to the highest.
|
||||
# Exactly 5 colors are expected.
|
||||
outputs: |
|
||||
dist/github-snake.svg
|
||||
dist/github-snake-dark.svg?palette=github-dark
|
||||
dist/ocean.gif?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9
|
||||
```
|
||||
|
||||
[example with cron job](https://github.com/Platane/Platane/blob/master/.github/workflows/main.yml#L24-L29)
|
||||
[example with cron job](https://github.com/Platane/Platane/blob/master/.github/workflows/main.yml#L26-L33)
|
||||
|
||||
If you are only interested in generating a svg, you can use this other faster action: `uses: Platane/snk/svg-only@v1`
|
||||
If you are only interested in generating a svg, consider using this faster action: `uses: Platane/snk/svg-only@v3`
|
||||
|
||||
**dark mode**
|
||||
|
||||
For **dark mode** support on github, use this [special syntax](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#specifying-the-theme-an-image-is-shown-to) in your readme.
|
||||
|
||||
```html
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="github-snake-dark.svg" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="github-snake.svg" />
|
||||
<img alt="github-snake" src="github-snake.svg" />
|
||||
</picture>
|
||||
```
|
||||
|
||||
**interactive demo**
|
||||
|
||||
|
||||
32
action.yml
32
action.yml
@@ -4,23 +4,31 @@ author: "platane"
|
||||
|
||||
runs:
|
||||
using: docker
|
||||
image: docker://platane/snk@sha256:74d02183a9a4adb8e00d9f50e6eb5035a5b6ef02644d848363ef3301235ebd1d
|
||||
image: docker://platane/snk@sha256:96390294299275740e5963058c9784c60c5393b3b8b16082dcf41b240db791f9
|
||||
|
||||
inputs:
|
||||
github_user_name:
|
||||
description: "github user name"
|
||||
required: true
|
||||
gif_out_path:
|
||||
description: "path of the generated gif file. If left empty, the gif file will not be generated."
|
||||
github_token:
|
||||
description: "github token used to fetch the contribution calendar. Default to the action token if empty."
|
||||
required: false
|
||||
default: null
|
||||
svg_out_path:
|
||||
description: "path of the generated svg file. If left empty, the svg file will not be generated."
|
||||
default: ${{ github.token }}
|
||||
outputs:
|
||||
required: false
|
||||
default: null
|
||||
description: |
|
||||
list of files to generate.
|
||||
one file per line. Each output can be customized with options as query string.
|
||||
|
||||
outputs:
|
||||
gif_out_path:
|
||||
description: "path of the generated gif"
|
||||
svg_out_path:
|
||||
description: "path of the generated svg"
|
||||
supported query string options:
|
||||
|
||||
- palette: A preset of color, one of [github, github-dark, github-light]
|
||||
- color_snake: Color of the snake
|
||||
- color_dots: Coma separated list of dots color.
|
||||
The first one is 0 contribution, then it goes from the low contribution to the highest.
|
||||
Exactly 5 colors are expected.
|
||||
example:
|
||||
outputs: |
|
||||
dark.svg?palette=github-dark&color_snake=blue
|
||||
light.svg?color_snake=#7845ab
|
||||
ocean.gif?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
testMatch: ["**/__tests__/**/?(*.)+(spec|test).ts"],
|
||||
};
|
||||
27
package.json
27
package.json
@@ -1,26 +1,25 @@
|
||||
{
|
||||
"name": "snk",
|
||||
"description": "Generates a snake game from a github user contributions grid",
|
||||
"version": "1.1.3",
|
||||
"version": "3.3.0",
|
||||
"private": true,
|
||||
"repository": "github:platane/snk",
|
||||
"devDependencies": {
|
||||
"@types/jest": "27.4.1",
|
||||
"@types/node": "16.11.7",
|
||||
"jest": "27.5.1",
|
||||
"prettier": "2.6.2",
|
||||
"ts-jest": "27.1.4",
|
||||
"typescript": "4.6.3"
|
||||
"@types/bun": "1.2.2",
|
||||
"prettier": "3.5.1",
|
||||
"typescript": "5.7.3"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/**"
|
||||
"packages/*"
|
||||
],
|
||||
"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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ Contains the github action code.
|
||||
|
||||
Because the gif generation requires some native libs, we cannot use a node.js action.
|
||||
|
||||
Use a docker action instead, the image is created from the [Dockerfile](./Dockerfile).
|
||||
Use a docker action instead, the image is created from the [Dockerfile](../../Dockerfile).
|
||||
|
||||
It's published to [dockerhub](https://hub.docker.com/r/platane/snk) which makes for faster build ( compare to building the image when the action runs )
|
||||
|
||||
Notice that the [action.yml](../../action.yml) point to the latest version of the image. Which makes releasing sematic versioning of the action pointless. Which is probably fine for a wacky project like this one.
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
*
|
||||
!.gitignore
|
||||
!.gitignore
|
||||
!*.snap
|
||||
@@ -0,0 +1,120 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should parse /out.svg?{"color_snake":"yellow","color_dots":["#000","#111","#222","#333","#444"]} 1`] = `
|
||||
{
|
||||
"animationOptions": {
|
||||
"frameDuration": 100,
|
||||
"step": 1,
|
||||
},
|
||||
"drawOptions": {
|
||||
"colorDotBorder": "#1b1f230a",
|
||||
"colorDots": [
|
||||
"#000",
|
||||
"#111",
|
||||
"#222",
|
||||
"#333",
|
||||
"#444",
|
||||
],
|
||||
"colorEmpty": "#000",
|
||||
"colorSnake": "yellow",
|
||||
"dark": undefined,
|
||||
"sizeCell": 16,
|
||||
"sizeDot": 12,
|
||||
"sizeDotBorderRadius": 2,
|
||||
},
|
||||
"filename": "/out.svg",
|
||||
"format": "svg",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`should parse /out.svg?color_snake=orange&color_dots=#000,#111,#222,#333,#444 1`] = `
|
||||
{
|
||||
"animationOptions": {
|
||||
"frameDuration": 100,
|
||||
"step": 1,
|
||||
},
|
||||
"drawOptions": {
|
||||
"colorDotBorder": "#1b1f230a",
|
||||
"colorDots": [
|
||||
"#000",
|
||||
"#111",
|
||||
"#222",
|
||||
"#333",
|
||||
"#444",
|
||||
],
|
||||
"colorEmpty": "#000",
|
||||
"colorSnake": "orange",
|
||||
"dark": undefined,
|
||||
"sizeCell": 16,
|
||||
"sizeDot": 12,
|
||||
"sizeDotBorderRadius": 2,
|
||||
},
|
||||
"filename": "/out.svg",
|
||||
"format": "svg",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`should parse /out.svg?color_snake=orange&color_dots=#000,#111,#222,#333,#444&dark_color_dots=#a00,#a11,#a22,#a33,#a44 1`] = `
|
||||
{
|
||||
"animationOptions": {
|
||||
"frameDuration": 100,
|
||||
"step": 1,
|
||||
},
|
||||
"drawOptions": {
|
||||
"colorDotBorder": "#1b1f230a",
|
||||
"colorDots": [
|
||||
"#000",
|
||||
"#111",
|
||||
"#222",
|
||||
"#333",
|
||||
"#444",
|
||||
],
|
||||
"colorEmpty": "#000",
|
||||
"colorSnake": "orange",
|
||||
"dark": {
|
||||
"colorDotBorder": "#1b1f230a",
|
||||
"colorDots": [
|
||||
"#a00",
|
||||
"#a11",
|
||||
"#a22",
|
||||
"#a33",
|
||||
"#a44",
|
||||
],
|
||||
"colorEmpty": "#a00",
|
||||
"colorSnake": "orange",
|
||||
},
|
||||
"sizeCell": 16,
|
||||
"sizeDot": 12,
|
||||
"sizeDotBorderRadius": 2,
|
||||
},
|
||||
"filename": "/out.svg",
|
||||
"format": "svg",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`should parse path/to/out.gif 1`] = `
|
||||
{
|
||||
"animationOptions": {
|
||||
"frameDuration": 100,
|
||||
"step": 1,
|
||||
},
|
||||
"drawOptions": {
|
||||
"colorDotBorder": "#1b1f230a",
|
||||
"colorDots": [
|
||||
"#ebedf0",
|
||||
"#9be9a8",
|
||||
"#40c463",
|
||||
"#30a14e",
|
||||
"#216e39",
|
||||
],
|
||||
"colorEmpty": "#ebedf0",
|
||||
"colorSnake": "purple",
|
||||
"dark": undefined,
|
||||
"sizeCell": 16,
|
||||
"sizeDot": 12,
|
||||
"sizeDotBorderRadius": 2,
|
||||
},
|
||||
"filename": "path/to/out.gif",
|
||||
"format": "gif",
|
||||
}
|
||||
`;
|
||||
@@ -1,19 +0,0 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { generateContributionSnake } from "../generateContributionSnake";
|
||||
|
||||
(async () => {
|
||||
const outputSvg = path.join(__dirname, "__snapshots__/out.svg");
|
||||
const outputGif = path.join(__dirname, "__snapshots__/out.gif");
|
||||
|
||||
const buffer = await generateContributionSnake("platane", {
|
||||
svg: true,
|
||||
gif: true,
|
||||
});
|
||||
|
||||
console.log("💾 writing to", outputSvg);
|
||||
fs.writeFileSync(outputSvg, buffer.svg);
|
||||
|
||||
console.log("💾 writing to", outputGif);
|
||||
fs.writeFileSync(outputGif, buffer.gif);
|
||||
})();
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { it, expect } from "bun:test";
|
||||
import { generateContributionSnake } from "../generateContributionSnake";
|
||||
|
||||
jest.setTimeout(2 * 60 * 1000);
|
||||
import { parseOutputsOption } from "../outputsOptions";
|
||||
|
||||
const silent = (handler: () => void | Promise<void>) => async () => {
|
||||
const originalConsoleLog = console.log;
|
||||
@@ -17,22 +17,29 @@ const silent = (handler: () => void | Promise<void>) => async () => {
|
||||
it(
|
||||
"should generate contribution snake",
|
||||
silent(async () => {
|
||||
const outputSvg = path.join(__dirname, "__snapshots__/out.svg");
|
||||
const outputGif = path.join(__dirname, "__snapshots__/out.gif");
|
||||
const entries = [
|
||||
path.join(__dirname, "__snapshots__/out.svg"),
|
||||
|
||||
console.log = () => undefined;
|
||||
const buffer = await generateContributionSnake("platane", {
|
||||
svg: true,
|
||||
gif: true,
|
||||
path.join(__dirname, "__snapshots__/out-dark.svg") +
|
||||
"?palette=github-dark&color_snake=orange",
|
||||
|
||||
path.join(__dirname, "__snapshots__/out.gif") +
|
||||
"?color_snake=orange&color_dots=#d4e0f0,#8dbdff,#64a1f4,#4b91f1,#3c7dd9",
|
||||
];
|
||||
|
||||
const outputs = parseOutputsOption(entries);
|
||||
|
||||
const results = await generateContributionSnake("platane", outputs, {
|
||||
githubToken: process.env.GITHUB_TOKEN!,
|
||||
});
|
||||
|
||||
expect(buffer.svg).toBeDefined();
|
||||
expect(buffer.gif).toBeDefined();
|
||||
expect(results[0]).toBeDefined();
|
||||
expect(results[1]).toBeDefined();
|
||||
expect(results[2]).toBeDefined();
|
||||
|
||||
console.log("💾 writing to", outputSvg);
|
||||
fs.writeFileSync(outputSvg, buffer.svg);
|
||||
|
||||
console.log("💾 writing to", outputGif);
|
||||
fs.writeFileSync(outputGif, buffer.gif);
|
||||
})
|
||||
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 },
|
||||
);
|
||||
|
||||
61
packages/action/__tests__/outputsOptions.spec.ts
Normal file
61
packages/action/__tests__/outputsOptions.spec.ts
Normal file
@@ -0,0 +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"]}`,
|
||||
|
||||
// overwrite dark colors
|
||||
"/out.svg?color_snake=orange&color_dots=#000,#111,#222,#333,#444&dark_color_dots=#a00,#a11,#a22,#a33,#a44",
|
||||
].forEach((entry) =>
|
||||
it(`should parse ${entry}`, () => {
|
||||
expect(parseEntry(entry)).toMatchSnapshot();
|
||||
}),
|
||||
);
|
||||
@@ -3,51 +3,50 @@ import { userContributionToGrid } from "./userContributionToGrid";
|
||||
import { getBestRoute } from "@snk/solver/getBestRoute";
|
||||
import { snake4 } from "@snk/types/__fixtures__/snake";
|
||||
import { getPathToPose } from "@snk/solver/getPathToPose";
|
||||
import type { DrawOptions as DrawOptions } from "@snk/svg-creator";
|
||||
import type { AnimationOptions } from "@snk/gif-creator";
|
||||
|
||||
export const generateContributionSnake = async (
|
||||
userName: string,
|
||||
format: { svg?: boolean; gif?: boolean }
|
||||
outputs: ({
|
||||
format: "svg" | "gif";
|
||||
drawOptions: DrawOptions;
|
||||
animationOptions: AnimationOptions;
|
||||
} | null)[],
|
||||
options: { githubToken: string },
|
||||
) => {
|
||||
console.log("🎣 fetching github user contribution");
|
||||
const { cells, colorScheme } = await getGithubUserContribution(userName);
|
||||
const cells = await getGithubUserContribution(userName, options);
|
||||
|
||||
const grid = userContributionToGrid(cells, colorScheme);
|
||||
const grid = userContributionToGrid(cells);
|
||||
const snake = snake4;
|
||||
|
||||
const drawOptions = {
|
||||
sizeBorderRadius: 2,
|
||||
sizeCell: 16,
|
||||
sizeDot: 12,
|
||||
colorBorder: "#1b1f230a",
|
||||
colorDots: colorScheme as any,
|
||||
colorEmpty: colorScheme[0],
|
||||
colorSnake: "purple",
|
||||
cells,
|
||||
dark: {
|
||||
colorEmpty: "#161b22",
|
||||
colorDots: { 1: "#01311f", 2: "#034525", 3: "#0f6d31", 4: "#00c647" },
|
||||
},
|
||||
};
|
||||
|
||||
const gifOptions = { frameDuration: 100, step: 1 };
|
||||
|
||||
console.log("📡 computing best route");
|
||||
const chain = getBestRoute(grid, snake)!;
|
||||
chain.push(...getPathToPose(chain.slice(-1)[0], snake)!);
|
||||
|
||||
const output: Record<string, Buffer | string> = {};
|
||||
|
||||
if (format.gif) {
|
||||
console.log("📹 creating gif");
|
||||
const { createGif } = await import("@snk/gif-creator");
|
||||
output.gif = await createGif(grid, chain, drawOptions, gifOptions);
|
||||
}
|
||||
|
||||
if (format.svg) {
|
||||
console.log("🖌 creating svg");
|
||||
const { createSvg } = await import("@snk/svg-creator");
|
||||
output.svg = createSvg(grid, chain, drawOptions, gifOptions);
|
||||
}
|
||||
|
||||
return output;
|
||||
return Promise.all(
|
||||
outputs.map(async (out, i) => {
|
||||
if (!out) return;
|
||||
const { format, drawOptions, animationOptions } = out;
|
||||
switch (format) {
|
||||
case "svg": {
|
||||
console.log(`🖌 creating svg (outputs[${i}])`);
|
||||
const { createSvg } = await import("@snk/svg-creator");
|
||||
return createSvg(grid, cells, chain, drawOptions, animationOptions);
|
||||
}
|
||||
case "gif": {
|
||||
console.log(`📹 creating gif (outputs[${i}])`);
|
||||
const { createGif } = await import("@snk/gif-creator");
|
||||
return await createGif(
|
||||
grid,
|
||||
cells,
|
||||
chain,
|
||||
drawOptions,
|
||||
animationOptions,
|
||||
);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as core from "@actions/core";
|
||||
import { generateContributionSnake } from "./generateContributionSnake";
|
||||
import { parseOutputsOption } from "./outputsOptions";
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const userName = core.getInput("github_user_name");
|
||||
const format = {
|
||||
svg: core.getInput("svg_out_path"),
|
||||
gif: core.getInput("gif_out_path"),
|
||||
};
|
||||
|
||||
const { svg, gif } = await generateContributionSnake(
|
||||
userName,
|
||||
format as any
|
||||
const outputs = parseOutputsOption(
|
||||
core.getMultilineInput("outputs") ?? [
|
||||
core.getInput("gif_out_path"),
|
||||
core.getInput("svg_out_path"),
|
||||
],
|
||||
);
|
||||
const githubToken =
|
||||
process.env.GITHUB_TOKEN ?? core.getInput("github_token");
|
||||
|
||||
if (svg) {
|
||||
fs.mkdirSync(path.dirname(format.svg), { recursive: true });
|
||||
fs.writeFileSync(format.svg, svg);
|
||||
core.setOutput("svg_out_path", format.svg);
|
||||
}
|
||||
if (gif) {
|
||||
fs.mkdirSync(path.dirname(format.gif), { recursive: true });
|
||||
fs.writeFileSync(format.gif, gif);
|
||||
core.setOutput("gif_out_path", format.gif);
|
||||
}
|
||||
const { generateContributionSnake } = await import(
|
||||
"./generateContributionSnake"
|
||||
);
|
||||
const results = await generateContributionSnake(userName, outputs, {
|
||||
githubToken,
|
||||
});
|
||||
|
||||
outputs.forEach((out, i) => {
|
||||
const result = results[i];
|
||||
if (out?.filename && result) {
|
||||
console.log(`💾 writing to ${out?.filename}`);
|
||||
fs.mkdirSync(path.dirname(out?.filename), { recursive: true });
|
||||
fs.writeFileSync(out?.filename, result);
|
||||
}
|
||||
});
|
||||
} catch (e: any) {
|
||||
core.setFailed(`Action failed with "${e.message}"`);
|
||||
}
|
||||
|
||||
86
packages/action/outputsOptions.ts
Normal file
86
packages/action/outputsOptions.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { AnimationOptions } from "@snk/gif-creator";
|
||||
import type { DrawOptions as DrawOptions } from "@snk/svg-creator";
|
||||
import { palettes } from "./palettes";
|
||||
|
||||
export const parseOutputsOption = (lines: string[]) => lines.map(parseEntry);
|
||||
|
||||
export const parseEntry = (entry: string) => {
|
||||
const m = entry.trim().match(/^(.+\.(svg|gif))(\?(.*)|\s*({.*}))?$/);
|
||||
|
||||
if (!m) return null;
|
||||
|
||||
const [, filename, format, _, q1, q2] = m;
|
||||
|
||||
const query = q1 ?? q2;
|
||||
|
||||
let sp = new URLSearchParams(query || "");
|
||||
|
||||
try {
|
||||
const o = JSON.parse(query);
|
||||
|
||||
if (Array.isArray(o.color_dots)) o.color_dots = o.color_dots.join(",");
|
||||
if (Array.isArray(o.dark_color_dots))
|
||||
o.dark_color_dots = o.dark_color_dots.join(",");
|
||||
|
||||
sp = new URLSearchParams(o);
|
||||
} catch (err) {
|
||||
if (!(err instanceof SyntaxError)) throw err;
|
||||
}
|
||||
|
||||
const drawOptions: DrawOptions = {
|
||||
sizeDotBorderRadius: 2,
|
||||
sizeCell: 16,
|
||||
sizeDot: 12,
|
||||
...palettes["default"],
|
||||
dark: palettes["default"].dark && { ...palettes["default"].dark },
|
||||
};
|
||||
const animationOptions: AnimationOptions = { step: 1, frameDuration: 100 };
|
||||
|
||||
{
|
||||
const palette = palettes[sp.get("palette")!];
|
||||
if (palette) {
|
||||
Object.assign(drawOptions, palette);
|
||||
drawOptions.dark = palette.dark && { ...palette.dark };
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
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(/[,;]/);
|
||||
drawOptions.colorDots = colors;
|
||||
drawOptions.colorEmpty = colors[0];
|
||||
drawOptions.dark = undefined;
|
||||
}
|
||||
if (sp.has("color_dot_border"))
|
||||
drawOptions.colorDotBorder = sp.get("color_dot_border")!;
|
||||
|
||||
if (sp.has("dark_color_dots")) {
|
||||
const colors = sp.get("dark_color_dots")!.split(/[,;]/);
|
||||
drawOptions.dark = {
|
||||
colorDotBorder: drawOptions.colorDotBorder,
|
||||
colorSnake: drawOptions.colorSnake,
|
||||
...drawOptions.dark,
|
||||
colorDots: colors,
|
||||
colorEmpty: colors[0],
|
||||
};
|
||||
}
|
||||
if (sp.has("dark_color_dot_border") && drawOptions.dark)
|
||||
drawOptions.dark.colorDotBorder = sp.get("color_dot_border")!;
|
||||
if (sp.has("dark_color_snake") && drawOptions.dark)
|
||||
drawOptions.dark.colorSnake = sp.get("color_snake")!;
|
||||
|
||||
return {
|
||||
filename,
|
||||
format: format as "svg" | "gif",
|
||||
drawOptions,
|
||||
animationOptions,
|
||||
};
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@snk/action",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@actions/core": "1.6.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,11 +10,9 @@
|
||||
"@snk/types": "1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vercel/ncc": "0.24.1",
|
||||
"ts-node": "10.7.0"
|
||||
"@vercel/ncc": "0.38.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "ncc build --external canvas --external gifsicle --out dist ./index.ts",
|
||||
"dev": "ts-node __tests__/dev.ts"
|
||||
"build": "ncc build --external canvas --external gifsicle --out dist ./index.ts"
|
||||
}
|
||||
}
|
||||
|
||||
27
packages/action/palettes.ts
Normal file
27
packages/action/palettes.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { DrawOptions as DrawOptions } from "@snk/svg-creator";
|
||||
|
||||
export const basePalettes: Record<
|
||||
string,
|
||||
Pick<
|
||||
DrawOptions,
|
||||
"colorDotBorder" | "colorEmpty" | "colorSnake" | "colorDots" | "dark"
|
||||
>
|
||||
> = {
|
||||
"github-light": {
|
||||
colorDotBorder: "#1b1f230a",
|
||||
colorDots: ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"],
|
||||
colorEmpty: "#ebedf0",
|
||||
colorSnake: "purple",
|
||||
},
|
||||
"github-dark": {
|
||||
colorDotBorder: "#1b1f230a",
|
||||
colorEmpty: "#161b22",
|
||||
colorDots: ["#161b22", "#01311f", "#034525", "#0f6d31", "#00c647"],
|
||||
colorSnake: "purple",
|
||||
},
|
||||
};
|
||||
|
||||
// aliases
|
||||
export const palettes = { ...basePalettes };
|
||||
palettes["github"] = palettes["github-light"];
|
||||
palettes["default"] = palettes["github"];
|
||||
@@ -2,17 +2,13 @@ import { setColor, createEmptyGrid, setColorEmpty } from "@snk/types/grid";
|
||||
import type { Cell } from "@snk/github-user-contribution";
|
||||
import type { Color } from "@snk/types/grid";
|
||||
|
||||
export const userContributionToGrid = (
|
||||
cells: Cell[],
|
||||
colorScheme: string[]
|
||||
) => {
|
||||
export const userContributionToGrid = (cells: Cell[]) => {
|
||||
const width = Math.max(0, ...cells.map((c) => c.x)) + 1;
|
||||
const height = Math.max(0, ...cells.map((c) => c.y)) + 1;
|
||||
|
||||
const grid = createEmptyGrid(width, height);
|
||||
for (const c of cells) {
|
||||
const k = colorScheme.indexOf(c.color);
|
||||
if (k > 0) setColor(grid, c.x, c.y, k as Color);
|
||||
if (c.level > 0) setColor(grid, c.x, c.y, c.level as Color);
|
||||
else setColorEmpty(grid, c.x, c.y);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Color, Grid } from "@snk/types/grid";
|
||||
import { drawLerpWorld, drawWorld } from "@snk/draw/drawWorld";
|
||||
import { Snake } from "@snk/types/snake";
|
||||
import type { DrawOptions as DrawOptions } from "@snk/svg-creator";
|
||||
|
||||
export const drawOptions = {
|
||||
sizeBorderRadius: 2,
|
||||
export const drawOptions: DrawOptions = {
|
||||
sizeDotBorderRadius: 2,
|
||||
sizeCell: 16,
|
||||
sizeDot: 12,
|
||||
colorBorder: "#1b1f230a",
|
||||
colorDotBorder: "#1b1f230a",
|
||||
colorDots: {
|
||||
1: "#9be9a8",
|
||||
2: "#40c463",
|
||||
@@ -67,7 +68,7 @@ export const createCanvas = ({
|
||||
|
||||
const draw = (grid: Grid, snake: Snake, stack: Color[]) => {
|
||||
ctx.clearRect(0, 0, 9999, 9999);
|
||||
drawWorld(ctx, grid, snake, stack, drawOptions);
|
||||
drawWorld(ctx, grid, null, snake, stack, drawOptions);
|
||||
};
|
||||
|
||||
const drawLerp = (
|
||||
@@ -75,10 +76,10 @@ export const createCanvas = ({
|
||||
snake0: Snake,
|
||||
snake1: Snake,
|
||||
stack: Color[],
|
||||
k: number
|
||||
k: number,
|
||||
) => {
|
||||
ctx.clearRect(0, 0, 9999, 9999);
|
||||
drawLerpWorld(ctx, grid, snake0, snake1, stack, k, drawOptions);
|
||||
drawLerpWorld(ctx, grid, null, snake0, snake1, stack, k, drawOptions);
|
||||
};
|
||||
|
||||
const highlightCell = (x: number, y: number, color = "orange") => {
|
||||
|
||||
@@ -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)!];
|
||||
|
||||
@@ -3,15 +3,18 @@ import { step } from "@snk/solver/step";
|
||||
import { isStableAndBound, stepSpring } from "./springUtils";
|
||||
import type { Res } from "@snk/github-user-contribution";
|
||||
import type { Snake } from "@snk/types/snake";
|
||||
import type { Point } from "@snk/types/point";
|
||||
import {
|
||||
drawLerpWorld,
|
||||
getCanvasWorldSize,
|
||||
Options,
|
||||
Options as DrawOptions,
|
||||
} from "@snk/draw/drawWorld";
|
||||
import { userContributionToGrid } from "@snk/action/userContributionToGrid";
|
||||
import { createSvg } from "@snk/svg-creator";
|
||||
import { createRpcClient } from "./worker-utils";
|
||||
import type { API as WorkerAPI } from "./demo.interactive.worker";
|
||||
import { AnimationOptions } from "@snk/gif-creator";
|
||||
import { basePalettes } from "@snk/action/palettes";
|
||||
|
||||
const createForm = ({
|
||||
onSubmit,
|
||||
@@ -116,12 +119,19 @@ const createGithubProfile = () => {
|
||||
const createViewer = ({
|
||||
grid0,
|
||||
chain,
|
||||
drawOptions,
|
||||
cells,
|
||||
}: {
|
||||
grid0: Grid;
|
||||
chain: Snake[];
|
||||
drawOptions: Options;
|
||||
cells: Point[];
|
||||
}) => {
|
||||
const drawOptions: DrawOptions = {
|
||||
sizeDotBorderRadius: 2,
|
||||
sizeCell: 16,
|
||||
sizeDot: 12,
|
||||
...basePalettes["github-light"],
|
||||
};
|
||||
|
||||
//
|
||||
// canvas
|
||||
const canvas = document.createElement("canvas");
|
||||
@@ -159,7 +169,7 @@ const createViewer = ({
|
||||
const k = spring.x % 1;
|
||||
|
||||
ctx.clearRect(0, 0, 9999, 9999);
|
||||
drawLerpWorld(ctx, grid, snake0, snake1, stack, k, drawOptions);
|
||||
drawLerpWorld(ctx, grid, cells, snake0, snake1, stack, k, drawOptions);
|
||||
|
||||
if (!stable) animationFrame = requestAnimationFrame(loop);
|
||||
};
|
||||
@@ -167,12 +177,12 @@ const createViewer = ({
|
||||
|
||||
//
|
||||
// controls
|
||||
const input = document.createElement("input") as any;
|
||||
const input = document.createElement("input");
|
||||
input.type = "range";
|
||||
input.value = 0;
|
||||
input.step = 1;
|
||||
input.min = 0;
|
||||
input.max = chain.length;
|
||||
input.value = "0";
|
||||
input.step = "1";
|
||||
input.min = "0";
|
||||
input.max = "" + chain.length;
|
||||
input.style.width = "calc( 100% - 20px )";
|
||||
input.addEventListener("input", () => {
|
||||
spring.target = +input.value;
|
||||
@@ -186,12 +196,51 @@ const createViewer = ({
|
||||
window.addEventListener("click", onClickBackground);
|
||||
document.body.append(input);
|
||||
|
||||
//
|
||||
const schemaSelect = document.createElement("select");
|
||||
schemaSelect.style.margin = "10px";
|
||||
schemaSelect.style.alignSelf = "flex-start";
|
||||
schemaSelect.value = "github-light";
|
||||
schemaSelect.addEventListener("change", () => {
|
||||
Object.assign(drawOptions, basePalettes[schemaSelect.value]);
|
||||
|
||||
svgString = createSvg(grid0, cells, chain, drawOptions, {
|
||||
frameDuration: 100,
|
||||
} as AnimationOptions);
|
||||
const svgImageUri = `data:image/*;charset=utf-8;base64,${btoa(svgString)}`;
|
||||
svgLink.href = svgImageUri;
|
||||
|
||||
if (schemaSelect.value.includes("dark"))
|
||||
document.body.parentElement?.classList.add("dark-mode");
|
||||
else document.body.parentElement?.classList.remove("dark-mode");
|
||||
|
||||
loop();
|
||||
});
|
||||
for (const name of Object.keys(basePalettes)) {
|
||||
const option = document.createElement("option");
|
||||
option.value = name;
|
||||
option.innerText = name;
|
||||
schemaSelect.appendChild(option);
|
||||
}
|
||||
document.body.append(schemaSelect);
|
||||
|
||||
//
|
||||
// dark mode
|
||||
const style = document.createElement("style");
|
||||
style.innerText = `
|
||||
html { transition:background-color 180ms }
|
||||
a { transition:color 180ms }
|
||||
html.dark-mode{ background-color:#0d1117 }
|
||||
html.dark-mode a{ color:rgb(201, 209, 217) }
|
||||
`;
|
||||
document.head.append(style);
|
||||
|
||||
//
|
||||
// svg
|
||||
const svgLink = document.createElement("a");
|
||||
const svgString = createSvg(grid0, chain, drawOptions, {
|
||||
let svgString = createSvg(grid0, cells, chain, drawOptions, {
|
||||
frameDuration: 100,
|
||||
});
|
||||
} as AnimationOptions);
|
||||
const svgImageUri = `data:image/*;charset=utf-8;base64,${btoa(svgString)}`;
|
||||
svgLink.href = svgImageUri;
|
||||
svgLink.innerText = "github-user-contribution.svg";
|
||||
@@ -199,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();
|
||||
});
|
||||
@@ -225,36 +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, colorScheme } = (await res.json()) as Res;
|
||||
const cells = (await res.json()) as Res;
|
||||
|
||||
const drawOptions = {
|
||||
sizeBorderRadius: 2,
|
||||
sizeCell: 16,
|
||||
sizeDot: 12,
|
||||
colorBorder: "#1b1f230a",
|
||||
colorDots: colorScheme as any,
|
||||
colorEmpty: colorScheme[0],
|
||||
colorSnake: "purple",
|
||||
cells,
|
||||
};
|
||||
|
||||
const grid = userContributionToGrid(cells, colorScheme);
|
||||
const grid = userContributionToGrid(cells);
|
||||
|
||||
const chain = await getChain(grid);
|
||||
|
||||
dispose();
|
||||
|
||||
createViewer({ grid0: grid, chain, drawOptions });
|
||||
createViewer({ grid0: grid, chain, cells });
|
||||
};
|
||||
|
||||
const worker = new Worker(
|
||||
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);
|
||||
}
|
||||
}
|
||||
})();
|
||||
@@ -4,12 +4,15 @@ import { createSvg } from "@snk/svg-creator";
|
||||
import { grid, snake } from "./sample";
|
||||
import { drawOptions } from "./canvas";
|
||||
import { getPathToPose } from "@snk/solver/getPathToPose";
|
||||
import type { AnimationOptions } from "@snk/gif-creator";
|
||||
|
||||
const chain = getBestRoute(grid, snake);
|
||||
chain.push(...getPathToPose(chain.slice(-1)[0], snake)!);
|
||||
|
||||
(async () => {
|
||||
const svg = await createSvg(grid, chain, drawOptions, { frameDuration: 200 });
|
||||
const svg = await createSvg(grid, null, chain, drawOptions, {
|
||||
frameDuration: 200,
|
||||
} as AnimationOptions);
|
||||
|
||||
const container = document.createElement("div");
|
||||
container.innerHTML = svg;
|
||||
|
||||
@@ -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.2.8",
|
||||
"ts-node": "10.7.0",
|
||||
"webpack": "5.72.0",
|
||||
"webpack-cli": "4.9.2",
|
||||
"webpack-dev-server": "4.8.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
|
||||
|
||||
@@ -6,36 +6,36 @@ import type { Point } from "@snk/types/point";
|
||||
type Options = {
|
||||
colorDots: Record<Color, string>;
|
||||
colorEmpty: string;
|
||||
colorBorder: string;
|
||||
colorDotBorder: string;
|
||||
sizeCell: number;
|
||||
sizeDot: number;
|
||||
sizeBorderRadius: number;
|
||||
cells?: Point[];
|
||||
sizeDotBorderRadius: number;
|
||||
};
|
||||
|
||||
export const drawGrid = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
grid: Grid,
|
||||
o: Options
|
||||
cells: Point[] | null,
|
||||
o: Options,
|
||||
) => {
|
||||
for (let x = grid.width; x--; )
|
||||
for (let y = grid.height; y--; ) {
|
||||
if (!o.cells || o.cells.some((c) => c.x === x && c.y === y)) {
|
||||
if (!cells || cells.some((c) => c.x === x && c.y === y)) {
|
||||
const c = getColor(grid, x, y);
|
||||
// @ts-ignore
|
||||
const color = !c ? o.colorEmpty : o.colorDots[c];
|
||||
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;
|
||||
ctx.strokeStyle = o.colorBorder;
|
||||
ctx.strokeStyle = o.colorDotBorder;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
|
||||
pathRoundedRect(ctx, o.sizeDot, o.sizeDot, o.sizeBorderRadius);
|
||||
pathRoundedRect(ctx, o.sizeDot, o.sizeDot, o.sizeDotBorderRadius);
|
||||
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { drawGrid } from "./drawGrid";
|
||||
import { drawSnake, drawSnakeLerp } from "./drawSnake";
|
||||
import type { Grid, Color } from "@snk/types/grid";
|
||||
import type { Point } from "@snk/types/point";
|
||||
import type { Snake } from "@snk/types/snake";
|
||||
import type { Point } from "@snk/types/point";
|
||||
|
||||
export type Options = {
|
||||
colorDots: Record<Color, string>;
|
||||
colorEmpty: string;
|
||||
colorBorder: string;
|
||||
colorDotBorder: string;
|
||||
colorSnake: string;
|
||||
sizeCell: number;
|
||||
sizeDot: number;
|
||||
sizeBorderRadius: number;
|
||||
cells?: Point[];
|
||||
sizeDotBorderRadius: number;
|
||||
};
|
||||
|
||||
export const drawStack = (
|
||||
@@ -20,7 +19,7 @@ export const drawStack = (
|
||||
stack: Color[],
|
||||
max: number,
|
||||
width: number,
|
||||
o: { colorDots: Record<Color, string> }
|
||||
o: { colorDots: Record<Color, string> },
|
||||
) => {
|
||||
ctx.save();
|
||||
|
||||
@@ -37,14 +36,15 @@ export const drawStack = (
|
||||
export const drawWorld = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
grid: Grid,
|
||||
cells: Point[] | null,
|
||||
snake: Snake,
|
||||
stack: Color[],
|
||||
o: Options
|
||||
o: Options,
|
||||
) => {
|
||||
ctx.save();
|
||||
|
||||
ctx.translate(1 * o.sizeCell, 2 * o.sizeCell);
|
||||
drawGrid(ctx, grid, o);
|
||||
drawGrid(ctx, grid, cells, o);
|
||||
drawSnake(ctx, snake, o);
|
||||
|
||||
ctx.restore();
|
||||
@@ -66,18 +66,19 @@ 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();
|
||||
|
||||
ctx.translate(1 * o.sizeCell, 2 * o.sizeCell);
|
||||
drawGrid(ctx, grid, o);
|
||||
drawGrid(ctx, grid, cells, o);
|
||||
drawSnakeLerp(ctx, snake0, snake1, k, o);
|
||||
|
||||
ctx.translate(0, (grid.height + 2) * o.sizeCell);
|
||||
|
||||
@@ -2,7 +2,7 @@ export const pathRoundedRect = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number,
|
||||
borderRadius: number
|
||||
borderRadius: number,
|
||||
) => {
|
||||
ctx.moveTo(borderRadius, 0);
|
||||
ctx.arcTo(width, 0, width, height, borderRadius);
|
||||
|
||||
@@ -2,12 +2,13 @@ import * as fs from "fs";
|
||||
import { performance } from "perf_hooks";
|
||||
import { createSnakeFromCells } from "@snk/types/snake";
|
||||
import { realistic as grid } from "@snk/types/__fixtures__/grid";
|
||||
import { createGif } from "..";
|
||||
import { AnimationOptions, createGif } from "..";
|
||||
import { getBestRoute } from "@snk/solver/getBestRoute";
|
||||
import { getPathToPose } from "@snk/solver/getPathToPose";
|
||||
import type { Options as DrawOptions } from "@snk/draw/drawWorld";
|
||||
|
||||
let snake = createSnakeFromCells(
|
||||
Array.from({ length: 4 }, (_, i) => ({ x: i, y: -1 }))
|
||||
Array.from({ length: 4 }, (_, i) => ({ x: i, y: -1 })),
|
||||
);
|
||||
|
||||
// const chain = [snake];
|
||||
@@ -24,17 +25,17 @@ let snake = createSnakeFromCells(
|
||||
const chain = getBestRoute(grid, snake)!;
|
||||
chain.push(...getPathToPose(chain.slice(-1)[0], snake)!);
|
||||
|
||||
const drawOptions = {
|
||||
sizeBorderRadius: 2,
|
||||
const drawOptions: DrawOptions = {
|
||||
sizeDotBorderRadius: 2,
|
||||
sizeCell: 16,
|
||||
sizeDot: 12,
|
||||
colorBorder: "#1b1f230a",
|
||||
colorDotBorder: "#1b1f230a",
|
||||
colorDots: { 1: "#9be9a8", 2: "#40c463", 3: "#30a14e", 4: "#216e39" },
|
||||
colorEmpty: "#ebedf0",
|
||||
colorSnake: "purple",
|
||||
};
|
||||
|
||||
const gifOptions = { frameDuration: 100, step: 1 };
|
||||
const animationOptions: AnimationOptions = { frameDuration: 100, step: 1 };
|
||||
|
||||
(async () => {
|
||||
for (
|
||||
@@ -44,12 +45,18 @@ const gifOptions = { 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++) {
|
||||
const s = performance.now();
|
||||
buffer = await createGif(grid, chainL, drawOptions, gifOptions);
|
||||
buffer = await createGif(
|
||||
grid,
|
||||
null,
|
||||
chainL,
|
||||
drawOptions,
|
||||
animationOptions,
|
||||
);
|
||||
stats.push(performance.now() - s);
|
||||
}
|
||||
|
||||
@@ -66,12 +73,12 @@ const gifOptions = { frameDuration: 100, step: 1 };
|
||||
})}ms`,
|
||||
"",
|
||||
].join("\n"),
|
||||
stats
|
||||
stats,
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
`__tests__/__snapshots__/benchmark-output-${length}.gif`,
|
||||
buffer!
|
||||
buffer!,
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { createGif } from "..";
|
||||
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";
|
||||
import { createSnakeFromCells, nextSnake } from "@snk/types/snake";
|
||||
import { getBestRoute } from "@snk/solver/getBestRoute";
|
||||
|
||||
jest.setTimeout(20 * 1000);
|
||||
import type { Options as DrawOptions } from "@snk/draw/drawWorld";
|
||||
|
||||
const upscale = 1;
|
||||
const drawOptions = {
|
||||
sizeBorderRadius: 2 * upscale,
|
||||
const drawOptions: DrawOptions = {
|
||||
sizeDotBorderRadius: 2 * upscale,
|
||||
sizeCell: 16 * upscale,
|
||||
sizeDot: 12 * upscale,
|
||||
colorBorder: "#1b1f230a",
|
||||
colorDotBorder: "#1b1f230a",
|
||||
colorDots: { 1: "#9be9a8", 2: "#40c463", 3: "#30a14e", 4: "#216e39" },
|
||||
colorEmpty: "#ebedf0",
|
||||
colorSnake: "purple",
|
||||
};
|
||||
|
||||
const gifOptions = { frameDuration: 200, step: 1 };
|
||||
const animationOptions: AnimationOptions = { frameDuration: 200, step: 1 };
|
||||
|
||||
const dir = path.resolve(__dirname, "__snapshots__");
|
||||
|
||||
@@ -34,38 +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, chain, drawOptions, gifOptions);
|
||||
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,
|
||||
);
|
||||
|
||||
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, chain, drawOptions, gifOptions);
|
||||
|
||||
expect(gif).toBeDefined();
|
||||
|
||||
fs.writeFileSync(path.resolve(dir, "swipper.gif"), gif);
|
||||
});
|
||||
fs.writeFileSync(path.resolve(dir, "swipper.gif"), gif);
|
||||
},
|
||||
{ timeout: 20 * 1000 },
|
||||
);
|
||||
|
||||
@@ -5,10 +5,11 @@ import { createCanvas } from "canvas";
|
||||
import { Grid, copyGrid, Color } from "@snk/types/grid";
|
||||
import { Snake } from "@snk/types/snake";
|
||||
import {
|
||||
Options,
|
||||
Options as DrawOptions,
|
||||
drawLerpWorld,
|
||||
getCanvasWorldSize,
|
||||
} from "@snk/draw/drawWorld";
|
||||
import type { Point } from "@snk/types/point";
|
||||
import { step } from "@snk/solver/step";
|
||||
import tmp from "tmp";
|
||||
import gifsicle from "gifsicle";
|
||||
@@ -16,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,
|
||||
@@ -29,24 +30,27 @@ const withTmpDir = async <T>(
|
||||
}
|
||||
};
|
||||
|
||||
export type AnimationOptions = { frameDuration: number; step: number };
|
||||
|
||||
export const createGif = async (
|
||||
grid0: Grid,
|
||||
cells: Point[] | null,
|
||||
chain: Snake[],
|
||||
drawOptions: Options,
|
||||
gifOptions: { frameDuration: number; step: number }
|
||||
drawOptions: DrawOptions,
|
||||
animationOptions: AnimationOptions,
|
||||
) =>
|
||||
withTmpDir(async (dir) => {
|
||||
const { width, height } = getCanvasWorldSize(grid0, drawOptions);
|
||||
|
||||
const canvas = createCanvas(width, height);
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
const ctx = canvas.getContext("2d") as any as CanvasRenderingContext2D;
|
||||
|
||||
const grid = copyGrid(grid0);
|
||||
const stack: Color[] = [];
|
||||
|
||||
const encoder = new GIFEncoder(width, height, "neuquant", true);
|
||||
encoder.setRepeat(0);
|
||||
encoder.setDelay(gifOptions.frameDuration);
|
||||
encoder.setDelay(animationOptions.frameDuration);
|
||||
encoder.start();
|
||||
|
||||
for (let i = 0; i < chain.length; i += 1) {
|
||||
@@ -54,18 +58,19 @@ export const createGif = async (
|
||||
const snake1 = chain[Math.min(chain.length - 1, i + 1)];
|
||||
step(grid, stack, snake0);
|
||||
|
||||
for (let k = 0; k < gifOptions.step; k++) {
|
||||
for (let k = 0; k < animationOptions.step; k++) {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
drawLerpWorld(
|
||||
ctx,
|
||||
grid,
|
||||
cells,
|
||||
snake0,
|
||||
snake1,
|
||||
stack,
|
||||
k / gifOptions.step,
|
||||
drawOptions
|
||||
k / animationOptions.step,
|
||||
drawOptions,
|
||||
);
|
||||
|
||||
encoder.addFrame(ctx);
|
||||
@@ -87,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.9.1",
|
||||
"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.24.1"
|
||||
"@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 { NowRequest, NowResponse } from "@vercel/node";
|
||||
|
||||
export default async (req: NowRequest, res: NowResponse) => {
|
||||
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": "1.14.0"
|
||||
"@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,54 +1,32 @@
|
||||
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;
|
||||
});
|
||||
|
||||
it("should get colorScheme", async () => {
|
||||
const { colorScheme } = await promise;
|
||||
|
||||
expect(colorScheme).toEqual([
|
||||
"#ebedf0",
|
||||
"#9be9a8",
|
||||
"#40c463",
|
||||
"#30a14e",
|
||||
"#216e39",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should get around 365 cells", async () => {
|
||||
const { cells } = await promise;
|
||||
const cells = await promise;
|
||||
|
||||
expect(cells.length).toBeGreaterThanOrEqual(365);
|
||||
expect(cells.length).toBeLessThanOrEqual(365 + 7);
|
||||
});
|
||||
|
||||
it("cells should have x / y coords representing to a 7 x (365/7) (minus unfilled last row)", async () => {
|
||||
const { cells, colorScheme } = await promise;
|
||||
const cells = await promise;
|
||||
|
||||
expect(cells.length).toBeGreaterThan(300);
|
||||
expect(colorScheme).toEqual([
|
||||
"#ebedf0",
|
||||
"#9be9a8",
|
||||
"#40c463",
|
||||
"#30a14e",
|
||||
"#216e39",
|
||||
]);
|
||||
|
||||
const undefinedDays = Array.from({ length: Math.floor(365 / 7) })
|
||||
.map((x) => Array.from({ length: 7 }).map((y) => ({ x, y })))
|
||||
.flat()
|
||||
.filter(({ x, y }) => cells.some((c) => c.x === x && c.y === y));
|
||||
.filter(({ x, y }) => cells.some((c: any) => c.x === x && c.y === y));
|
||||
|
||||
expect(undefinedDays).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should match snapshot for year=2019", async () => {
|
||||
expect(
|
||||
await getGithubUserContribution("platane", { year: 2019 })
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
export type Options = { from?: string; to?: string } | { year: number };
|
||||
|
||||
export const formatParams = (options: Options = {}) => {
|
||||
const sp = new URLSearchParams();
|
||||
|
||||
const o: any = { ...options };
|
||||
|
||||
if ("year" in options) {
|
||||
o.from = `${options.year}-01-01`;
|
||||
o.to = `${options.year}-12-31`;
|
||||
}
|
||||
|
||||
for (const s of ["from", "to"])
|
||||
if (o[s]) {
|
||||
const value = o[s];
|
||||
|
||||
if (value >= formatDate(new Date()))
|
||||
throw new Error(
|
||||
"Cannot get contribution for a date in the future.\nPlease limit your range to the current UTC day."
|
||||
);
|
||||
|
||||
sp.set(s, value);
|
||||
}
|
||||
|
||||
return sp.toString();
|
||||
};
|
||||
|
||||
const formatDate = (d: Date) => {
|
||||
const year = d.getUTCFullYear();
|
||||
const month = d.getUTCMonth() + 1;
|
||||
const date = d.getUTCDate();
|
||||
|
||||
return [
|
||||
year,
|
||||
month.toString().padStart(2, "0"),
|
||||
date.toString().padStart(2, "0"),
|
||||
].join("-");
|
||||
};
|
||||
@@ -1,7 +1,3 @@
|
||||
import fetch from "node-fetch";
|
||||
import * as cheerio from "cheerio";
|
||||
import { formatParams, Options } from "./formatParams";
|
||||
|
||||
/**
|
||||
* get the contribution grid from a github user page
|
||||
*
|
||||
@@ -20,110 +16,86 @@ 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 defaultColorScheme = [
|
||||
"#ebedf0",
|
||||
"#9be9a8",
|
||||
"#40c463",
|
||||
"#30a14e",
|
||||
"#216e39",
|
||||
];
|
||||
|
||||
const parseUserPage = (content: string) => {
|
||||
const $ = cheerio.load(content);
|
||||
|
||||
//
|
||||
// "parse" colorScheme
|
||||
const colorScheme = [...defaultColorScheme];
|
||||
|
||||
//
|
||||
// parse cells
|
||||
const rawCells = $(".js-calendar-graph rect[data-count]")
|
||||
.toArray()
|
||||
.map((x) => {
|
||||
const level = +x.attribs["data-level"];
|
||||
const count = +x.attribs["data-count"];
|
||||
const date = x.attribs["data-date"];
|
||||
|
||||
const color = colorScheme[level];
|
||||
|
||||
if (!color) throw new Error("could not determine the color of the cell");
|
||||
|
||||
return {
|
||||
svgPosition: getSvgPosition(x),
|
||||
color,
|
||||
count,
|
||||
date,
|
||||
};
|
||||
});
|
||||
|
||||
const xMap: Record<number, true> = {};
|
||||
const yMap: Record<number, true> = {};
|
||||
rawCells.forEach(({ svgPosition: { x, y } }) => {
|
||||
xMap[x] = true;
|
||||
yMap[y] = true;
|
||||
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 xRange = Object.keys(xMap)
|
||||
.map((x) => +x)
|
||||
.sort((a, b) => +a - +b);
|
||||
const yRange = Object.keys(yMap)
|
||||
.map((x) => +x)
|
||||
.sort((a, b) => +a - +b);
|
||||
if (!res.ok) throw new Error(await res.text().catch(() => res.statusText));
|
||||
|
||||
const cells = rawCells.map(({ svgPosition, ...c }) => ({
|
||||
...c,
|
||||
x: xRange.indexOf(svgPosition.x),
|
||||
y: yRange.indexOf(svgPosition.y),
|
||||
}));
|
||||
const { data, errors } = (await res.json()) as {
|
||||
data: GraphQLRes;
|
||||
errors?: { message: string }[];
|
||||
};
|
||||
|
||||
return { cells, colorScheme };
|
||||
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,
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
// returns the position of the svg elements, accounting for it's transform and it's parent transform
|
||||
// ( only accounts for translate transform )
|
||||
const getSvgPosition = (
|
||||
e: cheerio.Element | null
|
||||
): { x: number; y: number } => {
|
||||
if (!e || e.tagName === "svg") return { x: 0, y: 0 };
|
||||
|
||||
const p = getSvgPosition(e.parent as cheerio.Element);
|
||||
|
||||
if (e.attribs.x) p.x += +e.attribs.x;
|
||||
if (e.attribs.y) p.y += +e.attribs.y;
|
||||
|
||||
if (e.attribs.transform) {
|
||||
const m = e.attribs.transform.match(
|
||||
/translate\( *([\.\d]+) *, *([\.\d]+) *\)/
|
||||
);
|
||||
|
||||
if (m) {
|
||||
p.x += +m[1];
|
||||
p.y += +m[2];
|
||||
}
|
||||
}
|
||||
|
||||
return p;
|
||||
type GraphQLRes = {
|
||||
user: {
|
||||
contributionsCollection: {
|
||||
contributionCalendar: {
|
||||
weeks: {
|
||||
contributionDays: {
|
||||
contributionCount: number;
|
||||
contributionLevel:
|
||||
| "FOURTH_QUARTILE"
|
||||
| "THIRD_QUARTILE"
|
||||
| "SECOND_QUARTILE"
|
||||
| "FIRST_QUARTILE"
|
||||
| "NONE";
|
||||
date: string;
|
||||
weekday: number;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type Res = Awaited<ReturnType<typeof getGithubUserContribution>>;
|
||||
|
||||
export type Cell = Res["cells"][number];
|
||||
export type Cell = Res[number];
|
||||
|
||||
@@ -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,15 +1,17 @@
|
||||
import { it, expect } from "bun:test";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { createSvg } from "..";
|
||||
import { createSvg, DrawOptions as DrawOptions } from "..";
|
||||
import * as grids from "@snk/types/__fixtures__/grid";
|
||||
import { snake3 as snake } from "@snk/types/__fixtures__/snake";
|
||||
import { getBestRoute } from "@snk/solver/getBestRoute";
|
||||
import { AnimationOptions } from "@snk/gif-creator";
|
||||
|
||||
const drawOptions = {
|
||||
sizeBorderRadius: 2,
|
||||
const drawOptions: DrawOptions = {
|
||||
sizeDotBorderRadius: 2,
|
||||
sizeCell: 16,
|
||||
sizeDot: 12,
|
||||
colorBorder: "#1b1f230a",
|
||||
colorDotBorder: "#1b1f230a",
|
||||
colorDots: { 1: "#9be9a8", 2: "#40c463", 3: "#30a14e", 4: "#216e39" },
|
||||
colorEmpty: "#ebedf0",
|
||||
colorSnake: "purple",
|
||||
@@ -19,7 +21,7 @@ const drawOptions = {
|
||||
},
|
||||
};
|
||||
|
||||
const gifOptions = { frameDuration: 100, step: 1 };
|
||||
const animationOptions: AnimationOptions = { frameDuration: 100, step: 1 };
|
||||
|
||||
const dir = path.resolve(__dirname, "__snapshots__");
|
||||
|
||||
@@ -31,7 +33,13 @@ for (const [key, grid] of Object.entries(grids))
|
||||
it(`should generate ${key} svg`, async () => {
|
||||
const chain = [snake, ...getBestRoute(grid, snake)!];
|
||||
|
||||
const svg = await createSvg(grid, chain, drawOptions, gifOptions);
|
||||
const svg = await createSvg(
|
||||
grid,
|
||||
null,
|
||||
chain,
|
||||
drawOptions,
|
||||
animationOptions,
|
||||
);
|
||||
|
||||
expect(svg).toBeDefined();
|
||||
|
||||
|
||||
27
packages/svg-creator/__tests__/minifyCss.spec.ts
Normal file
27
packages/svg-creator/__tests__/minifyCss.spec.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { it, expect } from "bun:test";
|
||||
import { minifyCss } from "../css-utils";
|
||||
|
||||
it("should minify css", () => {
|
||||
expect(
|
||||
minifyCss(`
|
||||
.c {
|
||||
color : red ;
|
||||
}
|
||||
|
||||
`),
|
||||
).toBe(".c{color:red}");
|
||||
|
||||
expect(
|
||||
minifyCss(`
|
||||
.c {
|
||||
top : 0;
|
||||
color : red ;
|
||||
}
|
||||
|
||||
# {
|
||||
animation: linear 10;
|
||||
}
|
||||
|
||||
`),
|
||||
).toBe(".c{top:0;color:red}#{animation:linear 10}");
|
||||
});
|
||||
38
packages/svg-creator/css-utils.ts
Normal file
38
packages/svg-creator/css-utils.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
const percent = (x: number) =>
|
||||
parseFloat((x * 100).toFixed(2)).toString() + "%";
|
||||
|
||||
const mergeKeyFrames = (keyframes: { t: number; style: string }[]) => {
|
||||
const s = new Map<string, number[]>();
|
||||
for (const { t, style } of keyframes) {
|
||||
s.set(style, [...(s.get(style) ?? []), t]);
|
||||
}
|
||||
return Array.from(s.entries())
|
||||
.map(([style, ts]) => ({ style, ts }))
|
||||
.sort((a, b) => a.ts[0] - b.ts[0]);
|
||||
};
|
||||
|
||||
/**
|
||||
* generate the keyframe animation from a list of keyframe
|
||||
*/
|
||||
export const createAnimation = (
|
||||
name: string,
|
||||
keyframes: { t: number; style: string }[],
|
||||
) =>
|
||||
`@keyframes ${name}{` +
|
||||
mergeKeyFrames(keyframes)
|
||||
.map(({ style, ts }) => ts.map(percent).join(",") + `{${style}}`)
|
||||
.join("") +
|
||||
"}";
|
||||
|
||||
/**
|
||||
* remove white spaces
|
||||
*/
|
||||
export const minifyCss = (css: string) =>
|
||||
css
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/.\s+[,;:{}()]/g, (a) => a.replace(/\s+/g, ""))
|
||||
.replace(/[,;:{}()]\s+./g, (a) => a.replace(/\s+/g, ""))
|
||||
.replace(/.\s+[,;:{}()]/g, (a) => a.replace(/\s+/g, ""))
|
||||
.replace(/[,;:{}()]\s+./g, (a) => a.replace(/\s+/g, ""))
|
||||
.replace(/\;\s*\}/g, "}")
|
||||
.trim();
|
||||
@@ -1,22 +1,21 @@
|
||||
import type { Color, Empty } from "@snk/types/grid";
|
||||
import type { Point } from "@snk/types/point";
|
||||
import { h } from "./utils";
|
||||
import { createAnimation } from "./css-utils";
|
||||
import { h } from "./xml-utils";
|
||||
|
||||
export type Options = {
|
||||
colorDots: Record<Color, string>;
|
||||
colorEmpty: string;
|
||||
colorBorder: string;
|
||||
colorDotBorder: string;
|
||||
sizeCell: number;
|
||||
sizeDot: number;
|
||||
sizeBorderRadius: number;
|
||||
sizeDotBorderRadius: number;
|
||||
};
|
||||
|
||||
const percent = (x: number) => (x * 100).toFixed(2);
|
||||
|
||||
export const createGrid = (
|
||||
cells: (Point & { t: number | null; color: Color | Empty })[],
|
||||
{ sizeBorderRadius, sizeDot, sizeCell }: Options,
|
||||
duration: number
|
||||
{ sizeDotBorderRadius, sizeDot, sizeCell }: Options,
|
||||
duration: number,
|
||||
) => {
|
||||
const svgElements: string[] = [];
|
||||
const styles = [
|
||||
@@ -26,39 +25,41 @@ export const createGrid = (
|
||||
stroke-width: 1px;
|
||||
stroke: var(--cb);
|
||||
animation: none ${duration}ms linear infinite;
|
||||
width: ${sizeDot}px;
|
||||
height: ${sizeDot}px;
|
||||
}`,
|
||||
];
|
||||
|
||||
let i = 0;
|
||||
for (const { x, y, color, t } of cells) {
|
||||
const id = t && "c" + (i++).toString(36);
|
||||
const s = sizeCell;
|
||||
const d = sizeDot;
|
||||
const m = (s - d) / 2;
|
||||
const m = (sizeCell - sizeDot) / 2;
|
||||
|
||||
if (t !== null) {
|
||||
if (t !== null && id) {
|
||||
const animationName = id;
|
||||
|
||||
styles.push(
|
||||
`@keyframes ${animationName} {` +
|
||||
`${percent(t - 0.0001)}%{fill:var(--c${color})}` +
|
||||
`${percent(t + 0.0001)}%,100%{fill:var(--ce)}` +
|
||||
"}",
|
||||
createAnimation(animationName, [
|
||||
{ t: t - 0.0001, style: `fill:var(--c${color})` },
|
||||
{ t: t + 0.0001, style: `fill:var(--ce)` },
|
||||
{ t: 1, style: `fill:var(--ce)` },
|
||||
]),
|
||||
|
||||
`.c.${id}{fill: var(--c${color}); animation-name: ${animationName}}`
|
||||
`.c.${id}{
|
||||
fill: var(--c${color});
|
||||
animation-name: ${animationName}
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
svgElements.push(
|
||||
h("rect", {
|
||||
class: ["c", id].filter(Boolean).join(" "),
|
||||
x: x * s + m,
|
||||
y: y * s + m,
|
||||
rx: sizeBorderRadius,
|
||||
ry: sizeBorderRadius,
|
||||
width: d,
|
||||
height: d,
|
||||
})
|
||||
x: x * sizeCell + m,
|
||||
y: y * sizeCell + m,
|
||||
rx: sizeDotBorderRadius,
|
||||
ry: sizeDotBorderRadius,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,43 +9,43 @@ import { getHeadX, getHeadY } from "@snk/types/snake";
|
||||
import type { Snake } from "@snk/types/snake";
|
||||
import type { Grid, Color, Empty } from "@snk/types/grid";
|
||||
import type { Point } from "@snk/types/point";
|
||||
import type { AnimationOptions } from "@snk/gif-creator";
|
||||
import { createSnake } from "./snake";
|
||||
import { createGrid } from "./grid";
|
||||
import { createStack } from "./stack";
|
||||
import { h } from "./utils";
|
||||
import * as csso from "csso";
|
||||
import { h } from "./xml-utils";
|
||||
import { minifyCss } from "./css-utils";
|
||||
|
||||
export type Options = {
|
||||
export type DrawOptions = {
|
||||
colorDots: Record<Color, string>;
|
||||
colorEmpty: string;
|
||||
colorBorder: string;
|
||||
colorDotBorder: string;
|
||||
colorSnake: string;
|
||||
sizeCell: number;
|
||||
sizeDot: number;
|
||||
sizeBorderRadius: number;
|
||||
cells?: Point[];
|
||||
sizeDotBorderRadius: number;
|
||||
dark?: {
|
||||
colorDots: Record<Color, string>;
|
||||
colorEmpty: string;
|
||||
colorBorder?: string;
|
||||
colorDotBorder?: string;
|
||||
colorSnake?: string;
|
||||
};
|
||||
};
|
||||
|
||||
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[],
|
||||
drawOptions: Options
|
||||
cells: Point[] | null,
|
||||
) => {
|
||||
const cells: (Point & {
|
||||
const livingCells: (Point & {
|
||||
t: number | null;
|
||||
color: Color | Empty;
|
||||
})[] = (drawOptions.cells ?? getCellsFromGrid(grid0)).map(({ x, y }) => ({
|
||||
})[] = (cells ?? getCellsFromGrid(grid0)).map(({ x, y }) => ({
|
||||
x,
|
||||
y,
|
||||
t: null,
|
||||
@@ -60,35 +60,36 @@ const createLivingCells = (
|
||||
|
||||
if (isInside(grid, x, y) && !isEmpty(getColor(grid, x, y))) {
|
||||
setColorEmpty(grid, x, y);
|
||||
const cell = cells.find((c) => c.x === x && c.y === y)!;
|
||||
const cell = livingCells.find((c) => c.x === x && c.y === y)!;
|
||||
cell.t = i / chain.length;
|
||||
}
|
||||
}
|
||||
|
||||
return cells;
|
||||
return livingCells;
|
||||
};
|
||||
|
||||
export const createSvg = (
|
||||
grid: Grid,
|
||||
cells: Point[] | null,
|
||||
chain: Snake[],
|
||||
drawOptions: Options,
|
||||
gifOptions: { frameDuration: number }
|
||||
drawOptions: DrawOptions,
|
||||
animationOptions: Pick<AnimationOptions, "frameDuration">,
|
||||
) => {
|
||||
const width = (grid.width + 2) * drawOptions.sizeCell;
|
||||
const height = (grid.height + 5) * drawOptions.sizeCell;
|
||||
|
||||
const duration = gifOptions.frameDuration * chain.length;
|
||||
const duration = animationOptions.frameDuration * chain.length;
|
||||
|
||||
const cells = createLivingCells(grid, chain, drawOptions);
|
||||
const livingCells = createLivingCells(grid, chain, cells);
|
||||
|
||||
const elements = [
|
||||
createGrid(cells, drawOptions, duration),
|
||||
createGrid(livingCells, drawOptions, duration),
|
||||
createStack(
|
||||
cells,
|
||||
livingCells,
|
||||
drawOptions,
|
||||
grid.width * drawOptions.sizeCell,
|
||||
(grid.height + 2) * drawOptions.sizeCell,
|
||||
duration
|
||||
duration,
|
||||
),
|
||||
createSnake(chain, drawOptions, duration),
|
||||
];
|
||||
@@ -131,13 +132,13 @@ export const createSvg = (
|
||||
return optimizeSvg(svg);
|
||||
};
|
||||
|
||||
const optimizeCss = (css: string) => csso.minify(css).css;
|
||||
const optimizeCss = (css: string) => minifyCss(css);
|
||||
const optimizeSvg = (svg: string) => svg;
|
||||
|
||||
const generateColorVar = (drawOptions: Options) =>
|
||||
const generateColorVar = (drawOptions: DrawOptions) =>
|
||||
`
|
||||
:root {
|
||||
--cb: ${drawOptions.colorBorder};
|
||||
--cb: ${drawOptions.colorDotBorder};
|
||||
--cs: ${drawOptions.colorSnake};
|
||||
--ce: ${drawOptions.colorEmpty};
|
||||
${Object.entries(drawOptions.colorDots)
|
||||
@@ -149,7 +150,7 @@ const generateColorVar = (drawOptions: Options) =>
|
||||
? `
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--cb: ${drawOptions.dark.colorBorder || drawOptions.colorBorder};
|
||||
--cb: ${drawOptions.dark.colorDotBorder || drawOptions.colorDotBorder};
|
||||
--cs: ${drawOptions.dark.colorSnake || drawOptions.colorSnake};
|
||||
--ce: ${drawOptions.dark.colorEmpty};
|
||||
${Object.entries(drawOptions.dark.colorDots)
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
"name": "@snk/svg-creator",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@snk/solver": "1.0.0",
|
||||
"csso": "5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/csso": "5.0.0"
|
||||
"@snk/solver": "1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
import { getSnakeLength, snakeToCells } from "@snk/types/snake";
|
||||
import type { Snake } from "@snk/types/snake";
|
||||
import type { Color } from "@snk/types/grid";
|
||||
import type { Point } from "@snk/types/point";
|
||||
import { h } from "./utils";
|
||||
import { h } from "./xml-utils";
|
||||
import { createAnimation } from "./css-utils";
|
||||
|
||||
export type Options = {
|
||||
colorDots: Record<Color, string>;
|
||||
colorEmpty: string;
|
||||
colorBorder: string;
|
||||
colorSnake: string;
|
||||
sizeCell: number;
|
||||
sizeDot: number;
|
||||
sizeBorderRadius: number;
|
||||
};
|
||||
|
||||
const percent = (x: number) => (x * 100).toFixed(2);
|
||||
|
||||
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;
|
||||
|
||||
@@ -60,8 +54,8 @@ export const createSnake = (
|
||||
|
||||
const styles = [
|
||||
`.s{
|
||||
shape-rendering:geometricPrecision;
|
||||
fill:var(--cs);
|
||||
shape-rendering: geometricPrecision;
|
||||
fill: var(--cs);
|
||||
animation: none linear ${duration}ms infinite
|
||||
}`,
|
||||
|
||||
@@ -69,16 +63,17 @@ export const createSnake = (
|
||||
const id = `s${i}`;
|
||||
const animationName = id;
|
||||
|
||||
return [
|
||||
`@keyframes ${animationName} {` +
|
||||
removeInterpolatedPositions(
|
||||
positions.map((tr, i, { length }) => ({ ...tr, t: i / length }))
|
||||
)
|
||||
.map((p) => `${percent(p.t)}%{${transform(p)}}`)
|
||||
.join("") +
|
||||
"}",
|
||||
const keyframes = removeInterpolatedPositions(
|
||||
positions.map((tr, i, { length }) => ({ ...tr, t: i / length })),
|
||||
).map(({ t, ...p }) => ({ t, style: transform(p) }));
|
||||
|
||||
`.s.${id}{${transform(positions[0])};animation-name: ${animationName}}`,
|
||||
return [
|
||||
createAnimation(animationName, keyframes),
|
||||
|
||||
`.s.${id}{
|
||||
${transform(positions[0])};
|
||||
animation-name: ${animationName}
|
||||
}`,
|
||||
];
|
||||
}),
|
||||
].flat();
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import type { Color, Empty } from "@snk/types/grid";
|
||||
import { h } from "./utils";
|
||||
import { createAnimation } from "./css-utils";
|
||||
import { h } from "./xml-utils";
|
||||
|
||||
export type Options = {
|
||||
sizeDot: number;
|
||||
};
|
||||
|
||||
const percent = (x: number) => (x * 100).toFixed(2);
|
||||
|
||||
export const createStack = (
|
||||
cells: { t: number | null; color: Color | Empty }[],
|
||||
{ sizeDot }: Options,
|
||||
width: number,
|
||||
y: number,
|
||||
duration: number
|
||||
duration: number,
|
||||
) => {
|
||||
const svgElements: string[] = [];
|
||||
const styles = [
|
||||
@@ -52,27 +51,32 @@ export const createStack = (
|
||||
width: (ts.length * m + 0.6).toFixed(1),
|
||||
x,
|
||||
y,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
styles.push(
|
||||
`@keyframes ${animationName} {` +
|
||||
createAnimation(
|
||||
animationName,
|
||||
[
|
||||
...ts.map((t, i, { length }) => [
|
||||
{ scale: i / length, t: t - 0.0001 },
|
||||
{ scale: (i + 1) / length, t: t + 0.0001 },
|
||||
]),
|
||||
[{ scale: 1, t: 1 }],
|
||||
]
|
||||
.flat()
|
||||
.map(
|
||||
({ scale, t }) =>
|
||||
`${percent(t)}%{transform:scale(${scale.toFixed(2)},1)}`
|
||||
)
|
||||
.join("\n") +
|
||||
"}",
|
||||
...ts
|
||||
.map((t, i, { length }) => [
|
||||
{ scale: i / length, t: t - 0.0001 },
|
||||
{ scale: (i + 1) / length, t: t + 0.0001 },
|
||||
])
|
||||
.flat(),
|
||||
{ scale: 1, t: 1 },
|
||||
].map(({ scale, t }) => ({
|
||||
t,
|
||||
style: `transform:scale(${scale.toFixed(3)},1)`,
|
||||
})),
|
||||
),
|
||||
|
||||
`.u.${id}{fill:var(--c${color});animation-name:${animationName};transform-origin:${x}px 0}`
|
||||
`.u.${id} {
|
||||
fill: var(--c${color});
|
||||
animation-name: ${animationName};
|
||||
transform-origin: ${x}px 0
|
||||
}
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,18 +3,33 @@ description: "Generates a snake game from a github user contributions grid. Outp
|
||||
author: "platane"
|
||||
|
||||
runs:
|
||||
using: node16
|
||||
using: node20
|
||||
main: dist/index.js
|
||||
|
||||
inputs:
|
||||
github_user_name:
|
||||
description: "github user name"
|
||||
required: true
|
||||
svg_out_path:
|
||||
description: "path of the generated svg file. If left empty, the svg file will not be generated."
|
||||
github_token:
|
||||
description: "github token used to fetch the contribution calendar. Default to the action token if empty."
|
||||
required: false
|
||||
default: null
|
||||
default: ${{ github.token }}
|
||||
outputs:
|
||||
required: false
|
||||
description: |
|
||||
list of files to generate.
|
||||
one file per line. Each output can be customized with options as query string.
|
||||
|
||||
outputs:
|
||||
svg_out_path:
|
||||
description: "path of the generated svg"
|
||||
supported query string options:
|
||||
|
||||
- palette: A preset of color, one of [github, github-dark, github-light]
|
||||
- color_snake: Color of the snake
|
||||
- color_dots: Coma separated list of dots color.
|
||||
The first one is 0 contribution, then it goes from the low contribution to the highest.
|
||||
Exactly 5 colors are expected.
|
||||
|
||||
example:
|
||||
outputs: |
|
||||
dark.svg?palette=github-dark&color_snake=blue
|
||||
light.svg?color_snake=#7845ab
|
||||
ocean.svg?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9
|
||||
|
||||
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
768
svg-only/dist/324.index.js
vendored
Normal file
768
svg-only/dist/324.index.js
vendored
Normal file
@@ -0,0 +1,768 @@
|
||||
"use strict";
|
||||
exports.id = 324;
|
||||
exports.ids = [324];
|
||||
exports.modules = {
|
||||
|
||||
/***/ 324:
|
||||
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
|
||||
// EXPORTS
|
||||
__webpack_require__.d(__webpack_exports__, {
|
||||
generateContributionSnake: () => (/* binding */ generateContributionSnake)
|
||||
});
|
||||
|
||||
;// CONCATENATED MODULE: ../github-user-contribution/index.ts
|
||||
/**
|
||||
* get the contribution grid from a github user page
|
||||
*
|
||||
* use options.from=YYYY-MM-DD options.to=YYYY-MM-DD to get the contribution grid for a specific time range
|
||||
* or year=2019 as an alias for from=2019-01-01 to=2019-12-31
|
||||
*
|
||||
* otherwise return use the time range from today minus one year to today ( as seen in github profile page )
|
||||
*
|
||||
* @param userName github user name
|
||||
* @param options
|
||||
*
|
||||
* @example
|
||||
* getGithubUserContribution("platane", { from: "2019-01-01", to: "2019-12-31" })
|
||||
* getGithubUserContribution("platane", { year: 2019 })
|
||||
*
|
||||
*/
|
||||
const getGithubUserContribution = async (userName, o) => {
|
||||
const query = /* GraphQL */ `
|
||||
query ($login: String!) {
|
||||
user(login: $login) {
|
||||
contributionsCollection {
|
||||
contributionCalendar {
|
||||
weeks {
|
||||
contributionDays {
|
||||
contributionCount
|
||||
contributionLevel
|
||||
weekday
|
||||
date
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const variables = { login: userName };
|
||||
const res = await fetch("https://api.github.com/graphql", {
|
||||
headers: {
|
||||
Authorization: `bearer ${o.githubToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "me@platane.me",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({ variables, query }),
|
||||
});
|
||||
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__(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 */.Kb)(width, height);
|
||||
for (const c of cells) {
|
||||
if (c.level > 0)
|
||||
(0,types_grid/* setColor */.wW)(grid, c.x, c.y, c.level);
|
||||
else
|
||||
(0,types_grid/* setColorEmpty */.l$)(grid, c.x, c.y);
|
||||
}
|
||||
return grid;
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../types/point.ts
|
||||
const around4 = [
|
||||
{ x: 1, y: 0 },
|
||||
{ x: 0, y: -1 },
|
||||
{ x: -1, y: 0 },
|
||||
{ x: 0, y: 1 },
|
||||
];
|
||||
const pointEquals = (a, b) => a.x === b.x && a.y === b.y;
|
||||
|
||||
;// CONCATENATED MODULE: ../solver/outside.ts
|
||||
|
||||
|
||||
const createOutside = (grid, color = 0) => {
|
||||
const outside = (0,types_grid/* createEmptyGrid */.Kb)(grid.width, grid.height);
|
||||
for (let x = outside.width; x--;)
|
||||
for (let y = outside.height; y--;)
|
||||
(0,types_grid/* setColor */.wW)(outside, x, y, 1);
|
||||
fillOutside(outside, grid, color);
|
||||
return outside;
|
||||
};
|
||||
const fillOutside = (outside, grid, color = 0) => {
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (let x = outside.width; x--;)
|
||||
for (let y = outside.height; y--;)
|
||||
if ((0,types_grid/* getColor */.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 */.l$)(outside, x, y);
|
||||
}
|
||||
}
|
||||
return outside;
|
||||
};
|
||||
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__(777);
|
||||
;// CONCATENATED MODULE: ../solver/utils/sortPush.ts
|
||||
const sortPush = (arr, x, sortFn) => {
|
||||
let a = 0;
|
||||
let b = arr.length;
|
||||
if (arr.length === 0 || sortFn(x, arr[a]) <= 0) {
|
||||
arr.unshift(x);
|
||||
return;
|
||||
}
|
||||
while (b - a > 1) {
|
||||
const e = Math.ceil((a + b) / 2);
|
||||
const s = sortFn(x, arr[e]);
|
||||
if (s === 0)
|
||||
a = b = e;
|
||||
else if (s > 0)
|
||||
a = e;
|
||||
else
|
||||
b = e;
|
||||
}
|
||||
const e = Math.ceil((a + b) / 2);
|
||||
arr.splice(e, 0, x);
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../solver/tunnel.ts
|
||||
|
||||
|
||||
/**
|
||||
* get the sequence of snake to cross the tunnel
|
||||
*/
|
||||
const getTunnelPath = (snake0, tunnel) => {
|
||||
const chain = [];
|
||||
let snake = snake0;
|
||||
for (let i = 1; i < tunnel.length; i++) {
|
||||
const dx = tunnel[i].x - (0,types_snake/* getHeadX */.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;
|
||||
};
|
||||
/**
|
||||
* assuming the grid change and the colors got deleted, update the tunnel
|
||||
*/
|
||||
const updateTunnel = (grid, tunnel, toDelete) => {
|
||||
while (tunnel.length) {
|
||||
const { x, y } = tunnel[0];
|
||||
if (isEmptySafe(grid, x, y) ||
|
||||
toDelete.some((p) => p.x === x && p.y === y)) {
|
||||
tunnel.shift();
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
while (tunnel.length) {
|
||||
const { x, y } = tunnel[tunnel.length - 1];
|
||||
if (isEmptySafe(grid, x, y) ||
|
||||
toDelete.some((p) => p.x === x && p.y === y)) {
|
||||
tunnel.pop();
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
};
|
||||
const isEmptySafe = (grid, x, y) => !(0,types_grid/* isInside */.FK)(grid, x, y) || (0,types_grid/* isEmpty */.Im)((0,types_grid/* getColor */.oU)(grid, x, y));
|
||||
/**
|
||||
* remove empty cell from start
|
||||
*/
|
||||
const trimTunnelStart = (grid, tunnel) => {
|
||||
while (tunnel.length) {
|
||||
const { x, y } = tunnel[0];
|
||||
if (isEmptySafe(grid, x, y))
|
||||
tunnel.shift();
|
||||
else
|
||||
break;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* remove empty cell from end
|
||||
*/
|
||||
const trimTunnelEnd = (grid, tunnel) => {
|
||||
while (tunnel.length) {
|
||||
const i = tunnel.length - 1;
|
||||
const { x, y } = tunnel[i];
|
||||
if (isEmptySafe(grid, x, y) ||
|
||||
tunnel.findIndex((p) => p.x === x && p.y === y) < i)
|
||||
tunnel.pop();
|
||||
else
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../solver/getBestTunnel.ts
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const getColorSafe = (grid, x, y) => (0,types_grid/* isInside */.FK)(grid, x, y) ? (0,types_grid/* getColor */.oU)(grid, x, y) : 0;
|
||||
const setEmptySafe = (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 */.tN)(m.snake), y: (0,types_snake/* getHeadY */.Ap)(m.snake) }];
|
||||
/**
|
||||
* returns the path to reach the outside which contains the least color cell
|
||||
*/
|
||||
const getSnakeEscapePath = (grid, outside, snake0, color) => {
|
||||
const openList = [{ snake: snake0, w: 0 }];
|
||||
const closeList = [];
|
||||
while (openList[0]) {
|
||||
const o = openList.shift();
|
||||
const x = (0,types_snake/* getHeadX */.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 */.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
/**
|
||||
* compute the best tunnel to get to the cell and back to the outside ( best = less usage of <color> )
|
||||
*
|
||||
* notice that it's one of the best tunnels, more with the same score could exist
|
||||
*/
|
||||
const getBestTunnel = (grid, outside, x, y, color, snakeN) => {
|
||||
const c = { x, y };
|
||||
const snake0 = (0,types_snake/* createSnakeFromCells */.yS)(Array.from({ length: snakeN }, () => c));
|
||||
const one = getSnakeEscapePath(grid, outside, snake0, color);
|
||||
if (!one)
|
||||
return null;
|
||||
// get the position of the snake if it was going to leave the x,y cell
|
||||
const snakeICells = one.slice(0, snakeN);
|
||||
while (snakeICells.length < snakeN)
|
||||
snakeICells.push(snakeICells[snakeICells.length - 1]);
|
||||
const snakeI = (0,types_snake/* createSnakeFromCells */.yS)(snakeICells);
|
||||
// remove from the grid the colors that one eat
|
||||
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);
|
||||
if (!two)
|
||||
return null;
|
||||
one.shift();
|
||||
one.reverse();
|
||||
one.push(...two);
|
||||
trimTunnelStart(grid, one);
|
||||
trimTunnelEnd(grid, one);
|
||||
return one;
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../solver/getPathTo.ts
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* starting from snake0, get to the cell x,y
|
||||
* return the snake chain (reversed)
|
||||
*/
|
||||
const getPathTo = (grid, snake0, x, y) => {
|
||||
const openList = [{ snake: snake0, w: 0 }];
|
||||
const closeList = [];
|
||||
while (openList.length) {
|
||||
const c = openList.shift();
|
||||
const cx = (0,types_snake/* getHeadX */.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 */.Sc)(c.snake, dx, dy)];
|
||||
let e = c;
|
||||
while (e.parent) {
|
||||
path.push(e.snake);
|
||||
e = e.parent;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
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;
|
||||
const o = { snake: nsnake, parent: c, w, h, f };
|
||||
sortPush(openList, o, (a, b) => a.f - b.f);
|
||||
closeList.push(nsnake);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../solver/clearResidualColoredLayer.ts
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const clearResidualColoredLayer = (grid, outside, snake0, color) => {
|
||||
const snakeN = (0,types_snake/* getSnakeLength */.T$)(snake0);
|
||||
const tunnels = getTunnellablePoints(grid, outside, snakeN, color);
|
||||
// sort
|
||||
tunnels.sort((a, b) => b.priority - a.priority);
|
||||
const chain = [snake0];
|
||||
while (tunnels.length) {
|
||||
// get the best next tunnel
|
||||
let t = getNextTunnel(tunnels, chain[0]);
|
||||
// goes to the start of the tunnel
|
||||
chain.unshift(...getPathTo(grid, chain[0], t[0].x, t[0].y));
|
||||
// goes to the end of the tunnel
|
||||
chain.unshift(...getTunnelPath(chain[0], t));
|
||||
// update grid
|
||||
for (const { x, y } of t)
|
||||
clearResidualColoredLayer_setEmptySafe(grid, x, y);
|
||||
// update outside
|
||||
fillOutside(outside, grid);
|
||||
// update tunnels
|
||||
for (let i = tunnels.length; i--;)
|
||||
if ((0,types_grid/* isEmpty */.Im)((0,types_grid/* getColor */.oU)(grid, tunnels[i].x, tunnels[i].y)))
|
||||
tunnels.splice(i, 1);
|
||||
else {
|
||||
const t = tunnels[i];
|
||||
const tunnel = getBestTunnel(grid, outside, t.x, t.y, color, snakeN);
|
||||
if (!tunnel)
|
||||
tunnels.splice(i, 1);
|
||||
else {
|
||||
t.tunnel = tunnel;
|
||||
t.priority = getPriority(grid, color, tunnel);
|
||||
}
|
||||
}
|
||||
// re-sort
|
||||
tunnels.sort((a, b) => b.priority - a.priority);
|
||||
}
|
||||
chain.pop();
|
||||
return chain;
|
||||
};
|
||||
const getNextTunnel = (ts, snake) => {
|
||||
let minDistance = Infinity;
|
||||
let closestTunnel = null;
|
||||
const x = (0,types_snake/* getHeadX */.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;
|
||||
const d = distanceSq(t[0].x, t[0].y, x, y);
|
||||
if (d < minDistance) {
|
||||
minDistance = d;
|
||||
closestTunnel = t;
|
||||
}
|
||||
}
|
||||
return closestTunnel;
|
||||
};
|
||||
/**
|
||||
* get all the tunnels for all the cells accessible
|
||||
*/
|
||||
const getTunnellablePoints = (grid, outside, snakeN, color) => {
|
||||
const points = [];
|
||||
for (let x = grid.width; x--;)
|
||||
for (let y = grid.height; y--;) {
|
||||
const c = (0,types_grid/* getColor */.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);
|
||||
points.push({ x, y, priority, tunnel });
|
||||
}
|
||||
}
|
||||
}
|
||||
return points;
|
||||
};
|
||||
/**
|
||||
* get the score of the tunnel
|
||||
* prioritize tunnel with maximum color smaller than <color> and with minimum <color>
|
||||
* with some tweaks
|
||||
*/
|
||||
const getPriority = (grid, color, tunnel) => {
|
||||
let nColor = 0;
|
||||
let nLess = 0;
|
||||
for (let i = 0; i < tunnel.length; i++) {
|
||||
const { x, y } = tunnel[i];
|
||||
const c = clearResidualColoredLayer_getColorSafe(grid, x, y);
|
||||
if (!(0,types_grid/* isEmpty */.Im)(c) && i === tunnel.findIndex((p) => p.x === x && p.y === y)) {
|
||||
if (c === color)
|
||||
nColor += 1;
|
||||
else
|
||||
nLess += color - c;
|
||||
}
|
||||
}
|
||||
if (nColor === 0)
|
||||
return 99999;
|
||||
return nLess / nColor;
|
||||
};
|
||||
const distanceSq = (ax, ay, bx, by) => (ax - bx) ** 2 + (ay - by) ** 2;
|
||||
const clearResidualColoredLayer_getColorSafe = (grid, x, y) => (0,types_grid/* isInside */.FK)(grid, x, y) ? (0,types_grid/* getColor */.oU)(grid, x, y) : 0;
|
||||
const clearResidualColoredLayer_setEmptySafe = (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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const clearCleanColoredLayer = (grid, outside, snake0, color) => {
|
||||
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 */.tN)(snake), (0,types_snake/* getHeadY */.Ap)(snake));
|
||||
chain.unshift(...path);
|
||||
}
|
||||
fillOutside(outside, grid);
|
||||
chain.pop();
|
||||
return chain;
|
||||
};
|
||||
const clearCleanColoredLayer_unwrap = (m) => !m ? [] : [m.snake, ...clearCleanColoredLayer_unwrap(m.parent)];
|
||||
const getPathToNextPoint = (grid, snake0, color, points) => {
|
||||
const closeList = [];
|
||||
const openList = [{ snake: snake0 }];
|
||||
while (openList.length) {
|
||||
const o = openList.shift();
|
||||
const x = (0,types_snake/* getHeadX */.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 */.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 */.Sc)(o.snake, dx, dy);
|
||||
if (!closeList.some((s0) => (0,types_snake/* snakeEquals */.sW)(s0, snake))) {
|
||||
closeList.push(snake);
|
||||
openList.push({ snake, parent: o });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
/**
|
||||
* get all cells that are tunnellable
|
||||
*/
|
||||
const clearCleanColoredLayer_getTunnellablePoints = (grid, outside, snakeN, color) => {
|
||||
const points = [];
|
||||
for (let x = grid.width; x--;)
|
||||
for (let y = grid.height; y--;) {
|
||||
const c = (0,types_grid/* getColor */.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);
|
||||
if (tunnel)
|
||||
for (const p of tunnel)
|
||||
if (!clearCleanColoredLayer_isEmptySafe(grid, p.x, p.y))
|
||||
points.push(p);
|
||||
}
|
||||
}
|
||||
return points;
|
||||
};
|
||||
const clearCleanColoredLayer_getColorSafe = (grid, x, y) => (0,types_grid/* isInside */.FK)(grid, x, y) ? (0,types_grid/* getColor */.oU)(grid, x, y) : 0;
|
||||
const clearCleanColoredLayer_setEmptySafe = (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 */.FK)(grid, x, y) && (0,types_grid/* isEmpty */.Im)((0,types_grid/* getColor */.oU)(grid, x, y));
|
||||
|
||||
;// CONCATENATED MODULE: ../solver/getBestRoute.ts
|
||||
|
||||
|
||||
|
||||
|
||||
const getBestRoute = (grid0, snake0) => {
|
||||
const grid = (0,types_grid/* copyGrid */.mi)(grid0);
|
||||
const outside = createOutside(grid);
|
||||
const chain = [snake0];
|
||||
for (const color of extractColors(grid)) {
|
||||
if (color > 1)
|
||||
chain.unshift(...clearResidualColoredLayer(grid, outside, chain[0], color));
|
||||
chain.unshift(...clearCleanColoredLayer(grid, outside, chain[0], color));
|
||||
}
|
||||
return chain.reverse();
|
||||
};
|
||||
const extractColors = (grid) => {
|
||||
// @ts-ignore
|
||||
let maxColor = Math.max(...grid.data);
|
||||
return Array.from({ length: maxColor }, (_, i) => (i + 1));
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../types/__fixtures__/snake.ts
|
||||
|
||||
const create = (length) => (0,types_snake/* createSnakeFromCells */.yS)(Array.from({ length }, (_, i) => ({ x: i, y: -1 })));
|
||||
const snake1 = create(1);
|
||||
const snake3 = create(3);
|
||||
const snake4 = create(4);
|
||||
const snake5 = create(5);
|
||||
const snake9 = create(9);
|
||||
|
||||
;// CONCATENATED MODULE: ../solver/getPathToPose.ts
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const getPathToPose_isEmptySafe = (grid, x, y) => !(0,types_grid/* isInside */.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 */.sW)(snake0, target))
|
||||
return [];
|
||||
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 */.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 */.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;
|
||||
forbidden.slice(0, 3);
|
||||
const openList = [{ snake: snake0, w: 0 }];
|
||||
const closeList = [];
|
||||
while (openList.length) {
|
||||
const o = openList.shift();
|
||||
const x = (0,types_snake/* getHeadX */.tN)(o.snake);
|
||||
const y = (0,types_snake/* getHeadY */.Ap)(o.snake);
|
||||
if (x === t0.x && y === t0.y) {
|
||||
const path = [];
|
||||
let e = o;
|
||||
while (e) {
|
||||
path.push(e.snake);
|
||||
e = e.parent;
|
||||
}
|
||||
path.unshift(...getTunnelPath(path[0], targetCells));
|
||||
path.pop();
|
||||
path.reverse();
|
||||
return path;
|
||||
}
|
||||
for (let i = 0; i < around4.length; i++) {
|
||||
const { x: dx, y: dy } = around4[i];
|
||||
const nx = x + dx;
|
||||
const ny = y + dy;
|
||||
if (!(0,types_snake/* snakeWillSelfCollide */.J)(o.snake, dx, dy) &&
|
||||
(!grid || getPathToPose_isEmptySafe(grid, nx, ny)) &&
|
||||
(grid
|
||||
? (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 */.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;
|
||||
sortPush(openList, { f, w, snake, parent: o }, (a, b) => a.f - b.f);
|
||||
closeList.push(snake);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ./generateContributionSnake.ts
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const generateContributionSnake = async (userName, outputs, options) => {
|
||||
console.log("🎣 fetching github user contribution");
|
||||
const cells = await getGithubUserContribution(userName, options);
|
||||
const grid = userContributionToGrid(cells);
|
||||
const snake = snake4;
|
||||
console.log("📡 computing best route");
|
||||
const chain = getBestRoute(grid, snake);
|
||||
chain.push(...getPathToPose(chain.slice(-1)[0], snake));
|
||||
return Promise.all(outputs.map(async (out, i) => {
|
||||
if (!out)
|
||||
return;
|
||||
const { format, drawOptions, animationOptions } = out;
|
||||
switch (format) {
|
||||
case "svg": {
|
||||
console.log(`🖌 creating svg (outputs[${i}])`);
|
||||
const { createSvg } = await __webpack_require__.e(/* import() */ 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(155), __webpack_require__.e(642)]).then(__webpack_require__.bind(__webpack_require__, 3642));
|
||||
return await createGif(grid, cells, chain, drawOptions, animationOptions);
|
||||
}
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 105:
|
||||
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
||||
/* 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;
|
||||
const isInsideLarge = (grid, m, x, y) => x >= -m && y >= -m && x < grid.width + m && y < grid.height + m;
|
||||
const copyGrid = ({ width, height, data }) => ({
|
||||
width,
|
||||
height,
|
||||
data: Uint8Array.from(data),
|
||||
});
|
||||
const getIndex = (grid, x, y) => x * grid.height + y;
|
||||
const getColor = (grid, x, y) => grid.data[getIndex(grid, x, y)];
|
||||
const isEmpty = (color) => color === 0;
|
||||
const setColor = (grid, x, y, color) => {
|
||||
grid.data[getIndex(grid, x, y)] = color || 0;
|
||||
};
|
||||
const setColorEmpty = (grid, x, y) => {
|
||||
setColor(grid, x, y, 0);
|
||||
};
|
||||
/**
|
||||
* return true if the grid is empty
|
||||
*/
|
||||
const isGridEmpty = (grid) => grid.data.every((x) => x === 0);
|
||||
const gridEquals = (a, b) => a.data.every((_, i) => a.data[i] === b.data[i]);
|
||||
const createEmptyGrid = (width, height) => ({
|
||||
width,
|
||||
height,
|
||||
data: new Uint8Array(width * height),
|
||||
});
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 777:
|
||||
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
||||
/* 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;
|
||||
const getHeadY = (snake) => snake[1] - 2;
|
||||
const getSnakeLength = (snake) => snake.length / 2;
|
||||
const copySnake = (snake) => snake.slice();
|
||||
const snakeEquals = (a, b) => {
|
||||
for (let i = 0; i < a.length; i++)
|
||||
if (a[i] !== b[i])
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
/**
|
||||
* return a copy of the next snake, considering that dx, dy is the direction
|
||||
*/
|
||||
const nextSnake = (snake, dx, dy) => {
|
||||
const copy = new Uint8Array(snake.length);
|
||||
for (let i = 2; i < snake.length; i++)
|
||||
copy[i] = snake[i - 2];
|
||||
copy[0] = snake[0] + dx;
|
||||
copy[1] = snake[1] + dy;
|
||||
return copy;
|
||||
};
|
||||
/**
|
||||
* return true if the next snake will collide with itself
|
||||
*/
|
||||
const snakeWillSelfCollide = (snake, dx, dy) => {
|
||||
const nx = snake[0] + dx;
|
||||
const ny = snake[1] + dy;
|
||||
for (let i = 2; i < snake.length - 2; i += 2)
|
||||
if (snake[i + 0] === nx && snake[i + 1] === ny)
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
const snakeToCells = (snake) => Array.from({ length: snake.length / 2 }, (_, i) => ({
|
||||
x: snake[i * 2 + 0] - 2,
|
||||
y: snake[i * 2 + 1] - 2,
|
||||
}));
|
||||
const createSnakeFromCells = (points) => {
|
||||
const snake = new Uint8Array(points.length * 2);
|
||||
for (let i = points.length; i--;) {
|
||||
snake[i * 2 + 0] = points[i].x + 2;
|
||||
snake[i * 2 + 1] = points[i].y + 2;
|
||||
}
|
||||
return snake;
|
||||
};
|
||||
|
||||
|
||||
/***/ })
|
||||
|
||||
};
|
||||
;
|
||||
323
svg-only/dist/578.index.js
vendored
Normal file
323
svg-only/dist/578.index.js
vendored
Normal file
@@ -0,0 +1,323 @@
|
||||
"use strict";
|
||||
exports.id = 578;
|
||||
exports.ids = [578];
|
||||
exports.modules = {
|
||||
|
||||
/***/ 4578:
|
||||
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
|
||||
// EXPORTS
|
||||
__webpack_require__.d(__webpack_exports__, {
|
||||
createSvg: () => (/* binding */ createSvg)
|
||||
});
|
||||
|
||||
// EXTERNAL MODULE: ../types/grid.ts
|
||||
var types_grid = __webpack_require__(105);
|
||||
// EXTERNAL MODULE: ../types/snake.ts
|
||||
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)
|
||||
.filter(([, value]) => value !== null)
|
||||
.map(([name, value]) => `${name}="${value}"`)
|
||||
.join(" ");
|
||||
|
||||
;// CONCATENATED MODULE: ../svg-creator/css-utils.ts
|
||||
const percent = (x) => parseFloat((x * 100).toFixed(2)).toString() + "%";
|
||||
const mergeKeyFrames = (keyframes) => {
|
||||
const s = new Map();
|
||||
for (const { t, style } of keyframes) {
|
||||
s.set(style, [...(s.get(style) ?? []), t]);
|
||||
}
|
||||
return Array.from(s.entries())
|
||||
.map(([style, ts]) => ({ style, ts }))
|
||||
.sort((a, b) => a.ts[0] - b.ts[0]);
|
||||
};
|
||||
/**
|
||||
* generate the keyframe animation from a list of keyframe
|
||||
*/
|
||||
const createAnimation = (name, keyframes) => `@keyframes ${name}{` +
|
||||
mergeKeyFrames(keyframes)
|
||||
.map(({ style, ts }) => ts.map(percent).join(",") + `{${style}}`)
|
||||
.join("") +
|
||||
"}";
|
||||
/**
|
||||
* remove white spaces
|
||||
*/
|
||||
const minifyCss = (css) => css
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/.\s+[,;:{}()]/g, (a) => a.replace(/\s+/g, ""))
|
||||
.replace(/[,;:{}()]\s+./g, (a) => a.replace(/\s+/g, ""))
|
||||
.replace(/.\s+[,;:{}()]/g, (a) => a.replace(/\s+/g, ""))
|
||||
.replace(/[,;:{}()]\s+./g, (a) => a.replace(/\s+/g, ""))
|
||||
.replace(/\;\s*\}/g, "}")
|
||||
.trim();
|
||||
|
||||
;// CONCATENATED MODULE: ../svg-creator/snake.ts
|
||||
|
||||
|
||||
|
||||
const lerp = (k, a, b) => (1 - k) * a + k * b;
|
||||
const createSnake = (chain, { sizeCell, sizeDot }, duration) => {
|
||||
const snakeN = chain[0] ? (0,types_snake/* getSnakeLength */.T$)(chain[0]) : 0;
|
||||
const snakeParts = Array.from({ length: snakeN }, () => []);
|
||||
for (const snake of chain) {
|
||||
const cells = (0,types_snake/* snakeToCells */.HU)(snake);
|
||||
for (let i = cells.length; i--;)
|
||||
snakeParts[i].push(cells[i]);
|
||||
}
|
||||
const svgElements = snakeParts.map((_, i, { length }) => {
|
||||
// compute snake part size
|
||||
const dMin = sizeDot * 0.8;
|
||||
const dMax = sizeCell * 0.9;
|
||||
const iMax = Math.min(4, length);
|
||||
const u = (1 - Math.min(i, iMax) / iMax) ** 2;
|
||||
const s = lerp(u, dMin, dMax);
|
||||
const m = (sizeCell - s) / 2;
|
||||
const r = Math.min(4.5, (4 * s) / sizeDot);
|
||||
return h("rect", {
|
||||
class: `s s${i}`,
|
||||
x: m.toFixed(1),
|
||||
y: m.toFixed(1),
|
||||
width: s.toFixed(1),
|
||||
height: s.toFixed(1),
|
||||
rx: r.toFixed(1),
|
||||
ry: r.toFixed(1),
|
||||
});
|
||||
});
|
||||
const transform = ({ x, y }) => `transform:translate(${x * sizeCell}px,${y * sizeCell}px)`;
|
||||
const styles = [
|
||||
`.s{
|
||||
shape-rendering: geometricPrecision;
|
||||
fill: var(--cs);
|
||||
animation: none linear ${duration}ms infinite
|
||||
}`,
|
||||
...snakeParts.map((positions, i) => {
|
||||
const id = `s${i}`;
|
||||
const animationName = id;
|
||||
const keyframes = removeInterpolatedPositions(positions.map((tr, i, { length }) => ({ ...tr, t: i / length }))).map(({ t, ...p }) => ({ t, style: transform(p) }));
|
||||
return [
|
||||
createAnimation(animationName, keyframes),
|
||||
`.s.${id}{
|
||||
${transform(positions[0])};
|
||||
animation-name: ${animationName}
|
||||
}`,
|
||||
];
|
||||
}),
|
||||
].flat();
|
||||
return { svgElements, styles };
|
||||
};
|
||||
const removeInterpolatedPositions = (arr) => arr.filter((u, i, arr) => {
|
||||
if (i - 1 < 0 || i + 1 >= arr.length)
|
||||
return true;
|
||||
const a = arr[i - 1];
|
||||
const b = arr[i + 1];
|
||||
const ex = (a.x + b.x) / 2;
|
||||
const ey = (a.y + b.y) / 2;
|
||||
// return true;
|
||||
return !(Math.abs(ex - u.x) < 0.01 && Math.abs(ey - u.y) < 0.01);
|
||||
});
|
||||
|
||||
;// CONCATENATED MODULE: ../svg-creator/grid.ts
|
||||
|
||||
|
||||
const createGrid = (cells, { sizeDotBorderRadius, sizeDot, sizeCell }, duration) => {
|
||||
const svgElements = [];
|
||||
const styles = [
|
||||
`.c{
|
||||
shape-rendering: geometricPrecision;
|
||||
fill: var(--ce);
|
||||
stroke-width: 1px;
|
||||
stroke: var(--cb);
|
||||
animation: none ${duration}ms linear infinite;
|
||||
width: ${sizeDot}px;
|
||||
height: ${sizeDot}px;
|
||||
}`,
|
||||
];
|
||||
let i = 0;
|
||||
for (const { x, y, color, t } of cells) {
|
||||
const id = t && "c" + (i++).toString(36);
|
||||
const m = (sizeCell - sizeDot) / 2;
|
||||
if (t !== null && id) {
|
||||
const animationName = id;
|
||||
styles.push(createAnimation(animationName, [
|
||||
{ t: t - 0.0001, style: `fill:var(--c${color})` },
|
||||
{ t: t + 0.0001, style: `fill:var(--ce)` },
|
||||
{ t: 1, style: `fill:var(--ce)` },
|
||||
]), `.c.${id}{
|
||||
fill: var(--c${color});
|
||||
animation-name: ${animationName}
|
||||
}`);
|
||||
}
|
||||
svgElements.push(h("rect", {
|
||||
class: ["c", id].filter(Boolean).join(" "),
|
||||
x: x * sizeCell + m,
|
||||
y: y * sizeCell + m,
|
||||
rx: sizeDotBorderRadius,
|
||||
ry: sizeDotBorderRadius,
|
||||
}));
|
||||
}
|
||||
return { svgElements, styles };
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../svg-creator/stack.ts
|
||||
|
||||
|
||||
const createStack = (cells, { sizeDot }, width, y, duration) => {
|
||||
const svgElements = [];
|
||||
const styles = [
|
||||
`.u{
|
||||
transform-origin: 0 0;
|
||||
transform: scale(0,1);
|
||||
animation: none linear ${duration}ms infinite;
|
||||
}`,
|
||||
];
|
||||
const stack = cells
|
||||
.slice()
|
||||
.filter((a) => a.t !== null)
|
||||
.sort((a, b) => a.t - b.t);
|
||||
const blocks = [];
|
||||
stack.forEach(({ color, t }) => {
|
||||
const latest = blocks[blocks.length - 1];
|
||||
if (latest?.color === color)
|
||||
latest.ts.push(t);
|
||||
else
|
||||
blocks.push({ color, ts: [t] });
|
||||
});
|
||||
const m = width / stack.length;
|
||||
let i = 0;
|
||||
let nx = 0;
|
||||
for (const { color, ts } of blocks) {
|
||||
const id = "u" + (i++).toString(36);
|
||||
const animationName = id;
|
||||
const x = (nx * m).toFixed(1);
|
||||
nx += ts.length;
|
||||
svgElements.push(h("rect", {
|
||||
class: `u ${id}`,
|
||||
height: sizeDot,
|
||||
width: (ts.length * m + 0.6).toFixed(1),
|
||||
x,
|
||||
y,
|
||||
}));
|
||||
styles.push(createAnimation(animationName, [
|
||||
...ts
|
||||
.map((t, i, { length }) => [
|
||||
{ scale: i / length, t: t - 0.0001 },
|
||||
{ scale: (i + 1) / length, t: t + 0.0001 },
|
||||
])
|
||||
.flat(),
|
||||
{ scale: 1, t: 1 },
|
||||
].map(({ scale, t }) => ({
|
||||
t,
|
||||
style: `transform:scale(${scale.toFixed(3)},1)`,
|
||||
}))), `.u.${id} {
|
||||
fill: var(--c${color});
|
||||
animation-name: ${animationName};
|
||||
transform-origin: ${x}px 0
|
||||
}
|
||||
`);
|
||||
}
|
||||
return { svgElements, styles };
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../svg-creator/index.ts
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const getCellsFromGrid = ({ width, height }) => Array.from({ length: width }, (_, x) => Array.from({ length: height }, (_, y) => ({ x, y }))).flat();
|
||||
const createLivingCells = (grid0, chain, cells) => {
|
||||
const livingCells = (cells ?? getCellsFromGrid(grid0)).map(({ x, y }) => ({
|
||||
x,
|
||||
y,
|
||||
t: null,
|
||||
color: (0,types_grid/* getColor */.oU)(grid0, x, y),
|
||||
}));
|
||||
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 */.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;
|
||||
}
|
||||
}
|
||||
return livingCells;
|
||||
};
|
||||
const createSvg = (grid, cells, chain, drawOptions, animationOptions) => {
|
||||
const width = (grid.width + 2) * drawOptions.sizeCell;
|
||||
const height = (grid.height + 5) * drawOptions.sizeCell;
|
||||
const duration = animationOptions.frameDuration * chain.length;
|
||||
const livingCells = createLivingCells(grid, chain, cells);
|
||||
const elements = [
|
||||
createGrid(livingCells, drawOptions, duration),
|
||||
createStack(livingCells, drawOptions, grid.width * drawOptions.sizeCell, (grid.height + 2) * drawOptions.sizeCell, duration),
|
||||
createSnake(chain, drawOptions, duration),
|
||||
];
|
||||
const viewBox = [
|
||||
-drawOptions.sizeCell,
|
||||
-drawOptions.sizeCell * 2,
|
||||
width,
|
||||
height,
|
||||
].join(" ");
|
||||
const style = generateColorVar(drawOptions) +
|
||||
elements
|
||||
.map((e) => e.styles)
|
||||
.flat()
|
||||
.join("\n");
|
||||
const svg = [
|
||||
h("svg", {
|
||||
viewBox,
|
||||
width,
|
||||
height,
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
}).replace("/>", ">"),
|
||||
"<desc>",
|
||||
"Generated with https://github.com/Platane/snk",
|
||||
"</desc>",
|
||||
"<style>",
|
||||
optimizeCss(style),
|
||||
"</style>",
|
||||
...elements.map((e) => e.svgElements).flat(),
|
||||
"</svg>",
|
||||
].join("");
|
||||
return optimizeSvg(svg);
|
||||
};
|
||||
const optimizeCss = (css) => minifyCss(css);
|
||||
const optimizeSvg = (svg) => svg;
|
||||
const generateColorVar = (drawOptions) => `
|
||||
:root {
|
||||
--cb: ${drawOptions.colorDotBorder};
|
||||
--cs: ${drawOptions.colorSnake};
|
||||
--ce: ${drawOptions.colorEmpty};
|
||||
${Object.entries(drawOptions.colorDots)
|
||||
.map(([i, color]) => `--c${i}:${color};`)
|
||||
.join("")}
|
||||
}
|
||||
` +
|
||||
(drawOptions.dark
|
||||
? `
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--cb: ${drawOptions.dark.colorDotBorder || drawOptions.colorDotBorder};
|
||||
--cs: ${drawOptions.dark.colorSnake || drawOptions.colorSnake};
|
||||
--ce: ${drawOptions.dark.colorEmpty};
|
||||
${Object.entries(drawOptions.dark.colorDots)
|
||||
.map(([i, color]) => `--c${i}:${color};`)
|
||||
.join("")}
|
||||
}
|
||||
}
|
||||
`
|
||||
: "");
|
||||
|
||||
|
||||
/***/ })
|
||||
|
||||
};
|
||||
;
|
||||
231
svg-only/dist/642.index.js
vendored
Normal file
231
svg-only/dist/642.index.js
vendored
Normal file
@@ -0,0 +1,231 @@
|
||||
"use strict";
|
||||
exports.id = 642;
|
||||
exports.ids = [642];
|
||||
exports.modules = {
|
||||
|
||||
/***/ 3642:
|
||||
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
|
||||
// EXPORTS
|
||||
__webpack_require__.d(__webpack_exports__, {
|
||||
createGif: () => (/* binding */ createGif)
|
||||
});
|
||||
|
||||
// EXTERNAL MODULE: external "fs"
|
||||
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__(6928);
|
||||
var external_path_default = /*#__PURE__*/__webpack_require__.n(external_path_);
|
||||
// EXTERNAL MODULE: external "child_process"
|
||||
var external_child_process_ = __webpack_require__(5317);
|
||||
// EXTERNAL MODULE: external "canvas"
|
||||
var external_canvas_ = __webpack_require__(9919);
|
||||
// EXTERNAL MODULE: ../types/grid.ts
|
||||
var types_grid = __webpack_require__(105);
|
||||
;// CONCATENATED MODULE: ../draw/pathRoundedRect.ts
|
||||
const pathRoundedRect_pathRoundedRect = (ctx, width, height, borderRadius) => {
|
||||
ctx.moveTo(borderRadius, 0);
|
||||
ctx.arcTo(width, 0, width, height, borderRadius);
|
||||
ctx.arcTo(width, height, 0, height, borderRadius);
|
||||
ctx.arcTo(0, height, 0, 0, borderRadius);
|
||||
ctx.arcTo(0, 0, width, 0, borderRadius);
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../draw/drawGrid.ts
|
||||
|
||||
|
||||
const drawGrid_drawGrid = (ctx, grid, cells, o) => {
|
||||
for (let x = grid.width; x--;)
|
||||
for (let y = grid.height; y--;) {
|
||||
if (!cells || cells.some((c) => c.x === x && c.y === y)) {
|
||||
const c = (0,types_grid/* getColor */.oU)(grid, x, y);
|
||||
// @ts-ignore
|
||||
const color = !c ? o.colorEmpty : o.colorDots[c];
|
||||
ctx.save();
|
||||
ctx.translate(x * o.sizeCell + (o.sizeCell - o.sizeDot) / 2, y * o.sizeCell + (o.sizeCell - o.sizeDot) / 2);
|
||||
ctx.fillStyle = color;
|
||||
ctx.strokeStyle = o.colorDotBorder;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
pathRoundedRect_pathRoundedRect(ctx, o.sizeDot, o.sizeDot, o.sizeDotBorderRadius);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../draw/drawSnake.ts
|
||||
|
||||
|
||||
const drawSnake_drawSnake = (ctx, snake, o) => {
|
||||
const cells = snakeToCells(snake);
|
||||
for (let i = 0; i < cells.length; i++) {
|
||||
const u = (i + 1) * 0.6;
|
||||
ctx.save();
|
||||
ctx.fillStyle = o.colorSnake;
|
||||
ctx.translate(cells[i].x * o.sizeCell + u, cells[i].y * o.sizeCell + u);
|
||||
ctx.beginPath();
|
||||
pathRoundedRect(ctx, o.sizeCell - u * 2, o.sizeCell - u * 2, (o.sizeCell - u * 2) * 0.25);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
const lerp = (k, a, b) => (1 - k) * a + k * b;
|
||||
const clamp = (x, a, b) => Math.max(a, Math.min(b, x));
|
||||
const drawSnakeLerp = (ctx, snake0, snake1, k, o) => {
|
||||
const m = 0.8;
|
||||
const n = snake0.length / 2;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const u = (i + 1) * 0.6 * (o.sizeCell / 16);
|
||||
const a = (1 - m) * (i / Math.max(n - 1, 1));
|
||||
const ki = clamp((k - a) / m, 0, 1);
|
||||
const x = lerp(ki, snake0[i * 2 + 0], snake1[i * 2 + 0]) - 2;
|
||||
const y = lerp(ki, snake0[i * 2 + 1], snake1[i * 2 + 1]) - 2;
|
||||
ctx.save();
|
||||
ctx.fillStyle = o.colorSnake;
|
||||
ctx.translate(x * o.sizeCell + u, y * o.sizeCell + u);
|
||||
ctx.beginPath();
|
||||
pathRoundedRect_pathRoundedRect(ctx, o.sizeCell - u * 2, o.sizeCell - u * 2, (o.sizeCell - u * 2) * 0.25);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
|
||||
;// CONCATENATED MODULE: ../draw/drawWorld.ts
|
||||
|
||||
|
||||
const drawStack = (ctx, stack, max, width, o) => {
|
||||
ctx.save();
|
||||
const m = width / max;
|
||||
for (let i = 0; i < stack.length; i++) {
|
||||
// @ts-ignore
|
||||
ctx.fillStyle = o.colorDots[stack[i]];
|
||||
ctx.fillRect(i * m, 0, m + width * 0.005, 10);
|
||||
}
|
||||
ctx.restore();
|
||||
};
|
||||
const drawWorld = (ctx, grid, cells, snake, stack, o) => {
|
||||
ctx.save();
|
||||
ctx.translate(1 * o.sizeCell, 2 * o.sizeCell);
|
||||
drawGrid(ctx, grid, cells, o);
|
||||
drawSnake(ctx, snake, o);
|
||||
ctx.restore();
|
||||
ctx.save();
|
||||
ctx.translate(o.sizeCell, (grid.height + 4) * o.sizeCell);
|
||||
const max = grid.data.reduce((sum, x) => sum + +!!x, stack.length);
|
||||
drawStack(ctx, stack, max, grid.width * o.sizeCell, o);
|
||||
ctx.restore();
|
||||
// ctx.save();
|
||||
// ctx.translate(o.sizeCell + 100, (grid.height + 4) * o.sizeCell + 100);
|
||||
// ctx.scale(0.6, 0.6);
|
||||
// drawCircleStack(ctx, stack, o);
|
||||
// ctx.restore();
|
||||
};
|
||||
const drawLerpWorld = (ctx, grid, cells, snake0, snake1, stack, k, o) => {
|
||||
ctx.save();
|
||||
ctx.translate(1 * o.sizeCell, 2 * o.sizeCell);
|
||||
drawGrid_drawGrid(ctx, grid, cells, o);
|
||||
drawSnakeLerp(ctx, snake0, snake1, k, o);
|
||||
ctx.translate(0, (grid.height + 2) * o.sizeCell);
|
||||
const max = grid.data.reduce((sum, x) => sum + +!!x, stack.length);
|
||||
drawStack(ctx, stack, max, grid.width * o.sizeCell, o);
|
||||
ctx.restore();
|
||||
};
|
||||
const getCanvasWorldSize = (grid, o) => {
|
||||
const width = o.sizeCell * (grid.width + 2);
|
||||
const height = o.sizeCell * (grid.height + 4) + 30;
|
||||
return { width, height };
|
||||
};
|
||||
|
||||
// EXTERNAL MODULE: ../types/snake.ts
|
||||
var types_snake = __webpack_require__(777);
|
||||
;// CONCATENATED MODULE: ../solver/step.ts
|
||||
|
||||
|
||||
const step = (grid, stack, snake) => {
|
||||
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 */.l$)(grid, x, y);
|
||||
}
|
||||
};
|
||||
|
||||
// EXTERNAL MODULE: ../../node_modules/tmp/lib/tmp.js
|
||||
var tmp = __webpack_require__(2644);
|
||||
// EXTERNAL MODULE: external "gifsicle"
|
||||
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__(1680);
|
||||
var gif_encoder_2_default = /*#__PURE__*/__webpack_require__.n(gif_encoder_2);
|
||||
;// CONCATENATED MODULE: ../gif-creator/index.ts
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
|
||||
const withTmpDir = async (handler) => {
|
||||
const { name: dir, removeCallback: cleanUp } = tmp.dirSync({
|
||||
unsafeCleanup: true,
|
||||
});
|
||||
try {
|
||||
return await handler(dir);
|
||||
}
|
||||
finally {
|
||||
cleanUp();
|
||||
}
|
||||
};
|
||||
const createGif = async (grid0, cells, chain, drawOptions, animationOptions) => withTmpDir(async (dir) => {
|
||||
const { width, height } = getCanvasWorldSize(grid0, drawOptions);
|
||||
const canvas = (0,external_canvas_.createCanvas)(width, height);
|
||||
const ctx = canvas.getContext("2d");
|
||||
const grid = (0,types_grid/* copyGrid */.mi)(grid0);
|
||||
const stack = [];
|
||||
const encoder = new (gif_encoder_2_default())(width, height, "neuquant", true);
|
||||
encoder.setRepeat(0);
|
||||
encoder.setDelay(animationOptions.frameDuration);
|
||||
encoder.start();
|
||||
for (let i = 0; i < chain.length; i += 1) {
|
||||
const snake0 = chain[i];
|
||||
const snake1 = chain[Math.min(chain.length - 1, i + 1)];
|
||||
step(grid, stack, snake0);
|
||||
for (let k = 0; k < animationOptions.step; k++) {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
drawLerpWorld(ctx, grid, cells, snake0, snake1, stack, k / animationOptions.step, drawOptions);
|
||||
encoder.addFrame(ctx);
|
||||
}
|
||||
}
|
||||
const outFileName = external_path_default().join(dir, "out.gif");
|
||||
const optimizedFileName = external_path_default().join(dir, "out.optimized.gif");
|
||||
encoder.finish();
|
||||
external_fs_default().writeFileSync(outFileName, encoder.out.getData());
|
||||
(0,external_child_process_.execFileSync)((external_gifsicle_default()), [
|
||||
//
|
||||
"--optimize=3",
|
||||
"--color-method=diversity",
|
||||
"--colors=18",
|
||||
outFileName,
|
||||
["--output", optimizedFileName],
|
||||
].flat());
|
||||
return new Uint8Array(external_fs_default().readFileSync(optimizedFileName));
|
||||
});
|
||||
|
||||
|
||||
/***/ })
|
||||
|
||||
};
|
||||
;
|
||||
74804
svg-only/dist/index.js
vendored
74804
svg-only/dist/index.js
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user