Compare commits

...

3 Commits

Author SHA1 Message Date
xiamidaxia
099fd445f2
fix(form): validate feedback level support warning (#408) 2025-06-24 08:26:28 +00:00
小智
488013bb0b
feat(background-plugin): 增强背景插件, 支持自定义背景、Logo显示和新拟态效果 (#404)
* feat(background-plugin): enhance background plugin with logo support and neumorphism effects

- Add comprehensive logo support (text and image)
- Implement neumorphism visual effects for modern UI
- Add customizable background colors and dot patterns
- Fix logo position jumping during canvas scrolling
- Add complete Chinese and English documentation
- Add visual examples and type definitions
- Update navigation metadata for documentation

Features:
- Logo positioning with 5 preset locations
- Neumorphism effects with configurable shadows
- Custom background colors and dot styling
- Smooth logo rendering during viewport changes
- Comprehensive documentation with examples

* fix(playground-react): fix background plugin type compatibility

- Fix TypeScript error when background option is boolean
- Ensure proper type handling for BackgroundLayerOptions

* fix(docs): remove incorrect number from sub-canvas plugin heading

---------

Co-authored-by: husky-dot <xiaozhi@172-0-8-36.lightspeed.rcsntx.sbcglobal.net>
2025-06-24 05:01:55 +00:00
jzwnju
233b9befab
feat(materials): add className prop to JsonSchemaEditor and adjust button styling (#406) 2025-06-24 03:56:21 +00:00
19 changed files with 981 additions and 40 deletions

View File

@ -33,7 +33,7 @@ export function FormInputs() {
hasError={Object.keys(fieldState?.errors || {}).length > 0}
schema={property}
/>
<Feedback errors={fieldState?.errors} />
<Feedback errors={fieldState?.errors} warnings={fieldState?.warnings} />
</FormItem>
)}
</Field>

View File

@ -3,7 +3,12 @@ import {
provideJsonSchemaOutputs,
syncVariableTitle,
} from '@flowgram.ai/form-materials';
import { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/fixed-layout-editor';
import {
FormRenderProps,
FormMeta,
ValidateTrigger,
FeedbackLevel,
} from '@flowgram.ai/fixed-layout-editor';
import { FlowNodeJSON } from '../typings';
import { FormHeader, FormContent, FormInputs, FormOutputs } from '../form-components';
@ -30,7 +35,10 @@ export const defaultFormMeta: FormMeta<FlowNodeJSON['data']> = {
required.includes(valuePropetyKey) &&
(value === '' || value === undefined || value?.content === '')
) {
return `${valuePropetyKey} is required`;
return {
message: `${valuePropetyKey} is required`,
level: FeedbackLevel.Error, // Error || Warning
};
}
return undefined;
},

View File

@ -33,7 +33,7 @@ export function FormInputs() {
hasError={Object.keys(fieldState?.errors || {}).length > 0}
schema={property}
/>
<Feedback errors={fieldState?.errors} />
<Feedback errors={fieldState?.errors} warnings={fieldState?.warnings} />
</FormItem>
)}
</Field>

View File

@ -1,4 +1,9 @@
import { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/free-layout-editor';
import {
FormRenderProps,
FormMeta,
ValidateTrigger,
FeedbackLevel,
} from '@flowgram.ai/free-layout-editor';
import {
autoRenameRefEffect,
provideJsonSchemaOutputs,
@ -30,7 +35,10 @@ export const defaultFormMeta: FormMeta<FlowNodeJSON> = {
required.includes(valuePropetyKey) &&
(value === '' || value === undefined || value?.content === '')
) {
return `${valuePropetyKey} is required`;
return {
message: `${valuePropetyKey} is required`,
level: FeedbackLevel.Error, // Error || Warning
};
}
return undefined;
},

View File

@ -3,5 +3,6 @@
"node",
"line",
"port",
"sub-canvas"
"sub-canvas",
"background"
]

View File

@ -0,0 +1,270 @@
# Background
The background plugin is used to customize canvas background effects, supporting dot patterns, logo display, and neumorphism visual effects.
## Background Configuration
The background plugin is provided through `BackgroundPlugin`, configuration options include:
### Basic Configuration
<img loading="lazy" className="invert-img" src="/free-layout/background-color.png"/>
```ts pure
{
// Background color
backgroundColor: '#1a1a1a',
// Dot color
dotColor: '#ffffff',
// Dot size (pixels)
dotSize: 1,
// Grid spacing (pixels)
gridSize: 20,
// Dot opacity (0-1)
dotOpacity: 0.5,
// Dot fill color
dotFillColor: '#ffffff'
}
```
### Logo Configuration
Supports both text and image logo types:
<img loading="lazy" className="invert-img" src="/free-layout/background-logo.png"/>
```ts pure
{
logo: {
// Logo text
text: 'FLOWGRAM.AI',
// Image URL (optional, higher priority than text)
imageUrl: 'https://example.com/logo.png',
// Position: 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
position: 'center',
// Size
size: 200,
// Opacity (0-1)
opacity: 0.25,
// Color
color: '#ffffff',
// Font family
fontFamily: 'Arial, sans-serif',
// Font weight
fontWeight: 'bold',
// Custom offset
offset: { x: 0, y: 0 }
}
}
```
### Neumorphism Effect
Neumorphism is a modern visual design style that creates depth through dual soft shadows:
<img loading="lazy" className="invert-img" src="/free-layout/background-neumorphism.png"/>
```ts pure
{
logo: {
neumorphism: {
// Enable neumorphism effect
enabled: true,
// Text color
textColor: '#E0E0E0',
// Light shadow color
lightShadowColor: 'rgba(255,255,255,0.9)',
// Dark shadow color
darkShadowColor: 'rgba(0,0,0,0.15)',
// Shadow offset distance
shadowOffset: 6,
// Shadow blur radius
shadowBlur: 12,
// Shadow intensity
intensity: 0.6,
// Raised effect (true=raised, false=inset)
raised: true
}
}
}
```
## Usage Example
```tsx pure
// Use background property directly in editor configuration
const editorProps = {
// Background configuration
background: {
// Dark theme background
backgroundColor: '#1a1a1a',
dotColor: '#ffffff',
dotSize: 1,
gridSize: 20,
dotOpacity: 0.3,
// Brand logo
logo: {
text: 'FLOWGRAM.AI',
position: 'center',
size: 200,
opacity: 0.25,
color: '#ffffff',
fontFamily: 'Arial, sans-serif',
fontWeight: 'bold',
// Neumorphism effect
neumorphism: {
enabled: true,
textColor: '#E0E0E0',
lightShadowColor: 'rgba(255,255,255,0.9)',
darkShadowColor: 'rgba(0,0,0,0.15)',
shadowOffset: 6,
shadowBlur: 12,
intensity: 0.6,
raised: true
}
}
}
}
```
## Preset Styles
### Classic Dark Theme
```tsx pure
const editorProps = {
background: {
backgroundColor: '#1a1a1a',
dotColor: '#ffffff',
dotSize: 1,
gridSize: 20,
dotOpacity: 0.3,
logo: {
text: 'Your Brand',
position: 'center',
size: 200,
opacity: 0.25,
color: '#ffffff',
neumorphism: {
enabled: true,
textColor: '#E0E0E0',
lightShadowColor: 'rgba(255,255,255,0.9)',
darkShadowColor: 'rgba(0,0,0,0.15)',
shadowOffset: 6,
shadowBlur: 12,
intensity: 0.6,
raised: true
}
}
}
}
```
### Minimal White Theme
```tsx pure
const editorProps = {
background: {
backgroundColor: '#ffffff',
dotColor: '#000000',
dotSize: 1,
gridSize: 20,
dotOpacity: 0.1,
logo: {
text: 'Your Brand',
position: 'center',
size: 200,
opacity: 0.1,
color: '#000000'
}
}
}
```
## Notes
1. **Color Matching**: Ensure sufficient contrast between logo color and background color
2. **Opacity Settings**: Logo opacity should not be too high to avoid affecting content readability
3. **Neumorphism Effect**: Shadow parameters should be adjusted reasonably, overly strong effects may distract attention
4. **Performance Considerations**: Complex shadow effects may impact rendering performance, consider simplifying on low-end devices
## Type Definitions
```ts
interface BackgroundLayerOptions {
/** Grid spacing, default 20px */
gridSize?: number;
/** Dot size, default 1px */
dotSize?: number;
/** Dot color, default "#eceeef" */
dotColor?: string;
/** Dot opacity, default 0.5 */
dotOpacity?: number;
/** Background color, default transparent */
backgroundColor?: string;
/** Dot fill color, default same as stroke color */
dotFillColor?: string;
/** Logo configuration */
logo?: {
/** Logo text content */
text?: string;
/** Logo image URL */
imageUrl?: string;
/** Logo position, default 'center' */
position?: 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
/** Logo size, default 'medium' */
size?: 'small' | 'medium' | 'large' | number;
/** Logo opacity, default 0.1 */
opacity?: number;
/** Logo color (text only), default "#cccccc" */
color?: string;
/** Logo font family (text only), default 'Arial, sans-serif' */
fontFamily?: string;
/** Logo font weight (text only), default 'normal' */
fontWeight?: 'normal' | 'bold' | 'lighter' | number;
/** Custom offset */
offset?: { x: number; y: number };
/** Neumorphism effect configuration */
neumorphism?: {
/** Enable neumorphism effect */
enabled: boolean;
/** Text color */
textColor?: string;
/** Light shadow color */
lightShadowColor?: string;
/** Dark shadow color */
darkShadowColor?: string;
/** Shadow offset distance */
shadowOffset?: number;
/** Shadow blur radius */
shadowBlur?: number;
/** Shadow intensity */
intensity?: number;
/** Raised effect (true=raised, false=inset) */
raised?: boolean;
};
};
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

@ -3,5 +3,6 @@
"node",
"line",
"port",
"sub-canvas"
"sub-canvas",
"background"
]

View File

@ -0,0 +1,270 @@
# 背景
背景插件用于自定义画布的背景效果支持点阵背景、Logo显示和新拟态(Neumorphism)视觉效果。
## 背景配置
背景插件通过 `BackgroundPlugin` 提供,配置项包括:
### 基础配置
<img loading="lazy" className="invert-img" src="/free-layout/background-color.png"/>
```ts pure
{
// 背景颜色
backgroundColor: '#1a1a1a',
// 点的颜色
dotColor: '#ffffff',
// 点的大小(像素)
dotSize: 1,
// 网格间距(像素)
gridSize: 20,
// 点的透明度(0-1)
dotOpacity: 0.5,
// 点的填充颜色
dotFillColor: '#ffffff'
}
```
### Logo配置
支持文本和图片两种Logo类型
<img loading="lazy" className="invert-img" src="/free-layout/background-logo.png"/>
```ts pure
{
logo: {
// Logo文本
text: 'FLOWGRAM.AI',
// 图片URL (可选,优先级高于文本)
imageUrl: 'https://example.com/logo.png',
// 位置:'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
position: 'center',
// 大小
size: 200,
// 透明度(0-1)
opacity: 0.25,
// 颜色
color: '#ffffff',
// 字体
fontFamily: 'Arial, sans-serif',
// 字体粗细
fontWeight: 'bold',
// 自定义偏移量
offset: { x: 0, y: 0 }
}
}
```
### 新拟态效果
新拟态(Neumorphism)是一种现代化的视觉设计风格,通过双层柔和阴影创造立体感:
<img loading="lazy" className="invert-img" src="/free-layout/background-neumorphism.png"/>
```ts pure
{
logo: {
neumorphism: {
// 启用新拟态效果
enabled: true,
// 文字颜色
textColor: '#E0E0E0',
// 高光阴影颜色
lightShadowColor: 'rgba(255,255,255,0.9)',
// 暗色阴影颜色
darkShadowColor: 'rgba(0,0,0,0.15)',
// 阴影偏移距离
shadowOffset: 6,
// 阴影模糊半径
shadowBlur: 12,
// 阴影强度
intensity: 0.6,
// 凸起效果(true=凸起, false=凹陷)
raised: true
}
}
}
```
## 使用示例
```tsx pure
// 在编辑器配置中直接使用 background 属性
const editorProps = {
// 背景配置
background: {
// 深色主题背景
backgroundColor: '#1a1a1a',
dotColor: '#ffffff',
dotSize: 1,
gridSize: 20,
dotOpacity: 0.3,
// 品牌Logo
logo: {
text: 'FLOWGRAM.AI',
position: 'center',
size: 200,
opacity: 0.25,
color: '#ffffff',
fontFamily: 'Arial, sans-serif',
fontWeight: 'bold',
// 新拟态效果
neumorphism: {
enabled: true,
textColor: '#E0E0E0',
lightShadowColor: 'rgba(255,255,255,0.9)',
darkShadowColor: 'rgba(0,0,0,0.15)',
shadowOffset: 6,
shadowBlur: 12,
intensity: 0.6,
raised: true
}
}
}
}
```
## 预设样式
### 经典黑色主题
```tsx pure
const editorProps = {
background: {
backgroundColor: '#1a1a1a',
dotColor: '#ffffff',
dotSize: 1,
gridSize: 20,
dotOpacity: 0.3,
logo: {
text: '您的品牌',
position: 'center',
size: 200,
opacity: 0.25,
color: '#ffffff',
neumorphism: {
enabled: true,
textColor: '#E0E0E0',
lightShadowColor: 'rgba(255,255,255,0.9)',
darkShadowColor: 'rgba(0,0,0,0.15)',
shadowOffset: 6,
shadowBlur: 12,
intensity: 0.6,
raised: true
}
}
}
}
```
### 简约白色主题
```tsx pure
const editorProps = {
background: {
backgroundColor: '#ffffff',
dotColor: '#000000',
dotSize: 1,
gridSize: 20,
dotOpacity: 0.1,
logo: {
text: '您的品牌',
position: 'center',
size: 200,
opacity: 0.1,
color: '#000000'
}
}
}
```
## 注意事项
1. **颜色搭配**确保Logo颜色与背景色有足够的对比度
2. **透明度设置**Logo透明度不宜过高以免影响内容可读性
3. **新拟态效果**:需要合理调整阴影参数,过强的效果可能分散注意力
4. **性能考虑**:复杂的阴影效果可能影响渲染性能,建议在低端设备上适当简化
## 类型定义
```ts
interface BackgroundLayerOptions {
/** 网格间距,默认 20px */
gridSize?: number;
/** 点的大小,默认 1px */
dotSize?: number;
/** 点的颜色,默认 "#eceeef" */
dotColor?: string;
/** 点的透明度,默认 0.5 */
dotOpacity?: number;
/** 背景颜色,默认透明 */
backgroundColor?: string;
/** 点的填充颜色默认与stroke颜色相同 */
dotFillColor?: string;
/** Logo 配置 */
logo?: {
/** Logo 文本内容 */
text?: string;
/** Logo 图片 URL */
imageUrl?: string;
/** Logo 位置,默认 'center' */
position?: 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
/** Logo 大小,默认 'medium' */
size?: 'small' | 'medium' | 'large' | number;
/** Logo 透明度,默认 0.1 */
opacity?: number;
/** Logo 颜色(仅文本),默认 "#cccccc" */
color?: string;
/** Logo 字体家族(仅文本),默认 'Arial, sans-serif' */
fontFamily?: string;
/** Logo 字体粗细(仅文本),默认 'normal' */
fontWeight?: 'normal' | 'bold' | 'lighter' | number;
/** 自定义偏移 */
offset?: { x: number; y: number };
/** 新拟态效果配置 */
neumorphism?: {
/** 启用新拟态效果 */
enabled: boolean;
/** 文字颜色 */
textColor?: string;
/** 高光阴影颜色 */
lightShadowColor?: string;
/** 暗色阴影颜色 */
darkShadowColor?: string;
/** 阴影偏移距离 */
shadowOffset?: number;
/** 阴影模糊半径 */
shadowBlur?: number;
/** 阴影强度 */
intensity?: number;
/** 凸起效果(true=凸起, false=凹陷) */
raised?: boolean;
};
};
}
```

View File

@ -53,6 +53,7 @@ export {
useFieldValidate,
useWatch,
ValidateTrigger,
FeedbackLevel,
} from '@flowgram.ai/form';
export * from '@flowgram.ai/node';
export { FormModelV2 as FormModel };

View File

@ -21,7 +21,8 @@ export function createPlaygroundReactPreset<CTX extends PluginContext = PluginCo
* (),
*/
if (opts.background || opts.background === undefined) {
plugins.push(createBackgroundPlugin(opts.background || {}));
const backgroundOptions = typeof opts.background === 'object' ? opts.background : {};
plugins.push(createBackgroundPlugin(backgroundOptions));
}
/**
*

View File

@ -38,6 +38,7 @@ export function JsonSchemaEditor(props: {
value?: IJsonSchema;
onChange?: (value: IJsonSchema) => void;
config?: ConfigType;
className?: string;
}) {
const { value = { type: 'object' }, config = {}, onChange: onChangeProps } = props;
const { propertyList, onAddProperty, onRemoveProperty, onEditProperty } = usePropertiesEdit(
@ -46,7 +47,7 @@ export function JsonSchemaEditor(props: {
);
return (
<UIContainer>
<UIContainer className={props.className}>
<UIProperties>
{propertyList.map((_property, index) => (
<PropertyEdit
@ -63,7 +64,12 @@ export function JsonSchemaEditor(props: {
/>
))}
</UIProperties>
<Button size="small" style={{ marginTop: 10 }} icon={<IconPlus />} onClick={onAddProperty}>
<Button
size="small"
style={{ marginTop: 10, marginLeft: 16 }}
icon={<IconPlus />}
onClick={onAddProperty}
>
{config?.addButtonText ?? 'Add'}
</Button>
</UIContainer>

View File

@ -361,8 +361,8 @@ export class FieldModel<TValue extends FieldValue = FieldValue> implements Dispo
const groupedFeedbacks = groupBy(feedbacks, 'level');
warnings = warnings.concat(groupedFeedbacks[FeedbackLevel.Warning] as FieldWarning[]);
errors = errors.concat(groupedFeedbacks[FeedbackLevel.Error] as FieldError[]);
warnings = warnings.concat((groupedFeedbacks[FeedbackLevel.Warning] as FieldWarning[]) || []);
errors = errors.concat((groupedFeedbacks[FeedbackLevel.Error] as FieldError[]) || []);
}
return { errors, warnings };

View File

@ -15,7 +15,7 @@ import {
OnFormValuesUpdatedPayload,
} from '../types/form';
import { FieldName, FieldValue } from '../types/field';
import { Errors, FormValidateReturn, Warnings } from '../types';
import { Errors, FeedbackLevel, FormValidateReturn, Warnings } from '../types';
import { createFormModelState } from '../constants';
import { getValidByErrors, mergeFeedbacks } from './utils';
import { Store } from './store';
@ -281,8 +281,14 @@ export class FormModel<TValues = any> implements Disposable {
const feedback = toFeedback(result, path);
const field = this.getField(path);
const errors = feedbackToFieldErrorsOrWarnings<Errors>(path, feedback);
const warnings = feedbackToFieldErrorsOrWarnings<Warnings>(path, feedback);
const errors = feedbackToFieldErrorsOrWarnings<Errors>(
path,
feedback?.level === FeedbackLevel.Error ? feedback : undefined
);
const warnings = feedbackToFieldErrorsOrWarnings<Warnings>(
path,
feedback?.level === FeedbackLevel.Warning ? feedback : undefined
);
if (field) {
field.state.errors = errors;

View File

@ -19,7 +19,7 @@ export type {
Warnings,
} from './types';
export { ValidateTrigger } from './types';
export { ValidateTrigger, FeedbackLevel } from './types';
export { createForm, type CreateFormOptions } from './core/create-form';
export { Glob } from './utils';
export * from './core';

View File

@ -24,7 +24,7 @@ export interface Feedback<FeedbackLevel> {
/**
* Feedback message
*/
message: string;
message: string | React.ReactNode;
}
export type FieldError = Feedback<FeedbackLevel.Error>;

View File

@ -1,5 +1,5 @@
import { Layer, observeEntity, PlaygroundConfigEntity, SCALE_WIDTH } from '@flowgram.ai/core';
import { domUtils } from '@flowgram.ai/utils';
import { Layer, observeEntity, PlaygroundConfigEntity, SCALE_WIDTH } from '@flowgram.ai/core';
interface BackgroundScaleUnit {
realSize: number;
@ -8,12 +8,67 @@ interface BackgroundScaleUnit {
}
const PATTERN_PREFIX = 'gedit-background-pattern-';
const RENDER_SIZE = 20;
const DOT_SIZE = 1;
const DEFAULT_RENDER_SIZE = 20;
const DEFAULT_DOT_SIZE = 1;
let id = 0;
export interface BackgroundLayerOptions {
// 预留配置项目
/** 网格间距,默认 20px */
gridSize?: number;
/** 点的大小,默认 1px */
dotSize?: number;
/** 点的颜色,默认 "#eceeef" */
dotColor?: string;
/** 点的透明度,默认 0.5 */
dotOpacity?: number;
/** 背景颜色,默认透明 */
backgroundColor?: string;
/** 点的填充颜色默认与stroke颜色相同 */
dotFillColor?: string;
/** Logo 配置 */
logo?: {
/** Logo 文本内容 */
text?: string;
/** Logo 图片 URL */
imageUrl?: string;
/** Logo 位置,默认 'center' */
position?: 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
/** Logo 大小,默认 'medium' */
size?: 'small' | 'medium' | 'large' | number;
/** Logo 透明度,默认 0.1 */
opacity?: number;
/** Logo 颜色(仅文本),默认 "#cccccc" */
color?: string;
/** Logo 字体大小(仅文本),默认根据 size 计算 */
fontSize?: number;
/** Logo 字体家族(仅文本),默认 'Arial, sans-serif' */
fontFamily?: string;
/** Logo 字体粗细(仅文本),默认 'normal' */
fontWeight?: 'normal' | 'bold' | 'lighter' | number;
/** 自定义偏移 */
offset?: { x: number; y: number };
/** 新拟态Neumorphism效果配置 */
neumorphism?: {
/** 是否启用新拟态效果,默认 false */
enabled?: boolean;
/** 主要文字颜色,应该与背景色接近,默认自动计算 */
textColor?: string;
/** 亮色阴影颜色,默认自动计算(背景色的亮色版本) */
lightShadowColor?: string;
/** 暗色阴影颜色,默认自动计算(背景色的暗色版本) */
darkShadowColor?: string;
/** 阴影偏移距离,默认 6 */
shadowOffset?: number;
/** 阴影模糊半径,默认 12 */
shadowBlur?: number;
/** 效果强度0-1影响阴影的透明度默认 0.3 */
intensity?: number;
/** 凸起效果true还是凹陷效果false默认 true */
raised?: boolean;
};
};
}
/**
* dot
*/
@ -29,6 +84,55 @@ export class BackgroundLayer extends Layer<BackgroundLayerOptions> {
grid: HTMLElement = document.createElement('div');
/**
*
*/
private get gridSize(): number {
return this.options.gridSize ?? DEFAULT_RENDER_SIZE;
}
/**
*
*/
private get dotSize(): number {
return this.options.dotSize ?? DEFAULT_DOT_SIZE;
}
/**
*
*/
private get dotColor(): string {
return this.options.dotColor ?? '#eceeef';
}
/**
*
*/
private get dotOpacity(): number {
return this.options.dotOpacity ?? 0.5;
}
/**
*
*/
private get backgroundColor(): string {
return this.options.backgroundColor ?? 'transparent';
}
/**
*
*/
private get dotFillColor(): string {
return this.options.dotFillColor ?? this.dotColor;
}
/**
* Logo配置
*/
private get logoConfig() {
return this.options.logo;
}
/**
*
*/
@ -50,6 +154,11 @@ export class BackgroundLayer extends Layer<BackgroundLayerOptions> {
this.grid.style.position = 'relative';
this.node.appendChild(this.grid);
this.grid.className = 'gedit-grid-svg';
// 设置背景颜色
if (this.backgroundColor !== 'transparent') {
this.node.style.backgroundColor = this.backgroundColor;
}
}
/**
@ -59,8 +168,8 @@ export class BackgroundLayer extends Layer<BackgroundLayerOptions> {
const { zoom } = this;
return {
realSize: RENDER_SIZE, // 一个单元格代表的真实大小
renderSize: Math.round(RENDER_SIZE * zoom * 100) / 100, // 一个单元格渲染的大小值
realSize: this.gridSize, // 使用配置的网格大小
renderSize: Math.round(this.gridSize * zoom * 100) / 100, // 一个单元格渲染的大小值
zoom, // 缩放比
};
}
@ -82,7 +191,7 @@ export class BackgroundLayer extends Layer<BackgroundLayerOptions> {
left: scrollX - SCALE_WIDTH,
top: scrollY - SCALE_WIDTH,
});
this.drawGrid(scaleUnit);
this.drawGrid(scaleUnit, viewBoxWidth, viewBoxHeight);
// 设置网格
this.setSVGStyle(this.grid, {
width: viewBoxWidth,
@ -92,34 +201,294 @@ export class BackgroundLayer extends Layer<BackgroundLayerOptions> {
});
}
/**
* Logo位置
*/
private calculateLogoPosition(
viewBoxWidth: number,
viewBoxHeight: number
): { x: number; y: number } {
if (!this.logoConfig) return { x: 0, y: 0 };
const { position = 'center', offset = { x: 0, y: 0 } } = this.logoConfig;
const playgroundConfig = this.playgroundConfigEntity.config;
const scaleUnit = this.getScaleUnit();
const mod = scaleUnit.renderSize * 10;
// 计算SVG内的相对位置使Logo相对于可视区域固定
const { scrollX, scrollY } = playgroundConfig;
const scrollXDelta = this.getScrollDelta(scrollX, mod);
const scrollYDelta = this.getScrollDelta(scrollY, mod);
// 可视区域的基准点相对于SVG坐标系
const visibleLeft = mod + scrollXDelta;
const visibleTop = mod + scrollYDelta;
const visibleCenterX = visibleLeft + playgroundConfig.width / 2;
const visibleCenterY = visibleTop + playgroundConfig.height / 2;
let x = 0,
y = 0;
switch (position) {
case 'center':
x = visibleCenterX;
y = visibleCenterY;
break;
case 'top-left':
x = visibleLeft + 100;
y = visibleTop + 100;
break;
case 'top-right':
x = visibleLeft + playgroundConfig.width - 100;
y = visibleTop + 100;
break;
case 'bottom-left':
x = visibleLeft + 100;
y = visibleTop + playgroundConfig.height - 100;
break;
case 'bottom-right':
x = visibleLeft + playgroundConfig.width - 100;
y = visibleTop + playgroundConfig.height - 100;
break;
}
return { x: x + offset.x, y: y + offset.y };
}
/**
* Logo大小
*/
private getLogoSize(): number {
if (!this.logoConfig) return 0;
const { size = 'medium' } = this.logoConfig;
if (typeof size === 'number') {
return size;
}
switch (size) {
case 'small':
return 24;
case 'medium':
return 48;
case 'large':
return 72;
default:
return 48;
}
}
/**
* RGB
*/
private hexToRgb(hex: string): { r: number; g: number; b: number } | null {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
}
/**
*
*/
private adjustBrightness(hex: string, percent: number): string {
const rgb = this.hexToRgb(hex);
if (!rgb) return hex;
const adjust = (value: number) => {
const adjusted = Math.round(value + (255 - value) * percent);
return Math.max(0, Math.min(255, adjusted));
};
return `#${adjust(rgb.r).toString(16).padStart(2, '0')}${adjust(rgb.g)
.toString(16)
.padStart(2, '0')}${adjust(rgb.b).toString(16).padStart(2, '0')}`;
}
/**
*
*/
private generateNeumorphismFilter(
filterId: string,
lightShadow: string,
darkShadow: string,
offset: number,
blur: number,
intensity: number,
raised: boolean
): string {
const lightOffset = raised ? -offset : offset;
const darkOffset = raised ? offset : -offset;
return `
<defs>
<filter id="${filterId}" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="${lightOffset}" dy="${lightOffset}" stdDeviation="${blur}" flood-color="${lightShadow}" flood-opacity="${intensity}"/>
<feDropShadow dx="${darkOffset}" dy="${darkOffset}" stdDeviation="${blur}" flood-color="${darkShadow}" flood-opacity="${intensity}"/>
</filter>
</defs>`;
}
/**
* Logo SVG内容
*/
private generateLogoSVG(viewBoxWidth: number, viewBoxHeight: number): string {
if (!this.logoConfig) return '';
const {
text,
imageUrl,
opacity = 0.1,
color = '#cccccc',
fontSize,
fontFamily = 'Arial, sans-serif',
fontWeight = 'normal',
neumorphism,
} = this.logoConfig;
const position = this.calculateLogoPosition(viewBoxWidth, viewBoxHeight);
const logoSize = this.getLogoSize();
let logoSVG = '';
if (imageUrl) {
// 图片Logo暂不支持3D效果
logoSVG = `
<image
href="${imageUrl}"
x="${position.x - logoSize / 2}"
y="${position.y - logoSize / 2}"
width="${logoSize}"
height="${logoSize}"
opacity="${opacity}"
/>`;
} else if (text) {
// 文本Logo
const actualFontSize = fontSize ?? Math.max(logoSize / 2, 12);
// 检查是否启用新拟态效果
if (neumorphism?.enabled) {
const {
textColor,
lightShadowColor,
darkShadowColor,
shadowOffset = 6,
shadowBlur = 12,
intensity = 0.3,
raised = true,
} = neumorphism;
// 自动计算颜色(如果未提供)
const bgColor = this.backgroundColor !== 'transparent' ? this.backgroundColor : '#f0f0f0';
const finalTextColor = textColor || bgColor;
const finalLightShadow = lightShadowColor || this.adjustBrightness(bgColor, 0.2);
const finalDarkShadow = darkShadowColor || this.adjustBrightness(bgColor, -0.2);
const filterId = `neumorphism-${this._patternId}`;
// 添加新拟态滤镜定义
logoSVG += this.generateNeumorphismFilter(
filterId,
finalLightShadow,
finalDarkShadow,
shadowOffset,
shadowBlur,
intensity,
raised
);
// 创建新拟态文本
logoSVG += `
<text
x="${position.x}"
y="${position.y}"
font-family="${fontFamily}"
font-size="${actualFontSize}"
font-weight="${fontWeight}"
fill="${finalTextColor}"
opacity="${opacity}"
text-anchor="middle"
dominant-baseline="middle"
filter="url(#${filterId})"
>${text}</text>`;
} else {
// 普通文本无3D效果
logoSVG = `
<text
x="${position.x}"
y="${position.y}"
font-family="${fontFamily}"
font-size="${actualFontSize}"
font-weight="${fontWeight}"
fill="${color}"
opacity="${opacity}"
text-anchor="middle"
dominant-baseline="middle"
>${text}</text>`;
}
}
return logoSVG;
}
/**
*
*/
protected drawGrid(unit: BackgroundScaleUnit): void {
protected drawGrid(unit: BackgroundScaleUnit, viewBoxWidth: number, viewBoxHeight: number): void {
const minor = unit.renderSize;
if (!this.grid) {
return;
}
const patternSize = DOT_SIZE * this.zoom;
const newContent = `
<svg width="100%" height="100%">
const patternSize = this.dotSize * this.zoom;
// 构建SVG内容根据是否有背景颜色决定是否添加背景矩形
let svgContent = `<svg width="100%" height="100%">`;
// 如果设置了背景颜色,先绘制背景矩形
if (this.backgroundColor !== 'transparent') {
svgContent += `<rect width="100%" height="100%" fill="${this.backgroundColor}"/>`;
}
// 添加点阵图案
// 构建圆圈属性,保持与原始实现的兼容性
const circleAttributes = [
`cx="${patternSize}"`,
`cy="${patternSize}"`,
`r="${patternSize}"`,
`stroke="${this.dotColor}"`,
// 只有当 dotFillColor 被明确设置且与 dotColor 不同时才添加 fill 属性
this.options.dotFillColor && this.dotFillColor !== this.dotColor
? `fill="${this.dotFillColor}"`
: '',
`fill-opacity="${this.dotOpacity}"`,
]
.filter(Boolean)
.join(' ');
svgContent += `
<pattern id="${this._patternId}" width="${minor}" height="${minor}" patternUnits="userSpaceOnUse">
<circle
cx="${patternSize}"
cy="${patternSize}"
r="${patternSize}"
stroke="#eceeef"
fill-opacity="0.5"
/>
<circle ${circleAttributes} />
</pattern>
<rect width="100%" height="100%" fill="url(#${this._patternId})"/>
</svg>`;
this.grid.innerHTML = newContent;
<rect width="100%" height="100%" fill="url(#${this._patternId})"/>`;
// 添加Logo
const logoSVG = this.generateLogoSVG(viewBoxWidth, viewBoxHeight);
if (logoSVG) {
svgContent += logoSVG;
}
svgContent += `</svg>`;
this.grid.innerHTML = svgContent;
}
protected setSVGStyle(
svgElement: HTMLElement | undefined,
style: { width: number; height: number; left: number; top: number },
style: { width: number; height: number; left: number; top: number }
): void {
if (!svgElement) {
return;