修复滤镜两处问题: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:
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)]),
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user