🚿 split svg creator file
This commit is contained in:
75
packages/svg-creator/grid.ts
Normal file
75
packages/svg-creator/grid.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import type { Color, Empty } from "@snk/types/grid";
|
||||||
|
import type { Point } from "@snk/types/point";
|
||||||
|
import { h } from "./utils";
|
||||||
|
|
||||||
|
export type Options = {
|
||||||
|
colorDots: Record<Color, string>;
|
||||||
|
colorEmpty: string;
|
||||||
|
colorBorder: string;
|
||||||
|
sizeCell: number;
|
||||||
|
sizeDot: number;
|
||||||
|
sizeBorderRadius: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const percent = (x: number) => (x * 100).toFixed(2);
|
||||||
|
|
||||||
|
export const createGrid = (
|
||||||
|
cells: (Point & { t: number | null; color: Color | Empty })[],
|
||||||
|
{
|
||||||
|
colorEmpty,
|
||||||
|
colorBorder,
|
||||||
|
sizeBorderRadius,
|
||||||
|
colorDots,
|
||||||
|
sizeDot,
|
||||||
|
sizeCell,
|
||||||
|
}: Options,
|
||||||
|
duration: number
|
||||||
|
) => {
|
||||||
|
const svgElements: string[] = [];
|
||||||
|
const styles = [
|
||||||
|
`rect.c{
|
||||||
|
shape-rendering: geometricPrecision;
|
||||||
|
outline: 1px solid ${colorBorder};
|
||||||
|
outline-offset: -1px;
|
||||||
|
rx: ${sizeBorderRadius};
|
||||||
|
ry: ${sizeBorderRadius};
|
||||||
|
fill:${colorEmpty}
|
||||||
|
}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
for (const { x, y, color, t } of cells) {
|
||||||
|
const id = t && "c" + (i++).toString(36);
|
||||||
|
const s = sizeCell;
|
||||||
|
const d = sizeDot;
|
||||||
|
const m = (s - d) / 2;
|
||||||
|
|
||||||
|
if (t !== null) {
|
||||||
|
const animationName = "a" + id;
|
||||||
|
// @ts-ignore
|
||||||
|
const fill = colorDots[color];
|
||||||
|
|
||||||
|
styles.push(
|
||||||
|
`@keyframes ${animationName} {` +
|
||||||
|
`${percent(t - 0.0001)}%{fill:${fill}}` +
|
||||||
|
`${percent(t + 0.0001)}%,100%{fill:${colorEmpty}}` +
|
||||||
|
"}",
|
||||||
|
|
||||||
|
`#${id}{fill:${fill};animation: ${animationName} linear ${duration}ms infinite}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
svgElements.push(
|
||||||
|
h("rect", {
|
||||||
|
id,
|
||||||
|
class: "c",
|
||||||
|
x: x * s + m,
|
||||||
|
y: y * s + m,
|
||||||
|
width: d,
|
||||||
|
height: d,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { svgElements, styles };
|
||||||
|
};
|
||||||
@@ -5,15 +5,14 @@ import {
|
|||||||
isInside,
|
isInside,
|
||||||
setColorEmpty,
|
setColorEmpty,
|
||||||
} from "@snk/types/grid";
|
} from "@snk/types/grid";
|
||||||
import {
|
import { getHeadX, getHeadY } from "@snk/types/snake";
|
||||||
getHeadX,
|
|
||||||
getHeadY,
|
|
||||||
getSnakeLength,
|
|
||||||
snakeToCells,
|
|
||||||
} from "@snk/types/snake";
|
|
||||||
import type { Snake } from "@snk/types/snake";
|
import type { Snake } from "@snk/types/snake";
|
||||||
import type { Grid, Color, Empty } from "@snk/types/grid";
|
import type { Grid, Color, Empty } from "@snk/types/grid";
|
||||||
import type { Point } from "@snk/types/point";
|
import type { Point } from "@snk/types/point";
|
||||||
|
import { createSnake } from "./snake";
|
||||||
|
import { createGrid } from "./grid";
|
||||||
|
import { createStack } from "./stack";
|
||||||
|
import { toAttribute } from "./utils";
|
||||||
|
|
||||||
export type Options = {
|
export type Options = {
|
||||||
colorDots: Record<Color, string>;
|
colorDots: Record<Color, string>;
|
||||||
@@ -26,8 +25,6 @@ export type Options = {
|
|||||||
cells?: Point[];
|
cells?: Point[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const percent = (x: number) => (x * 100).toFixed(2);
|
|
||||||
|
|
||||||
const createCells = ({ width, height }: Grid) =>
|
const createCells = ({ width, height }: Grid) =>
|
||||||
Array.from({ length: width }, (_, x) =>
|
Array.from({ length: width }, (_, x) =>
|
||||||
Array.from({ length: height }, (_, y) => ({ x, y }))
|
Array.from({ length: height }, (_, y) => ({ x, y }))
|
||||||
@@ -67,23 +64,17 @@ export const createSvg = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { svgElements: snakeSvgElements, styles: snakeStyles } = createSnake(
|
const elements = [
|
||||||
chain,
|
createGrid(cells, drawOptions, duration),
|
||||||
drawOptions,
|
createStack(
|
||||||
duration
|
cells,
|
||||||
);
|
drawOptions,
|
||||||
const { svgElements: gridSvgElements, styles: gridStyles } = createGrid(
|
grid0.width * drawOptions.sizeCell,
|
||||||
cells,
|
(grid0.height + 2) * drawOptions.sizeCell,
|
||||||
drawOptions,
|
duration
|
||||||
duration
|
),
|
||||||
);
|
createSnake(chain, drawOptions, duration),
|
||||||
const { svgElements: stackSvgElements, styles: stackStyles } = createStack(
|
];
|
||||||
cells,
|
|
||||||
drawOptions,
|
|
||||||
grid0.width * drawOptions.sizeCell,
|
|
||||||
(grid0.height + 2) * drawOptions.sizeCell,
|
|
||||||
duration
|
|
||||||
);
|
|
||||||
|
|
||||||
const viewBox = [
|
const viewBox = [
|
||||||
-drawOptions.sizeCell,
|
-drawOptions.sizeCell,
|
||||||
@@ -103,197 +94,10 @@ export const createSvg = (
|
|||||||
>`,
|
>`,
|
||||||
|
|
||||||
"<style>",
|
"<style>",
|
||||||
...snakeStyles,
|
...elements.map((e) => e.styles).flat(),
|
||||||
...gridStyles,
|
|
||||||
...stackStyles,
|
|
||||||
"</style>",
|
"</style>",
|
||||||
|
...elements.map((e) => e.svgElements).flat(),
|
||||||
...gridSvgElements,
|
|
||||||
...snakeSvgElements,
|
|
||||||
...stackSvgElements,
|
|
||||||
|
|
||||||
"</svg>",
|
"</svg>",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
const h = (element: string, attributes: any) =>
|
|
||||||
`<${element} ${toAttribute(attributes)}/>`;
|
|
||||||
|
|
||||||
const toAttribute = (o: any) =>
|
|
||||||
Object.entries(o)
|
|
||||||
.filter(([, value]) => value !== null)
|
|
||||||
.map(([name, value]) => `${name}="${value}"`)
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
const createSnake = (
|
|
||||||
chain: Snake[],
|
|
||||||
{ sizeCell, colorSnake, sizeDot }: Options,
|
|
||||||
duration: number
|
|
||||||
) => {
|
|
||||||
const snakeN = chain[0] ? getSnakeLength(chain[0]) : 0;
|
|
||||||
|
|
||||||
const snakeParts: Point[][] = Array.from({ length: snakeN }, () => []);
|
|
||||||
|
|
||||||
for (const snake of chain) {
|
|
||||||
const cells = snakeToCells(snake);
|
|
||||||
for (let i = cells.length; i--; ) snakeParts[i].push(cells[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const svgElements = snakeParts.map((_, i) => {
|
|
||||||
const s = sizeCell;
|
|
||||||
const u = Math.min((i - 1) * 1.6 * (s / 16), s * 0.2);
|
|
||||||
const d = sizeDot - u;
|
|
||||||
const m = (s - d) / 2;
|
|
||||||
|
|
||||||
return h("rect", {
|
|
||||||
class: "s",
|
|
||||||
id: `s${i}`,
|
|
||||||
x: m,
|
|
||||||
y: m,
|
|
||||||
width: d,
|
|
||||||
height: d,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const transform = ({ x, y }: Point) =>
|
|
||||||
`transform:translate(${x * sizeCell}px,${y * sizeCell}px)`;
|
|
||||||
|
|
||||||
const styles = [
|
|
||||||
`rect.s{
|
|
||||||
shape-rendering: geometricPrecision;
|
|
||||||
rx: 4;
|
|
||||||
ry: 4;
|
|
||||||
fill:${colorSnake};
|
|
||||||
}`,
|
|
||||||
|
|
||||||
...snakeParts.map((positions, i) => {
|
|
||||||
const id = `s${i}`;
|
|
||||||
const animationName = "a" + id;
|
|
||||||
|
|
||||||
return [
|
|
||||||
`@keyframes ${animationName} {` +
|
|
||||||
removeInterpolatedPositions(
|
|
||||||
positions.map((tr, i, { length }) => ({ ...tr, t: i / length }))
|
|
||||||
)
|
|
||||||
.map((p) => `${percent(p.t)}%{${transform(p)}}`)
|
|
||||||
.join("") +
|
|
||||||
"}",
|
|
||||||
|
|
||||||
`#${id}{` +
|
|
||||||
`${transform(positions[0])};` +
|
|
||||||
`animation: ${animationName} linear ${duration}ms infinite` +
|
|
||||||
"}",
|
|
||||||
];
|
|
||||||
}),
|
|
||||||
].flat();
|
|
||||||
|
|
||||||
return { svgElements, styles };
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeInterpolatedPositions = <T extends Point>(arr: T[]) =>
|
|
||||||
arr.filter((u, i, arr) => {
|
|
||||||
if (i - 1 < 0 || i + 1 >= arr.length) return true;
|
|
||||||
|
|
||||||
const a = arr[i - 1];
|
|
||||||
const b = arr[i + 1];
|
|
||||||
|
|
||||||
const ex = (a.x + b.x) / 2;
|
|
||||||
const ey = (a.y + b.y) / 2;
|
|
||||||
|
|
||||||
// return true;
|
|
||||||
return !(Math.abs(ex - u.x) < 0.01 && Math.abs(ey - u.y) < 0.01);
|
|
||||||
});
|
|
||||||
|
|
||||||
const createGrid = (
|
|
||||||
cells: (Point & { t: number | null; color: Color | Empty })[],
|
|
||||||
{ colorEmpty, colorBorder, colorDots, sizeDot, sizeCell }: Options,
|
|
||||||
duration: number
|
|
||||||
) => {
|
|
||||||
const svgElements: string[] = [];
|
|
||||||
const styles = [
|
|
||||||
`rect.c{
|
|
||||||
shape-rendering: geometricPrecision;
|
|
||||||
outline: 1px solid ${colorBorder};
|
|
||||||
outline-offset: -1px;
|
|
||||||
rx: 2;
|
|
||||||
ry: 2;
|
|
||||||
fill:${colorEmpty}
|
|
||||||
}`,
|
|
||||||
];
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
for (const { x, y, color, t } of cells) {
|
|
||||||
const id = t && "c" + (i++).toString(36);
|
|
||||||
const s = sizeCell;
|
|
||||||
const d = sizeDot;
|
|
||||||
const m = (s - d) / 2;
|
|
||||||
|
|
||||||
if (t !== null) {
|
|
||||||
const animationName = "a" + id;
|
|
||||||
// @ts-ignore
|
|
||||||
const fill = colorDots[color];
|
|
||||||
|
|
||||||
styles.push(
|
|
||||||
`@keyframes ${animationName} {` +
|
|
||||||
`${percent(t - 0.0001)}%{fill:${fill}}` +
|
|
||||||
`${percent(t + 0.0001)}%,100%{fill:${colorEmpty}}` +
|
|
||||||
"}",
|
|
||||||
|
|
||||||
`#${id}{fill:${fill};animation: ${animationName} linear ${duration}ms infinite}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
svgElements.push(
|
|
||||||
h("rect", {
|
|
||||||
id,
|
|
||||||
class: "c",
|
|
||||||
x: x * s + m,
|
|
||||||
y: y * s + m,
|
|
||||||
width: d,
|
|
||||||
height: d,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { svgElements, styles };
|
|
||||||
};
|
|
||||||
|
|
||||||
const createStack = (
|
|
||||||
cells: { t: number | null; color: Color | Empty }[],
|
|
||||||
{ colorDots, sizeDot }: Options,
|
|
||||||
width: number,
|
|
||||||
y: number,
|
|
||||||
duration: number
|
|
||||||
) => {
|
|
||||||
const svgElements: string[] = [];
|
|
||||||
const styles = [];
|
|
||||||
|
|
||||||
const stack = cells
|
|
||||||
.slice()
|
|
||||||
.filter((a) => a.t !== null)
|
|
||||||
.sort((a, b) => a.t! - b.t!) as any[];
|
|
||||||
|
|
||||||
const m = width / stack.length;
|
|
||||||
let i = 0;
|
|
||||||
for (const { color, t } of stack) {
|
|
||||||
const x = ((i * width) / stack.length).toFixed(2);
|
|
||||||
const id = "t" + (i++).toString(36);
|
|
||||||
const animationName = "a" + id;
|
|
||||||
// @ts-ignore
|
|
||||||
const fill = colorDots[color];
|
|
||||||
|
|
||||||
svgElements.push(
|
|
||||||
h("rect", { id, height: sizeDot, width: (m + 0.6).toFixed(2), x, y })
|
|
||||||
);
|
|
||||||
styles.push(
|
|
||||||
`@keyframes ${animationName} {` +
|
|
||||||
`${percent(t - 0.0001)}%{fill:transparent}` +
|
|
||||||
`${percent(t + 0.0001)}%,100%{fill:${fill}}` +
|
|
||||||
"}",
|
|
||||||
|
|
||||||
`#${id}{fill:transparent;animation: ${animationName} linear ${duration}ms infinite}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { svgElements, styles };
|
|
||||||
};
|
|
||||||
|
|||||||
104
packages/svg-creator/snake.ts
Normal file
104
packages/svg-creator/snake.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { getSnakeLength, snakeToCells } from "@snk/types/snake";
|
||||||
|
import type { Snake } from "@snk/types/snake";
|
||||||
|
import type { Color } from "@snk/types/grid";
|
||||||
|
import type { Point } from "@snk/types/point";
|
||||||
|
import { h } from "./utils";
|
||||||
|
|
||||||
|
export type Options = {
|
||||||
|
colorDots: Record<Color, string>;
|
||||||
|
colorEmpty: string;
|
||||||
|
colorBorder: string;
|
||||||
|
colorSnake: string;
|
||||||
|
sizeCell: number;
|
||||||
|
sizeDot: number;
|
||||||
|
sizeBorderRadius: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const percent = (x: number) => (x * 100).toFixed(2);
|
||||||
|
|
||||||
|
const lerp = (k: number, a: number, b: number) => (1 - k) * a + k * b;
|
||||||
|
|
||||||
|
export const createSnake = (
|
||||||
|
chain: Snake[],
|
||||||
|
{ sizeCell, colorSnake, sizeDot }: Options,
|
||||||
|
duration: number
|
||||||
|
) => {
|
||||||
|
const snakeN = chain[0] ? getSnakeLength(chain[0]) : 0;
|
||||||
|
|
||||||
|
const snakeParts: Point[][] = Array.from({ length: snakeN }, () => []);
|
||||||
|
|
||||||
|
for (const snake of chain) {
|
||||||
|
const cells = snakeToCells(snake);
|
||||||
|
for (let i = cells.length; i--; ) snakeParts[i].push(cells[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const svgElements = snakeParts.map((_, i, { length }) => {
|
||||||
|
// compute snake part size
|
||||||
|
const dMin = sizeDot * 0.8;
|
||||||
|
const dMax = sizeCell * 0.9;
|
||||||
|
const iMax = Math.min(4, length);
|
||||||
|
const u = (1 - Math.min(i, iMax) / iMax) ** 2;
|
||||||
|
const s = lerp(u, dMin, dMax);
|
||||||
|
|
||||||
|
const m = (sizeCell - s) / 2;
|
||||||
|
|
||||||
|
const r = Math.min(4.5, (4 * s) / sizeDot).toFixed(2);
|
||||||
|
|
||||||
|
return h("rect", {
|
||||||
|
class: "s",
|
||||||
|
id: `s${i}`,
|
||||||
|
x: m,
|
||||||
|
y: m,
|
||||||
|
width: s,
|
||||||
|
height: s,
|
||||||
|
rx: r,
|
||||||
|
ry: r,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const transform = ({ x, y }: Point) =>
|
||||||
|
`transform:translate(${x * sizeCell}px,${y * sizeCell}px)`;
|
||||||
|
|
||||||
|
const styles = [
|
||||||
|
`rect.s{
|
||||||
|
shape-rendering: geometricPrecision;
|
||||||
|
fill:${colorSnake};
|
||||||
|
}`,
|
||||||
|
|
||||||
|
...snakeParts.map((positions, i) => {
|
||||||
|
const id = `s${i}`;
|
||||||
|
const animationName = "a" + id;
|
||||||
|
|
||||||
|
return [
|
||||||
|
`@keyframes ${animationName} {` +
|
||||||
|
removeInterpolatedPositions(
|
||||||
|
positions.map((tr, i, { length }) => ({ ...tr, t: i / length }))
|
||||||
|
)
|
||||||
|
.map((p) => `${percent(p.t)}%{${transform(p)}}`)
|
||||||
|
.join("") +
|
||||||
|
"}",
|
||||||
|
|
||||||
|
`#${id}{` +
|
||||||
|
`${transform(positions[0])};` +
|
||||||
|
`animation: ${animationName} linear ${duration}ms infinite` +
|
||||||
|
"}",
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
].flat();
|
||||||
|
|
||||||
|
return { svgElements, styles };
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeInterpolatedPositions = <T extends Point>(arr: T[]) =>
|
||||||
|
arr.filter((u, i, arr) => {
|
||||||
|
if (i - 1 < 0 || i + 1 >= arr.length) return true;
|
||||||
|
|
||||||
|
const a = arr[i - 1];
|
||||||
|
const b = arr[i + 1];
|
||||||
|
|
||||||
|
const ex = (a.x + b.x) / 2;
|
||||||
|
const ey = (a.y + b.y) / 2;
|
||||||
|
|
||||||
|
// return true;
|
||||||
|
return !(Math.abs(ex - u.x) < 0.01 && Math.abs(ey - u.y) < 0.01);
|
||||||
|
});
|
||||||
49
packages/svg-creator/stack.ts
Normal file
49
packages/svg-creator/stack.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { Color, Empty } from "@snk/types/grid";
|
||||||
|
import { h } from "./utils";
|
||||||
|
|
||||||
|
export type Options = {
|
||||||
|
colorDots: Record<Color, string>;
|
||||||
|
sizeDot: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const percent = (x: number) => (x * 100).toFixed(2);
|
||||||
|
|
||||||
|
export const createStack = (
|
||||||
|
cells: { t: number | null; color: Color | Empty }[],
|
||||||
|
{ colorDots, sizeDot }: Options,
|
||||||
|
width: number,
|
||||||
|
y: number,
|
||||||
|
duration: number
|
||||||
|
) => {
|
||||||
|
const svgElements: string[] = [];
|
||||||
|
const styles = [];
|
||||||
|
|
||||||
|
const stack = cells
|
||||||
|
.slice()
|
||||||
|
.filter((a) => a.t !== null)
|
||||||
|
.sort((a, b) => a.t! - b.t!) as any[];
|
||||||
|
|
||||||
|
const m = width / stack.length;
|
||||||
|
let i = 0;
|
||||||
|
for (const { color, t } of stack) {
|
||||||
|
const x = ((i * width) / stack.length).toFixed(2);
|
||||||
|
const id = "t" + (i++).toString(36);
|
||||||
|
const animationName = "a" + id;
|
||||||
|
// @ts-ignore
|
||||||
|
const fill = colorDots[color];
|
||||||
|
|
||||||
|
svgElements.push(
|
||||||
|
h("rect", { id, height: sizeDot, width: (m + 0.6).toFixed(2), x, y })
|
||||||
|
);
|
||||||
|
styles.push(
|
||||||
|
`@keyframes ${animationName} {` +
|
||||||
|
`${percent(t - 0.0001)}%{fill:transparent}` +
|
||||||
|
`${percent(t + 0.0001)}%,100%{fill:${fill}}` +
|
||||||
|
"}",
|
||||||
|
|
||||||
|
`#${id}{fill:transparent;animation: ${animationName} linear ${duration}ms infinite}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { svgElements, styles };
|
||||||
|
};
|
||||||
8
packages/svg-creator/utils.ts
Normal file
8
packages/svg-creator/utils.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export const h = (element: string, attributes: any) =>
|
||||||
|
`<${element} ${toAttribute(attributes)}/>`;
|
||||||
|
|
||||||
|
export const toAttribute = (o: any) =>
|
||||||
|
Object.entries(o)
|
||||||
|
.filter(([, value]) => value !== null)
|
||||||
|
.map(([name, value]) => `${name}="${value}"`)
|
||||||
|
.join(" ");
|
||||||
Reference in New Issue
Block a user