diff --git a/projects/f-examples/extensions/background-example/background-example.component.html b/projects/f-examples/extensions/background-example/background-example.component.html index 8113b899..58dc8e69 100644 --- a/projects/f-examples/extensions/background-example/background-example.component.html +++ b/projects/f-examples/extensions/background-example/background-example.component.html @@ -1,15 +1,23 @@ - - @if (background === 'circle') { - - - - } @else if (background === 'rect') { - - - + + @switch (background()) { + @case ('circle') { + + + + } + @case ('rect') { + + + + } + @case ('custom') { + + + + } } - - + +
I'm a node diff --git a/projects/f-examples/extensions/background-example/background-example.component.ts b/projects/f-examples/extensions/background-example/background-example.component.ts index 948b5080..81067e88 100644 --- a/projects/f-examples/extensions/background-example/background-example.component.ts +++ b/projects/f-examples/extensions/background-example/background-example.component.ts @@ -1,8 +1,9 @@ -import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core'; +import { ChangeDetectionStrategy, Component, signal, viewChild } from '@angular/core'; import { FCanvasComponent, FFlowModule } from '@foblex/flow'; import { MatFormField, MatLabel } from '@angular/material/form-field'; import { MatOption } from '@angular/material/core'; import { MatSelectModule } from '@angular/material/select'; +import { FlowBackground } from './custom-background-example/custom-background-example'; @Component({ selector: 'background-example', @@ -10,17 +11,16 @@ import { MatSelectModule } from '@angular/material/select'; templateUrl: './background-example.component.html', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [FFlowModule, MatFormField, MatLabel, MatOption, MatSelectModule], + imports: [FFlowModule, MatFormField, MatLabel, MatOption, MatSelectModule, FlowBackground], }) export class BackgroundExampleComponent { - @ViewChild(FCanvasComponent, { static: true }) - public fCanvas!: FCanvasComponent; + private readonly _canvas = viewChild.required(FCanvasComponent); - public background = 'circle'; + protected readonly background = signal('custom'); - public backgroundOptions = ['circle', 'rect', 'none']; + protected readonly backgroundOptions = ['circle', 'rect', 'custom', 'none']; - public onLoaded(): void { - this.fCanvas.resetScaleAndCenter(false); + protected loaded(): void { + this._canvas()?.resetScaleAndCenter(false); } } diff --git a/projects/f-examples/extensions/background-example/custom-background-example/custom-background-example.html b/projects/f-examples/extensions/background-example/custom-background-example/custom-background-example.html new file mode 100644 index 00000000..2232f1c7 --- /dev/null +++ b/projects/f-examples/extensions/background-example/custom-background-example/custom-background-example.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/projects/f-examples/extensions/background-example/custom-background-example/custom-background-example.ts b/projects/f-examples/extensions/background-example/custom-background-example/custom-background-example.ts new file mode 100644 index 00000000..cdbb4f08 --- /dev/null +++ b/projects/f-examples/extensions/background-example/custom-background-example/custom-background-example.ts @@ -0,0 +1,117 @@ +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + ElementRef, + inject, + input, + numberAttribute, + OnInit, + viewChild, +} from '@angular/core'; +import { + F_BACKGROUND_PATTERN, + FChannel, + FChannelHub, + FFlowModule, + IFBackgroundPattern, + notifyOnStart, +} from '@foblex/flow'; +import { + ITransformModel, + PointExtensions, + SizeExtensions, + TransformModelExtensions, +} from '@foblex/2d'; + +@Component({ + selector: 'custom-background-example', + templateUrl: './custom-background-example.html', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [FFlowModule], + providers: [{ provide: F_BACKGROUND_PATTERN, useExisting: FlowBackground }], +}) +export class FlowBackground implements OnInit, IFBackgroundPattern { + private readonly _destroyRef = inject(DestroyRef); + private readonly _elementReference = inject(ElementRef); + + private readonly _stateChanges = new FChannel(); + + public get hostElement(): HTMLElement { + return this._elementReference.nativeElement; + } + + public readonly size = input(80, { transform: numberAttribute }); + public readonly circleSize = input(16, { transform: numberAttribute }); + + private _transform = TransformModelExtensions.default(); + + private _position = PointExtensions.initialize(); + + private _size = SizeExtensions.initialize(this.size(), this.size()); + private _radius = this.circleSize(); + + private readonly _circlePattern = viewChild>('circlePattern'); + private readonly _circlePatternCircle = viewChild>('circle'); + + private readonly _rectPattern = viewChild>('rectPattern'); + + public ngOnInit(): void { + this._listenStateChanges(); + } + + private _listenStateChanges(): void { + new FChannelHub(this._stateChanges) + .pipe(notifyOnStart()) + .listen(this._destroyRef, () => this._redraw()); + } + + private _redraw(): void { + this._calculatePattern(); + this._redrawPattern(); + } + + private _calculatePattern(): void { + this._position.x = this._transform.position.x + this._transform.scaledPosition.x; + this._position.y = this._transform.position.y + this._transform.scaledPosition.y; + this._size = SizeExtensions.initialize( + this.size() * this._transform.scale, + this.size() * this._transform.scale, + ); + this._radius = this.circleSize() * this._transform.scale; + } + + private _redrawPattern(): void { + this._redrawRectPattern(); + this._redrawCirclePattern(); + } + + private _redrawRectPattern(): void { + this._rectPattern()?.nativeElement.setAttribute('x', `${this._position.x}`); + this._rectPattern()?.nativeElement.setAttribute('y', `${this._position.y}`); + this._rectPattern()?.nativeElement.setAttribute('width', `${this._size.width}`); + this._rectPattern()?.nativeElement.setAttribute('height', `${this._size.height}`); + } + + private _redrawCirclePattern(): void { + this._circlePattern()?.nativeElement.setAttribute('width', `${this._radius}`); + this._circlePattern()?.nativeElement.setAttribute('height', `${this._radius}`); + this._redrawCircleElement(); + } + + private _redrawCircleElement(): void { + this._circlePatternCircle()?.nativeElement.setAttribute('r', `${this._transform.scale}`); + this._circlePatternCircle()?.nativeElement.setAttribute('cx', `${this._transform.scale / 2}`); + this._circlePatternCircle()?.nativeElement.setAttribute('cy', `${this._transform.scale / 2}`); + } + + public setTransform(transform: ITransformModel): void { + this._transform = transform; + this._refresh(); + } + + private _refresh(): void { + this._stateChanges.notify(); + } +} diff --git a/projects/f-flow/package.json b/projects/f-flow/package.json index fa7eed4e..edd7badc 100644 --- a/projects/f-flow/package.json +++ b/projects/f-flow/package.json @@ -1,6 +1,6 @@ { "name": "@foblex/flow", - "version": "17.9.7", + "version": "17.9.8", "description": "An Angular library designed to simplify the creation and manipulation of dynamic flow. Provides components for flows, nodes, and connections, automating node manipulation and inter-node connections.", "main": "index.js", "types": "index.d.ts", diff --git a/projects/f-flow/src/domain/f-background/add-pattern-to-background/add-pattern-to-background.request.ts b/projects/f-flow/src/domain/f-background/add-pattern-to-background/add-pattern-to-background.request.ts index a91ade96..2b484dcf 100644 --- a/projects/f-flow/src/domain/f-background/add-pattern-to-background/add-pattern-to-background.request.ts +++ b/projects/f-flow/src/domain/f-background/add-pattern-to-background/add-pattern-to-background.request.ts @@ -2,8 +2,6 @@ import { IFBackgroundPattern } from '../../../f-backgroud'; export class AddPatternToBackgroundRequest { static readonly fToken = Symbol('AddPatternToBackgroundRequest'); - constructor( - public fPattern: IFBackgroundPattern | undefined, - ) { - } + + constructor(public fPattern: IFBackgroundPattern | undefined) {} } diff --git a/projects/f-flow/src/domain/f-background/add-pattern-to-background/add-pattern-to-background.execution.ts b/projects/f-flow/src/domain/f-background/add-pattern-to-background/add-pattern-to-background.ts similarity index 61% rename from projects/f-flow/src/domain/f-background/add-pattern-to-background/add-pattern-to-background.execution.ts rename to projects/f-flow/src/domain/f-background/add-pattern-to-background/add-pattern-to-background.ts index 2bbf9dfa..50505cec 100644 --- a/projects/f-flow/src/domain/f-background/add-pattern-to-background/add-pattern-to-background.execution.ts +++ b/projects/f-flow/src/domain/f-background/add-pattern-to-background/add-pattern-to-background.ts @@ -13,9 +13,7 @@ let uniqueId = 0; */ @Injectable() @FExecutionRegister(AddPatternToBackgroundRequest) -export class AddPatternToBackgroundExecution - implements IExecution -{ +export class AddPatternToBackground implements IExecution { private readonly _store = inject(FComponentsStore); private readonly _browser = inject(BrowserService); @@ -24,16 +22,23 @@ export class AddPatternToBackgroundExecution } public handle(request: AddPatternToBackgroundRequest): void { - const children = request.fPattern?.hostElement.getElementsByTagName('pattern') || []; - const pattern = children.length ? children[0] : undefined; - if (pattern) { - const defs = createSVGElement('defs', this._browser); - pattern.id = 'f-background-pattern-' + uniqueId++; - request.fPattern?.hostElement.remove(); - defs.appendChild(pattern); + const patterns = this._getPatterns(request.fPattern?.hostElement); + if (!patterns?.length) { + return; + } + const defs = createSVGElement('defs', this._browser); + request.fPattern?.hostElement.remove(); + + patterns.forEach((pattern) => { + defs.append(pattern); + }); + + if (patterns.length) { this._backgroundElement?.firstChild?.appendChild(defs); + patterns[patterns.length - 1].id = 'f-background-pattern-' + uniqueId++; + const lastPatternId = patterns[patterns.length - 1].id; const rect = createSVGElement('rect', this._browser); - rect.setAttribute('fill', 'url(#' + pattern.id + ')'); + rect.setAttribute('fill', 'url(#' + lastPatternId + ')'); rect.setAttribute('width', '100%'); rect.setAttribute('height', '100%'); this._backgroundElement.firstChild?.appendChild(rect); @@ -41,4 +46,8 @@ export class AddPatternToBackgroundExecution request.fPattern?.setTransform(transform); } } + + private _getPatterns(element?: HTMLElement | SVGElement | undefined): SVGPatternElement[] { + return Array.from(element?.getElementsByTagName('pattern') || []); + } } diff --git a/projects/f-flow/src/domain/f-background/add-pattern-to-background/index.ts b/projects/f-flow/src/domain/f-background/add-pattern-to-background/index.ts index 5da80aae..18a215f8 100644 --- a/projects/f-flow/src/domain/f-background/add-pattern-to-background/index.ts +++ b/projects/f-flow/src/domain/f-background/add-pattern-to-background/index.ts @@ -1,3 +1,3 @@ -export * from './add-pattern-to-background.execution'; +export * from './add-pattern-to-background'; export * from './add-pattern-to-background.request'; diff --git a/projects/f-flow/src/domain/f-background/providers.ts b/projects/f-flow/src/domain/f-background/providers.ts index 812c374a..804ab485 100644 --- a/projects/f-flow/src/domain/f-background/providers.ts +++ b/projects/f-flow/src/domain/f-background/providers.ts @@ -1,4 +1,4 @@ -import { AddPatternToBackgroundExecution } from './add-pattern-to-background'; +import { AddPatternToBackground } from './add-pattern-to-background'; import { AddBackgroundToStoreExecution } from './add-background-to-store'; import { RemoveBackgroundFromStoreExecution } from './remove-background-from-store'; import { SetBackgroundTransformExecution } from './set-background-transform'; @@ -8,10 +8,9 @@ import { SetBackgroundTransformExecution } from './set-background-transform'; * It includes executions for adding, removing, and setting the background in the FComponentsStore. */ export const F_BACKGROUND_FEATURES = [ - AddBackgroundToStoreExecution, - AddPatternToBackgroundExecution, + AddPatternToBackground, RemoveBackgroundFromStoreExecution, diff --git a/public/markdown/examples/background.md b/public/markdown/examples/background.md index 1f71b949..a6c71d8f 100644 --- a/public/markdown/examples/background.md +++ b/public/markdown/examples/background.md @@ -2,17 +2,35 @@ ## Description -This guide demonstrates how to add a background to the [canvas](./docs/f-canvas-component). To enable this feature, you need to add the [f-background component](./docs/f-background-component) inside the [f-flow component](./docs/f-flow-component). -There are two default patterns: ‘rect’ and ‘circle’, but you can also create your own custom pattern (instructions are available in the [f-background documentation](./docs/f-background-component)). +The background feature allows you to render an SVG pattern underneath all nodes and connections on the [canvas](./docs/f-canvas-component). +It is useful for creating grids, subtle textures, or branded backgrounds that stay fixed while the user interacts with the flow. + +To enable a background, place the [f-background component](./docs/f-background-component) **inside** the [f-flow component](./docs/f-flow-component). +The `f-background` component is responsible for defining and attaching the SVG pattern that will be used as the canvas background. + +Backgrounds can work in three modes: + +- **`rect`** – built-in rectangular pattern (useful for grid-like backgrounds); +- **`circle`** – built-in circular pattern; +- **`custom`** – a mode that lets you provide your **own SVG pattern**, so you can build complex or branded backgrounds (multiple patterns, custom shapes, textures, etc.). + +The `custom` mode is configured via standard SVG `` elements and a custom component. +Detailed instructions and API for creating custom patterns are described in the [f-background documentation](./docs/f-background-component). ## Example +The example below shows how to: + +- wrap `f-canvas` with `f-flow`; +- add `f-background` to enable a background; +- switch between the built-in `rect` and `circle` patterns; +- use the new `custom` mode with a custom background implementation. + ::: ng-component [height]="600" [component.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/extensions/background-example/background-example.component.html [component.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/extensions/background-example/background-example.component.ts [component.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/extensions/background-example/background-example.component.scss +[custom.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/extensions/background-example/custom-background-example/custom-background-example.html +[custom.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/extensions/background-example/custom-background-example/custom-background-example.ts [common.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/_flow-common.scss ::: - - - diff --git a/src/app/examples.config.ts b/src/app/examples.config.ts index 1de8d767..e9901bf1 100644 --- a/src/app/examples.config.ts +++ b/src/app/examples.config.ts @@ -883,13 +883,17 @@ function extensionGroup() { link: 'background', text: 'Background', description: - 'Add background shapes and frames to structure the canvas. Layering and styling patterns in Angular.', + 'Add SVG background patterns to the flow diagrams in Angular, using built-in rect and circle presets or a fully custom pattern for complex, branded backgrounds.', image: './previews/examples/background-example.light.png', image_dark: './previews/examples/background-example.dark.png', image_width: 821, image_height: 600, image_type: 'image/png', - date: new Date('2024-10-06 14:49:44'), + date: new Date('2025-11-29 14:49:44'), + badge: { + text: 'Updated', + type: 'info', + }, }, ]); }