diff --git a/apps/desktop/src/routes/editor/ConfigSidebar.tsx b/apps/desktop/src/routes/editor/ConfigSidebar.tsx index 4344bc09c7..b2c9c1dadb 100644 --- a/apps/desktop/src/routes/editor/ConfigSidebar.tsx +++ b/apps/desktop/src/routes/editor/ConfigSidebar.tsx @@ -72,7 +72,7 @@ import IconLucideWind from "~icons/lucide/wind"; import { CaptionsTab } from "./CaptionsTab"; import { syncCaptionWordsWithText } from "./captions"; import { getColorPreviewBorderColor, hexToRgb, RgbInput } from "./color-utils"; -import { type CornerRoundingType, useEditorContext } from "./context"; +import { useEditorContext } from "./context"; import { GradientEditor } from "./GradientEditor"; import { KeyboardTab } from "./KeyboardTab"; import { evaluateMask, type MaskKind, type MaskSegment } from "./masks"; @@ -220,11 +220,6 @@ const CAMERA_X_POSITIONS = [ ] satisfies CameraXPosition[]; const CAMERA_Y_POSITIONS = ["top", "bottom"] satisfies CameraYPosition[]; -const CORNER_STYLE_OPTIONS = [ - { name: "Squircle", value: "squircle" }, - { name: "Rounded", value: "rounded" }, -] satisfies Array<{ name: string; value: CornerRoundingType }>; - const BACKGROUND_THEMES = { macOS: "macOS", dark: "Dark", @@ -305,11 +300,11 @@ const findCursorPreset = ( (option) => option.preset && Math.abs(option.preset.tension - values.tension) <= - CURSOR_PRESET_TOLERANCE.tension && + CURSOR_PRESET_TOLERANCE.tension && Math.abs(option.preset.mass - values.mass) <= - CURSOR_PRESET_TOLERANCE.mass && + CURSOR_PRESET_TOLERANCE.mass && Math.abs(option.preset.friction - values.friction) <= - CURSOR_PRESET_TOLERANCE.friction, + CURSOR_PRESET_TOLERANCE.friction, ); return preset?.value ?? null; @@ -450,7 +445,7 @@ export function ConfigSidebar() { class={cx( "flex justify-center relative border-transparent border z-10 items-center rounded-md size-9 transition will-change-transform", state.selectedTab !== item.id && - "group-hover:border-gray-300 group-disabled:border-none", + "group-hover:border-gray-300 group-disabled:border-none", )} > @@ -1737,17 +1732,13 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) { ref={setBackgroundRef} class="flex overflow-x-auto overscroll-contain relative z-10 flex-row gap-2 items-center mb-3 text-xs hide-scroll" style={{ - "-webkit-mask-image": `linear-gradient(to right, transparent, black ${ - scrollX() > 0 ? "24px" : "0" - }, black calc(100% - ${ - reachedEndOfScroll() ? "0px" : "24px" - }), transparent)`, - - "mask-image": `linear-gradient(to right, transparent, black ${ - scrollX() > 0 ? "24px" : "0" - }, black calc(100% - ${ - reachedEndOfScroll() ? "0px" : "24px" - }), transparent);`, + "-webkit-mask-image": `linear-gradient(to right, transparent, black ${scrollX() > 0 ? "24px" : "0" + }, black calc(100% - ${reachedEndOfScroll() ? "0px" : "24px" + }), transparent)`, + + "mask-image": `linear-gradient(to right, transparent, black ${scrollX() > 0 ? "24px" : "0" + }, black calc(100% - ${reachedEndOfScroll() ? "0px" : "24px" + }), transparent);`, }} > @@ -1774,10 +1765,10 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) { value={ project.background.source.type === "wallpaper" ? (wallpapers()?.find((w) => - ( - project.background.source as { path?: string } - ).path?.includes(w.id), - )?.url ?? undefined) + ( + project.background.source as { path?: string } + ).path?.includes(w.id), + )?.url ?? undefined) : undefined } onChange={(photoUrl) => { @@ -1907,9 +1898,9 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) { if (!file) return; /* - this is a Tauri bug in WebKit so we need to validate the file type manually - https://github.com/tauri-apps/tauri/issues/9158 - */ + this is a Tauri bug in WebKit so we need to validate the file type manually + https://github.com/tauri-apps/tauri/issues/9158 + */ const validExtensions = [ "jpg", "jpeg", @@ -2055,23 +2046,27 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) { /> }> -
- setProject("background", "rounding", v[0])} - minValue={0} - maxValue={100} - step={0.1} - formatTooltip="%" - /> - - setProject("background", "roundingType", value) - } - /> -
+ setProject("background", "rounding", v[0])} + minValue={0} + maxValue={100} + step={0.1} + formatTooltip="%" + /> +
+ } + > + setProject("background", "roundingSmoothness", v[0])} + minValue={0} + maxValue={1} + step={0.01} + formatTooltip={(value) => `${Math.round(value * 100)}%`} + /> }> }> -
- setProject("camera", "rounding", v[0])} - minValue={0} - maxValue={100} - step={0.1} - formatTooltip="%" - /> - setProject("camera", "roundingType", value)} - /> -
+ setProject("camera", "rounding", v[0])} + minValue={0} + maxValue={100} + step={0.1} + formatTooltip="%" + /> +
+ } + > + setProject("camera", "roundingSmoothness", v[0])} + minValue={0} + maxValue={1} + step={0.01} + formatTooltip={(value) => `${Math.round(value * 100)}%`} + /> }>
@@ -2571,72 +2572,6 @@ function CameraConfig(props: { scrollRef: HTMLDivElement }) { ); } -function CornerStyleSelect(props: { - label?: string; - value: CornerRoundingType; - onChange: (value: CornerRoundingType) => void; -}) { - return ( -
- - {(label) => ( - - {label()} - - )} - - - options={CORNER_STYLE_OPTIONS} - optionValue="value" - optionTextValue="name" - value={CORNER_STYLE_OPTIONS.find( - (option) => option.value === props.value, - )} - onChange={(option) => option && props.onChange(option.value)} - disallowEmptySelection - itemComponent={(itemProps) => ( - - as={KSelect.Item} - item={itemProps.item} - > - - {itemProps.item.rawValue.name} - - - )} - > - - class="flex-1 text-sm text-left truncate text-[--gray-500] font-normal"> - {(state) => {state.selectedOption().name}} - - - as={(iconProps) => ( - - )} - /> - - - - as={KSelect.Content} - class={cx(topSlideAnimateClasses, "z-50")} - > - - class="overflow-y-auto max-h-32" - as={KSelect.Listbox} - /> - - - -
- ); -} - function HexColorInput(props: { value: string; onChange: (value: string) => void; @@ -3285,8 +3220,7 @@ function ZoomSegmentPreview(props: { createEffect(() => { // TODO: make this not hardcoded const path = convertFileSrc( - `${editorInstance.path}/content/segments/segment-${ - clipSegment()?.recordingSegment ?? 0 + `${editorInstance.path}/content/segments/segment-${clipSegment()?.recordingSegment ?? 0 }/display.mp4`, ); video.src = path; @@ -3489,8 +3423,7 @@ function ZoomSegmentConfig(props: { createEffect(() => { const path = convertFileSrc( // TODO: this shouldn't be so hardcoded - `${ - editorInstance.path + `${editorInstance.path }/content/segments/segment-${segmentIndex()}/display.mp4`, ); video.src = path; @@ -3620,7 +3553,7 @@ function ZoomSegmentConfig(props: { x: Math.max( Math.min( (moveEvent.clientX - bounds.left) / - bounds.width, + bounds.width, 1, ), 0, @@ -3628,7 +3561,7 @@ function ZoomSegmentConfig(props: { y: Math.max( Math.min( (moveEvent.clientY - bounds.top) / - bounds.height, + bounds.height, 1, ), 0, diff --git a/apps/desktop/src/routes/editor/context.ts b/apps/desktop/src/routes/editor/context.ts index 24987bd5f1..792270e45c 100644 --- a/apps/desktop/src/routes/editor/context.ts +++ b/apps/desktop/src/routes/editor/context.ts @@ -132,10 +132,6 @@ export type CustomDomainResponse = { domain_verified: boolean | null; }; -export type CornerRoundingType = "rounded" | "squircle"; - -type WithCornerStyle = T & { roundingType: CornerRoundingType }; - type EditorTimelineConfiguration = Omit< TimelineConfiguration, "sceneSegments" | "maskSegments" @@ -149,25 +145,12 @@ export type EditorProjectConfiguration = Omit< ProjectConfiguration, "background" | "camera" | "timeline" > & { - background: WithCornerStyle; - camera: WithCornerStyle; + background: ProjectConfiguration["background"]; + camera: ProjectConfiguration["camera"]; timeline?: EditorTimelineConfiguration | null; hiddenTextSegments?: number[]; }; -function withCornerDefaults< - T extends { - roundingType?: CornerRoundingType; - rounding_type?: CornerRoundingType; - }, ->(value: T): T & { roundingType: CornerRoundingType } { - const roundingType = value.roundingType ?? value.rounding_type ?? "squircle"; - return { - ...value, - roundingType, - }; -} - export function normalizeProject( config: ProjectConfiguration, ): EditorProjectConfiguration { @@ -209,8 +192,6 @@ export function normalizeProject( ...config, keyboard, timeline, - background: withCornerDefaults(config.background), - camera: withCornerDefaults(config.camera), }; } @@ -218,9 +199,6 @@ export function serializeProjectConfiguration( project: EditorProjectConfiguration, ): ProjectConfiguration { const { background, camera, ...rest } = project; - const { roundingType: backgroundRoundingType, ...backgroundRest } = - background; - const { roundingType: cameraRoundingType, ...cameraRest } = camera; const timeline = project.timeline ? { @@ -235,14 +213,8 @@ export function serializeProjectConfiguration( return { ...rest, timeline: timeline as unknown as ProjectConfiguration["timeline"], - background: { - ...backgroundRest, - roundingType: backgroundRoundingType, - }, - camera: { - ...cameraRest, - roundingType: cameraRoundingType, - }, + background, + camera, }; } diff --git a/apps/desktop/src/routes/screenshot-editor/popovers/RoundingPopover.tsx b/apps/desktop/src/routes/screenshot-editor/popovers/RoundingPopover.tsx index 85d0aec3a2..7bb1b9f3af 100644 --- a/apps/desktop/src/routes/screenshot-editor/popovers/RoundingPopover.tsx +++ b/apps/desktop/src/routes/screenshot-editor/popovers/RoundingPopover.tsx @@ -5,20 +5,7 @@ import { batch, Show, type ValidComponent } from "solid-js"; import IconCapChevronDown from "~icons/cap/chevron-down"; import IconCapCorners from "~icons/cap/corners"; import { useScreenshotEditorContext } from "../context"; -import { - EditorButton, - MenuItem, - MenuItemList, - PopperContent, - Slider, - topSlideAnimateClasses, -} from "../ui"; - -export type CornerRoundingType = "rounded" | "squircle"; -const CORNER_STYLE_OPTIONS = [ - { name: "Squircle", value: "squircle" }, - { name: "Rounded", value: "rounded" }, -] satisfies Array<{ name: string; value: CornerRoundingType }>; +import { EditorButton, Slider } from "../ui"; function hasNoVisibleBackground(source: { type: string; @@ -84,90 +71,40 @@ export function RoundingPopover() {
- Rounding - +
+ + + Rounding + + setProject("background", "rounding", v[0])} + minValue={0} + maxValue={100} + step={0.1} + formatTooltip="%" + /> +
+
+ + + Rounding Smoothness + + + setProject("background", "roundingSmoothness", v[0]) + } + minValue={0} + maxValue={1} + step={0.01} + formatTooltip={(value) => `${Math.round(value * 100)}%`} + /> +
- setProject("background", "roundingType", v)} - />
); } - -function CornerStyleSelect(props: { - label?: string; - value: CornerRoundingType; - onChange: (value: CornerRoundingType) => void; -}) { - return ( -
- - {(label) => ( - - {label()} - - )} - - - options={CORNER_STYLE_OPTIONS} - optionValue="value" - optionTextValue="name" - value={CORNER_STYLE_OPTIONS.find( - (option) => option.value === props.value, - )} - onChange={(option) => option && props.onChange(option.value)} - disallowEmptySelection - itemComponent={(itemProps) => ( - - as={KSelect.Item} - item={itemProps.item} - > - - {itemProps.item.rawValue.name} - - - )} - > - - class="flex-1 text-sm text-left truncate text-[--gray-500] font-normal"> - {(state) => {state.selectedOption().name}} - - - as={(iconProps) => ( - - )} - /> - - - - as={KSelect.Content} - class={cx(topSlideAnimateClasses, "z-50")} - > - - class="overflow-y-auto max-h-32" - as={KSelect.Listbox} - /> - - - -
- ); -} diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index 22b6d2acf8..245949316f 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -441,10 +441,10 @@ export type AuthSecret = { api_key: string } | { token: string; expires: number export type AuthStore = { secret: AuthSecret; user_id: string | null; plan: Plan | null; organizations?: Organization[] } export type BackgroundBlurConfig = { mode: BackgroundBlurMode } export type BackgroundBlurMode = "off" | "light" | "heavy" -export type BackgroundConfiguration = { source: BackgroundSource; blur: number; padding: number; rounding: number; roundingType: CornerStyle; inset: number; crop: Crop | null; shadow: number; advancedShadow: ShadowConfiguration | null; border: BorderConfiguration | null } +export type BackgroundConfiguration = { source: BackgroundSource; blur: number; padding: number; rounding: number; roundingSmoothness?: number; inset: number; crop: Crop | null; shadow: number; advancedShadow: ShadowConfiguration | null; border: BorderConfiguration | null } export type BackgroundSource = { type: "wallpaper"; path: string | null } | { type: "image"; path: string | null } | { type: "color"; value: [number, number, number]; alpha?: number } | { type: "gradient"; from: [number, number, number]; to: [number, number, number]; angle?: number; noise_intensity?: number | null; noise_scale?: number | null; animated?: boolean | null; animation_speed?: number | null } export type BorderConfiguration = { enabled: boolean; width: number; color: [number, number, number]; opacity: number } -export type Camera = { hide: boolean; mirror: boolean; position: CameraPosition; size: number; zoomSize: number | null; rounding: number; shadow: number; advancedShadow: ShadowConfiguration | null; shape: CameraShape; roundingType: CornerStyle; scaleDuringZoom?: number; backgroundBlur?: BackgroundBlurConfig } +export type Camera = { hide: boolean; mirror: boolean; position: CameraPosition; size: number; zoomSize: number | null; rounding: number; shadow: number; advancedShadow: ShadowConfiguration | null; shape: CameraShape; roundingSmoothness?: number; scaleDuringZoom?: number; backgroundBlur?: BackgroundBlurConfig } export type CameraDeviceSettings = { width: number | null; height: number | null; frameRate: number | null } export type CameraFormatInfo = { width: number; height: number; frameRate: number } export type CameraInfo = { device_id: string; model_id: ModelIDType | null; display_name: string } @@ -469,7 +469,6 @@ export type ClickSpringConfig = { tension: number; mass: number; friction: numbe export type ClipConfiguration = { index: number; offsets: ClipOffsets } export type ClipOffsets = { camera?: number; mic?: number; system_audio?: number } export type CommercialLicense = { licenseKey: string; expiryDate: number | null; refresh: number; activatedOn: number } -export type CornerStyle = "squircle" | "rounded" export type Crop = { position: XY; size: XY } export type CurrentRecording = { target: CurrentRecordingTarget; mode: RecordingMode; status: RecordingStatus } export type CurrentRecordingChanged = null diff --git a/crates/project/src/configuration.rs b/crates/project/src/configuration.rs index e6607e5c18..46e29a9c5d 100644 --- a/crates/project/src/configuration.rs +++ b/crates/project/src/configuration.rs @@ -60,6 +60,10 @@ fn default_alpha() -> u8 { u8::MAX } +fn default_rounding_smoothness() -> f32 { + 0.6 +} + impl Default for BackgroundSource { fn default() -> Self { BackgroundSource::Color { @@ -191,14 +195,6 @@ impl From<(T, T)> for XY { } } -#[derive(Type, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Default)] -#[serde(rename_all = "camelCase")] -pub enum CornerStyle { - #[default] - Squircle, - Rounded, -} - #[derive(Type, Serialize, Deserialize, Clone, Debug, Default)] #[serde(rename_all = "camelCase")] pub struct Crop { @@ -236,7 +232,8 @@ pub struct BackgroundConfiguration { pub blur: f64, pub padding: f64, pub rounding: f64, - pub rounding_type: CornerStyle, + #[serde(default = "default_rounding_smoothness")] + pub rounding_smoothness: f32, pub inset: u32, pub crop: Option, pub shadow: f32, @@ -262,12 +259,12 @@ impl Default for BackgroundConfiguration { blur: 0.0, padding: 0.0, rounding: 0.0, - rounding_type: CornerStyle::default(), + rounding_smoothness: 0.6, inset: 0, crop: None, shadow: 73.6, advanced_shadow: Some(ShadowConfiguration::default()), - border: None, // Border is disabled by default for backwards compatibility + border: None, } } } @@ -339,8 +336,8 @@ pub struct Camera { #[serde(alias = "advanced_shadow")] pub advanced_shadow: Option, pub shape: CameraShape, - #[serde(alias = "rounding_type")] - pub rounding_type: CornerStyle, + #[serde(default = "default_rounding_smoothness")] + pub rounding_smoothness: f32, #[serde(default = "Camera::default_scale_during_zoom")] pub scale_during_zoom: f32, #[serde(default)] @@ -385,7 +382,7 @@ impl Default for Camera { blur: 10.5, }), shape: CameraShape::Square, - rounding_type: CornerStyle::default(), + rounding_smoothness: 0.0, scale_during_zoom: Self::default_scale_during_zoom(), background_blur: BackgroundBlurConfig::default(), } diff --git a/crates/rendering/src/composite_frame.rs b/crates/rendering/src/composite_frame.rs index ccdad6b540..fde6bf364f 100644 --- a/crates/rendering/src/composite_frame.rs +++ b/crates/rendering/src/composite_frame.rs @@ -1,6 +1,8 @@ use bytemuck::{Pod, Zeroable}; use wgpu::{include_wgsl, util::DeviceExt}; +use crate::smoothness_to_exponent; + pub struct CompositeVideoFramePipeline { pub bind_group_layout: wgpu::BindGroupLayout, pub render_pipeline: wgpu::RenderPipeline, @@ -75,7 +77,7 @@ pub struct CompositeVideoFrameUniforms { pub motion_blur_params: [f32; 4], pub target_size: [f32; 2], pub rounding_px: f32, - pub rounding_type: f32, + pub corner_exponent: f32, pub mirror_x: f32, pub shadow: f32, pub shadow_size: f32, @@ -100,7 +102,7 @@ impl Default for CompositeVideoFrameUniforms { motion_blur_params: Default::default(), target_size: Default::default(), rounding_px: Default::default(), - rounding_type: 0.0, + corner_exponent: smoothness_to_exponent(0.6), mirror_x: Default::default(), shadow: Default::default(), shadow_size: Default::default(), diff --git a/crates/rendering/src/lib.rs b/crates/rendering/src/lib.rs index a87a27e61f..34b83a44ca 100644 --- a/crates/rendering/src/lib.rs +++ b/crates/rendering/src/lib.rs @@ -1,7 +1,7 @@ use anyhow::Result; use cap_project::{ - AspectRatio, CameraShape, CameraXPosition, CameraYPosition, ClipOffsets, CornerStyle, Crop, - CursorEvents, MaskKind, ProjectConfiguration, RecordingMeta, StudioRecordingMeta, XY, + AspectRatio, CameraShape, CameraXPosition, CameraYPosition, ClipOffsets, Crop, CursorEvents, + MaskKind, ProjectConfiguration, RecordingMeta, StudioRecordingMeta, XY, }; use composite_frame::CompositeVideoFrameUniforms; use core::f64; @@ -164,13 +164,6 @@ pub async fn probe_software_adapter() -> Option<(bool, String)> { const STANDARD_CURSOR_HEIGHT: f32 = 75.0; -fn rounding_type_value(style: CornerStyle) -> f32 { - match style { - CornerStyle::Rounded => 0.0, - CornerStyle::Squircle => 1.0, - } -} - #[derive(Debug, Clone, Copy, Type)] pub struct RenderOptions { pub camera_size: Option>, @@ -2602,9 +2595,11 @@ impl ProjectUniforms { ], target_bounds: [start.x as f32, start.y as f32, end.x as f32, end.y as f32], target_size: [target_size.x as f32, target_size.y as f32], - rounding_px: (project.background.rounding / 100.0 * 0.5 * min_target_axis) - as f32, - rounding_type: rounding_type_value(project.background.rounding_type), + rounding_px: compute_adjusted_radius( + project.background.rounding as f32 / 100.0 * 0.5 * min_target_axis as f32, + project.background.rounding_smoothness, + ), + corner_exponent: smoothness_to_exponent(project.background.rounding_smoothness), mirror_x: 0.0, motion_blur_vector: descriptor.movement_vector_uv, motion_blur_zoom_center: descriptor.zoom_center_uv, @@ -2778,8 +2773,11 @@ impl ProjectUniforms { target_bounds[2] - target_bounds[0], target_bounds[3] - target_bounds[1], ], - rounding_px: project.camera.rounding / 100.0 * 0.5 * size[0].min(size[1]), - rounding_type: rounding_type_value(project.camera.rounding_type), + rounding_px: compute_adjusted_radius( + project.camera.rounding / 100.0 * 0.5 * size[0].min(size[1]), + project.camera.rounding_smoothness, + ), + corner_exponent: smoothness_to_exponent(project.camera.rounding_smoothness), mirror_x: if project.camera.mirror { 1.0 } else { 0.0 }, motion_blur_vector: camera_descriptor.movement_vector_uv, motion_blur_zoom_center: camera_descriptor.zoom_center_uv, @@ -2875,7 +2873,7 @@ impl ProjectUniforms { target_bounds[3] - target_bounds[1], ], rounding_px: 0.0, - rounding_type: rounding_type_value(project.camera.rounding_type), + corner_exponent: 0.0, mirror_x: if project.camera.mirror { 1.0 } else { 0.0 }, motion_blur_vector: camera_only_descriptor.movement_vector_uv, motion_blur_zoom_center: camera_only_descriptor.zoom_center_uv, @@ -3986,6 +3984,26 @@ pub fn create_shader_render_pipeline( }) } +// Converts a smoothness value (0.0 to 1.0) to a super ellipse exponent. +#[inline] +pub(crate) fn smoothness_to_exponent(smoothness: f32) -> f32 { + 2.0 + smoothness * 6.0 +} + +// Adjusts radius to maintain visual consistency across different super ellipse exponents. +fn compute_adjusted_radius(radius: f32, smoothness: f32) -> f32 { + if smoothness == 0.0 { + return radius; + } + + let power = smoothness_to_exponent(smoothness); + let capsule_scale = 2.0f32.powf(0.5 - 1.0 / power); + + // Visual normalization: compensate for super ellipses appearing smaller + let visual_compensation = 1.0 + smoothness * 0.8; + radius * capsule_scale * visual_compensation +} + #[cfg(test)] mod project_uniforms_tests { use super::*; diff --git a/crates/rendering/src/shaders/composite-video-frame.wgsl b/crates/rendering/src/shaders/composite-video-frame.wgsl index 398a558626..a68b3b366c 100644 --- a/crates/rendering/src/shaders/composite-video-frame.wgsl +++ b/crates/rendering/src/shaders/composite-video-frame.wgsl @@ -8,7 +8,7 @@ struct Uniforms { motion_blur_params: vec4, target_size: vec2, rounding_px: f32, - rounding_type: f32, + corner_exponent: f32, mirror_x: f32, shadow: f32, shadow_size: f32, @@ -50,19 +50,20 @@ fn superellipse_norm(p: vec2, power: f32) -> f32 { return pow(x + y, 1.0 / power); } -fn rounded_corner_norm(p: vec2, rounding_type: f32) -> f32 { - if rounding_type < 0.5 { +// Accept an exponent directly. If exponent <= 0 -> use circular corner (length). +fn rounded_corner_norm(p: vec2, exponent: f32) -> f32 { + if exponent <= 0.0 { return length(p); } - - let power = 4.0; + // protect against pathological exponent values + let power = max(exponent, 1e-3); return superellipse_norm(p, power); } -fn sdf_rounded_rect(p: vec2, b: vec2, r: f32, rounding_type: f32) -> f32 { +fn sdf_rounded_rect(p: vec2, b: vec2, r: f32, exponent: f32) -> f32 { let q = abs(p) - b + vec2(r); let outside = max(q, vec2(0.0)); - let outside_norm = rounded_corner_norm(outside, rounding_type); + let outside_norm = rounded_corner_norm(outside, exponent); return outside_norm + min(max(q.x, q.y), 0.0) - r; } @@ -71,8 +72,8 @@ fn fs_main(@builtin(position) frag_coord: vec4) -> @location(0) vec4 { let p = frag_coord.xy; let center = (uniforms.target_bounds.xy + uniforms.target_bounds.zw) * 0.5; let size = (uniforms.target_bounds.zw - uniforms.target_bounds.xy) * 0.5; - - let dist = sdf_rounded_rect(p - center, size, uniforms.rounding_px, uniforms.rounding_type); + + let dist = sdf_rounded_rect(p - center, size, uniforms.rounding_px, uniforms.corner_exponent); let min_frame_size = min(size.x, size.y); let shadow_enabled = uniforms.shadow > 0.0; @@ -99,7 +100,7 @@ fn fs_main(@builtin(position) frag_coord: vec4) -> @location(0) vec4 { shadow_enabled ); - let shadow_dist = sdf_rounded_rect(p - center, size, uniforms.rounding_px, uniforms.rounding_type); + let shadow_dist = sdf_rounded_rect(p - center, size, uniforms.rounding_px, uniforms.corner_exponent); // Apply blur and size to shadow let shadow_strength_final = smoothstep(shadow_size + shadow_blur, -shadow_blur, abs(shadow_dist)); @@ -116,10 +117,10 @@ fn fs_main(@builtin(position) frag_coord: vec4) -> @location(0) vec4 { p - center, size + vec2(uniforms.border_width), uniforms.rounding_px + uniforms.border_width, - uniforms.rounding_type + uniforms.corner_exponent ); let border_inner_dist = - sdf_rounded_rect(p - center, size, uniforms.rounding_px, uniforms.rounding_type); + sdf_rounded_rect(p - center, size, uniforms.rounding_px, uniforms.corner_exponent); if (border_outer_dist <= 0.0 && border_inner_dist > 0.0) { let inner_alpha = smoothstep(-0.5, 0.5, border_inner_dist); @@ -130,7 +131,7 @@ fn fs_main(@builtin(position) frag_coord: vec4) -> @location(0) vec4 { return vec4(uniforms.border_color.xyz, border_alpha); } } - + if target_uv.x < 0.0 || target_uv.x > 1.0 || target_uv.y < 0.0 || target_uv.y > 1.0 { return shadow_color; } @@ -261,7 +262,7 @@ fn sample_texture(uv: vec2, crop_bounds_uv: vec4) -> vec4 { fn apply_rounded_corners(current_color: vec4, target_uv: vec2) -> vec4 { let centered_uv = (target_uv - vec2(0.5)) * uniforms.target_size; let half_size = uniforms.target_size * 0.5; - let distance = sdf_rounded_rect(centered_uv, half_size, uniforms.rounding_px, uniforms.rounding_type); + let distance = sdf_rounded_rect(centered_uv, half_size, uniforms.rounding_px, uniforms.corner_exponent); let anti_alias_width = max(fwidth(distance), 0.5); let coverage = clamp(1.0 - smoothstep(0.0, anti_alias_width, distance), 0.0, 1.0); diff --git a/packages/ui-solid/src/auto-imports.d.ts b/packages/ui-solid/src/auto-imports.d.ts index 6c6a4bad98..f21afc2e80 100644 --- a/packages/ui-solid/src/auto-imports.d.ts +++ b/packages/ui-solid/src/auto-imports.d.ts @@ -107,7 +107,9 @@ declare global { const IconLucideSettings: typeof import('~icons/lucide/settings.jsx')['default'] const IconLucideSparkles: typeof import('~icons/lucide/sparkles.jsx')['default'] const IconLucideSquarePlay: typeof import('~icons/lucide/square-play.jsx')['default'] + const IconLucideSquareRoundCorner: typeof import('~icons/lucide/square-round-corner.jsx')['default'] const IconLucideSubtitles: typeof import('~icons/lucide/subtitles.jsx')['default'] + const IconLucideTimer: typeof import('~icons/lucide/timer.jsx')['default'] const IconLucideType: typeof import('~icons/lucide/type.jsx')['default'] const IconLucideUnplug: typeof import('~icons/lucide/unplug.jsx')['default'] const IconLucideVideo: typeof import('~icons/lucide/video.jsx')['default']