← back to Dw War Room
src/showroom/TextureCrop.ts
160 lines
// TextureCrop.ts — Center-crop images to 512x768 with white-border trimming
// Loads image → trims white borders → center-crops to 2:3 aspect → returns CanvasTexture
import * as THREE from 'three';
const CANVAS_W = 512;
const CANVAS_H = 768;
const WHITE_THRESHOLD = 240; // RGB channels above this = "white"
const SCAN_DEPTH = 80; // Max pixels inward to scan for border
/**
* Detect how many pixels of white border exist on each edge.
* Scans rows/columns from outside in, stops when a non-white row/col is found.
*/
function detectWhiteBorders(
data: Uint8ClampedArray,
w: number,
h: number
): { top: number; bottom: number; left: number; right: number } {
const isWhitePixel = (x: number, y: number): boolean => {
const i = (y * w + x) * 4;
return data[i] > WHITE_THRESHOLD
&& data[i + 1] > WHITE_THRESHOLD
&& data[i + 2] > WHITE_THRESHOLD;
};
// Check if an entire row is white (sample every 4th pixel for speed)
const isWhiteRow = (y: number): boolean => {
for (let x = 0; x < w; x += 4) {
if (!isWhitePixel(x, y)) return false;
}
return true;
};
// Check if an entire column is white
const isWhiteCol = (x: number): boolean => {
for (let y = 0; y < h; y += 4) {
if (!isWhitePixel(x, y)) return false;
}
return true;
};
let top = 0;
while (top < Math.min(SCAN_DEPTH, h / 3) && isWhiteRow(top)) top++;
let bottom = 0;
while (bottom < Math.min(SCAN_DEPTH, h / 3) && isWhiteRow(h - 1 - bottom)) bottom++;
let left = 0;
while (left < Math.min(SCAN_DEPTH, w / 3) && isWhiteCol(left)) left++;
let right = 0;
while (right < Math.min(SCAN_DEPTH, w / 3) && isWhiteCol(w - 1 - right)) right++;
return { top, bottom, left, right };
}
/**
* Load an image URL, trim white borders, center-crop to 512x768 canvas.
* Returns a Promise<THREE.CanvasTexture>.
*/
export function cropTexture(imageUrl: string): Promise<THREE.CanvasTexture> {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
// Step 1: Draw full image to a scratch canvas for pixel analysis
const scratch = document.createElement('canvas');
scratch.width = img.naturalWidth;
scratch.height = img.naturalHeight;
const sCtx = scratch.getContext('2d')!;
sCtx.drawImage(img, 0, 0);
const imgData = sCtx.getImageData(0, 0, scratch.width, scratch.height);
const borders = detectWhiteBorders(imgData.data, scratch.width, scratch.height);
// Step 2: Compute content rect (after trimming white borders)
const contentX = borders.left;
const contentY = borders.top;
const contentW = scratch.width - borders.left - borders.right;
const contentH = scratch.height - borders.top - borders.bottom;
if (contentW <= 0 || contentH <= 0) {
// Entire image is white — use full image as fallback
const fallback = createFallbackTexture(imageUrl);
resolve(fallback);
return;
}
// Step 3: Center-crop the content rect to 2:3 aspect ratio
const targetAspect = CANVAS_W / CANVAS_H; // 0.667
const contentAspect = contentW / contentH;
let srcX: number, srcY: number, srcW: number, srcH: number;
if (contentAspect > targetAspect) {
// Content is wider than target — crop sides
srcH = contentH;
srcW = Math.round(contentH * targetAspect);
srcX = contentX + Math.round((contentW - srcW) / 2);
srcY = contentY;
} else {
// Content is taller than target — crop top/bottom
srcW = contentW;
srcH = Math.round(contentW / targetAspect);
srcX = contentX;
srcY = contentY + Math.round((contentH - srcH) / 2);
}
// Step 4: Draw cropped region onto final 512x768 canvas
const canvas = document.createElement('canvas');
canvas.width = CANVAS_W;
canvas.height = CANVAS_H;
const ctx = canvas.getContext('2d')!;
// Fill with warm gallery bg in case of rounding gaps
ctx.fillStyle = '#f0ebe4';
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
ctx.drawImage(
img,
srcX, srcY, srcW, srcH, // source rect (cropped)
0, 0, CANVAS_W, CANVAS_H // dest rect (fill canvas)
);
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace;
texture.generateMipmaps = true;
texture.minFilter = THREE.LinearMipmapLinearFilter;
texture.magFilter = THREE.LinearFilter;
resolve(texture);
};
img.onerror = () => reject(new Error(`Failed to load image: ${imageUrl}`));
img.src = imageUrl;
});
}
/**
* Fallback: warm canvas with a subtle message (for all-white or broken images).
*/
function createFallbackTexture(label: string): THREE.CanvasTexture {
const canvas = document.createElement('canvas');
canvas.width = CANVAS_W;
canvas.height = CANVAS_H;
const ctx = canvas.getContext('2d')!;
ctx.fillStyle = '#f0ebe4';
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
ctx.fillStyle = '#888888';
ctx.font = '18px Inter, -apple-system, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Image unavailable', CANVAS_W / 2, CANVAS_H / 2);
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace;
return texture;
}