Add latest gh-icon changes

This commit is contained in:
Eric Fennis
2023-05-10 00:12:35 +02:00
parent 44b602e6e4
commit ce16c3c569
5 changed files with 243 additions and 21 deletions
+20 -3
View File
@@ -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[^>]*>|<\/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')
@@ -0,0 +1,71 @@
import React from 'react';
interface BackdropProps {
src: string
backdropString: string
}
const Backdrop = ({ src, backdropString }: BackdropProps): JSX.Element => {
return (
<>
<defs xmlns="http://www.w3.org/2000/svg">
<pattern
id="pattern"
width=".1"
height=".1"
patternUnits="userSpaceOnUse"
patternTransform="rotate(45 50 50)"
>
<line stroke="red" strokeWidth={0.1} y2={1} />
<line stroke="red" strokeWidth={0.1} y2={1} />
</pattern>
</defs>
<mask id="svg-preview-backdrop-mask-outline" maskUnits="userSpaceOnUse">
<g stroke="#fff" dangerouslySetInnerHTML={{ __html: backdropString }} />
<g dangerouslySetInnerHTML={{ __html: src }} strokeWidth={2.05} />
</mask>
<mask id="svg-preview-backdrop-mask-fill" maskUnits="userSpaceOnUse">
<g stroke="#fff" dangerouslySetInnerHTML={{ __html: backdropString }} />
<g dangerouslySetInnerHTML={{ __html: src }} strokeWidth={2.05} />
<g strokeWidth={1.75} dangerouslySetInnerHTML={{ __html: backdropString }} />
</mask>
<g
strokeWidth={2.25}
stroke="url(#pattern)"
mask={'url(#svg-preview-backdrop-mask-outline)'}
>
<rect
x="0"
y="0"
width="24"
height="24"
fill="url(#pattern)"
opacity={0.5}
stroke="none"
/>
</g>
<rect
x="0"
y="0"
width="24"
height="24"
fill="url(#pattern)"
stroke="none"
mask={'url(#svg-preview-backdrop-mask-fill)'}
/>
<rect
x="0"
y="0"
width="24"
height="24"
fill="red"
opacity={0.5}
stroke="none"
mask={'url(#svg-preview-backdrop-mask-fill)'}
/>
</>
)
}
export default Backdrop;
+33
View File
@@ -173,6 +173,31 @@ const ControlPath = ({
);
};
const Radii = ({
paths,
...props
}: { paths: Path[] } & PathProps<
'strokeWidth' | 'stroke' | 'strokeDasharray' | 'strokeOpacity',
any
>) => {
return (
<g className="svg-preview-radii-group" {...props}>
{paths
.filter(({ circle }) => circle)
.map(({ c, prev, next, circle: { x, y, r } }) =>
c.name === 'circle' ? (
<path d={`M${x} ${y}h.01`} />
) : (
<>
<path d={`M${prev.x} ${prev.y} ${x} ${y} ${next.x} ${next.y}`} />
<circle cy={y} cx={x} r={r} />
</>
)
)}
</g>
);
};
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',
]}
/>
<Radii
paths={paths}
strokeWidth={0.12}
strokeDasharray="0 0.25 0.25"
stroke="#777"
strokeOpacity={0.3}
/>
<ControlPath radius={1} paths={paths} pointSize={1} stroke="#fff" strokeWidth={0.125} />
{children}
</svg>
+1
View File
@@ -8,6 +8,7 @@ export type Path = {
prev: Point;
next: Point;
isStart: boolean;
circle: { x: number; y: number; r: number };
c: ReturnType<typeof getCommands>[number];
};
+118 -18
View File
@@ -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('<svg') ? src : `<svg>${src}</svg>`));
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('<svg') ? src : `<svg>${src}</svg>`);
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] };
};