diff --git a/Extension/scripts/Auto-Image.js b/Extension/scripts/Auto-Image.js index ed749b1..91e7b23 100644 --- a/Extension/scripts/Auto-Image.js +++ b/Extension/scripts/Auto-Image.js @@ -1051,6 +1051,21 @@ function getText(key, params) { } }, + hideAlert: () => { + // Remove existing alerts immediately with safety checks + const alerts = document.querySelectorAll('.wplace-alert-base'); + alerts.forEach(alert => { + try { + if (alert && alert.parentNode) { + alert.parentNode.removeChild(alert); + } + } catch (error) { + // Ignore removeChild errors if element was already removed + console.debug('Alert already removed:', error); + } + }); + }, + // Color utilities colorDistance: (a, b) => window.globalUtilsManager ? window.globalUtilsManager.colorDistance(a, b) : Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2) + Math.pow(a[2] - b[2], 2)), findClosestPaletteColor: (r, g, b, palette) => window.globalUtilsManager ? window.globalUtilsManager.findClosestPaletteColor(r, g, b, palette) : [r, g, b], @@ -2095,6 +2110,10 @@ function getText(key, params) { ${Utils.t('resizeImage')} +
+
+ +
+
+ +
+ +
1px
+ +
+ +
+
+ `; + + // No overlay - append directly to body like other panels + document.body.appendChild(movePanel); + + // Make the panel draggable + makeDraggable(movePanel); + + // Add event listeners for movement + let currentMessageTimeout = null; + + function moveArtwork(deltaX, deltaY) { + if (state.startPosition && state.region) { + const newX = state.startPosition.x + deltaX; + const newY = state.startPosition.y + deltaY; + + console.log(`🔄 Moving artwork from (${state.startPosition.x}, ${state.startPosition.y}) to (${newX}, ${newY})`); + + state.startPosition.x = newX; + state.startPosition.y = newY; + + // Clear any existing message timeout and alerts immediately + if (currentMessageTimeout) { + clearTimeout(currentMessageTimeout); + currentMessageTimeout = null; + } + Utils.hideAlert(); // Hide current alert immediately + + // Update overlay position if available + if (overlayManager) { + overlayManager.setPosition(state.startPosition, state.region) + .then(() => { + console.log('✅ Overlay position updated'); + Utils.showAlert(`Moved to position (${newX}, ${newY})`, 'success'); + // Set new timeout for this message + currentMessageTimeout = setTimeout(() => { + Utils.hideAlert(); + currentMessageTimeout = null; + }, 1500); // Message disappears after 1.5 seconds + }) + .catch(err => { + console.error('❌ Failed to update overlay:', err); + Utils.showAlert('Failed to update overlay position', 'error'); + }); + } else { + Utils.showAlert(`Position updated to (${newX}, ${newY})`, 'success'); + // Set new timeout for this message + currentMessageTimeout = setTimeout(() => { + Utils.hideAlert(); + currentMessageTimeout = null; + }, 1500); + } + } else { + Utils.showAlert('Please select a position first before moving artwork', 'warning'); + } + } + + document.getElementById('moveUp').addEventListener('click', () => moveArtwork(0, -1)); + document.getElementById('moveDown').addEventListener('click', () => moveArtwork(0, 1)); + document.getElementById('moveLeft').addEventListener('click', () => moveArtwork(-1, 0)); + document.getElementById('moveRight').addEventListener('click', () => moveArtwork(1, 0)); + + // Close panel + function closeMovePanel() { + if (currentMessageTimeout) { + clearTimeout(currentMessageTimeout); + currentMessageTimeout = null; + } + Utils.hideAlert(); + document.body.removeChild(movePanel); + } + + document.getElementById('closeMovePanel').addEventListener('click', closeMovePanel); + } + function showResizeDialog(processor) { let baseProcessor = processor; let width, height; @@ -5325,7 +5462,8 @@ function getText(key, params) { state.totalPixels = totalValidPixels; state.paintedPixels = 0; state.imageLoaded = true; - state.lastPosition = { x: 0, y: 0 }; + // Keep existing lastPosition to continue from where we left off + // state.lastPosition = { x: 0, y: 0 }; // REMOVED: Don't reset position // Initialize painted map for tracking Utils.initializePaintedMap(width, height); @@ -5432,6 +5570,46 @@ function getText(key, params) { state.region = null; state.selectingPosition = true; + // For extracted files, force enable all necessary flags for full functionality + state.colorsChecked = true; + state.imageLoaded = true; + + // Ensure image processor is available for extracted files + if (state.imageData && !state.imageData.processor) { + try { + // Create a temporary canvas to generate a data URL for the processor + const canvas = document.createElement('canvas'); + canvas.width = state.imageData.width; + canvas.height = state.imageData.height; + const ctx = canvas.getContext('2d'); + + // Create image data from pixels + const imageData = new ImageData( + new Uint8ClampedArray(state.imageData.pixels), + state.imageData.width, + state.imageData.height + ); + ctx.putImageData(imageData, 0, 0); + + // Create processor with canvas data URL using the correct class + const dataUrl = canvas.toDataURL(); + state.imageData.processor = new window.WPlaceImageProcessor(dataUrl); + + // CRITICAL: Load the processor before using it + await state.imageData.processor.load(); + console.log('🔧 Created and loaded WPlaceImageProcessor for extracted artwork'); + } catch (error) { + console.error('❌ Failed to create image processor for extracted artwork:', error); + // Fallback: use global processor if available + if (window.globalImageProcessor) { + console.log('🔄 Using global image processor as fallback'); + state.imageData.processor = window.globalImageProcessor; + } + } + } + + console.log('🎨 Force-enabled all flags for extracted JSON to access resize panel'); + // For extracted images, ensure overlay is enabled but don't set position yet try { if (overlayManager && state.imageData) { @@ -5457,14 +5635,20 @@ function getText(key, params) { // For extracted images, don't try to set overlay position until user selects one // The overlay should already be enabled from the restoreProgress function - // Update UI to show position selection needed - Utils.showAlert('Extracted artwork loaded! Click on the canvas to select where to place it.', 'success'); + // Update UI with custom message for extracted artwork + Utils.showAlert('🎨 Extracted artwork loaded! Please click on the canvas to select where to place it.', 'info'); updateUI('ready', 'default'); // Use 'ready' instead of 'waitingPosition' to avoid translation issues // Enable relevant buttons selectPosBtn.disabled = false; - if (state.colorsChecked) { - resizeBtn.disabled = false; + // Always enable resize button for extracted files since they have image processor + resizeBtn.disabled = false; + console.log('🔧 Resize button enabled for extracted artwork'); + + // Enable move artwork button (will be fully enabled after position is set) + const moveArtworkBtn = document.getElementById('moveArtworkBtn'); + if (moveArtworkBtn) { + moveArtworkBtn.disabled = false; } // Set up position selection like the regular selectPosBtn @@ -5516,6 +5700,13 @@ function getText(key, params) { startBtn.disabled = false; selectPosBtn.textContent = Utils.t('selectPosition'); + + // Enable Move Artwork button when position is set + const moveArtworkBtn = document.getElementById('moveArtworkBtn'); + if (moveArtworkBtn) { + moveArtworkBtn.disabled = false; + } + updateUI('ready', 'success'); Utils.showAlert('Position selected! Ready to start painting.', 'success'); } @@ -5540,10 +5731,34 @@ function getText(key, params) { if (resizeBtn) { resizeBtn.addEventListener('click', () => { - if (state.imageLoaded && state.imageData.processor && state.colorsChecked) { + console.log('🔍 [DEBUG] Resize button clicked. State check:', { + imageLoaded: state.imageLoaded, + hasProcessor: !!(state.imageData && state.imageData.processor), + colorsChecked: state.colorsChecked, + hasAvailableColors: !!(state.availableColors && state.availableColors.length > 0) + }); + + if (state.imageLoaded && state.imageData && state.imageData.processor) { showResizeDialog(state.imageData.processor); - } else if (!state.colorsChecked) { - Utils.showAlert(Utils.t('uploadImageFirstColors'), 'warning'); + } else { + let message = 'Please upload an image and check colors first.'; + if (!state.imageLoaded) message = 'Please upload an image first.'; + else if (!state.imageData || !state.imageData.processor) message = 'Image processor not available. Please reload the image.'; + Utils.showAlert(message, 'warning'); + } + }); + } + + // Move Artwork button event listener + const moveArtworkBtn = document.getElementById('moveArtworkBtn'); + if (moveArtworkBtn) { + moveArtworkBtn.addEventListener('click', () => { + if (state.imageLoaded && (state.startPosition || state.selectingPosition)) { + showMoveArtworkPanel(); + } else if (!state.imageLoaded) { + Utils.showAlert('Please upload or load an image first', 'warning'); + } else { + Utils.showAlert('Please select a position for the artwork first', 'warning'); } }); } @@ -5556,6 +5771,12 @@ function getText(key, params) { state.startPosition = null; state.region = null; startBtn.disabled = true; + + // Disable Move Artwork button when selecting new position + const moveArtworkBtn = document.getElementById('moveArtworkBtn'); + if (moveArtworkBtn) { + moveArtworkBtn.disabled = true; + } Utils.showAlert(Utils.t('selectPositionAlert'), 'info'); updateUI('waitingPosition', 'default'); @@ -5586,7 +5807,8 @@ function getText(key, params) { x: payload.coords[0], y: payload.coords[1], }; - state.lastPosition = { x: 0, y: 0 }; + // Keep existing lastPosition to continue from where we left off + // state.lastPosition = { x: 0, y: 0 }; // REMOVED: Don't reset position // Update overlay position with validation try { @@ -5602,6 +5824,12 @@ function getText(key, params) { if (state.imageLoaded) { startBtn.disabled = false; + + // Enable Move Artwork button when position is set + const moveArtworkBtn = document.getElementById('moveArtworkBtn'); + if (moveArtworkBtn) { + moveArtworkBtn.disabled = false; + } } window.fetch = originalFetch; @@ -6296,7 +6524,12 @@ function getText(key, params) { } // Only include pixels that haven't been marked as painted yet - if (!Utils.isPixelPainted(x, y)) { + let absX = startX + x; + let absY = startY + y; + let adderX = Math.floor(absX / 1000); + let adderY = Math.floor(absY / 1000); + + if (!Utils.isPixelPainted(x, y, regionX + adderX, regionY + adderY)) { eligibleCoords.push([x, y, targetPixelInfo]); } } @@ -6422,7 +6655,7 @@ function getText(key, params) { // Check initial charges to calculate wait time let chargeCheckCount = 0; - const maxChargeChecks = 10; // Limit API calls during cooldown + // const maxChargeChecks = 10; // REMOVED: No limit on API calls during cooldown while (!state.stopFlag) { chargeCheckCount++; @@ -6469,11 +6702,11 @@ function getText(key, params) { console.log(`⏱️ Cooldown check ${chargeCheckCount}: ${state.displayCharges}/${state.cooldownChargeThreshold} charges, waiting 10s before next check`); await Utils.sleep(10000); - // Fail-safe: Don't exceed max checks - if (chargeCheckCount >= maxChargeChecks) { - console.warn('⚠️ Max charge checks reached during cooldown, continuing anyway'); - break; - } + // REMOVED: No limit on charge checks - bot will wait infinitely until charges are available + // if (chargeCheckCount >= maxChargeChecks) { + // console.warn('⚠️ Max charge checks reached during cooldown, continuing anyway'); + // break; + // } } return 'stopped'; @@ -6514,7 +6747,7 @@ function getText(key, params) { Utils.saveProgress(); } else { updateUI('paintingComplete', 'success', { count: state.paintedPixels }); - state.lastPosition = { x: 0, y: 0 }; + state.lastPosition = { x: 0, y: 0 }; // Only reset when truly complete Utils.saveProgress(); // Save final complete state overlayManager.clear(); const toggleOverlayBtn = document.getElementById('toggleOverlayBtn'); diff --git a/Extension/scripts/image-processor.js b/Extension/scripts/image-processor.js index cc8bff6..37beb2e 100644 --- a/Extension/scripts/image-processor.js +++ b/Extension/scripts/image-processor.js @@ -26,6 +26,13 @@ class ImageProcessor { // Color cache for performance optimization this._colorCache = new Map(); this._labCache = new Map(); + this._hsvCache = new Map(); + this._hslCache = new Map(); + this._xyzCache = new Map(); + this._luvCache = new Map(); + this._yuvCache = new Map(); + this._oklabCache = new Map(); + this._lchCache = new Map(); // Configuration constants this.TRANSPARENCY_THRESHOLD = 128; @@ -196,6 +203,403 @@ class ImageProcessor { return v; } + /** + * Convert RGB to HSV color space + * @param {number} r - Red value (0-255) + * @param {number} g - Green value (0-255) + * @param {number} b - Blue value (0-255) + * @returns {number[]} HSV values [H (0-360), S (0-1), V (0-1)] + * @private + */ + _rgbToHsv(r, g, b) { + r /= 255; + g /= 255; + b /= 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const delta = max - min; + + let h = 0; + const s = max === 0 ? 0 : delta / max; + const v = max; + + if (delta !== 0) { + if (max === r) { + h = ((g - b) / delta) % 6; + } else if (max === g) { + h = (b - r) / delta + 2; + } else { + h = (r - g) / delta + 4; + } + h *= 60; + if (h < 0) h += 360; + } + + return [h, s, v]; + } + + /** + * Convert HSV to RGB color space + * @param {number} h - Hue value (0-360) + * @param {number} s - Saturation value (0-1) + * @param {number} v - Value/Brightness (0-1) + * @returns {number[]} RGB values [R, G, B] (0-255) + * @private + */ + _hsvToRgb(h, s, v) { + const c = v * s; + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); + const m = v - c; + + let r = 0, g = 0, b = 0; + + if (h >= 0 && h < 60) { + r = c; g = x; b = 0; + } else if (h >= 60 && h < 120) { + r = x; g = c; b = 0; + } else if (h >= 120 && h < 180) { + r = 0; g = c; b = x; + } else if (h >= 180 && h < 240) { + r = 0; g = x; b = c; + } else if (h >= 240 && h < 300) { + r = x; g = 0; b = c; + } else if (h >= 300 && h < 360) { + r = c; g = 0; b = x; + } + + return [ + Math.round((r + m) * 255), + Math.round((g + m) * 255), + Math.round((b + m) * 255) + ]; + } + + /** + * Convert RGB to HSL color space + * @param {number} r - Red value (0-255) + * @param {number} g - Green value (0-255) + * @param {number} b - Blue value (0-255) + * @returns {number[]} HSL values [H (0-360), S (0-1), L (0-1)] + * @private + */ + _rgbToHsl(r, g, b) { + r /= 255; + g /= 255; + b /= 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const delta = max - min; + + let h = 0; + let s = 0; + const l = (max + min) / 2; + + if (delta !== 0) { + s = l > 0.5 ? delta / (2 - max - min) : delta / (max + min); + + if (max === r) { + h = ((g - b) / delta) % 6; + } else if (max === g) { + h = (b - r) / delta + 2; + } else { + h = (r - g) / delta + 4; + } + h *= 60; + if (h < 0) h += 360; + } + + return [h, s, l]; + } + + /** + * Convert HSL to RGB color space + * @param {number} h - Hue value (0-360) + * @param {number} s - Saturation value (0-1) + * @param {number} l - Lightness value (0-1) + * @returns {number[]} RGB values [R, G, B] (0-255) + * @private + */ + _hslToRgb(h, s, l) { + const c = (1 - Math.abs(2 * l - 1)) * s; + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); + const m = l - c / 2; + + let r = 0, g = 0, b = 0; + + if (h >= 0 && h < 60) { + r = c; g = x; b = 0; + } else if (h >= 60 && h < 120) { + r = x; g = c; b = 0; + } else if (h >= 120 && h < 180) { + r = 0; g = c; b = x; + } else if (h >= 180 && h < 240) { + r = 0; g = x; b = c; + } else if (h >= 240 && h < 300) { + r = x; g = 0; b = c; + } else if (h >= 300 && h < 360) { + r = c; g = 0; b = x; + } + + return [ + Math.round((r + m) * 255), + Math.round((g + m) * 255), + Math.round((b + m) * 255) + ]; + } + + /** + * Convert RGB to XYZ color space + * @param {number} r - Red value (0-255) + * @param {number} g - Green value (0-255) + * @param {number} b - Blue value (0-255) + * @returns {number[]} XYZ values [X, Y, Z] + * @private + */ + _rgbToXyz(r, g, b) { + // Normalize RGB values + let x = r / 255.0; + let y = g / 255.0; + let z = b / 255.0; + + // Apply gamma correction + x = x > 0.04045 ? Math.pow((x + 0.055) / 1.055, 2.4) : x / 12.92; + y = y > 0.04045 ? Math.pow((y + 0.055) / 1.055, 2.4) : y / 12.92; + z = z > 0.04045 ? Math.pow((z + 0.055) / 1.055, 2.4) : z / 12.92; + + // Convert to XYZ using sRGB matrix + const X = x * 0.4124564 + y * 0.3575761 + z * 0.1804375; + const Y = x * 0.2126729 + y * 0.7151522 + z * 0.0721750; + const Z = x * 0.0193339 + y * 0.1191920 + z * 0.9503041; + + return [X, Y, Z]; + } + + /** + * Convert XYZ to RGB color space + * @param {number} x - X value + * @param {number} y - Y value + * @param {number} z - Z value + * @returns {number[]} RGB values [R, G, B] (0-255) + * @private + */ + _xyzToRgb(x, y, z) { + // Convert XYZ to linear RGB + let r = x * 3.2404542 + y * -1.5371385 + z * -0.4985314; + let g = x * -0.9692660 + y * 1.8760108 + z * 0.0415560; + let b = x * 0.0556434 + y * -0.2040259 + z * 1.0572252; + + // Apply gamma correction + r = r > 0.0031308 ? 1.055 * Math.pow(r, 1.0 / 2.4) - 0.055 : 12.92 * r; + g = g > 0.0031308 ? 1.055 * Math.pow(g, 1.0 / 2.4) - 0.055 : 12.92 * g; + b = b > 0.0031308 ? 1.055 * Math.pow(b, 1.0 / 2.4) - 0.055 : 12.92 * b; + + // Clamp and convert to 0-255 range + return [ + Math.round(Math.max(0, Math.min(1, r)) * 255), + Math.round(Math.max(0, Math.min(1, g)) * 255), + Math.round(Math.max(0, Math.min(1, b)) * 255) + ]; + } + + /** + * Convert RGB to LUV color space + * @param {number} r - Red value (0-255) + * @param {number} g - Green value (0-255) + * @param {number} b - Blue value (0-255) + * @returns {number[]} LUV values [L, u, v] + * @private + */ + _rgbToLuv(r, g, b) { + const [X, Y, Z] = this._rgbToXyz(r, g, b); + + // Reference white D65 + const Xn = 0.95047; + const Yn = 1.00000; + const Zn = 1.08883; + + const yr = Y / Yn; + const L = yr > 0.008856 ? 116 * Math.pow(yr, 1/3) - 16 : 903.3 * yr; + + const denom = X + 15 * Y + 3 * Z; + const denomN = Xn + 15 * Yn + 3 * Zn; + + const up = denom === 0 ? 0 : (4 * X) / denom; + const vp = denom === 0 ? 0 : (9 * Y) / denom; + const upN = (4 * Xn) / denomN; + const vpN = (9 * Yn) / denomN; + + const u = 13 * L * (up - upN); + const v = 13 * L * (vp - vpN); + + return [L, u, v]; + } + + /** + * Convert LUV to RGB color space + * @param {number} l - L value + * @param {number} u - u value + * @param {number} v - v value + * @returns {number[]} RGB values [R, G, B] (0-255) + * @private + */ + _luvToRgb(l, u, v) { + // Reference white D65 + const Xn = 0.95047; + const Yn = 1.00000; + const Zn = 1.08883; + + const upN = (4 * Xn) / (Xn + 15 * Yn + 3 * Zn); + const vpN = (9 * Yn) / (Xn + 15 * Yn + 3 * Zn); + + const Y = l > 8 ? Yn * Math.pow((l + 16) / 116, 3) : Yn * l / 903.3; + + const up = u / (13 * l) + upN; + const vp = v / (13 * l) + vpN; + + const X = Y * 9 * up / (4 * vp); + const Z = Y * (12 - 3 * up - 20 * vp) / (4 * vp); + + return this._xyzToRgb(X, Y, Z); + } + + /** + * Convert RGB to YUV color space + * @param {number} r - Red value (0-255) + * @param {number} g - Green value (0-255) + * @param {number} b - Blue value (0-255) + * @returns {number[]} YUV values [Y, U, V] + * @private + */ + _rgbToYuv(r, g, b) { + const Y = 0.299 * r + 0.587 * g + 0.114 * b; + const U = -0.14713 * r - 0.28886 * g + 0.436 * b; + const V = 0.615 * r - 0.51499 * g - 0.10001 * b; + + return [Y, U, V]; + } + + /** + * Convert YUV to RGB color space + * @param {number} y - Y (luminance) value + * @param {number} u - U (chrominance) value + * @param {number} v - V (chrominance) value + * @returns {number[]} RGB values [R, G, B] (0-255) + * @private + */ + _yuvToRgb(y, u, v) { + const r = y + 1.13983 * v; + const g = y - 0.39465 * u - 0.58060 * v; + const b = y + 2.03211 * u; + + return [ + Math.round(Math.max(0, Math.min(255, r))), + Math.round(Math.max(0, Math.min(255, g))), + Math.round(Math.max(0, Math.min(255, b))) + ]; + } + + /** + * Convert RGB to Oklab color space (improved LAB) + * @param {number} r - Red value (0-255) + * @param {number} g - Green value (0-255) + * @param {number} b - Blue value (0-255) + * @returns {number[]} Oklab values [L, a, b] + * @private + */ + _rgbToOklab(r, g, b) { + // Normalize RGB + r /= 255; + g /= 255; + b /= 255; + + // Apply gamma correction + r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92; + g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92; + b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92; + + // Linear RGB to Oklab + const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b; + const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b; + const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b; + + const l_ = Math.cbrt(l); + const m_ = Math.cbrt(m); + const s_ = Math.cbrt(s); + + return [ + 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_, + 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_, + 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_ + ]; + } + + /** + * Convert Oklab to RGB color space + * @param {number} l - L value + * @param {number} a - a value + * @param {number} b - b value + * @returns {number[]} RGB values [R, G, B] (0-255) + * @private + */ + _oklabToRgb(l, a, b) { + const l_ = l + 0.3963377774 * a + 0.2158037573 * b; + const m_ = l - 0.1055613458 * a - 0.0638541728 * b; + const s_ = l - 0.0894841775 * a - 1.2914855480 * b; + + const l3 = l_ * l_ * l_; + const m3 = m_ * m_ * m_; + const s3 = s_ * s_ * s_; + + let r = 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3; + let g = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3; + let b2 = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.7076147010 * s3; + + // Apply gamma correction + r = r > 0.0031308 ? 1.055 * Math.pow(r, 1.0 / 2.4) - 0.055 : 12.92 * r; + g = g > 0.0031308 ? 1.055 * Math.pow(g, 1.0 / 2.4) - 0.055 : 12.92 * g; + b2 = b2 > 0.0031308 ? 1.055 * Math.pow(b2, 1.0 / 2.4) - 0.055 : 12.92 * b2; + + return [ + Math.round(Math.max(0, Math.min(1, r)) * 255), + Math.round(Math.max(0, Math.min(1, g)) * 255), + Math.round(Math.max(0, Math.min(1, b2)) * 255) + ]; + } + + /** + * Convert LAB to LCH color space (cylindrical representation) + * @param {number} l - L value + * @param {number} a - a value + * @param {number} b - b value + * @returns {number[]} LCH values [L, C, H] + * @private + */ + _labToLch(l, a, b) { + const c = Math.sqrt(a * a + b * b); + let h = Math.atan2(b, a) * 180 / Math.PI; + if (h < 0) h += 360; + + return [l, c, h]; + } + + /** + * Convert LCH to LAB color space + * @param {number} l - L value + * @param {number} c - C (chroma) value + * @param {number} h - H (hue) value (0-360) + * @returns {number[]} LAB values [L, a, b] + * @private + */ + _lchToLab(l, c, h) { + const hRad = h * Math.PI / 180; + const a = c * Math.cos(hRad); + const b = c * Math.sin(hRad); + + return [l, a, b]; + } + /** * Find closest color in palette using specified algorithm * @param {number} r - Red value @@ -235,7 +639,170 @@ class ImageProcessor { return cor; } - // LAB algorithm + // HSV algorithm + if (algorithm === 'hsv') { + const [ht, st, vt] = this._rgbToHsv(r, g, b); + let best = null; + let bestDist = Infinity; + + for (let i = 0; i < palette.length; i++) { + const [pr, pg, pb] = palette[i]; + const [hp, sp, vp] = this._rgbToHsv(pr, pg, pb); + const dh = Math.min(Math.abs(ht - hp), 360 - Math.abs(ht - hp)) / 360; // Normalize hue distance + const ds = st - sp; + const dv = vt - vp; + const dist = dh * dh + ds * ds + dv * dv; + + if (dist < bestDist) { + bestDist = dist; + best = palette[i]; + if (bestDist === 0) break; + } + } + return best || [0, 0, 0]; + } + + // HSL algorithm + if (algorithm === 'hsl') { + const [ht, st, lt] = this._rgbToHsl(r, g, b); + let best = null; + let bestDist = Infinity; + + for (let i = 0; i < palette.length; i++) { + const [pr, pg, pb] = palette[i]; + const [hp, sp, lp] = this._rgbToHsl(pr, pg, pb); + const dh = Math.min(Math.abs(ht - hp), 360 - Math.abs(ht - hp)) / 360; // Normalize hue distance + const ds = st - sp; + const dl = lt - lp; + const dist = dh * dh + ds * ds + dl * dl; + + if (dist < bestDist) { + bestDist = dist; + best = palette[i]; + if (bestDist === 0) break; + } + } + return best || [0, 0, 0]; + } + + // XYZ algorithm + if (algorithm === 'xyz') { + const [xt, yt, zt] = this._rgbToXyz(r, g, b); + let best = null; + let bestDist = Infinity; + + for (let i = 0; i < palette.length; i++) { + const [pr, pg, pb] = palette[i]; + const [xp, yp, zp] = this._rgbToXyz(pr, pg, pb); + const dx = xt - xp; + const dy = yt - yp; + const dz = zt - zp; + const dist = dx * dx + dy * dy + dz * dz; + + if (dist < bestDist) { + bestDist = dist; + best = palette[i]; + if (bestDist === 0) break; + } + } + return best || [0, 0, 0]; + } + + // LUV algorithm + if (algorithm === 'luv') { + const [lt, ut, vt] = this._rgbToLuv(r, g, b); + let best = null; + let bestDist = Infinity; + + for (let i = 0; i < palette.length; i++) { + const [pr, pg, pb] = palette[i]; + const [lp, up, vp] = this._rgbToLuv(pr, pg, pb); + const dl = lt - lp; + const du = ut - up; + const dv = vt - vp; + const dist = dl * dl + du * du + dv * dv; + + if (dist < bestDist) { + bestDist = dist; + best = palette[i]; + if (bestDist === 0) break; + } + } + return best || [0, 0, 0]; + } + + // YUV algorithm + if (algorithm === 'yuv') { + const [yt, ut, vt] = this._rgbToYuv(r, g, b); + let best = null; + let bestDist = Infinity; + + for (let i = 0; i < palette.length; i++) { + const [pr, pg, pb] = palette[i]; + const [yp, up, vp] = this._rgbToYuv(pr, pg, pb); + const dy = yt - yp; + const du = ut - up; + const dv = vt - vp; + const dist = dy * dy + du * du + dv * dv; + + if (dist < bestDist) { + bestDist = dist; + best = palette[i]; + if (bestDist === 0) break; + } + } + return best || [0, 0, 0]; + } + + // Oklab algorithm + if (algorithm === 'oklab') { + const [lt, at, bt] = this._rgbToOklab(r, g, b); + let best = null; + let bestDist = Infinity; + + for (let i = 0; i < palette.length; i++) { + const [pr, pg, pb] = palette[i]; + const [lp, ap, bp] = this._rgbToOklab(pr, pg, pb); + const dl = lt - lp; + const da = at - ap; + const db = bt - bp; + const dist = dl * dl + da * da + db * db; + + if (dist < bestDist) { + bestDist = dist; + best = palette[i]; + if (bestDist === 0) break; + } + } + return best || [0, 0, 0]; + } + + // LCH algorithm + if (algorithm === 'lch') { + const [Lt, at, bt] = this._getLab(r, g, b); + const [lt, ct, ht] = this._labToLch(Lt, at, bt); + let best = null; + let bestDist = Infinity; + + for (let i = 0; i < palette.length; i++) { + const [pr, pg, pb] = palette[i]; + const [Lp, ap, bp] = this._getLab(pr, pg, pb); + const [lp, cp, hp] = this._labToLch(Lp, ap, bp); + const dl = lt - lp; + const dc = ct - cp; + const dh = Math.min(Math.abs(ht - hp), 360 - Math.abs(ht - hp)); // Circular hue distance + const dist = dl * dl + dc * dc + (dh * dh) / 360; // Normalize hue + + if (dist < bestDist) { + bestDist = dist; + best = palette[i]; + if (bestDist === 0) break; + } + } + return best || [0, 0, 0]; + } + + // LAB algorithm (default) const [Lt, at, bt] = this._getLab(r, g, b); const targetChroma = Math.sqrt(at * at + bt * bt); let best = null; @@ -710,6 +1277,13 @@ class ImageProcessor { clearCaches() { this._colorCache.clear(); this._labCache.clear(); + this._hsvCache?.clear(); + this._hslCache?.clear(); + this._xyzCache?.clear(); + this._luvCache?.clear(); + this._yuvCache?.clear(); + this._oklabCache?.clear(); + this._lchCache?.clear(); this._ditherWorkBuf = null; this._ditherEligibleBuf = null; } @@ -738,4 +1312,4 @@ window.globalImageProcessor = new ImageProcessor(); // Legacy compatibility - expose key methods globally for backward compatibility window.ImageProcessor = ImageProcessor; -console.log('✅ WPlace Image Processor loaded and ready'); \ No newline at end of file +console.log('✅ WPlace Image Processor loaded and ready'); diff --git a/Extension/themes/acrylic.css b/Extension/themes/acrylic.css index b80032b..edee14c 100644 --- a/Extension/themes/acrylic.css +++ b/Extension/themes/acrylic.css @@ -779,4 +779,105 @@ .wplace-theme-acrylic .wplace-account-stats .fas.fa-tint { color: #00bfff; +} + +/* Move Artwork Panel Styles for Acrylic Theme */ +.wplace-theme-acrylic .wplace-move-panel { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--wplace-primary); + border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color); + border-radius: var(--wplace-radius); + padding: 24px; + z-index: 10000; + backdrop-filter: var(--wplace-backdrop); + min-width: 240px; + text-align: center; + font-family: var(--wplace-font); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.15); +} + +.wplace-theme-acrylic .wplace-move-panel-title { + color: var(--wplace-text); + margin: 0 0 20px 0; + font-size: 18px; + font-weight: 600; + letter-spacing: 0.5px; +} + +.wplace-theme-acrylic .wplace-move-controls { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 12px; + margin-bottom: 20px; +} + +.wplace-theme-acrylic .wplace-move-btn { + padding: 12px; + background: rgba(255, 255, 255, 0.2); + color: var(--wplace-text); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 12px; + cursor: pointer; + font-family: var(--wplace-font); + font-size: 16px; + font-weight: 600; + transition: all 0.3s ease; + backdrop-filter: blur(10px); +} + +.wplace-theme-acrylic .wplace-move-btn:hover { + background: rgba(255, 255, 255, 0.3); + border-color: rgba(255, 255, 255, 0.5); + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1); +} + +.wplace-theme-acrylic .wplace-move-btn:active { + transform: translateY(0); + background: rgba(255, 255, 255, 0.4); +} + +.wplace-theme-acrylic .wplace-move-center { + color: var(--wplace-text); + padding: 12px; + font-size: 14px; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + opacity: 0.8; +} + +.wplace-theme-acrylic .wplace-move-close-btn { + background: rgba(255, 59, 48, 0.8); + color: white; + border: 1px solid rgba(255, 59, 48, 0.3); + padding: 12px 20px; + border-radius: 12px; + cursor: pointer; + font-family: var(--wplace-font); + font-size: 15px; + font-weight: 600; + transition: all 0.3s ease; + backdrop-filter: blur(10px); +} + +.wplace-theme-acrylic .wplace-move-close-btn:hover { + background: rgba(255, 59, 48, 1); + transform: translateY(-1px); + box-shadow: 0 8px 20px rgba(255, 59, 48, 0.3); +} + +.wplace-theme-acrylic .wplace-move-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.4); + z-index: 9999; + backdrop-filter: blur(8px); } \ No newline at end of file diff --git a/Extension/themes/classic-light.css b/Extension/themes/classic-light.css index 742b9d9..5d2ecff 100644 --- a/Extension/themes/classic-light.css +++ b/Extension/themes/classic-light.css @@ -775,6 +775,101 @@ color: #2196f3; } +/* Move Artwork Panel Styles for Classic Light Theme */ +.wplace-theme-classic-light .wplace-move-panel { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--wplace-primary); + border: 2px solid var(--wplace-border-color); + border-radius: var(--wplace-radius); + padding: 20px; + z-index: 10000; + box-shadow: var(--wplace-shadow); + min-width: 200px; + text-align: center; + backdrop-filter: var(--wplace-backdrop); + font-family: var(--wplace-font); +} + +.wplace-theme-classic-light .wplace-move-panel-title { + color: var(--wplace-text); + margin: 0 0 15px 0; + font-size: 16px; + font-weight: 600; +} + +.wplace-theme-classic-light .wplace-move-controls { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 8px; + margin-bottom: 15px; +} + +.wplace-theme-classic-light .wplace-move-btn { + padding: 10px; + background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); + color: white; + border: 1px solid var(--wplace-border-color); + border-radius: var(--wplace-btn-radius); + cursor: pointer; + font-family: var(--wplace-font); + font-size: 14px; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgb(0 0 0 / 10%); +} + +.wplace-theme-classic-light .wplace-move-btn:hover { + background: linear-gradient(135deg, #45a049 0%, #4CAF50 100%); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgb(0 0 0 / 15%); +} + +.wplace-theme-classic-light .wplace-move-btn:active { + transform: translateY(0); + box-shadow: 0 2px 4px rgb(0 0 0 / 10%); +} + +.wplace-theme-classic-light .wplace-move-center { + color: var(--wplace-highlight); + padding: 10px; + font-size: 12px; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; +} + +.wplace-theme-classic-light .wplace-move-close-btn { + background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%); + color: white; + border: 1px solid var(--wplace-border-color); + padding: 10px 16px; + border-radius: var(--wplace-btn-radius); + cursor: pointer; + font-family: var(--wplace-font); + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgb(0 0 0 / 10%); +} + +.wplace-theme-classic-light .wplace-move-close-btn:hover { + background: linear-gradient(135deg, #d32f2f 0%, #f44336 100%); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgb(0 0 0 / 15%); +} + +.wplace-theme-classic-light .wplace-move-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 9999; + backdrop-filter: blur(4px); +} + diff --git a/Extension/themes/classic.css b/Extension/themes/classic.css index b20fd21..8aca765 100644 --- a/Extension/themes/classic.css +++ b/Extension/themes/classic.css @@ -921,3 +921,108 @@ color: #00bfff; } +/* Move Artwork Panel Styles for Classic Theme */ +:root .wplace-move-panel, +.wplace-theme-classic .wplace-move-panel { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--wplace-primary); + border: 2px solid rgb(255 255 255 / 20%); + border-radius: var(--wplace-radius); + padding: 20px; + z-index: 10000; + box-shadow: var(--wplace-shadow); + min-width: 200px; + text-align: center; + backdrop-filter: var(--wplace-backdrop); + font-family: var(--wplace-font); +} + +:root .wplace-move-panel-title, +.wplace-theme-classic .wplace-move-panel-title { + color: var(--wplace-text); + margin: 0 0 15px 0; + font-size: 16px; + text-shadow: 0 1px 2px rgb(0 0 0 / 50%); +} + +:root .wplace-move-controls, +.wplace-theme-classic .wplace-move-controls { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 8px; + margin-bottom: 15px; +} + +:root .wplace-move-btn, +.wplace-theme-classic .wplace-move-btn { + padding: 10px; + background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); + color: white; + border: 1px solid rgb(255 255 255 / 20%); + border-radius: var(--wplace-btn-radius); + cursor: pointer; + font-family: var(--wplace-font); + font-size: 14px; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgb(0 0 0 / 20%); +} + +:root .wplace-move-btn:hover, +.wplace-theme-classic .wplace-move-btn:hover { + background: linear-gradient(135deg, #45a049 0%, #4CAF50 100%); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgb(0 0 0 / 30%); +} + +:root .wplace-move-btn:active, +.wplace-theme-classic .wplace-move-btn:active { + transform: translateY(0); + box-shadow: 0 2px 4px rgb(0 0 0 / 20%); +} + +:root .wplace-move-center, +.wplace-theme-classic .wplace-move-center { + color: var(--wplace-highlight); + padding: 10px; + font-size: 12px; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; +} + +:root .wplace-move-close-btn, +.wplace-theme-classic .wplace-move-close-btn { + background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%); + color: white; + border: 1px solid rgb(255 255 255 / 20%); + padding: 10px 16px; + border-radius: var(--wplace-btn-radius); + cursor: pointer; + font-family: var(--wplace-font); + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgb(0 0 0 / 20%); +} + +:root .wplace-move-close-btn:hover, +.wplace-theme-classic .wplace-move-close-btn:hover { + background: linear-gradient(135deg, #d32f2f 0%, #f44336 100%); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgb(0 0 0 / 30%); +} + +:root .wplace-move-overlay, +.wplace-theme-classic .wplace-move-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + z-index: 9999; + backdrop-filter: blur(4px); +} + diff --git a/Extension/themes/neon-cyan.css b/Extension/themes/neon-cyan.css index 33f4858..6f6a155 100644 --- a/Extension/themes/neon-cyan.css +++ b/Extension/themes/neon-cyan.css @@ -701,3 +701,115 @@ color: #00ffff; text-shadow: 0 0 8px #00ffff; } + +/* Move Artwork Panel Styles for Neon Cyan Theme */ +.wplace-theme-neon-cyan .wplace-move-panel { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--wplace-primary) !important; + border: 2px solid var(--wplace-text) !important; + border-radius: 0 !important; + padding: 20px; + z-index: 10000; + box-shadow: 0 0 20px var(--wplace-text), inset 0 0 20px rgba(129, 220, 247, 0.1) !important; + min-width: 200px; + text-align: center; + font-family: var(--wplace-font) !important; + color: var(--wplace-text) !important; +} + +.wplace-theme-neon-cyan .wplace-move-panel-title { + color: var(--wplace-text) !important; + margin: 0 0 15px 0; + font-size: 12px !important; + text-transform: uppercase; + text-shadow: 0 0 10px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + animation: cyan-pulse 2s infinite; +} + +.wplace-theme-neon-cyan .wplace-move-controls { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 4px; + margin-bottom: 15px; +} + +.wplace-theme-neon-cyan .wplace-move-btn { + padding: 8px !important; + background: var(--wplace-secondary) !important; + color: var(--wplace-text) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + cursor: pointer; + font-family: var(--wplace-font) !important; + font-size: 10px !important; + text-shadow: 0 0 5px var(--wplace-text) !important; + transition: all 0.2s ease; + text-transform: uppercase; +} + +.wplace-theme-neon-cyan .wplace-move-btn:hover { + background: var(--wplace-accent) !important; + box-shadow: 0 0 15px var(--wplace-text) !important; + animation: cyan-flicker 0.5s infinite; +} + +.wplace-theme-neon-cyan .wplace-move-center { + color: var(--wplace-highlight) !important; + padding: 8px; + font-size: 9px !important; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + text-shadow: 0 0 5px var(--wplace-highlight) !important; + text-transform: uppercase; + font-family: var(--wplace-font) !important; +} + +.wplace-theme-neon-cyan .wplace-move-close-btn { + background: var(--wplace-error) !important; + color: white !important; + border: 1px solid var(--wplace-error) !important; + padding: 8px 12px; + border-radius: 0 !important; + cursor: pointer; + font-family: var(--wplace-font) !important; + font-size: 9px !important; + text-transform: uppercase; + text-shadow: 0 0 5px white !important; + transition: all 0.2s ease; +} + +.wplace-theme-neon-cyan .wplace-move-close-btn:hover { + box-shadow: 0 0 15px var(--wplace-error) !important; + animation: error-pulse 0.5s infinite; +} + +.wplace-theme-neon-cyan .wplace-move-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(25, 89, 161, 0.9) !important; + z-index: 9999; +} + +@keyframes cyan-pulse { + 0%, 100% { text-shadow: 0 0 5px var(--wplace-text), 0 0 10px var(--wplace-text); } + 50% { text-shadow: 0 0 10px var(--wplace-text), 0 0 20px var(--wplace-text); } +} + +@keyframes cyan-flicker { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0.8; } +} + +@keyframes error-pulse { + 0%, 100% { box-shadow: 0 0 5px var(--wplace-error); } + 50% { box-shadow: 0 0 15px var(--wplace-error); } +} diff --git a/Extension/themes/neon-light.css b/Extension/themes/neon-light.css index f5debd0..fbc2316 100644 --- a/Extension/themes/neon-light.css +++ b/Extension/themes/neon-light.css @@ -703,3 +703,179 @@ color: #203C5D; text-shadow: 0 0 5px #203C5D; } + +/* Move Artwork Panel */ +.wplace-theme-neon-light .wplace-move-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(32, 60, 93, 0.3); + backdrop-filter: blur(4px); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + animation: light-fade-in 0.3s ease-out; +} + +.wplace-theme-neon-light .wplace-move-panel { + background: var(--wplace-secondary); + border: 2px solid var(--wplace-accent); + border-radius: var(--wplace-radius); + padding: 20px; + box-shadow: var(--wplace-shadow), + 0 0 25px rgba(234, 156, 0, 0.6), + inset 0 0 15px rgba(234, 156, 0, 0.1); + color: var(--wplace-text); + font-family: var(--wplace-font); + text-align: center; + min-width: 200px; + animation: light-panel-pulse 2s ease-in-out infinite alternate; +} + +.wplace-theme-neon-light .wplace-move-panel h3 { + margin: 0 0 15px 0; + color: var(--wplace-accent); + text-shadow: 0 0 8px var(--wplace-accent); + font-size: 14px; + font-weight: bold; + letter-spacing: 1px; + text-transform: uppercase; +} + +.wplace-theme-neon-light .wplace-move-controls { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr 1fr; + gap: 8px; + width: 150px; + height: 150px; + margin: 0 auto 15px; +} + +.wplace-theme-neon-light .wplace-move-btn { + background: linear-gradient(135deg, var(--wplace-primary) 0%, var(--wplace-secondary) 100%); + border: 2px solid var(--wplace-accent); + border-radius: var(--wplace-radius); + color: var(--wplace-accent); + font-size: 16px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; + align-items: center; + justify-content: center; + text-shadow: 0 0 5px currentColor; + box-shadow: 0 0 10px rgba(234, 156, 0, 0.3); + font-family: var(--wplace-font); +} + +.wplace-theme-neon-light .wplace-move-btn:hover { + background: linear-gradient(135deg, var(--wplace-accent) 0%, var(--wplace-highlight) 100%); + color: var(--wplace-secondary); + text-shadow: 0 0 10px currentColor; + box-shadow: 0 0 20px rgba(234, 156, 0, 0.7), + inset 0 0 15px rgba(234, 156, 0, 0.2); + transform: scale(1.05); + animation: light-glow 0.5s ease-in-out; +} + +.wplace-theme-neon-light .wplace-move-btn:active { + transform: scale(0.95); + box-shadow: 0 0 15px rgba(234, 156, 0, 0.9), + inset 0 0 20px rgba(234, 156, 0, 0.3); +} + +.wplace-theme-neon-light .wplace-move-btn[data-direction="up"] { + grid-column: 2; + grid-row: 1; +} + +.wplace-theme-neon-light .wplace-move-btn[data-direction="left"] { + grid-column: 1; + grid-row: 2; +} + +.wplace-theme-neon-light .wplace-move-btn[data-direction="right"] { + grid-column: 3; + grid-row: 2; +} + +.wplace-theme-neon-light .wplace-move-btn[data-direction="down"] { + grid-column: 2; + grid-row: 3; +} + +.wplace-theme-neon-light .wplace-move-close { + background: linear-gradient(135deg, var(--wplace-error) 0%, #ff6b6b 100%); + border: 2px solid var(--wplace-error); + border-radius: var(--wplace-radius); + color: var(--wplace-secondary); + padding: 8px 16px; + font-size: 12px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s ease; + text-shadow: 0 0 5px currentColor; + box-shadow: 0 0 10px rgba(255, 7, 58, 0.3); + font-family: var(--wplace-font); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.wplace-theme-neon-light .wplace-move-close:hover { + background: linear-gradient(135deg, #ff6b6b 0%, var(--wplace-error) 100%); + box-shadow: 0 0 20px rgba(255, 7, 58, 0.7), + inset 0 0 10px rgba(255, 7, 58, 0.2); + transform: scale(1.05); + animation: light-error-pulse 0.5s ease-in-out; +} + +/* Neon Light Theme Animations */ +@keyframes light-fade-in { + from { + opacity: 0; + backdrop-filter: blur(0px); + } + to { + opacity: 1; + backdrop-filter: blur(4px); + } +} + +@keyframes light-panel-pulse { + 0% { + box-shadow: var(--wplace-shadow), + 0 0 25px rgba(234, 156, 0, 0.6), + inset 0 0 15px rgba(234, 156, 0, 0.1); + } + 100% { + box-shadow: var(--wplace-shadow), + 0 0 35px rgba(234, 156, 0, 0.8), + inset 0 0 20px rgba(234, 156, 0, 0.15); + } +} + +@keyframes light-glow { + 0%, 100% { + box-shadow: 0 0 20px rgba(234, 156, 0, 0.7), + inset 0 0 15px rgba(234, 156, 0, 0.2); + } + 50% { + box-shadow: 0 0 30px rgba(234, 156, 0, 0.9), + inset 0 0 25px rgba(234, 156, 0, 0.3); + } +} + +@keyframes light-error-pulse { + 0%, 100% { + box-shadow: 0 0 20px rgba(255, 7, 58, 0.7), + inset 0 0 10px rgba(255, 7, 58, 0.2); + } + 50% { + box-shadow: 0 0 30px rgba(255, 7, 58, 0.9), + inset 0 0 15px rgba(255, 7, 58, 0.3); + } +} diff --git a/Extension/themes/neon.css b/Extension/themes/neon.css index a5f0e4c..cad6dea 100644 --- a/Extension/themes/neon.css +++ b/Extension/themes/neon.css @@ -200,6 +200,115 @@ box-shadow: 0 0 15px var(--wplace-success) !important; } +/* Move Artwork Panel Styles for Neon Theme */ +.wplace-theme-neon .wplace-move-panel { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--wplace-primary) !important; + border: 2px solid var(--wplace-text) !important; + border-radius: 0 !important; + padding: 20px; + z-index: 10000; + box-shadow: 0 0 20px var(--wplace-text), inset 0 0 20px rgb(0 255 65 / 10%) !important; + min-width: 200px; + text-align: center; + font-family: var(--wplace-font) !important; + color: var(--wplace-text) !important; +} + +.wplace-theme-neon .wplace-move-panel-title { + color: var(--wplace-text) !important; + margin: 0 0 15px 0; + font-size: 12px !important; + text-transform: uppercase; + text-shadow: 0 0 5px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + animation: pixel-blink 2s infinite; +} + +.wplace-theme-neon .wplace-move-controls { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 4px; + margin-bottom: 15px; +} + +.wplace-theme-neon .wplace-move-btn { + padding: 8px !important; + background: var(--wplace-secondary) !important; + color: var(--wplace-text) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + cursor: pointer; + font-family: var(--wplace-font) !important; + font-size: 10px !important; + text-shadow: 0 0 5px var(--wplace-text) !important; + transition: all 0.2s ease; + text-transform: uppercase; +} + +.wplace-theme-neon .wplace-move-btn:hover { + background: var(--wplace-accent) !important; + box-shadow: 0 0 15px var(--wplace-text) !important; + animation: pixel-blink 0.5s infinite; +} + +.wplace-theme-neon .wplace-move-btn:active { + background: var(--wplace-text) !important; + color: var(--wplace-primary) !important; + box-shadow: 0 0 20px var(--wplace-text) !important; +} + +.wplace-theme-neon .wplace-move-center { + color: var(--wplace-highlight) !important; + padding: 8px; + font-size: 9px !important; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + text-shadow: 0 0 5px var(--wplace-highlight) !important; + text-transform: uppercase; + font-family: var(--wplace-font) !important; +} + +.wplace-theme-neon .wplace-move-close-btn { + background: var(--wplace-error) !important; + color: white !important; + border: 1px solid var(--wplace-error) !important; + padding: 8px 12px; + border-radius: 0 !important; + cursor: pointer; + font-family: var(--wplace-font) !important; + font-size: 9px !important; + text-transform: uppercase; + text-shadow: 0 0 5px white !important; + transition: all 0.2s ease; +} + +.wplace-theme-neon .wplace-move-close-btn:hover { + box-shadow: 0 0 15px var(--wplace-error) !important; + animation: pixel-blink 0.5s infinite; +} + +.wplace-theme-neon .wplace-move-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(26, 26, 46, 0.9) !important; + z-index: 9999; +} + +/* Neon theme animations */ +@keyframes pixel-blink { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0.7; } +} + .wplace-theme-neon #wplace-stats-container { background: var(--wplace-primary) !important; border: 2px solid var(--wplace-text) !important;