JavaScript

画像ビューワー

マウス操作で画像を拡大縮小、パンニング。見たい位置を拡大表示。拡大縮小で位置がずれません。
マウス操作向け(非タッチデバイス)に設計されています。

マウス左クリックで掴んで移動。マウスホイールで拡大縮小。

このビューワーは拡大縮小しても位置がずれません。位置を見失うことは有りません。拡大縮小を繰り返してもマウスポインタの位置をキープします。
チラシなどの軽量ビューワーとして使用いただけるでしょう。
現時点のコードは以下の通り: imageviewer.js
/**
 * 画像表示 エリア内で拡大縮小
 *
 * @author ao-system, Inc.
 * @date 2024-02-05
 */

(() => {
	'use strict';
	new class {
		////constant
		#contentMagnificationWidth	= 3000;				//画像サイズ
		#contentAspectRatio			= (1500 / 2121);	//画像アスペクト比
		#stage						= document.querySelector('div.stage');
		#figure						= document.querySelector('div.stage > figure');
		#rangeScale					= document.getElementById('rangeScale');
		#rangeScaleValue			= document.getElementById('rangeScaleValue');
		#stageHeight				= 600;
		#magnificationMax			= 100;	//拡大率最大% floatは誤差が出るのでintで計算する
		#magnificationMin			= 10;	//拡大率最小% floatは誤差が出るのでintで計算する
		#magnificationRate			= 50;	//拡大率%。初期値。floatは誤差が出るのでintで計算する
		#magnificationStep			= 5;	//拡大ステップ% floatは誤差が出るのでintで計算する
		////variable
		#isDragging					= false;	//ドラッグ操作 ドラッグ実行中
		#startMouseX				= 0;		//ドラッグ操作 マウス位置
		#startMouseY				= 0;		//ドラッグ操作 マウス位置
		#startFigureX				= 0;		//ドラッグ操作 画像位置
		#startFigureY				= 0;		//ドラッグ操作 画像位置
		constructor() {
			this.#init();
		}
		#init() {
			//初期値
			this.#figure.style.width = this.#contentMagnificationWidth + 'px';
			this.#figure.style.height = (this.#contentMagnificationWidth / this.#contentAspectRatio) + 'px';
			this.#rangeScale.setAttribute('min',this.#magnificationMin);
			this.#rangeScale.setAttribute('max',this.#magnificationMax);
			this.#rangeScale.setAttribute('step',this.#magnificationStep);
			this.#figure.style.left = '0px';
			this.#figure.style.top = '0px';
			//リスナー登録
			this.#setListenerSlider();
			this.#setListenerWheel();
			this.#setListenerDrag();
			//初期サイズにする
			this.#magnificationRate = this.#magnificationMin;
			const rect = this.#stage.getBoundingClientRect();
			const positionLeft = (rect.width / 2) + rect.left - ((this.#contentMagnificationWidth * this.#magnificationMin / this.#magnificationMax) / 4);
			const positionTop = (rect.height / 2) + (rect.top / 2) - (this.#stageHeight * this.#magnificationMin / this.#magnificationMax) - ((window.pageYOffset || document.documentElement.scrollTop) / 2);
			this.#scaleChange(positionLeft,positionTop);
		}
		//拡大スライダー操作のイベントリスナー登録
		#setListenerSlider() {
			this.#rangeScale.addEventListener('input', (event) => {
				this.#magnificationRate = parseInt(event.target.value);
				const rect = this.#stage.getBoundingClientRect();
				this.#scaleChange(rect.width / 2 + rect.left, rect.height / 2 + rect.top);
			});
		}
		//ホイール操作のイベントリスナー登録
		#setListenerWheel() {
			this.#stage.addEventListener('wheel', (event) => {
				event.preventDefault();
				//拡大率 this.#magnificationRate を求める
				const lastMagnificationRate = this.#magnificationRate;
				const delta = event.wheelDelta ? event.wheelDelta : -event.deltaY;
				if (delta > 0) {
					this.#magnificationRate += this.#magnificationStep;
					if (this.#magnificationRate > this.#magnificationMax) {
						this.#magnificationRate = this.#magnificationMax;
					}
				} else if (delta < 0) {
					this.#magnificationRate -= this.#magnificationStep;
					if (this.#magnificationRate < this.#magnificationMin) {
						this.#magnificationRate = this.#magnificationMin;
					}
				}
				if (lastMagnificationRate == this.#magnificationRate) {	//拡大率に変化が無ければ何もしない
					return;
				}
				this.#scaleChange(event.clientX, event.clientY);
			});
		}
		//拡大縮小実行
		#scaleChange(mouseX, mouseY) {
			//拡大縮小前の値
			const figureRect = this.#figure.getBoundingClientRect();
			const figureWidth = figureRect.width;
			const figureHeight = figureRect.height;
			const figureLeft = figureRect.left;
			const figureTop = figureRect.top;
			//拡大縮小
			this.#figure.style.transform = 'scale(' + (this.#magnificationRate / this.#magnificationMax) + ')';
			//拡大縮小後の値
			const figureRect2 = this.#figure.getBoundingClientRect();
			const figureWidth2 = figureRect2.width;
			const figureHeight2 = figureRect2.height;
			//拡大縮小の前と後を比較して差をずらす
			const pLeft = (parseFloat(this.#figure.style.left) - ((figureWidth - figureWidth2) / 2));		//左端基準に拡大縮小した場合に停止する位置
			const pRight = (parseFloat(this.#figure.style.left) + ((figureWidth - figureWidth2) / 2));	//右端基準に拡大縮小した場合に停止する位置
			const pRatioX = (mouseX - figureLeft) / figureWidth;	//figureに対するマウス位置の割合
			this.#figure.style.left = (pLeft + ((pRight - pLeft) * pRatioX)) + 'px';
			//拡大縮小の前と後を比較して差をずらす
			const pTop = (parseFloat(this.#figure.style.top) - ((figureHeight - figureHeight2) / 2));		//上端基準に拡大縮小した場合に停止する位置
			const pBottom = (parseFloat(this.#figure.style.top) + ((figureHeight - figureHeight2) / 2));	//下端基準に拡大縮小した場合に停止する位置
			const pRatioY = (mouseY - figureTop) / figureHeight;	//figureに対するマウス位置の割合
			this.#figure.style.top = (pTop + ((pBottom - pTop) * pRatioY)) + 'px';
			this.#rangeScaleValue.textContent = String(parseInt(this.#magnificationRate / this.#magnificationMax * 100)).padStart(3,'0') + '%';
			this.#rangeScale.value = this.#magnificationRate;
		}
		//ドラッグのイベントリスナー登録
		#setListenerDrag() {
			this.#figure.addEventListener('mousedown', (event) => {
				this.#isDragging = true;
				this.#startMouseX = event.clientX;
				this.#startMouseY = event.clientY;
				this.#startFigureX = this.#figure.offsetLeft;
				this.#startFigureY = this.#figure.offsetTop;
				this.#figure.style.cursor = 'grabbing';
			});
			this.#figure.addEventListener('mousemove', (event) => {
				if (this.#isDragging) {
					const deltaX = event.clientX - this.#startMouseX;
					const deltaY = event.clientY - this.#startMouseY;
					this.#figure.style.left = (this.#startFigureX + deltaX) + 'px';
					this.#figure.style.top = (this.#startFigureY + deltaY) + 'px';
					this.#figure.style.cursor = 'grabbing';
					if (!event.buttons) {
						this.#isDragging = false;
						this.#figure.style.cursor = 'grab';
					}
				}
			});
			this.#figure.addEventListener('mouseup', () => {
				if (this.#isDragging) {
					this.#isDragging = false;
					this.#figure.style.cursor = 'grab';
				}
			});
			this.#figure.addEventListener('touchstart', (event) => {
				this.#isDragging = true;
				const touch = event.touches[0];
				this.#startMouseX = touch.clientX;
				this.#startMouseY = touch.clientY;
				this.#startFigureX = this.#figure.offsetLeft;
				this.#startFigureY = this.#figure.offsetTop;
			});
			this.#figure.addEventListener('touchmove', (event) => {
				if (this.#isDragging) {
					const touch = event.touches[0];
					const deltaX = touch.clientX - this.#startMouseX;
					const deltaY = touch.clientY - this.#startMouseY;
					this.#figure.style.left = (this.#startFigureX + deltaX) + 'px';
					this.#figure.style.top = (this.#startFigureY + deltaY) + 'px';
				}
			});
			this.#figure.addEventListener('touchend', () => {
				if (this.#isDragging) {
					this.#isDragging = false;
				}
			});
		}
	}
})();
2025年1月初版
このサイトについてのお問い合わせはエーオーシステムまでお願いいたします。
ご使用上の過失の有無を問わず、本プログラムの運用において発生した損害に対するいかなる請求があったとしても、その責任を負うものではありません。