Мы хотели бы показать вам, как достичь очень простого, но интересного эффекта с изображением. Вдохновение пришло от плаката большого Каньона: некоторые куски изображения нарезаются и кладутся в другой позиции. Части очень малы, но они создают интересный и творческий эффект. Сегодня мы покажем вам, как создать подобный эффект с помощью 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'ах, но давайте сначала посмотрим на общие стили.
Стили
Основное изображение 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);
};
И на этом все! Посмотрите дэмо , чтобы увидеть некоторые примеры. Спасибо за чтение, и мы надеемся, что вам понравился этот маленький урок!