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
rodzic
30c959f930
commit
6aba6b665f
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
Ładowanie…
Reference in New Issue