Created
March 10, 2026 01:48
-
-
Save steren/e66b9e396c0f46ee104da479cb23126e to your computer and use it in GitHub Desktop.
Web web browser
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>CanvasSurf Browser</title> | |
| <style> | |
| /* Basic styling using vanilla CSS */ | |
| html, body { | |
| height: 100%; /* Ensure html and body take full height */ | |
| margin: 0; | |
| padding: 0; | |
| overflow: hidden; /* Prevent body scrollbars */ | |
| } | |
| body { | |
| font-family: Arial, Helvetica, sans-serif; | |
| /* Default body background, used if no CSS overrides */ | |
| background-color: #f0f0f0; | |
| display: flex; | |
| flex-direction: column; /* Stack elements vertically */ | |
| color: #333; | |
| } | |
| /* Window Title Bar Styles - Spans full width */ | |
| #titleBar { | |
| background: linear-gradient(to bottom, #eaeaea, #d5d5d5); | |
| padding: 6px 10px; | |
| border-bottom: 1px solid #aaa; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| cursor: default; | |
| font-size: 0.9rem; | |
| color: #444; | |
| height: 30px; | |
| box-sizing: border-box; | |
| flex-shrink: 0; /* Prevent title bar from shrinking */ | |
| } | |
| .window-title { | |
| font-weight: bold; | |
| flex-grow: 1; | |
| text-align: center; | |
| padding-right: 60px; /* Make space for controls */ | |
| } | |
| .window-controls { | |
| display: flex; | |
| gap: 6px; | |
| } | |
| .window-button { | |
| width: 14px; | |
| height: 14px; | |
| border-radius: 50%; | |
| border: 1px solid rgba(0,0,0,0.2); | |
| display: inline-block; | |
| } | |
| .close { background-color: #ff5f57; } | |
| .minimize { background-color: #ffbd2e; } | |
| .maximize { background-color: #28c940; } | |
| /* Controls Bar Styles - Spans full width below title bar */ | |
| .controls { | |
| display: flex; | |
| gap: 0.5rem; | |
| padding: 0.75rem 1rem; /* Padding for controls */ | |
| flex-wrap: wrap; | |
| align-items: center; | |
| background-color: #fff; /* White background for controls area */ | |
| border-bottom: 1px solid #ccc; /* Separator line */ | |
| flex-shrink: 0; /* Prevent controls from shrinking */ | |
| } | |
| #urlInput { | |
| flex-grow: 1; | |
| padding: 0.75rem; | |
| border: 1px solid #ccc; | |
| border-radius: 4px; | |
| font-size: 1rem; | |
| min-width: 150px; | |
| } | |
| .control-button { | |
| padding: 0.75rem 1.5rem; | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 1.2rem; | |
| line-height: 1; | |
| transition: background-color 0.2s ease, opacity 0.2s ease; | |
| white-space: nowrap; | |
| min-width: 50px; | |
| text-align: center; | |
| } | |
| #backButton { | |
| background-color: #6c757d; | |
| margin-right: 0.5rem; | |
| padding: 0.75rem 1rem; | |
| } | |
| #backButton:hover:not(:disabled) { | |
| background-color: #5a6268; | |
| } | |
| #backButton:disabled { | |
| opacity: 0.65; | |
| cursor: not-allowed; | |
| } | |
| /* Render Container Styles - Fills remaining space */ | |
| #renderContainer { | |
| flex-grow: 1; /* Allow container to take remaining vertical space */ | |
| overflow: auto; /* Add scrollbars IF canvas content overflows */ | |
| /* Background color set to match body default, actual canvas bg set in JS */ | |
| background-color: #f0f0f0; | |
| box-sizing: border-box; | |
| display: flex; /* Use flex for canvas alignment */ | |
| justify-content: center; /* Center canvas if it's narrower than container */ | |
| align-items: flex-start; /* Align canvas to top */ | |
| } | |
| #renderCanvas { | |
| display: block; | |
| /* background-color set dynamically via fillRect */ | |
| cursor: default; | |
| /* Width and height set in JS to match container */ | |
| } | |
| #errorMessage { | |
| color: #d9534f; | |
| margin: 0.5rem 1rem; /* Position error message appropriately */ | |
| font-weight: bold; | |
| display: none; | |
| background-color: #fff; | |
| padding: 0.5rem; | |
| border-radius: 4px; | |
| border: 1px solid #d9534f; | |
| flex-shrink: 0; /* Prevent error message from shrinking */ | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="titleBar"> | |
| <div class="window-controls"> | |
| <span class="window-button close"></span> | |
| <span class="window-button minimize"></span> | |
| <span class="window-button maximize"></span> | |
| </div> | |
| <div class="window-title">CanvasSurf</div> | |
| </div> | |
| <div class="controls"> | |
| <button id="backButton" class="control-button" disabled title="Back">←</button> | |
| <input type="text" id="urlInput" placeholder="Enter URL and press Enter"> | |
| </div> | |
| <div id="errorMessage"></div> | |
| <div id="renderContainer"> | |
| <canvas id="renderCanvas">Your browser does not support the canvas element.</canvas> | |
| </div> | |
| <script> | |
| // --- DOM Elements --- | |
| const urlInput = document.getElementById('urlInput'); | |
| const backButton = document.getElementById('backButton'); | |
| const renderCanvas = document.getElementById('renderCanvas'); | |
| const renderContainer = document.getElementById('renderContainer'); | |
| const errorMessage = document.getElementById('errorMessage'); | |
| const ctx = renderCanvas.getContext('2d'); | |
| // --- Constants --- | |
| const PROXY_URL = 'https://api.allorigins.win/raw?url='; | |
| const DEFAULT_FONT_SIZE = 14; | |
| const LINE_HEIGHT_MULTIPLIER = 1.5; | |
| const PADDING = 30; // Internal canvas padding | |
| const BLOCK_SPACING_MULTIPLIER = 0.7; | |
| const PARAGRAPH_MARGIN_MULTIPLIER = 1.0; | |
| const DEFAULT_BODY_BACKGROUND = '#ffffff'; // Default canvas background is white if none parsed | |
| // --- Global State --- | |
| let clickableLinks = []; | |
| let currentPageUrl = ''; | |
| let navigationHistory = []; | |
| let isNavigatingBack = false; | |
| let currentHtmlContent = ''; | |
| // --- Canvas Setup --- | |
| /** | |
| * Sets up the canvas dimensions and clears it with a specific background color. | |
| * @param {string} backgroundColor - The background color to clear the canvas with. | |
| */ | |
| function setupCanvas(backgroundColor = DEFAULT_BODY_BACKGROUND) { // Default to white | |
| const containerWidth = renderContainer.clientWidth; | |
| renderCanvas.width = Math.max(10, containerWidth); | |
| renderCanvas.height = 2000; // Fixed large drawing height | |
| if (renderCanvas.width <= 0) { | |
| console.warn("Canvas has zero or negative width during setup."); | |
| } | |
| renderCanvas.style.width = `${containerWidth}px`; | |
| // Clear with the provided background color | |
| ctx.fillStyle = backgroundColor; // Use the passed color | |
| ctx.fillRect(0, 0, renderCanvas.width, renderCanvas.height); | |
| // Reset default drawing styles | |
| ctx.fillStyle = '#333333'; // Default text color | |
| ctx.font = `${DEFAULT_FONT_SIZE}px Arial, Helvetica, sans-serif`; | |
| ctx.textAlign = 'left'; | |
| ctx.textBaseline = 'top'; | |
| clickableLinks = []; | |
| renderCanvas.style.cursor = 'default'; | |
| } | |
| // --- Update Back Button State --- | |
| function updateBackButtonState() { | |
| backButton.disabled = navigationHistory.length <= 1; | |
| } | |
| // --- Basic Inline CSS Parser --- | |
| function parseInlineCSS(cssText) { | |
| const styles = {}; | |
| const bodyRuleRegex = /body\s*\{([^}]*)\}/i; | |
| const bgColorRegex = /background-color\s*:\s*([^;]+);?/i; | |
| const bodyMatch = cssText.match(bodyRuleRegex); | |
| if (bodyMatch && bodyMatch[1]) { | |
| const bodyStyles = bodyMatch[1]; | |
| const bgColorMatch = bodyStyles.match(bgColorRegex); | |
| if (bgColorMatch && bgColorMatch[1]) { | |
| styles.body = { | |
| backgroundColor: bgColorMatch[1].trim() | |
| }; | |
| console.log("Parsed body background-color:", styles.body.backgroundColor); | |
| } | |
| } | |
| return styles; | |
| } | |
| // --- HTML Parsing and Rendering Logic --- | |
| function parseAndRenderHTML(htmlString) { | |
| // Determine background color *before* setting up canvas | |
| let bodyBackgroundColor = DEFAULT_BODY_BACKGROUND; // Start with default | |
| const parser = new DOMParser(); | |
| try { | |
| const doc = parser.parseFromString(htmlString, 'text/html'); | |
| const body = doc.body; | |
| if (!body) throw new Error("Could not find body element."); | |
| // --- Parse Inline Styles --- | |
| const styleTags = doc.querySelectorAll('style'); | |
| styleTags.forEach(tag => { | |
| const stylesFromTag = parseInlineCSS(tag.textContent); | |
| if (stylesFromTag.body && stylesFromTag.body.backgroundColor) { | |
| // Use the parsed color if found | |
| bodyBackgroundColor = stylesFromTag.body.backgroundColor; | |
| } | |
| }); | |
| // --- Apply Background Color to Container (optional, for area around canvas) --- | |
| // renderContainer.style.backgroundColor = bodyBackgroundColor; // Or use a fixed gray like #f0f0f0 | |
| // --- Setup Canvas with determined background color --- | |
| setupCanvas(bodyBackgroundColor); // Pass the color here | |
| const availableWidth = renderCanvas.width - 2 * PADDING; | |
| if (availableWidth <= 0) { | |
| console.warn("Calculated available width for text is zero or negative."); | |
| return; | |
| } | |
| // --- Render Content --- | |
| const finalPos = traverseDOM(body, PADDING, PADDING, availableWidth, { | |
| fontSize: DEFAULT_FONT_SIZE, | |
| fontWeight: 'normal', | |
| fillStyle: '#333333', // Default text color, could be parsed too | |
| isLink: false, | |
| href: null | |
| }); | |
| console.log(`Content drawn up to y=${finalPos.y}`); | |
| } catch (error) { | |
| console.error("Error parsing or rendering HTML:", error); | |
| errorMessage.textContent = `Error rendering content: ${error.message}`; | |
| errorMessage.style.display = 'block'; | |
| // Ensure canvas is set up even on error to draw message | |
| setupCanvas(DEFAULT_BODY_BACKGROUND); // Use default bg on error | |
| ctx.fillStyle = '#d9534f'; | |
| ctx.font = `bold ${DEFAULT_FONT_SIZE}px Arial, Helvetica, sans-serif`; | |
| ctx.fillText(`Rendering Error: ${error.message}`, PADDING, PADDING); | |
| } | |
| } | |
| function traverseDOM(node, currentX, currentY, availableWidth, style) { | |
| // This function remains unchanged. | |
| // ... (full traverseDOM code) ... | |
| let lineStartX = PADDING; | |
| let y = currentY; | |
| let x = currentX; | |
| // 1. Handle Element Nodes | |
| if (node.nodeType === Node.ELEMENT_NODE) { | |
| const tagName = node.tagName.toUpperCase(); | |
| let elementStyle = { ...style }; | |
| let blockElement = false; | |
| let isListItem = false; | |
| let isParagraph = false; | |
| // Ignore tags | |
| if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'HEAD', 'META', 'LINK', 'TITLE'].includes(tagName)) { | |
| return { x, y }; | |
| } | |
| // Style Modifications based on Tag | |
| switch (tagName) { | |
| case 'H1': elementStyle.fontSize = DEFAULT_FONT_SIZE * 2.0; elementStyle.fontWeight = 'bold'; blockElement = true; break; | |
| case 'H2': elementStyle.fontSize = DEFAULT_FONT_SIZE * 1.7; elementStyle.fontWeight = 'bold'; blockElement = true; break; | |
| case 'H3': elementStyle.fontSize = DEFAULT_FONT_SIZE * 1.4; elementStyle.fontWeight = 'bold'; blockElement = true; break; | |
| case 'H4': elementStyle.fontSize = DEFAULT_FONT_SIZE * 1.2; elementStyle.fontWeight = 'bold'; blockElement = true; break; | |
| case 'H5': elementStyle.fontSize = DEFAULT_FONT_SIZE * 1.0; elementStyle.fontWeight = 'bold'; blockElement = true; break; | |
| case 'H6': elementStyle.fontSize = DEFAULT_FONT_SIZE * 0.9; elementStyle.fontWeight = 'bold'; blockElement = true; break; | |
| case 'B': case 'STRONG': elementStyle.fontWeight = 'bold'; break; | |
| case 'A': | |
| elementStyle.fillStyle = '#0000EE'; | |
| elementStyle.isLink = true; | |
| elementStyle.href = node.getAttribute('href'); | |
| break; | |
| case 'P': | |
| isParagraph = true; | |
| blockElement = true; | |
| break; | |
| case 'DIV': case 'UL': case 'OL': | |
| case 'BLOCKQUOTE': case 'PRE': | |
| blockElement = true; | |
| break; | |
| case 'LI': | |
| blockElement = true; | |
| isListItem = true; | |
| break; | |
| case 'BR': | |
| blockElement = true; | |
| break; | |
| } | |
| const elementLineHeight = elementStyle.fontSize * LINE_HEIGHT_MULTIPLIER; | |
| const topMarginMultiplier = isParagraph ? PARAGRAPH_MARGIN_MULTIPLIER : BLOCK_SPACING_MULTIPLIER; | |
| const bottomMarginMultiplier = isParagraph ? PARAGRAPH_MARGIN_MULTIPLIER : BLOCK_SPACING_MULTIPLIER; | |
| // --- Vertical spacing BEFORE block elements --- | |
| if (blockElement && y > PADDING) { | |
| y += elementLineHeight * topMarginMultiplier; | |
| x = lineStartX; | |
| } else if (blockElement && y === PADDING) { | |
| x = lineStartX; | |
| } | |
| // --- Handle List Item Marker --- | |
| if (isListItem) { | |
| const marker = '\u2022 '; | |
| ctx.font = `${elementStyle.fontWeight} ${elementStyle.fontSize}px Arial, Helvetica, sans-serif`; | |
| ctx.fillStyle = elementStyle.fillStyle; | |
| const markerWidth = ctx.measureText(marker).width; | |
| ctx.fillText(marker, lineStartX, y); | |
| x = lineStartX + markerWidth; | |
| } else if (blockElement) { | |
| x = lineStartX; | |
| } | |
| // --- Recursively process child nodes --- | |
| let lastChildPos = { x: x, y: y }; | |
| node.childNodes.forEach(child => { | |
| const childAvailableWidth = availableWidth - (x - lineStartX); | |
| lastChildPos = traverseDOM(child, lastChildPos.x, lastChildPos.y, childAvailableWidth, elementStyle); | |
| }); | |
| y = lastChildPos.y; | |
| // --- Vertical spacing AFTER block elements --- | |
| if (blockElement) { | |
| const blockContentHeight = lastChildPos.y - currentY; | |
| const minYAfterBlock = currentY + Math.max(blockContentHeight, elementLineHeight); | |
| y = minYAfterBlock; | |
| y += elementLineHeight * bottomMarginMultiplier; | |
| x = lineStartX; | |
| } else { | |
| x = lastChildPos.x; | |
| } | |
| } | |
| // 2. Handle Text Nodes | |
| else if (node.nodeType === Node.TEXT_NODE) { | |
| let text = node.textContent.trim().replace(/\s+/g, ' '); | |
| if (text.length > 0) { | |
| ctx.font = `${style.fontWeight} ${style.fontSize}px Arial, Helvetica, sans-serif`; | |
| ctx.fillStyle = style.fillStyle; | |
| const lineHeight = style.fontSize * LINE_HEIGHT_MULTIPLIER; | |
| const words = text.split(' '); | |
| let currentLine = ''; | |
| let lineStartXForText = x; | |
| for (let i = 0; i < words.length; i++) { | |
| const word = words[i]; | |
| const testLine = currentLine + (currentLine ? ' ' : '') + word; | |
| const metrics = ctx.measureText(testLine); | |
| const testWidth = metrics.width; | |
| if (lineStartXForText + testWidth > lineStartX + availableWidth && currentLine !== '') { | |
| const lineMetrics = ctx.measureText(currentLine); | |
| ctx.fillText(currentLine, lineStartXForText, y); | |
| if (style.isLink && style.href) { | |
| const linkArea = { href: style.href, x: lineStartXForText, y: y, width: lineMetrics.width, height: lineHeight }; | |
| clickableLinks.push(linkArea); | |
| const underlineY = y + lineHeight - (lineHeight * 0.2); | |
| ctx.save(); | |
| ctx.fillStyle = style.fillStyle; | |
| ctx.fillRect(lineStartXForText, underlineY, lineMetrics.width, 1); | |
| ctx.restore(); | |
| } | |
| y += lineHeight; | |
| x = lineStartX; | |
| lineStartXForText = x; | |
| currentLine = word; | |
| } else { | |
| currentLine = testLine; | |
| // Update x based on the current line's width if it fits | |
| x = lineStartXForText + ctx.measureText(currentLine).width; | |
| } | |
| } | |
| if (currentLine) { | |
| const lineMetrics = ctx.measureText(currentLine); | |
| ctx.fillText(currentLine, lineStartXForText, y); | |
| if (style.isLink && style.href) { | |
| const linkArea = { href: style.href, x: lineStartXForText, y: y, width: lineMetrics.width, height: lineHeight }; | |
| clickableLinks.push(linkArea); | |
| const underlineY = y + lineHeight - (lineHeight * 0.2); | |
| ctx.save(); | |
| ctx.fillStyle = style.fillStyle; | |
| ctx.fillRect(lineStartXForText, underlineY, lineMetrics.width, 1); | |
| ctx.restore(); | |
| } | |
| x = lineStartXForText + lineMetrics.width; | |
| } | |
| } | |
| } | |
| return { x, y }; | |
| } | |
| // --- Fetching Logic --- | |
| async function fetchUrlContent(urlToFetchProvided = null) { | |
| const urlToFetch = urlToFetchProvided || urlInput.value.trim(); | |
| errorMessage.style.display = 'none'; | |
| errorMessage.textContent = ''; | |
| // Don't setup canvas yet, wait until styles are parsed | |
| urlInput.placeholder = 'Fetching...'; // Indicate fetching in placeholder | |
| if (!urlToFetch) { | |
| renderContainer.style.backgroundColor = '#f0f0f0'; // Ensure default container bg | |
| setupCanvas(DEFAULT_BODY_BACKGROUND); // Use default canvas bg | |
| ctx.fillStyle = '#d9534f'; | |
| ctx.font = `bold ${DEFAULT_FONT_SIZE}px Arial, Helvetica, sans-serif`; | |
| ctx.fillText('Please enter a URL.', PADDING, PADDING); | |
| updateBackButtonState(); | |
| urlInput.placeholder = 'Enter URL and press Enter'; // Reset placeholder | |
| return; | |
| } | |
| let fullUrl = urlToFetch; | |
| if (!urlToFetch.startsWith('http://') && !urlToFetch.startsWith('https://')) { | |
| fullUrl = 'https://' + urlToFetch; | |
| } | |
| const finalUrlToFetch = fullUrl; | |
| try { | |
| const requestUrl = `${PROXY_URL}${encodeURIComponent(finalUrlToFetch)}`; | |
| console.log(`Requesting via proxy: ${requestUrl}`); | |
| const response = await fetch(requestUrl); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! Status: ${response.status} - ${response.statusText}`); | |
| } | |
| currentHtmlContent = await response.text(); | |
| currentPageUrl = finalUrlToFetch; | |
| if (!isNavigatingBack) { | |
| if (navigationHistory.length === 0 || navigationHistory[navigationHistory.length - 1] !== currentPageUrl) { | |
| navigationHistory.push(currentPageUrl); | |
| console.log("History updated:", navigationHistory); | |
| } | |
| } | |
| urlInput.value = currentPageUrl; | |
| // Parse and render AFTER getting content | |
| parseAndRenderHTML(currentHtmlContent); | |
| } catch (error) { | |
| console.error('Fetch error:', error); | |
| errorMessage.textContent = `Error fetching content: ${error.message}. Check URL/proxy.`; | |
| errorMessage.style.display = 'block'; | |
| // Setup canvas to show error | |
| renderContainer.style.backgroundColor = '#f0f0f0'; // Ensure default container bg | |
| setupCanvas(DEFAULT_BODY_BACKGROUND); // Use default canvas bg | |
| ctx.fillStyle = '#d9534f'; | |
| ctx.font = `bold ${DEFAULT_FONT_SIZE}px Arial, Helvetica, sans-serif`; | |
| ctx.fillText('Failed to load content.', PADDING, PADDING); | |
| currentPageUrl = ''; | |
| currentHtmlContent = ''; | |
| } finally { | |
| isNavigatingBack = false; | |
| updateBackButtonState(); | |
| urlInput.placeholder = 'Enter URL and press Enter'; // Reset placeholder | |
| } | |
| } | |
| // --- Event Listeners --- | |
| urlInput.addEventListener('keypress', (event) => { | |
| if (event.key === 'Enter') { | |
| event.preventDefault(); | |
| fetchUrlContent(); | |
| } | |
| }); | |
| backButton.addEventListener('click', () => { | |
| if (navigationHistory.length > 1) { | |
| isNavigatingBack = true; | |
| navigationHistory.pop(); | |
| const previousUrl = navigationHistory[navigationHistory.length - 1]; | |
| console.log("Navigating back to:", previousUrl); | |
| urlInput.value = previousUrl; | |
| fetchUrlContent(previousUrl); | |
| } | |
| }); | |
| // Debounced resize handler | |
| let resizeTimeout; | |
| window.addEventListener('resize', () => { | |
| clearTimeout(resizeTimeout); | |
| resizeTimeout = setTimeout(() => { | |
| console.log("Window resized."); | |
| if (currentHtmlContent) { | |
| console.log("Re-rendering cached content."); | |
| // Re-parse and render needed as styles might affect layout/bg | |
| parseAndRenderHTML(currentHtmlContent); | |
| } else { | |
| renderContainer.style.backgroundColor = '#f0f0f0'; | |
| setupCanvas(DEFAULT_BODY_BACKGROUND); | |
| ctx.fillStyle = '#888888'; | |
| ctx.font = `16px Arial, Helvetica, sans-serif`; | |
| ctx.fillText('Enter a URL above and press Enter to start browsing with CanvasSurf.', PADDING, PADDING); | |
| } | |
| }, 250); | |
| }); | |
| // Click listener for canvas | |
| renderCanvas.addEventListener('click', handleCanvasClick); | |
| function handleCanvasClick(event) { | |
| const rect = renderCanvas.getBoundingClientRect(); | |
| const scaleX = renderCanvas.width / rect.width; | |
| const scaleY = renderCanvas.height / rect.height; | |
| const clickX = (event.clientX - rect.left) * scaleX; | |
| const clickY = (event.clientY - rect.top) * scaleY; | |
| for (let i = clickableLinks.length - 1; i >= 0; i--) { | |
| const link = clickableLinks[i]; | |
| if (clickX >= link.x && clickX <= link.x + link.width && | |
| clickY >= link.y && clickY <= link.y + link.height) { | |
| console.log(`Clicked link: href="${link.href}", base="${currentPageUrl}"`); | |
| if (!link.href) continue; | |
| try { | |
| const absoluteUrl = new URL(link.href, currentPageUrl || window.location.href).href; | |
| console.log(`Resolved URL: ${absoluteUrl}`); | |
| urlInput.value = absoluteUrl; | |
| fetchUrlContent(absoluteUrl); | |
| } catch (e) { | |
| console.error("Error resolving or fetching clicked URL:", e); | |
| errorMessage.textContent = `Error: Could not navigate to link (${link.href}). ${e.message}`; | |
| errorMessage.style.display = 'block'; | |
| } | |
| return; | |
| } | |
| } | |
| } | |
| // Mouse move listener for cursor change | |
| renderCanvas.addEventListener('mousemove', handleCanvasMouseMove); | |
| function handleCanvasMouseMove(event) { | |
| const rect = renderCanvas.getBoundingClientRect(); | |
| const scaleX = renderCanvas.width / rect.width; | |
| const scaleY = renderCanvas.height / rect.height; | |
| const moveX = (event.clientX - rect.left) * scaleX; | |
| const moveY = (event.clientY - rect.top) * scaleY; | |
| let hoveringLink = false; | |
| for (let i = clickableLinks.length - 1; i >= 0; i--) { | |
| const link = clickableLinks[i]; | |
| if (moveX >= link.x && moveX <= link.x + link.width && | |
| moveY >= link.y && moveY <= link.y + link.height) { | |
| hoveringLink = true; | |
| break; | |
| } | |
| } | |
| renderCanvas.style.cursor = hoveringLink ? 'pointer' : 'default'; | |
| } | |
| // --- Initial Setup --- | |
| document.addEventListener('DOMContentLoaded', () => { | |
| urlInput.value = 'https://example.com'; | |
| fetchUrlContent(urlInput.value); // Fetch initial content | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment