#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

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/d3/5.15.0/d3.min.js
  4. https://unpkg.com/[email protected]/dist/meanderer.min.js