A random image from Unsplash split into stripes and recomposed
This is a simple component built almost entirely with CSS. JavaScript is used to apply some randomness, to access the Unsplash API, and to configure the app.
You may find other (and better) ways to achieve the same effect. If so, please let me know. (You can find something similar in this pen of Fabio Ottaviani. It was my first inspiration for this effect.)
It consists of dividing an image into a predefined number of stripes (ten, in my example), placing each stripe outside the viewport, and alternating the top and bottom sides. Each stripe is then returned to its original position with a simple animation of random duration:
Also, the photos are randomly loaded from Unsplash, and you can also see that just before the image is visible, each stripe is filled with a random color.
The full code is available in my GitHub Repository. It also contains a working demo.
A simplified Vanilla JS version is available on Codepen.
The workflow is really simple:
Using its APIs, a random photo is loaded from Unsplash. I wrote about this in my previous article. Take a look at it for more info.
Once the image has been loaded, the HTML structure is created. We can also set up the following CSS custom properties that will be used by the CSS:
Effect(() => {
if(unsplashData && imgUrl) {
const colors = randomcolor({ count: props.stripes, hue: unsplashData.color }),
maxDuration = 1.5,
minDuration = .5;
setContent(
<div className={styles.outerWrapper} style={{
'--img': `url(${imgUrl})`,
'--items': props.stripes,
'--max-duration': `${maxDuration}s`
}}>
<div className={styles.imgStripesWrapper}>
{[...Array(props.stripes)].map((_, idx) => {
return <div className={styles.imgStripeItem} key={idx}
style={{
'--idx': idx,
'--color': colors[idx],
'--duration': `${(Math.random() * (maxDuration - minDuration) + minDuration).toFixed(2)}s`
}}>
<div></div> {/* additional div needed for proper image placement */}
</div>; // end of .imgStripeItem
})}
</div> {/* end of .imgStripesWrapper */}
<div className={styles.credits}>{/* ... content omitted ... */}</div>
</div> {/* end of .outerWrapper */}
);
}
}, [imgUrl, props.stripes, unsplashData]);
The structure consists of an .outerWrapper element, which is the main wrapper. This isn’t really necessary for the effect itself (it includes the .imgStripesWrapper and .credits DIVs), but it becomes necessary in the rotated strip version (see below). Note that some image credits are required by Unsplash.
The .outerWrapper element, in turn, contains an .imgStripeWrapper flexbox element, which is the direct container for the strips.
Stripes are generated from props.stripes values.
Each stripe (.imgStripeItem) contains an inner div: this is where the Unsplash image is set as background.
I found it easier to position the inner div relative to its .imgStripeItem container than to adjust the background-position property. This choice also proved to be very useful in the rotated version we will see later.
.imgStripeItem {
width: calc(100% / var(--items));
height: 100%;
overflow: hidden;
background-color: #ddd;
opacity: 0;
animation: var(--duration) ease-in var(--animation-delay) var(--animation-count) alternate forwards Stripes-show;
> div {
position: absolute;
left: calc(-1 * var(--idx) * 100dvw / var(--items));
width: 100dvw;
height: 100%;
background-image: var(--img);
background-repeat: no-repeat;
background-position: center;
background-size: cover;
}
&:nth-child(even) {
// transform: translateY(-101%);
translate: 0 -101%;
}
&:nth-child(odd) {
// transform: translateY(101%);
translate: 0 101%;
}
/* ... */
}
@keyframes Stripes-show {
to {
opacity: 1;
// transform: translateY(0);
translate: 0 0;
}
}
The duration of each striped animation is set by the –duration custom property, which was previously randomly generated (there is a clever technique to generate random values directly in CSS, but I haven’t had the opportunity to elaborate on it).
There is also a property to set the initial color of each stripe.
This is a secondary effect, but it adds something interesting to the whole thing. One of the properties provided by the Unsplash API is the color one. It represents the main color of the image, and we use it as a parameter of randomColor, which generates a list of colors based on the given one.
const colors = randomcolor({ count: props.stripes, hue: unsplashData.color });
Each generated color fills a pseudo-element that becomes transparent as the animation progresses.
You can change its duration or remove it entirely if you don’t like it.
.imgStripeItem {
/* ... */
&::after {
position: absolute;
inset: 0;
display: block;
content: '';
background-color: var(--color);
opacity: 1;
animation: calc(var(--max-duration) * var(--animation-count)) linear var(--animation-delay) forwards color-hide;
}
}
@keyframes color-hide {
to {
opacity: 0;
}
}
Caveats
- The code uses a single image for all screen resolutions — for use in a real project, you should use multiple sizes for better performance. This topic is not currently covered in the various examples.
- On Firefox, sometimes, some empty pixel lines are displayed between stripes (see this bug report)
Rotating the Stripes
Once the component was finished, it was natural for me to imagine it with the stripes rotated.
My first attempt was to rotate the container and then reverse-rotate each strip. Here’s the code:
.imgStripesWrapper {
--rotation: 15deg;
transform-origin: center;
rotate: calc(-1 * var(--rotation));
}
.imgStripeItem {
> div {
rotate: var(--rotation);
}
}
This is what I got:
What I needed was to enlarge the rotated image so it completely encompassed the viewport:
How can you calculate the minimum size needed?
As you can see, we get four right triangles by subtracting the viewport area from the image.
By construction, the two triangles positioned along the viewport’s height are equal, as are the two positioned along the horizontal side.
Since we know one of the angles (it’s our angle of rotation) and the lengths of the two hypotenuses (the dimensions of the viewport), we have all we need!
We have only to convert our angle in radians and then apply that simple formula:
const root_container = document.querySelector('.root'),
rotation = 15; // degrees
function calcImgScaleRatio() {
// container size is used to quickly convert this script to a component, if needed
const root_container_width = root_container.offsetWidth,
root_container_height = root_container.offsetHeight,
rotation_rad = rotation * Math.PI / 180,
image_container_width = root_container_height * Math.sin(rotation_rad) + root_container_width * Math.cos(rotation_rad), // BC + CD
image_container_height = root_container_height * Math.cos(rotation_rad) + root_container_width * Math.sin(rotation_rad), // AB + DE
scale_ratio = (image_container_width * image_container_height) / (root_container_width * root_container_height);
root_container.style.setProperty('--scale', scale_ratio);
return scale_ratio;
}
The function returns a scale_ratio value, which is assigned to the –scale custom property and then used by the scale CSS rule.
.imgStripesWrapper{
transform-origin: center;
rotate: calc(-1 * var(--rotation));
scale: var(--scale);
}
This is the simplest way to zoom the image: setting the transform-origin: center rule ensures that the image is centered correctly in the viewport and nothing else needs to be done.
The calcImgScaleRatio function is also invoked when the viewport is resized:
// recalculate on resize
const observer = new ResizeObserver( () => calcImgScaleRatio());
observer.observe(root_container);
On a 16:9 desktop screen, the scale factor is approximately 1.5. As in the previous example, you need to use an image with the necessary dimensions to prevent the final image from being low quality.
You can find the full code and a working demo in my GitHub repository.
A Split Image Effect in React was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.