let selectedPalette = []; let animationsEnabled = true; // --- LOCAL STORAGE LOADING --- // Load palette from Local Storage on startup const savedPaletteJSON = localStorage.getItem('userPalette'); if (savedPaletteJSON) { try { selectedPalette = JSON.parse(savedPaletteJSON); } catch (e) { console.error("Error parsing saved palette from Local Storage", e); selectedPalette = []; // Reset if data is corrupt } } // Main function to fetch color data and build the page fetch('colors.json') .then(response => response.json()) .then(data => { const colorFamiliesContainer = document.getElementById('color-families'); // Create the color swatch for each color in the JSON data data.forEach(family => { const familyDiv = document.createElement('div'); familyDiv.classList.add('color-family'); familyDiv.classList.add('has-text-dark'); familyDiv.innerHTML = `

${family.family}

`; const swatchContainer = document.createElement('div'); swatchContainer.classList.add('swatch-container'); family.colors.forEach(color => { const swatchWrapper = document.createElement('div'); swatchWrapper.classList.add('swatch-wrapper'); const swatch = document.createElement('div'); swatch.classList.add('color-swatch'); swatch.dataset.color = color.hex; swatch.style.color = color.hex; const backgroundDiv = document.createElement('div'); backgroundDiv.classList.add('color-background'); if (color.image) { backgroundDiv.style.backgroundImage = `url(${color.image})`; } else { backgroundDiv.style.backgroundColor = color.hex; } if (color.metallic && color.chromeType) { backgroundDiv.classList.add('metallic', `chrome-${color.chromeType}`); } const shineImg = document.createElement('img'); shineImg.classList.add('color-shine'); shineImg.src = "shine.svg"; shineImg.alt = ""; if (isLightColor(color.hex)) { backgroundDiv.style.border = '1px solid rgba(0, 0, 0, 0.2)'; shineImg.classList.add('has-halo'); } swatch.appendChild(backgroundDiv); swatch.appendChild(shineImg); const colorName = document.createElement('span'); colorName.classList.add('color-name'); colorName.textContent = color.name; // Event listener to add/remove a color from the palette swatch.addEventListener('click', () => { const isSelected = selectedPalette.some(c => c.hex === color.hex); if (isSelected) { selectedPalette = selectedPalette.filter(c => c.hex !== color.hex); } else { selectedPalette.push(color); } backgroundDiv.classList.add('pop'); backgroundDiv.addEventListener('animationend', () => { backgroundDiv.classList.remove('pop'); }, { once: true }); renderSelectedPalette(); updateSwatchHighlights(); }); swatchWrapper.appendChild(swatch); swatchWrapper.appendChild(colorName); swatchContainer.appendChild(swatchWrapper); }); familyDiv.appendChild(swatchContainer); colorFamiliesContainer.appendChild(familyDiv); }); // Initialize the palette, checking for shared links or local versions initializePaletteOnLoad(data); }) .catch(error => console.error('Error loading colors:', error)); /** * Saves the current 'selectedPalette' array to the browser's Local Storage. */ function savePaletteToLocalStorage() { localStorage.setItem('userPalette', JSON.stringify(selectedPalette)); } /** * Renders the selected colors as floating balloons in the top palette. */ function renderSelectedPalette() { const paletteColorsContainer = document.getElementById('palette-colors'); paletteColorsContainer.innerHTML = ''; selectedPalette.forEach(color => { const swatchWrapper = document.createElement('div'); swatchWrapper.classList.add('swatch-wrapper'); const floatGroup = document.createElement('div'); floatGroup.classList.add('balloon-float-group'); if (animationsEnabled) { floatGroup.style.animationDuration = `${(Math.random() * 3 + 3).toFixed(2)}s`; floatGroup.style.animationDelay = `${(Math.random() * 2).toFixed(2)}s`; } else { floatGroup.style.animation = 'none'; } const swatch = document.createElement('div'); swatch.classList.add('color-swatch'); swatch.dataset.color = color.hex; const backgroundDiv = document.createElement('div'); backgroundDiv.classList.add('color-background', 'chosen'); if (color.image) { backgroundDiv.style.backgroundImage = `url(${color.image})`; } else { backgroundDiv.style.backgroundColor = color.hex; } if (color.metallic && color.chromeType) { backgroundDiv.classList.add('metallic', `chrome-${color.chromeType}`); } const shineImg = document.createElement('img'); shineImg.classList.add('color-shine'); shineImg.src = "shine.svg"; shineImg.alt = ""; if (isLightColor(color.hex)) { backgroundDiv.style.border = '1px solid rgba(0, 0, 0, 0.2)'; shineImg.classList.add('has-halo'); } swatch.appendChild(backgroundDiv); swatch.appendChild(shineImg); const svgNS = "http://www.w3.org/2000/svg"; const stringSVG = document.createElementNS(svgNS, "svg"); stringSVG.setAttribute("class", "balloon-string-svg"); stringSVG.setAttribute("viewBox", "0 0 20 60"); const path = document.createElementNS(svgNS, "path"); path.setAttribute("d", "M10 0 C8 10, 12 20, 10 30 C8 40, 12 50, 10 60"); path.setAttribute("stroke", "#444"); path.setAttribute("stroke-width", "2"); path.setAttribute("fill", "none"); stringSVG.appendChild(path); floatGroup.appendChild(swatch); floatGroup.appendChild(stringSVG); swatchWrapper.appendChild(floatGroup); const colorName = document.createElement('span'); colorName.classList.add('color-name', 'highlighted-name'); colorName.textContent = color.name; swatchWrapper.appendChild(colorName); swatch.addEventListener('click', () => { selectedPalette = selectedPalette.filter(c => c.hex !== color.hex); renderSelectedPalette(); updateSwatchHighlights(); }); paletteColorsContainer.appendChild(swatchWrapper); }); savePaletteToLocalStorage(); } /** * Updates the visual highlight on the main color swatches to show which are selected. */ function updateSwatchHighlights() { const allSwatches = document.querySelectorAll('#color-families .color-swatch'); allSwatches.forEach(swatch => { const color = swatch.dataset.color; const background = swatch.querySelector('.color-background'); const nameEl = swatch.parentElement.querySelector('.color-name'); const isSelected = selectedPalette.some(c => c.hex === color); if (isSelected) { background.classList.add('chosen'); nameEl.classList.add('highlighted-name'); } else { background.classList.remove('chosen'); nameEl.classList.remove('highlighted-name'); } }); } /** * Determines if a hex color is light or dark to decide on border visibility. * @param {string} hex - The hex color code (e.g., "#FFFFFF"). * @returns {boolean} - True if the color is light, false otherwise. */ function isLightColor(hex) { if (!hex) return false; const rgb = hex.replace('#', '').match(/.{1,2}/g).map(x => parseInt(x, 16)); const brightness = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000; return brightness > 220; } /** * Shuffles an array in place. * @param {Array} array - The array to shuffle. * @returns {Array} - The shuffled array. */ function shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array; } /** * Initializes the palette on page load, prioritizing a new shared URL * over existing local storage, without overwriting it. * @param {Array} allColorData - The entire array of color families from colors.json. */ function initializePaletteOnLoad(allColorData) { const params = new URLSearchParams(window.location.search); const colorsFromURL = params.get('colors'); // Check if a 'colors' parameter exists in the URL if (colorsFromURL) { console.log("Loading palette from share link for this session..."); const flatColorList = allColorData.flatMap(family => family.colors); const hexCodesFromURL = colorsFromURL.split(','); const paletteFromURL = hexCodesFromURL.map(hex => { return flatColorList.find(color => color.hex === `#${hex}`); }).filter(Boolean); if (paletteFromURL.length > 0) { selectedPalette = paletteFromURL; // Load for viewing } // Clean the address bar but DO NOT save to local storage yet history.replaceState(null, '', window.location.pathname); } // If no URL parameter, the existing palette from local storage is used by default. else { console.log("Loading palette from Local Storage."); } // Render whatever palette was decided upon (from URL or local storage). // The render function itself will only save when the palette is changed. renderSelectedPalette(); updateSwatchHighlights(); } // --- Event Listeners --- document.getElementById('clear-palette').addEventListener('click', () => { selectedPalette = []; renderSelectedPalette(); updateSwatchHighlights(); }); document.getElementById('toggle-animation').addEventListener('click', (e) => { animationsEnabled = e.target.checked; renderSelectedPalette(); }); document.getElementById('shuffle-palette').addEventListener('click', () => { selectedPalette = shuffleArray(selectedPalette); renderSelectedPalette(); }); // --- Modal Functionality --- const shareButton = document.getElementById('share-palette'); const modalBackdrop = document.querySelector('.palette-modal-backdrop'); const closeModalButton = document.getElementById('close-modal'); const modalColorList = document.getElementById('modal-color-list'); shareButton.addEventListener('click', () => { if (selectedPalette.length === 0) { alert("Your palette is empty! Add some colors to create a shareable link."); return; } const baseURL = window.location.href.split('?')[0]; const colorParams = selectedPalette.map(color => color.hex.substring(1)).join(','); const shareableLink = `${baseURL}?colors=${colorParams}`; modalColorList.innerHTML = `

Copy this link to share your palette:

`; document.getElementById('copy-link-button').addEventListener('click', () => { const linkInput = document.getElementById('share-link-input'); navigator.clipboard.writeText(linkInput.value).then(() => { const copyButton = document.getElementById('copy-link-button'); copyButton.textContent = 'Copied! ✅'; setTimeout(() => { copyButton.textContent = 'Copy Link'; }, 2000); }).catch(err => { console.error('Failed to copy link: ', err); alert('Failed to copy link.'); }); }); modalBackdrop.style.display = 'flex'; }); closeModalButton.addEventListener('click', () => { modalBackdrop.style.display = 'none'; }); modalBackdrop.addEventListener('click', (event) => { if (event.target === modalBackdrop) { modalBackdrop.style.display = 'none'; } }); // --- Zoom Palette Functionality --- const zoomButton = document.getElementById('zoom-palette'); const zoomOverlay = document.getElementById('zoom-overlay'); const zoomedPaletteContent = document.getElementById('zoomed-palette-content'); // --- Event Listener to OPEN the zoom view --- zoomButton.addEventListener('click', () => { // Don't do anything if the palette is empty if (selectedPalette.length === 0) { alert("Palette is empty. Add some colors to zoom in!"); return; } // Get the original container of the colored balloons const originalPaletteColors = document.getElementById('palette-colors'); // Clear any old content and create a deep clone of the balloons zoomedPaletteContent.innerHTML = ''; const clonedContent = originalPaletteColors.cloneNode(true); // The clone doesn't need its own ID clonedContent.id = ''; // Append the cloned balloons to our zoom container zoomedPaletteContent.appendChild(clonedContent); // Show the overlay by adding the .is-active class zoomOverlay.classList.add('is-active'); }); // --- Function to CLOSE the zoom view --- function closeZoomView() { zoomOverlay.classList.remove('is-active'); } // --- Two ways to CLOSE the view for easy access --- // 1. Click on the dark background (the overlay itself) zoomOverlay.addEventListener('click', (event) => { // Only close if the click is on the overlay, not the content inside if (event.target === zoomOverlay) { closeZoomView(); } }); // 2. Press the 'Escape' key document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && zoomOverlay.classList.contains('is-active')) { closeZoomView(); } });