import { copyGrid, getColor, isEmpty, isInside, setColorEmpty, } from "@snk/types/grid"; import { getHeadX, getHeadY } from "@snk/types/snake"; import type { Snake } from "@snk/types/snake"; import type { Grid, Color, Empty } from "@snk/types/grid"; import type { Point } from "@snk/types/point"; import type { AnimationOptions } from "@snk/gif-creator"; import { createSnake } from "./snake"; import { createGrid } from "./grid"; import { createStack } from "./stack"; import { h } from "./xml-utils"; import { minifyCss } from "./css-utils"; export type DrawOptions = { colorDots: Record; colorEmpty: string; colorDotBorder: string; colorSnake: string; sizeCell: number; sizeDot: number; sizeDotBorderRadius: number; dark?: { colorDots: Record; colorEmpty: string; colorDotBorder?: string; colorSnake?: string; }; }; const getCellsFromGrid = ({ width, height }: Grid) => Array.from({ length: width }, (_, x) => Array.from({ length: height }, (_, y) => ({ x, y })) ).flat(); const createLivingCells = ( grid0: Grid, chain: Snake[], cells: Point[] | null ) => { const livingCells: (Point & { t: number | null; color: Color | Empty; })[] = (cells ?? getCellsFromGrid(grid0)).map(({ x, y }) => ({ x, y, t: null, 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); const y = getHeadY(snake); if (isInside(grid, x, y) && !isEmpty(getColor(grid, x, y))) { setColorEmpty(grid, x, y); const cell = livingCells.find((c) => c.x === x && c.y === y)!; cell.t = i / chain.length; } } return livingCells; }; export const createSvg = ( grid: Grid, cells: Point[] | null, chain: Snake[], drawOptions: DrawOptions, animationOptions: Pick ) => { const width = (grid.width + 2) * drawOptions.sizeCell; const height = (grid.height + 5) * drawOptions.sizeCell; const duration = animationOptions.frameDuration * chain.length; const livingCells = createLivingCells(grid, chain, cells); const elements = [ createGrid(livingCells, drawOptions, duration), createStack( livingCells, drawOptions, grid.width * drawOptions.sizeCell, (grid.height + 2) * drawOptions.sizeCell, duration ), createSnake(chain, drawOptions, duration), ]; const viewBox = [ -drawOptions.sizeCell, -drawOptions.sizeCell * 2, width, height, ].join(" "); const style = generateColorVar(drawOptions) + elements .map((e) => e.styles) .flat() .join("\n"); const svg = [ h("svg", { viewBox, width, height, xmlns: "http://www.w3.org/2000/svg", }).replace("/>", ">"), "", "Generated with https://github.com/Platane/snk", "", "", ...elements.map((e) => e.svgElements).flat(), "", ].join(""); return optimizeSvg(svg); }; const optimizeCss = (css: string) => minifyCss(css); const optimizeSvg = (svg: string) => svg; const generateColorVar = (drawOptions: DrawOptions) => ` :root { --cb: ${drawOptions.colorDotBorder}; --cs: ${drawOptions.colorSnake}; --ce: ${drawOptions.colorEmpty}; ${Object.entries(drawOptions.colorDots) .map(([i, color]) => `--c${i}:${color};`) .join("")} } ` + (drawOptions.dark ? ` @media (prefers-color-scheme: dark) { :root { --cb: ${drawOptions.dark.colorDotBorder || drawOptions.colorDotBorder}; --cs: ${drawOptions.dark.colorSnake || drawOptions.colorSnake}; --ce: ${drawOptions.dark.colorEmpty}; ${Object.entries(drawOptions.dark.colorDots) .map(([i, color]) => `--c${i}:${color};`) .join("")} } } ` : "");