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

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

canvasを使用したスイッチャー/スワイパーです。JavaScriptコードを参考に自分好みに仕上げよう。

Animation first と Animation second の混合になります。まずは Animation first で基本の動作を決めて、Animation second で掛け合わせる動作を決めます。

Animation first Animation second

 

Animation first -- 1:アニメ無し 2:左から右 3:左上から右下 4:モザイク 5:中央カーテン 6:菱形 7:楕円
Animation second -- 1:アニメ無し 2:上から下 3:左から右

マウスやタッチによる左右スワイプでの画像切り替え可能。
右下のボタンによる画像切り替え、および、時間経過による画像自動切り替え。
レスポンシブ対応。画像個別に内部リンク外部リンクを設置可能。このサンプルにはリンクは設置されていません。
現時点のコードは以下の通り:
/**
 * 画像切り替え canvas版
 *
 * @author ao-system, Inc.
 * @date 2024-01-08
 * @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: #eee;
	> div {
		max-width: 1920px;
		margin: 0 auto;
		overflow: hidden;
		> #switchPanelStage {
			user-select: none;
			display: grid;
			grid-template-rows: 1fr;
			grid-template-columns: 1fr;
			> canvas {
				z-index: 0;
				grid-row: 1/2;
				grid-column: 1/2;
				width: 100%;
			}
			> div {
				z-index: 1;
				grid-row: 1/2;
				grid-column: 1/2;
				justify-self: flex-end;
				align-self: end;
				display: flex;
				justify-content: center;
				align-items: center;
				column-gap: 5px;
				padding: 0 20px 20px 0;
				@media (width < 670px) {
					column-gap: 3px;
					padding: 0 3px 3px 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: 'canvas',				value: document.createElement('canvas')},
			{prop: 'layerBall',				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: 5000},	//画像と画像の切り替え間隔
			{prop: 'swipeShreshold',		value: 50},		//スワイプ判定幅
		];
		#ctx					= null;		//canvas context
		#loopTimeoutId			= null;		//自動動作のタイマー
		#isBusy					= false;	//false|true
		#pausePlayFlag			= true;		//falseで停止
		#valueIndex				= 0;		//this.#valuesの添え字
		#lastIsLarge			= null;		//null|true|false
		#animationArray			= [];		//描画のbuffer
		#swipeStartX			= 0;		//スワイプ開始位置
		#isSwiping				= false;	//スワイプ中 true|false
		#config					= {};
		#values					= [];
		//
		animationFirstNumber	= 7;	//1|2|3|4|5|6|7
		animationSecondNumber	= 2;	//1|2|3
		//
		constructor(config, values) {
			this.#config = config;
			this.#values = values;
			this.#constProperties.forEach((obj) => Object.defineProperty(this, obj.prop, {value:obj.value, writable:false}));
			this.#ctx = this.canvas.getContext('2d');
			this.#init();
		}
		#init() {
			this.stage.appendChild(this.canvas);
			this.stage.appendChild(this.layerBall);
			//this.#values = shuffle(this.#values);	//シャッフルさせる場合
			this.#setCanvasSize();
			this.#initBall();
			this.#setImage();
			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;
		}
		#initBall() {
			this.#values.map((x,index) => {
				const span = document.createElement('span');
				span.addEventListener('click',() => {this.#slideNumber(index);});
				span.innerHTML = '<img src="' + this.markNavOff + '"><img src="' + this.markNavOn + '">';
				this.layerBall.appendChild(span);
			});
			const pauseImg = new Image();
			pauseImg.src = this.markPause;
			this.buttonPause.appendChild(pauseImg);
			this.layerBall.appendChild(this.buttonPause);
			const playImg = new Image();
			playImg.src = this.markPlay;
			this.buttonPlay.appendChild(playImg);
			this.layerBall.appendChild(this.buttonPlay);
			this.buttonPlay.style.display = 'none';
			this.buttonPause.addEventListener('click',() => this.#setPause());
			this.buttonPlay.addEventListener('click',() => this.#setPlay());
		}
		#initSwipe() {
			this.canvas.addEventListener('click', (e) => { e.preventDefault();});
			this.canvas.addEventListener('mousedown', (e) => { this.#swipeStart(e.clientX); });
			this.canvas.addEventListener('mouseup', (e) => { this.#swipeEnd(e.clientX); });
			this.canvas.addEventListener('touchstart', (e) => { this.#swipeStart(e.touches[0].clientX); });
			this.canvas.addEventListener('touchend', (e) => { this.#swipeEnd(e.changedTouches[0].clientX); });
		}
		#swipeStart(positionX) {
			if (this.#isBusy) {
				return;
			}
			this.#swipeStartX = positionX;
			this.canvas.style.cursor = 'grabbing';
			this.#isSwiping = true;
		}
		#swipeEnd(positionX) {
			if (this.#isSwiping == false) {
				return;
			}
			if (this.#swipeStartX - positionX > this.swipeShreshold) {
				this.#slideNumber(this.#withinRange(this.#valueIndex + 1));
			} else if (this.#swipeStartX - positionX < -this.swipeShreshold) {
				this.#slideNumber(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.canvas.style.cursor = '';
			this.#isSwiping = false;
		}
		#setCanvasSize() {
			const isLarge = this.#getIsLarge();
			if (this.#lastIsLarge != isLarge) {
				this.canvas.width = isLarge ? this.#config.width1 : this.#config.width2;
				this.canvas.height = isLarge ? this.#config.height1 : this.#config.height2;
				this.#lastIsLarge = isLarge;
			}
		}
		#getIsLarge() {
			return window.matchMedia('(width >= ' + this.#config.breakpoint + 'px)').matches;
		}
		#setImage() {
			this.#setCanvasSize();
			const img = new Image();
			img.addEventListener('load',() => this.#imageAnimation(img));
			img.src = this.#getIsLarge() ? this.#values[this.#valueIndex].image1 : this.#values[this.#valueIndex].image2;
			this.#setBall();
		}
		#setBall() {
			const spans = this.layerBall.querySelectorAll('span');
			spans.forEach((elm) => elm.classList.remove('on'));
			spans[this.#valueIndex].classList.add('on');
		}
		#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';
		}
		#autoSlide() {
			this.#loopTimeoutId = setTimeout(() => {
				this.#slideAction();
			},this.imageChangeInterval);
		}
		#slideAction() {
			if (this.#pausePlayFlag) {
				this.#valueIndex = this.#withinRange(this.#valueIndex + 1);
				this.#setImage();
			}
			this.#autoSlide();
		}
		#slideNumber(num) {
			if (this.#isBusy) {
				return;
			}
			clearTimeout(this.#loopTimeoutId);
			//setPlay();
			this.#valueIndex = num;
			this.#setImage();
			this.#autoSlide();
		}
		#withinRange(nextNumber) {
			let num = nextNumber;
			if (num < 0) {
				num = this.#values.length - 1;
			}
			return num % this.#values.length;
		}

		//------------------------------

		//animationの振り分け後に描画
		#imageAnimation(img) {
			this.#animationArray = [];
			if (this.animationFirstNumber == 1) {
				this.#imageAnimationFirst01(img);
			} else if (this.animationFirstNumber == 2) {
				this.#imageAnimationFirst02(img);
			} else if (this.animationFirstNumber == 3) {
				this.#imageAnimationFirst03(img);
			} else if (this.animationFirstNumber == 4) {
				this.#imageAnimationFirst04(img);
			} else if (this.animationFirstNumber == 5) {
				this.#imageAnimationFirst05(img);
			} else if (this.animationFirstNumber == 6) {
				this.#imageAnimationFirst06(img);
			} else if (this.animationFirstNumber == 7) {
				this.#imageAnimationFirst07(img);
			}
			this.#ctxDrawImage(img);
		}
		//同じdelayの描画はまとめて処理する
		#ctxDrawImage(img) {
			this.#animationArray.sort((a,b) => {
				return a.delay - b.delay;
			});
			let ary = [];
			let lastDelay = -1;
			for (let i = 0; i < this.#animationArray.length; i++) {
				if (lastDelay != this.#animationArray[i].delay) {
					if (ary.length > 0) {
						const clonedAry = [...ary];
						setTimeout(() => {
							clonedAry.forEach((obj) => {
								this.#ctx.drawImage(img, obj.srcX, obj.srcY, obj.srcW, obj.srcH, obj.dstX, obj.dstY, obj.dstW, obj.dstH);
							});
						},clonedAry[0].delay);
					}
					lastDelay = this.#animationArray[i].delay;
					ary = [];
				}
				ary.push(this.#animationArray[i]);
			}
			if (ary.length > 0) {
				setTimeout(() => {
					ary.forEach((obj) => {
						this.#ctx.drawImage(img, obj.srcX, obj.srcY, obj.srcW, obj.srcH, obj.dstX, obj.dstY, obj.dstW, obj.dstH);
					});
				},ary[0].delay);
			}
		}

		//------------------------------

		//first animation | no animation
		#imageAnimationFirst01(img) {
			const delay = 0;
			this.#imageAnimationSecond(delay, img, 0, 0, img.width, img.height, 0, 0, this.canvas.width, this.canvas.height);
		}
		//first animation | left to right
		#imageAnimationFirst02(img) {
			const divX = 20;
			const delayRatio = 25;
			const srcW = img.width / divX;
			const srcH = img.height;
			const dstW = this.canvas.width / divX;
			const dstH = this.canvas.height;
			const srcY = 0;
			const dstY = 0;
			for (let x = 0; x < divX; x++) {
				const srcX = srcW * x;
				const dstX = dstW * x;
				const delay = delayRatio * x;
				this.#imageAnimationSecond(delay, img, srcX, srcY, srcW, srcH, dstX, dstY, dstW, dstH);
			}
		}
		//first animation | left top to right bottom
		#imageAnimationFirst03(img) {
			const divX = 10;
			const divY = 10;
			const delayRatio = 10;
			const srcW = img.width / divX;
			const srcH = img.height / divY;
			const dstW = this.canvas.width / divX;
			const dstH = this.canvas.height / divY;
			for (let y = 0; y < divY; y++) {
				for (let x = 0; x < divX; x++) {
					const srcX = srcW * x;
					const srcY = srcH * y;
					const dstX = dstW * x;
					const dstY = dstH * y;
					const delay = delayRatio * y * x;
					this.#imageAnimationSecond(delay, img, srcX, srcY, srcW, srcH, dstX, dstY, dstW, dstH);
				}
			}
		}
		//first animation | mosaic
		#imageAnimationFirst04(img) {
			const divX = 40;
			const divY = 40;
			const delayRatio = 0.4;
			const originalArray = Array.from({length: divX * divY}, (_, index) => index);
			const shuffledArray = this.#shuffle(originalArray);
			const srcW = img.width / divX;
			const srcH = img.height / divY;
			const dstW = this.canvas.width / divX;
			const dstH = this.canvas.height / divY;
			let c = 0;
			for (let y = 0; y < divY; y++) {
				for (let x = 0; x < divX; x++) {
					const srcX = srcW * x;
					const srcY = srcH * y;
					const dstX = dstW * x;
					const dstY = dstH * y;
					const delay = delayRatio * shuffledArray[c];
					this.#imageAnimationSecond(delay, img, srcX, srcY, srcW, srcH, dstX, dstY, dstW, dstH);
					c += 1;
				}
			}
		}
		//first animation | center curtain
		#imageAnimationFirst05(img) {
			const divX = 40;
			const divY = 40;
			const delayRatio = 1;
			const srcW = img.width / divX;
			const srcH = img.height / divY;
			const dstW = this.canvas.width / divX;
			const dstH = this.canvas.height / divY;
			for (let y = 0; y < divY; y++) {
				for (let x = 0; x < divX; x++) {
					const srcX = srcW * x;
					const srcY = srcH * y;
					const dstX = dstW * x;
					const dstY = dstH * y;
					const delay = delayRatio * (Math.abs(y - (divY / 2)) * Math.abs(x - (divX / 2)));
					this.#imageAnimationSecond(delay, img, srcX, srcY, srcW, srcH, dstX, dstY, dstW, dstH);
				}
			}
		}
		//first animation | center rhombus
		#imageAnimationFirst06(img) {
			const divX = 40;
			const divY = 40;
			const delayRatio = 15;
			const srcW = img.width / divX;
			const srcH = img.height / divY;
			const dstW = this.canvas.width / divX;
			const dstH = this.canvas.height / divY;
			for (let y = 0; y < divY; y++) {
				for (let x = 0; x < divX; x++) {
					const srcX = srcW * x;
					const srcY = srcH * y;
					const dstX = dstW * x;
					const dstY = dstH * y;
					const delay = delayRatio * (Math.abs(y - (divY / 2)) + Math.abs(x - (divX / 2)));
					this.#imageAnimationSecond(delay, img, srcX, srcY, srcW, srcH, dstX, dstY, dstW, dstH);
				}
			}
		}
		//first animation | center ellipse
		#imageAnimationFirst07(img) {
			const divX = 20;
			const divY = 20;
			const delayRatio = 50;
			const srcW = img.width / divX;
			const srcH = img.height / divY;
			const dstW = this.canvas.width / divX;
			const dstH = this.canvas.height / divY;
			for (let y = 0; y < divY; y++) {
				for (let x = 0; x < divX; x++) {
					const srcX = srcW * x;
					const srcY = srcH * y;
					const dstX = dstW * x;
					const dstY = dstH * y;
					const delay = delayRatio * Math.sqrt(Math.abs(y - (divY / 2)) ** 2 + Math.abs(x - (divX / 2)) ** 2);
					this.#imageAnimationSecond(delay, img, srcX, srcY, srcW, srcH, dstX, dstY, dstW, dstH);
				}
			}
		}

		//------------------------------

		//second animation
		#imageAnimationSecond(delay, img, srcX, srcY, srcW, srcH, dstX, dstY, dstW, dstH) {
			if (this.animationSecondNumber == 1) {
				this.#imageAnimationSecond01(delay, img, srcX, srcY, srcW, srcH, dstX, dstY, dstW, dstH);
			} else if (this.animationSecondNumber == 2) {
				this.#imageAnimationSecond02(delay, img, srcX, srcY, srcW, srcH, dstX, dstY, dstW, dstH);
			} else if (this.animationSecondNumber == 3) {
				this.#imageAnimationSecond03(delay, img, srcX, srcY, srcW, srcH, dstX, dstY, dstW, dstH);
			}
		}
		//second animation | no animation
		#imageAnimationSecond01(delay, img, srcX, srcY, srcW, srcH, dstX, dstY, dstW, dstH) {
			const delay2 = delay;
			this.#imageAnimationPush(delay2, img, srcX, srcY, srcW + 1, srcH + 1, dstX, dstY, dstW + 1, dstH + 1);
		}
		//second animation | top to bottom
		#imageAnimationSecond02(delay, img, srcX, srcY, srcW, srcH, dstX, dstY, dstW, dstH) {
			const divY = 6;
			const delayRatio = 60;
			const srcH2 = srcH / divY;
			const dstH2 = dstH / divY;
			for (let y = 0; y < divY; y++) {
				const srcY2 = srcY + (srcH2 * y);
				const dstY2 = dstY + (dstH2 * y);
				const delay2 = delay + (delayRatio * y);
				this.#imageAnimationPush(delay2, img, srcX, srcY2, srcW + 1, srcH2 + 1, dstX, dstY2, dstW + 1, dstH2 + 1);
			}
		}
		//second animation | left to right
		#imageAnimationSecond03(delay, img, srcX, srcY, srcW, srcH, dstX, dstY, dstW, dstH) {
			const divX = 6;
			const delayRatio = 60;
			const srcW2 = srcW / divX;
			const dstW2 = dstW / divX;
			for (let x = 0; x < divX; x++) {
				const srcX2 = srcX + (srcW2 * x);
				const dstX2 = dstX + (dstW2 * x);
				const delay2 = delay + (delayRatio * x);
				this.#imageAnimationPush(delay2, img, srcX2, srcY, srcW2 + 1, srcH + 1, dstX2, dstY, dstW2 + 1, dstH + 1);
			}
		}

		//------------------------------

		//画像描画を配列に収める
		#imageAnimationPush(delay2, img, srcX, srcY, srcW, srcH, dstX, dstY, dstW, dstH) {
			const delay3 = parseInt(delay2);
			this.#animationArray.push({
				'delay': delay3,
				//'img': img,
				'srcX': srcX,
				'srcY': srcY,
				'srcW': srcW,
				'srcH': srcH,
				'dstX': dstX,
				'dstY': dstY,
				'dstW': dstW,
				'dstH': dstH,
			});
		}
	}
	//
	const switchImage = new SwitchImage(
		//config
		{
			'breakpoint': 700,
			'width1': 1920,
			'height1': 700,
			'width2': 700,
			'height2': 1000,
		},
		//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',
			},
		]
	);

	//暫定機能ここから
	document.getElementById('animationFirst').addEventListener('input',(e) => {
		const val = e.target.value;
		switchImage.animationFirstNumber = val;
		document.getElementById('animationFirstValue').innerHTML = val;
	});
	document.getElementById('animationFirstValue').innerHTML = document.getElementById('animationFirst').value;
	document.getElementById('animationSecond').addEventListener('input',(e) => {
		const val = e.target.value;
		switchImage.animationSecondNumber = val;
		document.getElementById('animationSecondValue').innerHTML = val;
	});
	document.getElementById('animationSecondValue').innerHTML = document.getElementById('animationSecond').value;
	//暫定機能ここまで

})();
動作環境(2024-01-08時点)
・Google Chrome 120
・Microsoft Edge 120
・Mozilla Firefox 122.0b6
・Apple Safari 17.1.2
いつもながらスイッチャー/スワイパーは自作しており、最近はCSSの機能が充実しているのでJavaScriptの記述量は半分程度でスイッチャー/スワイパーが書けるようになった。
普段はCANVASを使用しないスイッチャー/スワイパーを書いているが、この度CANVASを使用した場合で書いてみた。
CANVASではフェード処理が苦手なのは分かっていたのでそれ以外の処理で作成してみた。
この処理を使いたい方はソースコードを参考に自分好みのものに仕上げて欲しい。
スイッチャー/スワイパーはその都度顧客のニーズや状況に合わせて作り変えている。よってライブラリの様に使い易くすることは無いしパラメータ類はコード内に埋め込んでいる。
何らかのライブラリを使うと用途がそのライブラリの制御範囲に限定されてしまう為好きではないし、概ね巨大である。
その都度作る方がコードも最小限で軽量に仕上がるしメンテナンスできる。
ブラウザ互換で困ることも最近は少ない。JavaScriptは使い捨てが原則。
2024年1月初版
このサイトについてのお問い合わせはエーオーシステムまでお願いいたします。
ご使用上の過失の有無を問わず、本プログラムの運用において発生した損害に対するいかなる請求があったとしても、その責任を負うものではありません。