修复滤镜两处问题:type 匹配不再靠 id 拼写、统一边缘格行为

- filters/index.ts 从 build() 实例读出真实 fabric type,导出 FILTER_TYPES 供面板使用
- FilterPanel.vue 改用 FILTER_TYPES[def.id],与注册表 id 命名解耦
- crystallize.ts applyTo2d 不再跳过越界格,越界种子参与取最近并 clamp 采样,与 WebGL 路径一致
This commit is contained in:
2026-09-04 16:39:05 +08:00
parent 9f2b14572d
commit 2f87a77992
3 changed files with 26 additions and 27 deletions
+4 -9
View File
@@ -2,7 +2,7 @@
import { onBeforeUnmount, reactive, watch } from "vue"; import { onBeforeUnmount, reactive, watch } from "vue";
import { FabricImage, filters } from "fabric"; import { FabricImage, filters } from "fabric";
import type { FilterDef, FilterParam } from "@/types"; import type { FilterDef, FilterParam } from "@/types";
import { FILTERS } from "@/editor/filters"; import { FILTERS, FILTER_TYPES } from "@/editor/filters";
import { useEditor } from "@/editor/useEditor"; import { useEditor } from "@/editor/useEditor";
/** /**
@@ -27,11 +27,6 @@ for (const def of FILTERS) {
state[def.id] = { enabled: false, values }; state[def.id] = { enabled: false, values };
} }
/** 注册表 id → fabric 滤镜的 type 名(首字母大写即可覆盖当前六种)。 */
function fabricTypeOf(def: FilterDef): string {
return def.id.charAt(0).toUpperCase() + def.id.slice(1);
}
function currentImage(): FabricImage | null { function currentImage(): FabricImage | null {
const obj = activeObject.value; const obj = activeObject.value;
return obj instanceof FabricImage ? obj : null; return obj instanceof FabricImage ? obj : null;
@@ -44,7 +39,7 @@ function readFromImage() {
? (image.filters as filters.BaseFilter<string>[]) ? (image.filters as filters.BaseFilter<string>[])
: []; : [];
for (const def of FILTERS) { for (const def of FILTERS) {
const type = fabricTypeOf(def); const type = FILTER_TYPES[def.id];
const found = list.find((f) => f.type === type); const found = list.find((f) => f.type === type);
const st = state[def.id]; const st = state[def.id];
if (found) { if (found) {
@@ -77,7 +72,7 @@ function scheduleApply() {
function toggle(def: FilterDef) { function toggle(def: FilterDef) {
const image = currentImage(); const image = currentImage();
if (!image) return; if (!image) return;
const type = fabricTypeOf(def); const type = FILTER_TYPES[def.id];
const st = state[def.id]; const st = state[def.id];
st.enabled = !st.enabled; st.enabled = !st.enabled;
if (st.enabled) { if (st.enabled) {
@@ -95,7 +90,7 @@ function onParam(def: FilterDef, p: FilterParam, event: Event) {
const value = Number((event.target as HTMLInputElement).value); const value = Number((event.target as HTMLInputElement).value);
state[def.id].values[p.key] = value; state[def.id].values[p.key] = value;
const type = fabricTypeOf(def); const type = FILTER_TYPES[def.id];
const instance = ( const instance = (
image.filters as filters.BaseFilter<string>[] image.filters as filters.BaseFilter<string>[]
).find((f) => f.type === type); ).find((f) => f.type === type);
+8 -18
View File
@@ -87,33 +87,23 @@ export class Crystallize extends BaseFilter<"Crystallize", CrystallizeOwnProps>
if (cellSize === 1) return; if (cellSize === 1) return;
const src = new Uint8ClampedArray(data); const src = new Uint8ClampedArray(data);
const cols = Math.ceil(width / cellSize);
const rows = Math.ceil(height / cellSize);
// 预计算每个格子的种子点位置(种子点颜色 = 原图该点颜色)。 // 抖动网格 Voronoi。越界格也参与「取最近」,与 WebGL 路径一致;
const seeds: { x: number; y: number }[] = new Array(cols * rows); // 采样坐标在下方 clamp 回图内,等价于纹理的 CLAMP_TO_EDGE。
for (let gy = 0; gy < rows; gy++) { const seedAt = (gx: number, gy: number) => ({
for (let gx = 0; gx < cols; gx++) { x: gx * cellSize + hash2(gx, gy, 0) * cellSize,
seeds[gy * cols + gx] = { y: gy * cellSize + hash2(gx, gy, 1) * cellSize,
x: gx * cellSize + hash2(gx, gy, 0) * cellSize, });
y: gy * cellSize + hash2(gx, gy, 1) * cellSize,
};
}
}
for (let y = 0; y < height; y++) { for (let y = 0; y < height; y++) {
const gy = Math.floor(y / cellSize); const gy = Math.floor(y / cellSize);
for (let x = 0; x < width; x++) { for (let x = 0; x < width; x++) {
const gx = Math.floor(x / cellSize); const gx = Math.floor(x / cellSize);
let bestDist = Infinity; let bestDist = Infinity;
let bestSeed = seeds[gy * cols + gx]; let bestSeed = seedAt(gx, gy);
for (let dy = -1; dy <= 1; dy++) { for (let dy = -1; dy <= 1; dy++) {
const cy = gy + dy;
if (cy < 0 || cy >= rows) continue;
for (let dx = -1; dx <= 1; dx++) { for (let dx = -1; dx <= 1; dx++) {
const cx = gx + dx; const s = seedAt(gx + dx, gy + dy);
if (cx < 0 || cx >= cols) continue;
const s = seeds[cy * cols + cx];
const ddx = x - s.x; const ddx = x - s.x;
const ddy = y - s.y; const ddy = y - s.y;
const d = ddx * ddx + ddy * ddy; const d = ddx * ddx + ddy * ddy;
+14
View File
@@ -91,3 +91,17 @@ export const FILTERS: FilterDef[] = [
build: (values) => new Crystallize({ cellSize: values.cellSize }), build: (values) => new Crystallize({ cellSize: values.cellSize }),
}, },
]; ];
/**
* 每个滤镜的 fabric 真实 `type`,从 `build()` 造出的实例上读取,
* 不靠 `id` 的首字母大写拼写猜测——id 与 fabric type 是两套独立命名。
*/
function typeOf(def: FilterDef): string {
const values: Record<string, number> = {};
for (const p of def.params) values[p.key] = p.default;
return (def.build(values) as filters.BaseFilter<string>).type;
}
export const FILTER_TYPES: Readonly<Record<string, string>> = Object.fromEntries(
FILTERS.map((def) => [def.id, typeOf(def)]),
);