Little Fragments: Creating a Simple Image Poster Effect

A tutorial on how to create a simple image effect with little image fragments inspired by some poster art and powered by clip-path.

Today we’d like to show you how to achieve a very simple, yet interesting effect with an image. The inspiration comes from a poster of the Grand Canyon with a fun distortion-like effect: some pieces of the image are cut out and placed in a different position. The pieces are very small which creates an interesting and creative look. We’ll be showing you today how to create a similar effect with CSS and some JavaScript.

The general idea is to create a division that has a background image and then add several new divisions dynamically. Each one of those child divs will be translated and another inner div will have a clip-path, showing only a tiny part of the image. To add some more fanciness, we want it to be possible to have an option parallax (or tilt) effect. That’s why we’ll be using two nested divisions.

Let’s get started!

The “Little Fragments” Markup

What we need initially in order to create our dynamic fragments is just a simple div with a background image:

<div class="fragment-wrap" style="background-image: url(img/1.jpg)"></div>

What we want our script to then create is the following:

<div class="fragment-wrap" style="background-image: url(img/1.jpg)">
	<div class="fragment">
		<div class="fragment__piece"></div>
	</div>
	<div class="fragment">
		<div class="fragment__piece"></div>
	</div>
	<!-- ... -->
</div>

Our script will still need to add some individual style properties to the divs but let’s first have a look at the common styles.

LittleFragments_01

The Styles

The main image div, the fragment-wrap needs a width and a height and some margin just so that it is positioned correctly in our layout. To make the image responsive, we’ll be using relative viewport units. Since we’ll want an alternate layout, we’ll also write a modifier class to put the image more to the right side:

.fragment-wrap {
	width: 30vw;
	height: 80vh;
	min-height: 550px;
	max-width: 800px;
	max-height: 1000px;
	position: relative;
	margin: 0 30vw 0 0;
}

.fragment-wrap--right {
	margin: 0 0 0 30vw;
}

The fragment and fragment__piece divisions will be of absolute position and occupy all available width and height. We will apply a clip-path and a translation to this div dynamically, so nothing else needs to be added at this point:

.fragment,
.fragment__piece {
	position: absolute;
	width: 100%;
	height: 100%;
	top: 0;
	left: 0;
	pointer-events: none;
}

For the parallax case, we’ll set a transition to the fragment div:


.fragment {
	transition: transform 0.2s ease-out;
}

We will also apply the parent’s background image to it. For both divs we set the following background image properties:

.fragment-wrap,
.fragment__piece {
	background-size: cover;
	background-repeat: no-repeat;
	background-position: 50% 0%;
}

And those are all the common styles we need for the elements. If we don’t have JS available, the image gets simply shown without the little fragments effect.

Let’s now code up the effect functionality.

Tiny break: 📬 Want to stay up to date with frontend and trends in web design? Subscribe and get our Collective newsletter twice a tweek.

The JavaScript

For the functionality of this effect, we’ll make a little plugin. Let’s have a look at the options:

FragmentsFx.prototype.options = {
	// Number of fragments.
	fragments: 25, 
	// The boundaries of the fragment translation (pixel values).
	boundaries: {x1: 100, x2: 100, y1: 50, y2: 50},
	// The area of the fragments in percentage values (clip-path).
	// We can also use random values by setting options.area to "random".
	area: 'random',
	/* example with 4 fragments (percentage values)
	[{top: 80, left: 10, width: 3, height: 20},{top: 2, left: 2, width: 4, height: 40},{top: 30, left: 60, width: 3, height: 60},{top: 10, left: 20, width: 50, height: 6}]
	*/
	// If using area:"random", we can define the area´s minimum and maximum values for the clip-path. (percentage values)
	randomIntervals: {
		top: {min: 0,max: 90},
		left: {min: 0,max: 90},
		// Either the width or the height will be selected with a fixed value (+- 0.1) for the other dimension (percentage values).
		dimension: {
			width: {min: 10,max: 60, fixedHeight: 1.1},
			height: {min: 10,max: 60, fixedWidth: 1.1}
		}
	},
	parallax: false,
	// Range of movement for the parallax effect (pixel values).
	randomParallax: {min: 10, max: 150}
};

The best way to understand how the randomIntervals and the dimensions can be used, is to have a look at the demo examples. There are five different ways we employ those and the visual result shows how they differ.

First thing to do is to build the layout from our fragment-wrap element and create the structure we mentioned earlier:

FragmentsFx.prototype._init = function() {
	// The dimensions of the main element.
	this.dimensions = {width: this.el.offsetWidth, height: this.el.offsetHeight};
	// The source of the main image.
	this.imgsrc = this.el.style.backgroundImage.replace('url(','').replace(')','').replace(/"/gi, "");;
	// Render all the fragments defined in the options.
	this._layout();
	// Init/Bind events
	this._initEvents();
};

We are going to create the amount of fragment elements specified in the options:

FragmentsFx.prototype._layout = function() {
	// Create the fragments and add them to the DOM (append it to the main element).
	this.fragments = [];
	for (var i = 0, len = this.options.fragments; i < len; ++i) {
		const fragment = this._createFragment(i);
		this.fragments.push(fragment);
	}
};

FragmentsFx.prototype._createFragment = function(pos) {
	var fragment = document.createElement('div');
	fragment.className = 'fragment';
	// Set up a random number for the translation of the fragment when using parallax (mousemove).
	if( this.options.parallax ) {
		fragment.setAttribute('data-parallax', getRandom(this.options.randomParallax.min,this.options.randomParallax.max));
	}
	// Create the fragment "piece" on which we define the clip-path configuration and the background image.
	var piece = document.createElement('div');
	piece.style.backgroundImage = 'url(' + this.imgsrc + ')';
	piece.className = 'fragment__piece';
	piece.style.backgroundImage = 'url(' + this.imgsrc + ')';
	this._positionFragment(pos, piece);
	fragment.appendChild(piece);
	this.el.appendChild(fragment);
	
	return fragment;
};

For setting the translations and the clip-path property (if supported; if not we use clip: rect()) we take our defined values from the options. The translations are always random, but we do need to make sure that the fragment pieces stay within the predefined boundaries. The clip-path can be either random (within the interval defined) or set explicitly.

FragmentsFx.prototype._positionFragment = function(pos, piece) {
	const isRandom = this.options.area === 'random',
		  data = this.options.area[pos],
		  top = isRandom ? getRandom(this.options.randomIntervals.top.min,this.options.randomIntervals.top.max) : data.top,
		  left = isRandom ? getRandom(this.options.randomIntervals.left.min,this.options.randomIntervals.left.max) : data.left;

	// Select either the width or the height with a fixed value for the other dimension.
	var width, height;

	if( isRandom ) {
		if(!!Math.round(getRandom(0,1))) {
			width = getRandom(this.options.randomIntervals.dimension.width.min,this.options.randomIntervals.dimension.width.max);
			height = getRandom(Math.max(this.options.randomIntervals.dimension.width.fixedHeight-0.1,0.1), this.options.randomIntervals.dimension.width.fixedHeight+0.1);
		}
		else {
			height = getRandom(this.options.randomIntervals.dimension.width.min,this.options.randomIntervals.dimension.width.max);
			width = getRandom(Math.max(this.options.randomIntervals.dimension.height.fixedWidth-0.1,0.1), this.options.randomIntervals.dimension.height.fixedWidth+0.1);
		}
	}
	else {
		width = data.width;
		height = data.height;
	}

	if( !isClipPathSupported ) {
		const clipTop = top/100 * this.dimensions.height,
			  clipLeft = left/100 * this.dimensions.width,
			  clipRight = width/100 * this.dimensions.width + clipLeft,
			  clipBottom = height/100 * this.dimensions.height + clipTop;

		piece.style.clip = 'rect(' + clipTop + 'px,' + clipRight + 'px,' + clipBottom + 'px,' + clipLeft + 'px)';
	}
	else {
		piece.style.WebkitClipPath = piece.style.clipPath = 'polygon(' + left + '% ' + top + '%, ' + (left + width) + '% ' + top + '%, ' + (left + width) + '% ' + (top + height) + '%, ' + left + '% ' + (top + height) + '%)';
	}

	// Translate the piece.
	// The translation has to respect the boundaries defined in the options.
	const translation = {
			x: getRandom(-1 * left/100 * this.dimensions.width - this.options.boundaries.x1, this.dimensions.width - left/100 * this.dimensions.width + this.options.boundaries.x2 - width/100 * this.dimensions.width),
			y: getRandom(-1 * top/100 * this.dimensions.height - this.options.boundaries.y1, this.dimensions.height - top/100 * this.dimensions.height + this.options.boundaries.y2 - height/100 * this.dimensions.height)
		  };

	piece.style.WebkitTransform = piece.style.transform = 'translate3d(' + translation.x + 'px,' + translation.y +'px,0)';
};

When we resize the window, the element’s dimensions might change so we want to make sure that everything is adjusted. To keep it simple we recalculate everything again, meaning that we’ll make a new layout.

If the parallax option is true, we want to follow the mouse position (if we are hovering the element) and translate the fragments in the range defined in the options. If we leave the element, we want that the fragments move back to their original positions.

FragmentsFx.prototype._initEvents = function() {
	const self = this;

	// Parallax movement.
	if( this.options.parallax ) {
		this.mousemoveFn = function(ev) {
			requestAnimationFrame(function() {
				// Mouse position relative to the document.
				const mousepos = getMousePos(ev),
					// Document scrolls.
					docScrolls = {left : document.body.scrollLeft + document.documentElement.scrollLeft, top : document.body.scrollTop + document.documentElement.scrollTop},
					bounds = self.el.getBoundingClientRect(),
					// Mouse position relative to the main element (this.el).
					relmousepos = { x : mousepos.x - bounds.left - docScrolls.left, y : mousepos.y - bounds.top - docScrolls.top };

				// Movement settings for the animatable elements.
				for(var i = 0, len = self.fragments.length; i <= len-1; ++i) {
					const fragment = self.fragments[i],
						t = fragment.getAttribute('data-parallax'),
						transX = t/(self.dimensions.width)*relmousepos.x - t/2,
						transY = t/(self.dimensions.height)*relmousepos.y - t/2;

						fragment.style.transform = fragment.style.WebkitTransform = 'translate3d(' + transX + 'px,' + transY + 'px,0)';
				}
			});
		};
		this.el.addEventListener('mousemove', this.mousemoveFn);

		this.mouseleaveFn = function(ev) {
			requestAnimationFrame(function() {
				// Movement settings for the animatable elements.
				for(var i = 0, len = self.fragments.length; i <= len-1; ++i) {
					const fragment = self.fragments[i];
					fragment.style.transform = fragment.style.WebkitTransform = 'translate3d(0,0,0)';
				}
			});
		};
		this.el.addEventListener('mouseleave', this.mouseleaveFn);
	}

	// Window resize - Recalculate clip values and translations.
	this.debounceResize = debounce(function(ev) {
		// total elements/configuration
		const areasTotal = self.options.area.length;
		// Recalculate dimensions.
		self.dimensions = {width: self.el.offsetWidth, height: self.el.offsetHeight};
		// recalculate the clip/clip-path and translations
		for(var i = 0, len = self.fragments.length; i <= len-1; ++i) {
			self._positionFragment(i, self.fragments[i].querySelector('.fragment__piece'));
		}
	}, 10);
	window.addEventListener('resize', this.debounceResize);
};

And that’s all! Check out the demo to see some examples. Thank you for reading and we hope you enjoyed this little tutorial!

Browser Support:
  • ChromeSupported
  • FirefoxSupported
  • Internet ExplorerSupported from version E
  • SafariSupported
  • OperaSupported

References and Credits

Manoela Ilic

Manoela is the main tinkerer at Codrops. With a background in coding and passion for all things design, she creates web experiments and keeps frontend professionals informed about the latest trends.

Stay in the loop: Get your dose of frontend twice a week

👾 Hey! Looking for the latest in frontend? Twice a week, we'll deliver the freshest frontend news, website inspo, cool code demos, videos and UI animations right to your inbox.

Zero fluff, all quality, to make your Mondays and Thursdays more creative!

Feedback 14

Comments are closed.
  1. Me First….Very nice and I was able to recreate it. Manoela: Just curious: How long does it take you to create one of your master pieces? From start (thinking) – doing and finally (documentation).