diff --git a/src/components/modals/paidReaction/StarSlider.tsx b/src/components/modals/paidReaction/StarSlider.tsx index 2a9034ea5..ab84513b6 100644 --- a/src/components/modals/paidReaction/StarSlider.tsx +++ b/src/components/modals/paidReaction/StarSlider.tsx @@ -33,10 +33,13 @@ type OwnProps = { const DEFAULT_POINTS = [50, 100, 500, 1000, 2000, 5000, 10000]; const LARGE_STEP = 10000; -const THUMB_SIZE_IN_PIXELS = 1.875 * REM; -const BEAK_WIDTH_IN_PIXELS = 28; -const DEFAULT_RADIUS_IN_REM = 2; -const MIN_RADIUS_IN_REM = 0.375; +const THUMB_SIZE = 1.875 * REM; +const CORNER_BEAK_WIDTH = 44; +const DEFAULT_BEAK_WIDTH = 52; +const BEAK_HEIGHT = 32; + +const BEAK_OFFSET_END = 10; +const TIP_OFFSET_END = 0; const BADGE_HORIZONTAL_PADDING = 2 * REM; const BADGE_ICON_SIZE = 1.5 * REM; @@ -169,11 +172,15 @@ const StarSlider = ({ const progress = value / points.length; const { - minBadgeX, maxBadgeX, beakOffset, cornerRadius, + minBadgeX, maxBadgeX, beakOffset, beakTipOffset, beakWidth: currentBeakWidth, } = useMemo(() => { return calcBadgePosition(containerWidth, badgeWidth, progress); }, [containerWidth, badgeWidth, progress]); + const beakPath = useMemo(() => { + return generateBeakPath(currentBeakWidth, beakTipOffset); + }, [currentBeakWidth, beakTipOffset]); + const handleChange = useLastCallback((event: React.ChangeEvent) => { const rawValue = Number(event.currentTarget.value); const clampedValue = Math.max(rawValue, minAllowedProgress); @@ -201,11 +208,9 @@ const StarSlider = ({ setIsDragging(false); }); - const { left: radiusLeft, right: radiusRight } = cornerRadius; const dynamicColor = shouldUseDynamicColor ? getColorForProgress(progress) : undefined; const badgeStyle = buildStyle( - `border-radius: 2rem 2rem ${radiusRight}rem ${radiusLeft}rem`, Boolean(badgeWidth) && `width: ${badgeWidth}px`, ); @@ -254,9 +259,9 @@ const StarSlider = ({ @@ -323,45 +328,61 @@ function calcBadgePosition( progress: number, ) { const halfBadgeWidth = badgeWidth / 2; - const halfThumbSize = THUMB_SIZE_IN_PIXELS / 2; + const cornerBeakHalfWidth = CORNER_BEAK_WIDTH / 2; + const maxBeakOffset = halfBadgeWidth - cornerBeakHalfWidth; - const baseTargetX = halfThumbSize + progress * (containerWidth - THUMB_SIZE_IN_PIXELS); - const cornerTargetX = progress * containerWidth; - - const edgeZone = THUMB_SIZE_IN_PIXELS / 2; - const distanceToLeftEdge = cornerTargetX; - const distanceToRightEdge = containerWidth - cornerTargetX; - const minEdgeDistance = Math.min(distanceToLeftEdge, distanceToRightEdge); - - const t = Math.min(1, minEdgeDistance / edgeZone); - const targetX = cornerTargetX + t * (baseTargetX - cornerTargetX); const minBadgeX = halfBadgeWidth; const maxBadgeX = containerWidth - halfBadgeWidth; - const clampedBadgeX = Math.max(minBadgeX, Math.min(targetX, maxBadgeX)); - const beakOffset = targetX - clampedBadgeX; + const thumbHalf = THUMB_SIZE / 2; + const trackLength = containerWidth - THUMB_SIZE; + const distanceFromLeft = progress * trackLength; + const distanceFromRight = trackLength - distanceFromLeft; - const thresholdPx = DEFAULT_RADIUS_IN_REM / 2 * REM; - const beakHalfWidth = BEAK_WIDTH_IN_PIXELS / 2; + const isLeftSide = distanceFromLeft < distanceFromRight; + const distanceToEdge = isLeftSide ? distanceFromLeft : distanceFromRight; + const direction = isLeftSide ? -1 : 1; - const distanceToEdge = halfBadgeWidth - Math.abs(beakOffset); - const normalizedDistance = Math.max(0, distanceToEdge - beakHalfWidth); + let beakOffset = 0; + let beakTipOffset = 0; + let beakWidth = DEFAULT_BEAK_WIDTH; - let edgeRadius = DEFAULT_RADIUS_IN_REM; - if (normalizedDistance < thresholdPx) { - const radiusT = 1 - (normalizedDistance / thresholdPx); - edgeRadius = DEFAULT_RADIUS_IN_REM - radiusT * (DEFAULT_RADIUS_IN_REM - MIN_RADIUS_IN_REM); + const beakOffsetStart = halfBadgeWidth - thumbHalf; + + if (distanceToEdge < beakOffsetStart) { + if (distanceToEdge > BEAK_OFFSET_END) { + const t = (beakOffsetStart - distanceToEdge) / (beakOffsetStart - BEAK_OFFSET_END); + beakOffset = direction * t * maxBeakOffset; + } else if (distanceToEdge > TIP_OFFSET_END) { + beakOffset = direction * maxBeakOffset; + const t = (BEAK_OFFSET_END - distanceToEdge) / (BEAK_OFFSET_END - TIP_OFFSET_END); + const tExp = 1 - (1 - t) * (1 - t) * (1 - t); + beakWidth = DEFAULT_BEAK_WIDTH - tExp * (DEFAULT_BEAK_WIDTH - CORNER_BEAK_WIDTH); + beakTipOffset = direction * t * (beakWidth / 2); + } else { + beakOffset = direction * maxBeakOffset; + beakWidth = CORNER_BEAK_WIDTH; + beakTipOffset = direction * (beakWidth / 2); + } } - const leftRadius = beakOffset < 0 ? edgeRadius : DEFAULT_RADIUS_IN_REM; - const rightRadius = beakOffset > 0 ? edgeRadius : DEFAULT_RADIUS_IN_REM; - return { minBadgeX, maxBadgeX, beakOffset, - cornerRadius: { left: leftRadius, right: rightRadius }, + beakTipOffset, + beakWidth, }; } +const TIP_RADIUS = 2; + +function generateBeakPath(beakWidth: number, tipOffset: number): string { + const tipX = beakWidth / 2 + tipOffset; + const r = TIP_RADIUS; + const y = BEAK_HEIGHT - r; + + return `M 0 0 L ${beakWidth} 0 L ${tipX + r} ${y} Q ${tipX} ${BEAK_HEIGHT} ${tipX - r} ${y} Z`; +} + export default memo(StarSlider);