From 5be11f439be68dd17c38ba31e2310fef6aba1a14 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 13 Sep 2024 20:27:10 +0300 Subject: [PATCH] UI: Add css animations to SCADA symbol API. --- .../widget/lib/scada/scada-symbol.models.ts | 304 +++++++++++++++++- 1 file changed, 302 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts index 837e3a44ee..0418665d69 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts @@ -15,7 +15,7 @@ /// import { ValueType } from '@shared/models/constants'; -import { Box, Element, Runner, SVG, Svg, Text, Timeline } from '@svgdotjs/svg.js'; +import { Box, Element, Runner, Style, SVG, Svg, Text, Timeline } from '@svgdotjs/svg.js'; import '@svgdotjs/svg.panzoom.js'; import { DataToValueType, @@ -58,6 +58,7 @@ export interface ScadaSymbolApi { enable: (element: Element | Element[]) => void; callAction: (event: Event, behaviorId: string, value?: any, observer?: Partial>) => void; setValue: (valueId: string, value: any) => void; + cssAnimate: CssScadaSymbolAnimations; } export interface ScadaSymbolContext { @@ -621,7 +622,8 @@ export class ScadaSymbolObject { disable: this.disableElement.bind(this), enable: this.enableElement.bind(this), callAction: this.callAction.bind(this), - setValue: this.setValue.bind(this) + setValue: this.setValue.bind(this), + cssAnimate: new CssScadaSymbolAnimations(this.svgShape) }, tags: {}, properties: {}, @@ -1008,3 +1010,301 @@ export class ScadaSymbolObject { } } } + +const scadaSymbolAnimationId = 'scadaSymbolAnimation'; + +class CssScadaSymbolAnimations { + constructor(private svgShape: Svg) {} + + public rotate(element: Element, rotation: number, duration = 1000, loop = false): CssScadaSymbolAnimation { + this.checkOldAnimation(element); + return this.setupAnimation(element, + new RotateCssScadaSymbolAnimation(this.svgShape, element, loop, rotation || 0)).duration(duration); + } + + public move(element: Element, deltaX: number, deltaY: number, duration = 1000, loop = false): CssScadaSymbolAnimation { + this.checkOldAnimation(element); + return this.setupAnimation(element, + new MoveCssScadaSymbolAnimation(this.svgShape, element, loop, deltaX, deltaY).duration(duration)); + } + + public attr(element: Element, attrName: string, value: any, duration = 1000, loop = false): CssScadaSymbolAnimation { + this.checkOldAnimation(element); + return this.setupAnimation(element, + new AttrsCssScadaSymbolAnimation(this.svgShape, element, loop, {[attrName]: value}).duration(duration)); + } + + public attrs(element: Element, attr: any, duration = 1000, loop = false): CssScadaSymbolAnimation { + this.checkOldAnimation(element); + return this.setupAnimation(element, + new AttrsCssScadaSymbolAnimation(this.svgShape, element, loop, attr).duration(duration)); + } + + public animation(element: Element): CssScadaSymbolAnimation | undefined { + return element.remember(scadaSymbolAnimationId); + } + + private checkOldAnimation(element: Element) { + const previousAnimation: CssScadaSymbolAnimation = element.remember(scadaSymbolAnimationId); + if (previousAnimation) { + previousAnimation.destroy(); + } + } + + private setupAnimation(element: Element, animation: CssScadaSymbolAnimation): CssScadaSymbolAnimation { + animation.init(); + element.remember(scadaSymbolAnimationId, animation); + return animation; + } +} + +interface ScadaSymbolAnimationKeyframe { + stop: string; + style: any; +} + +abstract class CssScadaSymbolAnimation { + + private _animationName: string; + private _animationStyle: Style; + + private _running = true; + private _speed = 1; + private _duration = 1000; + private _easing = 'linear'; + + protected constructor(protected svgShape: Svg, + protected element: Element, + protected loop: boolean) { + } + + public init() { + this.prepareAnimation(); + } + + public start() { + if (!this._running) { + this.updateAnimationStyle('animation-play-state', 'running'); + this._running = true; + } + } + + public pause() { + if (this._running) { + this.updateAnimationStyle('animation-play-state', 'paused'); + this._running = false; + } + } + + public running(): boolean { + return this._running; + } + + public destroy() { + if (this._animationStyle) { + this._animationStyle.remove(); + this.element.removeClass(this._animationName); + this._animationStyle = null; + this._animationName = null; + } + } + + public duration(duration: number): CssScadaSymbolAnimation { + this._duration = duration; + this.updateAnimationStyle(this.loop ? 'animation-duration' : 'transition-duration', + Math.round(this._duration / this._speed) + 'ms'); + return this; + } + + public speed(speed: number): CssScadaSymbolAnimation { + this._speed = speed; + this.updateAnimationStyle(this.loop ? 'animation-duration' : 'transition-duration', + Math.round(this._duration / this._speed) + 'ms'); + return this; + } + + public easing(easing: string): CssScadaSymbolAnimation { + this._easing = easing; + this.updateAnimationStyle(this.loop ? 'animation-timing-function' : 'transition-timing-function', this._easing); + return this; + } + + private prepareAnimation() { + this._animationName = 'animation_' + generateElementId(); + this._animationStyle = this.svgShape.style(); + let styles: any; + if (this.loop) { + styles = { + 'animation-name': this._animationName, + 'animation-duration': this._duration + 'ms', + 'animation-timing-function': this._easing, + 'animation-iteration-count': 'infinite', + 'animation-fill-mode': 'forwards', + 'animation-play-state': 'running', + ...this.animationStyles() + }; + } else { + styles = { + 'transition-property': this.transitionProperties(), + 'transition-duration': this._duration + 'ms', + 'transition-timing-function': this._easing, + ...this.animationStyles() + }; + } + this._animationStyle.rule('.' + this._animationName, styles); + + if (this.loop) { + const keyframes = this.animationKeyframes(); + let keyframesCss = `\n@keyframes ${this._animationName} {\n`; + for (const keyframe of keyframes) { + let keyframeCss = ` ${keyframe.stop} {\n`; + for (const i of Object.keys(keyframe.style)) { + keyframeCss += ' ' + i + ':' + keyframe.style[i] + ';\n'; + } + keyframeCss += ' }\n'; + keyframesCss += keyframeCss; + } + keyframesCss += '}'; + this._animationStyle.addText(keyframesCss); + } + this.element.addClass(this._animationName); + if (!this.loop) { + this.doTransform(); + } + } + + private updateAnimationStyle(attrName: string, value: any) { + if (this._animationStyle) { + const styleText = this._animationStyle.node.innerHTML; + const attrValueRegex = new RegExp(`${attrName}:([^;]+);`); + this._animationStyle.node.innerHTML = styleText.replace(attrValueRegex, `${attrName}:${value};`); + } + } + + protected animationStyles(): any { + return {}; + } + + protected abstract animationKeyframes(): ScadaSymbolAnimationKeyframe[]; + + protected abstract transitionProperties(): string; + + protected doTransform() { + } + +} + +class RotateCssScadaSymbolAnimation extends CssScadaSymbolAnimation { + + constructor(protected svgShape: Svg, + protected element: Element, + protected loop: boolean, + private rotation: number) { + super(svgShape, element, loop); + } + + protected animationStyles(): any { + return { + 'transform-origin': `${this.element.cx()}px ${this.element.cy()}px` + }; + } + + protected animationKeyframes(): ScadaSymbolAnimationKeyframe[] { + const transform = this.element.transform(); + return [ + { + stop: '0%', + style: { + transform: `translate(${transform.translateX}px, ${transform.translateY}px) rotate(${transform.rotate}deg)` + } + }, + { + stop: '100%', + style: { + transform: `translate(${transform.translateX}px, ${transform.translateY}px) rotate(${this.rotation}deg)` + } + } + ]; + } + + protected transitionProperties(): string { + return 'transform'; + } + + protected doTransform() { + const transform = this.element.transform(); + this.element.attr({transform: `translate(${transform.translateX} ${transform.translateY}) rotate(${this.rotation})`}); + } + +} + +class MoveCssScadaSymbolAnimation extends CssScadaSymbolAnimation { + + constructor(protected svgShape: Svg, + protected element: Element, + protected loop: boolean, + private deltaX: number, + private deltaY: number) { + super(svgShape, element, loop); + } + + protected animationStyles(): any { + return { + 'transform-origin': `${this.element.cx()}px ${this.element.cy()}px` + }; + } + + protected animationKeyframes(): ScadaSymbolAnimationKeyframe[] { + const transform = this.element.transform(); + return [ + { + stop: '0%', + style: { + transform: `translate(${transform.translateX}px, ${transform.translateY}px)` + } + }, + { + stop: '100%', + style: { + transform: `translate(${transform.translateX+this.deltaX}px, ${transform.translateY+this.deltaY}px)` + } + } + ]; + } + + protected transitionProperties(): string { + return 'transform'; + } + + protected doTransform() { + this.element.relative(this.deltaX, this.deltaY); + } + +} + +class AttrsCssScadaSymbolAnimation extends CssScadaSymbolAnimation { + + constructor(protected svgShape: Svg, + protected element: Element, + protected loop: boolean, + private attr: any) { + super(svgShape, element, loop); + } + + protected animationStyles(): any { + return {}; + } + + protected animationKeyframes(): ScadaSymbolAnimationKeyframe[] { + return []; + } + + protected transitionProperties(): string { + return Object.keys(this.attr).join(' '); + } + + protected doTransform() { + this.element.attr(this.attr); + } + +}