|
import { applyEffect, getAvailableEffects } from './effects.js'; |
|
import { InvertPostProcess } from './postprocess/invert.js'; |
|
import { HuePostProcess } from './postprocess/hue.js'; |
|
import { BasePostProcess } from './postprocess/base.js'; |
|
|
|
const tagDisplayNames = { |
|
japanese: "日本語", |
|
english: "英語", |
|
kanji: "漢字対応", |
|
business: "ビジネス", |
|
fancy: "装飾的", |
|
playful: "遊び心", |
|
display: "ディスプレイ", |
|
handwritten: "手書き", |
|
retro: "レトロ", |
|
calm: "落ち着いた", |
|
cute: "かわいい", |
|
script: "筆記体", |
|
bold: "太字", |
|
horror: "ホラー", |
|
comic: "コミック" |
|
}; |
|
|
|
const fontTags = [ |
|
|
|
{ name: "Aoboshi One", tags: ["japanese"] }, |
|
{ name: "BIZ UDGothic", tags: ["japanese", "kanji", "business"] }, |
|
{ name: "BIZ UDMincho", tags: ["japanese", "kanji", "business"] }, |
|
{ name: "BIZ UDPGothic", tags: ["japanese", "kanji", "business"] }, |
|
{ name: "BIZ UDPMincho", tags: ["japanese", "kanji", "business"] }, |
|
{ name: "Cherry Bomb One", tags: ["japanese", "cute"] }, |
|
{ name: "Chokokutai", tags: ["japanese", "fancy"] }, |
|
{ name: "Darumadrop One", tags: ["japanese", "playful"] }, |
|
{ name: "Dela Gothic One", tags: ["japanese", "kanji", "display"] }, |
|
{ name: "DotGothic16", tags: ["japanese", "kanji", "retro"] }, |
|
{ name: "Hachi Maru Pop", tags: ["japanese", "kanji", "cute"] }, |
|
{ name: "Hina Mincho", tags: ["japanese", "kanji", "fancy"] }, |
|
{ name: "IBM Plex Sans JP", tags: ["japanese", "kanji", "business"] }, |
|
{ name: "Kaisei Decol", tags: ["japanese", "kanji", "fancy"] }, |
|
{ name: "Kaisei HarunoUmi", tags: ["japanese", "kanji", "fancy"] }, |
|
{ name: "Kaisei Opti", tags: ["japanese", "kanji", "business"] }, |
|
{ name: "Kaisei Tokumin", tags: ["japanese", "kanji", "business"] }, |
|
{ name: "Kiwi Maru", tags: ["japanese", "kanji", "cute"] }, |
|
{ name: "Klee One", tags: ["japanese", "kanji", "handwritten"] }, |
|
{ name: "Kosugi", tags: ["japanese", "kanji", "business"] }, |
|
{ name: "Kosugi Maru", tags: ["japanese", "kanji", "calm"] }, |
|
{ name: "M PLUS 1", tags: ["japanese", "kanji", "business"] }, |
|
{ name: "M PLUS 1 Code", tags: ["japanese", "kanji", "display"] }, |
|
{ name: "M PLUS 1p", tags: ["japanese", "kanji", "business"] }, |
|
{ name: "M PLUS 2", tags: ["japanese", "kanji", "business"] }, |
|
{ name: "M PLUS Rounded 1c", tags: ["japanese", "kanji", "calm"] }, |
|
{ name: "Mochiy Pop One", tags: ["japanese", "kanji", "playful"] }, |
|
{ name: "Mochiy Pop P One", tags: ["japanese", "kanji", "playful"] }, |
|
{ name: "Monomaniac One", tags: ["japanese", "display"] }, |
|
{ name: "Murecho", tags: ["japanese", "business"] }, |
|
{ name: "New Tegomin", tags: ["japanese", "kanji", "fancy"] }, |
|
{ name: "Noto Sans JP", tags: ["japanese", "kanji", "business"] }, |
|
{ name: "Noto Serif JP", tags: ["japanese", "kanji", "business"] }, |
|
{ name: "Palette Mosaic", tags: ["japanese", "display"] }, |
|
{ name: "Potta One", tags: ["japanese", "kanji", "playful"] }, |
|
{ name: "Rampart One", tags: ["japanese", "kanji", "display"] }, |
|
{ name: "Reggae One", tags: ["japanese", "kanji", "display"] }, |
|
{ name: "Rock 3D", tags: ["japanese", "display"] }, |
|
{ name: "RocknRoll One", tags: ["japanese", "kanji", "playful"] }, |
|
{ name: "Sawarabi Gothic", tags: ["japanese", "kanji", "business"] }, |
|
{ name: "Sawarabi Mincho", tags: ["japanese", "kanji", "business"] }, |
|
{ name: "Shippori Antique", tags: ["japanese", "kanji", "retro"] }, |
|
{ name: "Shippori Antique B1", tags: ["japanese", "kanji", "retro"] }, |
|
{ name: "Shippori Mincho", tags: ["japanese", "kanji", "business"] }, |
|
{ name: "Shippori Mincho B1", tags: ["japanese", "kanji", "business"] }, |
|
{ name: "Shizuru", tags: ["japanese", "display"] }, |
|
{ name: "Slackside One", tags: ["japanese", "handwritten"] }, |
|
{ name: "Stick", tags: ["japanese", "kanji", "display"] }, |
|
{ name: "Train One", tags: ["japanese", "kanji", "display"] }, |
|
{ name: "Tsukimi Rounded", tags: ["japanese", "calm"] }, |
|
{ name: "Yomogi", tags: ["japanese", "kanji", "handwritten"] }, |
|
{ name: "Yuji Boku", tags: ["japanese", "kanji", "fancy"] }, |
|
{ name: "Yuji Hentaigana Akari", tags: ["japanese", "fancy"] }, |
|
{ name: "Yuji Hentaigana Akebono", tags: ["japanese", "fancy"] }, |
|
{ name: "Yuji Mai", tags: ["japanese", "kanji", "fancy"] }, |
|
{ name: "Yuji Syuku", tags: ["japanese", "kanji", "fancy"] }, |
|
{ name: "Yusei Magic", tags: ["japanese", "kanji", "playful"] }, |
|
{ name: "Zen Antique", tags: ["japanese", "kanji", "retro"] }, |
|
{ name: "Zen Antique Soft", tags: ["japanese", "kanji", "retro"] }, |
|
{ name: "Zen Kaku Gothic Antique", tags: ["japanese", "kanji", "business"] }, |
|
{ name: "Zen Kaku Gothic New", tags: ["japanese", "kanji", "business"] }, |
|
{ name: "Zen Kurenaido", tags: ["japanese", "calm"] }, |
|
{ name: "Zen Maru Gothic", tags: ["japanese", "calm"] }, |
|
{ name: "Zen Old Mincho", tags: ["japanese", "kanji", "retro"] }, |
|
|
|
|
|
{ name: "Montserrat", tags: ["english", "business"] }, |
|
{ name: "Playfair Display", tags: ["english", "business", "fancy"] }, |
|
{ name: "Roboto", tags: ["english", "business"] }, |
|
{ name: "Lato", tags: ["english", "business"] }, |
|
{ name: "Poppins", tags: ["english", "business", "calm"] }, |
|
{ name: "Quicksand", tags: ["english", "calm"] }, |
|
{ name: "Raleway", tags: ["english", "calm"] }, |
|
|
|
|
|
{ name: "Pacifico", tags: ["english", "fancy", "script"] }, |
|
{ name: "Great Vibes", tags: ["english", "fancy", "script"] }, |
|
{ name: "Lobster", tags: ["english", "fancy"] }, |
|
{ name: "Dancing Script", tags: ["english", "fancy", "script"] }, |
|
{ name: "Satisfy", tags: ["english", "fancy", "script"] }, |
|
{ name: "Courgette", tags: ["english", "fancy", "script"] }, |
|
{ name: "Kaushan Script", tags: ["english", "fancy", "script"] }, |
|
{ name: "Sacramento", tags: ["english", "fancy", "script", "handwritten"] }, |
|
|
|
|
|
{ name: "Bubblegum Sans", tags: ["english", "display", "cute", "playful"] }, |
|
{ name: "Comic Neue", tags: ["english", "comic", "cute", "handwritten"] }, |
|
{ name: "Sniglet", tags: ["english", "display", "cute", "playful"] }, |
|
{ name: "Patrick Hand", tags: ["english", "handwritten", "playful"] }, |
|
{ name: "Indie Flower", tags: ["english", "handwritten", "playful"] }, |
|
|
|
|
|
{ name: "Caveat", tags: ["english", "handwritten", "script"] }, |
|
{ name: "Shadows Into Light", tags: ["english", "handwritten"] }, |
|
{ name: "Architects Daughter", tags: ["english", "handwritten"] }, |
|
{ name: "Covered By Your Grace", tags: ["english", "handwritten"] }, |
|
{ name: "Just Another Hand", tags: ["english", "handwritten"] }, |
|
|
|
|
|
{ name: "Righteous", tags: ["english", "display"] }, |
|
{ name: "Permanent Marker", tags: ["english", "display", "handwritten"] }, |
|
{ name: "Press Start 2P", tags: ["english", "display", "retro"] }, |
|
{ name: "Fredoka One", tags: ["english", "display", "playful"] }, |
|
{ name: "Creepster", tags: ["english", "display", "horror"] }, |
|
{ name: "Bangers", tags: ["english", "display", "comic"] }, |
|
{ name: "Rubik Mono One", tags: ["english", "display", "bold"] }, |
|
{ name: "Bungee", tags: ["english", "display", "bold"] }, |
|
{ name: "Bungee Shade", tags: ["english", "display", "fancy"] }, |
|
{ name: "Monoton", tags: ["english", "display", "retro"] }, |
|
{ name: "Anton", tags: ["english", "display", "bold"] }, |
|
{ name: "Bebas Neue", tags: ["english", "display", "bold"] }, |
|
{ name: "Black Ops One", tags: ["english", "display", "bold"] }, |
|
{ name: "Bowlby One SC", tags: ["english", "display", "bold"] } |
|
]; |
|
|
|
|
|
|
|
async function loadGoogleFont(fontFamily) { |
|
|
|
const formattedFamily = fontFamily.replace(/ /g, '+'); |
|
|
|
|
|
const url = `https://fonts.googleapis.com/css2?family=${formattedFamily}&display=swap`; |
|
|
|
|
|
const existingLink = document.querySelector(`link[href*="${formattedFamily}"]`); |
|
if (existingLink) { |
|
existingLink.remove(); |
|
} |
|
|
|
|
|
const link = document.createElement('link'); |
|
link.href = url; |
|
link.rel = 'stylesheet'; |
|
document.head.appendChild(link); |
|
|
|
|
|
await new Promise((resolve, reject) => { |
|
link.onload = async () => { |
|
try { |
|
|
|
await document.fonts.load(`16px "${fontFamily}"`); |
|
|
|
setTimeout(resolve, 100); |
|
} catch (error) { |
|
reject(error); |
|
} |
|
}; |
|
link.onerror = reject; |
|
}); |
|
} |
|
|
|
|
|
async function textToImage(text, fontFamily, fontSize = '48px', effectType = 'simple') { |
|
console.debug(`テキスト描画開始: ${effectType}`, { text, fontFamily, fontSize }); |
|
try { |
|
await document.fonts.load(`${fontSize} "${fontFamily}"`); |
|
const fontSizeNum = parseInt(fontSize); |
|
const verticalText = document.getElementById('verticalText').checked; |
|
const verticalSpacing = document.getElementById('verticalSpacing').value; |
|
|
|
|
|
const canvas = await applyEffect(effectType, text, { |
|
font: fontFamily, |
|
fontSize: fontSizeNum, |
|
vertical: verticalText, |
|
verticalSpacing: verticalSpacing |
|
}); |
|
|
|
|
|
const processedCanvas = await applyPostProcessors(canvas); |
|
|
|
|
|
return BasePostProcess.toPng(processedCanvas); |
|
} catch (error) { |
|
console.error('フォント描画エラー:', error); |
|
throw error; |
|
} |
|
} |
|
|
|
|
|
let renderTimeout = null; |
|
let isRendering = false; |
|
|
|
function debounceRender(callback, delay = 200) { |
|
if (renderTimeout) { |
|
clearTimeout(renderTimeout); |
|
} |
|
|
|
if (isRendering) { |
|
return; |
|
} |
|
|
|
renderTimeout = setTimeout(async () => { |
|
isRendering = true; |
|
try { |
|
await callback(); |
|
} finally { |
|
isRendering = false; |
|
} |
|
}, delay); |
|
} |
|
|
|
|
|
const postProcessors = { |
|
invert: new InvertPostProcess(), |
|
hue: new HuePostProcess() |
|
}; |
|
|
|
|
|
|
|
|
|
function createPostProcessCard(processor) { |
|
const div = document.createElement('div'); |
|
div.className = 'col-md-6 col-lg-4'; |
|
div.innerHTML = ` |
|
<div class="card h-100"> |
|
<div class="card-header d-flex align-items-center"> |
|
<div class="form-check mb-0"> |
|
<input type="checkbox" class="form-check-input" name="postProcess" id="postProcess${processor.name}" value="${processor.name}"> |
|
<label class="form-check-label" for="postProcess${processor.name}"> |
|
${processor.label} |
|
</label> |
|
</div> |
|
</div> |
|
<div class="card-body"> |
|
${processor.ui.template} |
|
</div> |
|
</div> |
|
`; |
|
return div; |
|
} |
|
|
|
|
|
|
|
|
|
function initializePostProcessUI() { |
|
const container = document.getElementById('postProcessContainer'); |
|
container.innerHTML = ''; |
|
|
|
|
|
Object.values(postProcessors).forEach(processor => { |
|
const card = createPostProcessCard(processor); |
|
container.appendChild(card); |
|
}); |
|
|
|
|
|
const hueRotate = document.getElementById('hueRotate'); |
|
if (hueRotate) { |
|
hueRotate.addEventListener('input', (e) => { |
|
document.getElementById('hueRotateValue').textContent = e.target.value; |
|
debounceRender(renderAllPresets); |
|
}); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
function getSelectedPostProcessors() { |
|
const container = document.getElementById('postProcessContainer'); |
|
const checkboxes = container.querySelectorAll('input[type="checkbox"]:checked'); |
|
return Array.from(checkboxes).map(cb => postProcessors[cb.value]).filter(Boolean); |
|
} |
|
|
|
|
|
|
|
|
|
async function applyPostProcessors(canvas) { |
|
let currentCanvas = canvas; |
|
const processors = getSelectedPostProcessors(); |
|
|
|
for (const processor of processors) { |
|
currentCanvas = await processor.apply(currentCanvas); |
|
} |
|
|
|
return currentCanvas; |
|
} |
|
|
|
|
|
|
|
|
|
async function updatePreview(effectType) { |
|
const text = document.getElementById('textInput').value; |
|
const font = document.getElementById('googleFontInput').value; |
|
const fontSize = parseInt(document.getElementById('fontSize').value); |
|
const vertical = document.getElementById('verticalText').checked; |
|
const verticalSpacing = document.getElementById('verticalSpacing').value; |
|
|
|
try { |
|
|
|
if (!effectType) return; |
|
|
|
|
|
const canvas = await applyEffect(effectType, text, { |
|
font, |
|
fontSize, |
|
vertical, |
|
verticalSpacing |
|
}); |
|
|
|
|
|
const processedCanvas = await applyPostProcessors(canvas); |
|
|
|
|
|
const dataUrl = BasePostProcess.toPng(processedCanvas); |
|
const previewImage = document.querySelector(`.effect-item[data-effect="${effectType}"] img`); |
|
if (previewImage) { |
|
previewImage.src = dataUrl; |
|
} |
|
} catch (error) { |
|
console.error('プレビューの更新に失敗しました:', error); |
|
} |
|
} |
|
|
|
|
|
async function renderAllPresets() { |
|
const effectGrid = document.querySelector('.effect-grid'); |
|
const textInput = document.getElementById('textInput'); |
|
const fontSelect = document.getElementById('googleFontInput'); |
|
const fontSizeInput = document.getElementById('fontSize'); |
|
|
|
effectGrid.innerHTML = ''; |
|
const text = textInput.value || 'プレビュー'; |
|
const fontFamily = fontSelect.value; |
|
const fontSize = fontSizeInput.value + 'px'; |
|
|
|
const effects = getAvailableEffects(); |
|
for (const effect of effects) { |
|
try { |
|
const imageUrl = await textToImage(text, fontFamily, fontSize, effect.name); |
|
|
|
const presetCard = document.createElement('div'); |
|
presetCard.className = 'effect-item'; |
|
presetCard.dataset.effect = effect.name; |
|
presetCard.innerHTML = ` |
|
<div class="effect-name">${effect.name}</div> |
|
<div class="preview-container"> |
|
<img src="${imageUrl}" alt="${effect.name}"> |
|
</div> |
|
`; |
|
|
|
effectGrid.appendChild(presetCard); |
|
} catch (error) { |
|
console.error(`プリセット ${effect.name} の描画エラー:`, error); |
|
|
|
const errorCard = document.createElement('div'); |
|
errorCard.className = 'effect-item error'; |
|
errorCard.dataset.effect = effect.name; |
|
errorCard.innerHTML = ` |
|
<div class="effect-name text-danger">${effect.name}</div> |
|
<div class="preview-container"> |
|
<div class="text-danger"> |
|
<small>エラー: ${error.message}</small> |
|
</div> |
|
</div> |
|
`; |
|
effectGrid.appendChild(errorCard); |
|
} |
|
} |
|
} |
|
|
|
|
|
document.getElementById('postProcessContainer').addEventListener('change', () => { |
|
renderAllPresets(); |
|
}); |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => { |
|
const fontSelect = document.getElementById('googleFontInput'); |
|
const fontTagFilter = document.getElementById('fontTagFilter'); |
|
const textInput = document.getElementById('textInput'); |
|
const fontSizeInput = document.getElementById('fontSize'); |
|
const verticalTextInput = document.getElementById('verticalText'); |
|
const effectGrid = document.querySelector('.effect-grid'); |
|
const noFontsMessage = document.getElementById('noFontsMessage'); |
|
|
|
|
|
function getTagsWithCount() { |
|
const tagCount = new Map(); |
|
fontTags.forEach(font => { |
|
font.tags.forEach(tag => { |
|
tagCount.set(tag, (tagCount.get(tag) || 0) + 1); |
|
}); |
|
}); |
|
return tagCount; |
|
} |
|
|
|
|
|
function isLanguageTag(tag) { |
|
return ['japanese', 'english', 'kanji'].includes(tag); |
|
} |
|
|
|
|
|
function createFilterButtons() { |
|
const tagCount = getTagsWithCount(); |
|
fontTagFilter.innerHTML = ''; |
|
|
|
|
|
const languageTags = [...tagCount.entries()] |
|
.filter(([tag]) => isLanguageTag(tag)) |
|
.sort((a, b) => b[1] - a[1]); |
|
|
|
const otherTags = [...tagCount.entries()] |
|
.filter(([tag]) => !isLanguageTag(tag)) |
|
.sort((a, b) => b[1] - a[1]); |
|
|
|
|
|
if (languageTags.length > 0) { |
|
const langGroup = document.createElement('div'); |
|
langGroup.className = 'filter-group mb-2'; |
|
langGroup.innerHTML = '<div class="filter-group-label mb-1">言語</div>'; |
|
|
|
const langButtonGroup = document.createElement('div'); |
|
langButtonGroup.className = 'btn-group-wrapper'; |
|
|
|
languageTags.forEach(([tag, count]) => { |
|
const displayName = tagDisplayNames[tag] || tag; |
|
const button = document.createElement('div'); |
|
button.className = 'btn-check-wrapper'; |
|
button.innerHTML = ` |
|
<input type="checkbox" class="btn-check" name="fontFilter" id="filter${tag}" value="${tag}"> |
|
<label class="btn btn-outline-primary" for="filter${tag}"> |
|
${displayName} <span class="tag-count" data-tag="${tag}">(${count})</span> |
|
</label> |
|
`; |
|
langButtonGroup.appendChild(button); |
|
}); |
|
|
|
langGroup.appendChild(langButtonGroup); |
|
fontTagFilter.appendChild(langGroup); |
|
} |
|
|
|
|
|
if (otherTags.length > 0) { |
|
const otherGroup = document.createElement('div'); |
|
otherGroup.className = 'filter-group'; |
|
otherGroup.innerHTML = '<div class="filter-group-label mb-1">スタイル</div>'; |
|
|
|
const otherButtonGroup = document.createElement('div'); |
|
otherButtonGroup.className = 'btn-group-wrapper'; |
|
|
|
otherTags.forEach(([tag, count]) => { |
|
const displayName = tagDisplayNames[tag] || tag; |
|
const button = document.createElement('div'); |
|
button.className = 'btn-check-wrapper'; |
|
button.innerHTML = ` |
|
<input type="checkbox" class="btn-check" name="fontFilter" id="filter${tag}" value="${tag}"> |
|
<label class="btn btn-outline-primary" for="filter${tag}"> |
|
${displayName} <span class="tag-count" data-tag="${tag}">(${count})</span> |
|
</label> |
|
`; |
|
otherButtonGroup.appendChild(button); |
|
}); |
|
|
|
otherGroup.appendChild(otherButtonGroup); |
|
fontTagFilter.appendChild(otherGroup); |
|
} |
|
} |
|
|
|
|
|
function updateTagCounts() { |
|
const selectedFilters = Array.from(fontTagFilter.querySelectorAll('input[type="checkbox"]:checked')) |
|
.map(checkbox => checkbox.value); |
|
|
|
|
|
if (selectedFilters.length === 0) { |
|
const totalCounts = getTagsWithCount(); |
|
totalCounts.forEach((count, tag) => { |
|
const wrapper = document.querySelector(`#filter${tag}`).closest('.btn-check-wrapper'); |
|
wrapper.style.display = 'inline-block'; |
|
const countElement = document.querySelector(`.tag-count[data-tag="${tag}"]`); |
|
if (countElement) { |
|
countElement.textContent = `(${count})`; |
|
} |
|
}); |
|
return; |
|
} |
|
|
|
|
|
const allTags = [...new Set(fontTags.flatMap(font => font.tags))]; |
|
allTags.forEach(tag => { |
|
const countElement = document.querySelector(`.tag-count[data-tag="${tag}"]`); |
|
const wrapper = document.querySelector(`#filter${tag}`).closest('.btn-check-wrapper'); |
|
|
|
if (countElement && wrapper) { |
|
|
|
const filtersToCheck = selectedFilters.includes(tag) |
|
? selectedFilters |
|
: [...selectedFilters, tag]; |
|
|
|
const count = fontTags.filter(font => |
|
filtersToCheck.every(filter => font.tags.includes(filter)) |
|
).length; |
|
|
|
countElement.textContent = `(${count})`; |
|
|
|
|
|
if (count === 0 && !selectedFilters.includes(tag)) { |
|
wrapper.style.display = 'none'; |
|
} else { |
|
wrapper.style.display = 'inline-block'; |
|
} |
|
} |
|
}); |
|
} |
|
|
|
|
|
function initializeFontOptions() { |
|
|
|
const currentFont = fontSelect.value; |
|
|
|
|
|
const selectedFilters = Array.from(fontTagFilter.querySelectorAll('input[type="checkbox"]:checked')) |
|
.map(checkbox => checkbox.value); |
|
|
|
|
|
fontSelect.innerHTML = ''; |
|
|
|
|
|
const filteredFonts = selectedFilters.length === 0 |
|
? fontTags |
|
: fontTags.filter(font => |
|
selectedFilters.every(filter => font.tags.includes(filter)) |
|
); |
|
|
|
|
|
if (filteredFonts.length === 0) { |
|
noFontsMessage.style.display = 'block'; |
|
fontSelect.disabled = true; |
|
return Promise.resolve(); |
|
} else { |
|
noFontsMessage.style.display = 'none'; |
|
fontSelect.disabled = false; |
|
} |
|
|
|
|
|
filteredFonts.forEach((font, index) => { |
|
const option = document.createElement('option'); |
|
option.value = font.name; |
|
option.textContent = font.name; |
|
|
|
if (font.name === currentFont || (index === 0 && !currentFont)) { |
|
option.selected = true; |
|
} |
|
fontSelect.appendChild(option); |
|
}); |
|
|
|
|
|
updateTagCounts(); |
|
|
|
|
|
return loadGoogleFont(fontSelect.value); |
|
} |
|
|
|
|
|
fontTagFilter.addEventListener('change', async (e) => { |
|
if (e.target.type === 'checkbox') { |
|
await initializeFontOptions(); |
|
if (!fontSelect.disabled) { |
|
await renderAllPresets(); |
|
} |
|
} |
|
}); |
|
|
|
|
|
createFilterButtons(); |
|
await initializeFontOptions(); |
|
await loadGoogleFont(fontSelect.value); |
|
|
|
|
|
verticalTextInput.addEventListener('change', (e) => { |
|
effectGrid.dataset.vertical = e.target.checked; |
|
renderAllPresets(); |
|
}); |
|
|
|
|
|
fontSelect.addEventListener('change', async (e) => { |
|
try { |
|
const fontFamily = e.target.value; |
|
await loadGoogleFont(fontFamily); |
|
await renderAllPresets(); |
|
} catch (error) { |
|
console.error('フォント読み込みエラー:', error); |
|
} |
|
}); |
|
|
|
|
|
[textInput, fontSizeInput, verticalTextInput, verticalSpacing].forEach(element => { |
|
element.addEventListener('input', () => { |
|
debounceRender(renderAllPresets); |
|
}); |
|
}); |
|
|
|
|
|
await renderAllPresets(); |
|
|
|
|
|
initializePostProcessUI(); |
|
}); |
|
|
|
|
|
document.getElementById('verticalText').addEventListener('change', function (e) { |
|
const spacingContainer = document.getElementById('verticalSpacingContainer'); |
|
spacingContainer.style.display = e.target.checked ? 'block' : 'none'; |
|
|
|
}); |
|
|
|
|
|
document.getElementById('verticalSpacing').addEventListener('input', function (e) { |
|
document.getElementById('verticalSpacingValue').textContent = e.target.value; |
|
|
|
}); |
|
|