|
1 | 1 | // Import External Dependencies |
2 | 2 | import PropTypes from "prop-types"; |
3 | | -import { Component } from "react"; |
4 | | - |
5 | | -export default class Cube extends Component { |
6 | | - static propTypes = { |
7 | | - hover: PropTypes.bool, |
8 | | - theme: PropTypes.string, |
9 | | - depth: PropTypes.number, |
10 | | - repeatDelay: PropTypes.number, |
11 | | - className: PropTypes.string, |
12 | | - continuous: PropTypes.bool, |
13 | | - }; |
14 | | - |
15 | | - static defaultProps = { |
16 | | - hover: false, |
17 | | - theme: "dark", |
18 | | - depth: 30, |
19 | | - repeatDelay: 1000, |
20 | | - }; |
| 3 | +import { useEffect, useRef, useState } from "react"; |
21 | 4 |
|
22 | | - state = { |
23 | | - x: 0, |
24 | | - y: 0, |
25 | | - z: 0, |
26 | | - iteration: 0, |
27 | | - }; |
| 5 | +export default function Cube({ |
| 6 | + hover = false, |
| 7 | + theme = "dark", |
| 8 | + depth = 30, |
| 9 | + repeatDelay = 1000, |
| 10 | + className = "", |
| 11 | + continuous, |
| 12 | +}) { |
| 13 | + const [state, setState] = useState({ x: 0, y: 0, z: 0, iteration: 0 }); |
| 14 | + const containerRef = useRef(null); |
| 15 | + const timeoutRef = useRef(null); |
28 | 16 |
|
29 | | - render() { |
30 | | - const { x, y, z } = this.state; |
31 | | - const { theme, depth, className = "" } = this.props; |
| 17 | + const { x, y, z, iteration } = state; |
32 | 18 |
|
33 | | - return ( |
34 | | - <div |
35 | | - className={`cube__container ${className}`} |
36 | | - style={{ |
37 | | - width: `${depth * 1.5}px`, |
38 | | - height: `${depth * 1.5}px`, |
39 | | - paddingLeft: `${depth / 1.7}px`, |
40 | | - }} |
41 | | - > |
42 | | - <span |
43 | | - ref={(ref) => (this.container = ref)} |
44 | | - className={`cube cube--${theme} relative block [transform-style:preserve-3d]`} |
45 | | - style={{ |
46 | | - width: `${depth}px`, |
47 | | - paddingBottom: `${depth * 0.5}px`, |
48 | | - transform: "rotateX(-35.5deg) rotateY(45deg)", |
49 | | - }} |
50 | | - > |
51 | | - <figure |
52 | | - className="cube__outer inline-block [transform-style:preserve-3d] transition-transform duration-1000" |
53 | | - style={{ |
54 | | - width: `${depth}px`, |
55 | | - height: `${depth}px`, |
56 | | - transform: `translateX(-50%) |
57 | | - scale3d(1,1,1) |
58 | | - rotateX(${x}deg) |
59 | | - rotateY(${y}deg) |
60 | | - rotateZ(${z}deg)`, |
61 | | - }} |
62 | | - > |
63 | | - {this._getFaces("outer")} |
64 | | - </figure> |
65 | | - <figure |
66 | | - className="cube__inner absolute -top-[2px] left-0 inline-block [transform-style:preserve-3d] transition-transform duration-1000" |
67 | | - style={{ |
68 | | - width: `${depth}px`, |
69 | | - height: `${depth}px`, |
70 | | - transform: `translateX(-50%) translateY(2px) |
71 | | - scale3d(0.5,0.5,0.5) |
72 | | - rotateX(${-x}deg) |
73 | | - rotateY(${-y}deg) |
74 | | - rotateZ(${-z}deg)`, |
75 | | - }} |
76 | | - > |
77 | | - {this._getFaces("inner")} |
78 | | - </figure> |
79 | | - </span> |
80 | | - </div> |
81 | | - ); |
82 | | - } |
83 | | - |
84 | | - componentDidMount() { |
85 | | - const { hover, continuous, repeatDelay } = this.props; |
| 19 | + useEffect(() => { |
| 20 | + const container = containerRef.current; |
86 | 21 |
|
87 | 22 | if (hover) { |
88 | | - this.container.addEventListener("mouseenter", this._spin); |
89 | | - this.container.addEventListener("mouseleave", this._reset); |
| 23 | + const spin = () => { |
| 24 | + const axes = ["x", "y", "z", "iteration"]; |
| 25 | + const axis = axes[Math.floor(Math.random() * axes.length)]; |
| 26 | + const sign = Math.random() < 0.5 ? -1 : 1; |
| 27 | + setState((prev) => ({ ...prev, [axis]: sign * 90 })); |
| 28 | + }; |
| 29 | + |
| 30 | + const reset = () => { |
| 31 | + setState((prev) => ({ ...prev, x: 0, y: 0, z: 0 })); |
| 32 | + }; |
| 33 | + |
| 34 | + container.addEventListener("mouseenter", spin); |
| 35 | + container.addEventListener("mouseleave", reset); |
| 36 | + |
| 37 | + return () => { |
| 38 | + container.removeEventListener("mouseenter", spin); |
| 39 | + container.removeEventListener("mouseleave", reset); |
| 40 | + }; |
90 | 41 | } else if (continuous) { |
91 | 42 | let degrees = 0; |
92 | 43 | const axis = "y"; |
93 | 44 |
|
94 | 45 | const animation = () => { |
95 | 46 | const obj = {}; |
96 | 47 | obj[axis] = degrees += 90; |
97 | | - this.setState({ |
| 48 | + setState((prev) => ({ |
| 49 | + ...prev, |
98 | 50 | ...obj, |
99 | | - iteration: (this.state.iteration + 1) % 4, |
100 | | - }); |
| 51 | + iteration: (prev.iteration + 1) % 4, |
| 52 | + })); |
101 | 53 | // eslint-disable-next-line no-use-before-define |
102 | 54 | tick(); |
103 | 55 | }; |
104 | 56 |
|
105 | 57 | const tick = () => |
106 | 58 | setTimeout(() => requestAnimationFrame(animation), repeatDelay); |
107 | 59 |
|
108 | | - this._timeout = tick(); |
109 | | - } |
110 | | - } |
111 | | - |
112 | | - componentWillUnmount() { |
113 | | - const { hover, continuous } = this.props; |
| 60 | + timeoutRef.current = tick(); |
114 | 61 |
|
115 | | - if (hover) { |
116 | | - this.container.removeEventListener("mouseenter", this._spin); |
117 | | - this.container.removeEventListener("mouseleave", this._reset); |
118 | | - } else if (continuous) { |
119 | | - clearTimeout(this._timeout); |
| 62 | + return () => clearTimeout(timeoutRef.current); |
120 | 63 | } |
121 | | - } |
122 | | - |
123 | | - /** |
124 | | - * Get all faces for a cube |
125 | | - * |
126 | | - * @param {'inner' | 'outer' } type |
127 | | - * @return {array} - An array of nodes |
128 | | - */ |
129 | | - _getFaces(type) { |
130 | | - const { iteration } = this.state; |
| 64 | + }, []); // eslint-disable-line react-hooks/exhaustive-deps |
131 | 65 |
|
| 66 | + const getFaces = (type) => { |
132 | 67 | // Keep the thicker border on |
133 | 68 | // the outside on each iteration |
134 | 69 | const borderWidthMap = { |
@@ -201,50 +136,70 @@ export default class Cube extends Component { |
201 | 136 | key={i} |
202 | 137 | className={`cube__face ${baseFaceClasses} ${variantClasses}`} |
203 | 138 | style={{ |
204 | | - transform: `${rotation} translateZ(${this.props.depth / 2}px)`, |
| 139 | + transform: `${rotation} translateZ(${depth / 2}px)`, |
205 | 140 | ...borderStyles, |
206 | 141 | }} |
207 | 142 | /> |
208 | 143 | ); |
209 | 144 | }); |
210 | | - } |
211 | | - |
212 | | - /** |
213 | | - * Get a random axis |
214 | | - * |
215 | | - * @return {string} - A random axis (i.e. x, y, or z) |
216 | | - */ |
217 | | - _getRandomAxis() { |
218 | | - const axes = Object.keys(this.state); |
219 | | - |
220 | | - return axes[Math.floor(Math.random() * axes.length)]; |
221 | | - } |
222 | | - |
223 | | - /** |
224 | | - * Spin the cubes in opposite directions semi-randomly |
225 | | - * |
226 | | - * @param {object} e - Native event |
227 | | - */ |
228 | | - _spin = () => { |
229 | | - const obj = {}; |
230 | | - const axis = this._getRandomAxis(); |
231 | | - const sign = Math.random() < 0.5 ? -1 : 1; |
232 | | - |
233 | | - obj[axis] = sign * 90; |
234 | | - |
235 | | - this.setState(obj); |
236 | 145 | }; |
237 | 146 |
|
238 | | - /** |
239 | | - * Rotate the cubes back to their original position |
240 | | - * |
241 | | - * @param {object} e - Native event |
242 | | - */ |
243 | | - _reset = () => { |
244 | | - this.setState({ |
245 | | - x: 0, |
246 | | - y: 0, |
247 | | - z: 0, |
248 | | - }); |
249 | | - }; |
| 147 | + return ( |
| 148 | + <div |
| 149 | + className={`cube__container ${className}`} |
| 150 | + style={{ |
| 151 | + width: `${depth * 1.5}px`, |
| 152 | + height: `${depth * 1.5}px`, |
| 153 | + paddingLeft: `${depth / 1.7}px`, |
| 154 | + }} |
| 155 | + > |
| 156 | + <span |
| 157 | + ref={containerRef} |
| 158 | + className={`cube cube--${theme} relative block [transform-style:preserve-3d]`} |
| 159 | + style={{ |
| 160 | + width: `${depth}px`, |
| 161 | + paddingBottom: `${depth * 0.5}px`, |
| 162 | + transform: "rotateX(-35.5deg) rotateY(45deg)", |
| 163 | + }} |
| 164 | + > |
| 165 | + <figure |
| 166 | + className="cube__outer inline-block [transform-style:preserve-3d] transition-transform duration-1000" |
| 167 | + style={{ |
| 168 | + width: `${depth}px`, |
| 169 | + height: `${depth}px`, |
| 170 | + transform: `translateX(-50%) |
| 171 | + scale3d(1,1,1) |
| 172 | + rotateX(${x}deg) |
| 173 | + rotateY(${y}deg) |
| 174 | + rotateZ(${z}deg)`, |
| 175 | + }} |
| 176 | + > |
| 177 | + {getFaces("outer")} |
| 178 | + </figure> |
| 179 | + <figure |
| 180 | + className="cube__inner absolute -top-[2px] left-0 inline-block [transform-style:preserve-3d] transition-transform duration-1000" |
| 181 | + style={{ |
| 182 | + width: `${depth}px`, |
| 183 | + height: `${depth}px`, |
| 184 | + transform: `translateX(-50%) translateY(2px) |
| 185 | + scale3d(0.5,0.5,0.5) |
| 186 | + rotateX(${-x}deg) |
| 187 | + rotateY(${-y}deg) |
| 188 | + rotateZ(${-z}deg)`, |
| 189 | + }} |
| 190 | + > |
| 191 | + {getFaces("inner")} |
| 192 | + </figure> |
| 193 | + </span> |
| 194 | + </div> |
| 195 | + ); |
250 | 196 | } |
| 197 | + |
| 198 | +Cube.propTypes = { |
| 199 | + hover: PropTypes.bool, |
| 200 | + theme: PropTypes.string, |
| 201 | + depth: PropTypes.number, |
| 202 | + repeatDelay: PropTypes.number, |
| 203 | + className: PropTypes.string, |
| 204 | + continuous: PropTypes.bool, |
| 205 | +}; |
0 commit comments