Spaces:
Running
Running
| /** | |
| * AspectCalc サンプルページのメインスクリプト | |
| */ | |
| class AspectCalcApp { | |
| constructor() { | |
| this.initializeElements(); | |
| this.bindEvents(); | |
| this.setupClipboardPaste(); | |
| } | |
| /** | |
| * DOM要素の初期化 | |
| */ | |
| initializeElements() { | |
| this.dropZone = document.getElementById('dropZone'); | |
| this.fileInput = document.getElementById('fileInput'); | |
| this.fileSelectBtn = document.getElementById('fileSelectBtn'); | |
| this.pasteBtn = document.getElementById('pasteBtn'); | |
| this.widthInput = document.getElementById('widthInput'); | |
| this.heightInput = document.getElementById('heightInput'); | |
| this.calculateBtn = document.getElementById('calculateBtn'); | |
| this.previewArea = document.getElementById('previewArea'); | |
| this.resolutionInfo = document.getElementById('resolutionInfo'); | |
| this.actualWidth = document.getElementById('actualWidth'); | |
| this.actualHeight = document.getElementById('actualHeight'); | |
| this.resultsArea = document.getElementById('resultsArea'); | |
| this.integerRatio = document.getElementById('integerRatio'); | |
| this.approximateRatio = document.getElementById('approximateRatio'); | |
| this.standardRatio = document.getElementById('standardRatio'); | |
| this.toast = document.getElementById('toast'); | |
| this.toastMessage = document.getElementById('toastMessage'); | |
| } | |
| /** | |
| * イベントリスナーの設定 | |
| */ | |
| bindEvents() { | |
| // ファイル選択 | |
| this.fileSelectBtn.addEventListener('click', () => { | |
| this.fileInput.click(); | |
| }); | |
| this.fileInput.addEventListener('change', (e) => { | |
| this.handleFileSelect(e.target.files[0]); | |
| }); | |
| // ドラッグ&ドロップ | |
| this.dropZone.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| this.dropZone.classList.add('dragover'); | |
| }); | |
| this.dropZone.addEventListener('dragleave', () => { | |
| this.dropZone.classList.remove('dragover'); | |
| }); | |
| this.dropZone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| this.dropZone.classList.remove('dragover'); | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0) { | |
| this.handleFileSelect(files[0]); | |
| } | |
| }); | |
| // 手動計算 | |
| this.calculateBtn.addEventListener('click', () => { | |
| this.handleManualInput(); | |
| }); | |
| // Enterキーでの計算 | |
| this.widthInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| this.handleManualInput(); | |
| } | |
| }); | |
| this.heightInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| this.handleManualInput(); | |
| } | |
| }); | |
| // コピーボタン | |
| document.querySelectorAll('.copy-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| const ratioType = e.target.getAttribute('data-ratio'); | |
| this.copyRatio(ratioType); | |
| }); | |
| }); | |
| } | |
| /** | |
| * クリップボード貼り付け機能の設定 | |
| */ | |
| setupClipboardPaste() { | |
| // キーボードイベント(Ctrl+V) | |
| document.addEventListener('keydown', (e) => { | |
| if (e.ctrlKey && e.key === 'v') { | |
| e.preventDefault(); | |
| this.handleClipboardPaste(); | |
| } | |
| }); | |
| // 貼り付けボタン | |
| this.pasteBtn.addEventListener('click', () => { | |
| this.handleClipboardPaste(); | |
| }); | |
| } | |
| /** | |
| * ファイル選択の処理 | |
| */ | |
| handleFileSelect(file) { | |
| if (!file || !file.type.startsWith('image/')) { | |
| this.showToast('有効な画像ファイルを選択してください', 'error'); | |
| return; | |
| } | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| this.processImage(e.target.result, file.name); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| /** | |
| * クリップボード貼り付けの処理 | |
| */ | |
| async handleClipboardPaste() { | |
| try { | |
| const clipboardItems = await navigator.clipboard.read(); | |
| for (const clipboardItem of clipboardItems) { | |
| for (const type of clipboardItem.types) { | |
| if (type.startsWith('image/')) { | |
| const blob = await clipboardItem.getType(type); | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| this.processImage(e.target.result, 'クリップボード画像'); | |
| }; | |
| reader.readAsDataURL(blob); | |
| return; | |
| } | |
| } | |
| } | |
| this.showToast('クリップボードに画像が見つかりません', 'error'); | |
| } catch (error) { | |
| console.error('クリップボード読み取りエラー:', error); | |
| this.showToast('クリップボードの読み取りに失敗しました', 'error'); | |
| } | |
| } | |
| /** | |
| * 手動入力の処理 | |
| */ | |
| handleManualInput() { | |
| const width = parseInt(this.widthInput.value); | |
| const height = parseInt(this.heightInput.value); | |
| if (!width || !height || width <= 0 || height <= 0) { | |
| this.showToast('有効な数値を入力してください', 'error'); | |
| return; | |
| } | |
| this.calculateAspectRatios(width, height); | |
| this.showResults(); | |
| } | |
| /** | |
| * 画像の処理 | |
| */ | |
| processImage(imageSrc, fileName) { | |
| const img = new Image(); | |
| img.onload = () => { | |
| // プレビュー表示 | |
| this.showImagePreview(imageSrc, fileName); | |
| // 解像度情報表示 | |
| this.showResolutionInfo(img.width, img.height); | |
| // アスペクト比計算 | |
| this.calculateAspectRatios(img.width, img.height); | |
| // 結果表示 | |
| this.showResults(); | |
| }; | |
| img.onerror = () => { | |
| this.showToast('画像の読み込みに失敗しました', 'error'); | |
| }; | |
| img.src = imageSrc; | |
| } | |
| /** | |
| * 画像プレビューの表示 | |
| */ | |
| showImagePreview(imageSrc, fileName) { | |
| this.previewArea.innerHTML = ` | |
| <div class="mb-2"> | |
| <img src="${imageSrc}" alt="プレビュー" class="image-preview"> | |
| </div> | |
| <small class="text-muted">${fileName}</small> | |
| `; | |
| } | |
| /** | |
| * 解像度情報の表示 | |
| */ | |
| showResolutionInfo(width, height) { | |
| this.actualWidth.textContent = width.toLocaleString(); | |
| this.actualHeight.textContent = height.toLocaleString(); | |
| this.resolutionInfo.classList.remove('d-none'); | |
| } | |
| /** | |
| * アスペクト比の計算 | |
| */ | |
| calculateAspectRatios(width, height) { | |
| try { | |
| const results = AspectCalc.calculate(width, height); | |
| // 結果の保存(コピー機能用) | |
| this.currentResults = results; | |
| // 表示更新 | |
| this.integerRatio.textContent = `${results.integer.x}:${results.integer.y}`; | |
| this.approximateRatio.textContent = `${results.approximate.x}:${results.approximate.y}`; | |
| this.standardRatio.textContent = `${results.standard.x}:${results.standard.y}`; | |
| } catch (error) { | |
| console.error('アスペクト比計算エラー:', error); | |
| this.showToast('アスペクト比の計算に失敗しました', 'error'); | |
| } | |
| } | |
| /** | |
| * 結果エリアの表示 | |
| */ | |
| showResults() { | |
| this.resultsArea.classList.remove('d-none'); | |
| this.resultsArea.scrollIntoView({ behavior: 'smooth' }); | |
| } | |
| /** | |
| * アスペクト比のコピー | |
| */ | |
| copyRatio(ratioType) { | |
| if (!this.currentResults) { | |
| this.showToast('コピーするデータがありません', 'error'); | |
| return; | |
| } | |
| const ratio = this.currentResults[ratioType]; | |
| const text = `${ratio.x}:${ratio.y}`; | |
| navigator.clipboard.writeText(text).then(() => { | |
| this.showToast(`「${text}」をコピーしました!`); | |
| }).catch(() => { | |
| this.showToast('コピーに失敗しました', 'error'); | |
| }); | |
| } | |
| /** | |
| * トースト通知の表示 | |
| */ | |
| showToast(message, type = 'success') { | |
| this.toastMessage.textContent = message; | |
| // アイコンの変更 | |
| const icon = this.toast.querySelector('i'); | |
| if (type === 'error') { | |
| icon.className = 'fas fa-exclamation-circle text-danger me-2'; | |
| } else { | |
| icon.className = 'fas fa-check-circle text-success me-2'; | |
| } | |
| // トーストの表示 | |
| const toast = new bootstrap.Toast(this.toast); | |
| toast.show(); | |
| } | |
| } | |
| // アプリケーションの初期化 | |
| document.addEventListener('DOMContentLoaded', () => { | |
| new AspectCalcApp(); | |
| }); | |