Compare commits

...

33 Commits
v3.0.0 ... main

Author SHA1 Message Date
platane
83033510f0 add test case for color optiion 2025-02-21 08:37:20 +07:00
release bot
a69d1dbca7 📦 3.3.0 2025-02-20 17:07:41 +00:00
platane
f2057e5efe disable cloudflare logging 2025-02-20 23:52:24 +07:00
platane
7fbc58b61d use cloudflare endpoint 2025-02-20 23:51:16 +07:00
platane
1f7630d984 👷 cloudflare deploy ci 2025-02-20 23:48:16 +07:00
platane
852f0ae376 deploy github user contribution endpoint to cloudflare 2025-02-20 23:43:25 +07:00
platane
10c4c3c7bd 🔧 fix manual run 2025-02-20 20:07:36 +07:00
platane
ace186c41f ⬆️ update prettier 2025-02-20 19:58:59 +07:00
platane
79c252356c ⬆️ update typescript 2025-02-20 19:57:25 +07:00
platane
3c171061b3 ⬆️ update tooling dependencies 2025-02-20 19:50:24 +07:00
platane
4783e68ce7 ⬆️ update tooling dependencies 2025-02-20 19:41:00 +07:00
Platane
85da3901f5 use bun as package manager and runner for the docke rcontainer, plus some tweak on the github action 2025-02-20 19:34:18 +07:00
platane
74bc4f0651 ⬆️ update canvas 2025-02-20 18:35:26 +07:00
platane
e55fe1f13c ⬆️ bump dependencies 2024-07-06 11:48:21 +02:00
platane
876448a004 🔨 fix cors issue (2) 2024-07-06 11:47:56 +02:00
platane
d35dc83cf2 🔨 fix cors issue 2024-07-06 11:38:34 +02:00
platane
bb7d69dde8 vercel cache 2024-02-20 16:33:48 +01:00
platane
14a003db51 cache the request from vercel for 6h 2024-02-20 16:28:37 +01:00
platane
2479713155 🚑 2024-02-20 16:23:46 +01:00
platane
debec31440 ⬆️ bump dependencies 2024-02-20 16:15:42 +01:00
platane
5332254423 👷 add manual run 2023-10-19 19:23:20 +02:00
platane
a9052b7ca2 📓 2023-10-17 17:45:17 +02:00
release bot
8b7b3e6ace 📦 3.2.0 2023-10-17 15:44:23 +00:00
platane
92f4de3970 use process.env. instead of @action/core in test and local 2023-10-17 17:30:48 +02:00
Awayume
c9644d3dfa use input instead of env to receive github token
Co-authored-by: Platane <me@platane.me>
2023-10-17 17:26:17 +02:00
platane
01fa6d7aac 🚑 vix vercel endpoint 2023-10-06 10:38:48 +02:00
release bot
b58af55b7d 📦 3.1.0 2023-09-23 18:22:23 +00:00
platane
4e5805f8af ⬆️ use node 20 2023-09-23 20:18:51 +02:00
platane
743771147d ⬆️ 2023-09-23 20:07:23 +02:00
Platane
8eddcbdbea 📓 2023-09-13 20:59:51 +02:00
Alfi Maulana
6f0ace6560 docs: fix indentation of GITHUB_TOKEN env in the README's usage section 2023-07-20 14:02:52 +02:00
platane
835fdd6b84 🚑 fix vercel function 2023-07-17 23:14:15 +02:00
platane
e6034f3972 📓 update readme 2023-07-17 23:04:14 +02:00
74 changed files with 29538 additions and 17553 deletions

View File

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

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

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

View File

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

5
.gitignore vendored
View File

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

2
.nvmrc
View File

@@ -1 +1 @@
16
20

View File

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

View File

@@ -54,15 +54,11 @@ Available as github action. It can automatically generate a new image each day.
dist/github-snake.svg
dist/github-snake-dark.svg?palette=github-dark
dist/ocean.gif?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9
env:
# a github token is required to fetch the contribution calendar from github API
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
[example with cron job](https://github.com/Platane/Platane/blob/master/.github/workflows/main.yml#L24-L29)
[example with cron job](https://github.com/Platane/Platane/blob/master/.github/workflows/main.yml#L26-L33)
If you are only interested in generating a svg, consider using this faster action: `uses: Platane/snk/svg-only@v2`
If you are only interested in generating a svg, consider using this faster action: `uses: Platane/snk/svg-only@v3`
**dark mode**

View File

@@ -4,15 +4,18 @@ author: "platane"
runs:
using: docker
image: docker://platane/snk@sha256:753878055e52fbbaf3148fdac4590e396f97581f1dc4c1f861701add7a1dc1b5
image: docker://platane/snk@sha256:96390294299275740e5963058c9784c60c5393b3b8b16082dcf41b240db791f9
inputs:
github_user_name:
description: "github user name"
required: true
github_token:
description: "github token used to fetch the contribution calendar. Default to the action token if empty."
required: false
default: ${{ github.token }}
outputs:
required: false
default: null
description: |
list of files to generate.
one file per line. Each output can be customized with options as query string.

1489
bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,11 +1,8 @@
import * as fs from "fs";
import * as path from "path";
import { it, expect } from "bun:test";
import { generateContributionSnake } from "../generateContributionSnake";
import { parseOutputsOption } from "../outputsOptions";
import { config } from "dotenv";
config({ path: __dirname + "/../../../.env" });
jest.setTimeout(2 * 60 * 1000);
const silent = (handler: () => void | Promise<void>) => async () => {
const originalConsoleLog = console.log;
@@ -43,5 +40,6 @@ it(
fs.writeFileSync(outputs[0]!.filename, results[0]!);
fs.writeFileSync(outputs[1]!.filename, results[1]!);
fs.writeFileSync(outputs[2]!.filename, results[2]!);
})
}),
{ timeout: 2 * 60 * 1000 },
);

View File

@@ -1,43 +1,44 @@
import { parseEntry } from "../outputsOptions";
import { it, expect } from "bun:test";
it("should parse options as json", () => {
expect(
parseEntry(`/out.svg {"color_snake":"yellow"}`)?.drawOptions
parseEntry(`/out.svg {"color_snake":"yellow"}`)?.drawOptions,
).toHaveProperty("colorSnake", "yellow");
expect(
parseEntry(`/out.svg?{"color_snake":"yellow"}`)?.drawOptions
parseEntry(`/out.svg?{"color_snake":"yellow"}`)?.drawOptions,
).toHaveProperty("colorSnake", "yellow");
expect(
parseEntry(`/out.svg?{"color_dots":["#000","#111","#222","#333","#444"]}`)
?.drawOptions.colorDots
?.drawOptions.colorDots,
).toEqual(["#000", "#111", "#222", "#333", "#444"]);
});
it("should parse options as searchparams", () => {
expect(parseEntry(`/out.svg?color_snake=yellow`)?.drawOptions).toHaveProperty(
"colorSnake",
"yellow"
"yellow",
);
expect(
parseEntry(`/out.svg?color_dots=#000,#111,#222,#333,#444`)?.drawOptions
.colorDots
.colorDots,
).toEqual(["#000", "#111", "#222", "#333", "#444"]);
});
it("should parse filename", () => {
expect(parseEntry(`/a/b/c.svg?{"color_snake":"yellow"}`)).toHaveProperty(
"filename",
"/a/b/c.svg"
"/a/b/c.svg",
);
expect(
parseEntry(`/a/b/out.svg?.gif.svg?{"color_snake":"yellow"}`)
parseEntry(`/a/b/out.svg?.gif.svg?{"color_snake":"yellow"}`),
).toHaveProperty("filename", "/a/b/out.svg?.gif.svg");
expect(
parseEntry(`/a/b/{[-1].svg?.gif.svg?{"color_snake":"yellow"}`)
parseEntry(`/a/b/{[-1].svg?.gif.svg?{"color_snake":"yellow"}`),
).toHaveProperty("filename", "/a/b/{[-1].svg?.gif.svg");
});
@@ -56,5 +57,5 @@ it("should parse filename", () => {
].forEach((entry) =>
it(`should parse ${entry}`, () => {
expect(parseEntry(entry)).toMatchSnapshot();
})
}),
);

View File

@@ -13,7 +13,7 @@ export const generateContributionSnake = async (
drawOptions: DrawOptions;
animationOptions: AnimationOptions;
} | null)[],
options: { githubToken: string }
options: { githubToken: string },
) => {
console.log("🎣 fetching github user contribution");
const cells = await getGithubUserContribution(userName, options);
@@ -43,10 +43,10 @@ export const generateContributionSnake = async (
cells,
chain,
drawOptions,
animationOptions
animationOptions,
);
}
}
})
}),
);
};

View File

@@ -10,9 +10,10 @@ import { parseOutputsOption } from "./outputsOptions";
core.getMultilineInput("outputs") ?? [
core.getInput("gif_out_path"),
core.getInput("svg_out_path"),
]
],
);
const githubToken = process.env.GITHUB_TOKEN!;
const githubToken =
process.env.GITHUB_TOKEN ?? core.getInput("github_token");
const { generateContributionSnake } = await import(
"./generateContributionSnake"

View File

@@ -2,7 +2,7 @@
"name": "@snk/action",
"version": "1.0.0",
"dependencies": {
"@actions/core": "1.10.0",
"@actions/core": "1.11.1",
"@snk/gif-creator": "1.0.0",
"@snk/github-user-contribution": "1.0.0",
"@snk/solver": "1.0.0",
@@ -10,11 +10,9 @@
"@snk/types": "1.0.0"
},
"devDependencies": {
"@vercel/ncc": "0.36.1",
"dotenv": "16.3.1"
"@vercel/ncc": "0.38.3"
},
"scripts": {
"build": "ncc build --external canvas --external gifsicle --out dist ./index.ts",
"run:build": "INPUT_GITHUB_USER_NAME=platane INPUT_OUTPUTS='dist/out.svg' node dist/index.js"
"build": "ncc build --external canvas --external gifsicle --out dist ./index.ts"
}
}

View File

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

View File

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

View File

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

View File

@@ -253,7 +253,7 @@ const createViewer = ({
: "") +
`<a href="${svgLink.href}" download="github-user-contribution.svg">` +
svgString +
"<a/>"
"<a/>",
);
e.preventDefault();
});
@@ -277,7 +277,7 @@ const createViewer = ({
const onSubmit = async (userName: string) => {
const res = await fetch(
process.env.GITHUB_USER_CONTRIBUTION_API_ENDPOINT + userName
process.env.GITHUB_USER_CONTRIBUTION_API_ENDPOINT + userName,
);
const cells = (await res.json()) as Res;
@@ -294,8 +294,8 @@ const worker = new Worker(
new URL(
"./demo.interactive.worker.ts",
// @ts-ignore
import.meta.url
)
import.meta.url,
),
);
const { getChain } = createRpcClient<WorkerAPI>(worker);

View File

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

View File

@@ -10,14 +10,15 @@
"@snk/types": "1.0.0"
},
"devDependencies": {
"@types/dat.gui": "0.7.10",
"@types/dat.gui": "0.7.13",
"dat.gui": "0.7.9",
"html-webpack-plugin": "5.5.3",
"ts-loader": "9.4.4",
"ts-node": "10.9.1",
"webpack": "5.88.1",
"webpack-cli": "5.1.4",
"webpack-dev-server": "4.15.1"
"dotenv": "16.4.7",
"html-webpack-plugin": "5.6.3",
"ts-loader": "9.5.2",
"ts-node": "10.9.2",
"webpack": "5.98.0",
"webpack-cli": "6.0.1",
"webpack-dev-server": "5.2.0"
},
"scripts": {
"build": "webpack",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,20 +0,0 @@
import { getGithubUserContribution } from "@snk/github-user-contribution";
import { VercelRequest, VercelResponse } from "@vercel/node";
export default async (req: VercelRequest, res: VercelResponse) => {
const { userName } = req.query;
try {
res.setHeader("Access-Control-Allow-Origin", "https://platane.github.io");
res.statusCode = 200;
res.json(
await getGithubUserContribution(userName as string, {
githubToken: process.env.GITHUB!,
})
);
} catch (err) {
console.error(err);
res.statusCode = 500;
res.end();
}
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import { getGithubUserContribution } from "..";
import { config } from "dotenv";
config({ path: __dirname + "/../../../.env" });
import { describe, it, expect } from "bun:test";
describe("getGithubUserContribution", () => {
const promise = getGithubUserContribution("platane", {
@@ -8,11 +7,6 @@ describe("getGithubUserContribution", () => {
});
it("should resolve", async () => {
console.log(
"process.env.GITHUB_TOKEN",
process.env.GITHUB_TOKEN?.replace(/\d/g, "x")
);
await promise;
});

View File

@@ -1,5 +1,3 @@
import fetch from "node-fetch";
/**
* get the contribution grid from a github user page
*
@@ -18,7 +16,7 @@ import fetch from "node-fetch";
*/
export const getGithubUserContribution = async (
userName: string,
o: { githubToken: string }
o: { githubToken: string },
) => {
const query = /* GraphQL */ `
query ($login: String!) {
@@ -44,12 +42,13 @@ export const getGithubUserContribution = async (
headers: {
Authorization: `bearer ${o.githubToken}`,
"Content-Type": "application/json",
"User-Agent": "me@platane.me",
},
method: "POST",
body: JSON.stringify({ variables, query }),
});
if (!res.ok) throw new Error(res.statusText);
if (!res.ok) throw new Error(await res.text().catch(() => res.statusText));
const { data, errors } = (await res.json()) as {
data: GraphQLRes;
@@ -71,7 +70,7 @@ export const getGithubUserContribution = async (
(d.contributionLevel === "SECOND_QUARTILE" && 2) ||
(d.contributionLevel === "FIRST_QUARTILE" && 1) ||
0,
}))
})),
);
};

View File

@@ -1,11 +1,4 @@
{
"name": "@snk/github-user-contribution",
"version": "1.0.0",
"dependencies": {
"node-fetch": "2.6.12"
},
"devDependencies": {
"@types/node-fetch": "2.6.4",
"dotenv": "16.3.1"
}
"version": "1.0.0"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,16 +3,19 @@ description: "Generates a snake game from a github user contributions grid. Outp
author: "platane"
runs:
using: node16
using: node20
main: dist/index.js
inputs:
github_user_name:
description: "github user name"
required: true
github_token:
description: "github token used to fetch the contribution calendar. Default to the action token if empty."
required: false
default: ${{ github.token }}
outputs:
required: false
default: null
description: |
list of files to generate.
one file per line. Each output can be customized with options as query string.

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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,24 +1,18 @@
"use strict";
exports.id = 407;
exports.ids = [407];
exports.id = 324;
exports.ids = [324];
exports.modules = {
/***/ 407:
/***/ 324:
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
// ESM COMPAT FLAG
__webpack_require__.r(__webpack_exports__);
// EXPORTS
__webpack_require__.d(__webpack_exports__, {
"generateContributionSnake": () => (/* binding */ generateContributionSnake)
generateContributionSnake: () => (/* binding */ generateContributionSnake)
});
// EXTERNAL MODULE: ../../node_modules/node-fetch/lib/index.js
var lib = __webpack_require__(2197);
var lib_default = /*#__PURE__*/__webpack_require__.n(lib);
;// CONCATENATED MODULE: ../github-user-contribution/index.ts
/**
* get the contribution grid from a github user page
*
@@ -55,16 +49,17 @@ const getGithubUserContribution = async (userName, o) => {
}
`;
const variables = { login: userName };
const res = await lib_default()("https://api.github.com/graphql", {
const res = await fetch("https://api.github.com/graphql", {
headers: {
Authorization: `bearer ${o.githubToken}`,
"Content-Type": "application/json",
"User-Agent": "me@platane.me",
},
method: "POST",
body: JSON.stringify({ variables, query }),
});
if (!res.ok)
throw new Error(res.statusText);
throw new Error(await res.text().catch(() => res.statusText));
const { data, errors } = (await res.json());
if (errors?.[0])
throw errors[0];
@@ -82,18 +77,18 @@ const getGithubUserContribution = async (userName, o) => {
};
// EXTERNAL MODULE: ../types/grid.ts
var types_grid = __webpack_require__(2881);
var types_grid = __webpack_require__(105);
;// CONCATENATED MODULE: ./userContributionToGrid.ts
const userContributionToGrid = (cells) => {
const width = Math.max(0, ...cells.map((c) => c.x)) + 1;
const height = Math.max(0, ...cells.map((c) => c.y)) + 1;
const grid = (0,types_grid/* createEmptyGrid */.u1)(width, height);
const grid = (0,types_grid/* createEmptyGrid */.Kb)(width, height);
for (const c of cells) {
if (c.level > 0)
(0,types_grid/* setColor */.vk)(grid, c.x, c.y, c.level);
(0,types_grid/* setColor */.wW)(grid, c.x, c.y, c.level);
else
(0,types_grid/* setColorEmpty */.Dy)(grid, c.x, c.y);
(0,types_grid/* setColorEmpty */.l$)(grid, c.x, c.y);
}
return grid;
};
@@ -111,10 +106,10 @@ const pointEquals = (a, b) => a.x === b.x && a.y === b.y;
const createOutside = (grid, color = 0) => {
const outside = (0,types_grid/* createEmptyGrid */.u1)(grid.width, grid.height);
const outside = (0,types_grid/* createEmptyGrid */.Kb)(grid.width, grid.height);
for (let x = outside.width; x--;)
for (let y = outside.height; y--;)
(0,types_grid/* setColor */.vk)(outside, x, y, 1);
(0,types_grid/* setColor */.wW)(outside, x, y, 1);
fillOutside(outside, grid, color);
return outside;
};
@@ -124,19 +119,19 @@ const fillOutside = (outside, grid, color = 0) => {
changed = false;
for (let x = outside.width; x--;)
for (let y = outside.height; y--;)
if ((0,types_grid/* getColor */.Lq)(grid, x, y) <= color &&
if ((0,types_grid/* getColor */.oU)(grid, x, y) <= color &&
!isOutside(outside, x, y) &&
around4.some((a) => isOutside(outside, x + a.x, y + a.y))) {
changed = true;
(0,types_grid/* setColorEmpty */.Dy)(outside, x, y);
(0,types_grid/* setColorEmpty */.l$)(outside, x, y);
}
}
return outside;
};
const isOutside = (outside, x, y) => !(0,types_grid/* isInside */.V0)(outside, x, y) || (0,types_grid/* isEmpty */.xb)((0,types_grid/* getColor */.Lq)(outside, x, y));
const isOutside = (outside, x, y) => !(0,types_grid/* isInside */.FK)(outside, x, y) || (0,types_grid/* isEmpty */.Im)((0,types_grid/* getColor */.oU)(outside, x, y));
// EXTERNAL MODULE: ../types/snake.ts
var types_snake = __webpack_require__(9347);
var types_snake = __webpack_require__(777);
;// CONCATENATED MODULE: ../solver/utils/sortPush.ts
const sortPush = (arr, x, sortFn) => {
let a = 0;
@@ -169,9 +164,9 @@ const getTunnelPath = (snake0, tunnel) => {
const chain = [];
let snake = snake0;
for (let i = 1; i < tunnel.length; i++) {
const dx = tunnel[i].x - (0,types_snake/* getHeadX */.If)(snake);
const dy = tunnel[i].y - (0,types_snake/* getHeadY */.IP)(snake);
snake = (0,types_snake/* nextSnake */.kv)(snake, dx, dy);
const dx = tunnel[i].x - (0,types_snake/* getHeadX */.tN)(snake);
const dy = tunnel[i].y - (0,types_snake/* getHeadY */.Ap)(snake);
snake = (0,types_snake/* nextSnake */.Sc)(snake, dx, dy);
chain.unshift(snake);
}
return chain;
@@ -199,7 +194,7 @@ const updateTunnel = (grid, tunnel, toDelete) => {
break;
}
};
const isEmptySafe = (grid, x, y) => !(0,types_grid/* isInside */.V0)(grid, x, y) || (0,types_grid/* isEmpty */.xb)((0,types_grid/* getColor */.Lq)(grid, x, y));
const isEmptySafe = (grid, x, y) => !(0,types_grid/* isInside */.FK)(grid, x, y) || (0,types_grid/* isEmpty */.Im)((0,types_grid/* getColor */.oU)(grid, x, y));
/**
* remove empty cell from start
*/
@@ -234,14 +229,14 @@ const trimTunnelEnd = (grid, tunnel) => {
const getColorSafe = (grid, x, y) => (0,types_grid/* isInside */.V0)(grid, x, y) ? (0,types_grid/* getColor */.Lq)(grid, x, y) : 0;
const getColorSafe = (grid, x, y) => (0,types_grid/* isInside */.FK)(grid, x, y) ? (0,types_grid/* getColor */.oU)(grid, x, y) : 0;
const setEmptySafe = (grid, x, y) => {
if ((0,types_grid/* isInside */.V0)(grid, x, y))
(0,types_grid/* setColorEmpty */.Dy)(grid, x, y);
if ((0,types_grid/* isInside */.FK)(grid, x, y))
(0,types_grid/* setColorEmpty */.l$)(grid, x, y);
};
const unwrap = (m) => !m
? []
: [...unwrap(m.parent), { x: (0,types_snake/* getHeadX */.If)(m.snake), y: (0,types_snake/* getHeadY */.IP)(m.snake) }];
: [...unwrap(m.parent), { x: (0,types_snake/* getHeadX */.tN)(m.snake), y: (0,types_snake/* getHeadY */.Ap)(m.snake) }];
/**
* returns the path to reach the outside which contains the least color cell
*/
@@ -250,15 +245,15 @@ const getSnakeEscapePath = (grid, outside, snake0, color) => {
const closeList = [];
while (openList[0]) {
const o = openList.shift();
const x = (0,types_snake/* getHeadX */.If)(o.snake);
const y = (0,types_snake/* getHeadY */.IP)(o.snake);
const x = (0,types_snake/* getHeadX */.tN)(o.snake);
const y = (0,types_snake/* getHeadY */.Ap)(o.snake);
if (isOutside(outside, x, y))
return unwrap(o);
for (const a of around4) {
const c = getColorSafe(grid, x + a.x, y + a.y);
if (c <= color && !(0,types_snake/* snakeWillSelfCollide */.nJ)(o.snake, a.x, a.y)) {
const snake = (0,types_snake/* nextSnake */.kv)(o.snake, a.x, a.y);
if (!closeList.some((s0) => (0,types_snake/* snakeEquals */.kE)(s0, snake))) {
if (c <= color && !(0,types_snake/* snakeWillSelfCollide */.J)(o.snake, a.x, a.y)) {
const snake = (0,types_snake/* nextSnake */.Sc)(o.snake, a.x, a.y);
if (!closeList.some((s0) => (0,types_snake/* snakeEquals */.sW)(s0, snake))) {
const w = o.w + 1 + +(c === color) * 1000;
sortPush(openList, { snake, w, parent: o }, (a, b) => a.w - b.w);
closeList.push(snake);
@@ -275,7 +270,7 @@ const getSnakeEscapePath = (grid, outside, snake0, color) => {
*/
const getBestTunnel = (grid, outside, x, y, color, snakeN) => {
const c = { x, y };
const snake0 = (0,types_snake/* createSnakeFromCells */.xG)(Array.from({ length: snakeN }, () => c));
const snake0 = (0,types_snake/* createSnakeFromCells */.yS)(Array.from({ length: snakeN }, () => c));
const one = getSnakeEscapePath(grid, outside, snake0, color);
if (!one)
return null;
@@ -283,9 +278,9 @@ const getBestTunnel = (grid, outside, x, y, color, snakeN) => {
const snakeICells = one.slice(0, snakeN);
while (snakeICells.length < snakeN)
snakeICells.push(snakeICells[snakeICells.length - 1]);
const snakeI = (0,types_snake/* createSnakeFromCells */.xG)(snakeICells);
const snakeI = (0,types_snake/* createSnakeFromCells */.yS)(snakeICells);
// remove from the grid the colors that one eat
const gridI = (0,types_grid/* copyGrid */.VJ)(grid);
const gridI = (0,types_grid/* copyGrid */.mi)(grid);
for (const { x, y } of one)
setEmptySafe(gridI, x, y);
const two = getSnakeEscapePath(gridI, outside, snakeI, color);
@@ -313,15 +308,15 @@ const getPathTo = (grid, snake0, x, y) => {
const closeList = [];
while (openList.length) {
const c = openList.shift();
const cx = (0,types_snake/* getHeadX */.If)(c.snake);
const cy = (0,types_snake/* getHeadY */.IP)(c.snake);
const cx = (0,types_snake/* getHeadX */.tN)(c.snake);
const cy = (0,types_snake/* getHeadY */.Ap)(c.snake);
for (let i = 0; i < around4.length; i++) {
const { x: dx, y: dy } = around4[i];
const nx = cx + dx;
const ny = cy + dy;
if (nx === x && ny === y) {
// unwrap
const path = [(0,types_snake/* nextSnake */.kv)(c.snake, dx, dy)];
const path = [(0,types_snake/* nextSnake */.Sc)(c.snake, dx, dy)];
let e = c;
while (e.parent) {
path.push(e.snake);
@@ -329,11 +324,11 @@ const getPathTo = (grid, snake0, x, y) => {
}
return path;
}
if ((0,types_grid/* isInsideLarge */.HJ)(grid, 2, nx, ny) &&
!(0,types_snake/* snakeWillSelfCollide */.nJ)(c.snake, dx, dy) &&
(!(0,types_grid/* isInside */.V0)(grid, nx, ny) || (0,types_grid/* isEmpty */.xb)((0,types_grid/* getColor */.Lq)(grid, nx, ny)))) {
const nsnake = (0,types_snake/* nextSnake */.kv)(c.snake, dx, dy);
if (!closeList.some((s) => (0,types_snake/* snakeEquals */.kE)(nsnake, s))) {
if ((0,types_grid/* isInsideLarge */.Yd)(grid, 2, nx, ny) &&
!(0,types_snake/* snakeWillSelfCollide */.J)(c.snake, dx, dy) &&
(!(0,types_grid/* isInside */.FK)(grid, nx, ny) || (0,types_grid/* isEmpty */.Im)((0,types_grid/* getColor */.oU)(grid, nx, ny)))) {
const nsnake = (0,types_snake/* nextSnake */.Sc)(c.snake, dx, dy);
if (!closeList.some((s) => (0,types_snake/* snakeEquals */.sW)(nsnake, s))) {
const w = c.w + 1;
const h = Math.abs(nx - x) + Math.abs(ny - y);
const f = w + h;
@@ -354,7 +349,7 @@ const getPathTo = (grid, snake0, x, y) => {
const clearResidualColoredLayer = (grid, outside, snake0, color) => {
const snakeN = (0,types_snake/* getSnakeLength */.JJ)(snake0);
const snakeN = (0,types_snake/* getSnakeLength */.T$)(snake0);
const tunnels = getTunnellablePoints(grid, outside, snakeN, color);
// sort
tunnels.sort((a, b) => b.priority - a.priority);
@@ -373,7 +368,7 @@ const clearResidualColoredLayer = (grid, outside, snake0, color) => {
fillOutside(outside, grid);
// update tunnels
for (let i = tunnels.length; i--;)
if ((0,types_grid/* isEmpty */.xb)((0,types_grid/* getColor */.Lq)(grid, tunnels[i].x, tunnels[i].y)))
if ((0,types_grid/* isEmpty */.Im)((0,types_grid/* getColor */.oU)(grid, tunnels[i].x, tunnels[i].y)))
tunnels.splice(i, 1);
else {
const t = tunnels[i];
@@ -394,8 +389,8 @@ const clearResidualColoredLayer = (grid, outside, snake0, color) => {
const getNextTunnel = (ts, snake) => {
let minDistance = Infinity;
let closestTunnel = null;
const x = (0,types_snake/* getHeadX */.If)(snake);
const y = (0,types_snake/* getHeadY */.IP)(snake);
const x = (0,types_snake/* getHeadX */.tN)(snake);
const y = (0,types_snake/* getHeadY */.Ap)(snake);
const priority = ts[0].priority;
for (let i = 0; ts[i] && ts[i].priority === priority; i++) {
const t = ts[i].tunnel;
@@ -414,8 +409,8 @@ const getTunnellablePoints = (grid, outside, snakeN, color) => {
const points = [];
for (let x = grid.width; x--;)
for (let y = grid.height; y--;) {
const c = (0,types_grid/* getColor */.Lq)(grid, x, y);
if (!(0,types_grid/* isEmpty */.xb)(c) && c < color) {
const c = (0,types_grid/* getColor */.oU)(grid, x, y);
if (!(0,types_grid/* isEmpty */.Im)(c) && c < color) {
const tunnel = getBestTunnel(grid, outside, x, y, color, snakeN);
if (tunnel) {
const priority = getPriority(grid, color, tunnel);
@@ -436,7 +431,7 @@ const getPriority = (grid, color, tunnel) => {
for (let i = 0; i < tunnel.length; i++) {
const { x, y } = tunnel[i];
const c = clearResidualColoredLayer_getColorSafe(grid, x, y);
if (!(0,types_grid/* isEmpty */.xb)(c) && i === tunnel.findIndex((p) => p.x === x && p.y === y)) {
if (!(0,types_grid/* isEmpty */.Im)(c) && i === tunnel.findIndex((p) => p.x === x && p.y === y)) {
if (c === color)
nColor += 1;
else
@@ -448,10 +443,10 @@ const getPriority = (grid, color, tunnel) => {
return nLess / nColor;
};
const distanceSq = (ax, ay, bx, by) => (ax - bx) ** 2 + (ay - by) ** 2;
const clearResidualColoredLayer_getColorSafe = (grid, x, y) => (0,types_grid/* isInside */.V0)(grid, x, y) ? (0,types_grid/* getColor */.Lq)(grid, x, y) : 0;
const clearResidualColoredLayer_getColorSafe = (grid, x, y) => (0,types_grid/* isInside */.FK)(grid, x, y) ? (0,types_grid/* getColor */.oU)(grid, x, y) : 0;
const clearResidualColoredLayer_setEmptySafe = (grid, x, y) => {
if ((0,types_grid/* isInside */.V0)(grid, x, y))
(0,types_grid/* setColorEmpty */.Dy)(grid, x, y);
if ((0,types_grid/* isInside */.FK)(grid, x, y))
(0,types_grid/* setColorEmpty */.l$)(grid, x, y);
};
;// CONCATENATED MODULE: ../solver/clearCleanColoredLayer.ts
@@ -461,14 +456,14 @@ const clearResidualColoredLayer_setEmptySafe = (grid, x, y) => {
const clearCleanColoredLayer = (grid, outside, snake0, color) => {
const snakeN = (0,types_snake/* getSnakeLength */.JJ)(snake0);
const snakeN = (0,types_snake/* getSnakeLength */.T$)(snake0);
const points = clearCleanColoredLayer_getTunnellablePoints(grid, outside, snakeN, color);
const chain = [snake0];
while (points.length) {
const path = getPathToNextPoint(grid, chain[0], color, points);
path.pop();
for (const snake of path)
clearCleanColoredLayer_setEmptySafe(grid, (0,types_snake/* getHeadX */.If)(snake), (0,types_snake/* getHeadY */.IP)(snake));
clearCleanColoredLayer_setEmptySafe(grid, (0,types_snake/* getHeadX */.tN)(snake), (0,types_snake/* getHeadY */.Ap)(snake));
chain.unshift(...path);
}
fillOutside(outside, grid);
@@ -481,19 +476,19 @@ const getPathToNextPoint = (grid, snake0, color, points) => {
const openList = [{ snake: snake0 }];
while (openList.length) {
const o = openList.shift();
const x = (0,types_snake/* getHeadX */.If)(o.snake);
const y = (0,types_snake/* getHeadY */.IP)(o.snake);
const x = (0,types_snake/* getHeadX */.tN)(o.snake);
const y = (0,types_snake/* getHeadY */.Ap)(o.snake);
const i = points.findIndex((p) => p.x === x && p.y === y);
if (i >= 0) {
points.splice(i, 1);
return clearCleanColoredLayer_unwrap(o);
}
for (const { x: dx, y: dy } of around4) {
if ((0,types_grid/* isInsideLarge */.HJ)(grid, 2, x + dx, y + dy) &&
!(0,types_snake/* snakeWillSelfCollide */.nJ)(o.snake, dx, dy) &&
if ((0,types_grid/* isInsideLarge */.Yd)(grid, 2, x + dx, y + dy) &&
!(0,types_snake/* snakeWillSelfCollide */.J)(o.snake, dx, dy) &&
clearCleanColoredLayer_getColorSafe(grid, x + dx, y + dy) <= color) {
const snake = (0,types_snake/* nextSnake */.kv)(o.snake, dx, dy);
if (!closeList.some((s0) => (0,types_snake/* snakeEquals */.kE)(s0, snake))) {
const snake = (0,types_snake/* nextSnake */.Sc)(o.snake, dx, dy);
if (!closeList.some((s0) => (0,types_snake/* snakeEquals */.sW)(s0, snake))) {
closeList.push(snake);
openList.push({ snake, parent: o });
}
@@ -508,8 +503,8 @@ const clearCleanColoredLayer_getTunnellablePoints = (grid, outside, snakeN, colo
const points = [];
for (let x = grid.width; x--;)
for (let y = grid.height; y--;) {
const c = (0,types_grid/* getColor */.Lq)(grid, x, y);
if (!(0,types_grid/* isEmpty */.xb)(c) &&
const c = (0,types_grid/* getColor */.oU)(grid, x, y);
if (!(0,types_grid/* isEmpty */.Im)(c) &&
c <= color &&
!points.some((p) => p.x === x && p.y === y)) {
const tunnel = getBestTunnel(grid, outside, x, y, color, snakeN);
@@ -521,12 +516,12 @@ const clearCleanColoredLayer_getTunnellablePoints = (grid, outside, snakeN, colo
}
return points;
};
const clearCleanColoredLayer_getColorSafe = (grid, x, y) => (0,types_grid/* isInside */.V0)(grid, x, y) ? (0,types_grid/* getColor */.Lq)(grid, x, y) : 0;
const clearCleanColoredLayer_getColorSafe = (grid, x, y) => (0,types_grid/* isInside */.FK)(grid, x, y) ? (0,types_grid/* getColor */.oU)(grid, x, y) : 0;
const clearCleanColoredLayer_setEmptySafe = (grid, x, y) => {
if ((0,types_grid/* isInside */.V0)(grid, x, y))
(0,types_grid/* setColorEmpty */.Dy)(grid, x, y);
if ((0,types_grid/* isInside */.FK)(grid, x, y))
(0,types_grid/* setColorEmpty */.l$)(grid, x, y);
};
const clearCleanColoredLayer_isEmptySafe = (grid, x, y) => !(0,types_grid/* isInside */.V0)(grid, x, y) && (0,types_grid/* isEmpty */.xb)((0,types_grid/* getColor */.Lq)(grid, x, y));
const clearCleanColoredLayer_isEmptySafe = (grid, x, y) => !(0,types_grid/* isInside */.FK)(grid, x, y) && (0,types_grid/* isEmpty */.Im)((0,types_grid/* getColor */.oU)(grid, x, y));
;// CONCATENATED MODULE: ../solver/getBestRoute.ts
@@ -534,7 +529,7 @@ const clearCleanColoredLayer_isEmptySafe = (grid, x, y) => !(0,types_grid/* isIn
const getBestRoute = (grid0, snake0) => {
const grid = (0,types_grid/* copyGrid */.VJ)(grid0);
const grid = (0,types_grid/* copyGrid */.mi)(grid0);
const outside = createOutside(grid);
const chain = [snake0];
for (const color of extractColors(grid)) {
@@ -552,7 +547,7 @@ const extractColors = (grid) => {
;// CONCATENATED MODULE: ../types/__fixtures__/snake.ts
const create = (length) => (0,types_snake/* createSnakeFromCells */.xG)(Array.from({ length }, (_, i) => ({ x: i, y: -1 })));
const create = (length) => (0,types_snake/* createSnakeFromCells */.yS)(Array.from({ length }, (_, i) => ({ x: i, y: -1 })));
const snake1 = create(1);
const snake3 = create(3);
const snake4 = create(4);
@@ -565,20 +560,20 @@ const snake9 = create(9);
const getPathToPose_isEmptySafe = (grid, x, y) => !(0,types_grid/* isInside */.V0)(grid, x, y) || (0,types_grid/* isEmpty */.xb)((0,types_grid/* getColor */.Lq)(grid, x, y));
const getPathToPose_isEmptySafe = (grid, x, y) => !(0,types_grid/* isInside */.FK)(grid, x, y) || (0,types_grid/* isEmpty */.Im)((0,types_grid/* getColor */.oU)(grid, x, y));
const getPathToPose = (snake0, target, grid) => {
if ((0,types_snake/* snakeEquals */.kE)(snake0, target))
if ((0,types_snake/* snakeEquals */.sW)(snake0, target))
return [];
const targetCells = (0,types_snake/* snakeToCells */.Ks)(target).reverse();
const snakeN = (0,types_snake/* getSnakeLength */.JJ)(snake0);
const targetCells = (0,types_snake/* snakeToCells */.HU)(target).reverse();
const snakeN = (0,types_snake/* getSnakeLength */.T$)(snake0);
const box = {
min: {
x: Math.min((0,types_snake/* getHeadX */.If)(snake0), (0,types_snake/* getHeadX */.If)(target)) - snakeN - 1,
y: Math.min((0,types_snake/* getHeadY */.IP)(snake0), (0,types_snake/* getHeadY */.IP)(target)) - snakeN - 1,
x: Math.min((0,types_snake/* getHeadX */.tN)(snake0), (0,types_snake/* getHeadX */.tN)(target)) - snakeN - 1,
y: Math.min((0,types_snake/* getHeadY */.Ap)(snake0), (0,types_snake/* getHeadY */.Ap)(target)) - snakeN - 1,
},
max: {
x: Math.max((0,types_snake/* getHeadX */.If)(snake0), (0,types_snake/* getHeadX */.If)(target)) + snakeN + 1,
y: Math.max((0,types_snake/* getHeadY */.IP)(snake0), (0,types_snake/* getHeadY */.IP)(target)) + snakeN + 1,
x: Math.max((0,types_snake/* getHeadX */.tN)(snake0), (0,types_snake/* getHeadX */.tN)(target)) + snakeN + 1,
y: Math.max((0,types_snake/* getHeadY */.Ap)(snake0), (0,types_snake/* getHeadY */.Ap)(target)) + snakeN + 1,
},
};
const [t0, ...forbidden] = targetCells;
@@ -587,8 +582,8 @@ const getPathToPose = (snake0, target, grid) => {
const closeList = [];
while (openList.length) {
const o = openList.shift();
const x = (0,types_snake/* getHeadX */.If)(o.snake);
const y = (0,types_snake/* getHeadY */.IP)(o.snake);
const x = (0,types_snake/* getHeadX */.tN)(o.snake);
const y = (0,types_snake/* getHeadY */.Ap)(o.snake);
if (x === t0.x && y === t0.y) {
const path = [];
let e = o;
@@ -605,17 +600,17 @@ const getPathToPose = (snake0, target, grid) => {
const { x: dx, y: dy } = around4[i];
const nx = x + dx;
const ny = y + dy;
if (!(0,types_snake/* snakeWillSelfCollide */.nJ)(o.snake, dx, dy) &&
if (!(0,types_snake/* snakeWillSelfCollide */.J)(o.snake, dx, dy) &&
(!grid || getPathToPose_isEmptySafe(grid, nx, ny)) &&
(grid
? (0,types_grid/* isInsideLarge */.HJ)(grid, 2, nx, ny)
? (0,types_grid/* isInsideLarge */.Yd)(grid, 2, nx, ny)
: box.min.x <= nx &&
nx <= box.max.x &&
box.min.y <= ny &&
ny <= box.max.y) &&
!forbidden.some((p) => p.x === nx && p.y === ny)) {
const snake = (0,types_snake/* nextSnake */.kv)(o.snake, dx, dy);
if (!closeList.some((s) => (0,types_snake/* snakeEquals */.kE)(snake, s))) {
const snake = (0,types_snake/* nextSnake */.Sc)(o.snake, dx, dy);
if (!closeList.some((s) => (0,types_snake/* snakeEquals */.sW)(snake, s))) {
const w = o.w + 1;
const h = Math.abs(nx - x) + Math.abs(ny - y);
const f = w + h;
@@ -648,12 +643,12 @@ const generateContributionSnake = async (userName, outputs, options) => {
switch (format) {
case "svg": {
console.log(`🖌 creating svg (outputs[${i}])`);
const { createSvg } = await __webpack_require__.e(/* import() */ 340).then(__webpack_require__.bind(__webpack_require__, 8340));
const { createSvg } = await __webpack_require__.e(/* import() */ 578).then(__webpack_require__.bind(__webpack_require__, 4578));
return createSvg(grid, cells, chain, drawOptions, animationOptions);
}
case "gif": {
console.log(`📹 creating gif (outputs[${i}])`);
const { createGif } = await Promise.all(/* import() */[__webpack_require__.e(371), __webpack_require__.e(142)]).then(__webpack_require__.bind(__webpack_require__, 7142));
const { createGif } = await Promise.all(/* import() */[__webpack_require__.e(155), __webpack_require__.e(642)]).then(__webpack_require__.bind(__webpack_require__, 3642));
return await createGif(grid, cells, chain, drawOptions, animationOptions);
}
}
@@ -663,18 +658,18 @@ const generateContributionSnake = async (userName, outputs, options) => {
/***/ }),
/***/ 2881:
/***/ 105:
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "Dy": () => (/* binding */ setColorEmpty),
/* harmony export */ "HJ": () => (/* binding */ isInsideLarge),
/* harmony export */ "Lq": () => (/* binding */ getColor),
/* harmony export */ "V0": () => (/* binding */ isInside),
/* harmony export */ "VJ": () => (/* binding */ copyGrid),
/* harmony export */ "u1": () => (/* binding */ createEmptyGrid),
/* harmony export */ "vk": () => (/* binding */ setColor),
/* harmony export */ "xb": () => (/* binding */ isEmpty)
/* harmony export */ FK: () => (/* binding */ isInside),
/* harmony export */ Im: () => (/* binding */ isEmpty),
/* harmony export */ Kb: () => (/* binding */ createEmptyGrid),
/* harmony export */ Yd: () => (/* binding */ isInsideLarge),
/* harmony export */ l$: () => (/* binding */ setColorEmpty),
/* harmony export */ mi: () => (/* binding */ copyGrid),
/* harmony export */ oU: () => (/* binding */ getColor),
/* harmony export */ wW: () => (/* binding */ setColor)
/* harmony export */ });
/* unused harmony exports isGridEmpty, gridEquals */
const isInside = (grid, x, y) => x >= 0 && y >= 0 && x < grid.width && y < grid.height;
@@ -707,18 +702,18 @@ const createEmptyGrid = (width, height) => ({
/***/ }),
/***/ 9347:
/***/ 777:
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "IP": () => (/* binding */ getHeadY),
/* harmony export */ "If": () => (/* binding */ getHeadX),
/* harmony export */ "JJ": () => (/* binding */ getSnakeLength),
/* harmony export */ "Ks": () => (/* binding */ snakeToCells),
/* harmony export */ "kE": () => (/* binding */ snakeEquals),
/* harmony export */ "kv": () => (/* binding */ nextSnake),
/* harmony export */ "nJ": () => (/* binding */ snakeWillSelfCollide),
/* harmony export */ "xG": () => (/* binding */ createSnakeFromCells)
/* harmony export */ Ap: () => (/* binding */ getHeadY),
/* harmony export */ HU: () => (/* binding */ snakeToCells),
/* harmony export */ J: () => (/* binding */ snakeWillSelfCollide),
/* harmony export */ Sc: () => (/* binding */ nextSnake),
/* harmony export */ T$: () => (/* binding */ getSnakeLength),
/* harmony export */ sW: () => (/* binding */ snakeEquals),
/* harmony export */ tN: () => (/* binding */ getHeadX),
/* harmony export */ yS: () => (/* binding */ createSnakeFromCells)
/* harmony export */ });
/* unused harmony export copySnake */
const getHeadX = (snake) => snake[0] - 2;

File diff suppressed because it is too large Load Diff

View File

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

View File

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

26125
svg-only/dist/index.js vendored

File diff suppressed because one or more lines are too long

6541
yarn.lock

File diff suppressed because it is too large Load Diff