React-Spring-2 useTransition() update style, useGesture() drag, SVG display and useSpring() animation

The useTrasition() style props can be rendered only in an animated.div component.

The Enter and Leave prop only animates the useTransition() elements being added/removed to the DOM, both will be rendered when the element is .

//useTransition() leaves-> enter on useState() change
//exitBeforeEnter finishes the leave before the enter
const [mio, setMio] = useState(["another"])

const estilo = useTransition(mio, {
  from: {
    back: "transparent", long: "0%", lefto: "0%",
    color: "black", opacity: 0, width: "20%"
  },
  enter: [
    { back: "pink", color: "green", long: "90%", lefto: "10%", width: "100%", opacity: 1 },
    { back: "orange", color: "blue", long: "50%", lefto: "50%" },
    { back: "red", color: "red", long: "0%", lefto: "100%" }
  ],
  leave: [
    { width: "20%" ,opacity: 0 }
  ],
  exitBeforeEnter: true,
  config:{ duration: 1000 }
})

//We deconstruct and ...rest the custom style properties
//We get the translate--centered-text effect animating width/text-center 
<>
  {estilo(( {back, long, lefto, ...resto} , item) => (
  
    <animated.div className="centra d-flex align-items-center" >
      <animated.div 
        className="sfondo"
        style={{backgroundColor: back, width: long, left: lefto}}
      >
      </animated.div>

      <animated.div className="testo" style={resto}>
        {item}
      </animated.div>
    </animated.div>
  ))}
</>
useTransition() on a double absolute flex container

The update useTransition() styles the elements not updated during the animation but doesn't trigger if the array remains the same.

//update() if added styles after the leave{} any remaining elements
//Any new transitions need to be included in the from:{} even at default 0
const transitions = useTransition(testi, {
  from: { transform: 'perspective(600px) rotateX(0deg) rotateX(0deg)'},
  enter: { transform: 'perspective(600px) rotateX(180deg) translateX(0px)' },
  leave: { },
  update: [
    { color: 'pink' },
    { transform: "perspective(600px) rotateX(0deg) translateX(150px)" }
  ]  
})
useTransition() arrays changed on setTimeout() and update styling

The setTimeout() array updates will resolve sequentially, based on their timeout (2000 + 2000 + 1000), not their syntax order.

//The useEffect() triggers the animation onLoad()
//Even if rotateX(0) is the default, we declare it to Eenter animate

const [testi, setTesti] = useState([])

const transitions = useTransition(testi, {
  from: {
    innerHeight: 0,
    opacity: 0, color: 'orange', height: 0,
    transform: 'perspective(600px) rotateX(0deg)',
  },
  enter: [
    { opacity: 1, height: 80, innerHeight: 80, color: "green" },
    { transform: 'perspective(600px) rotateX(180deg)', color: 'yellow' },
    { transform: 'perspective(600px) rotateX(0deg)', color: "red" },
  ],
  leave: [
    { color: 'purple' }, 
    { innerHeight: 0 }, 
    { opacity: 0, height: 0 }
  ],
  update: { color: 'pink' },  
})

const reset = useCallback(() => {
  setTesti([])
  setTimeout(() => setTesti(['Apples', 'Kiwis']), 5000)
  setTimeout(() => setTesti(['Apples', 'Bananas', 'Kiwis']), 4000)
  setTimeout(() => setTesti(['Apples', 'Oranges', 'Kiwis']), 2000)
}, [])

//useEffect() triggers the animation once during the initial render
useEffect(() => {
  reset()
}, [])

//Flex is for the Y-align while block is to render the words
<div className='d-flex align-items-center' style={{ height: "320px" }} >
  <div style={{display: "block"}}>
  {transitions(({ innerHeight, ...rest }, item) => (
    <animated.div className="transitionsItem" style={rest}>
      <animated.div style={{ overflow: 'hidden', height: innerHeight }}> 
        {item} 
      </animated.div>
    </animated.div>
  ))}
  </div>
</div>

We use the CSS for the text.

.main {
  height: 320px;
  display: flex;
  align-items: center;
}

.transitionsItem {
  font-size: 4em;
  font-weight: 800;
  text-transform: uppercase;
  line-height: 80px;
}
Enter and Leave useTransition() array elements

The exitBeforeEnter property first animates the Leave element, then removes it and renders the Enter element in the same position.

The expires property animates and doesn't remove the Leave element while rendering the enter one.

let animato = useTransition(element, {
  from: {},
  expires: false,
  exitBeforeEnter: true,
}

The useTransition() events can be linked to a specific styler property.

//onStart() triggers at the start of each frame.
//onChange() on each pixel of animation 
//onRest(), onPause(), and onResume() for paused/resumed animations
//onProps() triggers on updates by new props, even if it remains idle
//onDestroyed() when the element is un-mounted.

//The TO property renders the target prop, not the current style.
let animato = useTransition(element, {
  from:{},
  onStart: {
    color: (x) => {console.log( "triggers on color" )},
    backgroundColor: (x) => {console.log( "back is " + (x.value.animation.to) )}
  },
  onProps: () => {console.log("will cover each updated property")}
}

React use-gesture and useSpring()

We npm install @use-gesture/react, a library that binds mouse and touch events to animate useSpring() NODE elements.

//We extract the useDrag() hook to animate the useSpring() and useState()
import { useDrag } from '@use-gesture/react'

const [{x, dietro}, api] = useSpring(()=>({
  x: 0,
  dietro: "brown"
}))

let [mosso, setMosso] = useState(0)

The useDrag() returns an object that applies event handlers to animated.div components. Its parameters are the drag action and the movement coordinates, and its gesture data animates the useSpring() style properties.

//The useDrag() includes the onPointerUp, onPointerDown, and onPointerMove events.
//active/down event is not a variable, the movement's array is [X-axis, Y-axis]
//We need the immediate property for a smooth property transition later
const muovi = useDrag( ({active, movement: [mx]}) =>(
  api.start({
    x: active ? mx : 0,
    dietro: active ? "orange" : "brown",
    immediate: name => active && name === "mx"
  }),

  setMosso( Math.abs(mx) )
))

//We set a variable based on the useDrag() updated X-style property
//The map property is a filter function for the input value
//The extrapolate property used for the range/output breakpoints
let kolor = x.to({
  map: Math.abs,      //we absolute the negative x coordinates
  range: [10, 300],
  output: [1, 0.2],
  extrapolate: "clamp"  //extends, extrapolate, extrapolateLeft, or extrapolateRight
})

We spread the useDrag() variable in the animated.div element we want to interact with.

//The useState()/style props are updated on useDrag() movement

<div className="d-block">
  <animated.div className="backo" style={{ backgroundColor: dietro }}>
  </animated.div>

  <div className="d-flex justify-content-center mt-3">
    <animated.div className="boxo" {...muovi()} style={{ x, opacity: kolor }} >
    </animated.div>
  </div>

  <animated.p className="text-center">
    We moved it by {Math.floor(mosso/13)} em
  </animated.p>
</div>
The useDrag() used to animate a useSpring() component
Replacing useSpring() properties with the useDrag() hook

We create variables for the useSpring() REST style properties.

//We rest the bg and justifySelf and set them (left)
import { useDrag } from '@use-gesture/react'

const [{ x, scale, bg, justifySelf }, api] = useSpring(() => ({
  x: 0,
  scale: 1,
  ...left
}))

const left = {
  bg: `linear-gradient(120deg, #f093fb 0%, #f5576c 100%)`,
  justifySelf: 'end',
}
const right = {
  bg: `linear-gradient(120deg, #96fbc4 0%, #f9f586 100%)`,
  justifySelf: 'start',
}

During useDrag() we replace the REST style properties based on the x-axis position.

//A left X position is negative
const bind = useDrag(({ active, movement: [x1] }) =>
  api.start({
    x: active ? x1 : 0,
    scale: active ? 1.1 : 1,
    ...(x1 < 0 ? left : right),
    immediate: name => active && name === "x1"
  })
)

const avSize = x.to({
  map: Math.abs,
  range: [50, 300],
  output: [0.5, 1],
  extrapolate: "clamp"
})

We don't drag the cover, we drag its container and animate its x style property, justifySelf and bg are replaced during onDrag().

//Scale is set based on the useDrag() but outside of it
<div className="d-flex justify-content-center mt-5 mb-2">

  <animated.div {...bind()} className="item" style={{ background: bg }}>
    <animated.div className="circle" style={{ scale: avSize, justifySelf }}/>
    <animated.div className="cover" style={{ x, scale }}>
      Slide11
    </animated.div>
  </animated.div>
</div>

//We can render a useSpring value but we can't add text to it
<animated.div className="text-center">
  { avSize }
</animated.div>

The CSS style being:

//display: grid used for the justifyself
.item {
  position: relative;
  width: 300px;
  height: 100px;
  user-select: none;
  display: grid;
  align-items: center;
}

.cover {
  position: absolute;
  height: 100%;
  width: 100%;
  display: grid;
  text-align: center;
  background-color: #272727;
  color: rgba(255, 255, 255, 0.8);
  font-size: 3em;
}

.circle {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background-color: white;
}
useDrag() used to replace useSpring()

The useDrag() state attribute velocity matches the drag speed of the element, the config.decay triggers the momentum at the drag stop.

The useDrag() state attribute movement records the gesture change of position relative to its starter useSpring(), which gets reset at the end of useDrag()

//decay:false returns the element to its useSpring(), no momentum tho
const [{x1, y1}, api1] = useSpring(()=>({
  x1: 100, y1: 0,
}))

let pinto = useDrag(({ down, movement: [mx, my], velocity, direction}) =>{
  
  api1.start({
    x1: down ? mx: 100, y1: down ? my : 0,
    config: { velocity , decay: false },
    immediate: down,
  })
})

<div>
  <animated.div className="rocket" {...pinto()}
    style={{ x: x1, y: y1}}
  />
</div>
animated.div onDrag() movement event

The arcTangent (Math.atan2) of the coordinates returns the current rotation position.

//We keep the position but no momentum
const [{x, y, transform}, apri] = useSpring(()=> ({
  x: 0, y: 0,
  transform: "rotate(0rad)",
}))

const bind4 = useDrag(({ down, offset: [x, y], velocity, direction }) => {

  apri.start({ 
    x, y,
    transform: `rotate(${ Math.atan2(direction[0], -direction[1]) }rad)`,
    immediate: down,
    config: { velocity, decay: false },
  })
})

<div>
  <animated.div className="rocket" {...bind4()}
    style= {{ x, y, transform }}
  />
</div>

Waving transitions with animated SVG filters and useSpring()

SVG, short for Scalable Vector Graphics, uses the viewBox attribute to define a visible window area within the SVG element.

The viewBox can scale or pan its element proportionally to the container size (while maintaining its aspect ratio).

//The zoom depends on the proportion between the container and the viewBox
//50/100 = 0.5x scale on the SVG, while 50/25 = 2x scale. 

<div>
  <svg width="100" height="100">
    <ellipse fill="orange" cx="50" cy="50" rx="50" ry="50"></ellipse>
  </svg>

  <svg width="50" height="50">
    <ellipse fill="orange" cx="50" cy="50" rx="50" ry="50"></ellipse>
  </svg>

  <svg width="50" height="50" viewBox='0 0 100 100'>
    <ellipse fill="orange" cx="50" cy="50" rx="50" ry="50"></ellipse>
  </svg>

  <svg width="50" height="50" viewBox='0 0 25 25'>
    <ellipse fill="orange" cx="50" cy="50" rx="50" ry="50"></ellipse>
  </svg>
</div>
circles rendered inside <svg> tag using viewBox

The <filter> tag contains the filter primitive elements, used to create svg visual effects. The <g> tag can render multiple svg elements, its d(ata) attribute defines the path and shape of the element.

We animate the useSpring() between its starting/ending state with the useState() reverse property and dependency. We use the React-spring animated() function to create animatable filter primitive components.

To animate the SVG wave effect, we use the baseFrequency attribute for its frequency and direction (X/Y values) and the scale (factor) for the strength of the displacement effect.

//The filter primitives <feTurbulence> and <feDisplacementMap> create the wave effect
//We can use animated.feDisplacementMap
//A +/- factor changes the direction of the waves
//frequency is 0 at the ending state to not have a distorted image

const AnimFeTurbulence = animated('feTurbulence')
const AnimFeDisplacementMap = animated('feDisplacementMap')

const [aperto, setAperto] = useState(false)

const [{ freq, factor, scale, opacity }] = useSpring(() => ({
    reverse: aperto,
    from: { factor: 10, opacity: 0, scale: 0.9, freq: '0.0, 0.025' },
    to: { factor: 150, opacity: 1, scale: 1, freq: '0.0, 0.0' },
    config: { duration: 3000 },
  }),
  [aperto]
)

<div onClick={() => setAperto(!aperto)}>
  <animated.svg className="svg" style={{ scale, opacity }} viewBox="0 0 850 480">

    <div>
      <filter id="acqua">
        <AnimFeTurbulence type="fractalNoise" baseFrequency={freq} />
        
        <AnimFeDisplacementMap in="SourceGraphic" scale={factor} />
      </filter>
    </div>
    
    <g filter="url(#acqua)" >
      <path fill="orange" d="svg-path..."></path>
    </g>
  </animated.svg>
</div>
vawe effect on useSpring() svg primitive filters
Complete list of <feDisplacementMap> and <feTurbulence> attributes.

We repeat the wave effect including all the fiter primitive attributes.

<feTurbulence/>
filter: output result of the filter
numOctaves: turbulence detail value 
seed: number used for the random generator on the filter
stitchTiles: "stitch" and "noStich"
type: "fractalNoise" or "turbulence" for a smoother effect

<feDisplacementMap> 
IN: used with an ID for the sourceGraphic of the filter
IN2: second input for the filter
scale: scale factor of the displacement, direction dependant on +/-
xChannelSelector: RGB color of the IN2 map that affects the X-axis
yChannelSelector: RGB color of the IN2 map that affects the Y-axis
result: name of the filter result

In the ReactJs:

<div onClick={() => toggle(!open)}>
  <animated.svg className="svg" style={{ scale, opacity }} 
    viewBox="0 0 1276 400">

    <div>
      <filter id="water">
        <AnimFeTurbulence type="fractalNoise" baseFrequency={freq} 
          numOctaves="2" result="TURB" seed="18" />
        
        <AnimFeDisplacementMap
          xChannelSelector="B"
          yChannelSelector="R"
          in="SourceGraphic"
          in2="TURB"
          result="DISP"
          scale={factor}
        />
      </filter>
    </div>
    
    <g filter="url(#water)">
      <path d="svgPath..." fill="green" />
    </g>
  </animated.svg>
</div>

Last updated

Was this helpful?