enable custom border radius
This commit is contained in:
@@ -57,6 +57,10 @@ interface SettingsPanelProps {
|
||||
onBlurChange?: (showBlur: boolean) => void;
|
||||
motionBlurEnabled?: boolean;
|
||||
onMotionBlurChange?: (enabled: boolean) => void;
|
||||
borderRadius?: number;
|
||||
onBorderRadiusChange?: (radius: number) => void;
|
||||
padding?: number;
|
||||
onPaddingChange?: (padding: number) => void;
|
||||
cropRegion?: CropRegion;
|
||||
onCropChange?: (region: CropRegion) => void;
|
||||
videoElement?: HTMLVideoElement | null;
|
||||
@@ -74,7 +78,7 @@ const ZOOM_DEPTH_OPTIONS: Array<{ depth: ZoomDepth; label: string }> = [
|
||||
{ depth: 6, label: "5×" },
|
||||
];
|
||||
|
||||
export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth, onZoomDepthChange, selectedZoomId, onZoomDelete, shadowIntensity = 0, onShadowChange, showBlur, onBlurChange, motionBlurEnabled = true, onMotionBlurChange, cropRegion, onCropChange, videoElement, onExport }: SettingsPanelProps) {
|
||||
export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth, onZoomDepthChange, selectedZoomId, onZoomDelete, shadowIntensity = 0, onShadowChange, showBlur, onBlurChange, motionBlurEnabled = true, onMotionBlurChange, borderRadius = 0, onBorderRadiusChange, padding = 0, onPaddingChange, cropRegion, onCropChange, videoElement, onExport }: SettingsPanelProps) {
|
||||
const [wallpaperPaths, setWallpaperPaths] = useState<string[]>([]);
|
||||
const [customImages, setCustomImages] = useState<string[]>([]);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -243,6 +247,36 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
|
||||
className="data-[state=checked]:bg-[#34B27B]"
|
||||
/>
|
||||
</div>
|
||||
{/* Corner Roundness Slider */}
|
||||
<div className="p-3 rounded-xl bg-white/5 border border-white/5 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs font-medium text-slate-200">Roundness</div>
|
||||
<span className="text-[10px] text-slate-400 font-mono">{borderRadius}px</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[borderRadius]}
|
||||
onValueChange={(values) => onBorderRadiusChange?.(values[0])}
|
||||
min={0}
|
||||
max={16}
|
||||
step={0.5}
|
||||
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B]"
|
||||
/>
|
||||
</div>
|
||||
{/* Padding Slider */}
|
||||
<div className="p-3 rounded-xl bg-white/5 border border-white/5 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs font-medium text-slate-200">Padding (TODO)</div>
|
||||
<span className="text-[10px] text-slate-400 font-mono">{padding}px</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[padding]}
|
||||
onValueChange={(values) => onPaddingChange?.(values[0])}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ export default function VideoEditor() {
|
||||
const [shadowIntensity, setShadowIntensity] = useState(0);
|
||||
const [showBlur, setShowBlur] = useState(false);
|
||||
const [motionBlurEnabled, setMotionBlurEnabled] = useState(true);
|
||||
const [borderRadius, setBorderRadius] = useState(0);
|
||||
const [padding, setPadding] = useState(0);
|
||||
const [cropRegion, setCropRegion] = useState<CropRegion>(DEFAULT_CROP_REGION);
|
||||
const [zoomRegions, setZoomRegions] = useState<ZoomRegion[]>([]);
|
||||
const [selectedZoomId, setSelectedZoomId] = useState<string | null>(null);
|
||||
@@ -303,6 +305,7 @@ export default function VideoEditor() {
|
||||
shadowIntensity,
|
||||
showBlur,
|
||||
motionBlurEnabled,
|
||||
borderRadius,
|
||||
cropRegion,
|
||||
onProgress: (progress: ExportProgress) => {
|
||||
setExportProgress(progress);
|
||||
@@ -344,7 +347,7 @@ export default function VideoEditor() {
|
||||
setIsExporting(false);
|
||||
exporterRef.current = null;
|
||||
}
|
||||
}, [videoPath, wallpaper, zoomRegions, trimRegions, shadowIntensity, showBlur, motionBlurEnabled, cropRegion, isPlaying]);
|
||||
}, [videoPath, wallpaper, zoomRegions, trimRegions, shadowIntensity, showBlur, motionBlurEnabled, borderRadius, cropRegion, isPlaying]);
|
||||
|
||||
const handleCancelExport = useCallback(() => {
|
||||
if (exporterRef.current) {
|
||||
@@ -409,6 +412,7 @@ export default function VideoEditor() {
|
||||
shadowIntensity={shadowIntensity}
|
||||
showBlur={showBlur}
|
||||
motionBlurEnabled={motionBlurEnabled}
|
||||
borderRadius={borderRadius}
|
||||
cropRegion={cropRegion}
|
||||
trimRegions={trimRegions}
|
||||
/>
|
||||
@@ -472,6 +476,10 @@ export default function VideoEditor() {
|
||||
onBlurChange={setShowBlur}
|
||||
motionBlurEnabled={motionBlurEnabled}
|
||||
onMotionBlurChange={setMotionBlurEnabled}
|
||||
borderRadius={borderRadius}
|
||||
onBorderRadiusChange={setBorderRadius}
|
||||
padding={padding}
|
||||
onPaddingChange={setPadding}
|
||||
cropRegion={cropRegion}
|
||||
onCropChange={setCropRegion}
|
||||
videoElement={videoPlaybackRef.current?.video || null}
|
||||
|
||||
@@ -28,6 +28,7 @@ interface VideoPlaybackProps {
|
||||
shadowIntensity?: number;
|
||||
showBlur?: boolean;
|
||||
motionBlurEnabled?: boolean;
|
||||
borderRadius?: number;
|
||||
cropRegion?: import('./types').CropRegion;
|
||||
trimRegions?: TrimRegion[];
|
||||
}
|
||||
@@ -57,6 +58,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
|
||||
shadowIntensity = 0,
|
||||
showBlur,
|
||||
motionBlurEnabled = true,
|
||||
borderRadius = 0,
|
||||
cropRegion,
|
||||
trimRegions = [],
|
||||
}, ref) => {
|
||||
@@ -150,6 +152,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
|
||||
videoElement,
|
||||
cropRegion,
|
||||
lockedVideoDimensions: lockedVideoDimensionsRef.current,
|
||||
borderRadius,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
@@ -171,7 +174,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
|
||||
|
||||
updateOverlayForRegion(activeRegion);
|
||||
}
|
||||
}, [updateOverlayForRegion, cropRegion]);
|
||||
}, [updateOverlayForRegion, cropRegion, borderRadius]);
|
||||
|
||||
useEffect(() => {
|
||||
layoutVideoContentRef.current = layoutVideoContent;
|
||||
|
||||
@@ -10,6 +10,7 @@ interface LayoutParams {
|
||||
videoElement: HTMLVideoElement;
|
||||
cropRegion?: CropRegion;
|
||||
lockedVideoDimensions?: { width: number; height: number } | null;
|
||||
borderRadius?: number;
|
||||
}
|
||||
|
||||
interface LayoutResult {
|
||||
@@ -22,7 +23,7 @@ interface LayoutResult {
|
||||
}
|
||||
|
||||
export function layoutVideoContent(params: LayoutParams): LayoutResult | null {
|
||||
const { container, app, videoSprite, maskGraphics, videoElement, cropRegion, lockedVideoDimensions } = params;
|
||||
const { container, app, videoSprite, maskGraphics, videoElement, cropRegion, lockedVideoDimensions, borderRadius = 0 } = params;
|
||||
|
||||
const videoWidth = lockedVideoDimensions?.width || videoElement.videoWidth;
|
||||
const videoHeight = lockedVideoDimensions?.height || videoElement.videoHeight;
|
||||
@@ -91,9 +92,10 @@ export function layoutVideoContent(params: LayoutParams): LayoutResult | null {
|
||||
// Create a mask that only shows the cropped region (centered in container)
|
||||
const maskX = centerOffsetX;
|
||||
const maskY = centerOffsetY;
|
||||
const radius = Math.min(croppedDisplayWidth, croppedDisplayHeight) * 0.02;
|
||||
|
||||
// Apply border radius
|
||||
maskGraphics.clear();
|
||||
maskGraphics.roundRect(maskX, maskY, croppedDisplayWidth, croppedDisplayHeight, radius);
|
||||
maskGraphics.roundRect(maskX, maskY, croppedDisplayWidth, croppedDisplayHeight, borderRadius);
|
||||
maskGraphics.fill({ color: 0xffffff });
|
||||
|
||||
return {
|
||||
|
||||
@@ -15,6 +15,7 @@ interface FrameRenderConfig {
|
||||
shadowIntensity: number;
|
||||
showBlur: boolean;
|
||||
motionBlurEnabled?: boolean;
|
||||
borderRadius?: number;
|
||||
cropRegion: CropRegion;
|
||||
videoWidth: number;
|
||||
videoHeight: number;
|
||||
@@ -301,10 +302,20 @@ export class FrameRenderer {
|
||||
if (!this.app || !this.videoSprite || !this.maskGraphics || !this.videoContainer) return;
|
||||
|
||||
const { width, height } = this.config;
|
||||
const { cropRegion } = this.config;
|
||||
const { cropRegion, borderRadius = 0 } = this.config;
|
||||
const videoWidth = this.config.videoWidth;
|
||||
const videoHeight = this.config.videoHeight;
|
||||
|
||||
// Log layout calculation once (only on first layout)
|
||||
if (!this.layoutCache) {
|
||||
console.log('[FrameRenderer] Initial updateLayout', {
|
||||
canvasSize: { width, height },
|
||||
videoSize: { width: videoWidth, height: videoHeight },
|
||||
cropRegion,
|
||||
borderRadius,
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate cropped video dimensions
|
||||
const cropStartX = cropRegion.x;
|
||||
const cropStartY = cropRegion.y;
|
||||
@@ -337,9 +348,24 @@ export class FrameRenderer {
|
||||
this.videoContainer.y = centerOffsetY;
|
||||
|
||||
// Update mask
|
||||
const radius = Math.min(croppedDisplayWidth, croppedDisplayHeight) * 0.02;
|
||||
// Scale the border radius if needed?
|
||||
// In preview, we use the raw pixel value from the slider.
|
||||
// In export, the canvas might be much larger (e.g. 4K vs 800px preview).
|
||||
// If we use the raw value (e.g. 20px), it will look tiny on 4K.
|
||||
// We should probably scale it based on the resolution ratio relative to a "standard" preview size (e.g. 1920x1080 or similar).
|
||||
// Or, we can assume the user sees it on a ~1000px wide preview.
|
||||
// Let's scale it by (width / 1280) as a rough heuristic to match visual appearance?
|
||||
// Actually, let's just use the raw value for now as requested "fine grain control".
|
||||
// If the user sets 20px, they might expect 20px.
|
||||
// BUT, if they are editing on a small screen and exporting to 4K, 20px will look different.
|
||||
// Let's stick to raw value first as it's safer than guessing.
|
||||
// Wait, the previous hardcoded value was percentage based: radius = min(w, h) * 0.02
|
||||
// If I use raw pixels, I break that "responsiveness".
|
||||
// However, the slider is in pixels (0-40).
|
||||
// I will use the raw value for now.
|
||||
|
||||
this.maskGraphics.clear();
|
||||
this.maskGraphics.roundRect(0, 0, croppedDisplayWidth, croppedDisplayHeight, radius);
|
||||
this.maskGraphics.roundRect(0, 0, croppedDisplayWidth, croppedDisplayHeight, borderRadius);
|
||||
this.maskGraphics.fill({ color: 0xffffff });
|
||||
|
||||
// Cache layout info
|
||||
|
||||
@@ -13,6 +13,7 @@ interface VideoExporterConfig extends ExportConfig {
|
||||
shadowIntensity: number;
|
||||
showBlur: boolean;
|
||||
motionBlurEnabled?: boolean;
|
||||
borderRadius?: number;
|
||||
videoPadding?: number;
|
||||
cropRegion: CropRegion;
|
||||
onProgress?: (progress: ExportProgress) => void;
|
||||
@@ -87,6 +88,7 @@ export class VideoExporter {
|
||||
shadowIntensity: this.config.shadowIntensity,
|
||||
showBlur: this.config.showBlur,
|
||||
motionBlurEnabled: this.config.motionBlurEnabled,
|
||||
borderRadius: this.config.borderRadius,
|
||||
cropRegion: this.config.cropRegion,
|
||||
videoWidth: videoInfo.width,
|
||||
videoHeight: videoInfo.height,
|
||||
|
||||
Reference in New Issue
Block a user