SenY commited on
Commit
92a523d
·
1 Parent(s): e8a23f8

ダミーのポストプロセス実行完了

Browse files
Files changed (5) hide show
  1. effects.js +2 -2
  2. effects/base.js +11 -15
  3. index.html +12 -0
  4. index.js +581 -504
  5. styles.css +179 -174
effects.js CHANGED
@@ -49,13 +49,13 @@ export function getAvailableEffects() {
49
  }
50
 
51
  /**
52
- * テキストにエフェクトを適用して画像URLを生成
53
  * @param {string} effectType - エフェクトの種類
54
  * @param {string} text - レンダリングするテキスト
55
  * @param {Object} options - オプション
56
  * @param {string} options.font - フォントファミリー
57
  * @param {number} options.fontSize - フォントサイズ
58
- * @returns {Promise<string>} - 生成された画像のData URL
59
  */
60
  export async function applyEffect(effectType, text, options) {
61
  const effect = effects[effectType];
 
49
  }
50
 
51
  /**
52
+ * テキストにエフェクトを適用
53
  * @param {string} effectType - エフェクトの種類
54
  * @param {string} text - レンダリングするテキスト
55
  * @param {Object} options - オプション
56
  * @param {string} options.font - フォントファミリー
57
  * @param {number} options.fontSize - フォントサイズ
58
+ * @returns {Promise<HTMLCanvasElement>} - 生成されたcanvas要素
59
  */
60
  export async function applyEffect(effectType, text, options) {
61
  const effect = effects[effectType];
effects/base.js CHANGED
@@ -133,7 +133,6 @@ export class BaseEffect {
133
  */
134
  calculateCoordinates(ctx, lines, metrics, lineSpacing, padding) {
135
  this.coordinates = [];
136
- console.log(ctx.canvas.dataset.verticalSpacing);
137
  const verticalLetterSpacing = parseFloat(ctx.canvas.dataset.verticalSpacing) || 1.3;
138
 
139
  if (ctx.canvas.dataset.vertical === 'true') {
@@ -326,36 +325,33 @@ export class BaseEffect {
326
  * エフェクトを適用する
327
  * @param {string} text - レンダリングするテキスト
328
  * @param {Object} options - オプション
329
- * @returns {Promise<string>} - 生成された画像のData URL
330
  */
331
  async apply(text, options) {
 
332
  const canvas = document.createElement('canvas');
333
  const ctx = canvas.getContext('2d');
334
-
335
- // 縦書きモードと文字間隔の状態を保存
336
- canvas.dataset.vertical = options.vertical;
337
- canvas.dataset.verticalSpacing = options.verticalSpacing;
338
 
339
- // キャンバスサイズの設定
340
- const padding = this.getPadding();
 
 
 
341
  const { width, height, metrics, lineSpacing, lines } = this.calculateSize(ctx, text, options);
342
-
343
  canvas.width = width + padding * 2;
344
  canvas.height = height + padding * 2;
345
 
346
- // 背景を透明に
347
- ctx.clearRect(0, 0, canvas.width, canvas.height);
348
-
349
- // テキストの基本設定
350
  await this.setupContext(ctx, options);
351
 
352
  // テキストの描画
353
  await this.renderText(ctx, lines, metrics, lineSpacing, padding);
354
 
355
- // エフェクト固有の処理
356
  await this.applySpecialEffect(ctx, canvas, options);
357
 
358
- return canvas.toDataURL('image/png');
 
359
  }
360
 
361
  /**
 
133
  */
134
  calculateCoordinates(ctx, lines, metrics, lineSpacing, padding) {
135
  this.coordinates = [];
 
136
  const verticalLetterSpacing = parseFloat(ctx.canvas.dataset.verticalSpacing) || 1.3;
137
 
138
  if (ctx.canvas.dataset.vertical === 'true') {
 
325
  * エフェクトを適用する
326
  * @param {string} text - レンダリングするテキスト
327
  * @param {Object} options - オプション
328
+ * @returns {Promise<HTMLCanvasElement>} - 生成された画像のHTMLCanvasElement
329
  */
330
  async apply(text, options) {
331
+ const padding = this.getPadding();
332
  const canvas = document.createElement('canvas');
333
  const ctx = canvas.getContext('2d');
 
 
 
 
334
 
335
+ // データセットの設定
336
+ canvas.dataset.vertical = options.vertical.toString();
337
+ canvas.dataset.verticalSpacing = options.verticalSpacing?.toString() || "1.3";
338
+
339
+ // サイズの計算
340
  const { width, height, metrics, lineSpacing, lines } = this.calculateSize(ctx, text, options);
 
341
  canvas.width = width + padding * 2;
342
  canvas.height = height + padding * 2;
343
 
344
+ // コンテキストの設定
 
 
 
345
  await this.setupContext(ctx, options);
346
 
347
  // テキストの描画
348
  await this.renderText(ctx, lines, metrics, lineSpacing, padding);
349
 
350
+ // 特殊効果の適用
351
  await this.applySpecialEffect(ctx, canvas, options);
352
 
353
+ // canvasを返す
354
+ return canvas;
355
  }
356
 
357
  /**
index.html CHANGED
@@ -60,6 +60,18 @@
60
  </div>
61
  </div>
62
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  <div class="effect-grid">
64
  <!-- プリセットギャラリーはJavaScriptで動的に追加されます -->
65
  </div>
 
60
  </div>
61
  </div>
62
 
63
+ <div class="mb-3">
64
+ <label class="form-label">ポストプロセス</label>
65
+ <div class="btn-group w-100 flex-wrap gap-1" role="group" id="postProcessContainer">
66
+ <div class="btn-check-wrapper">
67
+ <input type="checkbox" class="btn-check" name="postProcess" id="postProcessDummy" value="dummy">
68
+ <label class="btn btn-outline-success" for="postProcessDummy">
69
+ ダミー
70
+ </label>
71
+ </div>
72
+ </div>
73
+ </div>
74
+
75
  <div class="effect-grid">
76
  <!-- プリセットギャラリーはJavaScriptで動的に追加されます -->
77
  </div>
index.js CHANGED
@@ -1,504 +1,581 @@
1
- import { applyEffect, getAvailableEffects } from './effects.js';
2
-
3
- const tagDisplayNames = {
4
- japanese: "日本語",
5
- english: "英語",
6
- kanji: "漢字対応",
7
- business: "ビジネス",
8
- fancy: "装飾的",
9
- playful: "遊び心",
10
- display: "ディスプレイ",
11
- handwritten: "手書き",
12
- retro: "レトロ",
13
- calm: "落ち着いた",
14
- cute: "かわいい",
15
- script: "筆記体",
16
- bold: "太字",
17
- horror: "ホラー",
18
- comic: "コミック"
19
- };
20
-
21
- const fontTags = [
22
- // 日本語フォント
23
- { name: "Aoboshi One", tags: ["japanese"] },
24
- { name: "BIZ UDGothic", tags: ["japanese", "kanji", "business"] },
25
- { name: "BIZ UDMincho", tags: ["japanese", "kanji", "business"] },
26
- { name: "BIZ UDPGothic", tags: ["japanese", "kanji", "business"] },
27
- { name: "BIZ UDPMincho", tags: ["japanese", "kanji", "business"] },
28
- { name: "Cherry Bomb One", tags: ["japanese", "cute"] },
29
- { name: "Chokokutai", tags: ["japanese", "fancy"] },
30
- { name: "Darumadrop One", tags: ["japanese", "playful"] },
31
- { name: "Dela Gothic One", tags: ["japanese", "kanji", "display"] },
32
- { name: "DotGothic16", tags: ["japanese", "kanji", "retro"] },
33
- { name: "Hachi Maru Pop", tags: ["japanese", "kanji", "cute"] },
34
- { name: "Hina Mincho", tags: ["japanese", "kanji", "fancy"] },
35
- { name: "IBM Plex Sans JP", tags: ["japanese", "kanji", "business"] },
36
- { name: "Kaisei Decol", tags: ["japanese", "kanji", "fancy"] },
37
- { name: "Kaisei HarunoUmi", tags: ["japanese", "kanji", "fancy"] },
38
- { name: "Kaisei Opti", tags: ["japanese", "kanji", "business"] },
39
- { name: "Kaisei Tokumin", tags: ["japanese", "kanji", "business"] },
40
- { name: "Kiwi Maru", tags: ["japanese", "kanji", "cute"] },
41
- { name: "Klee One", tags: ["japanese", "kanji", "handwritten"] },
42
- { name: "Kosugi", tags: ["japanese", "kanji", "business"] },
43
- { name: "Kosugi Maru", tags: ["japanese", "kanji", "calm"] },
44
- { name: "M PLUS 1", tags: ["japanese", "kanji", "business"] },
45
- { name: "M PLUS 1 Code", tags: ["japanese", "kanji", "display"] },
46
- { name: "M PLUS 1p", tags: ["japanese", "kanji", "business"] },
47
- { name: "M PLUS 2", tags: ["japanese", "kanji", "business"] },
48
- { name: "M PLUS Rounded 1c", tags: ["japanese", "kanji", "calm"] },
49
- { name: "Mochiy Pop One", tags: ["japanese", "kanji", "playful"] },
50
- { name: "Mochiy Pop P One", tags: ["japanese", "kanji", "playful"] },
51
- { name: "Monomaniac One", tags: ["japanese", "display"] },
52
- { name: "Murecho", tags: ["japanese", "business"] },
53
- { name: "New Tegomin", tags: ["japanese", "kanji", "fancy"] },
54
- { name: "Noto Sans JP", tags: ["japanese", "kanji", "business"] },
55
- { name: "Noto Serif JP", tags: ["japanese", "kanji", "business"] },
56
- { name: "Palette Mosaic", tags: ["japanese", "display"] },
57
- { name: "Potta One", tags: ["japanese", "kanji", "playful"] },
58
- { name: "Rampart One", tags: ["japanese", "kanji", "display"] },
59
- { name: "Reggae One", tags: ["japanese", "kanji", "display"] },
60
- { name: "Rock 3D", tags: ["japanese", "display"] },
61
- { name: "RocknRoll One", tags: ["japanese", "kanji", "playful"] },
62
- { name: "Sawarabi Gothic", tags: ["japanese", "kanji", "business"] },
63
- { name: "Sawarabi Mincho", tags: ["japanese", "kanji", "business"] },
64
- { name: "Shippori Antique", tags: ["japanese", "kanji", "retro"] },
65
- { name: "Shippori Antique B1", tags: ["japanese", "kanji", "retro"] },
66
- { name: "Shippori Mincho", tags: ["japanese", "kanji", "business"] },
67
- { name: "Shippori Mincho B1", tags: ["japanese", "kanji", "business"] },
68
- { name: "Shizuru", tags: ["japanese", "display"] },
69
- { name: "Slackside One", tags: ["japanese", "handwritten"] },
70
- { name: "Stick", tags: ["japanese", "kanji", "display"] },
71
- { name: "Train One", tags: ["japanese", "kanji", "display"] },
72
- { name: "Tsukimi Rounded", tags: ["japanese", "calm"] },
73
- { name: "Yomogi", tags: ["japanese", "kanji", "handwritten"] },
74
- { name: "Yuji Boku", tags: ["japanese", "kanji", "fancy"] },
75
- { name: "Yuji Hentaigana Akari", tags: ["japanese", "fancy"] },
76
- { name: "Yuji Hentaigana Akebono", tags: ["japanese", "fancy"] },
77
- { name: "Yuji Mai", tags: ["japanese", "kanji", "fancy"] },
78
- { name: "Yuji Syuku", tags: ["japanese", "kanji", "fancy"] },
79
- { name: "Yusei Magic", tags: ["japanese", "kanji", "playful"] },
80
- { name: "Zen Antique", tags: ["japanese", "kanji", "retro"] },
81
- { name: "Zen Antique Soft", tags: ["japanese", "kanji", "retro"] },
82
- { name: "Zen Kaku Gothic Antique", tags: ["japanese", "kanji", "business"] },
83
- { name: "Zen Kaku Gothic New", tags: ["japanese", "kanji", "business"] },
84
- { name: "Zen Kurenaido", tags: ["japanese", "calm"] },
85
- { name: "Zen Maru Gothic", tags: ["japanese", "calm"] },
86
- { name: "Zen Old Mincho", tags: ["japanese", "kanji", "retro"] },
87
-
88
- // 英語フォント - ビジ���ス/フォーマル
89
- { name: "Montserrat", tags: ["english", "business"] },
90
- { name: "Playfair Display", tags: ["english", "business", "fancy"] },
91
- { name: "Roboto", tags: ["english", "business"] },
92
- { name: "Lato", tags: ["english", "business"] },
93
- { name: "Poppins", tags: ["english", "business", "calm"] },
94
- { name: "Quicksand", tags: ["english", "calm"] },
95
- { name: "Raleway", tags: ["english", "calm"] },
96
-
97
- // デコラティブ/ファンシー
98
- { name: "Pacifico", tags: ["english", "fancy", "script"] },
99
- { name: "Great Vibes", tags: ["english", "fancy", "script"] },
100
- { name: "Lobster", tags: ["english", "fancy"] },
101
- { name: "Dancing Script", tags: ["english", "fancy", "script"] },
102
- { name: "Satisfy", tags: ["english", "fancy", "script"] },
103
- { name: "Courgette", tags: ["english", "fancy", "script"] },
104
- { name: "Kaushan Script", tags: ["english", "fancy", "script"] },
105
- { name: "Sacramento", tags: ["english", "fancy", "script", "handwritten"] },
106
-
107
- // かわいい/プレイフル
108
- { name: "Bubblegum Sans", tags: ["english", "display", "cute", "playful"] },
109
- { name: "Comic Neue", tags: ["english", "comic", "cute", "handwritten"] },
110
- { name: "Sniglet", tags: ["english", "display", "cute", "playful"] },
111
- { name: "Patrick Hand", tags: ["english", "handwritten", "playful"] },
112
- { name: "Indie Flower", tags: ["english", "handwritten", "playful"] },
113
-
114
- // 手書き/筆記体
115
- { name: "Caveat", tags: ["english", "handwritten", "script"] },
116
- { name: "Shadows Into Light", tags: ["english", "handwritten"] },
117
- { name: "Architects Daughter", tags: ["english", "handwritten"] },
118
- { name: "Covered By Your Grace", tags: ["english", "handwritten"] },
119
- { name: "Just Another Hand", tags: ["english", "handwritten"] },
120
-
121
- // 太字/ディスプレイ
122
- { name: "Righteous", tags: ["english", "display"] },
123
- { name: "Permanent Marker", tags: ["english", "display", "handwritten"] },
124
- { name: "Press Start 2P", tags: ["english", "display", "retro"] },
125
- { name: "Fredoka One", tags: ["english", "display", "playful"] },
126
- { name: "Creepster", tags: ["english", "display", "horror"] },
127
- { name: "Bangers", tags: ["english", "display", "comic"] },
128
- { name: "Rubik Mono One", tags: ["english", "display", "bold"] },
129
- { name: "Bungee", tags: ["english", "display", "bold"] },
130
- { name: "Bungee Shade", tags: ["english", "display", "fancy"] },
131
- { name: "Monoton", tags: ["english", "display", "retro"] },
132
- { name: "Anton", tags: ["english", "display", "bold"] },
133
- { name: "Bebas Neue", tags: ["english", "display", "bold"] },
134
- { name: "Black Ops One", tags: ["english", "display", "bold"] },
135
- { name: "Bowlby One SC", tags: ["english", "display", "bold"] }
136
- ];
137
-
138
-
139
- // フォントの読み込みを管理する関数
140
- async function loadGoogleFont(fontFamily) {
141
- // フォントファミリー名を正しく整形
142
- const formattedFamily = fontFamily.replace(/ /g, '+');
143
-
144
- // Google Fonts APIのURLを構築
145
- const url = `https://fonts.googleapis.com/css2?family=${formattedFamily}&display=swap`;
146
-
147
- // 既存のリンクタグがあれば削除
148
- const existingLink = document.querySelector(`link[href*="${formattedFamily}"]`);
149
- if (existingLink) {
150
- existingLink.remove();
151
- }
152
-
153
- // 新しいリンクタグを追加
154
- const link = document.createElement('link');
155
- link.href = url;
156
- link.rel = 'stylesheet';
157
- document.head.appendChild(link);
158
-
159
- // フォントの読み込みを待つ
160
- await new Promise((resolve, reject) => {
161
- link.onload = async () => {
162
- try {
163
- // フォントの読み込みを確認
164
- await document.fonts.load(`16px "${fontFamily}"`);
165
- // 少し待機して確実にフォントを利用可能にする
166
- setTimeout(resolve, 100);
167
- } catch (error) {
168
- reject(error);
169
- }
170
- };
171
- link.onerror = reject;
172
- });
173
- }
174
-
175
- // テキストを画像に変換する関数を更新
176
- async function textToImage(text, fontFamily, fontSize = '48px', effectType = 'simple') {
177
- console.debug(`テキスト描画開始: ${effectType}`, { text, fontFamily, fontSize });
178
- try {
179
- await document.fonts.load(`${fontSize} "${fontFamily}"`);
180
- const fontSizeNum = parseInt(fontSize);
181
- const verticalText = document.getElementById('verticalText').checked;
182
- const verticalSpacing = document.getElementById('verticalSpacing').value;
183
-
184
- // エフェクトを適用
185
- const imageUrl = await applyEffect(effectType, text, {
186
- font: fontFamily,
187
- fontSize: fontSizeNum,
188
- vertical: verticalText,
189
- verticalSpacing: verticalSpacing
190
- });
191
-
192
- return imageUrl;
193
- } catch (error) {
194
- console.error('フォント描画エラー:', error);
195
- throw error;
196
- }
197
- }
198
-
199
- // デバウンス関数の実装
200
- let renderTimeout = null;
201
- let isRendering = false;
202
-
203
- function debounceRender(callback, delay = 200) {
204
- if (renderTimeout) {
205
- clearTimeout(renderTimeout);
206
- }
207
-
208
- if (isRendering) {
209
- return;
210
- }
211
-
212
- renderTimeout = setTimeout(async () => {
213
- isRendering = true;
214
- try {
215
- await callback();
216
- } finally {
217
- isRendering = false;
218
- }
219
- }, delay);
220
- }
221
-
222
- // イベントリスナーの設定を更新
223
- document.addEventListener('DOMContentLoaded', async () => {
224
- const fontSelect = document.getElementById('googleFontInput');
225
- const fontTagFilter = document.getElementById('fontTagFilter');
226
- const textInput = document.getElementById('textInput');
227
- const fontSizeInput = document.getElementById('fontSize');
228
- const verticalTextInput = document.getElementById('verticalText');
229
- const effectGrid = document.querySelector('.effect-grid');
230
- const noFontsMessage = document.getElementById('noFontsMessage');
231
-
232
- // 利用可能なタグを収集し、使用頻度をカウント
233
- function getTagsWithCount() {
234
- const tagCount = new Map();
235
- fontTags.forEach(font => {
236
- font.tags.forEach(tag => {
237
- tagCount.set(tag, (tagCount.get(tag) || 0) + 1);
238
- });
239
- });
240
- return tagCount;
241
- }
242
-
243
- // 言語関連のタグかどうかを判定
244
- function isLanguageTag(tag) {
245
- return ['japanese', 'english', 'kanji'].includes(tag);
246
- }
247
-
248
- // フィルターボタンを動的に生成
249
- function createFilterButtons() {
250
- const tagCount = getTagsWithCount();
251
- fontTagFilter.innerHTML = '';
252
-
253
- // タグを言語関連とその他に分類し、個数でソート
254
- const languageTags = [...tagCount.entries()]
255
- .filter(([tag]) => isLanguageTag(tag))
256
- .sort((a, b) => b[1] - a[1]);
257
-
258
- const otherTags = [...tagCount.entries()]
259
- .filter(([tag]) => !isLanguageTag(tag))
260
- .sort((a, b) => b[1] - a[1]);
261
-
262
- // 言語関連のタグを追加
263
- if (languageTags.length > 0) {
264
- const langGroup = document.createElement('div');
265
- langGroup.className = 'filter-group mb-2';
266
- langGroup.innerHTML = '<div class="filter-group-label mb-1">言語</div>';
267
-
268
- const langButtonGroup = document.createElement('div');
269
- langButtonGroup.className = 'btn-group-wrapper';
270
-
271
- languageTags.forEach(([tag, count]) => {
272
- const displayName = tagDisplayNames[tag] || tag;
273
- const button = document.createElement('div');
274
- button.className = 'btn-check-wrapper';
275
- button.innerHTML = `
276
- <input type="checkbox" class="btn-check" name="fontFilter" id="filter${tag}" value="${tag}">
277
- <label class="btn btn-outline-primary" for="filter${tag}">
278
- ${displayName} <span class="tag-count" data-tag="${tag}">(${count})</span>
279
- </label>
280
- `;
281
- langButtonGroup.appendChild(button);
282
- });
283
-
284
- langGroup.appendChild(langButtonGroup);
285
- fontTagFilter.appendChild(langGroup);
286
- }
287
-
288
- // その他のタグを追加
289
- if (otherTags.length > 0) {
290
- const otherGroup = document.createElement('div');
291
- otherGroup.className = 'filter-group';
292
- otherGroup.innerHTML = '<div class="filter-group-label mb-1">スタイル</div>';
293
-
294
- const otherButtonGroup = document.createElement('div');
295
- otherButtonGroup.className = 'btn-group-wrapper';
296
-
297
- otherTags.forEach(([tag, count]) => {
298
- const displayName = tagDisplayNames[tag] || tag;
299
- const button = document.createElement('div');
300
- button.className = 'btn-check-wrapper';
301
- button.innerHTML = `
302
- <input type="checkbox" class="btn-check" name="fontFilter" id="filter${tag}" value="${tag}">
303
- <label class="btn btn-outline-primary" for="filter${tag}">
304
- ${displayName} <span class="tag-count" data-tag="${tag}">(${count})</span>
305
- </label>
306
- `;
307
- otherButtonGroup.appendChild(button);
308
- });
309
-
310
- otherGroup.appendChild(otherButtonGroup);
311
- fontTagFilter.appendChild(otherGroup);
312
- }
313
- }
314
-
315
- // 選択されたフィルターに基づいてタグカウントを更新
316
- function updateTagCounts() {
317
- const selectedFilters = Array.from(fontTagFilter.querySelectorAll('input[type="checkbox"]:checked'))
318
- .map(checkbox => checkbox.value);
319
-
320
- // 選���されたフィルターがない場合は、全体の数を表示し、すべてのボタンを表示
321
- if (selectedFilters.length === 0) {
322
- const totalCounts = getTagsWithCount();
323
- totalCounts.forEach((count, tag) => {
324
- const wrapper = document.querySelector(`#filter${tag}`).closest('.btn-check-wrapper');
325
- wrapper.style.display = 'inline-block';
326
- const countElement = document.querySelector(`.tag-count[data-tag="${tag}"]`);
327
- if (countElement) {
328
- countElement.textContent = `(${count})`;
329
- }
330
- });
331
- return;
332
- }
333
-
334
- // 各タグについて、現在の選択と組み合わせた場合の数を計算
335
- const allTags = [...new Set(fontTags.flatMap(font => font.tags))];
336
- allTags.forEach(tag => {
337
- const countElement = document.querySelector(`.tag-count[data-tag="${tag}"]`);
338
- const wrapper = document.querySelector(`#filter${tag}`).closest('.btn-check-wrapper');
339
-
340
- if (countElement && wrapper) {
341
- // このタグが既に選択されている場合は、他の選択されたタグとの組み合わせ数を表示
342
- const filtersToCheck = selectedFilters.includes(tag)
343
- ? selectedFilters
344
- : [...selectedFilters, tag];
345
-
346
- const count = fontTags.filter(font =>
347
- filtersToCheck.every(filter => font.tags.includes(filter))
348
- ).length;
349
-
350
- countElement.textContent = `(${count})`;
351
-
352
- // カウントが0の場合、かつ現在選択されていないタグの場合は非表示
353
- if (count === 0 && !selectedFilters.includes(tag)) {
354
- wrapper.style.display = 'none';
355
- } else {
356
- wrapper.style.display = 'inline-block';
357
- }
358
- }
359
- });
360
- }
361
-
362
- // フォントオプションの初期化と絞り込み機能の実装
363
- function initializeFontOptions() {
364
- // 現在選択されているフォントを保持
365
- const currentFont = fontSelect.value;
366
-
367
- // 選択されているフィルターを取得
368
- const selectedFilters = Array.from(fontTagFilter.querySelectorAll('input[type="checkbox"]:checked'))
369
- .map(checkbox => checkbox.value);
370
-
371
- // 既存のオプションをクリア
372
- fontSelect.innerHTML = '';
373
-
374
- // フィルタリングされたフォントのリストを作成
375
- const filteredFonts = selectedFilters.length === 0
376
- ? fontTags
377
- : fontTags.filter(font =>
378
- selectedFilters.every(filter => font.tags.includes(filter))
379
- );
380
-
381
- // フォルター結果が0件の場合
382
- if (filteredFonts.length === 0) {
383
- noFontsMessage.style.display = 'block';
384
- fontSelect.disabled = true;
385
- return Promise.resolve(); // フォントの読み込みは不要
386
- } else {
387
- noFontsMessage.style.display = 'none';
388
- fontSelect.disabled = false;
389
- }
390
-
391
- // フォントオプションを追加
392
- filteredFonts.forEach((font, index) => {
393
- const option = document.createElement('option');
394
- option.value = font.name;
395
- option.textContent = font.name;
396
- // 以前選択されていたフォントがある場合はそれを選択、なければ最初のフォントを選択
397
- if (font.name === currentFont || (index === 0 && !currentFont)) {
398
- option.selected = true;
399
- }
400
- fontSelect.appendChild(option);
401
- });
402
-
403
- // タグカウントを更新
404
- updateTagCounts();
405
-
406
- // 選択されたフォントを読み込む
407
- return loadGoogleFont(fontSelect.value);
408
- }
409
-
410
- // タグフィルターの変更イベントを設定
411
- fontTagFilter.addEventListener('change', async (e) => {
412
- if (e.target.type === 'checkbox') {
413
- await initializeFontOptions();
414
- if (!fontSelect.disabled) { // フォントが選択可能な場合のみプレビューを更新
415
- await renderAllPresets();
416
- }
417
- }
418
- });
419
-
420
- // 初期化
421
- createFilterButtons();
422
- await initializeFontOptions();
423
- await loadGoogleFont(fontSelect.value);
424
-
425
- // 縦書きモードの状態をグリッドに反映
426
- verticalTextInput.addEventListener('change', (e) => {
427
- effectGrid.dataset.vertical = e.target.checked;
428
- renderAllPresets();
429
- });
430
-
431
- // すべてのプリセットを描画する関数
432
- async function renderAllPresets() {
433
- effectGrid.innerHTML = '';
434
- const text = textInput.value || 'プレビュー';
435
- const fontFamily = fontSelect.value;
436
- const fontSize = fontSizeInput.value + 'px';
437
-
438
- const effects = getAvailableEffects();
439
- for (const effect of effects) {
440
- try {
441
- const imageUrl = await textToImage(text, fontFamily, fontSize, effect.name);
442
-
443
- const presetCard = document.createElement('div');
444
- presetCard.className = 'effect-item';
445
- presetCard.innerHTML = `
446
- <div class="effect-name">${effect.name}</div>
447
- <div class="preview-container">
448
- <img src="${imageUrl}" alt="${effect.name}">
449
- </div>
450
- `;
451
-
452
- effectGrid.appendChild(presetCard);
453
- } catch (error) {
454
- console.error(`プリセット ${effect.name} の描画エラー:`, error);
455
-
456
- const errorCard = document.createElement('div');
457
- errorCard.className = 'effect-item error';
458
- errorCard.innerHTML = `
459
- <div class="effect-name text-danger">${effect.name}</div>
460
- <div class="preview-container">
461
- <div class="text-danger">
462
- <small>エラー: ${error.message}</small>
463
- </div>
464
- </div>
465
- `;
466
- effectGrid.appendChild(errorCard);
467
- }
468
- }
469
- }
470
-
471
- // フォント変更時の処理
472
- fontSelect.addEventListener('change', async (e) => {
473
- try {
474
- const fontFamily = e.target.value;
475
- await loadGoogleFont(fontFamily);
476
- await renderAllPresets();
477
- } catch (error) {
478
- console.error('フォント読み込みエラー:', error);
479
- }
480
- });
481
-
482
- // テキストとフォントサイズの変更時にすべてのプリセットを再描画
483
- [textInput, fontSizeInput, verticalTextInput, verticalSpacing].forEach(element => {
484
- element.addEventListener('input', () => {
485
- debounceRender(renderAllPresets);
486
- });
487
- });
488
-
489
- // 初期描画
490
- await renderAllPresets();
491
- });
492
-
493
- // 縦書きモードの切り替え時の処理
494
- document.getElementById('verticalText').addEventListener('change', function (e) {
495
- const spacingContainer = document.getElementById('verticalSpacingContainer');
496
- spacingContainer.style.display = e.target.checked ? 'block' : 'none';
497
- // ロゴの再生成処理を呼び出す
498
- });
499
-
500
- // 文字間隔の変更時の処理
501
- document.getElementById('verticalSpacing').addEventListener('input', function (e) {
502
- document.getElementById('verticalSpacingValue').textContent = e.target.value;
503
- // ロゴの再生成処理を呼び出す
504
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { applyEffect, getAvailableEffects } from './effects.js';
2
+ import { DummyPostProcess } from './postprocess/dummy.js';
3
+ import { BasePostProcess } from './postprocess/base.js';
4
+
5
+ const tagDisplayNames = {
6
+ japanese: "日本語",
7
+ english: "英語",
8
+ kanji: "漢字対応",
9
+ business: "ビジネス",
10
+ fancy: "装飾的",
11
+ playful: "遊び心",
12
+ display: "ディスプレイ",
13
+ handwritten: "手書き",
14
+ retro: "レトロ",
15
+ calm: "落ち着いた",
16
+ cute: "かわいい",
17
+ script: "筆記体",
18
+ bold: "太字",
19
+ horror: "ホラー",
20
+ comic: "コミック"
21
+ };
22
+
23
+ const fontTags = [
24
+ // 日本語フォント
25
+ { name: "Aoboshi One", tags: ["japanese"] },
26
+ { name: "BIZ UDGothic", tags: ["japanese", "kanji", "business"] },
27
+ { name: "BIZ UDMincho", tags: ["japanese", "kanji", "business"] },
28
+ { name: "BIZ UDPGothic", tags: ["japanese", "kanji", "business"] },
29
+ { name: "BIZ UDPMincho", tags: ["japanese", "kanji", "business"] },
30
+ { name: "Cherry Bomb One", tags: ["japanese", "cute"] },
31
+ { name: "Chokokutai", tags: ["japanese", "fancy"] },
32
+ { name: "Darumadrop One", tags: ["japanese", "playful"] },
33
+ { name: "Dela Gothic One", tags: ["japanese", "kanji", "display"] },
34
+ { name: "DotGothic16", tags: ["japanese", "kanji", "retro"] },
35
+ { name: "Hachi Maru Pop", tags: ["japanese", "kanji", "cute"] },
36
+ { name: "Hina Mincho", tags: ["japanese", "kanji", "fancy"] },
37
+ { name: "IBM Plex Sans JP", tags: ["japanese", "kanji", "business"] },
38
+ { name: "Kaisei Decol", tags: ["japanese", "kanji", "fancy"] },
39
+ { name: "Kaisei HarunoUmi", tags: ["japanese", "kanji", "fancy"] },
40
+ { name: "Kaisei Opti", tags: ["japanese", "kanji", "business"] },
41
+ { name: "Kaisei Tokumin", tags: ["japanese", "kanji", "business"] },
42
+ { name: "Kiwi Maru", tags: ["japanese", "kanji", "cute"] },
43
+ { name: "Klee One", tags: ["japanese", "kanji", "handwritten"] },
44
+ { name: "Kosugi", tags: ["japanese", "kanji", "business"] },
45
+ { name: "Kosugi Maru", tags: ["japanese", "kanji", "calm"] },
46
+ { name: "M PLUS 1", tags: ["japanese", "kanji", "business"] },
47
+ { name: "M PLUS 1 Code", tags: ["japanese", "kanji", "display"] },
48
+ { name: "M PLUS 1p", tags: ["japanese", "kanji", "business"] },
49
+ { name: "M PLUS 2", tags: ["japanese", "kanji", "business"] },
50
+ { name: "M PLUS Rounded 1c", tags: ["japanese", "kanji", "calm"] },
51
+ { name: "Mochiy Pop One", tags: ["japanese", "kanji", "playful"] },
52
+ { name: "Mochiy Pop P One", tags: ["japanese", "kanji", "playful"] },
53
+ { name: "Monomaniac One", tags: ["japanese", "display"] },
54
+ { name: "Murecho", tags: ["japanese", "business"] },
55
+ { name: "New Tegomin", tags: ["japanese", "kanji", "fancy"] },
56
+ { name: "Noto Sans JP", tags: ["japanese", "kanji", "business"] },
57
+ { name: "Noto Serif JP", tags: ["japanese", "kanji", "business"] },
58
+ { name: "Palette Mosaic", tags: ["japanese", "display"] },
59
+ { name: "Potta One", tags: ["japanese", "kanji", "playful"] },
60
+ { name: "Rampart One", tags: ["japanese", "kanji", "display"] },
61
+ { name: "Reggae One", tags: ["japanese", "kanji", "display"] },
62
+ { name: "Rock 3D", tags: ["japanese", "display"] },
63
+ { name: "RocknRoll One", tags: ["japanese", "kanji", "playful"] },
64
+ { name: "Sawarabi Gothic", tags: ["japanese", "kanji", "business"] },
65
+ { name: "Sawarabi Mincho", tags: ["japanese", "kanji", "business"] },
66
+ { name: "Shippori Antique", tags: ["japanese", "kanji", "retro"] },
67
+ { name: "Shippori Antique B1", tags: ["japanese", "kanji", "retro"] },
68
+ { name: "Shippori Mincho", tags: ["japanese", "kanji", "business"] },
69
+ { name: "Shippori Mincho B1", tags: ["japanese", "kanji", "business"] },
70
+ { name: "Shizuru", tags: ["japanese", "display"] },
71
+ { name: "Slackside One", tags: ["japanese", "handwritten"] },
72
+ { name: "Stick", tags: ["japanese", "kanji", "display"] },
73
+ { name: "Train One", tags: ["japanese", "kanji", "display"] },
74
+ { name: "Tsukimi Rounded", tags: ["japanese", "calm"] },
75
+ { name: "Yomogi", tags: ["japanese", "kanji", "handwritten"] },
76
+ { name: "Yuji Boku", tags: ["japanese", "kanji", "fancy"] },
77
+ { name: "Yuji Hentaigana Akari", tags: ["japanese", "fancy"] },
78
+ { name: "Yuji Hentaigana Akebono", tags: ["japanese", "fancy"] },
79
+ { name: "Yuji Mai", tags: ["japanese", "kanji", "fancy"] },
80
+ { name: "Yuji Syuku", tags: ["japanese", "kanji", "fancy"] },
81
+ { name: "Yusei Magic", tags: ["japanese", "kanji", "playful"] },
82
+ { name: "Zen Antique", tags: ["japanese", "kanji", "retro"] },
83
+ { name: "Zen Antique Soft", tags: ["japanese", "kanji", "retro"] },
84
+ { name: "Zen Kaku Gothic Antique", tags: ["japanese", "kanji", "business"] },
85
+ { name: "Zen Kaku Gothic New", tags: ["japanese", "kanji", "business"] },
86
+ { name: "Zen Kurenaido", tags: ["japanese", "calm"] },
87
+ { name: "Zen Maru Gothic", tags: ["japanese", "calm"] },
88
+ { name: "Zen Old Mincho", tags: ["japanese", "kanji", "retro"] },
89
+
90
+ // 英語フォント - ビジネス/フォーマル
91
+ { name: "Montserrat", tags: ["english", "business"] },
92
+ { name: "Playfair Display", tags: ["english", "business", "fancy"] },
93
+ { name: "Roboto", tags: ["english", "business"] },
94
+ { name: "Lato", tags: ["english", "business"] },
95
+ { name: "Poppins", tags: ["english", "business", "calm"] },
96
+ { name: "Quicksand", tags: ["english", "calm"] },
97
+ { name: "Raleway", tags: ["english", "calm"] },
98
+
99
+ // デコラティブ/ファンシー
100
+ { name: "Pacifico", tags: ["english", "fancy", "script"] },
101
+ { name: "Great Vibes", tags: ["english", "fancy", "script"] },
102
+ { name: "Lobster", tags: ["english", "fancy"] },
103
+ { name: "Dancing Script", tags: ["english", "fancy", "script"] },
104
+ { name: "Satisfy", tags: ["english", "fancy", "script"] },
105
+ { name: "Courgette", tags: ["english", "fancy", "script"] },
106
+ { name: "Kaushan Script", tags: ["english", "fancy", "script"] },
107
+ { name: "Sacramento", tags: ["english", "fancy", "script", "handwritten"] },
108
+
109
+ // かわいい/プレイフル
110
+ { name: "Bubblegum Sans", tags: ["english", "display", "cute", "playful"] },
111
+ { name: "Comic Neue", tags: ["english", "comic", "cute", "handwritten"] },
112
+ { name: "Sniglet", tags: ["english", "display", "cute", "playful"] },
113
+ { name: "Patrick Hand", tags: ["english", "handwritten", "playful"] },
114
+ { name: "Indie Flower", tags: ["english", "handwritten", "playful"] },
115
+
116
+ // 手書き/筆記体
117
+ { name: "Caveat", tags: ["english", "handwritten", "script"] },
118
+ { name: "Shadows Into Light", tags: ["english", "handwritten"] },
119
+ { name: "Architects Daughter", tags: ["english", "handwritten"] },
120
+ { name: "Covered By Your Grace", tags: ["english", "handwritten"] },
121
+ { name: "Just Another Hand", tags: ["english", "handwritten"] },
122
+
123
+ // 太字/ディスプレイ
124
+ { name: "Righteous", tags: ["english", "display"] },
125
+ { name: "Permanent Marker", tags: ["english", "display", "handwritten"] },
126
+ { name: "Press Start 2P", tags: ["english", "display", "retro"] },
127
+ { name: "Fredoka One", tags: ["english", "display", "playful"] },
128
+ { name: "Creepster", tags: ["english", "display", "horror"] },
129
+ { name: "Bangers", tags: ["english", "display", "comic"] },
130
+ { name: "Rubik Mono One", tags: ["english", "display", "bold"] },
131
+ { name: "Bungee", tags: ["english", "display", "bold"] },
132
+ { name: "Bungee Shade", tags: ["english", "display", "fancy"] },
133
+ { name: "Monoton", tags: ["english", "display", "retro"] },
134
+ { name: "Anton", tags: ["english", "display", "bold"] },
135
+ { name: "Bebas Neue", tags: ["english", "display", "bold"] },
136
+ { name: "Black Ops One", tags: ["english", "display", "bold"] },
137
+ { name: "Bowlby One SC", tags: ["english", "display", "bold"] }
138
+ ];
139
+
140
+
141
+ // フォントの読み込みを管理する関数
142
+ async function loadGoogleFont(fontFamily) {
143
+ // フォントファミリー名を正しく整形
144
+ const formattedFamily = fontFamily.replace(/ /g, '+');
145
+
146
+ // Google Fonts APIのURLを構築
147
+ const url = `https://fonts.googleapis.com/css2?family=${formattedFamily}&display=swap`;
148
+
149
+ // 既存のリンクタグがあれば削除
150
+ const existingLink = document.querySelector(`link[href*="${formattedFamily}"]`);
151
+ if (existingLink) {
152
+ existingLink.remove();
153
+ }
154
+
155
+ // 新しいリンクタグを追加
156
+ const link = document.createElement('link');
157
+ link.href = url;
158
+ link.rel = 'stylesheet';
159
+ document.head.appendChild(link);
160
+
161
+ // フォントの読み込みを待つ
162
+ await new Promise((resolve, reject) => {
163
+ link.onload = async () => {
164
+ try {
165
+ // フォントの読み込みを確認
166
+ await document.fonts.load(`16px "${fontFamily}"`);
167
+ // 少し待機して確実にフォントを利用可能にする
168
+ setTimeout(resolve, 100);
169
+ } catch (error) {
170
+ reject(error);
171
+ }
172
+ };
173
+ link.onerror = reject;
174
+ });
175
+ }
176
+
177
+ // テキストを画像に変換する関数を更新
178
+ async function textToImage(text, fontFamily, fontSize = '48px', effectType = 'simple') {
179
+ console.debug(`テキスト描画開始: ${effectType}`, { text, fontFamily, fontSize });
180
+ try {
181
+ await document.fonts.load(`${fontSize} "${fontFamily}"`);
182
+ const fontSizeNum = parseInt(fontSize);
183
+ const verticalText = document.getElementById('verticalText').checked;
184
+ const verticalSpacing = document.getElementById('verticalSpacing').value;
185
+
186
+ // エフェクトを適用してcanvasを取得
187
+ const canvas = await applyEffect(effectType, text, {
188
+ font: fontFamily,
189
+ fontSize: fontSizeNum,
190
+ vertical: verticalText,
191
+ verticalSpacing: verticalSpacing
192
+ });
193
+
194
+ // ポストプロセスを適用
195
+ const processedCanvas = await applyPostProcessors(canvas);
196
+
197
+ // PNG化して返す
198
+ return BasePostProcess.toPng(processedCanvas);
199
+ } catch (error) {
200
+ console.error('フォント描画エラー:', error);
201
+ throw error;
202
+ }
203
+ }
204
+
205
+ // デバウンス関数の実装
206
+ let renderTimeout = null;
207
+ let isRendering = false;
208
+
209
+ function debounceRender(callback, delay = 200) {
210
+ if (renderTimeout) {
211
+ clearTimeout(renderTimeout);
212
+ }
213
+
214
+ if (isRendering) {
215
+ return;
216
+ }
217
+
218
+ renderTimeout = setTimeout(async () => {
219
+ isRendering = true;
220
+ try {
221
+ await callback();
222
+ } finally {
223
+ isRendering = false;
224
+ }
225
+ }, delay);
226
+ }
227
+
228
+ // ポストプロセス処理のインスタンスを作成
229
+ const postProcessors = {
230
+ dummy: new DummyPostProcess()
231
+ };
232
+
233
+ /**
234
+ * 選択されているポストプロセスを取得
235
+ */
236
+ function getSelectedPostProcessors() {
237
+ const container = document.getElementById('postProcessContainer');
238
+ const checkboxes = container.querySelectorAll('input[type="checkbox"]:checked');
239
+ return Array.from(checkboxes).map(cb => postProcessors[cb.value]).filter(Boolean);
240
+ }
241
+
242
+ /**
243
+ * ポストプロセスを適用
244
+ */
245
+ async function applyPostProcessors(canvas) {
246
+ let currentCanvas = canvas;
247
+ const processors = getSelectedPostProcessors();
248
+
249
+ for (const processor of processors) {
250
+ currentCanvas = await processor.apply(currentCanvas);
251
+ }
252
+
253
+ return currentCanvas;
254
+ }
255
+
256
+ /**
257
+ * プレビューを更新
258
+ */
259
+ async function updatePreview(effectType) {
260
+ const text = document.getElementById('textInput').value;
261
+ const font = document.getElementById('googleFontInput').value;
262
+ const fontSize = parseInt(document.getElementById('fontSize').value);
263
+ const vertical = document.getElementById('verticalText').checked;
264
+ const verticalSpacing = document.getElementById('verticalSpacing').value;
265
+
266
+ try {
267
+ // エフェクトタイプが指定されていない場合は更新しない
268
+ if (!effectType) return;
269
+
270
+ // エフェクトを適用してcanvasを取得
271
+ const canvas = await applyEffect(effectType, text, {
272
+ font,
273
+ fontSize,
274
+ vertical,
275
+ verticalSpacing
276
+ });
277
+
278
+ // ポストプロセスを適用
279
+ const processedCanvas = await applyPostProcessors(canvas);
280
+
281
+ // PNG化してプレビューに表示
282
+ const dataUrl = BasePostProcess.toPng(processedCanvas);
283
+ const previewImage = document.querySelector(`.effect-item[data-effect="${effectType}"] img`);
284
+ if (previewImage) {
285
+ previewImage.src = dataUrl;
286
+ }
287
+ } catch (error) {
288
+ console.error('プレビューの更新に失敗しました:', error);
289
+ }
290
+ }
291
+
292
+ // イベントリスナーを追加(ポストプロセスの変更時には何もしない)
293
+ document.getElementById('postProcessContainer').addEventListener('change', () => {
294
+ // 現時点では何もしない
295
+ });
296
+
297
+ // イベントリスナーの設定を更新
298
+ document.addEventListener('DOMContentLoaded', async () => {
299
+ const fontSelect = document.getElementById('googleFontInput');
300
+ const fontTagFilter = document.getElementById('fontTagFilter');
301
+ const textInput = document.getElementById('textInput');
302
+ const fontSizeInput = document.getElementById('fontSize');
303
+ const verticalTextInput = document.getElementById('verticalText');
304
+ const effectGrid = document.querySelector('.effect-grid');
305
+ const noFontsMessage = document.getElementById('noFontsMessage');
306
+
307
+ // 利用可能なタグを収集し、使用頻度をカウント
308
+ function getTagsWithCount() {
309
+ const tagCount = new Map();
310
+ fontTags.forEach(font => {
311
+ font.tags.forEach(tag => {
312
+ tagCount.set(tag, (tagCount.get(tag) || 0) + 1);
313
+ });
314
+ });
315
+ return tagCount;
316
+ }
317
+
318
+ // 言語関連のタグかどうかを判定
319
+ function isLanguageTag(tag) {
320
+ return ['japanese', 'english', 'kanji'].includes(tag);
321
+ }
322
+
323
+ // フィルターボタンを動的に生成
324
+ function createFilterButtons() {
325
+ const tagCount = getTagsWithCount();
326
+ fontTagFilter.innerHTML = '';
327
+
328
+ // タグを言語関連とその他に分類し、個数でソート
329
+ const languageTags = [...tagCount.entries()]
330
+ .filter(([tag]) => isLanguageTag(tag))
331
+ .sort((a, b) => b[1] - a[1]);
332
+
333
+ const otherTags = [...tagCount.entries()]
334
+ .filter(([tag]) => !isLanguageTag(tag))
335
+ .sort((a, b) => b[1] - a[1]);
336
+
337
+ // 言語関連のタグを追加
338
+ if (languageTags.length > 0) {
339
+ const langGroup = document.createElement('div');
340
+ langGroup.className = 'filter-group mb-2';
341
+ langGroup.innerHTML = '<div class="filter-group-label mb-1">言語</div>';
342
+
343
+ const langButtonGroup = document.createElement('div');
344
+ langButtonGroup.className = 'btn-group-wrapper';
345
+
346
+ languageTags.forEach(([tag, count]) => {
347
+ const displayName = tagDisplayNames[tag] || tag;
348
+ const button = document.createElement('div');
349
+ button.className = 'btn-check-wrapper';
350
+ button.innerHTML = `
351
+ <input type="checkbox" class="btn-check" name="fontFilter" id="filter${tag}" value="${tag}">
352
+ <label class="btn btn-outline-primary" for="filter${tag}">
353
+ ${displayName} <span class="tag-count" data-tag="${tag}">(${count})</span>
354
+ </label>
355
+ `;
356
+ langButtonGroup.appendChild(button);
357
+ });
358
+
359
+ langGroup.appendChild(langButtonGroup);
360
+ fontTagFilter.appendChild(langGroup);
361
+ }
362
+
363
+ // その他のタグを追加
364
+ if (otherTags.length > 0) {
365
+ const otherGroup = document.createElement('div');
366
+ otherGroup.className = 'filter-group';
367
+ otherGroup.innerHTML = '<div class="filter-group-label mb-1">スタイル</div>';
368
+
369
+ const otherButtonGroup = document.createElement('div');
370
+ otherButtonGroup.className = 'btn-group-wrapper';
371
+
372
+ otherTags.forEach(([tag, count]) => {
373
+ const displayName = tagDisplayNames[tag] || tag;
374
+ const button = document.createElement('div');
375
+ button.className = 'btn-check-wrapper';
376
+ button.innerHTML = `
377
+ <input type="checkbox" class="btn-check" name="fontFilter" id="filter${tag}" value="${tag}">
378
+ <label class="btn btn-outline-primary" for="filter${tag}">
379
+ ${displayName} <span class="tag-count" data-tag="${tag}">(${count})</span>
380
+ </label>
381
+ `;
382
+ otherButtonGroup.appendChild(button);
383
+ });
384
+
385
+ otherGroup.appendChild(otherButtonGroup);
386
+ fontTagFilter.appendChild(otherGroup);
387
+ }
388
+ }
389
+
390
+ // 選択されたフィルターに基づいてタグカウントを更新
391
+ function updateTagCounts() {
392
+ const selectedFilters = Array.from(fontTagFilter.querySelectorAll('input[type="checkbox"]:checked'))
393
+ .map(checkbox => checkbox.value);
394
+
395
+ // 選択されたフィルターがない場合は、全体の数を表示し、すべてのボタンを表示
396
+ if (selectedFilters.length === 0) {
397
+ const totalCounts = getTagsWithCount();
398
+ totalCounts.forEach((count, tag) => {
399
+ const wrapper = document.querySelector(`#filter${tag}`).closest('.btn-check-wrapper');
400
+ wrapper.style.display = 'inline-block';
401
+ const countElement = document.querySelector(`.tag-count[data-tag="${tag}"]`);
402
+ if (countElement) {
403
+ countElement.textContent = `(${count})`;
404
+ }
405
+ });
406
+ return;
407
+ }
408
+
409
+ // 各タグについて、現在の選択と組み合わせた場合の数を計算
410
+ const allTags = [...new Set(fontTags.flatMap(font => font.tags))];
411
+ allTags.forEach(tag => {
412
+ const countElement = document.querySelector(`.tag-count[data-tag="${tag}"]`);
413
+ const wrapper = document.querySelector(`#filter${tag}`).closest('.btn-check-wrapper');
414
+
415
+ if (countElement && wrapper) {
416
+ // このタグが既に選択されている場合は、他の選択されたタグとの組み合わせ数を表示
417
+ const filtersToCheck = selectedFilters.includes(tag)
418
+ ? selectedFilters
419
+ : [...selectedFilters, tag];
420
+
421
+ const count = fontTags.filter(font =>
422
+ filtersToCheck.every(filter => font.tags.includes(filter))
423
+ ).length;
424
+
425
+ countElement.textContent = `(${count})`;
426
+
427
+ // カウントが0の場合、かつ現在選択されていないタグの場合は非表示
428
+ if (count === 0 && !selectedFilters.includes(tag)) {
429
+ wrapper.style.display = 'none';
430
+ } else {
431
+ wrapper.style.display = 'inline-block';
432
+ }
433
+ }
434
+ });
435
+ }
436
+
437
+ // フォントオプションの初期化と絞り込み機能の実装
438
+ function initializeFontOptions() {
439
+ // 現在選択されているフォントを保持
440
+ const currentFont = fontSelect.value;
441
+
442
+ // 選択されているフィルターを取得
443
+ const selectedFilters = Array.from(fontTagFilter.querySelectorAll('input[type="checkbox"]:checked'))
444
+ .map(checkbox => checkbox.value);
445
+
446
+ // 既存のオプションをクリア
447
+ fontSelect.innerHTML = '';
448
+
449
+ // フィルタリングされたフォントのリストを作成
450
+ const filteredFonts = selectedFilters.length === 0
451
+ ? fontTags
452
+ : fontTags.filter(font =>
453
+ selectedFilters.every(filter => font.tags.includes(filter))
454
+ );
455
+
456
+ // フォルター結果が0件の場合
457
+ if (filteredFonts.length === 0) {
458
+ noFontsMessage.style.display = 'block';
459
+ fontSelect.disabled = true;
460
+ return Promise.resolve(); // フォントの読み込みは不要
461
+ } else {
462
+ noFontsMessage.style.display = 'none';
463
+ fontSelect.disabled = false;
464
+ }
465
+
466
+ // フォントオプションを追加
467
+ filteredFonts.forEach((font, index) => {
468
+ const option = document.createElement('option');
469
+ option.value = font.name;
470
+ option.textContent = font.name;
471
+ // 以前選択されていたフォントがある場合はそれを選択、なければ最初のフォントを選択
472
+ if (font.name === currentFont || (index === 0 && !currentFont)) {
473
+ option.selected = true;
474
+ }
475
+ fontSelect.appendChild(option);
476
+ });
477
+
478
+ // タグカウントを更新
479
+ updateTagCounts();
480
+
481
+ // 選択されたフォントを読み込む
482
+ return loadGoogleFont(fontSelect.value);
483
+ }
484
+
485
+ // タグフィルターの変更イベントを設定
486
+ fontTagFilter.addEventListener('change', async (e) => {
487
+ if (e.target.type === 'checkbox') {
488
+ await initializeFontOptions();
489
+ if (!fontSelect.disabled) { // フォントが選択可能な場合のみプレビューを更新
490
+ await renderAllPresets();
491
+ }
492
+ }
493
+ });
494
+
495
+ // 初期化
496
+ createFilterButtons();
497
+ await initializeFontOptions();
498
+ await loadGoogleFont(fontSelect.value);
499
+
500
+ // 縦書きモードの状態をグリッドに反映
501
+ verticalTextInput.addEventListener('change', (e) => {
502
+ effectGrid.dataset.vertical = e.target.checked;
503
+ renderAllPresets();
504
+ });
505
+
506
+ // すべてのプリセットを描画する関数
507
+ async function renderAllPresets() {
508
+ effectGrid.innerHTML = '';
509
+ const text = textInput.value || 'プレビュー';
510
+ const fontFamily = fontSelect.value;
511
+ const fontSize = fontSizeInput.value + 'px';
512
+
513
+ const effects = getAvailableEffects();
514
+ for (const effect of effects) {
515
+ try {
516
+ const imageUrl = await textToImage(text, fontFamily, fontSize, effect.name);
517
+
518
+ const presetCard = document.createElement('div');
519
+ presetCard.className = 'effect-item';
520
+ presetCard.dataset.effect = effect.name;
521
+ presetCard.innerHTML = `
522
+ <div class="effect-name">${effect.name}</div>
523
+ <div class="preview-container">
524
+ <img src="${imageUrl}" alt="${effect.name}">
525
+ </div>
526
+ `;
527
+
528
+ effectGrid.appendChild(presetCard);
529
+ } catch (error) {
530
+ console.error(`プリセット ${effect.name} の描画エラー:`, error);
531
+
532
+ const errorCard = document.createElement('div');
533
+ errorCard.className = 'effect-item error';
534
+ errorCard.dataset.effect = effect.name;
535
+ errorCard.innerHTML = `
536
+ <div class="effect-name text-danger">${effect.name}</div>
537
+ <div class="preview-container">
538
+ <div class="text-danger">
539
+ <small>エラー: ${error.message}</small>
540
+ </div>
541
+ </div>
542
+ `;
543
+ effectGrid.appendChild(errorCard);
544
+ }
545
+ }
546
+ }
547
+
548
+ // フォント変更時の処理
549
+ fontSelect.addEventListener('change', async (e) => {
550
+ try {
551
+ const fontFamily = e.target.value;
552
+ await loadGoogleFont(fontFamily);
553
+ await renderAllPresets();
554
+ } catch (error) {
555
+ console.error('フォント読み込みエラー:', error);
556
+ }
557
+ });
558
+
559
+ // テキストとフォントサイズの変更時にすべてのプリセットを再描画
560
+ [textInput, fontSizeInput, verticalTextInput, verticalSpacing].forEach(element => {
561
+ element.addEventListener('input', () => {
562
+ debounceRender(renderAllPresets);
563
+ });
564
+ });
565
+
566
+ // 初期描画
567
+ await renderAllPresets();
568
+ });
569
+
570
+ // 縦書きモードの切り替え時の処理
571
+ document.getElementById('verticalText').addEventListener('change', function (e) {
572
+ const spacingContainer = document.getElementById('verticalSpacingContainer');
573
+ spacingContainer.style.display = e.target.checked ? 'block' : 'none';
574
+ // ロゴの再生成処理を呼び出す
575
+ });
576
+
577
+ // 文字間隔の変更時の処理
578
+ document.getElementById('verticalSpacing').addEventListener('input', function (e) {
579
+ document.getElementById('verticalSpacingValue').textContent = e.target.value;
580
+ // ロゴの再生成処理を呼び出す
581
+ });
styles.css CHANGED
@@ -1,175 +1,180 @@
1
- .preview-container {
2
- position: relative;
3
- padding: 1rem;
4
- min-height: 100px;
5
- display: flex;
6
- align-items: center;
7
- justify-content: center;
8
- background: repeating-conic-gradient(#80808010 0% 25%, transparent 0% 50%) 50% / 20px 20px;
9
- }
10
-
11
- .preview-container img {
12
- max-width: 100%;
13
- max-height: 300px;
14
- height: auto;
15
- object-fit: contain;
16
- }
17
-
18
- .effect-grid {
19
- display: grid;
20
- gap: 1rem;
21
- margin-top: 2rem;
22
- }
23
-
24
- /* 横書き時のグリッドレイアウト */
25
- .effect-grid:not([data-vertical="true"]) {
26
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
27
- }
28
-
29
- /* 縦書き時のグリッドレイアウト */
30
- .effect-grid[data-vertical="true"] {
31
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
32
- }
33
-
34
- .effect-item {
35
- background: #f8f9fa;
36
- border: 1px solid #dee2e6;
37
- border-radius: 0.5rem;
38
- padding: 1rem;
39
- text-align: center;
40
- display: flex;
41
- flex-direction: column;
42
- }
43
-
44
- .effect-name {
45
- font-size: 1.1rem;
46
- margin-bottom: 1rem;
47
- font-weight: 500;
48
- }
49
-
50
- .effect-item.error {
51
- background: #fff3f3;
52
- border-color: #ffcdd2;
53
- }
54
-
55
- .text-danger {
56
- color: #dc3545;
57
- }
58
-
59
- /* エフェクトレンダリング用のスタイル */
60
- .effect-renderer {
61
- position: absolute;
62
- top: 0;
63
- left: 0;
64
- width: 100%;
65
- height: 100%;
66
- pointer-events: none;
67
- }
68
-
69
- .effect-renderer canvas {
70
- position: absolute;
71
- top: 0;
72
- left: 0;
73
- }
74
-
75
- /* パーティクルエフェクト用のスタイル */
76
- .particle-container {
77
- position: absolute;
78
- top: 0;
79
- left: 0;
80
- width: 100%;
81
- height: 100%;
82
- pointer-events: none;
83
- z-index: 1;
84
- }
85
-
86
- /* 3Dエフェクト用のスタイル */
87
- .three-container {
88
- position: absolute;
89
- top: 0;
90
- left: 0;
91
- width: 100%;
92
- height: 100%;
93
- pointer-events: none;
94
- z-index: 2;
95
- }
96
-
97
- /* アニメーションエフェクト用のスタイル */
98
- @keyframes sparkle {
99
- 0%, 100% { opacity: 0; }
100
- 50% { opacity: 1; }
101
- }
102
-
103
- @keyframes fire {
104
- 0% { transform: translateY(0) scale(1); }
105
- 100% { transform: translateY(-20px) scale(0.8); opacity: 0; }
106
- }
107
-
108
- @keyframes electric {
109
- 0%, 100% { opacity: 1; }
110
- 50% { opacity: 0.7; }
111
- }
112
-
113
- @keyframes underwater {
114
- 0% { transform: translateY(0) translateX(0); }
115
- 50% { transform: translateY(-10px) translateX(5px); }
116
- 100% { transform: translateY(0) translateX(0); }
117
- }
118
-
119
- .btn-check-wrapper {
120
- display: inline-block;
121
- margin: 2px;
122
- }
123
-
124
- .btn-check-wrapper .btn {
125
- margin: 0;
126
- white-space: nowrap;
127
- }
128
-
129
- #fontTagFilter {
130
- display: flex;
131
- flex-direction: column;
132
- gap: 1rem;
133
- width: 100%;
134
- }
135
-
136
- #fontTagFilter .btn {
137
- border-radius: 0.25rem;
138
- font-size: 0.875rem;
139
- padding: 0.25rem 0.5rem;
140
- }
141
-
142
- /* チェックされた状態のスタイル */
143
- .btn-check:checked + .btn-outline-primary {
144
- background-color: #0d6efd;
145
- color: white;
146
- }
147
-
148
- .filter-group {
149
- margin-bottom: 1rem;
150
- width: 100%;
151
- }
152
-
153
- .filter-group-label {
154
- font-size: 0.875rem;
155
- font-weight: 600;
156
- color: #6c757d;
157
- margin-bottom: 0.5rem;
158
- }
159
-
160
- .btn-group-wrapper {
161
- display: flex;
162
- flex-wrap: wrap;
163
- gap: 0.25rem;
164
- }
165
-
166
- .tag-count {
167
- font-size: 0.75rem;
168
- opacity: 0.8;
169
- }
170
-
171
- /* ボタンのホバー時もタグカウントの視認性を保持 */
172
- .btn-check:checked + .btn-outline-primary .tag-count,
173
- .btn-outline-primary:hover .tag-count {
174
- opacity: 1;
 
 
 
 
 
175
  }
 
1
+ .preview-container {
2
+ position: relative;
3
+ padding: 1rem;
4
+ min-height: 100px;
5
+ display: flex;
6
+ align-items: center;
7
+ justify-content: center;
8
+ background: repeating-conic-gradient(#80808010 0% 25%, transparent 0% 50%) 50% / 20px 20px;
9
+ }
10
+
11
+ .preview-container img {
12
+ max-width: 100%;
13
+ max-height: 300px;
14
+ height: auto;
15
+ object-fit: contain;
16
+ }
17
+
18
+ .effect-grid {
19
+ display: grid;
20
+ gap: 1rem;
21
+ margin-top: 2rem;
22
+ }
23
+
24
+ /* 横書き時のグリッドレイアウト */
25
+ .effect-grid:not([data-vertical="true"]) {
26
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
27
+ }
28
+
29
+ /* 縦書き時のグリッドレイアウト */
30
+ .effect-grid[data-vertical="true"] {
31
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
32
+ }
33
+
34
+ .effect-item {
35
+ background: #f8f9fa;
36
+ border: 1px solid #dee2e6;
37
+ border-radius: 0.5rem;
38
+ padding: 1rem;
39
+ text-align: center;
40
+ display: flex;
41
+ flex-direction: column;
42
+ }
43
+
44
+ .effect-name {
45
+ font-size: 1.1rem;
46
+ margin-bottom: 1rem;
47
+ font-weight: 500;
48
+ }
49
+
50
+ .effect-item.error {
51
+ background: #fff3f3;
52
+ border-color: #ffcdd2;
53
+ }
54
+
55
+ .text-danger {
56
+ color: #dc3545;
57
+ }
58
+
59
+ /* エフェクトレンダリング用のスタイル */
60
+ .effect-renderer {
61
+ position: absolute;
62
+ top: 0;
63
+ left: 0;
64
+ width: 100%;
65
+ height: 100%;
66
+ pointer-events: none;
67
+ }
68
+
69
+ .effect-renderer canvas {
70
+ position: absolute;
71
+ top: 0;
72
+ left: 0;
73
+ }
74
+
75
+ /* パーティクルエフェクト用のスタイル */
76
+ .particle-container {
77
+ position: absolute;
78
+ top: 0;
79
+ left: 0;
80
+ width: 100%;
81
+ height: 100%;
82
+ pointer-events: none;
83
+ z-index: 1;
84
+ }
85
+
86
+ /* 3Dエフェクト用のスタイル */
87
+ .three-container {
88
+ position: absolute;
89
+ top: 0;
90
+ left: 0;
91
+ width: 100%;
92
+ height: 100%;
93
+ pointer-events: none;
94
+ z-index: 2;
95
+ }
96
+
97
+ /* アニメーションエフェクト用のスタイル */
98
+ @keyframes sparkle {
99
+ 0%, 100% { opacity: 0; }
100
+ 50% { opacity: 1; }
101
+ }
102
+
103
+ @keyframes fire {
104
+ 0% { transform: translateY(0) scale(1); }
105
+ 100% { transform: translateY(-20px) scale(0.8); opacity: 0; }
106
+ }
107
+
108
+ @keyframes electric {
109
+ 0%, 100% { opacity: 1; }
110
+ 50% { opacity: 0.7; }
111
+ }
112
+
113
+ @keyframes underwater {
114
+ 0% { transform: translateY(0) translateX(0); }
115
+ 50% { transform: translateY(-10px) translateX(5px); }
116
+ 100% { transform: translateY(0) translateX(0); }
117
+ }
118
+
119
+ .btn-check-wrapper {
120
+ display: inline-block;
121
+ margin: 2px;
122
+ }
123
+
124
+ .btn-check-wrapper .btn {
125
+ margin: 0;
126
+ white-space: nowrap;
127
+ }
128
+
129
+ #fontTagFilter {
130
+ display: flex;
131
+ flex-direction: column;
132
+ gap: 1rem;
133
+ width: 100%;
134
+ }
135
+
136
+ #fontTagFilter .btn {
137
+ border-radius: 0.25rem;
138
+ font-size: 0.875rem;
139
+ padding: 0.25rem 0.5rem;
140
+ }
141
+
142
+ /* チェックされた状態のスタイル */
143
+ .btn-check:checked + .btn-outline-primary {
144
+ background-color: #0d6efd;
145
+ color: white;
146
+ }
147
+
148
+ .btn-check:checked + .btn-outline-success {
149
+ background-color: #198754;
150
+ color: white;
151
+ }
152
+
153
+ .filter-group {
154
+ margin-bottom: 1rem;
155
+ width: 100%;
156
+ }
157
+
158
+ .filter-group-label {
159
+ font-size: 0.875rem;
160
+ font-weight: 600;
161
+ color: #6c757d;
162
+ margin-bottom: 0.5rem;
163
+ }
164
+
165
+ .btn-group-wrapper {
166
+ display: flex;
167
+ flex-wrap: wrap;
168
+ gap: 0.25rem;
169
+ }
170
+
171
+ .tag-count {
172
+ font-size: 0.75rem;
173
+ opacity: 0.8;
174
+ }
175
+
176
+ /* ボタンのホバー時もタグカウントの視認性を保持 */
177
+ .btn-check:checked + .btn-outline-primary .tag-count,
178
+ .btn-outline-primary:hover .tag-count {
179
+ opacity: 1;
180
  }