Skip to content

Commit 205fc77

Browse files
feat(background): add support for custom and complex SVG patterns
- allow multiple <pattern> elements to be collected from the host element - append all patterns to background <defs> and use the last one as fill - refactor AddPatternToBackground execution into reusable helpers - extend background example with "custom" option and custom-background component
1 parent a9ba2b8 commit 205fc77

File tree

9 files changed

+200
-43
lines changed

9 files changed

+200
-43
lines changed

projects/f-examples/extensions/background-example/background-example.component.html

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1-
<f-flow fDraggable (fLoaded)="onLoaded()">
2-
@if (background === 'circle') {
3-
<f-background>
4-
<f-circle-pattern></f-circle-pattern>
5-
</f-background>
6-
} @else if (background === 'rect') {
7-
<f-background>
8-
<f-rect-pattern></f-rect-pattern>
9-
</f-background>
1+
<f-flow fDraggable (fLoaded)="loaded()">
2+
@switch (background()) {
3+
@case ('circle') {
4+
<f-background>
5+
<f-circle-pattern />
6+
</f-background>
7+
}
8+
@case ('rect') {
9+
<f-background>
10+
<f-rect-pattern />
11+
</f-background>
12+
}
13+
@case ('custom') {
14+
<f-background>
15+
<custom-background-example />
16+
</f-background>
17+
}
1018
}
11-
<f-canvas>
12-
<f-connection fOutputId="output1" fInputId="input1" fBehavior="floating"></f-connection>
19+
<f-canvas fZoom>
20+
<f-connection fOutputId="output1" fInputId="input1" fBehavior="floating" />
1321
<div fNode [fNodePosition]="{ x: 0, y: 0 }" fDragHandle>
1422
<div fNodeOutput fOutputId="output1" class="right"></div>
1523
I'm a node
Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
1-
import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core';
1+
import { ChangeDetectionStrategy, Component, signal, viewChild } from '@angular/core';
22
import { FCanvasComponent, FFlowModule } from '@foblex/flow';
33
import { MatFormField, MatLabel } from '@angular/material/form-field';
44
import { MatOption } from '@angular/material/core';
55
import { MatSelectModule } from '@angular/material/select';
6+
import { FlowBackground } from './custom-background-example/custom-background-example';
67

78
@Component({
89
selector: 'background-example',
910
styleUrls: ['./background-example.component.scss'],
1011
templateUrl: './background-example.component.html',
1112
changeDetection: ChangeDetectionStrategy.OnPush,
1213
standalone: true,
13-
imports: [FFlowModule, MatFormField, MatLabel, MatOption, MatSelectModule],
14+
imports: [FFlowModule, MatFormField, MatLabel, MatOption, MatSelectModule, FlowBackground],
1415
})
1516
export class BackgroundExampleComponent {
16-
@ViewChild(FCanvasComponent, { static: true })
17-
public fCanvas!: FCanvasComponent;
17+
private readonly _canvas = viewChild.required(FCanvasComponent);
1818

19-
public background = 'circle';
19+
protected readonly background = signal('custom');
2020

21-
public backgroundOptions = ['circle', 'rect', 'none'];
21+
protected readonly backgroundOptions = ['circle', 'rect', 'custom', 'none'];
2222

23-
public onLoaded(): void {
24-
this.fCanvas.resetScaleAndCenter(false);
23+
protected loaded(): void {
24+
this._canvas()?.resetScaleAndCenter(false);
2525
}
2626
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<svg:pattern #circlePattern id="circleGrid" patternUnits="userSpaceOnUse">
2+
<svg:circle #circle></svg:circle>
3+
</svg:pattern>
4+
<svg:pattern #rectPattern patternUnits="userSpaceOnUse">
5+
<svg:rect width="100%" height="100%" fill="url(#circleGrid)"></svg:rect>
6+
<svg:line x1="0" y1="0" x2="100%" y2="0" stroke-width="2"></svg:line>
7+
<svg:line x1="0" y1="0" x2="0" y2="100%" stroke-width="2"></svg:line>
8+
</svg:pattern>
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
DestroyRef,
5+
ElementRef,
6+
inject,
7+
input,
8+
numberAttribute,
9+
OnInit,
10+
viewChild,
11+
} from '@angular/core';
12+
import {
13+
F_BACKGROUND_PATTERN,
14+
FChannel,
15+
FChannelHub,
16+
FFlowModule,
17+
IFBackgroundPattern,
18+
notifyOnStart,
19+
} from '@foblex/flow';
20+
import {
21+
ITransformModel,
22+
PointExtensions,
23+
SizeExtensions,
24+
TransformModelExtensions,
25+
} from '@foblex/2d';
26+
27+
@Component({
28+
selector: 'custom-background-example',
29+
templateUrl: './custom-background-example.html',
30+
changeDetection: ChangeDetectionStrategy.OnPush,
31+
standalone: true,
32+
imports: [FFlowModule],
33+
providers: [{ provide: F_BACKGROUND_PATTERN, useExisting: FlowBackground }],
34+
})
35+
export class FlowBackground implements OnInit, IFBackgroundPattern {
36+
private readonly _destroyRef = inject(DestroyRef);
37+
private readonly _elementReference = inject(ElementRef);
38+
39+
private readonly _stateChanges = new FChannel();
40+
41+
public get hostElement(): HTMLElement {
42+
return this._elementReference.nativeElement;
43+
}
44+
45+
public readonly size = input(80, { transform: numberAttribute });
46+
public readonly circleSize = input(16, { transform: numberAttribute });
47+
48+
private _transform = TransformModelExtensions.default();
49+
50+
private _position = PointExtensions.initialize();
51+
52+
private _size = SizeExtensions.initialize(this.size(), this.size());
53+
private _radius = this.circleSize();
54+
55+
private readonly _circlePattern = viewChild<ElementRef<SVGPatternElement>>('circlePattern');
56+
private readonly _circlePatternCircle = viewChild<ElementRef<SVGCircleElement>>('circle');
57+
58+
private readonly _rectPattern = viewChild<ElementRef<SVGPatternElement>>('rectPattern');
59+
60+
public ngOnInit(): void {
61+
this._listenStateChanges();
62+
}
63+
64+
private _listenStateChanges(): void {
65+
new FChannelHub(this._stateChanges)
66+
.pipe(notifyOnStart())
67+
.listen(this._destroyRef, () => this._redraw());
68+
}
69+
70+
private _redraw(): void {
71+
this._calculatePattern();
72+
this._redrawPattern();
73+
}
74+
75+
private _calculatePattern(): void {
76+
this._position.x = this._transform.position.x + this._transform.scaledPosition.x;
77+
this._position.y = this._transform.position.y + this._transform.scaledPosition.y;
78+
this._size = SizeExtensions.initialize(
79+
this.size() * this._transform.scale,
80+
this.size() * this._transform.scale,
81+
);
82+
this._radius = this.circleSize() * this._transform.scale;
83+
}
84+
85+
private _redrawPattern(): void {
86+
this._redrawRectPattern();
87+
this._redrawCirclePattern();
88+
}
89+
90+
private _redrawRectPattern(): void {
91+
this._rectPattern()?.nativeElement.setAttribute('x', `${this._position.x}`);
92+
this._rectPattern()?.nativeElement.setAttribute('y', `${this._position.y}`);
93+
this._rectPattern()?.nativeElement.setAttribute('width', `${this._size.width}`);
94+
this._rectPattern()?.nativeElement.setAttribute('height', `${this._size.height}`);
95+
}
96+
97+
private _redrawCirclePattern(): void {
98+
this._circlePattern()?.nativeElement.setAttribute('width', `${this._radius}`);
99+
this._circlePattern()?.nativeElement.setAttribute('height', `${this._radius}`);
100+
this._redrawCircleElement();
101+
}
102+
103+
private _redrawCircleElement(): void {
104+
this._circlePatternCircle()?.nativeElement.setAttribute('r', `${this._transform.scale}`);
105+
this._circlePatternCircle()?.nativeElement.setAttribute('cx', `${this._transform.scale / 2}`);
106+
this._circlePatternCircle()?.nativeElement.setAttribute('cy', `${this._transform.scale / 2}`);
107+
}
108+
109+
public setTransform(transform: ITransformModel): void {
110+
this._transform = transform;
111+
this._refresh();
112+
}
113+
114+
private _refresh(): void {
115+
this._stateChanges.notify();
116+
}
117+
}

projects/f-flow/src/domain/f-background/add-pattern-to-background/add-pattern-to-background.request.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import { IFBackgroundPattern } from '../../../f-backgroud';
22

33
export class AddPatternToBackgroundRequest {
44
static readonly fToken = Symbol('AddPatternToBackgroundRequest');
5-
constructor(
6-
public fPattern: IFBackgroundPattern | undefined,
7-
) {
8-
}
5+
6+
constructor(public fPattern: IFBackgroundPattern | undefined) {}
97
}

projects/f-flow/src/domain/f-background/add-pattern-to-background/add-pattern-to-background.execution.ts renamed to projects/f-flow/src/domain/f-background/add-pattern-to-background/add-pattern-to-background.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@ let uniqueId = 0;
1313
*/
1414
@Injectable()
1515
@FExecutionRegister(AddPatternToBackgroundRequest)
16-
export class AddPatternToBackgroundExecution
17-
implements IExecution<AddPatternToBackgroundRequest, void>
18-
{
16+
export class AddPatternToBackground implements IExecution<AddPatternToBackgroundRequest, void> {
1917
private readonly _store = inject(FComponentsStore);
2018
private readonly _browser = inject(BrowserService);
2119

@@ -24,21 +22,32 @@ export class AddPatternToBackgroundExecution
2422
}
2523

2624
public handle(request: AddPatternToBackgroundRequest): void {
27-
const children = request.fPattern?.hostElement.getElementsByTagName('pattern') || [];
28-
const pattern = children.length ? children[0] : undefined;
29-
if (pattern) {
30-
const defs = createSVGElement('defs', this._browser);
31-
pattern.id = 'f-background-pattern-' + uniqueId++;
32-
request.fPattern?.hostElement.remove();
33-
defs.appendChild(pattern);
25+
const patterns = this._getPatterns(request.fPattern?.hostElement);
26+
if (!patterns?.length) {
27+
return;
28+
}
29+
const defs = createSVGElement('defs', this._browser);
30+
request.fPattern?.hostElement.remove();
31+
32+
patterns.forEach((pattern) => {
33+
defs.append(pattern);
34+
});
35+
36+
if (patterns.length) {
3437
this._backgroundElement?.firstChild?.appendChild(defs);
38+
patterns[patterns.length - 1].id = 'f-background-pattern-' + uniqueId++;
39+
const lastPatternId = patterns[patterns.length - 1].id;
3540
const rect = createSVGElement('rect', this._browser);
36-
rect.setAttribute('fill', 'url(#' + pattern.id + ')');
41+
rect.setAttribute('fill', 'url(#' + lastPatternId + ')');
3742
rect.setAttribute('width', '100%');
3843
rect.setAttribute('height', '100%');
3944
this._backgroundElement.firstChild?.appendChild(rect);
4045
const transform = this._store.fCanvas?.transform || TransformModelExtensions.default();
4146
request.fPattern?.setTransform(transform);
4247
}
4348
}
49+
50+
private _getPatterns(element?: HTMLElement | SVGElement | undefined): SVGPatternElement[] {
51+
return Array.from(element?.getElementsByTagName('pattern') || []);
52+
}
4453
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export * from './add-pattern-to-background.execution';
1+
export * from './add-pattern-to-background';
22

33
export * from './add-pattern-to-background.request';

projects/f-flow/src/domain/f-background/providers.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AddPatternToBackgroundExecution } from './add-pattern-to-background';
1+
import { AddPatternToBackground } from './add-pattern-to-background';
22
import { AddBackgroundToStoreExecution } from './add-background-to-store';
33
import { RemoveBackgroundFromStoreExecution } from './remove-background-from-store';
44
import { SetBackgroundTransformExecution } from './set-background-transform';
@@ -8,10 +8,9 @@ import { SetBackgroundTransformExecution } from './set-background-transform';
88
* It includes executions for adding, removing, and setting the background in the FComponentsStore.
99
*/
1010
export const F_BACKGROUND_FEATURES = [
11-
1211
AddBackgroundToStoreExecution,
1312

14-
AddPatternToBackgroundExecution,
13+
AddPatternToBackground,
1514

1615
RemoveBackgroundFromStoreExecution,
1716

public/markdown/examples/background.md

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,35 @@
22

33
## Description
44

5-
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).
6-
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)).
5+
The background feature allows you to render an SVG pattern underneath all nodes and connections on the [canvas](./docs/f-canvas-component).
6+
It is useful for creating grids, subtle textures, or branded backgrounds that stay fixed while the user interacts with the flow.
7+
8+
To enable a background, place the [f-background component](./docs/f-background-component) **inside** the [f-flow component](./docs/f-flow-component).
9+
The `f-background` component is responsible for defining and attaching the SVG pattern that will be used as the canvas background.
10+
11+
Backgrounds can work in three modes:
12+
13+
- **`rect`** – built-in rectangular pattern (useful for grid-like backgrounds);
14+
- **`circle`** – built-in circular pattern;
15+
- **`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.).
16+
17+
The `custom` mode is configured via standard SVG `<pattern>` elements and a custom component.
18+
Detailed instructions and API for creating custom patterns are described in the [f-background documentation](./docs/f-background-component).
719

820
## Example
921

22+
The example below shows how to:
23+
24+
- wrap `f-canvas` with `f-flow`;
25+
- add `f-background` to enable a background;
26+
- switch between the built-in `rect` and `circle` patterns;
27+
- use the new `custom` mode with a custom background implementation.
28+
1029
::: ng-component <background-example></background-example> [height]="600"
1130
[component.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/extensions/background-example/background-example.component.html
1231
[component.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/extensions/background-example/background-example.component.ts
1332
[component.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/extensions/background-example/background-example.component.scss
33+
[custom.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/extensions/background-example/custom-background-example/custom-background-example.html
34+
[custom.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/extensions/background-example/custom-background-example/custom-background-example.ts
1435
[common.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/_flow-common.scss
1536
:::
16-
17-
18-

0 commit comments

Comments
 (0)