import { MutableRefObject , SVGProps , ReactElement , ReactNode , useRef , useState , useMemo , useEffect , HTMLProps } from 'react' ; import { CSSObject , jsx , useTheme } from '@emotion/react' ; import PropTypes from 'prop-types' ; import { cx } from '@emotion/css' ; import anime from 'animejs' ; import { AnimatedSettings , Animated , transitionVisibility } from '@arwes/animated' ; import { NoInfer } from '../utils/types' ; type FRAME_SVG_DIMENSION = number | string ; type FRAME_SVG_POINT = FRAME_SVG_DIMENSION [ ] ; type FRAME_SVG_POLYLINE = FRAME_SVG_POINT [ ] ; interface FRAME_SVG_POLYLINE_CUSTOM { polyline : FRAME_SVG_POLYLINE ; lineWidth ? : number ; animated ? : AnimatedSettings ; css ? : CSSObject ; } type FRAME_SVG_POLYLINE_GENERIC = FRAME_SVG_POLYLINE | FRAME_SVG_POLYLINE_CUSTOM ; interface FRAME_SVG_EFFECTS { highlight : ( ) => void ; } interface FrameSVGProps < E extends HTMLElement = HTMLDivElement > { as ? : keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap ; shapes ? : FRAME_SVG_POLYLINE [ ] ; polylines ? : FRAME_SVG_POLYLINE_GENERIC [ ] ; lineWidth ? : number ; palette ? : string ; hideShapes ? : boolean ; hidePolylines ? : boolean ; hover ? : boolean ; disabled ? : boolean ; className ? : string ; rootRef ? : MutableRefObject < E | null > | ( ( node : E ) => void ) ; effectsRef ? : MutableRefObject < FRAME_SVG_EFFECTS | null > | ( ( effects : FRAME_SVG_EFFECTS ) => void ) ; children ? : ReactNode ; } const FrameSVG = < E extends HTMLElement = HTMLDivElement , P extends HTMLProps < HTMLElement > = HTMLProps < E > > ( props ) : ReactElement => { const { as : asProvided , shapes , polylines , lineWidth , hideShapes , hidePolylines , palette , hover , disabled , className , rootRef , effectsRef , children , ... otherProps } = props ; const as = useMemo ( ( ) => asProvided || 'div' , [ ] ) ; const theme = useTheme ( ) ; const [ size , setSize ] = useState ( { width : 0 , height : 0 } ) ; const containerRef = useRef < HTMLDivElement > ( null ) ; const observerRef = useRef < any > ( null ) ; const blurPadding = theme . shadowBlur ( 1 ) ; const defaultPalette = theme . palette . primary ; const colorPalette = theme . palette [ palette as string ] ?? defaultPalette ; const color = disabled ? colorPalette . dark2 : colorPalette . main ; const colorHover = colorPalette . light2 ; const formatPoint = ( point ) : string => { const width = size . width - ( blurPadding * 2 ) ; const height = size . height - ( blurPadding * 2 ) ; return point . slice ( 0 , 2 ) . map ( ( value , index ) => { if ( typeof value === 'number' ) { return value ; } const isX = index === 0 ; const axisSize = isX ? width : height ; return String ( value ) . trim ( ) . replace ( /- /g , ' -' ) . replace ( /\+ /g , ' +' ) . split ( ' ' ) . reduce ( ( total , item ) => { const n = Number ( item . replace ( /[+\-%]/g , '' ) ) ; if ( n === 0 ) { return total ; } const isMinus = item . startsWith ( '-' ) ; const isPercentage = item . endsWith ( '%' ) ; const point = isPercentage ? axisSize * ( n / 100 ) : n ; return isMinus ? total - point : total + point ; } , 0 ) ; } ) . join ( ',' ) ; } ; const formatPolyline = ( polyline ) : string => { return polyline . map ( formatPoint ) . map ( ( point , index ) => ( index === 0 ? 'M' : 'L' ) + point ) . join ( ' ' ) ; } ; useEffect ( ( ) => { const root = containerRef . current as HTMLDivElement ; if ( effectsRef ) { const highlight = ( ) : void => { const shapes = root . querySelectorAll ( 'path[data-type=shape]' ) ; const startOpacity = hideShapes ? 0 : 0.1 ; const endOpacity = 0.5 ; anime . timeline ( { targets : shapes , duration : theme . transitionDuration ( ) / 4 , easing : 'easeOutSine' } ) . add ( { opacity : [ startOpacity , endOpacity ] } ) . add ( { opacity : [ endOpacity , startOpacity ] } ) ; } ; const effects = { highlight } ; if ( typeof effectsRef === 'function' ) { effectsRef ( effects ) ; } else { effectsRef . current = effects ; } } const onResize = ( ) : void => { const { offsetWidth : width , offsetHeight : height } = root ; setSize ( { width , height } ) ; } ; const win = window ; const ResizeObserver = win . ResizeObserver ; if ( ResizeObserver ) { observerRef . current = new ResizeObserver ( onResize ) ; observerRef . current . observe ( root ) ; return ( ) => observerRef . current ?. disconnect ( ) ; } else { onResize ( ) ; } } , [ ] ) ; return jsx ( as , { ... otherProps , className : cx ( 'arwes-frame-svg' , className ) , ref : rootRef , css : { position : 'relative' , display : 'inline-block' , padding : theme . space ( 2 ) , '&:hover svg, &:focus svg' : hover && ! disabled && { filter : `drop-shadow(0 0 ${ blurPadding }px ${ colorHover })` } , '&:hover path[data-type=shape], &:focus path[data-type=shape]' : hover && ! disabled && { fill : colorHover } , '&:hover path[data-type=polyline], &:focus path[data-type=polyline]' : hover && ! disabled && { stroke : colorHover } } } , < div ref = { containerRef } className = 'arwes-frame-svg__structure' css = { { position : 'absolute' , left : - blurPadding , right : - blurPadding , top : - blurPadding , bottom : - blurPadding , display : 'flex' } } > { ! ! size . width && ! ! size . height && ( < svg xmlns = 'http://www.w3.org/2000/svg' viewBox = { `0 0 ${ size . width } ${ size . height }` } preserveAspectRatio = 'none' css = { { display : 'block' , flex : 1 , transition : `filter ${ theme . transitionDuration ( ) }ms ease-out` , filter : `drop-shadow(0 0 ${ blurPadding }px ${ color })` } } > < Animated < SVGGElement , SVGProps < SVGGElement > > as = 'g' style = { { transform : `translate(${ blurPadding }px, ${ blurPadding }px)` } } > < Animated < SVGGElement , SVGProps < SVGGElement > > as = 'g' animated = { transitionVisibility as AnimatedSettings < SVGProps < SVGGElement >> } > { ( shapes || [ ] ) . map ( ( shape , index ) => ( < path key = { index } data - type = 'shape' d = { formatPolyline ( shape ) } css = { { strokeWidth : 0 , stroke : 'transparent' , fill : color , opacity : hideShapes ? 0 : 0.1 , transition : `fill ${ theme . transitionDuration ( ) }ms ease-out` } } />))} < /Animated> { ! hidePolylines && ( polylines || [ ] ) . map ( ( item , index ) => { const polyline = Array . isArray ( item ) ? item : item . polyline ; const strokeWidth = ( item as any ) . lineWidth || lineWidth ; const animated = ( item as any ) . animated ; const css = ( item as any ) . css ; return ( < Animated < SVGPathElement , SVGProps < SVGPathElement > > as = 'path' key = { index } data - type = 'polyline' d = { formatPolyline ( polyline ) } css = { { strokeWidth , stroke : color , vectorEffect : 'non-scaling-stroke' , fill : 'transparent' , transition : `stroke ${ theme . transitionDuration ( ) }ms ease-out` , ... css } } animated = { animated || { initialAttributes : { strokeDasharray : 999999 } , initialStyles : { strokeDashoffset : 999999 } , entering : { strokeDashoffset : [ anime . setDashoffset , 0 ] } , exiting : { strokeDashoffset : [ 0 , anime . setDashoffset ] } } } />); } ) } < /Animated> < /svg>)} < /div>, <div className='arwes-frame-svg__content' css={{ position: 'relative' }}> { children } < /div>); } ; FrameSVG . propTypes = { as : PropTypes . string . isRequired , shapes : PropTypes . array , polylines : PropTypes . array , lineWidth : PropTypes . number , palette : PropTypes . string , hideShapes : PropTypes . bool , hidePolylines : PropTypes . bool , hover : PropTypes . bool , disabled : PropTypes . bool , rootRef : PropTypes . any } ; FrameSVG . defaultProps = { as : 'div' , shapes : [ ] , polylines : [ ] , lineWidth : 1 , palette : 'primary' } ; export { FRAME_SVG_DIMENSION , FRAME_SVG_POINT , FRAME_SVG_POLYLINE , FRAME_SVG_POLYLINE_CUSTOM , FRAME_SVG_POLYLINE_GENERIC , FRAME_SVG_EFFECTS , FrameSVGProps , FrameSVG } ;