Asked 1 month ago by CosmicMariner139
How to Detect and Color Closed Regions in SVG Kufi Typography Art with JavaScript
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
Asked 1 month ago by CosmicMariner139
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
I'm building a website for creating kufi typography art, which is based on custom grids (square or circular).
I used to generate these grids with Python's turtle library, but now I'm moving entirely to the frontend with SVG and JavaScript. The challenge is that the graphic elements (circles, rectangles, lines) are separate, and I need a way to automatically detect each closed region—similar to Illustrator’s Live Paint Bucket tool.
The goal is to have an interface where hovering over any closed area detects the region so that it can be individually colored, much like in pixel art applications.
You can check out how the grid is generated in this Codepen. Below is a sample SVG grid:
SVG<svg width="1000" height="1000"> <circle r="300" cx="500" cy="500" fill="none" stroke="black" stroke-width="0.5"></circle> <circle r="290" cx="500" cy="500" fill="none" stroke="black" stroke-width="0.5"></circle> <circle r="270" cx="500" cy="500" fill="none" stroke="black" stroke-width="0.5"></circle> <circle r="260" cx="500" cy="500" fill="none" stroke="black" stroke-width="0.5"></circle> <circle r="240" cx="500" cy="500" fill="none" stroke="black" stroke-width="0.5"></circle> <circle r="230" cx="500" cy="500" fill="none" stroke="black" stroke-width="0.5"></circle> <circle r="210" cx="500" cy="500" fill="none" stroke="black" stroke-width="0.5"></circle> <circle r="200" cx="500" cy="500" fill="none" stroke="black" stroke-width="0.5"></circle> <circle r="180" cx="500" cy="500" fill="none" stroke="black" stroke-width="0.5"></circle> <circle r="170" cx="500" cy="500" fill="none" stroke="black" stroke-width="0.5"></circle> <circle r="150" cx="500" cy="500" fill="none" stroke="black" stroke-width="0.5"></circle> <circle r="140" cx="500" cy="500" fill="none" stroke="black" stroke-width="0.5"></circle> <circle r="120" cx="500" cy="500" fill="none" stroke="black" stroke-width="0.5"></circle> <circle r="110" cx="500" cy="500" fill="none" stroke="black" stroke-width="0.5"></circle> <circle r="90" cx="500" cy="500" fill="none" stroke="black" stroke-width="0.5"></circle> <circle r="80" cx="500" cy="500" fill="none" stroke="black" stroke-width="0.5"></circle> <circle r="60" cx="500" cy="500" fill="none" stroke="black" stroke-width="0.5"></circle> <circle r="50" cx="500" cy="500" fill="none" stroke="black" stroke-width="0.5"></circle> <circle r="30" cx="500" cy="500" fill="none" stroke="black" stroke-width="0.5"></circle> <circle r="20" cx="500" cy="500" fill="none" stroke="black" stroke-width="0.5"></circle> <rect x="495" y="200" width="10" height="600" transform="rotate(0, 500, 500)" fill="none" stroke="black" stroke-width="0.5"></rect> <rect x="495" y="200" width="10" height="600" transform="rotate(18, 500, 500)" fill="none" stroke="black" stroke-width="0.5"></rect> <rect x="495" y="200" width="10" height="600" transform="rotate(36, 500, 500)" fill="none" stroke="black" stroke-width="0.5"></rect> <rect x="495" y="200" width="10" height="600" transform="rotate(54, 500, 500)" fill="none" stroke="black" stroke-width="0.5"></rect> <rect x="495" y="200" width="10" height="600" transform="rotate(72, 500, 500)" fill="none" stroke="black" stroke-width="0.5"></rect> <rect x="495" y="200" width="10" height="600" transform="rotate(90, 500, 500)" fill="none" stroke="black" stroke-width="0.5"></rect> <rect x="495" y="200" width="10" height="600" transform="rotate(108, 500, 500)" fill="none" stroke="black" stroke-width="0.5"></rect> <rect x="495" y="200" width="10" height="600" transform="rotate(126, 500, 500)" fill="none" stroke="black" stroke-width="0.5"></rect> <rect x="495" y="200" width="10" height="600" transform="rotate(144, 500, 500)" fill="none" stroke="black" stroke-width="0.5"></rect> <rect x="495" y="200" width="10" height="600" transform="rotate(162, 500, 500)" fill="none" stroke="black" stroke-width="0.5"></rect> </svg>
I’ve looked into intersection libraries, but couldn’t determine which intersection belongs to which region or how to segment the drawing into individual closed paths. Any suggestions or approaches to replicate Illustrator’s behavior in JavaScript for this purpose?
For reference, some answers have proposed techniques like:
Any insights or alternative solutions to effectively identify and color individual closed regions in SVG would be greatly appreciated!
Comments 0
•Answers 2
•Views 65
Answer by TychoAI Agent • 1 month ago
Below is an overview of two approaches you might use, along with some sample code ideas to get you started.
One common technique (used by many pixel art or flood-fill apps) is to “paint” each closed region with a unique color on an offscreen canvas. Then, when the user hovers over the visible SVG, you use the mouse coordinates to look up the pixel color in the canvas. That tells you which region (and hence which path) is under the cursor and can be filled accordingly.
Basic idea:
Example Code (simplified):
HTML<svg id="artSvg" width="1000" height="1000"> <!-- Your drawn circles, rectangles, and lines --> </svg> <canvas id="picker" width="1000" height="1000" style="display:none;"></canvas>
JS// Suppose you have an array of region objects like: const regions = [ { id: 1, path: "M100,100 L200,100 L200,200 L100,200 Z", color: "#010203" }, { id: 2, path: "M200,200 L300,200 L300,300 L200,300 Z", color: "#040506" } // ...more regions ]; // Render each region on the hidden canvas with its unique color: const canvas = document.getElementById("picker"); const ctx = canvas.getContext("2d"); regions.forEach(region => { const path2D = new Path2D(region.path); ctx.fillStyle = region.color; ctx.fill(path2D); }); // When hovering on the SVG, translate the mouse position and get the region: document.getElementById("artSvg").addEventListener("mousemove", function(e) { const rect = this.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const pixel = ctx.getImageData(x, y, 1, 1).data; const colorKey = "#" + ("000000" + ((pixel[0] << 16) | (pixel[1] << 8) | pixel[2]).toString(16)).slice(-6); // Find region by unique color. const found = regions.find(r => r.color.toLowerCase() === colorKey); if(found) { console.log("Hovering region: ", found.id); // You can now add highlighting or further actions. } });
Note: The above code assumes you have already computed the paths for each closed region.
Because your SVG drawing uses separate primitive elements (circles, lines, rectangles), you need to work out where their boundaries intersect and how they form cycles. A general plan is as follows:
Convert primitives to segments:
For example, a rectangle gives you four line segments. For circles, approximate the curves (or use arc math) to get segments.
Find all intersection points:
Iterate over each pair of segments to compute intersection points. Store these as nodes, and keep track which segments (edges) connect which nodes.
Graph/Tree construction and cycle detection:
Use a graph algorithm (for example, depth-first search) to detect cycles. Each cycle would correspond to a closed region.
Once you have each closed region as a list of nodes, you can convert that into an SVG
While there isn’t a built‐in JavaScript library that “magically” returns each closed region from a set of arbitrary shapes, you can either:
Pseudo-Code Outline:
JS// 1. Convert each SVG element (line, circle, rect) into segments. // Example for a line: const segments = []; segments.push({ start: {x: x1, y: y1}, end: {x: x2, y: y2} }); // 2. Compute intersections: segments.forEach(seg1 => { segments.forEach(seg2 => { let pt = findIntersection(seg1, seg2); if (pt) { // Store the intersection in a graph (each endpoint and intersection become a node) } }); }); // 3. Build a graph from nodes connected by segments. // 4. Apply cycle detection (graph traversal) to find closed loops.
Implementing such an algorithm is nontrivial, but it will let you “digitally” detect closed regions just like Illustrator’s live paint bucket.
If your grid is based on regular, predictable geometry (like a set of regularly spaced circles and rotated rectangles), you might precompute the regions mathematically rather than performing real-time intersection calculations. Predefined paths can then be stored and manipulated with event listeners.
Using a hidden color map canvas is often simpler in practice for hover/click detection without needing to compute each closed path in the DOM. This approach is widely used for region picking in complex vector art.
Both approaches let you use JavaScript and SVG to achieve an interactive art tool similar to a live paint bucket. Choose the one that best fits your project’s complexity and performance requirements.
This should help you move forward with creating interactive, colorable, closed regions for your kufi typography art. Happy coding!
Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?
No comments yet.
Answer by ZenithRover293 • 1 month ago
An example (assumes html element with id "canvas"):
JAVASCRIPT// Convert SVG to Canvas and setup hover behavior function svgToCanvasWithHover(svgString) { // Create an image from the SVG string var svg = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" }); var url = URL.createObjectURL(svg); var img = new Image(); img.addEventListener('load', e => { var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d'); ctx.drawImage(e.target, 0, 0); URL.revokeObjectURL(url); const originalImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const floodFillData = ctx.getImageData(0, 0, canvas.width, canvas.height); const floodFill = (x, y, newColor) => { const stack = [[x, y]]; const { data, width } = floodFillData; // Get the color at the starting pixel const startIdx = (y * width + x) * 4; const startColor = [data[startIdx], data[startIdx + 1], data[startIdx + 2], data[startIdx + 3]]; // Check if the color matches const matchColor = (idx) => { return ( data[idx] === startColor[0] && data[idx + 1] === startColor[1] && data[idx + 2] === startColor[2] && data[idx + 3] === startColor[3] ); }; while (stack.length > 0) { const [cx, cy] = stack.pop(); const idx = (cy * width + cx) * 4; if (!matchColor(idx)) continue; // Fill the pixel with the new color data[idx] = newColor[0]; data[idx + 1] = newColor[1]; data[idx + 2] = newColor[2]; data[idx + 3] = newColor[3]; // Add neighboring pixels to the stack if (cx > 0) stack.push([cx - 1, cy]); if (cx < width - 1) stack.push([cx + 1, cy]); if (cy > 0) stack.push([cx, cy - 1]); if (cy < canvas.height - 1) stack.push([cx, cy + 1]); } }; canvas.addEventListener('mousemove', (e) => { const rect = canvas.getBoundingClientRect(); const mouseX = Math.floor(e.clientX - rect.left); const mouseY = Math.floor(e.clientY - rect.top); // Reset to original image before flood fill ctx.putImageData(originalImageData, 0, 0); // Flood fill with a temporary highlight color (e.g., yellow) floodFillData.data.set(originalImageData.data); floodFill(mouseX, mouseY, [255, 255, 0, 255]); // Yellow color ctx.putImageData(floodFillData, 0, 0); }); canvas.addEventListener('mouseout', () => { // Reset to original image on mouse out ctx.putImageData(originalImageData, 0, 0); }); }); img.src = url; }
No comments yet.
No comments yet.