Skip to content

Commit d98a5ef

Browse files
committed
Star Slider: Fix beak shape in badge (#6553)
1 parent ddc1df3 commit d98a5ef

File tree

1 file changed

+60
-39
lines changed

1 file changed

+60
-39
lines changed

src/components/modals/paidReaction/StarSlider.tsx

Lines changed: 60 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,13 @@ type OwnProps = {
3333

3434
const DEFAULT_POINTS = [50, 100, 500, 1000, 2000, 5000, 10000];
3535
const LARGE_STEP = 10000;
36-
const THUMB_SIZE_IN_PIXELS = 1.875 * REM;
37-
const BEAK_WIDTH_IN_PIXELS = 28;
38-
const DEFAULT_RADIUS_IN_REM = 2;
39-
const MIN_RADIUS_IN_REM = 0.375;
36+
const THUMB_SIZE = 1.875 * REM;
37+
const CORNER_BEAK_WIDTH = 44;
38+
const DEFAULT_BEAK_WIDTH = 52;
39+
const BEAK_HEIGHT = 32;
40+
41+
const BEAK_OFFSET_END = 10;
42+
const TIP_OFFSET_END = 0;
4043

4144
const BADGE_HORIZONTAL_PADDING = 2 * REM;
4245
const BADGE_ICON_SIZE = 1.5 * REM;
@@ -169,11 +172,15 @@ const StarSlider = ({
169172

170173
const progress = value / points.length;
171174
const {
172-
minBadgeX, maxBadgeX, beakOffset, cornerRadius,
175+
minBadgeX, maxBadgeX, beakOffset, beakTipOffset, beakWidth: currentBeakWidth,
173176
} = useMemo(() => {
174177
return calcBadgePosition(containerWidth, badgeWidth, progress);
175178
}, [containerWidth, badgeWidth, progress]);
176179

180+
const beakPath = useMemo(() => {
181+
return generateBeakPath(currentBeakWidth, beakTipOffset);
182+
}, [currentBeakWidth, beakTipOffset]);
183+
177184
const handleChange = useLastCallback((event: React.ChangeEvent<HTMLInputElement>) => {
178185
const rawValue = Number(event.currentTarget.value);
179186
const clampedValue = Math.max(rawValue, minAllowedProgress);
@@ -201,11 +208,9 @@ const StarSlider = ({
201208
setIsDragging(false);
202209
});
203210

204-
const { left: radiusLeft, right: radiusRight } = cornerRadius;
205211
const dynamicColor = shouldUseDynamicColor ? getColorForProgress(progress) : undefined;
206212

207213
const badgeStyle = buildStyle(
208-
`border-radius: 2rem 2rem ${radiusRight}rem ${radiusLeft}rem`,
209214
Boolean(badgeWidth) && `width: ${badgeWidth}px`,
210215
);
211216

@@ -254,9 +259,9 @@ const StarSlider = ({
254259
</div>
255260
<svg
256261
className={styles.floatingBadgeTriangle}
257-
width="28"
258-
height="28"
259-
viewBox="0 0 28 28"
262+
width={currentBeakWidth}
263+
height={BEAK_HEIGHT}
264+
viewBox={`0 0 ${currentBeakWidth} ${BEAK_HEIGHT}`}
260265
fill="none"
261266
aria-hidden="true"
262267
role="presentation"
@@ -272,7 +277,7 @@ const StarSlider = ({
272277
)}
273278
<path
274279
className={styles.floatingBadgeTrianglePath}
275-
d="m 28,4 v 9 c 0.0089,7.283278 -3.302215,5.319646 -6.750951,8.589815 l -5.8284,5.82843 c -0.781,0.78105 -2.0474,0.78104 -2.8284,0 L 6.7638083,21.589815 C 2.8288652,17.959047 0.04527024,20.332086 0,13 V 4 C 0,4 0.00150581,0.97697493 3,1 5.3786658,1.018266 22.594519,0.9142007 25,1 c 2.992326,0.1067311 3,3 3,3 z"
280+
d={beakPath}
276281
fill={dynamicColor || 'url(#StarBadgeTriangle)'}
277282
/>
278283
</svg>
@@ -323,45 +328,61 @@ function calcBadgePosition(
323328
progress: number,
324329
) {
325330
const halfBadgeWidth = badgeWidth / 2;
326-
const halfThumbSize = THUMB_SIZE_IN_PIXELS / 2;
327-
328-
const baseTargetX = halfThumbSize + progress * (containerWidth - THUMB_SIZE_IN_PIXELS);
329-
const cornerTargetX = progress * containerWidth;
330-
331-
const edgeZone = THUMB_SIZE_IN_PIXELS / 2;
332-
const distanceToLeftEdge = cornerTargetX;
333-
const distanceToRightEdge = containerWidth - cornerTargetX;
334-
const minEdgeDistance = Math.min(distanceToLeftEdge, distanceToRightEdge);
331+
const cornerBeakHalfWidth = CORNER_BEAK_WIDTH / 2;
332+
const maxBeakOffset = halfBadgeWidth - cornerBeakHalfWidth;
335333

336-
const t = Math.min(1, minEdgeDistance / edgeZone);
337-
const targetX = cornerTargetX + t * (baseTargetX - cornerTargetX);
338334
const minBadgeX = halfBadgeWidth;
339335
const maxBadgeX = containerWidth - halfBadgeWidth;
340-
const clampedBadgeX = Math.max(minBadgeX, Math.min(targetX, maxBadgeX));
341-
342-
const beakOffset = targetX - clampedBadgeX;
343-
344-
const thresholdPx = DEFAULT_RADIUS_IN_REM / 2 * REM;
345-
const beakHalfWidth = BEAK_WIDTH_IN_PIXELS / 2;
346-
347-
const distanceToEdge = halfBadgeWidth - Math.abs(beakOffset);
348-
const normalizedDistance = Math.max(0, distanceToEdge - beakHalfWidth);
349336

350-
let edgeRadius = DEFAULT_RADIUS_IN_REM;
351-
if (normalizedDistance < thresholdPx) {
352-
const radiusT = 1 - (normalizedDistance / thresholdPx);
353-
edgeRadius = DEFAULT_RADIUS_IN_REM - radiusT * (DEFAULT_RADIUS_IN_REM - MIN_RADIUS_IN_REM);
337+
const thumbHalf = THUMB_SIZE / 2;
338+
const trackLength = containerWidth - THUMB_SIZE;
339+
const distanceFromLeft = progress * trackLength;
340+
const distanceFromRight = trackLength - distanceFromLeft;
341+
342+
const isLeftSide = distanceFromLeft < distanceFromRight;
343+
const distanceToEdge = isLeftSide ? distanceFromLeft : distanceFromRight;
344+
const direction = isLeftSide ? -1 : 1;
345+
346+
let beakOffset = 0;
347+
let beakTipOffset = 0;
348+
let beakWidth = DEFAULT_BEAK_WIDTH;
349+
350+
const beakOffsetStart = halfBadgeWidth - thumbHalf;
351+
352+
if (distanceToEdge < beakOffsetStart) {
353+
if (distanceToEdge > BEAK_OFFSET_END) {
354+
const t = (beakOffsetStart - distanceToEdge) / (beakOffsetStart - BEAK_OFFSET_END);
355+
beakOffset = direction * t * maxBeakOffset;
356+
} else if (distanceToEdge > TIP_OFFSET_END) {
357+
beakOffset = direction * maxBeakOffset;
358+
const t = (BEAK_OFFSET_END - distanceToEdge) / (BEAK_OFFSET_END - TIP_OFFSET_END);
359+
const tExp = 1 - (1 - t) * (1 - t) * (1 - t);
360+
beakWidth = DEFAULT_BEAK_WIDTH - tExp * (DEFAULT_BEAK_WIDTH - CORNER_BEAK_WIDTH);
361+
beakTipOffset = direction * t * (beakWidth / 2);
362+
} else {
363+
beakOffset = direction * maxBeakOffset;
364+
beakWidth = CORNER_BEAK_WIDTH;
365+
beakTipOffset = direction * (beakWidth / 2);
366+
}
354367
}
355368

356-
const leftRadius = beakOffset < 0 ? edgeRadius : DEFAULT_RADIUS_IN_REM;
357-
const rightRadius = beakOffset > 0 ? edgeRadius : DEFAULT_RADIUS_IN_REM;
358-
359369
return {
360370
minBadgeX,
361371
maxBadgeX,
362372
beakOffset,
363-
cornerRadius: { left: leftRadius, right: rightRadius },
373+
beakTipOffset,
374+
beakWidth,
364375
};
365376
}
366377

378+
const TIP_RADIUS = 2;
379+
380+
function generateBeakPath(beakWidth: number, tipOffset: number): string {
381+
const tipX = beakWidth / 2 + tipOffset;
382+
const r = TIP_RADIUS;
383+
const y = BEAK_HEIGHT - r;
384+
385+
return `M 0 0 L ${beakWidth} 0 L ${tipX + r} ${y} Q ${tipX} ${BEAK_HEIGHT} ${tipX - r} ${y} Z`;
386+
}
387+
367388
export default memo(StarSlider);

0 commit comments

Comments
 (0)