1- import React from "react" ;
1+ import React , { useState , useRef , useEffect , useReducer } from "react" ;
22import "./styles.css" ;
33
4- export default class Carousel extends React . Component {
4+ //A hook for getting the previous state/a previous value, if needed
5+ const usePrevious = value => {
6+ const ref = useRef ( ) ;
7+ useEffect ( ( ) => {
8+ ref . current = value ;
9+ } ) ;
10+ return ref . current ;
11+ } ;
12+
13+ const useWidth = ref => {
14+ console . log ( "called useWidth" ) ;
15+ const [ width , setWidth ] = useState ( 0 ) ;
16+ useEffect ( ( ) => {
17+ setWidth ( ref . current . getBoundingClientRect ( ) . width ) ;
18+ } , [ ref ] ) ;
19+ } ;
20+
21+ const telemetryReducer = ( state , action ) => {
22+ switch ( action . type ) {
23+ case "user_move" :
24+ return {
25+ angle : action . angle ,
26+ change : action . change ,
27+ velocity : action . velocity
28+ } ;
29+ case "decay_move" :
30+ return {
31+ ...state ,
32+ angle : action . angle ,
33+ velocity : action . velocity
34+ } ;
35+ case "reset_change" :
36+ return {
37+ ...state ,
38+ change : 0
39+ } ;
40+ case "reset_velocity" :
41+ return { ...state , velocity : 0 } ;
42+
43+ default :
44+ return state ;
45+ }
46+ } ;
47+
48+ const Carousel2 = ( ) => {
49+ const [ dragState , setDragState ] = useState ( {
50+ dragging : false ,
51+ dragStartX : 0 ,
52+ dragStartTime : 0
53+ } ) ;
54+ const [ carouselTelemetry , dispatch ] = useReducer ( telemetryReducer , {
55+ angle : 0 ,
56+ change : 0 ,
57+ velocity : 0
58+ } ) ;
59+ const [ decayInterval , setDecayInterval ] = useState ( null ) ;
60+ const draggableRef = useRef ( ) ;
61+ const width = useWidth ( draggableRef ) ;
62+
63+ useEffect ( ( ) => {
64+ window . addEventListener ( "mouseup" , onDragEndMouse ) ;
65+ window . addEventListener ( "touchend" , onDragEndTouch ) ;
66+
67+ return ( ) => {
68+ window . removeEventListener ( "mouseup" , onDragEndMouse ) ;
69+ window . removeEventListener ( "touchend" , onDragEndTouch ) ;
70+ } ;
71+ } , [ ] ) ;
72+
73+ const onDragEndMouse = e => {
74+ window . removeEventListener ( "mousemove" , this . onMouseMove ) ;
75+ onDragEnd ( ) ;
76+ } ;
77+
78+ const onDragEndTouch = e => {
79+ window . removeEventListener ( "touchmove" , this . onTouchMove ) ;
80+ onDragEnd ( ) ;
81+ } ;
82+
83+ const onDragStartMouse = e => {
84+ onDragStart ( e . clientX ) ;
85+ window . addEventListener ( "mousemove" , this . onMouseMove ) ;
86+ } ;
87+
88+ const onDragStartTouch = e => {
89+ const touch = e . targetTouches [ 0 ] ;
90+ onDragStart ( touch . clientX ) ;
91+ window . addEventListener ( "touchmove" , this . onTouchMove ) ;
92+ } ;
93+
94+ const onDragStart = clientX => {
95+ if ( decayInterval !== null ) {
96+ clearInterval ( decayInterval ) ;
97+ }
98+ setDragState ( { dragStartX : clientX , dragging : true , velocity : 0 } ) ;
99+ requestAnimationFrame ( updatePosition ) ;
100+ } ;
101+
102+ const onDragEnd = ( ) => {
103+ setDragState ( state => ( { ...state , change : 0 , dragging : false } ) ) ;
104+ setDecayInterval ( state =>
105+ state === null ? setInterval ( animateSliding , 66 ) : null
106+ ) ;
107+ } ;
108+
109+ return (
110+ < div >
111+ < h1 > Spin the text below!</ h1 >
112+ < div
113+ ref = { this . draggableRef }
114+ onMouseDown = { onDragStartMouse }
115+ onTouchStart = { onDragStartTouch }
116+ className = "spinText"
117+ >
118+ touch move me wherever necessary
119+ </ div >
120+ < br />
121+ < br />
122+ < br />
123+ < span >
124+ { `pos: ${ carouselTelemetry . angle } velo:${ carouselTelemetry . velocity } ` }
125+ </ span >
126+ </ div >
127+ ) ;
128+ } ;
129+
130+ class Carousel extends React . Component {
5131 constructor ( props ) {
6132 super ( props ) ;
7133
@@ -13,8 +139,9 @@ export default class Carousel extends React.Component {
13139 dragStartTime : 0 ,
14140 carouselPosition : 0 , //This can be a value between 0 - 360,
15141 change : 0 ,
142+ velocity : 0 ,
16143 width : 0 ,
17- velocity : 0
144+ intervalId : null
18145 } ;
19146
20147 this . onDragStartMouse = this . onDragStartMouse . bind ( this ) ;
@@ -26,6 +153,7 @@ export default class Carousel extends React.Component {
26153 this . onDragStart = this . onDragStart . bind ( this ) ;
27154 this . onDragEndMouse = this . onDragEndMouse . bind ( this ) ;
28155 this . onDragEndTouch = this . onDragEndTouch . bind ( this ) ;
156+ this . animateSliding = this . animateSliding . bind ( this ) ;
29157 }
30158
31159 componentDidMount ( ) {
@@ -42,14 +170,22 @@ export default class Carousel extends React.Component {
42170 }
43171
44172 onDragStart ( clientX ) {
45- this . setState ( ( ) => ( { dragStartX : clientX , dragging : true } ) ) ;
173+ if ( this . state . intervalId !== null ) {
174+ clearInterval ( this . state . intervalId ) ;
175+ }
176+ this . setState ( ( ) => ( { dragStartX : clientX , dragging : true , velocity : 0 } ) ) ;
46177 requestAnimationFrame ( this . updatePosition ) ;
47178 }
48179
49180 onDragEnd ( ) {
50- if ( this . state . dragging ) {
51- this . setState ( ( ) => ( { dragging : false , change : 0 } ) ) ;
52- }
181+ this . setState ( prevState => ( {
182+ dragging : false ,
183+ change : 0 ,
184+ intervalId :
185+ prevState . intervalId === null
186+ ? setInterval ( this . animateSliding , 66 )
187+ : null
188+ } ) ) ;
53189 }
54190
55191 updatePosition ( ) {
@@ -61,8 +197,10 @@ export default class Carousel extends React.Component {
61197 const now = Date . now ( ) ;
62198 const elapsed = now - this . state . dragStartTime ;
63199
64- if ( this . state . dragging && elapsed > 20 ) {
65- console . log ( "carouselPos: " , this . state . carouselPosition ) ;
200+ if (
201+ ( this . state . dragging && elapsed > 20 ) ||
202+ this . state . intervalId !== null
203+ ) {
66204 this . draggableRef . current . style . transform = `rotate3d(0,1,0,${
67205 this . state . carouselPosition
68206 } deg)`;
@@ -79,7 +217,10 @@ export default class Carousel extends React.Component {
79217 change - prevState . change ,
80218 prevState . carouselPosition ,
81219 prevState . width
82- )
220+ ) ,
221+ velocity :
222+ ( ( change - prevState . change ) / ( Date . now ( ) - prevState . dragStartTime ) ) *
223+ 20
83224 } ) ) ;
84225 }
85226
@@ -92,7 +233,10 @@ export default class Carousel extends React.Component {
92233 change - prevState . change ,
93234 prevState . carouselPosition ,
94235 prevState . width
95- )
236+ ) ,
237+ velocity :
238+ ( ( change - prevState . change ) / ( Date . now ( ) - prevState . dragStartTime ) ) *
239+ 20
96240 } ) ) ;
97241 }
98242
@@ -118,30 +262,51 @@ export default class Carousel extends React.Component {
118262 }
119263
120264 calculateCarouselPosition ( currentMovement , previousCarouselState , width ) {
121- //current movement is something like +40 or -20 or whatever, depending on left or right movement
122- // previouCarousel is something between 1 & 360, so we need to map changes to that, we may need to factor in the
123- // component width when mapping the currentMovement to the carousel change
265+ //1) Here we take the distance spun since our last frame checked
266+ //2) We don't want to let a user spin more than the width of our element so that
267+ //a full spin is the start to the finish of the element in dragging distance,
268+ //so we min/max it.
269+ //3) We map the distance moved, relative to the elements with to 360 degrees to see
270+ //how much of the "carousel" we've spun
271+ //4) We don't ever want to be spinning more than 360 degrees or less than 0
124272 const cappedMovement =
125273 currentMovement > 0
126274 ? Math . min ( currentMovement , width )
127275 : Math . max ( currentMovement , - width ) ;
128- const degreesMoved = ( cappedMovement / width ) * 360 + previousCarouselState ; //may be -30 degrees or + 10 degrees? who knows
129- //which percentage of the current div have we moved? and how many degrees is that for our 360 carousel
130- //If mappedCarouselMovement + previousCarousel state is > 360, then we take the amount we go over by and instead add that to 0,
131- //so we always have a number between 0 and 360. Same for < 0, if its -30 after the calculation, then we take that different and
132- //subtract it from 360
276+ const degreesMoved = ( cappedMovement / width ) * 360 + previousCarouselState ;
133277 const currentCarouselDegrees =
134278 degreesMoved > 360
135279 ? degreesMoved - 360
136280 : degreesMoved < 0
137281 ? degreesMoved + 360
138282 : degreesMoved ;
139283
140- return currentCarouselDegrees ;
284+ return Math . round ( currentCarouselDegrees ) ;
285+ }
286+
287+ animateSliding ( ) {
288+ const { velocity, dragging } = this . state ;
289+ if ( ! dragging && Math . abs ( velocity ) > 0 ) {
290+ this . setState ( prevState => ( {
291+ carouselPosition : this . calculateCarouselPosition (
292+ velocity ,
293+ prevState . carouselPosition ,
294+ prevState . width
295+ ) ,
296+ velocity :
297+ Math . abs ( prevState . velocity / 1.5 ) < 0.3
298+ ? 0
299+ : prevState . velocity / 1.5
300+ } ) ) ;
301+ requestAnimationFrame ( this . updatePosition ) ;
302+ } else {
303+ clearInterval ( this . state . intervalId ) ;
304+ this . setState ( ( ) => ( { intervalId : null } ) ) ;
305+ }
141306 }
142307
143308 render ( ) {
144- const { change , carouselPosition } = this . state ;
309+ const { carouselPosition , velocity } = this . state ;
145310
146311 return (
147312 < div >
@@ -154,9 +319,13 @@ export default class Carousel extends React.Component {
154319 >
155320 touch move me wherever necessary
156321 </ div >
157- { /* <Carousel360 imageNumber={carouselPosition} /> */ }
158- { carouselPosition }
322+ < br />
323+ < br />
324+ < br />
325+ { `pos: ${ carouselPosition } velo:${ velocity } ` }
159326 </ div >
160327 ) ;
161328 }
162329}
330+
331+ export default Carousel ;
0 commit comments