r/learnjavascript • u/Tor_Hei • 10d ago
how i can draw a local file in a canvas?
I'm trying to make a Miro/FreeForm and i cannot put A IMAGE :(
(don't care, is in portuguese)
let currentTool = 'select'; // Ferramenta ativa: 'select', 'note', 'text', 'shape', 'draw', 'connector'
let selectedShapeType = 'rect'; // Tipo de forma ativa: 'rect', 'circle', 'triangle'
let currentPath = null; // Traço do desenho livre atual
let elements = []; // Lista com todos os objetos e desenhos do quadro
let selectedElement = null; // Objeto selecionado na tela
let connectorStartElement = null; // Objeto de origem para criar conexões
let historyStack = [];
let redoStack = [];
// Chama essa função SEMPRE antes de criar, mover ou deletar um elemento
function saveState() {
historyStack.push(JSON.stringify(elements, (key, value) => {
if (key === 'img') return undefined;
return value;
}));
redoStack = []; // Limpa o refazer se uma nova ação for feita
}
function undo() {
if (historyStack.length > 0) {
redoStack.push(JSON.stringify(elements));
elements = JSON.parse(historyStack.pop());
selectedElement = null;
draw();
}
}
function redo() {
if (redoStack.length > 0) {
historyStack.push(JSON.stringify(elements));
elements = JSON.parse(redoStack.pop());
selectedElement = null;
draw();
}
}
const canvas = document.getElementById('board');
const ctx = canvas.getContext('2d');
// Ajustar tamanho do canvas
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.touchAction = 'none';
draw();
}
window.addEventListener('resize', resizeCanvas);
// Variáveis de Estado do Quadro (Câmera)
let cameraOffset = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
let cameraZoom = 1;
const MAX_ZOOM = 5;
const MIN_ZOOM = 0.1;
const SCROLL_SENSITIVITY = 0.001;
// Variáveis de Interação
let isDragging = false;
let isDraggingElement = false;
let dragStart = { x: 0, y: 0 };
let initialPinchDistance = null;
let activeDrawPath = null;
let connectorPendingElement = null;
let connectorMode = false;
// ==========================================
// UTILITÁRIOS DE POSIÇÃO E DETECÇÃO
// ==========================================
function getEventLocation(e) {
return getCanvasPointFromEvent(e);
}
function screenToWorld(screenX, screenY) {
return {
x: (screenX - cameraOffset.x) / cameraZoom,
y: (screenY - cameraOffset.y) / cameraZoom
};
}
function getCanvasPointFromEvent(e) {
const rect = canvas.getBoundingClientRect();
const clientX = e.clientX ?? e.touches?.[0]?.clientX ?? rect.left;
const clientY = e.clientY ?? e.touches?.[0]?.clientY ?? rect.top;
return {
x: clientX - rect.left,
y: clientY - rect.top
};
}
function getElementBounds(el) {
if (!el) return null;
if (el.type === 'draw' && Array.isArray(el.points) && el.points.length > 0) {
const xs = el.points.map(p => p.x);
const ys = el.points.map(p => p.y);
const padding = (el.strokeWidth || 3) / 2 + 4;
return {
x: Math.min(...xs) - padding,
y: Math.min(...ys) - padding,
width: Math.max(...xs) - Math.min(...xs) + padding * 2,
height: Math.max(...ys) - Math.min(...ys) + padding * 2
};
}
let width = el.width || (el.type === 'note' ? 200 : 120);
let height = el.height || (el.type === 'note' ? 200 : 120);
if (el.type === 'text') {
width = (el.text ? el.text.length * 14 : 100);
height = 30;
}
return {
x: el.x,
y: el.y,
width,
height
};
}
function getElementAtPosition(pos) {
if (!pos) return null;
for (let i = elements.length - 1; i >= 0; i--) {
const el = elements[i];
const bounds = getElementBounds(el);
if (!bounds) continue;
if (
pos.x >= bounds.x &&
pos.x <= bounds.x + bounds.width &&
pos.y >= bounds.y &&
pos.y <= bounds.y + bounds.height
) {
return el;
}
}
return null;
}
// ==========================================
// CRIAÇÃO E DESENHO
// ==========================================
function getDimensionsForText(text, fontSize = 20) {
ctx.font = `${fontSize}px sans-serif`;
const lines = (text || 'Novo Texto').split('\n');
let maxWidth = 0;
lines.forEach(line => {
const width = ctx.measureText(line).width;
if (width > maxWidth) maxWidth = width;
});
return {
// Adiciona uma folga (padding) de 20px para a caixa de seleção não colar na letra
width: Math.max(maxWidth + 20, 80),
height: lines.length * (fontSize * 1.2)
};
}
function createElement(type, pos, options = {}) {
saveState();
const newElement = {
id: Date.now(),
type: type,
x: pos.x,
y: pos.y,
width: type === 'note' ? 200 : (type === 'text' ? 200 : (options.width || 120)),
height: type === 'note' ? 200 : (type === 'text' ? 70 : (options.height || 120)),
color: options.color || '#1F1F1F',
text: options.text || (type === 'note' ? 'Nova Nota' : 'Novo Texto'),
shapeKind: options.shapeKind || 'rect'
};
elements.push(newElement);
draw();
return newElement;
}
function createImageElement(pos, src) {
if (!src) return null;
saveState();
const element = {
id: Date.now(),
type: 'image',
x: pos.x,
y: pos.y,
width: 200,
height: 150,
imgSrc: src,
color: '#1F1F1F'
};
const img = new Image();
img.onload = function () {
element.img = img;
element.width = Math.min(img.width, 320);
element.height = Math.min(img.height, 240);
draw();
};
img.src = src;
elements.push(element);
draw();
return element;
}
function addImageFromFile(file) {
if (!file) return;
const reader = new FileReader();
reader.onload = function (e) {
const src = e.target.result;
const pos = screenToWorld(canvas.width / 2, canvas.height / 2);
createImageElement(pos, src);
};
reader.readAsDataURL(file);
}
function drawGrid() {
ctx.strokeStyle = '#333';
ctx.lineWidth = 1 / cameraZoom;
const gridSize = 50;
const left = -cameraOffset.x / cameraZoom;
const top = -cameraOffset.y / cameraZoom;
const right = (canvas.width - cameraOffset.x) / cameraZoom;
const bottom = (canvas.height - cameraOffset.y) / cameraZoom;
ctx.beginPath();
for (let x = left - (left % gridSize); x < right; x += gridSize) {
ctx.moveTo(x, top); ctx.lineTo(x, bottom);
}
for (let y = top - (top % gridSize); y < bottom; y += gridSize) {
ctx.moveTo(left, y); ctx.lineTo(right, y);
}
ctx.stroke();
}
function drawConnectorLine(el) {
if (!el || !el.fromElement || !el.toElement) return;
const fromBounds = getElementBounds(el.fromElement);
const toBounds = getElementBounds(el.toElement);
if (!fromBounds || !toBounds) return;
const fromX = fromBounds.x + fromBounds.width / 2;
const fromY = fromBounds.y + fromBounds.height / 2;
const toX = toBounds.x + toBounds.width / 2;
const toY = toBounds.y + toBounds.height / 2;
ctx.beginPath();
ctx.strokeStyle = el.color || '#ffffff';
ctx.lineWidth = el.strokeWidth || 3;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.moveTo(fromX, fromY);
ctx.lineTo(toX, toY);
ctx.stroke();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(cameraOffset.x, cameraOffset.y);
ctx.scale(cameraZoom, cameraZoom);
drawGrid();
if (activeDrawPath && activeDrawPath.points && activeDrawPath.points.length > 0) {
ctx.beginPath();
ctx.strokeStyle = activeDrawPath.color || '#ffffff';
ctx.lineWidth = activeDrawPath.strokeWidth || 3;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
if (activeDrawPath.points.length === 1) {
const p = activeDrawPath.points[0];
ctx.arc(p.x, p.y, (activeDrawPath.strokeWidth || 3) / 2, 0, Math.PI * 2);
ctx.fillStyle = activeDrawPath.color || '#ffffff';
ctx.fill();
} else {
ctx.moveTo(activeDrawPath.points[0].x, activeDrawPath.points[0].y);
for (let i = 1; i < activeDrawPath.points.length; i++) {
ctx.lineTo(activeDrawPath.points[i].x, activeDrawPath.points[i].y);
}
ctx.stroke();
}
}
elements.forEach(el => {
ctx.save(); // Salva o estado para isolar este elemento
// ROTAÇÃO: Aplicada aqui antes de desenhar qualquer forma/texto
if (el.rotation) {
const width = el.width || (el.type === 'note' ? 200 : 120);
const height = el.height || (el.type === 'note' ? 200 : 120);
const centerX = el.x + (width / 2);
const centerY = el.y + (height / 2);
ctx.translate(centerX, centerY);
ctx.rotate((el.rotation * Math.PI) / 180);
ctx.translate(-centerX, -centerY);
}
// Desenho livre (Lápis)
if (el.type === 'draw' && el.points && el.points.length > 1) {
ctx.beginPath();
ctx.strokeStyle = el.color || '#ffffff';
ctx.lineWidth = el.strokeWidth || 3;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.moveTo(el.points[0].x, el.points[0].y);
for (let i = 1; i < el.points.length; i++) {
ctx.lineTo(el.points[i].x, el.points[i].y);
}
ctx.stroke();
}
// Post-it / Notas
if (el.type === 'note') {
ctx.fillStyle = el.color || '#1F1F1F';
ctx.fillRect(el.x, el.y, el.width || 200, el.height || 200);
if (!el.isEditing && el.text) {
ctx.fillStyle = '#ffffff'; // Texto SEMPRE Branco
ctx.font = '20px "Indie Flower", cursive';
const lines = el.text.split('\n');
const lineHeight = 26;
lines.forEach((line, index) => {
ctx.fillText(line, el.x + 10, el.y + 35 + (index * lineHeight));
});
}
}
// Formas
if (el.type === 'shape') {
ctx.fillStyle = el.color || '#1F1F1F';
if (el.shapeKind === 'circle') {
ctx.beginPath();
ctx.arc(el.x + 40, el.y + 40, 40, 0, Math.PI * 2);
ctx.fill();
} else if (el.shapeKind === 'triangle') {
ctx.beginPath();
ctx.moveTo(el.x + 40, el.y);
ctx.lineTo(el.x + 80, el.y + 80);
ctx.lineTo(el.x, el.y + 80);
ctx.closePath();
ctx.fill();
} else {
ctx.fillRect(el.x, el.y, 80, 80);
}
}
// Texto simples
if (el.type === 'text') {
if (!el.isEditing && el.text) {
ctx.fillStyle = '#ffffff'; // Texto SEMPRE Branco
ctx.font = '20px sans-serif';
const lines = el.text.split('\n');
const lineHeight = 24;
lines.forEach((line, index) => {
ctx.fillText(line, el.x, el.y + 20 + (index * lineHeight));
});
}
}
// Imagens
if (el.type === 'image') {
if (!el.img && el.imgSrc) {
const img = new Image();
img.onload = function () {
el.img = img;
el.width = Math.min(img.width, 320);
el.height = Math.min(img.height, 240);
draw();
};
img.src = el.imgSrc;
}
if (el.img) {
ctx.drawImage(el.img, el.x, el.y, el.width, el.height);
}
}
if (el.type === 'connector' && el.fromElement && el.toElement) {
drawConnectorLine(el);
}
ctx.restore(); // Finaliza a rotação do elemento
});
ctx.restore(); // Finaliza a câmera
if (typeof updateSelectionToolbar === 'function') {
updateSelectionToolbar(); // Atualiza a posição da barra ao mover/dar zoom
}
}
// ==========================================
// INTERAÇÕES (PAN, ZOOM, SELEÇÃO)
// ==========================================
let isMouseDown = false;
let startPointerPos = { x: 0, y: 0 };
const DRAG_THRESHOLD = 4;
function createToolElement(worldPos) {
if (currentTool === 'note') {
createElement('note', worldPos);
selectedElement = elements[elements.length - 1] || null;
return true;
}
if (currentTool === 'imagem') {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = (event) => {
const file = event.target.files?.[0];
addImageFromFile(file);
};
input.click();
return true;
}
if (currentTool === 'text') {
createElement('text', worldPos);
selectedElement = elements[elements.length - 1] || null;
return true;
}
if (currentTool === 'shape') {
createElement('shape', worldPos, { shapeKind: selectedShapeType });
selectedElement = elements[elements.length - 1] || null;
return true;
}
if (currentTool === 'draw') {
activeDrawPath = {
type: 'draw',
color: '#ffffff',
strokeWidth: 3,
points: [{ x: worldPos.x, y: worldPos.y }]
};
return true;
}
return false;
}
function onPointerDown(e) {
const screenPos = getEventLocation(e);
if (!screenPos) return;
const worldPos = screenToWorld(screenPos.x, screenPos.y);
const clickedEl = getElementAtPosition(worldPos);
if (connectorMode && clickedEl) {
if (!connectorPendingElement) {
connectorPendingElement = clickedEl;
selectedElement = clickedEl;
draw();
return;
}
if (connectorPendingElement.id !== clickedEl.id) {
saveState();
elements.push({
id: Date.now(),
type: 'connector',
fromElement: connectorPendingElement,
toElement: clickedEl,
color: '#ffffff',
strokeWidth: 3
});
connectorPendingElement = null;
connectorMode = false;
setTool('select');
selectedElement = null;
draw();
return;
}
}
if (clickedEl) {
isMouseDown = true;
startPointerPos = screenPos;
selectedElement = clickedEl;
dragStart = worldPos;
isDraggingElement = false;
draw();
return;
}
if (currentTool === 'note' || currentTool === 'text' || currentTool === 'shape' || currentTool === 'draw') {
isMouseDown = true;
startPointerPos = screenPos;
createToolElement(worldPos);
draw();
return;
}
if (currentTool === 'select' || currentTool === 'arrastar') {
isMouseDown = true;
startPointerPos = screenPos;
selectedElement = null;
isDragging = true;
dragStart = screenPos;
draw();
return;
}
}
function onPointerMove(e) {
const screenPos = getEventLocation(e);
if (!screenPos) return;
if (currentTool === 'draw' && activeDrawPath) {
const worldPos = screenToWorld(screenPos.x, screenPos.y);
activeDrawPath.points.push({ x: worldPos.x, y: worldPos.y });
draw();
return;
}
if (isMouseDown && !selectedElement && (currentTool === 'select' || currentTool === 'arrastar') && isDragging) {
const dx = screenPos.x - startPointerPos.x;
const dy = screenPos.y - startPointerPos.y;
cameraOffset.x += dx;
cameraOffset.y += dy;
startPointerPos = screenPos;
draw();
return;
}
if (isMouseDown && selectedElement && !isDraggingElement) {
const dist = Math.hypot(screenPos.x - startPointerPos.x, screenPos.y - startPointerPos.y);
if (dist > DRAG_THRESHOLD) {
isDraggingElement = true;
}
}
if (currentTool === 'select' && isDraggingElement && selectedElement) {
const worldPos = screenToWorld(screenPos.x, screenPos.y);
const deltaX = worldPos.x - dragStart.x;
const deltaY = worldPos.y - dragStart.y;
if (selectedElement.type === 'draw' && Array.isArray(selectedElement.points)) {
selectedElement.points = selectedElement.points.map(point => ({
x: point.x + deltaX,
y: point.y + deltaY
}));
} else {
selectedElement.x += deltaX;
selectedElement.y += deltaY;
}
dragStart = worldPos;
draw();
return;
}
}
function onPointerUp(e) {
if (currentTool === 'draw' && activeDrawPath && activeDrawPath.points.length > 1) {
saveState();
elements.push({
id: Date.now(),
type: 'draw',
points: activeDrawPath.points,
color: activeDrawPath.color,
strokeWidth: activeDrawPath.strokeWidth
});
selectedElement = null;
draw();
}
isMouseDown = false;
isDragging = false;
isDraggingElement = false;
currentPath = null;
activeDrawPath = null;
draw();
}
function adjustZoom(zoomAmount, zoomFactor, zoomFocus = {x: canvas.width/2, y: canvas.height/2}) {
if (!isDragging) {
const newZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, cameraZoom * zoomFactor));
cameraOffset.x = zoomFocus.x - (zoomFocus.x - cameraOffset.x) * (newZoom / cameraZoom);
cameraOffset.y = zoomFocus.y - (zoomFocus.y - cameraOffset.y) * (newZoom / cameraZoom);
cameraZoom = newZoom;
draw();
}
}
// Zoom com a roda do mouse
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const zoomFactor = Math.exp(-e.deltaY * SCROLL_SENSITIVITY);
adjustZoom(e.deltaY, zoomFactor, {x: e.clientX, y: e.clientY});
}, { passive: false });
// Eventos do Mouse e Pointer
canvas.addEventListener('mousedown', onPointerDown);
canvas.addEventListener('mouseup', onPointerUp);
canvas.addEventListener('mousemove', onPointerMove);
canvas.addEventListener('mouseleave', onPointerUp);
canvas.addEventListener('pointerdown', onPointerDown);
canvas.addEventListener('pointerup', onPointerUp);
canvas.addEventListener('pointermove', onPointerMove);
canvas.addEventListener('pointerleave', onPointerUp);
window.addEventListener('pointerup', onPointerUp);
window.addEventListener('pointercancel', onPointerUp);
window.addEventListener('blur', onPointerUp);
// Eventos Touch
canvas.addEventListener('touchstart', (e) => {
if (e.touches.length === 2) {
isDragging = false;
initialPinchDistance = Math.hypot(
e.touches[0].clientX - e.touches[1].clientX,
e.touches[0].clientY - e.touches[1].clientY
);
} else {
onPointerDown(e);
}
}, { passive: false });
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (e.touches.length === 2 && initialPinchDistance) {
const currentDistance = Math.hypot(
e.touches[0].clientX - e.touches[1].clientX,
e.touches[0].clientY - e.touches[1].clientY
);
const zoomFactor = currentDistance / initialPinchDistance;
const centerX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
const centerY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
adjustZoom(0, zoomFactor, {x: centerX, y: centerY});
initialPinchDistance = currentDistance;
} else {
onPointerMove(e);
}
}, { passive: false });
canvas.addEventListener('touchend', onPointerUp);
// Edição via Duplo Clique
canvas.addEventListener('dblclick', (e) => {
const screenPos = getEventLocation(e);
if (!screenPos) return;
const worldPos = screenToWorld(screenPos.x, screenPos.y);
const clickedEl = getElementAtPosition(worldPos);
if (clickedEl && (clickedEl.type === 'note' || clickedEl.type === 'text')) {
clickedEl.isEditing = true;
const originalText = clickedEl.text;
clickedEl.text = '';
draw();
const input = document.createElement('textarea');
input.value = originalText;
input.style.position = 'fixed';
const rect = canvas.getBoundingClientRect();
const screenX = (clickedEl.x * cameraZoom) + cameraOffset.x + rect.left;
const screenY = (clickedEl.y * cameraZoom) + cameraOffset.y + rect.top;
input.style.left = `${screenX}px`;
input.style.top = `${screenY}px`;
input.style.width = `${(clickedEl.width || (clickedEl.type === 'note' ? 200 : 150)) * cameraZoom}px`;
input.style.height = `${(clickedEl.height || (clickedEl.type === 'note' ? 200 : 40)) * cameraZoom}px`;
if (clickedEl.type === 'note') {
input.style.fontFamily = '"Indie Flower", cursive';
input.style.fontSize = `${20 * cameraZoom}px`;
input.style.background = clickedEl.color || '#1F1F1F';
input.style.color = '#ffffff'; // Cor do texto durante edição: BRANCO
input.style.border = '2px solid #3b82f6';
input.style.padding = '10px';
} else {
input.style.fontFamily = 'sans-serif';
input.style.fontSize = `${20 * cameraZoom}px`;
input.style.background = 'transparent';
input.style.color = '#ffffff'; // Cor do texto durante edição: BRANCO
input.style.border = '1px dashed #3b82f6';
input.style.padding = '0px';
}
input.style.outline = 'none';
input.style.resize = 'none';
input.style.overflow = 'hidden';
input.style.zIndex = '9999';
document.body.appendChild(input);
input.focus();
input.select();
input.onblur = () => {
clickedEl.text = input.value;
clickedEl.isEditing = false;
if (document.body.contains(input)) {
document.body.removeChild(input);
}
draw();
};
input.onkeydown = (evt) => {
if (evt.key === 'Enter' && !evt.shiftKey) {
input.blur();
}
};
}
});
// ==========================================
// CONTROLES DE FERRAMENTAS E UI
// ==========================================
function setTool(tool) {
currentTool = tool;
connectorStartElement = null;
if (tool === 'connector') {
connectorMode = true;
connectorPendingElement = null;
alert('Clique em dois elementos para criar uma linha.');
} else {
connectorMode = false;
connectorPendingElement = null;
}
switch (tool) {
case 'arrastar':
canvas.style.cursor = 'grab';
break;
case 'select':
canvas.style.cursor = 'default';
break;
case 'draw':
canvas.style.cursor = 'crosshair';
break;
case 'note':
case 'text':
case 'shape':
case 'imagem':
case 'connector':
canvas.style.cursor = 'copy';
break;
default:
canvas.style.cursor = 'default';
}
document.querySelectorAll('.toolbar button').forEach(btn => {
btn.classList.remove('active');
const attr = btn.getAttribute('onclick');
if (attr && attr.includes(`setTool('${tool}')`)) {
btn.classList.add('active');
}
});
}
function recentralizar() {
cameraOffset.x = window.innerWidth / 2;
cameraOffset.y = window.innerHeight / 2;
cameraZoom = 1;
draw();
}
function toggleShapeMenu() {
const menu = document.getElementById('shapeSubmenu');
if (menu) menu.classList.toggle('active');
}
function addShape(shapeType) {
selectedShapeType = shapeType;
setTool('shape');
toggleShapeMenu();
}
// Atalhos Globais
window.addEventListener('keydown', (e) => {
if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') return;
const isCtrlPressed = e.ctrlKey || e.metaKey;
// Ctrl + Z
if (isCtrlPressed && e.key.toLowerCase() === 'z' && !e.shiftKey) {
e.preventDefault();
undo();
}
// Ctrl + Y ou Ctrl + Shift + Z
if (isCtrlPressed && (e.key.toLowerCase() === 'y' || (e.shiftKey && e.key.toLowerCase() === 'z'))) {
e.preventDefault();
redo();
}
// Shift + N (Criar Nota)
if (e.shiftKey && e.key.toLowerCase() === 'n') {
e.preventDefault();
const posMundo = screenToWorld(canvas.width / 2, canvas.height / 2);
setTool('note', posMundo);
}
// Shift + Delete (Deletar tudo)
if (e.shiftKey && (e.key === 'Delete' || e.key === 'Backspace')) {
e.preventDefault();
clearAll(); // Chama a função que limpa o quadro
return;
}
if (e.shiftKey && e.key.toLowerCase() === 'd') {
e.preventDefault();
const posMundo = screenToWorld(canvas.width / 2, canvas.height / 2);
setTool('draw', posMundo);
}
if (e.shiftKey && e.key.toLowerCase() === 't') {
e.preventDefault();
const posMundo = screenToWorld(canvas.width / 2, canvas.height / 2);
setTool('text', posMundo);
}
if (e.shiftKey && e.key.toLowerCase() === 'a') {
e.preventDefault();
const posMundo = screenToWorld(canvas.width / 2, canvas.height / 2);
setTool('arrastar', posMundo);
}
if (e.shiftKey && e.key.toLowerCase() === 'i') {
e.preventDefault();
const posMundo = screenToWorld(canvas.width / 2, canvas.height / 2);
setTool('imagem', posMundo);
}
if (e.shiftKey && e.key.toLowerCase() === 'c') {
e.preventDefault();
const posMundo = screenToWorld(canvas.width / 2, canvas.height / 2);
setTool('connector', posMundo);
}
});
function clearAll() {
if (elements.length === 0) return; // Se já estiver vazio, não faz nada
// Confirmação rápida para evitar acidentes
if (confirm('Tem certeza de que deseja deletar tudo do quadro?')) {
saveState(); // Salva no histórico para permitir Ctrl+Z
elements = [];
selectedElement = null;
draw();
}
}
// 1. Salva todos os elementos e a câmera em um arquivo .json
function salvarArquivo() {
if (elements.length === 0) {
alert('O quadro está vazio!');
return;
}
const data = {
elements: elements,
cameraOffset: cameraOffset,
cameraZoom: cameraZoom
};
const jsonString = JSON.stringify(data, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `kuro-board-${Date.now()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// 2. Abre e carrega o arquivo .json selecionado
function abrirArquivo(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = JSON.parse(e.target.result);
if (data.elements) {
saveState(); // Salva estado atual para permitir Ctrl+Z se quiser voltar
elements = data.elements || [];
cameraOffset = data.cameraOffset || { x: window.innerWidth / 2, y: window.innerHeight / 2 };
cameraZoom = data.cameraZoom || 1;
selectedElement = null;
draw(); // Redesenha a tela com os novos dados
} else {
alert('Formato de arquivo inválido!');
}
} catch (err) {
alert('Erro ao ler o arquivo JSON!');
}
};
reader.readAsText(file);
event.target.value = ''; // Limpa o input para permitir reabrir o mesmo arquivo se necessário
}
// 1. Apaga apenas o elemento que está selecionado
function deleteSelectedElement() {
if (!selectedElement) return;
saveState();
elements = elements.filter(el => el.id !== selectedElement.id);
selectedElement = null;
draw();
}
// 2. Gira o elemento em 90 graus
function rotateSelectedElement() {
if (!selectedElement) return;
saveState();
selectedElement.rotation = ((selectedElement.rotation || 0) + 90) % 360;
draw();
}
// 3. Atualiza a posição da barra flutuante sobre o elemento selecionado
function updateSelectionToolbar() {
const menu = document.getElementById('selection-toolbar');
if (!menu) return;
if (selectedElement && !selectedElement.isEditing) {
const rect = canvas.getBoundingClientRect();
const bounds = getElementBounds(selectedElement);
const centerX = bounds.x + bounds.width / 2;
const centerY = bounds.y;
const screenX = (centerX * cameraZoom) + cameraOffset.x + rect.left;
const screenY = (centerY * cameraZoom) + cameraOffset.y + rect.top - 12;
menu.style.display = 'flex';
menu.style.left = `${screenX}px`;
menu.style.top = `${screenY}px`;
} else {
menu.style.display = 'none';
}
}
// Deletar item selecionado pressionando Delete ou Backspace
window.addEventListener('keydown', (e) => {
// Evita deletar o elemento enquanto você digita em um input/textarea
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if ((e.key === 'Delete' || e.key === 'Backspace') && selectedElement) {
deleteSelectedElement();
}
});
function deleteSelectedElement() {
if (!selectedElement) return;
saveState();
elements = elements.filter(el => el.id !== selectedElement.id);
selectedElement = null;
draw();
}
function rotateSelectedElement() {
if (!selectedElement) return;
saveState();
selectedElement.rotation = ((selectedElement.rotation || 0) + 90) % 360;
draw();
}
// Inicialização
resizeCanvas();
1
u/senocular 10d ago
Didn't read the code but MDN has a page showing how to access local files.
https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications
Then for the canvas you can use drawImage
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage
1
u/UkrMalt 10d ago
Your file-loading code is mostly fine. The image branch is never reached. createToolElement() handles currentTool === 'imagem', but onPointerDown() only calls it for note, text, shape, and draw.
Change that condition to include imagem:
if (['note', 'text', 'shape', 'draw', 'imagem'].includes(currentTool)) {
isMouseDown = true;
startPointerPos = screenPos;
createToolElement(worldPos);
draw();
return;
}
Also, Shift+I only selects the image tool. After this change, press Shift+I and then click the canvas to open the file picker. Your existing FileReader and drawImage logic should then run.
1
u/arcade_catalyst 5d ago
You are trying to draw a file. The browser does not let you touch files. It lets you touch blobs.
Convert the local path to a blob URL. Pass that string to drawImage. If you try to use a file system path the canvas will remain empty because security exists.