Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Handwriting Canvas</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> | |
@import url('https://fonts.googleapis.com/css2?family=Caveat:wght@400;700&display=swap'); | |
.handwriting { | |
font-family: 'Caveat', cursive; | |
} | |
canvas { | |
touch-action: none; | |
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
} | |
.tool-btn { | |
transition: all 0.2s ease; | |
} | |
.tool-btn.active { | |
transform: scale(1.1); | |
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5); | |
} | |
#drawingCanvas { | |
background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImdyaWQiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTSAwIDEwIEwgNDAgMTAgTSAxMCAwIEwgMTAgNDAgTSAwIDIwIEwgNDAgMjAgTSAyMCAwIEwgMjAgNDAgTSAwIDMwIEwgNDAgMzAgTSAzMCAwIEwgMzAgNDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2QwZDBkMCIgb3BhY2l0eT0iMC4yIiBzdHJva2Utd2lkdGg9IjEiLz48L3BhdHRlcm4+PC9kZWZzPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JpZCkiLz48L3N2Zz4='); | |
} | |
.ink-effect { | |
position: relative; | |
} | |
.ink-effect::after { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
background: radial-gradient(circle, transparent 0%, rgba(0,0,0,0.05) 100%); | |
pointer-events: none; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-100 min-h-screen"> | |
<div class="container mx-auto px-4 py-8"> | |
<header class="mb-8 text-center"> | |
<h1 class="text-4xl font-bold text-indigo-700 handwriting mb-2">Handwriting Canvas</h1> | |
<p class="text-gray-600">Draw or write naturally on images or blank paper</p> | |
</header> | |
<div class="flex flex-col lg:flex-row gap-6"> | |
<!-- Tools Panel --> | |
<div class="bg-white rounded-lg shadow-md p-4 lg:w-1/4"> | |
<h2 class="text-xl font-semibold mb-4 text-gray-800 handwriting">Tools</h2> | |
<div class="space-y-4"> | |
<!-- Color Picker --> | |
<div> | |
<label class="block text-sm font-medium text-gray-700 mb-1 handwriting">Pen Color</label> | |
<div class="flex flex-wrap gap-2"> | |
<button data-color="#000000" class="tool-btn w-8 h-8 rounded-full bg-black border-2 border-gray-300"></button> | |
<button data-color="#e53e3e" class="tool-btn w-8 h-8 rounded-full bg-red-600 border-2 border-gray-300"></button> | |
<button data-color="#3182ce" class="tool-btn w-8 h-8 rounded-full bg-blue-600 border-2 border-gray-300"></button> | |
<button data-color="#38a169" class="tool-btn w-8 h-8 rounded-full bg-green-600 border-2 border-gray-300"></button> | |
<button data-color="#805ad5" class="tool-btn w-8 h-8 rounded-full bg-purple-600 border-2 border-gray-300"></button> | |
</div> | |
</div> | |
<!-- Brush Size --> | |
<div> | |
<label class="block text-sm font-medium text-gray-700 mb-1 handwriting">Brush Size</label> | |
<input type="range" id="brushSize" min="1" max="20" value="3" 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>Thin</span> | |
<span>Thick</span> | |
</div> | |
</div> | |
<!-- Background Options --> | |
<div> | |
<label class="block text-sm font-medium text-gray-700 mb-1 handwriting">Background</label> | |
<div class="flex flex-wrap gap-2"> | |
<button data-bg="blank" class="tool-btn px-3 py-1 bg-white border border-gray-300 rounded-md handwriting">White Paper</button> | |
<button data-bg="grid" class="tool-btn px-3 py-1 bg-white border border-gray-300 rounded-md handwriting">Grid Paper</button> | |
<button id="uploadImageBtn" class="tool-btn px-3 py-1 bg-indigo-100 text-indigo-700 border border-indigo-200 rounded-md handwriting"> | |
<i class="fas fa-image mr-1"></i> Upload Image | |
</button> | |
<input type="file" id="imageUpload" accept="image/*" class="hidden"> | |
</div> | |
</div> | |
<!-- Actions --> | |
<div class="pt-4 border-t border-gray-200"> | |
<button id="clearCanvas" class="w-full bg-red-100 text-red-700 py-2 rounded-md hover:bg-red-200 transition handwriting"> | |
<i class="fas fa-trash-alt mr-2"></i> Clear Canvas | |
</button> | |
<button id="downloadCanvas" class="w-full mt-2 bg-indigo-100 text-indigo-700 py-2 rounded-md hover:bg-indigo-200 transition handwriting"> | |
<i class="fas fa-download mr-2"></i> Download | |
</button> | |
</div> | |
<!-- Handwriting Toggle --> | |
<div class="pt-4 border-t border-gray-200"> | |
<label class="inline-flex items-center cursor-pointer"> | |
<input type="checkbox" id="handwritingEffect" class="sr-only peer" checked> | |
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-indigo-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div> | |
<span class="ml-3 text-sm font-medium text-gray-700 handwriting">Handwriting Effect</span> | |
</label> | |
</div> | |
</div> | |
</div> | |
<!-- Canvas Area --> | |
<div class="flex-1"> | |
<div class="bg-white rounded-lg shadow-md p-4"> | |
<div class="relative overflow-hidden rounded-md ink-effect"> | |
<canvas id="drawingCanvas" class="bg-white w-full h-96 lg:h-[500px] border border-gray-200"></canvas> | |
</div> | |
<div class="mt-4 flex justify-center space-x-4"> | |
<button id="undoBtn" class="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition handwriting"> | |
<i class="fas fa-undo mr-2"></i> Undo | |
</button> | |
<button id="redoBtn" class="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition handwriting"> | |
<i class="fas fa-redo mr-2"></i> Redo | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
const canvas = document.getElementById('drawingCanvas'); | |
const ctx = canvas.getContext('2d'); | |
const colorButtons = document.querySelectorAll('[data-color]'); | |
const bgButtons = document.querySelectorAll('[data-bg]'); | |
const brushSizeInput = document.getElementById('brushSize'); | |
const clearCanvasBtn = document.getElementById('clearCanvas'); | |
const downloadCanvasBtn = document.getElementById('downloadCanvas'); | |
const uploadImageBtn = document.getElementById('uploadImageBtn'); | |
const imageUpload = document.getElementById('imageUpload'); | |
const handwritingEffect = document.getElementById('handwritingEffect'); | |
const undoBtn = document.getElementById('undoBtn'); | |
const redoBtn = document.getElementById('redoBtn'); | |
let isDrawing = false; | |
let currentColor = '#000000'; | |
let currentBrushSize = 3; | |
let handwritingEnabled = true; | |
let canvasHistory = []; | |
let historyIndex = -1; | |
// Set canvas size | |
function resizeCanvas() { | |
const container = canvas.parentElement; | |
canvas.width = container.clientWidth; | |
canvas.height = container.clientHeight; | |
redrawCanvas(); | |
} | |
// Initialize canvas | |
function initCanvas() { | |
resizeCanvas(); | |
setBackground('blank'); | |
// Add event listeners for window resize | |
window.addEventListener('resize', () => { | |
resizeCanvas(); | |
}); | |
// Save initial state | |
saveCanvasState(); | |
} | |
// Set background | |
function setBackground(type) { | |
if (type === 'blank') { | |
canvas.style.backgroundImage = 'none'; | |
canvas.style.backgroundColor = '#ffffff'; | |
} else if (type === 'grid') { | |
canvas.style.backgroundImage = 'url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImdyaWQiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTSAwIDEwIEwgNDAgMTAgTSAxMCAwIEwgMTAgNDAgTSAwIDIwIEwgNDAgMjAgTSAyMCAwIEwgMjAgNDAgTSAwIDMwIEwgNDAgMzAgTSAzMCAwIEwgMzAgNDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2QwZDBkMCIgb3BhY2l0eT0iMC4yIiBzdHJva2Utd2lkdGg9IjEiLz48L3BhdHRlcm4+PC9kZWZzPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JpZCkiLz48L3N2Zz4=")'; | |
} | |
} | |
// Save canvas state to history | |
function saveCanvasState() { | |
// Remove any states after current index (for redo) | |
if (historyIndex < canvasHistory.length - 1) { | |
canvasHistory = canvasHistory.slice(0, historyIndex + 1); | |
} | |
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
canvasHistory.push(imageData); | |
historyIndex = canvasHistory.length - 1; | |
updateUndoRedoButtons(); | |
} | |
// Update undo/redo button states | |
function updateUndoRedoButtons() { | |
undoBtn.disabled = historyIndex <= 0; | |
redoBtn.disabled = historyIndex >= canvasHistory.length - 1; | |
} | |
// Undo action | |
function undo() { | |
if (historyIndex > 0) { | |
historyIndex--; | |
redrawCanvas(); | |
} | |
} | |
// Redo action | |
function redo() { | |
if (historyIndex < canvasHistory.length - 1) { | |
historyIndex++; | |
redrawCanvas(); | |
} | |
} | |
// Redraw canvas from history | |
function redrawCanvas() { | |
if (canvasHistory.length > 0) { | |
ctx.putImageData(canvasHistory[historyIndex], 0, 0); | |
} | |
} | |
// Start drawing | |
function startDrawing(e) { | |
isDrawing = true; | |
draw(e); | |
} | |
// Stop drawing | |
function stopDrawing() { | |
isDrawing = false; | |
ctx.beginPath(); | |
saveCanvasState(); | |
} | |
// Draw on canvas | |
function draw(e) { | |
if (!isDrawing) return; | |
ctx.lineCap = 'round'; | |
ctx.lineJoin = 'round'; | |
ctx.strokeStyle = currentColor; | |
ctx.lineWidth = currentBrushSize; | |
// Get position (handling both mouse and touch events) | |
let x, y; | |
if (e.type.includes('touch')) { | |
const rect = canvas.getBoundingClientRect(); | |
x = e.touches[0].clientX - rect.left; | |
y = e.touches[0].clientY - rect.top; | |
} else { | |
x = e.offsetX; | |
y = e.offsetY; | |
} | |
// Apply handwriting effect by adding slight randomness | |
if (handwritingEnabled) { | |
ctx.lineTo( | |
x + (Math.random() - 0.5) * currentBrushSize * 0.5, | |
y + (Math.random() - 0.5) * currentBrushSize * 0.5 | |
); | |
} else { | |
ctx.lineTo(x, y); | |
} | |
ctx.stroke(); | |
ctx.beginPath(); | |
ctx.moveTo(x, y); | |
} | |
// Clear canvas | |
function clearCanvas() { | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
saveCanvasState(); | |
} | |
// Download canvas as image | |
function downloadCanvas() { | |
const link = document.createElement('a'); | |
link.download = 'handwriting-' + new Date().toISOString().slice(0, 10) + '.png'; | |
link.href = canvas.toDataURL('image/png'); | |
link.click(); | |
} | |
// Handle image upload | |
function handleImageUpload(e) { | |
const file = e.target.files[0]; | |
if (!file) return; | |
const reader = new FileReader(); | |
reader.onload = function(event) { | |
const img = new Image(); | |
img.onload = function() { | |
// Clear canvas and draw the image | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
// Calculate dimensions to maintain aspect ratio | |
const ratio = Math.min( | |
canvas.width / img.width, | |
canvas.height / img.height | |
); | |
const width = img.width * ratio; | |
const height = img.height * ratio; | |
const x = (canvas.width - width) / 2; | |
const y = (canvas.height - height) / 2; | |
ctx.drawImage(img, x, y, width, height); | |
saveCanvasState(); | |
}; | |
img.src = event.target.result; | |
}; | |
reader.readAsDataURL(file); | |
} | |
// Event listeners | |
canvas.addEventListener('mousedown', startDrawing); | |
canvas.addEventListener('mousemove', draw); | |
canvas.addEventListener('mouseup', stopDrawing); | |
canvas.addEventListener('mouseout', stopDrawing); | |
// Touch events for mobile | |
canvas.addEventListener('touchstart', (e) => { | |
e.preventDefault(); | |
startDrawing(e); | |
}); | |
canvas.addEventListener('touchmove', (e) => { | |
e.preventDefault(); | |
draw(e); | |
}); | |
canvas.addEventListener('touchend', (e) => { | |
e.preventDefault(); | |
stopDrawing(); | |
}); | |
// Color buttons | |
colorButtons.forEach(button => { | |
button.addEventListener('click', () => { | |
currentColor = button.dataset.color; | |
// Update active state | |
colorButtons.forEach(btn => btn.classList.remove('active')); | |
button.classList.add('active'); | |
}); | |
}); | |
// Background buttons | |
bgButtons.forEach(button => { | |
button.addEventListener('click', () => { | |
setBackground(button.dataset.bg); | |
// Update active state | |
bgButtons.forEach(btn => btn.classList.remove('active')); | |
button.classList.add('active'); | |
}); | |
}); | |
// Brush size | |
brushSizeInput.addEventListener('input', () => { | |
currentBrushSize = brushSizeInput.value; | |
}); | |
// Clear canvas | |
clearCanvasBtn.addEventListener('click', clearCanvas); | |
// Download | |
downloadCanvasBtn.addEventListener('click', downloadCanvas); | |
// Image upload | |
uploadImageBtn.addEventListener('click', () => { | |
imageUpload.click(); | |
}); | |
imageUpload.addEventListener('change', handleImageUpload); | |
// Handwriting effect toggle | |
handwritingEffect.addEventListener('change', () => { | |
handwritingEnabled = handwritingEffect.checked; | |
}); | |
// Undo/redo buttons | |
undoBtn.addEventListener('click', undo); | |
redoBtn.addEventListener('click', redo); | |
// Initialize | |
initCanvas(); | |
// Set black as default active color | |
document.querySelector('[data-color="#000000"]').classList.add('active'); | |
document.querySelector('[data-bg="blank"]').classList.add('active'); | |
}); | |
</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=Mustafa7assan/hand-written" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |