diff --git a/docs/.vitepress/api/gh-icon/[...data].get.ts b/docs/.vitepress/api/gh-icon/[...data].get.ts index bdd4af66d..725db4848 100644 --- a/docs/.vitepress/api/gh-icon/[...data].get.ts +++ b/docs/.vitepress/api/gh-icon/[...data].get.ts @@ -1,7 +1,11 @@ -import { eventHandler, getQuery, setResponseHeader, defaultContentType } from 'h3' -import { renderToString } from 'react-dom/server' +import { eventHandler, setResponseHeader, defaultContentType } from 'h3' +import { renderToString, renderToStaticMarkup } from 'react-dom/server' import { createElement } from 'react' import SvgPreview from '../../lib/SvgPreview/index.tsx'; +import * as iconNodes from '../../data/iconNodes' +import { camelCase } from 'lodash-es' +import createLucideIcon from 'lucide-react/src/createLucideIcon' +import Backdrop from '../../lib/SvgPreview/Backdrop.tsx'; export default eventHandler((event) => { const { params } = event.context @@ -11,8 +15,21 @@ export default eventHandler((event) => { const src = Buffer.from(data, 'base64').toString('utf8'); + const children = [] + const camelCaseName = camelCase(name) + + if (camelCaseName in iconNodes) { + const iconNode = iconNodes[camelCaseName] + + const LucideIcon = createLucideIcon(name, iconNode) + const svg = renderToStaticMarkup(createElement(LucideIcon)) + const backdropString = svg.replace(/]*>|<\/svg>/g, ''); + + children.push(createElement(Backdrop, { backdropString, src })) + } + const svg = Buffer.from( - renderToString(createElement(SvgPreview, {src, showGrid: true})) + renderToString(createElement(SvgPreview, {src, showGrid: true}, children)) ).toString('utf8'); defaultContentType(event, 'image/svg+xml') diff --git a/docs/.vitepress/lib/SvgPreview/Backdrop.tsx b/docs/.vitepress/lib/SvgPreview/Backdrop.tsx new file mode 100644 index 000000000..dfb1a9ab4 --- /dev/null +++ b/docs/.vitepress/lib/SvgPreview/Backdrop.tsx @@ -0,0 +1,71 @@ +import React from 'react'; + +interface BackdropProps { + src: string + backdropString: string +} + +const Backdrop = ({ src, backdropString }: BackdropProps): JSX.Element => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ) +} + + +export default Backdrop; diff --git a/docs/.vitepress/lib/SvgPreview/index.tsx b/docs/.vitepress/lib/SvgPreview/index.tsx index 13cb0fc06..056b9ae5e 100644 --- a/docs/.vitepress/lib/SvgPreview/index.tsx +++ b/docs/.vitepress/lib/SvgPreview/index.tsx @@ -173,6 +173,31 @@ const ControlPath = ({ ); }; +const Radii = ({ + paths, + ...props +}: { paths: Path[] } & PathProps< + 'strokeWidth' | 'stroke' | 'strokeDasharray' | 'strokeOpacity', + any +>) => { + return ( + + {paths + .filter(({ circle }) => circle) + .map(({ c, prev, next, circle: { x, y, r } }) => + c.name === 'circle' ? ( + + ) : ( + <> + + + + ) + )} + + ); +}; + const SvgPreview = React.forwardRef< SVGSVGElement, { @@ -184,6 +209,7 @@ const SvgPreview = React.forwardRef< const darkModeCss = `@media screen and (prefers-color-scheme: dark) { .svg-preview-grid-group, + .svg-preview-radii-group, .svg-preview-shadow-mask-group, .svg-preview-shadow-group { stroke: #fff; @@ -223,6 +249,13 @@ const SvgPreview = React.forwardRef< '#52A675', ]} /> + {children} diff --git a/docs/.vitepress/lib/SvgPreview/types.ts b/docs/.vitepress/lib/SvgPreview/types.ts index 66af74a94..15023fd0e 100644 --- a/docs/.vitepress/lib/SvgPreview/types.ts +++ b/docs/.vitepress/lib/SvgPreview/types.ts @@ -8,6 +8,7 @@ export type Path = { prev: Point; next: Point; isStart: boolean; + circle: { x: number; y: number; r: number }; c: ReturnType[number]; }; diff --git a/docs/.vitepress/lib/SvgPreview/utils.ts b/docs/.vitepress/lib/SvgPreview/utils.ts index 03f767f84..7c9d79ecb 100644 --- a/docs/.vitepress/lib/SvgPreview/utils.ts +++ b/docs/.vitepress/lib/SvgPreview/utils.ts @@ -12,33 +12,66 @@ export function assert(value: unknown): asserts value { } } -const extractPaths = (node: INode): { d: string; name: typeof node.name }[] => { - if (/(rect|circle|ellipse|polygon|polyline|line|path)/.test(node.name)) { - return [{ d: toPath(node), name: node.name }]; +const convertToPathNode = (node: INode): { d: string; name: typeof node.name } => { + if (node.name === 'path') { + return { d: node.attributes.d, name: node.name }; + } + if (node.name === 'circle') { + const cx = parseFloat(node.attributes.cx); + const cy = parseFloat(node.attributes.cy); + const r = parseFloat(node.attributes.r); + return { + d: [ + `M ${cx} ${cy - r}`, + `a ${r} ${r} 0 0 1 ${r} ${r}`, + `a ${r} ${r} 0 0 1 ${0 - r} ${r}`, + `a ${r} ${r} 0 0 1 ${0 - r} ${0 - r}`, + `a ${r} ${r} 0 0 1 ${r} ${0 - r}`, + ].join(''), + name: node.name, + }; + } + return { d: toPath(node).replace(/z$/i, ''), name: node.name }; +}; + +const extractNodes = (node: INode): INode[] => { + if (['rect', 'circle', 'ellipse', 'polygon', 'polyline', 'line', 'path'].includes(node.name)) { + return [node]; } else if (node.children && Array.isArray(node.children)) { - return node.children.flatMap(extractPaths); + return node.children.flatMap(extractNodes); } return []; }; +export const getNodes = (src: string) => + extractNodes(parseSync(src.includes('${src}`)); + export const getCommands = (src: string) => - extractPaths(parseSync(src)).flatMap(({ d, name }, idx) => - new SVGPathData(d).toAbs().commands.map((c) => ({ ...c, id: idx, name })) - ); + getNodes(src) + .map(convertToPathNode) + .flatMap(({ d, name }, idx) => + new SVGPathData(d).toAbs().commands.map((c, cIdx) => ({ ...c, id: idx, idx: cIdx, name })) + ); export const getPaths = (src: string) => { const commands = getCommands(src.includes('${src}`); const paths: Path[] = []; let prev: Point | undefined = undefined; let start: Point | undefined = undefined; - const addPath = (c: (typeof commands)[number], next: Point, d?: string) => { + const addPath = ( + c: typeof commands[number], + next: Point, + d?: string, + circle?: Path['circle'] + ) => { assert(prev); paths.push({ c, - d: d || `M${prev.x} ${prev.y}L${next.x} ${next.y}`, + d: d || `M ${prev.x} ${prev.y} L ${next.x} ${next.y}`, prev, next, + circle, isStart: start === prev, }); prev = next; @@ -77,7 +110,7 @@ export const getPaths = (src: string) => { } case SVGPathData.CURVE_TO: { assert(prev); - addPath(c, c, `M ${prev.x},${prev.y} ${encodeSVGPath(c)}`); + addPath(c, c, `M ${prev.x} ${prev.y} ${encodeSVGPath(c)}`); break; } case SVGPathData.SMOOTH_CURVE_TO: { @@ -86,16 +119,16 @@ export const getPaths = (src: string) => { const reflectedCp1 = { x: previousCommand && - (previousCommand.type === SVGPathData.SMOOTH_CURVE_TO || - previousCommand.type === SVGPathData.CURVE_TO) + (previousCommand.type === SVGPathData.SMOOTH_CURVE_TO || + previousCommand.type === SVGPathData.CURVE_TO) ? previousCommand.relative ? previousCommand.x2 - previousCommand.x : previousCommand.x2 - prev.x : 0, y: previousCommand && - (previousCommand.type === SVGPathData.SMOOTH_CURVE_TO || - previousCommand.type === SVGPathData.CURVE_TO) + (previousCommand.type === SVGPathData.SMOOTH_CURVE_TO || + previousCommand.type === SVGPathData.CURVE_TO) ? previousCommand.relative ? previousCommand.y2 - previousCommand.y : previousCommand.y2 - prev.y @@ -104,7 +137,7 @@ export const getPaths = (src: string) => { addPath( c, c, - `M ${prev.x},${prev.y} ${encodeSVGPath({ + `M ${prev.x} ${prev.y} ${encodeSVGPath({ type: SVGPathData.CURVE_TO, relative: false, x: c.x, @@ -119,7 +152,7 @@ export const getPaths = (src: string) => { } case SVGPathData.QUAD_TO: { assert(prev); - addPath(c, c, `M ${prev.x},${prev.y} ${encodeSVGPath(c)}`); + addPath(c, c, `M ${prev.x} ${prev.y} ${encodeSVGPath(c)}`); break; } case SVGPathData.SMOOTH_QUAD_TO: { @@ -157,7 +190,7 @@ export const getPaths = (src: string) => { addPath( c, c, - `M ${prev.x},${prev.y} ${encodeSVGPath({ + `M ${prev.x} ${prev.y} ${encodeSVGPath({ type: SVGPathData.QUAD_TO, relative: false, x: c.x, @@ -170,10 +203,22 @@ export const getPaths = (src: string) => { } case SVGPathData.ARC: { assert(prev); + const center = arcEllipseCenter( + prev.x, + prev.y, + c.rX, + c.rY, + c.xRot, + c.lArcFlag, + c.sweepFlag, + c.x, + c.y + ); addPath( c, c, - `M ${prev.x},${prev.y} A ${c.rX} ${c.rY} ${c.xRot} ${c.lArcFlag} ${c.sweepFlag} ${c.x} ${c.y}` + `M ${prev.x} ${prev.y} A${c.rX} ${c.rY} ${c.xRot} ${c.lArcFlag} ${c.sweepFlag} ${c.x} ${c.y}`, + c.rX === c.rY ? { ...center, r: c.rX } : undefined ); break; } @@ -184,3 +229,58 @@ export const getPaths = (src: string) => { } return paths; }; + +export const arcEllipseCenter = ( + x1: number, + y1: number, + rx: number, + ry: number, + a: number, + fa: number, + fs: number, + x2: number, + y2: number +) => { + const phi = (a * Math.PI) / 180; + + const M = [ + [Math.cos(phi), Math.sin(phi)], + [-Math.sin(phi), Math.cos(phi)], + ]; + const V = [(x1 - x2) / 2, (y1 - y2) / 2]; + + const [x1p, y1p] = [M[0][0] * V[0] + M[0][1] * V[1], M[1][0] * V[0] + M[1][1] * V[1]]; + + rx = Math.abs(rx); + ry = Math.abs(ry); + + const lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry); + if (lambda > 1) { + rx = Math.sqrt(lambda) * rx; + ry = Math.sqrt(lambda) * ry; + } + + const sign = fa === fs ? -1 : 1; + + const co = + sign * + Math.sqrt( + Math.max(rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p, 0) / + (rx * rx * y1p * y1p + ry * ry * x1p * x1p) + ); + + const V2 = [(rx * y1p) / ry, (-ry * x1p) / rx]; + const Cp = [V2[0] * co, V2[1] * co]; + + const M2 = [ + [Math.cos(phi), -Math.sin(phi)], + [Math.sin(phi), Math.cos(phi)], + ]; + const V3 = [(x1 + x2) / 2, (y1 + y2) / 2]; + const C = [ + M2[0][0] * Cp[0] + M2[0][1] * Cp[1] + V3[0], + M2[1][0] * Cp[0] + M2[1][1] * Cp[1] + V3[1], + ]; + + return { x: C[0], y: C[1] }; +};