feat(canvas): 画布外观 - 默认使用黑色点网格并保留自定义配置

This commit is contained in:
ddCat
2026-09-22 15:47:49 +08:00
parent 2cd30b9951
commit da24992afe
8 changed files with 56 additions and 13 deletions

View File

@@ -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({
<div className="mt-3 text-[var(--fs-micro)] font-semibold uppercase opacity-45">主题模式</div>
<div className="mt-1 grid grid-cols-3 gap-1 rounded-[var(--dock-item-radius-labeled)] border p-1" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }}>
<ThemeButton active={draft.mode === "light"} label="浅色" theme={theme} onClick={() => selectFixedTheme("light")}><Sun className="size-3.5" /></ThemeButton>
<ThemeButton active={draft.mode === "dark"} label="深色" theme={theme} onClick={() => selectFixedTheme("dark")}><Moon className="size-3.5" /></ThemeButton>
<ThemeButton active={draft.mode === "dark"} label="黑色" theme={theme} onClick={() => selectFixedTheme("dark")}><Moon className="size-3.5" /></ThemeButton>
<ThemeButton active={draft.mode === "custom"} label="自定义" theme={theme} onClick={selectCustomTheme}><Paintbrush className="size-3.5" /></ThemeButton>
</div>

View File

@@ -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: {

View File

@@ -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<CanvasColorTheme, string> = { light: "#000000", dark: "#AFAFAF" };
const CUSTOM_GRID_OPACITY = 80;
const CUSTOM_GRID_COLOR: Record<CanvasColorTheme, string> = { light: "#000000", dark: "#000000" };
const CUSTOM_GRID_OPACITY: Record<CanvasColorTheme, number> = { 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]),
};
}

View File

@@ -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<CanvasStore>()(
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,

View File

@@ -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<CanvasThemeStore>()(
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<CanvasThemeStore> | null)?.theme === "dark" ? "dark" : "light" }),
merge: (persisted, current) => {
const theme = (persisted as Partial<CanvasThemeStore> | null)?.theme;
return { ...current, theme: theme === "light" || theme === "dark" ? theme : DEFAULT_CANVAS_COLOR_THEME };
},
},
),
);