oshiphoto

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

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>