|
| 1 | +## `prepareImageAnimation` |
| 2 | + |
| 3 | +Prepares an animation for an image from one size, location and crop to another. |
| 4 | + |
| 5 | +### Typical Usage |
| 6 | + |
| 7 | +```javascript |
| 8 | +const duration = 400; |
| 9 | +const { |
| 10 | + applyAnimation, |
| 11 | + cleanupAnimation, |
| 12 | +} = prepareImageAnimation({ |
| 13 | + srcImg, |
| 14 | + targetImg, |
| 15 | + styles: { |
| 16 | + animationDuration: `${duration}ms`, |
| 17 | + }, |
| 18 | +}); |
| 19 | + |
| 20 | +srcImg.style.visibility = 'hidden'; |
| 21 | +targetImg.style.visibility = 'hidden'; |
| 22 | +applyAnimation(); |
| 23 | +setTimeout(() => { |
| 24 | + targetImg.style.visibility = 'visible'; |
| 25 | + cleanupAnimation(); |
| 26 | +}, duration); |
| 27 | +``` |
| 28 | + |
| 29 | +### Demos |
| 30 | + |
| 31 | +* [Hero animation](./demo/hero) |
| 32 | +* [Lightbox](./demo/lightbox) |
| 33 | +* [Image gallery](./demo/gallery) |
| 34 | + |
| 35 | +### How `prepareImageAnimation` Works |
| 36 | + |
| 37 | +The animation is done by creating a temporary `<img>` element that is animated between the source and the target. Once the animation is completed, the temporary `<img>` is removed. The animation is done using `position: absolute`, to allow the image to move as the user scrolls. |
| 38 | + |
| 39 | +In order to animate the crop, the function looks at how the source and target images are rendered using the size and [`object-fit`](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) property. It then animates between the two states, which may cause the cropping to change as the animation proceeds. See the [hero animation demo](./demo/hero) for an example of this in action. |
| 40 | + |
| 41 | +The animation is first prepared, then applied and finally cleaned up. The creation and application are two different steps, which can be useful if you want to avoid [layout thrashing](https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing#avoid_forced_synchronous_layouts) using a library like [fastdom](https://github.com/wilsonpage/fastdom). |
| 42 | + |
| 43 | +### Function signature |
| 44 | + |
| 45 | +```javascript |
| 46 | +function prepareImageAnimation({ |
| 47 | + transitionContainer = document.body, |
| 48 | + styleContainer = document.head!, |
| 49 | + srcImg, |
| 50 | + targetImg, |
| 51 | + srcImgRect = srcImg.getBoundingClientRect(), |
| 52 | + targetImgRect = targetImg.getBoundingClientRect(), |
| 53 | + curve = EASE_IN_OUT, |
| 54 | + styles, |
| 55 | + keyframesNamespace = 'img-transform', |
| 56 | +} : { |
| 57 | + transitionContainer: HTMLElement, |
| 58 | + styleContainer: Element|Document|DocumentFragment, |
| 59 | + srcImg: HTMLImageElement, |
| 60 | + targetImg: HTMLImageElement, |
| 61 | + srcImgRect?: ClientRect, |
| 62 | + targetImgRect?: ClientRect, |
| 63 | + curve?: Curve, |
| 64 | + styles: Object, |
| 65 | + keyframesNamespace?: string, |
| 66 | +}) : { |
| 67 | + applyAnimation: () => void, |
| 68 | + cleanupAnimation: () => void, |
| 69 | +} |
| 70 | +``` |
| 71 | + |
| 72 | +### Return Value |
| 73 | + |
| 74 | +#### `applyAnimation` |
| 75 | + |
| 76 | +Applies the animation by inserting the temporary transition `<img>` into the `transitionContainer` as well as inserting a dynamically generated stylesheet into `styleContainer`. |
| 77 | + |
| 78 | +#### `cleanupAnimation` |
| 79 | + |
| 80 | +Undoes the effects of `applyAnimation`. |
| 81 | + |
| 82 | +### Parameters |
| 83 | + |
| 84 | +#### `transitionContainer` |
| 85 | + |
| 86 | +This option defaults to `document.body` and is where the the animating `<img>` is placed. Two cases where you might not want this to be the body are: |
| 87 | + |
| 88 | +1. The body is not the scrolling container. |
| 89 | +2. The body is the scrolling container, but is not currently scrolling. |
| 90 | + |
| 91 | +When the body is not the scrolling container, you will want to place the animating `<img>` somewhere in the scrolling container. As an exmaple, the [hero animation demo](./demo/hero) places the transition image on the newly active page. The structure looks like: |
| 92 | + |
| 93 | +```html |
| 94 | +<div class="page" style="position: absolute; overflow-y: auto"> |
| 95 | + <div class="content-container" style="position: relative;"> |
| 96 | + … content |
| 97 | + </div> |
| 98 | +</div> |
| 99 | +``` |
| 100 | + |
| 101 | +The demo uses `.content-container` as `transitionContainer`. Since the `transitionContainer` moves as the user scrolls, the animation moves in sync. Note that the `transitionContainer` may actually be a descendent of `content-container`, as `prepareImageAnimation` looks for the first positioned ancestor. |
| 102 | + |
| 103 | +#### `styleContainer` |
| 104 | + |
| 105 | +This defaults to `document.head` and is where generated CSS for the animation is placed. If you want the animation to be placed within [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) (i.e. specifying a `transitionContainer` within a `ShadowRoot`), then you will want the `ShadowRoot` to be the `styleContainer`. |
| 106 | + |
| 107 | +#### `srcImg` |
| 108 | + |
| 109 | +An `<img>` to animate from. This is used to determine the position, size, and the `object-fit` property to start the animation with. |
| 110 | + |
| 111 | + |
| 112 | +#### `targetImg` |
| 113 | + |
| 114 | +An `<img>` to animate to. This is used to determine the position, size, and the `object-fit` property to end the animation with. |
| 115 | + |
| 116 | +#### `srcImgRect` |
| 117 | + |
| 118 | +Defaults to `srcImg.getBoundingClientRect()`. If the `srcImg` is not laid out at the time you call `prepareImageAnimation`, you will want to capture the `ClientRect` beforehand and provide it to the call. |
| 119 | + |
| 120 | +One situtation this might be useful is if you are doing an animation between pages, where the content is in the `body` itself rather than in a separate scrolling container. For example. consider the following page structure: |
| 121 | + |
| 122 | +```html |
| 123 | +<body> |
| 124 | + <div class="page"> |
| 125 | + … |
| 126 | + </div> |
| 127 | + <div class="page" hidden> |
| 128 | + <h1>Some title that might wrap depending on the viewport width</h1> |
| 129 | + <img class="hero" …> |
| 130 | + </div> |
| 131 | +</body> |
| 132 | +``` |
| 133 | + |
| 134 | +To figure out where the `hero` will be positioned, we need to layout the target page (e.g. by adding `hidden` to the current page and removing it from the target page). However, hiding the current page will mean `prepareImageAnimation` will no longer know where to start the animation. By providing `srcImgRect`, the animation can know where to start from. |
| 135 | + |
| 136 | +#### `targetImgRect` |
| 137 | + |
| 138 | +Defaults to `targetImg.getBoundingClientRect()`. If you know where the `targetImg` will be rendered, but you have not laid out the containing content, you can provide it to `prepareImageAnimation`. You can use this to avoid a forced layout in some situations, for example in the [hero animation demo](./demo/hero), we do something like: |
| 139 | + |
| 140 | +```javascript |
| 141 | +// Layout the the target so that we know where targetImg is |
| 142 | +target.hidden = false; |
| 143 | + |
| 144 | +// Forced style calc + layout when we go to measure things |
| 145 | +const { |
| 146 | + applyAnimation, |
| 147 | + cleanupAnimation, |
| 148 | +} = prepareImageAnimation(…); |
| 149 | + |
| 150 | +// Regular style calc + layout for mutations |
| 151 | +current.hidden = true; |
| 152 | +applyAnimation(); |
| 153 | +``` |
| 154 | + |
| 155 | +The forced style calculation caused by `prepareImageAnimation` can be avoided if you already know where `targetImg` will be positioned. Note that in this case, you will still need to provide a `targetImg` to the function so that the animation knows the `object-fit` property to animate to. |
| 156 | + |
| 157 | +#### `curve` |
| 158 | + |
| 159 | +This option defaults to the built-in `ease-in-out` transition timing function (`{x1: 0.42, y1: 0, x2: 0.58, y2: 1}`). This is an object with the control points for a [`cubic-bezier()`](https://developer.mozilla.org/en-US/docs/Web/CSS/single-transition-timing-function#The_cubic-bezier()_class_of_timing_functions) curve and is used to determine the animation progress for the position, size and crop at any given time. |
| 160 | + |
| 161 | +#### `styles` |
| 162 | + |
| 163 | +An object of styles to apply to the animating elements. At the minimum, this should include `animationDuration`. Other useful properties may include `animationDelay` (if you want to synchronize this with another animation, which should start earlier) and `z-index`. |
| 164 | + |
| 165 | +#### `keyframesNamespace` |
| 166 | + |
| 167 | +This option defaults to `'img-transform'`. In order to play the animation, CSS keyframes need to be dynamically created. The prefix is used to make sure that the generated names will not colide with any other keyframes present. It is very unlikely that this needs to be specified. |
| 168 | + |
| 169 | +### Using different resolution images |
| 170 | + |
| 171 | +If you are doing an animation from a smaller image to a larger image, you may want to use a low resolution image for the smaller image to make it load faster and save bandwidth. The [image gallery demo](./demo/gallery) outlines an approach to accomplish this. In short, you will want to perform the following steps: |
| 172 | + |
| 173 | +1. Start preloading the higher resolution image (e.g. on `mousedown`/`touchstart` or when starting the animation) |
| 174 | +1. Set the `src` for `targetImg` (either via `src` or `srcset`/`sizes`) to the lower resolution image |
| 175 | +1. Perform the image animation |
| 176 | +1. Once the higher resolution image has finished downloaded, set the `src` for `targetImg` to the higher resolution image |
| 177 | + |
| 178 | +The [image gallery demo code](./demo/gallery/index.js) implements this approach using `srcset`. |
0 commit comments