diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index db7e33c4..9115d781 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -12,10 +12,7 @@ import { useState, } from "react"; import { getAssetPath, getRenderableAssetUrl, getRenderableVideoUrl } from "@/lib/assetPath"; -import { - getWebcamShadowStrength, - WEBCAM_SHADOW_LAYER_PROFILES, -} from "@/lib/exporter/shadowProfile"; +import { getWebcamShadowFilter } from "@/lib/exporter/shadowProfile"; import { clampMediaTimeToDuration, enablePitchPreservingPlayback, @@ -799,8 +796,6 @@ const VideoPlayback = forwardRef( const webcamCorner = webcam?.corner ?? "bottom-right"; const webcamRoundness = webcam?.roundness ?? DEFAULT_WEBCAM_ROUNDNESS; const webcamShadow = webcam?.shadow ?? DEFAULT_WEBCAM_SHADOW; - const webcamShadowStrength = getWebcamShadowStrength(webcamShadow); - const webcamShadowProfile = WEBCAM_SHADOW_LAYER_PROFILES[0]; const webcamTimeOffsetMs = webcam?.timeOffsetMs; const webcamCropRegion = webcam?.cropRegion; const webcamMirror = webcam?.mirror ?? false; @@ -891,15 +886,7 @@ const VideoPlayback = forwardRef( radius: scaledRadius, }); const shadowSize = Math.min(scaledDimensions.width, scaledDimensions.height); - const shadowOffset = - shadowSize * webcamShadowProfile.offsetScale * webcamShadowStrength; - const shadowBlur = - shadowSize * webcamShadowProfile.blurScale * webcamShadowStrength; - const shadowAlpha = webcamShadowProfile.alphaScale * webcamShadowStrength; - bubble.style.filter = - webcamShadowStrength > 0 - ? `drop-shadow(0 ${shadowOffset}px ${shadowBlur}px rgba(0, 0, 0, ${shadowAlpha}))` - : "none"; + bubble.style.filter = getWebcamShadowFilter(shadowSize, webcamShadow); bubble.style.borderRadius = "0px"; bubble.style.boxShadow = "none"; @@ -918,7 +905,7 @@ const VideoPlayback = forwardRef( webcamPositionX, webcamPositionY, webcamReactToZoom, - webcamShadowStrength, + webcamShadow, webcamHeight, webcamVideoPath, webcamWidth, diff --git a/src/components/video-editor/webcamOverlay.test.ts b/src/components/video-editor/webcamOverlay.test.ts index 502739ab..96ea0f4c 100644 --- a/src/components/video-editor/webcamOverlay.test.ts +++ b/src/components/video-editor/webcamOverlay.test.ts @@ -12,9 +12,10 @@ import { } from "./webcamOverlay"; describe("webcam roundness", () => { - it("uses a percentage of the maximum radius", () => { + it("uses a stronger response curve while preserving the endpoints", () => { expect(getWebcamCornerRadiusPx(0, 640, 360)).toBe(0); - expect(getWebcamCornerRadiusPx(50, 640, 360)).toBe(90); + expect(getWebcamCornerRadiusPx(25, 640, 360)).toBe(90); + expect(getWebcamCornerRadiusPx(50, 640, 360)).toBeCloseTo(127.28, 1); expect(getWebcamCornerRadiusPx(100, 640, 360)).toBe(180); }); diff --git a/src/components/video-editor/webcamOverlay.ts b/src/components/video-editor/webcamOverlay.ts index ae8603c5..dfe633e8 100644 --- a/src/components/video-editor/webcamOverlay.ts +++ b/src/components/video-editor/webcamOverlay.ts @@ -20,7 +20,8 @@ export function getWebcamCornerRadiusPx( height: number, ): number { const safeRoundness = Number.isFinite(roundnessPercent) ? clamp(roundnessPercent, 0, 100) : 0; - return (Math.max(0, Math.min(width, height)) / 2) * (safeRoundness / 100); + const strengthenedRoundness = Math.sqrt(safeRoundness / 100); + return (Math.max(0, Math.min(width, height)) / 2) * strengthenedRoundness; } export function convertLegacyWebcamRadiusToRoundness( diff --git a/src/lib/exporter/frameRenderer.ts b/src/lib/exporter/frameRenderer.ts index 81ce6dae..1d1fc19f 100644 --- a/src/lib/exporter/frameRenderer.ts +++ b/src/lib/exporter/frameRenderer.ts @@ -59,7 +59,7 @@ import { scaleWebcamOverlayPixels, } from "@/components/video-editor/webcamOverlay"; import { getAssetPath, getExportableVideoUrl, getRenderableAssetUrl } from "@/lib/assetPath"; -import { getWebcamShadowStrength } from "@/lib/exporter/shadowProfile"; +import { getWebcamShadowFilter } from "@/lib/exporter/shadowProfile"; import { drawSquircleOnCanvas, drawSquircleOnGraphics } from "@/lib/geometry/squircle"; import { clampMediaTimeToDuration, @@ -2166,10 +2166,9 @@ export class FrameRenderer { bubbleCtx.restore(); if ((webcam.shadow ?? 0) > 0) { - const shadow = getWebcamShadowStrength(webcam.shadow); const shadowSize = Math.min(dimensions.width, dimensions.height); ctx.save(); - ctx.filter = `drop-shadow(0 ${Math.round(shadowSize * 0.06)}px ${Math.round(shadowSize * 0.22)}px rgba(0,0,0,${shadow}))`; + ctx.filter = getWebcamShadowFilter(shadowSize, webcam.shadow); ctx.drawImage(bubbleCanvas, x, y, dimensions.width, dimensions.height); ctx.restore(); return; diff --git a/src/lib/exporter/shadowProfile.test.ts b/src/lib/exporter/shadowProfile.test.ts index 04849de0..7c8ce8c8 100644 --- a/src/lib/exporter/shadowProfile.test.ts +++ b/src/lib/exporter/shadowProfile.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "vitest"; -import { getWebcamShadowStrength } from "./shadowProfile"; +import { + getWebcamShadowFilter, + getWebcamShadowStrength, + WEBCAM_SHADOW_LAYER_PROFILES, +} from "./shadowProfile"; describe("getWebcamShadowStrength", () => { it("doubles the webcam shadow response and clamps it", () => { @@ -8,4 +12,13 @@ describe("getWebcamShadowStrength", () => { expect(getWebcamShadowStrength(0.5)).toBe(1); expect(getWebcamShadowStrength(1)).toBe(1); }); + + it("adds a tighter contact shadow so strength also increases density", () => { + expect(WEBCAM_SHADOW_LAYER_PROFILES).toHaveLength(2); + expect(getWebcamShadowFilter(100, 0)).toBe("none"); + const filter = getWebcamShadowFilter(100, 0.5); + expect(filter.match(/drop-shadow/g)).toHaveLength(2); + expect(filter).toContain("rgba(0, 0, 0, 0.9)"); + expect(filter).toContain("rgba(0, 0, 0, 0.8)"); + }); }); diff --git a/src/lib/exporter/shadowProfile.ts b/src/lib/exporter/shadowProfile.ts index 9117eb77..18bf0812 100644 --- a/src/lib/exporter/shadowProfile.ts +++ b/src/lib/exporter/shadowProfile.ts @@ -11,13 +11,28 @@ export const VIDEO_SHADOW_LAYER_PROFILES: ReadonlyArray = Ob ]); export const WEBCAM_SHADOW_LAYER_PROFILES: ReadonlyArray = Object.freeze([ - { offsetScale: 0.06, alphaScale: 1, blurScale: 0.22 }, + { offsetScale: 0.06, alphaScale: 0.9, blurScale: 0.22 }, + { offsetScale: 0.025, alphaScale: 0.8, blurScale: 0.07 }, ]); export function getWebcamShadowStrength(value: number): number { return Math.min(1, Math.max(0, Number.isFinite(value) ? value * 2 : 0)); } +export function getWebcamShadowFilter(shadowSize: number, value: number): string { + const strength = getWebcamShadowStrength(value); + if (strength <= 0) { + return "none"; + } + + return WEBCAM_SHADOW_LAYER_PROFILES.map((profile) => { + const offset = Math.max(0, shadowSize) * profile.offsetScale * strength; + const blur = Math.max(0, shadowSize) * profile.blurScale * strength; + const alpha = Math.min(1, profile.alphaScale * strength); + return `drop-shadow(0 ${offset}px ${blur}px rgba(0, 0, 0, ${alpha}))`; + }).join(" "); +} + export function getShadowFilterPadding(blur: number, offsetY: number): number { return Math.ceil(Math.max(0, blur * 2 + Math.abs(offsetY))); }