#app
View Compiled
*
box-sizing border-box
body
margin 0
padding 2rem
background #222
color #fff
display flex
align-items center
justify-content center
font-family -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif
min-height 100vh
#app
min-height 100vh
display flex
align-items center
justify-content center
flex-direction column
width 50vmin
min-width 200px
transform-style preserve-3d
perspective 1000px
perspective-origin 50% 25%
.motion-element
height 40px
width 40px
position absolute
top 0%
left 0%
offset-path path(var(--path))
animation travel 2s infinite var(--animation-direction, normal) linear
transform-style var(--transform-style, 'none')
transform translate3d(0, 0, 20px)
&__side
background rgba(128,191,255,0.1)
border 2px #80bfff solid
height 100%
position absolute
width 100%
&:nth-of-type(1)
transform translate3d(0, 0, 20px)
&:nth-of-type(2)
transform translate3d(0, 0, -20px)
&:nth-of-type(3)
transform rotateX(90deg) translate3d(0, 0, -20px)
&:nth-of-type(4)
transform rotateX(90deg) translate3d(0, 0, 20px)
&:nth-of-type(5)
transform rotateY(90deg) translate3d(0, 0, 20px)
&:nth-of-type(6)
transform rotateY(-90deg) translate3d(0, 0, 20px)
.container
height 50vmin
width 50vmin
min-width 200px
min-height 200px
border 4px solid hsla(0, 0%, 100%, 0.5)
overflow hidden
position relative
resize both
margin-bottom 2rem
transform-origin bottom center
transform-style preserve-3d
transform rotateX(calc(var(--rotation, 0) * 1deg))
button
padding 8px 16px
details
width 100%
summary
margin-bottom 1rem
padding 1rem 0
path
fill none
stroke 'hsl(%s, 100%, 50%)' % var(--hue, 260)
stroke-width 4px
transition stroke .25s ease
svg
height 100%
width 100%
@keyframes travel
from
offset-distance 0%
to
offset-distance 100%
label
display block
margin-bottom 0.5rem
font-weight bold
input
display block
[type=text]
[type=number]
margin 0
padding 8px 16px
width 100%
a
color 'hsl(%s, 100%, 50%)' % var(--hue)
p
line-height 1.5
text-align left
width 100%
form
display grid
grid-gap 20px
.form-field
margin-bottom 1.25rem
.form-field--grid
display grid
grid-template-columns auto 1fr
grid-template-rows auto auto
grid-gap 20px 10px
View Compiled
const {
React: { Fragment, useEffect, useReducer, useRef },
ReactDOM: { render },
} = window
const PATH =
'M10.362 18.996s-6.046 21.453 1.47 25.329c10.158 5.238 18.033-21.308 29.039-18.23 13.125 3.672 18.325 36.55 18.325 36.55l12.031-47.544'
const INITIAL_STATE = {
alternate: false,
path: PATH,
svg: true,
height: 79.375,
width: 79.375,
threeD: false,
}
const formReducer = (state, action) => {
switch (action.type) {
case 'UPDATE':
return { ...state, [action.name]: action.value }
case 'DROP':
return { ...state, ...action.data }
default:
return state
}
}
const App = () => {
const containerRef = useRef(null)
const elementRef = useRef(null)
const pathRef = useRef(null)
const svgRef = useRef(null)
const motionPathRef = useRef(null)
const [state, dispatch] = useReducer(formReducer, INITIAL_STATE)
const { alternate, path, svg, threeD, width, height } = state
const onFileDrop = e => {
e.preventDefault()
const file = e.dataTransfer.files[0]
if (
file.type === 'image/svg+xml' ||
file.name.slice(file.name.length - 4) === '.svg'
) {
// process the file.
const reader = new FileReader()
reader.onloadend = response => {
try {
// file.target.result is the SVG markup we want to use.
const wrapper = document.createElement('div')
wrapper.innerHTML = response.target.result
const svg = wrapper.querySelector('svg')
const path = wrapper.querySelector('path')
const viewBox = svg.getAttribute('viewBox').split(' ') // 0 0 x2 y2
const pathString = path.getAttribute('d')
dispatch({
type: 'DROP',
data: {
path: pathString,
width: viewBox[2],
height: viewBox[3],
},
})
} catch (e) {
throw Error('Something went wrong', e)
}
}
reader.readAsText(file)
}
}
const prevent = e => e.preventDefault()
const updateField = e =>
dispatch({
type: 'UPDATE',
name: e.target.name,
value: e.target.type === 'checkbox' ? e.target.checked : e.target.value,
})
useEffect(() => {
if (containerRef.current) {
const containerRefObserver = new ResizeObserver(entries => {
if (motionPathRef.current) {
const newPath = motionPathRef.current.generatePath(containerRef.current.offsetWidth, containerRef.current.offsetHeight)
containerRef.current.style.setProperty('--path', `"${newPath}"`)
pathRef.current.setAttribute('d', newPath)
}
})
containerRefObserver.observe(containerRef.current)
}
}, [])
useEffect(() => {
if (containerRef.current && elementRef.current) {
// Set up the initial responsive motion path
motionPathRef.current = new Meanderer({
path,
height,
width,
})
const newPath = motionPathRef.current.generatePath(containerRef.current.offsetWidth, containerRef.current.offsetHeight)
containerRef.current.style.setProperty('--path', `"${newPath}"`)
pathRef.current.setAttribute('d', newPath)
}
}, [path, width, height])
useEffect(() => {
document.body.addEventListener('dragover', prevent)
document.body.addEventListener('drop', onFileDrop)
return () => {
document.body.removeEventListener('dragover', prevent)
document.body.removeEventListener('drop', onFileDrop)
}
}, [])
const hue = Math.random() * 360
return (
<Fragment>
<div
ref={containerRef}
className="container"
style={{
'--rotation': threeD ? 75 : 0,
overflow: threeD ? 'visible' : 'hidden',
}}>
<svg
{...(!svg && { hidden: true })}
ref={svgRef}
style={{
'--hue': hue,
}}>
<path ref={pathRef}></path>
</svg>
<div
ref={elementRef}
style={{
'--animation-direction': alternate ? 'alternate' : 'normal',
'--transform-style': threeD ? 'preserve-3d' : 'none',
}}
className="motion-element">
<div className="motion-element__side"></div>
<div className="motion-element__side"></div>
<div className="motion-element__side"></div>
<div className="motion-element__side"></div>
<div className="motion-element__side"></div>
<div className="motion-element__side"></div>
</div>
</div>
<p
style={{
'--hue': hue,
}}>
Drag and drop an optimized SVG file onto the page that contains a path.
Clean up your SVG first with{' '}
<a
href="https://jakearchibald.github.io/svgomg/"
target="_blank"
rel="noreferrer noopener">
SVGOMG
</a>
. Alternatively, manually enter path info into the configuration form
below.
</p>
<p>
Resize the viewport and see your motion path scale!{' '}
<span aria-label="TADA!" role="img">
🎉
</span>
</p>
<details>
<summary>Path configuration</summary>
<form onDrop={onFileDrop}>
<section className="form-field">
<label htmlFor="path">Path</label>
<input
id="path"
type="text"
name="path"
value={path}
onChange={updateField}
/>
</section>
<section className="form-field">
<label htmlFor="width">Initial Width (viewBox x2)</label>
<input
id="with"
type="number"
name="width"
value={width}
onChange={updateField}
/>
</section>
<section className="form-field">
<label htmlFor="height">Initial Height (viewBox y2)</label>
<input
id="height"
type="number"
name="height"
value={height}
onChange={updateField}
/>
</section>
<section className="form-field form-field--grid">
<label htmlFor="svg">Show SVG path?</label>
<input
id="svg"
type="checkbox"
name="svg"
checked={svg}
onChange={updateField}
/>
<label htmlFor="alternate">Alternate direction?</label>
<input
id="alternate"
type="checkbox"
name="alternate"
checked={alternate}
onChange={updateField}
/>
<label htmlFor="threeD">See path in 3D?</label>
<input
id="threeD"
type="checkbox"
name="threeD"
checked={threeD}
onChange={updateField}
/>
</section>
<section className="form-field form-field--grid">
<button
onClick={e => {
e.preventDefault()
containerRef.current.removeAttribute('style')
}}>
Reset container size
</button>
</section>
</form>
</details>
</Fragment>
)
}
render(<App />, document.getElementById('app'))
View Compiled
This Pen doesn't use any external CSS resources.