// Desaturated (function() { // Override HTMLCanvasElement prototype to automatically set willReadFrequently const originalGetContext = HTMLCanvasElement.prototype.getContext; HTMLCanvasElement.prototype.getContext = function(contextType, contextAttributes) { if (contextType === '2d') { contextAttributes = contextAttributes || {}; contextAttributes.willReadFrequently = true; } return originalGetContext.call(this, contextType, contextAttributes); }; // For p5.js specific handling (belt and suspenders approach) if (typeof p5 !== 'undefined') { const originalCreateCanvas = p5.prototype.createCanvas; p5.prototype.createCanvas = function() { const canvas = originalCreateCanvas.apply(this, arguments); if (canvas && canvas.elt) { canvas.elt.setAttribute('willReadFrequently', 'true'); } return canvas; }; const originalCreateGraphics = p5.prototype.createGraphics; p5.prototype.createGraphics = function() { const graphics = originalCreateGraphics.apply(this, arguments); if (graphics && graphics.elt) { graphics.elt.setAttribute('willReadFrequently', 'true'); } return graphics; }; } })(); function generateNewSeed() { // Generate a random 8-character string const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < 8; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; } class SeededRandom { constructor(seed) { this.seed = typeof seed === 'string' ? seed.split('').reduce((a, b) => (a * 21 + b.charCodeAt(0)) >>> 0, 0) : seed; } random(min, max) { let t = this.seed += 0x6D2B79F5; t = Math.imul(t ^ t >>> 15, t | 1); t ^= t + Math.imul(t ^ t >>> 7, t | 61); const r = ((t ^ t >>> 14) >>> 0) / 4294967296; // Handle array input (like p5.js does) if (Array.isArray(min)) { return min[Math.floor(r * min.length)]; } // Handle number inputs if (min === undefined) return r; if (max === undefined) return r * min; return min + r * (max - min); } } const SKIP_PRERENDER = false; const BITCOIN = { DEFAULT_BLOCK_TIME: 600, // 10 minutes in seconds MAX_BLOCK_TIME: 3600, // Maximum time to scale from (1 hour) POLLING_INTERVAL: 30000, // Poll for new data every 30 seconds ANIMATION_SPEED: { MULTIPLIERS: [ { threshold: 600, value: 1.0 }, // 0-10 minutes: normal speed { threshold: 1200, value: 1.4 }, // 10-20 minutes: 1.4x speed { threshold: 1800, value: 1.8 }, // 20-30 minutes: 1.8x speed { threshold: 2400, value: 2.2 }, // 30-40 minutes: 2.2x speed { threshold: 3000, value: 2.6 }, // 40-50 minutes: 2.6x speed { threshold: Infinity, value: 3.0 } // 50+ minutes: 3.0x speed ] } }; let bitcoinData = { blockTime: BITCOIN.DEFAULT_BLOCK_TIME, // Current block time in seconds speedMultiplier: 1.0 // Calculated speed multiplier }; // =============== Saved Image Brightness Settings =============== const SAVE_SETTINGS = { BORDER: { BRIGHTNESS_SCALE: 0.85, OPACITY_SCALE: 0.85 }, DATASTREAM: { BRIGHTNESS_SCALE: 1.15, OPACITY_SCALE: 1.2 }, BACKGROUND: { INVERSE_BRIGHTNESS_SCALE: 1 // For inverse mode background } }; // =============== Basic Mode Constants =============== const WAVE_ORIGIN_STYLES = { ASYMMETRICAL: "Asymmetrical", SYMMETRICAL: "Symmetrical" }; const EXTREME_COLOR_MODES = { NORMAL: "Normal", DUAL_HUE_GRADIENT: "Dual Hue Gradient", DUAL_SOLID: "Dual Solid" }; const AMPLITUDE_MODES = { NORMAL: "Normal", EXTREME: { TYPE: "Extreme", COLOR_MODE: EXTREME_COLOR_MODES.NORMAL } }; const AMPLITUDE_RANGES = { NORMAL: { MIN: 0.10, MAX: 0.45 }, EXTREME: { TALL: { MIN: 0.30, MAX: 0.45 }, SHORT: { MIN: 0.10, MAX: 0.20 } } }; // =============== Canvas & Display Constants =============== const CANVAS_DIMENSIONS = { LANDSCAPE: { WIDTH: 2200, HEIGHT: 1650 }, PORTRAIT: { WIDTH: 1650, HEIGHT: 2200 } }; const ORIGINAL_ALPHA_VALUES = { STANDARD: 75, FULL: 100, FAINT: 30, LEADING_FULL: 75, LEADING_FAINT: 45, FADE_MAX_NORMAL: 150, FADE_MAX_FAINT: 90 }; const INCREASED_ALPHA_VALUES = { STANDARD: 150, // Increased from 75 FULL: 200, // Increased from 100 FAINT: 60, // Increased from 30 LEADING_FULL: 75, // Keeping original LEADING_FAINT: 45, // Keeping original FADE_MAX_NORMAL: 150, // Keeping original FADE_MAX_FAINT: 90 // Keeping original }; let ALPHA_VALUES = ORIGINAL_ALPHA_VALUES; // =============== Color Probability Configuration =============== const DUAL_MODE = { GRADIENT: "GRADIENT", LAYERED: "LAYERED" }; const COLOR_PROBABILITY_CONFIG = { COLOR_WEIGHTS: { RAINBOW: 0, GRADIENT: 0, SINGLE_COLOR: 0, DUAL: 100, MONOCHROME: 0, INVERSE: 0 }, GRADIENT_SUBTRAITS: { VARIABLE_SATURATION: 1, FIXED_SATURATION: 1 }, SINGLE_COLOR_SUBTRAITS: { SOLID: 1, GRADIENT: 1 }, EXTREME_COLOR_MODE_WEIGHTS: { NORMAL: 1, DUAL_HUE_GRADIENT: 1, DUAL_SOLID: 1 }, DUAL_MODE_WEIGHTS: { GRADIENT: 50, LAYERED: 50 } }; // =============== Color Constants =============== const DIRECTIONAL_STOP_POINTS = { ORIGINAL: [0.45, 0.5, 0.55, 0.6, 0.65, 0.70, 0.75], TILTED: [0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.80, 0.85, 0.90, 0.95], SYMMETRICAL: [0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.80, 0.85, 0.90, 0.95, 1.0] }; const DENSITY_VALUES = Array.from({ length: 12 }, (_, i) => (0.25 + i * 0.05).toFixed(3)); const PROJECT_COLOR_SETS = { // =============== Pastel & Muted Color Sets =============== SOFT_LAVENDER_PEACH: [ { h: 260, s: 40, b: 95 }, // Soft Lavender { h: 30, s: 40, b: 95 } // Peach ], MINT_POWDER_BLUE: [ { h: 150, s: 35, b: 90 }, // Mint { h: 210, s: 35, b: 95 } // Powder Blue ], BLUSH_SKY: [ { h: 350, s: 45, b: 95 }, // Blush Pink { h: 195, s: 45, b: 95 } // Sky Blue ], SAGE_ROSE: [ { h: 130, s: 30, b: 85 }, // Sage Green { h: 340, s: 35, b: 90 } // Dusty Rose ], PALE_YELLOW_PERIWINKLE: [ { h: 55, s: 40, b: 95 }, // Pale Yellow { h: 230, s: 40, b: 90 } // Periwinkle ], CORAL_SEAFOAM: [ { h: 10, s: 45, b: 90 }, // Soft Coral { h: 160, s: 35, b: 85 } // Seafoam ], LILAC_TURQUOISE: [ { h: 280, s: 35, b: 90 }, // Lilac { h: 175, s: 40, b: 85 } // Pale Turquoise ], DUSTY_BLUE_GOLD: [ { h: 220, s: 35, b: 80 }, // Dusty Blue { h: 45, s: 50, b: 85 } // Muted Gold ], MAUVE_TEAL: [ { h: 300, s: 25, b: 85 }, // Soft Mauve { h: 180, s: 30, b: 80 } // Pale Teal ], BUTTERCREAM_PINK: [ { h: 40, s: 30, b: 95 }, // Buttercream { h: 330, s: 25, b: 90 } // Powder Pink ], MISTY_VIOLET: [ { h: 210, s: 10, b: 85 }, // Misty Grey { h: 270, s: 30, b: 90 } // Pale Violet ], RASPBERRY_AQUA: [ { h: 340, s: 45, b: 80 }, // Muted Raspberry { h: 190, s: 35, b: 85 } // Dusty Aqua ], OLIVE_BLUSH: [ { h: 80, s: 25, b: 80 }, // Pale Olive { h: 0, s: 30, b: 90 } // Soft Blush ], LAVENDER_CANTALOUPE: [ { h: 270, s: 30, b: 80 }, // Dusty Lavender { h: 30, s: 35, b: 90 } // Pale Cantaloupe ], ICE_PEACH: [ { h: 200, s: 25, b: 95 }, // Ice Blue { h: 25, s: 40, b: 90 } // Muted Peach ], ROSE_QUARTZ_SERENITY: [ { h: 350, s: 25, b: 90 }, // Rose Quartz { h: 230, s: 30, b: 90 } // Serenity Blue ], PISTACHIO_BLUSH: [ { h: 90, s: 35, b: 85 }, // Pistachio { h: 350, s: 30, b: 90 } // Blush ], TERRACOTTA_DENIM: [ { h: 20, s: 50, b: 80 }, // Muted Terracotta { h: 210, s: 30, b: 85 } // Pale Denim ], MOSS_PINK: [ { h: 110, s: 20, b: 85 }, // Pale Moss { h: 340, s: 25, b: 90 } // Dusty Pink ], CORNFLOWER_APRICOT: [ { h: 225, s: 35, b: 90 }, // Light Cornflower { h: 30, s: 40, b: 85 } // Muted Apricot ], // =============== Rich & Sophisticated Sets =============== BURGUNDY_NAVY: [ { h: 345, s: 75, b: 45 }, // Deep Burgundy { h: 220, s: 70, b: 35 } // Navy Blue ], FOREST_AMBER: [ { h: 140, s: 65, b: 40 }, // Forest Green { h: 35, s: 90, b: 60 } // Amber ], CHARCOAL_RUST: [ { h: 200, s: 15, b: 25 }, // Charcoal { h: 15, s: 75, b: 50 } // Rust ], PLUM_ORANGE: [ { h: 280, s: 50, b: 45 }, // Plum { h: 25, s: 85, b: 55 } // Burnt Orange ], // =============== Earthy & Natural Sets =============== TERRACOTTA_MOSS: [ { h: 15, s: 70, b: 55 }, // Terracotta { h: 95, s: 45, b: 45 } // Mossy Green ], MUSTARD_INDIGO: [ { h: 45, s: 85, b: 70 }, // Mustard { h: 250, s: 50, b: 35 } // Indigo ], OLIVE_DUSTY_ROSE: [ { h: 80, s: 60, b: 45 }, // Olive { h: 350, s: 40, b: 65 } // Dusty Rose ], COFFEE_TEAL: [ { h: 30, s: 65, b: 35 }, // Coffee Brown { h: 185, s: 70, b: 45 } // Teal ], // =============== Muted with Pop Sets =============== SLATE_CORAL: [ { h: 210, s: 20, b: 55 }, // Slate Grey { h: 15, s: 90, b: 80 } // Coral ], MUTED_TEAL_YELLOW: [ { h: 180, s: 45, b: 50 }, // Muted Teal { h: 50, s: 95, b: 90 } // Vibrant Yellow ], MIDNIGHT_MAGENTA: [ { h: 240, s: 60, b: 25 }, // Deep Midnight { h: 320, s: 90, b: 80 } // Bright Magenta ], DARK_OLIVE_ELECTRIC_BLUE: [ { h: 85, s: 50, b: 40 }, // Dark Olive { h: 200, s: 85, b: 75 } // Electric Blue ], // =============== Vintage & Retro Sets =============== SEPIA_DENIM: [ { h: 35, s: 60, b: 45 }, // Sepia { h: 210, s: 35, b: 60 } // Faded Denim ], MAUVE_MUSTARD: [ { h: 300, s: 35, b: 65 }, // Dusty Mauve { h: 45, s: 80, b: 70 } // Mustard ], SIENNA_TEAL: [ { h: 20, s: 75, b: 55 }, // Burnt Sienna { h: 190, s: 60, b: 50 } // Teal Blue ], AVOCADO_DUSTY_PINK: [ { h: 95, s: 70, b: 45 }, // Avocado Green { h: 340, s: 40, b: 70 } // Dusty Pink ], // =============== Jewel Tones Sets =============== EMERALD_RUBY: [ { h: 140, s: 80, b: 50 }, // Emerald Green { h: 350, s: 85, b: 55 } // Ruby Red ], SAPPHIRE_AMETHYST: [ { h: 220, s: 85, b: 50 }, // Sapphire Blue { h: 275, s: 70, b: 55 } // Amethyst ], GARNET_GOLD: [ { h: 0, s: 80, b: 45 }, // Garnet { h: 45, s: 85, b: 65 } // Gold ], JADE_PURPLE: [ { h: 165, s: 60, b: 50 }, // Jade { h: 280, s: 70, b: 40 } // Deep Purple ], // =============== Monochromatic with Depth Sets =============== DEEP_LIGHT_TEAL: [ { h: 180, s: 80, b: 30 }, // Deep Teal { h: 180, s: 45, b: 75 } // Light Teal ], BLOOD_ROSE: [ { h: 0, s: 95, b: 40 }, // Blood Red { h: 350, s: 65, b: 75 } // Rose ], NAVY_SKY: [ { h: 220, s: 85, b: 30 }, // Navy { h: 205, s: 55, b: 80 } // Sky Blue ], FOREST_MINT: [ { h: 140, s: 85, b: 30 }, // Forest { h: 150, s: 45, b: 85 } // Mint ], // =============== Modern & Contemporary Sets =============== CHARCOAL_ACID: [ { h: 210, s: 15, b: 25 }, // Charcoal { h: 90, s: 85, b: 80 } // Acid Green ], MERLOT_COPPER: [ { h: 335, s: 80, b: 40 }, // Merlot { h: 30, s: 70, b: 60 } // Copper ], SMOKE_FLAME: [ { h: 210, s: 10, b: 65 }, // Smoke Grey { h: 20, s: 90, b: 65 } // Flame Orange ], TURQUOISE_SAND: [ { h: 175, s: 70, b: 45 }, // Deep Turquoise { h: 40, s: 55, b: 75 } // Warm Sand ] }; const DUAL_COLOR_PAIRS = PROJECT_COLOR_SETS; const COLOR_DEFAULTS = { SATURATION: 80, BRIGHTNESS: { MIN: 60, // Changed from 40 to 60 MAX: 100, STEPS: 5 // All brightness values will be in steps of 5 }, HUE_STEP: 1, GRADIENT_HUE_STEP: 0.5 }; const SINGLE_COLOR_GRADIENT = { SATURATION: { START: 50, END: 100 }, BRIGHTNESS: { START: 60, // Normalized to match other modes END: 100 }, HUE: { RANGE: 30, RATE: 2.0 }, RATE: 0.3 // Adjusted for smoother transitions }; const GRADIENT_DEFAULTS = { SATURATION: { FIXED: 80, // Default saturation for fixed mode VARIABLE: { START: 50, END: 100, RATE: 0.5 // Controls saturation transition speed } }, HUE_STEP: 0.5, // Moved from COLOR_DEFAULTS for better organization BRIGHTNESS: { MIN: 40, MAX: 100, STEPS: 5 // Ensure brightness values align with other modes }, ALPHA: { STANDARD: 75, FULL: 100 } }; const WAVE_HUE_RANGES = { DARK: [ { min: 201, max: 260 }, // Blue { min: 261, max: 290 }, // Purple { min: 291, max: 349 }, // Magenta { min: 350, max: 10 } // Red ], LIGHT: [ { min: 11, max: 40 }, // Orange { min: 41, max: 70 }, // Yellow { min: 71, max: 150 }, // Green { min: 151, max: 200 } // Cyan ] }; // =============== Border System Constants =============== const BORDER_COLORS = { MONOCHROME: "white", DATASTREAM: { WHITE: "white", GRADIENT: "gradient", WAVE: "wave" }, BLACK: { WHITE: "white", GRADIENT: "gradient", WAVE: "wave" } }; const BORDER_COLOR_WEIGHTS = { DATASTREAM: { WHITE: 20, GRADIENT: 40, WAVE: 40 }, BLACK: { WHITE: 20, GRADIENT: 40, WAVE: 40 } }; const BORDER_OPTIONS = { THIN: "Thin", NONE: "None" // Add this line }; const BORDER_APPEARANCE = { OPACITY: { SOLID: 95, GRADIENT: 95, WAVE: 95 }, BRIGHTNESS: { HIGH: 95, GRADIENT: 95, WAVE: 95 } }; const BORDER_SIZES = { THIN: 1.25, // Base border thickness THIN_MARGIN: 15, // Base margin from edge MIN_WEIGHT: 1.25, // Minimum stroke weight to prevent disappearing borders }; // =============== DataStream Constants =============== const DATA_STREAM_COLORS = { grey: { h: 0, s: 0, b: 30 }, // Pure greyscale - keep this for monochrome mode partial: { h: 0, s: 0, b: 15 } // New partial brightness option }; const DATASTREAM_CONSTANTS = { MIN_SPEED: 8, MAX_SPEED: 14, MIN_SIZE: 10, MAX_SIZE: 75, DELAY_MAX: 200, CHAR_SPEEDS: [0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 15, 20], BRIGHTNESS: { FULL: 30, PARTIAL: 10 } }; // =============== Other Constants =============== const TILT_ANGLE = 20 * (Math.PI / 180); const GRADIENT_CONSTANTS = { SEGMENTS_PER_SIDE: 180 }; const HUE_COLOR_NAMES = { RED: { min: 350, max: 10 }, ORANGE: { min: 11, max: 40 }, YELLOW: { min: 41, max: 70 }, GREEN: { min: 71, max: 150 }, CYAN: { min: 151, max: 200 }, BLUE: { min: 201, max: 260 }, PURPLE: { min: 261, max: 290 }, MAGENTA: { min: 291, max: 349 } }; const DEPTH_MAPPING = { ENABLED: "Enabled", DISABLED: "Disabled", AMPLITUDE_THRESHOLD: { MIN: 0.10, MAX: 0.30 }, OPACITY_RANGE: { BOOST: 100, // Maximum opacity for overlaps REDUCE: 0 // Minimum opacity for non-overlaps (changed from 0) } }; const FADE_SETTINGS = { START_DELAY: 1000, // Delay before fade starts (ms) DURATION: 3000, // How long fade lasts (ms) MIN_ALPHA: 0 // Minimum alpha value }; const BufferPoolConfig = { CLEANUP_INTERVAL: 2000, MAX_UNUSED_AGE: 5000 }; const BufferPool = { // Store buffers with their configurations buffers: new Map(), // Get or create a buffer with specified dimensions getBuffer(width, height, key = 'default') { const bufferKey = `${key}-${width}-${height}`; if (!this.buffers.has(bufferKey)) { const buffer = createGraphics(width, height); buffer.pixelDensity(1); buffer.colorMode(HSB, 360, 100, 100, 100); this.buffers.set(bufferKey, { graphics: buffer, lastUsed: Date.now(), inUse: false }); } const bufferData = this.buffers.get(bufferKey); bufferData.lastUsed = Date.now(); bufferData.inUse = true; return bufferData.graphics; }, // Release a buffer after use releaseBuffer(width, height, key = 'default') { const bufferKey = `${key}-${width}-${height}`; if (this.buffers.has(bufferKey)) { const bufferData = this.buffers.get(bufferKey); bufferData.inUse = false; bufferData.lastUsed = Date.now(); } }, // Clear unused buffers older than specified age (in milliseconds) cleanup(maxAge = BufferPoolConfig.MAX_UNUSED_AGE) { const now = Date.now(); for (const [key, data] of this.buffers.entries()) { if (!data.inUse && (now - data.lastUsed > maxAge)) { data.graphics.remove(); this.buffers.delete(key); } } }, // Clear all buffers reset() { for (const [_, data] of this.buffers.entries()) { data.graphics.remove(); } this.buffers.clear(); } }; const depthSampleCache = new Map(); const originalTimeEnd = console.timeEnd; console.time = function(label) { console[`_timerStart${label}`] = performance.now(); }; console.timeEnd = function(label) { const duration = performance.now() - console[`_timerStart${label}`]; console.log(`${label}: ${(duration / 1000).toFixed(3)}s`); }; const BUTTON_WIDTH = '46px'; const BUTTON_HEIGHT = '36px'; const BUTTON_PADDING = '8px 15px'; const RF_FREQUENCY_TYPES = [ { value: "HF", weight: 30 }, { value: "VHF", weight: 25 }, { value: "UHF", weight: 20 }, { value: "SHF", weight: 15 }, { value: "EHF", weight: 7 }, { value: "THF", weight: 3 } ]; const ENCRYPTED_AUDIO_TYPES = [ { value: "Distorted", weight: 35 }, { value: "Granular", weight: 30 }, { value: "Mysterious", weight: 20 }, { value: "Tonal", weight: 10 }, { value: "Disruption", weight: 5 } ]; const SONIFICATION_AUDIO_TYPES = [ { value: "Chandra Deep Field", weight: 30 }, { value: "Chandra-B1509-15-52", weight: 25 }, { value: "Chandra-IC-433", weight: 20 }, { value: "M51", weight: 15 }, { value: "M51-UV", weight: 7 }, { value: "Black Hole", weight: 3 } ]; // =============== Global Variables =============== let col = []; let scale = 14; let originalWidth, originalHeight; let canvasContainer; let dataType = "None"; let sineWaves = []; let currentWaveCount; let waveBuffer; let trailBuffer; let waveCountTrait; let colorTrait; let colorSubTrait; let densityTrait; let waveTypeTrait; let backgroundTrait; let scaleRatio = 1; let baseWidth = 4096; let baseHeight = 3072; let hasDataStream = false; let dataStreamColor; let fadeBuffer; let highResBuffer; let borderColor; let waveColorHistory = []; let depthMappingTrait; let isAnimating = false; let staticBuffer; let borderTrait; let waveJourneys = []; let currentJourneyFrame = 0; let lastFrameTime = 0; const FRAME_INTERVAL = 1000 / 60; // 60fps let isCapturingJourney = false; let cachedBorderWaveColor = null; let isPreRenderComplete = false; let isPaused = false; let animationFrameId = null; let currentFrame = 0; let audio; let isAudioPlaying = false; let menuButton; let menuContainer; let menuOpen = false; let playPauseButton; let stopButton; let restartButton; let volumeSlider; let exportPNGButton; let originalTrailBuffer; let originalFadeBuffer; let originalWaveStates; let rfFrequencyTrait; let encryptedAudioTrait; let sonificationAudioTrait; let START_AT_EDGE; class WaveJourney { constructor() { this.frames = []; this.totalFrames = 0; this.colorPair = null; // Missing } addFrame({ x, y, hue, saturation, brightness, opacity, period, amplitude, phase, gradientProgress, wavePoints = [], colorPairName = null }) { this.frames.push({ x, y, hue, saturation, brightness, opacity, period, amplitude, phase, gradientProgress, colorPairName, wavePoints: wavePoints.map(point => ({ ...point, depthValue: point.depthValue || null })) }); this.totalFrames++; } getFrame(index) { return this.frames[index]; } reset() { this.frames = []; this.totalFrames = 0; this.colorPair = null; // Missing } } class WaveType { static ORIGINAL = 'Original'; static TILTED = 'Tilted'; constructor(type) { this.type = type; } static random() { return new WaveType(random([this.ORIGINAL, this.TILTED])); } } class Orientation { static LEFT = 'Left'; static RIGHT = 'Right'; static TOP = 'Top'; static CENTER = 'Center'; constructor(type) { this.type = type; } static randomHorizontal() { return new Orientation(random([this.LEFT, this.RIGHT])); } static randomSymmetrical() { return new Orientation(random([this.TOP, this.CENTER])); } } class Char { constructor() { this.changeChar(); this.speed = floor(random(DATASTREAM_CONSTANTS.CHAR_SPEEDS)); this.value = this.generateValue(); } changeChar() { if (frameCount % this.speed == 0) { this.value = this.generateValue(); } } generateValue() { const symbols = [ 'Ͽ', 'Ͼ', 'Ξ', 'Σ', 'Δ', 'Φ', '⌬', '⍉', '⍕', '⍡', '⎍', '⏃', '◊', '☉', '⚎', '⚏', '⚍', "⌇", "⌖", "⍚", "⍜", "⍊", "⚸", "☍", "⟆", "⟊", "⟓", "⟔" ]; return random(symbols); } } class Column { constructor(x, startDelay) { this.x = x; this.string = []; this.speed = int(random(DATASTREAM_CONSTANTS.MIN_SPEED, DATASTREAM_CONSTANTS.MAX_SPEED)); this.size = int(random(DATASTREAM_CONSTANTS.MIN_SIZE, DATASTREAM_CONSTANTS.MAX_SIZE)); this.startDelay = startDelay; this.frozen = false; this.frozenChars = []; this.scale = scale; // Initialize with characters for (let i = 0; i < this.size; i++) { this.string.push({ char: new Char(), y: i }); } } getColor(y, isLeading, opacity, forSave = false) { // Start with default values const h = dataStreamColor.h || 0; const s = dataStreamColor.s || 0; const b = dataStreamColor.b || 30; // Get the scaling factors based on color mode const opacityScale = forSave ? SAVE_SETTINGS.DATASTREAM.OPACITY_SCALE : 1; const brightnessScale = forSave ? SAVE_SETTINGS.DATASTREAM.BRIGHTNESS_SCALE : 1; if (colorTrait === "Inverse") { let fadeAlpha = map(y, 0, this.size, 0, ALPHA_VALUES.FADE_MAX_NORMAL); const displayOpacity = opacity * 0.20; // Reduced opacity for inverse mode const scaledBrightness = b * brightnessScale; return color(0, 0, scaledBrightness, isLeading ? ALPHA_VALUES.LEADING_FULL * opacity * opacityScale * 0.5 : fadeAlpha * displayOpacity); } // Handle normal color mode let fadeAlpha = this.size > 0 ? map(y || 0, 0, this.size, 0, ALPHA_VALUES.FADE_MAX_NORMAL) : ALPHA_VALUES.FADE_MAX_NORMAL; const displayOpacity = opacity * 0.3 * opacityScale; const scaledBrightness = b * brightnessScale; return color(h, s, scaledBrightness, isLeading ? ALPHA_VALUES.LEADING_FULL * opacity * opacityScale : fadeAlpha * displayOpacity); } jump() { if (this.frozen || !hasDataStream) return; if (frameCount > this.startDelay && frameCount % this.speed == 0) { this.string.push({ char: new Char(), y: this.string.length }); if (this.string.length * scale > height + this.size * scale) { this.string = []; this.speed = int(random(DATASTREAM_CONSTANTS.MIN_SPEED, DATASTREAM_CONSTANTS.MAX_SPEED)); this.size = int(random(DATASTREAM_CONSTANTS.MIN_SIZE, DATASTREAM_CONSTANTS.MAX_SIZE)); this.startDelay = frameCount + random(0, DATASTREAM_CONSTANTS.DELAY_MAX); } } } freeze() { this.frozen = true; this.frozenChars = this.string.map(item => ({ value: item.char.value, y: item.y })); } show(buffer, opacity, forSave = false) { if (!hasDataStream) return; const currentScale = forSave ? (scale * 4) : scale; if (this.frozen) { buffer.textAlign(CENTER, CENTER); this.frozenChars.forEach((char, index) => { if (char && char.value) { let isLeading = (index === this.frozenChars.length - 1); let streamColor = this.getColor(index, isLeading, opacity, forSave); buffer.fill(streamColor); const xPos = this.x * currentScale + (buffer.width % currentScale) / 2; const yPos = (index + 1) * currentScale; buffer.text(char.value, xPos, yPos); } }); return; } if (frameCount > this.startDelay && this.string.length > 0) { buffer.textAlign(CENTER, CENTER); this.string.forEach((item, index) => { if (item && item.char && item.char.value) { let isLeading = (index === this.string.length - 1); let streamColor = this.getColor(index, isLeading, opacity, forSave); buffer.fill(streamColor); const xPos = this.x * currentScale + (buffer.width % currentScale) / 2; const yPos = (index + 1) * currentScale; buffer.text(item.char.value, xPos, yPos); item.char.changeChar(); } }); } } } class SineWave { constructor(amplitude, period, phase, speed, initialHue, initialBrightness, orientation, saturation = COLOR_DEFAULTS.SATURATION) { const validation = validateWaveParameters({ amplitude, period, phase, speed, initialHue, initialBrightness, orientation, saturation }); if (!validation.isValid) { console.error("Wave parameter validation failed:", validation.errors); throw new Error(`Invalid wave parameters: ${validation.errors.join(", ")}`); } this.amplitude = amplitude; this.period = period; this.phase = phase; this.speed = speed; this.orientation = orientation; this.waveType = waveTypeTrait; this.addedToTrail = false; this.waveOriginStyle = window.waveOriginStyle; this.stopped = false; this.baseHue = initialHue; this.hue = initialHue; this.saturation = saturation; this.brightness = initialBrightness; if (colorTrait === "Inverse") { const isDarkWave = random() < 0.5; if (isDarkWave) { this.brightness = random(15, 35); // Very dark range this.opacity = 95; // High opacity for dark waves } else { this.brightness = random(60, 75); // Light gray range this.opacity = 75; // Lower opacity for light waves } } else if (colorTrait === "Monochrome") { const isDarkWave = random() < 0.5; if (isDarkWave) { this.brightness = random(55, 65); // Darker range but not too dark this.opacity = 85; // High but not as high as inverse } else { this.brightness = random(70, 85); // Lighter range this.opacity = 65; // More subtle than inverse } } else { this.opacity = parseFloat(densityTrait) < 0.5 ? 255 : // Maximum opacity for thin lines (random() < 0.5 ? 180 : 220); this.brightness = initialBrightness; } const stopPoints = this.waveType === WaveType.TILTED ? DIRECTIONAL_STOP_POINTS.TILTED : (this.waveOriginStyle === WAVE_ORIGIN_STYLES.SYMMETRICAL ? DIRECTIONAL_STOP_POINTS.SYMMETRICAL : DIRECTIONAL_STOP_POINTS.ORIGINAL); this.stopProgress = random(stopPoints); if (this.waveOriginStyle === WAVE_ORIGIN_STYLES.SYMMETRICAL) { switch (this.orientation.type) { case Orientation.TOP: this.x = width * random(0.15, 0.25); this.y = height; break; case Orientation.CENTER: this.x = width * random(0.75, 0.85); this.y = height; break; default: this.x = width * 0.2; this.y = height; } } else { // Asymmetrical wave initialization switch (this.orientation.type) { case Orientation.LEFT: this.x = START_AT_EDGE ? 0 : -(this.amplitude * 1.5); this.y = height; break; case Orientation.RIGHT: this.x = START_AT_EDGE ? width : width + (this.amplitude * 1.5); this.y = height; break; default: this.x = START_AT_EDGE ? 0 : -(this.amplitude * 1.5); this.y = height; } } } update() { if (this.stopped) return; let progress = this.getProgress(); if (progress >= this.stopProgress) { this.stopped = true; return; } this.phase += 0.05; if (this.waveOriginStyle === WAVE_ORIGIN_STYLES.SYMMETRICAL) { if (this.waveType === WaveType.ORIGINAL) { switch (this.orientation.type) { case Orientation.TOP: this.x += this.speed; this.y -= this.speed; break; case Orientation.CENTER: this.x -= this.speed; this.y -= this.speed; break; } } else { const angle = this.orientation.type === Orientation.TOP ? -TILT_ANGLE : TILT_ANGLE; const dx = this.speed * Math.cos(angle); const dy = this.speed; switch (this.orientation.type) { case Orientation.TOP: this.x += dx; this.y -= dy; break; case Orientation.CENTER: this.x -= dx; this.y -= dy; break; } } } else { if (this.waveType === WaveType.ORIGINAL) { switch (this.orientation.type) { case Orientation.LEFT: this.x += this.speed; this.y -= this.speed; break; case Orientation.RIGHT: this.x -= this.speed; this.y -= this.speed; break; } } else { const angle = this.orientation.type === Orientation.LEFT ? -TILT_ANGLE : TILT_ANGLE; const dx = this.speed * Math.cos(angle); const dy = this.speed; switch (this.orientation.type) { case Orientation.LEFT: this.x += dx; this.y -= dy; break; case Orientation.RIGHT: this.x -= dx; this.y -= dy; break; } } } this.updateColor(progress); } getProgress() { if (this.waveOriginStyle === WAVE_ORIGIN_STYLES.SYMMETRICAL) { const visibleRange = 0.75; // 75% of canvas width for consistent gradient const startPoint = 0.15; // Start at 15% of canvas switch (this.orientation.type) { case Orientation.TOP: return Math.max(0, Math.min(1, (this.x - width * startPoint) / (width * visibleRange) )); case Orientation.CENTER: return Math.max(0, Math.min(1, 1 - ((this.x - width * startPoint) / (width * visibleRange)) )); default: return 0; } } else { // For asymmetrical waves, calculate progress from canvas edges switch (this.orientation.type) { case Orientation.LEFT: // 0% at left edge (x=0), 100% at right edge (x=width) return Math.max(0, Math.min(1, this.x / width)); case Orientation.RIGHT: // 0% at right edge (x=width), 100% at left edge (x=0) if (this.x > width) return 0; // Still approaching canvas return Math.max(0, Math.min(1, (width - this.x) / width)); default: return 0; } } } evaluate(pos) { let basePos = pos / scaleRatio; let result = this.amplitude * sin((TWO_PI / this.period) * basePos + this.phase); if (this.waveType === WaveType.TILTED) { let angle; if (this.waveOriginStyle === WAVE_ORIGIN_STYLES.SYMMETRICAL) { angle = this.orientation.type === Orientation.TOP ? -TILT_ANGLE : TILT_ANGLE; } else { angle = this.orientation.type === Orientation.LEFT ? -TILT_ANGLE : TILT_ANGLE; } result = result * cos(angle); } return result * scaleRatio; } updateColor(progress) { if (window.extremeColorMode === EXTREME_COLOR_MODES.DUAL_SOLID) return; if (window.extremeColorMode === EXTREME_COLOR_MODES.DUAL_HUE_GRADIENT) { this.updateDualHueGradient(progress); } else if (colorTrait === "Rainbow") { // Ensure constant color change during wave movement this.hue = (this.baseHue + (frameCount * COLOR_DEFAULTS.HUE_STEP)) % 360; } else if (colorTrait === "Dual") { // Use our updated function updateDualColor(this, progress); } else if (colorTrait === "Single Color" && this.colorSubTrait === "Gradient") { this.updateSingleColorGradient(progress); } else if (colorTrait === "Gradient") { this.hue = (this.hue + COLOR_DEFAULTS.GRADIENT_HUE_STEP) % 360; if (colorSubTrait === "Variable Saturation") { this.updateGradientSaturation(progress); } } } updateDualColor(progress) { if (isNaN(progress)) return; const pairKeys = Object.keys(DUAL_COLOR_PAIRS); const selectedPair = DUAL_COLOR_PAIRS[pairKeys.find(key => DUAL_COLOR_PAIRS[key].name === colorSubTrait)]; if (!selectedPair) return; const { start, end } = selectedPair; // Calculate normalized progress for symmetrical waves let visibleProgress; if (this.waveOriginStyle === WAVE_ORIGIN_STYLES.SYMMETRICAL) { if (this.orientation.type === Orientation.TOP) { // Calculate actual canvas traversal for TOP orientation const canvasStart = width * 0.15; // Wave starts at 15% of canvas const canvasEnd = width * 0.85; // Wave ends at 85% of canvas const traversalWidth = canvasEnd - canvasStart; visibleProgress = (this.x - canvasStart) / traversalWidth; } else { // CENTER // Calculate actual canvas traversal for CENTER orientation const canvasStart = width * 0.85; const canvasEnd = width * 0.15; const traversalWidth = canvasStart - canvasEnd; visibleProgress = 1 - ((this.x - canvasEnd) / traversalWidth); } } else { // Original asymmetrical calculation remains unchanged if (this.orientation.type === Orientation.LEFT) { visibleProgress = this.x / width; } else { // RIGHT visibleProgress = 1 - (this.x / width); } } // Clamp progress to 0-1 range visibleProgress = Math.max(0, Math.min(1, visibleProgress)); // Calculate shortest path for hue interpolation let hueDiff = end.h - start.h; if (Math.abs(hueDiff) > 180) { hueDiff = hueDiff > 0 ? hueDiff - 360 : hueDiff + 360; } // Apply color interpolation this.hue = (start.h + hueDiff * visibleProgress + 360) % 360; this.saturation = start.s + (end.s - start.s) * visibleProgress; this.brightness = start.b + (end.b - start.b) * visibleProgress; } updateSingleColorGradient(progress) { if (isNaN(progress)) return; const hueOffset = sin(progress * TWO_PI * SINGLE_COLOR_GRADIENT.HUE.RATE) * SINGLE_COLOR_GRADIENT.HUE.RANGE; this.hue = (this.baseHue + hueOffset + 360) % 360; const adjustedProgress = Math.pow(progress, SINGLE_COLOR_GRADIENT.RATE); this.saturation = map( adjustedProgress, 0, 1, SINGLE_COLOR_GRADIENT.SATURATION.START, SINGLE_COLOR_GRADIENT.SATURATION.END ); this.brightness = round(map( adjustedProgress, 0, 1, SINGLE_COLOR_GRADIENT.BRIGHTNESS.START, SINGLE_COLOR_GRADIENT.BRIGHTNESS.END ) / 5) * 5; } updateDualHueGradient(progress) { if (isNaN(progress)) return; const hueRange = 45; const hueOffset = sin(progress * TWO_PI * 1.5) * hueRange; this.hue = (this.baseHue + hueOffset + 360) % 360; this.saturation = COLOR_DEFAULTS.SATURATION; const brightnessVariation = sin(progress * TWO_PI) * 5; this.brightness = constrain( this.brightness + brightnessVariation, COLOR_DEFAULTS.BRIGHTNESS.MIN, COLOR_DEFAULTS.BRIGHTNESS.MAX ); } updateGradientSaturation(progress) { if (isNaN(progress)) return; const adjustedProgress = Math.pow(progress, SINGLE_COLOR_GRADIENT.RATE); this.saturation = map( adjustedProgress, 0, 1, SINGLE_COLOR_GRADIENT.SATURATION.START, SINGLE_COLOR_GRADIENT.SATURATION.END ); } isOutOfBounds() { if (!this.orientation) return true; if (this.waveOriginStyle === WAVE_ORIGIN_STYLES.SYMMETRICAL) { return this.y < 0 || (this.orientation.type === Orientation.TOP && this.x > width * 0.85) || (this.orientation.type === Orientation.CENTER && this.x < width * 0.15); } else { return this.y < 0 || (this.orientation.type === Orientation.LEFT && this.x - this.amplitude > width) || (this.orientation.type === Orientation.RIGHT && this.x + this.amplitude < 0); } } } SineWave.prototype.update = function() { if (this.stopped) return; // Save original speed const originalSpeed = this.speed; // Apply Bitcoin-based speed multiplier temporarily this.speed *= bitcoinData.speedMultiplier; let progress = this.getProgress(); if (progress >= this.stopProgress) { this.stopped = true; return; } this.phase += 0.05; if (this.waveOriginStyle === WAVE_ORIGIN_STYLES.SYMMETRICAL) { if (this.waveType === WaveType.ORIGINAL) { switch (this.orientation.type) { case Orientation.TOP: this.x += this.speed; this.y -= this.speed; break; case Orientation.CENTER: this.x -= this.speed; this.y -= this.speed; break; } } else { const angle = this.orientation.type === Orientation.TOP ? -TILT_ANGLE : TILT_ANGLE; const dx = this.speed * Math.cos(angle); const dy = this.speed; switch (this.orientation.type) { case Orientation.TOP: this.x += dx; this.y -= dy; break; case Orientation.CENTER: this.x -= dx; this.y -= dy; break; } } } else { if (this.waveType === WaveType.ORIGINAL) { switch (this.orientation.type) { case Orientation.LEFT: this.x += this.speed; this.y -= this.speed; break; case Orientation.RIGHT: this.x -= this.speed; this.y -= this.speed; break; } } else { const angle = this.orientation.type === Orientation.LEFT ? -TILT_ANGLE : TILT_ANGLE; const dx = this.speed * Math.cos(angle); const dy = this.speed; switch (this.orientation.type) { case Orientation.LEFT: this.x += dx; this.y -= dy; break; case Orientation.RIGHT: this.x -= dx; this.y -= dy; break; } } } this.updateColor(progress); // Restore original speed for future updates this.speed = originalSpeed; }; function isMobileDevice() { return /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); } function selectProjectColorSet() { // Get all available project keys const projectKeys = Object.keys(PROJECT_COLOR_SETS); // Randomly select a project const selectedProject = projectKeys[Math.floor(random() * projectKeys.length)]; // Get the color set from the selected project const colorSet = PROJECT_COLOR_SETS[selectedProject]; // We need to randomly select two colors from the set const colorIndices = []; const numColors = colorSet.length; // Select first color index const firstIndex = Math.floor(random() * numColors); colorIndices.push(firstIndex); // Select second color index (ensuring it's different from the first) let secondIndex; do { secondIndex = Math.floor(random() * numColors); } while (secondIndex === firstIndex && numColors > 1); colorIndices.push(secondIndex); // Get the selected colors and normalize their brightness const start = {...colorSet[colorIndices[0]]}; const end = {...colorSet[colorIndices[1]]}; // Ensure minimum brightness start.b = Math.max(70, start.b); // Raise colors below 70% brightness end.b = Math.max(70, end.b); // Raise colors below 70% brightness // Return the project name and selected colors return { name: selectedProject, start: start, end: end }; } function validateGradientProgress(wave) { const progress = wave.getProgress(); const x = wave.x; const gradientRange = {min: 0, max: 1}; // Check if progress is within valid range if (progress < gradientRange.min || progress > gradientRange.max) { console.warn(`Invalid gradient progress: ${progress} at x=${x}`); return false; } // Check if progress matches expected direction if (wave.orientation.type === Orientation.LEFT && x > 0 && progress === 0) { console.warn(`Left wave missing start gradient at x=${x}`); return false; } if (wave.orientation.type === Orientation.RIGHT && x < width && progress === 1) { console.warn(`Right wave missing end gradient at x=${x}`); return false; } return true; } function selectFromWeights(weights) { const total = Object.values(weights).reduce((sum, weight) => sum + weight, 0); const rand = random() * total; let cumulative = 0; for (const [key, weight] of Object.entries(weights)) { cumulative += weight; if (rand <= cumulative) return key; } return Object.keys(weights)[0]; // Fallback } function directColorBlend(startColor, endColor, progress, densityValue = null) { // Convert HSB to RGB for both colors const startRGB = hsbToRgb(startColor.h, startColor.s, startColor.b); const endRGB = hsbToRgb(endColor.h, endColor.s, endColor.b); // Linear interpolation in RGB space const r = startRGB[0] + (endRGB[0] - startRGB[0]) * progress; const g = startRGB[1] + (endRGB[1] - startRGB[1]) * progress; const b = startRGB[2] + (endRGB[2] - startRGB[2]) * progress; // Convert back to HSB const result = rgbToHsb(r, g, b); // Apply density-aware brightness boost if (densityValue !== null) { // Convert density string to float if needed const density = typeof densityValue === 'string' ? parseFloat(densityValue) : densityValue; // Apply inverse density boost - thinner lines (lower density) get more brightness const densityFactor = 1 - (Math.min(0.7, density) / 0.7); // 0 to 1 scale, max at lowest density const brightnessBoost = 15 * densityFactor; // Up to 15 points brightness boost for thinnest lines // Apply the boost result.b = Math.min(100, result.b + brightnessBoost); } // Also still apply the regular mid-transition brightness boost if (progress > 0.1 && progress < 0.9) { const midBoost = 10 * Math.sin(progress * Math.PI); // Max at progress=0.5 result.b = Math.min(100, result.b + midBoost); } return result; } function hsbToRgb(h, s, b) { h = h % 360; s = s / 100; b = b / 100; const c = b * s; const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); const m = b - c; let r, g, b1; if (h >= 0 && h < 60) { [r, g, b1] = [c, x, 0]; } else if (h >= 60 && h < 120) { [r, g, b1] = [x, c, 0]; } else if (h >= 120 && h < 180) { [r, g, b1] = [0, c, x]; } else if (h >= 180 && h < 240) { [r, g, b1] = [0, x, c]; } else if (h >= 240 && h < 300) { [r, g, b1] = [x, 0, c]; } else { [r, g, b1] = [c, 0, x]; } return [ Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b1 + m) * 255) ]; } function rgbToHsb(r, g, b) { r /= 255; g /= 255; b /= 255; const max = Math.max(r, g, b); const min = Math.min(r, g, b); const d = max - min; let h = 0; const s = max === 0 ? 0 : d / max; const v = max; if (max !== min) { switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return { h: Math.round(h * 360), s: Math.round(s * 100), b: Math.round(v * 100) }; } function interpolateColors(startColor, endColor, progress) { // Use direct RGB blending for all color interpolation return directColorBlend(startColor, endColor, progress); } function lerpHue(h1, h2, t) { // Simple wrapper around directColorBlend for hue-only interpolation // Create dummy colors with same saturation and brightness const result = directColorBlend( {h: h1, s: 100, b: 100}, {h: h2, s: 100, b: 100}, t ); return result.h; } function getHueFromRange(ranges) { const selectedRange = random(ranges); if (selectedRange.min > selectedRange.max) { // Handle colors that wrap around 360 (like red) return random() < 0.5 ? random(selectedRange.min, 360) : random(0, selectedRange.max); } return random(selectedRange.min, selectedRange.max); } function getContrastingHue(hue) { // Normalize hue to 0-360 range hue = (hue + 360) % 360; // Get complementary hue (180 degrees opposite) let complementary = (hue + 180) % 360; // Add some variation (+/- 30 degrees) let variation = random(-30, 30); return (complementary + variation + 360) % 360; } function validateWaveParameters(params) { const { amplitude, period, phase, speed, initialHue, initialBrightness, orientation, saturation } = params; const errors = []; // Required parameter checks if (amplitude === undefined || amplitude === null) { errors.push("Amplitude is required"); } else if (typeof amplitude !== 'number' || amplitude <= 0) { errors.push("Amplitude must be a positive number"); } if (period === undefined || period === null) { errors.push("Period is required"); } else if (typeof period !== 'number' || period <= 0) { errors.push("Period must be a positive number"); } if (phase === undefined || phase === null) { errors.push("Phase is required"); } else if (typeof phase !== 'number') { errors.push("Phase must be a number"); } if (speed === undefined || speed === null) { errors.push("Speed is required"); } else if (typeof speed !== 'number' || speed <= 0) { errors.push("Speed must be a positive number"); } if (!orientation || typeof orientation !== 'object') { errors.push("Orientation object is required"); } else if (!orientation.type || !Object.values(Orientation).includes(orientation.type)) { errors.push("Invalid orientation type"); } // Optional parameter checks with defaults if (initialHue !== undefined && (typeof initialHue !== 'number' || initialHue < 0 || initialHue > 360)) { errors.push("Initial hue must be between 0 and 360"); } if (initialBrightness !== undefined && (typeof initialBrightness !== 'number' || initialBrightness < COLOR_DEFAULTS.BRIGHTNESS.MIN || initialBrightness > COLOR_DEFAULTS.BRIGHTNESS.MAX)) { errors.push(`Brightness must be between ${COLOR_DEFAULTS.BRIGHTNESS.MIN} and ${COLOR_DEFAULTS.BRIGHTNESS.MAX}`); } if (saturation !== undefined && (typeof saturation !== 'number' || saturation < 0 || saturation > 100)) { errors.push("Saturation must be between 0 and 100"); } return { isValid: errors.length === 0, errors }; } function validateWaveCreationParameters(count, orientation, commonHue, commonBrightness) { const errors = []; // Validate wave count if (!count || typeof count !== 'number' || count < 1 || count > 4) { errors.push("Wave count must be between 1 and 4"); } // Validate orientation if (!orientation || typeof orientation !== 'object') { errors.push("Invalid orientation object"); } else if (!orientation.type || !Object.values(Orientation).includes(orientation.type)) { errors.push("Invalid orientation type"); } // Validate common hue if (commonHue !== undefined && (typeof commonHue !== 'number' || commonHue < 0 || commonHue > 360)) { errors.push("Common hue must be between 0 and 360"); } // Validate common brightness if (commonBrightness !== undefined && (typeof commonBrightness !== 'number' || commonBrightness < COLOR_DEFAULTS.BRIGHTNESS.MIN || commonBrightness > COLOR_DEFAULTS.BRIGHTNESS.MAX)) { errors.push(`Common brightness must be between ${COLOR_DEFAULTS.BRIGHTNESS.MIN} and ${COLOR_DEFAULTS.BRIGHTNESS.MAX}`); } return { isValid: errors.length === 0, errors }; } function calculateProbabilities(weights) { const total = Object.values(weights).reduce((sum, weight) => sum + weight, 0); const probabilities = {}; let cumulative = 0; for (const [key, weight] of Object.entries(weights)) { cumulative += (weight / total) * 100; probabilities[key] = cumulative; } return probabilities; } function selectFromWeights(weights) { const total = Object.values(weights).reduce((sum, weight) => sum + weight, 0); const rand = random() * total; let cumulative = 0; for (const [key, weight] of Object.entries(weights)) { cumulative += weight; if (rand <= cumulative) return key; } return Object.keys(weights)[0]; // Fallback } function calculateDimensions() { const canvasType = random([0, 1]); const sizeVariant = 0.45; if (canvasType === 0) { // Landscape scaleRatio = (CANVAS_DIMENSIONS.LANDSCAPE.WIDTH / baseWidth) * sizeVariant; window.canvasType = "Landscape"; return { width: Math.floor(CANVAS_DIMENSIONS.LANDSCAPE.WIDTH * sizeVariant), height: Math.floor(CANVAS_DIMENSIONS.LANDSCAPE.HEIGHT * sizeVariant) }; } else { // Portrait scaleRatio = (CANVAS_DIMENSIONS.PORTRAIT.HEIGHT / baseHeight) * sizeVariant; window.canvasType = "Portrait"; return { width: Math.floor(CANVAS_DIMENSIONS.PORTRAIT.WIDTH * sizeVariant), height: Math.floor(CANVAS_DIMENSIONS.PORTRAIT.HEIGHT * sizeVariant) }; } } function renderStaticElements(buffer) { // Set background color buffer.background(colorTrait === "Inverse" ? color(0, 0, 97) : 0); // Draw frozen datastream if present if (backgroundTrait === "Datastream") { buffer.textFont('monospace'); buffer.textSize(scale); drawDataStream(buffer, 1, false); } // Draw border if present if (borderTrait === BORDER_OPTIONS.THIN) { drawBorder(buffer); } } function calculateWavePosition(wave, pos, scale = 1) { let x, y; y = (height * scale) - pos; if (wave.waveType === WaveType.ORIGINAL) { x = wave.evaluate(pos) + wave.x; } else { // For tilted waves - ensure all orientations behave consistently const absAngle = TILT_ANGLE; const tiltOffset = pos * Math.sin(absAngle); // Always positive if (wave.orientation.type === Orientation.LEFT) { // For LEFT waves, SUBTRACT the offset (curves toward center) x = wave.evaluate(pos) + wave.x - tiltOffset; } else if (wave.orientation.type === Orientation.RIGHT) { // For RIGHT waves, ADD the offset (curves toward center) x = wave.evaluate(pos) + wave.x + tiltOffset; } else if (wave.orientation.type === Orientation.TOP) { // For TOP waves (from left side), ADD the offset (curves right) x = wave.evaluate(pos) + wave.x + tiltOffset; } else if (wave.orientation.type === Orientation.CENTER) { // For CENTER waves (from right side), SUBTRACT the offset (curves left) x = wave.evaluate(pos) + wave.x - tiltOffset; } } return { x, y }; } function getRandomHue() { const colorRanges = Object.values(HUE_COLOR_NAMES); const selectedRange = random(colorRanges); if (selectedRange.min > selectedRange.max) { // Handle colors that wrap around 360 (like red) return random() < 0.5 ? random(selectedRange.min, 360) : random(0, selectedRange.max); } return random(selectedRange.min, selectedRange.max); } function getNormalizedBrightness(min = COLOR_DEFAULTS.BRIGHTNESS.MIN, max = COLOR_DEFAULTS.BRIGHTNESS.MAX) { const steps = (max - min) / COLOR_DEFAULTS.BRIGHTNESS.STEPS; return min + floor(random(steps + 1)) * COLOR_DEFAULTS.BRIGHTNESS.STEPS; } function getColorNameFromHue(hue) { // Normalize hue to 0-360 range hue = (hue + 360) % 360; // Special case for red which wraps around 360 if (hue >= HUE_COLOR_NAMES.RED.min || hue <= HUE_COLOR_NAMES.RED.max) { return "Red"; } // Check other colors for (const [colorName, range] of Object.entries(HUE_COLOR_NAMES)) { if (hue >= range.min && hue <= range.max) { return colorName.charAt(0) + colorName.slice(1).toLowerCase(); } } return "Unknown"; // Fallback } function updateDualColor(wave, progress) { if (isNaN(progress)) return; // Get the selected color pair from the window object or fall back to selecting a new one const selectedPair = window.currentProjectColors || selectProjectColorSet(); if (!selectedPair) return; const { start, end } = selectedPair; // Calculate normalized progress for symmetrical waves let visibleProgress; if (wave.waveOriginStyle === WAVE_ORIGIN_STYLES.SYMMETRICAL) { if (wave.orientation.type === Orientation.TOP) { // Calculate actual canvas traversal for TOP orientation const canvasStart = width * 0.15; // Wave starts at 15% of canvas const canvasEnd = width * 0.85; // Wave ends at 85% of canvas const traversalWidth = canvasEnd - canvasStart; visibleProgress = (wave.x - canvasStart) / traversalWidth; } else { // CENTER // Calculate actual canvas traversal for CENTER orientation const canvasStart = width * 0.85; const canvasEnd = width * 0.15; const traversalWidth = canvasStart - canvasEnd; visibleProgress = 1 - ((wave.x - canvasEnd) / traversalWidth); } } else { // Original asymmetrical calculation remains unchanged if (wave.orientation.type === Orientation.LEFT) { visibleProgress = wave.x / width; } else { // RIGHT visibleProgress = 1 - (wave.x / width); } } // Clamp progress to 0-1 range visibleProgress = Math.max(0, Math.min(1, visibleProgress)); // Calculate shortest path for hue interpolation const blendedColor = directColorBlend(start, end, visibleProgress); wave.hue = blendedColor.h; wave.saturation = blendedColor.s; wave.brightness = blendedColor.b; } function selectColorTraits() { const mainColorProbs = calculateProbabilities(COLOR_PROBABILITY_CONFIG.COLOR_WEIGHTS); const selectedTrait = selectFromWeights(COLOR_PROBABILITY_CONFIG.COLOR_WEIGHTS); let colorTrait, colorSubTrait; switch(selectedTrait) { case 'RAINBOW': colorTrait = "Rainbow"; break; case 'GRADIENT': colorTrait = "Gradient"; colorSubTrait = selectFromWeights(COLOR_PROBABILITY_CONFIG.GRADIENT_SUBTRAITS) === 'VARIABLE_SATURATION' ? "Variable Saturation" : "Fixed Saturation"; break; case 'SINGLE_COLOR': colorTrait = "Single Color"; colorSubTrait = selectFromWeights(COLOR_PROBABILITY_CONFIG.SINGLE_COLOR_SUBTRAITS) === 'SOLID' ? "Solid" : "Gradient"; break; case 'DUAL': colorTrait = "Dual"; // Instead of using existing DUAL_COLOR_PAIRS, select from our new project colors const selectedColorSet = selectProjectColorSet(); colorSubTrait = selectedColorSet.name; // Store the selected color pair for this run window.currentProjectColors = selectedColorSet; break; case 'MONOCHROME': colorTrait = "Monochrome"; break; case 'INVERSE': colorTrait = "Inverse"; break; } // Handle extreme mode selection const useExtremeMode = random(100) < COLOR_PROBABILITY_CONFIG.EXTREME_MODE_PROBABILITY; if (useExtremeMode && colorTrait !== "Dual") { window.amplitudeMode = AMPLITUDE_MODES.EXTREME.TYPE; const extremeColorMode = selectFromWeights(COLOR_PROBABILITY_CONFIG.EXTREME_COLOR_MODE_WEIGHTS); window.extremeColorMode = EXTREME_COLOR_MODES[extremeColorMode]; if (window.extremeColorMode === EXTREME_COLOR_MODES.DUAL_HUE_GRADIENT || window.extremeColorMode === EXTREME_COLOR_MODES.DUAL_SOLID) { window.tallWaveHue = getRandomHue(); window.shortWaveHue = getContrastingHue(window.tallWaveHue); } } else { window.amplitudeMode = AMPLITUDE_MODES.NORMAL; window.extremeColorMode = EXTREME_COLOR_MODES.NORMAL; } return { colorTrait, colorSubTrait }; } function getBestHuePath(startHue, endHue) { // Normalize hues to 0-360 range startHue = (startHue + 360) % 360; endHue = (endHue + 360) % 360; // Calculate clockwise and counterclockwise distances const clockwiseDist = (endHue - startHue + 360) % 360; const counterclockwiseDist = (startHue - endHue + 360) % 360; // Standard shortest path logic if (clockwiseDist <= counterclockwiseDist) { return clockwiseDist; // Positive means clockwise } else { return -counterclockwiseDist; // Negative means counterclockwise } } function shouldPreferRedPath(startHue, endHue) { // Normalize hues startHue = (startHue + 360) % 360; endHue = (endHue + 360) % 360; // Check if we're dealing with gold/yellow and blue const isGoldish = (startHue >= 30 && startHue <= 70); const isBluish = (endHue >= 180 && endHue <= 240); // Or vice versa const isReverseCase = (endHue >= 30 && endHue <= 70) && (startHue >= 180 && startHue <= 240); return (isGoldish && isBluish) || isReverseCase; } function getInterpolatedHue(startHue, endHue, progress) { // Normalize hues to 0-360 range startHue = (startHue + 360) % 360; endHue = (endHue + 360) % 360; // Calculate standard shortest path hueDiff let hueDiff = endHue - startHue; if (Math.abs(hueDiff) > 180) { hueDiff = hueDiff > 0 ? hueDiff - 360 : hueDiff + 360; } // If we have special colors that should avoid green path, force red path if (shouldPreferRedPath(startHue, endHue)) { // Recalculate hueDiff to go through red/purple // This overrides the shortest path if needed const clockwiseDist = (endHue - startHue + 360) % 360; const counterclockwiseDist = (startHue - endHue + 360) % 360; // Force counterclockwise (negative) through red/purple if that's not already the case if (clockwiseDist < counterclockwiseDist) { hueDiff = -counterclockwiseDist; } } // Calculate the interpolated hue return (startHue + hueDiff * progress + 360) % 360; } function createWavesBasedOnType(waveType) { sineWaves = []; let commonHue = colorTrait === "Single Color" && colorSubTrait === "Gradient" ? getRandomHue() : random(360); let commonBrightness = random(COLOR_DEFAULTS.BRIGHTNESS.MIN, COLOR_DEFAULTS.BRIGHTNESS.MAX); let orientation; switch(window.waveOriginStyle) { case WAVE_ORIGIN_STYLES.SYMMETRICAL: orientation = Orientation.randomSymmetrical(); break; default: orientation = Orientation.randomHorizontal(); } createWaveSet(currentWaveCount, orientation, commonHue, commonBrightness); } function createWaveSet(count, orientation, commonHue, commonBrightness) { const validation = validateWaveCreationParameters(count, orientation, commonHue, commonBrightness); if (!validation.isValid) { console.error("Wave creation validation failed:", validation.errors); throw new Error(`Invalid wave creation parameters: ${validation.errors.join(", ")}`); } let mainAxis = baseWidth; let crossAxis = baseHeight; const useExtremeAmplitude = random() < 0.5; if (colorTrait !== "Dual" && useExtremeAmplitude && count === 1) { window.amplitudeMode = AMPLITUDE_MODES.EXTREME.TYPE; window.extremeColorMode = EXTREME_COLOR_MODES.DUAL_SOLID; window.shortWaveHue = getHueFromRange(WAVE_HUE_RANGES.LIGHT); // Short waves get light hues } else if (colorTrait !== "Dual") { if (useExtremeAmplitude) { window.amplitudeMode = AMPLITUDE_MODES.EXTREME.TYPE; const colorModeRand = random(); window.extremeColorMode = colorModeRand < 0.33 ? EXTREME_COLOR_MODES.NORMAL : colorModeRand < 0.66 ? EXTREME_COLOR_MODES.DUAL_HUE_GRADIENT : EXTREME_COLOR_MODES.DUAL_SOLID; if (window.extremeColorMode === EXTREME_COLOR_MODES.DUAL_HUE_GRADIENT || window.extremeColorMode === EXTREME_COLOR_MODES.DUAL_SOLID) { window.tallWaveHue = getHueFromRange(WAVE_HUE_RANGES.DARK); // Tall waves get dark hues window.shortWaveHue = getHueFromRange(WAVE_HUE_RANGES.LIGHT); // Short waves get light hues } } else { window.amplitudeMode = AMPLITUDE_MODES.NORMAL; window.extremeColorMode = EXTREME_COLOR_MODES.NORMAL; } } else { window.amplitudeMode = AMPLITUDE_MODES.NORMAL; window.extremeColorMode = EXTREME_COLOR_MODES.NORMAL; } sineWaves = []; for (let i = 0; i < count; i++) { const isLastWave = i === count - 1; let initialHue, initialBrightness, amplitude, baseSaturation; if (useExtremeAmplitude && colorTrait !== "Dual") { if (isLastWave) { amplitude = random( crossAxis * AMPLITUDE_RANGES.EXTREME.SHORT.MIN, crossAxis * AMPLITUDE_RANGES.EXTREME.SHORT.MAX ) * scaleRatio; } else { amplitude = random( crossAxis * AMPLITUDE_RANGES.EXTREME.TALL.MIN, crossAxis * AMPLITUDE_RANGES.EXTREME.TALL.MAX ) * scaleRatio; } } else { amplitude = random( crossAxis * AMPLITUDE_RANGES.NORMAL.MIN, crossAxis * AMPLITUDE_RANGES.NORMAL.MAX ) * scaleRatio; } if (window.extremeColorMode === EXTREME_COLOR_MODES.DUAL_SOLID) { if (isLastWave) { initialHue = window.shortWaveHue; initialBrightness = getNormalizedBrightness(80, 100); baseSaturation = 85; } else { initialHue = window.tallWaveHue; initialBrightness = getNormalizedBrightness(70, 90); baseSaturation = 90; } } else if (window.extremeColorMode === EXTREME_COLOR_MODES.DUAL_HUE_GRADIENT) { if (isLastWave) { initialHue = window.shortWaveHue; } else { initialHue = window.tallWaveHue; } initialBrightness = getNormalizedBrightness(70, 90); baseSaturation = COLOR_DEFAULTS.SATURATION; } else { initialHue = getRandomHue(); initialBrightness = getNormalizedBrightness(); baseSaturation = COLOR_DEFAULTS.SATURATION; } let period = random( mainAxis * 0.15, mainAxis * 1.0 ); let phase = random(TWO_PI); let speed = random( mainAxis * 0.0007, mainAxis * 0.0015 ) * scaleRatio; let wave = new SineWave( amplitude, period, phase, speed, initialHue, initialBrightness, orientation, baseSaturation ); wave.colorSubTrait = colorSubTrait; if (window.extremeColorMode === EXTREME_COLOR_MODES.DUAL_SOLID) { wave.opacity = isLastWave ? ALPHA_VALUES.FULL : ALPHA_VALUES.STANDARD; } sineWaves.push(wave); } } function initializeWaves() { // Determine wave count based on probability let rand = random(100); currentWaveCount = rand < 10 ? 1 : rand < 40 ? 2 : rand < 70 ? 3 : 4; waveCountTrait = currentWaveCount; // Randomly select wave origin style window.waveOriginStyle = random([ WAVE_ORIGIN_STYLES.ASYMMETRICAL, WAVE_ORIGIN_STYLES.SYMMETRICAL ]); // Reset amplitude mode to normal by default window.amplitudeMode = AMPLITUDE_MODES.NORMAL; window.extremeColorMode = EXTREME_COLOR_MODES.NORMAL; // Select color traits using new configuration system const { colorTrait: selectedColorTrait, colorSubTrait: selectedSubTrait } = selectColorTraits(); colorTrait = selectedColorTrait; colorSubTrait = selectedSubTrait; // Select density trait densityTrait = random(DENSITY_VALUES); // Select wave type trait waveTypeTrait = WaveType.random().type; if (colorTrait === "Dual") { window.dualMode = selectFromWeights(COLOR_PROBABILITY_CONFIG.DUAL_MODE_WEIGHTS); } // Select depth mapping trait depthMappingTrait = random() < 0.5 ? DEPTH_MAPPING.ENABLED : DEPTH_MAPPING.DISABLED; // Create waves based on type createWavesBasedOnType(waveTypeTrait); } function initializeColumns() { col = []; let columnCount = floor(width / scale); for (let x = 0; x < columnCount; x++) { col[x] = new Column(x, random(0, DATASTREAM_CONSTANTS.DELAY_MAX)); } col.forEach(column => column.freeze()); } function initializeBorder() { const shouldHaveBorder = random() < 1.0; if (!shouldHaveBorder) { borderTrait = BORDER_OPTIONS.NONE; borderColor = null; return; } if (colorTrait === "Inverse") { borderTrait = BORDER_OPTIONS.THIN; borderColor = "black"; } else if (colorTrait === "Monochrome") { borderTrait = BORDER_OPTIONS.THIN; borderColor = BORDER_COLORS.MONOCHROME; } else if (backgroundTrait === "Datastream") { borderTrait = BORDER_OPTIONS.THIN; // Use weighted selection for datastream borders const selectedType = selectFromWeights(BORDER_COLOR_WEIGHTS.DATASTREAM); switch(selectedType) { case 'WHITE': borderColor = BORDER_COLORS.DATASTREAM.WHITE; break; case 'GRADIENT': borderColor = BORDER_COLORS.DATASTREAM.GRADIENT; break; case 'WAVE': borderColor = BORDER_COLORS.DATASTREAM.WAVE; break; } } else { borderTrait = BORDER_OPTIONS.THIN; // Use weighted selection for black background borders const selectedType = selectFromWeights(BORDER_COLOR_WEIGHTS.BLACK); switch(selectedType) { case 'WHITE': borderColor = BORDER_COLORS.BLACK.WHITE; break; case 'GRADIENT': borderColor = BORDER_COLORS.BLACK.GRADIENT; break; case 'WAVE': borderColor = BORDER_COLORS.BLACK.WAVE; break; } } } function setBackground() { let rand = random(100); if (rand < 33) { backgroundTrait = "Datastream"; hasDataStream = true; dataType = "Encrypted"; // Initialize dataStreamColor with default values first dataStreamColor = { h: 0, s: 0, b: 30, name: "", opacityType: "Normal" }; if (colorTrait === "Monochrome") { // Always use grey for monochrome Object.assign(dataStreamColor, DATA_STREAM_COLORS.grey); dataStreamColor.name = "Grey"; } else if (colorTrait === "Inverse") { // For inverse mode, randomly choose between full and partial brightness const usePartial = random() < 0.5; Object.assign(dataStreamColor, { h: 0, s: 0, b: usePartial ? DATASTREAM_CONSTANTS.BRIGHTNESS.PARTIAL : DATASTREAM_CONSTANTS.BRIGHTNESS.FULL }); dataStreamColor.name = usePartial ? "Partial Black" : "Black"; } else if (colorTrait === "Rainbow" || colorTrait === "Gradient" || (colorTrait === "Single Color" && colorSubTrait === "Gradient")) { // Get colors from trail buffer const colors = getGradientColors(); if (colors && colors.length > 0) { const selectedColor = random(colors); dataStreamColor = { h: selectedColor.hue, s: selectedColor.saturation, b: DATASTREAM_CONSTANTS.BRIGHTNESS.FULL, name: getColorNameFromHue(selectedColor.hue), opacityType: "Normal" }; } else if (sineWaves && sineWaves.length > 0) { // Fallback to getting a color from existing waves const selectedWave = random(sineWaves); dataStreamColor = { h: selectedWave.hue, s: selectedWave.saturation, b: DATASTREAM_CONSTANTS.BRIGHTNESS.FULL, name: getColorNameFromHue(selectedWave.hue), opacityType: "Normal" }; } else { console.warn("No wave colors available for datastream"); } } // Set opacity type dataStreamColor.opacityType = (colorTrait === "Monochrome" || colorTrait === "Inverse") ? (random() < 0.5 ? "Normal" : "Faint") : "Normal"; } else { backgroundTrait = "Black"; hasDataStream = false; dataType = "None"; dataStreamColor = null; } } function getWaveColor() { // First try to get color from wave color history if (waveColorHistory && waveColorHistory.length > 0) { return random(waveColorHistory); } // Fallback to first wave's color if available if (sineWaves && sineWaves.length > 0) { return { h: sineWaves[0].hue, s: sineWaves[0].saturation, b: sineWaves[0].brightness }; } // Default to white if no colors available return { h: 0, s: 0, b: 100 }; } function getGradientColors() { if (!waveColorHistory || waveColorHistory.length === 0) { return [{ hue: 0, saturation: COLOR_DEFAULTS.SATURATION, brightness: COLOR_DEFAULTS.BRIGHTNESS, alpha: ALPHA_VALUES.STANDARD }]; } // Filter to ensure we only have valid colors that appeared in waves const validColors = waveColorHistory.filter(color => color && typeof color.hue === 'number' && typeof color.saturation === 'number' && typeof color.brightness === 'number' ); // Create unique key for each color to avoid duplicates while preserving order const seenColors = new Set(); const uniqueColors = validColors.filter(color => { const key = `${Math.round(color.hue)},${Math.round(color.saturation)},${Math.round(color.brightness)}`; if (seenColors.has(key)) return false; seenColors.add(key); return true; }); return uniqueColors; } function calculateSingleGradientColor(hue, baseHue) { // Calculate how far we are from base hue (0 to 1) const distanceFromBase = Math.min( Math.abs(hue - baseHue), Math.abs(hue - baseHue + 360), Math.abs(hue - baseHue - 360) ) / SINGLE_COLOR_GRADIENT.HUE.RANGE; // Calculate saturation and brightness based on distance const saturation = map( distanceFromBase, 0, 1, SINGLE_COLOR_GRADIENT.SATURATION.END, SINGLE_COLOR_GRADIENT.SATURATION.START ); const brightness = map( distanceFromBase, 0, 1, SINGLE_COLOR_GRADIENT.BRIGHTNESS.END, SINGLE_COLOR_GRADIENT.BRIGHTNESS.START ); return { hue, saturation, brightness, alpha: ALPHA_VALUES.STANDARD }; } function getBrightestWaveBrightness() { if (!sineWaves || sineWaves.length === 0) return BORDER_APPEARANCE.BRIGHTNESS.MEDIUM; return Math.max(...sineWaves.map(wave => wave.brightness)); } function drawWaveOnBuffer(wave, buffer, scale = 1, depthBuffer = null) { let thickness = parseFloat(densityTrait) * scaleRatio * scale; buffer.noFill(); buffer.strokeWeight(thickness); let steps = Math.floor(height * 4 * scale); let stepSize = (height * scale) / steps; const applyIntensity = shouldApplyIntensityMapping(wave, buffer.height / scale); if (depthBuffer) { depthBuffer.loadPixels(); } let pathStarted = false; let lastValidY = null; for (let i = 0; i < steps; i++) { let pos = i * stepSize; let { x, y } = calculateWavePosition(wave, pos, scale); // Strict boundary check - only draw if point is within canvas const isInBounds = x >= 0 && x <= buffer.width && y >= 0 && y <= buffer.height; if (isInBounds) { if (!pathStarted) { buffer.beginShape(); pathStarted = true; // If we have a lastValidY, create a clean entry point at canvas edge if (lastValidY !== null) { if (wave.orientation.type === Orientation.LEFT) { buffer.vertex(0, lastValidY); } else if (wave.orientation.type === Orientation.RIGHT) { buffer.vertex(buffer.width, lastValidY); } } } let currentColor = { hue: 0, saturation: 0, brightness: wave.brightness, alpha: wave.opacity }; if (colorTrait !== "Monochrome" && colorTrait !== "Inverse") { currentColor.hue = wave.hue; currentColor.saturation = wave.saturation; } if (depthBuffer && applyIntensity && depthMappingTrait === DEPTH_MAPPING.ENABLED) { let intensity = sampleBufferIntensity(depthBuffer, x, y); currentColor.alpha = adjustOpacityForDepth(currentColor.alpha, intensity); if (currentColor.alpha === 0) { continue; } } buffer.stroke( currentColor.hue, currentColor.saturation, currentColor.brightness, currentColor.alpha ); buffer.vertex(x, y); } else { if (pathStarted) { // End the path at exact canvas boundary if (x < 0) { buffer.vertex(0, y); } else if (x > buffer.width) { buffer.vertex(buffer.width, y); } buffer.endShape(); pathStarted = false; } // Store the y-position for potential edge entry points if (y >= 0 && y <= buffer.height) { lastValidY = y; } } } if (pathStarted) { buffer.endShape(); } if (depthBuffer) { depthBuffer.updatePixels(); } } function drawDualColorWave(wave, buffer, scale = 1, depthBuffer = null) { // Get the selected color pair from the window object or fall back to selecting a new one const selectedPair = window.currentProjectColors || selectProjectColorSet(); if (!selectedPair) return; const thickness = parseFloat(densityTrait) * scaleRatio * scale; buffer.noFill(); buffer.strokeWeight(thickness); const steps = Math.floor(height * 2 * scale); const stepSize = (height * scale) / steps; let pathStarted = false; let lastValidY = null; let lastPoint = null; // Calculate wave's actual length and progress ranges const waveLength = wave.stopProgress * buffer.width; const startX = wave.orientation.type === Orientation.LEFT ? 0 : buffer.width - waveLength; const endX = wave.orientation.type === Orientation.LEFT ? waveLength : buffer.width; for (let i = 0; i < steps; i++) { const pos = i * stepSize; const {x, y} = calculateWavePosition(wave, pos, scale); const isInBounds = x >= 0 && x <= buffer.width && y >= 0 && y <= buffer.height; if (isInBounds) { if (!pathStarted) { buffer.beginShape(); pathStarted = true; if (lastValidY !== null && lastPoint !== null) { const entryX = lastPoint.x < x ? 0 : buffer.width; buffer.vertex(entryX, lastValidY); } } let progress; if (window.dualMode === DUAL_MODE.LAYERED) { // Calculate progress based on position within actual wave length let waveProgress = (x - startX) / (endX - startX); if (wave.orientation.type === Orientation.RIGHT) { waveProgress = 1 - waveProgress; } // Apply layered transformation over the wave's actual length if (waveProgress <= 0.33) { progress = waveProgress * 3; // 0 to 1 in first third } else if (waveProgress <= 0.67) { progress = 1; // Hold at end color in middle third } else { progress = (1 - ((waveProgress - 0.67) / 0.33)); // 1 to 0 in final third } } else { // Original gradient mode calculation progress = wave.getProgress(); } // Clamp progress to ensure valid values progress = Math.max(0, Math.min(1, progress)); // Use directColorBlend with density parameter const blendedColor = directColorBlend( selectedPair.start, selectedPair.end, progress, densityTrait // Pass the density trait here ); // Apply the blended color let alpha = wave.opacity; if (depthBuffer && shouldApplyIntensityMapping(wave, buffer.height / scale)) { alpha = adjustOpacityForDepth(wave.opacity, sampleBufferIntensity(depthBuffer, x, y)); if (alpha === 0) { if (pathStarted) { buffer.endShape(); pathStarted = false; } continue; } } buffer.stroke(blendedColor.h, blendedColor.s, blendedColor.b, alpha); buffer.vertex(x, y); } else { if (pathStarted) { const exitX = x < 0 ? 0 : buffer.width; buffer.vertex(exitX, y); buffer.endShape(); pathStarted = false; } if (y >= 0 && y <= buffer.height) { lastValidY = y; lastPoint = {x, y}; } } } if (pathStarted) { buffer.endShape(); } } function drawGradientWavePath(wave, buffer, scale, pairConfig, depthBuffer) { const steps = Math.floor(height * 2 * scale); const stepSize = (height * scale) / steps; const applyIntensity = depthBuffer && shouldApplyIntensityMapping(wave, buffer.height / scale); if (depthBuffer) { depthBuffer.loadPixels(); } let pathStarted = false; let lastValidY = null; let lastPoint = null; for (let i = 0; i < steps; i++) { const pos = i * stepSize; const {x, y} = calculateWavePosition(wave, pos, scale); const isInBounds = x >= 0 && x <= buffer.width && y >= 0 && y <= buffer.height; if (isInBounds) { if (!pathStarted) { buffer.beginShape(); pathStarted = true; if (lastValidY !== null && lastPoint !== null) { const entryX = lastPoint.x < x ? 0 : buffer.width; const baseProgress = wave.orientation.type === Orientation.LEFT ? entryX / buffer.width : 1 - (entryX / buffer.width); let progress = baseProgress; if (pairConfig.mode === 'layered') { // Transform progress for layered mode progress = baseProgress < 0.5 ? baseProgress * 2 : 2 - (baseProgress * 2); } const color = interpolateColors(pairConfig.start, pairConfig.end, progress); buffer.stroke(color.hue, color.saturation, color.brightness, wave.opacity); buffer.vertex(entryX, lastValidY); } } let baseProgress = wave.orientation.type === Orientation.LEFT ? x / buffer.width : 1 - (x / buffer.width); let progress = baseProgress; if (pairConfig.mode === 'layered') { // Transform progress for layered mode progress = baseProgress < 0.5 ? baseProgress * 2 : 2 - (baseProgress * 2); } const color = interpolateColors(pairConfig.start, pairConfig.end, progress); let alpha = wave.opacity; if (applyIntensity) { alpha = adjustOpacityForDepth(wave.opacity, sampleBufferIntensity(depthBuffer, x, y)); if (alpha === 0) { if (pathStarted) { buffer.endShape(); pathStarted = false; } continue; } } buffer.stroke(color.hue, color.saturation, color.brightness, alpha); buffer.vertex(x, y); } else { if (pathStarted) { const exitX = x < 0 ? 0 : buffer.width; const baseProgress = wave.orientation.type === Orientation.LEFT ? exitX / buffer.width : 1 - (exitX / buffer.width); let progress = baseProgress; if (pairConfig.mode === 'layered') { // Transform progress for layered mode progress = baseProgress < 0.5 ? baseProgress * 2 : 2 - (baseProgress * 2); } const color = interpolateColors(pairConfig.start, pairConfig.end, progress); buffer.stroke(color.hue, color.saturation, color.brightness, wave.opacity); buffer.vertex(exitX, y); buffer.endShape(); pathStarted = false; } if (y >= 0 && y <= buffer.height) { lastValidY = y; lastPoint = {x, y}; } } } if (pathStarted) { buffer.endShape(); } if (depthBuffer) { depthBuffer.updatePixels(); } } function drawWavePath(wave, buffer, scale) { buffer.beginShape(); const steps = Math.floor(height * 2 * scale); const stepSize = (height * scale) / steps; for (let i = 0; i < steps; i++) { let pos = i * stepSize; let { x, y } = calculateWavePosition(wave, pos, scale); if (x >= 0 && x <= buffer.width && y >= 0 && y <= buffer.height) { buffer.vertex(x, y); } } buffer.endShape(); } function drawWavePathWithDepth(wave, buffer, scale, depthBuffer, color) { buffer.beginShape(); const steps = Math.floor(height * 2 * scale); const stepSize = (height * scale) / steps; for (let i = 0; i < steps; i++) { let pos = i * stepSize; let { x, y } = calculateWavePosition(wave, pos, scale); if (x >= 0 && x <= buffer.width && y >= 0 && y <= buffer.height) { // Sample depth at this position let intensity = sampleBufferIntensity(depthBuffer, x, y); let alpha = adjustOpacityForDepth(color.opacity, intensity); buffer.stroke( color.hue, color.saturation, color.brightness, alpha ); buffer.vertex(x, y); } } buffer.endShape(); } function drawDataStream(buffer, opacity, forSave = false) { buffer.textSize(scale); buffer.textAlign(CENTER, CENTER); for (let x = 0; x < col.length; x++) { if (col[x] && typeof col[x].jump === 'function') { col[x].jump(); col[x].show(buffer, opacity, forSave); } } } function drawBorder(buffer, scale = 1) { if (borderTrait === BORDER_OPTIONS.NONE) return; const borderSize = BORDER_SIZES.THIN * scale; const margin = BORDER_SIZES.THIN_MARGIN * scale; if (borderColor !== "gradient") { buffer.noFill(); buffer.strokeWeight(borderSize); if (colorTrait === "Inverse") { buffer.stroke(0, 0, 0, BORDER_APPEARANCE.OPACITY.SOLID); } else if (borderColor === BORDER_COLORS.MONOCHROME) { buffer.stroke(0, 0, BORDER_APPEARANCE.BRIGHTNESS.HIGH, BORDER_APPEARANCE.OPACITY.SOLID); } else if (borderColor === BORDER_COLORS.DATASTREAM.WHITE || borderColor === BORDER_COLORS.BLACK.WHITE) { buffer.stroke(0, 0, BORDER_APPEARANCE.BRIGHTNESS.HIGH, BORDER_APPEARANCE.OPACITY.SOLID); } else if (borderColor === BORDER_COLORS.DATASTREAM.WAVE || borderColor === BORDER_COLORS.BLACK.WAVE) { // Use cached wave color if available, otherwise select and cache new color if (!cachedBorderWaveColor) { if (colorTrait === "Dual") { const selectedPair = window.currentProjectColors || selectProjectColorSet(); if (selectedPair) { // Use the original hue/saturation interpolation const randomPoint = random(); const blendedColor = directColorBlend(selectedPair.start, selectedPair.end, randomPoint); const hue = blendedColor.h; const saturation = blendedColor.s; // Get average wave brightness instead of fixed border brightness const waveBrightness = sineWaves.length > 0 ? sineWaves.reduce((sum, wave) => sum + wave.brightness, 0) / sineWaves.length : selectedPair.start.b; // Fallback to start brightness cachedBorderWaveColor = { hue: hue, saturation: saturation, brightness: waveBrightness, // Use wave brightness instead of fixed alpha: BORDER_APPEARANCE.OPACITY.WAVE }; } } else if (waveColorHistory && waveColorHistory.length > 0) { const selectedColor = random(waveColorHistory); cachedBorderWaveColor = { hue: selectedColor.hue, saturation: selectedColor.saturation, brightness: BORDER_APPEARANCE.BRIGHTNESS.HIGH, alpha: BORDER_APPEARANCE.OPACITY.WAVE }; } else if (sineWaves && sineWaves.length > 0) { cachedBorderWaveColor = { hue: sineWaves[0].hue, saturation: sineWaves[0].saturation, brightness: BORDER_APPEARANCE.BRIGHTNESS.HIGH, alpha: BORDER_APPEARANCE.OPACITY.WAVE }; } } if (cachedBorderWaveColor) { buffer.stroke( cachedBorderWaveColor.hue, cachedBorderWaveColor.saturation, cachedBorderWaveColor.brightness, cachedBorderWaveColor.alpha ); } } else { buffer.stroke(0, 0, BORDER_APPEARANCE.BRIGHTNESS.HIGH, BORDER_APPEARANCE.OPACITY.SOLID); } buffer.rect(margin, margin, buffer.width - (margin * 2), buffer.height - (margin * 2)); return; } // Create border buffer for gradient const borderRect = createGraphics(buffer.width - (margin * 2), buffer.height - (margin * 2)); borderRect.colorMode(HSB, 360, 100, 100, 100); borderRect.noFill(); borderRect.strokeWeight(borderSize * 1.6); if (colorTrait === "Rainbow" || colorTrait === "Gradient" || (colorTrait === "Single Color" && colorSubTrait === "Gradient") || colorTrait === "Dual") { const sides = ['top', 'right', 'bottom', 'left']; const baseSegments = 8; const segmentCount = Math.ceil((borderRect.width + borderRect.height) / baseSegments); // Get dual color pair configuration if in dual mode let dualPairConfig; if (colorTrait === "Dual") { dualPairConfig = window.currentProjectColors || selectProjectColorSet(); } // Pre-calculate border segment colors for consistent saturation const borderColors = []; for (let sideIndex = 0; sideIndex < sides.length; sideIndex++) { for (let i = 0; i < segmentCount; i++) { let hue, saturation, brightness; if (colorTrait === "Dual" && dualPairConfig) { // Calculate border position for Dual mode const borderPosition = (sideIndex * segmentCount + i) / (segmentCount * 4); const cycleProgress = (borderPosition * 4) % 1; const triangleProgress = Math.abs((cycleProgress * 2) % 2 - 1); // Calculate color interpolation for dual color mode with enhanced hue interpolation const blendedColor = directColorBlend(dualPairConfig.start, dualPairConfig.end, triangleProgress); hue = blendedColor.h; saturation = blendedColor.s; brightness = blendedColor.b; } else if (colorTrait === "Single Color" && colorSubTrait === "Gradient") { if (waveColorHistory && waveColorHistory.length > 0) { const colorIndex = Math.floor(((sideIndex * segmentCount + i) / (segmentCount * 4)) * waveColorHistory.length); const color = waveColorHistory[colorIndex % waveColorHistory.length]; hue = color ? color.hue : sineWaves[0].baseHue; } else { hue = sineWaves[0].baseHue; } saturation = 80; // Fixed saturation for border segments brightness = BORDER_APPEARANCE.BRIGHTNESS.GRADIENT; } else { // Original rainbow mode hue = ((sideIndex * segmentCount + i) / (segmentCount * 4)) * 360; saturation = 80; // Fixed saturation for border segments brightness = BORDER_APPEARANCE.BRIGHTNESS.GRADIENT; } borderColors.push({ hue, saturation, brightness }); } } // Draw the border segments using pre-calculated colors sides.forEach((side, sideIndex) => { for (let i = 0; i < segmentCount; i++) { const progress = i / segmentCount; let x1, y1, x2, y2; switch(side) { case 'top': x1 = progress * borderRect.width; y1 = 0; x2 = (progress + 1/segmentCount) * borderRect.width; y2 = 0; break; case 'right': x1 = borderRect.width; y1 = progress * borderRect.height; x2 = borderRect.width; y2 = (progress + 1/segmentCount) * borderRect.height; break; case 'bottom': x1 = borderRect.width - (progress * borderRect.width); y1 = borderRect.height; x2 = borderRect.width - ((progress + 1/segmentCount) * borderRect.width); y2 = borderRect.height; break; case 'left': x1 = 0; y1 = borderRect.height - (progress * borderRect.height); x2 = 0; y2 = borderRect.height - ((progress + 1/segmentCount) * borderRect.height); break; } // Get the pre-calculated color for this segment const colorIndex = sideIndex * segmentCount + i; const color = borderColors[colorIndex]; borderRect.stroke( color.hue, color.saturation, color.brightness, BORDER_APPEARANCE.OPACITY.GRADIENT ); borderRect.line(x1, y1, x2, y2); } }); } else { // FALLBACK for color traits that don't support gradient borders // (like Single Color + Solid) borderRect.stroke(0, 0, BORDER_APPEARANCE.BRIGHTNESS.HIGH, BORDER_APPEARANCE.OPACITY.SOLID); borderRect.rect(0, 0, borderRect.width, borderRect.height); } buffer.image(borderRect, margin, margin); borderRect.remove(); } function drawGradientBorder(buffer, colors, margin, brightnessScale = 1, opacityScale = 1, scale = 1) { if (!buffer || !colors || colors.length === 0) { console.warn('Invalid parameters for gradient border'); return; } // Create gradient buffer with correct pixel density const gradientBuffer = createGraphics(buffer.width, buffer.height); gradientBuffer.pixelDensity(buffer.pixelDensity()); gradientBuffer.colorMode(HSB, 360, 100, 100, 100); gradientBuffer.smooth(); // Calculate effective stroke weight based on both scale and density const effectiveScale = scale * buffer.pixelDensity(); const strokeWeight = Math.max(BORDER_SIZES.MIN_WEIGHT, BORDER_SIZES.THIN * effectiveScale); gradientBuffer.noFill(); gradientBuffer.strokeWeight(strokeWeight); // Handle Dual Color mode with our new approach if (colorTrait === "Dual") { // Get the current project colors or select new ones const selectedPair = window.currentProjectColors || selectProjectColorSet(); if (selectedPair) { colors = [{ hue: selectedPair.start.h, saturation: selectedPair.start.s, brightness: selectedPair.start.b }, { hue: selectedPair.end.h, saturation: selectedPair.end.s, brightness: selectedPair.end.b }]; } } // Filter and validate colors colors = colors.filter(color => color && typeof color.hue === 'number' && typeof color.saturation === 'number' && typeof color.brightness === 'number'); if (colors.length === 0) { console.warn('No valid colors for gradient border'); return; } // Calculate corner points const corners = [ { x: margin, y: margin }, // Top-left { x: buffer.width - margin, y: margin }, // Top-right { x: buffer.width - margin, y: buffer.height - margin }, // Bottom-right { x: margin, y: buffer.height - margin } // Bottom-left ]; // Function to get interpolated color based on progress // Function to get interpolated color based on progress const getInterpolatedColor = (progress) => { if (colorTrait === "Dual") { const cycleProgress = (progress * 4) % 1; const triangleProgress = Math.abs((cycleProgress * 2) % 2 - 1); const currentColor = colors[0]; const nextColor = colors[1]; // Use direct RGB blending for gradient borders const blendedColor = directColorBlend( {h: currentColor.hue, s: currentColor.saturation, b: currentColor.brightness}, {h: nextColor.hue, s: nextColor.saturation, b: nextColor.brightness}, triangleProgress ); return { hue: blendedColor.h, saturation: blendedColor.s, brightness: blendedColor.b }; } else { const colorIndex = Math.floor(progress * colors.length) % colors.length; const nextColorIndex = (colorIndex + 1) % colors.length; const colorProgress = (progress * colors.length) % 1; const currentColor = colors[colorIndex]; const nextColor = colors[nextColorIndex]; return { hue: lerpHue(currentColor.hue, nextColor.hue, colorProgress), saturation: lerp(currentColor.saturation, nextColor.saturation, colorProgress), brightness: currentColor.brightness }; } }; // Draw each side of the border with increased segments for smoothness const segmentCount = 360; // One degree per segment for maximum smoothness gradientBuffer.beginShape(); // Add an extra vertex at the start for smooth beginning const startColor = getInterpolatedColor(0); gradientBuffer.stroke( startColor.hue, startColor.saturation, Math.min(100, startColor.brightness * brightnessScale), BORDER_APPEARANCE.OPACITY.GRADIENT * opacityScale ); gradientBuffer.vertex(corners[0].x, corners[0].y); // Draw each side for (let side = 0; side < 4; side++) { const start = corners[side]; const end = corners[(side + 1) % 4]; const sideLength = (side % 2 === 0) ? buffer.width - margin * 2 : buffer.height - margin * 2; // Draw segments along this side const sideSegments = Math.ceil(segmentCount * (sideLength / (buffer.width + buffer.height))); for (let i = 0; i <= sideSegments; i++) { const progress = (side * segmentCount / 4 + i * (segmentCount / 4) / sideSegments) / segmentCount; const t = i / sideSegments; // Calculate position const x = lerp(start.x, end.x, t); const y = lerp(start.y, end.y, t); // Get and apply color const color = getInterpolatedColor(progress); gradientBuffer.stroke( color.hue, color.saturation, Math.min(100, color.brightness * brightnessScale), BORDER_APPEARANCE.OPACITY.GRADIENT * opacityScale ); // Add vertex with smooth corner handling const cornerEasing = 0.1; // Corner smoothness factor if (i === 0 || i === sideSegments) { const prevSide = (side - 1 + 4) % 4; const nextSide = (side + 1) % 4; const prevCorner = corners[prevSide]; const nextCorner = corners[nextSide]; const dx = (nextCorner.x - prevCorner.x) * cornerEasing; const dy = (nextCorner.y - prevCorner.y) * cornerEasing; gradientBuffer.vertex(x + dx, y + dy); } else { gradientBuffer.vertex(x, y); } } } // Close the shape gradientBuffer.endShape(CLOSE); // Draw the gradient buffer onto the main buffer buffer.image(gradientBuffer, 0, 0); // Cleanup gradientBuffer.remove(); } function drawHighResDatastream(canvas) { canvas.textFont('monospace'); canvas.textSize(scale * 4); const colCount = floor(width / scale); let saveColumns = new Array(colCount).fill(null).map((_, x) => { const column = new Column(x, 0); const sourceCol = col[x % col.length]; column.size = sourceCol.size; column.frozen = true; column.frozenChars = [...sourceCol.frozenChars]; column.scale = scale * 4; return column; }); saveColumns.forEach(column => { column.show(canvas, 0.3, true); }); } function saveWaveStates() { return sineWaves.map(wave => ({ x: wave.x, y: wave.y, phase: wave.phase, stopped: wave.stopped, amplitude: wave.amplitude, speed: wave.speed, addedToTrail: wave.addedToTrail })); } function restoreWaveStates(states) { sineWaves.forEach((wave, i) => { Object.assign(wave, states[i]); }); } function drawWaveFromJourney(frameData, buffer, scale = 1) { if (!frameData || !frameData.wavePoints || frameData.wavePoints.length === 0) { return; } const thickness = parseFloat(densityTrait) * scaleRatio * scale; buffer.noFill(); buffer.strokeWeight(thickness); let pathStarted = false; let lastValidY = null; let lastPoint = null; frameData.wavePoints.forEach((point, index) => { const x = point.x * scale; const y = point.y * scale; // Strict boundary check const isInBounds = x >= 0 && x <= buffer.width && y >= 0 && y <= buffer.height; if (isInBounds) { if (!pathStarted) { buffer.beginShape(); pathStarted = true; // Add clean entry point at canvas edge if we have a lastValidY if (lastValidY !== null && lastPoint !== null) { const orientation = lastPoint.x < x ? "LEFT" : "RIGHT"; if (orientation === "LEFT") { buffer.vertex(0, lastValidY); } else { buffer.vertex(buffer.width, lastValidY); } } } let currentColor; if (colorTrait === "Dual" && frameData.gradientProgress !== null) { const pairConfig = DUAL_COLOR_PAIRS[Object.keys(DUAL_COLOR_PAIRS).find(key => DUAL_COLOR_PAIRS[key].name === colorSubTrait)]; if (pairConfig) { const { start, end } = pairConfig; const progress = frameData.gradientProgress; let hueDiff = end.h - start.h; if (Math.abs(hueDiff) > 180) { hueDiff = hueDiff > 0 ? hueDiff - 360 : hueDiff + 360; } currentColor = { hue: (start.h + hueDiff * progress + 360) % 360, saturation: start.s + (end.s - start.s) * progress, brightness: start.b + (end.b - start.b) * progress, alpha: frameData.opacity }; } else { currentColor = { hue: frameData.hue, saturation: frameData.saturation, brightness: frameData.brightness, alpha: frameData.opacity }; } } else if (colorTrait === "Inverse") { currentColor = { hue: 0, saturation: 0, brightness: 0, alpha: frameData.opacity }; } else if (colorTrait === "Monochrome") { currentColor = { hue: 0, saturation: 0, brightness: frameData.brightness, alpha: frameData.opacity }; } else { currentColor = { hue: frameData.hue, saturation: frameData.saturation, brightness: frameData.brightness, alpha: frameData.opacity }; } // Apply depth mapping if available if (point.depthValue !== null && depthMappingTrait === DEPTH_MAPPING.ENABLED) { currentColor.alpha = adjustOpacityForDepth(currentColor.alpha, point.depthValue); if (currentColor.alpha === 0) { if (pathStarted) { buffer.endShape(); pathStarted = false; } return; } } // Apply time-based fade if (point.timestamp !== undefined) { // Only if timestamps are present const pointAge = currentJourneyFrame - point.timestamp; let fadeAlpha = 1; if (pointAge > 90) { fadeAlpha = Math.max(0, 1 - (pointAge - 90) / 90); // How many frame before fade starts } currentColor.alpha *= fadeAlpha * 200; // Adjust brightness through fading slower } buffer.stroke( currentColor.hue, currentColor.saturation, currentColor.brightness, currentColor.alpha ); buffer.vertex(x, y); } else { if (pathStarted) { // End path at exact canvas boundary if (x < 0) { buffer.vertex(0, y); } else if (x > buffer.width) { buffer.vertex(buffer.width, y); } buffer.endShape(); pathStarted = false; } // Store the y-position for potential edge entry points if (y >= 0 && y <= buffer.height) { lastValidY = y; } } lastPoint = { x, y }; // Handle final point if (index === frameData.wavePoints.length - 1 && pathStarted) { buffer.endShape(); } }); } function cleanupBuffers() { // Release all buffers through BufferPool if (waveBuffer) BufferPool.releaseBuffer(waveBuffer.width, waveBuffer.height, 'wave'); if (trailBuffer) BufferPool.releaseBuffer(trailBuffer.width, trailBuffer.height, 'trail'); if (fadeBuffer) BufferPool.releaseBuffer(fadeBuffer.width, fadeBuffer.height, 'fade'); if (highResBuffer) BufferPool.releaseBuffer(highResBuffer.width, highResBuffer.height, 'highRes'); if (staticBuffer) BufferPool.releaseBuffer(staticBuffer.width, staticBuffer.height, 'static'); // Clear references waveBuffer = null; trailBuffer = null; fadeBuffer = null; highResBuffer = null; staticBuffer = null; } function renderHighRes() { const scaleFactor = 4; if (!highResBuffer) { highResBuffer = BufferPool.getBuffer(width * scaleFactor, height * scaleFactor, 'highRes'); } processWaves(highResBuffer, scaleFactor); return highResBuffer; } function processWaves(buffer, scale) { clearDepthCache(); buffer.clear(); let foundVisibleWaves = false; if (scale === 1) { if (!isCapturingJourney) { waveJourneys = sineWaves.map(() => new WaveJourney()); currentJourneyFrame = 0; isCapturingJourney = true; } } if (depthMappingTrait === DEPTH_MAPPING.ENABLED) { const depthBuffer = BufferPool.getBuffer(buffer.width, buffer.height, 'depth'); depthBuffer.colorMode(HSB, 360, 100, 100, 100); depthBuffer.clear(); // First process tall waves sineWaves.forEach((wave, waveIndex) => { if (!shouldApplyIntensityMapping(wave, buffer.height / scale)) { let originalX = wave.x; let originalAmplitude = wave.amplitude; let originalSpeed = wave.speed; if (scale !== 1) { wave.x *= scale; wave.amplitude *= scale; wave.speed *= scale; } let frameCounter = 0; while (!wave.stopped && frameCounter < 1000) { drawWaveOnBuffer(wave, buffer, scale); if (depthBuffer) { drawWaveOnBuffer(wave, depthBuffer, scale); } wave.update(); if (scale === 1 && isCapturingJourney) { const wavePoints = []; const steps = Math.floor(height * 4); const stepSize = height / steps; for (let i = 0; i < steps; i++) { const pos = i * stepSize; const point = calculateWavePosition(wave, pos, scale); if (point.x >= 0 && point.x <= width && point.y >= 0 && point.y <= height) { foundVisibleWaves = true; if (depthBuffer) { point.depthValue = sampleBufferIntensity(depthBuffer, point.x, point.y); } wavePoints.push(point); } } let gradientProgress = null; if (colorTrait === "Dual" || (colorTrait === "Single Color" && colorSubTrait === "Gradient")) { if (wave.waveOriginStyle === WAVE_ORIGIN_STYLES.SYMMETRICAL) { if (wave.orientation.type === Orientation.TOP) { const canvasStart = width * 0.15; const canvasEnd = width * 0.85; const traversalWidth = canvasEnd - canvasStart; gradientProgress = (wave.x - canvasStart) / traversalWidth; } else { const canvasStart = width * 0.85; const canvasEnd = width * 0.15; const traversalWidth = canvasStart - canvasEnd; gradientProgress = 1 - ((wave.x - canvasEnd) / traversalWidth); } } else { gradientProgress = wave.orientation.type === Orientation.LEFT ? wave.x / width : 1 - (wave.x / width); } } if (foundVisibleWaves || waveJourneys[waveIndex].totalFrames > 0) { waveJourneys[waveIndex].addFrame({ x: wave.x, y: wave.y, hue: wave.hue, saturation: wave.saturation, brightness: wave.brightness, opacity: wave.opacity, period: wave.period, amplitude: wave.amplitude, phase: wave.phase, gradientProgress, colorPairName: colorTrait === "Dual" ? colorSubTrait : null, wavePoints }); } } if (wave.getProgress() >= wave.stopProgress) { wave.stopped = true; wave.addedToTrail = true; } frameCounter++; } if (scale !== 1) { wave.x = originalX; wave.amplitude = originalAmplitude; wave.speed = originalSpeed; } } }); // Then process short waves with depth mapping sineWaves.forEach((wave, waveIndex) => { if (shouldApplyIntensityMapping(wave, buffer.height / scale)) { let originalX = wave.x; let originalAmplitude = wave.amplitude; let originalSpeed = wave.speed; if (scale !== 1) { wave.x *= scale; wave.amplitude *= scale; wave.speed *= scale; } let frameCounter = 0; while (!wave.stopped && frameCounter < 1000) { if (scale === 1 && isCapturingJourney) { const wavePoints = []; const steps = Math.floor(height * 4); const stepSize = height / steps; for (let i = 0; i < steps; i++) { const pos = i * stepSize; const point = calculateWavePosition(wave, pos, scale); if (point.x >= 0 && point.x <= width && point.y >= 0 && point.y <= height) { foundVisibleWaves = true; if (depthBuffer) { point.depthValue = sampleBufferIntensity(depthBuffer, point.x, point.y); } wavePoints.push(point); } } let gradientProgress = null; if (colorTrait === "Dual" || (colorTrait === "Single Color" && colorSubTrait === "Gradient")) { if (wave.waveOriginStyle === WAVE_ORIGIN_STYLES.SYMMETRICAL) { if (wave.orientation.type === Orientation.TOP) { const canvasStart = width * 0.15; const canvasEnd = width * 0.85; const traversalWidth = canvasEnd - canvasStart; gradientProgress = (wave.x - canvasStart) / traversalWidth; } else { const canvasStart = width * 0.85; const canvasEnd = width * 0.15; const traversalWidth = canvasStart - canvasEnd; gradientProgress = 1 - ((wave.x - canvasEnd) / traversalWidth); } } else { gradientProgress = wave.orientation.type === Orientation.LEFT ? wave.x / width : 1 - (wave.x / width); } } if (foundVisibleWaves || waveJourneys[waveIndex].totalFrames > 0) { waveJourneys[waveIndex].addFrame({ x: wave.x, y: wave.y, hue: wave.hue, saturation: wave.saturation, brightness: wave.brightness, opacity: wave.opacity, period: wave.period, amplitude: wave.amplitude, phase: wave.phase, gradientProgress, colorPairName: colorTrait === "Dual" ? colorSubTrait : null, wavePoints }); } } drawWaveOnBuffer(wave, buffer, scale); wave.update(); if (wave.getProgress() >= wave.stopProgress) { wave.stopped = true; wave.addedToTrail = true; } frameCounter++; } if (scale !== 1) { wave.x = originalX; wave.amplitude = originalAmplitude; wave.speed = originalSpeed; } } }); BufferPool.releaseBuffer(buffer.width, buffer.height, 'depth'); } else { // Non-depth-mapped processing if (colorTrait === "Dual") { const waveStates = sineWaves.map(wave => ({ x: wave.x, y: wave.y, phase: wave.phase, stopped: wave.stopped })); let minX = Infinity, maxX = -Infinity; // Find extents for (let wave of sineWaves) { let frameCounter = 0; while (!wave.stopped && frameCounter < 1000) { minX = Math.min(minX, wave.x); maxX = Math.max(maxX, wave.x); wave.update(); frameCounter++; } } // Restore states sineWaves.forEach((wave, i) => { wave.x = waveStates[i].x; wave.y = waveStates[i].y; wave.phase = waveStates[i].phase; wave.stopped = waveStates[i].stopped; }); // Draw with full gradient mapping sineWaves.forEach((wave, waveIndex) => { let originalX = wave.x; let originalAmplitude = wave.amplitude; let originalSpeed = wave.speed; if (scale !== 1) { wave.x *= scale; wave.amplitude *= scale; wave.speed *= scale; } let frameCounter = 0; while (!wave.stopped && frameCounter < 1000) { if (scale === 1) { const wavePoints = []; const steps = Math.floor(height * 2 * scale); const stepSize = (height * scale) / steps; for (let i = 0; i < steps; i++) { const pos = i * stepSize; const point = calculateWavePosition(wave, pos, scale); if (point.x >= 0 && point.x <= width * scale && point.y >= 0 && point.y <= height * scale) { foundVisibleWaves = true; wavePoints.push(point); } } let gradientProgress = null; if (wave.waveOriginStyle === WAVE_ORIGIN_STYLES.SYMMETRICAL) { if (wave.orientation.type === Orientation.TOP) { const canvasStart = width * 0.15; const canvasEnd = width * 0.85; const traversalWidth = canvasEnd - canvasStart; gradientProgress = (wave.x - canvasStart) / traversalWidth; } else { const canvasStart = width * 0.85; const canvasEnd = width * 0.15; const traversalWidth = canvasStart - canvasEnd; gradientProgress = 1 - ((wave.x - canvasEnd) / traversalWidth); } } else { gradientProgress = wave.orientation.type === Orientation.LEFT ? wave.x / width : 1 - (wave.x / width); } if (foundVisibleWaves || waveJourneys[waveIndex].totalFrames > 0) { waveJourneys[waveIndex].addFrame({ x: wave.x, y: wave.y, hue: wave.hue, saturation: wave.saturation, brightness: wave.brightness, opacity: wave.opacity, period: wave.period, amplitude: wave.amplitude, phase: wave.phase, gradientProgress, depthValue: null, colorPairName: colorSubTrait, wavePoints }); } } drawDualColorWave(wave, buffer, scale, null); wave.update(); if (wave.getProgress() >= wave.stopProgress) { wave.stopped = true; wave.addedToTrail = true; } frameCounter++; } if (scale !== 1) { wave.x = originalX; wave.amplitude = originalAmplitude; wave.speed = originalSpeed; } }); } else { sineWaves.forEach((wave, waveIndex) => { let originalX = wave.x; let originalAmplitude = wave.amplitude; let originalSpeed = wave.speed; if (scale !== 1) { wave.x *= scale; wave.amplitude *= scale; wave.speed *= scale; } let frameCounter = 0; while (!wave.stopped && frameCounter < 1000) { if (scale === 1) { const wavePoints = []; const steps = Math.floor(height * 2 * scale); const stepSize = (height * scale) / steps; for (let i = 0; i < steps; i++) { const pos = i * stepSize; const point = calculateWavePosition(wave, pos, scale); if (point.x >= 0 && point.x <= width * scale && point.y >= 0 && point.y <= height * scale) { foundVisibleWaves = true; wavePoints.push(point); } } if (foundVisibleWaves || waveJourneys[waveIndex].totalFrames > 0) { waveJourneys[waveIndex].addFrame({ x: wave.x, y: wave.y, hue: wave.hue, saturation: wave.saturation, brightness: wave.brightness, opacity: wave.opacity, period: wave.period, amplitude: wave.amplitude, phase: wave.phase, gradientProgress: null, depthValue: null, wavePoints }); } } drawWaveOnBuffer(wave, buffer, scale); wave.update(); if (wave.getProgress() >= wave.stopProgress) { wave.stopped = true; wave.addedToTrail = true; } frameCounter++; } if (scale !== 1) { wave.x = originalX; wave.amplitude = originalAmplitude; wave.speed = originalSpeed; } }); } } } function shouldApplyIntensityMapping(wave, canvasHeight) { if (depthMappingTrait !== DEPTH_MAPPING.ENABLED) return false; // Convert wave amplitude to percentage of canvas height const amplitudePercentage = wave.amplitude / (canvasHeight * scaleRatio); // Return true if wave is within the threshold range return amplitudePercentage >= DEPTH_MAPPING.AMPLITUDE_THRESHOLD.MIN && amplitudePercentage <= DEPTH_MAPPING.AMPLITUDE_THRESHOLD.MAX; } function sampleBufferIntensity(buffer, x, y, radius = 4) { const key = `${Math.floor(x)},${Math.floor(y)}`; if (depthSampleCache.has(key)) { return depthSampleCache.get(key); } let intensity = 0; let sampledPixels = 0; const xStart = Math.floor(x - radius); const xEnd = Math.floor(x + radius); const yStart = Math.floor(y - radius); const yEnd = Math.floor(y + radius); const bufferWidth = buffer.width; const bufferHeight = buffer.height; for (let py = yStart; py <= yEnd; py++) { if (py < 0 || py >= bufferHeight) continue; const rowOffset = py * bufferWidth * 4; for (let px = xStart; px <= xEnd; px++) { if (px < 0 || px >= bufferWidth) continue; const i = px - Math.floor(x); const j = py - Math.floor(y); const weight = 1.5 - (Math.sqrt(i*i + j*j) / radius); const index = rowOffset + px * 4; intensity += ((buffer.pixels[index] + buffer.pixels[index + 1] + buffer.pixels[index + 2]) / 3) * weight; sampledPixels++; } } const result = sampledPixels > 0 ? (intensity / (sampledPixels * 255)) * 1.5 : 0; depthSampleCache.set(key, result); return result; } function clearDepthCache() { depthSampleCache.clear(); } function adjustOpacityForDepth(baseOpacity, intensity) { // Make the curve smoother with a sigmoid-like function const transitionStart = 0.3; // Start transition earlier const transitionEnd = 0.5; // End transition later if (intensity <= transitionStart) { return DEPTH_MAPPING.OPACITY_RANGE.REDUCE; } else if (intensity >= transitionEnd) { return Math.min( DEPTH_MAPPING.OPACITY_RANGE.BOOST, baseOpacity + (Math.pow(intensity, 0.2) * 100) ); } else { // Smooth transition between reduce and boost const progress = (intensity - transitionStart) / (transitionEnd - transitionStart); const smoothProgress = progress * progress * (3 - 2 * progress); // Smoothstep interpolation return Math.min( DEPTH_MAPPING.OPACITY_RANGE.BOOST, DEPTH_MAPPING.OPACITY_RANGE.REDUCE + (smoothProgress * (baseOpacity + (Math.pow(intensity, 0.2) * 100) - DEPTH_MAPPING.OPACITY_RANGE.REDUCE)) ); } } function cleanupAnimationFrames(preserveFrames = false) { // First, cancel any pending animation frame if (window.animationFrameId) { cancelAnimationFrame(window.animationFrameId); window.animationFrameId = null; } // Set animation state variables isAnimating = false; isPaused = false; // If we want to preserve frames (when toggling to static view), // we don't release buffer memory or reset the render state if (!preserveFrames && window.prerenderedFrames && window.prerenderedFrames.length) { console.log(`Cleaning up ${window.prerenderedFrames.length} animation frames...`); try { // Release all frame buffers window.prerenderedFrames.forEach((frame, index) => { if (frame.fadeBuffer) { BufferPool.releaseBuffer(frame.fadeBuffer.width, frame.fadeBuffer.height, `fade-${index}`); } if (frame.trailBuffer) { BufferPool.releaseBuffer(frame.trailBuffer.width, frame.trailBuffer.height, `trail-${index}`); } }); } catch (error) { console.error("Error releasing buffers:", error); } // Clear the frames array window.prerenderedFrames = []; // Reset isPreRenderComplete so animation will be regenerated next time isPreRenderComplete = false; // Also reset isCapturingJourney to ensure new journey frames are created isCapturingJourney = false; } // Set an empty draw function to stop p5's animation loop draw = function() {}; // Make sure animation loop is stopped noLoop(); console.log("Animation paused"); } function clearEdgePixels(buffer, edgeWidth = 3) { if (!buffer) return; buffer.loadPixels(); const w = buffer.width; const h = buffer.height; // Process left and right edges for (let y = 0; y < h; y++) { // Left edge for (let x = 0; x < edgeWidth; x++) { const idx = (y * w + x) * 4 + 3; // Alpha channel buffer.pixels[idx] = 0; // Completely clear edge pixels } // Right edge for (let x = w - edgeWidth; x < w; x++) { const idx = (y * w + x) * 4 + 3; // Alpha channel buffer.pixels[idx] = 0; // Completely clear edge pixels } } buffer.updatePixels(); } function drawFinalState() { // Use the staticBuffer which already has the correct background and datastream image(staticBuffer, 0, 0); // Draw the wave buffers image(fadeBuffer, 0, 0); image(trailBuffer, 0, 0); // Draw border drawBorder(window); } function preRenderAnimation() { // Setup animation parameters isAnimating = false; // Don't start playing yet currentJourneyFrame = 0; lastFrameTime = Date.now(); isCapturingJourney = true; // Create frame buffer const frameBuffer = BufferPool.getBuffer(width, height, 'prerender'); frameBuffer.colorMode(HSB, 360, 100, 100, 100); window.prerenderedFrames = []; // Clear the trail buffers trailBuffer.clear(); fadeBuffer.clear(); // Initialize rendering state let allComplete = false; let hasVisiblePixels = true; let fadeOutProgress = 0; let fadeOutStarted = false; // Optimized chunking parameters const CHUNK_SIZE = 50; // Increased from 30 to 50 for better performance const CHUNK_DELAY = 5; // Reduced from 10ms to 5ms to speed up rendering // Define the processChunk function inside preRenderAnimation function processChunk() { // Process a chunk of frames let framesProcessed = 0; while (framesProcessed < CHUNK_SIZE && (!allComplete || hasVisiblePixels)) { frameBuffer.clear(); allComplete = true; // Process each wave for this frame sineWaves.forEach((wave, index) => { if (currentJourneyFrame < waveJourneys[index].totalFrames) { allComplete = false; const frameInfo = waveJourneys[index].getFrame(currentJourneyFrame); if (frameInfo) { drawWaveFromJourney(frameInfo, frameBuffer, 1); } } }); // Check if there are still visible pixels trailBuffer.loadPixels(); hasVisiblePixels = false; for (let i = 0; i < trailBuffer.pixels.length; i += 4) { let alpha = trailBuffer.pixels[i + 3]; if (alpha > 0) { hasVisiblePixels = true; trailBuffer.pixels[i + 3] = Math.max(0, Math.floor(alpha - 0.0000001)); // Trails fade slowly } } trailBuffer.updatePixels(); // Add wave data to trail buffer if not complete or not faded if (!allComplete || !fadeOutStarted) { trailBuffer.image(frameBuffer, 0, 0); // Clear edge pixels after composition to prevent accumulation clearEdgePixels(trailBuffer, 3); } // Store the frame state let frameFadeBuffer = BufferPool.getBuffer(width, height, `fade-${currentJourneyFrame}`); let frameTrailBuffer = BufferPool.getBuffer(width, height, `trail-${currentJourneyFrame}`); frameFadeBuffer.copy(fadeBuffer, 0, 0, width, height, 0, 0, width, height); frameTrailBuffer.copy(trailBuffer, 0, 0, width, height, 0, 0, width, height); window.prerenderedFrames.push({ fadeBuffer: frameFadeBuffer, trailBuffer: frameTrailBuffer, fadeOutProgress, fadeOutStarted, hasVisiblePixels }); // Handle fade out after all waves complete if (allComplete) { if (!fadeOutStarted) { fadeOutStarted = true; } else if (!hasVisiblePixels) { break; // No more visible pixels, we're done } fadeOutProgress += 0.01; } currentJourneyFrame++; framesProcessed++; } // If we have more frames to process, schedule the next chunk if (!allComplete || hasVisiblePixels) { setTimeout(processChunk, CHUNK_DELAY); } else { // Rendering complete finalizeRendering(); } } // Define the finalizeRendering function inside preRenderAnimation function finalizeRendering() { // Release resources BufferPool.releaseBuffer(width, height, 'prerender'); // Update state isPreRenderComplete = true; window.isRenderingAnimation = false; // Mark rendering as complete // Update UI updateMenuOptions(); } // Start processing the first chunk - crucial to actually start the process! processChunk(); } function playAnimation(startFrame = 0) { if (!window.prerenderedFrames || window.prerenderedFrames.length === 0) return; isAnimating = true; currentFrame = startFrame; lastFrameTime = Date.now(); // Cancel any existing animation frame request if (window.animationFrameId) { cancelAnimationFrame(window.animationFrameId); window.animationFrameId = null; } updateMenuOptions(); function drawFrame() { // Check if animation should still be running if (!isAnimating || isPaused) { console.log("Animation stopped or paused, not requesting next frame"); return; } // Check if frames still exist if (!window.prerenderedFrames || window.prerenderedFrames.length === 0) { console.log("No frames available, stopping animation"); isAnimating = false; return; } const currentTime = Date.now(); const deltaTime = currentTime - lastFrameTime; lastFrameTime = currentTime; // Calculate how many frames to advance based on speed multiplier const adjustedFrameInterval = FRAME_INTERVAL / bitcoinData.speedMultiplier; const framesToAdvance = Math.max(1, Math.floor(deltaTime / adjustedFrameInterval)); // Limit maximum frames to prevent big jumps if tab was inactive const maxFramesAtOnce = 10; const effectiveFrames = Math.min(framesToAdvance, maxFramesAtOnce); clear(); image(staticBuffer, 0, 0); try { // Get the FINAL frame after advancing const targetFrame = (currentFrame + effectiveFrames) % window.prerenderedFrames.length; const frame = window.prerenderedFrames[targetFrame]; if (frame && frame.fadeBuffer && frame.trailBuffer) { // Store the original stroke weight to restore it later const originalStrokeWeight = drawingContext.lineWidth; // Apply minimum stroke weight during animation only const minThickness = 0.7 * scaleRatio; const currentThickness = parseFloat(densityTrait) * scaleRatio; const effectiveThickness = Math.max(minThickness, currentThickness); // Set the stroke weight for the animation frame strokeWeight(effectiveThickness); // Draw the frame image(frame.fadeBuffer, 0, 0); image(frame.trailBuffer, 0, 0); // Restore original stroke weight strokeWeight(originalStrokeWeight); // Only update AFTER successfully drawing currentFrame = targetFrame; } else { console.log("Invalid frame data, stopping animation"); isAnimating = false; return; } } catch (error) { console.error("Error in animation:", error); isAnimating = false; drawFinalState(); // Show static image as fallback return; } // Always request next frame if animation is running if (isAnimating && !isPaused) { window.animationFrameId = requestAnimationFrame(drawFrame); } } draw = function() {}; // Clear the p5 draw function window.animationFrameId = requestAnimationFrame(drawFrame); } function saveOriginalState() { // Create copies of the original buffers originalTrailBuffer = BufferPool.getBuffer(width, height, 'originalTrail'); originalFadeBuffer = BufferPool.getBuffer(width, height, 'originalFade'); // Mark these buffers as permanent so they won't get cleaned up const trailKey = `originalTrail-${width}-${height}`; const fadeKey = `originalFade-${width}-${height}`; if (BufferPool.buffers.has(trailKey)) { BufferPool.buffers.get(trailKey).permanent = true; } if (BufferPool.buffers.has(fadeKey)) { BufferPool.buffers.get(fadeKey).permanent = true; } // Copy the static image data to these buffers originalTrailBuffer.copy(trailBuffer, 0, 0, width, height, 0, 0, width, height); originalFadeBuffer.copy(fadeBuffer, 0, 0, width, height, 0, 0, width, height); // Save the original wave states originalWaveStates = saveWaveStates(); } function restoreStaticImage() { console.log("Restoring static image..."); // Make sure we have the original state if (!originalTrailBuffer || !originalFadeBuffer) { console.warn("Original buffers not found. Recreating static image."); recreateStaticImage(); return; } // Clear current buffers trailBuffer.clear(); fadeBuffer.clear(); // Copy original data back trailBuffer.copy(originalTrailBuffer, 0, 0, width, height, 0, 0, width, height); fadeBuffer.copy(originalFadeBuffer, 0, 0, width, height, 0, 0, width, height); // Force a complete redraw clear(); // Clear the canvas first // Draw the static buffer which includes background and datastream correctly // This works for all modes including Inverse mode image(staticBuffer, 0, 0); // Draw the buffer contents image(fadeBuffer, 0, 0); image(trailBuffer, 0, 0); // Draw border drawBorder(window); console.log("Static image restored successfully."); } function recreateStaticImage() { console.log("Recreating static image from scratch..."); // Clear current buffers trailBuffer.clear(); fadeBuffer.clear(); // Restore original wave states if (originalWaveStates) { console.log("Restoring original wave states..."); restoreWaveStates(originalWaveStates); } else { console.log("No original wave states found. Reinitializing waves..."); initializeWaves(); } // Process waves into the trail buffer (as done in setup) console.log("Processing waves into trail buffer..."); processWaves(trailBuffer, 1); // Draw the final state console.log("Drawing final state..."); drawFinalState(); } function setup() { // If already set, don't override it if (!window.currentSeed) { const urlParams = new URLSearchParams(window.location.search); window.currentSeed = urlParams.get('seed') || generateNewSeed(); } console.log('🎲 SEED:', window.currentSeed); window.random = new SeededRandom(window.currentSeed).random.bind(new SeededRandom(window.currentSeed)); // THEN set up global variables that use random window.baseWidth = 4096; window.baseHeight = 3072; START_AT_EDGE = window.random() < 0.5; // Set alpha values with 50/50 chance ALPHA_VALUES = window.random() < 0.5 ? ORIGINAL_ALPHA_VALUES : INCREASED_ALPHA_VALUES; console.log("Using alpha values:", ALPHA_VALUES === ORIGINAL_ALPHA_VALUES ? "Original" : "Increased"); const dims = calculateDimensions(); // Store original dimensions - we'll keep these fixed originalWidth = dims.width; originalHeight = dims.height; // Create canvas with original dimensions const canvas = createCanvas(originalWidth, originalHeight); // Create a container div to help with scaling canvasContainer = document.createElement('div'); canvasContainer.style.position = 'absolute'; canvasContainer.style.transformOrigin = 'center'; document.body.appendChild(canvasContainer); // Move the canvas into our container canvas.elt.style.display = 'block'; canvasContainer.appendChild(canvas.elt); // Initial positioning positionCanvasContainer(); // Remove the console.timeEnd call here // Remove the console.time call here waveBuffer = BufferPool.getBuffer(dims.width, dims.height, 'wave'); trailBuffer = BufferPool.getBuffer(dims.width, dims.height, 'trail'); fadeBuffer = BufferPool.getBuffer(dims.width, dims.height, 'fade'); // Remove the console.timeEnd call here colorMode(HSB, 360, 100, 100, 100); textFont('monospace'); // Remove the console.time call here waveColorHistory = []; hasDataStream = false; dataType = "None"; dataStreamColor = null; // Remove the console.timeEnd call here // Remove the console.time call here initializeWaves(); // Remove the console.timeEnd call here // Remove the console.time call here setBackground(); initializeBorder(); // Remove the console.timeEnd call here // Remove the console.time call here cachedBorderWaveColor = null; initializeColumns(); // Remove the console.timeEnd call here // Remove the console.time call here staticBuffer = BufferPool.getBuffer(dims.width, dims.height, 'static'); staticBuffer.colorMode(HSB, 360, 100, 100, 100); renderStaticElements(staticBuffer); // Remove the console.timeEnd call here // Remove the console.time call here processWaves(trailBuffer, 1); drawFinalState(); // Remove the console.timeEnd call here saveOriginalState(); // MOVE THE TRAIT ASSIGNMENT HERE - after all visual elements are finalized assignAdditionalTraits(); logTraits(); noLoop(); // Remove the console.timeEnd call here // Minimal fix for the BufferPool.cleanup issue setInterval(function() { if (BufferPool && typeof BufferPool.cleanup === 'function') { BufferPool.cleanup(); } else { console.warn('BufferPool.cleanup is not available'); } }, BufferPoolConfig.CLEANUP_INTERVAL); // Check if mobile device const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); // Initialize animation rendering flag window.isRenderingAnimation = false; // Skip pre-rendering animation on mobile devices if (!SKIP_PRERENDER && !isMobile) { requestAnimationFrame(() => { // Use requestIdleCallback if available to avoid blocking the UI if (window.requestIdleCallback) { requestIdleCallback(() => { // Set rendering flag before starting pre-render window.isRenderingAnimation = true; // Update menu options to show rendering status if (menuContainer) updateMenuOptions(); // Start pre-rendering with chunking preRenderAnimation(); }); } else { setTimeout(() => { // Set rendering flag before starting pre-render window.isRenderingAnimation = true; // Update menu options to show rendering status if (menuContainer) updateMenuOptions(); // Start pre-rendering with chunking preRenderAnimation(); }, 0); } }); } else if (isMobile) { console.log("Mobile device detected. Animation pre-rendering skipped."); isPreRenderComplete = false; // Ensure animation is not available window.prerenderedFrames = []; // Ensure frames array is empty } setupAudio(); setupMenuButton(); setupBitcoinData(); } function setupAudio() { try { const audioInscriptions = { "Distorted": "1f350d8a733255089c4a12c07ea03557f40012f1af65918e869280bce4ff1a85i0", "Granular": "b56c7bb3cd43df14167c836d0f85d9f30f9c8acb4b9a10cc4fe3e8e222643f1bi0", "Mysterious": "e056f884b2c19542366fbf9da01b856007fc5a6f0ab856fcb7558eb3510d617ci0", "Tonal": "607b2343be1410d15333a27b38164b2fa163543e38a29cee9ae51b05918d1bb4i0", "Disruption": "7a3af0d6c7b773f62b448b45cc383ea44463cb345f71cd3d7b92ed50b3d5b3f3i0" }; const inscriptionId = audioInscriptions[encryptedAudioTrait] || audioInscriptions["Distorted"]; // For inscriptions, we need to use this specific format // Note: In inscription context, the leading slash with no domain is the correct format audio = new Audio(`/content/${inscriptionId}`); audio.loop = true; audio.volume = 0.5; // Add a more detailed error handler audio.onerror = function(e) { console.error("Audio error occurred. Details:", { code: this.error ? this.error.code : "unknown", message: this.error ? this.error.message : "unknown", event: e }); // Try an alternative format if the first fails console.log("Trying alternative reference format..."); audio = new Audio(`/inscription/${inscriptionId}/content`); audio.onerror = function(secondError) { console.error("Alternative audio format also failed:", secondError); }; }; console.log("Audio initialized with inscription ID:", inscriptionId); console.log("Using audio type:", encryptedAudioTrait); } catch (e) { console.warn("Could not initialize audio:", e); } } function setupMenuButton() { // Create menu button menuButton = document.createElement('button'); menuButton.innerHTML = ` `; // Apply styles menuButton.style.position = 'absolute'; menuButton.style.background = 'rgba(40, 40, 40, 0.7)'; menuButton.style.color = 'white'; menuButton.style.border = '1px solid rgba(140, 140, 140, 0.6)'; menuButton.style.borderRadius = '5px'; menuButton.style.cursor = 'pointer'; menuButton.style.fontFamily = 'Arial, sans-serif'; menuButton.style.zIndex = '9999'; menuButton.style.boxShadow = '0 0 5px rgba(0,0,0,0.3)'; menuButton.style.lineHeight = '0'; menuButton.style.padding = '8px 15px'; // Add hover effects menuButton.addEventListener('mouseover', function() { this.style.background = 'rgba(60, 60, 60, 0.8)'; this.style.border = '1px solid rgba(180, 180, 180, 0.7)'; }); menuButton.addEventListener('mouseout', function() { this.style.background = 'rgba(40, 40, 40, 0.7)'; this.style.border = '1px solid rgba(140, 140, 140, 0.6)'; }); // Add click handler menuButton.addEventListener('click', toggleMenu); // Add to canvasContainer canvasContainer.appendChild(menuButton); // Position at top-right of canvas menuButton.style.top = '17px'; menuButton.style.left = '17px'; // Setup the menu container setupMenuContainer(); applyMenuSizeByOrientation(); } function applyMenuSizeByOrientation() { // Use the smaller portrait size for both landscape and portrait // Smaller menu button menuButton.style.padding = '6px 12px'; menuButton.style.fontSize = '14px'; // Adjust SVG size if needed const svg = menuButton.querySelector('svg'); if (svg) { svg.setAttribute('width', '24'); svg.setAttribute('height', '16'); } // Make menu container smaller too if (menuContainer) { menuContainer.style.width = '180px'; // Slightly smaller width menuContainer.style.padding = '8px'; // Smaller padding } // Adjust button padding to be smaller const buttons = menuContainer ? menuContainer.querySelectorAll('button') : []; buttons.forEach(button => { button.style.padding = '6px 12px'; button.style.fontSize = '14px'; button.style.marginBottom = '8px'; // Smaller margin }); } function setupMenuContainer() { // Create menu container div menuContainer = document.createElement('div'); menuContainer.style.position = 'absolute'; menuContainer.style.top = '60px'; menuContainer.style.left = '17px'; // Position to left side menuContainer.style.background = 'rgba(40, 40, 40, 0.85)'; menuContainer.style.border = '1px solid rgba(140, 140, 140, 0.6)'; menuContainer.style.borderRadius = '5px'; menuContainer.style.padding = '10px'; menuContainer.style.zIndex = '9998'; menuContainer.style.boxShadow = '0 2px 10px rgba(0,0,0,0.5)'; menuContainer.style.display = 'none'; // Hide initially menuContainer.style.flexDirection = 'column'; menuContainer.style.width = '200px'; // Initialize the menu options updateMenuOptions(); // Add the menu container to the canvas container canvasContainer.appendChild(menuContainer); } function createMenuButton(label, clickHandler) { const button = document.createElement('button'); button.textContent = label; // Style the button button.style.display = 'block'; button.style.width = '100%'; button.style.marginBottom = '10px'; button.style.background = 'rgba(60, 60, 60, 0.5)'; button.style.color = 'rgba(255, 255, 255, 0.7)'; button.style.border = '1px solid rgba(140, 140, 140, 0.4)'; button.style.borderRadius = '15px'; button.style.cursor = 'pointer'; button.style.fontFamily = 'Arial, sans-serif'; button.style.padding = BUTTON_PADDING; button.style.fontSize = '16px'; button.style.textAlign = 'center'; // Add hover effects button.addEventListener('mouseover', function() { this.style.background = 'rgba(80, 80, 80, 0.7)'; this.style.color = 'white'; }); button.addEventListener('mouseout', function() { this.style.background = 'rgba(60, 60, 60, 0.5)'; this.style.color = 'rgba(255, 255, 255, 0.7)'; }); // Add click handler button.addEventListener('click', clickHandler); return button; } window.addEventListener('resize', function() { // Update menu button position if (menuButton && canvasContainer) { menuButton.style.top = '17px'; menuButton.style.left = '17px'; // Apply consistent sizing after resize applyMenuSizeByOrientation(); } }); function addMenuItem(label, clickHandler) { const button = document.createElement('button'); button.innerText = label; // Base styles button.style.display = 'block'; button.style.width = '100%'; button.style.marginBottom = '10px'; button.style.background = 'rgba(60, 60, 60, 0.5)'; button.style.color = 'rgba(255, 255, 255, 0.7)'; button.style.border = '1px solid rgba(140, 140, 140, 0.4)'; button.style.borderRadius = '15px'; button.style.cursor = 'pointer'; button.style.fontFamily = 'Arial, sans-serif'; button.style.textAlign = 'center'; // Adjust padding and font size based on orientation const isLandscape = window.canvasType === "Landscape"; if (isLandscape) { button.style.padding = '6px 12px'; button.style.fontSize = '14px'; } else { button.style.padding = '8px 15px'; button.style.fontSize = '16px'; } // Add hover effects button.addEventListener('mouseover', function() { this.style.background = 'rgba(80, 80, 80, 0.7)'; this.style.color = 'white'; }); button.addEventListener('mouseout', function() { this.style.background = 'rgba(60, 60, 60, 0.5)'; this.style.color = 'rgba(255, 255, 255, 0.7)'; }); // Add click handler button.addEventListener('click', clickHandler); // Add to menu container menuContainer.appendChild(button); } function updateMenuOptions() { // Clear existing buttons menuContainer.innerHTML = ''; // Always add audio controls, regardless of animation state playPauseButton = createMenuButton("Play Encrypted Signal", function() { if (audio) { if (isAudioPlaying) { audio.pause(); isAudioPlaying = false; playPauseButton.textContent = "Play Encrypted Signal"; } else { audio.play(); isAudioPlaying = true; playPauseButton.textContent = "Pause Encrypted Signal"; } } }); menuContainer.appendChild(playPauseButton); stopButton = createMenuButton("End Encrypted Signal", function() { if (audio) { audio.pause(); audio.currentTime = 0; isAudioPlaying = false; if (playPauseButton) { playPauseButton.textContent = "Play Encrypted Signal"; } } }); menuContainer.appendChild(stopButton); // Create Volume Control const volumeContainer = document.createElement('div'); volumeContainer.style.display = 'flex'; volumeContainer.style.alignItems = 'center'; volumeContainer.style.width = '100%'; volumeContainer.style.height = '30px'; volumeContainer.style.background = 'rgba(60, 60, 60, 0.5)'; volumeContainer.style.borderRadius = '15px'; volumeContainer.style.padding = '5px 10px'; volumeContainer.style.boxSizing = 'border-box'; volumeContainer.style.marginTop = '10px'; volumeContainer.style.marginBottom = '10px'; const volumeLabel = document.createElement('span'); volumeLabel.textContent = 'Volume'; volumeLabel.style.color = 'rgba(255, 255, 255, 0.7)'; volumeLabel.style.marginRight = '10px'; volumeLabel.style.flex = '0 0 60px'; volumeContainer.appendChild(volumeLabel); volumeSlider = document.createElement('input'); volumeSlider.type = 'range'; volumeSlider.min = 0; volumeSlider.max = 100; volumeSlider.value = 50; volumeSlider.style.width = '100%'; volumeSlider.style.margin = '0'; volumeSlider.style.flex = '1'; volumeSlider.addEventListener('input', function() { if (audio) { const volume = this.value / 100; audio.volume = volume; } }); volumeContainer.appendChild(volumeSlider); menuContainer.appendChild(volumeContainer); const decodeButton = createMenuButton("Decode Encrypted Signal", async function() { // Parent inscription ID - replace with your actual parent ID const parentId = "d8722eaf8716ba2adefa3470beff0cc15508e17518f60a61de0aa38a236effd5i0"; // Show loading state this.textContent = "Decoding..."; this.disabled = true; try { // Find the interface inscription for this Radio Wave const interfaceId = await findInterfaceForRadioWave(parentId); if (interfaceId) { // Navigate to the interface inscription window.location.href = `/content/${interfaceId}`; } else { alert("No interface found for this Radio Wave"); } } catch (error) { console.error("Error accessing encrypted signal:", error); alert("Error decoding encrypted signal"); } finally { // Reset button state this.textContent = "Decode Encrypted Signal"; this.disabled = false; } }); menuContainer.appendChild(decodeButton); // Get filename let filename = getExportFilename(); // Check if device is mobile const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); // Then add all the original menu options if (isAnimating) { // Menu options for animation mode (desktop only) addMenuItem("Save Animation", function() { handleExport('MP4', filename); }); addMenuItem("Static Radio Wave", function() { console.log("Static Radio Wave button clicked"); // Stop animation but preserve the prerendered frames cleanupAnimationFrames(true); // Make sure we get a full redraw with a short delay setTimeout(function() { // Clear the canvas clear(); // Restore the static image restoreStaticImage(); // Force a redraw to make sure it appears redraw(); // Critical: Update menu options to show "Animate Radio Wave" again updateMenuOptions(); }, 50); }); } else { // Menu options for static mode addMenuItem("Save Radio Wave", function() { handleExport('PNG', filename); }); addMenuItem("Save Radio Wave (High Res)", function() { handleHighResExport(filename); }); // Only show animation option on desktop devices if (!isMobile) { // Show correct button based on animation rendering state if (window.isRenderingAnimation) { // Create pulsing "Rendering Animation..." button const renderingButton = createMenuButton("Rendering Animation...", function() { // Do nothing when clicked while rendering }); // Style the button to show it's disabled renderingButton.style.opacity = "0.7"; renderingButton.style.cursor = "default"; renderingButton.disabled = true; menuContainer.appendChild(renderingButton); } else { addMenuItem("Animate Radio Wave", function() { // If we already have prerendered frames, use them directly if (isPreRenderComplete && window.prerenderedFrames && window.prerenderedFrames.length > 0) { console.log("Using cached animation frames"); playAnimation(); return; } // Otherwise, we need to generate the animation console.log("Animation loading..."); // Reset the sine waves to their original states if (originalWaveStates) { console.log("Restoring original wave states for animation..."); restoreWaveStates(originalWaveStates); } else { console.log("No original wave states found. Reinitializing waves..."); initializeWaves(); } // Clear any existing wave journeys to ensure fresh animation waveJourneys = sineWaves.map(() => new WaveJourney()); currentJourneyFrame = 0; // Start prerendering window.isRenderingAnimation = true; updateMenuOptions(); // Update UI to show rendering state // Start prerendering preRenderAnimation(); }); } } } applyMenuSizeByOrientation(); } function toggleMenu() { menuOpen = !menuOpen; menuContainer.style.display = menuOpen ? 'block' : 'none'; } function handleHighResExport(filename) { const EXPORT_DENSITY = 4; // Always use 4x const saveCanvas = createGraphics(width, height); saveCanvas.pixelDensity(EXPORT_DENSITY); saveCanvas.colorMode(HSB, 360, 100, 100, 100); // Draw content with proper scaling saveCanvas.image(get(), 0, 0, width, height); save(saveCanvas, `${filename}-${EXPORT_DENSITY}x.png`); saveCanvas.remove(); } function getExportFilename() { return "Radio Waves"; } function positionCanvasContainer() { if (!canvasContainer) return; // Calculate appropriate scale to fit the window while maintaining aspect ratio const aspect = originalWidth / originalHeight; const maxWidth = windowWidth * 0.9; // Use 90% of window width const maxHeight = windowHeight * 0.9; // Use 90% of window height let scale; if (maxWidth / aspect <= maxHeight) { // Width limited scale = maxWidth / originalWidth; } else { // Height limited scale = maxHeight / originalHeight; } // Apply CSS transform to scale the container canvasContainer.style.transform = `scale(${scale})`; // Calculate position to center the container // We need to account for the scaling effect const scaledWidth = originalWidth * scale; const scaledHeight = originalHeight * scale; // Position the container's transform origin point at the center of the window const left = (windowWidth - originalWidth) / 2; const top = (windowHeight - originalHeight) / 2; canvasContainer.style.left = `${left}px`; canvasContainer.style.top = `${top}px`; } function setupBitcoinData() { // Initial fetch fetchBitcoinData(); // Set up polling interval setInterval(fetchBitcoinData, BITCOIN.POLLING_INTERVAL); } async function fetchBitcoinData() { try { // Use default block time as fallback let blockTime = BITCOIN.DEFAULT_BLOCK_TIME; // Attempt to get current block time from ordinals recursive endpoint try { const blockTimeResponse = await fetch('/r/blocktime'); if (blockTimeResponse.ok) { const blockTimeText = await blockTimeResponse.text(); const blockTimestamp = parseInt(blockTimeText.trim(), 10); if (blockTimestamp) { const currentTime = Math.floor(Date.now() / 1000); blockTime = Math.min(currentTime - blockTimestamp, BITCOIN.MAX_BLOCK_TIME); console.log(`Block time found: ${blockTime} seconds (${Math.floor(blockTime/60)}m ${blockTime%60}s)`); } } } catch (error) { console.log('Unable to fetch block time, using default animation speed'); } bitcoinData.blockTime = Math.min(Math.max(blockTime, 0), BITCOIN.MAX_BLOCK_TIME); updateBitcoinDerivedValues(); } catch (error) { console.warn('Error in Bitcoin data handling, using default animation speed'); // Reset to default values bitcoinData.blockTime = BITCOIN.DEFAULT_BLOCK_TIME; bitcoinData.speedMultiplier = 1.0; } } function updateBitcoinDerivedValues() { // Calculate speed multiplier based on block time const blockTime = bitcoinData.blockTime; // Find the appropriate speed multiplier based on current block time for (const multiplier of BITCOIN.ANIMATION_SPEED.MULTIPLIERS) { if (blockTime < multiplier.threshold) { bitcoinData.speedMultiplier = multiplier.value; break; } } } updateBitcoinDerivedValues(); function draw() { if (!isAnimating) return; clear(); image(staticBuffer, 0, 0); processWaves(trailBuffer, 1); image(fadeBuffer, 0, 0); image(trailBuffer, 0, 0); let allWavesStopped = sineWaves.every(wave => wave.stopped); if (allWavesStopped) { isAnimating = false; noLoop(); } } function windowResized() { positionCanvasContainer(); } async function findInterfaceForRadioWave(parentId) { // Extract the sequence number directly from the window.radioWaveIndex const radioWaveIndex = window.radioWaveIndex - 1; // Convert from 1-based to 0-based // Calculate which page the interface would be on const pageSize = 100; // API returns 100 IDs per page const pageIndex = Math.floor(radioWaveIndex / pageSize); // Calculate the position within the page const positionInPage = radioWaveIndex % pageSize; try { // Fetch the appropriate page of children const response = await fetch(`/r/children/${parentId}/${pageIndex}`); if (!response.ok) { throw new Error(`Failed to fetch children: ${response.status}`); } const data = await response.json(); // Get the interface inscription at that position if (data.ids && data.ids.length > positionInPage) { return data.ids[positionInPage]; } else { console.error(`No interface found at index ${radioWaveIndex}`); return null; } } catch (error) { console.error("Error fetching interface:", error); return null; } } function loadMp4Muxer() { return new Promise((resolve, reject) => { if (typeof Mp4Muxer !== 'undefined') { console.log("MP4 muxer already loaded"); resolve(); return; } const muxerInscriptionId = "7a2a4dc2cc3692275c9b394da177acaa91e5b972ce695f2e451c1ee09404ebafi0"; console.log("Attempting to load MP4 muxer from inscription:", muxerInscriptionId); const script = document.createElement('script'); script.type = 'module'; script.src = `/content/${muxerInscriptionId}`; script.onload = () => { if (typeof window.Mp4Muxer !== 'undefined') { resolve(); } else { // Try alternative approach for module loading try { const extractModuleExports = new Function(` return import('/content/${muxerInscriptionId}') .then(module => { window.Mp4Muxer = module; document.dispatchEvent(new Event('mp4MuxerLoaded')); }); `); extractModuleExports(); document.addEventListener('mp4MuxerLoaded', () => { if (typeof window.Mp4Muxer !== 'undefined') { resolve(); } else { reject(new Error("MP4 muxer failed to initialize after loading script")); } }, { once: true }); } catch (error) { reject(new Error("Failed to load MP4 muxer as module")); } } }; script.onerror = (e) => { console.error("Error loading MP4 muxer script:", e); reject(new Error("Failed to load MP4 muxer script")); }; document.head.appendChild(script); }); } async function captureMP4(filename, density = 1) { try { // First ensure MP4 muxer is loaded await loadMp4Muxer(); if (typeof Mp4Muxer === 'undefined') { throw new Error('MP4 muxer library not loaded or not available.'); } const { Muxer, ArrayBufferTarget } = Mp4Muxer; // Consistent speed multiplier calculation from Bitcoin data const speedMultiplier = bitcoinData.speedMultiplier; console.log(`Using Bitcoin speed multiplier: ${speedMultiplier.toFixed(1)}x`); // Add a compensation factor to make MP4 and browser speeds match // This factor slows down the MP4 export to match browser playback const MP4_SPEED_COMPENSATION = 0.8; // Adjust this value as needed const effectiveSpeedMultiplier = speedMultiplier * MP4_SPEED_COMPENSATION; console.log(`Applying MP4 speed compensation factor: ${MP4_SPEED_COMPENSATION}`); console.log(`Effective speed multiplier for MP4: ${effectiveSpeedMultiplier.toFixed(2)}x`); const muxer = new Muxer({ target: new ArrayBufferTarget(), video: { codec: 'avc', width: width, height: height }, fastStart: 'in-memory' }); const videoEncoder = new VideoEncoder({ output: (chunk, meta) => muxer.addVideoChunk(chunk, meta), error: (e) => console.error('Video Encoder Error:', e) }); // Configure framerate with compensation factor const baseFramerate = 60; const adjustedFramerate = baseFramerate * effectiveSpeedMultiplier; await videoEncoder.configure({ codec: 'avc1.42001f', width: width, height: height, bitrate: 5000000, framerate: adjustedFramerate }); // Create canvas for capturing const canvas = createGraphics(width, height); canvas.pixelDensity(density); canvas.colorMode(HSB, 360, 100, 100, 100); // Draw static elements once const staticCanvas = createGraphics(width, height); staticCanvas.pixelDensity(density); staticCanvas.colorMode(HSB, 360, 100, 100, 100); staticCanvas.clear(); staticCanvas.image(staticBuffer, 0, 0, width, height); // Draw border once if needed if (borderTrait === BORDER_OPTIONS.THIN) { drawBorder(staticCanvas); } console.log(`Encoding ${window.prerenderedFrames.length} frames to MP4...`); console.log(`Original framerate: ${baseFramerate} fps`); console.log(`Adjusted framerate: ${adjustedFramerate.toFixed(2)} fps`); // Base frame time in microseconds (with compensation) const baseFrameTime = 1000000 / baseFramerate; const adjustedFrameTime = baseFrameTime / effectiveSpeedMultiplier; console.log(`Base frame time: ${baseFrameTime.toFixed(2)} µs`); console.log(`Adjusted frame time: ${adjustedFrameTime.toFixed(2)} µs`); for (let i = 0; i < window.prerenderedFrames.length; i++) { const frame = window.prerenderedFrames[i]; // Process frame buffer to clear edge pixels if (frame.trailBuffer) { clearEdgePixels(frame.trailBuffer, 3); } // Clear main canvas completely canvas.clear(); // Draw static elements from prepared canvas canvas.image(staticCanvas, 0, 0, width, height); // Draw animation frames canvas.image(frame.fadeBuffer, 0, 0, width, height); canvas.image(frame.trailBuffer, 0, 0, width, height); // Calculate timestamp and duration with compensation const videoFrame = new VideoFrame(canvas.elt, { timestamp: i * adjustedFrameTime, duration: adjustedFrameTime }); const keyFrame = i % 60 === 0; await videoEncoder.encode(videoFrame, { keyFrame }); videoFrame.close(); // Log progress every 60 frames if (i % 60 === 0) { console.log(`Encoding progress: ${Math.round((i / window.prerenderedFrames.length) * 100)}%`); } } console.log("Finalizing MP4..."); await videoEncoder.flush(); videoEncoder.close(); muxer.finalize(); console.log("Creating download blob..."); const blob = new Blob([muxer.target.buffer], { type: 'video/mp4' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${filename}-${density}x.mp4`; a.click(); URL.revokeObjectURL(url); // Cleanup canvas.remove(); staticCanvas.remove(); console.log("MP4 export complete!"); } catch (error) { console.error('MP4 capture error:', error); alert('Failed to capture MP4: ' + error.message); } } function handleExport(exportType, filename) { if (exportType === 'PNG') { const EXPORT_DENSITY = keyIsDown(SHIFT) ? 4 : 1; const saveCanvas = createGraphics(width, height); saveCanvas.pixelDensity(EXPORT_DENSITY); saveCanvas.colorMode(HSB, 360, 100, 100, 100); // Draw content with proper scaling saveCanvas.image(get(), 0, 0, width, height); save(saveCanvas, `${filename}-${EXPORT_DENSITY}x.png`); saveCanvas.remove(); } else if (exportType === 'MP4') { const EXPORT_DENSITY = keyIsDown(SHIFT) ? 2 : 1; captureMP4(filename, EXPORT_DENSITY); } } function exportWaveColorData() { // Create data object with only what's needed to reproduce colors const colorData = { seed: window.currentSeed, // Core color mode settings that directly affect color generation colorMode: { colorTrait, // Main color mode (Rainbow, Gradient, Single Color, Dual, Monochrome, Inverse) colorSubTrait, // Sub-trait specific to the color mode amplitudeMode: window.amplitudeMode, // Normal or Extreme extremeColorMode: window.extremeColorMode, // Color handling in extreme mode dualMode: window.dualMode // GRADIENT or LAYERED mode for dual colors }, // Essential color constants used in calculations constants: { // Core color defaults saturation: { min: COLOR_DEFAULTS.SATURATION.MIN, max: COLOR_DEFAULTS.SATURATION.MAX }, brightness: { min: COLOR_DEFAULTS.BRIGHTNESS.MIN, max: COLOR_DEFAULTS.BRIGHTNESS.MAX, steps: COLOR_DEFAULTS.BRIGHTNESS.STEPS }, hueStep: COLOR_DEFAULTS.HUE_STEP, gradientHueStep: COLOR_DEFAULTS.GRADIENT_HUE_STEP, // Alpha values used throughout alpha: { standard: ALPHA_VALUES.STANDARD, full: ALPHA_VALUES.FULL, faint: ALPHA_VALUES.FAINT }, // Gradient settings singleColorGradient: { saturation: SINGLE_COLOR_GRADIENT.SATURATION, brightness: SINGLE_COLOR_GRADIENT.BRIGHTNESS, hue: SINGLE_COLOR_GRADIENT.HUE, rate: SINGLE_COLOR_GRADIENT.RATE } }, // All actual colors used in waves waveColors: sineWaves.map(wave => ({ baseHue: wave.baseHue, hue: wave.hue, saturation: wave.saturation, brightness: wave.brightness, opacity: wave.opacity })), // Complete color history for all waves that appeared colorHistory: waveColorHistory.map(color => ({ hue: color.hue, saturation: color.saturation, brightness: color.brightness, alpha: color.alpha || 100 })), // Dual color pair specific data if relevant dualColorPair: colorTrait === "Dual" ? (() => { const pairName = colorSubTrait; const pair = Object.values(DUAL_COLOR_PAIRS).find(p => p.name === pairName); if (pair) { return { name: pair.name, start: { ...pair.start }, end: { ...pair.end } }; } return null; })() : null, // Extreme mode specific colors if relevant extremeColors: window.amplitudeMode === AMPLITUDE_MODES.EXTREME.TYPE ? { tallWaveHue: window.tallWaveHue, shortWaveHue: window.shortWaveHue, waveHueRanges: WAVE_HUE_RANGES } : null, // Datastream color if used dataStreamColor: hasDataStream ? dataStreamColor : null, // Border color if relevant borderWaveColor: cachedBorderWaveColor }; // Convert to JSON string const jsonData = JSON.stringify(colorData, null, 2); // Create download const blob = new Blob([jsonData], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); // Create filename with seed const filename = `wave-colors-${window.currentSeed}.json`; a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); console.log("Essential color data exported:", filename); } function keyPressed() { let filename = `wave-${waveCountTrait}-${waveTypeTrait}-${colorTrait}`; if (colorTrait === "Single Color") { filename += `-${colorSubTrait}`; } else if (colorTrait === "Dual") { filename += `-${colorSubTrait}`; // Adds color pair name (e.g., "Ocean Sunset") } else if (colorTrait === "Gradient") { filename += `-${colorSubTrait}`; // Adds "Variable Saturation" or "Fixed Saturation" } // PNG Export (Static High-Res Image) - 'P' key if (key === 'p' || key === 'P') { handleExport('PNG', filename); } // MP4 Export - 'V' key else if (key === 'v' || key === 'V') { handleExport('MP4', filename); } // Color data export - 'C' key else if (key === 'c' || key === 'C') { exportWaveColorData(); } // Animation playback control - Spacebar if (keyCode === 32) { if (!isPreRenderComplete) { console.log("Animation loading..."); let checkPrerender = setInterval(() => { if (isPreRenderComplete) { clearInterval(checkPrerender); playAnimation(); } }, 100); return; } if (isAnimating) { if (isPaused) { isPaused = false; playAnimation(currentFrame); } else { isPaused = true; noLoop(); } } else { playAnimation(); } } } function weightedRandom(items) { const totalWeight = items.reduce((sum, item) => sum + item.weight, 0); let randomValue = window.random() * totalWeight; for (const item of items) { randomValue -= item.weight; if (randomValue <= 0) { return item.value; } } // Fallback in case of rounding errors return items[items.length - 1].value; } function assignAdditionalTraits() { rfFrequencyTrait = weightedRandom(RF_FREQUENCY_TYPES); encryptedAudioTrait = weightedRandom(ENCRYPTED_AUDIO_TYPES); sonificationAudioTrait = weightedRandom(SONIFICATION_AUDIO_TYPES); } function logTraits() { console.log('🎲 SEED:', window.currentSeed); console.log("\nDesaturated"); console.log("\n=== Radio Wave Traits ==="); console.log("Canvas Orientation:", window.canvasType); console.log("Wave Count:", waveCountTrait); console.log("Color Classification:", colorTrait); console.log("Border Color:", colorTrait === "Inverse" ? "Black" : borderColor === BORDER_COLORS.MONOCHROME ? "White" : borderColor === BORDER_COLORS.DATASTREAM.WHITE || borderColor === BORDER_COLORS.BLACK.WHITE ? "White" : borderColor === BORDER_COLORS.DATASTREAM.WAVE || borderColor === BORDER_COLORS.BLACK.WAVE ? "Wave Color" : borderColor === BORDER_COLORS.DATASTREAM.GRADIENT || borderColor === BORDER_COLORS.BLACK.GRADIENT ? "Gradient" : "Unknown" ); console.log("Background:", backgroundTrait); console.log("\nRF Frequency:", rfFrequencyTrait); console.log("Encrypted Audio:", encryptedAudioTrait); console.log("Sonification Audio:", sonificationAudioTrait); console.groupCollapsed("📊 Metadata - Click to expand"); console.log("\n=== METADATA START ==="); const metadata = { // Core color mode settings that directly affect color generation colorMode: { colorTrait, // Main color mode (Rainbow, Gradient, Single Color, Dual, Monochrome, Inverse) colorSubTrait, // Sub-trait specific to the color mode amplitudeMode: window.amplitudeMode, // Normal or Extreme extremeColorMode: window.extremeColorMode, // Color handling in extreme mode dualMode: window.dualMode // GRADIENT or LAYERED mode for dual colors }, // Essential color constants used in calculations constants: { // Core color defaults saturation: { min: COLOR_DEFAULTS.SATURATION.MIN, max: COLOR_DEFAULTS.SATURATION.MAX }, brightness: { min: COLOR_DEFAULTS.BRIGHTNESS.MIN, max: COLOR_DEFAULTS.BRIGHTNESS.MAX, steps: COLOR_DEFAULTS.BRIGHTNESS.STEPS }, hueStep: COLOR_DEFAULTS.HUE_STEP, gradientHueStep: COLOR_DEFAULTS.GRADIENT_HUE_STEP, // Alpha values used throughout alpha: { standard: ALPHA_VALUES.STANDARD, full: ALPHA_VALUES.FULL, faint: ALPHA_VALUES.FAINT }, // Gradient settings singleColorGradient: { saturation: SINGLE_COLOR_GRADIENT.SATURATION, brightness: SINGLE_COLOR_GRADIENT.BRIGHTNESS, hue: SINGLE_COLOR_GRADIENT.HUE, rate: SINGLE_COLOR_GRADIENT.RATE } }, // All actual colors used in waves waveColors: sineWaves.map(wave => ({ baseHue: wave.baseHue, hue: wave.hue, saturation: wave.saturation, brightness: wave.brightness, opacity: wave.opacity })), // Complete color history for all waves that appeared colorHistory: waveColorHistory.map(color => ({ hue: color.hue, saturation: color.saturation, brightness: color.brightness, alpha: color.alpha || 100 })), // Dual color pair specific data if relevant dualColorPair: colorTrait === "Dual" ? (() => { const pairName = colorSubTrait; const pair = Object.values(DUAL_COLOR_PAIRS).find(p => p.name === pairName); if (pair) { return { name: pair.name, start: { ...pair.start }, end: { ...pair.end } }; } return null; })() : null, // Extreme mode specific colors if relevant extremeColors: window.amplitudeMode === AMPLITUDE_MODES.EXTREME.TYPE ? { tallWaveHue: window.tallWaveHue, shortWaveHue: window.shortWaveHue, waveHueRanges: WAVE_HUE_RANGES } : null, // Datastream color if used dataStreamColor: hasDataStream ? dataStreamColor : null, // Border color if relevant borderWaveColor: cachedBorderWaveColor, // New traits rfFrequency: rfFrequencyTrait, encryptedAudio: encryptedAudioTrait, sonificationAudio: sonificationAudioTrait }; // Output exact metadata in a scrapable format console.log(JSON.stringify(metadata, null, 2)); console.log("=== METADATA END ==="); }