This commit is contained in:
bobbert committed 2025-03-16 21:50:38 +00:00
1 parent 25e877cc3a
commit 2236dceac9
4 files changed
+441 -125

No files matched your search

+123
View File
@@ -0,0 +1,123 @@
/**
* Utility for debugging SVG rendering issues
*/
/**
* Adds a visible debug overlay to the SVG to test z-index rendering
* @param {SVGElement} svgElement - The SVG DOM element
*/
export function addDebugOverlay(svgElement) {
if (!svgElement) return;
const svgNS = "http://www.w3.org/2000/svg";
// Create a debug group at the highest level
const debugGroup = document.createElementNS(svgNS, "g");
debugGroup.setAttribute("id", "debug-overlay");
debugGroup.setAttribute(
"style",
"pointer-events: none; z-index: 10000; position: relative;"
);
// Add a semi-transparent rectangle across the entire SVG
const svgWidth = svgElement.viewBox?.baseVal?.width || 1000;
const svgHeight = svgElement.viewBox?.baseVal?.height || 1000;
const rect = document.createElementNS(svgNS, "rect");
rect.setAttribute("x", "0");
rect.setAttribute("y", "0");
rect.setAttribute("width", svgWidth.toString());
rect.setAttribute("height", svgHeight.toString());
rect.setAttribute("fill", "rgba(255, 255, 0, 0.1)");
rect.setAttribute("stroke", "none");
// Add some test circles at fixed positions
const testPositions = [
{ x: 50, y: 50, color: "red" },
{ x: 150, y: 150, color: "blue" },
{ x: 250, y: 250, color: "green" },
{ x: 350, y: 350, color: "purple" },
];
testPositions.forEach((pos, i) => {
const testCircle = document.createElementNS(svgNS, "circle");
testCircle.setAttribute("cx", pos.x.toString());
testCircle.setAttribute("cy", pos.y.toString());
testCircle.setAttribute("r", "20");
testCircle.setAttribute("fill", pos.color);
testCircle.setAttribute("fill-opacity", "0.5");
testCircle.setAttribute("stroke", "black");
testCircle.setAttribute("stroke-width", "2");
testCircle.setAttribute("class", "debug-circle");
debugGroup.appendChild(testCircle);
});
// Add the debug group to the SVG
svgElement.appendChild(debugGroup);
console.log("Debug overlay added to SVG");
}
/**
* Checks if clue points are visible and fixes rendering issues
* @param {SVGElement} svgElement - The SVG DOM element
*/
export function fixCluePointsRendering(svgElement) {
if (!svgElement) return;
// Find the clue points overlay group
let overlayGroup = svgElement.querySelector(".clue-points-overlay");
// If no overlay group exists, create one
if (!overlayGroup) {
console.log("No clue points overlay found, creating one");
overlayGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
overlayGroup.setAttribute("class", "clue-points-overlay");
overlayGroup.setAttribute(
"style",
"pointer-events: all !important; z-index: 9999 !important; position: relative !important;"
);
svgElement.appendChild(overlayGroup);
}
// Find all clue points
const cluePoints = svgElement.querySelectorAll(".clue-point");
const clickAreas = svgElement.querySelectorAll(".clue-click-area");
console.log(
`Found ${cluePoints.length} clue points and ${clickAreas.length} click areas`
);
// Ensure all clue points are in the overlay group and have proper styles
cluePoints.forEach((point, i) => {
// Clone the point to remove any existing event handlers and styling issues
const newPoint = point.cloneNode(true);
// Apply explicit styles to ensure visibility
newPoint.setAttribute(
"style",
"fill: #ff9800; stroke: #e65100; stroke-width: 3px; " +
"pointer-events: all !important; cursor: pointer !important; " +
"z-index: 9999 !important; position: relative !important; " +
"r: 14px; fill-opacity: 1; stroke-opacity: 1; " +
"filter: drop-shadow(0px 0px 5px rgba(0, 0, 0, 0.7));"
);
// Move to overlay group
if (point.parentNode !== overlayGroup) {
// Remove from current location
if (point.parentNode) {
point.parentNode.removeChild(point);
}
// Add to overlay group
overlayGroup.appendChild(newPoint);
}
});
// Move the overlay group to be the last child of the SVG
svgElement.appendChild(overlayGroup);
console.log("Clue points rendering fixed");
}
+199 -112
View File
@@ -20,23 +20,38 @@ function addCustomStyles() {
100% { stroke-width: 3px; transform: scale(1); filter: drop-shadow(0px 0px 3px rgba(255, 152, 0, 0.9)); }
}
/* Ensure SVG elements use CSS z-index correctly */
/* Force all SVG elements to be transparent to mouse events by default */
svg * {
transform-box: fill-box;
stroke-width: 2px;
pointer-events: none;
}
/* Create a stacking context for the overlay */
.clue-points-overlay {
pointer-events: all !important;
transition: fill 0.2s ease, stroke-width 0.2s ease, transform 0.2s ease !important;
z-index: 100;
position: relative;
position: relative !important;
z-index: 9999 !important;
}
/* Make clue points highly visible with strong styling */
.clue-point {
r: 14 !important;
cursor: pointer !important;
stroke-width: 3px !important;
pointer-events: all !important;
z-index: 9999 !important;
position: relative !important;
fill-opacity: 1 !important;
stroke-opacity: 1 !important;
filter: drop-shadow(0px 0px 3px rgba(0, 0, 0, 0.7)) !important;
}
.clue-point:hover {
fill: #ffb74d !important;
stroke: #ff6d00 !important;
stroke-width: 4px !important;
transform: scale(1.2) !important;
z-index: 200 !important;
filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.5)) !important;
stroke-width: 5px !important;
transform: scale(1.3) !important;
z-index: 10000 !important;
filter: drop-shadow(0px 0px 6px rgba(0, 0, 0, 0.8)) !important;
}
.clue-point-found {
@@ -61,11 +76,12 @@ function addCustomStyles() {
}
.clue-click-area {
opacity: 0;
fill: transparent;
stroke: transparent;
opacity: 0.01;
fill: transparent !important;
stroke: transparent !important;
pointer-events: all !important;
cursor: pointer !important;
z-index: 9998 !important;
}
`;
@@ -93,7 +109,7 @@ export async function populateMap(svgElement) {
// Clear any existing clue points and click areas to avoid duplicates
const existingCluePoints = svgElement.querySelectorAll(
".clue-point, .clue-click-area"
".clue-point, .clue-click-area, .clue-points-overlay"
);
existingCluePoints.forEach((element) => {
if (element.parentNode) {
@@ -108,111 +124,47 @@ export async function populateMap(svgElement) {
console.log(`Found ${allCircles.length} circles in SVG`);
if (allCircles.length === 0) {
console.error("No circles found in SVG");
return false;
}
console.error("No circles found in SVG. Creating fallback circles.");
// Hide all existing circles initially
allCircles.forEach((circle) => (circle.style.display = "none"));
// Create some fallback circles at predefined positions
const defs = document.createElementNS(
"http://www.w3.org/2000/svg",
"defs"
);
const positions = [
{ x: 100, y: 100 },
{ x: 200, y: 150 },
{ x: 300, y: 200 },
{ x: 400, y: 300 },
{ x: 500, y: 400 },
{ x: 600, y: 500 },
];
try {
// Fetch the number of clues from the API
const response = await fetch("/api/clues/count", {
method: "GET",
credentials: "include",
headers: {
"Cache-Control": "no-cache, no-store, must-revalidate",
Pragma: "no-cache",
},
positions.forEach((pos, i) => {
const circle = document.createElementNS(
"http://www.w3.org/2000/svg",
"circle"
);
circle.setAttribute("cx", pos.x.toString());
circle.setAttribute("cy", pos.y.toString());
circle.setAttribute("r", "5");
circle.setAttribute("fill", "transparent");
defs.appendChild(circle);
});
if (!response.ok) {
throw new Error(`Failed to fetch clue count: ${response.status}`);
}
// Add the defs to the SVG
svgElement.appendChild(defs);
const data = await response.json();
const clueCount = data.count;
console.log(`API returned clue count: ${clueCount}`);
if (!clueCount || clueCount <= 0) {
console.error("Invalid clue count returned from API");
// Now find all circles again
const fallbackCircles = Array.from(defs.querySelectorAll("circle"));
if (fallbackCircles.length > 0) {
console.log(`Created ${fallbackCircles.length} fallback circles`);
return await processCircles(svgElement, fallbackCircles);
} else {
return false;
}
// Sort circles by vertical position (top to bottom)
const sortedCircles = allCircles.sort((a, b) => {
const aY = parseFloat(a.getAttribute("cy") || "0");
const bY = parseFloat(b.getAttribute("cy") || "0");
return aY - bY;
});
// Determine how many circles to show
const circlesToShow = Math.min(sortedCircles.length, clueCount);
console.log(
`Will create ${circlesToShow} clue points from sorted circles`
);
const svgNamespace = "http://www.w3.org/2000/svg";
// Create a dedicated overlay group for clue points to ensure they're on top
let overlayGroup = svgElement.querySelector(".clue-points-overlay");
if (!overlayGroup) {
overlayGroup = document.createElementNS(svgNamespace, "g");
overlayGroup.setAttribute("class", "clue-points-overlay");
overlayGroup.style.pointerEvents = "all";
// Append to the end of SVG to ensure it's rendered last (on top)
svgElement.appendChild(overlayGroup);
}
// Create interactive clue points by cloning and enhancing each original circle
for (let i = 0; i < circlesToShow; i++) {
const originalCircle = sortedCircles[i];
// Get original circle attributes
const cx = originalCircle.getAttribute("cx") || "0";
const cy = originalCircle.getAttribute("cy") || "0";
const r = Math.max(
parseFloat(originalCircle.getAttribute("r") || "5"),
12
);
// Create a click area (larger invisible circle for easier clicking)
const clickArea = document.createElementNS(svgNamespace, "circle");
clickArea.setAttribute("cx", cx);
clickArea.setAttribute("cy", cy);
clickArea.setAttribute("r", (r * 2).toString());
clickArea.setAttribute("class", "clue-click-area");
clickArea.setAttribute("data-clue-id", (i + 1).toString());
clickArea.setAttribute("data-index", i.toString());
// Ensure click area has high z-index
clickArea.style.zIndex = "1000";
// Create a new visible circle for the clue point
const cluePoint = document.createElementNS(svgNamespace, "circle");
cluePoint.setAttribute("cx", cx);
cluePoint.setAttribute("cy", cy);
cluePoint.setAttribute("r", r.toString());
cluePoint.setAttribute("class", "clue-point clue-point-locked");
cluePoint.setAttribute("data-clue-id", (i + 1).toString());
cluePoint.setAttribute("data-index", i.toString());
// Ensure clue point has high z-index
cluePoint.style.zIndex = "1001";
// Add a tooltip for accessibility
const tooltip = document.createElementNS(svgNamespace, "title");
tooltip.textContent = `Clue #${i + 1}`;
cluePoint.appendChild(tooltip);
// Add the new elements to the overlay group instead of original parent
overlayGroup.appendChild(clickArea);
overlayGroup.appendChild(cluePoint);
}
console.log(`Successfully created ${circlesToShow} clue points`);
return true;
} catch (error) {
console.error("Error fetching clue count:", error);
return false;
} else {
return await processCircles(svgElement, allCircles);
}
} catch (error) {
console.error("Error populating map:", error);
@@ -220,6 +172,141 @@ export async function populateMap(svgElement) {
}
}
/**
* Process circles to create clue points
* @param {SVGElement} svgElement - The SVG DOM element
* @param {Array} circles - Array of circle elements
* @returns {Promise<boolean>} - Success flag
*/
async function processCircles(svgElement, circles) {
try {
// Hide original circles
circles.forEach((circle) => (circle.style.display = "none"));
// Fetch the number of clues from the API
const response = await fetch("/api/clues/count", {
method: "GET",
credentials: "include",
headers: {
"Cache-Control": "no-cache, no-store, must-revalidate",
Pragma: "no-cache",
},
});
if (!response.ok) {
throw new Error(`Failed to fetch clue count: ${response.status}`);
}
const data = await response.json();
const clueCount = parseInt(data.count, 10) || 0;
console.log(`API returned clue count: ${clueCount}`);
if (clueCount <= 0) {
console.error("Invalid clue count returned from API, using default of 5");
clueCount = 5; // Fallback to 5 clues if API returns invalid data
}
// Sort circles by vertical position (top to bottom)
const sortedCircles = circles.sort((a, b) => {
const aY = parseFloat(a.getAttribute("cy") || "0");
const bY = parseFloat(b.getAttribute("cy") || "0");
return aY - bY;
});
// Determine how many circles to show
const circlesToShow = Math.min(sortedCircles.length, clueCount);
console.log(`Will create ${circlesToShow} clue points from sorted circles`);
// Create the overlay element that will go on top of everything
const svgNamespace = "http://www.w3.org/2000/svg";
// Remove any old overlay group
const oldOverlay = svgElement.querySelector(".clue-points-overlay");
if (oldOverlay && oldOverlay.parentNode) {
oldOverlay.parentNode.removeChild(oldOverlay);
}
// Create a new overlay group that will be the last child of the SVG
// so it renders on top of everything else
const overlayGroup = document.createElementNS(svgNamespace, "g");
overlayGroup.setAttribute("class", "clue-points-overlay");
overlayGroup.setAttribute(
"style",
"pointer-events: all !important; z-index: 9999 !important;"
);
// Add the overlay group as the LAST element in the SVG to ensure proper rendering order
svgElement.appendChild(overlayGroup);
// Create interactive clue points
for (let i = 0; i < circlesToShow; i++) {
const originalCircle = sortedCircles[i];
// Get original circle attributes
const cx = originalCircle.getAttribute("cx") || "0";
const cy = originalCircle.getAttribute("cy") || "0";
const r = Math.max(
parseFloat(originalCircle.getAttribute("r") || "5"),
14
);
// Create a click area (larger invisible circle for easier clicking)
const clickArea = document.createElementNS(svgNamespace, "circle");
clickArea.setAttribute("cx", cx);
clickArea.setAttribute("cy", cy);
clickArea.setAttribute("r", (r * 2).toString());
clickArea.setAttribute("class", "clue-click-area");
clickArea.setAttribute("data-clue-id", (i + 1).toString());
clickArea.setAttribute("data-index", i.toString());
clickArea.setAttribute(
"style",
"opacity: 0.01; pointer-events: all !important; cursor: pointer !important; z-index: 9998 !important;"
);
// Create a new visible circle for the clue point with explicit inline styles
const cluePoint = document.createElementNS(svgNamespace, "circle");
cluePoint.setAttribute("cx", cx);
cluePoint.setAttribute("cy", cy);
cluePoint.setAttribute("r", r.toString());
cluePoint.setAttribute("class", "clue-point clue-point-locked");
cluePoint.setAttribute("data-clue-id", (i + 1).toString());
cluePoint.setAttribute("data-index", i.toString());
cluePoint.setAttribute(
"style",
"fill: #9e9e9e; stroke: #757575; stroke-width: 3px; cursor: pointer; " +
"pointer-events: all !important; z-index: 9999 !important; position: relative !important; " +
"fill-opacity: 1 !important; stroke-opacity: 1 !important; " +
"filter: drop-shadow(0px 0px 3px rgba(0, 0, 0, 0.7));"
);
// Add a tooltip for accessibility
const tooltip = document.createElementNS(svgNamespace, "title");
tooltip.textContent = `Clue #${i + 1}`;
cluePoint.appendChild(tooltip);
// Add the new elements to the overlay group
overlayGroup.appendChild(clickArea);
overlayGroup.appendChild(cluePoint);
}
// Set explicit width and height on the overlay group to ensure it covers the whole SVG
const svgWidth = svgElement.getAttribute("width") || "100%";
const svgHeight = svgElement.getAttribute("height") || "100%";
overlayGroup.setAttribute("width", svgWidth);
overlayGroup.setAttribute("height", svgHeight);
// Bring the overlay forward by appending it again
// This ensures it's the last child of the SVG
svgElement.appendChild(overlayGroup);
console.log(`Successfully created ${circlesToShow} clue points`);
return true;
} catch (error) {
console.error("Error processing circles:", error);
return false;
}
}
/**
* Updates the clue points based on the latest clue data
* @param {SVGElement} svgElement - The SVG DOM element