diff --git a/web/app.js b/web/app.js index 5bf737b..ed7feb8 100644 --- a/web/app.js +++ b/web/app.js @@ -223,20 +223,40 @@ function makeNotePin(note, idx) { c.setAttribute("stroke", "#1a1208"); c.setAttribute("stroke-width", "3"); g.appendChild(c); - const t = document.createElementNS(SVG_NS, "text"); - t.setAttribute("y", 9); - t.setAttribute("font-size", "26"); - t.setAttribute("text-anchor", "middle"); - t.setAttribute("fill", "#1a1208"); - t.setAttribute("font-weight", "900"); - t.setAttribute("font-style", "italic"); - t.setAttribute("font-family", "Georgia, serif"); - t.textContent = "i"; - g.appendChild(t); + const i = document.createElementNS(SVG_NS, "text"); + i.setAttribute("y", 9); + i.setAttribute("font-size", "26"); + i.setAttribute("text-anchor", "middle"); + i.setAttribute("fill", "#1a1208"); + i.setAttribute("font-weight", "900"); + i.setAttribute("font-style", "italic"); + i.setAttribute("font-family", "Georgia, serif"); + i.textContent = "i"; + g.appendChild(i); - const title = document.createElementNS(SVG_NS, "title"); - title.textContent = note.text || "(empty)"; - g.appendChild(title); + // Always-visible label: text rendered next to the pin in image-pixel + // space, with a dark stroke for readability over the parchment. + if (note.text) { + const label = document.createElementNS(SVG_NS, "text"); + label.setAttribute("class", "note-label"); + label.setAttribute("x", 30); // sits to the right of the circle + label.setAttribute("y", 10); + label.setAttribute("font-size", "28"); + label.setAttribute("font-family", "system-ui, sans-serif"); + label.setAttribute("font-weight", "600"); + label.setAttribute("fill", "#fff7d6"); + label.setAttribute("stroke", "#1a1208"); + label.setAttribute("stroke-width", "5"); + label.setAttribute("paint-order", "stroke"); + // Truncate very long text on the SVG side to ~80 chars so it doesn't + // overflow the map. The full text is still in the data tooltip. + const truncated = note.text.length > 80 ? note.text.slice(0, 77) + "..." : note.text; + label.textContent = truncated; + g.appendChild(label); + } + + // Custom-tooltip data (instant, used by ensureTooltip). + g.dataset.tooltip = note.text || "(empty)"; // drag-to-move + right-click delete + double-click to edit text g.addEventListener("pointerdown", (e) => { @@ -325,10 +345,8 @@ function makeEnemyPin(e) { t.textContent = "☠"; g.appendChild(t); } - const title = document.createElementNS(SVG_NS, "title"); const tag = cls === 5 ? " (rare)" : (e.skippable ? " (skippable)" : ""); - title.textContent = e.name + tag; - g.appendChild(title); + g.dataset.tooltip = e.name + tag; return g; } @@ -361,9 +379,9 @@ function makeIconMarker(ic) { g.appendChild(r); } if (ic.comment) { - const title = document.createElementNS(SVG_NS, "title"); - title.textContent = ic.comment; - g.appendChild(title); + g.dataset.tooltip = ic.comment; + } else if (ic.type) { + g.dataset.tooltip = ic.type; } return g; } @@ -559,6 +577,60 @@ function zoomBy(factor, anchorX, anchorY) { applyView(); } +/* --- instant tooltip ------------------------------------------------------ + * SVG uses the OS tooltip with a long delay. We render our own div + * so it shows the moment the cursor enters a pin/icon. Driven by a single + * pointer listener on #overlay that walks up to find any [data-tooltip]. + */ +function ensureTooltip() { + let el = document.getElementById("custom-tooltip"); + if (el) return el; + el = document.createElement("div"); + el.id = "custom-tooltip"; + el.className = "custom-tooltip"; + document.body.appendChild(el); + return el; +} + +function hookTooltip() { + const tip = ensureTooltip(); + const host = $("canvas-host"); + let activeText = ""; + + host.addEventListener("pointermove", (e) => { + const target = e.target.closest("[data-tooltip]"); + if (!target) { + tip.classList.remove("show"); + activeText = ""; + return; + } + const text = target.dataset.tooltip; + if (!text) { + tip.classList.remove("show"); + return; + } + if (text !== activeText) { + tip.textContent = text; + activeText = text; + } + // Position near cursor, but keep on-screen + const pad = 14; + let x = e.clientX + pad; + let y = e.clientY + pad; + const w = tip.offsetWidth, h = tip.offsetHeight; + if (x + w + pad > window.innerWidth) x = e.clientX - w - pad; + if (y + h + pad > window.innerHeight) y = e.clientY - h - pad; + tip.style.left = x + "px"; + tip.style.top = y + "px"; + tip.classList.add("show"); + }); + + host.addEventListener("pointerleave", () => { + tip.classList.remove("show"); + activeText = ""; + }); +} + function hookCanvasPanZoom() { const host = $("canvas-host"); @@ -847,6 +919,7 @@ function hookEvents() { }); window.addEventListener("hashchange", loadFromHash); hookCanvasPanZoom(); + hookTooltip(); } init().catch((e) => { diff --git a/web/style.css b/web/style.css index 66b4b3a..9661b57 100644 --- a/web/style.css +++ b/web/style.css @@ -373,6 +373,26 @@ body { } .waypoint-list li button:hover { color: var(--boss); } +/* --- instant tooltip ------------------------------------------------------ */ +.custom-tooltip { + position: fixed; + pointer-events: none; + z-index: 200; + background: rgba(20, 18, 24, 0.96); + color: var(--text); + border: 1px solid var(--accent); + border-radius: 4px; + padding: 6px 10px; + font-size: 13px; + line-height: 1.35; + max-width: 360px; + box-shadow: 0 6px 18px rgba(0, 0, 0, .45); + opacity: 0; + transition: opacity .08s ease; + white-space: pre-wrap; +} +.custom-tooltip.show { opacity: 1; } + /* --- toast ---------------------------------------------------------------- */ .toast { position: fixed;