scripts/transfer.jsjavascript
/**
 * @module transfer
 * @description
 * # Ghostyle Transfer Lab
 *
 * ## Project Objective
 *
 * The goal of this browser-only experiment is to let a user capture a painted
 * visual style from one face and preview it on another face without sending
 * images to a server. Instead of treating the transfer as a black box, the page
 * exposes the practical stages of the process: choosing source and target
 * images, marking face regions, extracting a transparent paint matte, previewing
 * that matte, and compositing the result with adjustable blend controls.
 *
 * ## Technical Stack
 *
 * The module is written in JavaScript and uses the DOM for the control surface,
 * the Canvas 2D API for pixel extraction, matte generation, preview drawing,
 * affine triangle warping, and final compositing, and `@vladmandic/face-api`
 * for optional 68-point landmark detection. All image processing happens in the
 * browser runtime.
 *
 * ## Interface Model
 *
 * The UI is organized around three upload slots: an optional before image, a
 * required painted after image, and a required target image. Each slot renders a
 * fitted preview canvas where the user draws a face box in image coordinates.
 * A side matte preview shows the extracted paint layer, while the result panel
 * displays the final target composite and enables download once a transfer has
 * completed.
 *
 * ## Functional Workflow
 *
 * The transfer starts in a setup state until the after and target slots both
 * have images and face boxes. Running the transfer normalizes the after face
 * crop into a canonical square, subtracts either the matching before crop or a
 * sampled reference color, feathers the resulting alpha plane, and then places
 * the matte onto the target. When landmarks are available and mesh mode is
 * enabled, corresponding face points are triangulated and the matte is warped
 * triangle by triangle; otherwise the system falls back to direct box placement
 * with a status note that explains which mode was used.
 */
import { applyI18n, initI18n, setupLocaleSelect, t } from './i18n.js';

const CANON = 512;
const RESULT_MAX = 900;
const MODEL_ROOT = 'https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model/';

/**
 * Two-dimensional point in image or canvas coordinates.
 *
 * @typedef {Object} Point
 * @property {number} x - Horizontal coordinate.
 * @property {number} y - Vertical coordinate.
 *
 * Rectangular face selection in original image coordinates.
 *
 * @typedef {Object} Box
 * @property {number} x - Left edge.
 * @property {number} y - Top edge.
 * @property {number} w - Width.
 * @property {number} h - Height.
 *
 * State tracked for one upload slot and its preview canvas.
 *
 * @typedef {Object} TransferSlot
 * @property {?HTMLImageElement} img - Loaded source image, or null before load.
 * @property {?Box} box - User-selected face box, or null until selected.
 * @property {number} scale - Ratio from preview pixels to original image pixels.
 * @property {number} dispW - Preview image width in CSS pixels.
 * @property {number} dispH - Preview image height in CSS pixels.
 *
 * Three-channel average color stored as red, green, and blue values.
 *
 * @typedef {Array.<number>} RgbTuple
 */

/** @type {Record<'before'|'after'|'target', TransferSlot>} */
const slots = {
   before: { img: null, box: null, scale: 1, dispW: 0, dispH: 0 },
   after: { img: null, box: null, scale: 1, dispW: 0, dispH: 0 },
   target: { img: null, box: null, scale: 1, dispW: 0, dispH: 0 }
};

let faceapiReady = false;
let lastResultCanvas = null;
let engineState = { key: 'transfer_engine_loading', ready: false };
let messageState = { key: 'transfer_msg_load_after_target', params: {} };
let modeState = { key: 'transfer_mode_not_run', params: {}, placed: false };

/**
 * Resolve a required DOM element by id.
 *
 * @param {string} id - DOM id.
 * @returns {HTMLElement} Matching element.
 * @throws {Error} When the expected element is missing from the page.
 */
function byId(id) {
   const el = document.getElementById(id);
   if (!el) throw new Error(`Missing transfer element: ${id}`);
   return el;
}

/**
 * Resolve a canvas rendering context with a clear failure mode.
 *
 * @param {HTMLCanvasElement} canvas - Canvas to draw into.
 * @returns {CanvasRenderingContext2D} Two-dimensional context.
 * @throws {Error} When the browser cannot allocate a 2D context.
 */
function ctx2d(canvas) {
   const ctx = canvas.getContext('2d');
   if (!ctx) throw new Error(t('canvas_2d_context_error'));
   return ctx;
}

/**
 * Clamp a numeric value to a closed interval.
 *
 * @param {number} value - Candidate value.
 * @param {number} min - Inclusive lower bound.
 * @param {number} max - Inclusive upper bound.
 * @returns {number} Clamped value.
 */
function clamp(value, min, max) {
   return value < min ? min : (value > max ? max : value);
}

/**
 * Change the currently displayed status message.
 *
 * @param {string} key - Translation key.
 * @param {Record<string, string|number>} [params] - Optional interpolation values.
 * @returns {void}
 */
function setMessage(key, params = {}) {
   messageState = { key, params };
   byId('msg').textContent = t(key, params);
}

/**
 * Change the engine availability badge.
 *
 * @param {string} key - Translation key for the badge text.
 * @param {boolean} ready - Whether to render the badge as ready.
 * @returns {void}
 */
function setEngineState(key, ready) {
   engineState = { key, ready };
   byId('engineText').textContent = t(key);
   byId('engineBadge').classList.toggle('is-ready', ready);
}

/**
 * Render the placement mode note. Wrapped modes read as
 * "Placed using: {mode}", while setup states are shown directly.
 *
 * @returns {void}
 */
function renderModeNote() {
   const modeText = t(modeState.key, modeState.params);
   byId('modeNote').textContent = modeState.placed
      ? t('transfer_mode_placed', { mode: modeText })
      : modeText;
}

/**
 * Change the placement mode note.
 *
 * @param {string} key - Translation key for the mode.
 * @param {Record<string, string|number>} [params] - Optional interpolation values.
 * @param {boolean} [placed=false] - Whether to prefix with the placement wrapper.
 * @returns {void}
 */
function setModeState(key, params = {}, placed = false) {
   modeState = { key, params, placed };
   renderModeNote();
}

/**
 * Re-render the empty result placeholder.
 *
 * @returns {void}
 */
function renderResultEmpty() {
   const holder = byId('result');
   holder.innerHTML = '';
   const empty = document.createElement('span');
   empty.className = 'tool-result__empty';
   empty.dataset.i18n = 'transfer_result_empty';
   empty.textContent = t('transfer_result_empty');
   holder.appendChild(empty);
}

/**
 * Re-apply locale-dependent runtime text that is not covered by static
 * `data-i18n` attributes.
 *
 * @returns {void}
 */
function refreshLocalizedState() {
   document.title = t('transfer_page_title');
   byId('engineText').textContent = t(engineState.key);
   byId('engineBadge').classList.toggle('is-ready', engineState.ready);
   byId('msg').textContent = t(messageState.key, messageState.params);
   renderModeNote();
   for (const name of Object.keys(slots)) refreshSlotNote(name);
   if (!lastResultCanvas) renderResultEmpty();
}

/**
 * Load the optional landmark models. The transfer remains usable in box mode
 * when the CDN or model download fails.
 *
 * @returns {Promise<void>} Resolves after the best-effort setup.
 */
async function initFaceapi() {
   const api = window.faceapi;
   if (!api) {
      setEngineState('transfer_engine_unavailable', false);
      return;
   }

   try {
      await api.nets.tinyFaceDetector.loadFromUri(MODEL_ROOT);
      await api.nets.faceLandmark68Net.loadFromUri(MODEL_ROOT);
      faceapiReady = true;
      setEngineState('transfer_engine_ready', true);
   } catch (err) {
      setEngineState('transfer_engine_failed', false);
      console.warn('face-api load failed:', err);
   }
}

/**
 * Fit a source image into its square preview stage and bind box drawing to the
 * generated canvas.
 *
 * @param {'before'|'after'|'target'} name - Slot to render.
 * @returns {void}
 */
function fitPreview(name) {
   const slot = slots[name];
   const stage = byId(`stage-${name}`);
   stage.querySelectorAll('canvas').forEach((canvas) => canvas.remove());
   const hint = byId(`hint-${name}`);
   hint.style.display = slot.img ? 'none' : '';
   if (!slot.img) return;

   const rect = stage.getBoundingClientRect();
   const stageWidth = rect.width;
   const stageHeight = rect.height;
   const aspectRatio = slot.img.width / slot.img.height;
   let displayWidth = stageWidth;
   let displayHeight = stageWidth / aspectRatio;

   if (displayHeight > stageHeight) {
      displayHeight = stageHeight;
      displayWidth = stageHeight * aspectRatio;
   }

   slot.dispW = displayWidth;
   slot.dispH = displayHeight;
   slot.scale = slot.img.width / displayWidth;

   const canvas = document.createElement('canvas');
   canvas.width = Math.round(displayWidth);
   canvas.height = Math.round(displayHeight);
   canvas.style.width = `${displayWidth}px`;
   canvas.style.height = `${displayHeight}px`;
   canvas.style.left = `${(stageWidth - displayWidth) / 2}px`;
   canvas.style.top = `${(stageHeight - displayHeight) / 2}px`;
   canvas.style.inset = 'auto';
   stage.appendChild(canvas);
   redrawSlot(name, canvas);
   attachBoxDrawing(name, canvas);
}

/**
 * Draw a slot image and the current crop box into its preview canvas.
 *
 * @param {'before'|'after'|'target'} name - Slot to draw.
 * @param {HTMLCanvasElement} canvas - Preview canvas.
 * @returns {void}
 */
function redrawSlot(name, canvas) {
   const slot = slots[name];
   if (!slot.img) return;

   const ctx = ctx2d(canvas);
   ctx.clearRect(0, 0, canvas.width, canvas.height);
   ctx.drawImage(slot.img, 0, 0, canvas.width, canvas.height);

   if (!slot.box) return;

   const box = slot.box;
   const scale = 1 / slot.scale;
   const x = box.x * scale;
   const y = box.y * scale;
   const width = box.w * scale;
   const height = box.h * scale;
   const tick = 10;

   ctx.lineWidth = 2;
   ctx.strokeStyle = '#f3c747';
   ctx.strokeRect(x, y, width, height);
   ctx.strokeStyle = '#ffe7a8';
   ctx.lineWidth = 3;
   ctx.beginPath();
   ctx.moveTo(x, y + tick); ctx.lineTo(x, y); ctx.lineTo(x + tick, y);
   ctx.moveTo(x + width - tick, y); ctx.lineTo(x + width, y); ctx.lineTo(x + width, y + tick);
   ctx.moveTo(x, y + height - tick); ctx.lineTo(x, y + height); ctx.lineTo(x + tick, y + height);
   ctx.moveTo(x + width - tick, y + height); ctx.lineTo(x + width, y + height); ctx.lineTo(x + width, y + height - tick);
   ctx.stroke();
}

/**
 * Attach pointer handlers that let the user draw a face box in image
 * coordinates while seeing the box in preview coordinates.
 *
 * @param {'before'|'after'|'target'} name - Slot being edited.
 * @param {HTMLCanvasElement} canvas - Interactive preview canvas.
 * @returns {void}
 */
function attachBoxDrawing(name, canvas) {
   const slot = slots[name];
   let drawing = false;
   let startX = 0;
   let startY = 0;

   const toImagePoint = (event) => {
      const rect = canvas.getBoundingClientRect();
      return {
         x: (event.clientX - rect.left) * slot.scale,
         y: (event.clientY - rect.top) * slot.scale
      };
   };

   canvas.addEventListener('pointerdown', (event) => {
      drawing = true;
      canvas.setPointerCapture(event.pointerId);
      const point = toImagePoint(event);
      startX = point.x;
      startY = point.y;
      slot.box = { x: startX, y: startY, w: 0, h: 0 };
   });

   canvas.addEventListener('pointermove', (event) => {
      if (!drawing) return;
      const point = toImagePoint(event);
      slot.box = {
         x: Math.min(startX, point.x),
         y: Math.min(startY, point.y),
         w: Math.abs(point.x - startX),
         h: Math.abs(point.y - startY)
      };
      redrawSlot(name, canvas);
   });

   const endDrawing = () => {
      if (!drawing) return;
      drawing = false;
      if (slot.box && (slot.box.w < 8 || slot.box.h < 8)) slot.box = null;
      redrawSlot(name, canvas);
      refreshState();
   };

   canvas.addEventListener('pointerup', endDrawing);
   canvas.addEventListener('pointercancel', endDrawing);
}

/**
 * Update a single slot note from its current image and box state.
 *
 * @param {string} name - Slot name.
 * @returns {void}
 */
function refreshSlotNote(name) {
   const slot = slots[name];
   const note = byId(`note-${name}`);
   if (slot.box) {
      note.textContent = t('transfer_note_box_set');
   } else if (slot.img) {
      note.textContent = t('transfer_note_draw_box');
   } else {
      note.textContent = '';
   }
}

/**
 * Recompute readiness and related user-facing prompts after image or box
 * changes.
 *
 * @returns {void}
 */
function refreshState() {
   for (const name of Object.keys(slots)) refreshSlotNote(name);
   const ready = Boolean(slots.after.img && slots.after.box && slots.target.img && slots.target.box);
   byId('run').disabled = !ready;
   setMessage(ready ? 'transfer_msg_ready' : 'transfer_msg_load_after_target');
}

/**
 * Load an image file selected or dropped by the user.
 *
 * @param {'before'|'after'|'target'} name - Destination slot.
 * @param {File} file - Browser file object.
 * @returns {Promise<HTMLImageElement>} Loaded image element.
 */
function loadFile(name, file) {
   return loadURL(name, URL.createObjectURL(file));
}

/**
 * Load an image URL into a slot. Exposed through `window.GT` for automated
 * checks and demos.
 *
 * @param {'before'|'after'|'target'} name - Destination slot.
 * @param {string} url - Image URL.
 * @returns {Promise<HTMLImageElement>} Loaded image element.
 */
function loadURL(name, url) {
   return new Promise((resolve, reject) => {
      const image = new Image();
      image.crossOrigin = 'anonymous';
      image.onload = () => {
         slots[name].img = image;
         slots[name].box = null;
         fitPreview(name);
         refreshState();
         resolve(image);
      };
      image.onerror = reject;
      image.src = url;
   });
}

/**
 * Draw a slot crop into the canonical transfer coordinate space.
 *
 * @param {'before'|'after'|'target'} name - Slot to crop.
 * @returns {HTMLCanvasElement} Canonical face-frame canvas.
 */
function boxToCanon(name) {
   const slot = slots[name];
   if (!slot.img || !slot.box) throw new Error(`Slot is missing image or box: ${name}`);

   const canvas = document.createElement('canvas');
   canvas.width = CANON;
   canvas.height = CANON;
   ctx2d(canvas).drawImage(slot.img, slot.box.x, slot.box.y, slot.box.w, slot.box.h, 0, 0, CANON, CANON);
   return canvas;
}

/**
 * Extract the painted pixels as an RGBA matte in canonical face coordinates.
 * With a before image, RGB distance is measured against that before crop. When
 * no before is present, a coarse median color approximation is used instead.
 *
 * @param {number} threshold - RGB distance threshold for paint extraction.
 * @param {number} feather - Blur radius for softening the matte alpha.
 * @returns {HTMLCanvasElement} Canonical paint matte.
 */
function extractMatte(threshold, feather) {
   const afterCanon = boxToCanon('after');
   const afterData = ctx2d(afterCanon).getImageData(0, 0, CANON, CANON);
   const refData = slots.before.img && slots.before.box
      ? ctx2d(boxToCanon('before')).getImageData(0, 0, CANON, CANON)
      : null;
   const median = refData ? null : medianColor(afterData);
   const count = CANON * CANON;
   const output = new ImageData(CANON, CANON);
   const alpha = new Float32Array(count);

   for (let i = 0; i < count; i += 1) {
      const p = i * 4;
      const ar = afterData.data[p];
      const ag = afterData.data[p + 1];
      const ab = afterData.data[p + 2];
      const rr = refData ? refData.data[p] : median[0];
      const rg = refData ? refData.data[p + 1] : median[1];
      const rb = refData ? refData.data[p + 2] : median[2];
      const distance = Math.sqrt((ar - rr) ** 2 + (ag - rg) ** 2 + (ab - rb) ** 2);
      alpha[i] = clamp((distance - threshold) / threshold, 0, 1);
      output.data[p] = ar;
      output.data[p + 1] = ag;
      output.data[p + 2] = ab;
   }

   if (feather > 0) boxBlur(alpha, CANON, CANON, feather);

   for (let i = 0; i < count; i += 1) {
      output.data[i * 4 + 3] = Math.round(alpha[i] * 255);
   }

   const matte = document.createElement('canvas');
   matte.width = CANON;
   matte.height = CANON;
   ctx2d(matte).putImageData(output, 0, 0);
   return matte;
}

/**
 * Approximate the dominant skin/reference color by sparse sampling.
 *
 * @param {ImageData} imageData - Canonical source image data.
 * @returns {RgbTuple} Average RGB tuple.
 */
function medianColor(imageData) {
   let r = 0;
   let g = 0;
   let b = 0;
   let n = 0;
   const data = imageData.data;

   for (let i = 0; i < data.length; i += 4 * 29) {
      r += data[i];
      g += data[i + 1];
      b += data[i + 2];
      n += 1;
   }

   return [r / n, g / n, b / n];
}

/**
 * Apply two-pass box blur to a float alpha plane.
 *
 * @param {Float32Array} alpha - Alpha plane, mutated in place.
 * @param {number} width - Plane width.
 * @param {number} height - Plane height.
 * @param {number} radius - Blur radius.
 * @returns {void}
 */
function boxBlur(alpha, width, height, radius) {
   const blurRadius = Math.round(radius);
   const tmp = new Float32Array(alpha.length);

   for (let pass = 0; pass < 2; pass += 1) {
      for (let y = 0; y < height; y += 1) {
         let sum = 0;
         const row = y * width;
         for (let x = -blurRadius; x <= blurRadius; x += 1) sum += alpha[row + clamp(x, 0, width - 1)];
         for (let x = 0; x < width; x += 1) {
            tmp[row + x] = sum / (2 * blurRadius + 1);
            sum += alpha[row + clamp(x + blurRadius + 1, 0, width - 1)] - alpha[row + clamp(x - blurRadius, 0, width - 1)];
         }
      }

      for (let x = 0; x < width; x += 1) {
         let sum = 0;
         for (let y = -blurRadius; y <= blurRadius; y += 1) sum += tmp[clamp(y, 0, height - 1) * width + x];
         for (let y = 0; y < height; y += 1) {
            alpha[y * width + x] = sum / (2 * blurRadius + 1);
            sum += tmp[clamp(y + blurRadius + 1, 0, height - 1) * width + x] - tmp[clamp(y - blurRadius, 0, height - 1) * width + x];
         }
      }
   }
}

/**
 * Detect 68 face landmarks inside a user-provided slot box, mapping the result
 * back into original image coordinates.
 *
 * @param {'after'|'target'} name - Slot to analyze.
 * @returns {Promise<Point[]|null>} Landmark points, or null when detection fails.
 */
async function detectLandmarks(name) {
   const api = window.faceapi;
   if (!faceapiReady || !api) return null;

   const slot = slots[name];
   if (!slot.img || !slot.box) return null;

   const margin = 0.25;
   const cropX = slot.box.x - slot.box.w * margin;
   const cropY = slot.box.y - slot.box.h * margin;
   const cropWidth = slot.box.w * (1 + 2 * margin);
   const cropHeight = slot.box.h * (1 + 2 * margin);
   const crop = document.createElement('canvas');
   crop.width = Math.round(cropWidth);
   crop.height = Math.round(cropHeight);
   ctx2d(crop).drawImage(slot.img, cropX, cropY, cropWidth, cropHeight, 0, 0, crop.width, crop.height);

   const detection = await api
      .detectSingleFace(crop, new api.TinyFaceDetectorOptions({ inputSize: 416, scoreThreshold: 0.2 }))
      .withFaceLandmarks();

   if (!detection) return null;
   return detection.landmarks.positions.map((point) => ({ x: cropX + point.x, y: cropY + point.y }));
}

/**
 * Convert image-coordinate landmarks to canonical face-frame coordinates.
 *
 * @param {Point[]} points - Source landmarks.
 * @param {Box} box - Face box used as canonical frame.
 * @returns {Point[]} Canonical points.
 */
function landmarksToCanon(points, box) {
   return points.map((point) => ({
      x: ((point.x - box.x) / box.w) * CANON,
      y: ((point.y - box.y) / box.h) * CANON
   }));
}

/**
 * Build a Delaunay-like triangulation with Bowyer-Watson. This keeps the mesh
 * dependency-free for a small 68-point landmark set.
 *
 * @param {Point[]} points - Points to triangulate.
 * @returns {number[][]} Triples of point indices.
 */
function triangulate(points) {
   const count = points.length;
   if (count < 3) return [];

   let minX = Infinity;
   let minY = Infinity;
   let maxX = -Infinity;
   let maxY = -Infinity;
   for (const point of points) {
      minX = Math.min(minX, point.x);
      minY = Math.min(minY, point.y);
      maxX = Math.max(maxX, point.x);
      maxY = Math.max(maxY, point.y);
   }

   const dx = maxX - minX || 1;
   const dy = maxY - minY || 1;
   const dmax = Math.max(dx, dy);
   const mx = (minX + maxX) / 2;
   const my = (minY + maxY) / 2;
   const working = points.map((point) => ({ x: point.x, y: point.y }));
   working.push({ x: mx - 20 * dmax, y: my - dmax });
   working.push({ x: mx, y: my + 20 * dmax });
   working.push({ x: mx + 20 * dmax, y: my - dmax });

   const s0 = count;
   const s1 = count + 1;
   const s2 = count + 2;
   let triangles = [[s0, s1, s2]];

   const circumcircle = (a, b, c) => {
      const ax = working[a].x;
      const ay = working[a].y;
      const bx = working[b].x;
      const by = working[b].y;
      const cx = working[c].x;
      const cy = working[c].y;
      const denominator = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
      if (Math.abs(denominator) < 1e-9) return null;

      const ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / denominator;
      const uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / denominator;
      return { x: ux, y: uy, r2: (ax - ux) ** 2 + (ay - uy) ** 2 };
   };

   const triangleHasEdge = (triangle, a, b) => (
      [[triangle[0], triangle[1]], [triangle[1], triangle[2]], [triangle[2], triangle[0]]]
         .some((edge) => (edge[0] === a && edge[1] === b) || (edge[0] === b && edge[1] === a))
   );

   for (let i = 0; i < count; i += 1) {
      const bad = [];
      for (const triangle of triangles) {
         const circle = circumcircle(triangle[0], triangle[1], triangle[2]);
         if (circle && ((working[i].x - circle.x) ** 2 + (working[i].y - circle.y) ** 2) <= circle.r2 * (1 + 1e-9)) {
            bad.push(triangle);
         }
      }

      const edges = [];
      for (const triangle of bad) {
         for (const edge of [[triangle[0], triangle[1]], [triangle[1], triangle[2]], [triangle[2], triangle[0]]]) {
            const shared = bad.some((other) => other !== triangle && triangleHasEdge(other, edge[0], edge[1]));
            if (!shared) edges.push(edge);
         }
      }

      triangles = triangles.filter((triangle) => !bad.includes(triangle));
      for (const edge of edges) triangles.push([edge[0], edge[1], i]);
   }

   return triangles.filter((triangle) => triangle[0] < count && triangle[1] < count && triangle[2] < count);
}

/**
 * Warp the source canvas through a single affine triangle into the target
 * rendering context.
 *
 * @param {CanvasRenderingContext2D} ctx - Target rendering context.
 * @param {HTMLCanvasElement} source - Source matte canvas.
 * @param {Point} s0 - First source point.
 * @param {Point} s1 - Second source point.
 * @param {Point} s2 - Third source point.
 * @param {Point} d0 - First destination point.
 * @param {Point} d1 - Second destination point.
 * @param {Point} d2 - Third destination point.
 * @returns {void}
 */
function warpTriangle(ctx, source, s0, s1, s2, d0, d1, d2) {
   ctx.save();
   ctx.beginPath();
   ctx.moveTo(d0.x, d0.y);
   ctx.lineTo(d1.x, d1.y);
   ctx.lineTo(d2.x, d2.y);
   ctx.closePath();
   ctx.clip();

   const denominator = (s1.x - s0.x) * (s2.y - s0.y) - (s2.x - s0.x) * (s1.y - s0.y);
   if (Math.abs(denominator) < 1e-6) {
      ctx.restore();
      return;
   }

   const a = ((d1.x - d0.x) * (s2.y - s0.y) - (d2.x - d0.x) * (s1.y - s0.y)) / denominator;
   const b = ((d2.x - d0.x) * (s1.x - s0.x) - (d1.x - d0.x) * (s2.x - s0.x)) / denominator;
   const c = ((d1.y - d0.y) * (s2.y - s0.y) - (d2.y - d0.y) * (s1.y - s0.y)) / denominator;
   const d = ((d2.y - d0.y) * (s1.x - s0.x) - (d1.y - d0.y) * (s2.x - s0.x)) / denominator;
   const e = d0.x - a * s0.x - b * s0.y;
   const f = d0.y - c * s0.x - d * s0.y;

   ctx.setTransform(a, c, b, d, e, f);
   ctx.drawImage(source, 0, 0);
   ctx.restore();
}

/**
 * Read the current transfer controls as typed values.
 *
 * @returns {{threshold:number, feather:number, opacity:number, blend:GlobalCompositeOperation, useMesh:boolean}} Transfer parameters.
 */
function readParams() {
   return {
      threshold: Number(byId('thr').value),
      feather: Number(byId('fth').value),
      opacity: Number(byId('op').value) / 100,
      blend: byId('blend').value,
      useMesh: byId('useMesh').checked
   };
}

/**
 * Draw the extracted matte into the side preview canvas.
 *
 * @param {HTMLCanvasElement} matte - Canonical matte canvas.
 * @returns {void}
 */
function drawMattePreview(matte) {
   const preview = byId('matte');
   preview.width = 180;
   preview.height = 180;
   const ctx = ctx2d(preview);
   ctx.clearRect(0, 0, 180, 180);
   ctx.drawImage(matte, 0, 0, 180, 180);
}

/**
 * Run extraction and compositing, using mesh warping when landmarks are
 * available and falling back to direct box placement otherwise.
 *
 * @returns {Promise<string>} Translation key for the mode used.
 */
async function transfer() {
   const params = readParams();
   setMessage('transfer_msg_extracting');
   const matte = extractMatte(params.threshold, params.feather);
   drawMattePreview(matte);

   const target = slots.target;
   if (!target.img || !target.box) throw new Error('Target image or box is missing.');

   let resultWidth = target.img.width;
   let resultHeight = target.img.height;
   const longEdge = Math.max(resultWidth, resultHeight);
   if (longEdge > RESULT_MAX) {
      const ratio = RESULT_MAX / longEdge;
      resultWidth = Math.round(resultWidth * ratio);
      resultHeight = Math.round(resultHeight * ratio);
   }

   const result = document.createElement('canvas');
   result.width = resultWidth;
   result.height = resultHeight;
   const resultCtx = ctx2d(result);
   resultCtx.drawImage(target.img, 0, 0, resultWidth, resultHeight);

   const scaleX = resultWidth / target.img.width;
   const scaleY = resultHeight / target.img.height;
   let modeKey = 'transfer_mode_box';
   let modeParams = {};

   if (params.useMesh && faceapiReady) {
      setMessage('transfer_msg_detecting');
      const [afterLandmarks, targetLandmarks] = await Promise.all([
         detectLandmarks('after'),
         detectLandmarks('target')
      ]);

      if (afterLandmarks && targetLandmarks && slots.after.box) {
         const sourcePoints = landmarksToCanon(afterLandmarks, slots.after.box);
         const destinationPoints = targetLandmarks.map((point) => ({ x: point.x * scaleX, y: point.y * scaleY }));
         const triangles = triangulate(sourcePoints);

         resultCtx.globalCompositeOperation = params.blend;
         resultCtx.globalAlpha = params.opacity;
         for (const triangle of triangles) {
            warpTriangle(
               resultCtx,
               matte,
               sourcePoints[triangle[0]],
               sourcePoints[triangle[1]],
               sourcePoints[triangle[2]],
               destinationPoints[triangle[0]],
               destinationPoints[triangle[1]],
               destinationPoints[triangle[2]]
            );
         }
         resultCtx.setTransform(1, 0, 0, 1, 0, 0);
         resultCtx.globalAlpha = 1;
         resultCtx.globalCompositeOperation = 'source-over';
         modeKey = 'transfer_mode_mesh';
         modeParams = { count: triangles.length };
      }
   }

   if (modeKey === 'transfer_mode_box') {
      const box = target.box;
      resultCtx.globalCompositeOperation = params.blend;
      resultCtx.globalAlpha = params.opacity;
      resultCtx.drawImage(matte, box.x * scaleX, box.y * scaleY, box.w * scaleX, box.h * scaleY);
      resultCtx.globalAlpha = 1;
      resultCtx.globalCompositeOperation = 'source-over';

      if (params.useMesh && faceapiReady) {
         modeKey = 'transfer_mode_box_no_face';
      } else if (params.useMesh) {
         modeKey = 'transfer_mode_box_unavailable';
      }
   }

   const holder = byId('result');
   holder.innerHTML = '';
   holder.appendChild(result);
   lastResultCanvas = result;
   byId('download').disabled = false;
   setModeState(modeKey, modeParams, true);
   setMessage('transfer_msg_done');
   return modeKey;
}

/**
 * Reset images, boxes, previews, output, and controls that are derived from
 * the last transfer.
 *
 * @returns {void}
 */
function resetTransfer() {
   for (const name of Object.keys(slots)) {
      slots[name].img = null;
      slots[name].box = null;
      fitPreview(name);
      refreshSlotNote(name);
   }

   renderResultEmpty();
   ctx2d(byId('matte')).clearRect(0, 0, 180, 180);
   setModeState('transfer_mode_not_run');
   lastResultCanvas = null;
   byId('download').disabled = true;
   refreshState();
}

/**
 * Wire DOM events for file loading, drag-and-drop, controls, commands, locale
 * changes, and the lightweight test API.
 *
 * @returns {void}
 */
function setupTransferPage() {
   initI18n();
   setupLocaleSelect(byId('localeSelect'), () => {
      applyI18n();
      refreshLocalizedState();
   });
   refreshLocalizedState();

   document.querySelectorAll('input[type="file"][data-slot]').forEach((input) => {
      input.addEventListener('change', (event) => {
         const file = event.target.files[0];
         if (file) loadFile(input.dataset.slot, file);
      });
   });

   for (const name of Object.keys(slots)) {
      const stage = byId(`stage-${name}`);
      stage.addEventListener('dragover', (event) => {
         event.preventDefault();
      });
      stage.addEventListener('drop', (event) => {
         event.preventDefault();
         const file = event.dataTransfer.files[0];
         if (file) loadFile(name, file);
      });
   }

   byId('thr').addEventListener('input', (event) => { byId('v-thr').textContent = event.target.value; });
   byId('fth').addEventListener('input', (event) => { byId('v-fth').textContent = event.target.value; });
   byId('op').addEventListener('input', (event) => { byId('v-op').textContent = event.target.value; });

   byId('run').addEventListener('click', () => {
      byId('run').disabled = true;
      transfer()
         .catch((err) => {
            console.error(err);
            setMessage('transfer_msg_error', { message: err instanceof Error ? err.message : String(err) });
         })
         .finally(() => refreshState());
   });

   byId('download').addEventListener('click', () => {
      if (!lastResultCanvas) return;
      lastResultCanvas.toBlob((blob) => {
         if (!blob) return;
         const link = document.createElement('a');
         link.href = URL.createObjectURL(blob);
         link.download = 'ghostyle-transfer.png';
         link.click();
      });
   });

   byId('reset').addEventListener('click', resetTransfer);
   window.addEventListener('resize', () => Object.keys(slots).forEach(fitPreview));
   window.addEventListener('i18n:localeChanged', refreshLocalizedState);

   initFaceapi();

   window.GT = {
      load: (name, url) => loadURL(name, url),
      setBox: (name, box) => {
         slots[name].box = box;
         fitPreview(name);
         refreshState();
      },
      setParams: (options) => {
         if (options.threshold != null) {
            byId('thr').value = options.threshold;
            byId('v-thr').textContent = options.threshold;
         }
         if (options.feather != null) {
            byId('fth').value = options.feather;
            byId('v-fth').textContent = options.feather;
         }
         if (options.opacity != null) {
            byId('op').value = options.opacity;
            byId('v-op').textContent = options.opacity;
         }
         if (options.blend != null) byId('blend').value = options.blend;
         if (options.useMesh != null) byId('useMesh').checked = options.useMesh;
      },
      transfer: () => transfer(),
      resultDataURL: () => (lastResultCanvas ? lastResultCanvas.toDataURL() : null),
      _internal: { triangulate, warpTriangle, extractMatte, slots }
   };
}

setupTransferPage();