Zum Inhalt

macos/Sources/PerlinLiveWallpaper/Resources/studio/perlin-presets.js

/**
 * perlin-presets.js — colour gradients, built-in presets, and param resolution.
 *
 * Shared by the studio controller (perlin-studio.js) and the embeddable
 * <perlin-noise> element (perlin-element.js) so both turn a preset into a
 * renderable params object identically.
 *
 * Plain script: attaches `window.MTPerlinPresets`. Also CommonJS-exportable so
 * the smoke test can import `resolveParams` under Node.
 *
 * @license AGPL-3.0-or-later
 */
(function (root) {
  "use strict";

  // ============= GRADIENTS =============
  const GRADIENTS = {
    viridis: [
      { pos: 0.0, color: [68, 1, 84] },
      { pos: 0.25, color: [59, 82, 139] },
      { pos: 0.5, color: [33, 145, 140] },
      { pos: 0.75, color: [94, 201, 98] },
      { pos: 1.0, color: [253, 231, 37] },
    ],
    magma: [
      { pos: 0.0, color: [0, 0, 4] },
      { pos: 0.25, color: [80, 18, 123] },
      { pos: 0.5, color: [183, 55, 121] },
      { pos: 0.75, color: [251, 136, 97] },
      { pos: 1.0, color: [252, 253, 191] },
    ],
    fire: [
      { pos: 0.0, color: [0, 0, 0] },
      { pos: 0.3, color: [136, 0, 0] },
      { pos: 0.6, color: [255, 89, 0] },
      { pos: 0.85, color: [255, 213, 0] },
      { pos: 1.0, color: [255, 255, 240] },
    ],
    ocean: [
      { pos: 0.0, color: [3, 11, 41] },
      { pos: 0.3, color: [5, 56, 107] },
      { pos: 0.6, color: [0, 144, 178] },
      { pos: 0.85, color: [120, 220, 220] },
      { pos: 1.0, color: [240, 248, 255] },
    ],
    forest: [
      { pos: 0.0, color: [25, 35, 20] },
      { pos: 0.3, color: [40, 80, 30] },
      { pos: 0.6, color: [80, 140, 50] },
      { pos: 0.85, color: [180, 200, 110] },
      { pos: 1.0, color: [240, 245, 200] },
    ],
    grayscale: [
      { pos: 0.0, color: [0, 0, 0] },
      { pos: 1.0, color: [255, 255, 255] },
    ],
    sunset: [
      { pos: 0.0, color: [12, 12, 60] },
      { pos: 0.3, color: [110, 30, 100] },
      { pos: 0.6, color: [240, 110, 80] },
      { pos: 0.85, color: [255, 200, 90] },
      { pos: 1.0, color: [255, 245, 200] },
    ],
    ice: [
      { pos: 0.0, color: [10, 20, 50] },
      { pos: 0.4, color: [80, 130, 200] },
      { pos: 0.8, color: [180, 220, 250] },
      { pos: 1.0, color: [255, 255, 255] },
    ],
  };

  // ============= BUILTIN PRESETS =============
  // All presets default to loop=on with a small scaleT for a gentle ripple.
  // loopRadius = 1 traces a full circle in noise space; scaleT shrinks it so
  // the loop stays within a single cell-sized region.
  const LOOP_DEFAULTS = { loop: true, loopDuration: 4, loopRadius: 1.0 };

  // Default warp = off. Presets that want warp opt in explicitly.
  const WARP_OFF = { warpStrength: 0, warpScale: 1 };
  // Default layer 2 = off. Presets that want a composite opt in explicitly.
  const LAYER2_OFF = {
    layer2On: false,
    layer2ScaleX: 0.06, layer2ScaleY: 0.06,
    layer2Octaves: 4, layer2Base: 101,
    layer2BlendMode: "multiply", layer2Mix: 0.5,
  };

  const BUILTIN_PRESETS = {
    default: {
      width: 256, height: 256,
      scaleX: 0.011, scaleY: 0.011, scaleT: 0.15,
      octaves: 5, persistence: 0.62, lacunarity: 2.1, base: 0,
      colorMode: "gradient", gradientName: "viridis",
      ...WARP_OFF, ...LAYER2_OFF, ...LOOP_DEFAULTS,
    },
    lava: {
      width: 384, height: 256,
      scaleX: 0.015, scaleY: 0.015, scaleT: 0.12,
      octaves: 5, persistence: 0.65, lacunarity: 2.2, base: 7,
      colorMode: "gradient", gradientName: "fire",
      ...WARP_OFF, ...LAYER2_OFF, ...LOOP_DEFAULTS, loopDuration: 6,
    },
    aurora: {
      width: 384, height: 256,
      scaleX: 0.009, scaleY: 0.024, scaleT: 0.18,
      octaves: 5, persistence: 0.6, lacunarity: 2.4, base: 19,
      colorMode: "hsv", hsvHueShift: 0.30, hsvHueRange: 0.48, hsvSat: 0.88, hsvVal: 1.0,
      ...WARP_OFF, ...LAYER2_OFF, ...LOOP_DEFAULTS, loopDuration: 5,
    },
    clouds: {
      width: 384, height: 256,
      scaleX: 0.0075, scaleY: 0.0075, scaleT: 0.1,
      octaves: 7, persistence: 0.62, lacunarity: 2.1, base: 3,
      colorMode: "gradient", gradientName: "grayscale",
      ...WARP_OFF, ...LAYER2_OFF, ...LOOP_DEFAULTS, loopDuration: 8,
    },
    terrain: {
      width: 384, height: 256,
      scaleX: 0.0085, scaleY: 0.0085, scaleT: 0.08,
      octaves: 6, persistence: 0.58, lacunarity: 2.1, base: 42,
      colorMode: "posterize", posterizeSteps: 8, gradientName: "forest",
      ...WARP_OFF, ...LAYER2_OFF, ...LOOP_DEFAULTS, loopDuration: 10,
    },
    circuit: {
      width: 384, height: 256,
      scaleX: 0.024, scaleY: 0.024, scaleT: 0.18,
      octaves: 4, persistence: 0.6, lacunarity: 2.8, base: 11,
      colorMode: "posterize", posterizeSteps: 5, gradientName: "ocean",
      ...WARP_OFF, ...LAYER2_OFF, ...LOOP_DEFAULTS, loopDuration: 3,
    },
    // Domain-warp showcase: chunky low-octave field gets pulled into
    // marble-vein swirls by a slower, stronger warp field. Warm earthy
    // gradient sells the stone look.
    marble: {
      width: 384, height: 256,
      scaleX: 0.012, scaleY: 0.012, scaleT: 0.1,
      octaves: 4, persistence: 0.55, lacunarity: 2.2, base: 31,
      warpStrength: 1.6, warpScale: 0.6,
      colorMode: "gradient", gradientName: "sunset",
      ...LAYER2_OFF, ...LOOP_DEFAULTS, loopDuration: 8,
    },
    // Per-channel-noise showcase: three independent noise fields drive
    // R / G / B separately. Warp adds vein-like coherence.
    nebula: {
      width: 384, height: 256,
      scaleX: 0.013, scaleY: 0.013, scaleT: 0.12,
      octaves: 5, persistence: 0.6, lacunarity: 2.2, base: 53,
      warpStrength: 1.3, warpScale: 0.7,
      colorMode: "rgb",
      rScale: 1.4, gScale: 0.55, bScale: 1.5,
      rInvert: false, gInvert: true, bInvert: false,
      rgbMultiChannel: true,
      ...LAYER2_OFF, ...LOOP_DEFAULTS, loopDuration: 9,
    },
    // Flow-field showcase: noise rendered as vector-field directions, so
    // the eye reads it as a storm front or a wind map. High gain pushes
    // even gentle gradients to readable saturation.
    flow: {
      width: 384, height: 256,
      scaleX: 0.02, scaleY: 0.02, scaleT: 0.1,
      octaves: 5, persistence: 0.5, lacunarity: 2.0, base: 23,
      ...WARP_OFF,
      colorMode: "flow",
      flowHueShift: 0.0, flowSat: 0.9, flowGain: 14, flowFloor: 0.24, flowEps: 0.5,
      ...LAYER2_OFF, ...LOOP_DEFAULTS, loopDuration: 6,
    },
    // Layer-2 showcase: slow rolling base + fine, denser second layer in
    // multiply blend. Reads like clouds with embossed contours. Composed
    // on the ice gradient for a frozen-pane look.
    composite: {
      width: 384, height: 256,
      scaleX: 0.008, scaleY: 0.008, scaleT: 0.1,
      octaves: 4, persistence: 0.5, lacunarity: 2.0, base: 17,
      ...WARP_OFF,
      layer2On: true,
      layer2ScaleX: 0.05, layer2ScaleY: 0.05,
      layer2Octaves: 5, layer2Base: 211,
      layer2BlendMode: "multiply", layer2Mix: 0.6,
      colorMode: "gradient", gradientName: "magma",
      ...LOOP_DEFAULTS, loopDuration: 7,
    },
  };

  /**
   * Returns a new, fully-resolved params object the engine/worker can consume.
   * For gradient/posterize modes with a named gradient but no explicit stops,
   * injects the concrete stop list (falling back to viridis for unknown
   * names). Never mutates the input.
   *
   * @param {Object} preset  A preset or params snapshot (may be partial)
   * @returns {Object} A shallow clone with `gradient` resolved when applicable
   */
  function resolveParams(preset) {
    const p = Object.assign({}, preset);
    const mode = p.colorMode;
    if (mode === "gradient" || mode === "posterize") {
      const hasStops = Array.isArray(p.gradient) && p.gradient.length >= 2;
      if (!hasStops) {
        p.gradient = GRADIENTS[p.gradientName] || GRADIENTS.viridis;
      }
    }
    return p;
  }

  root.MTPerlinPresets = { GRADIENTS, BUILTIN_PRESETS, resolveParams };
  if (typeof module !== "undefined" && module.exports) {
    module.exports = { GRADIENTS, BUILTIN_PRESETS, resolveParams };
  }
})(typeof window !== "undefined" ? window : globalThis);