From 2f87a77992037af1cbaa6cc7632e9b2672bc5410 Mon Sep 17 00:00:00 2001 From: Jz0ojiang Date: Fri, 4 Sep 2026 16:39:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=BB=A4=E9=95=9C=E4=B8=A4?= =?UTF-8?q?=E5=A4=84=E9=97=AE=E9=A2=98=EF=BC=9Atype=20=E5=8C=B9=E9=85=8D?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E9=9D=A0=20id=20=E6=8B=BC=E5=86=99=E3=80=81?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E8=BE=B9=E7=BC=98=E6=A0=BC=E8=A1=8C=E4=B8=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - filters/index.ts 从 build() 实例读出真实 fabric type,导出 FILTER_TYPES 供面板使用 - FilterPanel.vue 改用 FILTER_TYPES[def.id],与注册表 id 命名解耦 - crystallize.ts applyTo2d 不再跳过越界格,越界种子参与取最近并 clamp 采样,与 WebGL 路径一致 --- src/components/panels/FilterPanel.vue | 13 ++++--------- src/editor/filters/crystallize.ts | 26 ++++++++------------------ src/editor/filters/index.ts | 14 ++++++++++++++ 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/components/panels/FilterPanel.vue b/src/components/panels/FilterPanel.vue index 72f6bed..f361858 100644 --- a/src/components/panels/FilterPanel.vue +++ b/src/components/panels/FilterPanel.vue @@ -2,7 +2,7 @@ import { onBeforeUnmount, reactive, watch } from "vue"; import { FabricImage, filters } from "fabric"; import type { FilterDef, FilterParam } from "@/types"; -import { FILTERS } from "@/editor/filters"; +import { FILTERS, FILTER_TYPES } from "@/editor/filters"; import { useEditor } from "@/editor/useEditor"; /** @@ -27,11 +27,6 @@ for (const def of FILTERS) { 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 { const obj = activeObject.value; return obj instanceof FabricImage ? obj : null; @@ -44,7 +39,7 @@ function readFromImage() { ? (image.filters as filters.BaseFilter[]) : []; for (const def of FILTERS) { - const type = fabricTypeOf(def); + const type = FILTER_TYPES[def.id]; const found = list.find((f) => f.type === type); const st = state[def.id]; if (found) { @@ -77,7 +72,7 @@ function scheduleApply() { function toggle(def: FilterDef) { const image = currentImage(); if (!image) return; - const type = fabricTypeOf(def); + const type = FILTER_TYPES[def.id]; const st = state[def.id]; st.enabled = !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); state[def.id].values[p.key] = value; - const type = fabricTypeOf(def); + const type = FILTER_TYPES[def.id]; const instance = ( image.filters as filters.BaseFilter[] ).find((f) => f.type === type); diff --git a/src/editor/filters/crystallize.ts b/src/editor/filters/crystallize.ts index 2194927..866bf44 100644 --- a/src/editor/filters/crystallize.ts +++ b/src/editor/filters/crystallize.ts @@ -87,33 +87,23 @@ export class Crystallize extends BaseFilter<"Crystallize", CrystallizeOwnProps> if (cellSize === 1) return; const src = new Uint8ClampedArray(data); - const cols = Math.ceil(width / cellSize); - const rows = Math.ceil(height / cellSize); - // 预计算每个格子的种子点位置(种子点颜色 = 原图该点颜色)。 - const seeds: { x: number; y: number }[] = new Array(cols * rows); - for (let gy = 0; gy < rows; gy++) { - for (let gx = 0; gx < cols; gx++) { - seeds[gy * cols + gx] = { - x: gx * cellSize + hash2(gx, gy, 0) * cellSize, - y: gy * cellSize + hash2(gx, gy, 1) * cellSize, - }; - } - } + // 抖动网格 Voronoi。越界格也参与「取最近」,与 WebGL 路径一致; + // 采样坐标在下方 clamp 回图内,等价于纹理的 CLAMP_TO_EDGE。 + const seedAt = (gx: number, gy: number) => ({ + x: gx * cellSize + hash2(gx, gy, 0) * cellSize, + y: gy * cellSize + hash2(gx, gy, 1) * cellSize, + }); for (let y = 0; y < height; y++) { const gy = Math.floor(y / cellSize); for (let x = 0; x < width; x++) { const gx = Math.floor(x / cellSize); let bestDist = Infinity; - let bestSeed = seeds[gy * cols + gx]; + let bestSeed = seedAt(gx, gy); for (let dy = -1; dy <= 1; dy++) { - const cy = gy + dy; - if (cy < 0 || cy >= rows) continue; for (let dx = -1; dx <= 1; dx++) { - const cx = gx + dx; - if (cx < 0 || cx >= cols) continue; - const s = seeds[cy * cols + cx]; + const s = seedAt(gx + dx, gy + dy); const ddx = x - s.x; const ddy = y - s.y; const d = ddx * ddx + ddy * ddy; diff --git a/src/editor/filters/index.ts b/src/editor/filters/index.ts index 0d8b28a..276c2e4 100644 --- a/src/editor/filters/index.ts +++ b/src/editor/filters/index.ts @@ -91,3 +91,17 @@ export const FILTERS: FilterDef[] = [ build: (values) => new Crystallize({ cellSize: values.cellSize }), }, ]; + +/** + * 每个滤镜的 fabric 真实 `type`,从 `build()` 造出的实例上读取, + * 不靠 `id` 的首字母大写拼写猜测——id 与 fabric type 是两套独立命名。 + */ +function typeOf(def: FilterDef): string { + const values: Record = {}; + for (const p of def.params) values[p.key] = p.default; + return (def.build(values) as filters.BaseFilter).type; +} + +export const FILTER_TYPES: Readonly> = Object.fromEntries( + FILTERS.map((def) => [def.id, typeOf(def)]), +);