Draggable svg object

Draggable svg object

ต้องการ สร้าง หรือ แก้ไข ภาพ SVG โดยเราสามารถทำการ add text และสามารถ คลิกเพื่อเลื่อน Object ที่เราเห็นในไฟล์รูปภาพนามสกุล SVG ไปยังตำแหน่งต่างๆได้

  1. user เลือกไฟล์ภาพ SVG มันจะถูกนำขึ้นมาแสดงที่พื้นที่ด้านขวา
  2. user สามารถ add text เพื่อแทรกตัวอักษรที่ต้องการลงในภาพ
  3. user สามารถคลิกบน text ที่สร้างขึ้นมาแล้วลากมันไปวางที่ตำแหน่งที่ต้องการ
  4. user กด download เพื่อ save ภาพที่ผ่านการแก้ไข ลงเป็นไฟล์ใหม่
  5. user กด reset and clear เพื่อลบภาพออกจากพื้นที่แสดงผล

HTML ส่วน control button
มี control อยู่ทั้งหมด 1 file button + 3 buttons + 1 textbox
1. file button -> id = svg-upload
2. button -> id = add-text-btn
3. text -> id = text-input
4. button -> id = download-btn
5. button -> id = reset-btn

            <!-- Controls Column -->
            <aside class="lg:col-span-1 bg-white p-6 rounded-lg shadow-lg h-fit">
                <h2 class="text-2xl font-bold mb-4 border-b pb-2">Controls</h2>
                
                <!-- File Upload -->
                <div class="mb-6">
                    <label for="svg-upload" class="block text-sm font-medium text-slate-700 mb-2">1. Load SVG File</label>
                    <input type="file" id="svg-upload" accept=".svg,image/svg+xml" class="block w-full text-sm text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"/>
                    <p class="mt-1 text-xs text-slate-500">Elements with class="draggable" will be movable.</p>
                </div>

                <!-- Add Text -->
                <div class="mb-6">
                    <label for="text-input" class="block text-sm font-medium text-slate-700 mb-2">2. Add Text</label>
                    <input type="text" id="text-input" placeholder="Enter text to add" class="block w-full px-3 py-2 bg-white border border-slate-300 rounded-md text-sm shadow-sm placeholder-slate-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500">
                    <button id="add-text-btn" class="mt-2 w-full bg-blue-600 text-white font-bold py-2 px-4 rounded-lg hover:bg-blue-700 transition-colors">Add Text</button>
                </div>
                
                <!-- Save/Reset -->
                <div>
                    <label class="block text-sm font-medium text-slate-700 mb-2">3. Save or Reset</label>
                    <button id="download-btn" class="w-full bg-green-600 text-white font-bold py-2 px-4 rounded-lg hover:bg-green-700 transition-colors">Download New SVG File</button>
                    <button id="reset-btn" class="mt-2 w-full bg-red-600 text-white font-bold py-2 px-4 rounded-lg hover:bg-red-700 transition-colors">Reset and Clear</button>
                </div>
            </aside>

HTML ส่วนแสดงผล – เป็น div svg-container ธรรมดาเท่านั้น

            <!-- SVG Display Column -->
            <main class="lg:col-span-2 bg-white p-2 rounded-lg shadow-lg">
                <div id="svg-container" class="w-full h-[600px]">
                    <span id="svg-placeholder" class="text-slate-500 text-center p-4">Upload an SVG file to begin editing.</span>
                </div>
            </main>

เพิ่ม Javascript สำหรับ control button
1. loadState – สำหรับโหลดภาพที่เปิดทิ้งเอาไว้ล่าสุดขึ้นมา + รวมถึงมีการแก้ไขเอาไว้ด้วย
2. saveState – เก็บภาพที่แก้ไขล่าสุดเอาไว้เอาไว้เผื่อตอนเปิดครั้งหน้า
3. actionListener – reset button : click – ลบภาพออกจากหน้าจอ
4. displaySvg
5. actionListener – file input : change
6. initializeDraggable – สำหรับ add mouse listener (startDrag) ให้กับ svg element ที่มี class draggable ทุกตัว
7. getMousePosition – สำหรับหาตำแหน่งที่ mouse อยู่
8. startDrag – ย้ายตำแหน่งตาม Mouse + add mouse listener สำหรับ (drag + endDrag)
9. drag – ย้ายตำแหน่งตาม Mouse
10. endDrag – end drag แล้วก็ลบ mouse listener ออก จะได้ไม่ซ้ำซ้อน เวลาเริ่ม move ใหม่
11. actionListener – add text button : click
12. actionListener – download button : click

ไฟล์เต็ม

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Interactive SVG Editor</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
    <style>
        body {
            font-family: 'Inter', sans-serif;
        }
        /* Style for the SVG container */
        #svg-container {
            width: 100%;
            height: 100%;
            border: 2px dashed #cbd5e1;
            border-radius: 0.5rem;
            background-color: #f8fafc;
            display: flex;
            align-items: center;
            justify-content: center;
            overflow: hidden; /* Important for SVG scaling */
        }
        #svg-container.has-content {
            border-style: solid;
            border-color: #94a3b8;
        }
        /* Add a grabbing cursor when an element is being dragged */
        #svg-container.dragging {
             cursor: grabbing;
        }
        /* Add a move cursor to elements with the 'draggable' class */
        #svg-container .draggable {
            cursor: grab;
            user-select: none;
        }
    </style>
</head>
<body class="bg-slate-100 text-slate-800">

    <div class="container mx-auto p-4 md:p-8">
        <header class="text-center mb-8">
            <h1 class="text-4xl font-bold text-slate-900">Interactive SVG Editor</h1>
            <p class="mt-2 text-lg text-slate-600">Upload, drag, add text, and save your SVG. Your work is saved in the browser automatically.</p>
        </header>

        <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
            <!-- Controls Column -->
            <aside class="lg:col-span-1 bg-white p-6 rounded-lg shadow-lg h-fit">
                <h2 class="text-2xl font-bold mb-4 border-b pb-2">Controls</h2>
                
                <!-- File Upload -->
                <div class="mb-6">
                    <label for="svg-upload" class="block text-sm font-medium text-slate-700 mb-2">1. Load SVG File</label>
                    <input type="file" id="svg-upload" accept=".svg,image/svg+xml" class="block w-full text-sm text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"/>
                    <p class="mt-1 text-xs text-slate-500">Elements with class="draggable" will be movable.</p>
                </div>

                <!-- Add Text -->
                <div class="mb-6">
                    <label for="text-input" class="block text-sm font-medium text-slate-700 mb-2">2. Add Text</label>
                    <input type="text" id="text-input" placeholder="Enter text to add" class="block w-full px-3 py-2 bg-white border border-slate-300 rounded-md text-sm shadow-sm placeholder-slate-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500">
                    <button id="add-text-btn" class="mt-2 w-full bg-blue-600 text-white font-bold py-2 px-4 rounded-lg hover:bg-blue-700 transition-colors">Add Text</button>
                </div>
                
                <!-- Save/Reset -->
                <div>
                    <label class="block text-sm font-medium text-slate-700 mb-2">3. Save or Reset</label>
                    <button id="download-btn" class="w-full bg-green-600 text-white font-bold py-2 px-4 rounded-lg hover:bg-green-700 transition-colors">Download New SVG File</button>
                    <button id="reset-btn" class="mt-2 w-full bg-red-600 text-white font-bold py-2 px-4 rounded-lg hover:bg-red-700 transition-colors">Reset and Clear</button>
                </div>
            </aside>

            <!-- SVG Display Column -->
            <main class="lg:col-span-2 bg-white p-2 rounded-lg shadow-lg">
                <div id="svg-container" class="w-full h-[600px]">
                    <span id="svg-placeholder" class="text-slate-500 text-center p-4">Upload an SVG file to begin editing.</span>
                </div>
            </main>
        </div>
    </div>

    <script>
        window.onload = () => {
            // DOM Elements
            const svgContainer = document.getElementById('svg-container');
            const svgPlaceholder = document.getElementById('svg-placeholder');
            const fileInput = document.getElementById('svg-upload');
            const textInput = document.getElementById('text-input');
            const addTextBtn = document.getElementById('add-text-btn');
            const downloadBtn = document.getElementById('download-btn');
            const resetBtn = document.getElementById('reset-btn');

            // SVG Manipulation State
            const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
            let activeElement = null;
            let offset = { x: 0, y: 0 };
            let currentSVG = null;

            // --- LOCAL STORAGE & INITIALIZATION ---

            /**
             * Loads SVG content from localStorage if it exists.
             */
            function loadState() {
                const savedSVG = localStorage.getItem('savedUserSVG');
                if (savedSVG) {
                    displaySVG(savedSVG);
                }
            }

            /**
             * Saves the current SVG content to localStorage.
             */
            function saveState() {
                if (currentSVG) {
                    const svgString = new XMLSerializer().serializeToString(currentSVG);
                    localStorage.setItem('savedUserSVG', svgString);
                    console.log("SVG state saved.");
                }
            }

            /**
             * Resets the application state and clears localStorage.
             */
            resetBtn.addEventListener('click', () => {
                if (confirm("Are you sure you want to clear your current work? This cannot be undone.")) {
                    localStorage.removeItem('savedUserSVG');
                    svgContainer.innerHTML = '';
                    svgContainer.appendChild(svgPlaceholder);
                    svgContainer.classList.remove('has-content');
                    currentSVG = null;
                }
            });


            // --- SVG DISPLAY & FILE HANDLING ---

            /**
             * Injects SVG string into the container and sets up interactivity.
             * @param {string} svgString - The SVG content as a string.
             */
            function displaySVG(svgString) {
                svgContainer.innerHTML = svgString;
                currentSVG = svgContainer.querySelector('svg');
                if (currentSVG) {
                    svgContainer.classList.add('has-content');
                    // Ensure the SVG scales within the container
                    currentSVG.setAttribute('width', '100%');
                    currentSVG.setAttribute('height', '100%');
                    initializeDraggable(currentSVG);
                } else {
                     svgContainer.classList.remove('has-content');
                }
            }

            /**
             * Handles the file upload event.
             */
            fileInput.addEventListener('change', (event) => {
                const file = event.target.files[0];
                if (file && file.type === "image/svg+xml") {
                    const reader = new FileReader();
                    reader.onload = (e) => {
                        displaySVG(e.target.result);
                        saveState(); // Save the newly loaded SVG as the current state
                    };
                    reader.readAsText(file);
                } else {
                    alert("Please upload a valid .svg file.");
                }
            });


            // --- DRAG AND DROP LOGIC ---

            /**
             * Sets up mousedown listeners on all 'draggable' elements within the SVG.
             * @param {SVGElement} svgElement - The root SVG element.
             */
            function initializeDraggable(svgElement) {
                const draggables = svgElement.querySelectorAll('.draggable');
                draggables.forEach(el => {
                    el.addEventListener('mousedown', startDrag);
                });
            }

            /**
             * Converts screen coordinates to SVG coordinates.
             * @param {MouseEvent} event - The mouse event.
             * @returns {{x: number, y: number}} The coordinates within the SVG space.
             */
            function getMousePosition(event) {
                const CTM = currentSVG.getScreenCTM();
                return {
                    x: (event.clientX - CTM.e) / CTM.a,
                    y: (event.clientY - CTM.f) / CTM.d
                };
            }

            /**
             * Initiates the drag operation.
             * @param {MouseEvent} event - The mousedown event.
             */
            function startDrag(event) {
                event.preventDefault();
                if (event.target.classList.contains('draggable')) {
                    activeElement = event.target;
                    svgContainer.classList.add('dragging');
                    offset = getMousePosition(event);

                    // Handle existing transforms
                    const transform = activeElement.transform.baseVal;
                    if (transform.length === 0 || transform.getItem(0).type !== SVGTransform.SVG_TRANSFORM_TRANSLATE) {
                        const translate = currentSVG.createSVGTransform();
                        translate.setTranslate(0, 0);
                        activeElement.transform.baseVal.insertItemBefore(translate, 0);
                    }
                    const initialTransform = transform.getItem(0).matrix;
                    offset.x -= initialTransform.e;
                    offset.y -= initialTransform.f;

                    // Add listeners to the whole container for smoother dragging
                    svgContainer.addEventListener('mousemove', drag);
                    svgContainer.addEventListener('mouseup', endDrag);
                    svgContainer.addEventListener('mouseleave', endDrag);
                }
            }

            /**
             * Performs the drag translation.
             * @param {MouseEvent} event - The mousemove event.
             */
            function drag(event) {
                if (activeElement) {
                    event.preventDefault();
                    const coord = getMousePosition(event);
                    const transform = activeElement.transform.baseVal.getItem(0);
                    transform.setTranslate(coord.x - offset.x, coord.y - offset.y);
                }
            }

            /**
             * Ends the drag operation and saves the state.
             */
            function endDrag() {
                if (activeElement) {
                    activeElement = null;
                    svgContainer.classList.remove('dragging');
                    svgContainer.removeEventListener('mousemove', drag);
                    svgContainer.removeEventListener('mouseup', endDrag);
                    svgContainer.removeEventListener('mouseleave', endDrag);
                    saveState(); // Save state after modification
                }
            }

            // --- ADD TEXT LOGIC ---
            
            addTextBtn.addEventListener('click', () => {
                const textContent = textInput.value;
                if (!textContent || !currentSVG) {
                    alert("Please load an SVG and enter some text first.");
                    return;
                }

                const newText = document.createElementNS(SVG_NAMESPACE, "text");
                const viewBox = currentSVG.viewBox.baseVal;
                const randomX = viewBox.x + viewBox.width / 2;
                const randomY = viewBox.y + viewBox.height / 2;

                newText.setAttribute("x", randomX);
                newText.setAttribute("y", randomY);
                newText.setAttribute("font-size", "20");
                newText.setAttribute("font-family", "Inter, sans-serif");
                newText.setAttribute("fill", "#1e293b");
                newText.setAttribute("text-anchor", "middle");
                newText.classList.add('draggable'); // Make new text draggable by default
                newText.textContent = textContent;

                currentSVG.appendChild(newText);
                initializeDraggable(currentSVG); // Re-initialize to include the new text element
                saveState(); // Save state after adding text
                textInput.value = ''; // Clear input field
            });


            // --- DOWNLOAD LOGIC ---

            downloadBtn.addEventListener('click', () => {
                if (!currentSVG) {
                    alert("There is no SVG to download. Please load one first.");
                    return;
                }
                const svgData = new XMLSerializer().serializeToString(currentSVG);
                const blob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
                const url = URL.createObjectURL(blob);
                const link = document.createElement("a");
                link.href = url;
                link.download = "modified-svg-image.svg";
                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link);
                URL.revokeObjectURL(url);
            });

            // --- INITIAL LOAD ---
            loadState();
        };
    </script>
</body>
</html>