#app
View Compiled
*
*:after
*:before
box-sizing border-box
transform-style preserve-3d
:root
--spin-speed 0
--border-width 1.8vmin
--depth 20vmin
--stack-height 3vmin
--scene-size 16vmin
--ring-size calc(var(--scene-size) * 0.6)
--plane radial-gradient(hsla(0, 0%, 0%, 0.1) 50%, transparent 65%)
--ring-shadow hsla(0, 0%, 0%, 0.5)
--hue-one 320
--hue-two 210
--blur 10px
--speed 1.5s
--bg hsl(0, 0%, 98%)
--ring-filter brightness(1) drop-shadow(0 0 0 var(--accent))
@media(prefers-color-scheme dark)
--bg hsl(0, 0%, 15%)
--ring-shadow 'hsla(%s, 100%, 50%, 1)' % var(--hue-one)
--plane 'radial-gradient(hsla(%s, 90%, 60%, 0.1) 50%, transparent 65%)' % var(--hue-one)
--ring-filter brightness(1.75) drop-shadow(0 0 1vmin var(--accent))
body
background var(--bg)
min-height 100vh
display grid
place-items center
overflow hidden
.scene
height var(--scene-size)
width var(--scene-size)
animation step-up var(--speed) infinite linear
&__shadow
position absolute
top 0
left 100%
height 100%
width 100%
opacity var(--opacity)
animation fade-in var(--speed) infinite linear
background var(--plane)
transform scale(1.25)
filter blur(var(--blur))
.flipper
height var(--scene-size)
width var(--scene-size)
animation flip-flop calc(var(--speed) * 2) infinite steps(1)
.plane
height 100%
width 100%
transform translate3d(0, 0, var(--depth))
position relative
&__shadow
content ''
height 100%
width 100%
position absolute
top 50%
left 50%
background var(--plane)
filter blur(var(--blur))
transform translate(-50%, -50%) scale(1.25)
animation fade-out var(--speed) infinite linear
.scene__shadow
.plane__shadow
&:after
content ''
height var(--ring-size)
width var(--ring-size)
position absolute
top 50%
left 50%
border var(--border-width) solid var(--ring-shadow)
border-radius 50%
transform translate(-50%, -50%) scale(0.8)
.container
transform translateZ(100vmin) rotateX(-12deg) rotateY(0deg) rotateX(90deg) translateZ(calc(var(--depth) * -1.5)) rotate(0deg)
animation rotate-scene calc(var(--speed) * var(--spin-speed)) infinite linear
.ring
--origin-z calc(var(--stack-height) - (var(--stack-height) / var(--ring-count)) * var(--index))
--destination-z calc(((var(--depth) + var(--origin-z)) - (var(--stack-height) - var(--origin-z))) * -1)
--hue var(--hue-one)
--accent 'hsl(%s 100% 55%)' % var(--hue)
--ring-filter brightness(1) drop-shadow(0 0 0 var(--accent))
height var(--ring-size)
width var(--ring-size)
border-radius 50%
border var(--border-width) solid var(--accent)
position absolute
top 50%
left 50%
transform-origin calc(100% + (var(--scene-size) * 0.2)) 50%
animation-name var(--name)
animation-duration var(--speed)
animation-iteration-count infinite
animation-timing-function cubic-bezier(.25,0,1,1)
filter var(--ring-filter)
&:nth-of-type(odd)
--hue var(--hue-two)
@media(prefers-color-scheme dark)
--ring-filter brightness(2) drop-shadow(0 0 calc(var(--border-width) * 0.5) var(--accent))
View Compiled
import React from 'https://cdn.skypack.dev/react'
import { render } from 'https://cdn.skypack.dev/react-dom'
import { GUI } from 'https://cdn.skypack.dev/dat.gui'
const ROOT_NODE = document.querySelector('#app')
const DEFAULT_RING_COUNT = 39
const DEFAULT_ANIMATION_WINDOW = 50
const Slinky = () => {
const styleRef = React.useRef(null)
const [animationWindow, setAnimationWindow] = React.useState(
DEFAULT_ANIMATION_WINDOW
)
const [flip, setFlip] = React.useState(true)
const [count, setCount] = React.useState(DEFAULT_RING_COUNT)
React.useEffect(() => {
const CONFIG = {
animationWindow,
count,
'hue-one': 210,
'hue-two': 320,
'border-width': 1.2,
depth: 30,
'stack-height': 6,
'scene-size': 30,
'spin-speed': 50,
speed: 1.15,
'flip-flop': true,
}
const CTRL = new GUI({
width: 300,
})
const UPDATE = () => {
Object.keys(CONFIG)
.filter(key => key !== 'animationWindow' && key !== 'count')
.forEach(key => {
document.documentElement.style.setProperty(
`--${key}`,
`${CONFIG[key]}${
key.indexOf('hue') === -1 && key.indexOf('spin-speed') === -1
? 'vmin'
: ''
}`
)
})
document.documentElement.style.setProperty(
'--flip-flop',
CONFIG['flip-flop'] ? 1 : 0
)
document.documentElement.style.setProperty(
'--speed',
`${CONFIG['speed']}s`
)
}
const RINGS = CTRL.addFolder('Rings')
RINGS.add(CONFIG, 'animationWindow', 5, 50, 1)
.name('Animation Window')
.onFinishChange(setAnimationWindow)
RINGS.add(CONFIG, 'count', 5, 50, 1)
.name('Ring Count')
.onFinishChange(setCount)
RINGS.add(CONFIG, 'border-width', 0.1, 4, 0.1)
.name('Width (vmin)')
.onFinishChange(UPDATE)
RINGS.add(CONFIG, 'scene-size', 5, 30, 1)
.name('Size (vmin)')
.onFinishChange(UPDATE)
const STACK = CTRL.addFolder('Stack')
STACK.add(CONFIG, 'stack-height', 1, 20, 1)
.name('Height (vmin)')
.onFinishChange(UPDATE)
STACK.add(CONFIG, 'depth', 0, 40, 1)
.name('Drop (vmin)')
.onFinishChange(UPDATE)
STACK.add(CONFIG, 'spin-speed', 0, 60, 1)
.name('Spin Speed (s)')
.onFinishChange(UPDATE)
STACK.add(CONFIG, 'speed', 0.2, 10, 0.1)
.name('Slink Speed (s)')
.onFinishChange(UPDATE)
const COLORS = CTRL.addFolder('Colors')
COLORS.add(CONFIG, 'hue-one', 0, 360, 1)
.name('Hue One')
.onFinishChange(UPDATE)
COLORS.add(CONFIG, 'hue-two', 0, 360, 1)
.name('Hue Two')
.onFinishChange(UPDATE)
CTRL.add(CONFIG, 'flip-flop')
.name('Flip Flop?')
.onChange(flip => {
UPDATE()
setFlip(flip)
})
UPDATE()
}, [])
React.useEffect(() => {
if (styleRef.current) styleRef.current.remove()
const STYLE = document.createElement('style')
document.head.appendChild(STYLE)
const ANIMATION_STEP = animationWindow / count
// Loop over the count and generate the keyframes and insert them at each index.
for (let i = 0; i < count; i++) {
const START = ANIMATION_STEP * (i + 1)
const SLINKS = `
0%, ${START}% {
transform: translate3d(-50%, -50%, var(--origin-z)) translateZ(0) rotateY(0deg);
}
${START + animationWindow * 0.75}% {
transform: translate3d(-50%, -50%, var(--origin-z)) translateZ(0) rotateY(180deg);
}
${START + animationWindow}%, 100% {
transform: translate3d(-50%, -50%, var(--origin-z)) translateZ(var(--destination-z)) rotateY(180deg);
}
`
STYLE.sheet.insertRule(
`
@-webkit-keyframes slink-${i} {
${SLINKS}
}
`
)
STYLE.sheet.insertRule(
`
@keyframes slink-${i} {
${SLINKS}
}
`
)
}
const FADE_IN = `
${animationWindow}%, 0% {
opacity: 0;
}
${ANIMATION_STEP + animationWindow}%, 100% {
opacity: 1;
}
`
STYLE.sheet.insertRule(
`
@-webkit-keyframes fade-in {
${FADE_IN}
}
`
)
STYLE.sheet.insertRule(
`
@keyframes fade-in {
${FADE_IN}
}
`
)
const FADE_OUT = `
${ANIMATION_STEP * (count + 1)}%, 0% {
opacity: 1;
}
${ANIMATION_STEP * (count + 1) + animationWindow * 0.375}%, 100% {
opacity: 0;
}
`
STYLE.sheet.insertRule(
`
@-webkit-keyframes fade-out {
${FADE_OUT}
}
`
)
STYLE.sheet.insertRule(
`
@keyframes fade-out {
${FADE_OUT}
}
`
)
const STEP = `
0% {
transform: translate(${flip ? -50 : 0}%, 0) translateZ(0);
}
100% {
transform: translate(${flip ? -50 : -100}%, 0) translateZ(var(--depth));
}
`
STYLE.sheet.insertRule(`
@-webkit-keyframes step-up {
${STEP}
}
`)
STYLE.sheet.insertRule(`
@keyframes step-up {
${STEP}
}
`)
const ROTATE = `
to {
transform: translateZ(100vmin) rotateX(-12deg) rotateY(0deg) rotateX(90deg) translateZ(calc(var(--depth) * -1.5)) rotate(360deg);
}
`
STYLE.sheet.insertRule(`
@-webkit-keyframes rotate-scene {
${ROTATE}
}
`)
STYLE.sheet.insertRule(`
@keyframes rotate-scene {
${ROTATE}
}
`)
if (flip) {
const FLIP = `
0% {
transform: rotate(0deg);
}
50% {
transform: rotate(180deg);
}
100% {
transform: rotate(360deg);
}
`
STYLE.sheet.insertRule(`
@-webkit-keyframes flip-flop {
${FLIP}
}
`)
STYLE.sheet.insertRule(`
@keyframes flip-flop {
${FLIP}
}
`)
}
styleRef.current = STYLE
}, [count, animationWindow, flip])
return (
<div className="container" key={new Date().getTime()}>
<div className="flipper">
<div className="scene">
<div className="scene__shadow"></div>
<div className="plane" style={{ '--ring-count': count }}>
<div className="plane__shadow"></div>
{new Array(count).fill().map((_, index) => (
<div
className="ring"
key={`ring--${index}`}
style={{ '--index': index, '--name': `slink-${index}` }}></div>
))}
</div>
</div>
</div>
</div>
)
}
render(<Slinky />, ROOT_NODE)
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.