Remove cancelled webcam glass border while retaining geometry and renderer fixes

This commit is contained in:
webadderall committed 2026-11-07 06:19:11 +11:00
1 parent 4c81567dd5
commit d04cd7068d
9 files changed
+3 -200

No files matched your search

@@ -1,4 +1,3 @@
import { getWebcamGlassRimImage } from "../video-editor/webcamGlassRim";
import type { MouseEventHandler, ReactNode } from "react";
import type { useWebcamPreviewOverlay } from "./hooks/useWebcamPreviewOverlay";
import styles from "./LaunchWindow.module.css";
@@ -67,14 +66,6 @@ export function FloatingWebcamPreviewView({
clipPath: "circle(50%)",
}}
/>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0"
style={{
backgroundImage: getWebcamGlassRimImage(100, 100, 50, "circle"),
backgroundSize: "100% 100%",
}}
/>
</div>
</div>
);
@@ -1,5 +1,3 @@
import { getWebcamGlassRimImage } from "../../video-editor/webcamGlassRim";
import { getWebcamCornerRadiusPx } from "../../video-editor/webcamOverlay";
import type { WebcamShape } from "../../video-editor/types";
import { getWebcamClipPath } from "../../video-editor/webcamOverlay";
import {
@@ -106,7 +104,7 @@ export function WebcamPopover({
{showWebcamControls && (
<div className="flex justify-center px-3 py-2">
<div
className="relative h-24 w-24 overflow-hidden bg-[var(--launch-hover)]"
className="h-24 w-24 overflow-hidden bg-[var(--launch-hover)]"
style={{
clipPath: getWebcamClipPath(roundness, 96, 96, shape),
}}
@@ -118,19 +116,6 @@ export function WebcamPopover({
playsInline
style={{ transform: "scaleX(-1)" }}
/>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0"
style={{
backgroundImage: getWebcamGlassRimImage(
96,
96,
getWebcamCornerRadiusPx(roundness, 96, 96, shape),
shape,
),
backgroundSize: "100% 100%",
}}
/>
</div>
</div>
)}
@@ -272,15 +272,6 @@ export function PlaybackView(input: PlaybackViewInput) {
/>
</div>
</div>
<div
data-webcam-glass-rim
aria-hidden="true"
className="pointer-events-none absolute inset-0"
style={{
backgroundImage: "var(--webcam-glass-rim)",
backgroundSize: "100% 100%",
}}
/>
</div>
</div>
) : null}
@@ -16,7 +16,6 @@ import {
getWebcamOverlayPosition,
scaleWebcamOverlayPixels,
} from "../webcamOverlay";
import { getWebcamGlassRimImage } from "../webcamGlassRim";
import { findPreviewClipAtTimelineTime } from "./clipPlayback";
import type { PlaybackProps } from "./playbackProps";
import type { usePlaybackState } from "./usePlaybackState";
@@ -202,15 +201,6 @@ export function usePlaybackWebcamLayout(input: usePlaybackWebcamLayoutInput) {
bubbleInner.style.overflow = "hidden";
bubbleInner.style.contain = "paint";
bubbleInner.style.clipPath = webcamClipPath;
bubbleInner.style.setProperty(
"--webcam-glass-rim",
getWebcamGlassRimImage(
scaledDimensions.width,
scaledDimensions.height,
scaledRadius,
webcam?.shape,
),
);
bubbleInner.style.setProperty("-webkit-clip-path", webcamClipPath);
const video = webcamVideoRef.current;
if (!video || video.readyState < 2 || !webcamMediaSynchronized) {
@@ -1,74 +0,0 @@
import { drawSquircleOnCanvas, getSquircleSvgPath } from "@/lib/geometry/squircle";
import type { WebcamShape } from "./types";
import { getWebcamShapeExponent } from "./webcamOverlay";
// A translucent bevel catches light without covering the camera image. Widths
// scale with the camera, so preview and full-resolution exports agree.
const GLASS_STOPS = [
[0, "rgba(255,255,255,0.8)"],
[0.35, "rgba(255,255,255,0.24)"],
[0.65, "rgba(15,23,42,0.16)"],
[1, "rgba(255,255,255,0.5)"],
] as const;
export function getWebcamGlassRimWidth(width: number, height: number): number {
return Math.max(0, Math.min(width, height)) * 0.012;
}
let cachedImage: { key: string; image: string } | undefined;
export function getWebcamGlassRimImage(
width: number,
height: number,
radius: number,
shape?: WebcamShape,
): string {
const key = `${width}:${height}:${radius}:${shape}`;
if (cachedImage?.key === key) return cachedImage.image;
const path = getSquircleSvgPath({
x: 0,
y: 0,
width,
height,
radius,
exponent: getWebcamShapeExponent(shape),
});
const rim = getWebcamGlassRimWidth(width, height);
const stops = GLASS_STOPS.map(
([offset, color]) => `<stop offset="${offset}" stop-color="${color}"/>`,
).join("");
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><defs><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="${width}" y2="${height}">${stops}</linearGradient><clipPath id="c"><path d="${path}"/></clipPath></defs><g clip-path="url(#c)" fill="none"><path d="${path}" stroke="url(#g)" stroke-width="${rim * 2}"/><path d="${path}" stroke="rgba(255,255,255,0.55)" stroke-width="${rim * 0.4}"/></g></svg>`;
const image = `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
cachedImage = { key, image };
return image;
}
export function drawWebcamGlassRim(
ctx: CanvasRenderingContext2D,
width: number,
height: number,
radius: number,
shape?: WebcamShape,
): void {
if (width <= 0 || height <= 0) return;
ctx.save();
drawSquircleOnCanvas(ctx, {
x: 0,
y: 0,
width,
height,
radius,
exponent: getWebcamShapeExponent(shape),
});
ctx.clip();
const gradient = ctx.createLinearGradient(0, 0, width, height);
for (const [offset, color] of GLASS_STOPS) gradient.addColorStop(offset, color);
const rim = getWebcamGlassRimWidth(width, height);
ctx.strokeStyle = gradient;
ctx.lineWidth = rim * 2;
ctx.stroke();
ctx.strokeStyle = "rgba(255,255,255,0.55)";
ctx.lineWidth = rim * 0.4;
ctx.stroke();
ctx.restore();
}
@@ -1,7 +1,6 @@
import { getWebcamShadowFilter, getWebcamShadowPadding } from "@/lib/exporter/shadowProfile";
import { drawSquircleOnCanvas } from "@/lib/geometry/squircle";
import type { CropRegion, WebcamShape } from "./types";
import { drawWebcamGlassRim } from "./webcamGlassRim";
import { getWebcamCropSourceRect, getWebcamShapeExponent } from "./webcamOverlay";
export interface WebcamMotionLayout {
@@ -221,16 +220,6 @@ export class WebcamMotionBlur {
ctx.restore();
}
ctx.restore();
ctx.save();
ctx.scale(resolution, resolution);
ctx.globalAlpha = 1 / this.samples.length;
for (const sample of this.samples) {
ctx.save();
ctx.translate(sample.x - x, sample.y - y);
drawWebcamGlassRim(ctx, sample.width, sample.height, sample.radius, options.shape);
ctx.restore();
}
ctx.restore();
out.clearRect(0, 0, pixelWidth, pixelHeight);
out.save();
out.filter = getWebcamShadowFilter(shadowSize * resolution, shadow);
@@ -13,8 +13,6 @@ export function createMockContext() {
lineTo: vi.fn(),
closePath: vi.fn(),
clip: vi.fn(),
stroke: vi.fn(),
createLinearGradient: vi.fn(() => ({ addColorStop: vi.fn() })),
save: vi.fn(),
restore: vi.fn(),
getImageData: vi.fn(() => ({ data: new Uint8ClampedArray(0) })),
+2 -5
View File
@@ -124,7 +124,7 @@ test("new recordings default to a circle and retain the squircle option", async
await expect(mask).toHaveCSS("clip-path", "circle(50% at 50% 50%)");
});
test("Rectangle starts at 60% width, keeps Square roundness and shows a glass rim", async ({
test("Rectangle starts at 60% width, keeps Square roundness", async ({
page,
}) => {
await installDesktopBridge(page);
@@ -150,15 +150,12 @@ test("Rectangle starts at 60% width, keeps Square roundness and shows a glass ri
await expect(page.getByRole("slider", { name: "Webcam Width" })).toHaveValue("60");
await expect(page.getByRole("slider", { name: "Webcam Height" })).toHaveValue("40");
await expect(roundness).toHaveValue("25");
const rim = page.locator("[data-webcam-glass-rim]");
await expect(rim).toBeVisible();
await expect(rim).toHaveCSS("background-image", /data:image\/svg\+xml/);
await expect(page.locator("[data-webcam-overlay]")).toHaveAttribute(
"data-webcam-ready",
"true",
);
await page.screenshot({
path: "test-results/webcam-rectangle-glass-rim.png",
path: "test-results/webcam-rectangle-geometry.png",
animations: "disabled",
});
await page.getByRole("row", { name: "Square", exact: true }).click();
-64
View File
@@ -1,64 +0,0 @@
import { expect, test } from "@playwright/test";
test("glass rim has the same light direction and opacity in CSS and export at every shape", async ({
page,
}, info) => {
await page.goto("/tests/ui/webcam-edge.html");
const results = await page.evaluate(async () => {
const modulePath = "/src/components/video-editor/webcamGlassRim.ts";
const { drawWebcamGlassRim, getWebcamGlassRimImage } = await import(
/* @vite-ignore */ modulePath
);
const results = [];
for (const shape of ["circle", "squircle", "rectangle"]) {
const width = 240;
const height = shape === "rectangle" ? 160 : 240;
const radius = shape === "circle" ? 120 : height * 0.25;
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d")!;
drawWebcamGlassRim(ctx, width, height, radius, shape);
const css = document.createElement("canvas");
css.width = width;
css.height = height;
const cssCtx = css.getContext("2d")!;
const image = new Image();
image.src = getWebcamGlassRimImage(width, height, radius, shape).slice(5, -2);
await image.decode();
cssCtx.drawImage(image, 0, 0);
const points = [
[width / 2, 1],
[1, height / 2],
[width - 2, height / 2],
[width / 2, height - 2],
];
results.push({
shape,
edges: points.map(([x, y]) => ({
canvas: Array.from(ctx.getImageData(x, y, 1, 1).data),
css: Array.from(cssCtx.getImageData(x, y, 1, 1).data),
})),
center: Array.from(ctx.getImageData(width / 2, height / 2, 1, 1).data),
});
for (const output of [css, canvas]) {
output.style.background = "#707b8e";
output.dataset.shape = shape;
document.body.append(output);
}
}
return results;
});
for (const result of results) {
expect(result.center).toEqual([0, 0, 0, 0]);
for (const edge of result.edges) {
expect(edge.canvas[3]).toBeGreaterThan(0);
for (let channel = 0; channel < 4; channel++)
expect(
Math.abs(edge.canvas[channel] - edge.css[channel]),
`${result.shape} channel ${channel}`,
).toBeLessThanOrEqual(3);
}
}
await page.screenshot({ path: info.outputPath("glass-rim-css-export.png"), fullPage: true });
});