|
import { applyEffect, getAvailableEffects } from './effects.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 imageUrl = await applyEffect(effectType, text, {
|
|
font: fontFamily,
|
|
fontSize: fontSizeNum,
|
|
vertical: verticalText,
|
|
verticalSpacing: verticalSpacing
|
|
});
|
|
|
|
return imageUrl;
|
|
} 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);
|
|
}
|
|
|
|
|
|
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();
|
|
});
|
|
|
|
|
|
async function renderAllPresets() {
|
|
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.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.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);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
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();
|
|
});
|
|
|
|
|
|
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;
|
|
|
|
});
|
|
|