mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-11-09 15:33:31 +00:00
Remove cancelled webcam glass border while retaining geometry and renderer fixes
This commit is contained in:
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) })),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
Reference in new issue
Block a user