Add mouseover tooltips with debugging information (#35)

This adds a bit of debugging information on mouseover.

For example, a tooltip with the text `bird@tail.arm[0]` can be interpreted as "a bird symbol attached to the tail symbol's first arm attachment point."

The implementation is a bit funky: we basically annotate the SVG DOM with various `data` attributes, and on mouseover we traverse the DOM from the element the mouse is over all the way up to the SVG root element, picking out relevant `data` attributes and building a tooltip out of it.  This ended up being easier than e.g. passing a bunch of props down the whole tree in React.
pull/37/head
Atul Varma 2021-02-27 13:28:44 -05:00 zatwierdzone przez GitHub
rodzic 30c959f930
commit 6aba6b665f
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
8 zmienionych plików z 144 dodań i 37 usunięć

Wyświetl plik

@ -11,6 +11,15 @@ html, body {
background: #eee url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" fill-opacity=".1" ><rect x="200" width="200" height="200" /><rect y="200" width="200" height="200" /></svg>');
background-size: 20px 20px;
}
.hover-debug-helper {
font-family: "Consolas", "Monaco", monospace;
color: white;
background: rgba(0, 0, 0, 0.75);
padding: 4px;
margin-top: 4px;
margin-left: 4px;
}
</style>
<noscript>
<p>Alas, you need JavaScript to peruse this page.</p>

Wyświetl plik

@ -152,7 +152,13 @@ const NestedCreatureSymbol: React.FC<ChildCreatureSymbolProps> = ({
scale={t.scaling}
rotate={0}
>
{symbol}
<g
data-attach-parent={parent.name}
data-attach-type="nesting"
data-attach-index={nestIndex}
>
{symbol}
</g>
</AttachmentTransform>
);
}
@ -199,7 +205,13 @@ const AttachedCreatureSymbol: React.FC<
scale={{ x: ctx.attachmentScale * xFlip, y: ctx.attachmentScale }}
rotate={xFlip * t.rotation}
>
{symbol}
<g
data-attach-parent={parent.name}
data-attach-type={attachTo}
data-attach-index={attachIndex}
>
{symbol}
</g>
</AttachmentTransform>
);
}

Wyświetl plik

@ -0,0 +1,74 @@
import React, { useState } from "react";
function getTargetPathInfo(target: SVGElement): string[] {
const path: string[] = [];
let node = target;
while (true) {
const {
specType,
specIndex,
symbolName,
attachParent,
attachType,
attachIndex,
} = node.dataset;
if (specType && specIndex) {
path.unshift(`${specType}[${specIndex}]`);
} else if (symbolName) {
path.unshift(symbolName);
} else if (attachParent && attachType && attachIndex && path.length) {
const i = path.length - 1;
path[i] = `${path[i]}@${attachParent}.${attachType}[${attachIndex}]`;
}
if (node.parentNode instanceof SVGElement) {
node = node.parentNode;
} else {
break;
}
}
return path;
}
export const HoverDebugHelper: React.FC<{
children: any;
}> = (props) => {
type HoverInfo = {
x: number;
y: number;
text: string;
};
let [hoverInfo, setHoverInfo] = useState<HoverInfo | null>(null);
const clearHoverInfo = () => setHoverInfo(null);
const handleMouseMove: React.MouseEventHandler = (e) => {
const { target } = e;
if (target instanceof SVGElement) {
const x = e.clientX + window.scrollX;
const y = e.clientY + window.scrollY;
const path = getTargetPathInfo(target);
if (path.length) {
setHoverInfo({ x, y, text: path.join(".") });
return;
}
}
clearHoverInfo();
};
return (
<div onMouseMove={handleMouseMove} onMouseLeave={clearHoverInfo}>
{hoverInfo && (
<div
className="hover-debug-helper"
style={{
position: "absolute",
pointerEvents: "none",
top: `${hoverInfo.y}px`,
left: `${hoverInfo.x}px`,
}}
>
{hoverInfo.text}
</div>
)}
{props.children}
</div>
);
};

Wyświetl plik

@ -14,6 +14,7 @@ import {
CreatureSymbol,
CreatureSymbolProps,
} from "../creature-symbol";
import { HoverDebugHelper } from "../hover-debug-helper";
const DEFAULT_BG_COLOR = "#858585";
@ -259,9 +260,11 @@ export const CreaturePage: React.FC<{}> = () => {
<button onClick={handleSvgExport}>Export SVG</button>
</p>
<CreatureContext.Provider value={ctx}>
<AutoSizingSvg padding={20} ref={svgRef} bgColor={bgColor}>
<g transform="scale(0.5 0.5)">{creature}</g>
</AutoSizingSvg>
<HoverDebugHelper>
<AutoSizingSvg padding={20} ref={svgRef} bgColor={bgColor}>
<g transform="scale(0.5 0.5)">{creature}</g>
</AutoSizingSvg>
</HoverDebugHelper>
</CreatureContext.Provider>
</>
);

Wyświetl plik

@ -8,6 +8,7 @@ import {
import { SvgVocabulary } from "../svg-vocabulary";
import { SvgSymbolContext } from "../svg-symbol";
import { SymbolContextWidget } from "../symbol-context-widget";
import { HoverDebugHelper } from "../hover-debug-helper";
type SvgSymbolProps = {
data: SvgSymbolData;
@ -42,29 +43,31 @@ export const VocabularyPage: React.FC<{}> = () => {
<>
<h1>Mystic Symbolic Vocabulary</h1>
<SymbolContextWidget ctx={ctx} onChange={setCtx} />
{SvgVocabulary.map((symbolData) => (
<div
key={symbolData.name}
style={{
display: "inline-block",
border: "1px solid black",
margin: "4px",
}}
>
<HoverDebugHelper>
{SvgVocabulary.map((symbolData) => (
<div
key={symbolData.name}
style={{
backgroundColor: "black",
color: "white",
padding: "4px",
display: "inline-block",
border: "1px solid black",
margin: "4px",
}}
>
{symbolData.name}
<div
style={{
backgroundColor: "black",
color: "white",
padding: "4px",
}}
>
{symbolData.name}
</div>
<div className="checkerboard-bg" style={{ lineHeight: 0 }}>
<SvgSymbol data={symbolData} scale={0.25} {...ctx} />
</div>
</div>
<div className="checkerboard-bg" style={{ lineHeight: 0 }}>
<SvgSymbol data={symbolData} scale={0.25} {...ctx} />
</div>
</div>
))}
))}
</HoverDebugHelper>
</>
);
};

Wyświetl plik

@ -31,6 +31,7 @@ export type AttachmentPointType = keyof AttachmentPointSpecs;
export type AttachmentPoint = PointWithNormal & {
type: AttachmentPointType;
index: number;
};
export const ATTACHMENT_POINT_TYPES: AttachmentPointType[] = [
@ -46,8 +47,10 @@ export function* iterAttachmentPoints(specs: Specs): Iterable<AttachmentPoint> {
for (let type of ATTACHMENT_POINT_TYPES) {
const points = specs[type];
if (points) {
let index = 0;
for (let point of points) {
yield { ...point, type };
yield { ...point, type, index };
index += 1;
}
}
}

Wyświetl plik

@ -78,17 +78,18 @@ function reactifySvgSymbolElement(
strokeWidth = ctx.uniformStrokeWidth;
vectorEffect = "non-scaling-stroke";
}
const props: typeof el.props = {
...el.props,
id: undefined,
vectorEffect,
strokeWidth,
fill,
stroke,
key,
};
return React.createElement(
el.tagName,
{
...el.props,
id: undefined,
vectorEffect,
strokeWidth,
fill,
stroke,
key,
},
props,
el.children.map(reactifySvgSymbolElement.bind(null, ctx))
);
}
@ -99,9 +100,9 @@ export const SvgSymbolContent: React.FC<
const d = props.data;
return (
<>
<g data-symbol-name={d.name}>
{props.data.layers.map(reactifySvgSymbolElement.bind(null, props))}
{props.showSpecs && d.specs && <VisibleSpecs specs={d.specs} />}
</>
</g>
);
};

Wyświetl plik

@ -21,7 +21,7 @@ const VisibleAttachmentPoint: React.FC<{
const color = colors.ATTACHMENT_POINT_COLORS[ap.type];
return (
<>
<g data-spec-type={ap.type} data-spec-index={ap.index}>
<circle
fill={color}
r={ATTACHMENT_POINT_RADIUS}
@ -38,7 +38,7 @@ const VisibleAttachmentPoint: React.FC<{
stroke={color}
strokeWidth={ATTACHMENT_POINT_NORMAL_STROKE}
/>
</>
</g>
);
};
@ -48,6 +48,8 @@ const BoundingBoxes: React.FC<{ fill: string; bboxes: BBox[] }> = (props) => (
const [width, height] = getBoundingBoxSize(b);
return (
<rect
data-spec-type="nesting"
data-spec-index={i}
opacity={SPEC_OPACITY}
key={i}
x={b.x.min}