<div class="ghost__drag">
<svg xmlns="http://www.w3.org/2000/svg" class="ghost" viewBox="0 0 87.69 133.803">
<defs>
<clipPath id="bodyClip">
<path class="ghost__tails" d="M.08 0A.079.079 0 000 .08v122.084l.044-.002c5.322 0 5.622 11.62 10.935 11.64h.03c5.313-.02 5.613-11.64 10.935-11.64 5.323 0 5.623 11.62 10.936 11.64h.03c5.313-.02 5.612-11.64 10.935-11.64s5.622 11.62 10.936 11.64h.029c5.313-.02 5.613-11.64 10.936-11.64 5.323 0 5.622 11.62 10.936 11.64h.029c5.313-.02 5.613-11.64 10.936-11.64.014 0 .028 0 .043.002V.08a.079.079 0 00-.08-.08H43.846z" />
<path class="ghost__tails--two" d="M.08 0A.079.079 0 000 .08v122.084l.044-.002c5.322 0 5.622 11.62 10.935 11.64h.03c5.313-.02 5.613-11.64 10.935-11.64 5.323 0 5.623 11.62 10.936 11.64h.03c5.313-.02 5.612-11.64 10.935-11.64s5.622 11.62 10.936 11.64h.029c5.313-.02 5.613-11.64 10.936-11.64 5.323 0 5.622 11.62 10.936 11.64h.029c5.313-.02 5.613-11.64 10.936-11.64.014 0 .028 0 .043.002V.08a.079.079 0 00-.08-.08H43.846z" />
</clipPath>
<clipPath id="mouthClip">
<circle class="ghost__mouth-clip" cx="43.845000" cy="50.5" r="10"></circle>
</clipPath>
<clipPath id="shookClip">
<ellipse cx="43.7964" cy="51.6723" rx="4" ry="4"/>
</clipPath>
</defs>
<g class="ghost__ghost">
<g class="ghost__body-wrapper" clip-path="url(#bodyClip)">
<path class="ghost__body" d="M43.845 0C19.555 0 0 16.028 0 49.742v81.416c0 .887.017 1.768.043 2.645h87.604c.027-.877.043-1.758.043-2.645V49.742C87.69 16.028 68.135 0 43.845 0z"/>
</g>
<g class="ghost__features--shook">
<ellipse cx="43.7964" cy="51.6723" rx="4" ry="4" fill="black"/>
<g clip-path="url(#shookClip)">
<path class="ghost__tongue--shook" fill="red" d="M46.276 50.864a2.91 2.91 0 00-2.91 2.91 2.91 2.91 0 00.151.917 6.147 6.147 0 003.402-.81 6.147 6.147 0 001.833-1.633 2.91 2.91 0 00-2.476-1.384z" />
</g>
<path d="M13.0777 42.6763L21.8369 45.7902L13.0777 48.6123" fill="none" stroke="black" stroke-width="2" stroke-linejoin="round"/>
<path d="M74.8678 48.4368L65.8635 46.1247L74.3328 42.525" fill="none" stroke="black" stroke-width="2" stroke-linejoin="round"/>
</g>
<g class="ghost__eyes">
<g class="ghost__eye" transform="translate(-14.366 -64.075)">
<circle r="5.292" cy="109.431" cx="31.751"/>
<circle class="ghost__pupil" cx="29.751" cy="107.431" r="1.323" />
</g>
<g class="ghost__eye" transform="translate(38.553 -64.075)">
<circle cx="31.751" cy="109.431" r="5.292"/>
<circle class="ghost__pupil" r="1.323" cy="107.431" cx="29.751" />
</g>
</g>
<g class="ghost__cheeks" transform="translate(-14.526 -64.075)">
<circle cx="23.226" cy="117.689" r="2.646"/>
<circle r="2.646" cy="117.689" cx="93.517"/>
</g>
<g class="ghost__mouth-group" clip-path="url(#mouthClip)">
<path class="ghost__mouth" d="M49.992 48.558a6.147 6.147 0 01-3.073 5.323 6.147 6.147 0 01-6.147 0 6.147 6.147 0 01-3.074-5.323h6.147z"/>
<path class="ghost__tongue" d="M46.276 50.864a2.91 2.91 0 00-2.91 2.91 2.91 2.91 0 00.151.917 6.147 6.147 0 003.402-.81 6.147 6.147 0 001.833-1.633 2.91 2.91 0 00-2.476-1.384z" />
</g>
<g class="ghost__boo-features">
<path class="ghost__teeth" d="M39.235 51.373l1.537-2.116 1.536 2.116 1.537-2.116 1.537 2.116 1.537-2.116 1.537 2.116 1.536-2.815H37.698z" />
<path class="ghost__brow" d="M22.944 39.021c0 1.297-1.06 2.495-2.78 3.144-1.72.648-3.838.648-5.558 0-1.72-.649-2.78-1.847-2.78-3.144" stroke-width=".756" stroke-linecap="round" stroke-linejoin="round"/>
<path class="ghost__brow" d="M75.863 39.02c0 1.298-1.059 2.497-2.779 3.146-1.72.65-3.839.65-5.559 0-1.72-.649-2.779-1.848-2.779-3.147" stroke-width=".757" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</g>
</svg>
</div>
View Compiled
*
box-sizing border-box
body
display flex
align-items center
justify-content center
background 'hsl(%s, 0%, %s)' % (var(--hue, 0) calc(var(--light, 6) * 1%))
min-height 100vh
overflow hidden
h1
margin 0
position fixed
bottom 1rem
right 1rem
color hsl(280, 50%, 30%)
font-family sans-serif
font-size 2rem
user-select none
.ghost
height 25vmin
overflow visible !important
filter drop-shadow(0 0 4vmin var(--glow, white));
transition filter 0.2s
&__drag
display none
// &__ghost
// display none
&__cheeks
fill hsl(0, 80%, 85%)
&__pupil
fill hsl(0, 0%, 100%)
&__body
fill hsla(0, 0%, 100%, 1)
&__teeth
fill hsl(0, 0%, 100%)
&__brow
fill hsla(0, 0%, 100%, 1)
stroke hsl(0, 0%, 10%)
&__tongue
fill hsl(0, 80%, 50%)
&__teeth
&__brow
stroke none
// display none
.sweet-bowl
&__opening
fill hsl(30, 80%, 30%)
&__body
fill hsl(30, 80%, 50%)
&__groove
stroke hsl(35, 80%, 40%)
&__eye
&__mouth
fill hsl(0, 0%, 10%)
&__tongue
fill hsl(0, 80%, 50%)
.sweet
display none
&--lolly
path
fill hsl(0, 0%, 95%)
circle
fill 'hsl(%s, 70%, 50%)' % var(--hue, 0)
&--sweet
path
fill 'hsl(%s, 70%, 20%)' % var(--hue, 0)
circle
fill 'hsl(%s, 70%, 50%)' % var(--hue, 0)
View Compiled
const {
gsap: {
set,
to,
timeline,
registerPlugin,
utils: { mapRange, clamp, random },
},
Draggable,
InertiaPlugin,
} = window
registerPlugin(InertiaPlugin)
set(document.documentElement, {
'--hue': random(0, 359)
})
let shakeReset
let comparator
let INERT_COUNT = 0
let SHAKING = false
const BOUNDS = 800
const SHAKE_BOUND = 200
const ROTATE = 25
const SHAKE_THRESHOLD = 6
const SHAKE_TIMEOUT = 500
const BOO_TIMER = 2000
const TIMING = {
TAILS: 1,
}
set('.ghost__tails--two', { xPercent: -100 })
// Move pupils across 2 and down 2 to center: x: '+=2', y: '+=2'
set(['.ghost__eyes', '.ghost__mouth-clip', '.ghost__mouth-group'], {
transformOrigin: '50% 50%',
})
set('.ghost__pupil', { x: 0, y: 0, transformOrigin: '50% 50%' })
set(['.ghost__features--shook', '.ghost__boo-features'], { display: 'none' })
set('.ghost__tongue--shook', { yPercent: 50 })
set('.ghost__teeth', { scale: 1.15, transformOrigin: '50% 50%'})
const AUDIO = {
PAC: new Audio('https://assets.codepen.io/605876/pac-out.mp3'),
BOO: new Audio('https://assets.codepen.io/605876/boo.mp3')
}
AUDIO.BOO.volume = 0.5
const BLINK = () => {
const delay = random(2, 6)
timeline().to('.ghost__eyes', {
delay,
onComplete: () => BLINK(),
scaleY: 0.1,
repeat: 3,
yoyo: true,
duration: 0.05,
})
}
let tailsTl
const checkReverseLoop = (tl = tailsTl) => {
if (tl.reversed() && tl.totalTime() <= tl.duration()) {
tl.totalTime(tl.totalTime() + tl.duration() * 100, true) //just shoot it out 100 cycles forward and suppress events
}
}
tailsTl = timeline({
repeat: -1,
ease: 'none',
onRepeat: checkReverseLoop,
})
.to(
'.ghost__tails',
{ duration: TIMING.TAILS, xPercent: 100, ease: 'none' },
0
)
.to(
'.ghost__tails--two',
{ duration: TIMING.TAILS, xPercent: 0, ease: 'none' },
0
)
BLINK()
let direction = 'left'
let ghosting = false
const GHOST_DRAG = document.querySelector('.ghost__drag')
let booTimer
GHOST_DRAG.addEventListener('dblclick', () => {
if (!ghosting) {
AUDIO.BOO.play()
set('.ghost__boo-features', { display: 'block' })
if (booTimer) clearTimeout(booTimer)
booTimer = setTimeout(() => {
set('.ghost__boo-features', { display: 'none' })
}, BOO_TIMER)
}
})
Draggable.create(GHOST_DRAG, {
type: 'x,y',
trigger: GHOST_DRAG,
inertia: true,
bounds: document.body,
dragResistance: 0.25,
onDrag: function() {
set(GHOST_DRAG, {
x: this.x,
y: this.y,
})
const INE_X = InertiaPlugin.getVelocity(GHOST_DRAG, 'x')
const MOM_X = clamp(-BOUNDS, BOUNDS, INE_X)
const rotation = mapRange(-BOUNDS, BOUNDS, ROTATE, -ROTATE, MOM_X)
to(GHOST_DRAG, {
rotation,
duration: 0.25,
})
to('.ghost__body-wrapper', {
skewX: rotation * 0.5,
duration: 0.25,
})
// Handle changing tails direction
const currentDirection = this.getDirection()
if (
(currentDirection !== direction && currentDirection === 'left') ||
currentDirection === 'right'
) {
direction = currentDirection
if (direction === 'right') {
tailsTl.reverse()
checkReverseLoop()
} else tailsTl.play()
}
if (
((INE_X < -SHAKE_BOUND || INE_X > SHAKE_BOUND) && !SHAKING) ||
(SHAKING && comparator(INE_X))
) {
SHAKING = true
INERT_COUNT += 1
if (INERT_COUNT > 2) {
set('.ghost__features--shook', { display: 'block' })
set(['.ghost__eyes', '.ghost__mouth-group', '.ghost__boo-features'], { display: 'none' })
}
// Need to create a way of mapping the opposite
if (INE_X < -SHAKE_BOUND) {
// The next value must be INE_X > SHAKE_BOUND
// Create a function
comparator = val => val > SHAKE_BOUND
} else {
comparator = val => val < -SHAKE_BOUND
}
if (shakeReset) clearTimeout(shakeReset)
shakeReset = setTimeout(() => {
INERT_COUNT = 0
SHAKING = false
shakeReset = null
}, SHAKE_TIMEOUT)
}
},
onDragEnd: () => {
set('.ghost__features--shook', { display: 'none' })
set(['.ghost__eyes', '.ghost__mouth-group'], { display: 'block' })
if (INERT_COUNT > SHAKE_THRESHOLD) {
timeline({
onStart: () => {
set('.ghost', {'--glow': '#2121DE' })
ghosting = true
AUDIO.PAC.play()
set('.ghost__boo-features', { display: 'none' })
set('.ghost__body', {
fill: '#2121DE',
})
},
})
.to(document.documentElement, {
'--light': 0,
})
.to(
tailsTl,
{
timeScale: 2,
},
0
)
set(['.ghost__tongue', '.ghost__cheeks'], { display: 'none' })
set(['.ghost__eyes', '.ghost__mouth'], { fill: 'yellow' })
set('.ghost__mouth-group', { rotation: 180 })
}
// RESET
to(GHOST_DRAG, {
duration: 0.25,
rotation: 0,
})
to('.ghost__body-wrapper', {
skewX: 0,
duration: 0.25,
})
},
})
AUDIO.PAC.addEventListener('ended', () => {
timeline({
onStart: () => {
set('.ghost', {'--glow': 'white' })
},
onComplete: () => {
ghosting = false
}
})
.to(
document.documentElement,
{
'--light': 10,
},
0
)
.to(
tailsTl,
{
timeScale: 1,
},
0
)
set('.ghost__body', {
fill: 'white',
})
set(['.ghost__tongue', '.ghost__cheeks'], { display: 'block' })
set(['.ghost__eyes', '.ghost__mouth'], { fill: 'black' })
set('.ghost__mouth-group', { rotation: 0 })
})
set('.ghost__drag', {
display: 'block'
})
View Compiled
This Pen doesn't use any external CSS resources.