// Initialize the clock // Define Matrices const IDENTITY_MATRIX = [ [1, 0, 0], [0, 1, 0], [0, 0, 1] ]; const MODIFIED_MATRIX = [ [0.6, 0.3, 0.1], // Red: Strongly H, Medium M, Weak S [0.1, 0.6, 0.3], // Green: Weak H, Strongly M, Medium S [0.3, 0.1, 0.6] // Blue: Medium H, Weak M, Strongly S ]; const CLOCK_TYPES = { 'linear_crt': { class: CRTClock, matrix: IDENTITY_MATRIX, label: 'Linear CRT' }, 'modified_crt': { class: CRTClock, matrix: MODIFIED_MATRIX, label: 'Modified CRT' }, 'linear_cosine_crt': { class: CRTCosineClock, matrix: IDENTITY_MATRIX, label: 'Linear Cosine CRT' }, 'modified_cosine_crt': { class: CRTCosineClock, matrix: MODIFIED_MATRIX, label: 'Modified Cosine CRT' } }; let currentClockType = 'modified_cosine_crt'; // Default let clock = new CRTCosineClock(MODIFIED_MATRIX); const container = document.getElementById('boxContainer'); const clockSelect = document.getElementById('clockSelect'); const flushButton = document.getElementById('flushButton'); const infoPanel = document.getElementById('infoPanel'); const infoClock = document.getElementById('infoClock'); const infoTime = document.getElementById('infoTime'); const infoRGB = document.getElementById('infoRGB'); const colorPreview = document.getElementById('colorPreview'); const MAX_BOXES = 256; let zIndexCounter = 1; let isHovering = false; let pendingBoxes = []; function saveState() { const boxesData = []; for (let i = 0; i < container.children.length; i++) { const box = container.children[i]; boxesData.push({ time: box.dataset.time, rgb: box.dataset.rgb, clockinfo: box.dataset.clockinfo, zIndex: box.style.zIndex }); } const state = { boxes: boxesData, zIndexCounter: zIndexCounter }; localStorage.setItem('clockEngagementState', JSON.stringify(state)); } function loadState() { const savedState = localStorage.getItem('clockEngagementState'); if (savedState) { try { const state = JSON.parse(savedState); if (state.zIndexCounter) { zIndexCounter = state.zIndexCounter; } if (Array.isArray(state.boxes)) { state.boxes.forEach(data => { const box = document.createElement('div'); box.className = 'box'; box.style.backgroundColor = data.rgb; box.style.zIndex = data.zIndex; box.dataset.time = data.time; box.dataset.rgb = data.rgb; // Restore without animation for saved state box.classList.add('entered'); box.dataset.clockinfo = data.clockinfo; container.appendChild(box); }); updateLayout(); } } catch (e) { console.error("Failed to load state:", e); } } } function updateLayout() { // Dynamic compression logic const count = container.children.length; // Get current box size from CSS variable const boxSizePx = getComputedStyle(document.documentElement).getPropertyValue('--box-size').trim(); const boxSize = parseInt(boxSizePx, 10) || 300; let visibleWidth = 80; if (count > 10) { // Decrease visibility as count increases (basic linear interpolation) visibleWidth = Math.max(22, 80 - (1 * (count - 5))); } const overlap = -(boxSize - visibleWidth); container.style.setProperty('--card-overlap', `${overlap}px`); } function flushPendingBoxes() { if (pendingBoxes.length === 0) return; // Add all pending boxes while (pendingBoxes.length > 0) { const box = pendingBoxes.shift(); container.appendChild(box); // Force reflow and animate void box.offsetWidth; box.classList.add('entered'); } // Remove excess boxes while (container.children.length > MAX_BOXES) { container.removeChild(container.firstChild); } updateLayout(); saveState(); } container.addEventListener('mouseenter', () => { isHovering = true; }); container.addEventListener('mouseleave', () => { isHovering = false; infoPanel.classList.remove('visible'); // Hide info panel flushPendingBoxes(); }); // Event delegation for box hovering to show info container.addEventListener('mouseover', (e) => { const box = e.target.closest('.box'); if (box) { const time = box.dataset.time; const rgb = box.dataset.rgb; if (time && rgb) { infoTime.textContent = time; infoRGB.textContent = rgb; infoClock.textContent = box.dataset.clockinfo || "Unknown"; colorPreview.style.backgroundColor = rgb; infoPanel.classList.add('visible'); } } }); function createBox() { const box = document.createElement('div'); box.className = 'box'; // Calculate color based on current time const now = new Date(); const color = clock.transform(now); const r = Math.floor(color[0] * 255); const g = Math.floor(color[1] * 255); const b = Math.floor(color[2] * 255); const rgbStr = `rgb(${r}, ${g}, ${b})`; box.style.backgroundColor = rgbStr; // Store data for info panel box.dataset.time = now.toLocaleTimeString(); box.dataset.rgb = rgbStr; // Get label from CLOCK_TYPES using currentClockType let typeInfo = CLOCK_TYPES[currentClockType]; // Fallback if not found (should not happen normally) const typeLabel = typeInfo ? typeInfo.label : "Unknown Clock"; box.dataset.clockinfo = typeLabel; // Ensure the new box is visually on top of the older ones box.style.zIndex = zIndexCounter++; if (isHovering) { // Create it but don't show it yet (prevent visual shift) pendingBoxes.push(box); } else { // Append to the end (right side, newest) container.appendChild(box); // Trigger reflow to ensure the initial state is rendered before adding the class void box.offsetWidth; box.classList.add('entered'); // Limit the number of boxes if (container.children.length > MAX_BOXES) { container.removeChild(container.firstChild); } updateLayout(); saveState(); } } // Load saved state on startup loadState(); function setClockType(type) { const config = CLOCK_TYPES[type]; if (config) { currentClockType = type; // Instantiate the clock with the specific matrix clock = new config.class(config.matrix); clockSelect.value = type; localStorage.setItem('clockType', type); } } // Initialize clock selection from localStorage const savedClockType = localStorage.getItem('clockType'); // Validate if saved type exists in our new structure if (savedClockType && CLOCK_TYPES[savedClockType]) { setClockType(savedClockType); } else { // Default fallback setClockType('modified_cosine_crt'); } // Clock selection handler clockSelect.addEventListener('change', (e) => { setClockType(e.target.value); }); // Flush Button Logic flushButton.addEventListener('click', () => { // Confirm before flushing if there are many items if (container.children.length > 0) { // Clear DOM while (container.firstChild) { container.removeChild(container.firstChild); } // Clear pending boxes pendingBoxes = []; // Reset zIndex (optional, but cleaner) // zIndexCounter = 1; // Reset scroll currentOffset = 0; container.style.setProperty('--scroll-offset', '0px'); // Update stats and storage updateLayout(); saveState(); } }); // Swipe / Drag Logic let isDragging = false; let startX = 0; let currentOffset = 0; let initialDragOffset = 0; // Add event listeners to the container (or window/document if you want full screen drag) // Using window for smoother drag even if mouse leaves container window.addEventListener('mousedown', (e) => { // Only start drag if not clicking on the info panel if (e.target.closest('#infoPanel')) return; isDragging = true; startX = e.clientX; initialDragOffset = currentOffset; container.style.cursor = 'grabbing'; }); window.addEventListener('mousemove', (e) => { if (!isDragging) return; e.preventDefault(); // Prevent text selection etc const deltaX = e.clientX - startX; let newOffset = initialDragOffset + deltaX; // Bounds (optional but good) // Min offset 0 (cannot pull the newest card further left/center is looked) -> Actually prevent pull to left if (newOffset < 0) newOffset = 0; // Max offset? Rough estimate: Total width. // Just letting it be somewhat open for now or we can calculate. // Ideally we stop when the first card reaches center. // But let's keep it simple first. currentOffset = newOffset; container.style.setProperty('--scroll-offset', `${currentOffset}px`); }); window.addEventListener('mouseup', () => { isDragging = false; container.style.cursor = 'grab'; }); // Touch support for mobile window.addEventListener('touchstart', (e) => { if (e.target.closest('#infoPanel')) return; isDragging = true; startX = e.touches[0].clientX; initialDragOffset = currentOffset; }, { passive: false }); window.addEventListener('touchmove', (e) => { if (!isDragging) return; if (e.cancelable) e.preventDefault(); // Block browser scroll const deltaX = e.touches[0].clientX - startX; let newOffset = initialDragOffset + deltaX; if (newOffset < 0) newOffset = 0; currentOffset = newOffset; container.style.setProperty('--scroll-offset', `${currentOffset}px`); }); window.addEventListener('touchend', () => { isDragging = false; }); // Create a box every 1 second (1000ms) setInterval(createBox, 1000); // Initialize with one box immediately createBox();