|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Text Flow Drawing Canvas</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<style> |
|
body { |
|
overflow: hidden; |
|
touch-action: none; |
|
font-family: 'Georgia', serif; |
|
} |
|
canvas { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
z-index: 1; |
|
} |
|
.controls { |
|
position: absolute; |
|
bottom: 20px; |
|
left: 50%; |
|
transform: translateX(-50%); |
|
z-index: 10; |
|
background: rgba(255, 255, 255, 0.9); |
|
padding: 15px; |
|
border-radius: 15px; |
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); |
|
backdrop-filter: blur(5px); |
|
max-width: 90%; |
|
} |
|
.text-palette { |
|
display: flex; |
|
flex-wrap: wrap; |
|
gap: 8px; |
|
margin-bottom: 15px; |
|
justify-content: center; |
|
} |
|
.text-option { |
|
cursor: pointer; |
|
padding: 8px 12px; |
|
border-radius: 8px; |
|
background: white; |
|
border: 1px solid #e2e8f0; |
|
transition: all 0.2s; |
|
font-size: 14px; |
|
white-space: nowrap; |
|
} |
|
.text-option:hover, .text-option.active { |
|
background: #3b82f6; |
|
color: white; |
|
transform: translateY(-2px); |
|
} |
|
.color-picker { |
|
display: flex; |
|
gap: 10px; |
|
margin-bottom: 15px; |
|
justify-content: center; |
|
} |
|
.color-option { |
|
width: 28px; |
|
height: 28px; |
|
border-radius: 50%; |
|
cursor: pointer; |
|
border: 2px solid white; |
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1); |
|
transition: transform 0.2s; |
|
} |
|
.color-option:hover, .color-option.active { |
|
transform: scale(1.2); |
|
} |
|
.controls-group { |
|
display: flex; |
|
gap: 15px; |
|
justify-content: center; |
|
margin-bottom: 15px; |
|
flex-wrap: wrap; |
|
} |
|
.control-item { |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
} |
|
.control-label { |
|
font-size: 12px; |
|
margin-bottom: 5px; |
|
color: #4b5563; |
|
font-weight: 500; |
|
} |
|
.title { |
|
position: absolute; |
|
top: 20px; |
|
left: 50%; |
|
transform: translateX(-50%); |
|
z-index: 10; |
|
background: rgba(255, 255, 255, 0.9); |
|
padding: 12px 25px; |
|
border-radius: 30px; |
|
font-family: 'Playfair Display', serif; |
|
font-weight: 600; |
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); |
|
color: #1e40af; |
|
} |
|
.custom-text { |
|
width: 100%; |
|
padding: 8px 12px; |
|
border-radius: 8px; |
|
border: 1px solid #e2e8f0; |
|
margin-bottom: 10px; |
|
font-size: 14px; |
|
} |
|
.btn { |
|
padding: 8px 16px; |
|
border-radius: 8px; |
|
font-weight: 500; |
|
transition: all 0.2s; |
|
border: none; |
|
cursor: pointer; |
|
} |
|
.btn-primary { |
|
background: #3b82f6; |
|
color: white; |
|
} |
|
.btn-primary:hover { |
|
background: #2563eb; |
|
transform: translateY(-1px); |
|
} |
|
.btn-danger { |
|
background: #ef4444; |
|
color: white; |
|
} |
|
.btn-danger:hover { |
|
background: #dc2626; |
|
transform: translateY(-1px); |
|
} |
|
.btn-group { |
|
display: flex; |
|
gap: 10px; |
|
justify-content: center; |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-gray-50"> |
|
<div class="title">Text Flow Drawing Canvas</div> |
|
|
|
<canvas id="drawingCanvas"></canvas> |
|
|
|
<div class="controls"> |
|
<input type="text" id="customText" class="custom-text" placeholder="Type your own text here..." value="Love is patient, love is kind"> |
|
|
|
<div class="text-palette"> |
|
<div class="text-option active">Love is patient</div> |
|
<div class="text-option">Be the change</div> |
|
<div class="text-option">Dream big</div> |
|
<div class="text-option">Stay curious</div> |
|
<div class="text-option">Create magic</div> |
|
<div class="text-option">Find joy</div> |
|
<div class="text-option">Never give up</div> |
|
<div class="text-option">You matter</div> |
|
</div> |
|
|
|
<div class="color-picker"> |
|
<div class="color-option active" style="background-color: #3b82f6;" data-color="#3b82f6"></div> |
|
<div class="color-option" style="background-color: #ef4444;" data-color="#ef4444"></div> |
|
<div class="color-option" style="background-color: #10b981;" data-color="#10b981"></div> |
|
<div class="color-option" style="background-color: #f59e0b;" data-color="#f59e0b"></div> |
|
<div class="color-option" style="background-color: #8b5cf6;" data-color="#8b5cf6"></div> |
|
<div class="color-option" style="background-color: #000000;" data-color="#000000"></div> |
|
</div> |
|
|
|
<div class="controls-group"> |
|
<div class="control-item"> |
|
<span class="control-label">Font Size</span> |
|
<input type="range" id="sizeSlider" min="12" max="36" value="18" class="w-24"> |
|
<span id="sizeValue" class="text-xs mt-1">18px</span> |
|
</div> |
|
<div class="control-item"> |
|
<span class="control-label">Spacing</span> |
|
<input type="range" id="spacingSlider" min="0.5" max="2" step="0.1" value="1" class="w-24"> |
|
<span id="spacingValue" class="text-xs mt-1">1.0</span> |
|
</div> |
|
<div class="control-item"> |
|
<span class="control-label">Opacity</span> |
|
<input type="range" id="opacitySlider" min="0.2" max="1" step="0.1" value="0.8" class="w-24"> |
|
<span id="opacityValue" class="text-xs mt-1">80%</span> |
|
</div> |
|
</div> |
|
|
|
<div class="btn-group"> |
|
<button id="clearBtn" class="btn btn-danger">Clear Canvas</button> |
|
<button id="saveBtn" class="btn btn-primary">Save as Image</button> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', () => { |
|
const canvas = document.getElementById('drawingCanvas'); |
|
const ctx = canvas.getContext('2d'); |
|
let isDrawing = false; |
|
let currentText = "Love is patient, love is kind"; |
|
let currentColor = '#3b82f6'; |
|
let currentSize = 18; |
|
let currentSpacing = 1; |
|
let currentOpacity = 0.8; |
|
let lastX = 0; |
|
let lastY = 0; |
|
let textPosition = 0; |
|
|
|
|
|
function resizeCanvas() { |
|
canvas.width = window.innerWidth; |
|
canvas.height = window.innerHeight; |
|
|
|
ctx.fillStyle = '#f8fafc'; |
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
} |
|
|
|
resizeCanvas(); |
|
window.addEventListener('resize', resizeCanvas); |
|
|
|
|
|
function startDrawing(e) { |
|
isDrawing = true; |
|
const pos = getPosition(e); |
|
lastX = pos.x; |
|
lastY = pos.y; |
|
textPosition = 0; |
|
draw(e); |
|
} |
|
|
|
function stopDrawing() { |
|
isDrawing = false; |
|
} |
|
|
|
function getPosition(e) { |
|
let x, y; |
|
if (e.type.includes('touch')) { |
|
x = e.touches[0].clientX; |
|
y = e.touches[0].clientY; |
|
} else { |
|
x = e.clientX; |
|
y = e.clientY; |
|
} |
|
return { x, y }; |
|
} |
|
|
|
function draw(e) { |
|
if (!isDrawing) return; |
|
|
|
const pos = getPosition(e); |
|
const x = pos.x; |
|
const y = pos.y; |
|
|
|
|
|
const distance = Math.sqrt(Math.pow(x - lastX, 2) + Math.pow(y - lastY, 2)); |
|
|
|
if (distance > currentSize / 3) { |
|
|
|
const angle = Math.atan2(y - lastY, x - lastX); |
|
|
|
ctx.save(); |
|
ctx.translate(x, y); |
|
ctx.rotate(angle); |
|
|
|
|
|
ctx.font = `${currentSize}px Georgia, serif`; |
|
ctx.fillStyle = currentColor; |
|
ctx.globalAlpha = currentOpacity; |
|
|
|
|
|
const char = currentText[textPosition % currentText.length]; |
|
ctx.fillText(char, 0, 0); |
|
|
|
|
|
textPosition++; |
|
lastX = x; |
|
lastY = y; |
|
|
|
|
|
ctx.translate(currentSize * currentSpacing, 0); |
|
ctx.restore(); |
|
} |
|
} |
|
|
|
|
|
canvas.addEventListener('mousedown', startDrawing); |
|
canvas.addEventListener('mousemove', draw); |
|
canvas.addEventListener('mouseup', stopDrawing); |
|
canvas.addEventListener('mouseout', stopDrawing); |
|
|
|
|
|
canvas.addEventListener('touchstart', (e) => { |
|
e.preventDefault(); |
|
startDrawing(e); |
|
}); |
|
canvas.addEventListener('touchmove', (e) => { |
|
e.preventDefault(); |
|
draw(e); |
|
}); |
|
canvas.addEventListener('touchend', stopDrawing); |
|
|
|
|
|
document.querySelectorAll('.text-option').forEach(option => { |
|
option.addEventListener('click', () => { |
|
document.querySelectorAll('.text-option').forEach(opt => opt.classList.remove('active')); |
|
option.classList.add('active'); |
|
currentText = option.textContent; |
|
}); |
|
}); |
|
|
|
|
|
document.getElementById('customText').addEventListener('input', (e) => { |
|
currentText = e.target.value; |
|
|
|
document.querySelectorAll('.text-option').forEach(opt => opt.classList.remove('active')); |
|
}); |
|
|
|
|
|
document.querySelectorAll('.color-option').forEach(option => { |
|
option.addEventListener('click', () => { |
|
document.querySelectorAll('.color-option').forEach(opt => opt.classList.remove('active')); |
|
option.classList.add('active'); |
|
currentColor = option.getAttribute('data-color'); |
|
}); |
|
}); |
|
|
|
|
|
const sizeSlider = document.getElementById('sizeSlider'); |
|
const sizeValue = document.getElementById('sizeValue'); |
|
|
|
sizeSlider.addEventListener('input', () => { |
|
currentSize = parseInt(sizeSlider.value); |
|
sizeValue.textContent = `${currentSize}px`; |
|
}); |
|
|
|
|
|
const spacingSlider = document.getElementById('spacingSlider'); |
|
const spacingValue = document.getElementById('spacingValue'); |
|
|
|
spacingSlider.addEventListener('input', () => { |
|
currentSpacing = parseFloat(spacingSlider.value); |
|
spacingValue.textContent = currentSpacing.toFixed(1); |
|
}); |
|
|
|
|
|
const opacitySlider = document.getElementById('opacitySlider'); |
|
const opacityValue = document.getElementById('opacityValue'); |
|
|
|
opacitySlider.addEventListener('input', () => { |
|
currentOpacity = parseFloat(opacitySlider.value); |
|
opacityValue.textContent = `${Math.round(currentOpacity * 100)}%`; |
|
}); |
|
|
|
|
|
document.getElementById('clearBtn').addEventListener('click', () => { |
|
if (confirm('Are you sure you want to clear the canvas?')) { |
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
|
|
ctx.fillStyle = '#f8fafc'; |
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
} |
|
}); |
|
|
|
|
|
document.getElementById('saveBtn').addEventListener('click', () => { |
|
const link = document.createElement('a'); |
|
link.download = 'text-drawing.png'; |
|
link.href = canvas.toDataURL('image/png'); |
|
link.click(); |
|
}); |
|
|
|
|
|
document.addEventListener('touchmove', (e) => { |
|
if (isDrawing) { |
|
e.preventDefault(); |
|
} |
|
}, { passive: false }); |
|
}); |
|
</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=victor/text-flow" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> |
|
</html> |