@@ -33,10 +33,13 @@ type OwnProps = {
3333
3434const DEFAULT_POINTS = [ 50 , 100 , 500 , 1000 , 2000 , 5000 , 10000 ] ;
3535const 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
4144const BADGE_HORIZONTAL_PADDING = 2 * REM ;
4245const 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+
367388export default memo ( StarSlider ) ;
0 commit comments