(function attachPyroEffects(root) {
  'use strict';

  const MAX_EFFECTS = 16;
  const finite = (value, fallback = 0) => Number.isFinite(Number(value)) ? Number(value) : fallback;
  const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
  const round = (value, digits = 3) => Number(finite(value).toFixed(digits));
  const colorPattern = /^#[0-9a-f]{6}$/i;
  const DitherPatterns = root.PyroDitherPatterns
    || (typeof require === 'function' ? require('./dither-patterns.js') : null);

  const numeric = (key, label, min, max, step, unit = '', options = {}) => Object.freeze({ key, label, kind: 'number', min, max, step, unit, ...options });
  const color = (key, label) => Object.freeze({ key, label, kind: 'color' });
  const boolean = (key, label) => Object.freeze({ key, label, kind: 'boolean' });
  const choice = (key, label, options) => Object.freeze({
    key, label, kind: 'select',
    options: Object.freeze(options.map((option) => Object.freeze({ value: option.value, label: option.label, group: option.group || '' })))
  });
  const identityCurve = Object.freeze([Object.freeze([0, 0]), Object.freeze([255, 255])]);

  const effectDefinitions = Object.freeze({
    'outer-glow': Object.freeze({
      label: 'Außenglühen', category: 'shadow-glow', scope: 'all',
      defaults: Object.freeze({ size: 5, opacity: 65, softness: 12, rounding: 100, color: '#ff334f' }),
      controls: Object.freeze([
        numeric('size', 'Größe', 0, 100, 1, 'px'), numeric('softness', 'Weichheit', 0, 100, 1, 'px'),
        numeric('rounding', 'Rundung', 0, 100, 1, '%'), numeric('opacity', 'Deckkraft', 0, 100, 1, '%'), color('color', 'Farbe')
      ])
    }),
    'inner-glow': Object.freeze({
      label: 'Inneres Glühen', category: 'shadow-glow', scope: 'all',
      defaults: Object.freeze({ size: 5, opacity: 65, softness: 12, rounding: 100, color: '#ffffff' }),
      controls: Object.freeze([
        numeric('size', 'Größe', 0, 100, 1, 'px'), numeric('softness', 'Weichheit', 0, 100, 1, 'px'),
        numeric('rounding', 'Rundung', 0, 100, 1, '%'), numeric('opacity', 'Deckkraft', 0, 100, 1, '%'), color('color', 'Farbe')
      ])
    }),
    'drop-shadow': Object.freeze({
      label: 'Schlagschatten', category: 'shadow-glow', scope: 'all',
      defaults: Object.freeze({ offsetX: 6, offsetY: 6, blur: 8, opacity: 45, color: '#000000' }),
      controls: Object.freeze([
        numeric('offsetX', 'X', -200, 200, 1, 'px'), numeric('offsetY', 'Y', -200, 200, 1, 'px'),
        numeric('blur', 'Weichheit', 0, 64, 1, 'px'), numeric('opacity', 'Deckkraft', 0, 100, 1, '%'), color('color', 'Farbe')
      ])
    }),
    'inner-shadow': Object.freeze({
      label: 'Innenschatten', category: 'shadow-glow', scope: 'all',
      defaults: Object.freeze({ angle: 45, distance: 4, softness: 4, opacity: 45, color: '#000000' }),
      controls: Object.freeze([
        numeric('angle', 'Winkel', -180, 180, 1, '°'), numeric('distance', 'Abstand', 0, 100, 1, 'px'),
        numeric('softness', 'Weichheit', 0, 64, 1, 'px'), numeric('opacity', 'Deckkraft', 0, 100, 1, '%'), color('color', 'Farbe')
      ])
    }),
    'solid-shadow': Object.freeze({
      label: 'Volltonschatten', category: 'shadow-glow', scope: 'all',
      defaults: Object.freeze({ angle: 45, distance: 8, opacity: 70, color: '#000000' }),
      controls: Object.freeze([
        numeric('angle', 'Winkel', -180, 180, 1, '°'), numeric('distance', 'Abstand', 0, 100, 1, 'px'),
        numeric('opacity', 'Deckkraft', 0, 100, 1, '%'), color('color', 'Farbe')
      ])
    }),
    'inner-bevel': Object.freeze({
      label: 'Innere Abschrägung', category: 'bevel-emboss', scope: 'all',
      defaults: Object.freeze({ width: 5, contrast: 75, softness: 2, angle: 135 }),
      controls: Object.freeze([
        numeric('width', 'Breite', 0, 50, 1, 'px'), numeric('contrast', 'Kontrast', 0, 100, 1, '%'),
        numeric('softness', 'Weichheit', 0, 64, 1, 'px'), numeric('angle', 'Winkel', -180, 180, 1, '°')
      ])
    }),
    'outer-bevel': Object.freeze({
      label: 'Äußere Abschrägung', category: 'bevel-emboss', scope: 'all',
      defaults: Object.freeze({ width: 5, contrast: 75, softness: 2, angle: 135 }),
      controls: Object.freeze([
        numeric('width', 'Breite', 0, 50, 1, 'px'), numeric('contrast', 'Kontrast', 0, 100, 1, '%'),
        numeric('softness', 'Weichheit', 0, 64, 1, 'px'), numeric('angle', 'Winkel', -180, 180, 1, '°')
      ])
    }),
    'inset-emboss': Object.freeze({
      label: 'Eingelassenes Relief', category: 'bevel-emboss', scope: 'all',
      defaults: Object.freeze({ width: 4, contrast: 75, softness: 2, angle: 135, showObject: true }),
      controls: Object.freeze([
        numeric('width', 'Breite', 0, 50, 1, 'px'), numeric('contrast', 'Kontrast', 0, 100, 1, '%'),
        numeric('softness', 'Weichheit', 0, 64, 1, 'px'), numeric('angle', 'Winkel', -180, 180, 1, '°'),
        boolean('showObject', 'Objekt anzeigen')
      ])
    }),
    'raised-emboss': Object.freeze({
      label: 'Erhöhtes Relief', category: 'bevel-emboss', scope: 'all',
      defaults: Object.freeze({ width: 4, contrast: 75, softness: 2, angle: 135, showObject: true }),
      controls: Object.freeze([
        numeric('width', 'Breite', 0, 50, 1, 'px'), numeric('contrast', 'Kontrast', 0, 100, 1, '%'),
        numeric('softness', 'Weichheit', 0, 64, 1, 'px'), numeric('angle', 'Winkel', -180, 180, 1, '°'),
        boolean('showObject', 'Objekt anzeigen')
      ])
    }),
    blur: Object.freeze({
      label: 'Weichzeichnen', category: 'blur', scope: 'all',
      defaults: Object.freeze({ radius: 1 }),
      controls: Object.freeze([numeric('radius', 'Radius', 0, 64, .1, 'px', { hidden: true })])
    }),
    'blur-more': Object.freeze({
      label: 'Stärker weichzeichnen', category: 'blur', scope: 'all',
      defaults: Object.freeze({ radius: 2 }),
      controls: Object.freeze([numeric('radius', 'Radius', 0, 64, .1, 'px', { hidden: true })])
    }),
    'gaussian-blur': Object.freeze({
      label: 'Gaußscher Weichzeichner…', category: 'blur', scope: 'all',
      defaults: Object.freeze({ radius: 4 }),
      controls: Object.freeze([numeric('radius', 'Radius', 0, 64, .1, 'px')])
    }),
    'motion-blur': Object.freeze({
      label: 'Bewegungsunschärfe…', category: 'blur', scope: 'all',
      defaults: Object.freeze({ angle: 0, distance: 12, quality: 2 }),
      controls: Object.freeze([
        numeric('angle', 'Winkel', -180, 180, 1, '°'),
        numeric('distance', 'Abstand', 0, 200, 1, 'px'),
        numeric('quality', 'Qualität', 1, 3, 1, '', { integer: true })
      ])
    }),
    'radial-blur': Object.freeze({
      label: 'Radialer Weichzeichner…', category: 'blur', scope: 'all',
      defaults: Object.freeze({ amount: 12, quality: 2, centerX: 50, centerY: 50 }),
      controls: Object.freeze([
        numeric('amount', 'Stärke', 0, 100, 1, 'px'),
        numeric('quality', 'Qualität', 1, 3, 1, '', { integer: true }),
        numeric('centerX', 'Zentrum X', 0, 100, 1, '%'),
        numeric('centerY', 'Zentrum Y', 0, 100, 1, '%')
      ])
    }),
    'zoom-blur': Object.freeze({
      label: 'Zoom-Weichzeichner…', category: 'blur', scope: 'all',
      defaults: Object.freeze({ amount: 12, quality: 2, centerX: 50, centerY: 50 }),
      controls: Object.freeze([
        numeric('amount', 'Stärke', 0, 100, 1, 'px'),
        numeric('quality', 'Qualität', 1, 3, 1, '', { integer: true }),
        numeric('centerX', 'Zentrum X', 0, 100, 1, '%'),
        numeric('centerY', 'Zentrum Y', 0, 100, 1, '%')
      ])
    }),
    'add-noise': Object.freeze({
      label: 'Rauschen hinzufügen…', category: 'noise', scope: 'all',
      defaults: Object.freeze({ amount: 25, color: false }),
      controls: Object.freeze([
        numeric('amount', 'Stärke', 1, 400, 1),
        boolean('color', 'Farbe')
      ])
    }),
    dither: Object.freeze({
      label: 'Dithering…', category: 'other', scope: 'all',
      defaults: Object.freeze({ method: 'bayer8', amount: 50, size: 1, levels: 5 }),
      controls: Object.freeze([
        choice('method', 'Methode', DitherPatterns?.choices || [
          { value: 'bayer4', label: 'Bayer 4×4' }, { value: 'bayer8', label: 'Bayer 8×8' },
          { value: 'scatter', label: 'Streuraster' }, { value: 'halftone', label: 'Halbtonpunkte' }
        ]),
        numeric('amount', 'Stärke', 0, 100, 1, '%'),
        numeric('size', 'Punktgröße', 1, 16, 1, 'px', { integer: true }),
        numeric('levels', 'Farbstufen', 2, 16, 1, '', { integer: true })
      ])
    }),
    'find-edges': Object.freeze({
      label: 'Kanten suchen', category: 'other', scope: 'all',
      defaults: Object.freeze({}), controls: Object.freeze([])
    }),
    'convert-to-alpha': Object.freeze({
      label: 'In Alpha konvertieren', category: 'other', scope: 'all',
      defaults: Object.freeze({}), controls: Object.freeze([])
    }),
    'face-line-eyes': Object.freeze({
      label: 'Augen · Striche', category: 'face', scope: 'all',
      description: 'Zwei schlichte, senkrechte Augen wie bei freundlichen Spielfiguren.',
      defaults: Object.freeze({ positionX: 0, positionY: 0, size: 100, spacing: 100, thickness: 100, color: '#172033' }),
      controls: Object.freeze([
        numeric('positionX', 'Position X', -50, 50, 1, '%'), numeric('positionY', 'Position Y', -50, 50, 1, '%'),
        numeric('size', 'Größe', 25, 200, 1, '%'), numeric('spacing', 'Abstand', 35, 200, 1, '%'),
        numeric('thickness', 'Stärke', 25, 200, 1, '%'), color('color', 'Farbe')
      ])
    }),
    'face-dot-eyes': Object.freeze({
      label: 'Augen · Punkte', category: 'face', scope: 'all',
      defaults: Object.freeze({ positionX: 0, positionY: 0, size: 100, spacing: 100, color: '#172033' }),
      controls: Object.freeze([
        numeric('positionX', 'Position X', -50, 50, 1, '%'), numeric('positionY', 'Position Y', -50, 50, 1, '%'),
        numeric('size', 'Größe', 25, 200, 1, '%'), numeric('spacing', 'Abstand', 35, 200, 1, '%'), color('color', 'Farbe')
      ])
    }),
    'face-comic-eyes': Object.freeze({
      label: 'Augen · Comic', category: 'face', scope: 'all',
      defaults: Object.freeze({ positionX: 0, positionY: 0, size: 100, spacing: 100, gazeX: 0, gazeY: 12, eyeColor: '#ffffff', pupilColor: '#172033' }),
      controls: Object.freeze([
        numeric('positionX', 'Position X', -50, 50, 1, '%'), numeric('positionY', 'Position Y', -50, 50, 1, '%'),
        numeric('size', 'Größe', 25, 200, 1, '%'), numeric('spacing', 'Abstand', 35, 200, 1, '%'),
        numeric('gazeX', 'Blick X', -100, 100, 1, '%'), numeric('gazeY', 'Blick Y', -100, 100, 1, '%'),
        color('eyeColor', 'Augenweiß'), color('pupilColor', 'Pupille & Kontur')
      ])
    }),
    'face-wink-eyes': Object.freeze({
      label: 'Augen · Zwinkern', category: 'face', scope: 'all',
      defaults: Object.freeze({ positionX: 0, positionY: 0, size: 100, spacing: 100, thickness: 100, color: '#172033' }),
      controls: Object.freeze([
        numeric('positionX', 'Position X', -50, 50, 1, '%'), numeric('positionY', 'Position Y', -50, 50, 1, '%'),
        numeric('size', 'Größe', 25, 200, 1, '%'), numeric('spacing', 'Abstand', 35, 200, 1, '%'),
        numeric('thickness', 'Stärke', 25, 200, 1, '%'), color('color', 'Farbe')
      ])
    }),
    'face-smile': Object.freeze({
      label: 'Mund · Lächeln', category: 'face', scope: 'all',
      defaults: Object.freeze({ positionX: 0, positionY: 0, size: 100, curvature: 100, thickness: 100, color: '#172033' }),
      controls: Object.freeze([
        numeric('positionX', 'Position X', -50, 50, 1, '%'), numeric('positionY', 'Position Y', -50, 50, 1, '%'),
        numeric('size', 'Größe', 25, 200, 1, '%'), numeric('curvature', 'Krümmung', -100, 200, 1, '%'),
        numeric('thickness', 'Stärke', 25, 200, 1, '%'), color('color', 'Farbe')
      ])
    }),
    'face-grin': Object.freeze({
      label: 'Mund · Grinsen', category: 'face', scope: 'all',
      defaults: Object.freeze({ positionX: 0, positionY: 0, size: 100, color: '#172033', toothColor: '#ffffff' }),
      controls: Object.freeze([
        numeric('positionX', 'Position X', -50, 50, 1, '%'), numeric('positionY', 'Position Y', -50, 50, 1, '%'),
        numeric('size', 'Größe', 25, 200, 1, '%'), color('color', 'Kontur'), color('toothColor', 'Zähne')
      ])
    }),
    'face-tongue': Object.freeze({
      label: 'Mund · Zunge', category: 'face', scope: 'all',
      defaults: Object.freeze({ positionX: 0, positionY: 0, size: 100, mouthColor: '#172033', tongueColor: '#ff5f87' }),
      controls: Object.freeze([
        numeric('positionX', 'Position X', -50, 50, 1, '%'), numeric('positionY', 'Position Y', -50, 50, 1, '%'),
        numeric('size', 'Größe', 25, 200, 1, '%'), color('mouthColor', 'Mund'), color('tongueColor', 'Zunge')
      ])
    }),
    'auto-color': Object.freeze({
      label: 'Automatische Tonwertkorrektur', category: 'adjust-color', scope: 'bitmap',
      description: 'Schwarz- und Weißpunkte werden beim Hinzufügen aus den Bitmapfarben berechnet.',
      defaults: Object.freeze({ redLow: 0, redHigh: 255, greenLow: 0, greenHigh: 255, blueLow: 0, blueHigh: 255 }),
      controls: Object.freeze([
        numeric('redLow', 'Rot tief', 0, 255, 1, '', { hidden: true }), numeric('redHigh', 'Rot hoch', 0, 255, 1, '', { hidden: true }),
        numeric('greenLow', 'Grün tief', 0, 255, 1, '', { hidden: true }), numeric('greenHigh', 'Grün hoch', 0, 255, 1, '', { hidden: true }),
        numeric('blueLow', 'Blau tief', 0, 255, 1, '', { hidden: true }), numeric('blueHigh', 'Blau hoch', 0, 255, 1, '', { hidden: true })
      ])
    }),
    'brightness-contrast': Object.freeze({
      label: 'Helligkeit / Kontrast…', category: 'adjust-color', scope: 'bitmap',
      defaults: Object.freeze({ brightness: 0, contrast: 0 }),
      controls: Object.freeze([
        numeric('brightness', 'Helligkeit', -100, 100, 1, '%'), numeric('contrast', 'Kontrast', -100, 100, 1, '%')
      ])
    }),
    'color-fill': Object.freeze({
      label: 'Farbfüllung', category: 'adjust-color', scope: 'bitmap',
      defaults: Object.freeze({ color: '#ff334f', opacity: 100 }),
      controls: Object.freeze([color('color', 'Farbe'), numeric('opacity', 'Deckkraft', 0, 100, 1, '%')])
    }),
    curves: Object.freeze({
      label: 'Gradationskurven…', category: 'adjust-color', scope: 'bitmap', editor: 'curves',
      defaults: Object.freeze({ rgbCurve: identityCurve, redCurve: identityCurve, greenCurve: identityCurve, blueCurve: identityCurve }),
      controls: Object.freeze([])
    }),
    'hue-saturation': Object.freeze({
      label: 'Farbton / Sättigung…', category: 'adjust-color', scope: 'bitmap',
      defaults: Object.freeze({ hue: 0, saturation: 100 }),
      controls: Object.freeze([
        numeric('hue', 'Farbton', -180, 180, 1, '°'), numeric('saturation', 'Sättigung', 0, 200, 1, '%')
      ])
    }),
    invert: Object.freeze({
      label: 'Invertieren', category: 'adjust-color', scope: 'bitmap',
      description: 'Kehrt die RGB-Farben um und lässt die Transparenz unverändert.',
      defaults: Object.freeze({}), controls: Object.freeze([])
    }),
    levels: Object.freeze({
      label: 'Tonwertkorrektur…', category: 'adjust-color', scope: 'bitmap',
      defaults: Object.freeze({ inputBlack: 0, gamma: 1, inputWhite: 255, outputBlack: 0, outputWhite: 255 }),
      controls: Object.freeze([
        numeric('inputBlack', 'Schwarzpunkt', 0, 254, 1), numeric('gamma', 'Gamma', .1, 10, .01),
        numeric('inputWhite', 'Weißpunkt', 1, 255, 1), numeric('outputBlack', 'Ausgabe Schwarz', 0, 255, 1),
        numeric('outputWhite', 'Ausgabe Weiß', 0, 255, 1)
      ])
    }),
    sharpen: Object.freeze({
      label: 'Schärfen', category: 'sharpen', scope: 'bitmap',
      defaults: Object.freeze({ amount: 50 }),
      controls: Object.freeze([numeric('amount', 'Stärke', 0, 200, 1, '%')])
    }),
    'sharpen-more': Object.freeze({
      label: 'Stärker schärfen', category: 'sharpen', scope: 'bitmap',
      defaults: Object.freeze({}), controls: Object.freeze([])
    }),
    'unsharp-mask': Object.freeze({
      label: 'Unscharf maskieren…', category: 'sharpen', scope: 'bitmap',
      defaults: Object.freeze({ amount: 100, radius: 1, threshold: 0 }),
      controls: Object.freeze([
        numeric('amount', 'Stärke', 1, 500, 1, '%'),
        numeric('radius', 'Pixelradius', .1, 250, .1, 'px'),
        numeric('threshold', 'Schwellenwert', 0, 255, 1, '', { integer: true })
      ])
    })
  });

  const effectTypes = Object.freeze(Object.keys(effectDefinitions));

  function definitionFor(type) {
    return effectDefinitions[String(type || '')] || null;
  }

  function isAllowed(type, objectType) {
    const definition = definitionFor(type);
    return Boolean(definition && (definition.scope === 'all' || objectType === 'image'));
  }

  function normalizeId(value, index, createId) {
    const supplied = String(value || '').replace(/[^a-z0-9_-]/gi, '-').slice(0, 96);
    if (supplied) return supplied;
    const generated = typeof createId === 'function' ? String(createId('effect') || '') : '';
    return generated.replace(/[^a-z0-9_-]/gi, '-').slice(0, 96) || `effect-${index + 1}`;
  }

  function normalizeControlValue(control, value, fallback) {
    if (control.kind === 'color') return colorPattern.test(String(value || '')) ? String(value).toLowerCase() : fallback;
    if (control.kind === 'select') {
      const normalized = String(value == null ? fallback : value);
      return control.options.some((option) => option.value === normalized) ? normalized : fallback;
    }
    if (control.kind === 'boolean') {
      if (value == null) return Boolean(fallback);
      if (typeof value === 'string') return !['', '0', 'false', 'off'].includes(value.toLowerCase());
      return Boolean(value);
    }
    const normalized = clamp(finite(value, fallback), control.min, control.max);
    return control.integer ? Math.round(normalized) : round(normalized);
  }

  function normalizeSvgFilterMetadata(value) {
    const deviations = Array.isArray(value?.stdDeviation) ? value.stdDeviation.slice(0, 2).map((entry) => round(clamp(finite(entry), 0, 64))) : [];
    if (!deviations.length) return null;
    if (deviations.length === 1) deviations.push(deviations[0]);
    const inputs = new Set(['SourceGraphic', 'SourceAlpha', 'BackgroundImage', 'BackgroundAlpha', 'FillPaint', 'StrokePaint']);
    const blendModes = new Set(['normal', 'multiply', 'screen', 'darken', 'lighten', 'overlay', 'color-dodge', 'color-burn', 'hard-light', 'soft-light', 'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity']);
    return {
      stdDeviation: deviations, ...(inputs.has(value?.input) ? { input: value.input } : {}),
      ...(blendModes.has(value?.blendMode) ? { blendMode: value.blendMode, blendInput2: inputs.has(value?.blendInput2) ? value.blendInput2 : 'SourceGraphic' } : {})
    };
  }

  function normalizeCurvePoints(value, fallback = identityCurve) {
    const read = (source) => (Array.isArray(source) ? source : []).map((entry) => {
      const x = Array.isArray(entry) ? entry[0] : entry?.x;
      const y = Array.isArray(entry) ? entry[1] : entry?.y;
      if (!Number.isFinite(Number(x)) || !Number.isFinite(Number(y))) return null;
      return [round(clamp(finite(x), 0, 255), 2), round(clamp(finite(y), 0, 255), 2)];
    }).filter(Boolean);
    let points = read(value);
    if (points.length < 2) points = read(fallback);
    if (points.length < 2) points = [[0, 0], [255, 255]];
    points.sort((left, right) => left[0] - right[0]);
    const unique = [];
    points.forEach((point) => {
      if (unique.length && Math.abs(unique.at(-1)[0] - point[0]) < .01) unique[unique.length - 1] = point;
      else unique.push(point);
    });
    if (unique[0][0] > 0) unique.unshift([0, 0]); else unique[0][0] = 0;
    if (unique.at(-1)[0] < 255) unique.push([255, 255]); else unique.at(-1)[0] = 255;
    if (unique.length > 32) {
      const interior = unique.slice(1, -1);
      const selected = Array.from({ length: 30 }, (_entry, index) => interior[Math.round(index * (interior.length - 1) / 29)]);
      return [unique[0], ...selected, unique.at(-1)].map((point) => [point[0], point[1]]);
    }
    return unique.map((point) => [point[0], point[1]]);
  }

  function normalizeEffect(value, index = 0, options = {}) {
    if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
    const definition = definitionFor(value.type);
    if (!definition) return null;
    const normalized = {
      id: normalizeId(value.id, index, options.createId),
      type: String(value.type),
      enabled: value.enabled !== false
    };
    definition.controls.forEach((control) => {
      normalized[control.key] = normalizeControlValue(control, value[control.key], definition.defaults[control.key]);
    });
    if (normalized.type === 'curves') {
      const hasLegacyCurve = ['shadows', 'midtones', 'highlights'].some((key) => value[key] != null);
      const legacyCurve = hasLegacyCurve
        ? [[0, 0], [64, finite(value.shadows, 64)], [128, finite(value.midtones, 128)], [192, finite(value.highlights, 192)], [255, 255]]
        : identityCurve;
      normalized.rgbCurve = normalizeCurvePoints(value.rgbCurve, legacyCurve);
      normalized.redCurve = normalizeCurvePoints(value.redCurve);
      normalized.greenCurve = normalizeCurvePoints(value.greenCurve);
      normalized.blueCurve = normalizeCurvePoints(value.blueCurve);
    }
    const svgFilter = normalizeSvgFilterMetadata(value.svgFilter);
    if (svgFilter && ['gaussian-blur', 'drop-shadow', 'outer-glow'].includes(normalized.type)) normalized.svgFilter = svgFilter;
    return normalized;
  }

  function normalizeStack(value, options = {}) {
    const maximum = clamp(Math.floor(finite(options.maxEffects, MAX_EFFECTS)), 0, MAX_EFFECTS);
    const ids = new Set();
    return (Array.isArray(value) ? value : []).slice(0, maximum).map((effect, index) => normalizeEffect(effect, index, options)).filter(Boolean).map((effect, index) => {
      if (!ids.has(effect.id)) { ids.add(effect.id); return effect; }
      let replacement = normalizeId('', index, options.createId); let suffix = 2;
      while (ids.has(replacement)) replacement = `${effect.id}-${suffix++}`;
      ids.add(replacement); return { ...effect, id: replacement };
    });
  }

  function scaleStack(value, scaleX, scaleY, options = {}) {
    const sx = finite(scaleX, 1); const sy = finite(scaleY, 1);
    const absoluteX = Math.abs(sx); const absoluteY = Math.abs(sy);
    const appearanceScale = Math.sqrt(absoluteX * absoluteY);
    const scaled = normalizeStack(value, options).map((effect) => {
      const svgFilter = effect.svgFilter?.stdDeviation ? {
        stdDeviation: [
          round(effect.svgFilter.stdDeviation[0] * absoluteX),
          round(effect.svgFilter.stdDeviation[1] * absoluteY)
        ],
        ...(effect.svgFilter.input ? { input: effect.svgFilter.input } : {}),
        ...(effect.svgFilter.blendMode ? {
          blendMode: effect.svgFilter.blendMode,
          blendInput2: effect.svgFilter.blendInput2 || 'SourceGraphic'
        } : {})
      } : undefined;
      const metadata = svgFilter ? { svgFilter } : {};
      if (['outer-glow', 'inner-glow'].includes(effect.type)) {
        return { ...effect, size: round(effect.size * appearanceScale), softness: round(effect.softness * appearanceScale), ...metadata };
      }
      if (['blur', 'blur-more', 'gaussian-blur'].includes(effect.type)) {
        return { ...effect, radius: round(effect.radius * appearanceScale), ...metadata };
      }
      if (effect.type === 'unsharp-mask') return { ...effect, radius: round(effect.radius * appearanceScale) };
      if (effect.type === 'dither') return { ...effect, size: round(effect.size * appearanceScale) };
      if (effect.type === 'motion-blur') return { ...effect, distance: round(effect.distance * appearanceScale) };
      if (['radial-blur', 'zoom-blur'].includes(effect.type)) return { ...effect, amount: round(effect.amount * appearanceScale) };
      if (effect.type === 'drop-shadow') {
        return {
          ...effect,
          offsetX: round(effect.offsetX * sx),
          offsetY: round(effect.offsetY * sy),
          blur: round(effect.blur * appearanceScale),
          ...metadata
        };
      }
      if (effect.type === 'inner-shadow') {
        return { ...effect, distance: round(effect.distance * appearanceScale), softness: round(effect.softness * appearanceScale) };
      }
      if (effect.type === 'solid-shadow') return { ...effect, distance: round(effect.distance * appearanceScale) };
      if (['inner-bevel', 'outer-bevel', 'inset-emboss', 'raised-emboss'].includes(effect.type)) {
        return { ...effect, width: round(effect.width * appearanceScale), softness: round(effect.softness * appearanceScale) };
      }
      return effect;
    });
    return normalizeStack(scaled, options);
  }

  function createEffect(type, options = {}) {
    const definition = definitionFor(type);
    if (!definition) return null;
    return normalizeEffect({ type, enabled: true, ...definition.defaults }, 0, options);
  }

  function fromLegacyGlow(value, options = {}) {
    if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
    return normalizeEffect({ ...value, id: value.id, type: 'outer-glow', enabled: Boolean(value.enabled) }, 0, options);
  }

  function fromLegacyFillDither(value, options = {}) {
    if (!value || typeof value !== 'object' || Array.isArray(value) || !value.dither || !['linear', 'radial', 'conic'].includes(value.type)) return null;
    return normalizeEffect({
      type: 'dither',
      enabled: true,
      method: value.ditherMethod,
      amount: value.ditherAmount,
      size: value.ditherSize,
      levels: value.ditherLevels
    }, 0, options);
  }

  function availableFor(objectType) {
    return effectTypes.filter((type) => isAllowed(type, objectType)).map((type) => ({ type, ...effectDefinitions[type] }));
  }

  function shadowOffset(effect) {
    const angle = finite(effect?.angle) * Math.PI / 180;
    const distance = Math.max(0, finite(effect?.distance));
    return { x: round(Math.cos(angle) * distance, 4), y: round(Math.sin(angle) * distance, 4) };
  }

  function solidShadowOffsets(effect) {
    const normalized = normalizeEffect({ type: 'solid-shadow', ...effect }) || createEffect('solid-shadow');
    const target = shadowOffset(normalized);
    if (normalized.distance <= 0) return Object.freeze([{ x: 0, y: 0 }]);
    const steps = Math.max(1, Math.ceil(normalized.distance));
    return Object.freeze(Array.from({ length: steps }, (_entry, index) => {
      const progress = (index + 1) / steps;
      return Object.freeze({ x: round(target.x * progress, 4), y: round(target.y * progress, 4) });
    }));
  }

  function blurSampleCount(effect) {
    const quality = clamp(Math.round(finite(effect?.quality, 2)), 1, 3);
    return [5, 9, 13][quality - 1];
  }

  function motionBlurOffsets(effect) {
    const normalized = normalizeEffect({ type: 'motion-blur', ...effect }) || createEffect('motion-blur');
    const samples = blurSampleCount(normalized);
    const angle = normalized.angle * Math.PI / 180;
    return Object.freeze(Array.from({ length: samples }, (_entry, index) => {
      const progress = samples <= 1 ? 0 : index / (samples - 1) - .5;
      return Object.freeze({
        x: round(Math.cos(angle) * normalized.distance * progress, 4),
        y: round(Math.sin(angle) * normalized.distance * progress, 4)
      });
    }));
  }

  function bevelLighting(effect) {
    const width = Math.max(0, finite(effect?.width));
    const softness = Math.max(0, finite(effect?.softness));
    const contrast = clamp(finite(effect?.contrast), 0, 100) / 100;
    const angle = finite(effect?.angle);
    const azimuth = ((360 - angle + (effect?.type === 'inset-emboss' ? 180 : 0)) % 360 + 360) % 360;
    const slope = contrast * .8;
    return {
      azimuth: round(azimuth, 4),
      blur: round(Math.max(.25, width / 3 + softness / 2), 4),
      elevation: 30,
      highlightTable: '0 .25 .5 .82 1',
      opacity: .45,
      surfaceScale: round(Math.max(.1, width * .75), 4),
      slope: round(slope, 4),
      intercept: round(.5 - .5 * slope, 4)
    };
  }

  function visualOutset(value) {
    const bounds = { left: 0, right: 0, top: 0, bottom: 0 };
    normalizeStack(value).filter((effect) => effect.enabled).forEach((effect) => {
      if (effect.type === 'outer-glow') {
        const roundness = effect.size * effect.rounding / 100 * .5;
        const importedBlur = Math.max(...(effect.svgFilter?.stdDeviation || [0]));
        const spread = Math.max(5, effect.size + Math.max(effect.softness * 3, importedBlur * 3) + roundness * 3);
        bounds.left += spread; bounds.right += spread; bounds.top += spread; bounds.bottom += spread;
      } else if (['blur', 'blur-more', 'gaussian-blur'].includes(effect.type)) {
        const spread = Math.max(effect.radius, ...(effect.svgFilter?.stdDeviation || [0])) * 3;
        bounds.left += spread; bounds.right += spread; bounds.top += spread; bounds.bottom += spread;
      } else if (effect.type === 'motion-blur') {
        const offset = shadowOffset({ angle: effect.angle, distance: effect.distance / 2 });
        bounds.left += Math.max(0, -offset.x); bounds.right += Math.max(0, offset.x);
        bounds.top += Math.max(0, -offset.y); bounds.bottom += Math.max(0, offset.y);
      } else if (['radial-blur', 'zoom-blur'].includes(effect.type)) {
        bounds.left += effect.amount; bounds.right += effect.amount; bounds.top += effect.amount; bounds.bottom += effect.amount;
      } else if (effect.type === 'drop-shadow') {
        const spread = Math.max(effect.blur, ...(effect.svgFilter?.stdDeviation || [0])) * 3;
        bounds.left += Math.max(0, spread - effect.offsetX); bounds.right += Math.max(0, spread + effect.offsetX);
        bounds.top += Math.max(0, spread - effect.offsetY); bounds.bottom += Math.max(0, spread + effect.offsetY);
      } else if (effect.type === 'solid-shadow') {
        const offset = shadowOffset(effect);
        bounds.left += Math.max(0, -offset.x); bounds.right += Math.max(0, offset.x);
        bounds.top += Math.max(0, -offset.y); bounds.bottom += Math.max(0, offset.y);
      } else if (effect.type === 'outer-bevel') {
        const spread = effect.width;
        bounds.left += spread; bounds.right += spread; bounds.top += spread; bounds.bottom += spread;
      }
    });
    return Math.ceil(Math.max(bounds.left, bounds.right, bounds.top, bounds.bottom));
  }

  function brightnessContrastValues(effect) {
    const normalized = normalizeEffect({ type: 'brightness-contrast', ...effect }) || createEffect('brightness-contrast');
    const contrast = normalized.contrast;
    const slope = contrast < 0 ? 1 + contrast / 100 : 1 + contrast / 50;
    const intercept = .5 - .5 * slope + normalized.brightness / 100;
    return { slope: round(slope, 5), intercept: round(intercept, 5) };
  }

  function curveEvaluator(points) {
    const normalized = normalizeCurvePoints(points);
    const x = normalized.map((point) => point[0] / 255);
    const y = normalized.map((point) => point[1] / 255);
    const tangent = x.map((_point, index) => {
      if (index === 0) return (y[1] - y[0]) / Math.max(Number.EPSILON, x[1] - x[0]);
      if (index === x.length - 1) return (y[index] - y[index - 1]) / Math.max(Number.EPSILON, x[index] - x[index - 1]);
      return (y[index + 1] - y[index - 1]) / Math.max(Number.EPSILON, x[index + 1] - x[index - 1]);
    });
    return (inputValue) => {
      const input = clamp(finite(inputValue), 0, 1);
      let segment = x.length - 2;
      for (let index = 0; index < x.length - 1; index += 1) {
        if (input <= x[index + 1]) { segment = index; break; }
      }
      const width = x[segment + 1] - x[segment];
      const position = width ? (input - x[segment]) / width : 0;
      const position2 = position * position; const position3 = position2 * position;
      return clamp(
        (2 * position3 - 3 * position2 + 1) * y[segment]
          + (position3 - 2 * position2 + position) * width * tangent[segment]
          + (-2 * position3 + 3 * position2) * y[segment + 1]
          + (position3 - position2) * width * tangent[segment + 1],
        0,
        1
      );
    };
  }

  function curvePointsTable(points, samples = 65) {
    const count = clamp(Math.round(finite(samples, 65)), 2, 257);
    const evaluate = curveEvaluator(points);
    return Array.from({ length: count }, (_entry, sampleIndex) => round(evaluate(sampleIndex / (count - 1)), 5));
  }

  function curveTable(effect, channelOrSamples = 'rgb', samples = 65) {
    const normalized = normalizeEffect({ type: 'curves', ...effect }) || createEffect('curves');
    const channel = typeof channelOrSamples === 'number' ? 'rgb' : ['rgb', 'red', 'green', 'blue'].includes(channelOrSamples) ? channelOrSamples : 'rgb';
    const count = clamp(Math.round(finite(typeof channelOrSamples === 'number' ? channelOrSamples : samples, 65)), 2, 257);
    const master = curveEvaluator(normalized.rgbCurve);
    const component = channel === 'rgb' ? null : curveEvaluator(normalized[`${channel}Curve`]);
    return Array.from({ length: count }, (_entry, sampleIndex) => {
      const masterValue = master(sampleIndex / (count - 1));
      return round(component ? component(masterValue) : masterValue, 5);
    });
  }

  function levelsTable(effect, samples = 65) {
    const normalized = normalizeEffect({ type: 'levels', ...effect }) || createEffect('levels');
    const count = clamp(Math.round(finite(samples, 65)), 2, 257);
    const inputBlack = normalized.inputBlack / 255;
    const inputWhite = Math.max(normalized.inputBlack + 1, normalized.inputWhite) / 255;
    const outputBlack = normalized.outputBlack / 255;
    const outputRange = (normalized.outputWhite - normalized.outputBlack) / 255;
    const exponent = 1 / Math.max(.1, normalized.gamma);
    return Array.from({ length: count }, (_entry, sampleIndex) => {
      const input = sampleIndex / (count - 1);
      const ranged = clamp((input - inputBlack) / (inputWhite - inputBlack), 0, 1);
      return round(clamp(outputBlack + ranged ** exponent * outputRange, 0, 1), 5);
    });
  }

  function sharpenKernel(effect) {
    const type = effect?.type === 'sharpen-more' ? 'sharpen-more' : 'sharpen';
    const normalized = normalizeEffect({ ...effect, type }) || createEffect(type);
    const amount = type === 'sharpen-more' ? 1.5 : normalized.amount / 100;
    return [0, -amount, 0, -amount, 1 + amount * 4, -amount, 0, -amount, 0].map((value) => round(value, 5));
  }

  function unsharpMaskValues(effect) {
    const normalized = normalizeEffect({ ...effect, type: 'unsharp-mask' }) || createEffect('unsharp-mask');
    return {
      amount: round(normalized.amount / 100, 5),
      radius: normalized.radius,
      threshold: normalized.threshold,
      thresholdTable: Array.from({ length: 256 }, (_entry, index) => index <= normalized.threshold ? 0 : 1)
    };
  }

  function findEdgesKernel() {
    return [-1, -1, -1, -1, 8, -1, -1, -1, -1];
  }

  function convertToAlphaMatrix() {
    return [
      0, 0, 0, 0, 0,
      0, 0, 0, 0, 0,
      0, 0, 0, 0, 0,
      -.2126, -.7152, -.0722, 0, 1
    ];
  }

  function histogramQuantile(histogram, total, fraction) {
    if (!(total > 0)) return fraction < .5 ? 0 : 255;
    const target = Math.max(Number.EPSILON, clamp(fraction, 0, 1) * total); let sum = 0;
    for (let value = 0; value < histogram.length; value += 1) {
      sum += histogram[value];
      if (sum >= target) return value;
    }
    return 255;
  }

  function autoColorAnalysis(pixels, options = {}) {
    const values = pixels && typeof pixels.length === 'number' ? pixels : [];
    const histograms = [new Float64Array(256), new Float64Array(256), new Float64Array(256)];
    let total = 0;
    for (let index = 0; index + 3 < values.length; index += 4) {
      const alpha = finite(values[index + 3], 0) / 255; if (alpha <= .03) continue;
      histograms[0][clamp(Math.round(finite(values[index], 0)), 0, 255)] += alpha;
      histograms[1][clamp(Math.round(finite(values[index + 1], 0)), 0, 255)] += alpha;
      histograms[2][clamp(Math.round(finite(values[index + 2], 0)), 0, 255)] += alpha;
      total += alpha;
    }
    const clip = clamp(finite(options.clip, .01), 0, .2); const minimumRange = clamp(finite(options.minimumRange, 8), 1, 255);
    const bounds = histograms.map((histogram) => {
      const low = histogramQuantile(histogram, total, clip); const high = histogramQuantile(histogram, total, 1 - clip);
      return high - low >= minimumRange ? [low, high] : [0, 255];
    });
    return {
      redLow: bounds[0][0], redHigh: bounds[0][1], greenLow: bounds[1][0], greenHigh: bounds[1][1], blueLow: bounds[2][0], blueHigh: bounds[2][1]
    };
  }

  function autoColorTransfer(effect) {
    const normalized = normalizeEffect({ type: 'auto-color', ...effect }) || createEffect('auto-color');
    const channel = (low, high) => {
      const range = Math.max(1, high - low);
      return { slope: round(255 / range, 5), intercept: round(-low / range, 5) };
    };
    return {
      red: channel(normalized.redLow, normalized.redHigh), green: channel(normalized.greenLow, normalized.greenHigh), blue: channel(normalized.blueLow, normalized.blueHigh)
    };
  }

  const api = Object.freeze({
    MAX_EFFECTS, effectDefinitions, effectTypes, definitionFor, isAllowed, normalizeControlValue,
    normalizeEffect, normalizeStack, normalizeCurvePoints, scaleStack, createEffect, fromLegacyGlow, fromLegacyFillDither, availableFor, visualOutset, brightnessContrastValues, curvePointsTable, curveTable, levelsTable, sharpenKernel, unsharpMaskValues, findEdgesKernel, convertToAlphaMatrix,
    shadowOffset, solidShadowOffsets, blurSampleCount, motionBlurOffsets, bevelLighting, autoColorAnalysis, autoColorTransfer
  });
  root.PyroEffects = api;
  if (typeof module === 'object' && module?.exports) module.exports = api;
})(typeof globalThis !== 'undefined' ? globalThis : self);
