commit fa47da52e3982c32e899af7434cedc7ea71ebc48
parent cb693a078ad424b3a003cc704ee40a20014561f7
Author: Sunny <kajimalu10@gmail.com>
Date: Fri, 7 Nov 2025 23:33:10 +0900
add dl
Diffstat:
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