From da24992afe02a54e0705fd984801a3fe2cc4891a Mon Sep 17 00:00:00 2001
From: ddCat <2695092045@qq.com>
Date: Tue, 22 Sep 2026 15:47:49 +0800
Subject: [PATCH] =?UTF-8?q?feat(canvas):=20=E7=94=BB=E5=B8=83=E5=A4=96?=
=?UTF-8?q?=E8=A7=82=20-=20=E9=BB=98=E8=AE=A4=E4=BD=BF=E7=94=A8=E9=BB=91?=
=?UTF-8?q?=E8=89=B2=E7=82=B9=E7=BD=91=E6=A0=BC=E5=B9=B6=E4=BF=9D=E7=95=99?=
=?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/content/docs/progress/pending-test.mdx | 7 +++++
.../canvas/canvas-appearance-controls.tsx | 4 ++-
web/src/lib/canvas-theme.ts | 5 ++--
web/src/lib/canvas/canvas-appearance.ts | 8 +++---
web/src/stores/canvas/use-canvas-store.ts | 6 ++---
.../stores/canvas/use-canvas-theme-store.ts | 8 ++++--
web/test/canvas-appearance.test.ts | 26 ++++++++++++++++++-
web/test/canvas-theme-isolation.test.ts | 5 ++++
8 files changed, 56 insertions(+), 13 deletions(-)
diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx
index af15b5e76..147e893ef 100644
--- a/docs/content/docs/progress/pending-test.mdx
+++ b/docs/content/docs/progress/pending-test.mdx
@@ -16,6 +16,13 @@ description: 当前项目已实现功能的手工验证清单与未确认边界
- [ ] PostgreSQL 部署执行 schema 34 升级及多实例租约/共享暂存卷验收。未修改当前本地业务数据库、历史失败任务或账单。
- [ ] 无恢复记录的历史失败任务单独核实源地址有效期与计费状态;本功能不从旧日志自动回填,不承诺恢复已过期的源文件。
+## 画布默认黑色主题与点网格
+
+- 前端类型检查与 17 项外观、主题隔离测试通过;尚未完成真实浏览器验收。
+- [ ] 未保存自定义默认外观时,新建画布使用黑色主题:背景 `#000000`、明亮度 `0%`、网格颜色 `#000000`、网格强度 `100%`、空间网格为点。黑底黑点视觉上为纯黑。
+- [ ] 切换「黑色」或「浅色」预设时使用点网格;黑色首次转自定义及重置时使用上述预设数值,已有自定义配置仍保留,浅色颜色与强度保持原值。仍可手动选择线或空白网格。
+- [ ] 已保存的画布外观及用户自定义默认外观不被覆盖;画布主题变更不影响工作台主题。实际浏览器明暗主题与保存恢复待验收。
+
## Live2D 运行库随部署交付
- Cubism Core 已纳入版本控制,保留原始版权声明并附第三方许可说明;前端构建和静态镜像直接包含运行文件,不再要求部署者手工安装。
diff --git a/web/src/components/canvas/canvas-appearance-controls.tsx b/web/src/components/canvas/canvas-appearance-controls.tsx
index 51bd42e6e..a26890aac 100644
--- a/web/src/components/canvas/canvas-appearance-controls.tsx
+++ b/web/src/components/canvas/canvas-appearance-controls.tsx
@@ -4,6 +4,7 @@ import { CircleDot, Grid2x2, Moon, Paintbrush, RotateCcw, Save, Square, Sun } fr
import {
canvasAppearanceForTheme,
+ DEFAULT_CANVAS_BACKGROUND_MODE,
customCanvasAppearanceFromTheme,
enterCustomCanvasAppearance,
normalizeHexColor,
@@ -40,6 +41,7 @@ export function CanvasAppearanceControls({
const next = canvasAppearanceForTheme(target, draft);
setDraft(next);
onAppearanceChange(next);
+ onBackgroundModeChange(DEFAULT_CANVAS_BACKGROUND_MODE);
};
const selectCustomTheme = () => {
const next = enterCustomCanvasAppearance(draft, colorTheme);
@@ -71,7 +73,7 @@ export function CanvasAppearanceControls({
主题模式
selectFixedTheme("light")}>
-
selectFixedTheme("dark")}>
+
selectFixedTheme("dark")}>
diff --git a/web/src/lib/canvas-theme.ts b/web/src/lib/canvas-theme.ts
index 03a1a48b6..f6c9cdf2f 100644
--- a/web/src/lib/canvas-theme.ts
+++ b/web/src/lib/canvas-theme.ts
@@ -1,5 +1,6 @@
export type CanvasColorTheme = "light" | "dark";
export type CanvasBackgroundMode = "dots" | "lines" | "blank";
+export const DEFAULT_CANVAS_COLOR_THEME: CanvasColorTheme = "dark";
export const canvasThemes = {
light: {
@@ -72,8 +73,8 @@ export const canvasThemes = {
dark: {
canvas: {
background: "#000000",
- dot: "rgba(175,175,175,.80)",
- line: "rgba(175,175,175,.80)",
+ dot: "#000000",
+ line: "#000000",
selectionFill: "rgba(255,255,255,.12)",
},
node: {
diff --git a/web/src/lib/canvas/canvas-appearance.ts b/web/src/lib/canvas/canvas-appearance.ts
index 374ea7d16..56f422e80 100644
--- a/web/src/lib/canvas/canvas-appearance.ts
+++ b/web/src/lib/canvas/canvas-appearance.ts
@@ -31,8 +31,8 @@ export const DEFAULT_CANVAS_BACKGROUND_MODE: CanvasBackgroundMode = "dots";
const CANVAS_APPEARANCE_DEFAULT_KEY = "infinite-canvas:canvas-appearance-default";
const HEX_COLOR_PATTERN = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i;
-const CUSTOM_GRID_COLOR: Record = { light: "#000000", dark: "#AFAFAF" };
-const CUSTOM_GRID_OPACITY = 80;
+const CUSTOM_GRID_COLOR: Record = { light: "#000000", dark: "#000000" };
+const CUSTOM_GRID_OPACITY: Record = { light: 80, dark: 100 };
export function canvasAppearanceForTheme(theme: CanvasColorTheme, previous?: CanvasAppearance): CanvasAppearance {
return previous?.custom?.baseTheme === theme ? { mode: theme, custom: previous.custom } : { mode: theme };
@@ -46,7 +46,7 @@ export function customCanvasAppearanceFromTheme(theme: CanvasColorTheme): Canvas
backgroundColor: canvasThemes[theme].canvas.background.toUpperCase(),
backgroundBrightness: 0,
gridColor: CUSTOM_GRID_COLOR[theme],
- gridOpacity: CUSTOM_GRID_OPACITY,
+ gridOpacity: CUSTOM_GRID_OPACITY[theme],
},
};
}
@@ -143,7 +143,7 @@ function normalizeCustomAppearance(value: unknown): CanvasCustomAppearance | und
backgroundColor,
backgroundBrightness: clampNumber(candidate.backgroundBrightness, -30, 30, 0),
gridColor,
- gridOpacity: clampNumber(candidate.gridOpacity, 0, 100, CUSTOM_GRID_OPACITY),
+ gridOpacity: clampNumber(candidate.gridOpacity, 0, 100, CUSTOM_GRID_OPACITY[candidate.baseTheme]),
};
}
diff --git a/web/src/stores/canvas/use-canvas-store.ts b/web/src/stores/canvas/use-canvas-store.ts
index 886246e76..a7e11a43b 100644
--- a/web/src/stores/canvas/use-canvas-store.ts
+++ b/web/src/stores/canvas/use-canvas-store.ts
@@ -3,11 +3,11 @@ import { persist, type PersistStorage, type StorageValue } from "zustand/middlew
import { nanoid } from "nanoid";
import { sameCanvasContent } from "@/lib/canvas/canvas-content";
-import { DEFAULT_CANVAS_BACKGROUND_MODE, normalizeCanvasAppearance, readCanvasAppearanceDefault, type CanvasAppearance } from "@/lib/canvas/canvas-appearance";
+import { canvasAppearanceForTheme, DEFAULT_CANVAS_BACKGROUND_MODE, normalizeCanvasAppearance, readCanvasAppearanceDefault, type CanvasAppearance } from "@/lib/canvas/canvas-appearance";
import { parseCanvasStorageDocument, rebaseCanvasProjects, serializeCanvasStorageDocument, type CanvasStorageDocument } from "@/lib/canvas/canvas-storage-revision";
import { localForageStorageForScope } from "@/lib/localforage-storage";
import { getActiveUserScope } from "@/lib/user-scope";
-import type { CanvasBackgroundMode } from "@/lib/canvas-theme";
+import { DEFAULT_CANVAS_COLOR_THEME, type CanvasBackgroundMode } from "@/lib/canvas-theme";
import type { CanvasStarterMode } from "@/lib/canvas/canvas-starter";
import type { CanvasAssistantSession, CanvasConnection, CanvasNodeData, ViewportTransform } from "@/types/canvas";
import type { DirectorScene } from "@/types/director";
@@ -461,7 +461,7 @@ export const useCanvasStore = create()(
connections: [],
chatSessions: [],
activeChatId: null,
- appearance: appearanceDefault?.appearance,
+ appearance: appearanceDefault?.appearance ?? canvasAppearanceForTheme(DEFAULT_CANVAS_COLOR_THEME),
backgroundMode: appearanceDefault?.backgroundMode || DEFAULT_CANVAS_BACKGROUND_MODE,
showImageInfo: false,
viewport: initialViewport,
diff --git a/web/src/stores/canvas/use-canvas-theme-store.ts b/web/src/stores/canvas/use-canvas-theme-store.ts
index 4edb3d4ba..df44d430f 100644
--- a/web/src/stores/canvas/use-canvas-theme-store.ts
+++ b/web/src/stores/canvas/use-canvas-theme-store.ts
@@ -3,17 +3,21 @@ import { persist } from "zustand/middleware";
import type { ThemeName } from "@/stores/use-theme-store";
import { useThemeStore } from "@/stores/use-theme-store";
import { useLayoutEffect } from "react";
+import { DEFAULT_CANVAS_COLOR_THEME } from "@/lib/canvas-theme";
type CanvasThemeStore = { theme: ThemeName; active: boolean; setTheme: (theme: ThemeName) => void };
/** 画布外观是编辑器状态,不能写入用户工作台的全局主题。 */
export const useCanvasThemeStore = create()(
persist(
- (set) => ({ theme: "light", active: false, setTheme: (theme) => { if (theme === "light" || theme === "dark") set({ theme }); } }),
+ (set) => ({ theme: DEFAULT_CANVAS_COLOR_THEME, active: false, setTheme: (theme) => { if (theme === "light" || theme === "dark") set({ theme }); } }),
{
name: "infinite-canvas:canvas-theme",
partialize: ({ theme }) => ({ theme }),
- merge: (persisted, current) => ({ ...current, theme: (persisted as Partial | null)?.theme === "dark" ? "dark" : "light" }),
+ merge: (persisted, current) => {
+ const theme = (persisted as Partial | null)?.theme;
+ return { ...current, theme: theme === "light" || theme === "dark" ? theme : DEFAULT_CANVAS_COLOR_THEME };
+ },
},
),
);
diff --git a/web/test/canvas-appearance.test.ts b/web/test/canvas-appearance.test.ts
index de1880c91..b78f88648 100644
--- a/web/test/canvas-appearance.test.ts
+++ b/web/test/canvas-appearance.test.ts
@@ -1,4 +1,5 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
+import { DEFAULT_CANVAS_COLOR_THEME } from "../src/lib/canvas-theme";
import {
DEFAULT_CANVAS_BACKGROUND_MODE,
@@ -39,6 +40,29 @@ afterEach(() => {
describe("canvas custom appearance", () => {
test("uses point grid as the default for new canvases", () => {
expect(DEFAULT_CANVAS_BACKGROUND_MODE).toBe("dots");
+ expect(DEFAULT_CANVAS_COLOR_THEME).toBe("dark");
+ });
+
+ test("resolves the black preset to a black background and fully opaque black grid", () => {
+ const appearance = canvasAppearanceForTheme(DEFAULT_CANVAS_COLOR_THEME);
+ expect(resolveCanvasAppearance(appearance, "light")).toEqual({
+ baseTheme: "dark",
+ background: "#000000",
+ grid: "#000000",
+ });
+ expect(resolveCanvasGridColor(appearance, "light", "dots")).toBe("#000000");
+ expect(resolveCanvasAppearance(customCanvasAppearanceFromTheme("dark"), "light")).toEqual({
+ baseTheme: "dark",
+ background: "#000000",
+ grid: "rgba(0,0,0,1)",
+ });
+ });
+
+ test("selecting either fixed theme resets the spatial grid to dots", async () => {
+ const source = await Bun.file(new URL("../src/components/canvas/canvas-appearance-controls.tsx", import.meta.url)).text();
+ const fixedThemeSource = source.slice(source.indexOf("const selectFixedTheme"), source.indexOf("const selectCustomTheme"));
+ expect(fixedThemeSource).toContain("onBackgroundModeChange(DEFAULT_CANVAS_BACKGROUND_MODE)");
+ expect(resolveCanvasGridColor(canvasAppearanceForTheme("light"), "dark", "dots")).toBe("rgba(0,0,0,.80)");
});
test("inherits the active fixed theme the first time custom mode is selected", () => {
@@ -55,7 +79,7 @@ describe("canvas custom appearance", () => {
});
const dark = enterCustomCanvasAppearance(canvasAppearanceForTheme("dark"), "dark");
- expect(dark.custom).toMatchObject({ baseTheme: "dark", backgroundColor: "#000000", backgroundBrightness: 0, gridColor: "#AFAFAF", gridOpacity: 80 });
+ expect(dark.custom).toMatchObject({ baseTheme: "dark", backgroundColor: "#000000", backgroundBrightness: 0, gridColor: "#000000", gridOpacity: 100 });
});
test("restores a previous custom profile only under the same base theme", () => {
diff --git a/web/test/canvas-theme-isolation.test.ts b/web/test/canvas-theme-isolation.test.ts
index 118b1d37c..32dae36dc 100644
--- a/web/test/canvas-theme-isolation.test.ts
+++ b/web/test/canvas-theme-isolation.test.ts
@@ -10,6 +10,11 @@ afterEach(() => {
});
describe("canvas theme ownership", () => {
+ test("defaults to black while allowing an explicit light theme", () => {
+ expect(useCanvasThemeStore.getInitialState().theme).toBe("dark");
+ useCanvasThemeStore.getState().setTheme("light");
+ expect(useCanvasThemeStore.getState().theme).toBe("light");
+ });
test("editing the canvas theme never mutates the workspace preference", () => {
useThemeStore.setState({ theme: "light" });
useCanvasThemeStore.setState({ active: true });