Мы хотели бы показать вам, как достичь очень простого, но интересного эффекта с изображением. Вдохновение пришло от плаката большого Каньона: некоторые куски изображения нарезаются и кладутся в другой позиции. Части очень малы, но они создают интересный и творческий эффект. Сегодня мы покажем вам, как создать подобный эффект с помощью CSS и JavaScript.

Общая идея состоит в том, чтобы создать нечто похожее на плакат, которое имеет фоновое изображение, а затем в него добавить несколько новых элементов динамически. Каждый из этих дочерних div'ов будет наследован, а другой внутренний div будет иметь клип-путь, показывающий только крошечную часть изображения. Чтобы добавить еще немного фантазии, сделаем эффект параллакса (или наклона).

Давайте начнем!

Разметка "Маленькие Фрагменты"

То, что нам нужно изначально для создания наших динамических фрагментов - это просто div с фоновым изображением:

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

Мы хотим, чтобы наш скрипт создал следующее:

	
<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>
	

Нашему скрипту все равно нужно будет добавить некоторые индивидуальные свойства стиля в div'ах, но давайте сначала посмотрим на общие стили.

Эффект CSS

Стили

Основное изображение div, fragment-wrap нужна ширина и высота с некоторым запасом так, чтобы была правильная позиция в нашем макете. Чтобы сделать изображение отзывчивым, мы будем использовать относительные единицы видового экрана. Поскольку нам нужен альтернативный макет, мы также напишем класс модификатора, чтобы разместить изображение больше в правую сторону:

	
.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;
}
	

Элементы fragment и fragment__piece имеют абсолютное положение и будут занимать всю доступную ширину и высоту. Мы будем применять clip-path к этому div динамически, поэтому больше ничего не нужно добавлять на этом этапе:

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

Для случая параллакса мы установим переход к fragment div:

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

Мы также применим к нему фоновое изображение родителя. Для обоих div'вов мы устанавливаем следующие свойства фонового изображения:

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

И это все общие стили, которые нам нужны для элементов. Если у нас нет JS, изображение просто показывается без эффекта маленьких фрагментов.

Теперь давайте закодируем функциональность эффекта.

JavaScript

Для функциональности этого эффекта мы сделаем небольшой плагин. Давайте посмотрим на варианты:

	
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}
};
	

Лучший способ понять, как можно использовать случайные интервалы и измерения, - это взглянуть на демонстрационные примеры. Есть пять различных способов, которыми мы их используем, и визуальный результат показывает, как они отличаются.

Первое, что нужно сделать, это построить макет из нашего элемента fragment-wrap и создать структуру, о которой мы упоминали ранее:

	
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();
};
	

Мы создадим количество элементов фрагмента, в указанных опциях:

	
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;
};
	

Для установки translation и свойства clip-path (если поддерживается; если нет, мы используем clip: rect ()), мы берем наши определенные значения из опций. translation всегда случайны, но нам нужно убедиться, что фрагменты остаются в пределах предопределенных границ. Путь клипа может быть случайным (в пределах заданного интервала).

	
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)';
};
	

Когда мы изменяем размер окна, размеры элемента могут измениться, поэтому мы хотим, чтобы все было отрегулировано и было просто.

Если параметр parallax имеет значение true, мы следуем указателю мыши и переводим фрагменты в диапазон, который определен в параметрах. Если мы оставим элемент, то фрагменты должны переместиться обратно в исходное положение.

	
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);
};
	

И на этом все! Посмотрите дэмо , чтобы увидеть некоторые примеры. Спасибо за чтение, и мы надеемся, что вам понравился этот маленький урок!