PageSourceSearch

https://pupwife.com/scripts/effects/backgroundTuning.js

js pupwife.com collected 2026-09-30 15:06:23 UTC 24,019 bytes, 575 lines download raw bytes

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.