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

  const MODES = new Set(['free', 'perspective', 'envelope']);
  const finite = (value, fallback = 0) => Number.isFinite(Number(value)) ? Number(value) : fallback;
  const clamp = (value, minimum, maximum) => Math.min(maximum, Math.max(minimum, value));
  const point = (value, fallback = { x: 0, y: 0 }) => ({
    x: clamp(finite(value?.x ?? value?.[0], fallback.x), -20, 20),
    y: clamp(finite(value?.y ?? value?.[1], fallback.y), -20, 20)
  });
  const clonePoint = (value) => ({ x: value.x, y: value.y });
  const identityCorners = () => [
    { x: 0, y: 0 }, { x: 1, y: 0 }, { x: 1, y: 1 }, { x: 0, y: 1 }
  ];
  const identityEnvelope = () => Array.from({ length: 16 }, (_, index) => ({ x: index % 4 / 3, y: Math.floor(index / 4) / 3 }));

  function solveLinear(matrix, values) {
    const size = values.length;
    const rows = matrix.map((row, index) => [...row.map((value) => finite(value)), finite(values[index])]);
    for (let column = 0; column < size; column += 1) {
      let pivot = column;
      for (let row = column + 1; row < size; row += 1) if (Math.abs(rows[row][column]) > Math.abs(rows[pivot][column])) pivot = row;
      if (Math.abs(rows[pivot][column]) < 1e-12) return null;
      if (pivot !== column) [rows[pivot], rows[column]] = [rows[column], rows[pivot]];
      const divisor = rows[column][column];
      for (let item = column; item <= size; item += 1) rows[column][item] /= divisor;
      for (let row = 0; row < size; row += 1) {
        if (row === column) continue;
        const factor = rows[row][column];
        for (let item = column; item <= size; item += 1) rows[row][item] -= factor * rows[column][item];
      }
    }
    return rows.map((row) => row[size]);
  }

  function homography(corners) {
    const source = [[0, 0], [1, 0], [1, 1], [0, 1]];
    const matrix = []; const values = [];
    source.forEach(([u, v], index) => {
      const target = corners[index];
      matrix.push([u, v, 1, 0, 0, 0, -u * target.x, -v * target.x]); values.push(target.x);
      matrix.push([0, 0, 0, u, v, 1, -u * target.y, -v * target.y]); values.push(target.y);
    });
    return solveLinear(matrix, values);
  }

  function bilinear(corners, u, v) {
    const topX = corners[0].x + (corners[1].x - corners[0].x) * u;
    const topY = corners[0].y + (corners[1].y - corners[0].y) * u;
    const bottomX = corners[3].x + (corners[2].x - corners[3].x) * u;
    const bottomY = corners[3].y + (corners[2].y - corners[3].y) * u;
    return { x: topX + (bottomX - topX) * v, y: topY + (bottomY - topY) * v };
  }

  function perspective(corners, u, v) {
    const values = homography(corners);
    if (!values) return bilinear(corners, u, v);
    const [a, b, c, d, e, f, g, h] = values; const divisor = g * u + h * v + 1;
    if (Math.abs(divisor) < 1e-9) return bilinear(corners, u, v);
    return { x: (a * u + b * v + c) / divisor, y: (d * u + e * v + f) / divisor };
  }

  const bernstein = (t) => {
    const inverse = 1 - t;
    return [inverse ** 3, 3 * inverse ** 2 * t, 3 * inverse * t ** 2, t ** 3];
  };

  function envelope(mesh, u, v) {
    const bx = bernstein(u); const by = bernstein(v); let x = 0; let y = 0;
    for (let row = 0; row < 4; row += 1) for (let column = 0; column < 4; column += 1) {
      const weight = bx[column] * by[row]; const control = mesh[row * 4 + column];
      x += control.x * weight; y += control.y * weight;
    }
    return { x, y };
  }

  function normalize(value, requestedMode = value?.mode) {
    const mode = MODES.has(requestedMode) ? requestedMode : 'free';
    const fallbackCorners = identityCorners();
    const corners = fallbackCorners.map((fallback, index) => point(value?.corners?.[index], fallback));
    let mesh = identityEnvelope();
    if (Array.isArray(value?.mesh) && value.mesh.length === 16) mesh = mesh.map((fallback, index) => point(value.mesh[index], fallback));
    return {
      mode,
      corners,
      mesh,
      skewX: clamp(finite(value?.skewX), -89, 89),
      skewY: clamp(finite(value?.skewY), -89, 89)
    };
  }

  function mapPoint(value, u, v) {
    const transform = normalizeShallow(value);
    const x = finite(u); const y = finite(v);
    if (transform.mode === 'envelope') return envelope(transform.mesh, x, y);
    if (transform.mode === 'perspective') return perspective(transform.corners, x, y);
    return bilinear(transform.corners, x, y);
  }

  function normalizeShallow(value) {
    if (value && MODES.has(value.mode) && Array.isArray(value.corners) && value.corners.length === 4
      && Array.isArray(value.mesh) && value.mesh.length === 16) return value;
    return normalize(value || {});
  }

  function forMode(value, mode) {
    const source = normalize(value || {}, value?.mode || mode);
    if (!MODES.has(mode) || source.mode === mode) return source;
    if (mode === 'envelope') {
      const mesh = identityEnvelope().map((entry) => mapPoint(source, entry.x, entry.y));
      return { ...source, mode, mesh, corners: [mesh[0], mesh[3], mesh[15], mesh[12]].map(clonePoint) };
    }
    const corners = [[0, 0], [1, 0], [1, 1], [0, 1]].map(([u, v]) => mapPoint(source, u, v));
    return { ...source, mode, corners };
  }

  function moveControl(value, mode, index, target) {
    const transform = forMode(value || {}, mode); const next = point(target);
    if (mode === 'envelope') {
      if (!transform.mesh[index]) return transform;
      transform.mesh = transform.mesh.map(clonePoint); transform.mesh[index] = next;
      transform.corners = [transform.mesh[0], transform.mesh[3], transform.mesh[15], transform.mesh[12]].map(clonePoint);
    } else {
      if (!transform.corners[index]) return transform;
      transform.corners = transform.corners.map(clonePoint); transform.corners[index] = next;
    }
    return transform;
  }

  function withShear(value, axis, degrees) {
    const transform = normalize(value || {}, value?.mode || 'free'); const property = axis === 'y' ? 'skewY' : 'skewX';
    const nextDegrees = clamp(finite(degrees), -89, 89); const delta = Math.tan(nextDegrees * Math.PI / 180) - Math.tan(transform[property] * Math.PI / 180);
    const shear = (entry) => axis === 'y'
      ? point({ x: entry.x, y: entry.y + delta * (entry.x - .5) })
      : point({ x: entry.x + delta * (entry.y - .5), y: entry.y });
    transform.corners = transform.corners.map(shear); transform.mesh = transform.mesh.map(shear); transform[property] = nextDegrees;
    return transform;
  }

  function inversePoint(value, target) {
    const transform = normalizeShallow(value); const wanted = point(target); let u = wanted.x; let v = wanted.y;
    if (transform.mode === 'perspective') {
      const values = homography(transform.corners);
      if (values) {
        const [a, b, c, d, e, f, g, h] = values;
        const inverse = solveLinear([[a - wanted.x * g, b - wanted.x * h], [d - wanted.y * g, e - wanted.y * h]], [wanted.x - c, wanted.y - f]);
        if (inverse) [u, v] = inverse;
      }
    }
    for (let iteration = 0; iteration < 18; iteration += 1) {
      const current = mapPoint(transform, u, v); const dx = current.x - wanted.x; const dy = current.y - wanted.y;
      if (Math.hypot(dx, dy) < 1e-8) break;
      const epsilon = 1e-5; const alongU = mapPoint(transform, u + epsilon, v); const alongV = mapPoint(transform, u, v + epsilon);
      const ux = (alongU.x - current.x) / epsilon; const uy = (alongU.y - current.y) / epsilon;
      const vx = (alongV.x - current.x) / epsilon; const vy = (alongV.y - current.y) / epsilon;
      const determinant = ux * vy - uy * vx; if (Math.abs(determinant) < 1e-10) break;
      u -= (dx * vy - dy * vx) / determinant; v -= (ux * dy - uy * dx) / determinant;
    }
    return { x: u, y: v };
  }

  function isIdentity(value, tolerance = 1e-7) {
    if (!value) return true;
    const transform = normalize(value); const controls = transform.mode === 'envelope' ? transform.mesh : transform.corners;
    const expected = transform.mode === 'envelope' ? identityEnvelope() : identityCorners();
    return controls.every((entry, index) => Math.abs(entry.x - expected[index].x) <= tolerance && Math.abs(entry.y - expected[index].y) <= tolerance);
  }

  function bounds(value, samples = 16) {
    if (!value) return { x: 0, y: 0, width: 1, height: 1, right: 1, bottom: 1, cx: .5, cy: .5 };
    const points = [];
    for (let row = 0; row <= samples; row += 1) for (let column = 0; column <= samples; column += 1) {
      if (row !== 0 && row !== samples && column !== 0 && column !== samples) continue;
      points.push(mapPoint(value, column / samples, row / samples));
    }
    if (value.mode === 'envelope') points.push(...normalize(value).mesh);
    const left = Math.min(...points.map((entry) => entry.x)); const top = Math.min(...points.map((entry) => entry.y));
    const right = Math.max(...points.map((entry) => entry.x)); const bottom = Math.max(...points.map((entry) => entry.y));
    return { x: left, y: top, width: right - left, height: bottom - top, right, bottom, cx: (left + right) / 2, cy: (top + bottom) / 2 };
  }

  function affineFromTriangles(source, destination) {
    const matrix = source.map((entry) => [entry.x, entry.y, 1]);
    const horizontal = solveLinear(matrix, destination.map((entry) => entry.x));
    const vertical = solveLinear(matrix, destination.map((entry) => entry.y));
    if (!horizontal || !vertical) return [1, 0, 0, 1, 0, 0];
    return [horizontal[0], vertical[0], horizontal[1], vertical[1], horizontal[2], vertical[2]];
  }

  function triangles(value, width = 1, height = 1, subdivisions = value?.mode === 'envelope' ? 8 : 6) {
    const transform = normalize(value || {}); const columns = clamp(Math.round(finite(subdivisions, 6)), 1, 24); const result = [];
    const local = (u, v) => ({ x: u * width, y: v * height });
    const warped = (u, v) => { const mapped = mapPoint(transform, u, v); return { x: mapped.x * width, y: mapped.y * height }; };
    for (let row = 0; row < columns; row += 1) for (let column = 0; column < columns; column += 1) {
      const u0 = column / columns; const u1 = (column + 1) / columns; const v0 = row / columns; const v1 = (row + 1) / columns;
      const sourceA = [local(u0, v0), local(u1, v0), local(u1, v1)]; const destinationA = [warped(u0, v0), warped(u1, v0), warped(u1, v1)];
      const sourceB = [local(u0, v0), local(u1, v1), local(u0, v1)]; const destinationB = [warped(u0, v0), warped(u1, v1), warped(u0, v1)];
      result.push({ source: sourceA, destination: destinationA, matrix: affineFromTriangles(sourceA, destinationA) });
      result.push({ source: sourceB, destination: destinationB, matrix: affineFromTriangles(sourceB, destinationB) });
    }
    return result;
  }

  const api = Object.freeze({
    MODES, identityCorners, identityEnvelope, normalize, forMode, mapPoint, inversePoint, moveControl, withShear, isIdentity, bounds, affineFromTriangles, triangles
  });
  root.PyroFreeTransform = api;
  if (typeof module === 'object' && module?.exports) module.exports = api;
})(typeof globalThis !== 'undefined' ? globalThis : window);
