flowsketcher-pro / script.js
chrischen's picture
画布所有tools不能用
85a793b verified
Raw History Blame Contribute Delete
23 kB
// FlowSketcher Pro - Main Application Logic
class FlowSketcherApp {
constructor() {
this.canvas = null;
this.ctx = null;
this.shapes = [];
this.selectedShapes = [];
this.currentTool = 'select';
this.isDrawing = false;
this.isDragging = false;
this.isResizing = false;
this.startPoint = { x: 0, y: 0 };
this.currentShape = null;
this.resizeHandle = null;
this.gridSize = 20;
this.history = [];
this.historyIndex = -1;
this.colors = {
stroke: '#000000',
fill: '#ffffff'
};
this.strokeWidth = 2;
this.tools = {
SELECT: 'select',
RECTANGLE: 'rectangle',
CIRCLE: 'circle',
DIAMOND: 'diamond',
ARROW: 'arrow',
TEXT: 'text',
FREEHAND: 'freehand'
};
this.init();
}
init() {
this.setupEventListeners();
this.setupCanvas();
this.setupKeyboardShortcuts();
this.loadFromLocalStorage();
}
setupCanvas() {
// Wait for canvas-area to be fully rendered
const canvasArea = document.querySelector('canvas-area');
if (canvasArea) {
// Use setTimeout to ensure shadow DOM is rendered
setTimeout(() => {
this.canvas = canvasArea.shadowRoot.querySelector('#main-canvas');
if (this.canvas) {
this.ctx = this.canvas.getContext('2d');
this.resizeCanvas();
window.addEventListener('resize', () => this.resizeCanvas());
console.log('Canvas initialized successfully');
} else {
console.error('Canvas element not found in shadow DOM');
}
}, 100);
} else {
console.error('Canvas-area component not found');
}
}
resizeCanvas() {
const canvasArea = document.querySelector('canvas-area');
const rect = canvasArea.getBoundingClientRect();
this.canvas.width = rect.width;
this.canvas.height = rect.height;
this.redrawCanvas();
}
setupEventListeners() {
document.addEventListener('tool-changed', (e) => {
this.currentTool = e.detail.tool;
this.canvas.style.cursor = this.getCursorForTool(this.currentTool);
});
document.addEventListener('color-changed', (e) => {
this.colors[e.detail.type] = e.detail.color;
if (this.selectedShapes.length > 0) {
this.updateSelectedShapes({ [e.detail.type]: e.detail.color });
}
});
document.addEventListener('stroke-width-changed', (e) => {
this.strokeWidth = e.detail.width;
if (this.selectedShapes.length > 0) {
this.updateSelectedShapes({ strokeWidth: e.detail.width });
}
});
document.addEventListener('clear-canvas', () => this.clearCanvas());
document.addEventListener('export-png', () => this.exportAsPNG());
document.addEventListener('undo', () => this.undo());
document.addEventListener('redo', () => this.redo());
document.addEventListener('delete-selected', () => this.deleteSelected());
// Canvas events
document.addEventListener('canvas-mousedown', (e) => this.handleMouseDown(e.detail));
document.addEventListener('canvas-mousemove', (e) => this.handleMouseMove(e.detail));
document.addEventListener('canvas-mouseup', (e) => this.handleMouseUp(e.detail));
document.addEventListener('canvas-dblclick', (e) => this.handleDoubleClick(e.detail));
// Quick shape insertion
document.addEventListener('insert-quick-shape', (e) => {
this.insertQuickShape(e.detail);
});
}
setupKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
if (e.ctrlKey || e.metaKey) {
switch(e.key) {
case 'z':
e.preventDefault();
if (e.shiftKey) {
this.redo();
} else {
this.undo();
}
break;
case 'y':
e.preventDefault();
this.redo();
break;
case 's':
e.preventDefault();
this.exportAsPNG();
break;
case 'a':
e.preventDefault();
this.selectAll();
break;
}
} else if (e.key === 'Delete') {
this.deleteSelected();
} else if (e.key === 'Escape') {
this.deselectAll();
}
});
}
handleMouseDown(event) {
if (!this.canvas || !this.ctx) {
console.warn('Canvas not ready');
return;
}
const { offsetX, offsetY } = event;
this.startPoint = { x: offsetX, y: offsetY };
if (this.currentTool === this.tools.SELECT) {
const clickedShape = this.getShapeAtPoint(offsetX, offsetY);
if (clickedShape) {
if (!event.shiftKey && !this.selectedShapes.includes(clickedShape)) {
this.deselectAll();
}
this.selectedShapes.push(clickedShape);
this.isDragging = true;
this.updatePropertiesPanel();
} else {
this.deselectAll();
this.isDrawing = true;
}
} else {
this.isDrawing = true;
this.startNewShape(offsetX, offsetY);
}
this.redrawCanvas();
}
handleMouseMove(event) {
const { offsetX, offsetY } = event;
if (this.isDragging && this.selectedShapes.length > 0) {
this.dragSelectedShapes(offsetX - this.startPoint.x, offsetY - this.startPoint.y);
this.startPoint = { x: offsetX, y: offsetY };
} else if (this.isDrawing && this.currentShape) {
this.updateCurrentShape(offsetX, offsetY);
} else if (this.isResizing && this.resizeHandle) {
this.resizeSelectedShape(offsetX, offsetY);
}
this.redrawCanvas();
}
handleMouseUp(event) {
if (this.isDrawing && this.currentShape) {
if (this.currentShape.width > 5 || this.currentShape.height > 5 ||
(this.currentShape.points && this.currentShape.points.length > 2)) {
this.shapes.push(this.currentShape);
this.saveToHistory();
}
this.currentShape = null;
}
this.isDrawing = false;
this.isDragging = false;
this.isResizing = false;
this.resizeHandle = null;
if (this.currentTool !== this.tools.SELECT) {
this.currentTool = this.tools.SELECT;
document.dispatchEvent(new CustomEvent('tool-changed', { detail: { tool: this.tools.SELECT } }));
}
}
handleDoubleClick(event) {
const { offsetX, offsetY } = event;
const clickedShape = this.getShapeAtPoint(offsetX, offsetY);
if (clickedShape && clickedShape.type === this.tools.TEXT) {
this.editTextShape(clickedShape, event);
}
}
startNewShape(x, y) {
const shape = {
id: 'shape_' + Date.now(),
type: this.currentTool,
x: x,
y: y,
width: 0,
height: 0,
strokeColor: this.colors.stroke,
fillColor: this.colors.fill,
strokeWidth: this.strokeWidth,
roughness: 1.5,
text: '',
points: this.currentTool === this.tools.FREEHAND ? [{x, y}] : null
};
this.currentShape = shape;
}
updateCurrentShape(x, y) {
if (!this.currentShape) return;
if (this.currentShape.type === this.tools.FREEHAND) {
this.currentShape.points.push({ x, y });
} else if (this.currentShape.type === this.tools.ARROW) {
this.currentShape.x2 = x;
this.currentShape.y2 = y;
} else {
this.currentShape.width = x - this.startPoint.x;
this.currentShape.height = y - this.startPoint.y;
}
}
dragSelectedShapes(dx, dy) {
this.selectedShapes.forEach(shape => {
shape.x += dx;
shape.y += dy;
if (shape.points) {
shape.points.forEach(p => {
p.x += dx;
p.y += dy;
});
}
});
}
resizeSelectedShape(x, y) {
if (this.selectedShapes.length !== 1) return;
const shape = this.selectedShapes[0];
const handle = this.resizeHandle;
if (handle.includes('e')) {
shape.width = x - shape.x;
}
if (handle.includes('s')) {
shape.height = y - shape.y;
}
if (handle.includes('w')) {
const newWidth = shape.x + shape.width - x;
if (newWidth > 20) {
shape.width = newWidth;
shape.x = x;
}
}
if (handle.includes('n')) {
const newHeight = shape.y + shape.height - y;
if (newHeight > 20) {
shape.height = newHeight;
shape.y = y;
}
}
}
redrawCanvas() {
if (!this.ctx) return;
const { width, height } = this.canvas;
this.ctx.clearRect(0, 0, width, height);
// Draw shapes
this.shapes.concat(this.currentShape ? [this.currentShape] : []).forEach(shape => {
this.drawShape(shape);
});
// Draw selection
this.drawSelection();
}
drawShape(shape) {
if (!shape) return;
this.ctx.save();
this.ctx.strokeStyle = shape.strokeColor;
this.ctx.fillStyle = shape.fillColor;
this.ctx.lineWidth = shape.strokeWidth;
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
switch(shape.type) {
case this.tools.RECTANGLE:
this.drawRoughRectangle(shape);
break;
case this.tools.CIRCLE:
this.drawRoughCircle(shape);
break;
case this.tools.DIAMOND:
this.drawRoughDiamond(shape);
break;
case this.tools.ARROW:
this.drawArrow(shape);
break;
case this.tools.TEXT:
this.drawText(shape);
break;
case this.tools.FREEHAND:
this.drawFreehand(shape);
break;
}
this.ctx.restore();
}
drawRoughRectangle(shape) {
const { x, y, width, height } = shape;
if (width === 0 || height === 0) return;
this.ctx.beginPath();
this.ctx.moveTo(this.roughX(x), this.roughY(y));
this.ctx.lineTo(this.roughX(x + width), this.roughY(y));
this.ctx.lineTo(this.roughX(x + width), this.roughY(y + height));
this.ctx.lineTo(this.roughX(x), this.roughY(y + height));
this.ctx.closePath();
this.ctx.fill();
this.ctx.stroke();
}
drawRoughCircle(shape) {
const { x, y, width, height } = shape;
const centerX = x + width / 2;
const centerY = y + height / 2;
const radius = Math.min(Math.abs(width), Math.abs(height)) / 2;
this.ctx.beginPath();
this.ctx.arc(this.roughX(centerX), this.roughY(centerY), radius, 0, 2 * Math.PI);
this.ctx.fill();
this.ctx.stroke();
}
drawRoughDiamond(shape) {
const { x, y, width, height } = shape;
const centerX = x + width / 2;
const centerY = y + height / 2;
this.ctx.beginPath();
this.ctx.moveTo(this.roughX(centerX), this.roughY(y));
this.ctx.lineTo(this.roughX(x + width), this.roughY(centerY));
this.ctx.lineTo(this.roughX(centerX), this.roughY(y + height));
this.ctx.lineTo(this.roughX(x), this.roughY(centerY));
this.ctx.closePath();
this.ctx.fill();
this.ctx.stroke();
}
drawArrow(shape) {
if (!shape.x2 || !shape.y2) return;
const { x, y, x2, y2 } = shape;
const angle = Math.atan2(y2 - y, x2 - x);
const arrowLength = 15;
this.ctx.beginPath();
this.ctx.moveTo(this.roughX(x), this.roughY(y));
this.ctx.lineTo(this.roughX(x2), this.roughY(y2));
this.ctx.stroke();
// Arrow head
this.ctx.beginPath();
this.ctx.moveTo(this.roughX(x2), this.roughY(y2));
this.ctx.lineTo(
this.roughX(x2 - arrowLength * Math.cos(angle - Math.PI / 6)),
this.roughY(y2 - arrowLength * Math.sin(angle - Math.PI / 6))
);
this.ctx.moveTo(this.roughX(x2), this.roughY(y2));
this.ctx.lineTo(
this.roughX(x2 - arrowLength * Math.cos(angle + Math.PI / 6)),
this.roughY(y2 - arrowLength * Math.sin(angle + Math.PI / 6))
);
this.ctx.stroke();
}
drawText(shape) {
const { x, y, width, height, text } = shape;
this.ctx.font = `${Math.abs(height) || 20}px Inter, sans-serif`;
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
this.ctx.fillStyle = shape.strokeColor;
this.ctx.fillText(text || '双击编辑', x + width / 2, y + (height || 20) / 2);
}
drawFreehand(shape) {
if (!shape.points || shape.points.length < 2) return;
this.ctx.beginPath();
this.ctx.moveTo(this.roughX(shape.points[0].x), this.roughY(shape.points[0].y));
for (let i = 1; i < shape.points.length; i++) {
const point = shape.points[i];
this.ctx.lineTo(this.roughX(point.x), this.roughY(point.y));
}
this.ctx.stroke();
}
drawSelection() {
this.selectedShapes.forEach(shape => {
this.drawResizeHandles(shape);
});
}
drawResizeHandles(shape) {
const handles = this.getResizeHandles(shape);
handles.forEach(handle => {
this.ctx.fillStyle = '#6366f1';
this.ctx.strokeStyle = 'white';
this.ctx.lineWidth = 2;
this.ctx.beginPath();
this.ctx.arc(handle.x, handle.y, 5, 0, 2 * Math.PI);
this.ctx.fill();
this.ctx.stroke();
});
}
getResizeHandles(shape) {
const handles = [];
const { x, y, width, height } = shape;
if (shape.type !== this.tools.FREEHAND) {
handles.push({ x: x, y: y, position: 'nw' });
handles.push({ x: x + width, y: y, position: 'ne' });
handles.push({ x: x, y: y + height, position: 'sw' });
handles.push({ x: x + width, y: y + height, position: 'se' });
}
return handles;
}
roughX(x) {
return x + (Math.random() - 0.5) * this.getRoughness();
}
roughY(y) {
return y + (Math.random() - 0.5) * this.getRoughness();
}
getRoughness() {
return 1.5;
}
getShapeAtPoint(x, y) {
for (let i = this.shapes.length - 1; i >= 0; i--) {
const shape = this.shapes[i];
if (this.isPointInShape(x, y, shape)) {
return shape;
}
}
return null;
}
isPointInShape(x, y, shape) {
if (shape.type === this.tools.FREEHAND) {
return this.isPointNearFreehand(x, y, shape);
}
return x >= shape.x && x <= shape.x + shape.width &&
y >= shape.y && y <= shape.y + shape.height;
}
isPointNearFreehand(x, y, shape) {
if (!shape.points) return false;
for (const point of shape.points) {
const distance = Math.sqrt((x - point.x) ** 2 + (y - point.y) ** 2);
if (distance < 10) return true;
}
return false;
}
updateSelectedShapes(updates) {
this.selectedShapes.forEach(shape => {
Object.assign(shape, updates);
});
this.redrawCanvas();
}
deselectAll() {
this.selectedShapes = [];
this.updatePropertiesPanel();
this.redrawCanvas();
}
selectAll() {
this.selectedShapes = [...this.shapes];
this.redrawCanvas();
}
insertQuickShape(config) {
const canvasRect = this.canvas.getBoundingClientRect();
const shape = {
id: 'shape_' + Date.now(),
type: config.type,
x: canvasRect.width / 2 - config.width / 2,
y: canvasRect.height / 2 - config.height / 2,
width: config.width,
height: config.height,
strokeColor: this.colors.stroke,
fillColor: this.colors.fill,
strokeWidth: this.strokeWidth,
roughness: 1.5,
text: '',
points: null
};
this.shapes.push(shape);
this.saveToHistory();
this.redrawCanvas();
}
deleteSelected() {
this.shapes = this.shapes.filter(shape => !this.selectedShapes.includes(shape));
this.selectedShapes = [];
this.saveToHistory();
this.redrawCanvas();
}
editTextShape(shape, event) {
const input = document.createElement('textarea');
input.className = 'shape-text-input';
input.value = shape.text;
input.style.left = `${shape.x}px`;
input.style.top = `${shape.y}px`;
input.style.width = `${Math.max(shape.width, 100)}px`;
input.style.height = `${Math.max(shape.height, 30)}px`;
input.style.color = shape.strokeColor;
const canvasArea = document.querySelector('canvas-area');
canvasArea.shadowRoot.appendChild(input);
input.focus();
input.select();
const finishEdit = () => {
shape.text = input.value;
canvasArea.shadowRoot.removeChild(input);
this.saveToHistory();
this.redrawCanvas();
};
input.addEventListener('blur', finishEdit);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
finishEdit();
}
});
}
updatePropertiesPanel() {
const panel = document.querySelector('properties-panel');
if (panel) {
panel.updateSelection(this.selectedShapes);
}
}
saveToHistory() {
this.history = this.history.slice(0, this.historyIndex + 1);
this.history.push(JSON.stringify(this.shapes));
this.historyIndex++;
if (this.history.length > 50) {
this.history.shift();
this.historyIndex--;
}
this.saveToLocalStorage();
}
undo() {
if (this.historyIndex > 0) {
this.historyIndex--;
this.shapes = JSON.parse(this.history[this.historyIndex]);
this.selectedShapes = [];
this.redrawCanvas();
}
}
redo() {
if (this.historyIndex < this.history.length - 1) {
this.historyIndex++;
this.shapes = JSON.parse(this.history[this.historyIndex]);
this.selectedShapes = [];
this.redrawCanvas();
}
}
clearCanvas() {
if (confirm('Are you sure you want to clear the canvas? This action cannot be undone.')) {
this.shapes = [];
this.selectedShapes = [];
this.history = [];
this.historyIndex = -1;
this.saveToHistory();
this.redrawCanvas();
}
}
exportAsPNG() {
const tempCanvas = document.createElement('canvas');
tempCanvas.width = this.canvas.width;
tempCanvas.height = this.canvas.height;
const tempCtx = tempCanvas.getContext('2d');
// White background
tempCtx.fillStyle = 'white';
tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
// Draw all shapes
this.shapes.forEach(shape => {
this.drawShapeOnContext(shape, tempCtx);
});
// Download
const link = document.createElement('a');
link.download = `flowchart-${Date.now()}.png`;
link.href = tempCanvas.toDataURL();
link.click();
// Show success notification
document.dispatchEvent(new CustomEvent('show-notification', {
detail: { message: 'Flowchart exported successfully!' }
}));
}
drawShapeOnContext(shape, ctx) {
const originalCtx = this.ctx;
this.ctx = ctx;
this.drawShape(shape);
this.ctx = originalCtx;
}
saveToLocalStorage() {
try {
localStorage.setItem('flowsketcher-autosave', JSON.stringify({
shapes: this.shapes,
timestamp: Date.now()
}));
} catch (e) {
console.warn('Failed to save to localStorage:', e);
}
}
loadFromLocalStorage() {
try {
const saved = localStorage.getItem('flowsketcher-autosave');
if (saved) {
const data = JSON.parse(saved);
this.shapes = data.shapes || [];
this.saveToHistory();
this.redrawCanvas();
}
} catch (e) {
console.warn('Failed to load from localStorage:', e);
}
}
getCursorForTool(tool) {
const cursors = {
[this.tools.SELECT]: 'default',
[this.tools.RECTANGLE]: 'crosshair',
[this.tools.CIRCLE]: 'crosshair',
[this.tools.DIAMOND]: 'crosshair',
[this.tools.ARROW]: 'crosshair',
[this.tools.TEXT]: 'text',
[this.tools.FREEHAND]: 'crosshair'
};
return cursors[tool] || 'default';
}
}
// Initialize app when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
window.FlowSketcher = new FlowSketcherApp();
});