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

  const finite = (value, fallback = 0) => Number.isFinite(Number(value)) ? Number(value) : fallback;
  const clamp = (value, minimum, maximum) => Math.min(maximum, Math.max(minimum, value));

  function gridSize(value, fallback = 4) {
    return clamp(Math.round(finite(value, fallback)), 2, 32);
  }

  function createGrid(widthValue, heightValue, columnsValue = 4, rowsValue = columnsValue) {
    const width = Math.max(1, finite(widthValue, 1)); const height = Math.max(1, finite(heightValue, 1));
    const columns = gridSize(columnsValue); const rows = gridSize(rowsValue);
    const points = [];
    for (let row = 0; row < rows; row += 1) {
      const y = rows === 1 ? 0 : height * row / (rows - 1);
      for (let column = 0; column < columns; column += 1) {
        const x = columns === 1 ? 0 : width * column / (columns - 1);
        points.push({ x, y });
      }
    }
    return points;
  }

  function gridTriangles(columnsValue, rowsValue) {
    const columns = gridSize(columnsValue); const rows = gridSize(rowsValue); const triangles = [];
    for (let row = 0; row < rows - 1; row += 1) {
      for (let column = 0; column < columns - 1; column += 1) {
        const topLeft = row * columns + column; const topRight = topLeft + 1;
        const bottomLeft = topLeft + columns; const bottomRight = bottomLeft + 1;
        // Alternating diagonals keep a heavily deformed mesh visually balanced.
        if ((row + column) % 2) triangles.push([topLeft, topRight, bottomLeft], [topRight, bottomRight, bottomLeft]);
        else triangles.push([topLeft, topRight, bottomRight], [topLeft, bottomRight, bottomLeft]);
      }
    }
    return triangles;
  }

  function smoothFalloff(value) {
    const normalized = clamp(finite(value), 0, 1);
    return normalized * normalized * (3 - 2 * normalized);
  }

  function moveGridPoint(points, indexValue, target, options = {}) {
    const index = Math.round(finite(indexValue, -1));
    if (!Array.isArray(points) || !points[index]) return Array.isArray(points) ? points.map((point) => ({ ...point })) : [];
    const origin = points[index]; const targetX = finite(target?.x, origin.x); const targetY = finite(target?.y, origin.y);
    const deltaX = targetX - origin.x; const deltaY = targetY - origin.y;
    const radius = Math.max(0, finite(options.radius)); const strength = clamp(finite(options.strength, 1), 0, 1);
    return points.map((point, pointIndex) => {
      if (pointIndex === index) return { x: origin.x + deltaX * strength, y: origin.y + deltaY * strength };
      if (!radius || options.precise) return { x: point.x, y: point.y };
      const distance = Math.hypot(point.x - origin.x, point.y - origin.y);
      if (distance >= radius) return { x: point.x, y: point.y };
      const influence = smoothFalloff(1 - distance / radius) * strength;
      return { x: point.x + deltaX * influence, y: point.y + deltaY * influence };
    });
  }

  function defaultPuppetPins(widthValue, heightValue) {
    const width = Math.max(1, finite(widthValue, 1)); const height = Math.max(1, finite(heightValue, 1));
    return [
      { id: 'anchor-nw', source: { x: 0, y: 0 }, target: { x: 0, y: 0 }, locked: true },
      { id: 'anchor-ne', source: { x: width, y: 0 }, target: { x: width, y: 0 }, locked: true },
      { id: 'anchor-se', source: { x: width, y: height }, target: { x: width, y: height }, locked: true },
      { id: 'anchor-sw', source: { x: 0, y: height }, target: { x: 0, y: height }, locked: true }
    ];
  }

  function puppetGrid(points, pins, options = {}) {
    if (!Array.isArray(points)) return [];
    const controls = (Array.isArray(pins) ? pins : []).filter((pin) => pin?.source && pin?.target);
    if (!controls.length) return points.map((point) => ({ ...point }));
    const radius = Math.max(1, finite(options.radius, 120));
    const power = clamp(finite(options.power, 2.2), 1, 6);
    const rigidity = Math.max(.0001, finite(options.rigidity, .42));
    return points.map((point) => {
      let weightedX = 0; let weightedY = 0; let totalWeight = rigidity;
      for (const pin of controls) {
        const sourceX = finite(pin.source.x); const sourceY = finite(pin.source.y);
        const deltaX = finite(pin.target.x, sourceX) - sourceX; const deltaY = finite(pin.target.y, sourceY) - sourceY;
        const distance = Math.hypot(point.x - sourceX, point.y - sourceY);
        if (distance < .001) return { x: point.x + deltaX, y: point.y + deltaY };
        const scaled = distance / radius;
        const weight = 1 / Math.pow(scaled + .075, power);
        weightedX += deltaX * weight; weightedY += deltaY * weight; totalWeight += weight;
      }
      return { x: point.x + weightedX / totalWeight, y: point.y + weightedY / totalWeight };
    });
  }

  function affineFromTriangles(source, destination) {
    if (!Array.isArray(source) || !Array.isArray(destination) || source.length !== 3 || destination.length !== 3) return null;
    const [s1, s2, s3] = source; const [d1, d2, d3] = destination;
    const denominator = finite(s1.x) * (finite(s2.y) - finite(s3.y))
      + finite(s2.x) * (finite(s3.y) - finite(s1.y))
      + finite(s3.x) * (finite(s1.y) - finite(s2.y));
    if (Math.abs(denominator) < 1e-8) return null;
    const coefficients = (v1, v2, v3) => ({
      x: (v1 * (finite(s2.y) - finite(s3.y)) + v2 * (finite(s3.y) - finite(s1.y)) + v3 * (finite(s1.y) - finite(s2.y))) / denominator,
      y: (v1 * (finite(s3.x) - finite(s2.x)) + v2 * (finite(s1.x) - finite(s3.x)) + v3 * (finite(s2.x) - finite(s1.x))) / denominator,
      offset: (v1 * (finite(s2.x) * finite(s3.y) - finite(s3.x) * finite(s2.y))
        + v2 * (finite(s3.x) * finite(s1.y) - finite(s1.x) * finite(s3.y))
        + v3 * (finite(s1.x) * finite(s2.y) - finite(s2.x) * finite(s1.y))) / denominator
    });
    const horizontal = coefficients(finite(d1.x), finite(d2.x), finite(d3.x));
    const vertical = coefficients(finite(d1.y), finite(d2.y), finite(d3.y));
    return { a: horizontal.x, b: vertical.x, c: horizontal.y, d: vertical.y, e: horizontal.offset, f: vertical.offset };
  }

  function expandedTriangle(triangle, overlap = .7) {
    const centerX = triangle.reduce((sum, point) => sum + finite(point.x), 0) / 3;
    const centerY = triangle.reduce((sum, point) => sum + finite(point.y), 0) / 3;
    return triangle.map((point) => {
      const dx = finite(point.x) - centerX; const dy = finite(point.y) - centerY; const distance = Math.max(.001, Math.hypot(dx, dy));
      return { x: finite(point.x) + dx / distance * overlap, y: finite(point.y) + dy / distance * overlap };
    });
  }

  function drawMesh(sourceCanvas, targetCanvas, sourcePoints, destinationPoints, columnsValue, rowsValue, options = {}) {
    if (!sourceCanvas || !targetCanvas || !Array.isArray(sourcePoints) || !Array.isArray(destinationPoints)) return false;
    const columns = gridSize(columnsValue); const rows = gridSize(rowsValue);
    if (sourcePoints.length !== columns * rows || destinationPoints.length !== sourcePoints.length) return false;
    const context = targetCanvas.getContext('2d'); if (!context) return false;
    context.save(); context.setTransform(1, 0, 0, 1, 0, 0); context.clearRect(0, 0, targetCanvas.width, targetCanvas.height);
    context.imageSmoothingEnabled = options.smoothing !== false;
    for (const indices of gridTriangles(columns, rows)) {
      const source = indices.map((index) => sourcePoints[index]); const destination = indices.map((index) => destinationPoints[index]);
      const matrix = affineFromTriangles(source, destination); if (!matrix) continue;
      const clip = expandedTriangle(destination, finite(options.overlap, .7));
      context.save(); context.beginPath(); context.moveTo(clip[0].x, clip[0].y); context.lineTo(clip[1].x, clip[1].y); context.lineTo(clip[2].x, clip[2].y); context.closePath(); context.clip();
      context.transform(matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f); context.drawImage(sourceCanvas, 0, 0); context.restore();
    }
    context.restore(); return true;
  }

  function samplePremultiplied(pixels, width, height, xValue, yValue) {
    const x = clamp(finite(xValue), 0, width - 1); const y = clamp(finite(yValue), 0, height - 1);
    const x0 = Math.floor(x); const y0 = Math.floor(y); const x1 = Math.min(width - 1, x0 + 1); const y1 = Math.min(height - 1, y0 + 1);
    const fx = x - x0; const fy = y - y0;
    const samples = [[x0, y0, (1 - fx) * (1 - fy)], [x1, y0, fx * (1 - fy)], [x0, y1, (1 - fx) * fy], [x1, y1, fx * fy]];
    let alpha = 0; let red = 0; let green = 0; let blue = 0;
    for (const [sampleX, sampleY, weight] of samples) {
      const offset = (sampleY * width + sampleX) * 4; const sampleAlpha = pixels[offset + 3] / 255;
      alpha += sampleAlpha * weight; red += pixels[offset] * sampleAlpha * weight; green += pixels[offset + 1] * sampleAlpha * weight; blue += pixels[offset + 2] * sampleAlpha * weight;
    }
    if (alpha <= 1e-6) return [0, 0, 0, 0];
    return [Math.round(red / alpha), Math.round(green / alpha), Math.round(blue / alpha), Math.round(alpha * 255)];
  }

  function liquifyPixels(sourcePixels, widthValue, heightValue, center, delta, options = {}) {
    const width = Math.max(1, Math.round(finite(widthValue, 1))); const height = Math.max(1, Math.round(finite(heightValue, 1)));
    if (!sourcePixels || sourcePixels.length < width * height * 4) throw new Error('Bitmapdaten sind unvollständig');
    const output = new Uint8ClampedArray(sourcePixels); const radius = Math.max(1, finite(options.radius, 40));
    const strength = clamp(finite(options.strength, .55), 0, 1); const centerX = finite(center?.x); const centerY = finite(center?.y);
    const maximum = radius * .7; const rawX = finite(delta?.x) * strength; const rawY = finite(delta?.y) * strength;
    const magnitude = Math.hypot(rawX, rawY); const scale = magnitude > maximum ? maximum / magnitude : 1;
    const deltaX = rawX * scale; const deltaY = rawY * scale;
    const left = clamp(Math.floor(centerX - radius), 0, width - 1); const right = clamp(Math.ceil(centerX + radius), 0, width - 1);
    const top = clamp(Math.floor(centerY - radius), 0, height - 1); const bottom = clamp(Math.ceil(centerY + radius), 0, height - 1);
    for (let y = top; y <= bottom; y += 1) {
      for (let x = left; x <= right; x += 1) {
        const distance = Math.hypot(x + .5 - centerX, y + .5 - centerY); if (distance >= radius) continue;
        const influence = smoothFalloff(1 - distance / radius);
        const sample = samplePremultiplied(sourcePixels, width, height, x - deltaX * influence, y - deltaY * influence);
        const offset = (y * width + x) * 4;
        output[offset] = sample[0]; output[offset + 1] = sample[1]; output[offset + 2] = sample[2]; output[offset + 3] = sample[3];
      }
    }
    return output;
  }

  function radialLiquifyPixels(sourcePixels, widthValue, heightValue, center, options = {}) {
    const width = Math.max(1, Math.round(finite(widthValue, 1))); const height = Math.max(1, Math.round(finite(heightValue, 1)));
    if (!sourcePixels || sourcePixels.length < width * height * 4) throw new Error('Bitmapdaten sind unvollständig');
    const output = new Uint8ClampedArray(sourcePixels); const radius = Math.max(1, finite(options.radius, 40));
    const strength = clamp(finite(options.strength, .55), 0, 1); const centerX = finite(center?.x); const centerY = finite(center?.y);
    const mode = ['bloat', 'pucker', 'twirl'].includes(options.mode) ? options.mode : 'bloat';
    const direction = finite(options.direction, 1) < 0 ? -1 : 1;
    const left = clamp(Math.floor(centerX - radius), 0, width - 1); const right = clamp(Math.ceil(centerX + radius), 0, width - 1);
    const top = clamp(Math.floor(centerY - radius), 0, height - 1); const bottom = clamp(Math.ceil(centerY + radius), 0, height - 1);
    for (let y = top; y <= bottom; y += 1) {
      for (let x = left; x <= right; x += 1) {
        const vectorX = x + .5 - centerX; const vectorY = y + .5 - centerY; const distance = Math.hypot(vectorX, vectorY);
        if (distance >= radius) continue;
        const influence = smoothFalloff(1 - distance / radius); let sourceX; let sourceY;
        if (mode === 'twirl') {
          const angle = -direction * strength * influence * .62; const cosine = Math.cos(angle); const sine = Math.sin(angle);
          sourceX = centerX + vectorX * cosine - vectorY * sine - .5;
          sourceY = centerY + vectorX * sine + vectorY * cosine - .5;
        } else {
          const amount = strength * influence * (mode === 'bloat' ? .42 : .68);
          const scale = mode === 'bloat' ? Math.max(.08, 1 - amount) : 1 + amount;
          sourceX = centerX + vectorX * scale - .5; sourceY = centerY + vectorY * scale - .5;
        }
        const sample = samplePremultiplied(sourcePixels, width, height, sourceX, sourceY); const offset = (y * width + x) * 4;
        output[offset] = sample[0]; output[offset + 1] = sample[1]; output[offset + 2] = sample[2]; output[offset + 3] = sample[3];
      }
    }
    return output;
  }

  function restoreLiquifyPixels(currentPixels, originalPixels, widthValue, heightValue, center, options = {}) {
    const width = Math.max(1, Math.round(finite(widthValue, 1))); const height = Math.max(1, Math.round(finite(heightValue, 1)));
    if (!currentPixels || !originalPixels || currentPixels.length < width * height * 4 || originalPixels.length < width * height * 4) throw new Error('Bitmapdaten sind unvollständig');
    const output = new Uint8ClampedArray(currentPixels); const radius = Math.max(1, finite(options.radius, 40));
    const strength = clamp(finite(options.strength, .55), 0, 1); const smooth = options.mode === 'smooth';
    const centerX = finite(center?.x); const centerY = finite(center?.y);
    const left = clamp(Math.floor(centerX - radius), 0, width - 1); const right = clamp(Math.ceil(centerX + radius), 0, width - 1);
    const top = clamp(Math.floor(centerY - radius), 0, height - 1); const bottom = clamp(Math.ceil(centerY + radius), 0, height - 1);
    for (let y = top; y <= bottom; y += 1) {
      for (let x = left; x <= right; x += 1) {
        const distance = Math.hypot(x + .5 - centerX, y + .5 - centerY); if (distance >= radius) continue;
        const mix = smoothFalloff(1 - distance / radius) * strength * (smooth ? .24 : 1); const offset = (y * width + x) * 4;
        for (let channel = 0; channel < 4; channel += 1) output[offset + channel] = Math.round(currentPixels[offset + channel] * (1 - mix) + originalPixels[offset + channel] * mix);
      }
    }
    return output;
  }

  function axisStretchGeometry(lengthValue, startValue, endValue, amountValue = 0) {
    const length = Math.max(1, finite(lengthValue, 1));
    const rawStart = clamp(Math.min(finite(startValue), finite(endValue, length)), 0, Math.max(0, length - 1));
    const sourceEnd = clamp(Math.max(finite(startValue), finite(endValue, length)), rawStart + Math.min(1, length), length);
    const sourceStart = Math.min(rawStart, sourceEnd - Math.min(1, length));
    const sourceSize = Math.max(.001, sourceEnd - sourceStart);
    const scale = clamp(1 + finite(amountValue) / 100, .1, 3);
    const targetSize = clamp(sourceSize * scale, Math.min(1, length), length);
    const center = (sourceStart + sourceEnd) / 2;
    let targetStart = center - targetSize / 2; let targetEnd = center + targetSize / 2;
    if (targetStart < 0) { targetEnd -= targetStart; targetStart = 0; }
    if (targetEnd > length) { targetStart -= targetEnd - length; targetEnd = length; }
    targetStart = clamp(targetStart, 0, Math.max(0, length - targetSize)); targetEnd = targetStart + targetSize;
    return { length, sourceStart, sourceEnd, sourceSize, targetStart, targetEnd, targetSize, scale: targetSize / sourceSize };
  }

  function drawAxisStretch(sourceCanvas, targetCanvas, axisValue, startValue, endValue, amountValue = 0, options = {}) {
    if (!sourceCanvas || !targetCanvas) return false;
    const axis = axisValue === 'horizontal' ? 'horizontal' : 'vertical';
    const width = Math.max(1, finite(sourceCanvas.width, 1)); const height = Math.max(1, finite(sourceCanvas.height, 1));
    if (targetCanvas.width !== width) targetCanvas.width = width;
    if (targetCanvas.height !== height) targetCanvas.height = height;
    const context = targetCanvas.getContext('2d'); if (!context) return false;
    const geometry = axisStretchGeometry(axis === 'vertical' ? height : width, startValue, endValue, amountValue);
    const draw = (sourceX, sourceY, sourceWidth, sourceHeight, targetX, targetY, targetWidth, targetHeight) => {
      if (sourceWidth <= .001 || sourceHeight <= .001 || targetWidth <= .001 || targetHeight <= .001) return;
      context.drawImage(sourceCanvas, sourceX, sourceY, sourceWidth, sourceHeight, targetX, targetY, targetWidth, targetHeight);
    };
    context.save(); context.setTransform(1, 0, 0, 1, 0, 0); context.clearRect(0, 0, width, height);
    context.imageSmoothingEnabled = options.smoothing !== false;
    if (axis === 'vertical') {
      draw(0, 0, width, geometry.sourceStart, 0, 0, width, geometry.targetStart);
      draw(0, geometry.sourceStart, width, geometry.sourceSize, 0, geometry.targetStart, width, geometry.targetSize);
      draw(0, geometry.sourceEnd, width, height - geometry.sourceEnd, 0, geometry.targetEnd, width, height - geometry.targetEnd);
    } else {
      draw(0, 0, geometry.sourceStart, height, 0, 0, geometry.targetStart, height);
      draw(geometry.sourceStart, 0, geometry.sourceSize, height, geometry.targetStart, 0, geometry.targetSize, height);
      draw(geometry.sourceEnd, 0, width - geometry.sourceEnd, height, geometry.targetEnd, 0, width - geometry.targetEnd, height);
    }
    context.restore(); return geometry;
  }

  const api = Object.freeze({
    createGrid, gridTriangles, smoothFalloff, moveGridPoint, defaultPuppetPins, puppetGrid,
    affineFromTriangles, drawMesh, liquifyPixels, radialLiquifyPixels, restoreLiquifyPixels, axisStretchGeometry, drawAxisStretch
  });
  root.PyroBitmapWarp = api;
  if (typeof module === 'object' && module?.exports) module.exports = api;
})(typeof globalThis !== 'undefined' ? globalThis : self);
