diff --git a/.gitignore b/.gitignore index 4bb941c..b1b9f8b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .venv *.pyc -*.pyi \ No newline at end of file +*.pyi + +__pycache__/ diff --git a/main.py b/main.py index 7816d9c..82e1b14 100644 --- a/main.py +++ b/main.py @@ -7,24 +7,11 @@ from modules.clocks import crt app = flask.Flask(__name__) -clock = crt.CRTClock(R=((1, 0, 0), (0, 1, 0), (0, 0, 1))) - @app.route("/") def index(): return render_template("index.html") -@app.route("/color") -def get_color(): - global clock - now = datetime.datetime.now() - color = clock.transform(now) - r = int(color[0] * 255) - g = int(color[1] * 255) - b = int(color[2] * 255) - return flask.jsonify({"color": f"rgb({r}, {g}, {b})"}) - - if __name__ == "__main__": app.run(debug=True) diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..473a0f4 diff --git a/static/js/clocks/clock.js b/static/js/clocks/clock.js new file mode 100644 index 0000000..1aa70b5 --- /dev/null +++ b/static/js/clocks/clock.js @@ -0,0 +1,61 @@ +class CClock { + transform(time) { + return time; + } +} + +class CRTClock extends CClock { + /** + * @param {number[][]} R + */ + constructor(R) { + super(); + this.R = R; + } + + /** + * @param {Date} time + * @returns {number[]} + */ + transform(time) { + const h = time.getHours() / 24.0; + const m = time.getMinutes() / 60.0; + const s = time.getSeconds() / 60.0; + + const ret = [ + h * this.R[0][0] + m * this.R[0][1] + s * this.R[0][2], + h * this.R[1][0] + m * this.R[1][1] + s * this.R[1][2], + h * this.R[2][0] + m * this.R[2][1] + s * this.R[2][2] + ]; + + return ret; + } +} + +class CRTCosineClock extends CRTClock { + /** + * @param {Date} time + * @returns {number[]} + */ + transform(time) { + // Calculate raw progress (0.0 ~ 1.0) + const hProg = (time.getHours() + time.getMinutes() / 60.0 + time.getSeconds() / 3600.0) / 24.0; + const mProg = (time.getMinutes() + time.getSeconds() / 60.0) / 60.0; + const sProg = (time.getSeconds() + time.getMilliseconds() / 1000.0) / 60.0; + + // Use Cosine wave (0 -> 1 -> 0) to ensure continuity at the cycle boundary (1.0 -> 0.0) + // Formula: 0.5 - 0.5 * cos(2 * PI * t) + const h = 0.5 - 0.5 * Math.cos(2 * Math.PI * hProg); + const m = 0.5 - 0.5 * Math.cos(2 * Math.PI * mProg); + const s = 0.5 - 0.5 * Math.cos(2 * Math.PI * sProg); + + // Matrix multiplication + const ret = [ + h * this.R[0][0] + m * this.R[0][1] + s * this.R[0][2], + h * this.R[1][0] + m * this.R[1][1] + s * this.R[1][2], + h * this.R[2][0] + m * this.R[2][1] + s * this.R[2][2] + ]; + + return ret; + } +} \ No newline at end of file diff --git a/static/js/operation.js b/static/js/operation.js new file mode 100644 index 0000000..5d234a9 --- /dev/null +++ b/static/js/operation.js @@ -0,0 +1,342 @@ +// 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(); diff --git a/templates/index.html b/templates/index.html index b3ef6d7..eb2de3f 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,45 +1,320 @@ +