11import * as React from "react"
22import { createRef , useEffect , useMemo , useRef , useState } from "react"
33
4- import { clamp , isFunction , isSafariDesktop } from "./utils"
4+ import { applyStyles , clamp , isFunction } from "./utils"
55
66interface Size {
77 width : number
@@ -18,7 +18,7 @@ enum Interaction {
1818interface ILayeredImageStyles {
1919 root ?: React . CSSProperties
2020 container ?: React . CSSProperties
21- layers ?: React . CSSProperties
21+ stack ?: React . CSSProperties
2222 layer ?: React . CSSProperties | ( ( index : number ) => React . CSSProperties )
2323 light ?: React . CSSProperties
2424 shadow ?: React . CSSProperties
@@ -57,10 +57,10 @@ export const LayeredImage: React.FC<ILayeredImageProps> = ({
5757 const elementsRef = useRef ( {
5858 root : createRef < HTMLDivElement > ( ) ,
5959 container : createRef < HTMLDivElement > ( ) ,
60+ layers : layers . map ( ( ) => createRef < HTMLDivElement > ( ) ) ,
6061 shadow : createRef < HTMLDivElement > ( ) ,
6162 light : createRef < HTMLDivElement > ( ) ,
6263 } )
63- const layerRef = useRef ( layers . map ( ( ) => createRef < HTMLDivElement > ( ) ) )
6464
6565 const defaultStyles = useMemo < ILayeredImageStyles > (
6666 ( ) => getDefaultStyles ( transitionDuration , lightColor , shadowColor ) ,
@@ -79,10 +79,10 @@ export const LayeredImage: React.FC<ILayeredImageProps> = ({
7979 ...defaultStyles . container ,
8080 ...staticStyles . container ,
8181 } ,
82- layers : {
82+ stack : {
8383 borderRadius,
84- ...defaultStyles . layers ,
85- ...staticStyles . layers ,
84+ ...defaultStyles . stack ,
85+ ...staticStyles . stack ,
8686 } ,
8787 layer : {
8888 transitionTimingFunction,
@@ -107,11 +107,12 @@ export const LayeredImage: React.FC<ILayeredImageProps> = ({
107107 )
108108
109109 const getDimensions = ( ) => {
110+ const containerRef = elementsRef . current . container
110111 // prettier-ignore
111112 const width =
112- elementsRef . current . container . current . offsetWidth ||
113- elementsRef . current . container . current . clientWidth ||
114- elementsRef . current . container . current . scrollWidth ;
113+ containerRef . current . offsetWidth ||
114+ containerRef . current . clientWidth ||
115+ containerRef . current . scrollWidth ;
115116 const height = Math . round ( width / aspectRatio )
116117
117118 return { width, height }
@@ -138,12 +139,12 @@ export const LayeredImage: React.FC<ILayeredImageProps> = ({
138139 document . scrollingElement . scrollLeft ||
139140 window . scrollX ||
140141 window . pageXOffset
141- const containerClientRect = elementsRef . current . container . current . getBoundingClientRect ( )
142+ const containerRect = elementsRef . current . container . current . getBoundingClientRect ( )
142143
143- const offsetX = ( pageX - containerClientRect . left - bodyScrollLeft ) / width
144- const offsetY = ( pageY - containerClientRect . top - bodyScrollTop ) / height
145- const containerCenterX = pageX - containerClientRect . left - bodyScrollLeft - width / 2
146- const containerCenterY = pageY - containerClientRect . top - bodyScrollTop - height / 2
144+ const offsetX = ( pageX - containerRect . left - bodyScrollLeft ) / width
145+ const offsetY = ( pageY - containerRect . top - bodyScrollTop ) / height
146+ const containerCenterX = pageX - containerRect . left - bodyScrollLeft - width / 2
147+ const containerCenterY = pageY - containerRect . top - bodyScrollTop - height / 2
147148
148149 const containerRotationX = ( ( offsetY - containerCenterY ) / ( height / 2 ) ) * 8
149150 const containerRotationY = ( ( containerCenterX - offsetX ) / ( width / 2 ) ) * 8
@@ -152,8 +153,13 @@ export const LayeredImage: React.FC<ILayeredImageProps> = ({
152153 const lightAngle = ( Math . atan2 ( containerCenterY , containerCenterX ) * 180 ) / Math . PI - 90
153154
154155 const computedStyles : ILayeredImageStyles = {
155- [ Interaction . None ] : { ...defaultStyles } ,
156- [ Interaction . Resize ] : { root : { height : `${ height } px` , transform : `perspective(${ width * 3 } px)` } } ,
156+ [ Interaction . None ] : defaultStyles ,
157+ [ Interaction . Resize ] : {
158+ root : {
159+ height : `${ height } px` ,
160+ transform : `perspective(${ width * 3 } px)` ,
161+ } ,
162+ } ,
157163 [ Interaction . Hover ] : {
158164 container : {
159165 transform : `rotateX(${ - clamp ( containerRotationX , - 8 , 8 ) } deg)
@@ -176,10 +182,10 @@ export const LayeredImage: React.FC<ILayeredImageProps> = ({
176182 } ,
177183 [ Interaction . Active ] : {
178184 container : {
185+ transitionDuration : "0.075s" ,
179186 transform : `rotateX(${ containerRotationX / 1.4 } deg)
180187 rotateY(${ containerRotationY / 1.4 } deg)
181188 scale(1)` ,
182- transitionDuration : "0.075s" ,
183189 } ,
184190 layer : ( index : number ) => ( {
185191 transform : `translateX(${ - layerTranslationX * index } px)
@@ -200,17 +206,15 @@ export const LayeredImage: React.FC<ILayeredImageProps> = ({
200206 event . preventDefault ( )
201207 }
202208
203- Object . keys ( computedStyles ) . forEach ( ( element ) => {
204- const styles = computedStyles [ element ]
205-
209+ for ( const [ element , styles ] of Object . entries ( computedStyles ) ) {
206210 if ( element === "layer" ) {
207211 layers . forEach ( ( _ , index ) =>
208- applyStyles ( layerRef . current [ index ] . current , isFunction ( styles ) ? styles ( index ) : styles ) ,
212+ applyStyles ( elementsRef . current . layers [ index ] . current , isFunction ( styles ) ? styles ( index ) : styles ) ,
209213 )
210214 } else {
211215 applyStyles ( elementsRef . current [ element ] . current , styles )
212216 }
213- } )
217+ }
214218
215219 setSize ( { width, height } )
216220 setInteraction ( _interaction )
@@ -230,9 +234,9 @@ export const LayeredImage: React.FC<ILayeredImageProps> = ({
230234
231235 const handleInteractionEnd = ( ) => computeStyles ( Interaction . None )
232236
233- const handleWindowResize = ( ) => computeStyles ( Interaction . Resize )
234-
235237 useEffect ( ( ) => {
238+ const handleWindowResize = ( ) => computeStyles ( Interaction . Resize )
239+
236240 layers . forEach ( ( layer ) => {
237241 const image = new Image ( )
238242
@@ -268,15 +272,15 @@ export const LayeredImage: React.FC<ILayeredImageProps> = ({
268272 >
269273 < div style = { styles . container } ref = { elementsRef . current . container } >
270274 < div style = { styles . shadow } ref = { elementsRef . current . shadow } />
271- < div style = { styles . layers } >
275+ < div style = { styles . stack } >
272276 { layers . map ( ( src , index ) => (
273277 < div
274278 style = { {
275279 ...styles . layer ,
276280 backgroundImage : `url(${ src } )` ,
277281 opacity : loaded === layers . length ? 1 : 0 ,
278282 } }
279- ref = { layerRef . current [ index ] }
283+ ref = { elementsRef . current . layers [ index ] }
280284 key = { index }
281285 />
282286 ) ) }
@@ -288,8 +292,34 @@ export const LayeredImage: React.FC<ILayeredImageProps> = ({
288292}
289293LayeredImage . displayName = "LayeredImage"
290294
291- export default LayeredImage
295+ /*
296+ * Initial styles in resting state.
297+ */
298+ const getDefaultStyles = (
299+ transitionDuration : ILayeredImageProps [ "transitionDuration" ] ,
300+ lightColor : ILayeredImageProps [ "lightColor" ] ,
301+ shadowColor : ILayeredImageProps [ "shadowColor" ] ,
302+ ) : ILayeredImageStyles => ( {
303+ container : {
304+ transform : "none" ,
305+ transitionDuration : `${ transitionDuration } s` ,
306+ } ,
307+ layer : {
308+ transform : "none" ,
309+ transitionDuration : `${ transitionDuration } s, 500ms` ,
310+ } ,
311+ light : {
312+ backgroundImage : `linear-gradient(180deg, ${ lightColor } 0%, transparent 80%)` ,
313+ } ,
314+ shadow : {
315+ boxShadow : `0 10px 30px ${ shadowColor } , 0 6px 10px ${ shadowColor } ` ,
316+ transitionDuration : `${ transitionDuration } s` ,
317+ } ,
318+ } )
292319
320+ /*
321+ * Static styles that never change.
322+ */
293323const staticStyles : ILayeredImageStyles = {
294324 root : {
295325 position : "relative" ,
@@ -305,7 +335,7 @@ const staticStyles: ILayeredImageStyles = {
305335 transitionProperty : "transform" ,
306336 transformStyle : "preserve-3d" ,
307337 } ,
308- layers : {
338+ stack : {
309339 position : "absolute" ,
310340 width : "100%" ,
311341 height : "100%" ,
@@ -337,39 +367,3 @@ const staticStyles: ILayeredImageStyles = {
337367 transform : "translateZ(-10px) scale(0.95)" ,
338368 } ,
339369}
340-
341- const getDefaultStyles = (
342- transitionDuration : ILayeredImageProps [ "transitionDuration" ] ,
343- lightColor : ILayeredImageProps [ "lightColor" ] ,
344- shadowColor : ILayeredImageProps [ "shadowColor" ] ,
345- ) : ILayeredImageStyles => ( {
346- container : {
347- transform : "none" ,
348- transitionDuration : `${ transitionDuration } s` ,
349- } ,
350- layer : {
351- transform : "none" ,
352- transitionDuration : `${ transitionDuration } s, 500ms` ,
353- } ,
354- light : {
355- backgroundImage : `linear-gradient(180deg, ${ lightColor } 0%, transparent 80%)` ,
356- } ,
357- shadow : {
358- boxShadow : `0 10px 30px ${ shadowColor } , 0 6px 10px ${ shadowColor } ` ,
359- transitionDuration : `${ transitionDuration } s` ,
360- } ,
361- } )
362-
363- const applyStyles = ( element : HTMLDivElement , styles : React . CSSProperties ) => {
364- Object . keys ( styles ) . forEach ( ( style ) => {
365- // `requestAnimationFrame` doesn't play nice with CSS transition duration on
366- // desktop Safari
367- if ( isSafariDesktop ( ) ) {
368- element . style [ style ] = styles [ style ]
369- } else {
370- requestAnimationFrame ( ( ) => {
371- element . style [ style ] = styles [ style ]
372- } )
373- }
374- } )
375- }
0 commit comments