| <!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> |