Revisiting one of my favorite pens to update the React side of it and add sound
A Pen by Christian Anderson on CodePen.
Revisiting one of my favorite pens to update the React side of it and add sound
A Pen by Christian Anderson on CodePen.
| #app |
| 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%) |