From bb3d2bce11355fd9da13d206c2b9e678dd0ff61c Mon Sep 17 00:00:00 2001 From: platane Date: Sun, 1 Nov 2020 14:06:17 +0100 Subject: [PATCH] :shower: split svg creator file --- packages/svg-creator/grid.ts | 75 +++++++++++ packages/svg-creator/index.ts | 232 +++------------------------------- packages/svg-creator/snake.ts | 104 +++++++++++++++ packages/svg-creator/stack.ts | 49 +++++++ packages/svg-creator/utils.ts | 8 ++ 5 files changed, 254 insertions(+), 214 deletions(-) create mode 100644 packages/svg-creator/grid.ts create mode 100644 packages/svg-creator/snake.ts create mode 100644 packages/svg-creator/stack.ts create mode 100644 packages/svg-creator/utils.ts diff --git a/packages/svg-creator/grid.ts b/packages/svg-creator/grid.ts new file mode 100644 index 0000000..3368b53 --- /dev/null +++ b/packages/svg-creator/grid.ts @@ -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; + 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 }; +}; diff --git a/packages/svg-creator/index.ts b/packages/svg-creator/index.ts index 2bd6089..e920634 100644 --- a/packages/svg-creator/index.ts +++ b/packages/svg-creator/index.ts @@ -5,15 +5,14 @@ import { isInside, setColorEmpty, } from "@snk/types/grid"; -import { - getHeadX, - getHeadY, - getSnakeLength, - snakeToCells, -} from "@snk/types/snake"; +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 { createSnake } from "./snake"; +import { createGrid } from "./grid"; +import { createStack } from "./stack"; +import { toAttribute } from "./utils"; export type Options = { colorDots: Record; @@ -26,8 +25,6 @@ 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 })) @@ -67,23 +64,17 @@ 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 elements = [ + createGrid(cells, drawOptions, duration), + createStack( + cells, + drawOptions, + grid0.width * drawOptions.sizeCell, + (grid0.height + 2) * drawOptions.sizeCell, + duration + ), + createSnake(chain, drawOptions, duration), + ]; const viewBox = [ -drawOptions.sizeCell, @@ -103,197 +94,10 @@ export const createSvg = ( >`, "", - - ...gridSvgElements, - ...snakeSvgElements, - ...stackSvgElements, + ...elements.map((e) => e.svgElements).flat(), "", ].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 = (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 }; -}; diff --git a/packages/svg-creator/snake.ts b/packages/svg-creator/snake.ts new file mode 100644 index 0000000..91f416b --- /dev/null +++ b/packages/svg-creator/snake.ts @@ -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; + 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 = (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); + }); diff --git a/packages/svg-creator/stack.ts b/packages/svg-creator/stack.ts new file mode 100644 index 0000000..56b74b8 --- /dev/null +++ b/packages/svg-creator/stack.ts @@ -0,0 +1,49 @@ +import type { Color, Empty } from "@snk/types/grid"; +import { h } from "./utils"; + +export type Options = { + colorDots: Record; + 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 }; +}; diff --git a/packages/svg-creator/utils.ts b/packages/svg-creator/utils.ts new file mode 100644 index 0000000..d084533 --- /dev/null +++ b/packages/svg-creator/utils.ts @@ -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(" ");