1/** 2 * Per-background shader tuning â settings UI + live apply. 3 */ 4(function (global) { 5 'use strict'; 6 7 var STORAGE_COLLAPSED = 'pupwife_bg_tuning_collapsed'; 8 var LEGACY_CAUSTICS_NOISE_CUTOFF = 2; 9 10 function decField(key, label, def, min, max, step) { 11 return { key: key, label: label, min: min, max: max, step: step, default: def, format: 'decimal' }; 12 } 13 14 function colorFields(prefix, label, rgba) { 15 return [ 16 decField(prefix + 'R', label + ' red', rgba[0], 0, 1, 0.01), 17 decField(prefix + 'G', label + ' green', rgba[1], 0, 1, 0.01), 18 decField(prefix + 'B', label + ' blue', rgba[2], 0, 1, 0.01), 19 decField(prefix + 'A', label + ' alpha', rgba[3], 0, 1, 0.01) 20 ]; 21 } 22 23 var CLOUDS_GROUPS = [ 24 { 25 id: 'camera', 26 label: 'Camera', 27 icon: 'fa-camera', 28 fields: [ 29 decField('camPosX', 'Camera X', 0.0, -5, 5, 0.05), 30 decField('camPosY', 'Camera Y', 2.7, -2, 8, 0.05), 31 decField('camPosZ', 'Camera Z', -0.1, -8, 4, 0.05), 32 decField('camTargetX', 'Look-at X', 0.0, -5, 5, 0.05), 33 decField('camTargetY', 'Look-at Y', 1.7, -2, 8, 0.05), 34 decField('camTargetZ', 'Look-at Z', 6.0, -2, 12, 0.05), 35 decField('camFov', 'Camera FOV', 67.0, 20, 100, 1) 36 ] 37 }, 38 { 39 id: 'world', 40 label: 'World offset', 41 icon: 'fa-globe', 42 fields: [ 43 decField('worldOffsetX', 'World offset X', 125.0, 0, 200, 1), 44 decField('worldOffsetY', 'World offset Y', 0.0, -50, 50, 0.5), 45 decField('worldOffsetZ', 'World offset Z', 64.0, 0, 120, 0.5) 46 ] 47 }, 48 { 49 id: 'shape', 50 label: 'Cloud shape', 51 icon: 'fa-cloud', 52 fields: [ 53 decField('baseSize', 'Base size', 0.2, 0.05, 0.5, 0.01), 54 decField('baseOffset', 'Base offset', 0.5, -2, 2, 0.05), 55 decField('baseGradient', 'Base gradient', -3.3, -6, 2, 0.05), 56 decField('baseShape', 'Base shape', 1.54, 0.5, 4, 0.02), 57 decField('baseWeight', 'Base weight', 5.0, 0.5, 8, 0.05), 58 decField('detailWeight', 'Detail weight', 1.8, 0, 4, 0.05), 59 decField('detailCombine', 'Detail combine', 1.0, -1, 2, 0.05), 60 decField('densityCutoff', 'Density cutoff', 0.0, -1, 1, 0.05), 61 decField('detailCutoff', 'Detail cutoff', -1.2, -3, 1, 0.05) 62 ] 63 }, 64 { 65 id: 'animation', 66 label: 'Animation', 67 icon: 'fa-fan', 68 fields: [ 69 decField('animate', 'Animate speed', 0.2, 0, 2, 0.02), 70 decField('animateBase', 'Animate base', 0.2, -1, 2, 0.02), 71 decField('animateDetail', 'Animate detail', -0.6, -2, 2, 0.02), 72 decField('animateDetail2', 'Animate detail 2', 0.8, -2, 2, 0.02), 73 decField('animateBaseVx', 'Base drift X', 0.6, -2, 2, 0.02), 74 decField('animateBaseVy', 'Base drift Y', 0.2, -2, 2, 0.02), 75 decField('animateBaseVz', 'Base drift Z', 1.0, -2, 2, 0.02), 76 decField('animateDetailVx', 'Detail drift X', 1.0, -2, 2, 0.02), 77 decField('animateDetailVy', 'Detail drift Y', 0.31, -2, 2, 0.02), 78 decField('animateDetailVz', 'Detail drift Z', 0.55, -2, 2, 0.02), 79 decField('animateDetail2Vx', 'Detail 2 drift X', 1.0, -2, 2, 0.02), 80 decField('animateDetail2Vy', 'Detail 2 drift Y', 0.0, -2, 2, 0.02), 81 decField('animateDetail2Vz', 'Detail 2 drift Z', 0.0, -2, 2, 0.02) 82 ] 83 }, 84 { 85 id: 'colors', 86 label: 'Cloud colors', 87 icon: 'fa-palette', 88 fields: colorFields('lowDensityColor', 'Low density', [0.668, 0.586, 0.801, 0.043]) 89 .concat(colorFields('highDensityColor', 'High density', [0.317, 0.250, 0.368, 0.270])) 90 .concat(colorFields('sunColor', 'Sun tint', [1.0, 0.572, 0.41, 0.62])) 91 }, 92 { 93 id: 'sky', 94 label: 'Sky backdrop', 95 icon: 'fa-sun', 96 fields: colorFields('backgroundC', 'Sky background', [0.317, 0.250, 0.368, 0.886]) 97 .concat(colorFields('backgroundSunC', 'Sky sun glow', [0.698, 0.321, 0.239, 0.317])) 98 .concat([ 99 decField('backgroundSunSize', 'Sky sun size', 0.223, 0.05, 1, 0.01), 100 decField('sunDirX', 'Sun dir X', 0.1, -4, 4, 0.05), 101 decField('sunDirY', 'Sun dir Y', -0.1, -4, 4, 0.05), 102 decField('sunDirZ', 'Sun dir Z', -2.6, -4, 4, 0.05), 103 decField('sunCurve', 'Sun curve', 0.286, 0, 1, 0.01), 104 decField('sunOffset', 'Sun offset', 0.963, 0, 2, 0.01) 105 ]) 106 }, 107 { 108 id: 'render', 109 label: 'Ray marching', 110 icon: 'fa-gauge-high', 111 fields: [ 112 decField('near', 'Near fade', 10.4, 1, 30, 0.2), 113 decField('far', 'Far distance', 100, 20, 150, 1), 114 decField('farFade', 'Far fade', 0.9, 0.2, 1, 0.02), 115 decField('gamma', 'Gamma', 4.0, 1, 6, 0.1), 116 decField('alphaMax', 'Alpha max', 1.0, 0.2, 1, 0.02), 117 decField('stepSizeInside', 'Step inside clouds', 0.3, 0.05, 1, 0.02), 118 decField('stepSizeOutside', 'Step outside clouds', 0.8, 0.2, 2, 0.02), 119 decField('jitter', 'Ray jitter', 5.0, 0, 10, 0.1), 120 { key: 'quality', label: 'Ray quality', min: 0, max: 100, step: 1, default: 45, format: 'integer' },
121 { key: 'resolutionScale', label: 'Resolution scale', min: 25, max: 100, step: 5, default: 55, format: 'integer' } 122 ] 123 } 124 ]; 125 126 var SCHEMA = { 127 horizon: { 128 label: 'Horizon', 129 icon: 'fa-cloud-sun', 130 groups: [ 131 { 132 id: 'sky', 133 label: 'Sky & water', 134 icon: 'fa-water', 135 fields: [ 136 { key: 'skyExposure', label: 'Sky brightness', min: 0.4, max: 1.6, step: 0.02, default: 0.92, format: 'decimal' }, 137 { key: 'waterSkyReflect', label: 'Water reflection', min: 0.3, max: 1, step: 0.02, default: 0.97, format: 'decimal' }, 138 { key: 'normalInfluence', label: 'Normal strength', min: 0.2, max: 1, step: 0.02, default: 0.64, format: 'decimal' } 139 ] 140 }, 141 { 142 id: 'waves', 143 label: 'Waves', 144 icon: 'fa-wind', 145 fields: [ 146 { key: 'waveSwell', label: 'Wave swell', min: 0.5, max: 4, step: 0.05, default: 2.65, format: 'decimal' }, 147 { key: 'waveDisplace', label: 'Wave height', min: 0.1, max: 2, step: 0.02, default: 1.05, format: 'decimal' }, 148 { key: 'waveSteepness', label: 'Wave steepness', min: 0, max: 1.15, step: 0.02, default: 0.58, format: 'decimal' } 149 ] 150 }, 151 { 152 id: 'camera', 153 label: 'Camera', 154 icon: 'fa-camera', 155 fields: [ 156 { key: 'fieldOfView', label: 'Field of view', min: 0.4, max: 0.85, step: 0.01, default: 0.62, format: 'decimal' }, 157 { key: 'verticalScale', label: 'Vertical scale', min: 0.6, max: 1.1, step: 0.02, default: 0.88, format: 'decimal' } 158 ] 159 }, 160 { 161 id: 'performance', 162 label: 'Performance', 163 icon: 'fa-gauge-high', 164 fields: [ 165 { key: 'shaderQuality', label: 'Shader qual
165ity', min: 0, max: 100, step: 1, default: 40, format: 'integer' }, 166 { key: 'resolutionScale', label: 'Resolution scale', min: 25, max: 100, step: 5, default: 45, format: 'integer' } 167 ] 168 } 169 ] 170 }, 171 caustics: { 172 label: 'Caustics', 173 icon: 'fa-rainbow', 174 groups: [ 175 { 176 id: 'look', 177 label: 'Look', 178 icon: 'fa-eye', 179 fields: [ 180 { key: 'strength', label: 'Color strength', min: 0.15, max: 1.2, step: 0.02, default: 0.6, format: 'decimal' }, 181 { key: 'patternScale', label: 'Pattern scale', min: 1.2, max: 5, step: 0.1, default: 2.8, format: 'decimal' }, 182 { key: 'alpha', label: 'Opacity', min: 0.1, max: 0.85, step: 0.02, default: 0.4, format: 'decimal' } 183 ] 184 }, 185 { 186 id: 'motion', 187 label: 'Motion & texture', 188 icon: 'fa-sparkles', 189 fields: [ 190 { key: 'shimmer', label: 'Shimmer', min: 0, max: 0.35, step: 0.01, default: 0.1, format: 'decimal' }, 191 { key: 'colorSpeed', label: 'Color shift speed', min: 0.05, max: 0.8, step: 0.01, default: 0.3, format: 'decimal' }, 192 { key: 'noiseScale', label: 'Noise texture scale', min: 0.15, max: 2, step: 0.01, default: 0.69, format: 'decimal' } 193 ] 194 } 195 ] 196 }, 197 clouds: { 198 label: 'Clouds', 199 icon: 'fa-cloud', 200 groups: CLOUDS_GROUPS 201 } 202 }; 203 204 function getPresetGroups(preset) { 205 if (!preset) return []; 206 if (preset.groups && preset.groups.length) return preset.groups; 207 if (preset.fields && preset.fields.length) { 208 return [{ id: 'main', label: preset.label, icon: preset.icon || 'fa-sliders', fields: preset.fields }]; 209 } 210 return []; 211 } 212 213 function getPresetFields(preset) { 214 var groups = getPresetGroups(preset); 215 var fields = []; 216 groups.forEach(function (group) { 217 if (group.fields) fields = fields.concat(group.fields); 218 }); 219 return fields; 220 } 221 222 function findField(presetId, key) { 223 var preset = SCHEMA[presetId]; 224 if (!preset) return null; 225 var fields = getPresetFields(preset); 226 for (var i = 0; i < fields.length; i++) { 227 if (fields[i].key === key) return fields[i]; 228 } 229 return null; 230 } 231 232 function migratePresetPartial(presetId, partial) { 233 if (!partial || typeof partial !== 'object') return partial; 234 if (presetId !== 'caustics') return partial; 235 if (partial.noiseScale === undefined || partial.noiseScale === null) return partial; 236 if (Number(partial.noiseScale) > LEGACY_CAUSTICS_NOISE_CUTOFF) { 237 var next = {}; 238 Object.keys(partial).forEach(function (k) { next[k] = partial[k]; }); 239 next.noiseScale = SCHEMA.caustics.groups[1].fields[2].default; 240 return next; 241 } 242 return partial; 243 } 244 245 function clampField(presetId, field, value) { 246 var num = Number(value); 247 if (Number.isNaN(num)) num = field.default; 248 if (field.format === 'integer') num = Math.round(num); 249 return Math.min(field.max, Math.max(field.min, num)); 250 } 251 252 function defaultPresetTuning(presetId) { 253 var preset = SCHEMA[presetId]; 254 if (!preset) return {}; 255 var out = {}; 256 getPresetFields(preset).forEach(function (field) { 257 out[field.key] = field.default; 258 }); 259 return out; 260 } 261 262 function defaultTuning() { 263 return { 264 horizon: defaultPresetTuning('horizon'), 265 caustics: defaultPresetTuning('caustics'), 266 clouds: defaultPresetTuning('clouds') 267 }; 268 } 269 270 function mergePresetTuning(presetId, partial) { 271 var base = defaultPresetTuning(presetId); 272 partial = migratePresetPartial(presetId, partial); 273 if (!partial || typeof partial !== 'object') return base;
274 getPresetFields(SCHEMA[presetId]).forEach(function (field) { 275 if (partial[field.key] !== undefined && partial[field.key] !== null) { 276 base[field.key] = clampField(presetId, field, partial[field.key]); 277 } 278 }); 279 return base; 280 } 281 282 function mergeSettings(stored) { 283 var merged = defaultTuning(); 284 if (!stored || typeof stored !== 'object') return merged; 285 merged.horizon = mergePresetTuning('horizon', stored.horizon); 286 merged.caustics = mergePresetTuning('caustics', stored.caustics); 287 merged.clouds = mergePresetTuning('clouds', stored.clouds); 288 return merged; 289 } 290 291 function readTuning() { 292 if (global.SettingsMenu && typeof global.SettingsMenu.getSetting === 'function') { 293 return mergeSettings(global.SettingsMenu.getSetting('backgroundTuning')); 294 } 295 try { 296 var raw = localStorage.getItem('puppywifey_settings'); 297 if (raw) { 298 var parsed = JSON.parse(raw); 299 return mergeSettings(parsed && parsed.backgroundTuning); 300 } 301 } catch (e) { /* ignore */ } 302 return defaultTuning(); 303 } 304 305 function applyPreset(presetId, tuning) { 306 tuning = mergeSettings(tuning); 307 if (presetId === 'horizon' && global.HorizonBackground && 308 typeof global.HorizonBackground.applyTuning === 'function') { 309 global.HorizonBackground.applyTuning(tuning.horizon); 310 } else if (presetId === 'caustics' && global.CausticsBackground && 311 typeof global.CausticsBackground.applyTuning === 'function') { 312 global.CausticsBackground.applyTuning(tuning.caustics); 313 } else if (presetId === 'clouds' && global.CloudsBackground && 314 typeof global.CloudsBackground.applyTuning === 'function') { 315 global.CloudsBackground.applyTuning(tuning.clouds); 316 } 317 } 318 319 function applyAll(tuning) { 320 tuning = mergeSettings(tuning); 321 applyPreset('horizon', tuning); 322 applyPreset('caustics', tuning); 323 applyPreset('clouds', tuning); 324 } 325 326 function formatValue(field, value) { 327 if (field.format === 'integer') return String(Math.round(value)); 328 var decimals = field.step >= 0.1 ? 1 : 2; 329 return Number(value).toFixed(decimals); 330 } 331 332 function saveTuning(tuning, changedPresetId) { 333 tuning = mergeSettings(tuning); 334 335 if (global.SettingsMenu && typeof global.SettingsMenu.setSetting === 'function') { 336 global.SettingsMenu.setSetting('backgroundTuning', tuning); 337 } else { 338 try { 339 var raw = localStorage.getItem('puppywifey_settings'); 340 var parsed = raw ? JSON.parse(raw) : {}; 341 parsed.backgroundTuning = tuning; 342 localStorage.setItem('puppywifey_settings', JSON.stringify(parsed)); 343 } catch (e) { /* ignore */ } 344 } 345 346 if (changedPresetId) { 347 applyPreset(changedPresetId, tuning); 348 } else { 349 applyAll(tuning); 350 } 351 return tuning; 352 } 353 354 function buildSliderRow(presetId, field, value, onInput) { 355 var row = document.createElement('div'); 356 row.className = 'settings-field settings-woim-slider'; 357 row.dataset.preset = presetId; 358 row.dataset.key = field.key; 359 360 var label = document.createElement('span'); 361 label.className = 'settings-field__label'; 362 label.textContent = field.label; 363 364 var wrap = document.createElement('div'); 365 wrap.className = 'settings-trail-amount'; 366 367 var inputId = 'setting-bg-' + presetId + '-' + field.key; 368 var input = document.createElement('input'); 369 input.type = 'range'; 370 input.className = 'settings-trail-amount__slider settings-citrus-slider'; 371 input.id = inputId; 372 input.min = String(field.min); 373 input.max = String(field.max); 374 input.step = String(field.step); 375 input.value = String(value); 376 377 var output = document.createElement('output'); 378 output.className = 'settings-trail-amount__value'; 379 output.setAttribute('for', inputId); 380 output.textContent = formatValue(field, value); 381 382 input.addEventListener('input', function () { 383 var next = clampField(presetId, field, input.value);
384 input.value = String(next); 385 output.textContent = formatValue(field, next); 386 onInput(presetId, field.key, next); 387 }); 388 389 wrap.appendChild(input); 390 wrap.appendChild(output); 391 row.appendChild(label); 392 row.appendChild(wrap); 393 return row; 394 } 395 396 function bindSubpanelToggle(subpanel, toggle, startCollapsed) { 397 var collapsed = !!startCollapsed; 398 399 function applyCollapsed(state) { 400 collapsed = !!state; 401 subpanel.classList.toggle('is-collapsed', collapsed); 402 toggle.setAttribute('aria-expanded', collapsed ? 'false' : 'true'); 403 } 404 405 applyCollapsed(collapsed); 406 toggle.addEventListener('click', function () { 407 applyCollapsed(!collapsed); 408 }); 409 } 410 411 function buildGroupSubpanel(presetId, group, groupIndex, tuning, onInput) { 412 var subpanel = document.createElement('div'); 413 subpanel.className = 'settings-woim-panel settings-bg-tuning-subpanel is-visible'; 414 if (groupIndex > 0) subpanel.classList.add('is-collapsed'); 415 416 var toggle = document.createElement('button'); 417 toggle.type = 'button'; 418 toggle.className = 'settings-woim-panel__toggle'; 419 toggle.setAttribute('aria-expanded', groupIndex === 0 ? 'true' : 'false'); 420 toggle.innerHTML = 421 '<span class="settings-woim-panel__toggle-label">' + 422 '<i class="fa-solid ' + group.icon + ' fa-width-auto" aria-hidden="true"></i> ' + 423 group.label + 424 '</span>' + 425 '<i class="fa-solid fa-chevron-down settings-woim-panel__chevron fa-width-auto" aria-hidden="true"></i>'; 426 427 var body = document.createElement('div'); 428 body.className = 'settings-woim-panel__body'; 429 430 group.fields.forEach(function (field) { 431 body.appendChild(buildSliderRow(presetId, field, tuning[presetId][field.key], onInput)); 432 }); 433 434 subpanel.appendChild(toggle); 435 subpanel.appendChild(body); 436 bindSubpanelToggle(subpanel, toggle, groupIndex > 0); 437 return subpanel; 438 } 439 440 function mount(host, options) { 441 options = options || {}; 442 if (!host) return null; 443 444 var tuning = mergeSettings(options.tuning); 445 var activeBackground = options.activeBackground || 'caustics'; 446 var onChange = typeof options.onChange === 'function' ? options.onChange : null; 447 448 function renderActivePreset() { 449 host.innerHTML = ''; 450 var preset = SCHEMA[activeBackground]; 451 if (!preset) { 452 var empty = document.createElement('p'); 453 empty.className = 'settings-bg-tuning-empty'; 454 empty.textContent = 'No tuning options for this background yet.'; 455 host.appendChild(empty); 456 return; 457 } 458 459 var group = document.createElement('div'); 460 group.className = 'settings-bg-tuning-group settings-bg-tuning-group--active'; 461 group.dataset.preset = activeBackground; 462 463 var title = document.createElement('h3'); 464 title.className = 'settings-woim-panel__title settings-bg-tuning-heading'; 465 title.innerHTML = '<i class="fa-solid ' + preset.icon + ' fa-width-auto" aria-hidden="true"></i> ' + preset.label; 466 467 var resetBtn = document.createElement('button'); 468 resetBtn.type = 'button'; 469 resetBtn.className = 'settings-bg-tuning-reset'; 470 resetBtn.textContent = 'Reset ' + preset.label; 471 resetBtn.addEventListener('click', function () { 472 var next = mergeSettings(tuning); 473 next[activeBackground] = defaultPresetTuning(activeBackground); 474 tuning = next; 475 refreshValues(); 476 if (onChange) onChange(tuning, activeBackground); 477 }); 478 479 group.appendChild(title); 480 481 var groups = getPresetGroups(preset); 482 if (groups.length === 1) { 483 groups[0].fields.forEach(function (field) { 484 group.appendChild(buildSliderRow(activeBackgroun
484d, field, tuning[activeBackground][field.key], handleInput)); 485 }); 486 } else { 487 groups.forEach(function (subgroup, index) { 488 group.appendChild(buildGroupSubpanel(activeBackground, subgroup, index, tuning, handleInput)); 489 }); 490 } 491 492 group.appendChild(resetBtn); 493 host.appendChild(group); 494 } 495 496 function handleInput(pid, key, val) { 497 tuning[pid][key] = val; 498 if (onChange) onChange(tuning, pid); 499 } 500 501 function refreshValues() { 502 host.querySelectorAll('.settings-field[data-preset]').forEach(function (row) { 503 var pid = row.dataset.preset; 504 var key = row.dataset.key; 505 var field = findField(pid, key); 506 if (!field) return; 507 var input = row.querySelector('input[type="range"]'); 508 var output = row.querySelector('output'); 509 var val = tuning[pid][key]; 510 if (input) input.value = String(val); 511 if (output) output.textContent = formatValue(field, val); 512 }); 513 } 514 515 function setActiveBackground(id) { 516 if (!SCHEMA[id]) id = 'caustics'; 517 if (id === activeBackground && host.childElementCount) return; 518 activeBackground = id; 519 renderActivePreset(); 520 } 521 522 function setTuning(next) { 523 tuning = mergeSettings(next); 524 refreshValues(); 525 } 526 527 renderActivePreset(); 528 529 return { 530 setTuning: setTuning,
531 setActiveBackground: setActiveBackground, 532 getTuning: function () { return mergeSettings(tuning); } 533 }; 534 } 535 536 function bindCollapsible(panel) { 537 var wrap = panel.querySelector('#setting-background-tuning'); 538 var toggle = panel.querySelector('#setting-background-tuning-toggle'); 539 if (!wrap || !toggle || wrap.dataset.collapseBound === '1') return; 540 wrap.dataset.collapseBound = '1'; 541 542 var collapsed = true; 543 try { 544 var stored = localStorage.getItem(STORAGE_COLLAPSED); 545 if (stored === '0') collapsed = false; 546 } catch (e) { /* ignore */ } 547 548 function applyCollapsed(state) { 549 collapsed = !!state; 550 wrap.classList.toggle('is-collapsed', collapsed); 551 toggle.setAttribute('aria-expanded', collapsed ? 'false' : 'true'); 552 try { 553 localStorage.setItem(STORAGE_COLLAPSED, collapsed ? '1' : '0'); 554 } catch (err) { /* ignore */ } 555 } 556 557 applyCollapsed(collapsed); 558 toggle.addEventListener('click', function () { 559 applyCollapsed(!collapsed); 560 }); 561 } 562 563 global.SiteBackgroundTuning = { 564 SCHEMA: SCHEMA, 565 getPresetFields: getPresetFields, 566 defaultTuning: defaultTuning, 567 mergeSettings: mergeSettings, 568 readTuning: readTuning, 569 applyAll: applyAll, 570 applyPreset: applyPreset, 571 saveTuning: saveTuning, 572 mount: mount, 573 bindCollapsible: bindCollapsible 574 }; 575})(typeof window !== 'undefined' ? window : this);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.