Add "sound waves" to transparent spotlight tiles

To indicate when the user is speaking.
This commit is contained in:
Robin
2026-06-25 18:34:20 +02:00
parent 8fa3f33f37
commit a3d3a1e951
3 changed files with 94 additions and 1 deletions

View File

@@ -51,6 +51,54 @@ Please see LICENSE in the repository root for full details.
background-color: var(--video-tile-background);
}
.waves {
transition: opacity ease 0.3s;
}
.waves[data-visible="true"] {
opacity: 1;
}
.waves[data-visible="false"] {
opacity: 0;
@media not (prefers-reduced-motion) {
.wave {
transform: translate(-50%, -50%) scale(0.9);
}
}
}
.wave {
border: var(--cpd-border-width-1) solid var(--cpd-color-alpha-gray-300);
transition: transform ease 0.2s;
}
.wave,
.speakingBorder {
border-radius: var(--cpd-radius-pill-effect);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.speakingBorder {
background:
radial-gradient(#0467dd, #0bc491),
linear-gradient(0deg, #0467dd 0%, #0bc491 100%);
background-blend-mode: overlay, normal;
outline: var(--cpd-border-width-4) solid var(--cpd-color-bg-canvas-default);
&::after {
content: "";
position: absolute;
inset: var(--cpd-border-width-2);
border-radius: var(--cpd-radius-pill-effect);
background: var(--cpd-color-bg-canvas-default);
}
}
.avatar {
position: absolute;
top: 50%;
@@ -73,6 +121,35 @@ unconditionally select the container so we can use cqmin units */
inline-size: 50cqmin;
block-size: 50cqmin;
}
.waves + .avatar {
/* Make the avatar slightly smaller to accommodate sound waves, if present */
inline-size: 38cqmin;
block-size: 38cqmin;
}
.wave:nth-child(1) {
inline-size: calc(38cqmin + var(--cpd-space-5x) + 3 * var(--cpd-space-10x));
block-size: calc(38cqmin + var(--cpd-space-5x) + 3 * var(--cpd-space-10x));
background: var(--cpd-color-alpha-gray-200);
}
.wave:nth-child(2) {
inline-size: calc(38cqmin + var(--cpd-space-5x) + 2 * var(--cpd-space-10x));
block-size: calc(38cqmin + var(--cpd-space-5x) + 2 * var(--cpd-space-10x));
background: var(--cpd-color-alpha-gray-300);
}
.wave:nth-child(3) {
inline-size: calc(38cqmin + var(--cpd-space-5x) + var(--cpd-space-10x));
block-size: calc(38cqmin + var(--cpd-space-5x) + var(--cpd-space-10x));
background: var(--cpd-color-alpha-gray-400);
}
.speakingBorder {
inline-size: calc(38cqmin + var(--cpd-space-3x));
block-size: calc(38cqmin + var(--cpd-space-3x));
}
}
.avatar > img {

View File

@@ -34,6 +34,7 @@ interface Props extends ComponentProps<typeof animated.div> {
video: TrackReferenceOrPlaceholder | undefined;
videoFit: "cover" | "contain";
mirror: boolean;
soundWaves?: boolean;
userId: string;
videoEnabled: boolean;
unencryptedWarning: boolean;
@@ -66,6 +67,7 @@ export const MediaView: FC<Props> = ({
video,
videoFit,
mirror,
soundWaves,
userId,
videoEnabled,
unencryptedWarning,
@@ -92,7 +94,10 @@ export const MediaView: FC<Props> = ({
const [handRaiseTimerVisible] = useSetting(showHandRaisedTimer);
const [showConnectionStats] = useSetting(showConnectionStatsSetting);
const avatarSize = Math.round(Math.min(targetWidth, targetHeight) / 2);
const avatarSize = Math.round(
Math.min(targetWidth, targetHeight) *
(soundWaves === undefined ? 0.5 : 0.38),
);
const warnings = unencryptedWarning && (
<Tooltip
@@ -124,6 +129,14 @@ export const MediaView: FC<Props> = ({
{...props}
>
<div className={styles.bg}>
{soundWaves !== undefined && (
<div className={styles.waves} data-visible={soundWaves}>
<div className={styles.wave} />
<div className={styles.wave} />
<div className={styles.wave} />
<div className={styles.speakingBorder} />
</div>
)}
<Avatar
id={userId}
name={displayName}

View File

@@ -79,6 +79,7 @@ interface SpotlightMemberMediaItemBaseProps extends SpotlightItemBaseProps {
interface SpotlightUserMediaItemBaseProps extends SpotlightMemberMediaItemBaseProps {
videoFit: "contain" | "cover";
videoEnabled: boolean;
soundWaves: boolean | undefined;
}
interface SpotlightLocalUserMediaItemProps extends SpotlightUserMediaItemBaseProps {
@@ -121,6 +122,7 @@ const SpotlightUserMediaItem: FC<SpotlightUserMediaItemProps> = ({
}) => {
const videoFit = useBehavior(vm.videoFit$);
const videoEnabled = useBehavior(vm.videoEnabled$);
const speaking = useBehavior(vm.speaking$);
// Whenever target bounds change, inform the viewModel
useEffect(() => {
@@ -133,6 +135,7 @@ const SpotlightUserMediaItem: FC<SpotlightUserMediaItemProps> = ({
RefAttributes<HTMLDivElement> = {
videoFit,
videoEnabled,
soundWaves: props.bgStyle === "transparent" ? speaking : undefined,
targetWidth,
targetHeight,
...props,