oshiphoto

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit cb693a078ad424b3a003cc704ee40a20014561f7
parent 37e6de80b6d8d3746d331b608bea97dea63e4946
Author: Sunny <kajimalu10@gmail.com>
Date:   Fri,  7 Nov 2025 14:12:26 +0900

add delete-btn for oshi

Diffstat:
Msrc/App.vue | 391+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/components/EditorCanvas.vue | 137++++++++++++++++++++++++++++++++-----------------------------------------------
2 files changed, 297 insertions(+), 231 deletions(-)

diff --git a/src/App.vue b/src/App.vue @@ -5,78 +5,131 @@ <main class="main-content"> - <div class="canvas-wrapper"> - <EditorCanvas ref="editor" /> - </div> - - <div class="controls"> - <h3>操作パネル</h3> - - <div> - <label for="bg-upload">1. 背景画像をDBに追加:</label> <input - id="bg-upload" - type="file" - accept="image/*" - @change="onBackgroundFileChange" - /> + <div class="main-column-left"> + <div + class="canvas-wrapper" + ref="canvasWrapperRef" + :class="{ 'has-image': canvasHasImage }" + > + <div v_if="!canvasHasImage" class="canvas-placeholder"> + 背景画像を読み込んでください + </div> + <EditorCanvas ref="editor" /> </div> - <div> - <label for="oshi-upload">2. 推し画像をDBに追加:</label> <input - id="oshi-upload" - type="file" - accept="image/png, image/gif" - @change="onOshiFileChange" /> - <p>(透過PNG推奨)</p> + <div class="canvas-controls"> + <button @click="deleteSelectedOshi" class="delete-selected-btn"> + 選択した推しを削除 + </button> </div> </div> - - <div class="gallery oshi-gallery"> <h3>推し画像ギャラリー</h3> - <p>(画像クリックでCanvasに追加 / 保存数: {{ oshiGallery.oshiList.value.length }})</p> <div class="gallery-container"> - <ul> - <li v-for="image in oshiGallery.oshiList.value" :key="image.id"> - <img - :src="createBlobUrl(image.data)" - class="thumbnail" - @click="addOshiToCanvas(image.data)" /> - <button class="delete-btn" @click="oshiGallery.deleteOshiImage(image.id)">×</button> </li> - </ul> + <div class="main-column-right"> + <div class="gallery oshi-gallery"> + <div class="gallery-header"> + <div class="gallery-header-info"> + <h3>推し画像ギャラリー</h3> + <p>(クリックでCanvasに追加 / 保存数: {{ oshiGallery.oshiList.value.length }})</p> + </div> + <div class="gallery-header-input"> + <label for="oshi-upload" class="file-label">画像を追加</label> + <input + id="oshi-upload" + class="file-input" + type="file" + accept="image/png, image/gif" + @change="onOshiFileChange" + /> + </div> + </div> + <div class="gallery-container"> + <ul> + <li v-for="image in oshiGallery.oshiList.value" :key="image.id"> + <img + :src="createBlobUrl(image.data)" + class="thumbnail" + @click="addOshiToCanvas(image.data)" + /> + <button class="delete-btn" @click="oshiGallery.deleteOshiImage(image.id)">×</button> + </li> + </ul> + </div> </div> - </div> - <div class="gallery"> - <h3>背景画像ギャラリー</h3> <p>(画像クリックで背景セット / 保存数: {{ backgroundGallery.galleryList.value.length }})</p> <div class="gallery-container"> - <ul> - <li v-for="image in backgroundGallery.galleryList.value" :key="image.id"> - <img - :src="createBlobUrl(image.data)" - class="thumbnail" - @click="setBackground(image.data)" + + <div class="gallery"> + <div class="gallery-header"> + <div class="gallery-header-info"> + <h3>背景画像ギャラリー</h3> + <p>(クリックで背景セット / 保存数: {{ backgroundGallery.galleryList.value.length }})</p> + </div> + <div class="gallery-header-input"> + <label for="bg-upload" class="file-label">画像を追加</label> + <input + id="bg-upload" + class="file-input" + type="file" + accept="image/*" + @change="onBackgroundFileChange" /> - <button class="delete-btn" @click="backgroundGallery.deleteImage(image.id)">×</button> - </li> - </ul> + </div> + </div> + <div class="gallery-container"> + <ul> + <li v-for="image in backgroundGallery.galleryList.value" :key="image.id"> + <img + :src="createBlobUrl(image.data)" + class="thumbnail" + @click="setBackground(image.data)" + /> + <button class="delete-btn" @click="backgroundGallery.deleteImage(image.id)">×</button> + </li> + </ul> + </div> </div> </div> - </main> + </main> </template> <script setup> -import { ref, onMounted } from 'vue'; +import { ref, onMounted, onUnmounted } from 'vue'; import EditorCanvas from './components/EditorCanvas.vue'; -// ▼▼▼ 2つのストアをインポート ▼▼▼ import { useBackgroundStore } from './composables/useBackgroundStore.ts'; import { useOshiGallery } from './composables/useOshiStore.ts'; -// -------------------------------- -// ギャラリー機能 (IndexedDB) -// -------------------------------- -// ▼▼▼ 2つのストアを呼び出す ▼▼▼ const backgroundGallery = useBackgroundStore(); const oshiGallery = useOshiGallery(); +const currentBgDataUrl = ref(null); +const currentBgHtmlImage = ref(null); +const editor = ref(null); +const canvasWrapperRef = ref(null); +const canvasHasImage = ref(false); + +const handleResize = () => { + // 必要な要素が揃っていない(背景がまだない等)場合は何もしない + if (!editor.value || !canvasWrapperRef.value || !currentBgDataUrl.value || !currentBgHtmlImage.value) { + return; + } + + console.log('リサイズを検知'); + + // 1. 新しいコンテナ幅を取得 + const containerWidth = canvasWrapperRef.value.clientWidth; + + // 2. EditorCanvasのリサイズ関数を、保持している画像データで呼び出す + editor.value.resizeAndSetBackground( + currentBgDataUrl.value, + currentBgHtmlImage.value, + containerWidth + ); +}; onMounted(() => { backgroundGallery.loadImages(); - oshiGallery.loadOshiImages(); // ★推し画像も読み込む + oshiGallery.loadOshiImages(); + window.addEventListener('resize', handleResize); +}); + +onUnmounted(() => { + window.removeEventListener('resize', handleResize); }); const createBlobUrl = (blob) => { @@ -93,78 +146,61 @@ const convertBlobToDataURL = (blob) => { }); }; -// -------------------------------- -// Canvasコンポーネント -// -------------------------------- -const editor = ref(null); +const loadHtmlImage = (dataUrl) => { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = (err) => reject(err); + img.src = dataUrl; + }); +}; -/** - * ギャラリーの画像をクリックしたときに背景をセットする - */ const setBackground = async (imageBlob) => { - if (!editor.value) { - console.error('setBackground: editor (ref) が見つかりません'); + if (!editor.value || !canvasWrapperRef.value) { + console.error('setBackground: editor または wrapper が見つかりません'); return; } - console.log('setBackground が呼ばれました。'); - + try { const dataUrl = await convertBlobToDataURL(imageBlob); - editor.value.setBackgroundImage(dataUrl); + const imgElement = await loadHtmlImage(dataUrl); + const containerWidth = canvasWrapperRef.value.clientWidth; + currentBgDataUrl.value = dataUrl; + currentBgHtmlImage.value = imgElement; + editor.value.resizeAndSetBackground(dataUrl, imgElement, containerWidth); + canvasHasImage.value = true; } catch (error) { - console.error('背景画像の変換に失敗:', error); + console.error('背景画像のセットに失敗:', error); } }; -// --- ▼▼▼ 新設 ▼▼▼ --- -/** - * 推しギャラリーの画像をクリックしたときにCanvasに追加する - */ const addOshiToCanvas = async (imageBlob) => { - if (!editor.value) { - console.error('addOshiToCanvas: editor (ref) が見つかりません'); - return; - } - console.log('addOshiToCanvas が呼ばれました。'); - + if (!editor.value) return; try { const dataUrl = await convertBlobToDataURL(imageBlob); - // EditorCanvas.vue にある addOshiImage 関数を呼び出す editor.value.addOshiImage(dataUrl); } catch (error) { console.error('推し画像の変換に失敗:', error); } }; -// --- ▲▲▲ 新設ここまで ▲▲▲ --- +const deleteSelectedOshi = () => { + if (!editor.value) return; + editor.value.deleteActiveObject(); +}; -// -------------------------------- -// ファイル入力の処理 -// -------------------------------- -/** - * 背景画像ファイルが選択されたときの処理 - */ const onBackgroundFileChange = (event) => { const file = event.target.files[0]; if (!file) return; - // ★背景ストアに追加 - backgroundGallery.addImage(file); + backgroundGallery.addImage(file); event.target.value = null; }; -/** - * 推し画像ファイルが選択されたときの処理 - */ -const onOshiFileChange = async (event) => { // ★ async は不要になった +const onOshiFileChange = (event) => { const file = event.target.files[0]; if (!file) return; - - // --- ▼▼▼ 修正 ▼▼▼ --- - // Canvasに直接追加するのではなく、Oshiストアに追加する oshiGallery.addOshiImage(file); - // --- ▲▲▲ 修正ここまで --- - event.target.value = null; }; </script> @@ -211,85 +247,140 @@ header h1 { /* メインコンテンツエリア */ .main-content { - flex-grow: 1; /* 残りの高さをすべて使う */ + flex-grow: 1; display: flex; flex-direction: column; overflow-y: auto; - gap: 0; /* gapは使わず、marginで制御 */ - padding-bottom: 16px; /* 下端の余白 */ + gap: 0; + padding-bottom: 16px; padding: 16px; } +@media (min-width: 1024px) { + .main-content { + flex-direction: row; /* PCでは横並び */ + gap: 24px; + overflow-y: hidden; /* PCでは親をスクロールさせない */ + } + + .main-column-left { + flex-grow: 1; /* 左カラム(Canvas)が幅を占める */ + min-width: 0; /* 縮小できるように */ + display: flex; + flex-direction: column; + } + + .main-column-right { + flex-shrink: 0; /* 右カラム(操作部)は縮まない */ + width: 380px; /* 右カラムの幅を固定 */ + height: 100%; /* 親の高さに合わせる */ + overflow-y: auto; /* ★ 右カラムだけをスクロールさせる */ + + /* スクロールバーのスタイル(任意) */ + scrollbar-width: thin; + scrollbar-color: #ccc #f4f4f9; + } + + /* PC時はギャラリー間のマージンを調整 */ + .main-column-right .gallery { + margin-bottom: 24px; + } +} + /* --- Canvasエリア --- */ +/* --- 左カラム (Canvas周り) --- */ .canvas-wrapper { + flex-grow: 1; /* PC時に高さを広げる */ flex-shrink: 0; width: 100%; - height: 400px; /* Canvasの高さに合わせる */ - max-height: 50vh; /* 画面の半分まで */ - - /* Canvas(600px)がはみ出たら横スクロールさせる - これがスマホ対応のキモです - */ - overflow-x: auto; - background-color: #ddd; /* スクロール領域の背景 */ + background-color: #eee; border-radius: 8px; box-shadow: inset 0 1px 3px rgba(0,0,0,0.1); + min-height: 250px; - /* スクロールをスムーズに */ - -webkit-overflow-scrolling: touch; + /* プレースホルダーテキスト用 */ + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +/* 背景画像がセットされたら、プレースホルダーを隠す */ +.canvas-wrapper.has-image { + background-color: transparent; + min-height: 0; + align-items: flex-start; /* 左上に配置 */ + justify-content: flex-start; +} +.canvas-placeholder { + color: #888; + font-size: 0.9rem; +} +.canvas-wrapper.has-image .canvas-placeholder { + display: none; } -/* --- 操作パネル --- */ -.controls { +.canvas-controls { flex-shrink: 0; - background-color: #fff; - border-radius: 8px; - padding: 16px; - box-shadow: 0 2px 4px rgba(0,0,0,0.05); display: flex; - flex-direction: column; - gap: 16px; + justify-content: flex-end; /* ボタンを右寄せ */ + /* Canvasの真下に配置 */ + margin-top: 12px; + margin-bottom: 16px; } -.controls h3 { - font-size: 1rem; - font-weight: 600; - border-bottom: 1px solid #eee; - padding-bottom: 8px; +.delete-selected-btn { + border: none; + background-color: #e53935; /* 赤系の削除ボタン */ + color: white; + padding: 8px 12px; + border-radius: 6px; + font-weight: 500; + cursor: pointer; + font-size: 0.9rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); } - -.controls div { - display: flex; - flex-direction: column; - gap: 8px; +.delete-selected-btn:active { + background-color: #c62828; } /* --- ギャラリー --- */ -.gallery { - flex-shrink: 0; /* 縮まない */ - background-color: #fff; - border-radius: 8px; - padding: 16px; - box-shadow: 0 2px 4px rgba(0,0,0,0.05); +.gallery-header { display: flex; - flex-direction: column; - margin-bottom: 16px; + justify-content: space-between; /* 両端揃え */ + align-items: center; /* 中央揃え */ + margin-bottom: 12px; + gap: 16px; +} +.gallery-header-info { + flex-grow: 1; /* タイトル領域が伸びる */ +} +.gallery-header-input { + flex-shrink: 0; /* ボタンは縮まない */ } .gallery h3 { font-size: 1rem; font-weight: 600; + margin: 0; /* マージンリセット */ } + .gallery p { font-size: 0.8rem; color: #666; - margin-bottom: 12px; + margin: 0; /* マージンリセット */ } -.gallery.oshi-gallery { - /* 推しギャラリーだけ少し背景色を変える(任意) */ - background-color: #fdfdff; +.gallery { + flex-shrink: 0; + background-color: #fff; + border-radius: 8px; + padding: 16px; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + display: flex; + flex-direction: column; + margin-bottom: 16px; } .gallery-container { @@ -345,18 +436,19 @@ header h1 { line-height: 1; } -/* --- フォーム部品 --- */ -label { - font-size: 0.9rem; - font-weight: 500; -} - -input[type="file"] { - font-size: 0.9rem; +/* --- ▼▼▼ 修正 (ファイル入力) ▼▼▼ --- */ +/* デフォルトのinputを隠す */ +.file-input { + width: 0.1px; + height: 0.1px; + opacity: 0; + overflow: hidden; + position: absolute; + z-index: -1; } -/* ファイル選択ボタンのスタイルを整える (任意) */ -input[type="file"]::file-selector-button { +/* ラベルをボタン風にスタイリング */ +.file-label { border: none; background-color: #007aff; color: white; @@ -365,9 +457,10 @@ input[type="file"]::file-selector-button { font-weight: 500; cursor: pointer; transition: background-color 0.2s; + font-size: 0.9rem; + display: inline-block; } - -input[type="file"]::file-selector-button:active { +.file-label:active { background-color: #0056b3; } </style> \ No newline at end of file diff --git a/src/components/EditorCanvas.vue b/src/components/EditorCanvas.vue @@ -1,6 +1,6 @@ <template> <div> - <canvas ref="canvasElement" width="600" height="400"></canvas> + <canvas ref="canvasElement"></canvas> </div> </template> @@ -14,110 +14,93 @@ let fabricCanvas = null; onMounted(() => { if (canvasElement.value) { fabricCanvas = new Canvas(canvasElement.value, { - backgroundColor: '#eee' + width: 1, + height: 1, + backgroundColor: 'transparent' }); console.log('Fabric.jsのCanvasを初期化しました'); - fabricCanvas.renderAll(); } else { console.error('onMounted: canvasElement が見つかりません'); } }); -/** - * URLから「推し画像」をCanvasに追加する - * @param {String} imageUrl - Data URL - */ const addOshiImage = (imageUrl) => { - if (!fabricCanvas) { - console.error('addOshiImage: fabricCanvas が初期化されていません'); - return; - } + if (!fabricCanvas) return; console.log('addOshiImage が呼ばれました'); - - // --- ▼▼▼ ここから修正 ▼▼▼ --- - // 1. Web標準の Image オブジェクトを作成 const imgElement = new Image(); - - // 2. 読み込み完了時の処理を先に定義 imgElement.onload = () => { - console.log('推し画像の読み込み成功 (Image.onload)'); // ★表示されるはずのログ - - // 3. 読み込んだHTML要素から FabricImage を作成 - const fabricImg = new FabricImage(imgElement, { - scaleX: 200 / imgElement.width, // 幅200pxにスケール - scaleY: 200 / imgElement.width, + console.log('推し画像の読み込み成功 (Image.onload)'); + const fabricImg = new FabricImage(imgElement); + + // Canvasのサイズに対して、いい感じのサイズに調整 + // (例: Canvasの幅の 1/3 サイズにする) + const scale = (fabricCanvas.width * 0.33) / imgElement.naturalWidth; + + fabricImg.set({ left: 100, top: 100, + scaleX: scale, + scaleY: scale, }); - // 4. Canvasに追加して再描画 fabricCanvas.add(fabricImg); fabricCanvas.renderAll(); console.log('「推し」画像を追加しました'); }; - - // 5. 読み込み失敗時の処理 - imgElement.onerror = (err) => { - console.error('推し画像の読み込みに失敗 (Image.onerror)', err); - }; - - // 6. 最後に Data URL をセットして読み込みを開始 + imgElement.onerror = (err) => { /* ... (省略) ... */ }; imgElement.src = imageUrl; - - // --- ▲▲▲ 修正ここまで ▲▲▲ --- }; -/** - * Canvasの背景画像を設定する - * @param {String} imageUrl - Data URL - */ -const setBackgroundImage = (imageUrl) => { +const resizeAndSetBackground = (dataUrl, imgElement, containerWidth) => { if (!fabricCanvas) { - console.error('setBackgroundImage: fabricCanvas が初期化されていません'); + console.error('resizeAndSetBackground: fabricCanvas が初期化されていません'); return; } - console.log('setBackgroundImage が呼ばれました'); - - // 1. Web標準の Image オブジェクトを作成 - const imgElement = new Image(); + console.log('resizeAndSetBackground が呼ばれました'); - // 2. 読み込み完了時の処理 - imgElement.onload = () => { - console.log('背景画像の読み込み成功 (Image.onload)'); + // 1. 新しいCanvasのサイズを計算 + const imgRatio = imgElement.naturalHeight / imgElement.naturalWidth; + + // 親コンテナの幅(containerWidth)をそのままCanvasの幅として使用します + const newWidth = containerWidth; + const newHeight = newWidth * imgRatio; + + // 2. Fabric.jsのCanvasにサイズをセット + fabricCanvas.setDimensions({ + width: newWidth, + height: newHeight + }); + + // 3. <img> から FabricImage を作成 + const fabricImg = new FabricImage(imgElement); - // 3. 読み込んだHTML要素から FabricImage を作成 - const fabricImg = new FabricImage(imgElement); + // 4. 背景としてセット + fabricCanvas.backgroundColor = 'transparent'; // 以前の背景色をクリア + fabricCanvas.backgroundImage = fabricImg; + + // 5. 画像をCanvasの幅にフィットさせる + fabricImg.scaleToWidth(newWidth); - // --- ▼▼▼ ここから修正 ▼▼▼ --- + // 6. 変更をCanvasに反映 + fabricCanvas.renderAll(); - // 4. 背景としてセット (プロパティに直接代入) - fabricCanvas.backgroundImage = fabricImg; - - // 5. Canvasのサイズに画像をフィットさせる - fabricImg.scaleToWidth(fabricCanvas.width); - fabricImg.scaleToHeight(fabricCanvas.height); + console.log(`Canvasをリサイズしました: ${newWidth} x ${newHeight}`); +}; - // 6. 変更をCanvasに反映(強制再描画) +const deleteActiveObject = () => { + if (!fabricCanvas) return; + const activeObject = fabricCanvas.getActiveObject(); + if (activeObject) { + fabricCanvas.remove(activeObject); fabricCanvas.renderAll(); - - console.log('背景画像を設定しました'); - - // --- ▲▲▲ 修正ここまで ▲▲▲ --- - }; - - // 5. 読み込み失敗時の処理 - imgElement.onerror = (err) => { - console.error('背景画像の読み込みに失敗 (Image.onerror)', err); - }; - - // 6. 最後に Data URL をセットして読み込みを開始 - imgElement.src = imageUrl; + } }; defineExpose({ - setBackgroundImage, - addOshiImage + resizeAndSetBackground, + addOshiImage, + deleteActiveObject }); </script> @@ -131,21 +114,11 @@ defineExpose({ 親要素 (App.vueの .canvas-wrapper) の サイズに追従するようにします */ - width: 100% !important; - height: 100% !important; - max-width: 600px; /* 元のcanvas幅 */ - max-height: 400px; /* 元のcanvas高さ */ + max-width: 100%; + max-height: 100%; /* スマホ画面で縮小表示されたときもくっきりさせる */ image-rendering: -webkit-optimize-contrast; image-rendering: crisp-edges; } - -/* fabric.js が操作する <canvas> 自体 - (backgroundColor: '#eee' が適用される場所) -*/ -:deep(canvas) { - width: 100% !important; - height: 100% !important; -} </style>