+286
-29
@@ -40,6 +40,272 @@ class PaintManager {
|
||||
this.hideBrushCursor = this.hideBrushCursor.bind(this);
|
||||
}
|
||||
|
||||
buildFontString(fontFamily, fontSize, bold = false, italic = false) {
|
||||
const fontParts = [];
|
||||
if (italic) fontParts.push('italic');
|
||||
if (bold) fontParts.push('bold');
|
||||
fontParts.push(`${fontSize}px`);
|
||||
fontParts.push(fontFamily);
|
||||
return fontParts.join(' ');
|
||||
}
|
||||
|
||||
parseInlineMarkdown(text, baseStyle = {}) {
|
||||
const patterns = [
|
||||
{
|
||||
regex: /`([^`]+)`/g,
|
||||
style: { code: true, fontFamily: 'monospace', background: 'rgba(0, 0, 0, 0.08)' }
|
||||
},
|
||||
{ regex: /\*\*([^*]+)\*\*/g, style: { bold: true } },
|
||||
{ regex: /__([^_]+)__/g, style: { bold: true } },
|
||||
{ regex: /\*([^*]+)\*/g, style: { italic: true } },
|
||||
{ regex: /_([^_]+)_/g, style: { italic: true } },
|
||||
{ regex: /~~([^~]+)~~/g, style: { strike: true, color: '#666666' } }
|
||||
];
|
||||
|
||||
let segments = [{ text, style: { ...baseStyle } }];
|
||||
|
||||
patterns.forEach(({ regex, style }) => {
|
||||
const nextSegments = [];
|
||||
segments.forEach((segment) => {
|
||||
if (!segment.text) return;
|
||||
regex.lastIndex = 0;
|
||||
let lastIndex = 0;
|
||||
let match = regex.exec(segment.text);
|
||||
let hasMatch = false;
|
||||
|
||||
while (match) {
|
||||
hasMatch = true;
|
||||
if (match.index > lastIndex) {
|
||||
nextSegments.push({
|
||||
text: segment.text.slice(lastIndex, match.index),
|
||||
style: { ...segment.style }
|
||||
});
|
||||
}
|
||||
nextSegments.push({
|
||||
text: match[1],
|
||||
style: { ...segment.style, ...style }
|
||||
});
|
||||
lastIndex = match.index + match[0].length;
|
||||
match = regex.exec(segment.text);
|
||||
}
|
||||
|
||||
if (!hasMatch) {
|
||||
nextSegments.push(segment);
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastIndex < segment.text.length) {
|
||||
nextSegments.push({
|
||||
text: segment.text.slice(lastIndex),
|
||||
style: { ...segment.style }
|
||||
});
|
||||
}
|
||||
});
|
||||
segments = nextSegments;
|
||||
});
|
||||
|
||||
return segments.filter((segment) => segment.text);
|
||||
}
|
||||
|
||||
parseMarkdownText(markdownText, fontFamily, fontSize, color, defaultBold = false, defaultItalic = false) {
|
||||
const normalizedText = String(markdownText || '').replace(/\r\n/g, '\n');
|
||||
const lines = normalizedText.split('\n');
|
||||
const blocks = [];
|
||||
|
||||
lines.forEach((rawLine) => {
|
||||
const line = rawLine || '';
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
blocks.push({ type: 'blank', height: Math.max(12, Math.round(fontSize * 0.75)) });
|
||||
return;
|
||||
}
|
||||
|
||||
let type = 'paragraph';
|
||||
let content = line;
|
||||
let level = 0;
|
||||
let indent = 0;
|
||||
let prefix = '';
|
||||
|
||||
const headingMatch = trimmed.match(/^(#{1,6})\s+(.*)$/);
|
||||
const quoteMatch = line.match(/^\s*>\s?(.*)$/);
|
||||
const unorderedListMatch = line.match(/^(\s*)[-*+]\s+(.*)$/);
|
||||
const orderedListMatch = line.match(/^(\s*)(\d+)\.\s+(.*)$/);
|
||||
|
||||
if (headingMatch) {
|
||||
type = 'heading';
|
||||
level = headingMatch[1].length;
|
||||
content = headingMatch[2];
|
||||
} else if (quoteMatch) {
|
||||
type = 'quote';
|
||||
content = quoteMatch[1];
|
||||
} else if (unorderedListMatch) {
|
||||
type = 'list';
|
||||
content = unorderedListMatch[2];
|
||||
indent = Math.floor((unorderedListMatch[1] || '').length / 2);
|
||||
prefix = '•';
|
||||
} else if (orderedListMatch) {
|
||||
type = 'ordered-list';
|
||||
content = orderedListMatch[3];
|
||||
indent = Math.floor((orderedListMatch[1] || '').length / 2);
|
||||
prefix = `${orderedListMatch[2]}.`;
|
||||
}
|
||||
|
||||
const scale = type === 'heading' ? Math.max(1, 1.65 - (level - 1) * 0.16) : 1;
|
||||
const blockFontSize = Math.max(10, Math.round(fontSize * scale));
|
||||
const baseStyle = {
|
||||
color: type === 'quote' ? '#444444' : color,
|
||||
fontFamily,
|
||||
fontSize: blockFontSize,
|
||||
bold: type === 'heading' || defaultBold,
|
||||
italic: defaultItalic
|
||||
};
|
||||
|
||||
blocks.push({
|
||||
type,
|
||||
level,
|
||||
indent,
|
||||
prefix,
|
||||
fontSize: blockFontSize,
|
||||
lineHeight: Math.max(14, Math.round(blockFontSize * (type === 'heading' ? 1.45 : 1.35))),
|
||||
segments: this.parseInlineMarkdown(content, baseStyle)
|
||||
});
|
||||
});
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
measureMarkdownBlock(block) {
|
||||
if (block.type === 'blank') {
|
||||
return { width: 0, height: block.height || 12 };
|
||||
}
|
||||
|
||||
const indentWidth = block.indent ? block.indent * 18 : 0;
|
||||
let prefixWidth = 0;
|
||||
if (block.prefix) {
|
||||
this.ctx.font = this.buildFontString('Arial', block.fontSize, true, false);
|
||||
prefixWidth = this.ctx.measureText(block.prefix).width + 8;
|
||||
}
|
||||
|
||||
let contentWidth = 0;
|
||||
block.segments.forEach((segment) => {
|
||||
this.ctx.font = this.buildFontString(
|
||||
segment.style.fontFamily || 'Arial',
|
||||
segment.style.fontSize || block.fontSize,
|
||||
!!segment.style.bold,
|
||||
!!segment.style.italic
|
||||
);
|
||||
contentWidth += this.ctx.measureText(segment.text).width;
|
||||
});
|
||||
|
||||
return {
|
||||
width: indentWidth + prefixWidth + contentWidth + (block.type === 'quote' ? 16 : 0),
|
||||
height: block.lineHeight
|
||||
};
|
||||
}
|
||||
|
||||
measureTextElement(textElement) {
|
||||
if (textElement.type === 'markdown' && Array.isArray(textElement.blocks)) {
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
|
||||
textElement.blocks.forEach((block) => {
|
||||
const size = this.measureMarkdownBlock(block);
|
||||
width = Math.max(width, size.width);
|
||||
height += size.height;
|
||||
});
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
top: textElement.y - Math.max(14, textElement.baseFontSize || 16),
|
||||
left: textElement.x
|
||||
};
|
||||
}
|
||||
|
||||
this.ctx.font = textElement.font;
|
||||
const textWidth = this.ctx.measureText(textElement.text).width;
|
||||
const fontSizeMatch = textElement.font.match(/(\d+)px/);
|
||||
const textHeight = (fontSizeMatch ? parseInt(fontSizeMatch[1], 10) : 14) * 1.2;
|
||||
|
||||
return {
|
||||
width: textWidth,
|
||||
height: textHeight,
|
||||
top: textElement.y - textHeight,
|
||||
left: textElement.x
|
||||
};
|
||||
}
|
||||
|
||||
renderTextElement(textElement) {
|
||||
if (textElement.type === 'markdown' && Array.isArray(textElement.blocks)) {
|
||||
let currentY = textElement.y;
|
||||
|
||||
textElement.blocks.forEach((block) => {
|
||||
if (block.type === 'blank') {
|
||||
currentY += block.height || 12;
|
||||
return;
|
||||
}
|
||||
|
||||
let currentX = textElement.x + (block.indent ? block.indent * 18 : 0);
|
||||
if (block.type === 'quote') {
|
||||
this.ctx.strokeStyle = '#999999';
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(textElement.x + 6, currentY - block.lineHeight + 4);
|
||||
this.ctx.lineTo(textElement.x + 6, currentY + 2);
|
||||
this.ctx.stroke();
|
||||
currentX += 16;
|
||||
}
|
||||
|
||||
if (block.prefix) {
|
||||
this.ctx.font = this.buildFontString('Arial', block.fontSize, true, false);
|
||||
this.ctx.fillStyle = textElement.color;
|
||||
this.ctx.fillText(block.prefix, currentX, currentY);
|
||||
currentX += this.ctx.measureText(block.prefix).width + 8;
|
||||
}
|
||||
|
||||
block.segments.forEach((segment) => {
|
||||
const fontSize = segment.style.fontSize || block.fontSize;
|
||||
this.ctx.font = this.buildFontString(
|
||||
segment.style.fontFamily || textElement.fontFamily || 'Arial',
|
||||
fontSize,
|
||||
!!segment.style.bold,
|
||||
!!segment.style.italic
|
||||
);
|
||||
|
||||
const segmentWidth = this.ctx.measureText(segment.text).width;
|
||||
|
||||
if (segment.style.code) {
|
||||
this.ctx.fillStyle = segment.style.background || 'rgba(0, 0, 0, 0.08)';
|
||||
this.ctx.fillRect(currentX - 2, currentY - fontSize, segmentWidth + 4, block.lineHeight - 2);
|
||||
}
|
||||
|
||||
this.ctx.fillStyle = segment.style.color || textElement.color;
|
||||
this.ctx.fillText(segment.text, currentX, currentY);
|
||||
|
||||
if (segment.style.strike) {
|
||||
this.ctx.strokeStyle = segment.style.color || textElement.color;
|
||||
this.ctx.lineWidth = 1;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(currentX, currentY - fontSize * 0.35);
|
||||
this.ctx.lineTo(currentX + segmentWidth, currentY - fontSize * 0.35);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
|
||||
currentX += segmentWidth;
|
||||
});
|
||||
|
||||
currentY += block.lineHeight;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.ctx.font = textElement.font;
|
||||
this.ctx.fillStyle = textElement.color;
|
||||
this.ctx.fillText(textElement.text, textElement.x, textElement.y);
|
||||
}
|
||||
|
||||
saveToHistory() {
|
||||
// Remove any states after current step (when user drew something after undoing)
|
||||
this.historyStack = this.historyStack.slice(0, this.historyStep + 1);
|
||||
@@ -483,9 +749,7 @@ class PaintManager {
|
||||
} else {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
this.ctx.font = this.selectedTextElement.font;
|
||||
this.ctx.fillStyle = this.selectedTextElement.color;
|
||||
this.ctx.fillText(this.selectedTextElement.text, this.selectedTextElement.x, this.selectedTextElement.y);
|
||||
this.renderTextElement(this.selectedTextElement);
|
||||
}
|
||||
|
||||
findTextElementAt(e) {
|
||||
@@ -499,21 +763,18 @@ class PaintManager {
|
||||
for (let i = this.textElements.length - 1; i >= 0; i--) {
|
||||
const text = this.textElements[i];
|
||||
|
||||
// Calculate text dimensions
|
||||
this.ctx.font = text.font;
|
||||
const textWidth = this.ctx.measureText(text.text).width;
|
||||
|
||||
// Extract font size correctly from the font string
|
||||
const fontSizeMatch = text.font.match(/(\d+)px/);
|
||||
const fontSize = fontSizeMatch ? parseInt(fontSizeMatch[1]) : 14;
|
||||
const textHeight = fontSize * 1.2; // Approximate height
|
||||
const measurement = this.measureTextElement(text);
|
||||
const textWidth = measurement.width;
|
||||
const textHeight = measurement.height;
|
||||
const top = measurement.top;
|
||||
const left = measurement.left;
|
||||
|
||||
// Check if click is within text bounds (allowing for some margin)
|
||||
const margin = 5;
|
||||
if (x >= text.x - margin &&
|
||||
x <= text.x + textWidth + margin &&
|
||||
y >= text.y - textHeight + margin &&
|
||||
y <= text.y + margin) {
|
||||
if (x >= left - margin &&
|
||||
x <= left + textWidth + margin &&
|
||||
y >= top - margin &&
|
||||
y <= top + textHeight + margin) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -556,20 +817,20 @@ class PaintManager {
|
||||
|
||||
const text = document.getElementById('text-input').value;
|
||||
const fontFamily = document.getElementById('font-family').value;
|
||||
const fontSize = document.getElementById('font-size').value;
|
||||
|
||||
// Build font style string
|
||||
let fontStyle = '';
|
||||
if (this.textItalic) fontStyle += 'italic ';
|
||||
if (this.textBold) fontStyle += 'bold ';
|
||||
const fontSize = parseInt(document.getElementById('font-size').value, 10);
|
||||
const blocks = this.parseMarkdownText(text, fontFamily, fontSize, this.brushColor, this.textBold, this.textItalic);
|
||||
|
||||
// Create a new text element
|
||||
const newText = {
|
||||
type: 'markdown',
|
||||
text: text,
|
||||
x: x,
|
||||
y: y,
|
||||
font: `${fontStyle}${fontSize}px ${fontFamily}`,
|
||||
color: this.brushColor
|
||||
font: this.buildFontString(fontFamily, fontSize, this.textBold, this.textItalic),
|
||||
color: this.brushColor,
|
||||
fontFamily: fontFamily,
|
||||
baseFontSize: fontSize,
|
||||
blocks: blocks
|
||||
};
|
||||
|
||||
// Add to our list of text elements
|
||||
@@ -580,9 +841,7 @@ class PaintManager {
|
||||
this.draggingCanvasContext = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Draw text on canvas
|
||||
this.ctx.font = newText.font;
|
||||
this.ctx.fillStyle = newText.color;
|
||||
this.ctx.fillText(newText.text, newText.x, newText.y);
|
||||
this.renderTextElement(newText);
|
||||
|
||||
// Save to history after placing text
|
||||
this.saveToHistory();
|
||||
@@ -597,9 +856,7 @@ class PaintManager {
|
||||
redrawTextElements() {
|
||||
// Redraw all text elements after dithering
|
||||
this.textElements.forEach(item => {
|
||||
this.ctx.font = item.font;
|
||||
this.ctx.fillStyle = item.color;
|
||||
this.ctx.fillText(item.text, item.x, item.y);
|
||||
this.renderTextElement(item);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user