SoatDev IT Consulting
SoatDev IT Consulting
  • About us
  • Expertise
  • Services
  • How it works
  • Contact Us
  • News
  • August 24, 2023
  • Rss Fetcher

A random image from Unsplash split into stripes and recomposed

Photo by Schuyler Dugle on Unsplash

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:

Splitted image

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.

Previous Post
Next Post

Recent Posts

  • Still no AI-powered, ‘more personalized’ Siri from Apple at WWDC 25
  • Here’s what’s coming to macOS Tahoe
  • Apple brings back tabs to the Photos app in iOS 26
  • OpenAI claims to have hit $10B in annual revenue
  • From spatial widgets to realistic Personas: All the visionOS updates Apple announced at WWDC 

Categories

  • Industry News
  • Programming
  • RSS Fetched Articles
  • Uncategorized

Archives

  • June 2025
  • May 2025
  • April 2025
  • February 2025
  • January 2025
  • December 2024
  • November 2024
  • October 2024
  • September 2024
  • August 2024
  • July 2024
  • June 2024
  • May 2024
  • April 2024
  • March 2024
  • February 2024
  • January 2024
  • December 2023
  • November 2023
  • October 2023
  • September 2023
  • August 2023
  • July 2023
  • June 2023
  • May 2023
  • April 2023

Tap into the power of Microservices, MVC Architecture, Cloud, Containers, UML, and Scrum methodologies to bolster your project planning, execution, and application development processes.

Solutions

  • IT Consultation
  • Agile Transformation
  • Software Development
  • DevOps & CI/CD

Regions Covered

  • Montreal
  • New York
  • Paris
  • Mauritius
  • Abidjan
  • Dakar

Subscribe to Newsletter

Join our monthly newsletter subscribers to get the latest news and insights.

© Copyright 2023. All Rights Reserved by Soatdev IT Consulting Inc.