🚀 svg creator
This commit is contained in:
@@ -26,6 +26,13 @@ export type Options = {
|
||||
cells?: Point[];
|
||||
};
|
||||
|
||||
const percent = (x: number) => (x * 100).toFixed(2);
|
||||
|
||||
const createCells = ({ width, height }: Grid) =>
|
||||
Array.from({ length: width }, (_, x) =>
|
||||
Array.from({ length: height }, (_, y) => ({ x, y }))
|
||||
).flat();
|
||||
|
||||
export const createSvg = (
|
||||
grid0: Grid,
|
||||
chain: Snake[],
|
||||
@@ -33,41 +40,21 @@ export const createSvg = (
|
||||
gifOptions: { frameDuration: number }
|
||||
) => {
|
||||
const width = (grid0.width + 2) * drawOptions.sizeCell;
|
||||
const height = (grid0.height + 4) * drawOptions.sizeCell;
|
||||
|
||||
const grid = copyGrid(grid0);
|
||||
|
||||
const snakeParts: Point[][] = Array.from(
|
||||
{ length: chain[0] ? getSnakeLength(chain[0]) : 0 },
|
||||
() => []
|
||||
);
|
||||
for (const snake of chain) {
|
||||
const cells = snakeToCells(snake);
|
||||
for (let i = cells.length; i--; ) snakeParts[i].push(cells[i]);
|
||||
}
|
||||
const height = (grid0.height + 5) * drawOptions.sizeCell;
|
||||
|
||||
const duration = gifOptions.frameDuration * chain.length;
|
||||
|
||||
const percent = (x: number) => (x * 100).toFixed(3);
|
||||
|
||||
const cells: (Point & {
|
||||
t: number | null;
|
||||
color: Color | Empty;
|
||||
})[] = (
|
||||
drawOptions.cells ??
|
||||
Array.from({ length: grid.width }, (_, x) =>
|
||||
Array.from({ length: grid.height }, (_, y) => ({
|
||||
x,
|
||||
y,
|
||||
}))
|
||||
).flat()
|
||||
).map(({ x, y }) => ({
|
||||
})[] = (drawOptions.cells ?? createCells(grid0)).map(({ x, y }) => ({
|
||||
x,
|
||||
y,
|
||||
t: null,
|
||||
color: getColor(grid, x, y),
|
||||
color: getColor(grid0, x, y),
|
||||
}));
|
||||
|
||||
const grid = copyGrid(grid0);
|
||||
for (let i = 0; i < chain.length; i++) {
|
||||
const snake = chain[i];
|
||||
const x = getHeadX(snake);
|
||||
@@ -80,122 +67,53 @@ export const createSvg = (
|
||||
}
|
||||
}
|
||||
|
||||
const { svgElements: snakeSvgElements, styles: snakeStyles } = createSnake(
|
||||
chain,
|
||||
drawOptions,
|
||||
duration
|
||||
);
|
||||
const { svgElements: gridSvgElements, styles: gridStyles } = createGrid(
|
||||
cells,
|
||||
drawOptions,
|
||||
duration
|
||||
);
|
||||
const { svgElements: stackSvgElements, styles: stackStyles } = createStack(
|
||||
cells,
|
||||
drawOptions,
|
||||
grid0.width * drawOptions.sizeCell,
|
||||
(grid0.height + 2) * drawOptions.sizeCell,
|
||||
duration
|
||||
);
|
||||
|
||||
const viewBox = [
|
||||
-drawOptions.sizeCell,
|
||||
-drawOptions.sizeCell * 2,
|
||||
width,
|
||||
height,
|
||||
].join(" ");
|
||||
|
||||
return [
|
||||
`<svg
|
||||
${toAttribute({
|
||||
viewBox: [
|
||||
-drawOptions.sizeCell,
|
||||
-drawOptions.sizeCell * 2,
|
||||
width,
|
||||
height,
|
||||
].join(" "),
|
||||
viewBox,
|
||||
width,
|
||||
height,
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
})}
|
||||
>`,
|
||||
|
||||
`<style>`,
|
||||
"<style>",
|
||||
...snakeStyles,
|
||||
...gridStyles,
|
||||
...stackStyles,
|
||||
"</style>",
|
||||
|
||||
`rect.c{
|
||||
shape-rendering: geometricPrecision;
|
||||
outline: 1px solid ${drawOptions.colorBorder};
|
||||
outline-offset: -1px;
|
||||
rx: 2;
|
||||
ry: 2;
|
||||
}
|
||||
rect.s{
|
||||
shape-rendering: geometricPrecision;
|
||||
rx: 4;
|
||||
ry: 4;
|
||||
fill:${drawOptions.colorSnake};
|
||||
}
|
||||
`,
|
||||
|
||||
...snakeParts
|
||||
.map((positions, i) => {
|
||||
const animationName = `s${i}`;
|
||||
const id = `s${i}`;
|
||||
|
||||
return [
|
||||
`@keyframes ${animationName} {`,
|
||||
...positions.map(({ x, y }, i, { length, [0]: { x: x0, y: y0 } }) => [
|
||||
`${percent(i / length)}%`,
|
||||
"{",
|
||||
"transform:translate(",
|
||||
[(x - x0) * drawOptions.sizeCell, (y - y0) * drawOptions.sizeCell]
|
||||
.map((u) => u + "px")
|
||||
.join(","),
|
||||
")",
|
||||
"}",
|
||||
]),
|
||||
"}",
|
||||
`#${id}{animation: ${animationName} linear ${duration}ms infinite}`,
|
||||
];
|
||||
})
|
||||
.flat(Infinity),
|
||||
|
||||
...cells
|
||||
.map(({ t, color }, i) => {
|
||||
const animationName = `c${i}`;
|
||||
const id = `c${i}`;
|
||||
|
||||
if (t === null || isEmpty(color)) return [];
|
||||
|
||||
// @ts-ignore
|
||||
const fill = drawOptions.colorDots[color];
|
||||
|
||||
return [
|
||||
`@keyframes ${animationName} {`,
|
||||
`${percent(t - 0.0001)}%{fill:${fill}}`,
|
||||
`${percent(t + 0.0001)}%{fill:${drawOptions.colorEmpty}}`,
|
||||
`100%{fill:${drawOptions.colorEmpty}}`,
|
||||
"}",
|
||||
`#${id}{animation: ${animationName} linear ${duration}ms infinite}`,
|
||||
];
|
||||
})
|
||||
.flat(Infinity),
|
||||
|
||||
`</style>`,
|
||||
|
||||
...cells.map(({ x, y, color }, i) => {
|
||||
const s = drawOptions.sizeCell;
|
||||
const d = drawOptions.sizeDot;
|
||||
const m = (s - d) / 2;
|
||||
const fill = isEmpty(color)
|
||||
? drawOptions.colorEmpty
|
||||
: // @ts-ignore
|
||||
drawOptions.colorDots[color];
|
||||
|
||||
return h("rect", {
|
||||
class: "c",
|
||||
id: `c${i}`,
|
||||
x: x * s + m,
|
||||
y: y * s + m,
|
||||
width: d,
|
||||
height: d,
|
||||
fill: fill,
|
||||
});
|
||||
}),
|
||||
|
||||
...snakeParts.map(([{ x, y }], i) => {
|
||||
const s = drawOptions.sizeCell;
|
||||
const u = Math.min((i - 1) * 1.6 * (s / 16), s * 0.2);
|
||||
const d = drawOptions.sizeDot - u;
|
||||
const m = (s - d) / 2;
|
||||
|
||||
return h("rect", {
|
||||
class: "s",
|
||||
id: `s${i}`,
|
||||
x: x * s + m,
|
||||
y: y * s + m,
|
||||
width: d,
|
||||
height: d,
|
||||
});
|
||||
}),
|
||||
...gridSvgElements,
|
||||
...snakeSvgElements,
|
||||
...stackSvgElements,
|
||||
|
||||
"</svg>",
|
||||
].join("");
|
||||
].join("\n");
|
||||
};
|
||||
|
||||
const h = (element: string, attributes: any) =>
|
||||
@@ -203,5 +121,164 @@ const h = (element: string, attributes: any) =>
|
||||
|
||||
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} {` +
|
||||
positions
|
||||
.map(transform)
|
||||
.map((tr, i, { length }) => `${percent(i / length)}%{${tr}}`)
|
||||
.join("") +
|
||||
"}",
|
||||
|
||||
`#${id}{` +
|
||||
`${transform(positions[0])};` +
|
||||
`animation: ${animationName} linear ${duration}ms infinite` +
|
||||
"}",
|
||||
];
|
||||
}),
|
||||
].flat();
|
||||
|
||||
return { svgElements, styles };
|
||||
};
|
||||
|
||||
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.001)}%{fill:${fill}}` +
|
||||
`${percent(t + 0.001)}%,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.001)}%{fill:transparent}` +
|
||||
`${percent(t + 0.001)}%,100%{fill:${fill}}` +
|
||||
"}",
|
||||
|
||||
`#${id}{fill:transparent;animation: ${animationName} linear ${duration}ms infinite}`
|
||||
);
|
||||
}
|
||||
|
||||
return { svgElements, styles };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user