画布液化功能优化(图片清晰度、添加指示器)

This commit is contained in:
李志鹏
2025-09-26 15:12:57 +08:00
parent c3d7cbcc83
commit 844d5c0972
7 changed files with 1511 additions and 1339 deletions

View File

@@ -362,6 +362,11 @@ watch(
setBrushSize(newSize);
}
);
watch(()=>isVisible.value, (newVisible) => {
if (newVisible) {
setBrushSize(brushSize.value);
}
})
// 监听brushOpacity的变化更新到BrushStore
watch(

View File

@@ -258,6 +258,23 @@ const setClosePanel = ()=>{
closePanel.value = !closePanel.value
}
// 工具管理器和画布管理器
const toolManager = inject("toolManager");
const canvasManager = inject("canvasManager");
watch(size, (newSize, oldSize) => {
setBrushIndicatorSize(newSize)
})
// 设置笔刷指示器大小
function setBrushIndicatorSize(size) {
// 如果工具管理器存在,立即应用此更改
console.log(`=========== ${size}`,toolManager);
if (toolManager) {
toolManager.updateBrushIndicatorSize(size);
}
}
// 监听当前工具变化 - 参考 SelectionPanel 的实现方式
watch(
() => props.activeTool,
@@ -273,6 +290,7 @@ watch(
// 检查是否有可液化的对象
checkAndShowPanel();
}
setBrushIndicatorSize(size.value)
} else {
visible.value = false; // 切换到其他工具时隐藏面板
// 切换到其他工具,隐藏液化面板
@@ -1641,15 +1659,17 @@ function stopPressTimer() {
transform: rotate(90deg);
}
}
}
}
> .btn{
width: 100%;
text-align: center;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
> i{
font-size: 1.4rem;
display: block;
transform: rotate(270deg);
}
}

View File

@@ -1,4 +1,5 @@
import { fabric } from "fabric-with-all";
import { OperationType } from "../utils/layerHelper";
/**
* 笔刷指示器
@@ -94,6 +95,7 @@ export class BrushIndicator {
* @private
*/
_syncCanvasProperties() {
console.log("==========","笔刷同步大小")
if (!this.staticCanvas || !this.canvas) return;
// 检查是否为笔刷或橡皮擦模式,非相关模式直接返回
@@ -103,10 +105,8 @@ export class BrushIndicator {
this.canvas.isDrawingMode &&
this.canvas.freeDrawingBrush &&
this.canvas.freeDrawingBrush.type === "eraser";
if (!isBrushMode && !isEraserMode) {
return;
}
const isLiquifyMode = this.canvas.toolId === OperationType.LIQUIFY;// 检查是否在液化模式
if ([isBrushMode,isEraserMode,isLiquifyMode].every(v => !v)) return;
let hasChanges = false;
@@ -471,8 +471,12 @@ export class BrushIndicator {
* @returns {Boolean} 是否显示
*/
_shouldShowIndicator() {
// 检查画布是否在绘图模式
if (!this.canvas.isDrawingMode) return false;
const isDrawingMode = this.canvas.isDrawingMode;// 检查画布是否在绘图模式
const isLiquifyMode = this.canvas.toolId === OperationType.LIQUIFY;// 检查是否在液化模式
// console.log(`笔刷指示器\n绘图模式:${isDrawingMode}\n液化模式:${isLiquifyMode}`)
// 检查画布是否在绘图模式OR液化模式
if ([isDrawingMode, isLiquifyMode].every(v => !v)) return false;
// 检查是否有笔刷
if (!this.canvas.freeDrawingBrush) return false;

View File

@@ -373,6 +373,8 @@ export class ToolManager {
// 设置工具特定的状态
const tool = this.tools[toolId];
if (tool && typeof tool.setup === "function") {
console.log(`画布切换工具:${tool.name}(${toolId})`)
this.canvas.toolId = toolId;
tool.setup();
}
@@ -450,6 +452,7 @@ export class ToolManager {
if (!this.canvas) return;
this.canvas.isDrawingMode = false;
this.canvas.selection = true;
}
/**
@@ -750,6 +753,7 @@ export class ToolManager {
detail: panelDetail,
})
);
this._enableBrushIndicator();
}
/**
@@ -1465,6 +1469,7 @@ export class ToolManager {
OperationType.ERASER,
OperationType.RED_BRUSH,
OperationType.GREEN_BRUSH,
OperationType.LIQUIFY,
];
return brushTools.includes(currentTool);

View File

@@ -22,13 +22,13 @@ export class EnhancedLiquifyManager {
// 是否强制使用WebGL模式
forceWebGL: options.forceWebGL || false,
// 网格大小
gridSize: options.gridSize || 15,
gridSize: options.gridSize || 8,
// 最大变形强度
maxStrength: options.maxStrength || 100,
maxStrength: options.maxStrength || 200,
// 平滑迭代次数
smoothingIterations: options.smoothingIterations || 2,
smoothingIterations: options.smoothingIterations || 1,
// 网格弹性因子
relaxFactor: options.relaxFactor || 0.25,
relaxFactor: options.relaxFactor || 0.05,
// WebGL网格精度
meshResolution: options.meshResolution || 64,
};

View File

@@ -5,17 +5,20 @@
export class LiquifyCPUManager {
constructor(options = {}) {
this.config = {
gridSize: options.gridSize || 16, // 稍微增大网格提高性能
maxStrength: options.maxStrength || 200, // 适度降低最大强度
smoothingIterations: options.smoothingIterations || 1, // 增加平滑处理
relaxFactor: options.relaxFactor || 0.1, // 适度松弛
gridSize: 8, // 稍微增大网格提高性能
maxStrength: 200, // 适度降低最大强度
smoothingIterations: 1, // 增加平滑处理
relaxFactor: 0.05, // 适度松弛
sharpenAmount: 0.3, // 添加锐化强度参数
...options,
};
console.log("CPU版本的液化管理器config",this.config);
this.params = {
size: 80, // 增大默认尺寸
pressure: 0.8, // 增大默认压力
size: 60, // 增大默认尺寸
pressure: 0.6, // 增大默认压力
distortion: 0,
power: 0.8, // 增大默认动力
power: 0.7, // 增大默认动力
};
this.modes = {
@@ -132,6 +135,10 @@ export class LiquifyCPUManager {
}
setParam(param, value) {
if (param === 'sharpness') {
this.config.sharpenAmount = Math.max(0, Math.min(1, value));
return true;
}
if (param in this.params) {
this.params[param] = value;
return true;
@@ -140,15 +147,19 @@ export class LiquifyCPUManager {
}
getParams() {
return { ...this.params };
return { ...this.params, sharpness: this.config.sharpenAmount, };
}
// 添加清晰度控制方法
setSharpness(amount) {
this.config.sharpenAmount = Math.max(0, Math.min(1, amount));
return this;
}
resetParams() {
this.params = {
size: 80, // 增大默认尺寸
pressure: 0.8, // 增大默认压力
size: 60, // 增大默认尺寸
pressure: 0.6, // 增大默认压力
distortion: 0,
power: 0.8, // 增大默认动力
power: 0.7, // 增大默认动力
};
}
@@ -645,53 +656,69 @@ export class LiquifyCPUManager {
* @returns {Array|null} RGBA颜色值数组或null
*/
_bilinearSample(data, width, height, x, y) {
if (x < 0 || x >= width - 1 || y < 0 || y >= height - 1) {
return null;
return this._bicubicInterpolate(data, width, height, x, y);
}
/**
* 双三次插值实现 - 确保正确处理Alpha通道
* @param {Uint8ClampedArray} data 图像数据
* @param {number} width 图像宽度
* @param {number} height 图像高度
* @param {number} x X坐标
* @param {number} y Y坐标
* @returns {Array|null} RGBA颜色值数组或null
*/
_bicubicInterpolate(data, width, height, x, y) {
// 获取周围16个像素点
const x1 = Math.floor(x) - 1;
const y1 = Math.floor(y) - 1;
// 创建16个采样点的颜色数组
const pixels = [];
for (let ky = 0; ky < 4; ky++) {
for (let kx = 0; kx < 4; kx++) {
const px = Math.max(0, Math.min(width - 1, x1 + kx));
const py = Math.max(0, Math.min(height - 1, y1 + ky));
const idx = (py * width + px) * 4;
pixels[ky * 4 + kx] = [data[idx], data[idx + 1], data[idx + 2], data[idx + 3]];
}
}
const x1 = Math.floor(x);
const y1 = Math.floor(y);
const x2 = x1 + 1;
const y2 = y1 + 1;
// 计算小数部分
const fx = x - (x1 + 1);
const fy = y - (y1 + 1);
const fx = x - x1;
const fy = y - y1;
// 计算行插值
const row0 = this._cubicInterpolateRow(pixels[0], pixels[1], pixels[2], pixels[3], fx);
const row1 = this._cubicInterpolateRow(pixels[4], pixels[5], pixels[6], pixels[7], fx);
const row2 = this._cubicInterpolateRow(pixels[8], pixels[9], pixels[10], pixels[11], fx);
const row3 = this._cubicInterpolateRow(pixels[12], pixels[13], pixels[14], pixels[15], fx);
const getPixel = (px, py) => {
const idx = (py * width + px) * 4;
return [data[idx], data[idx + 1], data[idx + 2], data[idx + 3]];
};
// 计算最终结果
return this._cubicInterpolateRow(row0, row1, row2, row3, fy);
}
// 三次插值辅助方法 - 单行插值
_cubicInterpolateRow(p0, p1, p2, p3, t) {
// 使用三次多项式插值公式
const a = [0, 0, 0, 0];
const b = [0, 0, 0, 0];
const c = [0, 0, 0, 0];
const p1 = getPixel(x1, y1);
const p2 = getPixel(x2, y1);
const p3 = getPixel(x1, y2);
const p4 = getPixel(x2, y2);
// 为每个通道计算插值系数
for (let i = 0; i < 4; i++) {
a[i] = -0.5 * p0[i] + 1.5 * p1[i] - 1.5 * p2[i] + 0.5 * p3[i];
b[i] = p0[i] - 2.5 * p1[i] + 2 * p2[i] - 0.5 * p3[i];
c[i] = -0.5 * p0[i] + 0.5 * p2[i];
}
// 应用三次多项式
const t2 = t * t;
const t3 = t * t2;
return [
Math.round(
(1 - fx) * (1 - fy) * p1[0] +
fx * (1 - fy) * p2[0] +
(1 - fx) * fy * p3[0] +
fx * fy * p4[0]
),
Math.round(
(1 - fx) * (1 - fy) * p1[1] +
fx * (1 - fy) * p2[1] +
(1 - fx) * fy * p3[1] +
fx * fy * p4[1]
),
Math.round(
(1 - fx) * (1 - fy) * p1[2] +
fx * (1 - fy) * p2[2] +
(1 - fx) * fy * p3[2] +
fx * fy * p4[2]
),
Math.round(
(1 - fx) * (1 - fy) * p1[3] +
fx * (1 - fy) * p2[3] +
(1 - fx) * fy * p3[3] +
fx * fy * p4[3]
),
Math.round(a[0] * t3 + b[0] * t2 + c[0] * t + p1[0]),
Math.round(a[1] * t3 + b[1] * t2 + c[1] * t + p1[1]),
Math.round(a[2] * t3 + b[2] * t2 + c[2] * t + p1[2]),
Math.round(a[3] * t3 + b[3] * t2 + c[3] * t + p1[3]) // 确保Alpha通道也被正确插值
];
}
@@ -928,31 +955,40 @@ export class LiquifyCPUManager {
const srcData = this.originalImageData.data;
const dstData = result.data;
// 性能优化:使用步长采样减少计算量
const step = width > 1000 || height > 1000 ? 2 : 1;
for (let y = 0; y < height; y += step) {
for (let x = 0; x < width; x += step) {
// 移除步长采样始终使用1:1采样
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const srcPos = this._mapPointBack(x, y);
const dstIdx = (y * width + x) * 4;
if (srcPos.x >= 0 && srcPos.x < width && srcPos.y >= 0 && srcPos.y < height) {
const color = this._bilinearInterpolate(srcData, width, height, srcPos.x, srcPos.y);
// 如果使用步长采样,需要填充相邻像素
for (let dy = 0; dy < step && y + dy < height; dy++) {
for (let dx = 0; dx < step && x + dx < width; dx++) {
const dstIdx = ((y + dy) * width + (x + dx)) * 4;
// 使用双三次插值获取颜色
const color = this._bicubicInterpolate(srcData, width, height, srcPos.x, srcPos.y);
dstData[dstIdx] = color[0];
dstData[dstIdx + 1] = color[1];
dstData[dstIdx + 2] = color[2];
dstData[dstIdx + 3] = color[3];
}
}
dstData[dstIdx + 3] = color[3]; // 确保Alpha通道值被正确设置
} else {
// 对于边界外的点使用最近的有效像素或保持原Alpha通道
// 这里我们确保Alpha通道不为0防止出现透明区域
const nearestX = Math.max(0, Math.min(width - 1, Math.round(srcPos.x)));
const nearestY = Math.max(0, Math.min(height - 1, Math.round(srcPos.y)));
const nearestIdx = (nearestY * width + nearestX) * 4;
// 复制最近像素的颜色但保持Alpha通道为不透明
dstData[dstIdx] = srcData[nearestIdx];
dstData[dstIdx + 1] = srcData[nearestIdx + 1];
dstData[dstIdx + 2] = srcData[nearestIdx + 2];
dstData[dstIdx + 3] = 255; // 强制设置为完全不透明
}
}
}
this.currentImageData = result;
// 添加锐化处理
if (this.config.sharpenAmount > 0) {
this.currentImageData = this._sharpenImage(this.currentImageData, this.config.sharpenAmount);
}
return result;
}
@@ -1066,21 +1102,122 @@ export class LiquifyCPUManager {
const originalX = x1 * gridSize + fx * gridSize;
const originalY = y1 * gridSize + fy * gridSize;
// 计算偏移量并应用反向映射
const offsetX = deformedX - originalX;
const offsetY = deformedY - originalY;
// 检查是否接近边缘,如果是则减少偏移量
const isNearEdge = this._isNearEdge(originalX, originalY);
const edgeProtectionFactor = isNearEdge ? 0.2 : 1.0; // 边缘区域减少变形量
// 限制偏移量,防止过度扭曲
const maxOffset = gridSize * 0.5;
const limitedOffsetX = Math.max(-maxOffset, Math.min(maxOffset, offsetX));
const limitedOffsetY = Math.max(-maxOffset, Math.min(maxOffset, offsetY));
// 计算偏移量并应用反向映射
const offsetX = (deformedX - originalX) * edgeProtectionFactor;
const offsetY = (deformedY - originalY) * edgeProtectionFactor;
return {
x: Math.max(0, Math.min(this.mesh.width - 1, x - limitedOffsetX)),
y: Math.max(0, Math.min(this.mesh.height - 1, y - limitedOffsetY)),
x: Math.max(0, Math.min(this.mesh.width - 1, x - offsetX)),
y: Math.max(0, Math.min(this.mesh.height - 1, y - offsetY)),
};
}
// 边缘检测辅助方法
_isNearEdge(x, y, threshold = 10) {
if (!this.originalImageData) return false;
const data = this.originalImageData.data;
const width = this.originalImageData.width;
const height = this.originalImageData.height;
// 检查像素是否在边缘
if (x <= 0 || x >= width - 1 || y <= 0 || y >= height - 1) return true;
// 简单的Sobel边缘检测
const getPixelBrightness = (px, py) => {
const idx = (py * width + px) * 4;
return (data[idx] + data[idx + 1] + data[idx + 2]) / 3;
};
const kernelX = [
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]
];
const kernelY = [
[-1, -2, -1],
[0, 0, 0],
[1, 2, 1]
];
let gradientX = 0;
let gradientY = 0;
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const px = Math.min(Math.max(0, x + kx), width - 1);
const py = Math.min(Math.max(0, y + ky), height - 1);
const brightness = getPixelBrightness(px, py);
gradientX += brightness * kernelX[ky + 1][kx + 1];
gradientY += brightness * kernelY[ky + 1][kx + 1];
}
}
const gradientMagnitude = Math.sqrt(gradientX * gradientX + gradientY * gradientY);
return gradientMagnitude > threshold;
}
// 图像锐化方法
_sharpenImage(imageData, amount = 0.5) {
if (!imageData) return imageData;
const data = new Uint8ClampedArray(imageData.data);
const width = imageData.width;
const height = imageData.height;
const result = new ImageData(width, height);
const dstData = result.data;
// 锐化核 - 中心为5周围为-1
const kernel = [
[0, -1, 0],
[-1, 5, -1],
[0, -1, 0]
];
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
// 边缘像素不处理
if (x === 0 || x === width - 1 || y === 0 || y === height - 1) {
const idx = (y * width + x) * 4;
for (let c = 0; c < 4; c++) {
dstData[idx + c] = data[idx + c];
}
continue;
}
const sharpened = [0, 0, 0, 0];
// 应用锐化核
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const px = x + kx;
const py = y + ky;
const idx = (py * width + px) * 4;
const weight = kernel[ky + 1][kx + 1];
for (let c = 0; c < 3; c++) { // 只锐化RGB通道
sharpened[c] += data[idx + c] * weight;
}
sharpened[3] = data[idx + 3]; // 保持Alpha通道不变
}
}
// 应用锐化强度并裁剪值范围
const idx = (y * width + x) * 4;
for (let c = 0; c < 3; c++) {
const original = data[idx + c];
const diff = sharpened[c] - original;
dstData[idx + c] = Math.max(0, Math.min(255, original + diff * amount));
}
dstData[idx + 3] = sharpened[3];
}
}
return result;
}
_bilinearInterpolate(data, width, height, x, y) {
const x1 = Math.floor(x);
const y1 = Math.floor(y);
@@ -1253,8 +1390,9 @@ export class LiquifyCPUManager {
// 应用变形
this._applyDeformation(x, y, radius, finalStrength, mode, this.params.distortion);
// 平滑处理
if (this.config.smoothingIterations > 0) {
// 有条件地应用平滑处理,仅在特定模式下应用
const smoothingModes = [this.modes.CRYSTAL, this.modes.EDGE];
if (smoothingModes.includes(mode) && this.config.smoothingIterations > 0) {
this._smoothMesh();
}

View File

@@ -31,10 +31,10 @@ export class LiquifyManager {
// 创建增强版液化管理器实例
this.enhancedManager = new EnhancedLiquifyManager({
// 配置选项
gridSize: options.gridSize || 15,
maxStrength: options.maxStrength || 100,
smoothingIterations: options.smoothingIterations || 2,
relaxFactor: options.relaxFactor || 0.25,
gridSize: options.gridSize || 8,
maxStrength: options.maxStrength || 200,
smoothingIterations: options.smoothingIterations || 1,
relaxFactor: options.relaxFactor || 0.05,
meshResolution: options.meshResolution || 64,
// 根据环境选择合适的渲染模式
forceCPU: true, // 默认不强制使用CPU