diff options
author | elioat <{ID}+{username}@users.noreply.github.com> | 2025-02-12 16:10:47 -0500 |
---|---|---|
committer | elioat <{ID}+{username}@users.noreply.github.com> | 2025-02-12 16:10:47 -0500 |
commit | 4160cb412dfc8e4633cc7a30ac41ab14e686c43c (patch) | |
tree | c689f1e913a7d85836c15fb7200e7dabdd4fd98f /html/svg-viewer/index.html | |
parent | 9ae42717240082ae3351fb655a4c881fdc5ba183 (diff) | |
download | tour-4160cb412dfc8e4633cc7a30ac41ab14e686c43c.tar.gz |
*
Diffstat (limited to 'html/svg-viewer/index.html')
-rw-r--r-- | html/svg-viewer/index.html | 522 |
1 files changed, 522 insertions, 0 deletions
diff --git a/html/svg-viewer/index.html b/html/svg-viewer/index.html new file mode 100644 index 0000000..232e96b --- /dev/null +++ b/html/svg-viewer/index.html @@ -0,0 +1,522 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> + <title>SVG viewer</title> + <style> + body { + margin: 0; + padding: 10px; + font-family: system-ui, -apple-system, sans-serif; + min-height: 100vh; + /* Prevent content shift on mobile when keyboard appears */ + min-height: -webkit-fill-available; + } + + .container { + display: flex; + gap: 10px; + height: calc(100vh - 20px); + /* Support for mobile browsers */ + height: calc(100vh - 20px - env(safe-area-inset-bottom)); + } + + .editor, .preview { + flex: 1; + display: flex; + flex-direction: column; + min-height: 200px; /* Ensure minimum height on mobile */ + } + + textarea { + flex: 1; + resize: none; + padding: 10px; + font-family: monospace; + font-size: 14px; + border: 1px solid #ccc; + border-radius: 4px; + /* Improve mobile experience */ + -webkit-overflow-scrolling: touch; + /* Prevent zoom on mobile devices */ + font-size: 16px; + } + + canvas { + flex: 1; + border: 1px solid #ccc; + border-radius: 4px; + background: white; + } + + h2 { + margin-top: 0; + margin-bottom: 10px; + font-size: 16px; + } + + .toolbar { + display: flex; + gap: 8px; + margin-bottom: 16px; /* Increased margin for more space below */ + flex-wrap: wrap; + justify-content: space-between; /* Align error message to the right */ + } + + .toolbar button { + /* Improve touch targets */ + min-height: 44px; + padding: 8px 16px; + /* Prevent text wrapping inside button */ + white-space: nowrap; + /* Add visual feedback for touch */ + transition: background-color 0.2s; + -webkit-touch-callout: none; + user-select: none; + border: 1px solid #ccc; + border-radius: 6px; + background: #f8f8f8; + } + + .toolbar button:active { + background-color: #e0e0e0; + } + + @media (min-width: 1024px) { + .toolbar { + flex-wrap: nowrap; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + -ms-overflow-style: none; + scroll-snap-type: x mandatory; + } + + .toolbar::-webkit-scrollbar { + display: none; + } + + .toolbar button { + scroll-snap-align: start; + } + } + + /* Medium and small screens - stacked buttons */ + @media (max-width: 1023px) { + .toolbar { + flex-direction: column; + width: 100%; + gap: 4px; + } + + .toolbar button { + width: 100%; + text-align: left; + justify-content: flex-start; + padding: 12px 16px; + } + } + + /* Small mobile optimization */ + @media (max-width: 480px) { + .toolbar button { + padding: 10px 14px; + font-size: 14px; + } + } + + /* Landscape mode optimization */ + @media (orientation: landscape) and (max-height: 500px) { + .toolbar { + /* Revert to horizontal scroll for landscape */ + flex-direction: row; + flex-wrap: nowrap; + overflow-x: auto; + } + + .toolbar button { + width: auto; + padding: 8px 16px; + background-image: none; + } + } + + /* Media query for mobile devices */ + @media (max-width: 768px) { + .container { + flex-direction: column; + height: auto; + min-height: 100vh; + } + + .editor, .preview { + flex: none; + height: calc(50vh - 30px); /* Half viewport minus some spacing */ + overflow: hidden; /* Contain scrolling children */ + display: flex; + flex-direction: column; + } + + .toolbar { + flex-direction: column; + width: 100%; + gap: 12px; /* Increased gap for more space between buttons */ + } + + textarea, canvas { + min-height: 200px; /* Minimum height */ + height: 100%; + overflow-y: auto; /* Enable vertical scrolling */ + } + + h2 { + font-size: 14px; + margin-bottom: 8px; + flex-shrink: 0; /* Prevent header from shrinking */ + } + } + + /* Landscape mode optimization */ + @media (orientation: landscape) { + .container { + flex-direction: row; + height: calc(100vh - 20px); + } + + .editor, .preview { + width: 50%; + height: 100%; + overflow: hidden; /* Contain scrolling children */ + } + + .toolbar { + flex-direction: row; + flex-wrap: wrap; + flex-shrink: 0; /* Prevent toolbar from shrinking */ + } + } + + /* Support for notched phones */ + @supports (padding: max(0px)) { + body { + padding-left: max(10px, env(safe-area-inset-left)); + padding-right: max(10px, env(safe-area-inset-right)); + padding-bottom: max(10px, env(safe-area-inset-bottom)); + } + } + + /* Focus styles for better keyboard navigation */ + .toolbar button:focus { + outline: 2px solid #0066cc; + outline-offset: 2px; + } + + /* High contrast focus indicator for canvas */ + canvas:focus { + outline: 3px solid #0066cc; + } + + /* Error message styling */ + .error-message { + color: #cc0000; + font-size: 14px; + margin-top: 4px; + display: none; + flex-grow: 1; /* Allow error message to take available space */ + text-align: right; /* Align text to the right */ + } + + /* Enhance focus visibility for WCAG 2.4.7 */ + *:focus { + outline: 3px solid #0066cc; + outline-offset: 2px; + } + + /* Add focus-visible for modern browsers */ + *:focus:not(:focus-visible) { + outline: none; + } + + *:focus-visible { + outline: 3px solid #0066cc; + outline-offset: 2px; + } + + /* High contrast mode support */ + @media (forced-colors: active) { + .toolbar button { + border: 1px solid ButtonBorder; + background: ButtonFace; + color: ButtonText; + } + + canvas, textarea { + border: 1px solid ButtonBorder; + } + } + + .export-buttons { + display: flex; + gap: 8px; + margin-top: 8px; + } + + .export-buttons button { + min-height: 44px; + padding: 8px 16px; + white-space: nowrap; + transition: background-color 0.2s; + -webkit-touch-callout: none; + user-select: none; + border: 1px solid #ccc; + border-radius: 6px; + background: #f8f8f8; + } + + .export-buttons button:active { + background-color: #e0e0e0; + } + </style> +</head> +<body> + <main> + <div class="container" role="application" aria-label="SVG Editor Application"> + <div class="editor"> + <h2 id="editor-label">SVG Editor</h2> + <div class="toolbar" role="toolbar" aria-label="Error message"> + <div id="error-message" class="error-message" role="alert"></div> + </div> + <textarea + id="svgInput" + spellcheck="false" + aria-labelledby="editor-label" + aria-describedby="editor-desc error-message" + role="textbox" + ><svg width="200" height="200" xmlns="http://www.w3.org/2000/svg"> + <circle cx="100" cy="100" r="50" fill="blue" /> +</svg></textarea> + <div class="export-buttons"> + <button onclick="exportSVG()" aria-label="Export as SVG file">Export SVG</button> + <button onclick="exportPNG()" aria-label="Export as PNG image">Export PNG</button> + </div> + </div> + <div class="preview"> + <h2 id="preview-label">Preview</h2> + <canvas + id="preview" + role="img" + aria-labelledby="preview-label" + tabindex="0" + ></canvas> + </div> + </div> + </main> + + <script> + const textarea = document.getElementById('svgInput'); + const canvas = document.getElementById('preview'); + const ctx = canvas.getContext('2d'); + + // Function to convert SVG string to image data + const svgToImage = (svgString) => { + const blob = new Blob([svgString], { type: 'image/svg+xml' }); + const url = URL.createObjectURL(blob); + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + URL.revokeObjectURL(url); + resolve(img); + }; + img.onerror = () => { + URL.revokeObjectURL(url); + reject(new Error('Failed to load SVG')); + }; + img.src = url; + }); + }; + + // Function to parse SVG and check for syntax errors + const checkSVG = (svgString) => { + const parser = new DOMParser(); + const doc = parser.parseFromString(svgString, 'image/svg+xml'); + const errorNode = doc.querySelector('parsererror'); + return errorNode ? errorNode.textContent : null; + }; + + // Function to update the preview + const updatePreview = async () => { + const errorMessage = document.getElementById('error-message'); + const svgString = textarea.value; + const error = checkSVG(svgString); + + if (error) { + errorMessage.style.display = 'block'; + errorMessage.textContent = `SVG Syntax Error: ${error}`; + canvas.setAttribute('aria-label', 'Preview unavailable due to SVG error'); + return; + } + + try { + const img = await svgToImage(svgString); + + // Set canvas size to match its display size + const rect = canvas.getBoundingClientRect(); + canvas.width = rect.width; + canvas.height = rect.height; + + // Clear canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Calculate scaling to fit SVG while maintaining aspect ratio + const scale = Math.min( + canvas.width / img.width, + canvas.height / img.height + ); + + // Center the image + const x = (canvas.width - img.width * scale) / 2; + const y = (canvas.height - img.height * scale) / 2; + + // Draw scaled image + ctx.drawImage(img, x, y, img.width * scale, img.height * scale); + + errorMessage.style.display = 'none'; + errorMessage.textContent = ''; + canvas.setAttribute('aria-label', 'SVG preview showing current editor content'); + } catch (error) { + console.error('Error updating preview:', error); + errorMessage.style.display = 'block'; + errorMessage.textContent = 'Error in SVG syntax. Please check your markup.'; + canvas.setAttribute('aria-label', 'Preview unavailable due to SVG error'); + } + }; + + // Update preview when text changes + textarea.addEventListener('input', () => { + updatePreview(); + }); + + // Handle window resize + window.addEventListener('resize', () => { + updatePreview(); + }); + + // Initial preview + updatePreview(); + + // Template functions for SVG shapes + const shapeTemplates = { + rect: ' <rect x="10" y="10" width="100" height="80" fill="red" />\n', + circle: ' <circle cx="100" cy="100" r="50" fill="blue" />\n', + ellipse: ' <ellipse cx="100" cy="100" rx="80" ry="50" fill="green" />\n', + line: ' <line x1="10" y1="10" x2="190" y2="190" stroke="black" stroke-width="2" />\n', + path: ' <path d="M 10,10 L 100,10 L 100,100 Z" fill="purple" />\n', + text: ' <text x="100" y="100" text-anchor="middle">Hello SVG</text>\n' + }; + + // Function to insert template at cursor position + const insertShape = (shapeType) => { + const template = shapeTemplates[shapeType]; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const text = textarea.value; + + // Find the closing </svg> tag + const closingTag = '</svg>'; + const closingTagIndex = text.lastIndexOf(closingTag); + + if (closingTagIndex !== -1) { + // Insert the new shape before the closing tag + const newText = + text.substring(0, closingTagIndex) + + template + + text.substring(closingTagIndex); + + textarea.value = newText; + updatePreview(); + + // Set cursor after inserted template + const newPosition = closingTagIndex + template.length; + textarea.setSelectionRange(newPosition, newPosition); + textarea.focus(); + } + }; + + // Keyboard handling for toolbar + document.querySelector('.toolbar').addEventListener('keydown', (e) => { + const buttons = Array.from(e.currentTarget.querySelectorAll('button')); + const currentIndex = buttons.indexOf(document.activeElement); + + switch(e.key) { + case 'ArrowRight': + case 'ArrowDown': + e.preventDefault(); + buttons[(currentIndex + 1) % buttons.length].focus(); + break; + case 'ArrowLeft': + case 'ArrowUp': + e.preventDefault(); + buttons[(currentIndex - 1 + buttons.length) % buttons.length].focus(); + break; + } + }); + + // Announce successful exports + const announceExport = (format) => { + const message = `${format} file exported successfully`; + const announcement = document.createElement('div'); + announcement.setAttribute('role', 'status'); + announcement.setAttribute('aria-live', 'polite'); + announcement.style.position = 'absolute'; + announcement.style.width = '1px'; + announcement.style.height = '1px'; + announcement.style.overflow = 'hidden'; + announcement.textContent = message; + document.body.appendChild(announcement); + setTimeout(() => document.body.removeChild(announcement), 1000); + }; + + // Update export functions + const exportSVG = () => { + const svgBlob = new Blob([textarea.value], { type: 'image/svg+xml' }); + const url = URL.createObjectURL(svgBlob); + const link = document.createElement('a'); + link.href = url; + link.download = 'drawing.svg'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + announceExport('SVG'); + }; + + const exportPNG = () => { + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d'); + + // Create image from SVG + svgToImage(textarea.value).then(img => { + // Set canvas to SVG's native size + tempCanvas.width = img.width; + tempCanvas.height = img.height; + + // Draw with white background + tempCtx.fillStyle = 'white'; + tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height); + tempCtx.drawImage(img, 0, 0); + + // Export + const url = tempCanvas.toDataURL('image/png'); + const link = document.createElement('a'); + link.href = url; + link.download = 'drawing.png'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + announceExport('PNG'); + }); + }; + </script> +</body> +</html> \ No newline at end of file |