Следующая реализация позволяет произвольному количеству слайдов быть определенным непосредственно в DOM.
Каждый слайд отображается на целочисленную позицию в массиве slideOrder
.
Путем изменения значения в этом массиве, положение при вращении слайдов контролируется.
Функция render
вызывается только при изменении состояния. Функция рендеринга отображает порядок слайдов в DOM через атрибут данных на каждом слайде. CSS переходы зависают от этого.
Есть три действия: MOVE_PREVIOUS
, MOVE_NEXT
и TOGGLE_AUTOPLAY
. Они могут быть отправлены во внутреннюю очередь, которая очищается последовательно по мере выполнения действий, одно за другим. В полезной нагрузке действия можно указать продолжительность, чтобы гарантировать, что пользовательский интерфейс не будет перерисован в середине перехода CSS.
const $ = document.querySelector.bind(document);
const $s = document.querySelectorAll.bind(document);
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
const ACTIONS = {
MOVE_PREVIOUS: "MOVE_PREVIOUS",
MOVE_NEXT: "MOVE_NEXT",
TOGGLE_AUTOPLAY: "TOGGLE_AUTOPLAY"
};
function createAutoplayer(store) {
let timeoutId = null;
function start() {
function next() {
const { autoplay, slideChangeTime, autoplayInterval } = store.getState();
const now = performance.now();
if (!autoplay) {
timeoutId = setTimeout(next, autoplayInterval);
return;
}
if ((now - slideChangeTime) >= autoplayInterval) {
movePrevious(store);
timeoutId = setTimeout(next, autoplayInterval);
return;
}
timeoutId = setTimeout(
next,
autoplayInterval - (now - slideChangeTime)
);
}
const { autoplayInterval } = store.getState();
timeoutId = setTimeout(next, autoplayInterval);
}
function stop() {
clearTimeout(timeoutId);
}
return { start, stop };
}
const initialState = {
selector: ".slideshow",
slideSelector: ".slide",
autoplayButtonSelector: ".toggle-autoplay",
autoplay: false,
autoplayInterval: 5000,
slideChangeTime: null
};
function createShow(state = initialState) {
const slides = $s(state.slideSelector);
state = { ...initialState, slides, slideOrder: [...Object.keys(slides)] };
const store = createStore({ initialState: state, reducers });
createAutoplayer(store).start();
return {
start: start.bind(null, store),
moveNext: moveNext.bind(null, store),
movePrevious: movePrevious.bind(null, store),
toggleAutoplay: toggleAutoplay.bind(null, store)
};
}
function moveNext(store) {
const actions = [
createAction(ACTIONS.MOVE_NEXT, {
duration: 500
})
];
store.dispatch(...actions);
}
function movePrevious(store) {
store.dispatch(createAction(ACTIONS.MOVE_PREVIOUS, { duration: 500 }));
}
function rotateForward([first, ...rest]) {
return [...rest, first];
}
function rotateBackward(arr) {
return [arr[arr.length - 1], ...arr.slice(0, arr.length - 1)];
}
const reducers = {
[ACTIONS.MOVE_NEXT]: (state, payload) => ({
...state,
slideOrder: rotateForward(state.slideOrder),
slideChangeTime: performance.now()
}),
[ACTIONS.MOVE_PREVIOUS]: (state, payload) => ({
...state,
slideOrder: rotateBackward(state.slideOrder),
slideChangeTime: performance.now()
}),
[ACTIONS.TOGGLE_AUTOPLAY]: (state, payload) => ({
...state,
autoplay: !state.autoplay,
slideChangeTime: null
})
};
function render(state) {
const { slides, slideOrder, autoplay, autoplayButtonSelector } = state;
for (let x = 0; x < slides.length; x++) {
slides[x].dataset.index = slideOrder[x];
}
autoplay
? $(autoplayButtonSelector).classList.add('enabled')
: $(autoplayButtonSelector).classList.remove('enabled');
}
let previousState = null;
function start(store) {
function loop() {
const state = store.getState();
if (state === previousState) {
return requestAnimationFrame(loop);
}
previousState = state;
render(state);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
}
function toggleAutoplay(store) {
const actions = [createAction(ACTIONS.TOGGLE_AUTOPLAY)];
store.dispatch(...actions);
}
function createAction(type, payload = {}) {
return { type, payload };
}
function createDispatcher({ getState, setState, reducers }) {
const q = [];
let isDraining = false;
async function drain() {
isDraining = true;
while (q.length) {
const { type, payload } = q.shift();
setState(reducers[type](getState(), payload));
await wait(payload.duration);
}
isDraining = false;
}
function dispatch(...actions) {
q.push(...actions);
if (!isDraining) {
drain();
}
}
return { dispatch };
}
function createStore({ initialState, reducers }) {
let state = Object.freeze(initialState);
function getState() {
return state;
}
function setState(value) {
state = Object.freeze(value);
}
const { dispatch } = createDispatcher({ getState, setState, reducers });
return {
getState,
dispatch
};
}
const show = createShow();
$(".next").addEventListener("click", show.moveNext);
$(".previous").addEventListener("click", show.movePrevious);
$(".toggle-autoplay").addEventListener("click", show.toggleAutoplay);
show.start();
* {
font-size: 0;
border: 0;
padding: 0;
margin: 0;
color: #444;
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
}
html {
font-size: 12px;
}
body {
width: 100%;
font-family: sans-serif;
}
button {
width: 100px;
height: 50px;
font-size: 2.2rem;
background: none;
box-shadow: 0 0 0 2px rgba(0,200,200, .5) inset;
border-radius: 10px;
border: 0;
cursor: pointer;
outline: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
.toggle-autoplay.enabled {
box-shadow: 0 0 55px 5px rgba(200,0,50, .3) inset;
}
.controls-container {
clear: both;
display: block;
text-align: center;
padding: 15px;
}
.container {
padding: 0;
}
.h-spacer-container {
display: inline-block;
padding: 0 15px;
}
.slides-container {
width: 100%;
padding: 00px 0 30px calc(50% - 200px);
height: calc(200px + 30px);
}
.slide {
position: absolute;
width: 400px;
height: 200px;
background-size: cover;
border-radius: 10px;
opacity: 0;
transition: all 0.5s ease-in-out 0s;
font-size: 2rem;
padding: 10px;
}
.slide[data-index = '0'] {
z-index: 0;
transform: perspective(20px) rotate3d(1, -2.0, -3.0, 5deg) translate(-200px, 0px);
}
.slide[data-index = '1'] {
z-index: 9;
opacity: 1;
transform: translate3d(0px, 0px, 0px)
}
.slide[data-index = '2'] {
transform: perspective(20px) rotate3d(1, 2.0, 3.0, 5deg) translate(200px, 0px);
}
.slide-1 {
background-image: url(https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fi.redd.it%2Fheyde2iaughz.jpg&f=1&nofb=1);
}
.slide-2 {
background-image: url(https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fsailingheaven.com%2Fwp-content%2Fuploads%2F2015%2F03%2Fmega-yacht-3.jpg&f=1&nofb=1);
}
.slide-3 {
background-image: url(https://external-content.duckduckgo.com/iu/?u=http%3A%2F%2Fcdn.boatinternational.com%2Fbi_prd%2Fbi%2Flibrary_images%2FTHUqC2wQSeVWkkilwTFw_Unfurled-Vitters-sailing-yacht-hero-credit-Stuart-Pearce-1280x720.jpg&f=1&nofb=1);
}
.slide-4 {
background-image: url(https://external-content.duckduckgo.com/iu/?u=http%3A%2F%2Fmegayachtconcepts.weebly.com%2Fuploads%2F5%2F7%2F8%2F5%2F5785334%2F6248846.jpg%3F859&f=1&nofb=1);
}
.slide-5 {
background-image: url(https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fi.ytimg.com%2Fvi%2FmT5ZNjPqVuM%2Fmaxresdefault.jpg&f=1&nofb=1);
}
.slide-6 {
background-image: url(https://external-content.duckduckgo.com/iu/?u=http%3A%2F%2Fgtspirit.com%2Fwp-content%2Fuploads%2F2016%2F01%2FGanesha-Sailing-Superyacht-17.jpg&f=1&nofb=1);
}
<div class="slideshow container">
<div class="controls-container">
<div class="h-spacer-container">
<button class="next">⏮</button>
</div>
<div class="h-spacer-container">
<button class="toggle-autoplay">⏯</button>
</div>
<div class="h-spacer-container">
<button class="previous">⏭</button>
</div>
</div>
<div class="slides-container">
<div class="slide slide-6">Yacht F</div>
<div class="slide slide-1">Yacht A</div>
<div class="slide slide-2">Yacht B</div>
<div class="slide slide-3">Yacht C</div>
<div class="slide slide-4">Yacht D</div>
<div class="slide slide-5">Yacht E</div>
</div>
</div>