Skip to content

Instantly share code, notes, and snippets.

@daleweaver777
Created January 24, 2024 16:42
Show Gist options
  • Save daleweaver777/0645c714c8768cdd57aa8b8316fd7068 to your computer and use it in GitHub Desktop.
Save daleweaver777/0645c714c8768cdd57aa8b8316fd7068 to your computer and use it in GitHub Desktop.
Impossible Checkbox v2 🐻

Impossible Checkbox v2 🐻

Revisiting one of my favorite pens to update the React side of it and add sound

A Pen by Jhey on CodePen.

License.

const {
React: { useState, useRef, useEffect, Fragment },
ReactDOM: { render },
gsap: {
set,
to,
timeline,
utils: { random },
},
} = window
const rootNode = document.getElementById('app')
const armLimit = random(0, 3)
const headLimit = random(armLimit + 1, armLimit + 3)
const angerLimit = random(headLimit + 1, headLimit + 3)
const armDuration = 0.2
const bearDuration = 0.25
const checkboxDuration = 0.25
const pawDuration = 0.1
const SOUNDS = {
ON: new Audio('https://assets.codepen.io/605876/switch-on.mp3'),
OFF: new Audio('https://assets.codepen.io/605876/switch-off.mp3'),
GROAN: new Audio('https://assets.codepen.io/605876/bear-groan.mp3'),
}
SOUNDS.GROAN.playbackRate = 2
const App = () => {
const [checked, setChecked] = useState(false)
const [count, setCount] = useState(1)
const bearRef = useRef(null)
const swearRef = useRef(null)
const armWrapRef = useRef(null)
const pawRef = useRef(null)
const armRef = useRef(null)
const bgRef = useRef(null)
const indicatorRef = useRef(null)
const onHover = () => {
if (Math.random() > 0.5 && count > armLimit) {
to(bearRef.current, bearDuration / 2, { y: '40%' })
}
}
const offHover = () => {
if (!checked) {
to(bearRef.current, bearDuration / 2, { y: '100%' })
}
}
const onChange = () => {
if (checked) return
setChecked(true)
}
useEffect(() => {
const grabBearTL = () => {
/**
* Different height translations for the bear elements
*
* Paw will go to scaleX 0.8
* Arm scaleX goes down to 0.7
* Arm wrap translates to 50% or 50px
* Bear goes 100% -> 40% -> 0
*/
let bearTranslation
if (count > armLimit && count < headLimit) {
bearTranslation = '40%'
} else if (count >= headLimit) {
bearTranslation = '0%'
}
const onComplete = () => {
setChecked(false)
setCount(count + 1)
}
let onBearComplete = () => {}
if (Math.random() > 0.5 && count > angerLimit)
onBearComplete = () => {
SOUNDS.GROAN.play()
set(swearRef.current, { display: 'block' })
}
const base = armDuration + armDuration + pawDuration
const preDelay = Math.random()
const delay = count > armLimit ? base + bearDuration + preDelay : base
const bearTL = timeline({ delay: Math.random(), onComplete })
bearTL
.add(
count > armLimit
? to(bearRef.current, {
duration: bearDuration,
onComplete: onBearComplete,
y: bearTranslation,
})
: () => {}
)
.to(
armWrapRef.current,
{ x: 50, duration: armDuration },
count > armLimit ? preDelay : 0
)
.to(armRef.current, { scaleX: 0.7, duration: armDuration })
.to(pawRef.current, {
duration: pawDuration,
scaleX: 0.8,
onComplete: () => set(swearRef.current, { display: 'none' }),
})
.to(
bgRef.current,
{
onStart: () => {
SOUNDS.OFF.play()
},
duration: checkboxDuration,
backgroundColor: '#aaa',
},
delay
)
.to(
indicatorRef.current,
{ duration: checkboxDuration, x: '0%' },
delay
)
.to(pawRef.current, { duration: pawDuration, scaleX: 0 }, delay)
.to(
armRef.current,
{ duration: pawDuration, scaleX: 1 },
delay + pawDuration
)
.to(
armWrapRef.current,
{ duration: armDuration, x: 0 },
delay + pawDuration
)
.to(
bearRef.current,
{ duration: bearDuration, y: '100%' },
delay + pawDuration
)
return bearTL
}
const showTimeline = () => {
timeline({
onStart: () => SOUNDS.ON.play(),
})
.to(
bgRef.current,
{ duration: checkboxDuration, backgroundColor: '#2eec71' },
0
)
.to(indicatorRef.current, { duration: checkboxDuration, x: '100%' }, 0)
.add(grabBearTL(), checkboxDuration)
}
if (checked) showTimeline()
}, [checked, count])
return (
<Fragment>
<div className="bear__wrap">
<div ref={swearRef} className="bear__swear">
#@$%*!
</div>
<svg
ref={bearRef}
className="bear"
viewBox="0 0 284.94574 359.73706"
preserveAspectRatio="xMinYMin">
<g id="layer1" transform="translate(-7.5271369,-761.38595)">
<g
id="g5691"
transform="matrix(1.2335313,0,0,1.2335313,-35.029693,-212.83637)">
<path
id="path4372"
d="M 263.90933,1081.4151 A 113.96792,96.862576 0 0 0 149.99132,985.71456 113.96792,96.862576 0 0 0 36.090664,1081.4151 l 227.818666,0 z"
style={{ fill: '#784421', fillOpacity: 1 }}
/>
<path
id="path5634"
d="m 250.42825,903.36218 c 2e-5,66.27108 -44.75411,114.99442 -102.42825,114.99442 -57.674143,0 -98.428271,-48.72334 -98.428251,-114.99442 4e-6,-66.27106 40.754125,-92.99437 98.428251,-92.99437 57.67413,0 102.42825,26.72331 102.42825,92.99437 z"
style={{ fill: '#784421', fillOpacity: 1 }}
/>
<path
id="path5639"
d="m 217,972.86218 c 2e-5,21.53911 -30.44462,42.00002 -68,42.00002 -37.55538,0 -66.000019,-20.46091 -66,-42.00002 0,-21.53911 28.44464,-36 66,-36 37.55536,0 68,14.46089 68,36 z"
style={{ fill: '#e9c6af', illOpacity: 1 }}
/>
<path
id="path5636"
d="m 181.5,944.36218 c 0,8.28427 -20.59974,26.5 -32.75,26.5 -12.15026,0 -34.75,-18.21573 -34.75,-26.5 0,-8.28427 22.59974,-13.5 34.75,-13.5 12.15026,0 32.75,5.21573 32.75,13.5 z"
style={{ fill: '#000000', fillOpacity: 1 }}
/>
<g id="g5681">
<ellipse
style={{ fill: '#784421', fillOpacity: 1 }}
id="path5657"
cx="69"
cy="823.07269"
rx="34.5"
ry="33.289474"
/>
<path
style={{ fill: '#e9c6af', fillOpacity: 1 }}
d="M 69,47.310547 A 24.25,23.399124 0 0 0 44.75,70.710938 24.25,23.399124 0 0 0 64.720703,93.720703 c 0.276316,-0.40734 0.503874,-0.867778 0.787109,-1.267578 1.70087,-2.400855 3.527087,-4.666237 5.470704,-6.798828 1.943616,-2.132591 4.004963,-4.133318 6.179687,-6.003906 2.174725,-1.870589 4.461274,-3.611714 6.855469,-5.226563 2.394195,-1.614848 4.896019,-3.10338 7.498047,-4.46875 0.539935,-0.283322 1.133058,-0.500695 1.68164,-0.773437 A 24.25,23.399124 0 0 0 69,47.310547 Z"
id="ellipse5659"
transform="translate(0,752.36216)"
/>
</g>
<g transform="matrix(-1,0,0,1,300,0)" id="g5685">
<ellipse
ry="33.289474"
rx="34.5"
cy="823.07269"
cx="69"
id="ellipse5687"
style={{ fill: '#784421', illOpacity: 1 }}
/>
<path
transform="translate(0,752.36216)"
id="path5689"
d="M 69,47.310547 A 24.25,23.399124 0 0 0 44.75,70.710938 24.25,23.399124 0 0 0 64.720703,93.720703 c 0.276316,-0.40734 0.503874,-0.867778 0.787109,-1.267578 1.70087,-2.400855 3.527087,-4.666237 5.470704,-6.798828 1.943616,-2.132591 4.004963,-4.133318 6.179687,-6.003906 2.174725,-1.870589 4.461274,-3.611714 6.855469,-5.226563 2.394195,-1.614848 4.896019,-3.10338 7.498047,-4.46875 0.539935,-0.283322 1.133058,-0.500695 1.68164,-0.773437 A 24.25,23.399124 0 0 0 69,47.310547 Z"
style={{ fill: '#e9c6af', fillOpacity: 1 }}
/>
</g>
<ellipse
ry="9.6790915"
rx="9.2701159"
cy="900.38916"
cx="105.83063"
id="path4368"
style={{ fill: '#000000', fillOpacity: 1 }}
/>
<ellipse
style={{ fill: '#000000', fillOpacity: 1 }}
id="ellipse4370"
cx="186.89894"
cy="900.38916"
rx="9.2701159"
ry="9.6790915"
/>
{count >= angerLimit && (
<Fragment>
<path
id="path4396"
d="m 92.05833,865.4614 39.42665,22.76299"
style={{
stroke: '#000000',
strokeWidth: 4.86408424,
strokeLinecap: 'round',
strokeLinejoin: 'round',
strokeMiterlimit: 4,
strokeOpacity: 1,
}}
/>
<path
style={{
stroke: '#000000',
strokeWidth: 4.86408424,
strokeLinecap: 'round',
strokeLinejoin: 'round',
strokeMiterlimit: 4,
strokeOpacity: 1,
}}
d="m 202.82482,865.4614 -39.42664,22.76299"
id="path4400"
/>
</Fragment>
)}
</g>
</g>
</svg>
</div>
<div ref={armWrapRef} className="bear__arm-wrap">
<svg
ref={armRef}
className="bear__arm"
viewBox="0 0 250.00001 99.999997"
preserveAspectRatio="xMinYMin">
<g transform="translate(868.57141,-900.93359)" id="layer1">
<path
style={{ fill: '#784421', fillOpacity: 1 }}
d="m -619.43416,945.05124 c 4.18776,73.01076 -78.25474,53.24342 -150.21568,52.94118 -82.38711,-0.34602 -98.92158,-19.44459 -98.92157,-47.05883 0,-27.61424 4.78794,-42.54902 73.82353,-42.54902 69.03559,0 171.43607,-30.93764 175.31372,36.66667 z"
id="path4971"
/>
<ellipse
style={{ fill: '#e9c6af', fillOpacity: 1 }}
id="path4974"
cx="-683.02264"
cy="950.98572"
rx="29.910826"
ry="29.414362"
/>
</g>
</svg>
</div>
<div ref={pawRef} className="bear__paw" />
<div className="mask" />
<div className="checkbox" onMouseOver={onHover} onMouseOut={offHover}>
<input type="checkbox" onChange={onChange} checked={checked} />
<div ref={bgRef} className="checkbox__bg" />
<div ref={indicatorRef} className="checkbox__indicator" />
</div>
</Fragment>
)
}
render(<App />, rootNode)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.5.0/gsap.min.js"></script>
*
box-sizing border-box
background #947cb0
body
align-items center
display flex
justify-content center
font-family 'Arial', sans-serif
min-height 100vh
padding 0
margin 0
overflow hidden
.mask
position fixed
top 50%
left 0
right 0
bottom 0
background #947cb0
.bear
width 100%
background transparent
transform translate(0, 100%)
&__swear
display none
position absolute
left 105%
top 0
background #fff
font-weight bolder
padding 10px
border-radius 8px
&:before
content ''
background #fff
position absolute
top 90%
right 70%
height 30px
width 30px
clip-path polygon(0 100%, 100% 0, 50% 0)
-webkit-clip-path polygon(0 100%, 100% 0, 50% 0)
&__wrap
width 100px
left 50%
position absolute
top 50%
transform translate(-15%, -50%) rotate(5deg) translate(0, -75%)
background transparent
&__arm-wrap
background transparent
position fixed
height 30px
width 90px
z-index 4
top 50%
left 50%
transform translate(0, -50%) rotate(0deg)
&__arm
background transparent
transform-origin left
position absolute
height 100%
width 100%
top 50%
left 50%
transform translate(-35%, -50%) scaleX(1)
&__paw
background #784421
border-radius 100%
position fixed
height 30px
width 30px
z-index 10
top 50%
left 50%
transform-origin right
transform translate(80px, -15px) scaleX(0)
.checkbox
border-radius 50px
height 100px
position fixed
width 200px
z-index 5
top 50%
left 50%
transform translate(-50%, -50%)
[type='checkbox']
cursor pointer
border-radius 50px
position absolute
outline 0
top 0
right 0
bottom 0
left 0
opacity 0
z-index 10
height 100%
width 100%
margin 0
&__bg
background #aaa
border-radius 50px
height 100%
width 100%
z-index 10
&__indicator
background transparent
height 100%
width 50%
border-radius 100%
position absolute
top 0
left 0
&:after
content ''
border-radius 100%
height 85%
width 85%
background #fff
position absolute
top 50%
left 50%
transform translate(-50%, -50%)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment