You tell me..
Created
January 30, 2023 20:16
-
-
Save alirezarezamand/d471aed0ec11444c13bcd4dbac9153d3 to your computer and use it in GitHub Desktop.
Click the button!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div id="app"></div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { useState, useRef, useEffect, useLayoutEffect, createContext } = React; | |
/** | |
* Globals | |
*/ | |
const CONSTANTS = { | |
assetPath: "https://s3-us-west-2.amazonaws.com/s.cdpn.io/184729", | |
} | |
const ASSETS = { | |
head: `${CONSTANTS.assetPath}/head.svg`, | |
waiting: `${CONSTANTS.assetPath}/hand.svg`, | |
stalking: `${CONSTANTS.assetPath}/hand-waiting.svg`, | |
grabbing: `${CONSTANTS.assetPath}/hand.svg`, | |
grabbed: `${CONSTANTS.assetPath}/hand-with-cursor.svg`, | |
shaka: `${CONSTANTS.assetPath}/hand-surfs-up.svg` | |
} | |
// Preload images | |
Object.keys(ASSETS).forEach(key => { | |
const img = new Image(); | |
img.src = ASSETS[key]; | |
}); | |
/** | |
* Shared hooks | |
*/ | |
// Hover state - https://dev.to/spaciecat/hover-states-with-react-hooks-4023 | |
const useHover = () => { | |
const ref = useRef(); | |
const [hovered, setHovered] = useState(false); | |
const enter = () => setHovered(true); | |
const leave = () => setHovered(false); | |
useEffect( | |
() => { | |
ref.current.addEventListener("mouseenter", enter); | |
ref.current.addEventListener("mouseleave", leave); | |
return () => { | |
ref.current.removeEventListener("mouseenter", enter); | |
ref.current.removeEventListener("mouseleave", leave); | |
}; | |
}, | |
[ref] | |
); | |
return [ref, hovered]; | |
}; | |
// Mouse position | |
const useMousePosition = () => { | |
const [position, setPosition] = useState({ x: 0, y: 0 }); | |
useEffect(() => { | |
const setFromEvent = e => setPosition({ x: e.clientX, y: e.clientY }); | |
window.addEventListener("mousemove", setFromEvent); | |
return () => { | |
window.removeEventListener("mousemove", setFromEvent); | |
}; | |
}, []); | |
return position; | |
}; | |
// Element position | |
const usePosition = () => { | |
const ref = useRef(); | |
const [position, setPosition] = useState({}); | |
const handleResize = () => { | |
setPosition(ref.current.getBoundingClientRect()); | |
}; | |
useLayoutEffect(() => { | |
handleResize(); | |
window.addEventListener('resize', handleResize); | |
return () => { | |
window.removeEventListener('resize', handleResize); | |
}; | |
}, [ref.current]); | |
return [ref, position]; | |
}; | |
/** | |
* React Components | |
*/ | |
class App extends React.Component { | |
constructor(props) { | |
super(props); | |
this.state = { | |
debug: false, | |
cursorGrabbed: false, | |
gameOver: false, | |
}; | |
this.handleToggleDebug = this.handleToggleDebug.bind(this); | |
this.handleButtonClicked = this.handleButtonClicked.bind(this); | |
this.handleCursorGrabbed = this.handleCursorGrabbed.bind(this); | |
} | |
handleToggleDebug() { | |
this.setState({ | |
debug: !this.state.debug | |
}); | |
} | |
handleCursorGrabbed() { | |
this.setState({ | |
cursorGrabbed: true | |
}); | |
setTimeout(() => { | |
this.setState({ | |
cursorGrabbed: false | |
}); | |
}, 2000) | |
} | |
handleButtonClicked() { | |
this.setState({ | |
gameOver: true | |
}); | |
setTimeout(() => { | |
this.setState({ | |
gameOver: false | |
}); | |
}, 4000) | |
} | |
render() { | |
const { cursorGrabbed, gameOver, debug } = this.state; | |
const screenStyle = cursorGrabbed ? { cursor: "none" } : {}; | |
const appClass = debug ? "app app--debug" : "app"; | |
return ( | |
<div className={appClass} style={screenStyle}> | |
<section className="container"> | |
<h1>Hello!</h1> | |
<h2>Welcome to the internet.</h2> | |
<p>This is a classic website, no traps or weird stuff!</p> | |
<p>Feel free to browse, relax and, I don't know, click the button down there? Might as well, right?</p> | |
<button | |
className="debug-button" | |
onClick={this.handleToggleDebug}> | |
Debug | |
</button> | |
</section> | |
<button | |
className="trap-button" | |
onClick={this.handleButtonClicked}> | |
{ gameOver && "Nice one" } | |
{ cursorGrabbed && "Gotcha!" } | |
{ !gameOver && !cursorGrabbed && "Button!"} | |
</button> | |
<div className="grab-zone-wrapper"> | |
<GrabZone | |
onCursorGrabbed={this.handleCursorGrabbed} | |
cursorGrabbed={cursorGrabbed} | |
gameOver={gameOver} | |
/> | |
</div> | |
</div> | |
); | |
} | |
} | |
// GrabZone (The hover trigger zone) | |
const GrabZone = ({ cursorGrabbed, gameOver, onCursorGrabbed }) => { | |
const [outerRef, outerHovered] = useHover(); | |
const [innerRef, innerHovered] = useHover(); | |
const [isExtended, setExtendedArm] = useState(false); | |
let state = "waiting"; | |
if (outerHovered) { | |
state = "stalking"; | |
} | |
if (innerHovered) { | |
state = "grabbing"; | |
} | |
if (cursorGrabbed) { | |
state = "grabbed"; | |
} | |
if (gameOver) { | |
state = "shaka" | |
} | |
// If state is grabbing for a long time, they're being clever! | |
useEffect(() => { | |
let timer; | |
if (state === "grabbing") { | |
timer = setTimeout(() => { | |
// Not so clever now, are they? | |
setExtendedArm(true); | |
timer = null; | |
}, 2000); | |
} | |
return () => { | |
setExtendedArm(false); | |
if (timer) { | |
clearTimeout(timer); | |
} | |
}; | |
}, | |
[state] | |
); | |
return ( | |
<div className="grab-zone" ref={outerRef}> | |
<div className="grab-zone__debug"> | |
<strong>Debug info:</strong> | |
<p>Current state: {state}</p> | |
<p>Extended arm: {isExtended ? "Yes" : "No"}</p> | |
</div> | |
<div className="grab-zone__danger" ref={innerRef}> | |
<Grabber | |
state={state} | |
gameOver={gameOver} | |
extended={isExtended} | |
onCursorGrabbed={onCursorGrabbed} | |
/> | |
</div> | |
</div> | |
); | |
}; | |
// Grabber (The graphic) | |
const Grabber = ({ state, gameOver, extended, onCursorGrabbed }) => { | |
const mousePos = useMousePosition(); | |
const [ref, position] = usePosition(); | |
const hasCursor = false; | |
// Calculate rotation of armWrapper | |
const x = position.left + position.width * 0.5; | |
const y = position.top + position.height * 0.5; | |
const angle = gameOver ? 0 : Math.atan2(mousePos.x - x, -(mousePos.y - y)) * (180 / Math.PI); | |
// Ensure value is within acceptable range (-75 to 75) | |
const rotation = Math.min(Math.max(parseInt(angle), -79), 79); | |
const grabberClass = `grabber grabber--${state} ${extended && "grabber--extended"}`; | |
const wrapperStyle = { transform: `rotate(${rotation}deg)` }; | |
let handImageSrc = ASSETS[state]; | |
return ( | |
<div className={grabberClass}> | |
<div className="grabber__body"></div> | |
<img className="grabber__face" src={ASSETS.head} /> | |
<div className="grabber__arm-wrapper" ref={ref} style={wrapperStyle}> | |
<div className="grabber__arm"> | |
<img | |
className="grabber__hand" | |
src={handImageSrc} | |
onMouseEnter={onCursorGrabbed} | |
/> | |
</div> | |
</div> | |
</div> | |
); | |
}; | |
// Render app | |
ReactDOM.render(<App />, document.getElementById("app")); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.9.0/umd/react-dom.production.min.js"></script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
html { | |
font-size: 18px; | |
@media (min-width: 900px) { | |
font-size: 24px; | |
} | |
} | |
body { | |
font-family: 'Montserrat', sans-serif; | |
font-weight: 300; | |
line-height: 1.45; | |
color: #0F1108; | |
} | |
h1 { | |
font-size: 2.2rem; | |
margin: 0; | |
font-weight: 600; | |
line-height: 1.15; | |
@media (min-width: 900px) { | |
font-size: 2.488rem; | |
} | |
} | |
h2 { | |
font-size: 1.4rem; | |
margin: 0.5rem 0; | |
line-height: 1.15; | |
font-weight: 200; | |
@media (min-width: 900px) { | |
margin: 1rem 0; | |
font-size: 1.44rem; | |
} | |
} | |
p { | |
margin-top: 0.25rem; | |
@media (min-width: 900px) { | |
margin-top: 0.5rem; | |
} | |
} | |
a { | |
color: #0F1108; | |
text-decoration: none; | |
border-bottom: currentcolor 1px solid; | |
} | |
// General modules | |
.container { | |
max-width: 520px; | |
margin: 0 auto; | |
padding: 0 1rem 100px 1rem; | |
@media (min-width: 900px) { | |
max-width: 650px; | |
padding: 0 1rem 90px 1rem; | |
} | |
} | |
// Full-screen wrapper | |
.app { | |
position: relative; | |
background: #F2E9DE; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
min-height: calc(100vh - 2rem); | |
margin: 1rem; | |
overflow: hidden; | |
// Modifiers | |
&--debug { | |
.grab-zone { | |
background: rgba(0, 0, 0, 0.15); | |
} | |
.grab-zone__debug { | |
display: block; | |
} | |
.grab-zone__danger { | |
background: rgba(0, 0, 0, 0.15); | |
} | |
.grabber__arm-wrapper { | |
background: rgba(0, 0, 0, 0.15); | |
} | |
} | |
} | |
.grab-zone-wrapper { | |
position: absolute; | |
bottom: 0; | |
right: 0; | |
transform: translateX(30%) translateY(50%); | |
} | |
.grab-zone { | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
width: 700px; | |
height: 700px; | |
border-radius: 50%; | |
&__danger { | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
width: 400px; | |
height: 400px; | |
border-radius: 50%; | |
} | |
&__debug { | |
display: none; | |
position: absolute; | |
width: 300px; | |
top: -100px; | |
font-size: 14px; | |
text-align: center; | |
text-transform: uppercase; | |
} | |
} | |
.grabber { | |
position: relative; | |
width: 100px; | |
height: 100px; | |
&__arm-wrapper { | |
position: absolute; | |
top: -80px; | |
width: 24px; | |
height: 260px; | |
} | |
&__arm { | |
position: relative; | |
width: 24px; | |
height: 200px; | |
background: #7D9A9E; | |
border-radius: 20px; | |
overflow: visible; | |
transform: translateY(100%); | |
transition: transform 0.2s ease; | |
} | |
&__hand { | |
display: block; | |
position: absolute; | |
top: -12px; | |
transform: scale(1.4) rotate(-10deg) translateY(100%); | |
transform-origin: bottom center; | |
transition: transform 0.3s ease; | |
} | |
&__face { | |
position: absolute; | |
width: 75px; | |
height: 84px; | |
right: 5%; | |
transition: transform 0.3s ease; | |
} | |
&__body { | |
position: absolute; | |
top: 50%; | |
left: 0%; | |
width: 110px; | |
height: 95px; | |
border-radius: 50%; | |
background: #7D9A9E; | |
transition: transform 0.3s ease; | |
} | |
// Modifiers | |
&--waiting { | |
.grabber__hand { | |
transform: scale(1.4) rotate(-10deg); | |
} | |
.grabber__arm { | |
transform: translateY(80%); | |
} | |
.grabber__face { | |
transform: translateY(60%); | |
} | |
} | |
// Modifiers | |
&--stalking { | |
.grabber__hand { | |
transform: scale(1.4) rotate(-10deg); | |
} | |
.grabber__arm { | |
transform: translateY(70%); | |
} | |
.grabber__face { | |
transform: translateY(10%); | |
} | |
} | |
&--grabbing { | |
.grabber__face { | |
transform: translateY(-40%) rotate(10deg); | |
} | |
.grabber__arm { | |
transform: translateY(0%); | |
} | |
.grabber__body { | |
transform: translateY(-20%); | |
} | |
.grabber__hand { | |
transform: scale(1.7) rotate(10deg); | |
} | |
} | |
&--grabbed { | |
.grabber__arm { | |
transition: transform 1s ease; | |
} | |
.grabber__hand { | |
transition: transform 2.5s ease; | |
} | |
.grabber__face { | |
transform: translateY(70%); | |
transition: transform 1s ease; | |
} | |
.grabber__body { | |
transform: translateY(50%); | |
transition: transform 1s ease; | |
} | |
} | |
&--extended { | |
.grabber__arm { | |
transform: translateY(-20%); | |
} | |
.grabber__face { | |
transform: translateY(-60%) rotate(15deg); | |
} | |
.grabber__body { | |
transform: translateY(-40%); | |
} | |
} | |
&--shaka { | |
.grabber__arm { | |
transform: translateY(50%); | |
} | |
.grabber__hand { | |
transform: scale(2.5) translateY(10%); | |
animation: shaka 0.5s infinite alternate forwards; | |
transform-origin: 55% 60%; | |
} | |
.grabber__face { | |
transform: translateY(70%); | |
transition: transform 1s ease; | |
} | |
.grabber__body { | |
transform: translateY(50%); | |
transition: transform 1s ease; | |
} | |
} | |
} | |
.trap-button { | |
position: absolute; | |
bottom: 80px; | |
right: 70px; | |
min-width: 125px; | |
background: #8ECACC; | |
color: white; | |
border-radius: 5px; | |
padding: 0.4rem 0.5rem; | |
font-weight: 600; | |
font-size: 18px; | |
letter-spacing: 1px; | |
text-transform: uppercase; | |
} | |
.debug-button { | |
position: fixed; | |
top: 0; | |
right: 0; | |
background: transparent; | |
padding: 1rem; | |
margin: 1rem; | |
font-size: 16px; | |
text-transform: uppercase; | |
letter-spacing: 1px; | |
opacity: 0.5; | |
} | |
@keyframes shaka { | |
0% { transform: scale(2.5) translateY(0%) rotate(-20deg); } | |
100% { transform: scale(2.5) translateY(0%) rotate(20deg); } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<link href="https://unpkg.com/backpack.css" rel="stylesheet" /> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment