Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4d7bdf2bd | ||
|
|
9b05b7fdaa | ||
|
|
ccbe4e530c | ||
|
|
bc6405aca3 | ||
|
|
fc4e8d4436 | ||
|
|
978ec843d3 | ||
|
|
c713fd8dd3 | ||
|
|
c14ae566e0 | ||
|
|
724bc749a2 |
29
.github/workflows/deploy.yml
vendored
Normal file
29
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-demo:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- uses: actions/setup-node@v1.4.2
|
||||||
|
with:
|
||||||
|
node-version: 14
|
||||||
|
|
||||||
|
- uses: bahmutov/npm-install@v1.4.1
|
||||||
|
|
||||||
|
- run: yarn build:demo
|
||||||
|
env:
|
||||||
|
BASE_PATHNAME: "snk"
|
||||||
|
|
||||||
|
- uses: crazy-max/ghaction-github-pages@v2.1.1
|
||||||
|
with:
|
||||||
|
target_branch: gh-pages
|
||||||
|
build_dir: packages/demo/dist
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.MY_GITHUB_TOKEN_GH_PAGES }}
|
||||||
80
.github/workflows/main.yml
vendored
80
.github/workflows/main.yml
vendored
@@ -1,84 +1,28 @@
|
|||||||
name: main
|
name: main
|
||||||
|
|
||||||
on: [push]
|
on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
# - run: sudo apt-get install gifsicle graphicsmagick
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/checkout@v1
|
||||||
|
- uses: actions/setup-node@v1.4.2
|
||||||
with:
|
with:
|
||||||
cache: yarn
|
node-version: 14
|
||||||
node-version: 16
|
|
||||||
- run: yarn install --frozen-lockfile
|
|
||||||
|
|
||||||
- run: yarn type
|
- uses: bahmutov/npm-install@v1.4.1
|
||||||
- run: yarn lint
|
|
||||||
- run: yarn test --ci
|
|
||||||
|
|
||||||
test-benchmark:
|
# - run: yarn type
|
||||||
runs-on: ubuntu-latest
|
# - run: yarn lint
|
||||||
|
# - run: yarn test --ci
|
||||||
steps:
|
# - run: yarn build:lib
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
cache: yarn
|
|
||||||
node-version: 16
|
|
||||||
- run: yarn install --frozen-lockfile
|
|
||||||
|
|
||||||
- run: ( cd packages/gif-creator ; yarn benchmark )
|
|
||||||
|
|
||||||
test-action:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: update action.yml to use image from local Dockerfile
|
|
||||||
run: |
|
|
||||||
sed -i "s/image: .*/image: Dockerfile/" action.yml
|
|
||||||
|
|
||||||
- name: generate-snake-game-from-github-contribution-grid
|
- name: generate-snake-game-from-github-contribution-grid
|
||||||
id: generate-snake
|
uses: Platane/snk@master
|
||||||
uses: ./
|
|
||||||
with:
|
with:
|
||||||
github_user_name: platane
|
github_user_name: platane
|
||||||
gif_out_path: dist/github-contribution-grid-snake.gif
|
|
||||||
svg_out_path: dist/github-contribution-grid-snake.svg
|
|
||||||
|
|
||||||
- name: ensure the generated file exists
|
- run: ls
|
||||||
run: |
|
|
||||||
ls dist
|
|
||||||
test -f ${{ steps.generate-snake.outputs.gif_out_path }}
|
|
||||||
test -f ${{ steps.generate-snake.outputs.svg_out_path }}
|
|
||||||
|
|
||||||
- uses: crazy-max/ghaction-github-pages@v2.5.0
|
|
||||||
with:
|
|
||||||
target_branch: output
|
|
||||||
build_dir: dist
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
deploy-ghpages:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
cache: yarn
|
|
||||||
node-version: 16
|
|
||||||
- run: yarn install --frozen-lockfile
|
|
||||||
|
|
||||||
- run: yarn build:demo
|
|
||||||
env:
|
|
||||||
GITHUB_USER_CONTRIBUTION_API_ENDPOINT: https://snk-one.vercel.app/api/github-user-contribution/
|
|
||||||
|
|
||||||
- uses: crazy-max/ghaction-github-pages@v2.6.0
|
|
||||||
if: success() && github.ref == 'refs/heads/master'
|
|
||||||
with:
|
|
||||||
target_branch: gh-pages
|
|
||||||
build_dir: packages/demo/dist
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.MY_GITHUB_TOKEN_GH_PAGES }}
|
|
||||||
|
|||||||
85
.github/workflows/release.yml
vendored
85
.github/workflows/release.yml
vendored
@@ -1,85 +0,0 @@
|
|||||||
name: release
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: "Version"
|
|
||||||
default: "0.0.1"
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
description:
|
|
||||||
description: "Version description"
|
|
||||||
type: string
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- uses: docker/setup-qemu-action@v1
|
|
||||||
|
|
||||||
- uses: docker/setup-buildx-action@v1
|
|
||||||
|
|
||||||
- uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: build and publish the docker image
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
id: docker-build
|
|
||||||
with:
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
platane/snk:${{ github.sha }}
|
|
||||||
platane/snk:${{ github.event.inputs.version }}
|
|
||||||
|
|
||||||
- name: update action.yml to point to the newly created docker image
|
|
||||||
run: |
|
|
||||||
sed -i "s/image: .*/image: docker:\/\/platane\/snk@${{ steps.docker-build.outputs.digest }}/" action.yml
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
cache: yarn
|
|
||||||
node-version: 16
|
|
||||||
|
|
||||||
- name: build svg-only action
|
|
||||||
run: |
|
|
||||||
yarn install --frozen-lockfile
|
|
||||||
yarn build:action
|
|
||||||
rm -r svg-only/dist
|
|
||||||
mv packages/action/dist svg-only/dist
|
|
||||||
|
|
||||||
- name: bump package version
|
|
||||||
run: yarn version --no-git-tag-version --new-version ${{ github.event.inputs.version }}
|
|
||||||
|
|
||||||
- name: push new build, tag version and push
|
|
||||||
id: push-tags
|
|
||||||
run: |
|
|
||||||
VERSION=${{ github.event.inputs.version }}
|
|
||||||
|
|
||||||
git config --global user.email "bot@platane.me"
|
|
||||||
git config --global user.name "release bot"
|
|
||||||
git add package.json svg-only/dist action.yml
|
|
||||||
git commit -m "📦 $VERSION"
|
|
||||||
git tag v$VERSION
|
|
||||||
git push origin master --tags
|
|
||||||
|
|
||||||
if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
||||||
git tag v$( echo $VERSION | cut -d. -f 1-1 )
|
|
||||||
git tag v$( echo $VERSION | cut -d. -f 1-2 )
|
|
||||||
git push origin --tags --force
|
|
||||||
echo ::set-output name=prerelease::false
|
|
||||||
else
|
|
||||||
echo ::set-output name=prerelease::true
|
|
||||||
fi
|
|
||||||
|
|
||||||
- uses: actions/create-release@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
tag_name: v${{ github.event.inputs.version }}
|
|
||||||
body: ${{ github.event.inputs.description }}
|
|
||||||
prerelease: ${{ steps.push-tags.outputs.prerelease }}
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,5 +2,4 @@ node_modules
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
dist
|
dist
|
||||||
!svg-only/dist
|
|
||||||
build
|
build
|
||||||
35
Dockerfile
35
Dockerfile
@@ -1,32 +1,15 @@
|
|||||||
FROM node:16-slim as builder
|
FROM node:14-slim
|
||||||
|
|
||||||
WORKDIR /app
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends gifsicle graphicsmagick \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY package.json yarn.lock ./
|
COPY tsconfig.json package.json yarn.lock ./generate-snake-game-from-github-contribution-grid/
|
||||||
|
COPY packages ./generate-snake-game-from-github-contribution-grid/packages/
|
||||||
|
|
||||||
COPY tsconfig.json ./
|
RUN ( cd ./generate-snake-game-from-github-contribution-grid ; yarn install --frozen-lockfile )
|
||||||
|
|
||||||
COPY packages packages
|
RUN ( cd ./generate-snake-game-from-github-contribution-grid ; yarn build:action )
|
||||||
|
|
||||||
RUN export YARN_CACHE_FOLDER="$(mktemp -d)" \
|
CMD ["node", "./generate-snake-game-from-github-contribution-grid/packages/action/dist/index.js"]
|
||||||
&& yarn install --frozen-lockfile \
|
|
||||||
&& rm -r "$YARN_CACHE_FOLDER"
|
|
||||||
|
|
||||||
RUN yarn build:action
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
FROM node:16-slim
|
|
||||||
|
|
||||||
WORKDIR /action-release
|
|
||||||
|
|
||||||
RUN export YARN_CACHE_FOLDER="$(mktemp -d)" \
|
|
||||||
&& yarn add canvas@2.9.1 gifsicle@5.3.0 --no-lockfile \
|
|
||||||
&& rm -r "$YARN_CACHE_FOLDER"
|
|
||||||
|
|
||||||
COPY --from=builder /app/packages/action/dist/ /action-release/
|
|
||||||
|
|
||||||
CMD ["node", "/action-release/index.js"]
|
|
||||||
|
|
||||||
|
|||||||
60
README.md
60
README.md
@@ -1,61 +1,3 @@
|
|||||||
# snk
|
# snk
|
||||||
|
|
||||||
[](https://github.com/platane/snk/releases/latest)
|
Generates a snake game from a github user contributions grid and output a screen capture as gif
|
||||||
[](https://github.com/marketplace/actions/generate-snake-game-from-github-contribution-grid)
|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
Generates a snake game from a github user contributions graph
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Pull a github user's contribution graph.
|
|
||||||
Make it a snake Game, generate a snake path where the cells get eaten in an orderly fashion.
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
**github action**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- uses: Platane/snk@v1.1.0
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
[example with cron job](https://github.com/Platane/Platane/blob/master/.github/workflows/main.yml#L24-L29)
|
|
||||||
|
|
||||||
If you are only interested in generating a svg, you can use this other faster action: `uses: Platane/snk/svg-only@v1.1.0`
|
|
||||||
|
|
||||||
**interactive demo**
|
|
||||||
|
|
||||||
<a href="https://platane.github.io/snk">
|
|
||||||
<img height="300px" src="https://user-images.githubusercontent.com/1659820/121798244-7c86d700-cc25-11eb-8c1c-b8e65556ac0d.gif" ></img>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
[platane.github.io/snk](https://platane.github.io/snk)
|
|
||||||
|
|
||||||
**local**
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install
|
|
||||||
|
|
||||||
npm run dev:demo
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation
|
|
||||||
|
|
||||||
[solver algorithm](./packages/solver/README.md)
|
|
||||||
|
|||||||
27
action.yml
27
action.yml
@@ -1,26 +1,23 @@
|
|||||||
name: "generate-snake-game-from-github-contribution-grid"
|
name: "generate-snake-game-from-github-contribution-grid"
|
||||||
description: "Generates a snake game from a github user contributions grid. Output the animation as gif or svg"
|
description: "Generates a snake game from a github user contributions grid and output a screen capture as gif"
|
||||||
author: "platane"
|
author: "platane"
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
gif_out_path:
|
||||||
|
description: "path of the generated gif"
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: docker
|
using: "docker"
|
||||||
image: docker://platane/snk@sha256:74d02183a9a4adb8e00d9f50e6eb5035a5b6ef02644d848363ef3301235ebd1d
|
image: "Dockerfile"
|
||||||
|
# args:
|
||||||
|
# - ${{ inputs.github_user_name }}
|
||||||
|
# - ${{ inputs.gif_out_path }}
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
github_user_name:
|
github_user_name:
|
||||||
description: "github user name"
|
description: "github user name"
|
||||||
required: true
|
required: true
|
||||||
gif_out_path:
|
|
||||||
description: "path of the generated gif file. If left empty, the gif file will not be generated."
|
|
||||||
required: false
|
|
||||||
default: null
|
|
||||||
svg_out_path:
|
|
||||||
description: "path of the generated svg file. If left empty, the svg file will not be generated."
|
|
||||||
required: false
|
|
||||||
default: null
|
|
||||||
|
|
||||||
outputs:
|
|
||||||
gif_out_path:
|
gif_out_path:
|
||||||
description: "path of the generated gif"
|
description: "path of the generated gif"
|
||||||
svg_out_path:
|
required: false
|
||||||
description: "path of the generated svg"
|
default: "./github-contribution-grid-snake.gif"
|
||||||
|
|||||||
18
package.json
18
package.json
@@ -1,25 +1,23 @@
|
|||||||
{
|
{
|
||||||
"name": "snk",
|
"name": "snk",
|
||||||
"description": "Generates a snake game from a github user contributions grid",
|
"description": "Generates a snake game from a github user contributions grid and output a screen capture as gif",
|
||||||
"version": "1.1.3",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"repository": "github:platane/snk",
|
"repository": "github:platane/snk",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "27.4.1",
|
"@types/jest": "26.0.4",
|
||||||
"@types/node": "16.11.7",
|
"jest": "26.1.0",
|
||||||
"jest": "27.5.1",
|
"prettier": "2.0.5",
|
||||||
"prettier": "2.6.2",
|
"ts-jest": "26.1.2",
|
||||||
"ts-jest": "27.1.4",
|
"typescript": "3.9.6"
|
||||||
"typescript": "4.6.3"
|
|
||||||
},
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/**"
|
"packages/**"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"type": "tsc --noEmit",
|
"type": "tsc --noEmit",
|
||||||
"lint": "yarn prettier -c '**/*.{ts,js,json,md,yml,yaml}' '!packages/*/dist/**' '!svg-only/dist/**'",
|
"lint": "yarn prettier -c '**/*.{ts,js,json,md,yml,yaml}' '!packages/action/dist/**' '!packages/demo/dist/**' '!packages/demo/webpack.config.js'",
|
||||||
"test": "jest --verbose --passWithNoTests --no-cache",
|
"test": "jest --verbose --passWithNoTests --no-cache",
|
||||||
"dev:demo": "( cd packages/demo ; yarn dev )",
|
|
||||||
"build:demo": "( cd packages/demo ; yarn build )",
|
"build:demo": "( cd packages/demo ; yarn build )",
|
||||||
"build:action": "( cd packages/action ; yarn build )"
|
"build:action": "( cd packages/action ; yarn build )"
|
||||||
}
|
}
|
||||||
|
|||||||
3
packages/action/.gitignore
vendored
Normal file
3
packages/action/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
!dist
|
||||||
|
!dist/build
|
||||||
|
out.gif
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# @snk/action
|
|
||||||
|
|
||||||
Contains the github action code.
|
|
||||||
|
|
||||||
## Implementation
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
|
|
||||||
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).
|
|
||||||
|
|
||||||
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 +0,0 @@
|
|||||||
*
|
|
||||||
!.gitignore
|
|
||||||
@@ -1,19 +1,5 @@
|
|||||||
import * as fs from "fs";
|
|
||||||
import * as path from "path";
|
|
||||||
import { generateContributionSnake } from "../generateContributionSnake";
|
import { generateContributionSnake } from "../generateContributionSnake";
|
||||||
|
|
||||||
(async () => {
|
generateContributionSnake("platane").then((buffer) => {
|
||||||
const outputSvg = path.join(__dirname, "__snapshots__/out.svg");
|
process.stdout.write(buffer);
|
||||||
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,38 +0,0 @@
|
|||||||
import * as fs from "fs";
|
|
||||||
import * as path from "path";
|
|
||||||
import { generateContributionSnake } from "../generateContributionSnake";
|
|
||||||
|
|
||||||
jest.setTimeout(2 * 60 * 1000);
|
|
||||||
|
|
||||||
const silent = (handler: () => void | Promise<void>) => async () => {
|
|
||||||
const originalConsoleLog = console.log;
|
|
||||||
console.log = () => undefined;
|
|
||||||
try {
|
|
||||||
return await handler();
|
|
||||||
} finally {
|
|
||||||
console.log = originalConsoleLog;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
it(
|
|
||||||
"should generate contribution snake",
|
|
||||||
silent(async () => {
|
|
||||||
const outputSvg = path.join(__dirname, "__snapshots__/out.svg");
|
|
||||||
const outputGif = path.join(__dirname, "__snapshots__/out.gif");
|
|
||||||
|
|
||||||
console.log = () => undefined;
|
|
||||||
const buffer = await generateContributionSnake("platane", {
|
|
||||||
svg: true,
|
|
||||||
gif: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(buffer.svg).toBeDefined();
|
|
||||||
expect(buffer.gif).toBeDefined();
|
|
||||||
|
|
||||||
console.log("💾 writing to", outputSvg);
|
|
||||||
fs.writeFileSync(outputSvg, buffer.svg);
|
|
||||||
|
|
||||||
console.log("💾 writing to", outputGif);
|
|
||||||
fs.writeFileSync(outputGif, buffer.gif);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
BIN
packages/action/dist/build/Release/canvas.node
vendored
Executable file
BIN
packages/action/dist/build/Release/canvas.node
vendored
Executable file
Binary file not shown.
BIN
packages/action/dist/build/Release/libcairo.so.2
vendored
Normal file
BIN
packages/action/dist/build/Release/libcairo.so.2
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libcroco-0.6.so.3
vendored
Normal file
BIN
packages/action/dist/build/Release/libcroco-0.6.so.3
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libexpat.so.1
vendored
Normal file
BIN
packages/action/dist/build/Release/libexpat.so.1
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libffi.so.6
vendored
Normal file
BIN
packages/action/dist/build/Release/libffi.so.6
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libfontconfig.so.1
vendored
Normal file
BIN
packages/action/dist/build/Release/libfontconfig.so.1
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libfreetype.so.6
vendored
Normal file
BIN
packages/action/dist/build/Release/libfreetype.so.6
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libfribidi.so.0
vendored
Normal file
BIN
packages/action/dist/build/Release/libfribidi.so.0
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libgdk_pixbuf-2.0.so.0
vendored
Normal file
BIN
packages/action/dist/build/Release/libgdk_pixbuf-2.0.so.0
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libgif.so.7
vendored
Normal file
BIN
packages/action/dist/build/Release/libgif.so.7
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libgio-2.0.so.0
vendored
Normal file
BIN
packages/action/dist/build/Release/libgio-2.0.so.0
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libglib-2.0.so.0
vendored
Normal file
BIN
packages/action/dist/build/Release/libglib-2.0.so.0
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libgmodule-2.0.so.0
vendored
Normal file
BIN
packages/action/dist/build/Release/libgmodule-2.0.so.0
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libgobject-2.0.so.0
vendored
Normal file
BIN
packages/action/dist/build/Release/libgobject-2.0.so.0
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libgthread-2.0.so.0
vendored
Normal file
BIN
packages/action/dist/build/Release/libgthread-2.0.so.0
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libharfbuzz.so.0
vendored
Normal file
BIN
packages/action/dist/build/Release/libharfbuzz.so.0
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libjpeg.so.62
vendored
Normal file
BIN
packages/action/dist/build/Release/libjpeg.so.62
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libpango-1.0.so.0
vendored
Normal file
BIN
packages/action/dist/build/Release/libpango-1.0.so.0
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libpangocairo-1.0.so.0
vendored
Normal file
BIN
packages/action/dist/build/Release/libpangocairo-1.0.so.0
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libpangoft2-1.0.so.0
vendored
Normal file
BIN
packages/action/dist/build/Release/libpangoft2-1.0.so.0
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libpcre.so.1
vendored
Normal file
BIN
packages/action/dist/build/Release/libpcre.so.1
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libpixman-1.so.0
vendored
Normal file
BIN
packages/action/dist/build/Release/libpixman-1.so.0
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libpng16.so.16
vendored
Normal file
BIN
packages/action/dist/build/Release/libpng16.so.16
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/librsvg-2.so.2
vendored
Normal file
BIN
packages/action/dist/build/Release/librsvg-2.so.2
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libstdc++.so.6
vendored
Normal file
BIN
packages/action/dist/build/Release/libstdc++.so.6
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libxml2.so.2
vendored
Normal file
BIN
packages/action/dist/build/Release/libxml2.so.2
vendored
Normal file
Binary file not shown.
BIN
packages/action/dist/build/Release/libz.so.1
vendored
Normal file
BIN
packages/action/dist/build/Release/libz.so.1
vendored
Normal file
Binary file not shown.
174133
packages/action/dist/index.js
vendored
Normal file
174133
packages/action/dist/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
84
packages/action/dist/index1.js
vendored
Normal file
84
packages/action/dist/index1.js
vendored
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
const Canvas = require('./lib/canvas')
|
||||||
|
const Image = require('./lib/image')
|
||||||
|
const CanvasRenderingContext2D = require('./lib/context2d')
|
||||||
|
const parseFont = require('./lib/parse-font')
|
||||||
|
const packageJson = require('./package.json')
|
||||||
|
const bindings = require('./lib/bindings')
|
||||||
|
const fs = require('fs')
|
||||||
|
const PNGStream = require('./lib/pngstream')
|
||||||
|
const PDFStream = require('./lib/pdfstream')
|
||||||
|
const JPEGStream = require('./lib/jpegstream')
|
||||||
|
const DOMMatrix = require('./lib/DOMMatrix').DOMMatrix
|
||||||
|
const DOMPoint = require('./lib/DOMMatrix').DOMPoint
|
||||||
|
|
||||||
|
function createCanvas (width, height, type) {
|
||||||
|
return new Canvas(width, height, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
function createImageData (array, width, height) {
|
||||||
|
return new bindings.ImageData(array, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadImage (src) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const image = new Image()
|
||||||
|
|
||||||
|
function cleanup () {
|
||||||
|
image.onload = null
|
||||||
|
image.onerror = null
|
||||||
|
}
|
||||||
|
|
||||||
|
image.onload = () => { cleanup(); resolve(image) }
|
||||||
|
image.onerror = (err) => { cleanup(); reject(err) }
|
||||||
|
|
||||||
|
image.src = src
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve paths for registerFont. Must be called *before* creating a Canvas
|
||||||
|
* instance.
|
||||||
|
* @param src {string} Path to font file.
|
||||||
|
* @param fontFace {{family: string, weight?: string, style?: string}} Object
|
||||||
|
* specifying font information. `weight` and `style` default to `"normal"`.
|
||||||
|
*/
|
||||||
|
function registerFont (src, fontFace) {
|
||||||
|
// TODO this doesn't need to be on Canvas; it should just be a static method
|
||||||
|
// of `bindings`.
|
||||||
|
return Canvas._registerFont(fs.realpathSync(src), fontFace)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Canvas,
|
||||||
|
Context2d: CanvasRenderingContext2D, // Legacy/compat export
|
||||||
|
CanvasRenderingContext2D,
|
||||||
|
CanvasGradient: bindings.CanvasGradient,
|
||||||
|
CanvasPattern: bindings.CanvasPattern,
|
||||||
|
Image,
|
||||||
|
ImageData: bindings.ImageData,
|
||||||
|
PNGStream,
|
||||||
|
PDFStream,
|
||||||
|
JPEGStream,
|
||||||
|
DOMMatrix,
|
||||||
|
DOMPoint,
|
||||||
|
|
||||||
|
registerFont,
|
||||||
|
parseFont,
|
||||||
|
|
||||||
|
createCanvas,
|
||||||
|
createImageData,
|
||||||
|
loadImage,
|
||||||
|
|
||||||
|
backends: bindings.Backends,
|
||||||
|
|
||||||
|
/** Library version. */
|
||||||
|
version: packageJson.version,
|
||||||
|
/** Cairo version. */
|
||||||
|
cairoVersion: bindings.cairoVersion,
|
||||||
|
/** jpeglib version. */
|
||||||
|
jpegVersion: bindings.jpegVersion,
|
||||||
|
/** gif_lib version. */
|
||||||
|
gifVersion: bindings.gifVersion ? bindings.gifVersion.replace(/[^.\d]/g, '') : undefined,
|
||||||
|
/** freetype version. */
|
||||||
|
freetypeVersion: bindings.freetypeVersion
|
||||||
|
}
|
||||||
60
packages/action/dist/xhr-sync-worker.js
vendored
Normal file
60
packages/action/dist/xhr-sync-worker.js
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"use strict";
|
||||||
|
/* eslint-disable no-process-exit */
|
||||||
|
const util = require("util");
|
||||||
|
const { JSDOM } = require("../../../..");
|
||||||
|
const { READY_STATES } = require("./xhr-utils");
|
||||||
|
const idlUtils = require("../generated/utils");
|
||||||
|
const tough = require("tough-cookie");
|
||||||
|
|
||||||
|
const dom = new JSDOM();
|
||||||
|
const xhr = new dom.window.XMLHttpRequest();
|
||||||
|
const xhrImpl = idlUtils.implForWrapper(xhr);
|
||||||
|
|
||||||
|
const chunks = [];
|
||||||
|
|
||||||
|
process.stdin.on("data", chunk => {
|
||||||
|
chunks.push(chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stdin.on("end", () => {
|
||||||
|
const buffer = Buffer.concat(chunks);
|
||||||
|
|
||||||
|
const flag = JSON.parse(buffer.toString());
|
||||||
|
if (flag.body && flag.body.type === "Buffer" && flag.body.data) {
|
||||||
|
flag.body = Buffer.from(flag.body.data);
|
||||||
|
}
|
||||||
|
if (flag.cookieJar) {
|
||||||
|
flag.cookieJar = tough.CookieJar.fromJSON(flag.cookieJar);
|
||||||
|
}
|
||||||
|
|
||||||
|
flag.synchronous = false;
|
||||||
|
Object.assign(xhrImpl.flag, flag);
|
||||||
|
const { properties } = xhrImpl;
|
||||||
|
xhrImpl.readyState = READY_STATES.OPENED;
|
||||||
|
try {
|
||||||
|
xhr.addEventListener("loadend", () => {
|
||||||
|
if (properties.error) {
|
||||||
|
properties.error = properties.error.stack || util.inspect(properties.error);
|
||||||
|
}
|
||||||
|
process.stdout.write(JSON.stringify({
|
||||||
|
responseURL: xhrImpl.responseURL,
|
||||||
|
status: xhrImpl.status,
|
||||||
|
statusText: xhrImpl.statusText,
|
||||||
|
properties
|
||||||
|
}), () => {
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}, false);
|
||||||
|
xhr.send(flag.body);
|
||||||
|
} catch (error) {
|
||||||
|
properties.error += error.stack || util.inspect(error);
|
||||||
|
process.stdout.write(JSON.stringify({
|
||||||
|
responseURL: xhrImpl.responseURL,
|
||||||
|
status: xhrImpl.status,
|
||||||
|
statusText: xhrImpl.statusText,
|
||||||
|
properties
|
||||||
|
}), () => {
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,53 +1,56 @@
|
|||||||
import { getGithubUserContribution } from "@snk/github-user-contribution";
|
import { getGithubUserContribution, Cell } from "@snk/github-user-contribution";
|
||||||
import { userContributionToGrid } from "./userContributionToGrid";
|
import { generateEmptyGrid } from "@snk/compute/generateGrid";
|
||||||
import { getBestRoute } from "@snk/solver/getBestRoute";
|
import { setColor } from "@snk/compute/grid";
|
||||||
import { snake4 } from "@snk/types/__fixtures__/snake";
|
import { computeBestRun } from "@snk/compute";
|
||||||
import { getPathToPose } from "@snk/solver/getPathToPose";
|
import { createGif } from "../gif-creator";
|
||||||
|
|
||||||
export const generateContributionSnake = async (
|
export const userContributionToGrid = (cells: Cell[]) => {
|
||||||
userName: string,
|
const width = Math.max(0, ...cells.map((c) => c.x)) + 1;
|
||||||
format: { svg?: boolean; gif?: boolean }
|
const height = Math.max(0, ...cells.map((c) => c.y)) + 1;
|
||||||
) => {
|
|
||||||
console.log("🎣 fetching github user contribution");
|
const grid = generateEmptyGrid(width, height);
|
||||||
|
for (const c of cells) setColor(grid, c.x, c.y, c.k === 0 ? null : c.k);
|
||||||
|
|
||||||
|
return grid;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateContributionSnake = async (userName: string) => {
|
||||||
const { cells, colorScheme } = await getGithubUserContribution(userName);
|
const { cells, colorScheme } = await getGithubUserContribution(userName);
|
||||||
|
|
||||||
const grid = userContributionToGrid(cells, colorScheme);
|
const grid0 = userContributionToGrid(cells);
|
||||||
const snake = snake4;
|
|
||||||
|
const snake0 = [
|
||||||
|
{ x: 4, y: -1 },
|
||||||
|
{ x: 3, y: -1 },
|
||||||
|
{ x: 2, y: -1 },
|
||||||
|
{ x: 1, y: -1 },
|
||||||
|
{ x: 0, y: -1 },
|
||||||
|
];
|
||||||
|
|
||||||
const drawOptions = {
|
const drawOptions = {
|
||||||
sizeBorderRadius: 2,
|
sizeBorderRadius: 2,
|
||||||
sizeCell: 16,
|
sizeCell: 16,
|
||||||
sizeDot: 12,
|
sizeDot: 12,
|
||||||
colorBorder: "#1b1f230a",
|
colorBorder: "#1b1f230a",
|
||||||
colorDots: colorScheme as any,
|
colorDots: colorScheme,
|
||||||
colorEmpty: colorScheme[0],
|
colorEmpty: colorScheme[0],
|
||||||
colorSnake: "purple",
|
colorSnake: "purple",
|
||||||
cells,
|
|
||||||
dark: {
|
|
||||||
colorEmpty: "#161b22",
|
|
||||||
colorDots: { 1: "#01311f", 2: "#034525", 3: "#0f6d31", 4: "#00c647" },
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const gifOptions = { frameDuration: 100, step: 1 };
|
const gameOptions = { maxSnakeLength: 5 };
|
||||||
|
|
||||||
console.log("📡 computing best route");
|
const gifOptions = { delay: 10 };
|
||||||
const chain = getBestRoute(grid, snake)!;
|
|
||||||
chain.push(...getPathToPose(chain.slice(-1)[0], snake)!);
|
|
||||||
|
|
||||||
const output: Record<string, Buffer | string> = {};
|
const commands = computeBestRun(grid0, snake0, gameOptions);
|
||||||
|
|
||||||
if (format.gif) {
|
const buffer = await createGif(
|
||||||
console.log("📹 creating gif");
|
grid0,
|
||||||
const { createGif } = await import("@snk/gif-creator");
|
snake0,
|
||||||
output.gif = await createGif(grid, chain, drawOptions, gifOptions);
|
commands,
|
||||||
}
|
drawOptions,
|
||||||
|
gameOptions,
|
||||||
|
gifOptions
|
||||||
|
);
|
||||||
|
|
||||||
if (format.svg) {
|
return buffer;
|
||||||
console.log("🖌 creating svg");
|
|
||||||
const { createSvg } = await import("@snk/svg-creator");
|
|
||||||
output.svg = createSvg(grid, chain, drawOptions, gifOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,32 +1,27 @@
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as path from "path";
|
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { generateContributionSnake } from "./generateContributionSnake";
|
import { generateContributionSnake } from "./generateContributionSnake";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const userName = core.getInput("github_user_name");
|
console.log("argv", process.argv);
|
||||||
const format = {
|
|
||||||
svg: core.getInput("svg_out_path"),
|
|
||||||
gif: core.getInput("gif_out_path"),
|
|
||||||
};
|
|
||||||
|
|
||||||
const { svg, gif } = await generateContributionSnake(
|
console.log(core.getInput("user_name"));
|
||||||
userName,
|
console.log(core.getInput("gif_out_path"));
|
||||||
format as any
|
console.log("--");
|
||||||
);
|
console.log("--");
|
||||||
|
console.log(process.cwd());
|
||||||
|
console.log("--");
|
||||||
|
console.log(fs.readdirSync(process.cwd()));
|
||||||
|
console.log("--");
|
||||||
|
console.log("--");
|
||||||
|
console.log(process.env.GITHUB_WORKSPACE);
|
||||||
|
console.log("--");
|
||||||
|
console.log(fs.readdirSync(process.cwd()));
|
||||||
|
|
||||||
if (svg) {
|
const buffer = await generateContributionSnake(core.getInput("user_name"));
|
||||||
fs.mkdirSync(path.dirname(format.svg), { recursive: true });
|
fs.writeFileSync(core.getInput("gif_out_path"), buffer);
|
||||||
fs.writeFileSync(format.svg, svg);
|
} catch (e) {
|
||||||
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);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
core.setFailed(`Action failed with "${e.message}"`);
|
core.setFailed(`Action failed with "${e.message}"`);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -2,19 +2,15 @@
|
|||||||
"name": "@snk/action",
|
"name": "@snk/action",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "1.6.0",
|
"@actions/core": "1.2.4",
|
||||||
"@snk/gif-creator": "1.0.0",
|
"@snk/gif-creator": "1.0.0",
|
||||||
"@snk/github-user-contribution": "1.0.0",
|
"@snk/github-user-contribution": "1.0.0"
|
||||||
"@snk/solver": "1.0.0",
|
|
||||||
"@snk/svg-creator": "1.0.0",
|
|
||||||
"@snk/types": "1.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vercel/ncc": "0.24.1",
|
"@zeit/ncc": "0.22.3"
|
||||||
"ts-node": "10.7.0"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "ncc build --external canvas --external gifsicle --out dist ./index.ts",
|
"build": "ncc build ./index.ts --out dist",
|
||||||
"dev": "ts-node __tests__/dev.ts"
|
"dev": "ncc run __tests__/dev.ts --quiet | tail -n +2 > out.gif "
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
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[]
|
|
||||||
) => {
|
|
||||||
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);
|
|
||||||
else setColorEmpty(grid, c.x, c.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
return grid;
|
|
||||||
};
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { createEmptyGrid, setColor, getColor, isInside, Color } from "../grid";
|
import { generateEmptyGrid } from "../generateGrid";
|
||||||
|
import { setColor, getColor, isInside } from "../grid";
|
||||||
|
|
||||||
it("should set / get cell", () => {
|
it("should set / get cell", () => {
|
||||||
const grid = createEmptyGrid(2, 3);
|
const grid = generateEmptyGrid(2, 3);
|
||||||
|
|
||||||
expect(getColor(grid, 0, 1)).toBe(0);
|
expect(getColor(grid, 0, 1)).toBe(null);
|
||||||
|
|
||||||
setColor(grid, 0, 1, 1 as Color);
|
setColor(grid, 0, 1, 1);
|
||||||
|
|
||||||
expect(getColor(grid, 0, 1)).toBe(1);
|
expect(getColor(grid, 0, 1)).toBe(1);
|
||||||
});
|
});
|
||||||
@@ -19,7 +20,7 @@ test.each([
|
|||||||
[2, 1, false],
|
[2, 1, false],
|
||||||
[0, 3, false],
|
[0, 3, false],
|
||||||
])("isInside", (x, y, output) => {
|
])("isInside", (x, y, output) => {
|
||||||
const grid = createEmptyGrid(2, 3);
|
const grid = generateEmptyGrid(2, 3);
|
||||||
|
|
||||||
expect(isInside(grid, x, y)).toBe(output);
|
expect(isInside(grid, x, y)).toBe(output);
|
||||||
});
|
});
|
||||||
24
packages/compute/__tests__/snake.spec.ts
Normal file
24
packages/compute/__tests__/snake.spec.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { snakeSelfCollide } from "../snake";
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
[[{ x: 0, y: 0 }], false],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
{ x: 1, y: 7 },
|
||||||
|
{ x: 0, y: 6 },
|
||||||
|
{ x: 2, y: 8 },
|
||||||
|
{ x: 1, y: 7 },
|
||||||
|
{ x: 3, y: 9 },
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
])("should report snake collision", (snake, collide) => {
|
||||||
|
expect(snakeSelfCollide(snake)).toBe(collide);
|
||||||
|
});
|
||||||
94
packages/compute/__tests__/step.spec.ts
Normal file
94
packages/compute/__tests__/step.spec.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { step } from "../step";
|
||||||
|
import { generateEmptyGrid } from "../generateGrid";
|
||||||
|
import { around4 } from "../point";
|
||||||
|
import { setColor, getColor } from "../grid";
|
||||||
|
|
||||||
|
it("should move snake", () => {
|
||||||
|
const grid = generateEmptyGrid(4, 3);
|
||||||
|
const snake = [{ x: 1, y: 1 }];
|
||||||
|
const direction = around4[0];
|
||||||
|
const stack: number[] = [];
|
||||||
|
const options = { maxSnakeLength: 5 };
|
||||||
|
|
||||||
|
step(grid, snake, stack, direction, options);
|
||||||
|
|
||||||
|
expect(snake).toEqual([
|
||||||
|
{ x: 2, y: 1 },
|
||||||
|
{ x: 1, y: 1 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
step(grid, snake, stack, direction, options);
|
||||||
|
|
||||||
|
expect(snake).toEqual([
|
||||||
|
{ x: 3, y: 1 },
|
||||||
|
{ x: 2, y: 1 },
|
||||||
|
{ x: 1, y: 1 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
step(grid, snake, stack, direction, options);
|
||||||
|
|
||||||
|
expect(snake).toEqual([
|
||||||
|
{ x: 4, y: 1 },
|
||||||
|
{ x: 3, y: 1 },
|
||||||
|
{ x: 2, y: 1 },
|
||||||
|
{ x: 1, y: 1 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should move short snake", () => {
|
||||||
|
const grid = generateEmptyGrid(8, 3);
|
||||||
|
const snake = [{ x: 1, y: 1 }];
|
||||||
|
const direction = around4[0];
|
||||||
|
const stack: number[] = [];
|
||||||
|
const options = { maxSnakeLength: 3 };
|
||||||
|
|
||||||
|
step(grid, snake, stack, direction, options);
|
||||||
|
|
||||||
|
expect(snake).toEqual([
|
||||||
|
{ x: 2, y: 1 },
|
||||||
|
{ x: 1, y: 1 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
step(grid, snake, stack, direction, options);
|
||||||
|
|
||||||
|
expect(snake).toEqual([
|
||||||
|
{ x: 3, y: 1 },
|
||||||
|
{ x: 2, y: 1 },
|
||||||
|
{ x: 1, y: 1 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
step(grid, snake, stack, direction, options);
|
||||||
|
|
||||||
|
expect(snake).toEqual([
|
||||||
|
{ x: 4, y: 1 },
|
||||||
|
{ x: 3, y: 1 },
|
||||||
|
{ x: 2, y: 1 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
step(grid, snake, stack, direction, options);
|
||||||
|
|
||||||
|
expect(snake).toEqual([
|
||||||
|
{ x: 5, y: 1 },
|
||||||
|
{ x: 4, y: 1 },
|
||||||
|
{ x: 3, y: 1 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should pick up fruit", () => {
|
||||||
|
const grid = generateEmptyGrid(4, 3);
|
||||||
|
const snake = [{ x: 1, y: 1 }];
|
||||||
|
const direction = around4[0];
|
||||||
|
const stack: number[] = [];
|
||||||
|
const options = { maxSnakeLength: 2 };
|
||||||
|
setColor(grid, 3, 1, 9);
|
||||||
|
|
||||||
|
step(grid, snake, stack, direction, options);
|
||||||
|
|
||||||
|
expect(getColor(grid, 3, 1)).toBe(9);
|
||||||
|
expect(stack).toEqual([]);
|
||||||
|
|
||||||
|
step(grid, snake, stack, direction, options);
|
||||||
|
|
||||||
|
expect(getColor(grid, 3, 1)).toBe(null);
|
||||||
|
expect(stack).toEqual([9]);
|
||||||
|
});
|
||||||
27
packages/compute/generateGrid.ts
Normal file
27
packages/compute/generateGrid.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Grid, Color } from "./grid";
|
||||||
|
|
||||||
|
const rand = (a: number, b: number) => Math.floor(Math.random() * (b - a)) + a;
|
||||||
|
|
||||||
|
export const generateEmptyGrid = (width: number, height: number) =>
|
||||||
|
generateGrid(width, height, { colors: [], emptyP: 1 });
|
||||||
|
|
||||||
|
export const generateGrid = (
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
options: { colors: Color[]; emptyP: number } = {
|
||||||
|
colors: [1, 2, 3],
|
||||||
|
emptyP: 2,
|
||||||
|
}
|
||||||
|
): Grid => {
|
||||||
|
const g = {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
data: Array.from({ length: width * height }, () => {
|
||||||
|
const x = rand(-options.emptyP, options.colors.length);
|
||||||
|
|
||||||
|
return x < 0 ? null : options.colors[x];
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return g;
|
||||||
|
};
|
||||||
30
packages/compute/grid.ts
Normal file
30
packages/compute/grid.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export type Color = number;
|
||||||
|
|
||||||
|
export type Grid = {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
data: (Color | null)[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getIndex = (grid: Grid, x: number, y: number) =>
|
||||||
|
x * grid.height + y;
|
||||||
|
|
||||||
|
export const isInside = (grid: Grid, x: number, y: number) =>
|
||||||
|
x >= 0 && y >= 0 && x < grid.width && y < grid.height;
|
||||||
|
|
||||||
|
export const isInsideLarge = (grid: Grid, m: number, x: number, y: number) =>
|
||||||
|
x >= -m && y >= -m && x < grid.width + m && y < grid.height + m;
|
||||||
|
|
||||||
|
export const getColor = (grid: Grid, x: number, y: number) =>
|
||||||
|
grid.data[getIndex(grid, x, y)];
|
||||||
|
|
||||||
|
export const copyGrid = (grid: Grid) => ({ ...grid, data: grid.data.slice() });
|
||||||
|
|
||||||
|
export const setColor = (
|
||||||
|
grid: Grid,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
color: Color | null
|
||||||
|
) => {
|
||||||
|
grid.data[getIndex(grid, x, y)] = color;
|
||||||
|
};
|
||||||
44
packages/compute/index.ts
Normal file
44
packages/compute/index.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Grid, Color, copyGrid, isInsideLarge } from "./grid";
|
||||||
|
import { Point, around4 } from "./point";
|
||||||
|
import { stepSnake, step } from "./step";
|
||||||
|
import { copySnake, snakeSelfCollide } from "./snake";
|
||||||
|
|
||||||
|
const isGridEmpty = (grid: Grid) => grid.data.every((x) => x === null);
|
||||||
|
|
||||||
|
export const computeBestRun = (
|
||||||
|
grid: Grid,
|
||||||
|
snake: Point[],
|
||||||
|
options: { maxSnakeLength: number }
|
||||||
|
) => {
|
||||||
|
const g = copyGrid(grid);
|
||||||
|
const s = copySnake(snake);
|
||||||
|
const q: Color[] = [];
|
||||||
|
|
||||||
|
const commands: Point[] = [];
|
||||||
|
|
||||||
|
let u = 500;
|
||||||
|
|
||||||
|
while (!isGridEmpty(g) && u-- > 0) {
|
||||||
|
let direction;
|
||||||
|
|
||||||
|
for (let k = 10; k--; ) {
|
||||||
|
direction = around4[Math.floor(Math.random() * around4.length)];
|
||||||
|
|
||||||
|
const sn = copySnake(s);
|
||||||
|
stepSnake(sn, direction, options);
|
||||||
|
|
||||||
|
if (isInsideLarge(g, 1, sn[0].x, sn[0].y) && !snakeSelfCollide(sn)) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
direction = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction !== undefined) {
|
||||||
|
step(g, s, q, direction, options);
|
||||||
|
commands.push(direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands;
|
||||||
|
};
|
||||||
4
packages/compute/package.json
Normal file
4
packages/compute/package.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"name": "@snk/compute",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
@@ -5,6 +5,4 @@ export const around4 = [
|
|||||||
{ x: 0, y: -1 },
|
{ x: 0, y: -1 },
|
||||||
{ x: -1, y: 0 },
|
{ x: -1, y: 0 },
|
||||||
{ x: 0, y: 1 },
|
{ x: 0, y: 1 },
|
||||||
] as const;
|
];
|
||||||
|
|
||||||
export const pointEquals = (a: Point, b: Point) => a.x === b.x && a.y === b.y;
|
|
||||||
24
packages/compute/snake.ts
Normal file
24
packages/compute/snake.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Point } from "./point";
|
||||||
|
|
||||||
|
export const snakeSelfCollideNext = (
|
||||||
|
snake: Point[],
|
||||||
|
direction: Point,
|
||||||
|
options: { maxSnakeLength: number }
|
||||||
|
) => {
|
||||||
|
const hx = snake[0].x + direction.x;
|
||||||
|
const hy = snake[0].y + direction.y;
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(options.maxSnakeLength, snake.length); i++)
|
||||||
|
if (snake[i].x === hx && snake[i].y === hy) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const snakeSelfCollide = (snake: Point[]) => {
|
||||||
|
for (let i = 1; i < snake.length; i++)
|
||||||
|
if (snake[i].x === snake[0].x && snake[i].y === snake[0].y) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const copySnake = (x: Point[]) => x.map((p) => ({ ...p }));
|
||||||
48
packages/compute/step.ts
Normal file
48
packages/compute/step.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { Grid, Color, getColor, isInside, setColor } from "./grid";
|
||||||
|
import { Point } from "./point";
|
||||||
|
|
||||||
|
const moveSnake = (snake: Point[], headx: number, heady: number) => {
|
||||||
|
for (let k = snake.length - 1; k > 0; k--) {
|
||||||
|
snake[k].x = snake[k - 1].x;
|
||||||
|
snake[k].y = snake[k - 1].y;
|
||||||
|
}
|
||||||
|
snake[0].x = headx;
|
||||||
|
snake[0].y = heady;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stepSnake = (
|
||||||
|
snake: Point[],
|
||||||
|
direction: Point,
|
||||||
|
options: { maxSnakeLength: number }
|
||||||
|
) => {
|
||||||
|
const headx = snake[0].x + direction.x;
|
||||||
|
const heady = snake[0].y + direction.y;
|
||||||
|
|
||||||
|
if (snake.length === options.maxSnakeLength) {
|
||||||
|
moveSnake(snake, headx, heady);
|
||||||
|
} else {
|
||||||
|
snake.unshift({ x: headx, y: heady });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stepPicking = (grid: Grid, snake: Point[], stack: Color[]) => {
|
||||||
|
if (isInside(grid, snake[0].x, snake[0].y)) {
|
||||||
|
const c = getColor(grid, snake[0].x, snake[0].y);
|
||||||
|
|
||||||
|
if (c) {
|
||||||
|
setColor(grid, snake[0].x, snake[0].y, null);
|
||||||
|
stack.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const step = (
|
||||||
|
grid: Grid,
|
||||||
|
snake: Point[],
|
||||||
|
stack: Color[],
|
||||||
|
direction: Point,
|
||||||
|
options: { maxSnakeLength: number }
|
||||||
|
) => {
|
||||||
|
stepSnake(snake, direction, options);
|
||||||
|
stepPicking(grid, snake, stack);
|
||||||
|
};
|
||||||
1
packages/demo/.gitignore
vendored
Normal file
1
packages/demo/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
webpack.config.js
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# @snk/demo
|
|
||||||
|
|
||||||
Contains various demo to test and validate some pieces of the algorithm.
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import { Color, Grid } from "@snk/types/grid";
|
|
||||||
import { drawLerpWorld, drawWorld } from "@snk/draw/drawWorld";
|
|
||||||
import { Snake } from "@snk/types/snake";
|
|
||||||
|
|
||||||
export const drawOptions = {
|
|
||||||
sizeBorderRadius: 2,
|
|
||||||
sizeCell: 16,
|
|
||||||
sizeDot: 12,
|
|
||||||
colorBorder: "#1b1f230a",
|
|
||||||
colorDots: {
|
|
||||||
1: "#9be9a8",
|
|
||||||
2: "#40c463",
|
|
||||||
3: "#30a14e",
|
|
||||||
4: "#216e39",
|
|
||||||
},
|
|
||||||
colorEmpty: "#ebedf0",
|
|
||||||
colorSnake: "purple",
|
|
||||||
dark: {
|
|
||||||
colorEmpty: "#161b22",
|
|
||||||
colorDots: { 1: "#01311f", 2: "#034525", 3: "#0f6d31", 4: "#00c647" },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPointedCell =
|
|
||||||
(canvas: HTMLCanvasElement) =>
|
|
||||||
({ pageX, pageY }: MouseEvent) => {
|
|
||||||
const { left, top } = canvas.getBoundingClientRect();
|
|
||||||
|
|
||||||
const x = Math.floor((pageX - left) / drawOptions.sizeCell) - 1;
|
|
||||||
const y = Math.floor((pageY - top) / drawOptions.sizeCell) - 2;
|
|
||||||
|
|
||||||
return { x, y };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createCanvas = ({
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
}: {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}) => {
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
const upscale = 2;
|
|
||||||
const w = drawOptions.sizeCell * (width + 4);
|
|
||||||
const h = drawOptions.sizeCell * (height + 4) + 200;
|
|
||||||
canvas.width = w * upscale;
|
|
||||||
canvas.height = h * upscale;
|
|
||||||
canvas.style.width = w + "px";
|
|
||||||
canvas.style.height = h + "px";
|
|
||||||
canvas.style.display = "block";
|
|
||||||
// canvas.style.pointerEvents = "none";
|
|
||||||
|
|
||||||
const cellInfo = document.createElement("div");
|
|
||||||
cellInfo.style.height = "20px";
|
|
||||||
|
|
||||||
document.body.appendChild(cellInfo);
|
|
||||||
document.body.appendChild(canvas);
|
|
||||||
canvas.addEventListener("mousemove", (e) => {
|
|
||||||
const { x, y } = getPointedCell(canvas)(e);
|
|
||||||
cellInfo.innerText = [x, y]
|
|
||||||
.map((u) => u.toString().padStart(2, " "))
|
|
||||||
.join(" / ");
|
|
||||||
});
|
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d")!;
|
|
||||||
ctx.scale(upscale, upscale);
|
|
||||||
|
|
||||||
const draw = (grid: Grid, snake: Snake, stack: Color[]) => {
|
|
||||||
ctx.clearRect(0, 0, 9999, 9999);
|
|
||||||
drawWorld(ctx, grid, snake, stack, drawOptions);
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawLerp = (
|
|
||||||
grid: Grid,
|
|
||||||
snake0: Snake,
|
|
||||||
snake1: Snake,
|
|
||||||
stack: Color[],
|
|
||||||
k: number
|
|
||||||
) => {
|
|
||||||
ctx.clearRect(0, 0, 9999, 9999);
|
|
||||||
drawLerpWorld(ctx, grid, snake0, snake1, stack, k, drawOptions);
|
|
||||||
};
|
|
||||||
|
|
||||||
const highlightCell = (x: number, y: number, color = "orange") => {
|
|
||||||
ctx.fillStyle = color;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.fillRect((1 + x + 0.5) * 16 - 2, (2 + y + 0.5) * 16 - 2, 4, 4);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
draw,
|
|
||||||
drawLerp,
|
|
||||||
highlightCell,
|
|
||||||
canvas,
|
|
||||||
getPointedCell: getPointedCell(canvas),
|
|
||||||
ctx,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import "./menu";
|
|
||||||
import { createCanvas } from "./canvas";
|
|
||||||
import { getBestRoute } from "@snk/solver/getBestRoute";
|
|
||||||
import { Color, copyGrid } from "@snk/types/grid";
|
|
||||||
import { grid, snake } from "./sample";
|
|
||||||
import { step } from "@snk/solver/step";
|
|
||||||
|
|
||||||
const chain = getBestRoute(grid, snake)!;
|
|
||||||
|
|
||||||
//
|
|
||||||
// draw
|
|
||||||
let k = 0;
|
|
||||||
|
|
||||||
const { canvas, draw } = createCanvas(grid);
|
|
||||||
document.body.appendChild(canvas);
|
|
||||||
|
|
||||||
const onChange = () => {
|
|
||||||
const gridN = copyGrid(grid);
|
|
||||||
const stack: Color[] = [];
|
|
||||||
for (let i = 0; i <= k; i++) step(gridN, stack, chain[i]);
|
|
||||||
|
|
||||||
draw(gridN, chain[k], stack);
|
|
||||||
};
|
|
||||||
onChange();
|
|
||||||
|
|
||||||
const input = document.createElement("input") as any;
|
|
||||||
input.type = "range";
|
|
||||||
input.value = 0;
|
|
||||||
input.step = 1;
|
|
||||||
input.min = 0;
|
|
||||||
input.max = chain.length - 1;
|
|
||||||
input.style.width = "90%";
|
|
||||||
input.addEventListener("input", () => {
|
|
||||||
k = +input.value;
|
|
||||||
onChange();
|
|
||||||
});
|
|
||||||
document.body.append(input);
|
|
||||||
window.addEventListener("click", (e) => {
|
|
||||||
if (e.target === document.body || e.target === document.body.parentElement)
|
|
||||||
input.focus();
|
|
||||||
});
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import "./menu";
|
|
||||||
import { createCanvas } from "./canvas";
|
|
||||||
import { getSnakeLength } from "@snk/types/snake";
|
|
||||||
import { grid, snake } from "./sample";
|
|
||||||
import { getColor } from "@snk/types/grid";
|
|
||||||
import { getBestTunnel } from "@snk/solver/getBestTunnel";
|
|
||||||
import { createOutside } from "@snk/solver/outside";
|
|
||||||
import type { Color } from "@snk/types/grid";
|
|
||||||
import type { Point } from "@snk/types/point";
|
|
||||||
|
|
||||||
const { canvas, ctx, draw, highlightCell } = createCanvas(grid);
|
|
||||||
document.body.appendChild(canvas);
|
|
||||||
|
|
||||||
const ones: Point[] = [];
|
|
||||||
|
|
||||||
for (let x = 0; x < grid.width; x++)
|
|
||||||
for (let y = 0; y < grid.height; y++)
|
|
||||||
if (getColor(grid, x, y) === 1) ones.push({ x, y });
|
|
||||||
|
|
||||||
const tunnels = ones.map(({ x, y }) => ({
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
tunnel: getBestTunnel(
|
|
||||||
grid,
|
|
||||||
createOutside(grid),
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
3 as Color,
|
|
||||||
getSnakeLength(snake)
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const onChange = () => {
|
|
||||||
const k = +inputK.value;
|
|
||||||
const i = +inputI.value;
|
|
||||||
|
|
||||||
ctx.clearRect(0, 0, 9999, 9999);
|
|
||||||
|
|
||||||
if (!tunnels[k]) return;
|
|
||||||
|
|
||||||
const { x, y, tunnel } = tunnels[k]!;
|
|
||||||
|
|
||||||
draw(grid, snake, []);
|
|
||||||
|
|
||||||
highlightCell(x, y, "red");
|
|
||||||
|
|
||||||
if (tunnel) {
|
|
||||||
tunnel.forEach(({ x, y }) => highlightCell(x, y));
|
|
||||||
highlightCell(x, y, "red");
|
|
||||||
highlightCell(tunnel[i].x, tunnel[i].y, "blue");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const inputK = document.createElement("input") as any;
|
|
||||||
inputK.type = "range";
|
|
||||||
inputK.value = 0;
|
|
||||||
inputK.step = 1;
|
|
||||||
inputK.min = 0;
|
|
||||||
inputK.max = tunnels ? tunnels.length - 1 : 0;
|
|
||||||
inputK.style.width = "90%";
|
|
||||||
inputK.style.padding = "20px 0";
|
|
||||||
inputK.addEventListener("input", () => {
|
|
||||||
inputI.value = 0;
|
|
||||||
inputI.max = (tunnels[+inputK.value]?.tunnel?.length || 1) - 1;
|
|
||||||
onChange();
|
|
||||||
});
|
|
||||||
document.body.append(inputK);
|
|
||||||
|
|
||||||
const inputI = document.createElement("input") as any;
|
|
||||||
inputI.type = "range";
|
|
||||||
inputI.value = 0;
|
|
||||||
inputI.step = 1;
|
|
||||||
inputI.min = 0;
|
|
||||||
inputI.max = (tunnels[+inputK.value]?.tunnel?.length || 1) - 1;
|
|
||||||
inputI.style.width = "90%";
|
|
||||||
inputI.style.padding = "20px 0";
|
|
||||||
inputI.addEventListener("input", onChange);
|
|
||||||
document.body.append(inputI);
|
|
||||||
|
|
||||||
onChange();
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import "./menu";
|
|
||||||
import { createCanvas } from "./canvas";
|
|
||||||
import { copySnake, snakeToCells } from "@snk/types/snake";
|
|
||||||
import { grid, snake as snake0 } from "./sample";
|
|
||||||
import { getPathTo } from "@snk/solver/getPathTo";
|
|
||||||
|
|
||||||
const { canvas, ctx, draw, getPointedCell, highlightCell } = createCanvas(grid);
|
|
||||||
canvas.style.pointerEvents = "auto";
|
|
||||||
|
|
||||||
let snake = copySnake(snake0);
|
|
||||||
let chain = [snake];
|
|
||||||
|
|
||||||
canvas.addEventListener("mousemove", (e) => {
|
|
||||||
const { x, y } = getPointedCell(e);
|
|
||||||
|
|
||||||
chain = [...(getPathTo(grid, snake, x, y) || []), snake].reverse();
|
|
||||||
|
|
||||||
inputI.max = chain.length - 1;
|
|
||||||
i = inputI.value = chain.length - 1;
|
|
||||||
|
|
||||||
onChange();
|
|
||||||
});
|
|
||||||
|
|
||||||
canvas.addEventListener("click", () => {
|
|
||||||
snake = chain.slice(-1)[0];
|
|
||||||
|
|
||||||
chain = [snake];
|
|
||||||
inputI.max = chain.length - 1;
|
|
||||||
i = inputI.value = chain.length - 1;
|
|
||||||
|
|
||||||
onChange();
|
|
||||||
});
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
const onChange = () => {
|
|
||||||
ctx.clearRect(0, 0, 9999, 9999);
|
|
||||||
|
|
||||||
draw(grid, chain[i], []);
|
|
||||||
chain
|
|
||||||
.map(snakeToCells)
|
|
||||||
.flat()
|
|
||||||
.forEach(({ x, y }) => highlightCell(x, y));
|
|
||||||
};
|
|
||||||
|
|
||||||
onChange();
|
|
||||||
|
|
||||||
const inputI = document.createElement("input") as any;
|
|
||||||
inputI.type = "range";
|
|
||||||
inputI.value = 0;
|
|
||||||
inputI.max = chain ? chain.length - 1 : 0;
|
|
||||||
inputI.step = 1;
|
|
||||||
inputI.min = 0;
|
|
||||||
inputI.style.width = "90%";
|
|
||||||
inputI.style.padding = "20px 0";
|
|
||||||
inputI.addEventListener("input", () => {
|
|
||||||
i = +inputI.value;
|
|
||||||
onChange();
|
|
||||||
});
|
|
||||||
document.body.append(inputI);
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import "./menu";
|
|
||||||
import { createCanvas } from "./canvas";
|
|
||||||
import { createSnakeFromCells, snakeToCells } from "@snk/types/snake";
|
|
||||||
import { grid, snake } from "./sample";
|
|
||||||
import { getPathToPose } from "@snk/solver/getPathToPose";
|
|
||||||
|
|
||||||
const { canvas, ctx, draw, highlightCell } = createCanvas(grid);
|
|
||||||
canvas.style.pointerEvents = "auto";
|
|
||||||
|
|
||||||
const target = createSnakeFromCells(
|
|
||||||
snakeToCells(snake).map((p) => ({ ...p, x: p.x - 1 }))
|
|
||||||
);
|
|
||||||
|
|
||||||
let chain = [snake, ...getPathToPose(snake, target)!];
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
const onChange = () => {
|
|
||||||
ctx.clearRect(0, 0, 9999, 9999);
|
|
||||||
|
|
||||||
draw(grid, chain[i], []);
|
|
||||||
chain
|
|
||||||
.map(snakeToCells)
|
|
||||||
.flat()
|
|
||||||
.forEach(({ x, y }) => highlightCell(x, y));
|
|
||||||
};
|
|
||||||
|
|
||||||
onChange();
|
|
||||||
|
|
||||||
const inputI = document.createElement("input") as any;
|
|
||||||
inputI.type = "range";
|
|
||||||
inputI.value = 0;
|
|
||||||
inputI.max = chain ? chain.length - 1 : 0;
|
|
||||||
inputI.step = 1;
|
|
||||||
inputI.min = 0;
|
|
||||||
inputI.style.width = "90%";
|
|
||||||
inputI.style.padding = "20px 0";
|
|
||||||
inputI.addEventListener("input", () => {
|
|
||||||
i = +inputI.value;
|
|
||||||
onChange();
|
|
||||||
});
|
|
||||||
document.body.append(inputI);
|
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
import { Color, copyGrid, Grid } from "@snk/types/grid";
|
|
||||||
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 {
|
|
||||||
drawLerpWorld,
|
|
||||||
getCanvasWorldSize,
|
|
||||||
Options,
|
|
||||||
} 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";
|
|
||||||
|
|
||||||
const createForm = ({
|
|
||||||
onSubmit,
|
|
||||||
onChangeUserName,
|
|
||||||
}: {
|
|
||||||
onSubmit: (s: string) => Promise<void>;
|
|
||||||
onChangeUserName: (s: string) => void;
|
|
||||||
}) => {
|
|
||||||
const form = document.createElement("form");
|
|
||||||
form.style.position = "relative";
|
|
||||||
form.style.display = "flex";
|
|
||||||
form.style.flexDirection = "row";
|
|
||||||
const input = document.createElement("input");
|
|
||||||
input.addEventListener("input", () => onChangeUserName(input.value));
|
|
||||||
input.style.padding = "16px";
|
|
||||||
input.placeholder = "github user";
|
|
||||||
const submit = document.createElement("button");
|
|
||||||
submit.style.padding = "16px";
|
|
||||||
submit.type = "submit";
|
|
||||||
submit.innerText = "ok";
|
|
||||||
|
|
||||||
const label = document.createElement("label");
|
|
||||||
label.style.position = "absolute";
|
|
||||||
label.style.textAlign = "center";
|
|
||||||
label.style.top = "60px";
|
|
||||||
label.style.left = "0";
|
|
||||||
label.style.right = "0";
|
|
||||||
|
|
||||||
form.appendChild(input);
|
|
||||||
form.appendChild(submit);
|
|
||||||
document.body.appendChild(form);
|
|
||||||
|
|
||||||
form.addEventListener("submit", (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
onSubmit(input.value)
|
|
||||||
.finally(() => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
label.innerText = "error :(";
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
input.disabled = true;
|
|
||||||
submit.disabled = true;
|
|
||||||
form.appendChild(label);
|
|
||||||
label.innerText = "loading ...";
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
label.innerText = "loading ( it might take a while ) ... ";
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
|
|
||||||
//
|
|
||||||
// dispose
|
|
||||||
const dispose = () => {
|
|
||||||
document.body.removeChild(form);
|
|
||||||
};
|
|
||||||
|
|
||||||
return { dispose };
|
|
||||||
};
|
|
||||||
|
|
||||||
const clamp = (x: number, a: number, b: number) => Math.max(a, Math.min(b, x));
|
|
||||||
|
|
||||||
const createGithubProfile = () => {
|
|
||||||
const container = document.createElement("div");
|
|
||||||
container.style.padding = "20px";
|
|
||||||
container.style.opacity = "0";
|
|
||||||
container.style.display = "flex";
|
|
||||||
container.style.flexDirection = "column";
|
|
||||||
container.style.height = "120px";
|
|
||||||
container.style.alignItems = "flex-start";
|
|
||||||
const image = document.createElement("img");
|
|
||||||
image.style.width = "100px";
|
|
||||||
image.style.height = "100px";
|
|
||||||
image.style.borderRadius = "50px";
|
|
||||||
const name = document.createElement("a");
|
|
||||||
name.style.padding = "4px 0 0 0";
|
|
||||||
|
|
||||||
document.body.appendChild(container);
|
|
||||||
container.appendChild(image);
|
|
||||||
container.appendChild(name);
|
|
||||||
|
|
||||||
image.addEventListener("load", () => {
|
|
||||||
container.style.opacity = "1";
|
|
||||||
});
|
|
||||||
const onChangeUser = (userName: string) => {
|
|
||||||
container.style.opacity = "0";
|
|
||||||
name.innerText = userName;
|
|
||||||
name.href = `https://github.com/${userName}`;
|
|
||||||
image.src = `https://github.com/${userName}.png`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const dispose = () => {
|
|
||||||
document.body.removeChild(container);
|
|
||||||
};
|
|
||||||
|
|
||||||
return { dispose, onChangeUser };
|
|
||||||
};
|
|
||||||
|
|
||||||
const createViewer = ({
|
|
||||||
grid0,
|
|
||||||
chain,
|
|
||||||
drawOptions,
|
|
||||||
}: {
|
|
||||||
grid0: Grid;
|
|
||||||
chain: Snake[];
|
|
||||||
drawOptions: Options;
|
|
||||||
}) => {
|
|
||||||
//
|
|
||||||
// canvas
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
const { width, height } = getCanvasWorldSize(grid0, drawOptions);
|
|
||||||
canvas.width = width;
|
|
||||||
canvas.height = height;
|
|
||||||
|
|
||||||
const w = Math.min(width, window.innerWidth);
|
|
||||||
const h = (height / width) * w;
|
|
||||||
canvas.style.width = w + "px";
|
|
||||||
canvas.style.height = h + "px";
|
|
||||||
canvas.style.pointerEvents = "none";
|
|
||||||
|
|
||||||
document.body.appendChild(canvas);
|
|
||||||
|
|
||||||
//
|
|
||||||
// draw
|
|
||||||
let animationFrame: number;
|
|
||||||
const spring = { x: 0, v: 0, target: 0 };
|
|
||||||
const springParams = { tension: 120, friction: 20, maxVelocity: 50 };
|
|
||||||
const ctx = canvas.getContext("2d")!;
|
|
||||||
const loop = () => {
|
|
||||||
cancelAnimationFrame(animationFrame);
|
|
||||||
|
|
||||||
stepSpring(spring, springParams, spring.target);
|
|
||||||
const stable = isStableAndBound(spring, spring.target);
|
|
||||||
|
|
||||||
const grid = copyGrid(grid0);
|
|
||||||
const stack: Color[] = [];
|
|
||||||
for (let i = 0; i < Math.min(chain.length, spring.x); i++)
|
|
||||||
step(grid, stack, chain[i]);
|
|
||||||
|
|
||||||
const snake0 = chain[clamp(Math.floor(spring.x), 0, chain.length - 1)];
|
|
||||||
const snake1 = chain[clamp(Math.ceil(spring.x), 0, chain.length - 1)];
|
|
||||||
const k = spring.x % 1;
|
|
||||||
|
|
||||||
ctx.clearRect(0, 0, 9999, 9999);
|
|
||||||
drawLerpWorld(ctx, grid, snake0, snake1, stack, k, drawOptions);
|
|
||||||
|
|
||||||
if (!stable) animationFrame = requestAnimationFrame(loop);
|
|
||||||
};
|
|
||||||
loop();
|
|
||||||
|
|
||||||
//
|
|
||||||
// controls
|
|
||||||
const input = document.createElement("input") as any;
|
|
||||||
input.type = "range";
|
|
||||||
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;
|
|
||||||
cancelAnimationFrame(animationFrame);
|
|
||||||
animationFrame = requestAnimationFrame(loop);
|
|
||||||
});
|
|
||||||
const onClickBackground = (e: MouseEvent) => {
|
|
||||||
if (e.target === document.body || e.target === document.body.parentElement)
|
|
||||||
input.focus();
|
|
||||||
};
|
|
||||||
window.addEventListener("click", onClickBackground);
|
|
||||||
document.body.append(input);
|
|
||||||
|
|
||||||
//
|
|
||||||
// svg
|
|
||||||
const svgLink = document.createElement("a");
|
|
||||||
const svgString = createSvg(grid0, chain, drawOptions, {
|
|
||||||
frameDuration: 100,
|
|
||||||
});
|
|
||||||
const svgImageUri = `data:image/*;charset=utf-8;base64,${btoa(svgString)}`;
|
|
||||||
svgLink.href = svgImageUri;
|
|
||||||
svgLink.innerText = "github-user-contribution.svg";
|
|
||||||
svgLink.download = "github-user-contribution.svg";
|
|
||||||
svgLink.addEventListener("click", (e) => {
|
|
||||||
const w = window.open("")!;
|
|
||||||
w.document.write(
|
|
||||||
`<a href="${svgImageUri}" download="github-user-contribution.svg">` +
|
|
||||||
svgString +
|
|
||||||
"<a/>"
|
|
||||||
);
|
|
||||||
e.preventDefault();
|
|
||||||
});
|
|
||||||
svgLink.style.padding = "20px";
|
|
||||||
svgLink.style.paddingTop = "60px";
|
|
||||||
svgLink.style.alignSelf = "flex-start";
|
|
||||||
document.body.append(svgLink);
|
|
||||||
|
|
||||||
//
|
|
||||||
// dispose
|
|
||||||
const dispose = () => {
|
|
||||||
window.removeEventListener("click", onClickBackground);
|
|
||||||
cancelAnimationFrame(animationFrame);
|
|
||||||
document.body.removeChild(canvas);
|
|
||||||
document.body.removeChild(input);
|
|
||||||
document.body.removeChild(svgLink);
|
|
||||||
};
|
|
||||||
|
|
||||||
return { dispose };
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async (userName: string) => {
|
|
||||||
const res = await fetch(
|
|
||||||
process.env.GITHUB_USER_CONTRIBUTION_API_ENDPOINT + userName
|
|
||||||
);
|
|
||||||
const { cells, colorScheme } = (await res.json()) as Res;
|
|
||||||
|
|
||||||
const 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 chain = await getChain(grid);
|
|
||||||
|
|
||||||
dispose();
|
|
||||||
|
|
||||||
createViewer({ grid0: grid, chain, drawOptions });
|
|
||||||
};
|
|
||||||
|
|
||||||
const worker = new Worker(
|
|
||||||
new URL(
|
|
||||||
"./demo.interactive.worker.ts",
|
|
||||||
// @ts-ignore
|
|
||||||
import.meta.url
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const { getChain } = createRpcClient<WorkerAPI>(worker);
|
|
||||||
|
|
||||||
const profile = createGithubProfile();
|
|
||||||
const { dispose } = createForm({
|
|
||||||
onSubmit,
|
|
||||||
onChangeUserName: profile.onChangeUser,
|
|
||||||
});
|
|
||||||
|
|
||||||
document.body.style.margin = "0";
|
|
||||||
document.body.style.display = "flex";
|
|
||||||
document.body.style.flexDirection = "column";
|
|
||||||
document.body.style.alignItems = "center";
|
|
||||||
document.body.style.justifyContent = "center";
|
|
||||||
document.body.style.height = "100%";
|
|
||||||
document.body.style.width = "100%";
|
|
||||||
document.body.style.position = "absolute";
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { getBestRoute } from "@snk/solver/getBestRoute";
|
|
||||||
import { getPathToPose } from "@snk/solver/getPathToPose";
|
|
||||||
import { snake4 as snake } from "@snk/types/__fixtures__/snake";
|
|
||||||
import type { Grid } from "@snk/types/grid";
|
|
||||||
import { createRpcServer } from "./worker-utils";
|
|
||||||
|
|
||||||
const getChain = (grid: Grid) => {
|
|
||||||
const chain = getBestRoute(grid, snake)!;
|
|
||||||
chain.push(...getPathToPose(chain.slice(-1)[0], snake)!);
|
|
||||||
|
|
||||||
return chain;
|
|
||||||
};
|
|
||||||
|
|
||||||
const api = { getChain };
|
|
||||||
export type API = typeof api;
|
|
||||||
|
|
||||||
createRpcServer(api);
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
[
|
|
||||||
"interactive",
|
|
||||||
"getBestRoute",
|
|
||||||
"getBestTunnel",
|
|
||||||
"outside",
|
|
||||||
"getPathToPose",
|
|
||||||
"getPathTo",
|
|
||||||
"svg"
|
|
||||||
]
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import "./menu";
|
|
||||||
import { createCanvas } from "./canvas";
|
|
||||||
import { grid } from "./sample";
|
|
||||||
import type { Color } from "@snk/types/grid";
|
|
||||||
import { createOutside, isOutside } from "@snk/solver/outside";
|
|
||||||
|
|
||||||
const { canvas, ctx, draw, highlightCell } = createCanvas(grid);
|
|
||||||
document.body.appendChild(canvas);
|
|
||||||
|
|
||||||
let k = 0;
|
|
||||||
|
|
||||||
const onChange = () => {
|
|
||||||
ctx.clearRect(0, 0, 9999, 9999);
|
|
||||||
|
|
||||||
draw(grid, [] as any, []);
|
|
||||||
|
|
||||||
const outside = createOutside(grid, k as Color);
|
|
||||||
|
|
||||||
for (let x = outside.width; x--; )
|
|
||||||
for (let y = outside.height; y--; )
|
|
||||||
if (isOutside(outside, x, y)) highlightCell(x, y);
|
|
||||||
};
|
|
||||||
|
|
||||||
onChange();
|
|
||||||
|
|
||||||
const inputK = document.createElement("input") as any;
|
|
||||||
inputK.type = "range";
|
|
||||||
inputK.value = 0;
|
|
||||||
inputK.step = 1;
|
|
||||||
inputK.min = 0;
|
|
||||||
inputK.max = 4;
|
|
||||||
inputK.style.width = "90%";
|
|
||||||
inputK.style.padding = "20px 0";
|
|
||||||
inputK.addEventListener("input", () => {
|
|
||||||
k = +inputK.value;
|
|
||||||
onChange();
|
|
||||||
});
|
|
||||||
document.body.append(inputK);
|
|
||||||
window.addEventListener("click", (e) => {
|
|
||||||
if (e.target === document.body || e.target === document.body.parentElement)
|
|
||||||
inputK.focus();
|
|
||||||
});
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import "./menu";
|
|
||||||
import { getBestRoute } from "@snk/solver/getBestRoute";
|
|
||||||
import { createSvg } from "@snk/svg-creator";
|
|
||||||
import { grid, snake } from "./sample";
|
|
||||||
import { drawOptions } from "./canvas";
|
|
||||||
import { getPathToPose } from "@snk/solver/getPathToPose";
|
|
||||||
|
|
||||||
const chain = getBestRoute(grid, snake);
|
|
||||||
chain.push(...getPathToPose(chain.slice(-1)[0], snake)!);
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const svg = await createSvg(grid, chain, drawOptions, { frameDuration: 200 });
|
|
||||||
|
|
||||||
const container = document.createElement("div");
|
|
||||||
container.innerHTML = svg;
|
|
||||||
document.body.appendChild(container);
|
|
||||||
})();
|
|
||||||
81
packages/demo/index.ts
Normal file
81
packages/demo/index.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
// import { generateGrid } from "@snk/compute/generateGrid";
|
||||||
|
|
||||||
|
import { generateGrid } from "@snk/compute/generateGrid";
|
||||||
|
import { Color, copyGrid } from "@snk/compute/grid";
|
||||||
|
import { computeBestRun } from "@snk/compute";
|
||||||
|
import { step } from "@snk/compute/step";
|
||||||
|
import { drawWorld } from "@snk/draw/drawWorld";
|
||||||
|
import { Point } from "@snk/compute/point";
|
||||||
|
|
||||||
|
const copySnake = (x: Point[]) => x.map((p) => ({ ...p }));
|
||||||
|
|
||||||
|
export const run = async () => {
|
||||||
|
const drawOptions = {
|
||||||
|
sizeBorderRadius: 2,
|
||||||
|
sizeCell: 16,
|
||||||
|
sizeDot: 12,
|
||||||
|
colorBorder: "#1b1f230a",
|
||||||
|
colorDots: { 1: "#9be9a8", 2: "#40c463", 3: "#30a14e", 4: "#216e39" },
|
||||||
|
colorEmpty: "#ebedf0",
|
||||||
|
colorSnake: "purple",
|
||||||
|
};
|
||||||
|
|
||||||
|
const gameOptions = { maxSnakeLength: 5 };
|
||||||
|
|
||||||
|
const grid0 = generateGrid(42, 7, { colors: [1, 2, 3, 4], emptyP: 3 });
|
||||||
|
|
||||||
|
const snake0 = [
|
||||||
|
{ x: 4, y: -1 },
|
||||||
|
{ x: 3, y: -1 },
|
||||||
|
{ x: 2, y: -1 },
|
||||||
|
{ x: 1, y: -1 },
|
||||||
|
{ x: 0, y: -1 },
|
||||||
|
];
|
||||||
|
const stack0: Color[] = [];
|
||||||
|
|
||||||
|
const chain = computeBestRun(grid0, snake0, gameOptions);
|
||||||
|
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = drawOptions.sizeCell * (grid0.width + 4);
|
||||||
|
canvas.height = drawOptions.sizeCell * (grid0.height + 4) + 100;
|
||||||
|
document.body.appendChild(canvas);
|
||||||
|
const ctx = canvas.getContext("2d")!;
|
||||||
|
|
||||||
|
const update = (n: number) => {
|
||||||
|
const snake = copySnake(snake0);
|
||||||
|
const stack = stack0.slice();
|
||||||
|
const grid = copyGrid(grid0);
|
||||||
|
|
||||||
|
for (let i = 0; i < n; i++) step(grid, snake, stack, chain[i], gameOptions);
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, 9999, 9999);
|
||||||
|
drawWorld(ctx, grid, snake, stack, drawOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const input: any = document.createElement("input");
|
||||||
|
input.type = "range";
|
||||||
|
input.style.width = "100%";
|
||||||
|
input.min = 0;
|
||||||
|
input.max = chain.length;
|
||||||
|
input.step = 1;
|
||||||
|
input.value = 0;
|
||||||
|
input.addEventListener("input", () => update(+input.value));
|
||||||
|
document.addEventListener("click", () => input.focus());
|
||||||
|
|
||||||
|
document.body.appendChild(input);
|
||||||
|
|
||||||
|
update(+input.value);
|
||||||
|
|
||||||
|
// while (chain.length) {
|
||||||
|
// await wait(100);
|
||||||
|
|
||||||
|
// step(grid, snake, stack, chain.shift()!, gameOptions);
|
||||||
|
|
||||||
|
// ctx.clearRect(0, 0, 9999, 9999);
|
||||||
|
// drawWorld(ctx, grid, snake, stack, options);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const wait = (delay = 0) => new Promise((r) => setTimeout(r, delay));
|
||||||
|
};
|
||||||
|
|
||||||
|
run();
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { GUI } from "dat.gui";
|
|
||||||
import * as grids from "@snk/types/__fixtures__/grid";
|
|
||||||
import * as snakes from "@snk/types/__fixtures__/snake";
|
|
||||||
import { grid, snake } from "./sample";
|
|
||||||
|
|
||||||
const demos: string[] = require("./demo.json");
|
|
||||||
|
|
||||||
export const gui = new GUI();
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
snake: Object.entries(snakes).find(([_, s]) => s === snake)![0],
|
|
||||||
grid: Object.entries(grids).find(([_, s]) => s === grid)![0],
|
|
||||||
demo: demos[0],
|
|
||||||
};
|
|
||||||
{
|
|
||||||
const d = window.location.pathname.match(/(\w+)\.html/);
|
|
||||||
if (d && demos.includes(d[1])) config.demo = d[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
const onChange = () => {
|
|
||||||
const search = new URLSearchParams({
|
|
||||||
snake: config.snake,
|
|
||||||
grid: config.grid,
|
|
||||||
}).toString();
|
|
||||||
|
|
||||||
const url = new URL(
|
|
||||||
config.demo + ".html?" + search,
|
|
||||||
window.location.href
|
|
||||||
).toString();
|
|
||||||
|
|
||||||
window.location.href = url;
|
|
||||||
};
|
|
||||||
|
|
||||||
gui.add(config, "demo", demos).onChange(onChange);
|
|
||||||
gui.add(config, "grid", Object.keys(grids)).onChange(onChange);
|
|
||||||
gui.add(config, "snake", Object.keys(snakes)).onChange(onChange);
|
|
||||||
@@ -2,25 +2,18 @@
|
|||||||
"name": "@snk/demo",
|
"name": "@snk/demo",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@snk/action": "1.0.0",
|
"@snk/compute": "1.0.0"
|
||||||
"@snk/draw": "1.0.0",
|
|
||||||
"@snk/github-user-contribution": "1.0.0",
|
|
||||||
"@snk/solver": "1.0.0",
|
|
||||||
"@snk/svg-creator": "1.0.0",
|
|
||||||
"@snk/types": "1.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/dat.gui": "0.7.7",
|
"webpack": "4.43.0",
|
||||||
"dat.gui": "0.7.9",
|
"webpack-cli": "3.3.12",
|
||||||
"html-webpack-plugin": "5.5.0",
|
"webpack-dev-server": "3.11.0",
|
||||||
"ts-loader": "9.2.8",
|
"ts-loader": "8.0.1",
|
||||||
"ts-node": "10.7.0",
|
"html-webpack-plugin": "4.3.0"
|
||||||
"webpack": "5.72.0",
|
|
||||||
"webpack-cli": "4.9.2",
|
|
||||||
"webpack-dev-server": "4.8.1"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack",
|
"prepare": "tsc webpack.config.ts",
|
||||||
"dev": "webpack serve"
|
"build": "yarn prepare ; webpack",
|
||||||
|
"dev": "yarn prepare ; webpack-dev-server --port ${PORT-3000}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import * as grids from "@snk/types/__fixtures__/grid";
|
|
||||||
import * as snakes from "@snk/types/__fixtures__/snake";
|
|
||||||
import type { Snake } from "@snk/types/snake";
|
|
||||||
import type { Grid } from "@snk/types/grid";
|
|
||||||
|
|
||||||
const sp = new URLSearchParams(window.location.search);
|
|
||||||
|
|
||||||
const gLabel = sp.get("grid") || "simple";
|
|
||||||
const sLabel = sp.get("snake") || "snake3";
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
export const grid: Grid = grids[gLabel] || grids.simple;
|
|
||||||
//@ts-ignore
|
|
||||||
export const snake: Snake = snakes[sLabel] || snakes.snake3;
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
const epsilon = 0.01;
|
|
||||||
|
|
||||||
export const clamp = (a: number, b: number) => (x: number) =>
|
|
||||||
Math.max(a, Math.min(b, x));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* step the spring, mutate the state to reflect the state at t+dt
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
const stepSpringOne = (
|
|
||||||
s: { x: number; v: number },
|
|
||||||
{
|
|
||||||
tension,
|
|
||||||
friction,
|
|
||||||
maxVelocity = Infinity,
|
|
||||||
}: { tension: number; friction: number; maxVelocity?: number },
|
|
||||||
target: number,
|
|
||||||
dt = 1 / 60
|
|
||||||
) => {
|
|
||||||
const a = -tension * (s.x - target) - friction * s.v;
|
|
||||||
|
|
||||||
s.v += a * dt;
|
|
||||||
s.v = clamp(-maxVelocity / dt, maxVelocity / dt)(s.v);
|
|
||||||
s.x += s.v * dt;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* return true if the spring is to be considered in a stable state
|
|
||||||
* ( close enough to the target and with a small enough velocity )
|
|
||||||
*/
|
|
||||||
export const isStable = (
|
|
||||||
s: { x: number; v: number },
|
|
||||||
target: number,
|
|
||||||
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
|
|
||||||
) => {
|
|
||||||
const stable = isStable(s, target, dt);
|
|
||||||
if (stable) {
|
|
||||||
s.x = target;
|
|
||||||
s.v = 0;
|
|
||||||
}
|
|
||||||
return stable;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const stepSpring = (
|
|
||||||
s: { x: number; v: number },
|
|
||||||
params: { tension: number; friction: number; maxVelocity?: number },
|
|
||||||
target: number,
|
|
||||||
dt = 1 / 60
|
|
||||||
) => {
|
|
||||||
const interval = 1 / 60;
|
|
||||||
|
|
||||||
while (dt > 0) {
|
|
||||||
stepSpringOne(s, params, target, Math.min(interval, dt));
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
dt -= interval;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,75 +1,47 @@
|
|||||||
import path from "path";
|
import * as path from "path";
|
||||||
import HtmlWebpackPlugin from "html-webpack-plugin";
|
|
||||||
|
|
||||||
import type { Configuration as WebpackConfiguration } from "webpack";
|
// @ts-ignore
|
||||||
import type { Configuration as WebpackDevServerConfiguration } from "webpack-dev-server";
|
import * as HtmlWebpackPlugin from "html-webpack-plugin";
|
||||||
import webpack from "webpack";
|
import type { Configuration } from "webpack";
|
||||||
import { getGithubUserContribution } from "@snk/github-user-contribution";
|
|
||||||
|
|
||||||
const demos: string[] = require("./demo.json");
|
const basePathname = (process.env.BASE_PATHNAME || "")
|
||||||
|
.split("/")
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
const webpackDevServerConfiguration: WebpackDevServerConfiguration = {
|
const config: Configuration = {
|
||||||
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));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const webpackConfiguration: WebpackConfiguration = {
|
|
||||||
mode: "development",
|
mode: "development",
|
||||||
entry: Object.fromEntries(
|
entry: "./index",
|
||||||
demos.map((demo: string) => [demo, `./demo.${demo}`])
|
|
||||||
),
|
|
||||||
target: ["web", "es2019"],
|
|
||||||
resolve: { extensions: [".ts", ".js"] },
|
resolve: { extensions: [".ts", ".js"] },
|
||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, "dist"),
|
path: path.join(__dirname, "dist"),
|
||||||
filename: "[contenthash].js",
|
filename: "[contenthash].js",
|
||||||
|
publicPath: "/" + basePathname.map((x) => x + "/").join(""),
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
test: /\.ts$/,
|
test: /\.(js|ts)$/,
|
||||||
loader: "ts-loader",
|
loader: "ts-loader",
|
||||||
options: {
|
|
||||||
transpileOnly: true,
|
|
||||||
compilerOptions: {
|
|
||||||
lib: ["dom", "es2020"],
|
|
||||||
target: "es2019",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
...demos.map(
|
// game
|
||||||
(demo) =>
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
title: "snk - " + demo,
|
|
||||||
filename: `${demo}.html`,
|
|
||||||
chunks: [demo],
|
|
||||||
})
|
|
||||||
),
|
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
title: "snk - " + demos[0],
|
title: "demo",
|
||||||
filename: `index.html`,
|
filename: "index.html",
|
||||||
chunks: [demos[0]],
|
meta: {
|
||||||
}),
|
viewport: "width=device-width, initial-scale=1, shrink-to-fit=no",
|
||||||
new webpack.EnvironmentPlugin({
|
},
|
||||||
GITHUB_USER_CONTRIBUTION_API_ENDPOINT:
|
|
||||||
process.env.GITHUB_USER_CONTRIBUTION_API_ENDPOINT ??
|
|
||||||
"/api/github-user-contribution/",
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|
||||||
devtool: false,
|
devtool: false,
|
||||||
|
stats: "errors-only",
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
devServer: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default config;
|
||||||
...webpackConfiguration,
|
|
||||||
devServer: webpackDevServerConfiguration,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
type API = Record<string, (...args: any[]) => any>;
|
|
||||||
|
|
||||||
const symbol = "worker-rpc__";
|
|
||||||
|
|
||||||
export const createRpcServer = (api: API) =>
|
|
||||||
self.addEventListener("message", async (event) => {
|
|
||||||
if (event.data?.symbol === symbol) {
|
|
||||||
try {
|
|
||||||
const res = await api[event.data.methodName](...event.data.args);
|
|
||||||
self.postMessage({ symbol, key: event.data.key, res });
|
|
||||||
} catch (error: any) {
|
|
||||||
postMessage({ symbol, key: event.data.key, error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createRpcClient = <API_ extends API>(worker: Worker) => {
|
|
||||||
const originalTerminate = worker.terminate;
|
|
||||||
worker.terminate = () => {
|
|
||||||
worker.dispatchEvent(new Event("terminate"));
|
|
||||||
originalTerminate.call(worker);
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Proxy(
|
|
||||||
{} as {
|
|
||||||
[K in keyof API_]: (
|
|
||||||
...args: Parameters<API_[K]>
|
|
||||||
) => Promise<Awaited<ReturnType<API_[K]>>>;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
get:
|
|
||||||
(_, methodName) =>
|
|
||||||
(...args: any[]) =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const key = Math.random().toString();
|
|
||||||
|
|
||||||
const onTerminate = () => {
|
|
||||||
worker.removeEventListener("terminate", onTerminate);
|
|
||||||
worker.removeEventListener("message", onMessageHandler);
|
|
||||||
reject(new Error("worker terminated"));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMessageHandler = (event: MessageEvent) => {
|
|
||||||
if (event.data?.symbol === symbol && event.data.key === key) {
|
|
||||||
if (event.data.error) reject(event.data.error);
|
|
||||||
else if (event.data.res) resolve(event.data.res);
|
|
||||||
|
|
||||||
worker.removeEventListener("terminate", onTerminate);
|
|
||||||
worker.removeEventListener("message", onMessageHandler);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
worker.addEventListener("message", onMessageHandler);
|
|
||||||
worker.addEventListener("terminate", onTerminate);
|
|
||||||
worker.postMessage({ symbol, key, methodName, args });
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# @snk/draw
|
|
||||||
|
|
||||||
Draw grids and snakes on a canvas.
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import { pathRoundedRect } from "./pathRoundedRect";
|
|
||||||
import type { Color } from "@snk/types/grid";
|
|
||||||
import type { Point } from "@snk/types/point";
|
|
||||||
|
|
||||||
type Options = {
|
|
||||||
colorDots: Record<Color, string>;
|
|
||||||
colorBorder: string;
|
|
||||||
sizeCell: number;
|
|
||||||
sizeDot: number;
|
|
||||||
sizeBorderRadius: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isInsideCircle = (x: number, y: number, r: number) => {
|
|
||||||
const l = 6;
|
|
||||||
let k = 0;
|
|
||||||
for (let dx = 0; dx < l; dx++)
|
|
||||||
for (let dy = 0; dy < l; dy++) {
|
|
||||||
const ux = x + (dx + 0.5) / l;
|
|
||||||
const uy = y + (dy + 0.5) / l;
|
|
||||||
|
|
||||||
if (ux * ux + uy * uy < r * r) k++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return k > l * l * 0.6;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCellPath = (n: number): Point[] => {
|
|
||||||
const l = Math.ceil(Math.sqrt(n));
|
|
||||||
|
|
||||||
const cells = [];
|
|
||||||
|
|
||||||
for (let x = -l; x <= l; x++)
|
|
||||||
for (let y = -l; y <= l; y++) {
|
|
||||||
const a = (Math.atan2(y, x) + (5 * Math.PI) / 2) % (Math.PI * 2);
|
|
||||||
|
|
||||||
let r = 0;
|
|
||||||
|
|
||||||
while (!isInsideCircle(x, y, r + 0.5)) r++;
|
|
||||||
|
|
||||||
cells.push({ x, y, f: r * 100 + a });
|
|
||||||
}
|
|
||||||
|
|
||||||
return cells.sort((a, b) => a.f - b.f).slice(0, n);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cellPath = getCellPath(52 * 7 + 5);
|
|
||||||
|
|
||||||
export const getCircleSize = (n: number) => {
|
|
||||||
const c = cellPath.slice(0, n);
|
|
||||||
const xs = c.map((p) => p.x);
|
|
||||||
const ys = c.map((p) => p.y);
|
|
||||||
|
|
||||||
return {
|
|
||||||
max: { x: Math.max(0, ...xs), y: Math.max(0, ...ys) },
|
|
||||||
min: { x: Math.min(0, ...xs), y: Math.min(0, ...ys) },
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const drawCircleStack = (
|
|
||||||
ctx: CanvasRenderingContext2D,
|
|
||||||
stack: Color[],
|
|
||||||
o: Options
|
|
||||||
) => {
|
|
||||||
for (let i = stack.length; i--; ) {
|
|
||||||
const { x, y } = cellPath[i];
|
|
||||||
|
|
||||||
ctx.save();
|
|
||||||
ctx.translate(
|
|
||||||
x * o.sizeCell + (o.sizeCell - o.sizeDot) / 2,
|
|
||||||
y * o.sizeCell + (o.sizeCell - o.sizeDot) / 2
|
|
||||||
);
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
ctx.fillStyle = o.colorDots[stack[i]];
|
|
||||||
ctx.strokeStyle = o.colorBorder;
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.beginPath();
|
|
||||||
|
|
||||||
pathRoundedRect(ctx, o.sizeDot, o.sizeDot, o.sizeBorderRadius);
|
|
||||||
|
|
||||||
ctx.fill();
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.closePath();
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import { getColor } from "@snk/types/grid";
|
import { Grid, getColor, Color } from "@snk/compute/grid";
|
||||||
import { pathRoundedRect } from "./pathRoundedRect";
|
import { pathRoundedRect } from "./pathRoundedRect";
|
||||||
import type { Grid, Color } from "@snk/types/grid";
|
|
||||||
import type { Point } from "@snk/types/point";
|
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
colorDots: Record<Color, string>;
|
colorDots: Record<Color, string>;
|
||||||
@@ -10,7 +8,6 @@ type Options = {
|
|||||||
sizeCell: number;
|
sizeCell: number;
|
||||||
sizeDot: number;
|
sizeDot: number;
|
||||||
sizeBorderRadius: number;
|
sizeBorderRadius: number;
|
||||||
cells?: Point[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const drawGrid = (
|
export const drawGrid = (
|
||||||
@@ -20,28 +17,26 @@ export const drawGrid = (
|
|||||||
) => {
|
) => {
|
||||||
for (let x = grid.width; x--; )
|
for (let x = grid.width; x--; )
|
||||||
for (let y = grid.height; y--; ) {
|
for (let y = grid.height; y--; ) {
|
||||||
if (!o.cells || o.cells.some((c) => c.x === x && c.y === y)) {
|
const c = getColor(grid, x, y);
|
||||||
const c = getColor(grid, x, y);
|
// @ts-ignore
|
||||||
// @ts-ignore
|
const color = c === null ? o.colorEmpty : o.colorDots[c];
|
||||||
const color = !c ? o.colorEmpty : o.colorDots[c];
|
ctx.save();
|
||||||
ctx.save();
|
ctx.translate(
|
||||||
ctx.translate(
|
x * o.sizeCell + (o.sizeCell - o.sizeDot) / 2,
|
||||||
x * o.sizeCell + (o.sizeCell - o.sizeDot) / 2,
|
y * o.sizeCell + (o.sizeCell - o.sizeDot) / 2
|
||||||
y * o.sizeCell + (o.sizeCell - o.sizeDot) / 2
|
);
|
||||||
);
|
|
||||||
|
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
ctx.strokeStyle = o.colorBorder;
|
ctx.strokeStyle = o.colorBorder;
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
|
||||||
pathRoundedRect(ctx, o.sizeDot, o.sizeDot, o.sizeBorderRadius);
|
pathRoundedRect(ctx, o.sizeDot, o.sizeDot, o.sizeBorderRadius);
|
||||||
|
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.closePath();
|
ctx.closePath();
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import { pathRoundedRect } from "./pathRoundedRect";
|
|
||||||
import { snakeToCells } from "@snk/types/snake";
|
|
||||||
import type { Snake } from "@snk/types/snake";
|
|
||||||
|
|
||||||
type Options = {
|
|
||||||
colorSnake: string;
|
|
||||||
sizeCell: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const drawSnake = (
|
|
||||||
ctx: CanvasRenderingContext2D,
|
|
||||||
snake: Snake,
|
|
||||||
o: Options
|
|
||||||
) => {
|
|
||||||
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: number, a: number, b: number) => (1 - k) * a + k * b;
|
|
||||||
const clamp = (x: number, a: number, b: number) => Math.max(a, Math.min(b, x));
|
|
||||||
|
|
||||||
export const drawSnakeLerp = (
|
|
||||||
ctx: CanvasRenderingContext2D,
|
|
||||||
snake0: Snake,
|
|
||||||
snake1: Snake,
|
|
||||||
k: number,
|
|
||||||
o: Options
|
|
||||||
) => {
|
|
||||||
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(
|
|
||||||
ctx,
|
|
||||||
o.sizeCell - u * 2,
|
|
||||||
o.sizeCell - u * 2,
|
|
||||||
(o.sizeCell - u * 2) * 0.25
|
|
||||||
);
|
|
||||||
ctx.fill();
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
|
import { Grid, Color } from "@snk/compute/grid";
|
||||||
|
import { pathRoundedRect } from "./pathRoundedRect";
|
||||||
|
import { Point } from "@snk/compute/point";
|
||||||
import { drawGrid } from "./drawGrid";
|
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";
|
|
||||||
|
|
||||||
export type Options = {
|
type Options = {
|
||||||
colorDots: Record<Color, string>;
|
colorDots: Record<Color, string>;
|
||||||
colorEmpty: string;
|
colorEmpty: string;
|
||||||
colorBorder: string;
|
colorBorder: string;
|
||||||
@@ -12,85 +11,53 @@ export type Options = {
|
|||||||
sizeCell: number;
|
sizeCell: number;
|
||||||
sizeDot: number;
|
sizeDot: number;
|
||||||
sizeBorderRadius: number;
|
sizeBorderRadius: number;
|
||||||
cells?: Point[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const drawStack = (
|
export const drawSnake = (
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
stack: Color[],
|
snake: Point[],
|
||||||
max: number,
|
o: Options
|
||||||
width: number,
|
|
||||||
o: { colorDots: Record<Color, string> }
|
|
||||||
) => {
|
) => {
|
||||||
ctx.save();
|
for (let i = 0; i < snake.length; i++) {
|
||||||
|
const u = (i + 1) * 0.6;
|
||||||
|
|
||||||
const m = width / max;
|
ctx.save();
|
||||||
|
ctx.fillStyle = o.colorSnake;
|
||||||
for (let i = 0; i < stack.length; i++) {
|
ctx.translate(snake[i].x * o.sizeCell + u, snake[i].y * o.sizeCell + u);
|
||||||
// @ts-ignore
|
ctx.beginPath();
|
||||||
ctx.fillStyle = o.colorDots[stack[i]];
|
pathRoundedRect(
|
||||||
ctx.fillRect(i * m, 0, m + width * 0.005, 10);
|
ctx,
|
||||||
|
o.sizeCell - u * 2,
|
||||||
|
o.sizeCell - u * 2,
|
||||||
|
(o.sizeCell - u * 2) * 0.25
|
||||||
|
);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.restore();
|
||||||
}
|
}
|
||||||
ctx.restore();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const drawWorld = (
|
export const drawWorld = (
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
grid: Grid,
|
grid: Grid,
|
||||||
snake: Snake,
|
snake: Point[],
|
||||||
stack: Color[],
|
stack: Color[],
|
||||||
o: Options
|
o: Options
|
||||||
) => {
|
) => {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
||||||
ctx.translate(1 * o.sizeCell, 2 * o.sizeCell);
|
ctx.translate(2 * o.sizeCell, 2 * o.sizeCell);
|
||||||
drawGrid(ctx, grid, o);
|
drawGrid(ctx, grid, o);
|
||||||
drawSnake(ctx, snake, o);
|
drawSnake(ctx, snake, o);
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
ctx.save();
|
const m = 5;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
ctx.translate(o.sizeCell, (grid.height + 4) * o.sizeCell);
|
ctx.translate(o.sizeCell, (grid.height + 4) * o.sizeCell);
|
||||||
|
for (let i = 0; i < stack.length; i++) {
|
||||||
const max = grid.data.reduce((sum, x) => sum + +!!x, stack.length);
|
ctx.fillStyle = o.colorDots[stack[i]];
|
||||||
drawStack(ctx, stack, max, grid.width * o.sizeCell, o);
|
ctx.fillRect(i * m, 0, m, 10);
|
||||||
|
}
|
||||||
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();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const drawLerpWorld = (
|
|
||||||
ctx: CanvasRenderingContext2D,
|
|
||||||
grid: Grid,
|
|
||||||
snake0: Snake,
|
|
||||||
snake1: Snake,
|
|
||||||
stack: Color[],
|
|
||||||
k: number,
|
|
||||||
o: Options
|
|
||||||
) => {
|
|
||||||
ctx.save();
|
|
||||||
|
|
||||||
ctx.translate(1 * o.sizeCell, 2 * o.sizeCell);
|
|
||||||
drawGrid(ctx, grid, 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();
|
ctx.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCanvasWorldSize = (grid: Grid, o: { sizeCell: number }) => {
|
|
||||||
const width = o.sizeCell * (grid.width + 2);
|
|
||||||
const height = o.sizeCell * (grid.height + 4) + 30;
|
|
||||||
|
|
||||||
return { width, height };
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
"name": "@snk/draw",
|
"name": "@snk/draw",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@snk/solver": "1.0.0"
|
"@snk/compute": "1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
packages/gif-creator/.gitignore
vendored
Normal file
1
packages/gif-creator/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
out.gif
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# @snk/gif-creator
|
|
||||||
|
|
||||||
Generate a gif file from the grid and snake path.
|
|
||||||
|
|
||||||
Relies on graphics magic and gifsicle binaries.
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
*
|
|
||||||
!.gitignore
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
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 { getBestRoute } from "@snk/solver/getBestRoute";
|
|
||||||
import { getPathToPose } from "@snk/solver/getPathToPose";
|
|
||||||
|
|
||||||
let snake = createSnakeFromCells(
|
|
||||||
Array.from({ length: 4 }, (_, i) => ({ x: i, y: -1 }))
|
|
||||||
);
|
|
||||||
|
|
||||||
// const chain = [snake];
|
|
||||||
// for (let y = -1; y < grid.height; y++) {
|
|
||||||
// snake = nextSnake(snake, 0, 1);
|
|
||||||
// chain.push(snake);
|
|
||||||
|
|
||||||
// for (let x = grid.width - 1; x--; ) {
|
|
||||||
// snake = nextSnake(snake, (y + 100) % 2 ? 1 : -1, 0);
|
|
||||||
// chain.push(snake);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
const chain = getBestRoute(grid, snake)!;
|
|
||||||
chain.push(...getPathToPose(chain.slice(-1)[0], snake)!);
|
|
||||||
|
|
||||||
const drawOptions = {
|
|
||||||
sizeBorderRadius: 2,
|
|
||||||
sizeCell: 16,
|
|
||||||
sizeDot: 12,
|
|
||||||
colorBorder: "#1b1f230a",
|
|
||||||
colorDots: { 1: "#9be9a8", 2: "#40c463", 3: "#30a14e", 4: "#216e39" },
|
|
||||||
colorEmpty: "#ebedf0",
|
|
||||||
colorSnake: "purple",
|
|
||||||
};
|
|
||||||
|
|
||||||
const gifOptions = { frameDuration: 100, step: 1 };
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
for (
|
|
||||||
let length = 10;
|
|
||||||
length < chain.length;
|
|
||||||
length += Math.floor((chain.length - 10) / 3 / 10) * 10
|
|
||||||
) {
|
|
||||||
const stats: number[] = [];
|
|
||||||
|
|
||||||
let buffer: Buffer;
|
|
||||||
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);
|
|
||||||
stats.push(performance.now() - s);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
[
|
|
||||||
"---",
|
|
||||||
`grid dimension: ${grid.width}x${grid.height}`,
|
|
||||||
`chain length: ${length}`,
|
|
||||||
`resulting size: ${(buffer!.length / 1024).toFixed(1)}ko`,
|
|
||||||
`generation duration (mean): ${(
|
|
||||||
stats.reduce((s, x) => x + s) / stats.length
|
|
||||||
).toLocaleString(undefined, {
|
|
||||||
maximumFractionDigits: 0,
|
|
||||||
})}ms`,
|
|
||||||
"",
|
|
||||||
].join("\n"),
|
|
||||||
stats
|
|
||||||
);
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
`__tests__/__snapshots__/benchmark-output-${length}.gif`,
|
|
||||||
buffer!
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
@@ -1,71 +1,42 @@
|
|||||||
import * as fs from "fs";
|
|
||||||
import * as path from "path";
|
|
||||||
import { createGif } from "..";
|
import { createGif } from "..";
|
||||||
import * as grids from "@snk/types/__fixtures__/grid";
|
import { generateGrid } from "@snk/compute/generateGrid";
|
||||||
import { snake3 as snake } from "@snk/types/__fixtures__/snake";
|
import { computeBestRun } from "@snk/compute";
|
||||||
import { createSnakeFromCells, nextSnake } from "@snk/types/snake";
|
|
||||||
import { getBestRoute } from "@snk/solver/getBestRoute";
|
|
||||||
|
|
||||||
jest.setTimeout(20 * 1000);
|
|
||||||
|
|
||||||
const upscale = 1;
|
|
||||||
const drawOptions = {
|
const drawOptions = {
|
||||||
sizeBorderRadius: 2 * upscale,
|
sizeBorderRadius: 2,
|
||||||
sizeCell: 16 * upscale,
|
sizeCell: 16,
|
||||||
sizeDot: 12 * upscale,
|
sizeDot: 12,
|
||||||
colorBorder: "#1b1f230a",
|
colorBorder: "#1b1f230a",
|
||||||
colorDots: { 1: "#9be9a8", 2: "#40c463", 3: "#30a14e", 4: "#216e39" },
|
colorDots: { 1: "#9be9a8", 2: "#40c463", 3: "#30a14e", 4: "#216e39" },
|
||||||
colorEmpty: "#ebedf0",
|
colorEmpty: "#ebedf0",
|
||||||
colorSnake: "purple",
|
colorSnake: "purple",
|
||||||
};
|
};
|
||||||
|
|
||||||
const gifOptions = { frameDuration: 200, step: 1 };
|
const gameOptions = { maxSnakeLength: 5 };
|
||||||
|
|
||||||
const dir = path.resolve(__dirname, "__snapshots__");
|
const gifOptions = { delay: 200 };
|
||||||
|
|
||||||
try {
|
it("should generate gif", async () => {
|
||||||
fs.mkdirSync(dir);
|
const grid = generateGrid(14, 7, { colors: [1, 2, 3, 4], emptyP: 3 });
|
||||||
} catch (err) {}
|
|
||||||
|
|
||||||
for (const key of [
|
const snake = [
|
||||||
"empty",
|
{ x: 4, y: -1 },
|
||||||
"simple",
|
{ x: 3, y: -1 },
|
||||||
"corner",
|
{ x: 2, y: -1 },
|
||||||
"small",
|
{ x: 1, y: -1 },
|
||||||
"smallPacked",
|
{ x: 0, y: -1 },
|
||||||
] as const)
|
];
|
||||||
it(`should generate ${key} gif`, async () => {
|
|
||||||
const grid = grids[key];
|
|
||||||
|
|
||||||
const chain = [snake, ...getBestRoute(grid, snake)!];
|
const commands = computeBestRun(grid, snake, gameOptions).slice(0, 9);
|
||||||
|
|
||||||
const gif = await createGif(grid, chain, drawOptions, gifOptions);
|
const gif = await createGif(
|
||||||
|
grid,
|
||||||
expect(gif).toBeDefined();
|
snake,
|
||||||
|
commands,
|
||||||
fs.writeFileSync(path.resolve(dir, key + ".gif"), gif);
|
drawOptions,
|
||||||
});
|
gameOptions,
|
||||||
|
gifOptions
|
||||||
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();
|
expect(gif).toBeDefined();
|
||||||
|
|
||||||
fs.writeFileSync(path.resolve(dir, "swipper.gif"), gif);
|
|
||||||
});
|
});
|
||||||
|
|||||||
35
packages/gif-creator/__tests__/dev.ts
Normal file
35
packages/gif-creator/__tests__/dev.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { createGif } from "..";
|
||||||
|
import { generateGrid } from "@snk/compute/generateGrid";
|
||||||
|
import { computeBestRun } from "@snk/compute";
|
||||||
|
|
||||||
|
const drawOptions = {
|
||||||
|
sizeBorderRadius: 2,
|
||||||
|
sizeCell: 16,
|
||||||
|
sizeDot: 12,
|
||||||
|
colorBorder: "#1b1f230a",
|
||||||
|
colorDots: { 1: "#9be9a8", 2: "#40c463", 3: "#30a14e", 4: "#216e39" },
|
||||||
|
colorEmpty: "#ebedf0",
|
||||||
|
colorSnake: "purple",
|
||||||
|
};
|
||||||
|
|
||||||
|
const gameOptions = { maxSnakeLength: 5 };
|
||||||
|
|
||||||
|
const gifOptions = { delay: 20 };
|
||||||
|
|
||||||
|
const grid = generateGrid(42, 7, { colors: [1, 2, 3, 4], emptyP: 3 });
|
||||||
|
|
||||||
|
const snake = [
|
||||||
|
{ x: 4, y: -1 },
|
||||||
|
{ x: 3, y: -1 },
|
||||||
|
{ x: 2, y: -1 },
|
||||||
|
{ x: 1, y: -1 },
|
||||||
|
{ x: 0, y: -1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const commands = computeBestRun(grid, snake, gameOptions);
|
||||||
|
|
||||||
|
createGif(grid, snake, commands, drawOptions, gameOptions, gifOptions).then(
|
||||||
|
(buffer) => {
|
||||||
|
process.stdout.write(buffer);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,94 +1,92 @@
|
|||||||
import fs from "fs";
|
import * as fs from "fs";
|
||||||
import path from "path";
|
import * as path from "path";
|
||||||
import { execFileSync } from "child_process";
|
|
||||||
import { createCanvas } from "canvas";
|
import { createCanvas } from "canvas";
|
||||||
import { Grid, copyGrid, Color } from "@snk/types/grid";
|
import { Grid, copyGrid, Color } from "@snk/compute/grid";
|
||||||
import { Snake } from "@snk/types/snake";
|
import { Point } from "@snk/compute/point";
|
||||||
import {
|
import { copySnake } from "@snk/compute/snake";
|
||||||
Options,
|
import { drawWorld } from "@snk/draw/drawWorld";
|
||||||
drawLerpWorld,
|
import { step } from "@snk/compute/step";
|
||||||
getCanvasWorldSize,
|
import * as tmp from "tmp";
|
||||||
} from "@snk/draw/drawWorld";
|
|
||||||
import { step } from "@snk/solver/step";
|
|
||||||
import tmp from "tmp";
|
|
||||||
import gifsicle from "gifsicle";
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import GIFEncoder from "gif-encoder-2";
|
import * as execa from "execa";
|
||||||
|
|
||||||
|
export const createGif = async (
|
||||||
|
grid0: Grid,
|
||||||
|
snake0: Point[],
|
||||||
|
commands: Point[],
|
||||||
|
drawOptions: Parameters<typeof drawWorld>[4],
|
||||||
|
gameOptions: Parameters<typeof step>[4],
|
||||||
|
gifOptions: { delay: number }
|
||||||
|
) => {
|
||||||
|
const grid = copyGrid(grid0);
|
||||||
|
const snake = copySnake(snake0);
|
||||||
|
const stack: Color[] = [];
|
||||||
|
|
||||||
|
const width = drawOptions.sizeCell * (grid.width + 4);
|
||||||
|
const height = drawOptions.sizeCell * (grid.height + 4) + 100;
|
||||||
|
|
||||||
const withTmpDir = async <T>(
|
|
||||||
handler: (dir: string) => Promise<T>
|
|
||||||
): Promise<T> => {
|
|
||||||
const { name: dir, removeCallback: cleanUp } = tmp.dirSync({
|
const { name: dir, removeCallback: cleanUp } = tmp.dirSync({
|
||||||
unsafeCleanup: true,
|
unsafeCleanup: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const canvas = createCanvas(width, height);
|
||||||
|
const ctx = canvas.getContext("2d")!;
|
||||||
|
|
||||||
|
const writeImage = (i: number) => {
|
||||||
|
ctx.clearRect(0, 0, 99999, 99999);
|
||||||
|
ctx.fillStyle = "#fff";
|
||||||
|
ctx.fillRect(0, 0, 99999, 99999);
|
||||||
|
drawWorld(ctx, grid, snake, stack, drawOptions);
|
||||||
|
|
||||||
|
const buffer = canvas.toBuffer("image/png", {
|
||||||
|
compressionLevel: 0,
|
||||||
|
filters: canvas.PNG_FILTER_NONE,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileName = path.join(dir, `${i.toString().padStart(4, "0")}.png`);
|
||||||
|
|
||||||
|
fs.writeFileSync(fileName, buffer);
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await handler(dir);
|
writeImage(0);
|
||||||
} finally {
|
|
||||||
cleanUp();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createGif = async (
|
for (let i = 0; i < commands.length; i++) {
|
||||||
grid0: Grid,
|
step(grid, snake, stack, commands[i], gameOptions);
|
||||||
chain: Snake[],
|
writeImage(i + 1);
|
||||||
drawOptions: Options,
|
|
||||||
gifOptions: { frameDuration: number; step: number }
|
|
||||||
) =>
|
|
||||||
withTmpDir(async (dir) => {
|
|
||||||
const { width, height } = getCanvasWorldSize(grid0, drawOptions);
|
|
||||||
|
|
||||||
const canvas = createCanvas(width, height);
|
|
||||||
const ctx = canvas.getContext("2d")!;
|
|
||||||
|
|
||||||
const grid = copyGrid(grid0);
|
|
||||||
const stack: Color[] = [];
|
|
||||||
|
|
||||||
const encoder = new GIFEncoder(width, height, "neuquant", true);
|
|
||||||
encoder.setRepeat(0);
|
|
||||||
encoder.setDelay(gifOptions.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 < gifOptions.step; k++) {
|
|
||||||
ctx.clearRect(0, 0, width, height);
|
|
||||||
ctx.fillStyle = "#fff";
|
|
||||||
ctx.fillRect(0, 0, width, height);
|
|
||||||
drawLerpWorld(
|
|
||||||
ctx,
|
|
||||||
grid,
|
|
||||||
snake0,
|
|
||||||
snake1,
|
|
||||||
stack,
|
|
||||||
k / gifOptions.step,
|
|
||||||
drawOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
encoder.addFrame(ctx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const outFileName = path.join(dir, "out.gif");
|
const outFileName = path.join(dir, "out.gif");
|
||||||
const optimizedFileName = path.join(dir, "out.optimized.gif");
|
const optimizedFileName = path.join(dir, "out.optimized.gif");
|
||||||
|
|
||||||
encoder.finish();
|
await execa(
|
||||||
fs.writeFileSync(outFileName, encoder.out.getData());
|
"gm",
|
||||||
|
[
|
||||||
|
"convert",
|
||||||
|
["-loop", "0"],
|
||||||
|
["-delay", gifOptions.delay.toString()],
|
||||||
|
["-dispose", "2"],
|
||||||
|
// ["-layers", "OptimizeFrame"],
|
||||||
|
["-compress", "LZW"],
|
||||||
|
["-strip"],
|
||||||
|
|
||||||
execFileSync(
|
path.join(dir, "*.png"),
|
||||||
gifsicle,
|
outFileName,
|
||||||
|
].flat()
|
||||||
|
);
|
||||||
|
|
||||||
|
await execa(
|
||||||
|
"gifsicle",
|
||||||
[
|
[
|
||||||
//
|
//
|
||||||
"--optimize=3",
|
"--optimize=3",
|
||||||
"--color-method=diversity",
|
|
||||||
"--colors=18",
|
|
||||||
outFileName,
|
outFileName,
|
||||||
["--output", optimizedFileName],
|
["--output", optimizedFileName],
|
||||||
].flat()
|
].flat()
|
||||||
);
|
);
|
||||||
|
|
||||||
return fs.readFileSync(optimizedFileName);
|
return fs.readFileSync(optimizedFileName);
|
||||||
});
|
} finally {
|
||||||
|
cleanUp();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,19 +2,18 @@
|
|||||||
"name": "@snk/gif-creator",
|
"name": "@snk/gif-creator",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@snk/compute": "1.0.0",
|
||||||
"@snk/draw": "1.0.0",
|
"@snk/draw": "1.0.0",
|
||||||
"@snk/solver": "1.0.0",
|
"canvas": "2.6.1",
|
||||||
"canvas": "2.9.1",
|
"execa": "4.0.3",
|
||||||
"gif-encoder-2": "1.0.5",
|
|
||||||
"gifsicle": "5.3.0",
|
|
||||||
"tmp": "0.2.1"
|
"tmp": "0.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/gifsicle": "5.2.0",
|
"@types/execa": "2.0.0",
|
||||||
"@types/tmp": "0.2.3",
|
"@types/tmp": "0.2.0",
|
||||||
"@vercel/ncc": "0.24.1"
|
"@zeit/ncc": "0.22.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"benchmark": "ncc run __tests__/benchmark.ts --quiet"
|
"dev": "ncc run __tests__/dev.ts --quiet | tail -n +2 > out.gif "
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# @snk/github-user-contribution-service
|
|
||||||
|
|
||||||
Expose github-user-contribution as an endpoint, using vercel.sh
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@snk/github-user-contribution-service",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@snk/github-user-contribution": "1.0.0",
|
|
||||||
"@vercel/node": "1.14.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"github": {
|
|
||||||
"silent": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# @snk/github-user-contribution
|
|
||||||
|
|
||||||
Get the github user contribution graph
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```js
|
|
||||||
const { cells, colorScheme } = await getGithubUserContribution("platane");
|
|
||||||
|
|
||||||
// colorScheme = [
|
|
||||||
// "#ebedf0",
|
|
||||||
// "#9be9a8",
|
|
||||||
// ...
|
|
||||||
// ]
|
|
||||||
// cells = [
|
|
||||||
// {
|
|
||||||
// x: 3,
|
|
||||||
// y: 0,
|
|
||||||
// count: 3,
|
|
||||||
// color: '#ebedf0',
|
|
||||||
// date:'2019-01-18'
|
|
||||||
// },
|
|
||||||
// ...
|
|
||||||
// ]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation
|
|
||||||
|
|
||||||
Based on the html page. Which is very unstable. We might switch to using github api but afaik it's a bit complex.
|
|
||||||
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,14 @@
|
|||||||
import { getGithubUserContribution } from "..";
|
import { getGithubUserContribution } from "..";
|
||||||
|
|
||||||
describe("getGithubUserContribution", () => {
|
it("should get user contribution", async () => {
|
||||||
const promise = getGithubUserContribution("platane");
|
const { cells, colorScheme } = await getGithubUserContribution("platane");
|
||||||
|
|
||||||
it("should resolve", async () => {
|
expect(cells).toBeDefined();
|
||||||
await promise;
|
expect(colorScheme).toEqual([
|
||||||
});
|
"#ebedf0",
|
||||||
|
"#9be9a8",
|
||||||
it("should get colorScheme", async () => {
|
"#40c463",
|
||||||
const { colorScheme } = await promise;
|
"#30a14e",
|
||||||
|
"#216e39",
|
||||||
expect(colorScheme).toEqual([
|
]);
|
||||||
"#ebedf0",
|
|
||||||
"#9be9a8",
|
|
||||||
"#40c463",
|
|
||||||
"#30a14e",
|
|
||||||
"#216e39",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should get around 365 cells", async () => {
|
|
||||||
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;
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
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("-");
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user