画像切り替え switcher スイッチャー swiper スワイパー

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

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

画像切り替え switcher スイッチャー swiper スワイパー

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

マウスやタッチによる左右スワイプでの画像切り替え可能。
右下のボタンによる画像切り替え、および、時間経過による画像自動切り替え。
レスポンシブ対応。画像個別に内部リンク外部リンクを設置可能。このサンプルにはリンクは設置されていません。
現時点のコードは以下の通り:
/**
 * 画像切り替え
 *
 * @author ao-system, Inc.
 * @date 2022-10-06
 * @date 2024-01-15
 * @date 2024-01-17 Rewrite the code using the class syntax.
 *

<section class="switchpanel">
	<div>
		<div id="switchPanelStage"></div>
	</div>
</section>

<style>
section.switchPanel {
	background-color: #000;
	> div {
		max-width: 1920px;
		margin: 0 auto;
		overflow: hidden;
		> #switchPanelStage {
			user-select: none;
			display: grid;
			grid-template-rows: 1fr;
			grid-template-columns: 1fr;
			> img:nth-of-type(1) {
				z-index: 0;
				grid-row: 1/2;
				grid-column: 1/2;
				width: 100%;
			}
			> img:nth-of-type(2) {
				z-index: 1;
				grid-row: 1/2;
				grid-column: 1/2;
				width: 100%;
				cursor: ew-resize;
			}
			> div {
				grid-row: 1/2;
				grid-column: 1/2;
				z-index: 2;
				justify-self: flex-end;
				align-self: end;
				display: flex;
				justify-content: center;
				align-items: center;
				column-gap: 5px;
				padding: 0 20px 17px 0;
				@media (width < 670px) {
					column-gap: 3px;
					padding: 0 4px 4px 0;
					> span {
						> img {
							width: 16px;
						}
					}
				}
				> span {
					cursor: pointer;
					> img:nth-of-type(1) {
						display: block;
					}
					> img:nth-of-type(2) {
						display: none;
					}
					&.on {
						> img:nth-of-type(1) {
							display: none;
						}
						> img:nth-of-type(2) {
							display: block;
						}
					}
				}
			}
		}
	}
}
</style>
 *
 */

(() => {
	'use strict';
	class SwitchImage {
		#constProperties = [
			{prop: 'stage',					value: document.getElementById('switchPanelStage')},
			{prop: 'layerImg0',				value: document.createElement('img')},
			{prop: 'layerImg1',				value: document.createElement('img')},
			{prop: 'layerNav',				value: document.createElement('div')},
			{prop: 'markPause',				value: './image/panel_pause.svg'},
			{prop: 'markPlay',				value: './image/panel_play.svg'},
			{prop: 'markNavOn',				value: './image/panel_ball_on.svg'},
			{prop: 'markNavOff',			value: './image/panel_ball_off.svg'},
			{prop: 'buttonPause',			value: document.createElement('span')},
			{prop: 'buttonPlay',			value: document.createElement('span')},
			{prop: 'imageChangeInterval',	value: 4000},	//画像と画像の時間間隔
			{prop: 'slideWidth',			value: 50},		//切り替わるスライド幅
			{prop: 'slideTime',				value: 500},	//スライド時間
			{prop: 'swipeShreshold',		value: 50},		//スワイプ判定幅
		];
		#loopTimeoutId			= null;		//自動動作のタイマー
		#isBusy					= false;	//動作中
		#pausePlayFlag			= true;		//falseで停止
		#focusFlag				= true;		//フォーカスが外れている時は動作させないため
		#valueIndex				= 0;		//this.#valuesの添え字
		#slideDirection			= 1;		//1|-1	正方向,逆方向
		#swipeStartX			= 0;		//スワイプ開始位置
		#isSwiping				= false;	//スワイプ中
		#config					= {};
		#values					= [];
		//
		constructor(config, values) {
			this.#config = config;
			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.stage.appendChild(this.layerImg0);
			this.stage.appendChild(this.layerImg1);
			this.stage.appendChild(this.layerNav);
			this.layerImg0.setAttribute('draggable','false');
			this.layerImg1.setAttribute('draggable','false');
			this.layerImg1.src = this.#getImage(1);
			this.layerImg1.animate(
				[{opacity: 0}],
				{delay:0,duration:1, easing:'linear',fill:'forwards', },
			);
			this.layerImg0.src = this.#getImage(0);
			this.layerImg0.onload = () => {
				this.#initNav();
				this.#initSwipe();
				this.#ballOn();
				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;
		}
		#getIsLarge() {
			return window.matchMedia('(width >= ' + this.#config.breakpoint + 'px)').matches;
		}
		#getImage(ptr) {
			return this.#getIsLarge() ? this.#values[ptr].image1 : this.#values[ptr].image2;
		}
		#initNav() {
			this.#values.map((x,index) => {
				const span = document.createElement('span');
				span.addEventListener('click',() => {
					if (this.#isBusy) {
						return;
					}
					this.#slideAction(index);
				});
				span.innerHTML = '<img src="' + this.markNavOff + '"><img src="' + this.markNavOn + '">';
				this.layerNav.appendChild(span);
			});
			const pauseImg = new Image();
			pauseImg.src = this.markPause;
			this.buttonPause.appendChild(pauseImg);
			this.layerNav.appendChild(this.buttonPause);
			const playImg = new Image();
			playImg.src = this.markPlay;
			this.buttonPlay.appendChild(playImg);
			this.layerNav.appendChild(this.buttonPlay);
			this.buttonPlay.style.display = 'none';
			this.buttonPause.addEventListener('click',() => this.#setPause());
			this.buttonPlay.addEventListener('click',() => this.#setPlay());
		}
		#initSwipe() {
			this.layerImg1.addEventListener('click', (e) => { e.preventDefault();});
			this.layerImg1.addEventListener('mousedown', (e) => { this.#swipeStart(e.clientX); });
			this.layerImg1.addEventListener('mouseup', (e) => { this.#swipeEnd(e.clientX); });
			this.layerImg1.addEventListener('touchstart', (e) => { this.#swipeStart(e.touches[0].clientX); });
			this.layerImg1.addEventListener('touchend', (e) => { this.#swipeEnd(e.changedTouches[0].clientX); });
		}
		#swipeStart(positionX) {
			if (this.#isBusy) {
				return;
			}
			this.#swipeStartX = positionX;
			this.stage.style.cursor = 'grabbing';
			this.#isSwiping = true;
		}
		#swipeEnd(positionX) {
			if (this.#isSwiping == false) {
				return;
			}
			if (this.#swipeStartX - positionX > this.swipeShreshold) {
				this.#slideAction(this.#withinRange(this.#valueIndex + 1));
			} else if (this.#swipeStartX - positionX < -this.swipeShreshold) {
				this.#slideDirection = -1;
				this.#slideAction(this.#withinRange(this.#valueIndex - 1));
			} else {
				if (this.#values[this.#valueIndex].link) {
					if (this.#values[this.#valueIndex].external) {
						window.open(this.#values[this.#valueIndex].link, '_blank');
					} else {
						window.location.href = this.#values[this.#valueIndex].link;
					}
					return;
				}
			}
			this.stage.style.cursor = '';
			this.#isSwiping = false;
		}
		#setPause() {
			this.#pausePlayFlag = false;
			this.buttonPause.style.display = 'none';
			this.buttonPlay.style.display = 'block';
		}
		#setPlay() {
			this.#pausePlayFlag = true;
			this.buttonPause.style.display = 'block';
			this.buttonPlay.style.display = 'none';
		}
		#slideForward() {
			if (this.#isBusy) {
				return;
			}
			this.#isBusy = true;
			this.#ballOn();
			this.layerImg1.src = this.#getImage(this.#valueIndex);
			this.layerImg1.onload = () => {
				this.layerImg1.animate(
					[
						{opacity: 0,transform:'translateX(' + (this.slideWidth * this.#slideDirection) + 'px)'},
						{opacity: 1,transform:'translateX(0px)'},
					],
					{delay:0,duration:this.slideTime, easing:'ease-out',fill:'forwards', },
				);
				setTimeout(() => {
					this.layerImg0.src = this.#getImage(this.#valueIndex);
					this.layerImg0.onload = () => {
						this.layerImg1.animate(
							[{opacity: 0}],
							{delay:0,duration:1, easing:'linear',fill:'forwards', },
						);
					};
					this.#slideDirection = 1;
					this.#isBusy = false;
				},this.slideTime + 100);
			};
		}
		#autoSlide() {
			this.#loopTimeoutId = setTimeout(() => {
				if (this.#pausePlayFlag && this.#focusFlag) {
					this.#slideAction();
				} else {
					this.#autoSlide();
				}
			}, this.imageChangeInterval - this.slideTime);
		}
		#slideAction(num = this.#valueIndex + 1) {
			clearTimeout(this.#loopTimeoutId);
			this.#valueIndex = this.#withinRange(num);
			this.#slideForward();
			this.#autoSlide();
		}
		#ballOn() {
			const spans = this.layerNav.querySelectorAll('span');
			spans.forEach((elm) => elm.classList.remove('on'));
			spans[this.#valueIndex].classList.add('on');
		}
		#withinRange(nextNumber) {
			const num = (nextNumber >= 0) ? nextNumber : this.#values.length - 1;
			return num % this.#values.length;
		}
	}
	//
	new SwitchImage(
		//config
		{
			'breakpoint': 700,
		},
		//values
		[
			{
				'link': '',
				'external': false,
				'image1': './image/panel01.webp',
				'image2': './image/panel01b.webp',
			},
			{
				'link': '',
				'external': false,
				'image1': './image/panel02.webp',
				'image2': './image/panel02b.webp',
			},
			{
				'link': '',
				'external': false,
				'image1': './image/panel03.webp',
				'image2': './image/panel03b.webp',
			},
			{
				'link': '',
				'external': false,
				'image1': './image/panel04.webp',
				'image2': './image/panel04b.webp',
			},
			{
				'link': '',
				'external': false,
				'image1': './image/panel05.webp',
				'image2': './image/panel05b.webp',
			},
			{
				'link': '',
				'external': false,
				'image1': './image/panel06.webp',
				'image2': './image/panel06b.webp',
			},
		]
	);
})();
動作環境(2024-01-09時点)
・Google Chrome 120
・Microsoft Edge 120
・Mozilla Firefox 122.0b6
・Apple Safari 17.1.2
いつもながらスイッチャー/スワイパーは自作しており、最近はCSSの機能が充実しているのでJavaScriptの記述量は半分程度でスイッチャー/スワイパーが書けるようになった。
この版は極力JavaScriptを使用せずにCSSの記述に重点を置き作成を試みたもので有るが、スワイプ操作等々を含めるとそれなりのJavaScriptコード量になってしまった。ただし実質200行足らずのコンパクト設計である。
この処理を使いたい方はソースコードを参考に自分好みのものに仕上げて欲しい。
スイッチャー/スワイパーはその都度顧客のニーズや状況に合わせて作り変えている。よってライブラリの様に使い易くすることは無いしパラメータ類はコード内に埋め込んでいる。
何らかのライブラリを使うと用途がそのライブラリの制御範囲に限定されてしまう為好きではないし、概ね巨大である。
その都度作る方がコードも最小限で軽量に仕上がるしメンテナンスできる。
ブラウザ互換で困ることも最近は少ない。JavaScriptは使い捨てが原則。
2024年1月初版
このサイトについてのお問い合わせはエーオーシステムまでお願いいたします。
ご使用上の過失の有無を問わず、本プログラムの運用において発生した損害に対するいかなる請求があったとしても、その責任を負うものではありません。