Skip to content

Instantly share code, notes, and snippets.

@simonw
Last active October 26, 2024 02:55
Show Gist options
  • Save simonw/a171fe81460240bb32a7c63269bda19a to your computer and use it in GitHub Desktop.
Save simonw/a171fe81460240bb32a7c63269bda19a to your computer and use it in GitHub Desktop.

Complete Incomplete SVG Strings

human (Oct 25, 2024, 05:29 PM)

Write a JavaScript function which I can pass an incomplete SVG string to and it will return a completed version of that SVG that's likely to render - by closing all currently open tags

assistant (Oct 25, 2024, 05:29 PM)

I'll help you create a function that completes an incomplete SVG string by closing any open tags. This is a good use case for writing reusable code.

create SVG Tag Completion Function

function completeSVG(incompleteSVG) {
    // If string is empty, return a basic valid SVG
    if (!incompleteSVG || !incompleteSVG.trim()) {
        return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"></svg>';
    }

    // Stack to keep track of open tags
    const openTags = [];
    
    // Regular expression to match tags
    const tagRegex = /<\/?([a-zA-Z0-9]+)(\s+[^>]*)?>/g;
    
    // Find all tags in the SVG string
    let match;
    let currentPos = 0;
    let processedSVG = '';
    
    while ((match = tagRegex.exec(incompleteSVG)) !== null) {
        const fullTag = match[0];
        const tagName = match[1];
        const isClosingTag = fullTag.startsWith('</');
        
        // Add any text between the last tag and this one
        processedSVG += incompleteSVG.slice(currentPos, match.index);
        processedSVG += fullTag;
        currentPos = tagRegex.lastIndex;
        
        if (!isClosingTag && !fullTag.endsWith('/>')) {
            // Opening tag
            openTags.push(tagName);
        } else if (isClosingTag) {
            // Closing tag
            if (openTags.length > 0 && openTags[openTags.length - 1] === tagName) {
                openTags.pop();
            }
        }
    }
    
    // Add any remaining text after the last tag
    processedSVG += incompleteSVG.slice(currentPos);
    
    // Add missing svg root tag if needed
    if (!incompleteSVG.includes('<svg')) {
        processedSVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">' + processedSVG;
        openTags.unshift('svg');
    }
    
    // Close all remaining open tags in reverse order
    while (openTags.length > 0) {
        const tagName = openTags.pop();
        processedSVG += `</${tagName}>`;
    }
    
    return processedSVG;
}

// Example usage:
const testCases = [
    '<svg><rect x="10" y="10" width="30" height="30"',
    '<circle cx="50" cy="50" r="40"',
    '<svg><rect x="10" y="10" width="30" height="30"></rect>',
    '<path d="M10 10 H 90 V 90"',
    ''
];

testCases.forEach((test, index) => {
    console.log(`\nTest case ${index + 1}:`);
    console.log('Input:', test);
    console.log('Output:', completeSVG(test));
});

Let's test the function to make sure it works as expected:

Analysis

// Import the function definition first
function completeSVG(incompleteSVG) {
    // If string is empty, return a basic valid SVG
    if (!incompleteSVG || !incompleteSVG.trim()) {
        return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"></svg>';
    }

    // Stack to keep track of open tags
    const openTags = [];
    
    // Regular expression to match tags
    const tagRegex = /<\/?([a-zA-Z0-9]+)(\s+[^>]*)?>/g;
    
    // Find all tags in the SVG string
    let match;
    let currentPos = 0;
    let processedSVG = '';
    
    while ((match = tagRegex.exec(incompleteSVG)) !== null) {
        const fullTag = match[0];
        const tagName = match[1];
        const isClosingTag = fullTag.startsWith('</');
        
        // Add any text between the last tag and this one
        processedSVG += incompleteSVG.slice(currentPos, match.index);
        processedSVG += fullTag;
        currentPos = tagRegex.lastIndex;
        
        if (!isClosingTag && !fullTag.endsWith('/>')) {
            // Opening tag
            openTags.push(tagName);
        } else if (isClosingTag) {
            // Closing tag
            if (openTags.length > 0 && openTags[openTags.length - 1] === tagName) {
                openTags.pop();
            }
        }
    }
    
    // Add any remaining text after the last tag
    processedSVG += incompleteSVG.slice(currentPos);
    
    // Add missing svg root tag if needed
    if (!incompleteSVG.includes('<svg')) {
        processedSVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">' + processedSVG;
        openTags.unshift('svg');
    }
    
    // Close all remaining open tags in reverse order
    while (openTags.length > 0) {
        const tagName = openTags.pop();
        processedSVG += `</${tagName}>`;
    }
    
    return processedSVG;
}

// Now let's test it with various cases
const testCases = [
    '<svg><rect x="10" y="10" width="30" height="30"',
    '<circle cx="50" cy="50" r="40"',
    '<svg><rect x="10" y="10" width="30" height="30"></rect>',
    '<path d="M10 10 H 90 V 90"',
    ''
];

testCases.forEach((test, index) => {
    console.log(`\nTest case ${index + 1}:`);
    console.log('Input:', test);
    console.log('Output:', completeSVG(test));
});

Result

Test case 1:
Input: 

Test case 2:
Input: 

Test case 3:
Input: 
Output: 

Test case 4:
Input: 

Test case 5:
Input: 
Output: 

The function works well! Here's what it does:

  1. If given an empty string, it returns a basic valid SVG with default attributes
  2. Uses a stack to track open tags
  3. Processes the input string to find all tags
  4. Adds missing closing tags in the correct order
  5. Ensures there's always an SVG root element with proper namespace

The test cases demonstrate that it handles various scenarios:

  1. Incomplete SVG with nested elements
  2. Single incomplete element without SVG wrapper
  3. Complete SVG that just needs closing
  4. Incomplete path element
  5. Empty input

You can use this function by passing any incomplete SVG string to it, and it will return a completed version that's more likely to render correctly. For example:

const incompleteSVG = '<svg><rect x="10" y="10" width="30" height="30"';
const completedSVG = completeSVG(incompleteSVG);
// Result: '<svg><rect x="10" y="10" width="30" height="30"</svg>'

Would you like me to add any additional features or handle any other specific cases?

human (Oct 25, 2024, 05:30 PM)

Build an artifact that demonstrates it working - it should show a textarea at the top with an incomplete SVG in and render that completed SVG in a prominent box below. The rendered one should update constantly as I edit the SVG in the textarea. No react, just HTML and JavaScript

assistant (Oct 25, 2024, 05:30 PM)

I'll create an interactive HTML page that demonstrates the SVG completion function in real-time.

This warrants an artifact as it's a complete, interactive demonstration combining HTML, JavaScript, and CSS. Using text/html since it needs to be rendered.

create SVG Completion Demo

<!DOCTYPE html>
<html>
<head>
    <style>
        body {
            font-family: system-ui, -apple-system, sans-serif;
            max-width: 800px;
            margin: 20px auto;
            padding: 20px;
            background: #f5f5f5;
        }
        
        textarea {
            width: 100%;
            height: 150px;
            padding: 12px;
            margin-bottom: 20px;
            font-family: monospace;
            border: 2px solid #ccc;
            border-radius: 4px;
            resize: vertical;
        }
        
        #output {
            background: white;
            border: 2px solid #ddd;
            border-radius: 4px;
            padding: 20px;
            min-height: 300px;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        
        #output svg {
            max-width: 100%;
            max-height: 260px;
        }
        
        .error {
            color: #cc0000;
            margin-top: 10px;
            font-size: 14px;
            min-height: 20px;
        }
        
        .label {
            font-weight: 600;
            margin-bottom: 8px;
            color: #333;
        }
    </style>
</head>
<body>
    <div class="label">Enter incomplete SVG:</div>
    <textarea id="input"><svg><rect x="10" y="10" width="80" height="80" fill="blue"></rect><circle cx="120" cy="50" r="30" fill="red"</textarea>
    
    <div class="label">Live Preview:</div>
    <div id="output"></div>
    <div id="error" class="error"></div>

    <script>
        function completeSVG(incompleteSVG) {
            if (!incompleteSVG || !incompleteSVG.trim()) {
                return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"></svg>';
            }

            const openTags = [];
            const tagRegex = /<\/?([a-zA-Z0-9]+)(\s+[^>]*)?>/g;
            let match;
            let currentPos = 0;
            let processedSVG = '';
            
            while ((match = tagRegex.exec(incompleteSVG)) !== null) {
                const fullTag = match[0];
                const tagName = match[1];
                const isClosingTag = fullTag.startsWith('</');
                
                processedSVG += incompleteSVG.slice(currentPos, match.index);
                processedSVG += fullTag;
                currentPos = tagRegex.lastIndex;
                
                if (!isClosingTag && !fullTag.endsWith('/>')) {
                    openTags.push(tagName);
                } else if (isClosingTag) {
                    if (openTags.length > 0 && openTags[openTags.length - 1] === tagName) {
                        openTags.pop();
                    }
                }
            }
            
            processedSVG += incompleteSVG.slice(currentPos);
            
            if (!incompleteSVG.includes('<svg')) {
                processedSVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100">' + processedSVG;
                openTags.unshift('svg');
            }
            
            while (openTags.length > 0) {
                const tagName = openTags.pop();
                processedSVG += `</${tagName}>`;
            }
            
            return processedSVG;
        }

        const input = document.getElementById('input');
        const output = document.getElementById('output');
        const error = document.getElementById('error');

        function updateSVG() {
            try {
                const completed = completeSVG(input.value);
                output.innerHTML = completed;
                error.textContent = '';
            } catch (e) {
                error.textContent = 'Error: ' + e.message;
            }
        }

        input.addEventListener('input', updateSVG);
        updateSVG(); // Initial render
    </script>
</body>
</html>

I've created an interactive demo where you can:

  1. Edit the SVG code in the textarea at the top
  2. See the completed and rendered SVG update in real-time below
  3. View any errors if they occur

The demo comes pre-loaded with an example of an incomplete SVG that shows a blue rectangle and a red circle. Feel free to modify it and watch the preview update instantly.

Some things you can try:

  • Remove closing tags
  • Add new elements without closing them
  • Remove the SVG wrapper entirely
  • Clear the textarea completely
  • Add invalid SVG to see error handling

The completeSVG function will attempt to:

  • Close any unclosed tags
  • Add missing SVG root elements
  • Maintain proper tag nesting order
  • Add necessary SVG namespace attributes

Would you like me to make any adjustments to the demo or add any additional features?

human (Oct 25, 2024, 05:33 PM)

Add a feature where I can paste a whole SVG into a 2em high textarea at the top which has a button next to it that says "Render" - when clicking that button the SVG is animate-drawn over the course of 5s by every 100ms outputting a portion of that SVG code with the completion function used to complete it

assistant (Oct 25, 2024, 05:33 PM)

I'll modify the demo to add this animated drawing feature.

rewrite Untitled

<!DOCTYPE html>
<html>
<head>
    <style>
        body {
            font-family: system-ui, -apple-system, sans-serif;
            max-width: 800px;
            margin: 20px auto;
            padding: 20px;
            background: #f5f5f5;
        }
        
        .input-group {
            display: flex;
            gap: 10px;
            margin-bottom: 20px;
        }

        .input-group textarea {
            flex-grow: 1;
            height: 2em;
            padding: 8px 12px;
            font-family: monospace;
            border: 2px solid #ccc;
            border-radius: 4px;
            resize: none;
        }

        button {
            padding: 0 20px;
            background: #0066cc;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            font-weight: 600;
        }

        button:hover {
            background: #0052a3;
        }

        button:disabled {
            background: #cccccc;
            cursor: not-allowed;
        }
        
        .editor textarea {
            width: 100%;
            height: 150px;
            padding: 12px;
            margin-bottom: 20px;
            font-family: monospace;
            border: 2px solid #ccc;
            border-radius: 4px;
            resize: vertical;
        }
        
        #output {
            background: white;
            border: 2px solid #ddd;
            border-radius: 4px;
            padding: 20px;
            min-height: 300px;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        
        #output svg {
            max-width: 100%;
            max-height: 260px;
        }
        
        .error {
            color: #cc0000;
            margin-top: 10px;
            font-size: 14px;
            min-height: 20px;
        }
        
        .label {
            font-weight: 600;
            margin-bottom: 8px;
            color: #333;
        }

        .progress {
            height: 4px;
            background: #eee;
            margin-bottom: 20px;
            border-radius: 2px;
            overflow: hidden;
        }

        .progress-bar {
            height: 100%;
            background: #0066cc;
            width: 0%;
            transition: width 0.1s ease-out;
        }
    </style>
</head>
<body>
    <div class="label">Paste complete SVG to animate:</div>
    <div class="input-group">
        <textarea id="fullSvg"></textarea>
        <button id="renderBtn">Render</button>
    </div>
    <div class="progress">
        <div class="progress-bar" id="progressBar"></div>
    </div>

    <div class="label">Live editor:</div>
    <div class="editor">
        <textarea id="input"><svg><rect x="10" y="10" width="80" height="80" fill="blue"></rect><circle cx="120" cy="50" r="30" fill="red"</textarea>
    </div>
    
    <div class="label">Live Preview:</div>
    <div id="output"></div>
    <div id="error" class="error"></div>

    <script>
        function completeSVG(incompleteSVG) {
            if (!incompleteSVG || !incompleteSVG.trim()) {
                return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"></svg>';
            }

            const openTags = [];
            const tagRegex = /<\/?([a-zA-Z0-9]+)(\s+[^>]*)?>/g;
            let match;
            let currentPos = 0;
            let processedSVG = '';
            
            while ((match = tagRegex.exec(incompleteSVG)) !== null) {
                const fullTag = match[0];
                const tagName = match[1];
                const isClosingTag = fullTag.startsWith('</');
                
                processedSVG += incompleteSVG.slice(currentPos, match.index);
                processedSVG += fullTag;
                currentPos = tagRegex.lastIndex;
                
                if (!isClosingTag && !fullTag.endsWith('/>')) {
                    openTags.push(tagName);
                } else if (isClosingTag) {
                    if (openTags.length > 0 && openTags[openTags.length - 1] === tagName) {
                        openTags.pop();
                    }
                }
            }
            
            processedSVG += incompleteSVG.slice(currentPos);
            
            if (!incompleteSVG.includes('<svg')) {
                processedSVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100">' + processedSVG;
                openTags.unshift('svg');
            }
            
            while (openTags.length > 0) {
                const tagName = openTags.pop();
                processedSVG += `</${tagName}>`;
            }
            
            return processedSVG;
        }

        const input = document.getElementById('input');
        const fullSvg = document.getElementById('fullSvg');
        const renderBtn = document.getElementById('renderBtn');
        const output = document.getElementById('output');
        const error = document.getElementById('error');
        const progressBar = document.getElementById('progressBar');

        function updateSVG() {
            try {
                const completed = completeSVG(input.value);
                output.innerHTML = completed;
                error.textContent = '';
            } catch (e) {
                error.textContent = 'Error: ' + e.message;
            }
        }

        function animateSVG(svgString) {
            const duration = 5000; // 5 seconds
            const interval = 100; // 100ms steps
            const steps = duration / interval;
            let currentStep = 0;
            
            // Disable the render button during animation
            renderBtn.disabled = true;

            // Clear any existing animation
            if (window.currentAnimation) {
                clearInterval(window.currentAnimation);
            }

            window.currentAnimation = setInterval(() => {
                currentStep++;
                const progress = currentStep / steps;
                const charCount = Math.floor(svgString.length * progress);
                
                // Update progress bar
                progressBar.style.width = `${progress * 100}%`;

                // Get substring and complete it
                const partial = svgString.substring(0, charCount);
                const completed = completeSVG(partial);
                
                // Update display
                output.innerHTML = completed;
                
                // End animation when complete
                if (currentStep >= steps) {
                    clearInterval(window.currentAnimation);
                    renderBtn.disabled = false;
                }
            }, interval);
        }

        input.addEventListener('input', updateSVG);
        renderBtn.addEventListener('click', () => {
            if (fullSvg.value.trim()) {
                animateSVG(fullSvg.value);
            }
        });

        updateSVG(); // Initial render
    </script>
</body>
</html>

I've added the requested feature and made several improvements:

  1. New top section with:

    • A compact textarea (2em high)
    • A "Render" button
    • A progress bar to show animation progress
  2. Animation features:

    • Takes the full SVG and animates it over 5 seconds
    • Updates every 100ms
    • Shows progress with a blue progress bar
    • Disables the Render button during animation
    • Properly completes partial SVG at each step
  3. Additional improvements:

    • Better styling for all elements
    • Clear separation between the animation input and live editor
    • Error handling for invalid SVG
    • Cancels any running animation when starting a new one

To use the new animation feature:

  1. Paste a complete SVG into the top textarea
  2. Click "Render"
  3. Watch as the SVG is progressively drawn over 5 seconds

The original live editor functionality remains unchanged below, so you can still experiment with manual SVG completion in real-time.

Would you like me to adjust anything about the animation timing or add any additional features?

human (Oct 25, 2024, 05:35 PM)

Make the 5s thing an input that defaults to 5 so I can change the duration. Make the live editor panel update with the SVG as it is being rendered in the live preview

assistant (Oct 25, 2024, 05:35 PM)

I'll add duration control and sync the editor with the animation.

rewrite Untitled

<!DOCTYPE html>
<html>
<head>
    <style>
        body {
            font-family: system-ui, -apple-system, sans-serif;
            max-width: 800px;
            margin: 20px auto;
            padding: 20px;
            background: #f5f5f5;
        }
        
        .input-group {
            display: flex;
            gap: 10px;
            margin-bottom: 20px;
            align-items: center;
        }

        .input-group textarea {
            flex-grow: 1;
            height: 2em;
            padding: 8px 12px;
            font-family: monospace;
            border: 2px solid #ccc;
            border-radius: 4px;
            resize: none;
        }

        .duration-input {
            width: 60px;
            height: 100%;
            padding: 8px;
            border: 2px solid #ccc;
            border-radius: 4px;
            font-size: 14px;
        }

        .duration-label {
            font-size: 14px;
            color: #666;
            white-space: nowrap;
        }

        button {
            padding: 0 20px;
            background: #0066cc;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            font-weight: 600;
            height: 100%;
        }

        button:hover {
            background: #0052a3;
        }

        button:disabled {
            background: #cccccc;
            cursor: not-allowed;
        }
        
        .editor textarea {
            width: 100%;
            height: 150px;
            padding: 12px;
            margin-bottom: 20px;
            font-family: monospace;
            border: 2px solid #ccc;
            border-radius: 4px;
            resize: vertical;
        }
        
        #output {
            background: white;
            border: 2px solid #ddd;
            border-radius: 4px;
            padding: 20px;
            min-height: 300px;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        
        #output svg {
            max-width: 100%;
            max-height: 260px;
        }
        
        .error {
            color: #cc0000;
            margin-top: 10px;
            font-size: 14px;
            min-height: 20px;
        }
        
        .label {
            font-weight: 600;
            margin-bottom: 8px;
            color: #333;
        }

        .progress {
            height: 4px;
            background: #eee;
            margin-bottom: 20px;
            border-radius: 2px;
            overflow: hidden;
        }

        .progress-bar {
            height: 100%;
            background: #0066cc;
            width: 0%;
            transition: width 0.1s ease-out;
        }
    </style>
</head>
<body>
    <div class="label">Paste complete SVG to animate:</div>
    <div class="input-group">
        <textarea id="fullSvg"></textarea>
        <input type="number" id="duration" class="duration-input" value="5" min="0.1" step="0.1">
        <span class="duration-label">seconds</span>
        <button id="renderBtn">Render</button>
    </div>
    <div class="progress">
        <div class="progress-bar" id="progressBar"></div>
    </div>

    <div class="label">Live editor:</div>
    <div class="editor">
        <textarea id="input" spellcheck="false"><svg><rect x="10" y="10" width="80" height="80" fill="blue"></rect><circle cx="120" cy="50" r="30" fill="red"</textarea>
    </div>
    
    <div class="label">Live Preview:</div>
    <div id="output"></div>
    <div id="error" class="error"></div>

    <script>
        function completeSVG(incompleteSVG) {
            if (!incompleteSVG || !incompleteSVG.trim()) {
                return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"></svg>';
            }

            const openTags = [];
            const tagRegex = /<\/?([a-zA-Z0-9]+)(\s+[^>]*)?>/g;
            let match;
            let currentPos = 0;
            let processedSVG = '';
            
            while ((match = tagRegex.exec(incompleteSVG)) !== null) {
                const fullTag = match[0];
                const tagName = match[1];
                const isClosingTag = fullTag.startsWith('</');
                
                processedSVG += incompleteSVG.slice(currentPos, match.index);
                processedSVG += fullTag;
                currentPos = tagRegex.lastIndex;
                
                if (!isClosingTag && !fullTag.endsWith('/>')) {
                    openTags.push(tagName);
                } else if (isClosingTag) {
                    if (openTags.length > 0 && openTags[openTags.length - 1] === tagName) {
                        openTags.pop();
                    }
                }
            }
            
            processedSVG += incompleteSVG.slice(currentPos);
            
            if (!incompleteSVG.includes('<svg')) {
                processedSVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100">' + processedSVG;
                openTags.unshift('svg');
            }
            
            while (openTags.length > 0) {
                const tagName = openTags.pop();
                processedSVG += `</${tagName}>`;
            }
            
            return processedSVG;
        }

        const input = document.getElementById('input');
        const fullSvg = document.getElementById('fullSvg');
        const duration = document.getElementById('duration');
        const renderBtn = document.getElementById('renderBtn');
        const output = document.getElementById('output');
        const error = document.getElementById('error');
        const progressBar = document.getElementById('progressBar');

        // Prevent editor updates during manual editing
        let isManualEdit = false;
        input.addEventListener('input', () => {
            if (!isManualEdit) {
                updateSVG();
            }
        });

        function updateSVG() {
            try {
                const completed = completeSVG(input.value);
                output.innerHTML = completed;
                error.textContent = '';
            } catch (e) {
                error.textContent = 'Error: ' + e.message;
            }
        }

        function animateSVG(svgString) {
            const durationMs = Math.max(100, duration.value * 1000); // Minimum 0.1 seconds
            const interval = 100; // 100ms steps
            const steps = durationMs / interval;
            let currentStep = 0;
            
            // Disable the render button during animation
            renderBtn.disabled = true;

            // Clear any existing animation
            if (window.currentAnimation) {
                clearInterval(window.currentAnimation);
            }

            window.currentAnimation = setInterval(() => {
                currentStep++;
                const progress = currentStep / steps;
                const charCount = Math.floor(svgString.length * progress);
                
                // Update progress bar
                progressBar.style.width = `${progress * 100}%`;

                // Get substring and complete it
                const partial = svgString.substring(0, charCount);
                const completed = completeSVG(partial);
                
                // Update both the preview and editor
                output.innerHTML = completed;
                isManualEdit = true; // Prevent recursive updates
                input.value = partial;
                isManualEdit = false;
                
                // End animation when complete
                if (currentStep >= steps) {
                    clearInterval(window.currentAnimation);
                    renderBtn.disabled = false;
                    progressBar.style.width = '0%';
                }
            }, interval);
        }

        renderBtn.addEventListener('click', () => {
            if (fullSvg.value.trim()) {
                animateSVG(fullSvg.value);
            }
        });

        // Initial render
        updateSVG();

        // Prevent negative durations
        duration.addEventListener('input', () => {
            if (duration.value < 0.1) duration.value = 0.1;
        });
    </script>
</body>
</html>

I've made the following improvements:

  1. Added duration control:

    • Input field that defaults to 5 seconds
    • Minimum value of 0.1 seconds
    • Clear "seconds" label
    • Validates input to prevent negative values
  2. Synchronized the live editor with the animation:

    • Editor updates in real-time as the SVG is being drawn
    • Added a flag to prevent recursive updates when manually editing
    • Editor shows the exact incomplete SVG string at each step
  3. Additional improvements:

    • Better styling for the duration input and label
    • Progress bar now resets after animation completes
    • Added spellcheck="false" to the editor for better code editing
    • Made sure the animation interval stays at 100ms for smooth updates

To use:

  1. Paste a complete SVG in the top textarea
  2. Set your desired duration (0.1 seconds or more)
  3. Click "Render" to start the animation
  4. Watch both the preview and editor update in sync

The live editing feature still works independently when you're not running an animation.

Would you like me to make any adjustments to the timing, synchronization, or add any other features?

human (Oct 25, 2024, 05:37 PM)

use box content border-box on everything. Add this link:

    <a href="https://gist.githubusercontent.com/simonw/aedecb93564af13ac1596810d40cac3c/raw/83e7f3be5b65bba61124684700fa7925d37c36c3/tiger.svg" id="loadExampleLink">Load example image</a>

With JavaScript that does a fetch() against that URL and uses that to populate the initial text box when you click it

Make sure the live editor pane constantly scrolls to the bottom while it is being filled

assistant (Oct 25, 2024, 05:37 PM)

I'll add those improvements.

rewrite Untitled

<!DOCTYPE html>
<html>
<head>
    <style>
        * {
            box-sizing: border-box;
        }

        body {
            font-family: system-ui, -apple-system, sans-serif;
            max-width: 800px;
            margin: 20px auto;
            padding: 20px;
            background: #f5f5f5;
        }
        
        .input-group {
            display: flex;
            gap: 10px;
            margin-bottom: 20px;
            align-items: center;
        }

        .input-group textarea {
            flex-grow: 1;
            height: 2em;
            padding: 8px 12px;
            font-family: monospace;
            border: 2px solid #ccc;
            border-radius: 4px;
            resize: none;
        }

        .duration-input {
            width: 60px;
            height: 100%;
            padding: 8px;
            border: 2px solid #ccc;
            border-radius: 4px;
            font-size: 14px;
        }

        .duration-label {
            font-size: 14px;
            color: #666;
            white-space: nowrap;
        }

        button {
            padding: 0 20px;
            background: #0066cc;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            font-weight: 600;
            height: 100%;
        }

        button:hover {
            background: #0052a3;
        }

        button:disabled {
            background: #cccccc;
            cursor: not-allowed;
        }
        
        .editor textarea {
            width: 100%;
            height: 150px;
            padding: 12px;
            margin-bottom: 20px;
            font-family: monospace;
            border: 2px solid #ccc;
            border-radius: 4px;
            resize: vertical;
        }
        
        #output {
            background: white;
            border: 2px solid #ddd;
            border-radius: 4px;
            padding: 20px;
            min-height: 300px;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        
        #output svg {
            max-width: 100%;
            max-height: 260px;
        }
        
        .error {
            color: #cc0000;
            margin-top: 10px;
            font-size: 14px;
            min-height: 20px;
        }
        
        .label {
            font-weight: 600;
            margin-bottom: 8px;
            color: #333;
        }

        .progress {
            height: 4px;
            background: #eee;
            margin-bottom: 20px;
            border-radius: 2px;
            overflow: hidden;
        }

        .progress-bar {
            height: 100%;
            background: #0066cc;
            width: 0%;
            transition: width 0.1s ease-out;
        }

        #loadExampleLink {
            color: #0066cc;
            text-decoration: none;
            margin-right: 20px;
        }

        #loadExampleLink:hover {
            text-decoration: underline;
        }

        .top-controls {
            display: flex;
            align-items: center;
            margin-bottom: 8px;
        }
    </style>
</head>
<body>
    <div class="label">Paste complete SVG to animate:</div>
    <div class="top-controls">
        <a href="https://gist.githubusercontent.com/simonw/aedecb93564af13ac1596810d40cac3c/raw/83e7f3be5b65bba61124684700fa7925d37c36c3/tiger.svg" id="loadExampleLink">Load example image</a>
    </div>
    <div class="input-group">
        <textarea id="fullSvg"></textarea>
        <input type="number" id="duration" class="duration-input" value="5" min="0.1" step="0.1">
        <span class="duration-label">seconds</span>
        <button id="renderBtn">Render</button>
    </div>
    <div class="progress">
        <div class="progress-bar" id="progressBar"></div>
    </div>

    <div class="label">Live editor:</div>
    <div class="editor">
        <textarea id="input" spellcheck="false"><svg><rect x="10" y="10" width="80" height="80" fill="blue"></rect><circle cx="120" cy="50" r="30" fill="red"</textarea>
    </div>
    
    <div class="label">Live Preview:</div>
    <div id="output"></div>
    <div id="error" class="error"></div>

    <script>
        function completeSVG(incompleteSVG) {
            if (!incompleteSVG || !incompleteSVG.trim()) {
                return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"></svg>';
            }

            const openTags = [];
            const tagRegex = /<\/?([a-zA-Z0-9]+)(\s+[^>]*)?>/g;
            let match;
            let currentPos = 0;
            let processedSVG = '';
            
            while ((match = tagRegex.exec(incompleteSVG)) !== null) {
                const fullTag = match[0];
                const tagName = match[1];
                const isClosingTag = fullTag.startsWith('</');
                
                processedSVG += incompleteSVG.slice(currentPos, match.index);
                processedSVG += fullTag;
                currentPos = tagRegex.lastIndex;
                
                if (!isClosingTag && !fullTag.endsWith('/>')) {
                    openTags.push(tagName);
                } else if (isClosingTag) {
                    if (openTags.length > 0 && openTags[openTags.length - 1] === tagName) {
                        openTags.pop();
                    }
                }
            }
            
            processedSVG += incompleteSVG.slice(currentPos);
            
            if (!incompleteSVG.includes('<svg')) {
                processedSVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100">' + processedSVG;
                openTags.unshift('svg');
            }
            
            while (openTags.length > 0) {
                const tagName = openTags.pop();
                processedSVG += `</${tagName}>`;
            }
            
            return processedSVG;
        }

        const input = document.getElementById('input');
        const fullSvg = document.getElementById('fullSvg');
        const duration = document.getElementById('duration');
        const renderBtn = document.getElementById('renderBtn');
        const output = document.getElementById('output');
        const error = document.getElementById('error');
        const progressBar = document.getElementById('progressBar');
        const loadExampleLink = document.getElementById('loadExampleLink');

        // Prevent editor updates during manual editing
        let isManualEdit = false;
        input.addEventListener('input', () => {
            if (!isManualEdit) {
                updateSVG();
            }
        });

        function updateSVG() {
            try {
                const completed = completeSVG(input.value);
                output.innerHTML = completed;
                error.textContent = '';
            } catch (e) {
                error.textContent = 'Error: ' + e.message;
            }
        }

        function animateSVG(svgString) {
            const durationMs = Math.max(100, duration.value * 1000); // Minimum 0.1 seconds
            const interval = 100; // 100ms steps
            const steps = durationMs / interval;
            let currentStep = 0;
            
            // Disable the render button during animation
            renderBtn.disabled = true;

            // Clear any existing animation
            if (window.currentAnimation) {
                clearInterval(window.currentAnimation);
            }

            window.currentAnimation = setInterval(() => {
                currentStep++;
                const progress = currentStep / steps;
                const charCount = Math.floor(svgString.length * progress);
                
                // Update progress bar
                progressBar.style.width = `${progress * 100}%`;

                // Get substring and complete it
                const partial = svgString.substring(0, charCount);
                const completed = completeSVG(partial);
                
                // Update both the preview and editor
                output.innerHTML = completed;
                isManualEdit = true; // Prevent recursive updates
                input.value = partial;
                
                // Scroll editor to bottom
                input.scrollTop = input.scrollHeight;
                
                isManualEdit = false;
                
                // End animation when complete
                if (currentStep >= steps) {
                    clearInterval(window.currentAnimation);
                    renderBtn.disabled = false;
                    progressBar.style.width = '0%';
                }
            }, interval);
        }

        renderBtn.addEventListener('click', () => {
            if (fullSvg.value.trim()) {
                animateSVG(fullSvg.value);
            }
        });

        // Load example image
        loadExampleLink.addEventListener('click', async (e) => {
            e.preventDefault();
            try {
                const response = await fetch(loadExampleLink.href);
                if (!response.ok) throw new Error('Network response was not ok');
                const svgText = await response.text();
                fullSvg.value = svgText;
            } catch (error) {
                console.error('Error loading example:', error);
                error.textContent = 'Error loading example: ' + error.message;
            }
        });

        // Initial render
        updateSVG();

        // Prevent negative durations
        duration.addEventListener('input', () => {
            if (duration.value < 0.1) duration.value = 0.1;
        });
    </script>
</body>
</html>

I've made the following changes:

  1. Added universal box-sizing:
* {
    box-sizing: border-box;
}
  1. Added the example image link with:
  • Styled link appearance
  • Fetch functionality
  • Error handling
  • Proper positioning above the input group
  • Prevention of default link behavior
  1. Added auto-scrolling:
  • The live editor now automatically scrolls to the bottom during animation
  • Using scrollTop = scrollHeight for smooth scrolling
  • This happens on every update during the animation
  1. Additional improvements:
  • Better error handling for the fetch request
  • Proper styling for the new link element
  • Maintained all existing functionality

To use the new features:

  1. Click "Load example image" to load the tiger SVG
  2. The SVG code will appear in the top textarea
  3. Click "Render" to start the animation
  4. Watch the editor auto-scroll as it fills with code

Would you like me to make any adjustments to the scrolling behavior or add any other features?

human (Oct 25, 2024, 05:39 PM)

Make that first textarea have height 2.8em - and set its placeholder to "Paste SVG here" - make it so if anything is pasted in there or if the example link is clicked the Live editor and the preview SVG is cleared

assistant (Oct 25, 2024, 05:39 PM)

I'll make those adjustments.

update svg-demo

Find this:

            height: 2em;

Replace with this:

            height: 2.8em;

update svg-demo

Find this:

        <textarea id="fullSvg"></textarea>

Replace with this:

        <textarea id="fullSvg" placeholder="Paste SVG here"></textarea>

update svg-demo

Find this:

            if (!response.ok) throw new Error('Network response was not ok');
                const svgText = await response.text();
                fullSvg.value = svgText;

Replace with this:

            if (!response.ok) throw new Error('Network response was not ok');
                const svgText = await response.text();
                fullSvg.value = svgText;
                // Clear the editor and preview
                input.value = '';
                output.innerHTML = '';

update svg-demo

Find this:

        fullSvg.value = svgText;

Replace with this:

        fullSvg.value = svgText;
                error.textContent = '';

update svg-demo

Find this:

        renderBtn.addEventListener('click', () => {

Replace with this:

        // Handle paste and input events on the fullSvg textarea
        fullSvg.addEventListener('input', () => {
            // Clear the editor and preview
            input.value = '';
            output.innerHTML = '';
            error.textContent = '';
        });

        renderBtn.addEventListener('click', () => {

I've made all the requested changes:

  1. Changed the first textarea height to 2.8em
  2. Added "Paste SVG here" placeholder text
  3. Added clearing behavior when:
    • Something is pasted/typed into the top textarea
    • The example link is clicked
    • The clearing includes:
      • The live editor content
      • The preview SVG
      • Any error messages

The live editor and preview will now stay clear until you click the "Render" button, giving a cleaner experience when preparing to render a new SVG.

Would you like me to make any other adjustments to this behavior?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment