🗂️ ShadowGate
Path:
home
/
newsgini
/
freefirewala.com
/
Tools
/
✏️ Editing: Editor.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=1024, initial-scale=0.5, maximum-scale=1"> <style> html, body { min-width: 1024px; width: 100%; overflow-x: auto; } </style> <title>Advanced Gaming Thumbnail Creator - FreeFireWala</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700;900&family=Montserrat:wght@700&family=Bebas+Neue&family=Oswald:wght@700&family=Anton&family=Poppins:wght@700&family=Lato:wght@900&display=swap" rel="stylesheet"> <style> * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); color: #fff; line-height: 1.6; min-height: 100vh; display: flex; flex-direction: column; } header { background: linear-gradient(135deg, #2a2a42 0%, #1a1a2e 100%); padding: 15px 20px; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5); position: sticky; top: 0; z-index: 1000; } .header-container { max-width: 1400px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; } .header-container h1 { font-size: 2rem; color: #4cc9f0; text-shadow: 0 0 10px rgba(76, 201, 240, 0.7); } nav ul { display: flex; gap: 20px; list-style: none; } nav ul li a { color: #a9d6e5; text-decoration: none; font-weight: 500; transition: color 0.3s ease; } nav ul li a:hover { color: #f72585; } .menu-icon { display: none; font-size: 1.8rem; cursor: pointer; color: #a9d6e5; } .container { max-width: 1400px; margin: 20px auto; padding: 20px; flex: 1; } .creator-container { display: flex; flex-wrap: wrap; gap: 30px; background: rgba(30, 30, 46, 0.8); border-radius: 15px; padding: 25px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); backdrop-filter: blur(10px); } .preview-section { flex: 1.5; min-width: 300px; } .controls-section { flex: 1; min-width: 300px; } .canvas-container { background: #0c0c15; border-radius: 10px; overflow: hidden; position: relative; box-shadow: 0 5px 25px rgba(0, 0, 0, 0.7); border: 2px solid #4cc9f0; } #thumbnailCanvas { display: block; width: 100%; background: #131313; cursor: default; transition: transform 0.3s ease; } .canvas-overlay { position: absolute; bottom: 0; left: 0; right: 0; padding: 10px; background: rgba(0, 0, 0, 0.7); text-align: center; font-size: 0.9rem; color: #4cc9f0; display: none; } .control-group { margin-bottom: 20px; padding: 20px; background: rgba(20, 20, 35, 0.8); border-radius: 10px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); } .control-group h3 { margin-bottom: 15px; color: #f72585; font-size: 1.3rem; display: flex; align-items: center; gap: 10px; } .control-group h3::before { content: "•"; color: #4cc9f0; } label { display: block; margin-bottom: 8px; font-weight: 500; color: #e0e0e0; } input[type="text"], input[type="color"], input[type="number"], input[type="file"], select { width: 100%; padding: 12px; margin-bottom: 15px; border: 2px solid #2a2a42; border-radius: 8px; background: #2a2a42; color: white; transition: all 0.3s ease; } input[type="text"]:focus, input[type="color"]:focus, input[type="number"]:focus, input[type="file"]:focus, select:focus { border-color: #4cc9f0; outline: none; } input[type="range"] { width: 100%; margin-bottom: 15px; height: 8px; -webkit-appearance: none; background: #2a2a42; border-radius: 4px; } input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 20px; height: 20px; border-radius: 50%; background: #4cc9f0; cursor: pointer; box-shadow: 0 0 10px rgba(76, 201, 240, 0.8); } .button-group { display: flex; gap: 15px; margin-top: 25px; flex-wrap: wrap; } button { padding: 14px 25px; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; gap: 8px; } #generateBtn { background: linear-gradient(135deg, #4361ee 0%, #3a56d4 100%); color: white; flex: 2; } #generateBtn:hover { transform: translateY(-3px); box-shadow: 0 5px 15px rgba(67, 97, 238, 0.4); } #downloadBtn, #undoBtn, #redoBtn { background: linear-gradient(135deg, #f72585 0%, #e21c76 100%); color: white; flex: 1; } #downloadBtn:hover, #undoBtn:hover, #redoBtn:hover { transform: translateY(-3px); box-shadow: 0 5px 15px rgba(247, 37, 133, 0.4); } .range-value { display: flex; justify-content: space-between; color: #b8b8cc; margin-top: 5px; } .template-gallery { display: flex; gap: 15px; margin-top: 15px; flex-wrap: wrap; } .template { width: 80px; height: 45px; border-radius: 8px; cursor: pointer; border: 2px solid transparent; transition: all 0.2s ease; box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3); } .template:hover { transform: scale(1.08); border-color: #4361ee; } .template.active { border-color: #f72585; border-width: 3px; } .error-message { color: #f72585; margin-top: -10px; margin-bottom: 10px; font-size: 0.9rem; } #unsplashResults { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px; margin-top: 15px; } #unsplashResults img { width: 100%; height: 150px; object-fit: cover; cursor: pointer; border-radius: 8px; border: 2px solid transparent; opacity: 0; transition: opacity 0.3s ease; } #unsplashResults img:hover { border-color: #4cc9f0; } #unsplashResults img.selected { border-color: #f72585; border-width: 3px; } .search-group { display: flex; gap: 10px; } .search-group input { flex: 1; } .search-group button, #loadMoreBtn, #retryBtn { padding: 12px 20px; background: linear-gradient(135deg, #4361ee 0%, #3a56d4 100%); color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; } #loadMoreBtn, #retryBtn { margin-top: 15px; width: 100%; display: none; } .loading-spinner { display: none; margin: 20px auto; width: 40px; height: 40px; border: 5px solid #4cc9f0; border-top: 5px solid transparent; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .position-buttons { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 15px; } .position-buttons button { background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%); color: white; padding: 10px; flex: 1; min-width: 100px; } .position-buttons button:hover { transform: translateY(-3px); box-shadow: 0 5px 15px rgba(107, 114, 128, 0.4); } footer { background: linear-gradient(135deg, #2a2a42 0%, #1a1a2e 100%); padding: 20px; text-align: center; margin-top: auto; } .footer-container { max-width: 1400px; margin: 0 auto; } .footer-container p { color: #a9d6e5; margin-bottom: 10px; } .footer-container ul { display: flex; justify-content: center; gap: 20px; list-style: none; } .footer-container ul li a { color: #a9d6e5; text-decoration: none; font-weight: 500; transition: color 0.3s ease; } .footer-container ul li a:hover { color: #f72585; } @media (max-width: 768px) { .header-container { flex-direction: row; align-items: center; } nav ul { display: none; flex-direction: column; gap: 10px; background: #2a2a42; position: absolute; top: 60px; right: 20px; padding: 10px; border-radius: 8px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); } nav ul.show { display: flex; } .menu-icon { display: block; } .creator-container { flex-direction: column; } .button-group { flex-direction: column; } #unsplashResults { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } #unsplashResults img { height: 120px; } } @media (min-width: 769px) { nav ul { display: flex !important; } .menu-icon { display: none; } } </style> </head> <body> <header> <div class="header-container"> <h1>FreeFireWala</h1> <nav> <ul> <li><a href="https://freefirewala.com/" title="Go to homepage">Home</a></li> <li><a href="https://freefirewala.com/about-us/" title="Learn about us">About</a></li> <li><a href="https://freefirewala.com/contact-us/" title="Get in touch">Contact</a></li> <li><a href="https://freefirewala.com/privacy-policy/" title="View privacy policy">Privacy Policy</a></li> <li><a href="https://freefirewala.com/terms-and-conditions/" title="View terms of service">Terms of Service</a></li> </ul> </nav> <i class="fas fa-bars menu-icon" title="Toggle menu"></i> </div> </header> <div class="container"> <div class="creator-container"> <div class="preview-section"> <div class="canvas-container"> <canvas id="thumbnailCanvas" width="1280" height="720"></canvas> <div class="canvas-overlay">Drag text to move • Use rotation sliders to adjust angle • Zoom for precision</div> </div> <div class="control-group"> <h3>Canvas Controls</h3> <label for="canvasZoom">Canvas Zoom</label> <input type="range" id="canvasZoom" min="0.5" max="2" step="0.1" value="1" title="Adjust canvas zoom level"> <div class="range-value"><span>0.5x</span><span id="canvasZoomValue">1x</span><span>2x</span></div> </div> <div class="control-group"> <h3>Rotation Controls</h3> <label for="mainTextRotation">Main Text Rotation (degrees)</label> <input type="range" id="mainTextRotation" min="-180" max="180" value="0" title="Rotate main text"> <div class="range-value"><span>-180</span><span id="mainTextRotationValue">0</span><span>180</span></div> <label for="secondaryTextRotation">Secondary Text Rotation (degrees)</label> <input type="range" id="secondaryTextRotation" min="-180" max="180" value="0" title="Rotate secondary text"> <div class="range-value"><span>-180</span><span id="secondaryTextRotationValue">0</span><span>180</span></div> </div> <div class="control-group"> <h3>Unsplash Image Search</h3> <div class="search-group"> <input type="text" id="unsplashSearch" placeholder="Search Unsplash for images..." title="Enter search term for Unsplash images"> <button id="searchUnsplashBtn" title="Search Unsplash"><i class="fas fa-search"></i> Search</button> </div> <div class="loading-spinner" id="loadingSpinner"></div> <div id="unsplashResults" class="template-gallery"></div> <button id="loadMoreBtn" title="Load more Unsplash images">Load More</button> <button id="retryBtn" title="Retry Unsplash search">Retry Search</button> <div class="error-message" id="bgImageError"></div> </div> </div> <div class="controls-section"> <div class="control-group"> <h3>Text Options</h3> <label for="mainText">Main Text</label> <input type="text" id="mainText" value="NEW GAMEPLAY" placeholder="Enter main text" title="Enter main text content"> <label for="mainTextColor">Main Text Color</label> <input type="color" id="mainTextColor" value="#ff0000" title="Choose main text color"> <label for="mainTextStyle">Main Text Style</label> <select id="mainTextStyle" title="Select main text style"> <option value="impact-bold">Impact Bold</option> <option value="arial-black">Arial Black</option> <option value="roboto-bold">Roboto Bold</option> <option value="montserrat-bold">Montserrat Bold</option> <option value="bebas-neue">Bebas Neue</option> <option value="oswald-bold">Oswald Bold</option> <option value="anton">Anton</option> <option value="poppins-bold">Poppins Bold</option> <option value="lato-bold">Lato Bold</option> <option value="impact-glow">Impact with Glow</option> <option value="arial-shadow">Arial with Shadow</option> <option value="roboto-gradient">Roboto Gradient</option> <option value="montserrat-stroke">Montserrat Thick Stroke</option> <option value="bebas-neon">Bebas Neue Neon</option> <option value="oswald-double-stroke">Oswald Double Stroke</option> <option value="anton-shadow">Anton Shadow</option> <option value="poppins-glow">Poppins Glow</option> <option value="lato-gradient">Lato Gradient</option> <option value="impact-outline">Impact Outline Only</option> <option value="roboto-neon">Roboto Neon</option> </select> <label for="mainTextSize">Main Text Size</label> <input type="range" id="mainTextSize" min="20" max="100" value="60" title="Adjust main text size"> <div class="range-value"><span>20</span><span id="mainTextSizeValue">60</span><span>100</span></div> <label for="mainTextBold">Main Text Bold Intensity</label> <input type="range" id="mainTextBold" min="100" max="900" step="100" value="700" title="Adjust main text bold intensity"> <div class="range-value"><span>100</span><span id="mainTextBoldValue">700</span><span>900</span></div> <label for="mainTextNeon">Main Text Neon Effect</label> <input type="range" id="mainTextNeon" min="0" max="50" value="0" title="Adjust main text neon effect"> <div class="range-value"><span>0</span><span id="mainTextNeonValue">0</span><span>50</span></div> <label for="mainTextStroke">Main Text Stroke Width</label> <input type="range" id="mainTextStroke" min="0" max="20" value="8" title="Adjust main text stroke width"> <div class="range-value"><span>0</span><span id="mainTextStrokeValue">8</span><span>20</span></div> <label for="mainTextShadowColor">Main Text Shadow Color</label> <input type="color" id="mainTextShadowColor" value="#4cc9f0" title="Choose main text shadow color for neon/glow"> <label for="secondaryText">Secondary Text</label> <input type="text" id="secondaryText" value="EPIC BATTLE!" placeholder="Enter secondary text" title="Enter secondary text content"> <label for="secondaryTextColor">Secondary Text Color</label> <input type="color" id="secondaryTextColor" value="#ffffff" title="Choose secondary text color"> <label for="secondaryTextStyle">Secondary Text Style</label> <select id="secondaryTextStyle" title="Select secondary text style"> <option value="impact-bold">Impact Bold</option> <option value="arial-black">Arial Black</option> <option value="roboto-bold">Roboto Bold</option> <option value="montserrat-bold">Montserrat Bold</option> <option value="bebas-neue">Bebas Neue</option> <option value="oswald-bold">Oswald Bold</option> <option value="anton">Anton</option> <option value="poppins-bold">Poppins Bold</option> <option value="lato-bold">Lato Bold</option> <option value="impact-glow">Impact with Glow</option> <option value="arial-shadow">Arial with Shadow</option> <option value="roboto-gradient">Roboto Gradient</option> <option value="montserrat-stroke">Montserrat Thick Stroke</option> <option value="bebas-neon">Bebas Neue Neon</option> <option value="oswald-double-stroke">Oswald Double Stroke</option> <option value="anton-shadow">Anton Shadow</option> <option value="poppins-glow">Poppins Glow</option> <option value="lato-gradient">Lato Gradient</option> <option value="impact-outline">Impact Outline Only</option> <option value="roboto-neon">Roboto Neon</option> </select> <label for="secondaryTextSize">Secondary Text Size</label> <input type="range" id="secondaryTextSize" min="15" max="80" value="40" title="Adjust secondary text size"> <div class="range-value"><span>15</span><span id="secondaryTextSizeValue">40</span><span>80</span></div> <label for="secondaryTextBold">Secondary Text Bold Intensity</label> <input type="range" id="secondaryTextBold" min="100" max="900" step="100" value="700" title="Adjust secondary text bold intensity"> <div class="range-value"><span>100</span><span id="secondaryTextBoldValue">700</span><span>900</span></div> <label for="secondaryTextNeon">Secondary Text Neon Effect</label> <input type="range" id="secondaryTextNeon" min="0" max="50" value="0" title="Adjust secondary text neon effect"> <div class="range-value"><span>0</span><span id="secondaryTextNeonValue">0</span><span>50</span></div> <label for="secondaryTextStroke">Secondary Text Stroke Width</label> <input type="range" id="secondaryTextStroke" min="0" max="20" value="6" title="Adjust secondary text stroke width"> <div class="range-value"><span>0</span><span id="secondaryTextStrokeValue">6</span><span>20</span></div> <label for="secondaryTextShadowColor">Secondary Text Shadow Color</label> <input type="color" id="secondaryTextShadowColor" value="#ffffff" title="Choose secondary text shadow color for neon/glow"> <div class="position-buttons"> <button onclick="setTextPosition('main', 'top-left')" title="Move main text to top-left">Main Top-Left</button> <button onclick="setTextPosition('main', 'center')" title="Move main text to center">Main Center</button> <button onclick="setTextPosition('main', 'bottom-right')" title="Move main text to bottom-right">Main Bottom-Right</button> <button onclick="setTextPosition('secondary', 'top-left')" title="Move secondary text to top-left">Secondary Top-Left</button> <button onclick="setTextPosition('secondary', 'center')" title="Move secondary text to center">Secondary Center</button> <button onclick="setTextPosition('secondary', 'bottom-right')" title="Move secondary text to bottom-right">Secondary Bottom-Right</button> </div> </div> <div class="control-group"> <h3>Background Options</h3> <label for="bgColor">Background Color</label> <input type="color" id="bgColor" value="#000000" title="Choose background color"> <label for="bgImageFile">Upload Local Background Image</label> <input type="file" id="bgImageFile" accept="image/*" title="Upload a local image"> <label for="overlayColor">Overlay Color</label> <input type="color" id="overlayColor" value="#000000" title="Choose overlay color"> <label for="overlayOpacity">Overlay Opacity</label> <input type="range" id="overlayOpacity" min="0" max="1" step="0.1" value="0.5" title="Adjust overlay opacity"> <div class="range-value"><span>0</span><span id="overlayOpacityValue">0.5</span><span>1</span></div> </div> <div class="control-group"> <h3>Quick Templates</h3> <div class="template-gallery"> <div class="template active" style="background: linear-gradient(135deg, #ff0000, #000000);" data-template="red-dark"></div> <div class="template" style="background: linear-gradient(135deg, #00ff00, #000000);" data-template="green-dark"></div> <div class="template" style="background: linear-gradient(135deg, #0000ff, #000000);" data-template="blue-dark"></div> <div class="template" style="background: linear-gradient(135deg, #ff9900, #000000);" data-template="orange-dark"></div> <div class="template" style="background: linear-gradient(135deg, #ff00ff, #000000);" data-template="pink-dark"></div> </div> </div> <div class="button-group"> <button id="generateBtn" title="Update thumbnail preview"><i class="fas fa-sync-alt"></i> Update Thumbnail</button> <button id="downloadBtn" title="Download thumbnail as PNG"><i class="fas fa-download"></i> Download</button> <button id="undoBtn" title="Undo last change"><i class="fas fa-undo"></i> Undo</button> <button id="redoBtn" title="Redo last change"><i class="fas fa-redo"></i> Redo</button> </div> </div> </div> </div> <footer> <div class="footer-container"> <p>© 2025 <a href="https://FreeFirewala.com" title="Visit FreeFireWala">FreeFireWala</a>. All rights reserved.</p> <ul> <li><a href="https://freefirewala.com/" title="Go to homepage">Home</a></li> <li><a href="https://freefirewala.com/about-us/" title="Learn about us">About</a></li> <li><a href="https://freefirewala.com/contact-us/" title="Get in touch">Contact</a></li> <li><a href="https://freefirewala.com/privacy-policy/" title="View privacy policy">Privacy Policy</a></li> <li><a href="https://freefirewala.com/terms-and-conditions/" title="View terms of service">Terms of Service</a></li> </ul> </div> </footer> <script> (function() { // State const state = { textObjects: { main: { text: "NEW GAMEPLAY", x: 1280 / 2, y: 720 / 2 - 50, fontSize: 60, color: "#ff0000", rotation: 0, style: "impact-bold", bold: 700, neon: 0, stroke: 8, shadowColor: "#4cc9f0" }, secondary: { text: "EPIC BATTLE!", x: 1280 / 2, y: 720 / 2 + 50, fontSize: 40, color: "#ffffff", rotation: 0, style: "impact-bold", bold: 700, neon: 0, stroke: 6, shadowColor: "#ffffff" } }, bgColor: "#000000", overlayColor: "#000000", overlayOpacity: 0.5, currentImage: null, zoom: 1, history: [], historyIndex: -1, unsplashPage: 1, unsplashQuery: "" }; // DOM elements const elements = { canvas: document.getElementById('thumbnailCanvas'), mainTextInput: document.getElementById('mainText'), mainTextColorInput: document.getElementById('mainTextColor'), mainTextStyleInput: document.getElementById('mainTextStyle'), mainTextSizeInput: document.getElementById('mainTextSize'), mainTextSizeValue: document.getElementById('mainTextSizeValue'), mainTextRotationInput: document.getElementById('mainTextRotation'), mainTextRotationValue: document.getElementById('mainTextRotationValue'), mainTextBoldInput: document.getElementById('mainTextBold'), mainTextBoldValue: document.getElementById('mainTextBoldValue'), mainTextNeonInput: document.getElementById('mainTextNeon'), mainTextNeonValue: document.getElementById('mainTextNeonValue'), mainTextStrokeInput: document.getElementById('mainTextStroke'), mainTextStrokeValue: document.getElementById('mainTextStrokeValue'), mainTextShadowColorInput: document.getElementById('mainTextShadowColor'), secondaryTextInput: document.getElementById('secondaryText'), secondaryTextColorInput: document.getElementById('secondaryTextColor'), secondaryTextStyleInput: document.getElementById('secondaryTextStyle'), secondaryTextSizeInput: document.getElementById('secondaryTextSize'), secondaryTextSizeValue: document.getElementById('secondaryTextSizeValue'), secondaryTextRotationInput: document.getElementById('secondaryTextRotation'), secondaryTextRotationValue: document.getElementById('secondaryTextRotationValue'), secondaryTextBoldInput: document.getElementById('secondaryTextBold'), secondaryTextBoldValue: document.getElementById('secondaryTextBoldValue'), secondaryTextNeonInput: document.getElementById('secondaryTextNeon'), secondaryTextNeonValue: document.getElementById('secondaryTextNeonValue'), secondaryTextStrokeInput: document.getElementById('secondaryTextStroke'), secondaryTextStrokeValue: document.getElementById('secondaryTextStrokeValue'), secondaryTextShadowColorInput: document.getElementById('secondaryTextShadowColor'), bgColorInput: document.getElementById('bgColor'), unsplashSearchInput: document.getElementById('unsplashSearch'), searchUnsplashBtn: document.getElementById('searchUnsplashBtn'), unsplashResults: document.getElementById('unsplashResults'), loadMoreBtn: document.getElementById('loadMoreBtn'), retryBtn: document.getElementById('retryBtn'), loadingSpinner: document.getElementById('loadingSpinner'), bgImageFileInput: document.getElementById('bgImageFile'), overlayColorInput: document.getElementById('overlayColor'), overlayOpacityInput: document.getElementById('overlayOpacity'), overlayOpacityValue: document.getElementById('overlayOpacityValue'), canvasZoomInput: document.getElementById('canvasZoom'), canvasZoomValue: document.getElementById('canvasZoomValue'), generateBtn: document.getElementById('generateBtn'), downloadBtn: document.getElementById('downloadBtn'), undoBtn: document.getElementById('undoBtn'), redoBtn: document.getElementById('redoBtn'), templates: document.querySelectorAll('.template'), errorContainer: document.getElementById('bgImageError') }; // Canvas setup const canvasWidth = 1280; const canvasHeight = 720; const ctx = elements.canvas.getContext('2d'); const offscreenCanvas = document.createElement('canvas'); offscreenCanvas.width = canvasWidth; offscreenCanvas.height = canvasHeight; const offscreenCtx = offscreenCanvas.getContext('2d'); // Interaction state let activeText = null; let isDragging = false; let lastX, lastY; // Unsplash API key - Replace with your own key from https://unsplash.com/developers const UNSPLASH_ACCESS_KEY = 'LQO_HiDn0i3hdgaE3vdf76pAxcrdopA_p8JBEWISNUg'; // Load saved state function loadState() { const savedState = localStorage.getItem('thumbnailCreatorState'); if (savedState) { const parsed = JSON.parse(savedState); Object.assign(state.textObjects.main, parsed.textObjects?.main || {}); Object.assign(state.textObjects.secondary, parsed.textObjects?.secondary || {}); state.bgColor = parsed.bgColor || state.bgColor; state.overlayColor = parsed.overlayColor || state.overlayColor; state.overlayOpacity = parsed.overlayOpacity || state.overlayOpacity; state.currentImage = parsed.currentImage || null; state.zoom = parsed.zoom || 1; // Update DOM elements.mainTextInput.value = state.textObjects.main.text; elements.mainTextColorInput.value = state.textObjects.main.color; elements.mainTextStyleInput.value = state.textObjects.main.style; elements.mainTextSizeInput.value = state.textObjects.main.fontSize; elements.mainTextSizeValue.textContent = state.textObjects.main.fontSize; elements.mainTextRotationInput.value = state.textObjects.main.rotation * 180 / Math.PI; elements.mainTextRotationValue.textContent = elements.mainTextRotationInput.value; elements.mainTextBoldInput.value = state.textObjects.main.bold; elements.mainTextBoldValue.textContent = state.textObjects.main.bold; elements.mainTextNeonInput.value = state.textObjects.main.neon; elements.mainTextNeonValue.textContent = state.textObjects.main.neon; elements.mainTextStrokeInput.value = state.textObjects.main.stroke; elements.mainTextStrokeValue.textContent = state.textObjects.main.stroke; elements.mainTextShadowColorInput.value = state.textObjects.main.shadowColor; elements.secondaryTextInput.value = state.textObjects.secondary.text; elements.secondaryTextColorInput.value = state.textObjects.secondary.color; elements.secondaryTextStyleInput.value = state.textObjects.secondary.style; elements.secondaryTextSizeInput.value = state.textObjects.secondary.fontSize; elements.secondaryTextSizeValue.textContent = state.textObjects.secondary.fontSize; elements.secondaryTextRotationInput.value = state.textObjects.secondary.rotation * 180 / Math.PI; elements.secondaryTextRotationValue.textContent = elements.secondaryTextRotationInput.value; elements.secondaryTextBoldInput.value = state.textObjects.secondary.bold; elements.secondaryTextBoldValue.textContent = state.textObjects.secondary.bold; elements.secondaryTextNeonInput.value = state.textObjects.secondary.neon; elements.secondaryTextNeonValue.textContent = state.textObjects.secondary.neon; elements.secondaryTextStrokeInput.value = state.textObjects.secondary.stroke; elements.secondaryTextStrokeValue.textContent = state.textObjects.secondary.stroke; elements.secondaryTextShadowColorInput.value = state.textObjects.secondary.shadowColor; elements.bgColorInput.value = state.bgColor; elements.overlayColorInput.value = state.overlayColor; elements.overlayOpacityInput.value = state.overlayOpacity; elements.overlayOpacityValue.textContent = state.overlayOpacity; elements.canvasZoomInput.value = state.zoom; elements.canvasZoomValue.textContent = state.zoom + 'x'; } } // Save state function saveState() { localStorage.setItem('thumbnailCreatorState', JSON.stringify({ textObjects: state.textObjects, bgColor: state.bgColor, overlayColor: state.overlayColor, overlayOpacity: state.overlayOpacity, currentImage: state.currentImage, zoom: state.zoom })); } // Save history function saveHistory() { state.history = state.history.slice(0, state.historyIndex + 1); state.history.push(JSON.parse(JSON.stringify({ textObjects: state.textObjects, bgColor: state.bgColor, overlayColor: state.overlayColor, overlayOpacity: state.overlayOpacity, currentImage: state.currentImage, zoom: state.zoom }))); state.historyIndex++; updateUndoRedoButtons(); saveState(); } // Undo/Redo function undo() { if (state.historyIndex > 0) { state.historyIndex--; applyHistoryState(); } } function redo() { if (state.historyIndex < state.history.length - 1) { state.historyIndex++; applyHistoryState(); } } function applyHistoryState() { const historyState = state.history[state.historyIndex]; Object.assign(state.textObjects.main, historyState.textObjects.main); Object.assign(state.textObjects.secondary, historyState.textObjects.secondary); state.bgColor = historyState.bgColor; state.overlayColor = historyState.overlayColor; state.overlayOpacity = historyState.overlayOpacity; state.currentImage = historyState.currentImage; state.zoom = historyState.zoom; // Update DOM elements.mainTextInput.value = state.textObjects.main.text; elements.mainTextColorInput.value = state.textObjects.main.color; elements.mainTextStyleInput.value = state.textObjects.main.style; elements.mainTextSizeInput.value = state.textObjects.main.fontSize; elements.mainTextSizeValue.textContent = state.textObjects.main.fontSize; elements.mainTextRotationInput.value = state.textObjects.main.rotation * 180 / Math.PI; elements.mainTextRotationValue.textContent = elements.mainTextRotationInput.value; elements.mainTextBoldInput.value = state.textObjects.main.bold; elements.mainTextBoldValue.textContent = state.textObjects.main.bold; elements.mainTextNeonInput.value = state.textObjects.main.neon; elements.mainTextNeonValue.textContent = state.textObjects.main.neon; elements.mainTextStrokeInput.value = state.textObjects.main.stroke; elements.mainTextStrokeValue.textContent = state.textObjects.main.stroke; elements.mainTextShadowColorInput.value = state.textObjects.main.shadowColor; elements.secondaryTextInput.value = state.textObjects.secondary.text; elements.secondaryTextColorInput.value = state.textObjects.secondary.color; elements.secondaryTextStyleInput.value = state.textObjects.secondary.style; elements.secondaryTextSizeInput.value = state.textObjects.secondary.fontSize; elements.secondaryTextSizeValue.textContent = state.textObjects.secondary.fontSize; elements.secondaryTextRotationInput.value = state.textObjects.secondary.rotation * 180 / Math.PI; elements.secondaryTextRotationValue.textContent = elements.secondaryTextRotationInput.value; elements.secondaryTextBoldInput.value = state.textObjects.secondary.bold; elements.secondaryTextBoldValue.textContent = state.textObjects.secondary.bold; elements.secondaryTextNeonInput.value = state.textObjects.secondary.neon; elements.secondaryTextNeonValue.textContent = state.textObjects.secondary.neon; elements.secondaryTextStrokeInput.value = state.textObjects.secondary.stroke; elements.secondaryTextStrokeValue.textContent = state.textObjects.secondary.stroke; elements.secondaryTextShadowColorInput.value = state.textObjects.secondary.shadowColor; elements.bgColorInput.value = state.bgColor; elements.overlayColorInput.value = state.overlayColor; elements.overlayOpacityInput.value = state.overlayOpacity; elements.overlayOpacityValue.textContent = state.overlayOpacity; elements.canvasZoomInput.value = state.zoom; elements.canvasZoomValue.textContent = state.zoom + 'x'; updateUndoRedoButtons(); drawThumbnail(); } function updateUndoRedoButtons() { elements.undoBtn.disabled = state.historyIndex <= 0; elements.redoBtn.disabled = state.historyIndex >= state.history.length - 1; } // Initialize canvas size function initCanvas() { elements.canvas.width = canvasWidth; elements.canvas.height = canvasHeight; updateCanvasSize(); } function updateCanvasSize() { const displayWidth = elements.canvas.clientWidth; const scale = displayWidth / canvasWidth; elements.canvas.style.height = (canvasHeight * scale * state.zoom) + 'px'; elements.canvas.style.transform = `scale(${state.zoom})`; elements.canvas.style.transformOrigin = 'top left'; } // Apply text style function applyTextStyle(context, text, key) { const fontSize = text.fontSize; const color = text.color; const bold = text.bold; const neon = text.neon; const stroke = text.stroke; const shadowColor = text.shadowColor; let fontFamily = 'Impact'; let strokeColor = '#000'; let shadowBlur = neon; let shadowOffsetX = 0; let shadowOffsetY = 0; let fillStyle = color; switch(text.style) { case 'impact-bold': fontFamily = 'Impact'; break; case 'arial-black': fontFamily = 'Arial Black'; break; case 'roboto-bold': fontFamily = 'Roboto'; break; case 'montserrat-bold': fontFamily = 'Montserrat'; break; case 'bebas-neue': fontFamily = 'Bebas Neue'; break; case 'oswald-bold': fontFamily = 'Oswald'; break; case 'anton': fontFamily = 'Anton'; break; case 'poppins-bold': fontFamily = 'Poppins'; break; case 'lato-bold': fontFamily = 'Lato'; break; case 'impact-glow': fontFamily = 'Impact'; shadowBlur = neon || 15; break; case 'arial-shadow': fontFamily = 'Arial Black'; shadowOffsetX = 5; shadowOffsetY = 5; break; case 'roboto-gradient': fontFamily = 'Roboto'; const gradient = context.createLinearGradient(0, -fontSize/2, 0, fontSize/2); gradient.addColorStop(0, color); gradient.addColorStop(1, '#ffffff'); fillStyle = gradient; break; case 'montserrat-stroke': fontFamily = 'Montserrat'; strokeColor = '#f72585'; break; case 'bebas-neon': fontFamily = 'Bebas Neue'; shadowBlur = neon || 20; strokeColor = '#f72585'; break; case 'oswald-double-stroke': fontFamily = 'Oswald'; context.strokeStyle = '#000'; context.strokeText(text.text, 0, 0); strokeColor = '#4cc9f0'; break; case 'anton-shadow': fontFamily = 'Anton'; shadowOffsetX = 8; shadowOffsetY = 8; break; case 'poppins-glow': fontFamily = 'Poppins'; shadowBlur = neon || 10; break; case 'lato-gradient': fontFamily = 'Lato'; const gradient2 = context.createLinearGradient(0, -fontSize/2, 0, fontSize/2); gradient2.addColorStop(0, '#ff00ff'); gradient2.addColorStop(1, color); fillStyle = gradient2; break; case 'impact-outline': fontFamily = 'Impact'; fillStyle = 'transparent'; strokeColor = color; break; case 'roboto-neon': fontFamily = 'Roboto'; shadowBlur = neon || 25; strokeColor = color; break; } context.font = `${bold} ${fontSize}px ${fontFamily}, sans-serif`; context.fillStyle = fillStyle; context.strokeStyle = strokeColor; context.lineWidth = stroke; context.shadowColor = shadowColor; context.shadowBlur = shadowBlur; context.shadowOffsetX = shadowOffsetX; context.shadowOffsetY = shadowOffsetY; } // Load image function loadImage(source) { return new Promise((resolve, reject) => { if (!source || !source.value) { reject(new Error('No image source provided')); return; } const img = new Image(); img.crossOrigin = 'anonymous'; img.src = source.value; img.onload = () => resolve(img); img.onerror = () => reject(new Error(`Failed to load ${source.type === 'data' ? 'local' : 'Unsplash'} image`)); }); } // Draw on canvas function drawOnCanvas(context, width, height, showBoundingBox = true, callback = () => {}) { elements.loadingSpinner.style.display = 'block'; context.clearRect(0, 0, width, height); context.fillStyle = state.bgColor; context.fillRect(0, 0, width, height); const drawOverlayAndText = (img = null) => { if (img) { context.drawImage(img, 0, 0, width, height); } context.fillStyle = state.overlayColor; context.globalAlpha = state.overlayOpacity; context.fillRect(0, 0, width, height); context.globalAlpha = 1; for (const key in state.textObjects) { const text = state.textObjects[key]; context.save(); context.translate(text.x, text.y); context.rotate(text.rotation); context.textAlign = 'center'; context.textBaseline = 'middle'; applyTextStyle(context, text, key); context.strokeText(text.text, 0, 0); context.fillText(text.text, 0, 0); if (showBoundingBox && key === activeText) { const metrics = context.measureText(text.text); const textWidth = metrics.width; const textHeight = text.fontSize; const padding = 20; context.strokeStyle = '#4cc9f0'; context.lineWidth = 2; context.strokeRect(-textWidth/2 - padding, -textHeight/2 - padding, textWidth + 2*padding, textHeight + 2*padding); } context.restore(); } elements.loadingSpinner.style.display = 'none'; callback(); }; if (state.currentImage) { loadImage(state.currentImage) .then(img => drawOverlayAndText(img)) .catch(err => { elements.errorContainer.textContent = err.message; drawOverlayAndText(); }); } else { drawOverlayAndText(); } } // Draw thumbnail function drawThumbnail() { requestAnimationFrame(() => { drawOnCanvas(offscreenCtx, offscreenCanvas.width, offscreenCanvas.height, true, () => { ctx.clearRect(0, 0, canvasWidth, canvasHeight); ctx.drawImage(offscreenCanvas, 0, 0); }); }); } // Apply template function applyTemplate(template) { switch(template) { case 'red-dark': state.textObjects.main.color = '#ff0000'; state.textObjects.secondary.color = '#ffffff'; state.bgColor = '#000000'; state.overlayColor = '#8b0000'; state.overlayOpacity = 0.4; break; case 'green-dark': state.textObjects.main.color = '#00ff00'; state.textObjects.secondary.color = '#ffffff'; state.bgColor = '#000000'; state.overlayColor = '#003300'; state.overlayOpacity = 0.4; break; case 'blue-dark': state.textObjects.main.color = '#00ffff'; state.textObjects.secondary.color = '#ffffff'; state.bgColor = '#000000'; state.overlayColor = '#00008b'; state.overlayOpacity = 0.4; break; case 'orange-dark': state.textObjects.main.color = '#ff9900'; state.textObjects.secondary.color = '#ffffff'; state.bgColor = '#000000'; state.overlayColor = '#663300'; state.overlayOpacity = 0.4; break; case 'pink-dark': state.textObjects.main.color = '#ff00ff'; state.textObjects.secondary.color = '#ffffff'; state.bgColor = '#000000'; state.overlayColor = '#660066'; state.overlayOpacity = 0.4; break; } elements.mainTextColorInput.value = state.textObjects.main.color; elements.secondaryTextColorInput.value = state.textObjects.secondary.color; elements.bgColorInput.value = state.bgColor; elements.overlayColorInput.value = state.overlayColor; elements.overlayOpacityInput.value = state.overlayOpacity; elements.overlayOpacityValue.textContent = state.overlayOpacity; saveHistory(); drawThumbnail(); } // Set text position window.setTextPosition = function(key, position) { const text = state.textObjects[key]; const metrics = offscreenCtx.measureText(text.text); const textWidth = metrics.width; const textHeight = text.fontSize; const padding = 20; switch(position) { case 'top-left': text.x = textWidth / 2 + padding; text.y = textHeight / 2 + padding; break; case 'center': text.x = canvasWidth / 2; text.y = canvasHeight / 2 + (key === 'main' ? -50 : 50); break; case 'bottom-right': text.x = canvasWidth - textWidth / 2 - padding; text.y = canvasHeight - textHeight / 2 - padding; break; } saveHistory(); drawThumbnail(); }; // Unsplash search function searchUnsplash(page = 1, append = false) { const query = elements.unsplashSearchInput.value.trim(); if (!query) { elements.errorContainer.textContent = 'Please enter a search query.'; elements.loadMoreBtn.style.display = 'none'; elements.retryBtn.style.display = 'none'; return; } if (UNSPLASH_ACCESS_KEY === 'YOUR_UNSPLASH_ACCESS_KEY') { elements.errorContainer.textContent = 'Please replace YOUR_UNSPLASH_ACCESS_KEY with your Unsplash API key.'; elements.loadMoreBtn.style.display = 'none'; elements.retryBtn.style.display = 'none'; return; } state.unsplashQuery = query; state.unsplashPage = page; elements.loadingSpinner.style.display = 'block'; elements.loadMoreBtn.style.display = 'none'; elements.retryBtn.style.display = 'none'; if (!append) { elements.unsplashResults.innerHTML = ''; } fetch(`https://api.unsplash.com/search/photos?query=${encodeURIComponent(query)}&page=${page}&per_page=20&client_id=${UNSPLASH_ACCESS_KEY}`, { mode: 'cors', headers: { 'Accept': 'application/json' } }) .then(response => { if (!response.ok) { if (response.status === 403) { throw new Error('Unsplash API key is invalid or rate limit exceeded.'); } else if (response.status === 429) { throw new Error('Unsplash API rate limit exceeded. Try again later.'); } else { throw new Error(`HTTP error! Status: ${response.status}`); } } return response.json(); }) .then(data => { elements.loadingSpinner.style.display = 'none'; if (data.results && data.results.length > 0) { data.results.forEach(photo => { const img = document.createElement('img'); img.src = photo.urls.regular; img.alt = photo.alt_description || 'Unsplash image'; img.crossOrigin = 'anonymous'; img.style.opacity = 0; img.onload = () => { img.style.opacity = 1; }; img.onerror = () => { img.style.display = 'none'; }; img.onclick = () => { state.currentImage = { type: 'url', value: `${photo.urls.regular}&w=1280&h=720&fit=crop&auto=format` }; elements.unsplashResults.querySelectorAll('img').forEach(i => i.classList.remove('selected')); img.classList.add('selected'); saveHistory(); drawThumbnail(); }; elements.unsplashResults.appendChild(img); }); elements.loadMoreBtn.style.display = 'block'; elements.errorContainer.textContent = ''; } else { elements.errorContainer.textContent = 'No results found on Unsplash for this query.'; elements.loadMoreBtn.style.display = 'none'; } }) .catch(err => { elements.loadingSpinner.style.display = 'none'; elements.errorContainer.textContent = err.message || 'Failed to search Unsplash. Check your API key or internet connection.'; elements.retryBtn.style.display = 'block'; elements.loadMoreBtn.style.display = 'none'; console.error('Unsplash Error:', err); }); } // Event listeners function initEventListeners() { elements.mainTextInput.addEventListener('input', () => { state.textObjects.main.text = elements.mainTextInput.value; saveHistory(); drawThumbnail(); }); elements.mainTextColorInput.addEventListener('input', () => { state.textObjects.main.color = elements.mainTextColorInput.value; saveHistory(); drawThumbnail(); }); elements.mainTextStyleInput.addEventListener('change', () => { state.textObjects.main.style = elements.mainTextStyleInput.value; saveHistory(); drawThumbnail(); }); elements.mainTextSizeInput.addEventListener('input', () => { state.textObjects.main.fontSize = parseInt(elements.mainTextSizeInput.value); elements.mainTextSizeValue.textContent = state.textObjects.main.fontSize; saveHistory(); drawThumbnail(); }); elements.mainTextRotationInput.addEventListener('input', () => { state.textObjects.main.rotation = parseInt(elements.mainTextRotationInput.value) * Math.PI / 180; elements.mainTextRotationValue.textContent = elements.mainTextRotationInput.value; saveHistory(); drawThumbnail(); }); elements.mainTextBoldInput.addEventListener('input', () => { state.textObjects.main.bold = parseInt(elements.mainTextBoldInput.value); elements.mainTextBoldValue.textContent = state.textObjects.main.bold; saveHistory(); drawThumbnail(); }); elements.mainTextNeonInput.addEventListener('input', () => { state.textObjects.main.neon = parseInt(elements.mainTextNeonInput.value); elements.mainTextNeonValue.textContent = state.textObjects.main.neon; saveHistory(); drawThumbnail(); }); elements.mainTextStrokeInput.addEventListener('input', () => { state.textObjects.main.stroke = parseInt(elements.mainTextStrokeInput.value); elements.mainTextStrokeValue.textContent = state.textObjects.main.stroke; saveHistory(); drawThumbnail(); }); elements.mainTextShadowColorInput.addEventListener('input', () => { state.textObjects.main.shadowColor = elements.mainTextShadowColorInput.value; saveHistory(); drawThumbnail(); }); elements.secondaryTextInput.addEventListener('input', () => { state.textObjects.secondary.text = elements.secondaryTextInput.value; saveHistory(); drawThumbnail(); }); elements.secondaryTextColorInput.addEventListener('input', () => { state.textObjects.secondary.color = elements.secondaryTextColorInput.value; saveHistory(); drawThumbnail(); }); elements.secondaryTextStyleInput.addEventListener('change', () => { state.textObjects.secondary.style = elements.secondaryTextStyleInput.value; saveHistory(); drawThumbnail(); }); elements.secondaryTextSizeInput.addEventListener('input', () => { state.textObjects.secondary.fontSize = parseInt(elements.secondaryTextSizeInput.value); elements.secondaryTextSizeValue.textContent = state.textObjects.secondary.fontSize; saveHistory(); drawThumbnail(); }); elements.secondaryTextRotationInput.addEventListener('input', () => { state.textObjects.secondary.rotation = parseInt(elements.secondaryTextRotationInput.value) * Math.PI / 180; elements.secondaryTextRotationValue.textContent = elements.secondaryTextRotationInput.value; saveHistory(); drawThumbnail(); }); elements.secondaryTextBoldInput.addEventListener('input', () => { state.textObjects.secondary.bold = parseInt(elements.secondaryTextBoldInput.value); elements.secondaryTextBoldValue.textContent = state.textObjects.secondary.bold; saveHistory(); drawThumbnail(); }); elements.secondaryTextNeonInput.addEventListener('input', () => { state.textObjects.secondary.neon = parseInt(elements.secondaryTextNeonInput.value); elements.secondaryTextNeonValue.textContent = state.textObjects.secondary.neon; saveHistory(); drawThumbnail(); }); elements.secondaryTextStrokeInput.addEventListener('input', () => { state.textObjects.secondary.stroke = parseInt(elements.secondaryTextStrokeInput.value); elements.secondaryTextStrokeValue.textContent = state.textObjects.secondary.stroke; saveHistory(); drawThumbnail(); }); elements.secondaryTextShadowColorInput.addEventListener('input', () => { state.textObjects.secondary.shadowColor = elements.secondaryTextShadowColorInput.value; saveHistory(); drawThumbnail(); }); elements.bgColorInput.addEventListener('input', () => { state.bgColor = elements.bgColorInput.value; saveHistory(); drawThumbnail(); }); elements.overlayColorInput.addEventListener('input', () => { state.overlayColor = elements.overlayColorInput.value; saveHistory(); drawThumbnail(); }); elements.overlayOpacityInput.addEventListener('input', () => { state.overlayOpacity = parseFloat(elements.overlayOpacityInput.value); elements.overlayOpacityValue.textContent = state.overlayOpacity; saveHistory(); drawThumbnail(); }); elements.canvasZoomInput.addEventListener('input', () => { state.zoom = parseFloat(elements.canvasZoomInput.value); elements.canvasZoomValue.textContent = state.zoom + 'x'; updateCanvasSize(); saveHistory(); drawThumbnail(); }); elements.bgImageFileInput.addEventListener('change', () => { elements.errorContainer.textContent = ''; if (elements.bgImageFileInput.files && elements.bgImageFileInput.files[0]) { const reader = new FileReader(); reader.onload = function(e) { state.currentImage = { type: 'data', value: e.target.result }; elements.unsplashResults.innerHTML = ''; saveHistory(); drawThumbnail(); }; reader.onerror = () => { elements.errorContainer.textContent = 'Failed to read local image file.'; }; reader.readAsDataURL(elements.bgImageFileInput.files[0]); } }); elements.searchUnsplashBtn.addEventListener('click', () => searchUnsplash(1)); elements.loadMoreBtn.addEventListener('click', () => searchUnsplash(state.unsplashPage + 1, true)); elements.retryBtn.addEventListener('click', () => searchUnsplash(state.unsplashPage)); elements.generateBtn.addEventListener('click', drawThumbnail); elements.downloadBtn.addEventListener('click', () => { const tempCanvas = document.createElement('canvas'); tempCanvas.width = canvasWidth; tempCanvas.height = canvasHeight; const tempCtx = tempCanvas.getContext('2d'); drawOnCanvas(tempCtx, canvasWidth, canvasHeight, false, () => { const link = document.createElement('a'); link.download = 'gaming-thumbnail.png'; link.href = tempCanvas.toDataURL('image/png'); document.body.appendChild(link); link.click(); document.body.removeChild(link); }); }); elements.undoBtn.addEventListener('click', undo); elements.redoBtn.addEventListener('click', redo); elements.templates.forEach(template => { template.addEventListener('click', () => { elements.templates.forEach(t => t.classList.remove('active')); template.classList.add('active'); applyTemplate(template.getAttribute('data-template')); }); }); // Mouse events elements.canvas.addEventListener('mousemove', e => handleMove(e, 'mouse')); elements.canvas.addEventListener('mousedown', e => handleDown(e, 'mouse')); elements.canvas.addEventListener('mouseup', () => handleUp()); elements.canvas.addEventListener('mouseleave', handleUp); // Touch events elements.canvas.addEventListener('touchstart', e => handleDown(e, 'touch'), { passive: false }); elements.canvas.addEventListener('touchmove', e => handleMove(e, 'touch'), { passive: false }); elements.canvas.addEventListener('touchend', handleUp); // Handle drag function handleDown(e, type) { const rect = elements.canvas.getBoundingClientRect(); const x = (type === 'mouse' ? e.clientX : e.touches[0].clientX) - rect.left; const y = (type === 'mouse' ? e.clientY : e.touches[0].clientY) - rect.top; const canvasX = x * (canvasWidth / rect.width) / state.zoom; const canvasY = y * (canvasHeight / rect.height) / state.zoom; activeText = null; for (const key in state.textObjects) { const text = state.textObjects[key]; offscreenCtx.font = `${text.bold} ${text.fontSize}px Impact, sans-serif`; const metrics = offscreenCtx.measureText(text.text); const textWidth = metrics.width; const textHeight = text.fontSize; const padding = 20; const cos = Math.cos(-text.rotation); const sin = Math.sin(-text.rotation); const localX = (canvasX - text.x) * cos - (canvasY - text.y) * sin; const localY = (canvasX - text.x) * sin + (canvasY - text.y) * cos; if (localX > -textWidth/2 - padding && localX < textWidth/2 + padding && localY > -textHeight/2 - padding && localY < textHeight/2 + padding) { activeText = key; elements.canvas.style.cursor = 'move'; isDragging = true; lastX = x; lastY = y; if (type === 'touch') e.preventDefault(); break; } } if (!activeText) { elements.canvas.style.cursor = 'default'; } drawThumbnail(); } function handleMove(e, type) { const rect = elements.canvas.getBoundingClientRect(); const x = (type === 'mouse' ? e.clientX : e.touches[0].clientX) - rect.left; const y = (type === 'mouse' ? e.clientY : e.touches[0].clientY) - rect.top; const canvasX = x * (canvasWidth / rect.width) / state.zoom; const canvasY = y * (canvasHeight / rect.height) / state.zoom; if (isDragging && activeText) { const dx = (x - lastX) * (canvasWidth / rect.width) / state.zoom; const dy = (y - lastY) * (canvasHeight / rect.height) / state.zoom; state.textObjects[activeText].x += dx; state.textObjects[activeText].y += dy; lastX = x; lastY = y; saveHistory(); drawThumbnail(); if (type === 'touch') e.preventDefault(); } else { let cursor = 'default'; for (const key in state.textObjects) { const text = state.textObjects[key]; offscreenCtx.font = `${text.bold} ${text.fontSize}px Impact, sans-serif`; const metrics = offscreenCtx.measureText(text.text); const textWidth = metrics.width; const textHeight = text.fontSize; const padding = 20; const cos = Math.cos(-text.rotation); const sin = Math.sin(-text.rotation); const localX = (canvasX - text.x) * cos - (canvasY - text.y) * sin; const localY = (canvasX - text.x) * sin + (canvasY - text.y) * cos; if (localX > -textWidth/2 - padding && localX < textWidth/2 + padding && localY > -textHeight/2 - padding && localY < textHeight/2 + padding) { cursor = 'move'; activeText = key; break; } } elements.canvas.style.cursor = cursor; drawThumbnail(); } } function handleUp() { isDragging = false; activeText = null; elements.canvas.style.cursor = 'default'; drawThumbnail(); } // Menu toggle document.querySelector('.menu-icon').addEventListener('click', () => { document.querySelector('nav ul').classList.toggle('show'); }); } // Initialize document.addEventListener('DOMContentLoaded', () => { loadState(); initCanvas(); initEventListeners(); drawThumbnail(); }); })(); </script> </body> </html>
🔙 Back