403Webshell
Server IP : 127.0.0.1  /  Your IP : 216.73.216.48
Web Server : Apache/2.4.58 (Win64) OpenSSL/3.1.3 PHP/8.2.12
System : Windows NT DESKTOP-3H4FHQJ 10.0 build 19045 (Windows 10) AMD64
User : win 10 ( 0)
PHP Version : 8.2.12
Disable Function : NONE
MySQL : OFF |  cURL : ON |  WGET : OFF |  Perl : OFF |  Python : OFF |  Sudo : OFF |  Pkexec : OFF
Directory :  D:/xampp/htdocs-coblaa/CB_quote_editor/

Upload File :
current_dir [ Writeable] document_root [ Writeable]

 

Command :


[ Back ]     

Current File : D:/xampp/htdocs-coblaa/CB_quote_editor/index.php
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CB Quote Editor</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Roboto:wght@400;700&family=Playfair+Display:wght@400;700&family=Open+Sans:wght@400;700&family=Lora:wght@440;700&family=Times+New+Roman&family=Didot&family=Georgia&family=Palatino&family=Bookman&family=New+Century+Schoolbook&family=American+Typewriter&family=Andale+Mono&family=Courier+New&family=Arial&family=Helvetica&family=Verdana&family=Trebuchet+MS&family=Gill+Sans&family=Noto+Sans&family=Optima&family=Comic+Sans+MS&family=Apple+Chancery&family=Bradley+Hand&family=Brush+Script+MT&family=Snell+Roundhand&family=Impact&family=Luminari&family=Chalkduster&family=Jazz+LET&family=Blippo&family=Stencil+Std&family=Marker+Felt&family=Trattatello&display=swap" rel="stylesheet">
    <style>
        body {
            font-family: 'Inter', sans-serif;
            margin: 0;
            overflow-y: auto; /* Allow scrolling for more controls and overall page */
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center; /* Center content vertically */
            min-height: 100vh;
            background-color: #f0f2f5; /* Light background */
            color: #333;
        }

        canvas {
            border-radius: 1rem; /* Rounded corners for canvas */
            box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
            background-color: #ffffff; /* White canvas background */
            display: block;
            touch-action: none; /* Disable default touch actions to prevent scrolling/zooming during drag */
            cursor: grab; /* Indicate draggable */
            /* Canvas dimensions will be set by JS for responsiveness */
        }

        canvas.dragging {
            cursor: grabbing; /* Indicate dragging in progress */
        }

        .section-container {
            width: 90%;
            max-width: 600px;
            margin-top: 1.5rem;
            padding: 1.25rem;
            background-color: #ffffff;
            border-radius: 1rem;
            box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
        }

        .section-title {
            font-size: 1.25rem;
            font-weight: 700;
            color: #333;
            margin-bottom: 1rem;
            text-align: center;
        }

        .button-group {
            display: flex;
            gap: 0.75rem;
            flex-wrap: wrap;
            justify-content: center;
            margin-bottom: 1rem;
        }

        .button {
            padding: 0.75rem 1.5rem;
            border-radius: 0.75rem;
            font-weight: 700;
            cursor: pointer;
            transition: all 0.2s ease-in-out;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            font-size: 1rem;
        }

        .button-primary {
            background-color: #4CAF50; /* Green */
            color: white;
            border: none;
        }

        .button-primary:hover {
            background-color: #45a049;
            transform: translateY(-2px);
        }

        .button-secondary {
            background-color: #007BFF; /* Blue */
            color: white;
            border: none;
        }

        .button-secondary:hover {
            background-color: #0056b3;
            transform: translateY(-2px);
        }

        .button-tertiary {
            background-color: #FFC107; /* Amber */
            color: #333;
            border: none;
        }

        .button-tertiary:hover {
            background-color: #e0a800;
            transform: translateY(-2px);
        }

        .button-danger {
            background-color: #dc3545; /* Red */
            color: white;
            border: none;
        }

        .button-danger:hover {
            background-color: #c82333;
            transform: translateY(-2px);
        }

        .control-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
            gap: 1rem;
        }

        .control-group {
            display: flex;
            flex-direction: column;
            gap: 0.25rem;
        }

        .control-group label {
            font-size: 0.9rem;
            font-weight: 600;
            color: #555;
        }

        .control-group input[type="color"],
        .control-group input[type="number"],
        .control-group input[type="text"],
        .control-group textarea,
        .control-group select {
            padding: 0.5rem;
            border-radius: 0.5rem;
            border: 1px solid #ccc;
            font-family: 'Inter', sans-serif;
            font-size: 0.9rem;
            box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
            width: 100%; /* Full width within its grid column */
        }

        .control-group textarea {
            height: 70px; /* Adjust height for textareas */
            resize: vertical;
        }

        /* Specific font family styles for the select option previews */
        .font-inter { font-family: 'Inter', sans-serif; }
        .font-roboto { font-family: 'Roboto', sans-serif; }
        .font-playfair-display { font-family: 'Playfair Display', serif; }
        .font-open-sans { font-family: 'Open Sans', sans-serif; }
        .font-lora { font-family: 'Lora', serif; }
        .font-times { font-family: 'Times', 'Times New Roman', serif; }
        .font-didot { font-family: 'Didot', serif; }
        .font-georgia { font-family: 'Georgia', serif; }
        .font-palatino { font-family: 'Palatino', 'URW Palladio L', serif; }
        .font-bookman { font-family: 'Bookman', 'URW Bookman L', serif; }
        .font-new-century-schoolbook { font-family: 'New Century Schoolbook', 'TeX Gyre Schola', serif; }
        .font-american-typewriter { font-family: 'American Typewriter', serif; }
        .font-serif { font-family: 'serif'; }
        .font-monospace { font-family: 'monospace'; }
        .font-andale-mono { font-family: 'Andale Mono', monospace; }
        .font-courier-new { font-family: 'Courier New', monospace; }
        .font-courier { font-family: 'Courier', monospace; }
        .font-arial { font-family: 'Arial', sans-serif; }
        .font-helvetica { font-family: 'Helvetica', sans-serif; }
        .font-verdana { font-family: 'Verdana', sans-serif; }
        .font-trebuchet-ms { font-family: 'Trebuchet MS', sans-serif; }
        .font-gill-sans { font-family: 'Gill Sans', sans-serif; }
        .font-noto-sans { font-family: 'Noto Sans', sans-serif; }
        .font-avantgarde { font-family: 'Avantgarde', 'TeX Gyre Adventor', 'URW Gothic L', sans-serif; }
        .font-optima { font-family: 'Optima', sans-serif; }
        .font-arial-narrow { font-family: 'Arial Narrow', sans-serif; }
        .font-sans-serif { font-family: 'sans-serif'; }
        .font-freemono { font-family: 'FreeMono', monospace; }
        .font-ocr-a-std { font-family: 'OCR A Std', monospace; }
        .font-dejavu-sans-mono { font-family: 'DejaVu Sans Mono', monospace; }
        .font-cursive { font-family: 'cursive'; }
        .font-comic-sans-ms { font-family: 'Comic Sans MS', 'Comic Sans', cursive; }
        .font-apple-chancery { font-family: 'Apple Chancery', cursive; }
        .font-bradley-hand { font-family: 'Bradley Hand', cursive; }
        .font-brush-script-mt { font-family: 'Brush Script MT', 'Brush Script Std', cursive; }
        .font-snell-roundhand { font-family: 'Snell Roundhand', cursive; }
        .font-urw-chancery-l { font-family: 'URW Chancery L', cursive; }
        .font-impact { font-family: 'Impact', fantasy; }
        .font-luminari { font-family: 'Luminari', fantasy; }
        .font-chalkduster { font-family: 'Chalkduster', fantasy; }
        .font-jazz-let { font-family: 'Jazz LET', fantasy; }
        .font-blippo { font-family: 'Blippo', fantasy; }
        .font-stencil-std { font-family: 'Stencil Std', fantasy; }
        .font-marker-felt { font-family: 'Marker Felt', fantasy; }
        .font-trattatello { font-family: 'Trattatello', fantasy; }
        .font-fantasy { font-family: 'fantasy'; }


        /* Responsive adjustments */
        @media (max-width: 768px) {
            body {
                padding: 1rem;
            }
            .section-container {
                width: 95%;
                padding: 1rem;
            }
            .button-group {
                flex-direction: column;
                gap: 0.5rem;
            }
            .button {
                width: 100%;
            }
            .control-grid {
                grid-template-columns: 1fr; /* Stack controls vertically on small screens */
            }
            /* Removed canvas-wrapper height adjustment as it's no longer needed */
        }
    </style>
</head>
<body class="bg-gray-100 flex flex-col items-center min-h-screen">
    <div class="section-title" style="height:50px;line-height:50px;">CB Quote Editor</div>

    <!-- Canvas is now directly in the body, and its size will be set by JS -->
    <canvas id="quoteCanvas"></canvas>

    <div class="section-container">
        <div class="section-title">Quote Text</div>
        <div class="control-grid">
            <div class="control-group">
                <label for="quoteLine1Textarea">Title:</label>
                <textarea id="quoteLine1Textarea" placeholder="Enter title..."></textarea>
            </div>
            <div class="control-group">
                <label for="quoteLine2Textarea">Body Text (Optional):</label>
                <textarea id="quoteLine2Textarea" placeholder="Enter body text..."></textarea>
            </div>
            <div class="control-group">
                <label for="authorInput">Author:</label>
                <input type="text" id="authorInput" placeholder="Enter author..." value="Coblaa">
            </div>
        </div>
        <!-- Removed Update Quote and New Random Quote buttons -->
    </div>

    <div class="section-container">
        <div class="section-title">Text Settings</div>
        <div class="control-grid">
            <div class="control-group">
                <label for="quoteLine1Color">Title Color:</label>
                <input type="color" id="quoteLine1Color" value="#333333">
            </div>
            <div class="control-group">
                <label for="quoteLine1FontSize">Title Font Size:</label>
                <input type="number" id="quoteLine1FontSize" value="18" min="10" max="100">
            </div>
            <div class="control-group">
                <label for="titleYAdjust">Title Vertical Adjust (px):</label>
                <input type="number" id="titleYAdjust" value="0" step="5">
            </div>
            <div class="control-group">
                <label for="titleXAdjust">Title Horizontal Adjust (px):</label>
                <input type="number" id="titleXAdjust" value="0" step="5">
            </div>

            <div class="control-group">
                <label for="quoteLine2Color">Body Text Color:</label>
                <input type="color" id="quoteLine2Color" value="#333333">
            </div>
            <div class="control-group">
                <label for="quoteLine2FontSize">Body Text Font Size:</label>
                <input type="number" id="quoteLine2FontSize" value="16" min="10" max="100">
            </div>
            <div class="control-group">
                <label for="bodyYAdjust">Body Vertical Adjust (px):</label>
                <input type="number" id="bodyYAdjust" value="0" step="5">
            </div>
            <div class="control-group">
                <label for="bodyXAdjust">Body Horizontal Adjust (px):</label>
                <input type="number" id="bodyXAdjust" value="0" step="5">
            </div>

            <div class="control-group">
                <label for="authorTextColor">Author Color:</label>
                <input type="color" id="authorTextColor" value="#555555">
            </div>
            <div class="control-group">
                <label for="authorFontSize">Author Font Size:</label>
                <input type="number" id="authorFontSize" value="12" min="10" max="100">
            </div>
            <div class="control-group">
                <label for="authorYAdjust">Author Vertical Adjust (px):</label>
                <input type="number" id="authorYAdjust" value="0" step="5">
            </div>
            <div class="control-group">
                <label for="authorXAdjust">Author Horizontal Adjust (px):</label>
                <input type="number" id="authorXAdjust" value="0" step="5">
            </div>

            <div class="control-group">
                <label for="fontFamilySelector">Font Family:</label>
                <select id="fontFamilySelector">
                    <option value="Inter, sans-serif" class="font-inter">Inter</option>
                    <option value="Roboto, sans-serif" class="font-roboto">Roboto</option>
                    <option value="Playfair Display, serif" class="font-playfair-display">Playfair Display</option>
                    <option value="Open Sans, sans-serif" class="font-open-sans">Open Sans</option>
                    <option value="Lora, serif" class="font-lora">Lora</option>
                    <option value="Times, Times New Roman, serif" class="font-times">Times, Times New Roman</option>
                    <option value="Didot, serif" class="font-didot">Didot</option>
                    <option value="Georgia, serif" class="font-georgia">Georgia</option>
                    <option value="Palatino, URW Palladio L, serif" class="font-palatino">Palatino</option>
                    <option value="Bookman, URW Bookman L, serif" class="font-bookman">Bookman</option>
                    <option value="New Century Schoolbook, TeX Gyre Schola, serif" class="font-new-century-schoolbook">New Century Schoolbook</option>
                    <option value="American Typewriter, serif" class="font-american-typewriter">American Typewriter</option>
                    <option value="serif" class="font-serif">serif (Generic)</option>
                    <option value="Andale Mono, monospace" class="font-andale-mono">Andale Mono</option>
                    <option value="Courier New, monospace" class="font-courier-new">Courier New</option>
                    <option value="Courier, monospace" class="font-courier">Courier</option>
                    <option value="Arial, sans-serif" class="font-arial">Arial</option>
                    <option value="Helvetica, sans-serif" class="font-helvetica">Helvetica</option>
                    <option value="Verdana, sans-serif" class="font-verdana">Verdana</option>
                    <option value="Trebuchet MS, sans-serif" class="font-trebuchet-ms">Trebuchet MS</option>
                    <option value="Gill Sans, sans-serif" class="font-gill-sans">Gill Sans</option>
                    <option value="Noto Sans, sans-serif" class="font-noto-sans">Noto Sans</option>
                    <option value="Avantgarde, TeX Gyre Adventor, URW Gothic L, sans-serif" class="font-avantgarde">Avantgarde</option>
                    <option value="Optima, sans-serif" class="font-optima">Optima</option>
                    <option value="Arial Narrow, sans-serif" class="font-arial-narrow">Arial Narrow</option>
                    <option value="sans-serif" class="font-sans-serif">sans-serif (Generic)</option>
                    <option value="FreeMono, monospace" class="font-freemono">FreeMono</option>
                    <option value="OCR A Std, monospace" class="font-ocr-a-std">OCR A Std</option>
                    <option value="DejaVu Sans Mono, monospace" class="font-dejavu-sans-mono">DejaVu Sans Mono</option>
                    <option value="monospace" class="font-monospace">monospace (Generic)</option>
                    <option value="Comic Sans MS, Comic Sans, cursive" class="font-comic-sans-ms">Comic Sans MS</option>
                    <option value="Apple Chancery, cursive" class="font-apple-chancery">Apple Chancery</option>
                    <option value="Bradley Hand, cursive" class="font-bradley-hand">Bradley Hand</option>
                    <option value="Brush Script MT, Brush Script Std, cursive" class="font-brush-script-mt">Brush Script MT</option>
                    <option value="Snell Roundhand, cursive" class="font-snell-roundhand">Snell Roundhand</option>
                    <option value="URW Chancery L, cursive" class="font-urw-chancery-l">URW Chancery L</option>
                    <option value="cursive" class="font-cursive">cursive (Generic)</option>
                    <option value="Impact, fantasy" class="font-impact">Impact</option>
                    <option value="Luminari, fantasy" class="font-luminari">Luminari</option>
                    <option value="Chalkduster, fantasy" class="font-chalkduster">Chalkduster</option>
                    <option value="Jazz LET, fantasy" class="font-jazz-let">Jazz LET</option>
                    <option value="Blippo, fantasy" class="font-blippo">Blippo</option>
                    <option value="Stencil Std, fantasy" class="font-stencil-std">Stencil Std</option>
                    <option value="Marker Felt, fantasy" class="font-marker-felt">Marker Felt</option>
                    <option value="Trattatello, fantasy" class="font-trattatello">Trattatello</option>
                    <option value="fantasy" class="font-fantasy">fantasy (Generic)</option>
                </select>
            </div>
        </div>
    </div>

    <div class="section-container">
        <div class="section-title">Background Settings</div>
        <div class="control-grid">
            <div class="control-group">
                <label for="bgColorPicker">Background Color:</label>
                <input type="color" id="bgColorPicker" value="#ffffff">
            </div>
            <div class="control-group">
                <label>Background Image:</label>
                <button id="bgImageBtn" class="button button-tertiary">Add Image</button>
                <input type="file" id="imageUpload" accept="image/*" style="display: none;">
            </div>
            <div class="control-group">
                <label>&nbsp;</label> <!-- Placeholder for alignment -->
                <button id="clearBgImageBtn" class="button button-danger">Clear Image</button>
            </div>
        </div>
    </div>

    <div class="section-container">
        <div class="section-title">Export</div>
        <div class="button-group">
            <button id="downloadBtn" class="button button-primary">Download Quote Image</button>
            <button id="applyDownloadStyleBtn" class="button button-secondary">Apply Download Style</button>
            <button id="resetDownloadStyleBtn" class="button button-secondary" style="display: none;">Reset Download Style</button>
        </div>
    </div>


    <script>
        // Get the canvas element and its 2D rendering context
        const canvas = document.getElementById('quoteCanvas');
        const ctx = canvas.getContext('2d');
        // Removed canvasWrapper as it's no longer needed for scrolling the canvas itself

        // Get buttons
        const downloadBtn = document.getElementById('downloadBtn');
        const bgImageBtn = document.getElementById('bgImageBtn');
        const clearBgImageBtn = document.getElementById('clearBgImageBtn');
        const imageUpload = document.getElementById('imageUpload');
        const applyDownloadStyleBtn = document.getElementById('applyDownloadStyleBtn');
        const resetDownloadStyleBtn = document.getElementById('resetDownloadStyleBtn');

        // Get text input fields
        const quoteLine1Textarea = document.getElementById('quoteLine1Textarea');
        const quoteLine2Textarea = document.getElementById('quoteLine2Textarea');
        const authorInput = document.getElementById('authorInput');

        // Get text setting controls
        const quoteLine1Color = document.getElementById('quoteLine1Color');
        const quoteLine1FontSize = document.getElementById('quoteLine1FontSize'); // Get actual element
        const quoteLine2Color = document.getElementById('quoteLine2Color');
        const quoteLine2FontSizeInput = document.getElementById('quoteLine2FontSize');
        const authorTextColor = document.getElementById('authorTextColor');
        const authorFontSizeInput = document.getElementById('authorFontSize');
        const fontFamilySelector = document.getElementById('fontFamilySelector');

        // Position adjustment controls
        const titleYAdjust = document.getElementById('titleYAdjust');
        const bodyYAdjust = document.getElementById('bodyYAdjust');
        const authorYAdjust = document.getElementById('authorYAdjust');
        const titleXAdjust = document.getElementById('titleXAdjust');
        const bodyXAdjust = document.getElementById('bodyXAdjust');
        const authorXAdjust = document.getElementById('authorXAdjust');

        // Get background setting controls
        const bgColorPicker = document.getElementById('bgColorPicker');

        // State variables for drag and drop
        let isDragging = false;
        let draggedElement = null; // 'title', 'body', or 'author'
        let dragStartX = 0; // X position where drag started (client coordinates)
        let dragStartY = 0; // Y position where drag started (client coordinates)
        let initialElementXAdjust = 0; // Initial X adjustment value of the dragged element
        let initialElementYAdjust = 0; // Initial Y adjustment value of the dragged element

        // Bounding boxes for hit testing (updated in drawQuote)
        let titleBBox = null;
        let bodyBBox = null;
        let authorBBox = null;

        let backgroundImage = null; // To store the background image
        let customBackgroundColor = '#ffffff'; // Variable for custom background color, now defaulting to white
        let applyDownloadStyle = true; // State variable for controlling both margin and border-radius

        /**
         * Converts client (mouse/touch) coordinates to canvas-relative coordinates.
         * @param {HTMLCanvasElement} canvas The canvas element.
         * @param {MouseEvent|TouchEvent} evt The event object.
         * @returns {{x: number, y: number}} The coordinates relative to the canvas's top-left corner.
         */
        function getCanvasMousePos(canvas, evt) {
            const rect = canvas.getBoundingClientRect();
            let clientX, clientY;
            if (evt.touches) { // Touch event
                clientX = evt.touches[0].clientX;
                clientY = evt.touches[0].clientY;
            } else { // Mouse event
                clientX = evt.clientX;
                clientY = evt.clientY;
            }

            // Calculate position relative to the canvas element
            const x = clientX - rect.left;
            const y = clientY - rect.top;

            // Scale coordinates if canvas is scaled by CSS
            const scaleX = canvas.width / rect.width;
            const scaleY = canvas.height / rect.height;

            return { x: x * scaleX, y: y * scaleY };
        }

        /**
         * Adjusts the canvas size to fit the screen and redraws the quote.
         */
        function resizeCanvas() {
            const viewportWidth = window.innerWidth;
            const viewportHeight = window.innerHeight;

            // Target aspect ratio for the canvas (e.g., 9:16 for portrait mobile)
            const targetAspectRatio = 9 / 16;

            // Canvas will take up 90% of viewport width, and height will adjust to maintain aspect ratio
            let canvasWidth = viewportWidth * 0.9;
            let canvasHeight = canvasWidth / targetAspectRatio;

            // If calculated height exceeds viewport height (minus header/footer space), adjust based on height
            // Approx 150px for header/footer/controls
            const availableHeight = viewportHeight - 150;
            if (canvasHeight > availableHeight) {
                canvasHeight = availableHeight;
                canvasWidth = canvasHeight * targetAspectRatio;
            }

            // Ensure canvas dimensions are at least a minimum size
            const minCanvasWidth = 250;
            const minCanvasHeight = 400;

            if (canvasWidth < minCanvasWidth) {
                canvasWidth = minCanvasWidth;
                canvasHeight = minCanvasWidth / targetAspectRatio;
            }
            if (canvasHeight < minCanvasHeight) {
                canvasHeight = minCanvasHeight;
                canvasWidth = minCanvasHeight * targetAspectRatio;
            }

            // Set canvas drawing buffer size (internal resolution)
            canvas.width = canvasWidth;
            canvas.height = canvasHeight;

            // Apply CSS to make it fit the container if needed (though direct width/height usually suffice)
            canvas.style.width = `${canvasWidth}px`;
            canvas.style.height = `${canvasHeight}px`;

            drawQuote(); // Redraw the quote after resizing
        }

        /**
         * Calculates the height and max width of a text block without drawing it.
         * @param {CanvasRenderingContext2D} context The canvas rendering context.
         * @param {string} text The text to measure.
         * @param {number} maxWidth The maximum width for text wrapping.
         * @param {number} fontSize The font size.
         * @param {number} lineHeight The line height.
         * @param {string} fontFamily The font family.
         * @param {string} fontStyle The font style (e.g., 'normal', 'italic').
         * @param {string} fontWeight The font weight (e.g., 'normal', 'bold').
         * @returns {{height: number, actualMaxWidth: number}} The total height and the maximum line width of the text block.
         */
        function calculateTextBlockMetrics(context, text, maxWidth, fontSize, lineHeight, fontFamily, fontStyle = 'normal', fontWeight = 'bold') {
            if (!text) return { height: 0, actualMaxWidth: 0 };
            context.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
            const paragraphs = text.split('\n');
            let totalHeight = 0;
            let actualMaxWidth = 0;

            for (let p = 0; p < paragraphs.length; p++) {
                const words = paragraphs[p].split(' ');
                let line = '';
                let currentLineMetrics = [];

                for (let n = 0; n < words.length; n++) {
                    const testLine = line + words[n] + ' ';
                    const metrics = context.measureText(testLine);
                    if (metrics.width > maxWidth && n > 0) {
                        currentLineMetrics.push({ text: line, width: context.measureText(line).width });
                        line = words[n] + ' ';
                    } else {
                        line = testLine;
                    }
                }
                currentLineMetrics.push({ text: line, width: context.measureText(line).width });

                for (const lineMet of currentLineMetrics) {
                    if (lineMet.width > actualMaxWidth) {
                        actualMaxWidth = lineMet.width;
                    }
                }
                totalHeight += currentLineMetrics.length * lineHeight;
            }
            return { height: totalHeight, actualMaxWidth: actualMaxWidth };
        }


        /**
         * Draws the current quote on the canvas.
         */
        function drawQuote() {
            // Clear the canvas
            ctx.clearRect(0, 0, canvas.width, canvas.height);

            // Draw background image if available
            if (backgroundImage) {
                const imgAspectRatio = backgroundImage.width / backgroundImage.height;
                const canvasAspectRatio = canvas.width / canvas.height;

                let sx, sy, sWidth, sHeight;
                let dx, dy, dWidth, dHeight;

                if (imgAspectRatio > canvasAspectRatio) {
                    sHeight = backgroundImage.height;
                    sWidth = sHeight * canvasAspectRatio;
                    sx = (backgroundImage.width - sWidth) / 2;
                    sy = 0;
                } else {
                    sWidth = backgroundImage.width;
                    sHeight = sWidth / canvasAspectRatio;
                    sx = 0;
                    sy = (backgroundImage.height - sHeight) / 2;
                }

                dx = 0;
                dy = 0;
                dWidth = canvas.width;
                dHeight = canvas.height;

                ctx.drawImage(backgroundImage, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

            } else {
                ctx.fillStyle = customBackgroundColor;
                ctx.fillRect(0, 0, canvas.width, canvas.height);
            }

            // Get text from the input fields
            const quoteLine1Text = quoteLine1Textarea.value;
            const quoteLine2Text = quoteLine2Textarea.value;
            const authorText = authorInput.value;

            // Get selected text properties from input elements
            const currentLine1FontSize = parseFloat(quoteLine1FontSize.value); // Get value directly
            const currentLine2FontSize = parseFloat(quoteLine2FontSizeInput.value);
            const currentAuthorFontSize = parseFloat(authorFontSizeInput.value);

            const line1Color = quoteLine1Color.value;
            const line2Color = quoteLine2Color.value;
            const authorColor = authorTextColor.value;
            const fontFamily = fontFamilySelector.value;

            // Get vertical adjustments
            const titleY = parseFloat(titleYAdjust.value);
            const bodyY = parseFloat(bodyYAdjust.value);
            const authorY = parseFloat(authorYAdjust.value);
            // Get horizontal adjustments
            const titleX = parseFloat(titleXAdjust.value);
            const bodyX = parseFloat(bodyXAdjust.value);
            const authorX = parseFloat(authorXAdjust.value);

            const maxWidth = canvas.width * 0.8; // 80% of canvas width
            const centerX = canvas.width / 2;

            /**
             * Wraps and draws text on the canvas, handling explicit newlines.
             * Returns the final Y position after drawing and also the height and max width of the drawn block.
             * @param {CanvasRenderingContext2D} context The canvas rendering context.
             * @param {string} text The text to draw.
             * @param {number} xCenter The x-coordinate for drawing (center aligned).
             * @param {number} yStart The initial y-coordinate for drawing.
             * @param {number} maxWidth The maximum width for text wrapping.
             * @param {number} fontSize The font size.
             * @param {number} lineHeight The line height.
             * @param {string} color The text color.
             * @param {string} fontFamily The font family.
             * @param {string} fontStyle The font style (e.g., 'normal', 'italic').
             * @param {string} fontWeight The font weight (e.g., 'normal', 'bold').
             * @returns {{finalY: number, height: number, actualMaxWidth: number}} Metrics of the drawn text block.
             */
            function wrapAndDrawText(context, text, xCenter, yStart, maxWidth, fontSize, lineHeight, color, fontFamily, fontStyle = 'normal', fontWeight = 'bold') {
                if (!text) return { finalY: yStart, height: 0, actualMaxWidth: 0 };

                const paragraphs = text.split('\n');
                let currentY = yStart;
                let totalDrawnHeight = 0;
                let maxLineWidth = 0;

                context.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
                context.fillStyle = color;
                context.textAlign = 'center'; // Keep text aligned to the center of its effective X position

                for (let p = 0; p < paragraphs.length; p++) {
                    const words = paragraphs[p].split(' ');
                    let line = '';
                    let linesToDraw = [];
                    let currentParagraphMaxWidth = 0;

                    for (let n = 0; n < words.length; n++) {
                        const testLine = line + words[n] + ' ';
                        const metrics = context.measureText(testLine);
                        if (metrics.width > maxWidth && n > 0) {
                            linesToDraw.push(line);
                            currentParagraphMaxWidth = Math.max(currentParagraphMaxWidth, context.measureText(line).width);
                            line = words[n] + ' ';
                        } else {
                            line = testLine;
                        }
                    }
                    linesToDraw.push(line);
                    currentParagraphMaxWidth = Math.max(currentParagraphMaxWidth, context.measureText(line).width);


                    for (let i = 0; i < linesToDraw.length; i++) {
                        context.fillText(linesToDraw[i], xCenter, currentY + (i * lineHeight));
                    }
                    totalDrawnHeight += (linesToDraw.length * lineHeight);
                    currentY += (linesToDraw.length * lineHeight);
                    maxLineWidth = Math.max(maxLineWidth, currentParagraphMaxWidth); // Update overall max width
                }
                return { finalY: currentY, height: totalDrawnHeight, actualMaxWidth: maxLineWidth };
            }

            let currentDrawingY = 0;
            let totalContentHeight = 0;

            // Calculate heights and max widths for initial centering
            const titleMetrics = calculateTextBlockMetrics(ctx, quoteLine1Text, maxWidth, currentLine1FontSize, currentLine1FontSize * 1.5, fontFamily, 'normal', 'bold');
            const bodyMetrics = calculateTextBlockMetrics(ctx, quoteLine2Text, maxWidth, currentLine2FontSize, currentLine2FontSize * 1.5, fontFamily, 'normal', 'normal');
            const authorMetrics = calculateTextBlockMetrics(ctx, `- ${authorText}`, maxWidth, currentAuthorFontSize, currentAuthorFontSize * 1.5, fontFamily, 'italic', 'normal');

            totalContentHeight = titleMetrics.height;
            if (quoteLine2Text) totalContentHeight += (currentLine1FontSize * 0.5) + bodyMetrics.height;
            if (authorText) totalContentHeight += (currentLine1FontSize * 1.5) + authorMetrics.height;

            const initialLayoutY = (canvas.height / 2) - (totalContentHeight / 2);

            // Draw Title
            if (quoteLine1Text) {
                const startY = initialLayoutY + titleY;
                const drawX = centerX + titleX;
                const { height, actualMaxWidth } = wrapAndDrawText(ctx, quoteLine1Text, drawX, startY, maxWidth, currentLine1FontSize, currentLine1FontSize * 1.5, line1Color, fontFamily, 'normal', 'bold');
                titleBBox = {
                    x: drawX - (actualMaxWidth / 2),
                    y: startY - (currentLine1FontSize * 0.75), // Adjust y to be closer to the top of the text block
                    width: actualMaxWidth,
                    height: height + (currentLine1FontSize * 0.75) // Adjust height to cover the text block
                };
            } else {
                titleBBox = null;
            }

            // Draw Body Text
            if (quoteLine2Text) {
                // If title is present, start body text after title's bounding box, plus a gap
                const prevElementBottom = titleBBox ? titleBBox.y + titleBBox.height : initialLayoutY;
                const startY = prevElementBottom + (currentLine1FontSize * 0.5) + bodyY;
                const drawX = centerX + bodyX;
                const { height, actualMaxWidth } = wrapAndDrawText(ctx, quoteLine2Text, drawX, startY, maxWidth, currentLine2FontSize, currentLine2FontSize * 1.5, line2Color, fontFamily, 'normal', 'normal');
                bodyBBox = {
                    x: drawX - (actualMaxWidth / 2),
                    y: startY - (currentLine2FontSize * 0.75),
                    width: actualMaxWidth,
                    height: height + (currentLine2FontSize * 0.75)
                };
            } else {
                bodyBBox = null;
            }

            // Draw Author
            if (authorText) {
                // Start author after body text's bounding box (if present), or title's, plus a gap
                const prevElementBottom = bodyBBox ? bodyBBox.y + bodyBBox.height : (titleBBox ? titleBBox.y + titleBBox.height : initialLayoutY);
                const startY = prevElementBottom + (currentLine1FontSize * 1.5) + authorY;
                const drawX = centerX + authorX;
                const { height, actualMaxWidth } = wrapAndDrawText(ctx, `- ${authorText}`, drawX, startY, maxWidth, currentAuthorFontSize, currentAuthorFontSize * 1.5, authorColor, fontFamily, 'italic', 'normal');
                authorBBox = {
                    x: drawX - (actualMaxWidth / 2),
                    y: startY - (currentAuthorFontSize * 0.75),
                    width: actualMaxWidth,
                    height: height + (currentAuthorFontSize * 0.75)
                };
            } else {
                authorBBox = null;
            }
        }

        /**
         * Checks if a point (x, y) is within any text element's bounding box.
         * @param {number} x The x-coordinate to test (canvas coordinates).
         * @param {number} y The y-coordinate to test (canvas coordinates).
         * @returns {string|null} The name of the element ('title', 'body', 'author') if hit, otherwise null.
         */
        function hitTest(x, y) {
            if (titleBBox && x >= titleBBox.x && x <= titleBBox.x + titleBBox.width && y >= titleBBox.y && y <= titleBBox.y + titleBBox.height) {
                return 'title';
            }
            if (bodyBBox && x >= bodyBBox.x && x <= bodyBBox.x + bodyBBox.width && y >= bodyBBox.y && y <= bodyBBox.y + bodyBBox.height) {
                return 'body';
            }
            if (authorBBox && x >= authorBBox.x && x <= authorBBox.x + authorBBox.width && y >= authorBBox.y && y <= authorBBox.y + authorBBox.height) {
                return 'author';
            }
            return null;
        }

        /**
         * Handles mouse down and touch start events for dragging.
         * @param {MouseEvent|TouchEvent} e The event object.
         */
        function handleDragStart(e) {
            e.preventDefault(); // Prevent default touch actions (scrolling, zooming) and text selection
            const pos = getCanvasMousePos(canvas, e);
            const hitElement = hitTest(pos.x, pos.y);

            if (hitElement) {
                isDragging = true;
                draggedElement = hitElement;
                dragStartX = e.touches ? e.touches[0].clientX : e.clientX;
                dragStartY = e.touches ? e.touches[0].clientY : e.clientY;

                switch (draggedElement) {
                    case 'title':
                        initialElementXAdjust = parseFloat(titleXAdjust.value);
                        initialElementYAdjust = parseFloat(titleYAdjust.value);
                        break;
                    case 'body':
                        initialElementXAdjust = parseFloat(bodyXAdjust.value);
                        initialElementYAdjust = parseFloat(bodyYAdjust.value);
                        break;
                    case 'author':
                        initialElementXAdjust = parseFloat(authorXAdjust.value);
                        initialElementYAdjust = parseFloat(authorYAdjust.value);
                        break;
                }
                canvas.classList.add('dragging'); // Add dragging cursor
            }
        }

        /**
         * Handles mouse move and touch move events for dragging.
         * @param {MouseEvent|TouchEvent} e The event object.
         */
        function handleDragMove(e) {
            if (!isDragging) return;
            e.preventDefault(); // Prevent default touch actions (scrolling, zooming)

            const currentX = e.touches ? e.touches[0].clientX : e.clientX;
            const currentY = e.touches ? e.touches[0].clientY : e.clientY;
            const dx = currentX - dragStartX;
            const dy = currentY - dragStartY;

            switch (draggedElement) {
                case 'title':
                    titleXAdjust.value = initialElementXAdjust + dx;
                    titleYAdjust.value = initialElementYAdjust + dy;
                    break;
                case 'body':
                    bodyXAdjust.value = initialElementXAdjust + dx;
                    bodyYAdjust.value = initialElementYAdjust + dy;
                    break;
                case 'author':
                    authorXAdjust.value = initialElementXAdjust + dx;
                    authorYAdjust.value = initialElementYAdjust + dy;
                    break;
            }
            drawQuote(); // Redraw with new position
        }

        /**
         * Handles mouse up and touch end events to stop dragging.
         * @param {MouseEvent|TouchEvent} e The event object.
         */
        function handleDragEnd(e) {
            isDragging = false;
            draggedElement = null;
            canvas.classList.remove('dragging'); // Remove dragging cursor
        }


        /**
         * Downloads the canvas content as a PNG image with border-radius and margin.
         */
        function downloadQuote() {
            // Create a temporary canvas that matches the full resolution of the drawing canvas
            const tempCanvas = document.createElement('canvas');
            tempCanvas.width = canvas.width;
            tempCanvas.height = canvas.height;
            const tempCtx = tempCanvas.getContext('2d');

            // Draw the current state of the main canvas onto the temporary canvas
            tempCtx.drawImage(canvas, 0, 0);

            // Now, apply the download style (margin and border-radius) if requested
            const finalCanvas = document.createElement('canvas');
            const originalWidth = tempCanvas.width;
            const originalHeight = tempCanvas.height;

            const marginPx = applyDownloadStyle ? Math.round(originalWidth * 0.02) : 0; // 2% of original width
            const borderRadiusPx = applyDownloadStyle ? 20 : 0;

            finalCanvas.width = originalWidth + (2 * marginPx);
            finalCanvas.height = originalHeight + (2 * marginPx);
            const finalCtx = finalCanvas.getContext('2d');

            // Fill the margin area with the body background color
            finalCtx.fillStyle = '#f0f2f5'; // Using the body background color
            finalCtx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);

            if (applyDownloadStyle) {
                // Draw the temporary canvas content with border-radius
                finalCtx.save(); // Save the context state

                // Create a rounded rectangle path for clipping
                finalCtx.beginPath();
                finalCtx.moveTo(marginPx + borderRadiusPx, marginPx);
                finalCtx.lineTo(originalWidth + marginPx - borderRadiusPx, marginPx);
                finalCtx.arcTo(originalWidth + marginPx, marginPx, originalWidth + marginPx, marginPx + borderRadiusPx, borderRadiusPx);
                finalCtx.lineTo(originalWidth + marginPx, originalHeight + marginPx - borderRadiusPx);
                finalCtx.arcTo(originalWidth + marginPx, originalHeight + marginPx, originalWidth + marginPx - borderRadiusPx, originalHeight + marginPx, borderRadiusPx);
                finalCtx.lineTo(marginPx + borderRadiusPx, originalHeight + marginPx);
                finalCtx.arcTo(marginPx, originalHeight + marginPx, marginPx, originalHeight + marginPx - borderRadiusPx, borderRadiusPx);
                finalCtx.lineTo(marginPx, marginPx + borderRadiusPx);
                finalCtx.arcTo(marginPx, marginPx, marginPx + borderRadiusPx, marginPx, borderRadiusPx);
                finalCtx.closePath();
                finalCtx.clip(); // Clip subsequent drawing to this path
            }

            // Draw the content from the temporary canvas onto the final canvas, offset by the margin
            finalCtx.drawImage(tempCanvas, marginPx, marginPx, originalWidth, originalHeight);

            if (applyDownloadStyle) {
                finalCtx.restore(); // Restore the context state (remove clipping path)
            }

            // Convert the final canvas to an image and trigger download
            const image = finalCanvas.toDataURL('image/png');
            const link = document.createElement('a');
            link.download = 'quote_canvas_with_style.png';
            document.body.appendChild(link); // Append to body to make click work reliably
            link.href = image;
            link.click();
            document.body.removeChild(link);
        }

        /**
         * Handles the selection of a background image.
         */
        function handleImageUpload(event) {
            const file = event.target.files[0];
            if (file) {
                const reader = new FileReader();
                reader.onload = function(e) {
                    const img = new Image();
                    img.onload = function() {
                        backgroundImage = img;
                        customBackgroundColor = '#ffffff'; // Clear custom background color
                        bgColorPicker.value = '#ffffff'; // Reset picker
                        drawQuote();
                    };
                    img.onerror = function() {
                        console.error("Error loading image.");
                        backgroundImage = null;
                        drawQuote();
                    };
                    img.src = e.target.result;
                };
                reader.onerror = function() {
                    console.error("Error reading file.");
                    backgroundImage = null;
                    drawQuote();
                };
                reader.readAsDataURL(file);
            }
        }

        /**
         * Clears the background image and resets background color to default.
         */
        function clearBackgroundImage() {
            backgroundImage = null;
            customBackgroundColor = '#ffffff';
            bgColorPicker.value = '#ffffff';
            drawQuote();
        }

        // Event Listeners
        window.addEventListener('resize', resizeCanvas); // Re-added resize listener
        downloadBtn.addEventListener('click', downloadQuote);
        bgImageBtn.addEventListener('click', () => imageUpload.click());
        imageUpload.addEventListener('change', handleImageUpload);
        clearBgImageBtn.addEventListener('click', clearBackgroundImage);

        // Text input field listeners (real-time update)
        quoteLine1Textarea.addEventListener('input', drawQuote);
        quoteLine2Textarea.addEventListener('input', drawQuote);
        authorInput.addEventListener('input', drawQuote);

        // Text settings control listeners
        quoteLine1Color.addEventListener('input', drawQuote);
        quoteLine1FontSize.addEventListener('input', drawQuote); // Corrected to use the element directly
        quoteLine2Color.addEventListener('input', drawQuote);
        quoteLine2FontSizeInput.addEventListener('input', drawQuote);
        authorTextColor.addEventListener('input', drawQuote);
        authorFontSizeInput.addEventListener('input', drawQuote);
        fontFamilySelector.addEventListener('change', drawQuote); // Use 'change' for select

        // Position adjustment control listeners (now also triggered by drag)
        titleYAdjust.addEventListener('input', drawQuote);
        bodyYAdjust.addEventListener('input', drawQuote);
        authorYAdjust.addEventListener('input', drawQuote);
        titleXAdjust.addEventListener('input', drawQuote);
        bodyXAdjust.addEventListener('input', drawQuote);
        authorXAdjust.addEventListener('input', drawQuote);

        // Background color picker listener
        bgColorPicker.addEventListener('input', () => {
            customBackgroundColor = bgColorPicker.value;
            backgroundImage = null; // Clear background image if a color is selected
            drawQuote();
        });

        // New button event listeners for download style
        applyDownloadStyleBtn.addEventListener('click', () => {
            applyDownloadStyle = true;
            applyDownloadStyleBtn.style.display = 'none';
            resetDownloadStyleBtn.style.display = 'inline-block';
        });

        resetDownloadStyleBtn.addEventListener('click', () => {
            applyDownloadStyle = false;
            resetDownloadStyleBtn.style.display = 'none';
            applyDownloadStyleBtn.style.display = 'inline-block';
        });

        // Canvas drag and drop event listeners
        canvas.addEventListener('mousedown', handleDragStart);
        canvas.addEventListener('mousemove', handleDragMove);
        canvas.addEventListener('mouseup', handleDragEnd);
        canvas.addEventListener('mouseleave', handleDragEnd); // End drag if mouse leaves canvas

        // Touch events for mobile
        canvas.addEventListener('touchstart', handleDragStart);
        canvas.addEventListener('touchmove', handleDragMove);
        canvas.addEventListener('touchend', handleDragEnd);
        canvas.addEventListener('touchcancel', handleDragEnd);


        // Initial setup
        window.onload = function() {
            resizeCanvas(); // Set initial canvas size and draw quote
        };
    </script>
</body>
</html>

Youez - 2016 - github.com/yon3zu
LinuXploit