画像切り替え機能の制作例

画像切り替え swiper スワイパー

主な用途としてメイン画像用のスイッチャー/スワイパーです。JavaScriptコードを参考に自分好みに仕上げよう。

マウスやタッチによる左右スワイプでの画像切り替え可能。
時間経過による画像自動切り替え。
レスポンシブ対応。画像個別に内部リンク外部リンクを設置可能。このサンプルにはリンクは設置されていません。
現時点のコードは以下の通り:
/**
 * 画像切り替え
 *
 * @author ao-system, Inc.
 * @date 2020-07-01
 * @date 2024-01-12
 * @date 2024-01-17 Rewrite the code using the class syntax.
 *
 * this.stage.style.transition、this.stage.style.transform の処理を
 * this.stage.animate()での実装を試みたが正常に動作しなかった。
 *
<section class="swipepanel">
	<div>
		<div id="swipePanelStage"></div>
	</div>
</section>

<style>
section.swipepanel {
	background-color: #000;
	padding: 20px;
	> div {
		> #swipePanelStage {
			display: flex;
			justify-content: center;
			column-gap: 10px;
			> div {
				user-select: none;
			}
		}
	}
}
</style>
 *
 */


(() => {
	'use strict';
	class SwipePanel {
		#constProperties = [
			{prop: 'stage',					value: document.getElementById('swipePanelStage')},
			{prop: 'imageWidth',			value: 300},
			{prop: 'imageGap',				value: 10},
			{prop: 'itemMinCount',			value: 14},		//3840 / (300 + 10) + 1
			{prop: 'imageSlideInterval',	value: 4000},	//画像切り替え間隔
			{prop: 'swipeShreshold',		value: 50},		//スワイプ判定幅
		];
		#itemCount			= 0;		//奇数か偶数かで処理が分岐
		#loopTimeoutId		= null;		//自動スワイプのタイムアウト
		#isBusy				= false;	//動作中
		#focusFlag			= true;		//フォーカスが外れている時は動作させない
		#swipeStartX		= 0;		//スワイプ開始位置
		#isSwiping			= false;	//スワイプ中
		#swipeTimeoutId		= null;		//スワイプ操作のタイムアウト
		#values				= [];
		//
		constructor(values) {
			this.#values = values;
			this.#constProperties.forEach((obj) => Object.defineProperty(this, obj.prop, {value:obj.value, writable:false}));
			this.#init();
		}
		#init() {
			//シャッフルさせる場合
			//this.#values = shuffle(this.#values);
			//window.addEventListener('focus',() => { this.#focusFlag = true;});	//out focusで停止させる場合の処理
			//window.addEventListener('blur',() => { this.#focusFlag = false;});	//out focusで停止させる場合の処理
			this.#setItemCount();
			this.#initImages();
			this.#initSwipe();
			this.#autoSlide();
		}
		#shuffle(array) {
			for (let i = array.length - 1; i > 0; i--) {
				const j = Math.floor(Math.random() * (i + 1));
				[array[i], array[j]] = [array[j], array[i]];
			}
			return array;
		}
		#setItemCount() {
			this.#itemCount = this.#values.length;
			for (let i = 0; i < this.itemMinCount; i++) {
				if (this.#itemCount < this.itemMinCount) {
					this.#itemCount += this.#values.length;
				} else {
					break;
				}
			}
		}
		#initImages() {
			for (let i = 0; i < this.#itemCount; i++) {
				const obj = this.#values[i % this.#values.length];
				const img = new Image();
				img.src = obj.src;
				img.setAttribute('data-link',obj.link);
				img.setAttribute('data-external',obj.external);
				img.setAttribute('draggable','false');
				const div = document.createElement('div');
				div.appendChild(img);
				this.stage.appendChild(div);
			}
			//画像数が偶数の場合は先頭に1個ダミーを置く
			if (this.#itemCount % 2 == 0) {
				const obj = this.#values[0];
				const img = new Image();
				img.src = obj.src;
				img.setAttribute('draggable','false');
				const div = document.createElement('div');
				div.appendChild(img);
				const divs = this.stage.querySelectorAll('div');
				this.stage.insertBefore(div,divs[0]);
			}
		}
		#initSwipe() {
			this.stage.addEventListener('click', (e) => { e.preventDefault();});
			this.stage.addEventListener('mousedown', (e) => { this.#swipeStart(e.clientX); });
			this.stage.addEventListener('mousemove', (e) => { this.#swipeMove(e.clientX); });
			this.stage.addEventListener('mouseup', (e) => { this.#swipeEnd(e.target, e.clientX); });
			this.stage.addEventListener('touchstart', (e) => { this.#swipeStart(e.touches[0].clientX); });
			this.stage.addEventListener('touchmove', (e) => { this.#swipeMove(e.touches[0].clientX); });
			this.stage.addEventListener('touchend', (e) => { this.#swipeEnd(e.target, e.changedTouches[0].clientX); });
		}
		#swipeStart(positionX) {
			if (this.#isBusy) {
				return;
			}
			this.#swipeStartX = positionX;
			this.stage.style.cursor = 'grabbing';
			this.#isSwiping = true;
			this.#swipeTimeoutId = setTimeout(() => {
				this.stage.style.transform = '';
				this.stage.style.cursor = '';
				this.#isSwiping = false;
			},1000);
		}
		#swipeMove(positionX) {
			if (this.#isSwiping == false) {
				return;
			}
			let x = positionX - this.#swipeStartX;
			const maxSlideWidth = this.imageWidth + this.imageGap;
			if (x <= -maxSlideWidth) {
				x = -maxSlideWidth;
			} else if (x >= maxSlideWidth) {
				x = maxSlideWidth;
			}
			this.stage.style.transform = 'translateX(' + x + 'px)';
		}
		#swipeEnd(targetElm,positionX) {
			clearTimeout(this.#swipeTimeoutId);
			if (this.#isSwiping == false) {
				return;
			}
			if (this.#swipeStartX - positionX > this.swipeShreshold) {
				this.#slideAction(true);	//正方向
			} else if (this.#swipeStartX - positionX < -this.swipeShreshold) {
				this.#slideAction(false);	//逆方向
			} else {
				this.stage.style.transform = 'translateX(0px)';
				const dataLink = targetElm.getAttribute('data-link');
				if (dataLink) {
					const dataExternal = targetElm.getAttribute('data-external');
					if (dataExternal) {
						window.open(dataLink, '_blank');
					} else {
						window.location.href = dataLink;
					}
				}
			}
			setTimeout(() => {
				this.stage.style.cursor = '';
				this.stage.style.transition = '';
				clearTimeout(this.#loopTimeoutId);
				this.#autoSlide();
				this.#isSwiping = false;
			},20);
		}
		#slideAction(direction) {
			this.#isBusy = true;
			if (direction) {
				setTimeout(() => {
					this.stage.style.transition = '0.3s';
					this.stage.style.transform = 'translateX(-' + (this.imageWidth + this.imageGap) + 'px)';
					setTimeout(() => {
						this.stage.style.transition = '';
						const divs = this.stage.querySelectorAll('div');
						if (this.#itemCount % 2 == 0) {
							this.stage.removeChild(divs[1]);
							this.stage.appendChild(divs[1]);
						} else {
							this.stage.removeChild(divs[0]);
							this.stage.appendChild(divs[0]);
						}
						this.stage.style.transform = '';
						this.#isBusy = false;
					},300);
				},50);
			} else {
				setTimeout(() => {
					this.stage.style.transition = '0.3s';
					this.stage.style.transform = 'translateX(' + (this.imageWidth + this.imageGap) + 'px)';
					setTimeout(() => {
						this.stage.style.transition = '';
						const divs = this.stage.querySelectorAll('div');
						if (this.#itemCount % 2 == 0) {
							this.stage.removeChild(divs[divs.length - 1]);
							this.stage.insertBefore(divs[divs.length - 1],divs[1]);
						} else {
							this.stage.removeChild(divs[divs.length - 1]);
							this.stage.insertBefore(divs[divs.length - 1],divs[0]);
						}
						this.stage.style.transform = '';
						this.#isBusy = false;
					},300);
				},50);
			}
		}
		#autoSlide() {
			if (this.#isSwiping == false && this.#focusFlag) {
				this.#slideAction(true);
			}
			this.#loopTimeoutId = setTimeout(() => {
				this.#autoSlide();
			},this.imageSlideInterval);
		}
	}
	//
	new SwipePanel(
		[
			{'src':'./image/panel01.webp','link':'','external':false},
			{'src':'./image/panel02.webp','link':'','external':false},
			{'src':'./image/panel03.webp','link':'','external':false},
			{'src':'./image/panel04.webp','link':'','external':false},
			{'src':'./image/panel05.webp','link':'','external':false},
			{'src':'./image/panel06.webp','link':'','external':false},
		]
	);
})();
動作環境(2024-01-09時点)
・Google Chrome 120
・Microsoft Edge 120
・Mozilla Firefox 122.0b6
・Apple Safari 17.1.2
いつもながらスイッチャー/スワイパーは自作しており、最近はCSSの機能が充実しているのでJavaScriptの記述量は半分程度でスイッチャー/スワイパーが書けるようになった。
この版は極力JavaScriptを使用せずにCSSの記述に重点を置き作成を試みたもので有るが、スワイプ操作等々を含めるとそれなりのJavaScriptコード量になってしまった。ただし実質200行足らずのコンパクト設計である。
この処理を使いたい方はソースコードを参考に自分好みのものに仕上げて欲しい。
スイッチャー/スワイパーはその都度顧客のニーズに合わせて作り変えている。よってライブラリの様に使い易くすることは無いしパラメータ類はコード内に埋め込んでいる。
何らかのライブラリを使うと用途がそのライブラリの制御範囲に限定されてしまう為好きではないし、概ね巨大である。
その都度作る方がコードも最小限で軽量に仕上がるしメンテナンスできる。
ブラウザ互換で困ることも最近は少ない。
2024年1月初版
このサイトについてのお問い合わせはエーオーシステムまでお願いいたします。
ご使用上の過失の有無を問わず、本プログラムの運用において発生した損害に対するいかなる請求があったとしても、その責任を負うものではありません。