修改样式,增强文字功能
Vercel Deploy / deploy (push) Successful in 1m13s

This commit is contained in:
2026-04-25 14:22:18 +08:00
Unverified
parent 0773b20ed7
commit 3d463c91d8
3 changed files with 552 additions and 99 deletions
+286 -29
View File
@@ -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);
});
}