fix(gate-b): drawMaskOverlay — cutout on checkerboard, no green overlay (#55)

Replace green overlay rendering with cutout-on-checkerboard preview:
- Slab pixels show actual photo RGB with mask-based alpha
- Removed background area shows 10px checkerboard (224/192 gray)
- Knob debounce from a1d4a96 preserved unchanged

Update Playwright test to assert checkerboard + slab pixels
visible, not green overlay avgAlpha.
This commit is contained in:
grokbot_ty
2026-09-08 00:30:24 +00:00
parent 3496670045
commit 83171fab6e
2 changed files with 92 additions and 55 deletions
+60 -42
View File
@@ -2,7 +2,7 @@
* LIVE mask verification — issue #55, Gate B.
*
* Tests three fundamentals against the deployed host:
* 1. Overlay translucent — photo visible, not solid green washout.
* 1. Cutout preview — slab pixels visible, checkerboard where background removed.
* 2. Knobs respond fast — debounce working, no multi-second lag.
* 3. Auto/green/black sheet does not mark entire frame as slab.
*
@@ -34,7 +34,7 @@ async function loadToMaskStep(page: import('@playwright/test').Page) {
}
test.describe('LIVE mask fundamentals (issue #55)', () => {
test('1. Overlay translucent — photo visible under green mask, not solid green washout', async ({ page }) => {
test('1. Cutout preview — slab pixels visible on checkerboard, no green overlay', async ({ page }) => {
await page.goto('/');
await loadToMaskStep(page);
@@ -44,7 +44,8 @@ test.describe('LIVE mask fundamentals (issue #55)', () => {
path: resolve(evidenceDir, 'mask-overlay-live.png'),
});
// Read canvas pixel data to assert overlay is translucent.
// Read canvas pixel data: slab pixels show actual photo RGB,
// background shows checkerboard (224/192 gray, 255 alpha), no green.
const stats = await page.evaluate(() => {
const canvas = document.querySelector('canvas.mask-canvas') as HTMLCanvasElement;
if (!canvas) return { width: 0, height: 0 };
@@ -52,47 +53,62 @@ test.describe('LIVE mask fundamentals (issue #55)', () => {
if (!ctx) return { width: 0, height: 0 };
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const { data } = imgData;
const len = data.length;
let totalAlpha = 0;
let greenPixelCount = 0;
let maxAlpha = 0;
for (let i = 0; i < len; i += 4) {
let slabPixels = 0;
let checkerPixels = 0;
let greenTintPixels = 0;
let hasColorVariance = false;
const totalPixels = canvas.width * canvas.height;
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const a = data[i + 3];
totalAlpha += a;
if (a > 0) greenPixelCount++;
if (a > maxAlpha) maxAlpha = a;
// Checkerboard pixel: gray (224 or 192), full alpha, no color.
if (a === 255 && (r === 224 || r === 192) && r === g && g === b) {
checkerPixels++;
}
// Slab pixel: not checkerboard, alpha from mask (may vary).
else if (a > 0) {
slabPixels++;
// Check if any slab pixel has color variance (not monochrome gray).
const maxC = Math.max(r, g, b);
const minC = Math.min(r, g, b);
if (maxC - minC > 5) hasColorVariance = true;
}
// Green-tint detection: green channel significantly > red and blue.
if (g > 180 && r < 100 && b < 100) {
greenTintPixels++;
}
}
const avgAlpha = totalAlpha / (canvas.width * canvas.height);
return {
width: canvas.width,
height: canvas.height,
avgAlpha,
maxAlpha,
greenPixelCount,
totalPixels: canvas.width * canvas.height,
slabPixels,
checkerPixels,
greenTintPixels,
hasColorVariance,
totalPixels,
};
});
console.log(`Canvas: ${stats.width}x${stats.height}, avgAlpha=${stats.avgAlpha.toFixed(1)}, maxAlpha=${stats.maxAlpha}, greenPixels=${stats.greenPixelCount}/${stats.totalPixels} (${(stats.greenPixelCount / stats.totalPixels * 100).toFixed(1)}%)`);
console.log(`Canvas: ${stats.width}x${stats.height}, slab=${stats.slabPixels}, checker=${stats.checkerPixels}, greenTint=${stats.greenTintPixels}`);
// Check 1 assertions:
// a) Canvas has dimensions (not blank)
// a) Canvas has dimensions
expect(stats.width).toBeGreaterThan(0);
expect(stats.height).toBeGreaterThan(0);
// b) Average alpha must be well below 255 — the overlay is translucent.
// Max alpha is 80 (~31% of 255), so average should be < 128 (50% opacity).
expect(stats.avgAlpha).toBeLessThan(128);
expect(stats.avgAlpha).toBeGreaterThan(0); // Must have some overlay at all.
// b) Must have checkerboard pixels (removed background)
expect(stats.checkerPixels).toBeGreaterThan(0);
// c) Max alpha must be 80 or close (the fix set max alpha to 80).
expect(stats.maxAlpha).toBeLessThan(170); // Well below full opacity.
// c) Must have slab pixels (the wood showing through)
expect(stats.slabPixels).toBeGreaterThan(0);
// d) Green pixel fraction should not be ~100% — the overlay should NOT cover
// the entire canvas with green washout. There should be visible photo areas.
const greenFraction = stats.greenPixelCount / stats.totalPixels;
expect(greenFraction).toBeLessThan(0.95);
console.log(`Green overlay fraction: ${greenFraction.toFixed(2)} (must be <0.95)`);
// d) Slab pixels must have color variance — real wood, not green wash
expect(stats.hasColorVariance).toBe(true);
// e) No green tint pixels — the green overlay is gone
expect(stats.greenTintPixels).toBe(0);
});
test('2. Knobs respond fast on real photo — debounce working, no multi-second lag', async ({ page }) => {
@@ -143,11 +159,9 @@ test.describe('LIVE mask fundamentals (issue #55)', () => {
await loadToMaskStep(page);
// Read the canvas pixel data and compute slab fraction.
// The canvas shows: photo + green overlay on slab pixels.
// Green overlay = (60, 220, 60) with alpha proportional to mask value.
// Sheet pixels have alpha=0 (fully transparent).
// So: count pixels where the green channel is > 150 AND red channel < 100
// to detect the green overlay, which indicates slab pixels.
// Canvas now shows: slab pixels (actual photo RGB, mask alpha) +
// checkerboard (224/192 gray, 255 alpha) where background was removed.
// Slab pixels are any non-checkerboard pixels with alpha > 0.
const slabStats = await page.evaluate(() => {
const canvas = document.querySelector('canvas.mask-canvas') as HTMLCanvasElement;
if (!canvas) return { width: 0, height: 0 };
@@ -162,12 +176,9 @@ test.describe('LIVE mask fundamentals (issue #55)', () => {
const g = data[i + 1];
const b = data[i + 2];
const a = data[i + 3];
// Green overlay is (60, 220, 60). Slab pixels have alpha > 0.
// At max alpha (80), the blended pixel on white (240,240,240) would be:
// R: 240*(1-0.31) + 60*0.31 ≈ 184
// G: 240*(1-0.31) + 220*0.31 ≈ 234
// So slab pixels have R < 240, G > 200, B < 240, a > 0
if (a > 0 && g > 150 && r < 230 && b < 230) {
// Checkerboard: r==g==b and r is 224 or 192, alpha 255.
const isChecker = a === 255 && r === g && g === b && (r === 224 || r === 192);
if (!isChecker && a > 0) {
slabPixels++;
}
}
@@ -214,8 +225,15 @@ test.describe('LIVE mask fundamentals (issue #55)', () => {
const { data } = imgData;
let slabPixels = 0;
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const a = data[i + 3];
if (a > 0) slabPixels++;
// Checkerboard: r==g==b and r is 224 or 192, alpha 255.
const isChecker = a === 255 && r === g && g === b && (r === 224 || r === 192);
if (!isChecker && a > 0) {
slabPixels++;
}
}
resolve({
width: canvas.width,