oshiphoto

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

commit fa47da52e3982c32e899af7434cedc7ea71ebc48
parent cb693a078ad424b3a003cc704ee40a20014561f7
Author: Sunny <kajimalu10@gmail.com>
Date:   Fri,  7 Nov 2025 23:33:10 +0900

add dl

Diffstat:
Mindex.html | 6+++---
Msrc/App.vue | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/components/EditorCanvas.vue | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Asrc/env.d.ts | 8++++++++
4 files changed, 180 insertions(+), 7 deletions(-)

diff --git a/index.html b/index.html @@ -1,10 +1,10 @@ -<!doctype html> -<html lang="en"> +<!DOCTYPE html> +<html lang="ja"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>oshidori</title> + <title>推しふぉと!</title> </head> <body> <div id="app"></div> diff --git a/src/App.vue b/src/App.vue @@ -1,6 +1,6 @@ <template> <header> - <h1>推しと一緒!アプリ</h1> + <h1>推しふぉと!</h1> </header> <main class="main-content"> @@ -18,6 +18,9 @@ </div> <div class="canvas-controls"> + <button @click="handleSaveImage" class="save-btn"> + 画像を保存 + </button> <button @click="deleteSelectedOshi" class="delete-selected-btn"> 選択した推しを削除 </button> @@ -189,6 +192,55 @@ const deleteSelectedOshi = () => { editor.value.deleteActiveObject(); }; +const handleSaveImage = () => { + if (!editor.value) { + console.error('handleSaveImage: editor (ref) が見つかりません'); + return; + } + + // ★ 保存する元画像のHTMLImageElementを取得 + const originalImage = currentBgHtmlImage.value; + + if (!originalImage) { + console.error('handleSaveImage: 元の背景画像がありません'); + alert('先に背景画像をセットしてください'); + return; + } + + try { + const dataUrl = editor.value.exportOriginalSizeDataURL(originalImage); + + if (!dataUrl) { + console.error('画像の書き出しに失敗しました (dataUrl is null)'); + alert('画像の書き出しに失敗しました。'); + return; + } + + // 1. 現在時刻からDateオブジェクトを生成 + const now = new Date(); + + // 2. 各パーツを取得し、padStart(2, '0') で2桁(0埋め)にする + const year = now.getFullYear().toString().slice(-2); // 年(下2桁) + const month = (now.getMonth() + 1).toString().padStart(2, '0'); // 月 (0-11なので+1) + const day = now.getDate().toString().padStart(2, '0'); // 日 + const hours = now.getHours().toString().padStart(2, '0'); // 時 + const minutes = now.getMinutes().toString().padStart(2, '0'); // 分 + const seconds = now.getSeconds().toString().padStart(2, '0'); // 秒 + + // 3. ご希望のフォーマットに組み立て + const fileName = `osph-${year}${month}${day}-${hours}${minutes}${seconds}.png`; + + // 4. リンクを作成してダウンロード + const link = document.createElement('a'); + link.download = fileName; + link.href = dataUrl; + link.click(); + + } catch (error) { + console.error('handleSaveImage でエラーが発生しました:', error); + alert('画像の保存中にエラーが発生しました。'); + } +}; const onBackgroundFileChange = (event) => { const file = event.target.files[0]; @@ -325,9 +377,25 @@ header h1 { flex-shrink: 0; display: flex; justify-content: flex-end; /* ボタンを右寄せ */ - /* Canvasの真下に配置 */ + gap: 12px; /* ★ ボタン間のスペース */ margin-top: 12px; margin-bottom: 16px; + flex-wrap: wrap; +} + +.save-btn { + border: none; + background-color: #007aff; /* iOSの青色 */ + 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); +} +.save-btn:active { + background-color: #0056b3; } .delete-selected-btn { diff --git a/src/components/EditorCanvas.vue b/src/components/EditorCanvas.vue @@ -6,7 +6,7 @@ <script setup> import { ref, onMounted } from 'vue'; -import { Canvas, Image as FabricImage } from 'fabric'; +import { Canvas, FabricImage, StaticCanvas } from 'fabric'; const canvasElement = ref(null); let fabricCanvas = null; @@ -97,10 +97,107 @@ const deleteActiveObject = () => { } }; +const exportOriginalSizeDataURL = (originalImageElement) => { + try { + if (!fabricCanvas || !originalImageElement) { + console.error('exportOriginalSizeDataURL: 引数が不足しています'); + return null; + } + + // 1. オリジナル解像度 + const originalWidth = originalImageElement.naturalWidth; + const originalHeight = originalImageElement.naturalHeight; + + // 2. 現在の表示サイズ + const currentWidth = fabricCanvas.width; + + // 3. スケーリング比率 + const ratio = originalWidth / currentWidth; + + if (ratio <= 0 || !isFinite(ratio)) { + console.error('無効なスケーリング比率です', ratio); + return null; + } + + // 4. メモリ上に元サイズの StaticCanvas を作成 + const staticCanvas = new StaticCanvas(null, { + width: originalWidth, + height: originalHeight + }); + + // 5. 背景画像を手動で「再構築」 + const bgImage = fabricCanvas.backgroundImage; + if (bgImage) { + // getElement() で <img> 要素を取得 + const bgImgElement = bgImage.getElement(); + const newBgScale = bgImage.scaleX * ratio; + + // <img> 要素から新しい FabricImage を作成 + const newBg = new FabricImage(bgImgElement, { + scaleX: newBgScale, + scaleY: newBgScale + }); + staticCanvas.backgroundImage = newBg; + } + + // 6. オブジェクト(推し)を個別に「再構築」 + const objects = fabricCanvas.getObjects(); + if (objects.length > 0) { + objects.forEach(obj => { + // 7. <img> 要素を取得 (FabricImage のみ) + if (obj.type === 'image' && obj.getElement) { + const oshiImgElement = obj.getElement(); + + // 8. <img> とスケーリング済みのプロパティから新しい FabricImage を作成 + const newOshi = new FabricImage(oshiImgElement, { + // スケーリングするプロパティ + left: obj.left * ratio, + top: obj.top * ratio, + scaleX: obj.scaleX * ratio, + scaleY: obj.scaleY * ratio, + + // そのままコピーするプロパティ + angle: obj.angle, + flipX: obj.flipX, + flipY: obj.flipY, + skewX: obj.skewX, + skewY: obj.skewY, + originX: obj.originX, + originY: obj.originY, + // ... (他にも必要なプロパティがあればここに追加) + }); + + // 9. StaticCanvas に追加 + staticCanvas.add(newOshi); + } + }); + } + + // 10. 最終描画 + staticCanvas.renderAll(); + + // 11. 高解像度のData URLを生成 + const dataUrl = staticCanvas.toDataURL({ + format: 'png', + quality: 1.0 + }); + + // 12. メモリ解放 + staticCanvas.dispose(); + + return dataUrl; // ★ 成功パス + + } catch (error) { + console.error('exportOriginalSizeDataURL 内部でエラー:', error); + return null; // ★ 失敗パス + } +}; + defineExpose({ resizeAndSetBackground, addOshiImage, - deleteActiveObject + deleteActiveObject, + exportOriginalSizeDataURL }); </script> diff --git a/src/env.d.ts b/src/env.d.ts @@ -0,0 +1,7 @@ +// <reference types="vite/client" /> + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +}+ \ No newline at end of file