|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Framemoji - Create Emoji Frames Around Text</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
<style> |
|
.emoji-grid { |
|
display: grid; |
|
grid-template-columns: repeat(auto-fill, minmax(40px, 1fr)); |
|
gap: 8px; |
|
} |
|
|
|
#preview { |
|
font-family: 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', sans-serif; |
|
line-height: 1.2; |
|
white-space: pre; |
|
overflow-x: auto; |
|
} |
|
|
|
@media (max-width: 640px) { |
|
.emoji-grid { |
|
grid-template-columns: repeat(auto-fill, minmax(32px, 1fr)); |
|
} |
|
} |
|
|
|
|
|
#exportCanvas { |
|
display: none; |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-gray-100 min-h-screen"> |
|
<div class="container mx-auto px-4 py-8"> |
|
<header class="text-center mb-8"> |
|
<h1 class="text-4xl font-bold text-indigo-700 mb-2">Framemoji</h1> |
|
<p class="text-gray-600">Create beautiful emoji frames around your text</p> |
|
</header> |
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> |
|
|
|
<div class="bg-white rounded-xl shadow-md p-6"> |
|
<h2 class="text-2xl font-semibold mb-4 text-gray-800">Customize Your Frame</h2> |
|
|
|
|
|
<div class="mb-6"> |
|
<label for="textInput" class="block text-sm font-medium text-gray-700 mb-2">Center Text (Supports multiple lines)</label> |
|
<textarea id="textInput" rows="4" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="Enter your text here..."></textarea> |
|
</div> |
|
|
|
|
|
<div class="mb-6"> |
|
<label for="frameWidth" class="block text-sm font-medium text-gray-700 mb-2">Frame Width (in emojis)</label> |
|
<input type="range" id="frameWidth" min="1" max="10" value="2" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> |
|
<div class="flex justify-between text-xs text-gray-500 mt-1"> |
|
<span>1</span> |
|
<span>2</span> |
|
<span>3</span> |
|
<span>4</span> |
|
<span>5</span> |
|
<span>6</span> |
|
<span>7</span> |
|
<span>8</span> |
|
<span>9</span> |
|
<span>10</span> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="mb-6"> |
|
<label class="block text-sm font-medium text-gray-700 mb-2">Select Frame Emoji</label> |
|
<div class="emoji-grid mb-4"> |
|
|
|
<button class="emoji-btn text-2xl p-2 rounded hover:bg-gray-100" data-emoji="⭐">⭐</button> |
|
<button class="emoji-btn text-2xl p-2 rounded hover:bg-gray-100" data-emoji="✨">✨</button> |
|
<button class="emoji-btn text-2xl p-2 rounded hover:bg-gray-100" data-emoji="❤️">❤️</button> |
|
<button class="emoji-btn text-2xl p-2 rounded hover:bg-gray-100" data-emoji="🔥">🔥</button> |
|
<button class="emoji-btn text-2xl p-2 rounded hover:bg-gray-100" data-emoji="🌸">🌸</button> |
|
<button class="emoji-btn text-2xl p-2 rounded hover:bg-gray-100" data-emoji="🍕">🍕</button> |
|
<button class="emoji-btn text-2xl p-2 rounded hover:bg-gray-100" data-emoji="🎉">🎉</button> |
|
<button class="emoji-btn text-2xl p-2 rounded hover:bg-gray-100" data-emoji="🌈">🌈</button> |
|
<button class="emoji-btn text-2xl p-2 rounded hover:bg-gray-100" data-emoji="🦄">🦄</button> |
|
<button class="emoji-btn text-2xl p-2 rounded hover:bg-gray-100" data-emoji="🍄">🍄</button> |
|
<button class="emoji-btn text-2xl p-2 rounded hover:bg-gray-100" data-emoji="🪐">🪐</button> |
|
<button class="emoji-btn text-2xl p-2 rounded hover:bg-gray-100" data-emoji="💎">💎</button> |
|
<button class="emoji-btn text-2xl p-2 rounded hover:bg-gray-100" data-emoji="🍀">🍀</button> |
|
<button class="emoji-btn text-2xl p-2 rounded hover:bg-gray-100" data-emoji="🦋">🦋</button> |
|
<button class="emoji-btn text-2xl p-2 rounded hover:bg-gray-100" data-emoji="🧿">🧿</button> |
|
<button class="emoji-btn text-2xl p-2 rounded hover:bg-gray-100" data-emoji="🕶️">🕶️</button> |
|
</div> |
|
|
|
|
|
<div class="flex items-center mb-2"> |
|
<input type="checkbox" id="arrowMode" class="mr-2 h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"> |
|
<label for="arrowMode" class="text-sm font-medium text-gray-700">Use arrow frame (pointing inward)</label> |
|
</div> |
|
|
|
|
|
<div class="flex items-center mb-4"> |
|
<input type="checkbox" id="blankBorders" class="mr-2 h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"> |
|
<label for="blankBorders" class="text-sm font-medium text-gray-700">Add blank line borders</label> |
|
</div> |
|
|
|
|
|
<div> |
|
<label for="customEmoji" class="block text-sm font-medium text-gray-700 mb-2">Or enter custom emoji:</label> |
|
<div class="flex"> |
|
<input type="text" id="customEmoji" maxlength="5" class="flex-1 px-3 py-2 border border-gray-300 rounded-l-md focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="Paste any emoji"> |
|
<button id="useCustomEmoji" class="px-4 py-2 bg-indigo-600 text-white rounded-r-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500">Use</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="flex flex-wrap gap-3"> |
|
<button id="copyText" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 flex items-center"> |
|
<i class="fas fa-copy mr-2"></i> Copy as Text |
|
</button> |
|
<button id="exportPNG" class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 flex items-center"> |
|
<i class="fas fa-image mr-2"></i> Export as PNG |
|
</button> |
|
<button id="clearAll" class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 flex items-center"> |
|
<i class="fas fa-trash-alt mr-2"></i> Clear |
|
</button> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-xl shadow-md p-6"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<h2 class="text-2xl font-semibold text-gray-800">Preview</h2> |
|
<div class="text-sm text-gray-500" id="charCount">0 characters</div> |
|
</div> |
|
|
|
<div class="bg-gray-50 p-4 rounded-lg border border-gray-200 min-h-64 flex items-center justify-center"> |
|
<div id="preview" class="text-center"></div> |
|
</div> |
|
|
|
<div class="mt-4 text-sm text-gray-500"> |
|
<p>Tip: The frame will automatically adjust based on your text length.</p> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<canvas id="exportCanvas"></canvas> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
const textInput = document.getElementById('textInput'); |
|
const frameWidth = document.getElementById('frameWidth'); |
|
const preview = document.getElementById('preview'); |
|
const emojiBtns = document.querySelectorAll('.emoji-btn'); |
|
const customEmojiInput = document.getElementById('customEmoji'); |
|
const useCustomEmojiBtn = document.getElementById('useCustomEmoji'); |
|
const copyTextBtn = document.getElementById('copyText'); |
|
const exportPNGBtn = document.getElementById('exportPNG'); |
|
const clearAllBtn = document.getElementById('clearAll'); |
|
const arrowMode = document.getElementById('arrowMode'); |
|
const charCount = document.getElementById('charCount'); |
|
const exportCanvas = document.getElementById('exportCanvas'); |
|
const ctx = exportCanvas.getContext('2d'); |
|
|
|
|
|
let selectedEmoji = '⭐'; |
|
let isArrowMode = false; |
|
let hasBlankBorders = false; |
|
|
|
|
|
const cornerEmojis = { |
|
topLeft: '↘️', |
|
topRight: '↙️', |
|
bottomLeft: '↗️', |
|
bottomRight: '↖️', |
|
top: '⬇️', |
|
right: '⬅️', |
|
bottom: '⬆️', |
|
left: '➡️' |
|
}; |
|
|
|
|
|
updatePreview(); |
|
|
|
|
|
textInput.addEventListener('input', updatePreview); |
|
frameWidth.addEventListener('input', updatePreview); |
|
arrowMode.addEventListener('change', function() { |
|
isArrowMode = this.checked; |
|
updatePreview(); |
|
}); |
|
|
|
document.getElementById('blankBorders').addEventListener('change', function() { |
|
hasBlankBorders = this.checked; |
|
updatePreview(); |
|
}); |
|
|
|
emojiBtns.forEach(btn => { |
|
btn.addEventListener('click', function() { |
|
selectedEmoji = this.getAttribute('data-emoji'); |
|
arrowMode.checked = false; |
|
isArrowMode = false; |
|
updatePreview(); |
|
|
|
emojiBtns.forEach(b => b.classList.remove('bg-indigo-100', 'border-indigo-300')); |
|
this.classList.add('bg-indigo-100', 'border', 'border-indigo-300'); |
|
}); |
|
}); |
|
|
|
useCustomEmojiBtn.addEventListener('click', function() { |
|
if (customEmojiInput.value.trim()) { |
|
selectedEmoji = customEmojiInput.value.trim(); |
|
arrowMode.checked = false; |
|
isArrowMode = false; |
|
updatePreview(); |
|
|
|
emojiBtns.forEach(b => b.classList.remove('bg-indigo-100', 'border-indigo-300')); |
|
} |
|
}); |
|
|
|
copyTextBtn.addEventListener('click', function() { |
|
const textToCopy = preview.textContent; |
|
navigator.clipboard.writeText(textToCopy).then(() => { |
|
|
|
const originalText = this.innerHTML; |
|
this.innerHTML = '<i class="fas fa-check mr-2"></i> Copied!'; |
|
this.classList.remove('bg-indigo-600'); |
|
this.classList.add('bg-green-600'); |
|
setTimeout(() => { |
|
this.innerHTML = originalText; |
|
this.classList.remove('bg-green-600'); |
|
this.classList.add('bg-indigo-600'); |
|
}, 2000); |
|
}); |
|
}); |
|
|
|
exportPNGBtn.addEventListener('click', exportAsPNG); |
|
clearAllBtn.addEventListener('click', clearAll); |
|
|
|
|
|
function updatePreview() { |
|
const text = textInput.value; |
|
const width = parseInt(frameWidth.value); |
|
|
|
|
|
const lines = text.split('\n'); |
|
if (lines.length === 1 && lines[0] === '') { |
|
preview.textContent = 'Your framed text will appear here'; |
|
charCount.textContent = '0 characters'; |
|
return; |
|
} |
|
|
|
|
|
let maxLineLength = 0; |
|
lines.forEach(line => { |
|
if (line.length > maxLineLength) maxLineLength = line.length; |
|
}); |
|
|
|
|
|
const totalWidth = maxLineLength + 2 * width; |
|
|
|
|
|
let framedText = ''; |
|
|
|
|
|
if (hasBlankBorders) { |
|
framedText += ' '.repeat(totalWidth) + '\n'; |
|
} |
|
for (let i = 0; i < width; i++) { |
|
framedText += buildFrameLine(totalWidth, i, 'top') + '\n'; |
|
} |
|
|
|
|
|
lines.forEach(line => { |
|
|
|
for (let i = 0; i < width; i++) { |
|
framedText += getFrameEmoji(i, 'left'); |
|
} |
|
|
|
|
|
const paddingLeft = Math.floor((maxLineLength - line.length) / 2); |
|
const paddingRight = maxLineLength - line.length - paddingLeft; |
|
framedText += ' '.repeat(paddingLeft) + line + ' '.repeat(paddingRight); |
|
|
|
|
|
for (let i = 0; i < width; i++) { |
|
framedText += getFrameEmoji(width - i - 1, 'right'); |
|
} |
|
|
|
framedText += '\n'; |
|
}); |
|
|
|
|
|
for (let i = 0; i < width; i++) { |
|
framedText += buildFrameLine(totalWidth, width - i - 1, 'bottom') + '\n'; |
|
} |
|
if (hasBlankBorders) { |
|
framedText += ' '.repeat(totalWidth) + '\n'; |
|
} |
|
|
|
preview.textContent = framedText; |
|
charCount.textContent = `${framedText.length} characters`; |
|
} |
|
|
|
function buildFrameLine(width, position, side) { |
|
let line = ''; |
|
const textWidth = width; |
|
|
|
if (isArrowMode) { |
|
|
|
if (position === 0) { |
|
line += side === 'top' ? cornerEmojis.topLeft : cornerEmojis.bottomLeft; |
|
for (let i = 1; i < textWidth - 1; i++) { |
|
line += side === 'top' ? cornerEmojis.top : cornerEmojis.bottom; |
|
} |
|
line += side === 'top' ? cornerEmojis.topRight : cornerEmojis.bottomRight; |
|
} else { |
|
for (let i = 0; i < textWidth; i++) { |
|
line += selectedEmoji; |
|
} |
|
} |
|
} else { |
|
for (let i = 0; i < textWidth; i++) { |
|
line += selectedEmoji; |
|
} |
|
} |
|
return line; |
|
} |
|
|
|
function getFrameEmoji(position, side) { |
|
if (isArrowMode) { |
|
if (side === 'left') return cornerEmojis.left; |
|
if (side === 'right') return cornerEmojis.right; |
|
} |
|
return selectedEmoji; |
|
} |
|
|
|
function exportAsPNG() { |
|
const text = preview.textContent; |
|
if (!text || text === 'Your framed text will appear here') { |
|
alert('Please create some content first!'); |
|
return; |
|
} |
|
|
|
|
|
const lines = text.split('\n'); |
|
const lineHeight = 30; |
|
const fontSize = 20; |
|
const padding = 20; |
|
|
|
|
|
let maxLineLength = 0; |
|
lines.forEach(line => { |
|
if (line.length > maxLineLength) maxLineLength = line.length; |
|
}); |
|
|
|
|
|
exportCanvas.width = maxLineLength * fontSize * 0.6 + padding * 2; |
|
exportCanvas.height = lines.length * lineHeight + padding * 2; |
|
|
|
|
|
ctx.clearRect(0, 0, exportCanvas.width, exportCanvas.height); |
|
|
|
|
|
ctx.fillStyle = '#ffffff'; |
|
ctx.fillRect(0, 0, exportCanvas.width, exportCanvas.height); |
|
|
|
|
|
ctx.font = `${fontSize}px 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', sans-serif`; |
|
ctx.fillStyle = '#000000'; |
|
ctx.textAlign = 'left'; |
|
ctx.textBaseline = 'top'; |
|
|
|
|
|
lines.forEach((line, index) => { |
|
ctx.fillText(line, padding, padding + index * lineHeight); |
|
}); |
|
|
|
|
|
const link = document.createElement('a'); |
|
link.download = 'framemoji-export.png'; |
|
link.href = exportCanvas.toDataURL('image/png'); |
|
link.click(); |
|
} |
|
|
|
function clearAll() { |
|
textInput.value = ''; |
|
frameWidth.value = 2; |
|
selectedEmoji = '⭐'; |
|
customEmojiInput.value = ''; |
|
arrowMode.checked = false; |
|
isArrowMode = false; |
|
emojiBtns.forEach(b => b.classList.remove('bg-indigo-100', 'border-indigo-300')); |
|
updatePreview(); |
|
} |
|
}); |
|
</script> |
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=guidouil/framojis" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
</html> |