Files
LaoMaoBoss-ComfyUI-WBLESS/web/preview.js
T
2025-08-13 11:07:22 +08:00

694 lines
29 KiB
JavaScript

/**
* 预览窗口功能模块
* 包含文本预览画布的创建、绘制和相关工具函数
*/
import { app } from "../../scripts/app.js"
/**
* 创建预览画布元素
* @param {number} width - 画布宽度
* @param {number} height - 画布高度
* @returns {HTMLCanvasElement} 创建的画布元素
*/
export function createPreviewCanvas(width = 200, height = 150) {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
canvas.style.border = '1px solid #666';
canvas.style.borderRadius = '4px';
canvas.style.background = '#222';
canvas.style.display = 'block';
canvas.style.margin = '5px 0';
// 优化:设置willReadFrequently属性,提高频繁读取性能
const ctx = canvas.getContext('2d', { willReadFrequently: true });
return canvas;
}
/**
* 清除画布并绘制默认背景
* @param {HTMLCanvasElement} canvas - 目标画布
*/
export function clearPreviewCanvas(canvas) {
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#222';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制网格背景
ctx.strokeStyle = '#444';
ctx.lineWidth = 1;
// 水平线
for (let y = 0; y <= canvas.height; y += 20) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
// 垂直线
for (let x = 0; x <= canvas.width; x += 20) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
}
/**
* 测量所有文本块的实际尺寸
* @param {CanvasRenderingContext2D} ctx - 画布上下文
* @param {Array} textBlocks - 文本块数组
* @returns {Object} 包含所有文本块测量结果和边界框的对象
*/
function measureAllTextBlocks(ctx, textBlocks, overlaySettings = {}) {
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
const blockMeasurements = [];
let currentY = 20; // 初始Y位置
let currentX = 20; // 初始X位置
// 先分组文本块(按auto_newline分组)
const textGroups = [];
let currentGroup = [];
for (let i = 0; i < textBlocks.length; i++) {
const block = textBlocks[i];
if (!block || !block.text) continue;
// 如果当前块设置了auto_newline且不是第一个块,开始新组
if (block.auto_newline && currentGroup.length > 0) {
textGroups.push(currentGroup);
currentGroup = [];
}
currentGroup.push({block, index: i});
}
// 添加最后一组
if (currentGroup.length > 0) {
textGroups.push(currentGroup);
}
// 逐组测量文本
for (let groupIndex = 0; groupIndex < textGroups.length; groupIndex++) {
const group = textGroups[groupIndex];
let groupStartX = currentX;
let groupMaxHeight = 0;
let groupMaxFontSize = 0; // 记录当前组的最大字体大小,用于基线对齐
// 预览窗口特殊处理:先计算当前行的总宽度,用于内部对齐
const justify = overlaySettings.justify || "center";
let totalGroupWidth = 0;
// 第一遍:计算当前组的总宽度和最大字体大小
for (const {block} of group) {
const originalFontSize = block.font_size || 50;
const fontFamily = block.font_family || 'Arial';
const processedText = processTextCase(block.text || '', block.text_case);
// 临时设置字体来测量
const savedFont = ctx.font;
ctx.font = `${originalFontSize}px ${fontFamily}`;
const textMetrics = ctx.measureText(processedText);
ctx.font = savedFont;
let textWidth = textMetrics.width;
if (block.letter_spacing && block.letter_spacing !== 0 && processedText.length > 1) {
textWidth += (processedText.length - 1) * block.letter_spacing;
}
totalGroupWidth += textWidth + (block.horizontal_spacing || 0);
groupMaxFontSize = Math.max(groupMaxFontSize, originalFontSize); // 记录最大字体大小
}
// 第二遍:创建测量结果,使用统一的基线
for (let itemIndex = 0; itemIndex < group.length; itemIndex++) {
const {block, index} = group[itemIndex];
// 获取原始字体大小
const originalFontSize = block.font_size || 50;
const fontFamily = block.font_family || 'Arial';
// 应用文本样式进行测量
const processedText = processTextCase(block.text || '', block.text_case);
ctx.font = `${originalFontSize}px ${fontFamily}`;
const textMetrics = ctx.measureText(processedText);
// 计算字间距对宽度的影响
let textWidth = textMetrics.width;
if (block.letter_spacing && block.letter_spacing !== 0 && processedText.length > 1) {
textWidth += (processedText.length - 1) * block.letter_spacing;
}
const blockWidth = textWidth + (block.horizontal_spacing || 0);
const blockHeight = originalFontSize + (block.vertical_spacing || 0);
// 记录这个文本块的位置和尺寸
const measurement = {
x: currentX,
y: currentY,
width: blockWidth,
height: blockHeight,
originalFontSize,
text: processedText,
block: block,
groupWidth: totalGroupWidth, // 添加组宽度信息,用于后续对齐计算
groupIndex: groupIndex,
groupMaxFontSize: groupMaxFontSize // 添加组最大字体大小,用于基线对齐
};
blockMeasurements.push(measurement);
// 更新边界框
minX = Math.min(minX, currentX);
minY = Math.min(minY, currentY);
maxX = Math.max(maxX, currentX + blockWidth);
maxY = Math.max(maxY, currentY + blockHeight);
// 更新当前位置
currentX += blockWidth;
groupMaxHeight = Math.max(groupMaxHeight, blockHeight);
}
// 换行(应用行间距)
const lineSpacing = overlaySettings.line_spacing || 0;
currentY += groupMaxHeight + lineSpacing;
currentX = groupStartX;
}
return {
blocks: blockMeasurements,
bounds: {
minX: minX === Infinity ? 0 : minX,
minY: minY === Infinity ? 0 : minY,
maxX: maxX === -Infinity ? 0 : maxX,
maxY: maxY === -Infinity ? 0 : maxY,
width: (maxX === -Infinity || minX === Infinity) ? 0 : maxX - minX,
height: (maxY === -Infinity || minY === Infinity) ? 0 : maxY - minY
}
};
}
/**
* 计算全局缩放比例,确保所有文本都能适应画布
* @param {HTMLCanvasElement} canvas - 目标画布
* @param {Object} textMeasurements - 文本测量结果
* @returns {number} 全局缩放比例
*/
function calculateGlobalScale(canvas, textMeasurements) {
const { bounds } = textMeasurements;
// 如果没有文本,返回默认缩放
if (bounds.width === 0 || bounds.height === 0) {
return 0.5;
}
// 计算画布可用空间(留出边距)
const margin = 20;
const availableWidth = canvas.width - margin * 2;
const availableHeight = canvas.height - margin * 2;
// 计算宽度和高度的缩放比例
const scaleX = availableWidth / bounds.width;
const scaleY = availableHeight / bounds.height;
// 使用较小的缩放比例,确保文本完全适应
const scale = Math.min(scaleX, scaleY);
// 限制缩放范围,避免过小或过大
return Math.max(0.1, Math.min(2.0, scale));
}
/**
* 处理文本大小写转换
* @param {string} text - 原始文本
* @param {string} textCase - 大小写类型
* @returns {string} 处理后的文本
*/
function processTextCase(text, textCase) {
if (!text || !textCase || textCase === 'normal') return text;
switch (textCase) {
case 'uppercase': return text.toUpperCase();
case 'lowercase': return text.toLowerCase();
case 'capitalize': return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
case 'title': return text.replace(/\w\S*/g, (txt) =>
txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase());
default: return text;
}
}
/**
* 使用全局缩放绘制所有文本块
* @param {CanvasRenderingContext2D} ctx - 画布上下文
* @param {Object} textMeasurements - 文本测量结果
* @param {number} globalScale - 全局缩放比例
* @param {HTMLCanvasElement} canvas - 画布
* @param {boolean} debugMode - 调试模式
*/
function renderScaledTextBlocks(ctx, textMeasurements, globalScale, canvas, debugMode, overlaySettings = {}) {
const { blocks, bounds } = textMeasurements;
// 预览窗口优化:让文本充满整个预览窗口,justify控制行内对齐
const scaledWidth = bounds.width * globalScale;
const scaledHeight = bounds.height * globalScale;
// 预览窗口中,我们总是让文本组合居中显示,充满预览窗口
const baseOffsetX = (canvas.width - scaledWidth) / 2 - bounds.minX * globalScale;
const baseOffsetY = (canvas.height - scaledHeight) / 2 - bounds.minY * globalScale;
// 获取justify设置,用于控制每行内部的文字对齐
const justify = overlaySettings.justify || "center";
// 按组分组文本块,用于行内对齐处理
const blocksByGroup = {};
for (const measurement of blocks) {
const groupIndex = measurement.groupIndex || 0;
if (!blocksByGroup[groupIndex]) {
blocksByGroup[groupIndex] = [];
}
blocksByGroup[groupIndex].push(measurement);
}
// 绘制每个文本块
for (const measurement of blocks) {
const { x, y, originalFontSize, text, block, groupWidth, groupIndex, groupMaxFontSize } = measurement;
// 计算当前组的行内对齐偏移
let lineAlignOffset = 0;
if (groupWidth && justify !== "left") {
const currentGroup = blocksByGroup[groupIndex || 0];
const scaledGroupWidth = groupWidth * globalScale;
const availableWidth = canvas.width - 40; // 预留左右各20px边距
if (scaledGroupWidth < availableWidth) {
if (justify === "center") {
// 当前行在预览窗口中居中显示
lineAlignOffset = (availableWidth - scaledGroupWidth) / 2;
} else if (justify === "right") {
// 当前行在预览窗口中右对齐
lineAlignOffset = availableWidth - scaledGroupWidth;
}
}
}
// 应用全局缩放和行内对齐偏移
const scaledX = x * globalScale + baseOffsetX + lineAlignOffset;
const scaledY = y * globalScale + baseOffsetY;
const scaledFontSize = Math.max(6, originalFontSize * globalScale);
// 设置文本样式
const fontFamily = block.font_family || 'Arial';
let fontWeight = 'normal';
// 处理字重(不包括bold参数,因为bold使用多次绘制模拟)
if (block.font_weight && block.font_weight !== 'Regular') {
const weightMap = {
'Thin': '100', 'ExtraLight': '200', 'UltraLight': '200', 'Light': '300',
'Regular': '400', 'Normal': '400', 'Medium': '500', 'SemiBold': '600',
'DemiBold': '600', 'Bold': '700', 'ExtraBold': '800', 'UltraBold': '800',
'Black': '900', 'Heavy': '900'
};
fontWeight = weightMap[block.font_weight] || '400';
}
// 注意:不再使用CSS font-weight处理bold参数,改用多次绘制模拟
// 设置字体样式
let fontStyle = 'normal';
if (block.italic) {
fontStyle = 'italic';
}
ctx.font = `${fontStyle} ${fontWeight} ${scaledFontSize}px ${fontFamily}`;
// 设置颜色和透明度
const color = getColorValue(block.font_color, block.font_color_hex);
const opacity = block.opacity !== undefined ? block.opacity : 1.0;
ctx.fillStyle = color;
ctx.globalAlpha = opacity;
if (debugMode) {
console.log(`[WBLESS] Block color: font_color="${block.font_color}", font_color_hex="${block.font_color_hex}" -> "${color}"`);
}
// 计算垂直对齐偏移(上标/下标)
let verticalOffset = 0;
let verticalScale = 1;
if (block.vertical_align === 'superscript') {
verticalOffset = -scaledFontSize * 0.3; // 上移30%
verticalScale = 0.7; // 缩小到70%
} else if (block.vertical_align === 'subscript') {
verticalOffset = scaledFontSize * 0.2; // 下移20%
verticalScale = 0.7; // 缩小到70%
}
// 应用垂直对齐的字体大小
const adjustedFontSize = scaledFontSize * verticalScale;
if (verticalScale !== 1) {
ctx.font = `${fontStyle} ${fontWeight} ${adjustedFontSize}px ${fontFamily}`;
}
// 计算当前实际使用的字体大小(用于bold偏移计算)
const currentFontSize = verticalScale !== 1 ? adjustedFontSize : scaledFontSize;
// 计算基线对齐的Y坐标
// 使用组内最大字体大小作为基线参考,确保同一行中所有文本共享相同的基线
const scaledGroupMaxFontSize = (groupMaxFontSize || originalFontSize) * globalScale;
const baselineY = scaledY + scaledGroupMaxFontSize * 0.8; // 基于组内最大字体的基线
const finalY = baselineY + verticalOffset;
// 应用旋转(如果有)
const rotationAngle = (block.rotation_angle || 0) * Math.PI / 180; // 转换为弧度
if (rotationAngle !== 0) {
ctx.save();
// 根据rotation_options确定旋转中心
const rotationOptions = block.rotation_options || "text center";
let rotationCenterX = scaledX;
let rotationCenterY = finalY;
if (rotationOptions === "text center") {
// 文字中心旋转:计算文本中心点
let textWidth = ctx.measureText(text).width;
if (block.letter_spacing && block.letter_spacing !== 0 && text.length > 1) {
textWidth += (text.length - 1) * block.letter_spacing * globalScale;
}
rotationCenterX = scaledX + textWidth / 2;
rotationCenterY = finalY - adjustedFontSize / 2; // 文本垂直中心
} else if (rotationOptions === "image center") {
// 图像中心旋转:使用画布中心
rotationCenterX = canvas.width / 2;
rotationCenterY = canvas.height / 2;
}
// 如果是其他值或默认,使用文本左下角(当前位置)
ctx.translate(rotationCenterX, rotationCenterY);
ctx.rotate(rotationAngle);
// 计算文本相对于旋转中心的偏移
const textOffsetX = scaledX - rotationCenterX;
const textOffsetY = finalY - rotationCenterY;
// 绘制文本(支持字间距和加粗模拟)
if (block.bold) {
// 在预览环境中,应该基于原始字体大小计算偏移,然后缩放到预览大小
const originalFontSizeForBold = verticalScale !== 1 ? (originalFontSize * verticalScale) : originalFontSize;
const baseBoldOffset = Math.max(1, Math.floor(originalFontSizeForBold / 30)); // 基于原始字体大小
const actualBoldOffset = Math.max(0.5, baseBoldOffset * globalScale); // 缩放到预览大小,允许小于1的偏移
// 调试信息
if (window.WBLESS_DEBUG) {
console.log(`[WBLESS] Bold calculation (rotated): originalFontSize=${originalFontSize}, scaledFontSize=${scaledFontSize}, globalScale=${globalScale.toFixed(3)}, baseBoldOffset=${baseBoldOffset}, actualBoldOffset=${actualBoldOffset.toFixed(2)}`);
}
// 在周围位置绘制文本来模拟加粗(密集步长,避免分离同时控制性能)
const stepSize = Math.max(0.25, Math.min(0.5, actualBoldOffset / 2)); // 动态步长,但不小于0.25px
const maxSteps = Math.ceil(actualBoldOffset / stepSize); // 计算最大步数
for (let i = -maxSteps; i <= maxSteps; i++) {
for (let j = -maxSteps; j <= maxSteps; j++) {
const dx = i * stepSize;
const dy = j * stepSize;
if (Math.abs(dx) > 0.1 || Math.abs(dy) > 0.1) { // 不在原位置重复绘制
if (block.letter_spacing && block.letter_spacing !== 0) {
drawTextWithLetterSpacing(ctx, text, textOffsetX + dx, textOffsetY + dy, block.letter_spacing * globalScale);
} else {
ctx.fillText(text, textOffsetX + dx, textOffsetY + dy);
}
}
}
}
}
// 绘制主文本
if (block.letter_spacing && block.letter_spacing !== 0) {
drawTextWithLetterSpacing(ctx, text, textOffsetX, textOffsetY, block.letter_spacing * globalScale);
} else {
ctx.fillText(text, textOffsetX, textOffsetY);
}
ctx.restore();
} else {
// 绘制文本(支持字间距和加粗模拟)
if (block.bold) {
// 在预览环境中,应该基于原始字体大小计算偏移,然后缩放到预览大小
// 这样可以保持与实际输出的比例一致
const originalFontSizeForBold = verticalScale !== 1 ? (originalFontSize * verticalScale) : originalFontSize;
const baseBoldOffset = Math.max(1, Math.floor(originalFontSizeForBold / 30)); // 基于原始字体大小
const actualBoldOffset = Math.max(0.5, baseBoldOffset * globalScale); // 缩放到预览大小,允许小于1的偏移
// 调试信息
if (window.WBLESS_DEBUG) {
console.log(`[WBLESS] Bold calculation: originalFontSize=${originalFontSize}, scaledFontSize=${scaledFontSize}, globalScale=${globalScale.toFixed(3)}, baseBoldOffset=${baseBoldOffset}, actualBoldOffset=${actualBoldOffset.toFixed(2)}`);
}
// 在周围位置绘制文本来模拟加粗(密集步长,避免分离同时控制性能)
const stepSize = Math.max(0.25, Math.min(0.5, actualBoldOffset / 2)); // 动态步长,但不小于0.25px
const maxSteps = Math.ceil(actualBoldOffset / stepSize); // 计算最大步数
for (let i = -maxSteps; i <= maxSteps; i++) {
for (let j = -maxSteps; j <= maxSteps; j++) {
const dx = i * stepSize;
const dy = j * stepSize;
if (Math.abs(dx) > 0.1 || Math.abs(dy) > 0.1) { // 不在原位置重复绘制
if (block.letter_spacing && block.letter_spacing !== 0) {
drawTextWithLetterSpacing(ctx, text, scaledX + dx, finalY + dy, block.letter_spacing * globalScale);
} else {
ctx.fillText(text, scaledX + dx, finalY + dy);
}
}
}
}
}
// 绘制主文本
if (block.letter_spacing && block.letter_spacing !== 0) {
drawTextWithLetterSpacing(ctx, text, scaledX, finalY, block.letter_spacing * globalScale);
} else {
ctx.fillText(text, scaledX, finalY);
}
}
// 绘制文本装饰(下划线、删除线)- 只在没有旋转时绘制,避免复杂的旋转计算
if ((block.underline || block.strikethrough) && rotationAngle === 0) {
ctx.strokeStyle = color;
ctx.lineWidth = Math.max(1, adjustedFontSize * 0.05);
// 计算文本宽度(考虑字间距)
let textWidth = ctx.measureText(text).width;
if (block.letter_spacing && block.letter_spacing !== 0 && text.length > 1) {
textWidth += (text.length - 1) * block.letter_spacing * globalScale;
}
if (block.underline) {
const underlineY = finalY + adjustedFontSize * 0.1; // 相对于文本基线
ctx.beginPath();
ctx.moveTo(scaledX, underlineY);
ctx.lineTo(scaledX + textWidth, underlineY);
ctx.stroke();
}
if (block.strikethrough) {
const strikeY = finalY - adjustedFontSize * 0.3; // 相对于文本基线
ctx.beginPath();
ctx.moveTo(scaledX, strikeY);
ctx.lineTo(scaledX + textWidth, strikeY);
ctx.stroke();
}
}
// 重置透明度
ctx.globalAlpha = 1.0;
if (debugMode) {
console.log(`[WBLESS] Rendered text "${text}" at (${scaledX.toFixed(1)}, ${scaledY.toFixed(1)}) with font size ${scaledFontSize.toFixed(1)}px`);
}
}
}
/**
* 绘制带字间距的文本
* @param {CanvasRenderingContext2D} ctx - 画布上下文
* @param {string} text - 要绘制的文本
* @param {number} x - 起始X坐标
* @param {number} y - 起始Y坐标
* @param {number} letterSpacing - 字间距(像素)
*/
function drawTextWithLetterSpacing(ctx, text, x, y, letterSpacing) {
let currentX = x;
for (let i = 0; i < text.length; i++) {
const char = text[i];
ctx.fillText(char, currentX, y);
const charWidth = ctx.measureText(char).width;
currentX += charWidth + letterSpacing;
}
}
/**
* 获取颜色值
* @param {string} colorName - 颜色名称
* @param {string} hexColor - 十六进制颜色
* @returns {string} CSS颜色值
*/
function getColorValue(colorName, hexColor) {
// 颜色映射表(与后端COLOR_MAPPING保持一致)
const COLOR_MAPPING = {
'white': '#FFFFFF', 'black': '#000000', 'red': '#FF0000', 'green': '#00FF00',
'blue': '#0000FF', 'yellow': '#FFFF00', 'cyan': '#00FFFF', 'magenta': '#FF00FF',
'orange': '#FFA500', 'purple': '#800080', 'pink': '#FFC0CB', 'brown': '#A0550F',
'gray': '#808080', 'lightgray': '#D3D3D3', 'darkgray': '#666666',
'olive': '#808000', 'lime': '#008000', 'teal': '#008080', 'navy': '#000080',
'maroon': '#800000', 'fuchsia': '#FF0080', 'aqua': '#00FF80', 'silver': '#C0C0C0',
'gold': '#FFD700', 'turquoise': '#40E0D0', 'lavender': '#E6E6FA', 'violet': '#EE82EE',
'coral': '#FF7F50', 'indigo': '#4B0082'
};
// 处理可能的列表类型输入
if (Array.isArray(colorName)) {
colorName = colorName.length > 0 ? colorName[0] : 'white';
}
if (Array.isArray(hexColor)) {
hexColor = hexColor.length > 0 ? hexColor[0] : '#FFFFFF';
}
// 如果选择了"custom",优先使用十六进制颜色
if (colorName === 'custom' && hexColor && hexColor.startsWith('#')) {
return hexColor;
}
// 使用颜色名称映射
if (colorName && COLOR_MAPPING[colorName.toLowerCase()]) {
return COLOR_MAPPING[colorName.toLowerCase()];
}
// 如果没有找到颜色名称,但有有效的十六进制颜色,则使用它
if (hexColor && hexColor.startsWith('#')) {
return hexColor;
}
// 默认白色
return '#FFFFFF';
}
/**
* 在画布上绘制文本预览
* @param {HTMLCanvasElement} canvas - 目标画布
* @param {Array} textBlocks - 文本块数组
* @param {Object} overlaySettings - Overlay Text节点的设置
*/
export function drawTextPreview(canvas, textBlocks, overlaySettings) {
if (!canvas || !textBlocks || textBlocks.length === 0) {
clearPreviewCanvas(canvas);
return;
}
// 启用调试模式的全局开关
const debugMode = window.WBLESS_DEBUG || false;
if (debugMode) {
console.log('[WBLESS] Drawing text preview with:', {
textBlockCount: textBlocks.length,
textBlocks: textBlocks,
overlaySettings: overlaySettings,
justifyMode: overlaySettings.justify || 'center'
});
}
const ctx = canvas.getContext('2d');
clearPreviewCanvas(canvas);
// 第一步:测量所有文本的实际尺寸
const textMeasurements = measureAllTextBlocks(ctx, textBlocks, overlaySettings);
// 第二步:计算全局缩放比例,确保所有文本都能适应窗口
const globalScale = calculateGlobalScale(canvas, textMeasurements);
if (debugMode) {
console.log('[WBLESS] Global scale calculated:', {
canvasSize: `${canvas.width}x${canvas.height}`,
textBounds: textMeasurements.bounds,
globalScale: globalScale.toFixed(3),
overlayRotation: `${overlaySettings.rotation_angle || 0}°`
});
}
try {
// 应用Overlay Text的旋转角度(整个画布旋转)
const overlayRotationAngle = (overlaySettings.rotation_angle || 0) * Math.PI / 180;
if (overlayRotationAngle !== 0) {
ctx.save();
// 以画布中心为旋转点
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(overlayRotationAngle);
ctx.translate(-canvas.width / 2, -canvas.height / 2);
}
// 第三步:使用全局缩放绘制所有文本
renderScaledTextBlocks(ctx, textMeasurements, globalScale, canvas, debugMode, overlaySettings);
// 恢复旋转变换
if (overlayRotationAngle !== 0) {
ctx.restore();
}
} catch (error) {
console.error('[WBLESS] Error drawing text preview:', error);
// 绘制错误信息
ctx.fillStyle = '#ff6666';
ctx.font = '14px Arial';
ctx.textAlign = 'center';
ctx.fillText('Preview Error', canvas.width / 2, canvas.height / 2);
ctx.fillStyle = '#999';
ctx.font = '10px Arial';
ctx.fillText('Check console for details', canvas.width / 2, canvas.height / 2 + 20);
}
}
/**
* 防抖函数
* @param {Function} func - 要防抖的函数
* @param {number} wait - 等待时间(毫秒)
* @returns {Function} 防抖后的函数
*/
export function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
/**
* 优化的防抖函数,针对预览更新进行性能优化
* 使用requestAnimationFrame确保在浏览器的绘制周期内执行
* @param {Function} func - 要防抖的函数
* @param {number} wait - 等待时间(毫秒)
* @returns {Function} 防抖后的函数
*/
export function debounceRAF(func, wait = 16) {
let timeout;
let rafId;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
if (rafId) {
cancelAnimationFrame(rafId);
}
rafId = requestAnimationFrame(() => {
func(...args);
rafId = null;
});
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}