App.vue (9648B)
1 <template> 2 <header> 3 <h1>推しふぉと!</h1> 4 <button 5 @click="toggleMenu" 6 class="menu-button" 7 :class="{ 'is-open': isMenuOpen }" aria-label="メニュー開閉" 8 > 9 <span class="material-symbols-rounded icon-menu">menu</span> 10 <span class="material-symbols-rounded icon-close">close</span> 11 </button> 12 </header> 13 14 <main class="main-content"> 15 16 <div class="main-column-left"> 17 <div 18 class="canvas-wrapper" 19 ref="canvasWrapperRef" 20 :class="{ 'has-image': canvasHasImage }" 21 > 22 <div v-if="!canvasHasImage" class="canvas-placeholder"> 23 背景画像を読み込んでください 24 </div> 25 <EditorCanvas ref="editor" /> 26 </div> 27 28 <div class="canvas-controls"> 29 <button @click="deleteSelectedOshi" class="btn btn-del-selected" title="選択した推しを削除"> 30 <span class="material-symbols-rounded">delete</span> 31 <span class="button-text">削除</span> 32 </button> 33 <button @click="handleSaveImage" class="btn btn-save" title="画像を保存"> 34 <span class="material-symbols-rounded">download</span> 35 <span class="button-text">保存</span> 36 </button> 37 </div> 38 </div> 39 <div class="main-column-right"> 40 <div class="gallery oshi-gallery"> 41 <div class="gallery-header"> 42 <div class="gallery-header-info"> 43 <h3>推し画像</h3> <p>(保存数: {{ oshiGallery.oshiList.value.length }})</p> 44 </div> 45 </div> 46 <div class="gallery-container"> 47 <ul> 48 <li v-for="image in oshiGallery.oshiList.value" :key="image.id"> 49 <img 50 :src="createBlobUrl(image.data)" 51 class="thumbnail" 52 @click="addOshiToCanvas(image.data)" 53 /> 54 <button class="delete-btn" @click="oshiGallery.deleteOshiImage(image.id)" title="推し画像を削除"> 55 <span class="material-symbols-rounded">close</span> 56 </button> 57 </li> 58 <div class="gallery-input"> 59 <label for="oshi-upload" class="btn" title="推し画像を追加"> 60 <span class="material-symbols-rounded">add_photo_alternate</span>追加 61 </label> 62 <input 63 id="oshi-upload" 64 class="file-input" 65 type="file" 66 accept="image/png, image/gif" 67 @change="onOshiFileChange" 68 /> 69 </div> 70 </ul> 71 </div> 72 </div> 73 74 <div class="gallery"> 75 <div class="gallery-header"> 76 <div class="gallery-header-info"> 77 <h3>背景画像</h3> <p>(保存数: {{ backgroundGallery.galleryList.value.length }})</p> 78 </div> 79 </div> 80 <div class="gallery-container"> 81 <ul> 82 <li v-for="image in backgroundGallery.galleryList.value" :key="image.id"> 83 <img 84 :src="createBlobUrl(image.data)" 85 class="thumbnail" 86 @click="setBackground(image.data)" 87 /> 88 <button class="delete-btn" @click="backgroundGallery.deleteImage(image.id)" title="背景画像を削除"> 89 <span class="material-symbols-rounded">close</span> 90 </button> 91 </li> 92 <div class="gallery-input"> 93 <label for="bg-upload" class="btn" title="背景画像を追加"> 94 <span class="material-symbols-rounded">add_photo_alternate</span>追加 95 </label> 96 <input 97 id="bg-upload" 98 class="file-input" 99 type="file" 100 accept="image/*" 101 @change="onBackgroundFileChange" 102 /> 103 </div> 104 </ul> 105 </div> 106 </div> 107 </div> 108 </main> 109 110 <Transition name="fade"> 111 <div 112 v-if="isMenuOpen" 113 class="sidebar-overlay" 114 @click="toggleMenu" 115 ></div> 116 </Transition> 117 118 <Transition name="slide"> 119 <nav v-if="isMenuOpen" class="sidebar-menu"> 120 <h2>メニュー</h2> 121 <ul> 122 <li> 123 <a href="#">設定</a> 124 </li> 125 <li> 126 <a href="#">利用規約</a> 127 </li> 128 </ul> 129 </nav> 130 </Transition> 131 </template> 132 133 <script setup> 134 import { ref, onMounted, onUnmounted } from 'vue'; 135 import EditorCanvas from './components/EditorCanvas.vue'; 136 import { useBackgroundStore } from './composables/useBackgroundStore.ts'; 137 import { useOshiGallery } from './composables/useOshiStore.ts'; 138 139 const backgroundGallery = useBackgroundStore(); 140 const oshiGallery = useOshiGallery(); 141 const currentBgDataUrl = ref(null); 142 const currentBgHtmlImage = ref(null); 143 const editor = ref(null); 144 const canvasWrapperRef = ref(null); 145 const canvasHasImage = ref(false); 146 const isMenuOpen = ref(false); 147 148 const toggleMenu = () => { 149 isMenuOpen.value = !isMenuOpen.value; 150 }; 151 152 const handleResize = () => { 153 // 必要な要素が揃っていない(背景がまだない等)場合は何もしない 154 if (!editor.value || !canvasWrapperRef.value || !currentBgDataUrl.value || !currentBgHtmlImage.value) { 155 return; 156 } 157 158 console.log('リサイズを検知'); 159 160 // 1. 新しいコンテナ幅を取得 161 const containerWidth = canvasWrapperRef.value.clientWidth; 162 163 // 2. EditorCanvasのリサイズ関数を、保持している画像データで呼び出す 164 editor.value.resizeAndSetBackground( 165 currentBgDataUrl.value, 166 currentBgHtmlImage.value, 167 containerWidth 168 ); 169 }; 170 171 onMounted(() => { 172 backgroundGallery.loadImages(); 173 oshiGallery.loadOshiImages(); 174 window.addEventListener('resize', handleResize); 175 }); 176 177 onUnmounted(() => { 178 window.removeEventListener('resize', handleResize); 179 }); 180 181 const createBlobUrl = (blob) => { 182 if (!blob) return ''; 183 return URL.createObjectURL(blob); 184 }; 185 186 const convertBlobToDataURL = (blob) => { 187 return new Promise((resolve, reject) => { 188 const reader = new FileReader(); 189 reader.onload = () => resolve(reader.result); 190 reader.onerror = (error) => reject(error); 191 reader.readAsDataURL(blob); 192 }); 193 }; 194 195 const loadHtmlImage = (dataUrl) => { 196 return new Promise((resolve, reject) => { 197 const img = new Image(); 198 img.onload = () => resolve(img); 199 img.onerror = (err) => reject(err); 200 img.src = dataUrl; 201 }); 202 }; 203 204 const setBackground = async (imageBlob) => { 205 if (!editor.value || !canvasWrapperRef.value) { 206 console.error('setBackground: editor または wrapper が見つかりません'); 207 return; 208 } 209 210 try { 211 const dataUrl = await convertBlobToDataURL(imageBlob); 212 const imgElement = await loadHtmlImage(dataUrl); 213 const containerWidth = canvasWrapperRef.value.clientWidth; 214 currentBgDataUrl.value = dataUrl; 215 currentBgHtmlImage.value = imgElement; 216 editor.value.resizeAndSetBackground(dataUrl, imgElement, containerWidth); 217 canvasHasImage.value = true; 218 } catch (error) { 219 console.error('背景画像のセットに失敗:', error); 220 } 221 }; 222 223 const addOshiToCanvas = async (imageBlob) => { 224 if (!editor.value) return; 225 try { 226 const dataUrl = await convertBlobToDataURL(imageBlob); 227 editor.value.addOshiImage(dataUrl); 228 } catch (error) { 229 console.error('推し画像の変換に失敗:', error); 230 } 231 }; 232 233 const deleteSelectedOshi = () => { 234 if (!editor.value) return; 235 editor.value.deleteActiveObject(); 236 }; 237 238 const handleSaveImage = () => { 239 if (!editor.value) { 240 console.error('handleSaveImage: editor (ref) が見つかりません'); 241 return; 242 } 243 244 // ★ 保存する元画像のHTMLImageElementを取得 245 const originalImage = currentBgHtmlImage.value; 246 247 if (!originalImage) { 248 console.error('handleSaveImage: 元の背景画像がありません'); 249 alert('先に背景画像をセットしてください'); 250 return; 251 } 252 253 try { 254 const dataUrl = editor.value.exportOriginalSizeDataURL(originalImage); 255 256 if (!dataUrl) { 257 console.error('画像の書き出しに失敗しました (dataUrl is null)'); 258 alert('画像の書き出しに失敗しました。'); 259 return; 260 } 261 262 // 1. 現在時刻からDateオブジェクトを生成 263 const now = new Date(); 264 265 // 2. 各パーツを取得し、padStart(2, '0') で2桁(0埋め)にする 266 const year = now.getFullYear().toString().slice(-2); // 年(下2桁) 267 const month = (now.getMonth() + 1).toString().padStart(2, '0'); // 月 (0-11なので+1) 268 const day = now.getDate().toString().padStart(2, '0'); // 日 269 const hours = now.getHours().toString().padStart(2, '0'); // 時 270 const minutes = now.getMinutes().toString().padStart(2, '0'); // 分 271 const seconds = now.getSeconds().toString().padStart(2, '0'); // 秒 272 273 // 3. ご希望のフォーマットに組み立て 274 const fileName = `osph-${year}${month}${day}-${hours}${minutes}${seconds}.png`; 275 276 // 4. リンクを作成してダウンロード 277 const link = document.createElement('a'); 278 link.download = fileName; 279 link.href = dataUrl; 280 link.click(); 281 282 } catch (error) { 283 console.error('handleSaveImage でエラーが発生しました:', error); 284 alert('画像の保存中にエラーが発生しました。'); 285 } 286 }; 287 288 const onBackgroundFileChange = (event) => { 289 const file = event.target.files[0]; 290 if (!file) return; 291 backgroundGallery.addImage(file); 292 event.target.value = null; 293 }; 294 295 const onOshiFileChange = (event) => { 296 const file = event.target.files[0]; 297 if (!file) return; 298 oshiGallery.addOshiImage(file); 299 event.target.value = null; 300 }; 301 </script>