From 0b4a78d78eb09284d295909ee6937996b7ee0bfa Mon Sep 17 00:00:00 2001 From: skie1997 Date: Wed, 9 Apr 2025 17:59:57 +0800 Subject: [PATCH 001/179] fix: fix brush bug of aeolus and self test --- .../fix-brush-error_2025-04-09-09-57.json | 10 + .../__tests__/browser/examples/brush.ts | 3 +- .../vrender-components/src/brush/brush.ts | 215 +++++++++--------- .../vrender-components/src/brush/config.ts | 2 +- packages/vrender-components/src/brush/type.ts | 2 +- 5 files changed, 125 insertions(+), 107 deletions(-) create mode 100644 common/changes/@visactor/vrender-components/fix-brush-error_2025-04-09-09-57.json diff --git a/common/changes/@visactor/vrender-components/fix-brush-error_2025-04-09-09-57.json b/common/changes/@visactor/vrender-components/fix-brush-error_2025-04-09-09-57.json new file mode 100644 index 000000000..30643c6ca --- /dev/null +++ b/common/changes/@visactor/vrender-components/fix-brush-error_2025-04-09-09-57.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-components", + "comment": "fix: brush bug from aeolus and self test", + "type": "none" + } + ], + "packageName": "@visactor/vrender-components" +} \ No newline at end of file diff --git a/packages/vrender-components/__tests__/browser/examples/brush.ts b/packages/vrender-components/__tests__/browser/examples/brush.ts index d9258fd57..f21eabb77 100644 --- a/packages/vrender-components/__tests__/browser/examples/brush.ts +++ b/packages/vrender-components/__tests__/browser/examples/brush.ts @@ -95,7 +95,8 @@ export function run() { console.log('brushClear'); }); - const stage = render([brush], 'main'); + const stage = render([brush], 'main', { autoRender: true }); + console.log('stage', stage); const gui = new GUI(); gui.add(guiObject, 'name'); diff --git a/packages/vrender-components/src/brush/brush.ts b/packages/vrender-components/src/brush/brush.ts index 16d40b247..0fd3d4599 100644 --- a/packages/vrender-components/src/brush/brush.ts +++ b/packages/vrender-components/src/brush/brush.ts @@ -3,10 +3,10 @@ */ import type { FederatedPointerEvent, IGroup, IPolygon } from '@visactor/vrender-core'; // eslint-disable-next-line no-duplicate-imports -import { graphicCreator, vglobal } from '@visactor/vrender-core'; +import { graphicCreator } from '@visactor/vrender-core'; import type { IBounds, IPointLike } from '@visactor/vutils'; // eslint-disable-next-line no-duplicate-imports -import { array, cloneDeep, debounce, merge, polygonContainPoint, throttle } from '@visactor/vutils'; +import { array, cloneDeep, debounce, isEmpty, merge, polygonContainPoint, throttle } from '@visactor/vutils'; import { AbstractComponent } from '../core/base'; import type { BrushAttributes } from './type'; // eslint-disable-next-line no-duplicate-imports @@ -60,7 +60,7 @@ export class Brush extends AbstractComponent> { resetTrigger = DEFAULT_BRUSH_ATTRIBUTES.resetTrigger } = this.attribute as BrushAttributes; // 拖拽绘制开始 - array(trigger).forEach(t => vglobal.addEventListener(t, this._onBrushStart as EventListener)); + array(trigger).forEach(t => this.stage.addEventListener(t, this._onBrushStart as EventListener)); // 拖拽绘制时 array(updateTrigger).forEach(t => this.stage.addEventListener(t, this._onBrushingWithDelay as EventListener)); @@ -69,25 +69,6 @@ export class Brush extends AbstractComponent> { array(resetTrigger).forEach(t => this.stage.addEventListener(t, this._onBrushClear as EventListener)); } - private _isPosInBrushMask(e: FederatedPointerEvent) { - const pos = this.eventPosToStagePos(e); - const brushMasks = this._container.getChildren(); - for (let i = 0; i < brushMasks.length; i++) { - const { points = [], dx = 0, dy = 0 } = (brushMasks[i] as IPolygon).attribute; - const pointsConsiderOffset: IPointLike[] = points.map((point: IPointLike) => { - return { - x: point.x + dx, - y: point.y + dy - }; - }); - if (polygonContainPoint(pointsConsiderOffset, pos.x, pos.y)) { - this._operatingMask = brushMasks[i] as IPolygon; - return true; - } - } - return false; - } - /** * 开始绘制 或 移动 * @description @@ -102,6 +83,7 @@ export class Brush extends AbstractComponent> { this._isDownBeforeUpOutside = true; return; } + this._isDownBeforeUpOutside = false; e.stopPropagation(); const brushMoved = this.attribute.brushMoved ?? true; @@ -141,41 +123,47 @@ export class Brush extends AbstractComponent> { * @description 取消绘制 和 移动 状态 */ private _onBrushEnd = (e: FederatedPointerEvent) => { + // 如果在交互范围外点击,则直接清空 + if (this._isDownBeforeUpOutside) { + if (!isEmpty(this._brushMaskAABBBoundsDict)) { + this._clearMask(); + this._dispatchBrushEvent(IOperateType.brushClear, e); + } + + this._isDownBeforeUpOutside = false; + return; + } if (!this._activeDrawState && !this._activeMoveState) { return; } - e.preventDefault(); const { removeOnClick = true } = this.attribute as BrushAttributes; + if (this._activeDrawState && !this._isDrawedBeforeEnd && removeOnClick) { - // _isDrawedBeforeEnd有两种情况: - // 1. 没有绘制mask - // 2. 绘制了mask但没有超过阈值 - // 只有第2种情况才会触发clear, 可以理解为双击才触发clear + // _isDrawedBeforeEnd(无效绘制)有两种情况: + // 1. 没有绘制mask, 触发clear, 可以理解为双击才触发clear if (this._operatingMask?._AABBBounds.empty()) { - this._dispatchEvent(IOperateType.brushClear, { - operateMask: this._operatingMask as any, - operatedMaskAABBBounds: this._brushMaskAABBBoundsDict, - event: e - }); + if (!isEmpty(this._brushMaskAABBBoundsDict)) { + this._clearMask(); + this._dispatchBrushEvent(IOperateType.brushClear, e); + } + } else { + // 2. 绘制了mask但没有超过阈值, 仅清空当前操作的mask + delete this._brushMaskAABBBoundsDict[this._operatingMask.name]; + this._container.setAttributes({}); // hack逻辑, 待优化: removeChild后, vrender 无法 autoRender, setAttr手动触发render + this._container.removeChild(this._operatingMask); + if (isEmpty(this._brushMaskAABBBoundsDict)) { + this._dispatchBrushEvent(IOperateType.brushClear, e); + } else { + this._dispatchBrushEvent(IOperateType.drawEnd, e); + } } - this._container.incrementalClearChild(); - this._brushMaskAABBBoundsDict = {}; } else { if (this._activeDrawState) { - this._dispatchEvent(IOperateType.drawEnd, { - operateMask: this._operatingMask as any, - operatedMaskAABBBounds: this._brushMaskAABBBoundsDict, - event: e - }); + this._dispatchBrushEvent(IOperateType.drawEnd, e); } - if (this._activeMoveState) { - this._dispatchEvent(IOperateType.moveEnd, { - operateMask: this._operatingMask as any, - operatedMaskAABBBounds: this._brushMaskAABBBoundsDict, - event: e - }); + this._dispatchBrushEvent(IOperateType.moveEnd, e); } } @@ -189,24 +177,14 @@ export class Brush extends AbstractComponent> { private _onBrushClear = (e: FederatedPointerEvent) => { e.preventDefault(); - const { removeOnClick = true } = this.attribute as BrushAttributes; - if (this._isDownBeforeUpOutside && removeOnClick) { - this._dispatchEvent(IOperateType.brushClear, { - operateMask: this._operatingMask as any, - operatedMaskAABBBounds: this._brushMaskAABBBoundsDict, - event: e - }); - this._container.incrementalClearChild(); - this._brushMaskAABBBoundsDict = {}; + if (!isEmpty(this._brushMaskAABBBoundsDict)) { + this._clearMask(); + this._dispatchBrushEvent(IOperateType.brushClear, e); } - this._activeDrawState = false; this._activeMoveState = false; this._isDrawedBeforeEnd = false; this._isDownBeforeUpOutside = false; - if (this._operatingMask) { - this._operatingMask.setAttribute('pickable', false); - } }; /** @@ -218,16 +196,9 @@ export class Brush extends AbstractComponent> { const pos = this.eventPosToStagePos(e); this._cacheDrawPoints = [pos]; this._isDrawedBeforeEnd = false; - if (brushMode === 'single') { - this._brushMaskAABBBoundsDict = {}; - this._container.incrementalClearChild(); - } + brushMode === 'single' && this._clearMask(); this._addBrushMask(); - this._dispatchEvent(IOperateType.drawStart, { - operateMask: this._operatingMask as any, - operatedMaskAABBBounds: this._brushMaskAABBBoundsDict, - event: e - }); + this._dispatchBrushEvent(IOperateType.drawStart, e); } /** @@ -254,11 +225,7 @@ export class Brush extends AbstractComponent> { this._operatingMaskMoveRangeY = [minMoveStepY, maxMoveStepY]; this._operatingMask.setAttribute('pickable', true); - this._dispatchEvent(IOperateType.moveStart, { - operateMask: this._operatingMask as any, - operatedMaskAABBBounds: this._brushMaskAABBBoundsDict, - event: e - }); + this._dispatchBrushEvent(IOperateType.moveStart, e); } /** @@ -288,21 +255,16 @@ export class Brush extends AbstractComponent> { const maskPoints = this._computeMaskPoints(); this._operatingMask.setAttribute('points', maskPoints); - // 更新形状之后再判断是否需要正在绘制 - // if not, 则_isDrawedBeforeEnd false - // then: 1. 不暴露drawing状态 2. 在brushEnd时该形状会被清空 + // 更新形状之后再判断是否: 有效绘制 const { x1 = 0, x2 = 0, y1 = 0, y2 = 0 } = this._operatingMask?._AABBBounds; this._isDrawedBeforeEnd = !this._operatingMask._AABBBounds.empty() && !!(Math.abs(x2 - x1) > sizeThreshold || Math.abs(y1 - y2) > sizeThreshold); if (this._isDrawedBeforeEnd) { + // 有效绘制, 更新mask this._brushMaskAABBBoundsDict[this._operatingMask.name] = this._operatingMask.AABBBounds; - this._dispatchEvent(IOperateType.drawing, { - operateMask: this._operatingMask as any, - operatedMaskAABBBounds: this._brushMaskAABBBoundsDict, - event: e - }); } + this._dispatchBrushEvent(IOperateType.drawing, e); } /** @@ -331,11 +293,27 @@ export class Brush extends AbstractComponent> { dy: moveY }); this._brushMaskAABBBoundsDict[this._operatingMask.name] = this._operatingMask.AABBBounds; - this._dispatchEvent(IOperateType.moving, { - operateMask: this._operatingMask as any, - operatedMaskAABBBounds: this._brushMaskAABBBoundsDict, - event: e - }); + this._dispatchBrushEvent(IOperateType.moving, e); + } + + protected render() { + this.releaseBrushEvents(); + this._bindBrushEvents(); + const group = this.createOrUpdateChild('brush-container', {}, 'group') as unknown as IGroup; + this._container = group; + } + + releaseBrushEvents(): void { + const { + trigger = DEFAULT_BRUSH_ATTRIBUTES.trigger, + updateTrigger = DEFAULT_BRUSH_ATTRIBUTES.updateTrigger, + endTrigger = DEFAULT_BRUSH_ATTRIBUTES.endTrigger, + resetTrigger = DEFAULT_BRUSH_ATTRIBUTES.resetTrigger + } = this.attribute as BrushAttributes; + array(trigger).forEach(t => this.stage.removeEventListener(t, this._onBrushStart as EventListener)); + array(updateTrigger).forEach(t => this.stage.removeEventListener(t, this._onBrushingWithDelay as EventListener)); + array(endTrigger).forEach(t => this.stage.removeEventListener(t, this._onBrushEnd as EventListener)); + array(resetTrigger).forEach(t => this.stage.removeEventListener(t, this._onBrushClear as EventListener)); } /** @@ -409,6 +387,9 @@ export class Brush extends AbstractComponent> { return maskPoints; } + /** + * 添加brushMask + */ private _addBrushMask() { const { brushStyle, hasMask } = this.attribute as BrushAttributes; const brushMask = graphicCreator.polygon({ @@ -416,7 +397,7 @@ export class Brush extends AbstractComponent> { cursor: 'move', pickable: false, ...brushStyle, - opacity: hasMask ? brushStyle.opacity ?? 1 : 0 + opacity: hasMask ? (brushStyle.opacity ?? 1) : 0 }); brushMask.name = `brush-${Date.now()}`; // 用Date给mask唯一标记 this._operatingMask = brushMask; @@ -424,6 +405,31 @@ export class Brush extends AbstractComponent> { this._brushMaskAABBBoundsDict[brushMask.name] = brushMask.AABBBounds; } + /** + * 遍历_container的所有子元素,判断鼠标是否在子元素的范围内 + */ + private _isPosInBrushMask(e: FederatedPointerEvent) { + const pos = this.eventPosToStagePos(e); + const brushMasks = this._container.getChildren(); + for (let i = 0; i < brushMasks.length; i++) { + const { points = [], dx = 0, dy = 0 } = (brushMasks[i] as IPolygon).attribute; + const pointsConsiderOffset: IPointLike[] = points.map((point: IPointLike) => { + return { + x: point.x + dx, + y: point.y + dy + }; + }); + if (polygonContainPoint(pointsConsiderOffset, pos.x, pos.y)) { + this._operatingMask = brushMasks[i] as IPolygon; + return true; + } + } + return false; + } + + /** + * 判断鼠标是否在交互范围内 + */ private _outOfInteractiveRange(e: FederatedPointerEvent) { // 在返回坐标时,将其限制在交互范围内 const { interactiveRange } = this.attribute as BrushAttributes; @@ -435,29 +441,30 @@ export class Brush extends AbstractComponent> { return false; } - /** 事件系统坐标转换为stage坐标 */ + /** + * 事件系统坐标转换为stage坐标 + */ protected eventPosToStagePos(e: FederatedPointerEvent) { return this.stage.eventPointTransform(e); } - protected render() { - this._bindBrushEvents(); - const group = this.createOrUpdateChild('brush-container', {}, 'group') as unknown as IGroup; - this._container = group; + /** + * 根据操作类型触发对应的事件 + */ + private _dispatchBrushEvent(operateType: IOperateType, e: any) { + this._dispatchEvent(operateType, { + operateMask: this._operatingMask as any, + operatedMaskAABBBounds: this._brushMaskAABBBoundsDict, + event: e + }); } - releaseBrushEvents(): void { - const { - delayType = 'throttle', - delayTime = 0, - trigger = DEFAULT_BRUSH_ATTRIBUTES.trigger, - updateTrigger = DEFAULT_BRUSH_ATTRIBUTES.updateTrigger, - endTrigger = DEFAULT_BRUSH_ATTRIBUTES.endTrigger, - resetTrigger = DEFAULT_BRUSH_ATTRIBUTES.resetTrigger - } = this.attribute as BrushAttributes; - array(trigger).forEach(t => vglobal.removeEventListener(t, this._onBrushStart as EventListener)); - array(updateTrigger).forEach(t => this.stage.removeEventListener(t, this._onBrushingWithDelay as EventListener)); - array(endTrigger).forEach(t => this.stage.removeEventListener(t, this._onBrushEnd as EventListener)); - array(resetTrigger).forEach(t => this.stage.removeEventListener(t, this._onBrushClear as EventListener)); + /** + * 重置brush状态 + */ + private _clearMask() { + this._brushMaskAABBBoundsDict = {}; + this._container.incrementalClearChild(); + this._operatingMask = null; } } diff --git a/packages/vrender-components/src/brush/config.ts b/packages/vrender-components/src/brush/config.ts index f952fdfe5..d1971bc20 100644 --- a/packages/vrender-components/src/brush/config.ts +++ b/packages/vrender-components/src/brush/config.ts @@ -1,7 +1,7 @@ export const DEFAULT_BRUSH_ATTRIBUTES = { trigger: 'pointerdown', updateTrigger: 'pointermove', - endTrigger: 'pointerup', + endTrigger: ['pointerup', 'pointerleave'], resetTrigger: 'pointerupoutside', hasMask: true, brushMode: 'single', diff --git a/packages/vrender-components/src/brush/type.ts b/packages/vrender-components/src/brush/type.ts index 53ab205ca..1a8dd351d 100644 --- a/packages/vrender-components/src/brush/type.ts +++ b/packages/vrender-components/src/brush/type.ts @@ -59,7 +59,7 @@ export interface BrushAttributes extends IGroupGraphicAttribute { */ brushMoved?: boolean; /** - * brushMode为'single'时,是否单击清除选框 + * 是否单击空白处, 清除选框 * @default true */ removeOnClick?: boolean; From e34df2e27e8ff06c12a2f959c8a20f54be328da4 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Wed, 9 Apr 2025 22:10:18 +0800 Subject: [PATCH 002/179] fix: add empty mask judgement --- packages/vrender-components/src/brush/brush.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/vrender-components/src/brush/brush.ts b/packages/vrender-components/src/brush/brush.ts index 0fd3d4599..1c5ca1056 100644 --- a/packages/vrender-components/src/brush/brush.ts +++ b/packages/vrender-components/src/brush/brush.ts @@ -125,7 +125,7 @@ export class Brush extends AbstractComponent> { private _onBrushEnd = (e: FederatedPointerEvent) => { // 如果在交互范围外点击,则直接清空 if (this._isDownBeforeUpOutside) { - if (!isEmpty(this._brushMaskAABBBoundsDict)) { + if (!this._isEmptyMask()) { this._clearMask(); this._dispatchBrushEvent(IOperateType.brushClear, e); } @@ -143,7 +143,7 @@ export class Brush extends AbstractComponent> { // _isDrawedBeforeEnd(无效绘制)有两种情况: // 1. 没有绘制mask, 触发clear, 可以理解为双击才触发clear if (this._operatingMask?._AABBBounds.empty()) { - if (!isEmpty(this._brushMaskAABBBoundsDict)) { + if (!this._isEmptyMask()) { this._clearMask(); this._dispatchBrushEvent(IOperateType.brushClear, e); } @@ -152,7 +152,7 @@ export class Brush extends AbstractComponent> { delete this._brushMaskAABBBoundsDict[this._operatingMask.name]; this._container.setAttributes({}); // hack逻辑, 待优化: removeChild后, vrender 无法 autoRender, setAttr手动触发render this._container.removeChild(this._operatingMask); - if (isEmpty(this._brushMaskAABBBoundsDict)) { + if (!this._isEmptyMask()) { this._dispatchBrushEvent(IOperateType.brushClear, e); } else { this._dispatchBrushEvent(IOperateType.drawEnd, e); @@ -177,7 +177,7 @@ export class Brush extends AbstractComponent> { private _onBrushClear = (e: FederatedPointerEvent) => { e.preventDefault(); - if (!isEmpty(this._brushMaskAABBBoundsDict)) { + if (!this._isEmptyMask()) { this._clearMask(); this._dispatchBrushEvent(IOperateType.brushClear, e); } @@ -467,4 +467,14 @@ export class Brush extends AbstractComponent> { this._container.incrementalClearChild(); this._operatingMask = null; } + + /** + * 判断当前画布中,是否存在有效mask + */ + private _isEmptyMask() { + return ( + isEmpty(this._brushMaskAABBBoundsDict) || + Object.keys(this._brushMaskAABBBoundsDict).every(key => this._brushMaskAABBBoundsDict[key].empty()) + ); + } } From 3e26febce73a0bcd515a63e8bdfa8f2ee21eaf13 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Mon, 14 Apr 2025 15:02:52 +0800 Subject: [PATCH 003/179] feat: refactor brush interactive pipe --- .../vrender-components/src/brush/brush.ts | 173 +++++++++--------- .../vrender-components/src/brush/config.ts | 8 +- packages/vrender-components/src/brush/type.ts | 1 + 3 files changed, 92 insertions(+), 90 deletions(-) diff --git a/packages/vrender-components/src/brush/brush.ts b/packages/vrender-components/src/brush/brush.ts index 1c5ca1056..094075a66 100644 --- a/packages/vrender-components/src/brush/brush.ts +++ b/packages/vrender-components/src/brush/brush.ts @@ -30,8 +30,6 @@ export class Brush extends AbstractComponent> { // 绘制mask时的相关属性 private _activeDrawState = false; // 用于标记绘制状态 private _cacheDrawPoints: IPointLike[] = []; // 用于维护鼠标走过的路径,主要用于绘制mask的点 - private _isDrawedBeforeEnd = false; - private _isDownBeforeUpOutside = false; // 用于标记出画布外单击事件,以便清空 // 移动mask时的相关属性 private _activeMoveState = false; // 用于标记移动状态 private _operatingMaskMoveDx = 0; // 用于标记移动的位移量 @@ -45,27 +43,22 @@ export class Brush extends AbstractComponent> { // 透出给上层的属性(主要是所有mask的AABBBounds,这里用的是dict存储方便添加和修改) private _brushMaskAABBBoundsDict: { [name: string]: IBounds } = {}; + private _firstUpdate = true; // 用于标记第一次更新 + private _startPos!: IPointLike; // 用于标记开始绘制的位置 + constructor(attributes: BrushAttributes, options?: ComponentOptions) { super(options?.skipDefault ? attributes : merge({}, Brush.defaultAttributes, attributes)); } private _bindBrushEvents(): void { + // 绑定前先解绑, 确保事件不会重复绑定 + this.releaseBrushEvents(); if (this.attribute.disableTriggerEvent) { return; } - const { - trigger = DEFAULT_BRUSH_ATTRIBUTES.trigger, - updateTrigger = DEFAULT_BRUSH_ATTRIBUTES.updateTrigger, - endTrigger = DEFAULT_BRUSH_ATTRIBUTES.endTrigger, - resetTrigger = DEFAULT_BRUSH_ATTRIBUTES.resetTrigger - } = this.attribute as BrushAttributes; - // 拖拽绘制开始 + const { trigger = DEFAULT_BRUSH_ATTRIBUTES.trigger, resetTrigger = DEFAULT_BRUSH_ATTRIBUTES.resetTrigger } = this + .attribute as BrushAttributes; array(trigger).forEach(t => this.stage.addEventListener(t, this._onBrushStart as EventListener)); - - // 拖拽绘制时 - array(updateTrigger).forEach(t => this.stage.addEventListener(t, this._onBrushingWithDelay as EventListener)); - // 拖拽绘制结束 - array(endTrigger).forEach(t => this.stage.addEventListener(t, this._onBrushEnd as EventListener)); array(resetTrigger).forEach(t => this.stage.addEventListener(t, this._onBrushClear as EventListener)); } @@ -80,18 +73,25 @@ export class Brush extends AbstractComponent> { */ private _onBrushStart = (e: FederatedPointerEvent) => { if (this._outOfInteractiveRange(e)) { - this._isDownBeforeUpOutside = true; + if (!this._isEmptyMask()) { + this._clearMask(); + this._dispatchBrushEvent(IOperateType.brushClear, e); + } return; } - this._isDownBeforeUpOutside = false; - e.stopPropagation(); + const { + updateTrigger = DEFAULT_BRUSH_ATTRIBUTES.updateTrigger, + endTrigger = DEFAULT_BRUSH_ATTRIBUTES.endTrigger, + brushMoved = true + } = this.attribute as BrushAttributes; + array(updateTrigger).forEach(t => this.stage.addEventListener(t, this._onBrushingWithDelay as EventListener)); + array(endTrigger).forEach(t => this.stage.addEventListener(t, this._onBrushEnd as EventListener)); - const brushMoved = this.attribute.brushMoved ?? true; + e.stopPropagation(); + this._firstUpdate = true; this._activeMoveState = brushMoved && this._isPosInBrushMask(e); // 如果是移动状态,在这里会标记operatingMask为正在移动的mask this._activeDrawState = !this._activeMoveState; - - this._activeDrawState && this._initDraw(e); // 如果是绘制状态,在这里会标记operatingMask为正在绘制的mask - this._activeMoveState && this._initMove(e); + this._startPos = this.eventPosToStagePos(e); }; /** @@ -104,13 +104,15 @@ export class Brush extends AbstractComponent> { if (this._outOfInteractiveRange(e)) { return; } - - if (this._activeDrawState || this._activeMoveState) { - e.stopPropagation(); + e.stopPropagation(); + if (this._firstUpdate) { + this._activeDrawState && this._initDraw(e); + this._activeMoveState && this._initMove(e); + this._firstUpdate = false; + } else { + this._activeDrawState && this._drawing(e); + this._activeMoveState && this._moving(e); } - - this._activeDrawState && this._drawing(e); // 如果是绘制状态,在这里会标记operatingMask为正在绘制的mask - this._activeMoveState && this._moving(e); }; private _onBrushingWithDelay = @@ -123,56 +125,17 @@ export class Brush extends AbstractComponent> { * @description 取消绘制 和 移动 状态 */ private _onBrushEnd = (e: FederatedPointerEvent) => { - // 如果在交互范围外点击,则直接清空 - if (this._isDownBeforeUpOutside) { - if (!this._isEmptyMask()) { - this._clearMask(); - this._dispatchBrushEvent(IOperateType.brushClear, e); - } + const { updateTrigger = DEFAULT_BRUSH_ATTRIBUTES.updateTrigger, endTrigger = DEFAULT_BRUSH_ATTRIBUTES.endTrigger } = + this.attribute as BrushAttributes; + array(updateTrigger).forEach(t => this.stage.removeEventListener(t, this._onBrushingWithDelay as EventListener)); + array(endTrigger).forEach(t => this.stage.removeEventListener(t, this._onBrushEnd as EventListener)); - this._isDownBeforeUpOutside = false; - return; - } - if (!this._activeDrawState && !this._activeMoveState) { - return; - } e.preventDefault(); - const { removeOnClick = true } = this.attribute as BrushAttributes; - - if (this._activeDrawState && !this._isDrawedBeforeEnd && removeOnClick) { - // _isDrawedBeforeEnd(无效绘制)有两种情况: - // 1. 没有绘制mask, 触发clear, 可以理解为双击才触发clear - if (this._operatingMask?._AABBBounds.empty()) { - if (!this._isEmptyMask()) { - this._clearMask(); - this._dispatchBrushEvent(IOperateType.brushClear, e); - } - } else { - // 2. 绘制了mask但没有超过阈值, 仅清空当前操作的mask - delete this._brushMaskAABBBoundsDict[this._operatingMask.name]; - this._container.setAttributes({}); // hack逻辑, 待优化: removeChild后, vrender 无法 autoRender, setAttr手动触发render - this._container.removeChild(this._operatingMask); - if (!this._isEmptyMask()) { - this._dispatchBrushEvent(IOperateType.brushClear, e); - } else { - this._dispatchBrushEvent(IOperateType.drawEnd, e); - } - } - } else { - if (this._activeDrawState) { - this._dispatchBrushEvent(IOperateType.drawEnd, e); - } - if (this._activeMoveState) { - this._dispatchBrushEvent(IOperateType.moveEnd, e); - } - } + this._activeDrawState && this._drawEnd(e); + this._activeMoveState && this._moveEnd(e); this._activeDrawState = false; this._activeMoveState = false; - this._isDrawedBeforeEnd = false; - if (this._operatingMask) { - this._operatingMask.setAttribute('pickable', false); - } }; private _onBrushClear = (e: FederatedPointerEvent) => { @@ -183,8 +146,6 @@ export class Brush extends AbstractComponent> { } this._activeDrawState = false; this._activeMoveState = false; - this._isDrawedBeforeEnd = false; - this._isDownBeforeUpOutside = false; }; /** @@ -195,10 +156,15 @@ export class Brush extends AbstractComponent> { const { brushMode } = this.attribute as BrushAttributes; const pos = this.eventPosToStagePos(e); this._cacheDrawPoints = [pos]; - this._isDrawedBeforeEnd = false; brushMode === 'single' && this._clearMask(); this._addBrushMask(); this._dispatchBrushEvent(IOperateType.drawStart, e); + // 无论是多选,还是单选 + // 如果这是第一个brush mask + // 证明这第一次绘制, 则触发brushActive事件 + if (Object.keys(this._brushMaskAABBBoundsDict).length === 1) { + this._dispatchBrushEvent(IOperateType.brushActive, e); + } } /** @@ -234,7 +200,7 @@ export class Brush extends AbstractComponent> { */ private _drawing(e: FederatedPointerEvent) { const pos = this.eventPosToStagePos(e); - const { sizeThreshold = DEFAULT_SIZE_THRESHOLD, brushType } = this.attribute as BrushAttributes; + const { brushType } = this.attribute as BrushAttributes; const cacheLength = this._cacheDrawPoints.length; @@ -254,16 +220,6 @@ export class Brush extends AbstractComponent> { // 更新mask形状 const maskPoints = this._computeMaskPoints(); this._operatingMask.setAttribute('points', maskPoints); - - // 更新形状之后再判断是否: 有效绘制 - const { x1 = 0, x2 = 0, y1 = 0, y2 = 0 } = this._operatingMask?._AABBBounds; - this._isDrawedBeforeEnd = - !this._operatingMask._AABBBounds.empty() && - !!(Math.abs(x2 - x1) > sizeThreshold || Math.abs(y1 - y2) > sizeThreshold); - if (this._isDrawedBeforeEnd) { - // 有效绘制, 更新mask - this._brushMaskAABBBoundsDict[this._operatingMask.name] = this._operatingMask.AABBBounds; - } this._dispatchBrushEvent(IOperateType.drawing, e); } @@ -296,6 +252,51 @@ export class Brush extends AbstractComponent> { this._dispatchBrushEvent(IOperateType.moving, e); } + private _drawEnd(e: FederatedPointerEvent) { + const { removeOnClick = true, sizeThreshold = DEFAULT_SIZE_THRESHOLD } = this.attribute as BrushAttributes; + if (this._outOfInteractiveRange(e)) { + if (!this._isEmptyMask()) { + this._clearMask(); + this._dispatchBrushEvent(IOperateType.brushClear, e); + } + } else { + const { x: x1, y: y1 } = this._startPos; + const { x: x2, y: y2 } = this.eventPosToStagePos(e); + // 1. 无效绘制: 单击 + if (Math.abs(x2 - x1) <= 1 && Math.abs(y2 - y1) <= 1 && removeOnClick) { + if (!this._isEmptyMask()) { + this._clearMask(); + this._dispatchBrushEvent(IOperateType.brushClear, e); + } + } + // 2. 无效绘制: 绘制了mask但没有超过阈值, 仅清空当前操作的mask + else if (Math.abs(x2 - x1) < sizeThreshold && Math.abs(y1 - y2) < sizeThreshold) { + delete this._brushMaskAABBBoundsDict[this._operatingMask.name]; + this._container.setAttributes({}); // hack逻辑, 待优化: removeChild后, vrender 无法 autoRender, setAttr手动触发render + this._container.removeChild(this._operatingMask); + if (this._isEmptyMask()) { + // 说明是最后一个mask被清空了, 要抛出clear事件, 重置图元状态 + this._dispatchBrushEvent(IOperateType.brushClear, e); + } else { + // 说明还有其他mask, 这次无效绘制不会重置状态 + // do nothing + } + } + // 3. 有效绘制 + else { + this._brushMaskAABBBoundsDict[this._operatingMask.name] = this._operatingMask.AABBBounds; + this._dispatchBrushEvent(IOperateType.drawEnd, e); + } + } + } + + private _moveEnd(e: FederatedPointerEvent) { + if (this._operatingMask) { + this._operatingMask.setAttribute('pickable', false); + } + this._dispatchBrushEvent(IOperateType.moveEnd, e); + } + protected render() { this.releaseBrushEvents(); this._bindBrushEvents(); diff --git a/packages/vrender-components/src/brush/config.ts b/packages/vrender-components/src/brush/config.ts index d1971bc20..6a576a4ce 100644 --- a/packages/vrender-components/src/brush/config.ts +++ b/packages/vrender-components/src/brush/config.ts @@ -17,10 +17,10 @@ export const DEFAULT_BRUSH_ATTRIBUTES = { delayType: 'throttle', delayTime: 10, interactiveRange: { - y1: -Infinity, - y2: Infinity, - x1: -Infinity, - x2: Infinity + minY: -Infinity, + maxY: Infinity, + minX: -Infinity, + maxX: Infinity } }; diff --git a/packages/vrender-components/src/brush/type.ts b/packages/vrender-components/src/brush/type.ts index 1a8dd351d..ec0c8792f 100644 --- a/packages/vrender-components/src/brush/type.ts +++ b/packages/vrender-components/src/brush/type.ts @@ -94,5 +94,6 @@ export enum IOperateType { moving = 'moving', moveStart = 'moveStart', moveEnd = 'moveEnd', + brushActive = 'brushActive', brushClear = 'brushClear' } From f8004d0e6d4d325eebc39e8eaf96cd6c91b9d8b7 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Tue, 15 Apr 2025 11:26:33 +0800 Subject: [PATCH 004/179] fix: code review problem --- .../vrender-components/src/brush/brush.ts | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/vrender-components/src/brush/brush.ts b/packages/vrender-components/src/brush/brush.ts index 094075a66..f9d0d87d0 100644 --- a/packages/vrender-components/src/brush/brush.ts +++ b/packages/vrender-components/src/brush/brush.ts @@ -125,11 +125,7 @@ export class Brush extends AbstractComponent> { * @description 取消绘制 和 移动 状态 */ private _onBrushEnd = (e: FederatedPointerEvent) => { - const { updateTrigger = DEFAULT_BRUSH_ATTRIBUTES.updateTrigger, endTrigger = DEFAULT_BRUSH_ATTRIBUTES.endTrigger } = - this.attribute as BrushAttributes; - array(updateTrigger).forEach(t => this.stage.removeEventListener(t, this._onBrushingWithDelay as EventListener)); - array(endTrigger).forEach(t => this.stage.removeEventListener(t, this._onBrushEnd as EventListener)); - + this._releaseBrushUpdateEvents(); e.preventDefault(); this._activeDrawState && this._drawEnd(e); this._activeMoveState && this._moveEnd(e); @@ -298,23 +294,24 @@ export class Brush extends AbstractComponent> { } protected render() { - this.releaseBrushEvents(); this._bindBrushEvents(); const group = this.createOrUpdateChild('brush-container', {}, 'group') as unknown as IGroup; this._container = group; } releaseBrushEvents(): void { - const { - trigger = DEFAULT_BRUSH_ATTRIBUTES.trigger, - updateTrigger = DEFAULT_BRUSH_ATTRIBUTES.updateTrigger, - endTrigger = DEFAULT_BRUSH_ATTRIBUTES.endTrigger, - resetTrigger = DEFAULT_BRUSH_ATTRIBUTES.resetTrigger - } = this.attribute as BrushAttributes; + const { trigger = DEFAULT_BRUSH_ATTRIBUTES.trigger, resetTrigger = DEFAULT_BRUSH_ATTRIBUTES.resetTrigger } = this + .attribute as BrushAttributes; array(trigger).forEach(t => this.stage.removeEventListener(t, this._onBrushStart as EventListener)); + array(resetTrigger).forEach(t => this.stage.removeEventListener(t, this._onBrushClear as EventListener)); + this._releaseBrushUpdateEvents(); + } + + private _releaseBrushUpdateEvents(): void { + const { updateTrigger = DEFAULT_BRUSH_ATTRIBUTES.updateTrigger, endTrigger = DEFAULT_BRUSH_ATTRIBUTES.endTrigger } = + this.attribute as BrushAttributes; array(updateTrigger).forEach(t => this.stage.removeEventListener(t, this._onBrushingWithDelay as EventListener)); array(endTrigger).forEach(t => this.stage.removeEventListener(t, this._onBrushEnd as EventListener)); - array(resetTrigger).forEach(t => this.stage.removeEventListener(t, this._onBrushClear as EventListener)); } /** From fde84c3262a703f3b6ccd139f7e51ca775e06d88 Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Tue, 15 Apr 2025 14:58:04 +0800 Subject: [PATCH 005/179] feat: add export gif image --- .../vrender-kits/src/register/register-gif.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 packages/vrender-kits/src/register/register-gif.ts diff --git a/packages/vrender-kits/src/register/register-gif.ts b/packages/vrender-kits/src/register/register-gif.ts new file mode 100644 index 000000000..80b600cf1 --- /dev/null +++ b/packages/vrender-kits/src/register/register-gif.ts @@ -0,0 +1,22 @@ +import { container, graphicCreator } from '@visactor/vrender-core'; +import { createGifImage } from '../graphic/gif-image'; +import { gifImageModule } from '../render/contributions/canvas/gif-image-module'; +import { gifImageCanvasPickModule } from '../picker/contributions/canvas-picker/gif-image-module'; + +export function registerGifGraphic() { + graphicCreator.RegisterGraphicCreator('gif', createGifImage); +} + +function _registerGifImage() { + if (_registerGifImage.__loaded) { + return; + } + _registerGifImage.__loaded = true; + registerGifGraphic(); + container.load(gifImageModule); + container.load(gifImageCanvasPickModule); +} + +_registerGifImage.__loaded = false; + +export const registerGifImage = _registerGifImage; From 61453d40698df97cb48d120dc7468988af8313db Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Tue, 15 Apr 2025 15:44:19 +0800 Subject: [PATCH 006/179] feat: add export gif image to index --- packages/vrender-kits/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vrender-kits/src/index.ts b/packages/vrender-kits/src/index.ts index 748cbed46..8aa9561fd 100644 --- a/packages/vrender-kits/src/index.ts +++ b/packages/vrender-kits/src/index.ts @@ -79,6 +79,7 @@ export * from './register/register-symbol'; export * from './register/register-text'; export * from './register/register-star'; export * from './register/register-wraptext'; +export * from './register/register-gif'; export * from './tools/dynamicTexture/effect'; // export const canvasModuleLoader = _canvasModuleLoader; // export { nodeLoader } from './node-bind'; // nodeLoader只在node入口暴露 From 06c43a1d7e81d8691f6eee67528794fb57434d61 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 16 Apr 2025 03:23:55 +0000 Subject: [PATCH 007/179] build: prelease version 0.22.8 --- .../fix-brush-error_2025-04-09-09-57.json | 10 ------- common/config/rush/pnpm-lock.yaml | 26 +++++++++---------- common/config/rush/version-policies.json | 2 +- docs/package.json | 2 +- packages/react-vrender-utils/CHANGELOG.json | 6 +++++ packages/react-vrender-utils/CHANGELOG.md | 7 ++++- packages/react-vrender-utils/package.json | 6 ++--- packages/react-vrender/CHANGELOG.json | 6 +++++ packages/react-vrender/CHANGELOG.md | 7 ++++- packages/react-vrender/package.json | 4 +-- packages/vrender-components/CHANGELOG.json | 12 +++++++++ packages/vrender-components/CHANGELOG.md | 9 ++++++- packages/vrender-components/package.json | 6 ++--- packages/vrender-core/CHANGELOG.json | 6 +++++ packages/vrender-core/CHANGELOG.md | 7 ++++- packages/vrender-core/package.json | 2 +- packages/vrender-kits/CHANGELOG.json | 6 +++++ packages/vrender-kits/CHANGELOG.md | 7 ++++- packages/vrender-kits/package.json | 4 +-- packages/vrender/CHANGELOG.json | 6 +++++ packages/vrender/CHANGELOG.md | 7 ++++- packages/vrender/package.json | 6 ++--- tools/bugserver-trigger/package.json | 8 +++--- 23 files changed, 113 insertions(+), 49 deletions(-) delete mode 100644 common/changes/@visactor/vrender-components/fix-brush-error_2025-04-09-09-57.json diff --git a/common/changes/@visactor/vrender-components/fix-brush-error_2025-04-09-09-57.json b/common/changes/@visactor/vrender-components/fix-brush-error_2025-04-09-09-57.json deleted file mode 100644 index 30643c6ca..000000000 --- a/common/changes/@visactor/vrender-components/fix-brush-error_2025-04-09-09-57.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender-components", - "comment": "fix: brush bug from aeolus and self test", - "type": "none" - } - ], - "packageName": "@visactor/vrender-components" -} \ No newline at end of file diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 622a1cb91..9e1f209f5 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: specifier: ~0.5.7 version: 0.5.7 '@visactor/vrender': - specifier: workspace:0.22.7 + specifier: workspace:0.22.8 version: link:../packages/vrender '@visactor/vutils': specifier: ~0.19.5 @@ -95,7 +95,7 @@ importers: ../../packages/react-vrender: dependencies: '@visactor/vrender': - specifier: workspace:0.22.7 + specifier: workspace:0.22.8 version: link:../vrender '@visactor/vutils': specifier: ~0.19.5 @@ -153,10 +153,10 @@ importers: ../../packages/react-vrender-utils: dependencies: '@visactor/react-vrender': - specifier: workspace:0.22.7 + specifier: workspace:0.22.8 version: link:../react-vrender '@visactor/vrender': - specifier: workspace:0.22.7 + specifier: workspace:0.22.8 version: link:../vrender '@visactor/vutils': specifier: ~0.19.5 @@ -211,10 +211,10 @@ importers: ../../packages/vrender: dependencies: '@visactor/vrender-core': - specifier: workspace:0.22.7 + specifier: workspace:0.22.8 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:0.22.7 + specifier: workspace:0.22.8 version: link:../vrender-kits devDependencies: '@internal/bundler': @@ -281,10 +281,10 @@ importers: ../../packages/vrender-components: dependencies: '@visactor/vrender-core': - specifier: workspace:0.22.7 + specifier: workspace:0.22.8 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:0.22.7 + specifier: workspace:0.22.8 version: link:../vrender-kits '@visactor/vscale': specifier: ~0.19.5 @@ -403,7 +403,7 @@ importers: specifier: 2.4.1 version: 2.4.1 '@visactor/vrender-core': - specifier: workspace:0.22.7 + specifier: workspace:0.22.8 version: link:../vrender-core '@visactor/vutils': specifier: ~0.19.5 @@ -519,16 +519,16 @@ importers: ../../tools/bugserver-trigger: dependencies: '@visactor/vrender': - specifier: workspace:0.22.7 + specifier: workspace:0.22.8 version: link:../../packages/vrender '@visactor/vrender-components': - specifier: workspace:0.22.7 + specifier: workspace:0.22.8 version: link:../../packages/vrender-components '@visactor/vrender-core': - specifier: workspace:0.22.7 + specifier: workspace:0.22.8 version: link:../../packages/vrender-core '@visactor/vrender-kits': - specifier: workspace:0.22.7 + specifier: workspace:0.22.8 version: link:../../packages/vrender-kits devDependencies: '@internal/bundler': diff --git a/common/config/rush/version-policies.json b/common/config/rush/version-policies.json index ef3db46ea..56fc5626a 100644 --- a/common/config/rush/version-policies.json +++ b/common/config/rush/version-policies.json @@ -1 +1 @@ -[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"0.22.7","nextBump":"patch"}] +[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"0.22.8","nextBump":"patch"}] diff --git a/docs/package.json b/docs/package.json index 00626770a..d12fccddb 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,7 +13,7 @@ "@visactor/vchart": "1.3.0", "@visactor/vutils": "~0.19.5", "@visactor/vgrammar": "~0.5.7", - "@visactor/vrender": "workspace:0.22.7", + "@visactor/vrender": "workspace:0.22.8", "markdown-it": "^13.0.0", "highlight.js": "^11.8.0", "axios": "^1.4.0", diff --git a/packages/react-vrender-utils/CHANGELOG.json b/packages/react-vrender-utils/CHANGELOG.json index 6329fd6b3..1cbc85c96 100644 --- a/packages/react-vrender-utils/CHANGELOG.json +++ b/packages/react-vrender-utils/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/react-vrender-utils", "entries": [ + { + "version": "0.22.8", + "tag": "@visactor/react-vrender-utils_v0.22.8", + "date": "Wed, 16 Apr 2025 03:17:31 GMT", + "comments": {} + }, { "version": "0.22.7", "tag": "@visactor/react-vrender-utils_v0.22.7", diff --git a/packages/react-vrender-utils/CHANGELOG.md b/packages/react-vrender-utils/CHANGELOG.md index b8bf354fc..efa2be238 100644 --- a/packages/react-vrender-utils/CHANGELOG.md +++ b/packages/react-vrender-utils/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/react-vrender-utils -This log was last generated on Fri, 11 Apr 2025 07:37:57 GMT and should not be manually modified. +This log was last generated on Wed, 16 Apr 2025 03:17:31 GMT and should not be manually modified. + +## 0.22.8 +Wed, 16 Apr 2025 03:17:31 GMT + +_Version update only_ ## 0.22.7 Fri, 11 Apr 2025 07:37:57 GMT diff --git a/packages/react-vrender-utils/package.json b/packages/react-vrender-utils/package.json index 8de7c145d..65fa50856 100644 --- a/packages/react-vrender-utils/package.json +++ b/packages/react-vrender-utils/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender-utils", - "version": "0.22.7", + "version": "0.22.8", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -24,8 +24,8 @@ "react-dom": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:0.22.7", - "@visactor/react-vrender": "workspace:0.22.7", + "@visactor/vrender": "workspace:0.22.8", + "@visactor/react-vrender": "workspace:0.22.8", "@visactor/vutils": "~0.19.5", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/react-vrender/CHANGELOG.json b/packages/react-vrender/CHANGELOG.json index e2a51069d..7c92dff0e 100644 --- a/packages/react-vrender/CHANGELOG.json +++ b/packages/react-vrender/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/react-vrender", "entries": [ + { + "version": "0.22.8", + "tag": "@visactor/react-vrender_v0.22.8", + "date": "Wed, 16 Apr 2025 03:17:31 GMT", + "comments": {} + }, { "version": "0.22.7", "tag": "@visactor/react-vrender_v0.22.7", diff --git a/packages/react-vrender/CHANGELOG.md b/packages/react-vrender/CHANGELOG.md index 3241297b3..c564cb938 100644 --- a/packages/react-vrender/CHANGELOG.md +++ b/packages/react-vrender/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/react-vrender -This log was last generated on Fri, 11 Apr 2025 07:37:57 GMT and should not be manually modified. +This log was last generated on Wed, 16 Apr 2025 03:17:31 GMT and should not be manually modified. + +## 0.22.8 +Wed, 16 Apr 2025 03:17:31 GMT + +_Version update only_ ## 0.22.7 Fri, 11 Apr 2025 07:37:57 GMT diff --git a/packages/react-vrender/package.json b/packages/react-vrender/package.json index 890b058a4..6033f1ec7 100644 --- a/packages/react-vrender/package.json +++ b/packages/react-vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender", - "version": "0.22.7", + "version": "0.22.8", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -23,7 +23,7 @@ "react": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:0.22.7", + "@visactor/vrender": "workspace:0.22.8", "@visactor/vutils": "~0.19.5", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/vrender-components/CHANGELOG.json b/packages/vrender-components/CHANGELOG.json index b96a0bc1e..4621219ed 100644 --- a/packages/vrender-components/CHANGELOG.json +++ b/packages/vrender-components/CHANGELOG.json @@ -1,6 +1,18 @@ { "name": "@visactor/vrender-components", "entries": [ + { + "version": "0.22.8", + "tag": "@visactor/vrender-components_v0.22.8", + "date": "Wed, 16 Apr 2025 03:17:31 GMT", + "comments": { + "none": [ + { + "comment": "fix: brush bug from aeolus and self test" + } + ] + } + }, { "version": "0.22.7", "tag": "@visactor/vrender-components_v0.22.7", diff --git a/packages/vrender-components/CHANGELOG.md b/packages/vrender-components/CHANGELOG.md index ecf3aea5c..921298293 100644 --- a/packages/vrender-components/CHANGELOG.md +++ b/packages/vrender-components/CHANGELOG.md @@ -1,6 +1,13 @@ # Change Log - @visactor/vrender-components -This log was last generated on Fri, 11 Apr 2025 07:37:57 GMT and should not be manually modified. +This log was last generated on Wed, 16 Apr 2025 03:17:31 GMT and should not be manually modified. + +## 0.22.8 +Wed, 16 Apr 2025 03:17:31 GMT + +### Updates + +- fix: brush bug from aeolus and self test ## 0.22.7 Fri, 11 Apr 2025 07:37:57 GMT diff --git a/packages/vrender-components/package.json b/packages/vrender-components/package.json index 0dd316dd8..ea13ed173 100644 --- a/packages/vrender-components/package.json +++ b/packages/vrender-components/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-components", - "version": "0.22.7", + "version": "0.22.8", "description": "components library for dp visualization", "sideEffects": false, "main": "cjs/index.js", @@ -27,8 +27,8 @@ "dependencies": { "@visactor/vutils": "~0.19.5", "@visactor/vscale": "~0.19.5", - "@visactor/vrender-core": "workspace:0.22.7", - "@visactor/vrender-kits": "workspace:0.22.7" + "@visactor/vrender-core": "workspace:0.22.8", + "@visactor/vrender-kits": "workspace:0.22.8" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-core/CHANGELOG.json b/packages/vrender-core/CHANGELOG.json index 85fdd8de1..78baa69d1 100644 --- a/packages/vrender-core/CHANGELOG.json +++ b/packages/vrender-core/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender-core", "entries": [ + { + "version": "0.22.8", + "tag": "@visactor/vrender-core_v0.22.8", + "date": "Wed, 16 Apr 2025 03:17:31 GMT", + "comments": {} + }, { "version": "0.22.7", "tag": "@visactor/vrender-core_v0.22.7", diff --git a/packages/vrender-core/CHANGELOG.md b/packages/vrender-core/CHANGELOG.md index 2b08198c4..83b7d149a 100644 --- a/packages/vrender-core/CHANGELOG.md +++ b/packages/vrender-core/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender-core -This log was last generated on Fri, 11 Apr 2025 07:37:57 GMT and should not be manually modified. +This log was last generated on Wed, 16 Apr 2025 03:17:31 GMT and should not be manually modified. + +## 0.22.8 +Wed, 16 Apr 2025 03:17:31 GMT + +_Version update only_ ## 0.22.7 Fri, 11 Apr 2025 07:37:57 GMT diff --git a/packages/vrender-core/package.json b/packages/vrender-core/package.json index e7da82d4c..33b3a7e1a 100644 --- a/packages/vrender-core/package.json +++ b/packages/vrender-core/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-core", - "version": "0.22.7", + "version": "0.22.8", "description": "", "sideEffects": [ "./src/modules.ts", diff --git a/packages/vrender-kits/CHANGELOG.json b/packages/vrender-kits/CHANGELOG.json index e9d994c83..21762493f 100644 --- a/packages/vrender-kits/CHANGELOG.json +++ b/packages/vrender-kits/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender-kits", "entries": [ + { + "version": "0.22.8", + "tag": "@visactor/vrender-kits_v0.22.8", + "date": "Wed, 16 Apr 2025 03:17:31 GMT", + "comments": {} + }, { "version": "0.22.7", "tag": "@visactor/vrender-kits_v0.22.7", diff --git a/packages/vrender-kits/CHANGELOG.md b/packages/vrender-kits/CHANGELOG.md index 0cc197bf5..886c8edae 100644 --- a/packages/vrender-kits/CHANGELOG.md +++ b/packages/vrender-kits/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender-kits -This log was last generated on Fri, 11 Apr 2025 07:37:57 GMT and should not be manually modified. +This log was last generated on Wed, 16 Apr 2025 03:17:31 GMT and should not be manually modified. + +## 0.22.8 +Wed, 16 Apr 2025 03:17:31 GMT + +_Version update only_ ## 0.22.7 Fri, 11 Apr 2025 07:37:57 GMT diff --git a/packages/vrender-kits/package.json b/packages/vrender-kits/package.json index 068d4f080..42f124ffb 100644 --- a/packages/vrender-kits/package.json +++ b/packages/vrender-kits/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-kits", - "version": "0.22.7", + "version": "0.22.8", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -21,7 +21,7 @@ }, "dependencies": { "@visactor/vutils": "~0.19.5", - "@visactor/vrender-core": "workspace:0.22.7", + "@visactor/vrender-core": "workspace:0.22.8", "@resvg/resvg-js": "2.4.1", "roughjs": "4.5.2", "gifuct-js": "2.1.2", diff --git a/packages/vrender/CHANGELOG.json b/packages/vrender/CHANGELOG.json index 288df3240..2976481fa 100644 --- a/packages/vrender/CHANGELOG.json +++ b/packages/vrender/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender", "entries": [ + { + "version": "0.22.8", + "tag": "@visactor/vrender_v0.22.8", + "date": "Wed, 16 Apr 2025 03:17:31 GMT", + "comments": {} + }, { "version": "0.22.7", "tag": "@visactor/vrender_v0.22.7", diff --git a/packages/vrender/CHANGELOG.md b/packages/vrender/CHANGELOG.md index 7650d9d38..41698e47b 100644 --- a/packages/vrender/CHANGELOG.md +++ b/packages/vrender/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender -This log was last generated on Fri, 11 Apr 2025 07:37:57 GMT and should not be manually modified. +This log was last generated on Wed, 16 Apr 2025 03:17:31 GMT and should not be manually modified. + +## 0.22.8 +Wed, 16 Apr 2025 03:17:31 GMT + +_Version update only_ ## 0.22.7 Fri, 11 Apr 2025 07:37:57 GMT diff --git a/packages/vrender/package.json b/packages/vrender/package.json index 217fef31e..ee5d4ffad 100644 --- a/packages/vrender/package.json +++ b/packages/vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender", - "version": "0.22.7", + "version": "0.22.8", "description": "", "sideEffects": true, "main": "cjs/index.js", @@ -24,8 +24,8 @@ "test-watch": "cross-env DEBUG_MODE=1 jest --watch" }, "dependencies": { - "@visactor/vrender-core": "workspace:0.22.7", - "@visactor/vrender-kits": "workspace:0.22.7" + "@visactor/vrender-core": "workspace:0.22.8", + "@visactor/vrender-kits": "workspace:0.22.8" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/tools/bugserver-trigger/package.json b/tools/bugserver-trigger/package.json index 2060a0c3c..8e1b86a79 100644 --- a/tools/bugserver-trigger/package.json +++ b/tools/bugserver-trigger/package.json @@ -8,10 +8,10 @@ "ci": "ts-node --transpileOnly --skipProject ./scripts/trigger-test.ts" }, "dependencies": { - "@visactor/vrender": "workspace:0.22.7", - "@visactor/vrender-core": "workspace:0.22.7", - "@visactor/vrender-kits": "workspace:0.22.7", - "@visactor/vrender-components": "workspace:0.22.7" + "@visactor/vrender": "workspace:0.22.8", + "@visactor/vrender-core": "workspace:0.22.8", + "@visactor/vrender-kits": "workspace:0.22.8", + "@visactor/vrender-components": "workspace:0.22.8" }, "devDependencies": { "@rushstack/eslint-patch": "~1.1.4", From 128e2418fa8f93dc53c75741983498a23a1bc277 Mon Sep 17 00:00:00 2001 From: neuqzxy Date: Wed, 16 Apr 2025 03:39:53 +0000 Subject: [PATCH 008/179] docs: generate changelog of release v0.22.8 --- docs/assets/changelog/en/changelog.md | 13 +++++++++++++ docs/assets/changelog/zh/changelog.md | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/docs/assets/changelog/en/changelog.md b/docs/assets/changelog/en/changelog.md index 790d9ca15..f84b8740b 100644 --- a/docs/assets/changelog/en/changelog.md +++ b/docs/assets/changelog/en/changelog.md @@ -1,3 +1,16 @@ +# v0.22.8 + +2025-04-16 + + +**🐛 Bug fix** + +- **@visactor/vrender-components**: brush bug from aeolus and self test + + + +[more detail about v0.22.8](https://github.com/VisActor/VRender/releases/tag/v0.22.8) + # v0.22.5 2025-03-13 diff --git a/docs/assets/changelog/zh/changelog.md b/docs/assets/changelog/zh/changelog.md index 97f7a5ee7..e604f5b6e 100644 --- a/docs/assets/changelog/zh/changelog.md +++ b/docs/assets/changelog/zh/changelog.md @@ -1,3 +1,16 @@ +# v0.22.8 + +2025-04-16 + + +**🐛 功能修复** + +- **@visactor/vrender-components**: brush bug from aeolus and self test + + + +[更多详情请查看 v0.22.8](https://github.com/VisActor/VRender/releases/tag/v0.22.8) + # v0.22.5 2025-03-13 From 790946dba3b6a63f66f2fb2200fdabbb073e1a49 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 16 Apr 2025 17:16:47 +0800 Subject: [PATCH 009/179] feat: support EventTransformer to enable user dom transform --- .../src/common/event-listener-manager.ts | 147 ++++++++++++ .../src/common/event-transformer.ts | 142 ++++++++++++ packages/vrender-core/src/core/global.ts | 79 +++---- packages/vrender-core/src/core/window.ts | 59 +++-- .../vrender-core/src/event/event-system.ts | 28 ++- packages/vrender-core/src/index.ts | 1 + .../src/interface/event-listener-manager.ts | 45 ++++ packages/vrender-core/src/interface/global.ts | 3 +- packages/vrender-core/src/interface/window.ts | 2 + .../__tests__/browser/src/pages/vchart.ts | 215 ++++++------------ 10 files changed, 491 insertions(+), 230 deletions(-) create mode 100644 packages/vrender-core/src/common/event-listener-manager.ts create mode 100644 packages/vrender-core/src/common/event-transformer.ts create mode 100644 packages/vrender-core/src/interface/event-listener-manager.ts diff --git a/packages/vrender-core/src/common/event-listener-manager.ts b/packages/vrender-core/src/common/event-listener-manager.ts new file mode 100644 index 000000000..036ac309f --- /dev/null +++ b/packages/vrender-core/src/common/event-listener-manager.ts @@ -0,0 +1,147 @@ +import type { IEventListenerManager } from '../interface/event-listener-manager'; + +/** + * Base class to manage event listeners with support for event transformation + * Used by DefaultGlobal and DefaultWindow to handle the transformation of event coordinates + */ +export class EventListenerManager implements IEventListenerManager { + /** + * Map that stores the mapping from original listeners to wrapped listeners + * Structure: Map> + */ + protected _listenerMap: Map>; + + /** + * Transformer function that transforms the event + */ + protected _eventListenerTransformer: (event: Event) => Event; + + constructor() { + this._listenerMap = new Map(); + this._eventListenerTransformer = event => event; // Default: no transformation + } + + /** + * Set the event transformer function + * @param transformer Function that transforms events + */ + setEventListenerTransformer(transformer: (event: Event) => Event): void { + this._eventListenerTransformer = transformer || (event => event); + } + + /** + * Add an event listener with event transformation + * @param type Event type + * @param listener Original event listener + * @param options Event listener options + */ + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions + ): void { + if (!listener) { + return; + } + + // Create a wrapped listener that applies the transformation + const wrappedListener = (event: Event) => { + const transformedEvent = this._eventListenerTransformer(event); + if (typeof listener === 'function') { + listener(transformedEvent); + } else if (listener.handleEvent) { + listener.handleEvent(transformedEvent); + } + }; + + // Store the mapping between original and wrapped listener + if (!this._listenerMap.has(type)) { + this._listenerMap.set(type, new Map()); + } + this._listenerMap.get(type)!.set(listener, wrappedListener); + + // Add the wrapped listener + this._nativeAddEventListener(type, wrappedListener, options); + } + + /** + * Remove an event listener + * @param type Event type + * @param listener Event listener to remove + * @param options Event listener options + */ + removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions + ): void { + if (!listener) { + return; + } + + // Get the wrapped listener from our map + const wrappedListener = this._listenerMap.get(type)?.get(listener); + if (wrappedListener) { + // Remove the wrapped listener + this._nativeRemoveEventListener(type, wrappedListener, options); + + // Remove from our map + this._listenerMap.get(type)!.delete(listener); + if (this._listenerMap.get(type)!.size === 0) { + this._listenerMap.delete(type); + } + } + } + + /** + * Dispatch an event + * @param event Event to dispatch + */ + dispatchEvent(event: Event): boolean { + return this._nativeDispatchEvent(event); + } + + /** + * Clear all event listeners + */ + clearAllEventListeners(): void { + this._listenerMap.forEach((listenersMap, type) => { + listenersMap.forEach((wrappedListener, originalListener) => { + this._nativeRemoveEventListener(type, wrappedListener, undefined); + }); + }); + this._listenerMap.clear(); + } + + /** + * Native implementation of addEventListener + * To be implemented by derived classes + */ + protected _nativeAddEventListener( + type: string, + listener: EventListener, + options?: boolean | AddEventListenerOptions + ): void { + throw new Error('_nativeAddEventListener must be implemented by derived classes'); + } + + /** + * Native implementation of removeEventListener + * To be implemented by derived classes + */ + protected _nativeRemoveEventListener( + type: string, + listener: EventListener, + options?: boolean | EventListenerOptions + ): void { + throw new Error('_nativeRemoveEventListener must be implemented by derived classes'); + } + + /** + * Native implementation of dispatchEvent + * To be implemented by derived classes + */ + protected _nativeDispatchEvent(event: Event): boolean { + throw new Error('_nativeDispatchEvent must be implemented by derived classes'); + } +} diff --git a/packages/vrender-core/src/common/event-transformer.ts b/packages/vrender-core/src/common/event-transformer.ts new file mode 100644 index 000000000..ea8b0cb24 --- /dev/null +++ b/packages/vrender-core/src/common/event-transformer.ts @@ -0,0 +1,142 @@ +import type { IAABBBounds } from '@visactor/vutils'; +import type { Matrix } from '@visactor/vutils'; +import type { IGlobal, IWindow } from '../interface'; +import { isNumber } from '../canvas/util'; + +function isIdentityMatrix(matrix: Matrix): boolean { + return matrix.a === 1 && matrix.b === 0 && matrix.c === 0 && matrix.d === 1 && matrix.e === 0 && matrix.f === 0; +} + +/** + * Create an event transformer that corrects event coordinates based on container transformations + * @param containerElement The container element + * @param matrix The transformation matrix to apply + * @param rect Optional DOMRect of the container, if not provided will use getBoundingClientRect + * @returns A function that transforms events to correct coordinates + */ +export function createEventTransformer( + containerElement: HTMLElement, + getMatrix: () => Matrix, + getRect: () => IAABBBounds, + transformPoint: (clientX: number, clientY: number, matrix: Matrix, rect: IAABBBounds, transformedEvent: Event) => void +): (event: Event) => Event { + return (event: Event): Event => { + // Only transform mouse and touch events that have coordinates + if (!(event instanceof MouseEvent) && !(event instanceof TouchEvent) && !(event instanceof PointerEvent)) { + return event; + } + + // Use provided matrix + const transformMatrix = getMatrix(); + + // If there's no transformation, return the original event + if (isIdentityMatrix(transformMatrix)) { + return event; + } + + // Get the container's bounding rect for coordinate conversion + const containerRect = getRect(); + + // Create a copy of the event to modify + const transformedEvent = new (event.constructor as any)(event.type, event); + Object.defineProperties(transformedEvent, { + target: { value: event.target }, + currentTarget: { value: event.currentTarget } + }); + + if (event instanceof MouseEvent || event instanceof PointerEvent) { + transformPoint(event.clientX, event.clientY, transformMatrix, containerRect, transformedEvent); + } else if (event instanceof TouchEvent) { + // For touch events, we need to transform each touch point + // This is a simplified version that assumes we're only using the first touch + if (event.touches.length > 0) { + const touch = event.touches[0]; + transformPoint(touch.clientX, touch.clientY, transformMatrix, containerRect, transformedEvent); + } + } + + return transformedEvent; + }; +} + +/** + * Create an event transformer for the given canvas element + * @param canvasElement The canvas element + * @param getMatrix The transformation matrix to apply + * @param getRect Optional DOMRect of the container + * @returns A function that transforms events to correct coordinates + */ +export function createCanvasEventTransformer( + canvasElement: HTMLCanvasElement, + getMatrix: () => Matrix, + getRect: () => IAABBBounds, + transformPoint: (clientX: number, clientY: number, matrix: Matrix, rect: IAABBBounds, transformedEvent: Event) => void +): (event: Event) => Event { + return createEventTransformer(canvasElement.parentElement || canvasElement, getMatrix, getRect, transformPoint); +} + +/** + * Register the event transformer with a DefaultWindow instance + * @param window The window instance + * @param container The container element + * @param getMatrix The transformation matrix to apply + * @param getRect Optional DOMRect of the container + */ +export function registerWindowEventTransformer( + window: IWindow, + container: HTMLElement, + getMatrix: () => Matrix, + getRect: () => IAABBBounds, + transformPoint: (clientX: number, clientY: number, matrix: Matrix, rect: IAABBBounds, transformedEvent: Event) => void +): void { + const transformer = createEventTransformer(container, getMatrix, getRect, transformPoint); + window.setEventListenerTransformer(transformer); +} + +/** + * Register the event transformer with a DefaultGlobal instance + * @param global The global instance + * @param container The container element + * @param getMatrix The transformation matrix to apply + * @param getRect Optional DOMRect of the container + */ +export function registerGlobalEventTransformer( + global: IGlobal, + container: HTMLElement, + getMatrix: () => Matrix, + getRect: () => IAABBBounds, + transformPoint: (clientX: number, clientY: number, matrix: Matrix, rect: IAABBBounds, transformedEvent: Event) => void +): void { + const transformer = createEventTransformer(container, getMatrix, getRect, transformPoint); + global.setEventListenerTransformer(transformer); +} + +export function transformPointForCanvas( + clientX: number, + clientY: number, + matrix: Matrix, + rect: IAABBBounds, + transformedEvent: Event +) { + // Apply the inverse transformation + const transformedPoint = { x: clientX, y: clientY }; + + matrix.transformPoint(transformedPoint, transformedPoint); + + // Update the event properties + Object.defineProperties(transformedEvent, { + _canvasX: { value: transformedPoint.x }, + _canvasY: { value: transformedPoint.y } + }); + return; +} + +export function mapToCanvasPointForCanvas(nativeEvent: any) { + if (isNumber(nativeEvent._canvasX) && isNumber(nativeEvent._canvasY)) { + return { + x: nativeEvent._canvasX, + y: nativeEvent._canvasY + }; + } + return; +} diff --git a/packages/vrender-core/src/core/global.ts b/packages/vrender-core/src/core/global.ts index 21fa87ad0..f94e8edbe 100644 --- a/packages/vrender-core/src/core/global.ts +++ b/packages/vrender-core/src/core/global.ts @@ -16,10 +16,11 @@ import { EnvContribution } from '../constants'; import type { IAABBBoundsLike } from '@visactor/vutils'; import { container } from '../container'; import { Generator } from '../common/generator'; +import { EventListenerManager } from '../common/event-listener-manager'; const defaultEnv: EnvType = 'browser'; @injectable() -export class DefaultGlobal implements IGlobal { +export class DefaultGlobal extends EventListenerManager implements IGlobal { readonly id: number; private _env: EnvType; private _isSafari?: boolean; @@ -124,12 +125,17 @@ export class DefaultGlobal implements IGlobal { onSetEnv: ISyncHook<[EnvType | undefined, EnvType, IGlobal]>; }; + // 事件监听器转换器,用于进行Event属性的转换,接收一个原生的Event,返回一个修改后的Event(默认不进行转换直接返回原始Event) + // 注意返回的Event和原始的Event不是同一个对象,但也不能拷贝,返回的Event和原始Event是同一个Event类的实例(比如MouseEvent、FederatedPointerEvent等,不能直接拷贝或者用CustomEvent) + eventListenerTransformer: (event: Event) => Event = event => event; + constructor( // todo: 不需要创建,动态获取就行? @inject(ContributionProvider) @named(EnvContribution) protected readonly contributions: IContributionProvider ) { + super(); this.id = Generator.GenAutoIncrementId(); this.hooks = { onSetEnv: new SyncHook<[EnvType | undefined, EnvType, IGlobal]>(['lastEnv', 'env', 'global']) @@ -138,6 +144,38 @@ export class DefaultGlobal implements IGlobal { this.optimizeVisible = false; } + // Override from EventListenerManager + protected _nativeAddEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions + ): void { + if (!this._env) { + this.setEnv(defaultEnv); + } + return this.envContribution.addEventListener(type, listener, options); + } + + // Override from EventListenerManager + protected _nativeRemoveEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions + ): void { + if (!this._env) { + this.setEnv(defaultEnv); + } + return this.envContribution.removeEventListener(type, listener, options); + } + + // Override from EventListenerManager + protected _nativeDispatchEvent(event: Event): boolean { + if (!this._env) { + this.setEnv(defaultEnv); + } + return this.envContribution.dispatchEvent(event); + } + protected bindContribution(params?: any): void | Promise { const promiseArr: any[] = []; this.contributions.getContributions().forEach(contribution => { @@ -230,43 +268,6 @@ export class DefaultGlobal implements IGlobal { return this.envContribution.releaseCanvas(canvas); } - addEventListener( - type: K, - listener: (this: Document, ev: DocumentEventMap[K]) => any, - options?: boolean | AddEventListenerOptions - ): void; - addEventListener( - type: string, - listener: EventListenerOrEventListenerObject, - options?: boolean | AddEventListenerOptions - ): void { - if (!this._env) { - this.setEnv(defaultEnv); - } - return this.envContribution.addEventListener(type, listener, options); - } - removeEventListener( - type: K, - listener: (this: Document, ev: DocumentEventMap[K]) => any, - options?: boolean | EventListenerOptions - ): void; - removeEventListener( - type: string, - listener: EventListenerOrEventListenerObject, - options?: boolean | EventListenerOptions - ): void { - if (!this._env) { - this.setEnv(defaultEnv); - } - return this.envContribution.removeEventListener(type, listener, options); - } - dispatchEvent(event: any): boolean { - if (!this._env) { - this.setEnv(defaultEnv); - } - return this.envContribution.dispatchEvent(event); - } - getRequestAnimationFrame() { if (!this._env) { this.setEnv(defaultEnv); @@ -356,7 +357,7 @@ export class DefaultGlobal implements IGlobal { return this.envContribution.loadBlob(url); } - async loadFont(name: string, source: string | BinaryData, descriptors?: FontFaceDescriptors) { + async loadFont(name: string, source: string | ArrayBuffer, descriptors?: FontFaceDescriptors) { if (!this._env) { this.setEnv('browser'); } diff --git a/packages/vrender-core/src/core/window.ts b/packages/vrender-core/src/core/window.ts index ab44691cb..e0c310ec7 100644 --- a/packages/vrender-core/src/core/window.ts +++ b/packages/vrender-core/src/core/window.ts @@ -14,6 +14,7 @@ import type { import { container } from '../container'; import { SyncHook } from '../tapable'; import { application } from '../application'; +import { EventListenerManager } from '../common/event-listener-manager'; export const VWindow = Symbol.for('VWindow'); @@ -25,7 +26,7 @@ export const WindowHandlerContribution = Symbol.for('WindowHandlerContribution') * 对于原生,就是管理这个系统窗口 */ @injectable() -export class DefaultWindow implements IWindow { +export class DefaultWindow extends EventListenerManager implements IWindow { protected _width: number; protected _height: number; protected _handler: IWindowHandlerContribution; @@ -76,11 +77,35 @@ export class DefaultWindow implements IWindow { } constructor() { + super(); this._uid = Generator.GenAutoIncrementId(); this.global = application.global; this.postInit(); } + // Override from EventListenerManager + protected _nativeAddEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions + ): void { + return this._handler.addEventListener(type, listener, options); + } + + // Override from EventListenerManager + protected _nativeRemoveEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions + ): void { + return this._handler.removeEventListener(type, listener, options); + } + + // Override from EventListenerManager + protected _nativeDispatchEvent(event: Event): boolean { + return this._handler.dispatchEvent(event); + } + protected postInit() { this.global.hooks.onSetEnv.tap('window', this.active); this.active(); @@ -151,7 +176,8 @@ export class DefaultWindow implements IWindow { } release(): void { this.global.hooks.onSetEnv.unTap('window', this.active); - + // Clean up all event listeners + this.clearAllEventListeners(); return this._handler.releaseWindow(); } getContext(): IContext2d { @@ -167,35 +193,6 @@ export class DefaultWindow implements IWindow { return this._handler.getImageBuffer(type); } - addEventListener( - type: K, - listener: (this: Document, ev: DocumentEventMap[K]) => any, - options?: boolean | AddEventListenerOptions - ): void; - addEventListener( - type: string, - listener: EventListenerOrEventListenerObject, - options?: boolean | AddEventListenerOptions - ): void { - return this._handler.addEventListener(type, listener, options); - } - removeEventListener( - type: K, - listener: (this: Document, ev: DocumentEventMap[K]) => any, - options?: boolean | EventListenerOptions - ): void; - removeEventListener( - type: string, - listener: EventListenerOrEventListenerObject, - options?: boolean | EventListenerOptions - ): void { - return this._handler.removeEventListener(type, listener, options); - } - - dispatchEvent(event: any): boolean { - return this._handler.dispatchEvent(event); - } - getBoundingClientRect(): IDomRectLike { return this._handler.getBoundingClientRect(); } diff --git a/packages/vrender-core/src/event/event-system.ts b/packages/vrender-core/src/event/event-system.ts index 963ef1355..b71e88aca 100644 --- a/packages/vrender-core/src/event/event-system.ts +++ b/packages/vrender-core/src/event/event-system.ts @@ -271,8 +271,8 @@ export class EventSystem { if (this.supportsPointerEvents) { if (globalObj.getDocument()) { - globalObj.getDocument().addEventListener('pointermove', this.onPointerMove, true); - globalObj.getDocument().addEventListener('pointerup', this.onPointerUp, true); + globalObj.addEventListener('pointermove', this.onPointerMove, true); + globalObj.addEventListener('pointerup', this.onPointerUp, true); } else { domElement.addEventListener('pointermove', this.onPointerMove, true); domElement.addEventListener('pointerup', this.onPointerUp, true); @@ -282,8 +282,8 @@ export class EventSystem { domElement.addEventListener('pointerover', this.onPointerOverOut, true); } else { if (globalObj.getDocument()) { - globalObj.getDocument().addEventListener('mousemove', this.onPointerMove, true); - globalObj.getDocument().addEventListener('mouseup', this.onPointerUp, true); + globalObj.addEventListener('mousemove', this.onPointerMove, true); + globalObj.addEventListener('mouseup', this.onPointerUp, true); } else { domElement.addEventListener('mousemove', this.onPointerMove, true); domElement.addEventListener('mouseup', this.onPointerUp, true); @@ -311,17 +311,27 @@ export class EventSystem { return; } const { globalObj, domElement } = this; - const globalDocument = globalObj.getDocument() ?? domElement; + // const globalDocument = globalObj.getDocument() ?? domElement; if (this.supportsPointerEvents) { - globalDocument.removeEventListener('pointermove', this.onPointerMove, true); - globalDocument.removeEventListener('pointerup', this.onPointerUp, true); + if (globalObj.getDocument()) { + globalObj.removeEventListener('pointermove', this.onPointerMove, true); + globalObj.removeEventListener('pointerup', this.onPointerUp, true); + } else { + domElement.removeEventListener('pointermove', this.onPointerMove, true); + domElement.removeEventListener('pointerup', this.onPointerUp, true); + } domElement.removeEventListener('pointerdown', this.onPointerDown, true); domElement.removeEventListener('pointerleave', this.onPointerOverOut, true); domElement.removeEventListener('pointerover', this.onPointerOverOut, true); } else { - globalDocument.removeEventListener('mousemove', this.onPointerMove, true); - globalDocument.removeEventListener('mouseup', this.onPointerUp, true); + if (globalObj.getDocument()) { + globalObj.removeEventListener('mousemove', this.onPointerMove, true); + globalObj.removeEventListener('mouseup', this.onPointerUp, true); + } else { + domElement.removeEventListener('mousemove', this.onPointerMove, true); + domElement.removeEventListener('mouseup', this.onPointerUp, true); + } domElement.removeEventListener('mousedown', this.onPointerDown, true); domElement.removeEventListener('mouseout', this.onPointerOverOut, true); domElement.removeEventListener('mouseover', this.onPointerOverOut, true); diff --git a/packages/vrender-core/src/index.ts b/packages/vrender-core/src/index.ts index 042131874..171ebd7c1 100644 --- a/packages/vrender-core/src/index.ts +++ b/packages/vrender-core/src/index.ts @@ -45,6 +45,7 @@ export * from './common/morphing-utils'; export * from './common/split-path'; export * from './common/enums'; export * from './common/generator'; +export * from './common/event-transformer'; export * from './plugins/constants'; export * from './plugins/builtin-plugin/richtext-edit-plugin'; export * from './allocator/matrix-allocate'; diff --git a/packages/vrender-core/src/interface/event-listener-manager.ts b/packages/vrender-core/src/interface/event-listener-manager.ts new file mode 100644 index 000000000..a7cc75e55 --- /dev/null +++ b/packages/vrender-core/src/interface/event-listener-manager.ts @@ -0,0 +1,45 @@ +/** + * Interface for event listener management with transformation capabilities + */ +export interface IEventListenerManager { + /** + * Set the event transformer function + * @param transformer Function that transforms events + */ + setEventListenerTransformer: (transformer: (event: Event) => Event) => void; + + /** + * Add an event listener with event transformation + * @param type Event type + * @param listener Event listener function or object + * @param options Event listener options + */ + addEventListener: ( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions + ) => void; + + /** + * Remove an event listener + * @param type Event type + * @param listener Event listener to remove + * @param options Event listener options + */ + removeEventListener: ( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions + ) => void; + + /** + * Dispatch an event + * @param event Event to dispatch + */ + dispatchEvent: (event: Event) => boolean; + + /** + * Clear all event listeners + */ + clearAllEventListeners: () => void; +} diff --git a/packages/vrender-core/src/interface/global.ts b/packages/vrender-core/src/interface/global.ts index 3675c02da..1f26204ec 100644 --- a/packages/vrender-core/src/interface/global.ts +++ b/packages/vrender-core/src/interface/global.ts @@ -129,7 +129,7 @@ export interface IEnvContribution */ loadFont: ( font: string, - source: string | BinaryData, + source: string | any, descriptors?: FontFaceDescriptors ) => Promise<{ loadState: 'success' | 'fail'; @@ -208,6 +208,7 @@ export interface IGlobal extends Omit HTMLCanvasElement | any; createOffscreenCanvas: (params: ICreateCanvasParams) => HTMLCanvasElement | any; releaseCanvas: (canvas: HTMLCanvasElement | string | any) => void; + setEventListenerTransformer: (transformer: (event: Event) => Event) => void; /** * 获取环境中最大动态canvas的数量,offscreenCanvas或者framebuffer diff --git a/packages/vrender-core/src/interface/window.ts b/packages/vrender-core/src/interface/window.ts index b403f2d50..3af828dbc 100644 --- a/packages/vrender-core/src/interface/window.ts +++ b/packages/vrender-core/src/interface/window.ts @@ -74,6 +74,8 @@ export interface IWindow onVisibleChange: (cb: (currentVisible: boolean) => void) => void; getTopLeft: (baseWindow?: boolean) => { top: number; left: number }; + + setEventListenerTransformer: (transformer: (event: Event) => Event) => void; } export interface IWindowHandlerContribution diff --git a/packages/vrender/__tests__/browser/src/pages/vchart.ts b/packages/vrender/__tests__/browser/src/pages/vchart.ts index 635b5fcf7..593416a59 100644 --- a/packages/vrender/__tests__/browser/src/pages/vchart.ts +++ b/packages/vrender/__tests__/browser/src/pages/vchart.ts @@ -2,7 +2,6 @@ import * as VRender from '@visactor/vrender'; import * as VRenderCore from '@visactor/vrender-core'; import * as VRenderKits from '@visactor/vrender-kits'; import * as VRenderComponents from '@visactor/vrender-components'; -import { addShapesToStage, colorPools } from '../utils'; import { pi, pi2 } from '@visactor/vutils'; export const page = () => { @@ -23,89 +22,40 @@ export const page = () => { 'M 8.25 -11 L 11 -11 V -8.25 L -8.25 11 H -11 V 8.25 L 8.25 -11 Z M -11 -11 H -8.3789 L -11 -8.2539 V -11 Z M 11 11 H 8.3789 L 11 8.2539 V 11 Z'; const spec = { - type: 'linearProgress', + type: 'bar', + width: 600, + height: 300, data: [ { - id: 'id0', + id: 'barData', values: [ - { - type: 'Tradition Industries', - value: 0.795, - text: '79.5%' - }, - { - type: 'Business Companies', - value: 0.25, - text: '25%' - }, - { - type: 'Customer-facing Companies', - value: 0.065, - text: '6.5%' - } + { month: 'Monday', sales: 22 }, + { month: 'Tuesday', sales: 13 }, + { month: 'Wednesday', sales: 25 }, + { month: 'Thursday', sales: 29 }, + { month: 'Friday', sales: 38 } ] } ], - direction: 'horizontal', - xField: 'value', - yField: 'type', - seriesField: 'type', - progress: { - style: { - // boundsMode: 'imprecise', - _debug_bounds: true, - texture: path, - textureSize: 30, - texturePadding: 0, - textureRatio: 1, - textureColor: 'orange', - textureOptions: datum => { - return { - // useNewCanvas: true, - beforeDynamicTexture: (ctx, row, column, rowCount, columnCount, ratio, graphic) => { - const dx = ratio - 0.5; - const size = 30; - ctx.translate(dx * size, 0); - }, - dynamicTexture: (ctx, row, column, rowCount, columnCount, ratio, graphic) => { - const dx = ratio - 0.5; - const size = 30; - ctx.translate(-dx * size, 0); - ctx.fillStyle = 'white'; - ctx.globalAlpha = 0.6; - ctx.fill(); - } - }; + bar: { + state: { + hover: { + fill: 'red' } } }, - animationAppear: { - progress: { - channel: { - textureRatio: { - from: 0, - to: 1 - } - }, - easing: 'linear', - duration: 3000, - loop: true - } + tooltip: { + parentElement: 'container' }, - cornerRadius: 20, - bandWidth: 30, - axes: [ - { - orient: 'left', - label: { visible: true }, - type: 'band', - domainLine: { visible: false }, - tick: { visible: false } - }, - { orient: 'bottom', label: { visible: true }, type: 'linear', visible: false } - ] + xField: 'month', + yField: 'sales' }; + const container = document.getElementById('container')!; + + Array.from(container.childNodes).forEach(child => { + container.removeChild(child); + }); // const spec = { // type: 'area', // data: { @@ -446,87 +396,52 @@ export const page = () => { // }; const chartSpace = new window.ChartSpace.default(spec, { - dom: 'container' + dom: container }); - // setTimeout(() => { - // chartSpace.updateSpec({ - // type: 'treemap', - // color: ['#F2F6FF', '#D9E3FF', '#BFD0FF', '#A6BDFF', '#8CAAFF', '#7397FF', '#5984FF', '#4071FF', '#2E5DE5'], - // label: { - // visible: true, - // style: { - // html: (_, a) => { - // return { - // dom: `
测试
`, - // width: 70, - // height: 60 - // }; - // } - // } - // }, - // categoryField: 'name', - // valueField: 'value', - // data: [ - // { - // values: [ - // { - // name: 'A', - // value: 1 - // }, - // { - // name: 'B', - // value: 2 - // }, - // { - // name: 'C', - // value: 6 - // }, - // { - // name: 'D', - // value: 12 - // }, - // { - // name: 'E', - // value: 29 - // } - // ] - // } - // ] - // }); + const domRect = container.getBoundingClientRect(); + const x1 = domRect.left; + const y1 = domRect.top; + const x2 = domRect.right; + const y2 = domRect.bottom; + const getRect = () => { + return { + x1, + y1, + x2, + y2 + }; + }; + + container.style.transform = 'rotate(90deg)'; + console.log('aaa', x1, y1, x2, y2); + + const getMatrix = () => { + const matrix = VRender.matrixAllocate.allocate(1, 0, 0, 1, 0, 0); + matrix.translate(x1, y1); + const width = x2 - x1; + const height = y2 - y1; + matrix.translate(width / 2, height / 2); + matrix.rotate(pi / 2); + matrix.translate(-width / 2, -height / 2); - // // chartSpace.updateData('data0', [ - // // { - // // name: 'A', - // // value: 1 - // // }, - // // { - // // name: 'B', - // // value: 2 - // // }, - // // { - // // name: 'C', - // // value: 6 - // // }, - // // { - // // name: 'D', - // // value: 12 - // // }, - // // { - // // name: 'E', - // // value: 29 - // // } - // // ]); - // setTimeout(() => { - // console.log( - // '2', - // chartSpace - // .getStage() - // .getElementsByType('text') - // .map(item => item._uid) - // ); - // }, 2000); - // }, 3000); + return matrix; + }; + VRender.registerGlobalEventTransformer( + VRender.vglobal, + container, + getMatrix, + getRect, + VRender.transformPointForCanvas + ); + VRender.registerWindowEventTransformer( + chartSpace.getStage().window as any, + container, + getMatrix, + getRect, + VRender.transformPointForCanvas + ); + VRender.vglobal.mapToCanvasPoint = VRender.mapToCanvasPointForCanvas; chartSpace.renderSync(); console.log( From 5d6134be92559634544929582c18557a38b64760 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 17 Apr 2025 13:30:46 +0800 Subject: [PATCH 010/179] fix: fix issue with string anchor and scale --- .../vrender-core/src/common/segment/index.ts | 4 ++- .../vrender-core/src/common/segment/step.ts | 33 +++++++++++++++++++ packages/vrender-core/src/graphic/graphic.ts | 13 ++++---- packages/vrender-core/src/interface/common.ts | 1 + 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/packages/vrender-core/src/common/segment/index.ts b/packages/vrender-core/src/common/segment/index.ts index 5cfdaf783..4c065929d 100644 --- a/packages/vrender-core/src/common/segment/index.ts +++ b/packages/vrender-core/src/common/segment/index.ts @@ -3,7 +3,7 @@ import type { ICurveType, IGenSegmentParams, ISegPath2D } from '../../interface' import { genLinearSegments } from './linear'; import { genBasisSegments } from './basis'; import { genMonotoneXSegments, genMonotoneYSegments } from './monotone'; -import { genStepSegments } from './step'; +import { genStepClosedSegments, genStepSegments } from './step'; import { genLinearClosedSegments } from './linear-closed'; import { genCatmullRomSegments } from './catmull-rom'; import { genCatmullRomClosedSegments } from './catmull-rom-close'; @@ -31,6 +31,8 @@ export function calcLineCache( return genMonotoneYSegments(points, params); case 'step': return genStepSegments(points, 0.5, params); + case 'stepClosed': + return genStepClosedSegments(points, 0.5, params); case 'stepBefore': return genStepSegments(points, 0, params); case 'stepAfter': diff --git a/packages/vrender-core/src/common/segment/step.ts b/packages/vrender-core/src/common/segment/step.ts index f6981e038..c3e589219 100644 --- a/packages/vrender-core/src/common/segment/step.ts +++ b/packages/vrender-core/src/common/segment/step.ts @@ -107,6 +107,16 @@ export class Step implements ICurvedSegment { } } +export class StepClosed extends Step implements ILinearSegment { + declare context: ISegPath2D; + + protected declare startPoint?: IPointLike; + + lineEnd() { + this.context.closePath(); + } +} + export function genStepSegments(points: IPointLike[], t: number, params: IGenSegmentParams = {}): ISegPath2D | null { const { direction, startPoint } = params; if (points.length < 2 - Number(!!startPoint)) { @@ -129,3 +139,26 @@ export function genStepSegments(points: IPointLike[], t: number, params: IGenSeg export function genStepTypeSegments(path: ILinearSegment, points: IPointLike[]): void { return genCurveSegments(path, points, 1); } + +export function genStepClosedSegments( + points: IPointLike[], + t: number, + params: IGenSegmentParams = {} +): ISegPath2D | null { + const { direction, startPoint } = params; + if (points.length < 2 - Number(!!startPoint)) { + return null; + } + const segContext = new SegContext( + 'step', + direction ?? + (abs(points[points.length - 1].x - points[0].x) > abs(points[points.length - 1].y - points[0].y) + ? Direction.ROW + : Direction.COLUMN) + ); + const step = new StepClosed(segContext, t, startPoint); + + genStepTypeSegments(step, points); + + return segContext; +} diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index ffac66a6d..b45420219 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -1211,15 +1211,14 @@ export abstract class Graphic = Partial Date: Wed, 16 Apr 2025 20:50:32 +0800 Subject: [PATCH 011/179] feat: support reInit and refreshAllContributions to fix contribution can not be load when multi app use vrender --- .../src/common/contribution-provider.ts | 29 +++++++++++++++++++ packages/vrender-core/src/core/stage.ts | 5 ++++ packages/vrender-core/src/index.ts | 1 + packages/vrender-core/src/interface/picker.ts | 1 + packages/vrender-core/src/interface/render.ts | 4 +++ .../src/picker/global-picker-service.ts | 4 +++ .../vrender-core/src/picker/picker-service.ts | 4 +++ .../render/contributions/render/arc-render.ts | 4 +-- .../contributions/render/area-render.ts | 4 +-- .../contributions/render/base-render.ts | 5 ++++ .../contributions/render/circle-render.ts | 4 +-- .../contributions/render/draw-contribution.ts | 7 +++++ .../contributions/render/glyph-render.ts | 4 +++ .../contributions/render/graphic-render.ts | 2 ++ .../contributions/render/group-render.ts | 5 ++++ .../contributions/render/image-render.ts | 4 +-- .../contributions/render/path-render.ts | 4 +-- .../contributions/render/polygon-render.ts | 4 +-- .../contributions/render/rect-render.ts | 4 +-- .../contributions/render/symbol-render.ts | 4 +-- .../contributions/render/text-render.ts | 4 +-- .../vrender-core/src/render/render-service.ts | 4 +++ .../render/contributions/rough/base-render.ts | 4 +++ .../__tests__/browser/src/pages/symbol.ts | 4 +-- 24 files changed, 99 insertions(+), 20 deletions(-) diff --git a/packages/vrender-core/src/common/contribution-provider.ts b/packages/vrender-core/src/common/contribution-provider.ts index 0f06f0136..043e6b71c 100644 --- a/packages/vrender-core/src/common/contribution-provider.ts +++ b/packages/vrender-core/src/common/contribution-provider.ts @@ -11,6 +11,7 @@ class ContributionProviderCache implements IContributionProvider { constructor(serviceIdentifier: interfaces.ServiceIdentifier, container: interfaces.Container) { this.serviceIdentifier = serviceIdentifier; this.container = container; + ContributionStore.setStore(this.serviceIdentifier, this); } getContributions(): T[] { @@ -22,6 +23,16 @@ class ContributionProviderCache implements IContributionProvider { } return this.caches; } + + refresh() { + if (!this.caches) { + return; + } + this.caches.length = 0; + this.container && + this.container.isBound(this.serviceIdentifier) && + this.caches.push(...this.container.getAll(this.serviceIdentifier)); + } } export function bindContributionProvider(bind: interfaces.Bind, id: any): void { @@ -36,3 +47,21 @@ export function bindContributionProviderNoSingletonScope(bind: interfaces.Bind, .toDynamicValue(({ container }) => new ContributionProviderCache(id, container)) .whenTargetNamed(id); } + +export class ContributionStore { + static store: Map, ContributionProviderCache> = new Map(); + + static getStore(id: interfaces.ServiceIdentifier): ContributionProviderCache { + return this.store.get(id); + } + + static setStore(id: interfaces.ServiceIdentifier, cache: ContributionProviderCache): void { + this.store.set(id, cache); + } + + static refreshAllContributions(): void { + this.store.forEach(cache => { + cache.refresh(); + }); + } +} diff --git a/packages/vrender-core/src/core/stage.ts b/packages/vrender-core/src/core/stage.ts index e16c43d50..58277b4d9 100644 --- a/packages/vrender-core/src/core/stage.ts +++ b/packages/vrender-core/src/core/stage.ts @@ -1123,4 +1123,9 @@ export class Stage extends Group implements IStage { } return this.pickerService; } + + reInit() { + this.renderService.reInit(); + this.pickerService.reInit(); + } } diff --git a/packages/vrender-core/src/index.ts b/packages/vrender-core/src/index.ts index 042131874..a0df654d0 100644 --- a/packages/vrender-core/src/index.ts +++ b/packages/vrender-core/src/index.ts @@ -50,6 +50,7 @@ export * from './plugins/builtin-plugin/richtext-edit-plugin'; export * from './allocator/matrix-allocate'; export * from './allocator/canvas-allocate'; export * from './allocator/graphic-allocate'; +export * from './common/contribution-provider'; export * from './animate/default-ticker'; export { wrapCanvas, wrapContext } from './canvas/util'; diff --git a/packages/vrender-core/src/interface/picker.ts b/packages/vrender-core/src/interface/picker.ts index 9a21981dd..171c2cf84 100644 --- a/packages/vrender-core/src/interface/picker.ts +++ b/packages/vrender-core/src/interface/picker.ts @@ -63,6 +63,7 @@ export interface IPickerService { ) => PickResult | null; containsPoint: (graphic: IGraphic, point: IPointLike, params?: IPickParams) => boolean; drawContribution?: IDrawContribution; + reInit: () => void; } export interface IPickItemInterceptorContribution { diff --git a/packages/vrender-core/src/interface/render.ts b/packages/vrender-core/src/interface/render.ts index 8a2da061a..27e9831d8 100644 --- a/packages/vrender-core/src/interface/render.ts +++ b/packages/vrender-core/src/interface/render.ts @@ -38,6 +38,7 @@ export interface IRenderService { draw: (params: IRenderServiceDrawParams) => void; afterDraw: (params: IRenderServiceDrawParams) => void; render: (groups: IGroup[], params: IRenderServiceDrawParams) => MaybePromise; + reInit: () => void; } export interface IDrawContext extends IRenderServiceDrawParams { @@ -70,6 +71,7 @@ export interface IDrawContribution { getRenderContribution: (graphic: IGraphic) => IGraphicRender | null; renderGroup: (group: IGroup, drawContext: IDrawContext, matrix: IMatrixLike, skipSort?: boolean) => void; renderItem: (graphic: IGraphic, drawContext: IDrawContext, params?: IGraphicRenderDrawParams) => void; + reInit: () => void; } export interface IGraphicRenderDrawParams { @@ -109,6 +111,8 @@ export interface IGraphicRender { themeAttribute: IThemeAttribute ) => boolean ) => void; + + reInit: () => void; } export interface IBeforeRenderConstribution { diff --git a/packages/vrender-core/src/picker/global-picker-service.ts b/packages/vrender-core/src/picker/global-picker-service.ts index 0250c18c9..33eff0864 100644 --- a/packages/vrender-core/src/picker/global-picker-service.ts +++ b/packages/vrender-core/src/picker/global-picker-service.ts @@ -30,6 +30,10 @@ export class DefaultGlobalPickerService implements IPickerService { this.configure(this.global, this.global.env); } + reInit() { + return; + } + configure(global: IGlobal, env: EnvType) { // if (!this.global.env) return; // this.contributions.getContributions().forEach(handlerContribution => { diff --git a/packages/vrender-core/src/picker/picker-service.ts b/packages/vrender-core/src/picker/picker-service.ts index 3b37a2c07..8b0d6a397 100644 --- a/packages/vrender-core/src/picker/picker-service.ts +++ b/packages/vrender-core/src/picker/picker-service.ts @@ -48,6 +48,10 @@ export abstract class DefaultPickService implements IPickerService { this.global = application.global; } + reInit() { + this._init(); + } + protected _init() { this.InterceptorContributions = this.pickItemInterceptorContributions .getContributions() diff --git a/packages/vrender-core/src/render/contributions/render/arc-render.ts b/packages/vrender-core/src/render/contributions/render/arc-render.ts index 574fe0956..295b20bd2 100644 --- a/packages/vrender-core/src/render/contributions/render/arc-render.ts +++ b/packages/vrender-core/src/render/contributions/render/arc-render.ts @@ -56,7 +56,7 @@ export class DefaultCanvasArcRender extends BaseRender implements IGraphic constructor( @inject(ContributionProvider) @named(ArcRenderContribution) - protected readonly arcRenderContribitions: IContributionProvider + protected readonly graphicRenderContributions: IContributionProvider ) { super(); this.builtinContributions = [ @@ -64,7 +64,7 @@ export class DefaultCanvasArcRender extends BaseRender implements IGraphic defaultArcBackgroundRenderContribution, defaultArcTextureRenderContribution ]; - this.init(arcRenderContribitions); + this.init(graphicRenderContributions); } // 绘制尾部cap diff --git a/packages/vrender-core/src/render/contributions/render/area-render.ts b/packages/vrender-core/src/render/contributions/render/area-render.ts index 8eb5f8921..c4cbb4536 100644 --- a/packages/vrender-core/src/render/contributions/render/area-render.ts +++ b/packages/vrender-core/src/render/contributions/render/area-render.ts @@ -41,11 +41,11 @@ export class DefaultCanvasAreaRender extends BaseRender implements IGraph constructor( @inject(ContributionProvider) @named(AreaRenderContribution) - protected readonly areaRenderContribitions: IContributionProvider + protected readonly graphicRenderContributions: IContributionProvider ) { super(); this.builtinContributions = [defaultAreaTextureRenderContribution, defaultAreaBackgroundRenderContribution]; - this.init(areaRenderContribitions); + this.init(graphicRenderContributions); } drawLinearAreaHighPerformance( diff --git a/packages/vrender-core/src/render/contributions/render/base-render.ts b/packages/vrender-core/src/render/contributions/render/base-render.ts index 535fc7a57..f27b8dce9 100644 --- a/packages/vrender-core/src/render/contributions/render/base-render.ts +++ b/packages/vrender-core/src/render/contributions/render/base-render.ts @@ -29,6 +29,7 @@ export abstract class BaseRender { declare z: number; builtinContributions: IBaseRenderContribution[]; + protected declare graphicRenderContributions: IContributionProvider>; // declare renderContribitions: IContributionProvider> | null; @@ -61,6 +62,10 @@ export abstract class BaseRender { } } + reInit() { + this.init(this.graphicRenderContributions); + } + beforeRenderStep( graphic: T, context: IContext2d, diff --git a/packages/vrender-core/src/render/contributions/render/circle-render.ts b/packages/vrender-core/src/render/contributions/render/circle-render.ts index 7a19d25a0..8854a0d60 100644 --- a/packages/vrender-core/src/render/contributions/render/circle-render.ts +++ b/packages/vrender-core/src/render/contributions/render/circle-render.ts @@ -32,7 +32,7 @@ export class DefaultCanvasCircleRender extends BaseRender implements IG constructor( @inject(ContributionProvider) @named(CircleRenderContribution) - protected readonly circleRenderContribitions: IContributionProvider + protected readonly graphicRenderContributions: IContributionProvider ) { super(); this.builtinContributions = [ @@ -40,7 +40,7 @@ export class DefaultCanvasCircleRender extends BaseRender implements IG defaultCircleBackgroundRenderContribution, defaultCircleTextureRenderContribution ]; - this.init(circleRenderContribitions); + this.init(graphicRenderContributions); } drawShape( diff --git a/packages/vrender-core/src/render/contributions/render/draw-contribution.ts b/packages/vrender-core/src/render/contributions/render/draw-contribution.ts index 9344c465d..0aed84f12 100644 --- a/packages/vrender-core/src/render/contributions/render/draw-contribution.ts +++ b/packages/vrender-core/src/render/contributions/render/draw-contribution.ts @@ -72,6 +72,13 @@ export class DefaultDrawContribution implements IDrawContribution { this.init(); } + reInit() { + this.init(); + this.contributions.forEach(item => { + item.reInit(); + }); + } + init() { this.contributions.forEach(item => { if (item.style) { diff --git a/packages/vrender-core/src/render/contributions/render/glyph-render.ts b/packages/vrender-core/src/render/contributions/render/glyph-render.ts index ff6f3250d..33381aa60 100644 --- a/packages/vrender-core/src/render/contributions/render/glyph-render.ts +++ b/packages/vrender-core/src/render/contributions/render/glyph-render.ts @@ -20,6 +20,10 @@ export class DefaultCanvasGlyphRender implements IGraphicRender { // constructor() {} + reInit() { + return; + } + drawShape( glyph: IGlyph, context: IContext2d, diff --git a/packages/vrender-core/src/render/contributions/render/graphic-render.ts b/packages/vrender-core/src/render/contributions/render/graphic-render.ts index 39eca50c6..92ff262b4 100644 --- a/packages/vrender-core/src/render/contributions/render/graphic-render.ts +++ b/packages/vrender-core/src/render/contributions/render/graphic-render.ts @@ -7,4 +7,6 @@ export abstract class AbstractGraphicRender implements IGraphicRender { numberType: number; abstract draw(graphic: IGraphic, renderService: IRenderService): void; + + abstract reInit(): void; } diff --git a/packages/vrender-core/src/render/contributions/render/group-render.ts b/packages/vrender-core/src/render/contributions/render/group-render.ts index ec44d6450..8ba74ab6c 100644 --- a/packages/vrender-core/src/render/contributions/render/group-render.ts +++ b/packages/vrender-core/src/render/contributions/render/group-render.ts @@ -40,6 +40,11 @@ export class DefaultCanvasGroupRender implements IGraphicRender { protected readonly groupRenderContribitions: IContributionProvider ) {} + reInit() { + this._groupRenderContribitions = this.groupRenderContribitions.getContributions() || []; + this._groupRenderContribitions.push(defaultGroupBackgroundRenderContribution); + } + drawShape( group: IGroup, context: IContext2d, diff --git a/packages/vrender-core/src/render/contributions/render/image-render.ts b/packages/vrender-core/src/render/contributions/render/image-render.ts index f4d0c9beb..83c4cac25 100644 --- a/packages/vrender-core/src/render/contributions/render/image-render.ts +++ b/packages/vrender-core/src/render/contributions/render/image-render.ts @@ -35,11 +35,11 @@ export class DefaultCanvasImageRender extends BaseRender implements IGra constructor( @inject(ContributionProvider) @named(ImageRenderContribution) - protected readonly imageRenderContribitions: IContributionProvider + protected readonly graphicRenderContributions: IContributionProvider ) { super(); this.builtinContributions = [defaultImageRenderContribution, defaultImageBackgroundRenderContribution]; - this.init(imageRenderContribitions); + this.init(graphicRenderContributions); } drawShape( diff --git a/packages/vrender-core/src/render/contributions/render/path-render.ts b/packages/vrender-core/src/render/contributions/render/path-render.ts index 5691807f1..974b8807f 100644 --- a/packages/vrender-core/src/render/contributions/render/path-render.ts +++ b/packages/vrender-core/src/render/contributions/render/path-render.ts @@ -35,11 +35,11 @@ export class DefaultCanvasPathRender extends BaseRender implements IGraph constructor( @inject(ContributionProvider) @named(PathRenderContribution) - protected readonly pathRenderContribitions: IContributionProvider + protected readonly graphicRenderContributions: IContributionProvider ) { super(); this.builtinContributions = [defaultPathBackgroundRenderContribution, defaultPathTextureRenderContribution]; - this.init(pathRenderContribitions); + this.init(graphicRenderContributions); } drawShape( diff --git a/packages/vrender-core/src/render/contributions/render/polygon-render.ts b/packages/vrender-core/src/render/contributions/render/polygon-render.ts index 193032806..eeb15856f 100644 --- a/packages/vrender-core/src/render/contributions/render/polygon-render.ts +++ b/packages/vrender-core/src/render/contributions/render/polygon-render.ts @@ -33,11 +33,11 @@ export class DefaultCanvasPolygonRender extends BaseRender implements constructor( @inject(ContributionProvider) @named(PolygonRenderContribution) - protected readonly polygonRenderContribitions: IContributionProvider + protected readonly graphicRenderContributions: IContributionProvider ) { super(); this.builtinContributions = [defaultPolygonBackgroundRenderContribution, defaultPolygonTextureRenderContribution]; - this.init(polygonRenderContribitions); + this.init(graphicRenderContributions); } drawShape( diff --git a/packages/vrender-core/src/render/contributions/render/rect-render.ts b/packages/vrender-core/src/render/contributions/render/rect-render.ts index c0e6f6bfc..9500d4be8 100644 --- a/packages/vrender-core/src/render/contributions/render/rect-render.ts +++ b/packages/vrender-core/src/render/contributions/render/rect-render.ts @@ -37,7 +37,7 @@ export class DefaultCanvasRectRender extends BaseRender implements IGraph constructor( @inject(ContributionProvider) @named(RectRenderContribution) - protected readonly rectRenderContribitions: IContributionProvider + protected readonly graphicRenderContributions: IContributionProvider ) { super(); this.builtinContributions = [ @@ -45,7 +45,7 @@ export class DefaultCanvasRectRender extends BaseRender implements IGraph defaultRectBackgroundRenderContribution, defaultRectTextureRenderContribution ]; - this.init(rectRenderContribitions); + this.init(graphicRenderContributions); } drawShape( diff --git a/packages/vrender-core/src/render/contributions/render/symbol-render.ts b/packages/vrender-core/src/render/contributions/render/symbol-render.ts index b3429f70c..8e6d96733 100644 --- a/packages/vrender-core/src/render/contributions/render/symbol-render.ts +++ b/packages/vrender-core/src/render/contributions/render/symbol-render.ts @@ -36,7 +36,7 @@ export class DefaultCanvasSymbolRender extends BaseRender implements IG constructor( @inject(ContributionProvider) @named(SymbolRenderContribution) - protected readonly symbolRenderContribitions: IContributionProvider + protected readonly graphicRenderContributions: IContributionProvider ) { super(); this.builtinContributions = [ @@ -45,7 +45,7 @@ export class DefaultCanvasSymbolRender extends BaseRender implements IG defaultSymbolTextureRenderContribution, defaultSymbolClipRangeStrokeRenderContribution ]; - this.init(symbolRenderContribitions); + this.init(graphicRenderContributions); } drawShape( diff --git a/packages/vrender-core/src/render/contributions/render/text-render.ts b/packages/vrender-core/src/render/contributions/render/text-render.ts index 24c108301..a6df7f3a6 100644 --- a/packages/vrender-core/src/render/contributions/render/text-render.ts +++ b/packages/vrender-core/src/render/contributions/render/text-render.ts @@ -31,11 +31,11 @@ export class DefaultCanvasTextRender extends BaseRender implements IGraph constructor( @inject(ContributionProvider) @named(TextRenderContribution) - protected readonly textRenderContribitions: IContributionProvider + protected readonly graphicRenderContributions: IContributionProvider ) { super(); this.builtinContributions = [defaultTextBackgroundRenderContribution as any]; - this.init(textRenderContribitions); + this.init(graphicRenderContributions); } drawShape( diff --git a/packages/vrender-core/src/render/render-service.ts b/packages/vrender-core/src/render/render-service.ts index f6239386a..fddf41590 100644 --- a/packages/vrender-core/src/render/render-service.ts +++ b/packages/vrender-core/src/render/render-service.ts @@ -58,6 +58,10 @@ export class DefaultRenderService implements IRenderService { this.drawContribution.afterDraw && this.drawContribution.afterDraw(this, { ...this.drawParams }); return; } + + reInit() { + this.drawContribution.reInit(); + } // 对外暴露的绘制方法 render(groups: IGroup[], params: IRenderServiceDrawParams): void { this.renderTreeRoots = groups; diff --git a/packages/vrender-kits/src/render/contributions/rough/base-render.ts b/packages/vrender-kits/src/render/contributions/rough/base-render.ts index f5811eeaf..5636248ab 100644 --- a/packages/vrender-kits/src/render/contributions/rough/base-render.ts +++ b/packages/vrender-kits/src/render/contributions/rough/base-render.ts @@ -33,4 +33,8 @@ export abstract class RoughBaseRender { return this.canvasRenderer.drawShape(graphic, ctx, x, y, drawContext, params, fillCb, strokeCb); } } + + reInit() { + this.canvasRenderer?.reInit(); + } } diff --git a/packages/vrender/__tests__/browser/src/pages/symbol.ts b/packages/vrender/__tests__/browser/src/pages/symbol.ts index bc62ef4af..a9ef3e130 100644 --- a/packages/vrender/__tests__/browser/src/pages/symbol.ts +++ b/packages/vrender/__tests__/browser/src/pages/symbol.ts @@ -15,9 +15,9 @@ export const page = () => { // // ` - `` + `` ); - console.log(result); + console.log(result, parser); // const isSvg = XMLValidator.validate( // ` From 9999f2f6b2425116fb42f4738cb763809c32f7c6 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 17 Apr 2025 17:28:04 +0800 Subject: [PATCH 012/179] fix: fix issue with scaleCenter --- packages/vrender-core/src/graphic/graphic.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index b45420219..14b80c5aa 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -1205,7 +1205,7 @@ export abstract class Graphic = Partial { if (params.b) { @@ -1215,6 +1215,10 @@ export abstract class Graphic = Partial = Partial Date: Thu, 17 Apr 2025 23:51:34 +0800 Subject: [PATCH 013/179] fix: brush start pos --- packages/vrender-components/src/brush/brush.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/vrender-components/src/brush/brush.ts b/packages/vrender-components/src/brush/brush.ts index f9d0d87d0..4da78c53c 100644 --- a/packages/vrender-components/src/brush/brush.ts +++ b/packages/vrender-components/src/brush/brush.ts @@ -92,6 +92,7 @@ export class Brush extends AbstractComponent> { this._activeMoveState = brushMoved && this._isPosInBrushMask(e); // 如果是移动状态,在这里会标记operatingMask为正在移动的mask this._activeDrawState = !this._activeMoveState; this._startPos = this.eventPosToStagePos(e); + this._cacheDrawPoints = [this._startPos]; }; /** @@ -151,7 +152,8 @@ export class Brush extends AbstractComponent> { private _initDraw(e: FederatedPointerEvent) { const { brushMode } = this.attribute as BrushAttributes; const pos = this.eventPosToStagePos(e); - this._cacheDrawPoints = [pos]; + + this._cacheDrawPoints.push(pos); brushMode === 'single' && this._clearMask(); this._addBrushMask(); this._dispatchBrushEvent(IOperateType.drawStart, e); From b2cba138819d05ca264f14648bdadfffc0a80949 Mon Sep 17 00:00:00 2001 From: xile611 Date: Fri, 18 Apr 2025 11:23:26 +0800 Subject: [PATCH 014/179] fix: fix state not sync with mouse --- .../vrender-components/src/data-zoom/data-zoom.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/vrender-components/src/data-zoom/data-zoom.ts b/packages/vrender-components/src/data-zoom/data-zoom.ts index 4d60d41d2..3f3c605a2 100644 --- a/packages/vrender-components/src/data-zoom/data-zoom.ts +++ b/packages/vrender-components/src/data-zoom/data-zoom.ts @@ -242,10 +242,12 @@ export class DataZoom extends AbstractComponent> { const evtTarget = vglobal.env === 'browser' ? vglobal : this.stage; const triggers = getEndTriggersOfDrag(); - evtTarget.removeEventListener('pointermove', this._onHandlerPointerMove, { capture: true, passive: false }); + evtTarget.removeEventListener('pointermove', this._onHandlerPointerMove, { capture: true }); triggers.forEach((trigger: string) => { evtTarget.removeEventListener(trigger, this._onHandlerPointerUp); }); + + (this as unknown as IGroup).removeEventListener('pointermove', this._onHandlerPointerMove, { capture: true }); } /** @@ -279,9 +281,10 @@ export class DataZoom extends AbstractComponent> { /** * move的时候,需要通过 capture: true,能够在捕获截断被拦截, - * move的时候,需要显示的设置passive: false,因为在移动端需要禁用浏览器默认行为 */ - evtTarget.addEventListener('pointermove', this._onHandlerPointerMove, { capture: true, passive: false }); + evtTarget.addEventListener('pointermove', this._onHandlerPointerMove, { capture: true }); + (this as unknown as IGroup).addEventListener('pointermove', this._onHandlerPointerMove, { capture: true }); + triggers.forEach((trigger: string) => { evtTarget.addEventListener(trigger, this._onHandlerPointerUp); }); @@ -326,6 +329,7 @@ export class DataZoom extends AbstractComponent> { end = end + dis; } } + this._activeCache.lastPos = pos; brushSelect && this.renderDragMask(); } start = Math.min(Math.max(start, 0), 1); @@ -333,7 +337,6 @@ export class DataZoom extends AbstractComponent> { // 避免attributes相同时, 重复渲染 if (startAttr !== start || endAttr !== end) { - this._activeCache.lastPos = pos; this.setStateAttr(start, end, true); if (realTime) { this._dispatchEvent('change', { From 918ed99aab41fc33966550d7a9f0c31c6bb9ef22 Mon Sep 17 00:00:00 2001 From: xile611 Date: Fri, 18 Apr 2025 11:35:24 +0800 Subject: [PATCH 015/179] docs: update changlog of rush --- .../fix-data-zoom-middle-drag_2025-04-18-03-35.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@visactor/vrender-components/fix-data-zoom-middle-drag_2025-04-18-03-35.json diff --git a/common/changes/@visactor/vrender-components/fix-data-zoom-middle-drag_2025-04-18-03-35.json b/common/changes/@visactor/vrender-components/fix-data-zoom-middle-drag_2025-04-18-03-35.json new file mode 100644 index 000000000..ffdf7bdee --- /dev/null +++ b/common/changes/@visactor/vrender-components/fix-data-zoom-middle-drag_2025-04-18-03-35.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: fix state not sync with mouse\n\n", + "type": "none", + "packageName": "@visactor/vrender-components" + } + ], + "packageName": "@visactor/vrender-components", + "email": "dingling112@gmail.com" +} \ No newline at end of file From 3f941bcbb98764149e542f3067f326041fd497a1 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 18 Apr 2025 12:19:15 +0800 Subject: [PATCH 016/179] fix: fix issue with touch event --- .../src/common/event-transformer.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/vrender-core/src/common/event-transformer.ts b/packages/vrender-core/src/common/event-transformer.ts index ea8b0cb24..bc753cdaf 100644 --- a/packages/vrender-core/src/common/event-transformer.ts +++ b/packages/vrender-core/src/common/event-transformer.ts @@ -50,8 +50,12 @@ export function createEventTransformer( // For touch events, we need to transform each touch point // This is a simplified version that assumes we're only using the first touch if (event.touches.length > 0) { - const touch = event.touches[0]; - transformPoint(touch.clientX, touch.clientY, transformMatrix, containerRect, transformedEvent); + const touch = transformedEvent.touches[0]; + transformPoint(touch.clientX, touch.clientY, transformMatrix, containerRect, touch); + } + if (event.changedTouches.length > 0) { + const touch = transformedEvent.changedTouches[0]; + transformPoint(touch.clientX, touch.clientY, transformMatrix, containerRect, touch); } } @@ -137,6 +141,17 @@ export function mapToCanvasPointForCanvas(nativeEvent: any) { x: nativeEvent._canvasX, y: nativeEvent._canvasY }; + } else if ((nativeEvent as TouchEvent).changedTouches) { + const data = (nativeEvent as TouchEvent).changedTouches[0] ?? ({} as any); + return { + x: data._canvasX, + y: data._canvasY + }; } - return; + const x = (nativeEvent as any)._canvasX || 0; + const y = (nativeEvent as any)._canvasY || 0; + return { + x, + y + }; } From 6c9f5c0e4054cf523da7402979b8045fb98f26a1 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 18 Apr 2025 14:53:51 +0800 Subject: [PATCH 017/179] feat: add rush change --- .../fix-anchor-scale_2025-04-18-06-53.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@visactor/vrender-core/fix-anchor-scale_2025-04-18-06-53.json diff --git a/common/changes/@visactor/vrender-core/fix-anchor-scale_2025-04-18-06-53.json b/common/changes/@visactor/vrender-core/fix-anchor-scale_2025-04-18-06-53.json new file mode 100644 index 000000000..455337ed9 --- /dev/null +++ b/common/changes/@visactor/vrender-core/fix-anchor-scale_2025-04-18-06-53.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-core", + "comment": "fix: fix issue with string anchor and scale", + "type": "none" + } + ], + "packageName": "@visactor/vrender-core" +} \ No newline at end of file From 93a88ac739f426314760c4970200dc0e3a666dcf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 18 Apr 2025 07:04:40 +0000 Subject: [PATCH 018/179] build: prelease version 0.22.9 --- ...ata-zoom-middle-drag_2025-04-18-03-35.json | 11 -------- .../fix-anchor-scale_2025-04-18-06-53.json | 10 ------- common/config/rush/pnpm-lock.yaml | 26 +++++++++---------- common/config/rush/version-policies.json | 2 +- docs/package.json | 2 +- packages/react-vrender-utils/CHANGELOG.json | 6 +++++ packages/react-vrender-utils/CHANGELOG.md | 7 ++++- packages/react-vrender-utils/package.json | 6 ++--- packages/react-vrender/CHANGELOG.json | 6 +++++ packages/react-vrender/CHANGELOG.md | 7 ++++- packages/react-vrender/package.json | 4 +-- packages/vrender-components/CHANGELOG.json | 12 +++++++++ packages/vrender-components/CHANGELOG.md | 11 +++++++- packages/vrender-components/package.json | 6 ++--- packages/vrender-core/CHANGELOG.json | 12 +++++++++ packages/vrender-core/CHANGELOG.md | 9 ++++++- packages/vrender-core/package.json | 2 +- packages/vrender-kits/CHANGELOG.json | 6 +++++ packages/vrender-kits/CHANGELOG.md | 7 ++++- packages/vrender-kits/package.json | 4 +-- packages/vrender/CHANGELOG.json | 6 +++++ packages/vrender/CHANGELOG.md | 7 ++++- packages/vrender/package.json | 6 ++--- tools/bugserver-trigger/package.json | 8 +++--- 24 files changed, 123 insertions(+), 60 deletions(-) delete mode 100644 common/changes/@visactor/vrender-components/fix-data-zoom-middle-drag_2025-04-18-03-35.json delete mode 100644 common/changes/@visactor/vrender-core/fix-anchor-scale_2025-04-18-06-53.json diff --git a/common/changes/@visactor/vrender-components/fix-data-zoom-middle-drag_2025-04-18-03-35.json b/common/changes/@visactor/vrender-components/fix-data-zoom-middle-drag_2025-04-18-03-35.json deleted file mode 100644 index ffdf7bdee..000000000 --- a/common/changes/@visactor/vrender-components/fix-data-zoom-middle-drag_2025-04-18-03-35.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "changes": [ - { - "comment": "fix: fix state not sync with mouse\n\n", - "type": "none", - "packageName": "@visactor/vrender-components" - } - ], - "packageName": "@visactor/vrender-components", - "email": "dingling112@gmail.com" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-core/fix-anchor-scale_2025-04-18-06-53.json b/common/changes/@visactor/vrender-core/fix-anchor-scale_2025-04-18-06-53.json deleted file mode 100644 index 455337ed9..000000000 --- a/common/changes/@visactor/vrender-core/fix-anchor-scale_2025-04-18-06-53.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender-core", - "comment": "fix: fix issue with string anchor and scale", - "type": "none" - } - ], - "packageName": "@visactor/vrender-core" -} \ No newline at end of file diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 9e1f209f5..96fbf6dfe 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: specifier: ~0.5.7 version: 0.5.7 '@visactor/vrender': - specifier: workspace:0.22.8 + specifier: workspace:0.22.9 version: link:../packages/vrender '@visactor/vutils': specifier: ~0.19.5 @@ -95,7 +95,7 @@ importers: ../../packages/react-vrender: dependencies: '@visactor/vrender': - specifier: workspace:0.22.8 + specifier: workspace:0.22.9 version: link:../vrender '@visactor/vutils': specifier: ~0.19.5 @@ -153,10 +153,10 @@ importers: ../../packages/react-vrender-utils: dependencies: '@visactor/react-vrender': - specifier: workspace:0.22.8 + specifier: workspace:0.22.9 version: link:../react-vrender '@visactor/vrender': - specifier: workspace:0.22.8 + specifier: workspace:0.22.9 version: link:../vrender '@visactor/vutils': specifier: ~0.19.5 @@ -211,10 +211,10 @@ importers: ../../packages/vrender: dependencies: '@visactor/vrender-core': - specifier: workspace:0.22.8 + specifier: workspace:0.22.9 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:0.22.8 + specifier: workspace:0.22.9 version: link:../vrender-kits devDependencies: '@internal/bundler': @@ -281,10 +281,10 @@ importers: ../../packages/vrender-components: dependencies: '@visactor/vrender-core': - specifier: workspace:0.22.8 + specifier: workspace:0.22.9 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:0.22.8 + specifier: workspace:0.22.9 version: link:../vrender-kits '@visactor/vscale': specifier: ~0.19.5 @@ -403,7 +403,7 @@ importers: specifier: 2.4.1 version: 2.4.1 '@visactor/vrender-core': - specifier: workspace:0.22.8 + specifier: workspace:0.22.9 version: link:../vrender-core '@visactor/vutils': specifier: ~0.19.5 @@ -519,16 +519,16 @@ importers: ../../tools/bugserver-trigger: dependencies: '@visactor/vrender': - specifier: workspace:0.22.8 + specifier: workspace:0.22.9 version: link:../../packages/vrender '@visactor/vrender-components': - specifier: workspace:0.22.8 + specifier: workspace:0.22.9 version: link:../../packages/vrender-components '@visactor/vrender-core': - specifier: workspace:0.22.8 + specifier: workspace:0.22.9 version: link:../../packages/vrender-core '@visactor/vrender-kits': - specifier: workspace:0.22.8 + specifier: workspace:0.22.9 version: link:../../packages/vrender-kits devDependencies: '@internal/bundler': diff --git a/common/config/rush/version-policies.json b/common/config/rush/version-policies.json index 56fc5626a..80b2a6aa6 100644 --- a/common/config/rush/version-policies.json +++ b/common/config/rush/version-policies.json @@ -1 +1 @@ -[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"0.22.8","nextBump":"patch"}] +[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"0.22.9","nextBump":"patch"}] diff --git a/docs/package.json b/docs/package.json index d12fccddb..6676750b8 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,7 +13,7 @@ "@visactor/vchart": "1.3.0", "@visactor/vutils": "~0.19.5", "@visactor/vgrammar": "~0.5.7", - "@visactor/vrender": "workspace:0.22.8", + "@visactor/vrender": "workspace:0.22.9", "markdown-it": "^13.0.0", "highlight.js": "^11.8.0", "axios": "^1.4.0", diff --git a/packages/react-vrender-utils/CHANGELOG.json b/packages/react-vrender-utils/CHANGELOG.json index 1cbc85c96..cce0637e0 100644 --- a/packages/react-vrender-utils/CHANGELOG.json +++ b/packages/react-vrender-utils/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/react-vrender-utils", "entries": [ + { + "version": "0.22.9", + "tag": "@visactor/react-vrender-utils_v0.22.9", + "date": "Fri, 18 Apr 2025 06:59:46 GMT", + "comments": {} + }, { "version": "0.22.8", "tag": "@visactor/react-vrender-utils_v0.22.8", diff --git a/packages/react-vrender-utils/CHANGELOG.md b/packages/react-vrender-utils/CHANGELOG.md index efa2be238..2766bbab5 100644 --- a/packages/react-vrender-utils/CHANGELOG.md +++ b/packages/react-vrender-utils/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/react-vrender-utils -This log was last generated on Wed, 16 Apr 2025 03:17:31 GMT and should not be manually modified. +This log was last generated on Fri, 18 Apr 2025 06:59:46 GMT and should not be manually modified. + +## 0.22.9 +Fri, 18 Apr 2025 06:59:46 GMT + +_Version update only_ ## 0.22.8 Wed, 16 Apr 2025 03:17:31 GMT diff --git a/packages/react-vrender-utils/package.json b/packages/react-vrender-utils/package.json index 65fa50856..9bfd32b00 100644 --- a/packages/react-vrender-utils/package.json +++ b/packages/react-vrender-utils/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender-utils", - "version": "0.22.8", + "version": "0.22.9", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -24,8 +24,8 @@ "react-dom": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:0.22.8", - "@visactor/react-vrender": "workspace:0.22.8", + "@visactor/vrender": "workspace:0.22.9", + "@visactor/react-vrender": "workspace:0.22.9", "@visactor/vutils": "~0.19.5", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/react-vrender/CHANGELOG.json b/packages/react-vrender/CHANGELOG.json index 7c92dff0e..18480de5c 100644 --- a/packages/react-vrender/CHANGELOG.json +++ b/packages/react-vrender/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/react-vrender", "entries": [ + { + "version": "0.22.9", + "tag": "@visactor/react-vrender_v0.22.9", + "date": "Fri, 18 Apr 2025 06:59:46 GMT", + "comments": {} + }, { "version": "0.22.8", "tag": "@visactor/react-vrender_v0.22.8", diff --git a/packages/react-vrender/CHANGELOG.md b/packages/react-vrender/CHANGELOG.md index c564cb938..4ac8872bc 100644 --- a/packages/react-vrender/CHANGELOG.md +++ b/packages/react-vrender/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/react-vrender -This log was last generated on Wed, 16 Apr 2025 03:17:31 GMT and should not be manually modified. +This log was last generated on Fri, 18 Apr 2025 06:59:46 GMT and should not be manually modified. + +## 0.22.9 +Fri, 18 Apr 2025 06:59:46 GMT + +_Version update only_ ## 0.22.8 Wed, 16 Apr 2025 03:17:31 GMT diff --git a/packages/react-vrender/package.json b/packages/react-vrender/package.json index 6033f1ec7..def92138a 100644 --- a/packages/react-vrender/package.json +++ b/packages/react-vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender", - "version": "0.22.8", + "version": "0.22.9", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -23,7 +23,7 @@ "react": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:0.22.8", + "@visactor/vrender": "workspace:0.22.9", "@visactor/vutils": "~0.19.5", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/vrender-components/CHANGELOG.json b/packages/vrender-components/CHANGELOG.json index 4621219ed..d852d23ba 100644 --- a/packages/vrender-components/CHANGELOG.json +++ b/packages/vrender-components/CHANGELOG.json @@ -1,6 +1,18 @@ { "name": "@visactor/vrender-components", "entries": [ + { + "version": "0.22.9", + "tag": "@visactor/vrender-components_v0.22.9", + "date": "Fri, 18 Apr 2025 06:59:46 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix state not sync with mouse\n\n" + } + ] + } + }, { "version": "0.22.8", "tag": "@visactor/vrender-components_v0.22.8", diff --git a/packages/vrender-components/CHANGELOG.md b/packages/vrender-components/CHANGELOG.md index 921298293..3901d7053 100644 --- a/packages/vrender-components/CHANGELOG.md +++ b/packages/vrender-components/CHANGELOG.md @@ -1,6 +1,15 @@ # Change Log - @visactor/vrender-components -This log was last generated on Wed, 16 Apr 2025 03:17:31 GMT and should not be manually modified. +This log was last generated on Fri, 18 Apr 2025 06:59:46 GMT and should not be manually modified. + +## 0.22.9 +Fri, 18 Apr 2025 06:59:46 GMT + +### Updates + +- fix: fix state not sync with mouse + + ## 0.22.8 Wed, 16 Apr 2025 03:17:31 GMT diff --git a/packages/vrender-components/package.json b/packages/vrender-components/package.json index ea13ed173..e594a0919 100644 --- a/packages/vrender-components/package.json +++ b/packages/vrender-components/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-components", - "version": "0.22.8", + "version": "0.22.9", "description": "components library for dp visualization", "sideEffects": false, "main": "cjs/index.js", @@ -27,8 +27,8 @@ "dependencies": { "@visactor/vutils": "~0.19.5", "@visactor/vscale": "~0.19.5", - "@visactor/vrender-core": "workspace:0.22.8", - "@visactor/vrender-kits": "workspace:0.22.8" + "@visactor/vrender-core": "workspace:0.22.9", + "@visactor/vrender-kits": "workspace:0.22.9" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-core/CHANGELOG.json b/packages/vrender-core/CHANGELOG.json index 78baa69d1..7bc520928 100644 --- a/packages/vrender-core/CHANGELOG.json +++ b/packages/vrender-core/CHANGELOG.json @@ -1,6 +1,18 @@ { "name": "@visactor/vrender-core", "entries": [ + { + "version": "0.22.9", + "tag": "@visactor/vrender-core_v0.22.9", + "date": "Fri, 18 Apr 2025 06:59:46 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix issue with string anchor and scale" + } + ] + } + }, { "version": "0.22.8", "tag": "@visactor/vrender-core_v0.22.8", diff --git a/packages/vrender-core/CHANGELOG.md b/packages/vrender-core/CHANGELOG.md index 83b7d149a..c0e34d7fc 100644 --- a/packages/vrender-core/CHANGELOG.md +++ b/packages/vrender-core/CHANGELOG.md @@ -1,6 +1,13 @@ # Change Log - @visactor/vrender-core -This log was last generated on Wed, 16 Apr 2025 03:17:31 GMT and should not be manually modified. +This log was last generated on Fri, 18 Apr 2025 06:59:46 GMT and should not be manually modified. + +## 0.22.9 +Fri, 18 Apr 2025 06:59:46 GMT + +### Updates + +- fix: fix issue with string anchor and scale ## 0.22.8 Wed, 16 Apr 2025 03:17:31 GMT diff --git a/packages/vrender-core/package.json b/packages/vrender-core/package.json index 33b3a7e1a..af080b288 100644 --- a/packages/vrender-core/package.json +++ b/packages/vrender-core/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-core", - "version": "0.22.8", + "version": "0.22.9", "description": "", "sideEffects": [ "./src/modules.ts", diff --git a/packages/vrender-kits/CHANGELOG.json b/packages/vrender-kits/CHANGELOG.json index 21762493f..17d95376b 100644 --- a/packages/vrender-kits/CHANGELOG.json +++ b/packages/vrender-kits/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender-kits", "entries": [ + { + "version": "0.22.9", + "tag": "@visactor/vrender-kits_v0.22.9", + "date": "Fri, 18 Apr 2025 06:59:46 GMT", + "comments": {} + }, { "version": "0.22.8", "tag": "@visactor/vrender-kits_v0.22.8", diff --git a/packages/vrender-kits/CHANGELOG.md b/packages/vrender-kits/CHANGELOG.md index 886c8edae..fa21475b8 100644 --- a/packages/vrender-kits/CHANGELOG.md +++ b/packages/vrender-kits/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender-kits -This log was last generated on Wed, 16 Apr 2025 03:17:31 GMT and should not be manually modified. +This log was last generated on Fri, 18 Apr 2025 06:59:46 GMT and should not be manually modified. + +## 0.22.9 +Fri, 18 Apr 2025 06:59:46 GMT + +_Version update only_ ## 0.22.8 Wed, 16 Apr 2025 03:17:31 GMT diff --git a/packages/vrender-kits/package.json b/packages/vrender-kits/package.json index 42f124ffb..b1578c435 100644 --- a/packages/vrender-kits/package.json +++ b/packages/vrender-kits/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-kits", - "version": "0.22.8", + "version": "0.22.9", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -21,7 +21,7 @@ }, "dependencies": { "@visactor/vutils": "~0.19.5", - "@visactor/vrender-core": "workspace:0.22.8", + "@visactor/vrender-core": "workspace:0.22.9", "@resvg/resvg-js": "2.4.1", "roughjs": "4.5.2", "gifuct-js": "2.1.2", diff --git a/packages/vrender/CHANGELOG.json b/packages/vrender/CHANGELOG.json index 2976481fa..1c7a9f390 100644 --- a/packages/vrender/CHANGELOG.json +++ b/packages/vrender/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender", "entries": [ + { + "version": "0.22.9", + "tag": "@visactor/vrender_v0.22.9", + "date": "Fri, 18 Apr 2025 06:59:46 GMT", + "comments": {} + }, { "version": "0.22.8", "tag": "@visactor/vrender_v0.22.8", diff --git a/packages/vrender/CHANGELOG.md b/packages/vrender/CHANGELOG.md index 41698e47b..588345246 100644 --- a/packages/vrender/CHANGELOG.md +++ b/packages/vrender/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender -This log was last generated on Wed, 16 Apr 2025 03:17:31 GMT and should not be manually modified. +This log was last generated on Fri, 18 Apr 2025 06:59:46 GMT and should not be manually modified. + +## 0.22.9 +Fri, 18 Apr 2025 06:59:46 GMT + +_Version update only_ ## 0.22.8 Wed, 16 Apr 2025 03:17:31 GMT diff --git a/packages/vrender/package.json b/packages/vrender/package.json index ee5d4ffad..83380e51b 100644 --- a/packages/vrender/package.json +++ b/packages/vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender", - "version": "0.22.8", + "version": "0.22.9", "description": "", "sideEffects": true, "main": "cjs/index.js", @@ -24,8 +24,8 @@ "test-watch": "cross-env DEBUG_MODE=1 jest --watch" }, "dependencies": { - "@visactor/vrender-core": "workspace:0.22.8", - "@visactor/vrender-kits": "workspace:0.22.8" + "@visactor/vrender-core": "workspace:0.22.9", + "@visactor/vrender-kits": "workspace:0.22.9" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/tools/bugserver-trigger/package.json b/tools/bugserver-trigger/package.json index 8e1b86a79..7390b2f15 100644 --- a/tools/bugserver-trigger/package.json +++ b/tools/bugserver-trigger/package.json @@ -8,10 +8,10 @@ "ci": "ts-node --transpileOnly --skipProject ./scripts/trigger-test.ts" }, "dependencies": { - "@visactor/vrender": "workspace:0.22.8", - "@visactor/vrender-core": "workspace:0.22.8", - "@visactor/vrender-kits": "workspace:0.22.8", - "@visactor/vrender-components": "workspace:0.22.8" + "@visactor/vrender": "workspace:0.22.9", + "@visactor/vrender-core": "workspace:0.22.9", + "@visactor/vrender-kits": "workspace:0.22.9", + "@visactor/vrender-components": "workspace:0.22.9" }, "devDependencies": { "@rushstack/eslint-patch": "~1.1.4", From eec42a419a6c6b03e64ca1c76cce04b5b8ff9a54 Mon Sep 17 00:00:00 2001 From: xile611 Date: Fri, 18 Apr 2025 09:46:52 +0000 Subject: [PATCH 019/179] docs: generate changelog of release v0.22.9 --- docs/assets/changelog/en/changelog.md | 14 ++++++++++++++ docs/assets/changelog/zh/changelog.md | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/docs/assets/changelog/en/changelog.md b/docs/assets/changelog/en/changelog.md index f84b8740b..e15135ddb 100644 --- a/docs/assets/changelog/en/changelog.md +++ b/docs/assets/changelog/en/changelog.md @@ -1,3 +1,17 @@ +# v0.22.9 + +2025-04-18 + + +**🐛 Bug fix** + +- **@visactor/vrender-components**: fix state not sync with mouse +- **@visactor/vrender-core**: fix issue with string anchor and scale + + + +[more detail about v0.22.9](https://github.com/VisActor/VRender/releases/tag/v0.22.9) + # v0.22.8 2025-04-16 diff --git a/docs/assets/changelog/zh/changelog.md b/docs/assets/changelog/zh/changelog.md index e604f5b6e..e6ed0693b 100644 --- a/docs/assets/changelog/zh/changelog.md +++ b/docs/assets/changelog/zh/changelog.md @@ -1,3 +1,17 @@ +# v0.22.9 + +2025-04-18 + + +**🐛 功能修复** + +- **@visactor/vrender-components**: fix state not sync with mouse +- **@visactor/vrender-core**: fix issue with string anchor and scale + + + +[更多详情请查看 v0.22.9](https://github.com/VisActor/VRender/releases/tag/v0.22.9) + # v0.22.8 2025-04-16 From 634cd9ff875d96527c23dce41cdfb449e85663e3 Mon Sep 17 00:00:00 2001 From: xile611 Date: Fri, 18 Apr 2025 17:41:23 +0800 Subject: [PATCH 020/179] fix: fix preventDefault() error when passive: true --- .../src/data-zoom/data-zoom.ts | 14 +++++++++---- .../src/scrollbar/scrollbar.ts | 21 +++++++++++++++++-- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/packages/vrender-components/src/data-zoom/data-zoom.ts b/packages/vrender-components/src/data-zoom/data-zoom.ts index 3f3c605a2..939dd098a 100644 --- a/packages/vrender-components/src/data-zoom/data-zoom.ts +++ b/packages/vrender-components/src/data-zoom/data-zoom.ts @@ -242,12 +242,15 @@ export class DataZoom extends AbstractComponent> { const evtTarget = vglobal.env === 'browser' ? vglobal : this.stage; const triggers = getEndTriggersOfDrag(); - evtTarget.removeEventListener('pointermove', this._onHandlerPointerMove, { capture: true }); + evtTarget.removeEventListener('pointermove', this._onHandlerPointerMove, { capture: true, passive: false }); triggers.forEach((trigger: string) => { evtTarget.removeEventListener(trigger, this._onHandlerPointerUp); }); - (this as unknown as IGroup).removeEventListener('pointermove', this._onHandlerPointerMove, { capture: true }); + (this as unknown as IGroup).removeEventListener('pointermove', this._onHandlerPointerMove, { + capture: true, + passive: false + }); } /** @@ -282,8 +285,11 @@ export class DataZoom extends AbstractComponent> { /** * move的时候,需要通过 capture: true,能够在捕获截断被拦截, */ - evtTarget.addEventListener('pointermove', this._onHandlerPointerMove, { capture: true }); - (this as unknown as IGroup).addEventListener('pointermove', this._onHandlerPointerMove, { capture: true }); + evtTarget.addEventListener('pointermove', this._onHandlerPointerMove, { capture: true, passive: false }); + (this as unknown as IGroup).addEventListener('pointermove', this._onHandlerPointerMove, { + capture: true, + passive: false + }); triggers.forEach((trigger: string) => { evtTarget.addEventListener(trigger, this._onHandlerPointerUp); diff --git a/packages/vrender-components/src/scrollbar/scrollbar.ts b/packages/vrender-components/src/scrollbar/scrollbar.ts index 146a6aaa7..286af469f 100644 --- a/packages/vrender-components/src/scrollbar/scrollbar.ts +++ b/packages/vrender-components/src/scrollbar/scrollbar.ts @@ -4,7 +4,7 @@ import type { IRectGraphicAttribute, FederatedPointerEvent, IGroup, IRect } from '@visactor/vrender-core'; // eslint-disable-next-line no-duplicate-imports import { vglobal } from '@visactor/vrender-core'; -import { merge, normalizePadding, clamp, clampRange, debounce, throttle } from '@visactor/vutils'; +import { merge, normalizePadding, clamp, clampRange, debounce, throttle, isValid } from '@visactor/vutils'; import { AbstractComponent } from '../core/base'; import type { ScrollBarAttributes } from './type'; @@ -141,8 +141,24 @@ export class ScrollBar extends AbstractComponent> if (this._slider) { this._slider.addEventListener('pointerdown', this._onSliderPointerDown as EventListener); } + + (vglobal.env === 'browser' ? vglobal : this.stage).addEventListener('touchmove', this._handleTouchMove, { + passive: false + }); } + private _handleTouchMove = (e: TouchEvent) => { + if (isValid(this._prePos)) { + // 正在滚动中的时候 + /** + * https://developer.mozilla.org/zh-CN/docs/Web/CSS/overscroll-behavior + * 由于浏览器的overscroll-behavior属性,需要在move的时候阻止浏览器默认行为,否则会因为浏览器检测到scroll行为,阻止pointer事件, + * 抛出pointercancel事件,导致拖拽行为中断。 + */ + e.preventDefault(); + } + }; + protected render() { this._reset(); const { @@ -327,7 +343,7 @@ export class ScrollBar extends AbstractComponent> * move的时候,需要通过 capture: true,能够在捕获截断被拦截, * move的时候,需要显示的设置passive: false,因为在移动端需要禁用浏览器默认行为 */ - obj.addEventListener('pointermove', this._onSliderPointerMoveWithDelay, { capture: true, passive: true }); + obj.addEventListener('pointermove', this._onSliderPointerMoveWithDelay, { capture: true, passive: false }); triggers.forEach((trigger: string) => { obj.addEventListener(trigger, this._onSliderPointerUp); }); @@ -387,6 +403,7 @@ export class ScrollBar extends AbstractComponent> const preScrollRange = this.getScrollRange(); const [currentPos, currentScrollValue] = this._computeScrollValue(e); const range: [number, number] = [preScrollRange[0] + currentScrollValue, preScrollRange[1] + currentScrollValue]; + this._prePos = null; this._dispatchEvent(SCROLLBAR_END_EVENT, { pre: preRange, From af5d81bd0555751bc0f852b4c312334b61998abb Mon Sep 17 00:00:00 2001 From: xile611 Date: Fri, 18 Apr 2025 17:42:05 +0800 Subject: [PATCH 021/179] docs: update changlog of rush --- .../fix-scrolbar-preventDefault_2025-04-18-09-42.json | 11 +++++++++++ .../fix-scrolbar-preventDefault_2025-04-18-09-42.json | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 common/changes/@visactor/vrender-components/fix-scrolbar-preventDefault_2025-04-18-09-42.json create mode 100644 common/changes/@visactor/vrender-core/fix-scrolbar-preventDefault_2025-04-18-09-42.json diff --git a/common/changes/@visactor/vrender-components/fix-scrolbar-preventDefault_2025-04-18-09-42.json b/common/changes/@visactor/vrender-components/fix-scrolbar-preventDefault_2025-04-18-09-42.json new file mode 100644 index 000000000..65e5ce5bf --- /dev/null +++ b/common/changes/@visactor/vrender-components/fix-scrolbar-preventDefault_2025-04-18-09-42.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: fix preventDefault() error when passive: true\n\n", + "type": "none", + "packageName": "@visactor/vrender-components" + } + ], + "packageName": "@visactor/vrender-components", + "email": "dingling112@gmail.com" +} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-core/fix-scrolbar-preventDefault_2025-04-18-09-42.json b/common/changes/@visactor/vrender-core/fix-scrolbar-preventDefault_2025-04-18-09-42.json new file mode 100644 index 000000000..fcc8b100b --- /dev/null +++ b/common/changes/@visactor/vrender-core/fix-scrolbar-preventDefault_2025-04-18-09-42.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: fix preventDefault() error when passive: true\n\n", + "type": "none", + "packageName": "@visactor/vrender-core" + } + ], + "packageName": "@visactor/vrender-core", + "email": "dingling112@gmail.com" +} \ No newline at end of file From 2a5a6b5cce42f06d70dea3c9215f979d30288d98 Mon Sep 17 00:00:00 2001 From: xile611 Date: Fri, 18 Apr 2025 17:44:04 +0800 Subject: [PATCH 022/179] docs: update changlog of rush --- .../fix-scrolbar-preventDefault_2025-04-18-09-44.json | 11 +++++++++++ .../fix-scrolbar-preventDefault_2025-04-18-09-44.json | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 common/changes/@visactor/vrender-components/fix-scrolbar-preventDefault_2025-04-18-09-44.json create mode 100644 common/changes/@visactor/vrender-core/fix-scrolbar-preventDefault_2025-04-18-09-44.json diff --git a/common/changes/@visactor/vrender-components/fix-scrolbar-preventDefault_2025-04-18-09-44.json b/common/changes/@visactor/vrender-components/fix-scrolbar-preventDefault_2025-04-18-09-44.json new file mode 100644 index 000000000..2bcf645ad --- /dev/null +++ b/common/changes/@visactor/vrender-components/fix-scrolbar-preventDefault_2025-04-18-09-44.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "docs: update changlog of rush\n\n", + "type": "none", + "packageName": "@visactor/vrender-components" + } + ], + "packageName": "@visactor/vrender-components", + "email": "dingling112@gmail.com" +} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-core/fix-scrolbar-preventDefault_2025-04-18-09-44.json b/common/changes/@visactor/vrender-core/fix-scrolbar-preventDefault_2025-04-18-09-44.json new file mode 100644 index 000000000..adb7f8118 --- /dev/null +++ b/common/changes/@visactor/vrender-core/fix-scrolbar-preventDefault_2025-04-18-09-44.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "docs: update changlog of rush\n\n", + "type": "none", + "packageName": "@visactor/vrender-core" + } + ], + "packageName": "@visactor/vrender-core", + "email": "dingling112@gmail.com" +} \ No newline at end of file From 416c2661212ad99aa0303cf4284f4d6677152d4b Mon Sep 17 00:00:00 2001 From: xile611 Date: Fri, 18 Apr 2025 18:03:00 +0800 Subject: [PATCH 023/179] fix: pointermove should not preventDefault() --- .../src/data-zoom/data-zoom.ts | 11 ++++------- .../src/scrollbar/scrollbar.ts | 16 +++++++++++++--- .../vrender-components/src/slider/slider.ts | 18 ++++++++---------- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/packages/vrender-components/src/data-zoom/data-zoom.ts b/packages/vrender-components/src/data-zoom/data-zoom.ts index 939dd098a..eb97228b3 100644 --- a/packages/vrender-components/src/data-zoom/data-zoom.ts +++ b/packages/vrender-components/src/data-zoom/data-zoom.ts @@ -242,14 +242,13 @@ export class DataZoom extends AbstractComponent> { const evtTarget = vglobal.env === 'browser' ? vglobal : this.stage; const triggers = getEndTriggersOfDrag(); - evtTarget.removeEventListener('pointermove', this._onHandlerPointerMove, { capture: true, passive: false }); + evtTarget.removeEventListener('pointermove', this._onHandlerPointerMove, { capture: true }); triggers.forEach((trigger: string) => { evtTarget.removeEventListener(trigger, this._onHandlerPointerUp); }); (this as unknown as IGroup).removeEventListener('pointermove', this._onHandlerPointerMove, { - capture: true, - passive: false + capture: true }); } @@ -285,10 +284,9 @@ export class DataZoom extends AbstractComponent> { /** * move的时候,需要通过 capture: true,能够在捕获截断被拦截, */ - evtTarget.addEventListener('pointermove', this._onHandlerPointerMove, { capture: true, passive: false }); + evtTarget.addEventListener('pointermove', this._onHandlerPointerMove, { capture: true }); (this as unknown as IGroup).addEventListener('pointermove', this._onHandlerPointerMove, { - capture: true, - passive: false + capture: true }); triggers.forEach((trigger: string) => { @@ -305,7 +303,6 @@ export class DataZoom extends AbstractComponent> { * 4. 在endHandler上拖拽,同上 */ private _pointerMove = (e: FederatedPointerEvent) => { - e.preventDefault(); const { start: startAttr, end: endAttr, brushSelect, realTime = true } = this.attribute as DataZoomAttributes; const pos = this.eventPosToStagePos(e); const { attPos, max } = this._layoutCache; diff --git a/packages/vrender-components/src/scrollbar/scrollbar.ts b/packages/vrender-components/src/scrollbar/scrollbar.ts index 286af469f..8346010a2 100644 --- a/packages/vrender-components/src/scrollbar/scrollbar.ts +++ b/packages/vrender-components/src/scrollbar/scrollbar.ts @@ -343,7 +343,7 @@ export class ScrollBar extends AbstractComponent> * move的时候,需要通过 capture: true,能够在捕获截断被拦截, * move的时候,需要显示的设置passive: false,因为在移动端需要禁用浏览器默认行为 */ - obj.addEventListener('pointermove', this._onSliderPointerMoveWithDelay, { capture: true, passive: false }); + obj.addEventListener('pointermove', this._onSliderPointerMoveWithDelay, { capture: true }); triggers.forEach((trigger: string) => { obj.addEventListener(trigger, this._onSliderPointerUp); }); @@ -371,7 +371,6 @@ export class ScrollBar extends AbstractComponent> }; private _onSliderPointerMove = (e: any) => { - e.preventDefault(); const { stopSliderMovePropagation = true } = this.attribute as ScrollBarAttributes; if (stopSliderMovePropagation) { e.stopPropagation(); @@ -391,7 +390,7 @@ export class ScrollBar extends AbstractComponent> const triggers = getEndTriggersOfDrag(); const obj = vglobal.env === 'browser' ? vglobal : this.stage; - obj.removeEventListener('pointermove', this._onSliderPointerMoveWithDelay, { capture: true, passive: false }); + obj.removeEventListener('pointermove', this._onSliderPointerMoveWithDelay, { capture: true }); triggers.forEach((trigger: string) => { obj.removeEventListener(trigger, this._onSliderPointerUp); }); @@ -417,4 +416,15 @@ export class ScrollBar extends AbstractComponent> this._sliderRenderBounds = null; this._sliderLimitRange = null; } + + release(all?: boolean): void { + /** + * 浏览器上的事件必须解绑,防止内存泄漏,场景树上的事件会自动解绑 + */ + super.release(all); + (vglobal.env === 'browser' ? vglobal : this.stage).addEventListener('touchmove', this._handleTouchMove, { + passive: false + }); + this._clearDragEvents(); + } } diff --git a/packages/vrender-components/src/slider/slider.ts b/packages/vrender-components/src/slider/slider.ts index 1dfcf44e3..bf0cc5e37 100644 --- a/packages/vrender-components/src/slider/slider.ts +++ b/packages/vrender-components/src/slider/slider.ts @@ -665,7 +665,9 @@ export class Slider extends AbstractComponent> { 'pointerdown', this._onRailPointerDown as EventListenerOrEventListenerObject ); - + /** + * move的时候,需要显示的设置passive: false,因为在移动端需要禁用浏览器默认行为 + */ (vglobal.env === 'browser' ? vglobal : this.stage).addEventListener('touchmove', this._handleTouchMove, { passive: false }); @@ -747,10 +749,9 @@ export class Slider extends AbstractComponent> { const obj = vglobal.env === 'browser' ? vglobal : this.stage; /** - * move的时候,需要通过 capture: true,能够在捕获截断被拦截, - * move的时候,需要显示的设置passive: false,因为在移动端需要禁用浏览器默认行为 + * move的时候,需要通过 capture: true,能够在捕获截断被拦截 */ - obj.addEventListener('pointermove', this._onHandlerPointerMove, { capture: true, passive: false }); + obj.addEventListener('pointermove', this._onHandlerPointerMove, { capture: true }); triggers.forEach((trigger: string) => { obj.addEventListener(trigger, this._onHandlerPointerUp); }); @@ -760,20 +761,18 @@ export class Slider extends AbstractComponent> { const triggers = getEndTriggersOfDrag(); const obj = vglobal.env === 'browser' ? vglobal : this.stage; - obj.removeEventListener('pointermove', this._onHandlerPointerMove, { capture: true, passive: false }); + obj.removeEventListener('pointermove', this._onHandlerPointerMove, { capture: true }); triggers.forEach((trigger: string) => { obj.removeEventListener(trigger, this._onHandlerPointerUp); }); - obj.removeEventListener('pointermove', this._onTrackPointerMove, { capture: true, passive: false }); + obj.removeEventListener('pointermove', this._onTrackPointerMove, { capture: true }); triggers.forEach((trigger: string) => { obj.removeEventListener(trigger, this._onTrackPointerUp); }); } private _onHandlerPointerMove = (e: FederatedPointerEvent) => { - e.preventDefault(); - this._isChanging = true; const { railWidth, railHeight, min, max } = this.attribute as SliderAttributes; if (max === min) { @@ -839,14 +838,13 @@ export class Slider extends AbstractComponent> { const triggers = getEndTriggersOfDrag(); const obj = vglobal.env === 'browser' ? vglobal : this.stage; - obj.addEventListener('pointermove', this._onTrackPointerMove, { capture: true, passive: false }); + obj.addEventListener('pointermove', this._onTrackPointerMove, { capture: true }); triggers.forEach((trigger: string) => { obj.addEventListener(trigger, this._onTrackPointerUp); }); }; private _onTrackPointerMove = (e: FederatedPointerEvent) => { - e.preventDefault(); this._isChanging = true; const { railWidth, railHeight, min, max, inverse } = this.attribute as SliderAttributes; From f6836921dac84ca1100a04cd065a5d02c0ccf785 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 18 Apr 2025 18:11:31 +0800 Subject: [PATCH 024/179] fix: fix issue with symbol gradient, and support native conical gradient --- ...ical-gradient-symbol_2025-04-18-10-10.json | 10 ++++++ ...ical-gradient-symbol_2025-04-18-10-10.json | 10 ++++++ .../vrender-core/src/common/canvas-utils.ts | 9 +++-- packages/vrender-core/src/graphic/graphic.ts | 2 ++ packages/vrender-core/src/graphic/symbol.ts | 2 ++ .../canvas/contributions/browser/context.ts | 5 +++ .../__tests__/browser/src/pages/rect.ts | 34 +++++++++++++++---- 7 files changed, 63 insertions(+), 9 deletions(-) create mode 100644 common/changes/@visactor/vrender-core/fix-conical-gradient-symbol_2025-04-18-10-10.json create mode 100644 common/changes/@visactor/vrender-kits/fix-conical-gradient-symbol_2025-04-18-10-10.json diff --git a/common/changes/@visactor/vrender-core/fix-conical-gradient-symbol_2025-04-18-10-10.json b/common/changes/@visactor/vrender-core/fix-conical-gradient-symbol_2025-04-18-10-10.json new file mode 100644 index 000000000..658e4c7e7 --- /dev/null +++ b/common/changes/@visactor/vrender-core/fix-conical-gradient-symbol_2025-04-18-10-10.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-core", + "comment": "fix: fix issue with symbol gradient, and support native conical gradient", + "type": "none" + } + ], + "packageName": "@visactor/vrender-core" +} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-kits/fix-conical-gradient-symbol_2025-04-18-10-10.json b/common/changes/@visactor/vrender-kits/fix-conical-gradient-symbol_2025-04-18-10-10.json new file mode 100644 index 000000000..d4279c568 --- /dev/null +++ b/common/changes/@visactor/vrender-kits/fix-conical-gradient-symbol_2025-04-18-10-10.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-kits", + "comment": "fix: fix issue with symbol gradient, and support native conical gradient", + "type": "none" + } + ], + "packageName": "@visactor/vrender-kits" +} \ No newline at end of file diff --git a/packages/vrender-core/src/common/canvas-utils.ts b/packages/vrender-core/src/common/canvas-utils.ts index 08086dc50..946e66dbf 100644 --- a/packages/vrender-core/src/common/canvas-utils.ts +++ b/packages/vrender-core/src/common/canvas-utils.ts @@ -59,8 +59,9 @@ export function createColor( x /= scaleX; y /= scaleY; if (angle || scaleX !== 1 || scaleY !== 1) { - x = 0; - y = 0; + // symbol的时候锚点是在中间的,所以bounds不能按0算 + x = (params as any).x1WithoutTransform ?? 0; + y = (params as any).y1WithoutTransform ?? 0; w = (params as any).widthWithoutTransform ?? w; h = (params as any).heightWithoutTransform ?? h; } @@ -117,5 +118,7 @@ function createConicGradient(context: IContext2d, color: IConicalGradient, x: nu }); let deltaAngle; - return (canvasGradient as any).GetPattern(w + x, h + y, deltaAngle); + return (canvasGradient as any).GetPattern + ? (canvasGradient as any).GetPattern(w + x, h + y, deltaAngle) + : canvasGradient; } diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index 14b80c5aa..b4239bf63 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -225,6 +225,8 @@ export abstract class Graphic = Partial implements ISymbol this.widthWithoutTransform = aabbBounds.x2 - aabbBounds.x1; this.heightWithoutTransform = aabbBounds.y2 - aabbBounds.y1; + this.x1WithoutTransform = aabbBounds.x1; + this.y1WithoutTransform = aabbBounds.y1; const { lineJoin = symbolTheme.lineJoin } = attribute; application.graphicService.transformAABBBounds(attribute, aabbBounds, symbolTheme, lineJoin === 'miter', this); diff --git a/packages/vrender-kits/src/canvas/contributions/browser/context.ts b/packages/vrender-kits/src/canvas/contributions/browser/context.ts index 942319f9c..2723173ae 100644 --- a/packages/vrender-kits/src/canvas/contributions/browser/context.ts +++ b/packages/vrender-kits/src/canvas/contributions/browser/context.ts @@ -706,6 +706,11 @@ export class BrowserContext2d implements IContext2d { } createConicGradient(x: number, y: number, startAngle: number, endAngle: number): IConicalGradientData { + // 检测浏览器是否原生支持conic-gradient + const isNative = this.nativeContext.createConicGradient; + if (isNative && Math.abs(endAngle - startAngle - Math.PI * 2) < 0.001) { + return this.nativeContext.createConicGradient(0, x); + } let edit = false; let pattern: CanvasPattern | null; // eslint-disable-next-line @typescript-eslint/no-this-alias diff --git a/packages/vrender/__tests__/browser/src/pages/rect.ts b/packages/vrender/__tests__/browser/src/pages/rect.ts index bd64b8f70..53682c067 100644 --- a/packages/vrender/__tests__/browser/src/pages/rect.ts +++ b/packages/vrender/__tests__/browser/src/pages/rect.ts @@ -1,4 +1,4 @@ -import { createStage, createRect, IGraphic, createGroup } from '@visactor/vrender'; +import { createStage, createRect, IGraphic, createGroup, createSymbol } from '@visactor/vrender'; import { roughModule } from '@visactor/vrender-kits'; import { addShapesToStage, colorPools } from '../utils'; @@ -68,17 +68,39 @@ export const page = () => { lineDash: [100, 10], lineDashOffset: -100 }); - - const group = createGroup({ + const star = createSymbol({ x: 300, y: 100, + scaleX: 2, + scaleY: 2, + angle: 30, + size: 100, + symbolType: 'square', + // cornerRadius: [0, 10, 10, 0], + stroke: 'red', + // scaleCenter: ['50%', '50%'], + // _debug_bounds: true, + fill: 'conic-gradient(from 90deg, rgba(5,0,255,1) 16%, rgba(0,255,10,1) 41%, rgba(9,9,121,1) 53%, rgba(0,212,255,1) 100%)', + // fill: 'linear-gradient(90deg, #215F97 0%, #FF948F 100%)', + // cornerRadius: [5, 10, 15, 20], + lineWidth: 5, + anchor: ['50%', '50%'], + // anchor: [400, 200], + lineDash: [100, 10], + lineDashOffset: -100 + }); + + const group = createGroup({ + x: 0, + y: 0, width: 200, - height: 200, - angle: 45, - anchor: ['50%', '50%'] + height: 200 + // angle: 45, + // anchor: ['50%', '50%'] }); group.appendChild(r); + group.appendChild(star); // r.animate().to({ lineDash: [2000, 1000], lineDashOffset: 100 }, 1000, 'linear'); From 8617610b3b8eea2cb174bd9e6d745a8c0739bbe6 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 18 Apr 2025 18:32:47 +0800 Subject: [PATCH 025/179] fix: fix typo error --- .../vrender-kits/src/canvas/contributions/browser/context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vrender-kits/src/canvas/contributions/browser/context.ts b/packages/vrender-kits/src/canvas/contributions/browser/context.ts index 2723173ae..cbd776360 100644 --- a/packages/vrender-kits/src/canvas/contributions/browser/context.ts +++ b/packages/vrender-kits/src/canvas/contributions/browser/context.ts @@ -709,7 +709,7 @@ export class BrowserContext2d implements IContext2d { // 检测浏览器是否原生支持conic-gradient const isNative = this.nativeContext.createConicGradient; if (isNative && Math.abs(endAngle - startAngle - Math.PI * 2) < 0.001) { - return this.nativeContext.createConicGradient(0, x); + return this.nativeContext.createConicGradient(0, x, y); } let edit = false; let pattern: CanvasPattern | null; From 3558cfd88a26e3ae1de9e45ab0467f00ae8e02de Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 18 Apr 2025 19:17:22 +0800 Subject: [PATCH 026/179] fix: fix typo error --- .../vrender-kits/src/canvas/contributions/browser/context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vrender-kits/src/canvas/contributions/browser/context.ts b/packages/vrender-kits/src/canvas/contributions/browser/context.ts index cbd776360..3e5fef416 100644 --- a/packages/vrender-kits/src/canvas/contributions/browser/context.ts +++ b/packages/vrender-kits/src/canvas/contributions/browser/context.ts @@ -709,7 +709,7 @@ export class BrowserContext2d implements IContext2d { // 检测浏览器是否原生支持conic-gradient const isNative = this.nativeContext.createConicGradient; if (isNative && Math.abs(endAngle - startAngle - Math.PI * 2) < 0.001) { - return this.nativeContext.createConicGradient(0, x, y); + return this.nativeContext.createConicGradient(startAngle, x, y); } let edit = false; let pattern: CanvasPattern | null; From 97d07210598856fff3a73ab2eb688f2ab6dc165f Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 18 Apr 2025 20:39:55 +0800 Subject: [PATCH 027/179] feat: enhance rough --- packages/vrender-core/src/graphic/config.ts | 1 + .../vrender-core/src/interface/graphic.ts | 7 + .../contributions/render/area-render.ts | 4 +- .../contributions/render/line-render.ts | 2 +- .../render/contributions/rough/base-render.ts | 129 +++- .../src/render/contributions/rough/config.ts | 8 +- .../src/render/contributions/rough/context.ts | 579 ++++++++++++++++++ .../render/contributions/rough/rough-line.ts | 167 +---- .../render/contributions/rough/rough-rect.ts | 111 +--- .../contributions/rough/rough-symbol.ts | 116 +--- .../__tests__/browser/src/pages/line.ts | 3 +- .../__tests__/browser/src/pages/rect.ts | 17 +- 12 files changed, 760 insertions(+), 384 deletions(-) create mode 100644 packages/vrender-kits/src/render/contributions/rough/context.ts diff --git a/packages/vrender-core/src/graphic/config.ts b/packages/vrender-core/src/graphic/config.ts index 1265809da..25664bc9b 100644 --- a/packages/vrender-core/src/graphic/config.ts +++ b/packages/vrender-core/src/graphic/config.ts @@ -205,6 +205,7 @@ export const DefaultAttribute: Required = { shadowPickMode: 'graphic', keepStrokeScale: false, clipConfig: null, + roughStyle: null, ...DefaultDebugAttribute, ...DefaultStyle, ...DefaultTransform diff --git a/packages/vrender-core/src/interface/graphic.ts b/packages/vrender-core/src/interface/graphic.ts index 36b3232e9..cce026232 100644 --- a/packages/vrender-core/src/interface/graphic.ts +++ b/packages/vrender-core/src/interface/graphic.ts @@ -376,6 +376,12 @@ export interface CommonDomOptions { anchorType?: 'position' | 'boundsLeftTop' | BoundsAnchorType; } +export type IRoughStyle = { + fillStyle: 'hachure' | 'solid' | 'zigzag' | 'cross-hatch' | 'dots' | 'sunburst' | 'dashed' | 'zigzag-line'; + roughness: number; + bowing: number; +}; + export type IGraphicStyle = ILayout & IFillStyle & IStrokeStyle & @@ -497,6 +503,7 @@ export type IGraphicStyle = ILayout & cursor: Cursor | null; filter: string; renderStyle?: 'default' | 'rough' | any; + roughStyle?: IRoughStyle | null; /** * HTML的dom或者string */ diff --git a/packages/vrender-core/src/render/contributions/render/area-render.ts b/packages/vrender-core/src/render/contributions/render/area-render.ts index 8eb5f8921..6e132f58c 100644 --- a/packages/vrender-core/src/render/contributions/render/area-render.ts +++ b/packages/vrender-core/src/render/contributions/render/area-render.ts @@ -551,7 +551,7 @@ export class DefaultCanvasAreaRender extends BaseRender implements IGraph } else { direction = xTotalLength > yTotalLength ? Direction.ROW : Direction.COLUMN; } - drawAreaSegments(context.camera ? context : context.nativeContext, cache, clipRange, { + drawAreaSegments(context, cache, clipRange, { offsetX, offsetY, offsetZ, @@ -599,7 +599,7 @@ export class DefaultCanvasAreaRender extends BaseRender implements IGraph if (isArray(stroke) && (stroke[0] || stroke[2]) && stroke[1] === false) { context.beginPath(); drawSegments( - context.camera ? context : context.nativeContext, + context, stroke[0] ? cache.top : cache.bottom, clipRange, direction === Direction.ROW ? 'x' : 'y', diff --git a/packages/vrender-core/src/render/contributions/render/line-render.ts b/packages/vrender-core/src/render/contributions/render/line-render.ts index ed7730d8d..55ba43608 100644 --- a/packages/vrender-core/src/render/contributions/render/line-render.ts +++ b/packages/vrender-core/src/render/contributions/render/line-render.ts @@ -83,7 +83,7 @@ export class DefaultCanvasLineRender extends BaseRender implements IGraph const z = this.z ?? 0; - drawSegments(context.camera ? context : context.nativeContext, cache, clipRange, clipRangeByDimension, { + drawSegments(context, cache, clipRange, clipRangeByDimension, { offsetX, offsetY, offsetZ: z diff --git a/packages/vrender-kits/src/render/contributions/rough/base-render.ts b/packages/vrender-kits/src/render/contributions/rough/base-render.ts index f5811eeaf..2fefd8c26 100644 --- a/packages/vrender-kits/src/render/contributions/rough/base-render.ts +++ b/packages/vrender-kits/src/render/contributions/rough/base-render.ts @@ -1,13 +1,18 @@ -import type { - IGraphicAttribute, - IContext2d, - IGraphic, - IMarkAttribute, - IThemeAttribute, - IDrawContext, - IGraphicRenderDrawParams, - IGraphicRender +import type { IRenderService } from '@visactor/vrender-core'; +import { + type IGraphicAttribute, + type IContext2d, + type IGraphic, + type IMarkAttribute, + type IThemeAttribute, + type IDrawContext, + type IGraphicRenderDrawParams, + type IGraphicRender, + CustomPath2D } from '@visactor/vrender-core'; +import rough from 'roughjs'; +import { RoughContext2d } from './context'; +import { defaultRouthThemeSpec } from './config'; export abstract class RoughBaseRender { canvasRenderer!: IGraphicRender; @@ -33,4 +38,110 @@ export abstract class RoughBaseRender { return this.canvasRenderer.drawShape(graphic, ctx, x, y, drawContext, params, fillCb, strokeCb); } } + + doDraw( + graphic: IGraphic, + renderService: IRenderService, + drawContext: IDrawContext, + params?: IGraphicRenderDrawParams + ) { + const { context } = drawContext; + if (!context) { + return; + } + // 获取到原生canvas + const canvas = context.canvas.nativeCanvas; + const rc = rough.canvas(canvas); + + // context.highPerformanceSave(); + + const customPath = new CustomPath2D(); + const roughContext = new RoughContext2d(context, customPath); + + context.save(); + // 不管怎么样,都transform + context.transformFromMatrix(graphic.transMatrix, true); + + const { fill, stroke, roughStyle = {}, lineWidth } = graphic.attribute as any; + + const { + maxRandomnessOffset = defaultRouthThemeSpec.maxRandomnessOffset, + roughness = defaultRouthThemeSpec.roughness, + bowing = defaultRouthThemeSpec.bowing, + curveFitting = defaultRouthThemeSpec.curveFitting, + curveTightness = defaultRouthThemeSpec.curveTightness, + curveStepCount = defaultRouthThemeSpec.curveStepCount, + fillStyle = defaultRouthThemeSpec.fillStyle, + fillWeight = defaultRouthThemeSpec.fillWeight, + hachureAngle = defaultRouthThemeSpec.hachureAngle, + hachureGap = defaultRouthThemeSpec.hachureGap, + simplification = defaultRouthThemeSpec.simplification, + dashOffset = defaultRouthThemeSpec.dashOffset, + dashGap = defaultRouthThemeSpec.dashGap, + zigzagOffset = defaultRouthThemeSpec.zigzagOffset, + seed = defaultRouthThemeSpec.seed, + fillLineDash = defaultRouthThemeSpec.fillLineDash, + fillLineDashOffset = defaultRouthThemeSpec.fillLineDashOffset, + disableMultiStroke = defaultRouthThemeSpec.disableMultiStroke, + disableMultiStrokeFill = defaultRouthThemeSpec.disableMultiStrokeFill, + preserveVertices = defaultRouthThemeSpec.preserveVertices, + fixedDecimalPlaceDigits = defaultRouthThemeSpec.fixedDecimalPlaceDigits + } = roughStyle; + + let rendered = false; + const doRender = () => { + if (rendered) { + return; + } + rendered = true; + const path = customPath.toString(); + context.beginPath(); + rc.path(path, { + fill, + stroke, + strokeWidth: lineWidth, + maxRandomnessOffset, + roughness, + bowing, + curveFitting, + curveTightness, + curveStepCount, + fillStyle, + fillWeight, + hachureAngle, + hachureGap, + simplification, + dashOffset, + dashGap, + zigzagOffset, + seed, + fillLineDash, + fillLineDashOffset, + disableMultiStroke, + disableMultiStrokeFill, + preserveVertices, + fixedDecimalPlaceDigits + }); + }; + this.canvasRenderer.drawShape( + graphic, + roughContext, + 0, + 0, + drawContext, + params, + () => { + doRender(); + return false; + }, + () => { + doRender(); + return false; + } + ); + + context.restore(); + + // context.highPerformanceRestore(); + } } diff --git a/packages/vrender-kits/src/render/contributions/rough/config.ts b/packages/vrender-kits/src/render/contributions/rough/config.ts index 0a817da27..d1a50ebe6 100644 --- a/packages/vrender-kits/src/render/contributions/rough/config.ts +++ b/packages/vrender-kits/src/render/contributions/rough/config.ts @@ -3,7 +3,7 @@ import type { Options } from 'roughjs/bin/core'; export const defaultRouthThemeSpec: Options = { maxRandomnessOffset: 3, // 粗糙度,值越大绘制的越乱 - roughness: 1, + roughness: 1.6, // 线段的弯曲度 bowing: 1, // 曲线拟合程度 @@ -12,7 +12,7 @@ export const defaultRouthThemeSpec: Options = { // 近似曲线的点数 curveStepCount: 9, // 填充形式,默认斜线 - fillStyle: 'hachure', + fillStyle: 'cross-hatch', // 填充线的粗细、填充点的大小 fillWeight: undefined, // 填充为hachure时的转角 @@ -25,12 +25,12 @@ export const defaultRouthThemeSpec: Options = { dashGap: undefined, zigzagOffset: undefined, // 生成随机形状的种子 - seed: 1, + seed: 3, fillLineDash: undefined, fillLineDashOffset: undefined, // 禁止用多个笔画绘制 disableMultiStroke: false, - disableMultiStrokeFill: false, + disableMultiStrokeFill: true, preserveVertices: true, fixedDecimalPlaceDigits: undefined }; diff --git a/packages/vrender-kits/src/render/contributions/rough/context.ts b/packages/vrender-kits/src/render/contributions/rough/context.ts new file mode 100644 index 000000000..df1c64b93 --- /dev/null +++ b/packages/vrender-kits/src/render/contributions/rough/context.ts @@ -0,0 +1,579 @@ +/** + * 部分源码参考konva + * MIT License + + Original work Copyright (C) 2011 - 2013 by Eric Rowell (KineticJS) + Modified work Copyright (C) 2014 - present by Anton Lavrenov (Konva) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ +import type { IPointLike, TextMeasure, ITextMeasureSpec, IMatrix, Matrix } from '@visactor/vutils'; +import type { + ICamera, + ICanvas, + ICommonStyleParams, + IConicalGradientData, + IContext2d, + ISetCommonStyleParams, + ISetStrokeStyleParams, + IStrokeStyleParams, + ITextStyleParams, + mat4, + EnvType, + vec3, + CustomPath2D +} from '@visactor/vrender-core'; + +/** + * RoughContext2d serves as a proxy to the original context (BrowserContext2d) + * while also updating a custom path for path-related operations + */ +export class RoughContext2d implements IContext2d { + static env: EnvType = 'browser'; + originContext: IContext2d; + customPath: CustomPath2D; + + constructor(originContext: IContext2d, customPath: CustomPath2D) { + this.originContext = originContext; + this.customPath = customPath; + } + + // Path-related methods that affect both the original context and the custom path + beginPath(): void { + this.originContext.beginPath(); + this.customPath.beginPath(); + } + + moveTo(x: number, y: number, z?: number): void { + this.originContext.moveTo(x, y, z); + this.customPath.moveTo(x, y); + } + + lineTo(x: number, y: number, z?: number): void { + this.originContext.lineTo(x, y, z); + this.customPath.lineTo(x, y); + } + + bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number, z?: number): void { + (this.originContext.bezierCurveTo as any)(cp1x, cp1y, cp2x, cp2y, x, y, z); + this.customPath.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); + } + + quadraticCurveTo(cpx: number, cpy: number, x: number, y: number, z?: number): void { + this.originContext.quadraticCurveTo(cpx, cpy, x, y, z); + this.customPath.quadraticCurveTo(cpx, cpy, x, y); + } + + arc( + x: number, + y: number, + radius: number, + startAngle: number, + endAngle: number, + anticlockwise?: boolean, + z?: number + ): void { + this.originContext.arc(x, y, radius, startAngle, endAngle, anticlockwise, z); + this.customPath.arc(x, y, radius, startAngle, endAngle, anticlockwise); + } + + arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void { + this.originContext.arcTo(x1, y1, x2, y2, radius); + this.customPath.arcTo(x1, y1, x2, y2, radius); + } + + ellipse( + x: number, + y: number, + radiusX: number, + radiusY: number, + rotation: number, + startAngle: number, + endAngle: number, + anticlockwise?: boolean + ): void { + this.originContext.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise); + this.customPath.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise); + } + + rect(x: number, y: number, w: number, h: number, z?: number): void { + this.originContext.rect(x, y, w, h, z); + this.customPath.rect(x, y, w, h); + } + + closePath(): void { + this.originContext.closePath(); + this.customPath.closePath(); + } + + // Property forwarding using getters and setters + // Define getters and setters for all properties to forward to originContext + // Canvas + get canvas(): ICanvas { + return this.originContext.canvas; + } + set canvas(value: ICanvas) { + this.originContext.canvas = value; + } + + // Camera + get camera(): ICamera | undefined { + return this.originContext.camera; + } + set camera(value: ICamera | undefined) { + this.originContext.camera = value; + } + + // ModelMatrix + get modelMatrix(): mat4 | undefined { + return this.originContext.modelMatrix; + } + set modelMatrix(value: mat4 | undefined) { + this.originContext.modelMatrix = value; + } + + // NativeContext + get nativeContext(): CanvasRenderingContext2D | any { + return this.originContext.nativeContext; + } + set nativeContext(value: CanvasRenderingContext2D | any) { + this.originContext.nativeContext = value; + } + + // Inuse + get _inuse(): boolean { + return this.originContext._inuse; + } + set _inuse(value: boolean) { + this.originContext._inuse = value; + } + + get inuse(): boolean { + return this.originContext.inuse; + } + set inuse(value: boolean) { + this.originContext.inuse = value; + } + + // Stack + get stack(): Matrix[] { + return this.originContext.stack; + } + set stack(value: Matrix[]) { + this.originContext.stack = value; + } + + // Disable flags + get disableFill(): boolean | undefined { + return this.originContext.disableFill; + } + set disableFill(value: boolean | undefined) { + this.originContext.disableFill = value; + } + + get disableStroke(): boolean | undefined { + return this.originContext.disableStroke; + } + set disableStroke(value: boolean | undefined) { + this.originContext.disableStroke = value; + } + + get disableBeginPath(): boolean | undefined { + return this.originContext.disableBeginPath; + } + set disableBeginPath(value: boolean | undefined) { + this.originContext.disableBeginPath = value; + } + + // Font properties + get fontFamily(): string { + return this.originContext.fontFamily; + } + set fontFamily(value: string) { + this.originContext.fontFamily = value; + } + + get fontSize(): number { + return this.originContext.fontSize; + } + set fontSize(value: number) { + this.originContext.fontSize = value; + } + + // Matrix + get _clearMatrix(): IMatrix { + return this.originContext._clearMatrix; + } + set _clearMatrix(value: IMatrix) { + this.originContext._clearMatrix = value; + } + + // DPR + get dpr(): number { + return this.originContext.dpr; + } + set dpr(value: number) { + this.originContext.dpr = value; + } + + // Other properties + get baseGlobalAlpha(): number { + return this.originContext.baseGlobalAlpha; + } + set baseGlobalAlpha(value: number) { + this.originContext.baseGlobalAlpha = value; + } + + get drawPromise(): Promise | undefined { + return this.originContext.drawPromise; + } + set drawPromise(value: Promise | undefined) { + this.originContext.drawPromise = value; + } + + get mathTextMeasure(): TextMeasure { + return this.originContext.mathTextMeasure; + } + set mathTextMeasure(value: TextMeasure) { + this.originContext.mathTextMeasure = value; + } + + // Canvas context style properties + get fillStyle(): string | CanvasGradient | CanvasPattern { + return this.originContext.fillStyle; + } + set fillStyle(value: string | CanvasGradient | CanvasPattern) { + this.originContext.fillStyle = value; + } + + get font(): string { + return this.originContext.font; + } + set font(value: string) { + this.originContext.font = value; + } + + get globalAlpha(): number { + return this.originContext.globalAlpha; + } + set globalAlpha(value: number) { + this.originContext.globalAlpha = value; + } + + get lineCap(): CanvasLineCap { + return this.originContext.lineCap as any; + } + set lineCap(value: CanvasLineCap) { + this.originContext.lineCap = value; + } + + get lineDashOffset(): number { + return this.originContext.lineDashOffset; + } + set lineDashOffset(value: number) { + this.originContext.lineDashOffset = value; + } + + get lineJoin(): CanvasLineJoin { + return this.originContext.lineJoin as any; + } + set lineJoin(value: CanvasLineJoin) { + this.originContext.lineJoin = value; + } + + get lineWidth(): number { + return this.originContext.lineWidth; + } + set lineWidth(value: number) { + this.originContext.lineWidth = value; + } + + get miterLimit(): number { + return this.originContext.miterLimit; + } + set miterLimit(value: number) { + this.originContext.miterLimit = value; + } + + get shadowBlur(): number { + return this.originContext.shadowBlur; + } + set shadowBlur(value: number) { + this.originContext.shadowBlur = value; + } + + get shadowColor(): string { + return this.originContext.shadowColor; + } + set shadowColor(value: string) { + this.originContext.shadowColor = value; + } + + get shadowOffsetX(): number { + return this.originContext.shadowOffsetX; + } + set shadowOffsetX(value: number) { + this.originContext.shadowOffsetX = value; + } + + get shadowOffsetY(): number { + return this.originContext.shadowOffsetY; + } + set shadowOffsetY(value: number) { + this.originContext.shadowOffsetY = value; + } + + get strokeStyle(): string | CanvasGradient | CanvasPattern { + return this.originContext.strokeStyle; + } + set strokeStyle(value: string | CanvasGradient | CanvasPattern) { + this.originContext.strokeStyle = value; + } + + get textAlign(): CanvasTextAlign { + return this.originContext.textAlign as any; + } + set textAlign(value: CanvasTextAlign) { + this.originContext.textAlign = value as any; + } + + get textBaseline(): CanvasTextBaseline { + return this.originContext.textBaseline as any; + } + set textBaseline(value: CanvasTextBaseline) { + this.originContext.textBaseline = value; + } + + // Matrix-related getter + get currentMatrix() { + return this.originContext.currentMatrix; + } + + // Forward all other methods to originContext + // Transform methods + save(): void { + return this.originContext.save(); + } + restore(): void { + return this.originContext.restore(); + } + highPerformanceSave(): void { + return this.originContext.highPerformanceSave(); + } + highPerformanceRestore(): void { + return this.originContext.highPerformanceRestore(); + } + rotate(rad: number, setTransform?: boolean): void { + return this.originContext.rotate(rad, setTransform); + } + scale(sx: number, sy: number, setTransform?: boolean): void { + return this.originContext.scale(sx, sy, setTransform); + } + setScale(sx: number, sy: number, setTransform?: boolean): void { + return this.originContext.setScale(sx, sy, setTransform); + } + scalePoint(sx: number, sy: number, px: number, py: number, setTransform?: boolean): void { + return this.originContext.scalePoint(sx, sy, px, py, setTransform); + } + setTransform( + a: number, + b: number, + c: number, + d: number, + e: number, + f: number, + setTransform?: boolean, + dpr?: number + ): void { + return this.originContext.setTransform(a, b, c, d, e, f, setTransform, dpr); + } + setTransformFromMatrix(matrix: Matrix, setTransform?: boolean, dpr?: number): void { + return this.originContext.setTransformFromMatrix(matrix, setTransform, dpr); + } + resetTransform(setTransform?: boolean, dpr?: number): void { + return this.originContext.resetTransform(setTransform, dpr); + } + transform(a: number, b: number, c: number, d: number, e: number, f: number, setTransform?: boolean): void { + return this.originContext.transform(a, b, c, d, e, f, setTransform); + } + transformFromMatrix(matrix: Matrix, setTransform?: boolean): void { + return this.originContext.transformFromMatrix(matrix, setTransform); + } + translate(x: number, y: number, setTransform?: boolean): void { + return this.originContext.translate(x, y, setTransform); + } + rotateDegrees(deg: number, setTransform?: boolean): void { + return this.originContext.rotateDegrees(deg, setTransform); + } + rotateAbout(rad: number, x: number, y: number, setTransform?: boolean): void { + return this.originContext.rotateAbout(rad, x, y, setTransform); + } + rotateDegreesAbout(deg: number, x: number, y: number, setTransform?: boolean): void { + return this.originContext.rotateDegreesAbout(deg, x, y, setTransform); + } + + setTransformForCurrent(force: boolean = false) { + return this.originContext.setTransformForCurrent(force); + } + + // Drawing methods + clip(fillRule?: CanvasFillRule): void; + clip(path: Path2D, fillRule?: CanvasFillRule): void; + clip(path?: Path2D | CanvasFillRule, fillRule?: CanvasFillRule): void { + return (this.originContext.clip as any)(path, fillRule); + } + fill(path?: Path2D, fillRule?: CanvasFillRule): void { + return this.originContext.fill(path, fillRule); + } + stroke(path?: Path2D): void { + return this.originContext.stroke(path); + } + fillRect(x: number, y: number, width: number, height: number): void { + return this.originContext.fillRect(x, y, width, height); + } + strokeRect(x: number, y: number, width: number, height: number): void { + return this.originContext.strokeRect(x, y, width, height); + } + fillText(text: string, x: number, y: number, z?: number): void { + return this.originContext.fillText(text, x, y, z); + } + strokeText(text: string, x: number, y: number, z?: number): void { + return this.originContext.strokeText(text, x, y, z); + } + clearRect(x: number, y: number, w: number, h: number): void { + return this.originContext.clearRect(x, y, w, h); + } + + // Image methods + drawImage(...args: any[]): void { + return (this.originContext.drawImage as any)(...args); + } + createImageData(...args: any[]): ImageData { + return (this.originContext.createImageData as any)(...args); + } + getImageData(sx: number, sy: number, sw: number, sh: number): ImageData { + return this.originContext.getImageData(sx, sy, sw, sh); + } + putImageData(imagedata: ImageData, dx: number, dy: number): void { + return this.originContext.putImageData(imagedata, dx, dy); + } + + // Gradient/Pattern methods + createLinearGradient(x0: number, y0: number, x1: number, y1: number): CanvasGradient { + return this.originContext.createLinearGradient(x0, y0, x1, y1); + } + createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): CanvasGradient { + return this.originContext.createRadialGradient(x0, y0, r0, x1, y1, r1); + } + createConicGradient(x: number, y: number, startAngle: number, endAngle: number): IConicalGradientData { + return this.originContext.createConicGradient(x, y, startAngle, endAngle); + } + createPattern(image: HTMLImageElement | HTMLCanvasElement | HTMLVideoElement, repetition: string): CanvasPattern { + return this.originContext.createPattern(image, repetition); + } + + // Line style methods + getLineDash(): number[] { + return this.originContext.getLineDash(); + } + setLineDash(segments: number[]): void { + return this.originContext.setLineDash(segments); + } + + // Utility methods + clear(): void { + return this.originContext.clear(); + } + measureText(text: string, method?: 'native' | 'simple' | 'quick'): { width: number } { + return this.originContext.measureText(text, method); + } + isPointInPath(x: number, y: number): boolean { + return this.originContext.isPointInPath(x, y); + } + isPointInStroke(x: number, y: number): boolean { + return this.originContext.isPointInStroke(x, y); + } + project(x: number, y: number, z?: number): IPointLike { + return this.originContext.project(x, y, z); + } + view(x: number, y: number, z?: number): vec3 { + return this.originContext.view(x, y, z); + } + + // Helper methods + getCanvas(): ICanvas { + return this.originContext.getCanvas(); + } + getContext(): CanvasRenderingContext2D | any { + return this.originContext.getContext(); + } + setCommonStyle( + params: ISetCommonStyleParams, + attribute: ICommonStyleParams, + offsetX: number, + offsetY: number, + defaultParams?: ICommonStyleParams | Partial[] + ): void { + return this.originContext.setCommonStyle(params, attribute, offsetX, offsetY, defaultParams); + } + setShadowBlendStyle( + params: ISetCommonStyleParams, + attribute: ICommonStyleParams, + defaultParams?: ICommonStyleParams | Partial[] + ): void { + return this.originContext.setShadowBlendStyle(params, attribute, defaultParams); + } + setStrokeStyle( + params: ISetStrokeStyleParams, + attribute: IStrokeStyleParams, + offsetX: number, + offsetY: number, + defaultParams?: IStrokeStyleParams | any + ): void { + return this.originContext.setStrokeStyle(params, attribute, offsetX, offsetY, defaultParams); + } + setTextStyle(params: Partial, defaultParams?: ITextStyleParams, z?: number): void { + return this.originContext.setTextStyle(params, defaultParams, z); + } + setTextStyleWithoutAlignBaseline( + params: Partial, + defaultParams?: ITextStyleParams, + z?: number + ): void { + return this.originContext.setTextStyleWithoutAlignBaseline(params, defaultParams, z); + } + clearMatrix(setTransform?: boolean, dpr?: number): void { + return this.originContext.clearMatrix(setTransform, dpr); + } + setClearMatrix(a: number, b: number, c: number, d: number, e: number, f: number): void { + return this.originContext.setClearMatrix(a, b, c, d, e, f); + } + onlyTranslate(dpr?: number): boolean { + return this.originContext.onlyTranslate(dpr); + } + cloneMatrix(m: Matrix): Matrix { + return this.originContext.cloneMatrix(m); + } + draw(): void { + return this.originContext.draw(); + } + release(...params: any): void { + return this.originContext.release(...params); + } +} diff --git a/packages/vrender-kits/src/render/contributions/rough/rough-line.ts b/packages/vrender-kits/src/render/contributions/rough/rough-line.ts index eaf933ce8..02fe6ea48 100644 --- a/packages/vrender-kits/src/render/contributions/rough/rough-line.ts +++ b/packages/vrender-kits/src/render/contributions/rough/rough-line.ts @@ -7,163 +7,30 @@ import type { ISegPath2D, ILine, ILineGraphicAttribute, - IClipRangeByDimensionType + IClipRangeByDimensionType, + IDrawContext, + IGraphicRenderDrawParams, + IRenderService } from '@visactor/vrender-core'; -import { - IRenderService, - IGraphic, - DefaultCanvasLineRender, - getTheme, - CustomPath2D, - drawSegments, - injectable -} from '@visactor/vrender-core'; -import rough from 'roughjs'; -import { defaultRouthThemeSpec } from './config'; +import { DefaultCanvasLineRender, injectable, inject, LINE_NUMBER_TYPE } from '@visactor/vrender-core'; +import { RoughBaseRender } from './base-render'; @injectable() -export class RoughCanvasLineRender extends DefaultCanvasLineRender implements IGraphicRender { +export class RoughCanvasLineRender extends RoughBaseRender implements IGraphicRender { declare type: 'line'; declare numberType: number; style: 'rough' = 'rough'; - /** - * 绘制segment - * @param context - * @param cache - * @param fill - * @param stroke - * @param attribute - * @param defaultAttribute - * @param clipRange - * @param offsetX - * @param offsetY - * @param fillCb - * @param strokeCb - * @returns 返回true代表跳过后续绘制 - */ - protected drawSegmentItem( - context: IContext2d, - cache: ISegPath2D, - fill: boolean, - stroke: boolean, - fillOpacity: number, - strokeOpacity: number, - attribute: Partial, - defaultAttribute: Required | Partial[], - clipRange: number, - clipRangeByDimension: IClipRangeByDimensionType, - offsetX: number, - offsetY: number, - line: ILine, - fillCb?: ( - ctx: IContext2d, - lineAttribute: Partial, - themeAttribute: IThemeAttribute | IThemeAttribute[] - ) => boolean, - strokeCb?: ( - ctx: IContext2d, - lineAttribute: Partial, - themeAttribute: IThemeAttribute | IThemeAttribute[] - ) => boolean - ): boolean { - if (fillCb || strokeCb) { - return super.drawSegmentItem( - context, - cache, - fill, - stroke, - fillOpacity, - strokeOpacity, - attribute, - defaultAttribute, - clipRange, - clipRangeByDimension, - offsetX, - offsetY, - line, - fillCb, - strokeCb - ); - } - context.highPerformanceSave(); - // 获取到原生canvas - const canvas = context.canvas.nativeCanvas; - const rc = rough.canvas(canvas, {}); - - const customPath = new CustomPath2D(); - - drawSegments(context.camera ? context : context.nativeContext, cache, clipRange, clipRangeByDimension, { - offsetX, - offsetY - }); - const { - maxRandomnessOffset = defaultRouthThemeSpec.maxRandomnessOffset, - roughness = defaultRouthThemeSpec.roughness, - bowing = defaultRouthThemeSpec.bowing, - curveFitting = defaultRouthThemeSpec.curveFitting, - curveTightness = defaultRouthThemeSpec.curveTightness, - curveStepCount = defaultRouthThemeSpec.curveStepCount, - fillStyle = defaultRouthThemeSpec.fillStyle, - fillWeight = defaultRouthThemeSpec.fillWeight, - hachureAngle = defaultRouthThemeSpec.hachureAngle, - hachureGap = defaultRouthThemeSpec.hachureGap, - simplification = defaultRouthThemeSpec.simplification, - dashOffset = defaultRouthThemeSpec.dashOffset, - dashGap = defaultRouthThemeSpec.dashGap, - zigzagOffset = defaultRouthThemeSpec.zigzagOffset, - seed = defaultRouthThemeSpec.seed, - fillLineDash = defaultRouthThemeSpec.fillLineDash, - fillLineDashOffset = defaultRouthThemeSpec.fillLineDashOffset, - disableMultiStroke = defaultRouthThemeSpec.disableMultiStroke, - disableMultiStrokeFill = defaultRouthThemeSpec.disableMultiStrokeFill, - preserveVertices = defaultRouthThemeSpec.preserveVertices, - fixedDecimalPlaceDigits = defaultRouthThemeSpec.fixedDecimalPlaceDigits - } = attribute as any; - - let { fill: fillColor, stroke: strokeColor, lineWidth } = attribute; - - if (Array.isArray(defaultAttribute)) { - defaultAttribute.forEach(item => { - fillColor = fillColor ?? item.fill; - strokeColor = strokeColor ?? item.stroke; - lineWidth = lineWidth ?? item.lineWidth; - }); - } else { - fillColor = fillColor ?? defaultAttribute.fill; - strokeColor = strokeColor ?? defaultAttribute.stroke; - lineWidth = lineWidth ?? defaultAttribute.lineWidth; - } - - rc.path(customPath.toString(), { - fill: fill ? (fillColor as string) : undefined, - stroke: stroke ? (strokeColor as string) : undefined, - strokeWidth: lineWidth, - maxRandomnessOffset, - roughness, - bowing, - curveFitting, - curveTightness, - curveStepCount, - fillStyle, - fillWeight, - hachureAngle, - hachureGap, - simplification, - dashOffset, - dashGap, - zigzagOffset, - seed, - fillLineDash, - fillLineDashOffset, - disableMultiStroke, - disableMultiStrokeFill, - preserveVertices, - fixedDecimalPlaceDigits - }); - - context.highPerformanceRestore(); + constructor( + @inject(DefaultCanvasLineRender) + public readonly canvasRenderer: IGraphicRender + ) { + super(); + this.type = 'line'; + this.numberType = LINE_NUMBER_TYPE; + } - return false; + draw(line: ILine, renderService: IRenderService, drawContext: IDrawContext, params?: IGraphicRenderDrawParams) { + this.doDraw(line, renderService, drawContext, params); } } diff --git a/packages/vrender-kits/src/render/contributions/rough/rough-rect.ts b/packages/vrender-kits/src/render/contributions/rough/rough-rect.ts index 60b5397f8..a6c76cf1a 100644 --- a/packages/vrender-kits/src/render/contributions/rough/rough-rect.ts +++ b/packages/vrender-kits/src/render/contributions/rough/rough-rect.ts @@ -1,14 +1,20 @@ -import type { - IGraphicRender, - IRenderService, - IRect, - IDrawContext, - IGraphicRenderDrawParams +import { + type IGraphicRender, + type IRenderService, + type IRect, + type IDrawContext, + type IGraphicRenderDrawParams, + RECT_NUMBER_TYPE, + DefaultCanvasRectRender, + inject, + injectable, + CustomPath2D } from '@visactor/vrender-core'; -import { RECT_NUMBER_TYPE, DefaultCanvasRectRender, getTheme, inject, injectable } from '@visactor/vrender-core'; + import rough from 'roughjs'; import { defaultRouthThemeSpec } from './config'; import { RoughBaseRender } from './base-render'; +import { RoughContext2d } from './context'; @injectable() export class RoughCanvasRectRender extends RoughBaseRender implements IGraphicRender { @@ -26,95 +32,6 @@ export class RoughCanvasRectRender extends RoughBaseRender implements IGraphicRe } draw(rect: IRect, renderService: IRenderService, drawContext: IDrawContext, params?: IGraphicRenderDrawParams) { - const { context } = drawContext; - if (!context) { - return; - } - // 获取到原生canvas - const canvas = context.canvas.nativeCanvas; - const rc = rough.canvas(canvas); - - context.highPerformanceSave(); - - // const rectAttribute = graphicService.themeService.getCurrentTheme().rectAttribute; - const rectAttribute = rect.getGraphicTheme(); - let { x = rectAttribute.x, y = rectAttribute.y } = rect.attribute; - if (!rect.transMatrix.onlyTranslate()) { - // 性能较差 - x = 0; - y = 0; - context.transformFromMatrix(rect.transMatrix, true); - } else { - const { dx = rectAttribute.dx, dy = rectAttribute.dy } = rect.attribute; - x += dx; - y += dy; - // 当前context有rotate/scale,重置matrix - context.setTransformForCurrent(); - } - - const { - fill = rectAttribute.fill, - stroke = rectAttribute.stroke, - fillColor = rectAttribute.fill, - strokeColor = rectAttribute.stroke, - x1, - y1, - lineWidth = rectAttribute.lineWidth, - maxRandomnessOffset = defaultRouthThemeSpec.maxRandomnessOffset, - roughness = defaultRouthThemeSpec.roughness, - bowing = defaultRouthThemeSpec.bowing, - curveFitting = defaultRouthThemeSpec.curveFitting, - curveTightness = defaultRouthThemeSpec.curveTightness, - curveStepCount = defaultRouthThemeSpec.curveStepCount, - fillStyle = defaultRouthThemeSpec.fillStyle, - fillWeight = defaultRouthThemeSpec.fillWeight, - hachureAngle = defaultRouthThemeSpec.hachureAngle, - hachureGap = defaultRouthThemeSpec.hachureGap, - simplification = defaultRouthThemeSpec.simplification, - dashOffset = defaultRouthThemeSpec.dashOffset, - dashGap = defaultRouthThemeSpec.dashGap, - zigzagOffset = defaultRouthThemeSpec.zigzagOffset, - seed = defaultRouthThemeSpec.seed, - fillLineDash = defaultRouthThemeSpec.fillLineDash, - fillLineDashOffset = defaultRouthThemeSpec.fillLineDashOffset, - disableMultiStroke = defaultRouthThemeSpec.disableMultiStroke, - disableMultiStrokeFill = defaultRouthThemeSpec.disableMultiStrokeFill, - preserveVertices = defaultRouthThemeSpec.preserveVertices, - fixedDecimalPlaceDigits = defaultRouthThemeSpec.fixedDecimalPlaceDigits - } = rect.attribute as any; - - let { width = rectAttribute.width, height = rectAttribute.height } = rect.attribute; - - width = (width ?? x1 - x) || 0; - height = (height ?? y1 - y) || 0; - - rc.rectangle(x, y, width, height, { - fill: fill ? (fillColor as string) : undefined, - stroke: stroke ? (strokeColor as string) : undefined, - strokeWidth: lineWidth, - maxRandomnessOffset, - roughness, - bowing, - curveFitting, - curveTightness, - curveStepCount, - fillStyle, - fillWeight, - hachureAngle, - hachureGap, - simplification, - dashOffset, - dashGap, - zigzagOffset, - seed, - fillLineDash, - fillLineDashOffset, - disableMultiStroke, - disableMultiStrokeFill, - preserveVertices, - fixedDecimalPlaceDigits - }); - - context.highPerformanceRestore(); + this.doDraw(rect, renderService, drawContext, params); } } diff --git a/packages/vrender-kits/src/render/contributions/rough/rough-symbol.ts b/packages/vrender-kits/src/render/contributions/rough/rough-symbol.ts index 9950db09b..f338efe17 100644 --- a/packages/vrender-kits/src/render/contributions/rough/rough-symbol.ts +++ b/packages/vrender-kits/src/render/contributions/rough/rough-symbol.ts @@ -21,9 +21,11 @@ import { } from '@visactor/vrender-core'; import rough from 'roughjs'; import { defaultRouthThemeSpec } from './config'; +import { RoughContext2d } from './context'; +import { RoughBaseRender } from './base-render'; @injectable() -export class RoughCanvasSymbolRender extends BaseRender implements IGraphicRender { +export class RoughCanvasSymbolRender extends RoughBaseRender implements IGraphicRender { type: 'symbol'; numberType: number; style: 'rough'; @@ -39,116 +41,6 @@ export class RoughCanvasSymbolRender extends BaseRender implements IGra } draw(symbol: ISymbol, renderService: IRenderService, drawContext: IDrawContext, params?: IGraphicRenderDrawParams) { - const { context } = drawContext; - if (!context) { - return; - } - // 获取到原生canvas - const canvas = context.canvas.nativeCanvas; - const rc = rough.canvas(canvas); - - context.highPerformanceSave(); - const symbolAttribute = symbol.getGraphicTheme(); - const data = this.transform(symbol, symbolAttribute, context); - const { x, y, z, lastModelMatrix } = data; - - const parsedPath = symbol.getParsedPath(); - // todo: 考虑使用path - if (!parsedPath) { - return; - } - - const { - fill = symbolAttribute.fill, - stroke = symbolAttribute.stroke, - fillColor = symbolAttribute.fill, - strokeColor = symbolAttribute.stroke, - size = symbolAttribute.size, - lineWidth = symbolAttribute.lineWidth, - maxRandomnessOffset = defaultRouthThemeSpec.maxRandomnessOffset, - roughness = defaultRouthThemeSpec.roughness, - bowing = defaultRouthThemeSpec.bowing, - curveFitting = defaultRouthThemeSpec.curveFitting, - curveTightness = defaultRouthThemeSpec.curveTightness, - curveStepCount = defaultRouthThemeSpec.curveStepCount, - fillStyle = defaultRouthThemeSpec.fillStyle, - fillWeight = defaultRouthThemeSpec.fillWeight, - hachureAngle = defaultRouthThemeSpec.hachureAngle, - hachureGap = defaultRouthThemeSpec.hachureGap, - simplification = defaultRouthThemeSpec.simplification, - dashOffset = defaultRouthThemeSpec.dashOffset, - dashGap = defaultRouthThemeSpec.dashGap, - zigzagOffset = defaultRouthThemeSpec.zigzagOffset, - seed = defaultRouthThemeSpec.seed, - fillLineDash = defaultRouthThemeSpec.fillLineDash, - fillLineDashOffset = defaultRouthThemeSpec.fillLineDashOffset, - disableMultiStroke = defaultRouthThemeSpec.disableMultiStroke, - disableMultiStrokeFill = defaultRouthThemeSpec.disableMultiStrokeFill, - preserveVertices = defaultRouthThemeSpec.preserveVertices, - fixedDecimalPlaceDigits = defaultRouthThemeSpec.fixedDecimalPlaceDigits - } = symbol.attribute as any; - - let svgPath = ''; - if (parsedPath.drawToSvgPath) { - svgPath = parsedPath.drawToSvgPath(size, x, y); - } else { - const customPath = new CustomPath2D(); - if (parsedPath.draw(customPath, size, x, y)) { - customPath.closePath(); - } - svgPath = customPath.toString(); - } - - rc.path(svgPath, { - fill: fill ? (fillColor as string) : undefined, - stroke: stroke ? (strokeColor as string) : undefined, - strokeWidth: lineWidth, - maxRandomnessOffset, - roughness, - bowing, - curveFitting, - curveTightness, - curveStepCount, - fillStyle, - fillWeight, - hachureAngle, - hachureGap, - simplification, - dashOffset, - dashGap, - zigzagOffset, - seed, - fillLineDash, - fillLineDashOffset, - disableMultiStroke, - disableMultiStrokeFill, - preserveVertices, - fixedDecimalPlaceDigits - }); - - context.highPerformanceRestore(); - } - - drawShape( - graphic: IGraphic, - ctx: IContext2d, - x: number, - y: number, - drawContext: IDrawContext, - params?: IGraphicRenderDrawParams, - fillCb?: ( - ctx: IContext2d, - markAttribute: Partial, - themeAttribute: IThemeAttribute - ) => boolean, - strokeCb?: ( - ctx: IContext2d, - markAttribute: Partial, - themeAttribute: IThemeAttribute - ) => boolean - ): void { - if (this.canvasRenderer.drawShape) { - return this.canvasRenderer.drawShape(graphic, ctx, x, y, drawContext, params, fillCb, strokeCb); - } + this.doDraw(symbol, renderService, drawContext, params); } } diff --git a/packages/vrender/__tests__/browser/src/pages/line.ts b/packages/vrender/__tests__/browser/src/pages/line.ts index 2219418ab..3f88041d9 100644 --- a/packages/vrender/__tests__/browser/src/pages/line.ts +++ b/packages/vrender/__tests__/browser/src/pages/line.ts @@ -3,7 +3,7 @@ import { roughModule } from '@visactor/vrender-kits'; import { addShapesToStage, colorPools } from '../utils'; import { createSymbol } from '@visactor/vrender'; -// container.load(roughModule); +container.load(roughModule); const subP1 = [ [0, 100], @@ -174,6 +174,7 @@ export const page = () => { x: ((i * 300) % 900) + 100, y: Math.floor((i * 300) / 900) * 200, closePath: true, + renderStyle: 'rough', // segments: [ // { // points: subP1, diff --git a/packages/vrender/__tests__/browser/src/pages/rect.ts b/packages/vrender/__tests__/browser/src/pages/rect.ts index 53682c067..01a7284f1 100644 --- a/packages/vrender/__tests__/browser/src/pages/rect.ts +++ b/packages/vrender/__tests__/browser/src/pages/rect.ts @@ -1,8 +1,7 @@ -import { createStage, createRect, IGraphic, createGroup, createSymbol } from '@visactor/vrender'; +import { createStage, container, createRect, IGraphic, createGroup, createSymbol } from '@visactor/vrender'; import { roughModule } from '@visactor/vrender-kits'; -import { addShapesToStage, colorPools } from '../utils'; -// container.load(roughModule); +container.load(roughModule); export const page = () => { const graphics: IGraphic[] = []; // graphics.push( @@ -60,13 +59,14 @@ export const page = () => { stroke: 'red', // scaleCenter: ['50%', '50%'], // _debug_bounds: true, - fill: 'conic-gradient(from 90deg, rgba(5,0,255,1) 16%, rgba(0,255,10,1) 41%, rgba(9,9,121,1) 53%, rgba(0,212,255,1) 100%)', + fill: 'red', // cornerRadius: [5, 10, 15, 20], - lineWidth: 5, + lineWidth: 2, anchor: ['50%', '50%'], // anchor: [400, 200], lineDash: [100, 10], - lineDashOffset: -100 + lineDashOffset: -100, + renderStyle: 'rough' }); const star = createSymbol({ x: 300, @@ -75,7 +75,7 @@ export const page = () => { scaleY: 2, angle: 30, size: 100, - symbolType: 'square', + symbolType: 'star', // cornerRadius: [0, 10, 10, 0], stroke: 'red', // scaleCenter: ['50%', '50%'], @@ -87,7 +87,8 @@ export const page = () => { anchor: ['50%', '50%'], // anchor: [400, 200], lineDash: [100, 10], - lineDashOffset: -100 + lineDashOffset: -100, + renderStyle: 'rough' }); const group = createGroup({ From dd07bdcedaed02c485ddad9813956048a2e40933 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 21 Apr 2025 02:52:19 +0000 Subject: [PATCH 028/179] build: prelease version 0.22.10 --- ...olbar-preventDefault_2025-04-18-09-42.json | 11 -------- ...olbar-preventDefault_2025-04-18-09-44.json | 11 -------- ...ical-gradient-symbol_2025-04-18-10-10.json | 10 ------- ...olbar-preventDefault_2025-04-18-09-42.json | 11 -------- ...olbar-preventDefault_2025-04-18-09-44.json | 11 -------- ...ical-gradient-symbol_2025-04-18-10-10.json | 10 ------- common/config/rush/pnpm-lock.yaml | 26 +++++++++---------- common/config/rush/version-policies.json | 2 +- docs/package.json | 2 +- packages/react-vrender-utils/CHANGELOG.json | 6 +++++ packages/react-vrender-utils/CHANGELOG.md | 7 ++++- packages/react-vrender-utils/package.json | 6 ++--- packages/react-vrender/CHANGELOG.json | 6 +++++ packages/react-vrender/CHANGELOG.md | 7 ++++- packages/react-vrender/package.json | 4 +-- packages/vrender-components/CHANGELOG.json | 15 +++++++++++ packages/vrender-components/CHANGELOG.md | 14 +++++++++- packages/vrender-components/package.json | 6 ++--- packages/vrender-core/CHANGELOG.json | 18 +++++++++++++ packages/vrender-core/CHANGELOG.md | 15 ++++++++++- packages/vrender-core/package.json | 2 +- packages/vrender-kits/CHANGELOG.json | 12 +++++++++ packages/vrender-kits/CHANGELOG.md | 9 ++++++- packages/vrender-kits/package.json | 4 +-- packages/vrender/CHANGELOG.json | 6 +++++ packages/vrender/CHANGELOG.md | 7 ++++- packages/vrender/package.json | 6 ++--- tools/bugserver-trigger/package.json | 8 +++--- 28 files changed, 149 insertions(+), 103 deletions(-) delete mode 100644 common/changes/@visactor/vrender-components/fix-scrolbar-preventDefault_2025-04-18-09-42.json delete mode 100644 common/changes/@visactor/vrender-components/fix-scrolbar-preventDefault_2025-04-18-09-44.json delete mode 100644 common/changes/@visactor/vrender-core/fix-conical-gradient-symbol_2025-04-18-10-10.json delete mode 100644 common/changes/@visactor/vrender-core/fix-scrolbar-preventDefault_2025-04-18-09-42.json delete mode 100644 common/changes/@visactor/vrender-core/fix-scrolbar-preventDefault_2025-04-18-09-44.json delete mode 100644 common/changes/@visactor/vrender-kits/fix-conical-gradient-symbol_2025-04-18-10-10.json diff --git a/common/changes/@visactor/vrender-components/fix-scrolbar-preventDefault_2025-04-18-09-42.json b/common/changes/@visactor/vrender-components/fix-scrolbar-preventDefault_2025-04-18-09-42.json deleted file mode 100644 index 65e5ce5bf..000000000 --- a/common/changes/@visactor/vrender-components/fix-scrolbar-preventDefault_2025-04-18-09-42.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "changes": [ - { - "comment": "fix: fix preventDefault() error when passive: true\n\n", - "type": "none", - "packageName": "@visactor/vrender-components" - } - ], - "packageName": "@visactor/vrender-components", - "email": "dingling112@gmail.com" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-components/fix-scrolbar-preventDefault_2025-04-18-09-44.json b/common/changes/@visactor/vrender-components/fix-scrolbar-preventDefault_2025-04-18-09-44.json deleted file mode 100644 index 2bcf645ad..000000000 --- a/common/changes/@visactor/vrender-components/fix-scrolbar-preventDefault_2025-04-18-09-44.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "changes": [ - { - "comment": "docs: update changlog of rush\n\n", - "type": "none", - "packageName": "@visactor/vrender-components" - } - ], - "packageName": "@visactor/vrender-components", - "email": "dingling112@gmail.com" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-core/fix-conical-gradient-symbol_2025-04-18-10-10.json b/common/changes/@visactor/vrender-core/fix-conical-gradient-symbol_2025-04-18-10-10.json deleted file mode 100644 index 658e4c7e7..000000000 --- a/common/changes/@visactor/vrender-core/fix-conical-gradient-symbol_2025-04-18-10-10.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender-core", - "comment": "fix: fix issue with symbol gradient, and support native conical gradient", - "type": "none" - } - ], - "packageName": "@visactor/vrender-core" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-core/fix-scrolbar-preventDefault_2025-04-18-09-42.json b/common/changes/@visactor/vrender-core/fix-scrolbar-preventDefault_2025-04-18-09-42.json deleted file mode 100644 index fcc8b100b..000000000 --- a/common/changes/@visactor/vrender-core/fix-scrolbar-preventDefault_2025-04-18-09-42.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "changes": [ - { - "comment": "fix: fix preventDefault() error when passive: true\n\n", - "type": "none", - "packageName": "@visactor/vrender-core" - } - ], - "packageName": "@visactor/vrender-core", - "email": "dingling112@gmail.com" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-core/fix-scrolbar-preventDefault_2025-04-18-09-44.json b/common/changes/@visactor/vrender-core/fix-scrolbar-preventDefault_2025-04-18-09-44.json deleted file mode 100644 index adb7f8118..000000000 --- a/common/changes/@visactor/vrender-core/fix-scrolbar-preventDefault_2025-04-18-09-44.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "changes": [ - { - "comment": "docs: update changlog of rush\n\n", - "type": "none", - "packageName": "@visactor/vrender-core" - } - ], - "packageName": "@visactor/vrender-core", - "email": "dingling112@gmail.com" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-kits/fix-conical-gradient-symbol_2025-04-18-10-10.json b/common/changes/@visactor/vrender-kits/fix-conical-gradient-symbol_2025-04-18-10-10.json deleted file mode 100644 index d4279c568..000000000 --- a/common/changes/@visactor/vrender-kits/fix-conical-gradient-symbol_2025-04-18-10-10.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender-kits", - "comment": "fix: fix issue with symbol gradient, and support native conical gradient", - "type": "none" - } - ], - "packageName": "@visactor/vrender-kits" -} \ No newline at end of file diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 96fbf6dfe..9da7a082f 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: specifier: ~0.5.7 version: 0.5.7 '@visactor/vrender': - specifier: workspace:0.22.9 + specifier: workspace:0.22.10 version: link:../packages/vrender '@visactor/vutils': specifier: ~0.19.5 @@ -95,7 +95,7 @@ importers: ../../packages/react-vrender: dependencies: '@visactor/vrender': - specifier: workspace:0.22.9 + specifier: workspace:0.22.10 version: link:../vrender '@visactor/vutils': specifier: ~0.19.5 @@ -153,10 +153,10 @@ importers: ../../packages/react-vrender-utils: dependencies: '@visactor/react-vrender': - specifier: workspace:0.22.9 + specifier: workspace:0.22.10 version: link:../react-vrender '@visactor/vrender': - specifier: workspace:0.22.9 + specifier: workspace:0.22.10 version: link:../vrender '@visactor/vutils': specifier: ~0.19.5 @@ -211,10 +211,10 @@ importers: ../../packages/vrender: dependencies: '@visactor/vrender-core': - specifier: workspace:0.22.9 + specifier: workspace:0.22.10 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:0.22.9 + specifier: workspace:0.22.10 version: link:../vrender-kits devDependencies: '@internal/bundler': @@ -281,10 +281,10 @@ importers: ../../packages/vrender-components: dependencies: '@visactor/vrender-core': - specifier: workspace:0.22.9 + specifier: workspace:0.22.10 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:0.22.9 + specifier: workspace:0.22.10 version: link:../vrender-kits '@visactor/vscale': specifier: ~0.19.5 @@ -403,7 +403,7 @@ importers: specifier: 2.4.1 version: 2.4.1 '@visactor/vrender-core': - specifier: workspace:0.22.9 + specifier: workspace:0.22.10 version: link:../vrender-core '@visactor/vutils': specifier: ~0.19.5 @@ -519,16 +519,16 @@ importers: ../../tools/bugserver-trigger: dependencies: '@visactor/vrender': - specifier: workspace:0.22.9 + specifier: workspace:0.22.10 version: link:../../packages/vrender '@visactor/vrender-components': - specifier: workspace:0.22.9 + specifier: workspace:0.22.10 version: link:../../packages/vrender-components '@visactor/vrender-core': - specifier: workspace:0.22.9 + specifier: workspace:0.22.10 version: link:../../packages/vrender-core '@visactor/vrender-kits': - specifier: workspace:0.22.9 + specifier: workspace:0.22.10 version: link:../../packages/vrender-kits devDependencies: '@internal/bundler': diff --git a/common/config/rush/version-policies.json b/common/config/rush/version-policies.json index 80b2a6aa6..24769dfed 100644 --- a/common/config/rush/version-policies.json +++ b/common/config/rush/version-policies.json @@ -1 +1 @@ -[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"0.22.9","nextBump":"patch"}] +[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"0.22.10","nextBump":"patch"}] diff --git a/docs/package.json b/docs/package.json index 6676750b8..9867285a2 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,7 +13,7 @@ "@visactor/vchart": "1.3.0", "@visactor/vutils": "~0.19.5", "@visactor/vgrammar": "~0.5.7", - "@visactor/vrender": "workspace:0.22.9", + "@visactor/vrender": "workspace:0.22.10", "markdown-it": "^13.0.0", "highlight.js": "^11.8.0", "axios": "^1.4.0", diff --git a/packages/react-vrender-utils/CHANGELOG.json b/packages/react-vrender-utils/CHANGELOG.json index cce0637e0..ab02c64b8 100644 --- a/packages/react-vrender-utils/CHANGELOG.json +++ b/packages/react-vrender-utils/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/react-vrender-utils", "entries": [ + { + "version": "0.22.10", + "tag": "@visactor/react-vrender-utils_v0.22.10", + "date": "Mon, 21 Apr 2025 02:48:16 GMT", + "comments": {} + }, { "version": "0.22.9", "tag": "@visactor/react-vrender-utils_v0.22.9", diff --git a/packages/react-vrender-utils/CHANGELOG.md b/packages/react-vrender-utils/CHANGELOG.md index 2766bbab5..877733226 100644 --- a/packages/react-vrender-utils/CHANGELOG.md +++ b/packages/react-vrender-utils/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/react-vrender-utils -This log was last generated on Fri, 18 Apr 2025 06:59:46 GMT and should not be manually modified. +This log was last generated on Mon, 21 Apr 2025 02:48:16 GMT and should not be manually modified. + +## 0.22.10 +Mon, 21 Apr 2025 02:48:16 GMT + +_Version update only_ ## 0.22.9 Fri, 18 Apr 2025 06:59:46 GMT diff --git a/packages/react-vrender-utils/package.json b/packages/react-vrender-utils/package.json index 9bfd32b00..e5ee46d87 100644 --- a/packages/react-vrender-utils/package.json +++ b/packages/react-vrender-utils/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender-utils", - "version": "0.22.9", + "version": "0.22.10", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -24,8 +24,8 @@ "react-dom": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:0.22.9", - "@visactor/react-vrender": "workspace:0.22.9", + "@visactor/vrender": "workspace:0.22.10", + "@visactor/react-vrender": "workspace:0.22.10", "@visactor/vutils": "~0.19.5", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/react-vrender/CHANGELOG.json b/packages/react-vrender/CHANGELOG.json index 18480de5c..e784078db 100644 --- a/packages/react-vrender/CHANGELOG.json +++ b/packages/react-vrender/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/react-vrender", "entries": [ + { + "version": "0.22.10", + "tag": "@visactor/react-vrender_v0.22.10", + "date": "Mon, 21 Apr 2025 02:48:16 GMT", + "comments": {} + }, { "version": "0.22.9", "tag": "@visactor/react-vrender_v0.22.9", diff --git a/packages/react-vrender/CHANGELOG.md b/packages/react-vrender/CHANGELOG.md index 4ac8872bc..ac8e4dbb1 100644 --- a/packages/react-vrender/CHANGELOG.md +++ b/packages/react-vrender/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/react-vrender -This log was last generated on Fri, 18 Apr 2025 06:59:46 GMT and should not be manually modified. +This log was last generated on Mon, 21 Apr 2025 02:48:16 GMT and should not be manually modified. + +## 0.22.10 +Mon, 21 Apr 2025 02:48:16 GMT + +_Version update only_ ## 0.22.9 Fri, 18 Apr 2025 06:59:46 GMT diff --git a/packages/react-vrender/package.json b/packages/react-vrender/package.json index def92138a..4741b0619 100644 --- a/packages/react-vrender/package.json +++ b/packages/react-vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender", - "version": "0.22.9", + "version": "0.22.10", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -23,7 +23,7 @@ "react": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:0.22.9", + "@visactor/vrender": "workspace:0.22.10", "@visactor/vutils": "~0.19.5", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/vrender-components/CHANGELOG.json b/packages/vrender-components/CHANGELOG.json index d852d23ba..7c3e3516e 100644 --- a/packages/vrender-components/CHANGELOG.json +++ b/packages/vrender-components/CHANGELOG.json @@ -1,6 +1,21 @@ { "name": "@visactor/vrender-components", "entries": [ + { + "version": "0.22.10", + "tag": "@visactor/vrender-components_v0.22.10", + "date": "Mon, 21 Apr 2025 02:48:16 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix preventDefault() error when passive: true\n\n" + }, + { + "comment": "docs: update changlog of rush\n\n" + } + ] + } + }, { "version": "0.22.9", "tag": "@visactor/vrender-components_v0.22.9", diff --git a/packages/vrender-components/CHANGELOG.md b/packages/vrender-components/CHANGELOG.md index 3901d7053..4fa9d4762 100644 --- a/packages/vrender-components/CHANGELOG.md +++ b/packages/vrender-components/CHANGELOG.md @@ -1,6 +1,18 @@ # Change Log - @visactor/vrender-components -This log was last generated on Fri, 18 Apr 2025 06:59:46 GMT and should not be manually modified. +This log was last generated on Mon, 21 Apr 2025 02:48:16 GMT and should not be manually modified. + +## 0.22.10 +Mon, 21 Apr 2025 02:48:16 GMT + +### Updates + +- fix: fix preventDefault() error when passive: true + + +- docs: update changlog of rush + + ## 0.22.9 Fri, 18 Apr 2025 06:59:46 GMT diff --git a/packages/vrender-components/package.json b/packages/vrender-components/package.json index e594a0919..60831aa7b 100644 --- a/packages/vrender-components/package.json +++ b/packages/vrender-components/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-components", - "version": "0.22.9", + "version": "0.22.10", "description": "components library for dp visualization", "sideEffects": false, "main": "cjs/index.js", @@ -27,8 +27,8 @@ "dependencies": { "@visactor/vutils": "~0.19.5", "@visactor/vscale": "~0.19.5", - "@visactor/vrender-core": "workspace:0.22.9", - "@visactor/vrender-kits": "workspace:0.22.9" + "@visactor/vrender-core": "workspace:0.22.10", + "@visactor/vrender-kits": "workspace:0.22.10" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-core/CHANGELOG.json b/packages/vrender-core/CHANGELOG.json index 7bc520928..837f526fd 100644 --- a/packages/vrender-core/CHANGELOG.json +++ b/packages/vrender-core/CHANGELOG.json @@ -1,6 +1,24 @@ { "name": "@visactor/vrender-core", "entries": [ + { + "version": "0.22.10", + "tag": "@visactor/vrender-core_v0.22.10", + "date": "Mon, 21 Apr 2025 02:48:16 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix issue with symbol gradient, and support native conical gradient" + }, + { + "comment": "fix: fix preventDefault() error when passive: true\n\n" + }, + { + "comment": "docs: update changlog of rush\n\n" + } + ] + } + }, { "version": "0.22.9", "tag": "@visactor/vrender-core_v0.22.9", diff --git a/packages/vrender-core/CHANGELOG.md b/packages/vrender-core/CHANGELOG.md index c0e34d7fc..49c1c6b2a 100644 --- a/packages/vrender-core/CHANGELOG.md +++ b/packages/vrender-core/CHANGELOG.md @@ -1,6 +1,19 @@ # Change Log - @visactor/vrender-core -This log was last generated on Fri, 18 Apr 2025 06:59:46 GMT and should not be manually modified. +This log was last generated on Mon, 21 Apr 2025 02:48:16 GMT and should not be manually modified. + +## 0.22.10 +Mon, 21 Apr 2025 02:48:16 GMT + +### Updates + +- fix: fix issue with symbol gradient, and support native conical gradient +- fix: fix preventDefault() error when passive: true + + +- docs: update changlog of rush + + ## 0.22.9 Fri, 18 Apr 2025 06:59:46 GMT diff --git a/packages/vrender-core/package.json b/packages/vrender-core/package.json index af080b288..605bec07d 100644 --- a/packages/vrender-core/package.json +++ b/packages/vrender-core/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-core", - "version": "0.22.9", + "version": "0.22.10", "description": "", "sideEffects": [ "./src/modules.ts", diff --git a/packages/vrender-kits/CHANGELOG.json b/packages/vrender-kits/CHANGELOG.json index 17d95376b..4546344ae 100644 --- a/packages/vrender-kits/CHANGELOG.json +++ b/packages/vrender-kits/CHANGELOG.json @@ -1,6 +1,18 @@ { "name": "@visactor/vrender-kits", "entries": [ + { + "version": "0.22.10", + "tag": "@visactor/vrender-kits_v0.22.10", + "date": "Mon, 21 Apr 2025 02:48:16 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix issue with symbol gradient, and support native conical gradient" + } + ] + } + }, { "version": "0.22.9", "tag": "@visactor/vrender-kits_v0.22.9", diff --git a/packages/vrender-kits/CHANGELOG.md b/packages/vrender-kits/CHANGELOG.md index fa21475b8..b3df28c49 100644 --- a/packages/vrender-kits/CHANGELOG.md +++ b/packages/vrender-kits/CHANGELOG.md @@ -1,6 +1,13 @@ # Change Log - @visactor/vrender-kits -This log was last generated on Fri, 18 Apr 2025 06:59:46 GMT and should not be manually modified. +This log was last generated on Mon, 21 Apr 2025 02:48:16 GMT and should not be manually modified. + +## 0.22.10 +Mon, 21 Apr 2025 02:48:16 GMT + +### Updates + +- fix: fix issue with symbol gradient, and support native conical gradient ## 0.22.9 Fri, 18 Apr 2025 06:59:46 GMT diff --git a/packages/vrender-kits/package.json b/packages/vrender-kits/package.json index b1578c435..7424ad503 100644 --- a/packages/vrender-kits/package.json +++ b/packages/vrender-kits/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-kits", - "version": "0.22.9", + "version": "0.22.10", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -21,7 +21,7 @@ }, "dependencies": { "@visactor/vutils": "~0.19.5", - "@visactor/vrender-core": "workspace:0.22.9", + "@visactor/vrender-core": "workspace:0.22.10", "@resvg/resvg-js": "2.4.1", "roughjs": "4.5.2", "gifuct-js": "2.1.2", diff --git a/packages/vrender/CHANGELOG.json b/packages/vrender/CHANGELOG.json index 1c7a9f390..32f8ba4d6 100644 --- a/packages/vrender/CHANGELOG.json +++ b/packages/vrender/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender", "entries": [ + { + "version": "0.22.10", + "tag": "@visactor/vrender_v0.22.10", + "date": "Mon, 21 Apr 2025 02:48:16 GMT", + "comments": {} + }, { "version": "0.22.9", "tag": "@visactor/vrender_v0.22.9", diff --git a/packages/vrender/CHANGELOG.md b/packages/vrender/CHANGELOG.md index 588345246..44bfb60cf 100644 --- a/packages/vrender/CHANGELOG.md +++ b/packages/vrender/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender -This log was last generated on Fri, 18 Apr 2025 06:59:46 GMT and should not be manually modified. +This log was last generated on Mon, 21 Apr 2025 02:48:16 GMT and should not be manually modified. + +## 0.22.10 +Mon, 21 Apr 2025 02:48:16 GMT + +_Version update only_ ## 0.22.9 Fri, 18 Apr 2025 06:59:46 GMT diff --git a/packages/vrender/package.json b/packages/vrender/package.json index 83380e51b..7a442588b 100644 --- a/packages/vrender/package.json +++ b/packages/vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender", - "version": "0.22.9", + "version": "0.22.10", "description": "", "sideEffects": true, "main": "cjs/index.js", @@ -24,8 +24,8 @@ "test-watch": "cross-env DEBUG_MODE=1 jest --watch" }, "dependencies": { - "@visactor/vrender-core": "workspace:0.22.9", - "@visactor/vrender-kits": "workspace:0.22.9" + "@visactor/vrender-core": "workspace:0.22.10", + "@visactor/vrender-kits": "workspace:0.22.10" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/tools/bugserver-trigger/package.json b/tools/bugserver-trigger/package.json index 7390b2f15..68c6b42e9 100644 --- a/tools/bugserver-trigger/package.json +++ b/tools/bugserver-trigger/package.json @@ -8,10 +8,10 @@ "ci": "ts-node --transpileOnly --skipProject ./scripts/trigger-test.ts" }, "dependencies": { - "@visactor/vrender": "workspace:0.22.9", - "@visactor/vrender-core": "workspace:0.22.9", - "@visactor/vrender-kits": "workspace:0.22.9", - "@visactor/vrender-components": "workspace:0.22.9" + "@visactor/vrender": "workspace:0.22.10", + "@visactor/vrender-core": "workspace:0.22.10", + "@visactor/vrender-kits": "workspace:0.22.10", + "@visactor/vrender-components": "workspace:0.22.10" }, "devDependencies": { "@rushstack/eslint-patch": "~1.1.4", From 35f1acf6163614b93f60a7a822a324cdd5355a77 Mon Sep 17 00:00:00 2001 From: neuqzxy Date: Mon, 21 Apr 2025 03:34:29 +0000 Subject: [PATCH 029/179] docs: generate changelog of release v0.22.10 --- docs/assets/changelog/en/changelog.md | 22 ++++++++++++++++++++++ docs/assets/changelog/zh/changelog.md | 22 ++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/docs/assets/changelog/en/changelog.md b/docs/assets/changelog/en/changelog.md index e15135ddb..1d3a91d51 100644 --- a/docs/assets/changelog/en/changelog.md +++ b/docs/assets/changelog/en/changelog.md @@ -1,3 +1,25 @@ +# v0.22.10 + +2025-04-21 + + +**🐛 Bug fix** + +- **@visactor/vrender-components**: fix preventDefault() error when passive: true +- **@visactor/vrender-kits**: fix issue with symbol gradient, and support native conical gradient +- **@visactor/vrender-core**: fix issue with symbol gradient, and support native conical gradient +- **@visactor/vrender-core**: fix preventDefault() error when passive: true + +**📖 Site / documentation update** + +- **@visactor/vrender-components**: update changlog of rush +- **@visactor/vrender-core**: update changlog of rush + + +**Full Changelog**: https://github.com/VisActor/VRender/compare/v0.22.9...v0.22.10 + +[more detail about v0.22.10](https://github.com/VisActor/VRender/releases/tag/v0.22.10) + # v0.22.9 2025-04-18 diff --git a/docs/assets/changelog/zh/changelog.md b/docs/assets/changelog/zh/changelog.md index e6ed0693b..c6a42adcc 100644 --- a/docs/assets/changelog/zh/changelog.md +++ b/docs/assets/changelog/zh/changelog.md @@ -1,3 +1,25 @@ +# v0.22.10 + +2025-04-21 + + +**🐛 功能修复** + +- **@visactor/vrender-components**: fix preventDefault() error when passive: true +- **@visactor/vrender-kits**: fix issue with symbol gradient, and support native conical gradient +- **@visactor/vrender-core**: fix issue with symbol gradient, and support native conical gradient +- **@visactor/vrender-core**: fix preventDefault() error when passive: true + +**📖 文档更新** + +- **@visactor/vrender-components**: update changlog of rush +- **@visactor/vrender-core**: update changlog of rush + + +**Full Changelog**: https://github.com/VisActor/VRender/compare/v0.22.9...v0.22.10 + +[更多详情请查看 v0.22.10](https://github.com/VisActor/VRender/releases/tag/v0.22.10) + # v0.22.9 2025-04-18 From 4a4726f15f4a83da5b088bbcf43abfbe4753b6e2 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 12 Mar 2025 19:20:31 +0800 Subject: [PATCH 030/179] feat: add animate base code --- common/config/rush/pnpm-lock.yaml | 61 + packages/vrender-animate/.eslintrc.js | 7 + packages/vrender-animate/CHANGELOG.json | 1458 +++++++++++++++++ packages/vrender-animate/CHANGELOG.md | 941 +++++++++++ packages/vrender-animate/README.md | 177 ++ packages/vrender-animate/bundler.config.js | 13 + .../cross-env DEBUG='Bundler*' bundle | 73 + packages/vrender-animate/package.json | 72 + packages/vrender-animate/src/index.ts | 0 .../vrender-animate/src/intreface/animate.ts | 145 ++ .../vrender-animate/src/intreface/easing.ts | 41 + .../vrender-animate/src/intreface/timeline.ts | 39 + .../vrender-animate/src/intreface/type.ts | 15 + packages/vrender-animate/tsconfig.eslint.json | 16 + packages/vrender-animate/tsconfig.json | 16 + packages/vrender-animate/vite/index.html | 13 + packages/vrender-animate/vite/public/vite.svg | 1 + packages/vrender-animate/vite/src/App.css | 42 + packages/vrender-animate/vite/src/App.tsx | 31 + .../vrender-animate/vite/src/assets/react.svg | 1 + packages/vrender-animate/vite/src/index.css | 69 + packages/vrender-animate/vite/src/main.tsx | 11 + .../vrender-animate/vite/src/vite-env.d.ts | 1 + packages/vrender-animate/vite/tsconfig.json | 32 + .../vrender-animate/vite/tsconfig.node.json | 10 + packages/vrender-animate/vite/vite.config.ts | 7 + .../src/animate/Ticker/default-ticker.ts | 246 --- .../vrender-core/src/animate/Ticker/index.ts | 5 - .../animate/Ticker/manual-ticker-handler.ts | 35 - .../src/animate/Ticker/manual-ticker.ts | 54 - .../src/animate/Ticker/raf-tick-handler.ts | 30 - .../animate/Ticker/timeout-tick-handler.ts | 29 - .../vrender-core/src/animate/Ticker/type.ts | 7 - packages/vrender-core/src/animate/animate.ts | 1308 --------------- packages/vrender-core/src/animate/config.ts | 11 - .../src/animate/custom-animate.ts | 1364 --------------- .../src/animate/default-ticker.ts | 7 - .../vrender-core/src/animate/easing-func.ts | 12 - packages/vrender-core/src/animate/easing.ts | 273 --- .../vrender-core/src/animate/group-fade.ts | 70 - packages/vrender-core/src/animate/index.ts | 8 - packages/vrender-core/src/animate/morphing.ts | 680 -------- packages/vrender-core/src/animate/timeline.ts | 102 -- .../src/common/performance-raf.ts | 30 + packages/vrender-core/src/core/stage.ts | 4 +- packages/vrender-core/src/index.ts | 1 + packages/vrender-core/src/interface/stage.ts | 4 + .../richtext-edit-plugin-old.ts | 671 -------- packages/vrender/package.json | 3 +- rush.json | 7 + 50 files changed, 3339 insertions(+), 4914 deletions(-) create mode 100644 packages/vrender-animate/.eslintrc.js create mode 100644 packages/vrender-animate/CHANGELOG.json create mode 100644 packages/vrender-animate/CHANGELOG.md create mode 100644 packages/vrender-animate/README.md create mode 100644 packages/vrender-animate/bundler.config.js create mode 100644 packages/vrender-animate/cross-env DEBUG='Bundler*' bundle create mode 100644 packages/vrender-animate/package.json create mode 100644 packages/vrender-animate/src/index.ts create mode 100644 packages/vrender-animate/src/intreface/animate.ts create mode 100644 packages/vrender-animate/src/intreface/easing.ts create mode 100644 packages/vrender-animate/src/intreface/timeline.ts create mode 100644 packages/vrender-animate/src/intreface/type.ts create mode 100644 packages/vrender-animate/tsconfig.eslint.json create mode 100644 packages/vrender-animate/tsconfig.json create mode 100644 packages/vrender-animate/vite/index.html create mode 100644 packages/vrender-animate/vite/public/vite.svg create mode 100644 packages/vrender-animate/vite/src/App.css create mode 100644 packages/vrender-animate/vite/src/App.tsx create mode 100644 packages/vrender-animate/vite/src/assets/react.svg create mode 100644 packages/vrender-animate/vite/src/index.css create mode 100644 packages/vrender-animate/vite/src/main.tsx create mode 100644 packages/vrender-animate/vite/src/vite-env.d.ts create mode 100644 packages/vrender-animate/vite/tsconfig.json create mode 100644 packages/vrender-animate/vite/tsconfig.node.json create mode 100644 packages/vrender-animate/vite/vite.config.ts delete mode 100644 packages/vrender-core/src/animate/Ticker/default-ticker.ts delete mode 100644 packages/vrender-core/src/animate/Ticker/index.ts delete mode 100644 packages/vrender-core/src/animate/Ticker/manual-ticker-handler.ts delete mode 100644 packages/vrender-core/src/animate/Ticker/manual-ticker.ts delete mode 100644 packages/vrender-core/src/animate/Ticker/raf-tick-handler.ts delete mode 100644 packages/vrender-core/src/animate/Ticker/timeout-tick-handler.ts delete mode 100644 packages/vrender-core/src/animate/Ticker/type.ts delete mode 100644 packages/vrender-core/src/animate/animate.ts delete mode 100644 packages/vrender-core/src/animate/config.ts delete mode 100644 packages/vrender-core/src/animate/custom-animate.ts delete mode 100644 packages/vrender-core/src/animate/default-ticker.ts delete mode 100644 packages/vrender-core/src/animate/easing-func.ts delete mode 100644 packages/vrender-core/src/animate/easing.ts delete mode 100644 packages/vrender-core/src/animate/group-fade.ts delete mode 100644 packages/vrender-core/src/animate/index.ts delete mode 100644 packages/vrender-core/src/animate/morphing.ts delete mode 100644 packages/vrender-core/src/animate/timeline.ts create mode 100644 packages/vrender-core/src/common/performance-raf.ts delete mode 100644 packages/vrender-core/src/plugins/builtin-plugin/richtext-edit-plugin-old.ts diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 9e1f209f5..2bb7d8699 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -210,6 +210,9 @@ importers: ../../packages/vrender: dependencies: + '@visactor/vrender-animate': + specifier: workspace:0.22.8 + version: link:../vrender-animate '@visactor/vrender-core': specifier: workspace:0.22.8 version: link:../vrender-core @@ -278,6 +281,64 @@ importers: specifier: 3.2.6 version: 3.2.6(@types/node@22.13.17)(less@4.1.3)(terser@5.17.1) + ../../packages/vrender-animate: + dependencies: + '@visactor/vrender-core': + specifier: workspace:0.22.8 + version: link:../vrender-core + '@visactor/vutils': + specifier: ~0.19.5 + version: 0.19.5 + devDependencies: + '@internal/bundler': + specifier: workspace:* + version: link:../../tools/bundler + '@internal/eslint-config': + specifier: workspace:* + version: link:../../share/eslint-config + '@internal/ts-config': + specifier: workspace:* + version: link:../../share/ts-config + '@rushstack/eslint-patch': + specifier: ~1.1.4 + version: 1.1.4 + '@types/node-fetch': + specifier: 2.6.4 + version: 2.6.4 + '@types/react': + specifier: ^18.0.0 + version: 18.3.20 + '@types/react-dom': + specifier: ^18.0.0 + version: 18.3.5(@types/react@18.3.20) + '@vitejs/plugin-react': + specifier: 3.1.0 + version: 3.1.0(vite@3.2.6(@types/node@22.13.17)(less@4.1.3)(terser@5.17.1)) + canvas: + specifier: 2.11.2 + version: 2.11.2 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + eslint: + specifier: ~8.18.0 + version: 8.18.0 + node-fetch: + specifier: 2.6.6 + version: 2.6.6 + react: + specifier: ^18.0.0 + version: 18.3.1 + react-dom: + specifier: ^18.0.0 + version: 18.3.1(react@18.3.1) + typescript: + specifier: 4.9.5 + version: 4.9.5 + vite: + specifier: 3.2.6 + version: 3.2.6(@types/node@22.13.17)(less@4.1.3)(terser@5.17.1) + ../../packages/vrender-components: dependencies: '@visactor/vrender-core': diff --git a/packages/vrender-animate/.eslintrc.js b/packages/vrender-animate/.eslintrc.js new file mode 100644 index 000000000..c8e50d056 --- /dev/null +++ b/packages/vrender-animate/.eslintrc.js @@ -0,0 +1,7 @@ +require('@rushstack/eslint-patch/modern-module-resolution'); + +module.exports = { + extends: ['@internal/eslint-config/profile/lib'], + parserOptions: { tsconfigRootDir: __dirname, project: './tsconfig.eslint.json' } + // ignorePatterns: [], +}; diff --git a/packages/vrender-animate/CHANGELOG.json b/packages/vrender-animate/CHANGELOG.json new file mode 100644 index 000000000..78d2e60be --- /dev/null +++ b/packages/vrender-animate/CHANGELOG.json @@ -0,0 +1,1458 @@ +{ + "name": "@visactor/vrender-kits", + "entries": [ + { + "version": "0.22.1", + "tag": "@visactor/vrender-kits_v0.22.1", + "date": "Tue, 18 Feb 2025 10:14:45 GMT", + "comments": { + "none": [ + { + "comment": "fix: datazoom error when spec is updating. fix@visactor/vchart#3712" + }, + { + "comment": "feat: support dynamicTexture" + }, + { + "comment": "feat: `removeState` of graphic should support array or string\n\n" + }, + { + "comment": "fix: fix the bug of dpr will not work when createWindowByCanvas in node env\n\n" + } + ] + } + }, + { + "version": "0.22.0", + "tag": "@visactor/vrender-kits_v0.22.0", + "date": "Fri, 07 Feb 2025 14:23:36 GMT", + "comments": {} + }, + { + "version": "0.21.14", + "tag": "@visactor/vrender-kits_v0.21.14", + "date": "Fri, 07 Feb 2025 13:42:01 GMT", + "comments": {} + }, + { + "version": "0.21.13", + "tag": "@visactor/vrender-kits_v0.21.13", + "date": "Thu, 06 Feb 2025 08:25:45 GMT", + "comments": {} + }, + { + "version": "0.21.12", + "tag": "@visactor/vrender-kits_v0.21.12", + "date": "Wed, 05 Feb 2025 07:04:09 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix issue with inversify error when nobind" + } + ] + } + }, + { + "version": "0.21.11", + "tag": "@visactor/vrender-kits_v0.21.11", + "date": "Wed, 15 Jan 2025 12:13:28 GMT", + "comments": {} + }, + { + "version": "0.21.10", + "tag": "@visactor/vrender-kits_v0.21.10", + "date": "Wed, 15 Jan 2025 03:14:32 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix issue with duplicate getContext in wx env" + } + ] + } + }, + { + "version": "0.21.9", + "tag": "@visactor/vrender-kits_v0.21.9", + "date": "Mon, 13 Jan 2025 03:23:50 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix drawShape function in gif-image render " + } + ] + } + }, + { + "version": "0.21.8", + "tag": "@visactor/vrender-kits_v0.21.8", + "date": "Mon, 06 Jan 2025 11:07:36 GMT", + "comments": {} + }, + { + "version": "0.21.7", + "tag": "@visactor/vrender-kits_v0.21.7", + "date": "Wed, 25 Dec 2024 07:53:11 GMT", + "comments": { + "none": [ + { + "comment": "fix: upgrade vutils to 0.19.3\n\n" + } + ] + } + }, + { + "version": "0.21.6", + "tag": "@visactor/vrender-kits_v0.21.6", + "date": "Tue, 24 Dec 2024 12:46:37 GMT", + "comments": {} + }, + { + "version": "0.21.5", + "tag": "@visactor/vrender-kits_v0.21.5", + "date": "Tue, 24 Dec 2024 07:53:11 GMT", + "comments": {} + }, + { + "version": "0.21.4", + "tag": "@visactor/vrender-kits_v0.21.4", + "date": "Mon, 23 Dec 2024 10:16:00 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix issue with gesture emitEvent when gesture is released" + } + ] + } + }, + { + "version": "0.21.3", + "tag": "@visactor/vrender-kits_v0.21.3", + "date": "Mon, 23 Dec 2024 08:28:14 GMT", + "comments": { + "none": [ + { + "comment": "feat: support loadFont" + } + ] + } + }, + { + "version": "0.21.2", + "tag": "@visactor/vrender-kits_v0.21.2", + "date": "Thu, 12 Dec 2024 10:23:51 GMT", + "comments": {} + }, + { + "version": "0.21.1", + "tag": "@visactor/vrender-kits_v0.21.1", + "date": "Thu, 05 Dec 2024 07:50:47 GMT", + "comments": {} + }, + { + "version": "0.21.0", + "tag": "@visactor/vrender-kits_v0.21.0", + "date": "Thu, 28 Nov 2024 03:30:36 GMT", + "comments": {} + }, + { + "version": "0.20.16", + "tag": "@visactor/vrender-kits_v0.20.16", + "date": "Thu, 21 Nov 2024 06:58:23 GMT", + "comments": {} + }, + { + "version": "0.20.15", + "tag": "@visactor/vrender-kits_v0.20.15", + "date": "Fri, 15 Nov 2024 08:34:34 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix press in andiord" + } + ] + } + }, + { + "version": "0.20.14", + "tag": "@visactor/vrender-kits_v0.20.14", + "date": "Wed, 13 Nov 2024 07:47:16 GMT", + "comments": {} + }, + { + "version": "0.20.13", + "tag": "@visactor/vrender-kits_v0.20.13", + "date": "Wed, 13 Nov 2024 06:35:02 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix trigger of press in mobile\n\n" + }, + { + "comment": "fix: fix smartInvert of gradient bar\n\n" + } + ] + } + }, + { + "version": "0.20.12", + "tag": "@visactor/vrender-kits_v0.20.12", + "date": "Thu, 31 Oct 2024 02:49:49 GMT", + "comments": {} + }, + { + "version": "0.20.11", + "tag": "@visactor/vrender-kits_v0.20.11", + "date": "Wed, 30 Oct 2024 13:10:03 GMT", + "comments": {} + }, + { + "version": "0.20.10", + "tag": "@visactor/vrender-kits_v0.20.10", + "date": "Wed, 23 Oct 2024 08:37:33 GMT", + "comments": {} + }, + { + "version": "0.20.9", + "tag": "@visactor/vrender-kits_v0.20.9", + "date": "Tue, 15 Oct 2024 03:50:15 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix max width of arc label in left\n\n" + } + ] + } + }, + { + "version": "0.20.8", + "tag": "@visactor/vrender-kits_v0.20.8", + "date": "Sun, 29 Sep 2024 09:44:02 GMT", + "comments": {} + }, + { + "version": "0.20.7", + "tag": "@visactor/vrender-kits_v0.20.7", + "date": "Fri, 27 Sep 2024 03:22:31 GMT", + "comments": {} + }, + { + "version": "0.20.6", + "tag": "@visactor/vrender-kits_v0.20.6", + "date": "Thu, 26 Sep 2024 09:28:36 GMT", + "comments": {} + }, + { + "version": "0.20.5", + "tag": "@visactor/vrender-kits_v0.20.5", + "date": "Fri, 20 Sep 2024 06:37:57 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix path string of arc, fix #1434\n\n" + } + ] + } + }, + { + "version": "0.20.4", + "tag": "@visactor/vrender-kits_v0.20.4", + "date": "Thu, 12 Sep 2024 07:33:20 GMT", + "comments": {} + }, + { + "version": "0.20.3", + "tag": "@visactor/vrender-kits_v0.20.3", + "date": "Sat, 07 Sep 2024 09:16:33 GMT", + "comments": {} + }, + { + "version": "0.20.2", + "tag": "@visactor/vrender-kits_v0.20.2", + "date": "Wed, 04 Sep 2024 12:52:31 GMT", + "comments": {} + }, + { + "version": "0.20.1", + "tag": "@visactor/vrender-kits_v0.20.1", + "date": "Fri, 30 Aug 2024 09:55:08 GMT", + "comments": {} + }, + { + "version": "0.20.0", + "tag": "@visactor/vrender-kits_v0.20.0", + "date": "Thu, 15 Aug 2024 07:26:54 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix bug of auto-render when remove some graphics\n\n" + }, + { + "comment": "fix: optimize triangle symbols\n\n" + }, + { + "comment": "refactor: optimize cornerRadius parse of arc\n\n" + } + ] + } + }, + { + "version": "0.19.24", + "tag": "@visactor/vrender-kits_v0.19.24", + "date": "Tue, 13 Aug 2024 07:47:29 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix wrong stroke style is applied to area\n\n" + } + ] + } + }, + { + "version": "0.19.23", + "tag": "@visactor/vrender-kits_v0.19.23", + "date": "Tue, 06 Aug 2024 05:17:39 GMT", + "comments": {} + }, + { + "version": "0.19.22", + "tag": "@visactor/vrender-kits_v0.19.22", + "date": "Mon, 05 Aug 2024 09:08:30 GMT", + "comments": {} + }, + { + "version": "0.19.21", + "tag": "@visactor/vrender-kits_v0.19.21", + "date": "Mon, 05 Aug 2024 01:39:45 GMT", + "comments": {} + }, + { + "version": "0.19.20", + "tag": "@visactor/vrender-kits_v0.19.20", + "date": "Wed, 31 Jul 2024 09:48:37 GMT", + "comments": {} + }, + { + "version": "0.19.19", + "tag": "@visactor/vrender-kits_v0.19.19", + "date": "Tue, 23 Jul 2024 11:56:39 GMT", + "comments": {} + }, + { + "version": "0.19.18", + "tag": "@visactor/vrender-kits_v0.19.18", + "date": "Fri, 12 Jul 2024 07:18:10 GMT", + "comments": {} + }, + { + "version": "0.19.17", + "tag": "@visactor/vrender-kits_v0.19.17", + "date": "Fri, 05 Jul 2024 17:26:17 GMT", + "comments": {} + }, + { + "version": "0.19.16", + "tag": "@visactor/vrender-kits_v0.19.16", + "date": "Fri, 05 Jul 2024 14:29:15 GMT", + "comments": {} + }, + { + "version": "0.19.15", + "tag": "@visactor/vrender-kits_v0.19.15", + "date": "Fri, 28 Jun 2024 10:32:37 GMT", + "comments": {} + }, + { + "version": "0.19.14", + "tag": "@visactor/vrender-kits_v0.19.14", + "date": "Wed, 26 Jun 2024 09:16:23 GMT", + "comments": { + "none": [ + { + "comment": "feat: upgrade @visactor/vutils" + } + ] + } + }, + { + "version": "0.19.13", + "tag": "@visactor/vrender-kits_v0.19.13", + "date": "Tue, 25 Jun 2024 11:17:14 GMT", + "comments": {} + }, + { + "version": "0.19.12", + "tag": "@visactor/vrender-kits_v0.19.12", + "date": "Fri, 21 Jun 2024 06:52:50 GMT", + "comments": {} + }, + { + "version": "0.19.11", + "tag": "@visactor/vrender-kits_v0.19.11", + "date": "Fri, 14 Jun 2024 09:50:59 GMT", + "comments": {} + }, + { + "version": "0.19.10", + "tag": "@visactor/vrender-kits_v0.19.10", + "date": "Thu, 13 Jun 2024 09:52:46 GMT", + "comments": {} + }, + { + "version": "0.19.9", + "tag": "@visactor/vrender-kits_v0.19.9", + "date": "Wed, 05 Jun 2024 12:25:00 GMT", + "comments": {} + }, + { + "version": "0.19.8", + "tag": "@visactor/vrender-kits_v0.19.8", + "date": "Wed, 05 Jun 2024 08:24:28 GMT", + "comments": {} + }, + { + "version": "0.19.7", + "tag": "@visactor/vrender-kits_v0.19.7", + "date": "Tue, 04 Jun 2024 11:10:08 GMT", + "comments": {} + }, + { + "version": "0.19.6", + "tag": "@visactor/vrender-kits_v0.19.6", + "date": "Wed, 29 May 2024 06:57:11 GMT", + "comments": {} + }, + { + "version": "0.19.5", + "tag": "@visactor/vrender-kits_v0.19.5", + "date": "Fri, 24 May 2024 09:21:23 GMT", + "comments": {} + }, + { + "version": "0.19.4", + "tag": "@visactor/vrender-kits_v0.19.4", + "date": "Fri, 17 May 2024 06:46:41 GMT", + "comments": { + "none": [ + { + "comment": "feat: support harmony env" + } + ] + } + }, + { + "version": "0.19.3", + "tag": "@visactor/vrender-kits_v0.19.3", + "date": "Fri, 10 May 2024 09:24:39 GMT", + "comments": { + "none": [ + { + "comment": "feat: support baseOpacity for group" + } + ] + } + }, + { + "version": "0.19.2", + "tag": "@visactor/vrender-kits_v0.19.2", + "date": "Thu, 09 May 2024 12:26:00 GMT", + "comments": { + "none": [ + { + "comment": "feat: support tt env, closed #1129" + } + ] + } + }, + { + "version": "0.19.1", + "tag": "@visactor/vrender-kits_v0.19.1", + "date": "Wed, 08 May 2024 08:47:35 GMT", + "comments": {} + }, + { + "version": "0.19.0", + "tag": "@visactor/vrender-kits_v0.19.0", + "date": "Tue, 30 Apr 2024 08:40:53 GMT", + "comments": { + "none": [ + { + "comment": "feat: support style callback in html and react, fix 1102\n\n" + } + ] + } + }, + { + "version": "0.18.17", + "tag": "@visactor/vrender-kits_v0.18.17", + "date": "Tue, 30 Apr 2024 07:48:41 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix issue with setLineDash crash, closed #1047" + } + ] + } + }, + { + "version": "0.18.16", + "tag": "@visactor/vrender-kits_v0.18.16", + "date": "Mon, 29 Apr 2024 07:40:31 GMT", + "comments": {} + }, + { + "version": "0.18.15", + "tag": "@visactor/vrender-kits_v0.18.15", + "date": "Fri, 26 Apr 2024 10:37:19 GMT", + "comments": {} + }, + { + "version": "0.18.14", + "tag": "@visactor/vrender-kits_v0.18.14", + "date": "Wed, 24 Apr 2024 08:07:48 GMT", + "comments": {} + }, + { + "version": "0.18.13", + "tag": "@visactor/vrender-kits_v0.18.13", + "date": "Fri, 19 Apr 2024 08:46:08 GMT", + "comments": {} + }, + { + "version": "0.18.12", + "tag": "@visactor/vrender-kits_v0.18.12", + "date": "Fri, 19 Apr 2024 07:48:17 GMT", + "comments": {} + }, + { + "version": "0.18.11", + "tag": "@visactor/vrender-kits_v0.18.11", + "date": "Wed, 17 Apr 2024 03:02:22 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix for dragenter triggering error in drag event" + } + ] + } + }, + { + "version": "0.18.10", + "tag": "@visactor/vrender-kits_v0.18.10", + "date": "Fri, 29 Mar 2024 08:02:16 GMT", + "comments": {} + }, + { + "version": "0.18.9", + "tag": "@visactor/vrender-kits_v0.18.9", + "date": "Thu, 28 Mar 2024 10:13:24 GMT", + "comments": {} + }, + { + "version": "0.18.8", + "tag": "@visactor/vrender-kits_v0.18.8", + "date": "Wed, 27 Mar 2024 11:33:58 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix issue with pointer tap event point map" + } + ] + } + }, + { + "version": "0.18.7", + "tag": "@visactor/vrender-kits_v0.18.7", + "date": "Fri, 22 Mar 2024 08:46:19 GMT", + "comments": { + "none": [ + { + "comment": "fix: set vtag params to optional" + } + ] + } + }, + { + "version": "0.18.6", + "tag": "@visactor/vrender-kits_v0.18.6", + "date": "Tue, 19 Mar 2024 10:10:17 GMT", + "comments": {} + }, + { + "version": "0.18.5", + "tag": "@visactor/vrender-kits_v0.18.5", + "date": "Tue, 12 Mar 2024 15:16:46 GMT", + "comments": {} + }, + { + "version": "0.18.4", + "tag": "@visactor/vrender-kits_v0.18.4", + "date": "Tue, 12 Mar 2024 09:40:06 GMT", + "comments": {} + }, + { + "version": "0.18.3", + "tag": "@visactor/vrender-kits_v0.18.3", + "date": "Mon, 11 Mar 2024 08:24:00 GMT", + "comments": {} + }, + { + "version": "0.18.2", + "tag": "@visactor/vrender-kits_v0.18.2", + "date": "Fri, 08 Mar 2024 03:19:08 GMT", + "comments": {} + }, + { + "version": "0.18.1", + "tag": "@visactor/vrender-kits_v0.18.1", + "date": "Mon, 04 Mar 2024 08:29:15 GMT", + "comments": {} + }, + { + "version": "0.18.0", + "tag": "@visactor/vrender-kits_v0.18.0", + "date": "Wed, 28 Feb 2024 10:09:04 GMT", + "comments": {} + }, + { + "version": "0.17.26", + "tag": "@visactor/vrender-kits_v0.17.26", + "date": "Wed, 28 Feb 2024 08:06:31 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix issue with load svg sync, fix issue with decode react dom" + } + ] + } + }, + { + "version": "0.17.25", + "tag": "@visactor/vrender-kits_v0.17.25", + "date": "Fri, 23 Feb 2024 04:29:58 GMT", + "comments": { + "none": [ + { + "comment": "feat: support offscreenCanvas in lynx env, closed #994" + } + ] + } + }, + { + "version": "0.17.24", + "tag": "@visactor/vrender-kits_v0.17.24", + "date": "Tue, 06 Feb 2024 09:48:26 GMT", + "comments": {} + }, + { + "version": "0.17.23", + "tag": "@visactor/vrender-kits_v0.17.23", + "date": "Sun, 04 Feb 2024 12:41:45 GMT", + "comments": {} + }, + { + "version": "0.17.22", + "tag": "@visactor/vrender-kits_v0.17.22", + "date": "Fri, 02 Feb 2024 07:17:07 GMT", + "comments": {} + }, + { + "version": "0.17.21", + "tag": "@visactor/vrender-kits_v0.17.21", + "date": "Thu, 01 Feb 2024 12:22:29 GMT", + "comments": {} + }, + { + "version": "0.17.20", + "tag": "@visactor/vrender-kits_v0.17.20", + "date": "Thu, 01 Feb 2024 09:26:17 GMT", + "comments": {} + }, + { + "version": "0.17.19", + "tag": "@visactor/vrender-kits_v0.17.19", + "date": "Wed, 24 Jan 2024 13:11:27 GMT", + "comments": {} + }, + { + "version": "0.17.18", + "tag": "@visactor/vrender-kits_v0.17.18", + "date": "Wed, 24 Jan 2024 10:10:41 GMT", + "comments": { + "none": [ + { + "comment": "feat: compatible canvas in lynx env" + }, + { + "comment": "fix: fix issue with interface" + } + ] + } + }, + { + "version": "0.17.17", + "tag": "@visactor/vrender-kits_v0.17.17", + "date": "Mon, 22 Jan 2024 08:19:38 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix issue with loaded tree-shaking" + } + ] + } + }, + { + "version": "0.17.16", + "tag": "@visactor/vrender-kits_v0.17.16", + "date": "Wed, 17 Jan 2024 09:02:13 GMT", + "comments": {} + }, + { + "version": "0.17.15", + "tag": "@visactor/vrender-kits_v0.17.15", + "date": "Wed, 17 Jan 2024 06:43:01 GMT", + "comments": {} + }, + { + "version": "0.17.14", + "tag": "@visactor/vrender-kits_v0.17.14", + "date": "Fri, 12 Jan 2024 10:33:32 GMT", + "comments": {} + }, + { + "version": "0.17.13", + "tag": "@visactor/vrender-kits_v0.17.13", + "date": "Wed, 10 Jan 2024 14:18:21 GMT", + "comments": {} + }, + { + "version": "0.17.12", + "tag": "@visactor/vrender-kits_v0.17.12", + "date": "Wed, 10 Jan 2024 03:56:46 GMT", + "comments": {} + }, + { + "version": "0.17.11", + "tag": "@visactor/vrender-kits_v0.17.11", + "date": "Fri, 05 Jan 2024 11:54:56 GMT", + "comments": {} + }, + { + "version": "0.17.10", + "tag": "@visactor/vrender-kits_v0.17.10", + "date": "Wed, 03 Jan 2024 13:19:34 GMT", + "comments": { + "none": [ + { + "comment": "feat: support fillPickable and strokePickable for area, closed #792" + } + ] + } + }, + { + "version": "0.17.9", + "tag": "@visactor/vrender-kits_v0.17.9", + "date": "Fri, 29 Dec 2023 09:59:13 GMT", + "comments": {} + }, + { + "version": "0.17.8", + "tag": "@visactor/vrender-kits_v0.17.8", + "date": "Fri, 29 Dec 2023 07:20:26 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix issue with mapToCanvasPoint in miniapp, closed #828" + } + ] + } + }, + { + "version": "0.17.7", + "tag": "@visactor/vrender-kits_v0.17.7", + "date": "Wed, 20 Dec 2023 10:05:55 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix issue with create layer in miniapp env" + } + ] + } + }, + { + "version": "0.17.6", + "tag": "@visactor/vrender-kits_v0.17.6", + "date": "Wed, 20 Dec 2023 07:39:54 GMT", + "comments": {} + }, + { + "version": "0.17.5", + "tag": "@visactor/vrender-kits_v0.17.5", + "date": "Tue, 19 Dec 2023 09:13:27 GMT", + "comments": {} + }, + { + "version": "0.17.4", + "tag": "@visactor/vrender-kits_v0.17.4", + "date": "Thu, 14 Dec 2023 11:00:38 GMT", + "comments": {} + }, + { + "version": "0.17.3", + "tag": "@visactor/vrender-kits_v0.17.3", + "date": "Wed, 13 Dec 2023 12:04:17 GMT", + "comments": {} + }, + { + "version": "0.17.2", + "tag": "@visactor/vrender-kits_v0.17.2", + "date": "Tue, 12 Dec 2023 13:05:58 GMT", + "comments": { + "none": [ + { + "comment": "feat(dataZoom): add mask to modify hot zone. feat @visactor/vchart#1415'" + } + ] + } + }, + { + "version": "0.17.1", + "tag": "@visactor/vrender-kits_v0.17.1", + "date": "Wed, 06 Dec 2023 11:19:22 GMT", + "comments": { + "none": [ + { + "comment": "feat: support pickStrokeBuffer, closed #758" + }, + { + "comment": "fix: fix issue with rebind pick-contribution" + } + ] + } + }, + { + "version": "0.17.0", + "tag": "@visactor/vrender-kits_v0.17.0", + "date": "Thu, 30 Nov 2023 12:58:15 GMT", + "comments": { + "none": [ + { + "comment": "feat: rect support x1 and y1" + }, + { + "comment": "refactor: refact inversify completely, closed #657" + } + ], + "minor": [ + { + "comment": "feat: optmize bounds performance" + } + ] + } + }, + { + "version": "0.16.18", + "tag": "@visactor/vrender-kits_v0.16.18", + "date": "Thu, 30 Nov 2023 09:40:58 GMT", + "comments": { + "none": [ + { + "comment": "fix: doubletap should not be triggered when the target is different twice before and after" + } + ] + } + }, + { + "version": "0.16.17", + "tag": "@visactor/vrender-kits_v0.16.17", + "date": "Thu, 23 Nov 2023 13:32:49 GMT", + "comments": { + "none": [ + { + "comment": "feat: support 'tap' gesture for Gesture plugin" + }, + { + "comment": "fix: \\`pickMode: 'imprecise'\\` not work in polygon" + } + ] + } + }, + { + "version": "0.16.16", + "tag": "@visactor/vrender-kits_v0.16.16", + "date": "Fri, 17 Nov 2023 02:33:59 GMT", + "comments": {} + }, + { + "version": "0.16.15", + "tag": "@visactor/vrender-kits_v0.16.15", + "date": "Thu, 16 Nov 2023 02:46:27 GMT", + "comments": {} + }, + { + "version": "0.16.14", + "tag": "@visactor/vrender-kits_v0.16.14", + "date": "Wed, 15 Nov 2023 09:56:28 GMT", + "comments": {} + }, + { + "version": "0.16.13", + "tag": "@visactor/vrender-kits_v0.16.13", + "date": "Thu, 09 Nov 2023 11:49:33 GMT", + "comments": { + "none": [ + { + "comment": "fix: temp fix issue with lynx measuretext" + } + ] + } + }, + { + "version": "0.16.12", + "tag": "@visactor/vrender-kits_v0.16.12", + "date": "Tue, 07 Nov 2023 10:52:54 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix node-canvas max count issue" + } + ] + } + }, + { + "version": "0.16.11", + "tag": "@visactor/vrender-kits_v0.16.11", + "date": "Thu, 02 Nov 2023 13:43:18 GMT", + "comments": {} + }, + { + "version": "0.16.10", + "tag": "@visactor/vrender-kits_v0.16.10", + "date": "Thu, 02 Nov 2023 11:17:24 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix issue with xul and html attribute, closed #634" + } + ] + } + }, + { + "version": "0.16.9", + "tag": "@visactor/vrender-kits_v0.16.9", + "date": "Fri, 27 Oct 2023 02:21:19 GMT", + "comments": {} + }, + { + "version": "0.16.8", + "tag": "@visactor/vrender-kits_v0.16.8", + "date": "Mon, 23 Oct 2023 11:38:47 GMT", + "comments": {} + }, + { + "version": "0.16.7", + "tag": "@visactor/vrender-kits_v0.16.7", + "date": "Mon, 23 Oct 2023 08:53:33 GMT", + "comments": {} + }, + { + "version": "0.16.6", + "tag": "@visactor/vrender-kits_v0.16.6", + "date": "Mon, 23 Oct 2023 06:30:33 GMT", + "comments": {} + }, + { + "version": "0.16.5", + "tag": "@visactor/vrender-kits_v0.16.5", + "date": "Fri, 20 Oct 2023 04:22:42 GMT", + "comments": {} + }, + { + "version": "0.16.4", + "tag": "@visactor/vrender-kits_v0.16.4", + "date": "Thu, 19 Oct 2023 10:30:12 GMT", + "comments": {} + }, + { + "version": "0.16.3", + "tag": "@visactor/vrender-kits_v0.16.3", + "date": "Wed, 18 Oct 2023 07:43:09 GMT", + "comments": { + "none": [ + { + "comment": "fix: add canvas resize support in lynx env, closed #581" + } + ] + } + }, + { + "version": "0.16.2", + "tag": "@visactor/vrender-kits_v0.16.2", + "date": "Tue, 10 Oct 2023 11:48:48 GMT", + "comments": {} + }, + { + "version": "0.16.1", + "tag": "@visactor/vrender-kits_v0.16.1", + "date": "Mon, 09 Oct 2023 09:51:01 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix flex issue, closed, fix node dpr issue, closed #554, closed #555" + }, + { + "comment": "fix: fix reinit env issue" + } + ] + } + }, + { + "version": "0.16.0", + "tag": "@visactor/vrender-kits_v0.16.0", + "date": "Thu, 28 Sep 2023 07:23:52 GMT", + "comments": {} + }, + { + "version": "0.15.5", + "tag": "@visactor/vrender-kits_v0.15.5", + "date": "Wed, 27 Sep 2023 09:33:50 GMT", + "comments": {} + }, + { + "version": "0.15.4", + "tag": "@visactor/vrender-kits_v0.15.4", + "date": "Mon, 25 Sep 2023 03:05:18 GMT", + "comments": {} + }, + { + "version": "0.15.3", + "tag": "@visactor/vrender-kits_v0.15.3", + "date": "Wed, 20 Sep 2023 13:12:13 GMT", + "comments": {} + }, + { + "version": "0.15.2", + "tag": "@visactor/vrender-kits_v0.15.2", + "date": "Thu, 14 Sep 2023 09:55:56 GMT", + "comments": {} + }, + { + "version": "0.15.1", + "tag": "@visactor/vrender-kits_v0.15.1", + "date": "Wed, 13 Sep 2023 02:21:58 GMT", + "comments": {} + }, + { + "version": "0.15.0", + "tag": "@visactor/vrender-kits_v0.15.0", + "date": "Tue, 12 Sep 2023 12:20:48 GMT", + "comments": {} + }, + { + "version": "0.14.8", + "tag": "@visactor/vrender-kits_v0.14.8", + "date": "Thu, 07 Sep 2023 11:56:00 GMT", + "comments": {} + }, + { + "version": "0.12.21", + "tag": "@visactor/vrender-kits_v0.12.21", + "date": "Thu, 31 Aug 2023 10:03:38 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.14.6` to `0.14.7`" + } + ] + } + }, + { + "version": "0.12.20", + "tag": "@visactor/vrender-kits_v0.12.20", + "date": "Tue, 29 Aug 2023 11:30:33 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.14.5` to `0.14.6`" + } + ] + } + }, + { + "version": "0.12.19", + "tag": "@visactor/vrender-kits_v0.12.19", + "date": "Wed, 23 Aug 2023 11:53:28 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.14.4` to `0.14.5`" + } + ] + } + }, + { + "version": "0.12.18", + "tag": "@visactor/vrender-kits_v0.12.18", + "date": "Fri, 18 Aug 2023 10:16:08 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.14.3` to `0.14.4`" + } + ] + } + }, + { + "version": "0.12.17", + "tag": "@visactor/vrender-kits_v0.12.17", + "date": "Wed, 16 Aug 2023 06:46:13 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.14.2` to `0.14.3`" + } + ] + } + }, + { + "version": "0.12.16", + "tag": "@visactor/vrender-kits_v0.12.16", + "date": "Fri, 11 Aug 2023 10:05:27 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.14.1` to `0.14.2`" + } + ] + } + }, + { + "version": "0.12.15", + "tag": "@visactor/vrender-kits_v0.12.15", + "date": "Thu, 10 Aug 2023 12:14:14 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.14.0` to `0.14.1`" + } + ] + } + }, + { + "version": "0.12.14", + "tag": "@visactor/vrender-kits_v0.12.14", + "date": "Thu, 10 Aug 2023 07:22:55 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.13.9` to `0.14.0`" + } + ] + } + }, + { + "version": "0.12.13", + "tag": "@visactor/vrender-kits_v0.12.13", + "date": "Wed, 09 Aug 2023 07:34:23 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.13.8` to `0.13.9`" + } + ] + } + }, + { + "version": "0.12.12", + "tag": "@visactor/vrender-kits_v0.12.12", + "date": "Tue, 08 Aug 2023 09:27:52 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.13.7` to `0.13.8`" + } + ] + } + }, + { + "version": "0.12.11", + "tag": "@visactor/vrender-kits_v0.12.11", + "date": "Thu, 03 Aug 2023 10:04:34 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.13.6` to `0.13.7`" + } + ] + } + }, + { + "version": "0.12.10", + "tag": "@visactor/vrender-kits_v0.12.10", + "date": "Wed, 02 Aug 2023 03:13:00 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.13.5` to `0.13.6`" + } + ] + } + }, + { + "version": "0.12.9", + "tag": "@visactor/vrender-kits_v0.12.9", + "date": "Thu, 27 Jul 2023 12:27:43 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.13.4` to `0.13.5`" + } + ] + } + }, + { + "version": "0.12.8", + "tag": "@visactor/vrender-kits_v0.12.8", + "date": "Tue, 25 Jul 2023 13:33:47 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.13.3` to `0.13.4`" + } + ] + } + }, + { + "version": "0.12.7", + "tag": "@visactor/vrender-kits_v0.12.7", + "date": "Tue, 25 Jul 2023 07:34:59 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.13.2` to `0.13.3`" + } + ] + } + }, + { + "version": "0.12.6", + "tag": "@visactor/vrender-kits_v0.12.6", + "date": "Fri, 21 Jul 2023 10:50:41 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.13.1` to `0.13.2`" + } + ] + } + }, + { + "version": "0.12.5", + "tag": "@visactor/vrender-kits_v0.12.5", + "date": "Thu, 20 Jul 2023 10:41:23 GMT", + "comments": { + "patch": [ + { + "comment": "fix: fix the error caused by reflect-metadata in react env" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.13.0` to `0.13.1`" + } + ] + } + }, + { + "version": "0.12.4", + "tag": "@visactor/vrender-kits_v0.12.4", + "date": "Wed, 19 Jul 2023 08:29:52 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.12.3` to `0.13.0`" + } + ] + } + }, + { + "version": "0.12.3", + "tag": "@visactor/vrender-kits_v0.12.3", + "date": "Wed, 12 Jul 2023 12:30:46 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.12.2` to `0.12.3`" + } + ] + } + }, + { + "version": "0.12.2", + "tag": "@visactor/vrender-kits_v0.12.2", + "date": "Tue, 11 Jul 2023 13:17:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.12.1` to `0.12.2`" + } + ] + } + }, + { + "version": "0.12.1", + "tag": "@visactor/vrender-kits_v0.12.1", + "date": "Fri, 07 Jul 2023 09:04:45 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.12.0` to `0.12.1`" + } + ] + } + }, + { + "version": "0.12.0", + "tag": "@visactor/vrender-kits_v0.12.0", + "date": "Thu, 06 Jul 2023 09:09:12 GMT", + "comments": { + "minor": [ + { + "comment": "refactor: refactor interfaces and types of typescript" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.11.1` to `0.12.0`" + } + ] + } + }, + { + "version": "0.11.1", + "tag": "@visactor/vrender-kits_v0.11.1", + "date": "Tue, 27 Jun 2023 13:38:36 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.11.0` to `0.11.1`" + } + ] + } + }, + { + "version": "0.11.0", + "tag": "@visactor/vrender-kits_v0.11.0", + "date": "Tue, 27 Jun 2023 03:18:18 GMT", + "comments": { + "minor": [ + { + "comment": "update vUtils version" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.11.0-alpha.2` to `0.11.0`" + } + ] + } + }, + { + "version": "0.10.3", + "tag": "@visactor/vrender-kits_v0.10.3", + "date": "Tue, 20 Jun 2023 06:23:42 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.10.2` to `0.10.3`" + } + ] + } + }, + { + "version": "0.10.2", + "tag": "@visactor/vrender-kits_v0.10.2", + "date": "Tue, 20 Jun 2023 03:25:23 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.10.1` to `0.10.2`" + } + ] + } + }, + { + "version": "0.10.1", + "tag": "@visactor/vrender-kits_v0.10.1", + "date": "Mon, 19 Jun 2023 09:49:38 GMT", + "comments": { + "patch": [ + { + "comment": "fix the bug of node-canvas wh" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.10.0` to `0.10.1`" + } + ] + } + }, + { + "version": "0.10.0", + "tag": "@visactor/vrender-kits_v0.10.0", + "date": "Fri, 16 Jun 2023 03:13:09 GMT", + "comments": { + "minor": [ + { + "comment": "code style" + } + ], + "patch": [ + { + "comment": "upgrade vrender" + }, + { + "comment": "fix enableView3dTranform" + }, + { + "comment": "upgrade vrender" + } + ], + "none": [ + { + "comment": "release" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.9.2-alpha.0` to `0.10.0`" + } + ] + } + }, + { + "version": "0.9.1", + "tag": "@visactor/vrender-kits_v0.9.1", + "date": "Thu, 08 Jun 2023 11:34:32 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.9.0` to `0.9.1`" + } + ] + } + }, + { + "version": "0.9.0", + "tag": "@visactor/vrender-kits_v0.9.0", + "date": "Wed, 07 Jun 2023 12:20:05 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@visactor/vrender\" from `0.9.0-alpha.1` to `0.9.0`" + } + ] + } + } + ] +} diff --git a/packages/vrender-animate/CHANGELOG.md b/packages/vrender-animate/CHANGELOG.md new file mode 100644 index 000000000..bf342f138 --- /dev/null +++ b/packages/vrender-animate/CHANGELOG.md @@ -0,0 +1,941 @@ +# Change Log - @visactor/vrender-kits + +This log was last generated on Tue, 18 Feb 2025 10:14:45 GMT and should not be manually modified. + +## 0.22.1 +Tue, 18 Feb 2025 10:14:45 GMT + +### Updates + +- fix: datazoom error when spec is updating. fix@visactor/vchart#3712 +- feat: support dynamicTexture +- feat: `removeState` of graphic should support array or string + + +- fix: fix the bug of dpr will not work when createWindowByCanvas in node env + + + +## 0.22.0 +Fri, 07 Feb 2025 14:23:36 GMT + +_Version update only_ + +## 0.21.14 +Fri, 07 Feb 2025 13:42:01 GMT + +_Version update only_ + +## 0.21.13 +Thu, 06 Feb 2025 08:25:45 GMT + +_Version update only_ + +## 0.21.12 +Wed, 05 Feb 2025 07:04:09 GMT + +### Updates + +- fix: fix issue with inversify error when nobind + +## 0.21.11 +Wed, 15 Jan 2025 12:13:28 GMT + +_Version update only_ + +## 0.21.10 +Wed, 15 Jan 2025 03:14:32 GMT + +### Updates + +- fix: fix issue with duplicate getContext in wx env + +## 0.21.9 +Mon, 13 Jan 2025 03:23:50 GMT + +### Updates + +- fix: fix drawShape function in gif-image render + +## 0.21.8 +Mon, 06 Jan 2025 11:07:36 GMT + +_Version update only_ + +## 0.21.7 +Wed, 25 Dec 2024 07:53:11 GMT + +### Updates + +- fix: upgrade vutils to 0.19.3 + + + +## 0.21.6 +Tue, 24 Dec 2024 12:46:37 GMT + +_Version update only_ + +## 0.21.5 +Tue, 24 Dec 2024 07:53:11 GMT + +_Version update only_ + +## 0.21.4 +Mon, 23 Dec 2024 10:16:00 GMT + +### Updates + +- fix: fix issue with gesture emitEvent when gesture is released + +## 0.21.3 +Mon, 23 Dec 2024 08:28:14 GMT + +### Updates + +- feat: support loadFont + +## 0.21.2 +Thu, 12 Dec 2024 10:23:51 GMT + +_Version update only_ + +## 0.21.1 +Thu, 05 Dec 2024 07:50:47 GMT + +_Version update only_ + +## 0.21.0 +Thu, 28 Nov 2024 03:30:36 GMT + +_Version update only_ + +## 0.20.16 +Thu, 21 Nov 2024 06:58:23 GMT + +_Version update only_ + +## 0.20.15 +Fri, 15 Nov 2024 08:34:34 GMT + +### Updates + +- fix: fix press in andiord + +## 0.20.14 +Wed, 13 Nov 2024 07:47:16 GMT + +_Version update only_ + +## 0.20.13 +Wed, 13 Nov 2024 06:35:02 GMT + +### Updates + +- fix: fix trigger of press in mobile + + +- fix: fix smartInvert of gradient bar + + + +## 0.20.12 +Thu, 31 Oct 2024 02:49:49 GMT + +_Version update only_ + +## 0.20.11 +Wed, 30 Oct 2024 13:10:03 GMT + +_Version update only_ + +## 0.20.10 +Wed, 23 Oct 2024 08:37:33 GMT + +_Version update only_ + +## 0.20.9 +Tue, 15 Oct 2024 03:50:15 GMT + +### Updates + +- fix: fix max width of arc label in left + + + +## 0.20.8 +Sun, 29 Sep 2024 09:44:02 GMT + +_Version update only_ + +## 0.20.7 +Fri, 27 Sep 2024 03:22:31 GMT + +_Version update only_ + +## 0.20.6 +Thu, 26 Sep 2024 09:28:36 GMT + +_Version update only_ + +## 0.20.5 +Fri, 20 Sep 2024 06:37:57 GMT + +### Updates + +- fix: fix path string of arc, fix #1434 + + + +## 0.20.4 +Thu, 12 Sep 2024 07:33:20 GMT + +_Version update only_ + +## 0.20.3 +Sat, 07 Sep 2024 09:16:33 GMT + +_Version update only_ + +## 0.20.2 +Wed, 04 Sep 2024 12:52:31 GMT + +_Version update only_ + +## 0.20.1 +Fri, 30 Aug 2024 09:55:08 GMT + +_Version update only_ + +## 0.20.0 +Thu, 15 Aug 2024 07:26:54 GMT + +### Updates + +- fix: fix bug of auto-render when remove some graphics + + +- fix: optimize triangle symbols + + +- refactor: optimize cornerRadius parse of arc + + + +## 0.19.24 +Tue, 13 Aug 2024 07:47:29 GMT + +### Updates + +- fix: fix wrong stroke style is applied to area + + + +## 0.19.23 +Tue, 06 Aug 2024 05:17:39 GMT + +_Version update only_ + +## 0.19.22 +Mon, 05 Aug 2024 09:08:30 GMT + +_Version update only_ + +## 0.19.21 +Mon, 05 Aug 2024 01:39:45 GMT + +_Version update only_ + +## 0.19.20 +Wed, 31 Jul 2024 09:48:37 GMT + +_Version update only_ + +## 0.19.19 +Tue, 23 Jul 2024 11:56:39 GMT + +_Version update only_ + +## 0.19.18 +Fri, 12 Jul 2024 07:18:10 GMT + +_Version update only_ + +## 0.19.17 +Fri, 05 Jul 2024 17:26:17 GMT + +_Version update only_ + +## 0.19.16 +Fri, 05 Jul 2024 14:29:15 GMT + +_Version update only_ + +## 0.19.15 +Fri, 28 Jun 2024 10:32:37 GMT + +_Version update only_ + +## 0.19.14 +Wed, 26 Jun 2024 09:16:23 GMT + +### Updates + +- feat: upgrade @visactor/vutils + +## 0.19.13 +Tue, 25 Jun 2024 11:17:14 GMT + +_Version update only_ + +## 0.19.12 +Fri, 21 Jun 2024 06:52:50 GMT + +_Version update only_ + +## 0.19.11 +Fri, 14 Jun 2024 09:50:59 GMT + +_Version update only_ + +## 0.19.10 +Thu, 13 Jun 2024 09:52:46 GMT + +_Version update only_ + +## 0.19.9 +Wed, 05 Jun 2024 12:25:00 GMT + +_Version update only_ + +## 0.19.8 +Wed, 05 Jun 2024 08:24:28 GMT + +_Version update only_ + +## 0.19.7 +Tue, 04 Jun 2024 11:10:08 GMT + +_Version update only_ + +## 0.19.6 +Wed, 29 May 2024 06:57:11 GMT + +_Version update only_ + +## 0.19.5 +Fri, 24 May 2024 09:21:23 GMT + +_Version update only_ + +## 0.19.4 +Fri, 17 May 2024 06:46:41 GMT + +### Updates + +- feat: support harmony env + +## 0.19.3 +Fri, 10 May 2024 09:24:39 GMT + +### Updates + +- feat: support baseOpacity for group + +## 0.19.2 +Thu, 09 May 2024 12:26:00 GMT + +### Updates + +- feat: support tt env, closed #1129 + +## 0.19.1 +Wed, 08 May 2024 08:47:35 GMT + +_Version update only_ + +## 0.19.0 +Tue, 30 Apr 2024 08:40:53 GMT + +### Updates + +- feat: support style callback in html and react, fix 1102 + + + +## 0.18.17 +Tue, 30 Apr 2024 07:48:41 GMT + +### Updates + +- fix: fix issue with setLineDash crash, closed #1047 + +## 0.18.16 +Mon, 29 Apr 2024 07:40:31 GMT + +_Version update only_ + +## 0.18.15 +Fri, 26 Apr 2024 10:37:19 GMT + +_Version update only_ + +## 0.18.14 +Wed, 24 Apr 2024 08:07:48 GMT + +_Version update only_ + +## 0.18.13 +Fri, 19 Apr 2024 08:46:08 GMT + +_Version update only_ + +## 0.18.12 +Fri, 19 Apr 2024 07:48:17 GMT + +_Version update only_ + +## 0.18.11 +Wed, 17 Apr 2024 03:02:22 GMT + +### Updates + +- fix: fix for dragenter triggering error in drag event + +## 0.18.10 +Fri, 29 Mar 2024 08:02:16 GMT + +_Version update only_ + +## 0.18.9 +Thu, 28 Mar 2024 10:13:24 GMT + +_Version update only_ + +## 0.18.8 +Wed, 27 Mar 2024 11:33:58 GMT + +### Updates + +- fix: fix issue with pointer tap event point map + +## 0.18.7 +Fri, 22 Mar 2024 08:46:19 GMT + +### Updates + +- fix: set vtag params to optional + +## 0.18.6 +Tue, 19 Mar 2024 10:10:17 GMT + +_Version update only_ + +## 0.18.5 +Tue, 12 Mar 2024 15:16:46 GMT + +_Version update only_ + +## 0.18.4 +Tue, 12 Mar 2024 09:40:06 GMT + +_Version update only_ + +## 0.18.3 +Mon, 11 Mar 2024 08:24:00 GMT + +_Version update only_ + +## 0.18.2 +Fri, 08 Mar 2024 03:19:08 GMT + +_Version update only_ + +## 0.18.1 +Mon, 04 Mar 2024 08:29:15 GMT + +_Version update only_ + +## 0.18.0 +Wed, 28 Feb 2024 10:09:04 GMT + +_Version update only_ + +## 0.17.26 +Wed, 28 Feb 2024 08:06:31 GMT + +### Updates + +- fix: fix issue with load svg sync, fix issue with decode react dom + +## 0.17.25 +Fri, 23 Feb 2024 04:29:58 GMT + +### Updates + +- feat: support offscreenCanvas in lynx env, closed #994 + +## 0.17.24 +Tue, 06 Feb 2024 09:48:26 GMT + +_Version update only_ + +## 0.17.23 +Sun, 04 Feb 2024 12:41:45 GMT + +_Version update only_ + +## 0.17.22 +Fri, 02 Feb 2024 07:17:07 GMT + +_Version update only_ + +## 0.17.21 +Thu, 01 Feb 2024 12:22:29 GMT + +_Version update only_ + +## 0.17.20 +Thu, 01 Feb 2024 09:26:17 GMT + +_Version update only_ + +## 0.17.19 +Wed, 24 Jan 2024 13:11:27 GMT + +_Version update only_ + +## 0.17.18 +Wed, 24 Jan 2024 10:10:41 GMT + +### Updates + +- feat: compatible canvas in lynx env +- fix: fix issue with interface + +## 0.17.17 +Mon, 22 Jan 2024 08:19:38 GMT + +### Updates + +- fix: fix issue with loaded tree-shaking + +## 0.17.16 +Wed, 17 Jan 2024 09:02:13 GMT + +_Version update only_ + +## 0.17.15 +Wed, 17 Jan 2024 06:43:01 GMT + +_Version update only_ + +## 0.17.14 +Fri, 12 Jan 2024 10:33:32 GMT + +_Version update only_ + +## 0.17.13 +Wed, 10 Jan 2024 14:18:21 GMT + +_Version update only_ + +## 0.17.12 +Wed, 10 Jan 2024 03:56:46 GMT + +_Version update only_ + +## 0.17.11 +Fri, 05 Jan 2024 11:54:56 GMT + +_Version update only_ + +## 0.17.10 +Wed, 03 Jan 2024 13:19:34 GMT + +### Updates + +- feat: support fillPickable and strokePickable for area, closed #792 + +## 0.17.9 +Fri, 29 Dec 2023 09:59:13 GMT + +_Version update only_ + +## 0.17.8 +Fri, 29 Dec 2023 07:20:26 GMT + +### Updates + +- fix: fix issue with mapToCanvasPoint in miniapp, closed #828 + +## 0.17.7 +Wed, 20 Dec 2023 10:05:55 GMT + +### Updates + +- fix: fix issue with create layer in miniapp env + +## 0.17.6 +Wed, 20 Dec 2023 07:39:54 GMT + +_Version update only_ + +## 0.17.5 +Tue, 19 Dec 2023 09:13:27 GMT + +_Version update only_ + +## 0.17.4 +Thu, 14 Dec 2023 11:00:38 GMT + +_Version update only_ + +## 0.17.3 +Wed, 13 Dec 2023 12:04:17 GMT + +_Version update only_ + +## 0.17.2 +Tue, 12 Dec 2023 13:05:58 GMT + +### Updates + +- feat(dataZoom): add mask to modify hot zone. feat @visactor/vchart#1415' + +## 0.17.1 +Wed, 06 Dec 2023 11:19:22 GMT + +### Updates + +- feat: support pickStrokeBuffer, closed #758 +- fix: fix issue with rebind pick-contribution + +## 0.17.0 +Thu, 30 Nov 2023 12:58:15 GMT + +### Minor changes + +- feat: optmize bounds performance + +### Updates + +- feat: rect support x1 and y1 +- refactor: refact inversify completely, closed #657 + +## 0.16.18 +Thu, 30 Nov 2023 09:40:58 GMT + +### Updates + +- fix: doubletap should not be triggered when the target is different twice before and after + +## 0.16.17 +Thu, 23 Nov 2023 13:32:49 GMT + +### Updates + +- feat: support 'tap' gesture for Gesture plugin +- fix: \`pickMode: 'imprecise'\` not work in polygon + +## 0.16.16 +Fri, 17 Nov 2023 02:33:59 GMT + +_Version update only_ + +## 0.16.15 +Thu, 16 Nov 2023 02:46:27 GMT + +_Version update only_ + +## 0.16.14 +Wed, 15 Nov 2023 09:56:28 GMT + +_Version update only_ + +## 0.16.13 +Thu, 09 Nov 2023 11:49:33 GMT + +### Updates + +- fix: temp fix issue with lynx measuretext + +## 0.16.12 +Tue, 07 Nov 2023 10:52:54 GMT + +### Updates + +- fix: fix node-canvas max count issue + +## 0.16.11 +Thu, 02 Nov 2023 13:43:18 GMT + +_Version update only_ + +## 0.16.10 +Thu, 02 Nov 2023 11:17:24 GMT + +### Updates + +- fix: fix issue with xul and html attribute, closed #634 + +## 0.16.9 +Fri, 27 Oct 2023 02:21:19 GMT + +_Version update only_ + +## 0.16.8 +Mon, 23 Oct 2023 11:38:47 GMT + +_Version update only_ + +## 0.16.7 +Mon, 23 Oct 2023 08:53:33 GMT + +_Version update only_ + +## 0.16.6 +Mon, 23 Oct 2023 06:30:33 GMT + +_Version update only_ + +## 0.16.5 +Fri, 20 Oct 2023 04:22:42 GMT + +_Version update only_ + +## 0.16.4 +Thu, 19 Oct 2023 10:30:12 GMT + +_Version update only_ + +## 0.16.3 +Wed, 18 Oct 2023 07:43:09 GMT + +### Updates + +- fix: add canvas resize support in lynx env, closed #581 + +## 0.16.2 +Tue, 10 Oct 2023 11:48:48 GMT + +_Version update only_ + +## 0.16.1 +Mon, 09 Oct 2023 09:51:01 GMT + +### Updates + +- fix: fix flex issue, closed, fix node dpr issue, closed #554, closed #555 +- fix: fix reinit env issue + +## 0.16.0 +Thu, 28 Sep 2023 07:23:52 GMT + +_Version update only_ + +## 0.15.5 +Wed, 27 Sep 2023 09:33:50 GMT + +_Version update only_ + +## 0.15.4 +Mon, 25 Sep 2023 03:05:18 GMT + +_Version update only_ + +## 0.15.3 +Wed, 20 Sep 2023 13:12:13 GMT + +_Version update only_ + +## 0.15.2 +Thu, 14 Sep 2023 09:55:56 GMT + +_Version update only_ + +## 0.15.1 +Wed, 13 Sep 2023 02:21:58 GMT + +_Version update only_ + +## 0.15.0 +Tue, 12 Sep 2023 12:20:48 GMT + +_Version update only_ + +## 0.14.8 +Thu, 07 Sep 2023 11:56:00 GMT + +_Version update only_ + +## 0.12.21 +Thu, 31 Aug 2023 10:03:38 GMT + +_Version update only_ + +## 0.12.20 +Tue, 29 Aug 2023 11:30:33 GMT + +_Version update only_ + +## 0.12.19 +Wed, 23 Aug 2023 11:53:28 GMT + +_Version update only_ + +## 0.12.18 +Fri, 18 Aug 2023 10:16:08 GMT + +_Version update only_ + +## 0.12.17 +Wed, 16 Aug 2023 06:46:13 GMT + +_Version update only_ + +## 0.12.16 +Fri, 11 Aug 2023 10:05:27 GMT + +_Version update only_ + +## 0.12.15 +Thu, 10 Aug 2023 12:14:14 GMT + +_Version update only_ + +## 0.12.14 +Thu, 10 Aug 2023 07:22:55 GMT + +_Version update only_ + +## 0.12.13 +Wed, 09 Aug 2023 07:34:23 GMT + +_Version update only_ + +## 0.12.12 +Tue, 08 Aug 2023 09:27:52 GMT + +_Version update only_ + +## 0.12.11 +Thu, 03 Aug 2023 10:04:34 GMT + +_Version update only_ + +## 0.12.10 +Wed, 02 Aug 2023 03:13:00 GMT + +_Version update only_ + +## 0.12.9 +Thu, 27 Jul 2023 12:27:43 GMT + +_Version update only_ + +## 0.12.8 +Tue, 25 Jul 2023 13:33:47 GMT + +_Version update only_ + +## 0.12.7 +Tue, 25 Jul 2023 07:34:59 GMT + +_Version update only_ + +## 0.12.6 +Fri, 21 Jul 2023 10:50:41 GMT + +_Version update only_ + +## 0.12.5 +Thu, 20 Jul 2023 10:41:23 GMT + +### Patches + +- fix: fix the error caused by reflect-metadata in react env + +## 0.12.4 +Wed, 19 Jul 2023 08:29:52 GMT + +_Version update only_ + +## 0.12.3 +Wed, 12 Jul 2023 12:30:46 GMT + +_Version update only_ + +## 0.12.2 +Tue, 11 Jul 2023 13:17:12 GMT + +_Version update only_ + +## 0.12.1 +Fri, 07 Jul 2023 09:04:45 GMT + +_Version update only_ + +## 0.12.0 +Thu, 06 Jul 2023 09:09:12 GMT + +### Minor changes + +- refactor: refactor interfaces and types of typescript + +## 0.11.1 +Tue, 27 Jun 2023 13:38:36 GMT + +_Version update only_ + +## 0.11.0 +Tue, 27 Jun 2023 03:18:18 GMT + +### Minor changes + +- update vUtils version + +## 0.10.3 +Tue, 20 Jun 2023 06:23:42 GMT + +_Version update only_ + +## 0.10.2 +Tue, 20 Jun 2023 03:25:23 GMT + +_Version update only_ + +## 0.10.1 +Mon, 19 Jun 2023 09:49:38 GMT + +### Patches + +- fix the bug of node-canvas wh + +## 0.10.0 +Fri, 16 Jun 2023 03:13:09 GMT + +### Minor changes + +- code style + +### Patches + +- upgrade vrender +- fix enableView3dTranform +- upgrade vrender + +### Updates + +- release + +## 0.9.1 +Thu, 08 Jun 2023 11:34:32 GMT + +_Version update only_ + +## 0.9.0 +Wed, 07 Jun 2023 12:20:05 GMT + +_Initial release_ + diff --git a/packages/vrender-animate/README.md b/packages/vrender-animate/README.md new file mode 100644 index 000000000..0aa75cebf --- /dev/null +++ b/packages/vrender-animate/README.md @@ -0,0 +1,177 @@ +# VRender Animation Module + +This module provides a graph-based animation system for VRender. + +## Features + +- Graph-based animation system +- Support for simple property animations +- Support for sequence and parallel animations +- Support for composite animations +- Support for custom animations +- Support for path animations +- Compatible with the legacy animation system +- Advanced composition capabilities for nested animations +- Proper propagation of animation events in complex choreography + +## Recent Updates + +- **Improved Animation Node Composition**: Fixed issues with propagation in `GraphAdapterNode` to ensure proper animation sequencing +- **Enhanced Duration Calculation**: Better handling of duration for sequence and parallel animations +- **Better Event Handling**: Improved monitoring of animation completion and successor activation + +## Usage + +### Basic Property Animation + +```typescript +import { createAnimationNode, createGraphManager } from '@visactor/vrender-animate'; + +// Create a simple animation node +const moveNode = createAnimationNode({ + id: 'move', + target: myRect, + propertyName: 'x', + toValue: 200, + duration: 1000 +}); + +// Create a graph manager +const graphManager = createGraphManager(moveNode, stage); + +// Start the animation +graphManager.start(); +``` + +### Sequence Animation + +```typescript +import { createAnimationNode, createSequence, createGraphManager } from '@visactor/vrender-animate'; + +// Create animation nodes +const moveNode = createAnimationNode({ + id: 'move', + target: myRect, + propertyName: 'x', + toValue: 200, + duration: 1000 +}); + +const colorNode = createAnimationNode({ + id: 'color', + target: myRect, + propertyName: 'fill', + toValue: 'blue', + duration: 1000 +}); + +// Create a sequence +const sequence = createSequence([moveNode, colorNode]); + +// Create a graph manager +const graphManager = createGraphManager(sequence, stage); + +// Start the animation +graphManager.start(); +``` + +### Nested Animations (Advanced Composition) + +The animation system supports nesting of animations, allowing complex choreography: + +```typescript +import { createAnimationNode, createSequence, createParallel, createGraphManager } from '@visactor/vrender-animate'; + +// Create simple animations for rectangle 1 +const moveRect1 = createAnimationNode({ + target: rect1, + propertyName: 'x', + toValue: 500, + duration: 2000 +}); + +const colorRect1 = createAnimationNode({ + target: rect1, + propertyName: 'fill', + toValue: 'red', + duration: 1000 +}); + +// Create simple animations for rectangle 2 +const moveRect2 = createAnimationNode({ + target: rect2, + propertyName: 'y', + toValue: 300, + duration: 1500 +}); + +const colorRect2 = createAnimationNode({ + target: rect2, + propertyName: 'fill', + toValue: 'blue', + duration: 800 +}); + +// Create sequences for each rectangle +const sequence1 = createSequence([moveRect1, colorRect1]); +const sequence2 = createSequence([moveRect2, colorRect2]); + +// Run both sequences in parallel +const parallelAnimation = createParallel([sequence1, sequence2]); + +// Create additional animations +const finalAnimation = createAnimationNode({ + target: rect3, + propertyName: 'opacity', + toValue: 0, + duration: 1000 +}); + +// Create a final sequence that runs the parallel animations and then the final animation +const fullAnimation = createSequence([parallelAnimation, finalAnimation]); + +// Create graph manager and start the animation +const graphManager = createGraphManager(fullAnimation, stage); +graphManager.start(); +``` + +### Cleanup Completed Animations + +The animation system includes a mechanism to clean up completed animations to free memory and resources: + +```typescript +import { createAnimationNode, createSequence, createGraphManager } from '@visactor/vrender-animate'; + +// Create animation nodes (as shown in previous examples) +// ... + +// Create a graph manager +const graphManager = createGraphManager(myAnimationGraph, stage); + +// Start the animation +graphManager.start(); + +// Later, when animations have completed, you can clean up: +graphManager.onAllComplete = () => { + console.log('All animations completed'); + + // Remove completed animation nodes + const removed = graphManager.cleanupCompletedNodes(); + console.log(`Cleaned up ${removed} completed nodes`); +}; + +// You can also manually trigger cleanup at any time: +function handleCleanupButtonClick() { + const removed = graphManager.cleanupCompletedNodes(); + console.log(`Cleaned up ${removed} completed nodes`); +} + +// By default, the cleanup is performed in "safe mode", which only removes nodes +// that have no successors or whose successors have also completed. +// To forcibly remove all completed nodes: +graphManager.cleanupCompletedNodes(false); +``` + +## API + +See the API documentation for more details. diff --git a/packages/vrender-animate/bundler.config.js b/packages/vrender-animate/bundler.config.js new file mode 100644 index 000000000..d0fe57b42 --- /dev/null +++ b/packages/vrender-animate/bundler.config.js @@ -0,0 +1,13 @@ +/** + * @type {Partial} + */ +module.exports = { + formats: ['cjs', 'es'], + name: 'VRender.Kits', + umdOutputFilename: 'index', + globals: { + '@visactor/vrender-core': 'VRenderCore', + '@visactor/vutils': 'VUtils' + }, + external: ['@visactor/vrender-core', '@visactor/vutils'] +}; diff --git a/packages/vrender-animate/cross-env DEBUG='Bundler*' bundle b/packages/vrender-animate/cross-env DEBUG='Bundler*' bundle new file mode 100644 index 000000000..1fb468141 --- /dev/null +++ b/packages/vrender-animate/cross-env DEBUG='Bundler*' bundle @@ -0,0 +1,73 @@ + + Bundler:config CLI args { _: [] } +0ms + Bundler:config PROJECT_ROOT /Users/bytedance/dev/github/vr/packages/vrender-animate +1ms + Bundler:config User config file path /Users/bytedance/dev/github/vr/packages/vrender-animate/bundl + Bundler:config User config file path /Users/bytedance/dev/github/vr/packages/vrender-animate/bundler.config.js +1ms + Bundler:config Final config { + root: '/Users/bytedance/dev/github/vr/packages/vrender-animate', + sourceDir: 'src', + tsconfig: 'tsconfig.json', + input: { es: 'index.ts', cjs: 'index.ts', umd: 'index.ts' }, + formats: [ 'cjs', 'es' ], + outputDir: { es: 'es', cjs: 'cjs', umd: 'dist' }, + name: 'VRender.Kits', + umdOutputFilename: 'index', + copy: [], + sourcemap: true, + watch: false, + less: false, + envs: { __VERSION__: '"0.22.4"' }, + clean: false, + minify: true, + noEmitOnError: true, + targets: 'defaults and not IE 11', + external: [ '@visactor/vrender-core', '@visactor/vutils' ], + alias: [], + rollupOptions: {}, + preTasks: {}, + postTasks: {}, + globals: { + '@visactor/vrender-core': 'VRenderCore', + '@visactor/vutils': 'VUtils' + }, + _: [] +} +0ms + Bundler:config tsCompilerOptions { + moduleResolution: 'node', + target: 'ES2016', + noEmit: false, + emitDeclarationOnly: false, + declaration: true, + isolatedModules: false, + allowSyntheticDefaultImports: true, + module: 'commonjs', + skipLibCheck: true, + noEmitOnError: true, + baseUrl: '.', + rootDir: './src', + composite: true +} +8ms + Bundler:config tsCompilerOptions { + moduleResolution: 'node', + target: 'ES2016', + noEmit: false, + emitDeclarationOnly: false, + declaration: true, + isolatedModules: false, + allowSyntheticDefaultImports: true, + module: 'es2015', + skipLibCheck: true, + noEmitOnError: true, + baseUrl: '.', + rootDir: './src', + composite: true +} +12ms + Bundler:config RollupOptions {"input":"/Users/bytedance/dev/github/vr/packages/vrender-animate/src + Bundler:config RollupOptions {"input":"/Users/bytedance/dev/github/vr/packages/vrender-animate/src/index.ts","external":["@visactor/vrender-core","@visactor/vutils"],"plugins":[{"name":"node-resolve + Bundler:config RollupOptions {"input":"/Users/bytedance/dev/github/vr/packages/vrender-animate/src/index.ts","external":["@visactor/vrender-core","@visactor/vutils"],"plugins":[{"name":"node-resolve","version":"15.0.2","resolveId":{"order":"post"}},{"name":"commonjs","version":"24.1.0"},{"name":"b + Bundler:config RollupOptions {"input":"/Users/bytedance/dev/github/vr/packages/vrender-animate/src/index.ts","external":["@visactor/vrender-core","@visactor/vutils"],"plugins":[{"name":"node-resolve","version":"15.0.2","resolveId":{"order":"post"}},{"name":"commonjs","version":"24.1.0"},{"name":"babel"},{"name":"replace"},{"name":"typescript"},{"name":"url"},{"name":"alias"}]} +24ms +@rollup/plugin-typescript TS6304: Composite projects may not disable declaration emit. +Circular dependency: src/index.ts -> src/compat/legacy-adapter.ts -> src/index.ts +@rollup/plugin-typescript: outputToFilesystem option is defaulting to true. +✓ Finished [Build cjs] +3s +✓ Finished [Build es] +3s diff --git a/packages/vrender-animate/package.json b/packages/vrender-animate/package.json new file mode 100644 index 000000000..2f06734b5 --- /dev/null +++ b/packages/vrender-animate/package.json @@ -0,0 +1,72 @@ +{ + "name": "@visactor/vrender-animate", + "version": "0.22.8", + "description": "", + "sideEffects": false, + "main": "cjs/index.js", + "module": "es/index.js", + "types": "es/index.d.ts", + "files": [ + "cjs", + "es", + "dist" + ], + "scripts": { + "compile": "tsc --noEmit", + "eslint": "eslint --debug --fix src/", + "build": "cross-env DEBUG='Bundler*' bundle", + "dev": "cross-env DEBUG='Bundler*' bundle --clean -f es -w", + "start": "vite ./vite", + "test": "" + }, + "dependencies": { + "@visactor/vutils": "~0.19.5", + "@visactor/vrender-core": "workspace:0.22.8" + }, + "devDependencies": { + "@internal/bundler": "workspace:*", + "@internal/eslint-config": "workspace:*", + "@internal/ts-config": "workspace:*", + "@rushstack/eslint-patch": "~1.1.4", + "canvas": "2.11.2", + "node-fetch": "2.6.6", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@types/node-fetch": "2.6.4", + "@vitejs/plugin-react": "3.1.0", + "eslint": "~8.18.0", + "vite": "3.2.6", + "typescript": "4.9.5", + "cross-env": "^7.0.3" + }, + "keywords": [ + "VisActor", + "graphics", + "renderer", + "vrender", + "vrender-kits" + ], + "homepage": "", + "bugs": "https://github.com/VisActor/VRender/issues", + "repository": { + "type": "git", + "url": "https://github.com/VisActor/VRender.git", + "directory": "packages/vrender-kits" + }, + "author": { + "name": "VisActor", + "url": "https://VisActor.io/" + }, + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "import": "./es/index.js", + "require": "./cjs/index.js" + } + } +} diff --git a/packages/vrender-animate/src/index.ts b/packages/vrender-animate/src/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/vrender-animate/src/intreface/animate.ts b/packages/vrender-animate/src/intreface/animate.ts new file mode 100644 index 000000000..a5b618a73 --- /dev/null +++ b/packages/vrender-animate/src/intreface/animate.ts @@ -0,0 +1,145 @@ +import type { ICustomAnimate, IGraphic, ITimeline } from '@visactor/vrender-core'; +import type { EasingType, EasingTypeFunc } from './easing'; +import type { AnimateStatus, IAnimateStepType } from './type'; + +export interface IStep { + type: IAnimateStepType; + prev?: IStep; + // 持续时间 + duration: number; + // 在animate中的位置 + position: number; + // 链表,下一个 + next?: IStep; + // 属性 + props?: Record; + // 解析后的属性(用于性能优化,避免每次tick都解析) + parsedProps?: any; + // 解析后的属性列表(用于性能优化,避免每次tick都解析) + propKeys?: string[]; + // 缓动函数 + easing?: EasingTypeFunc; + + // 添加一个 + append: (step: IStep) => void; + // 获取上一个props,用于完成这次的fromValue 和 toValue的插值 + getLastProps: () => any; + + animate: IAnimate; + + // 设置持续时间 + setDuration: (duration: number) => void; + // 获取持续时间 + getDuration: () => number; + // 确定插值函数(在开始的时候就确定,避免每次tick都解析) + determineInterpolationFunction: () => void; + // 确定更新函数(在开始的时候就确定,避免每次tick都解析) + determineUpdateFunction: () => void; + + // 设置开始时间 + setStartTime: (time: number) => void; + // 获取开始时间 + getStartTime: () => number; + + // 在第一次绑定到Animate的时候触发 + onBind: () => void; + // 第一次执行的时候调用 + onFirstRun: () => void; + // 开始执行的时候调用(如果有循环,那每个周期都会调用) + onStart: () => void; + // 结束执行的时候调用(如果有循环,那每个周期都会调用) + onEnd: () => void; + // 更新执行的时候调用(如果有循环,那每个周期都会调用) + onUpdate: (end: boolean, ratio: number, out: Record) => void; + // 结束的时候调用(如果有循环,会在整个循环最终结束的时候调用) + onFinish: () => void; +} + +export interface IAnimate { + readonly id: string | number; + status: AnimateStatus; + target: IGraphic; + + interpolateFunc: (key: string, ratio: number, from: any, to: any, nextAttributes: any) => boolean; + + _onStart?: (() => void)[]; + _onFrame?: ((step: IStep, ratio: number) => void)[]; + _onEnd?: (() => void)[]; + _onRemove?: (() => void)[]; + + getStartProps: () => Record; + getEndProps: () => Record; + + // 设置timeline + setTimeline: (timeline: ITimeline) => void; + // 获取timeline + getTimeline: () => ITimeline; + readonly timeline: ITimeline; + + bind: (target: IGraphic) => this; + to: (props: Record, duration: number, easing: EasingType) => this; + from: (props: Record, duration: number, easing: EasingType) => this; + pause: () => void; + resume: () => void; + onStart: (cb: () => void) => void; + onEnd: (cb: () => void) => void; + onFrame: (cb: (step: IStep, ratio: number) => void) => void; + // 屏蔽属性 + preventAttr: (key: string) => void; + // 屏蔽属性 + preventAttrs: (key: string[]) => void; + // 属性是否合法 + validAttr: (key: string) => boolean; + + runCb: (cb: (a: IAnimate, step: IStep) => void) => IAnimate; + + // 自定义插值,返回false表示没有匹配上 + customInterpolate: ( + key: string, + ratio: number, + from: any, + to: any, + target: IGraphic, + ret: Record + ) => boolean; + play: (customAnimate: ICustomAnimate) => this; + + getFromValue: () => Record; + getToValue: () => Record; + // 停止,可以设置停止后设置target的属性为开始的值(fromValue),还是结束的值(toValue) + stop: (type?: 'start' | 'end' | Record) => void; + /** 打上END标志,下一帧被删除 */ + release: () => void; + // 获取持续的时长 + getDuration: () => number; + // 获取动画开始时间(注意并不是子动画的startAt) + getStartTime: () => number; + // 等待delay + wait: (delay: number) => this; + + /* 动画编排 */ + // 所有动画结束后执行 + afterAll: (list: IAnimate[]) => this; + // 在某个动画结束后执行 + after: (animate: IAnimate) => this; + // 并行执行 + parallel: (animate: IAnimate) => this; + + // 反转动画 + reversed: (r: boolean) => IAnimate; + // 循环动画 + loop: (n: number) => IAnimate; + // 反弹动画 + bounce: (b: boolean) => IAnimate; + + // 下一个动画指针 + nextAnimate?: IAnimate; + // 上一个动画指针 + prevAnimate?: IAnimate; + + advance: (delta: number) => void; + + // 设置开始时间(startAt之前是完全不会进入动画生命周期的) + // 它和wait不一样,如果调用的是wait,wait过程中还算是一个动画阶段,只是空的阶段,而startAt之前是完全不会进入动画生命周期的 + startAt: (t: number) => IAnimate; +} diff --git a/packages/vrender-animate/src/intreface/easing.ts b/packages/vrender-animate/src/intreface/easing.ts new file mode 100644 index 000000000..b488418c7 --- /dev/null +++ b/packages/vrender-animate/src/intreface/easing.ts @@ -0,0 +1,41 @@ +export type EasingTypeStr = + | 'linear' + | 'quadIn' + | 'quadOut' + | 'quadInOut' + | 'quadInOut' + | 'cubicIn' + | 'cubicOut' + | 'cubicInOut' + | 'quartIn' + | 'quartOut' + | 'quartInOut' + | 'quintIn' + | 'quintOut' + | 'quintInOut' + | 'backIn' + | 'backOut' + | 'backInOut' + | 'circIn' + | 'circOut' + | 'circInOut' + | 'bounceOut' + | 'bounceIn' + | 'bounceInOut' + | 'elasticIn' + | 'elasticOut' + | 'elasticInOut' + | 'sineIn' + | 'sineOut' + | 'sineInOut' + | 'expoIn' + | 'expoOut' + | 'expoInOut' + // @since 0.21.0 + | 'easeInOutQuad' + | 'easeOutElastic' + | 'easeInOutElastic' + | ''; +export type EasingTypeFunc = (t: number) => number; + +export type EasingType = EasingTypeStr | EasingTypeFunc; diff --git a/packages/vrender-animate/src/intreface/timeline.ts b/packages/vrender-animate/src/intreface/timeline.ts new file mode 100644 index 000000000..b9c0e1bc4 --- /dev/null +++ b/packages/vrender-animate/src/intreface/timeline.ts @@ -0,0 +1,39 @@ +import type { IAnimate } from './animate'; + +export interface ITimeline { + id: number; + // 包含的动画数量(animate数组的数量),包含所有动画 + animateCount: number; + // 添加动画 + addAnimate: (animate: IAnimate) => void; + // 移除动画 + removeAnimate: (animate: IAnimate, release?: boolean) => void; + // 更新动画 + tick: (delta: number) => void; + // 清除动画 + clear: () => void; + // 暂停动画 + pause: () => void; + // 恢复动画 + resume: () => void; + // 设置开始时间 + setStartTime: (time: number) => void; + // 获取开始时间 + getStartTime: () => number; + // 获取当前时间 + getCurrentTime: () => number; + // 设置当前时间 + setCurrentTime: (time: number) => void; + // 获取动画总时长 + getTotalDuration: () => number; + // 获取动画的播放状态 + getPlayState: () => 'playing' | 'paused' | 'stopped'; + // 获取动画的播放速度 + getPlaySpeed: () => number; + // 设置动画的播放速度 + setPlaySpeed: (speed: number) => void; + // 获取动画的播放方向 + getPlayDirection: () => 'forward' | 'backward'; + // 设置动画的播放方向 + setPlayDirection: (direction: 'forward' | 'backward') => void; +} diff --git a/packages/vrender-animate/src/intreface/type.ts b/packages/vrender-animate/src/intreface/type.ts new file mode 100644 index 000000000..780eb36ef --- /dev/null +++ b/packages/vrender-animate/src/intreface/type.ts @@ -0,0 +1,15 @@ +export enum AnimateStepType { + wait = 'wait', + from = 'from', + to = 'to', + customAnimate = 'customAnimate' +} + +export enum AnimateStatus { + INITIAL = 0, + RUNNING = 1, + PAUSED = 2, + END = 3 +} + +export type IAnimateStepType = keyof typeof AnimateStepType; diff --git a/packages/vrender-animate/tsconfig.eslint.json b/packages/vrender-animate/tsconfig.eslint.json new file mode 100644 index 000000000..7f6149b2c --- /dev/null +++ b/packages/vrender-animate/tsconfig.eslint.json @@ -0,0 +1,16 @@ +{ + "extends": "@internal/ts-config/tsconfig.base.json", + "compilerOptions": { + "types": ["jest", "node"], + "lib": ["DOM", "ESNext"], + "baseUrl": "./", + "rootDir": "./" + }, + "include": ["src", "__tests__", "examples"], + "exclude": ["bugserver-config"], + "references": [ + { + "path": "../vrender-core" + } + ] +} diff --git a/packages/vrender-animate/tsconfig.json b/packages/vrender-animate/tsconfig.json new file mode 100644 index 000000000..9ad2e6ba9 --- /dev/null +++ b/packages/vrender-animate/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "@internal/ts-config/tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "./es", + "rootDir": "./src", + "composite": true, + "target": "ES2016" + }, + "include": ["src"], + "references": [ + { + "path": "../vrender-core" + } + ] +} diff --git a/packages/vrender-animate/vite/index.html b/packages/vrender-animate/vite/index.html new file mode 100644 index 000000000..e0d1c8408 --- /dev/null +++ b/packages/vrender-animate/vite/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/packages/vrender-animate/vite/public/vite.svg b/packages/vrender-animate/vite/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/packages/vrender-animate/vite/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/vrender-animate/vite/src/App.css b/packages/vrender-animate/vite/src/App.css new file mode 100644 index 000000000..b9d355df2 --- /dev/null +++ b/packages/vrender-animate/vite/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/packages/vrender-animate/vite/src/App.tsx b/packages/vrender-animate/vite/src/App.tsx new file mode 100644 index 000000000..d6dcc835b --- /dev/null +++ b/packages/vrender-animate/vite/src/App.tsx @@ -0,0 +1,31 @@ +import React, { useState } from 'react'; +import reactLogo from './assets/react.svg'; +import viteLogo from '/vite.svg'; +import './App.css'; + +function App() { + const [count, setCount] = useState(0); + + return ( + <> + +

Vite + React

+
+ +

+ Edit src/App.tsx and save to test HMR +

+
+

Click on the Vite and React logos to learn more

+ + ); +} + +export default App; diff --git a/packages/vrender-animate/vite/src/assets/react.svg b/packages/vrender-animate/vite/src/assets/react.svg new file mode 100644 index 000000000..6c87de9bb --- /dev/null +++ b/packages/vrender-animate/vite/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/vrender-animate/vite/src/index.css b/packages/vrender-animate/vite/src/index.css new file mode 100644 index 000000000..2c3fac689 --- /dev/null +++ b/packages/vrender-animate/vite/src/index.css @@ -0,0 +1,69 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/packages/vrender-animate/vite/src/main.tsx b/packages/vrender-animate/vite/src/main.tsx new file mode 100644 index 000000000..b3ad8a593 --- /dev/null +++ b/packages/vrender-animate/vite/src/main.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App.tsx'; +import './index.css'; + +ReactDOM.render( + + + , + document.getElementById('root') as HTMLElement +); diff --git a/packages/vrender-animate/vite/src/vite-env.d.ts b/packages/vrender-animate/vite/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/packages/vrender-animate/vite/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/vrender-animate/vite/tsconfig.json b/packages/vrender-animate/vite/tsconfig.json new file mode 100644 index 000000000..4af7feedf --- /dev/null +++ b/packages/vrender-animate/vite/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "module": "ESNext", + "skipLibCheck": true, + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react", + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "src" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/packages/vrender-animate/vite/tsconfig.node.json b/packages/vrender-animate/vite/tsconfig.node.json new file mode 100644 index 000000000..42872c59f --- /dev/null +++ b/packages/vrender-animate/vite/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/packages/vrender-animate/vite/vite.config.ts b/packages/vrender-animate/vite/vite.config.ts new file mode 100644 index 000000000..c8f4f283c --- /dev/null +++ b/packages/vrender-animate/vite/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react({ jsxRuntime: 'classic' })] +}); diff --git a/packages/vrender-core/src/animate/Ticker/default-ticker.ts b/packages/vrender-core/src/animate/Ticker/default-ticker.ts deleted file mode 100644 index 4bf64ece7..000000000 --- a/packages/vrender-core/src/animate/Ticker/default-ticker.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { EventEmitter, Logger } from '@visactor/vutils'; -import type { ITickHandler, ITickerHandlerStatic, ITimeline, ITicker } from '../../interface'; -import { application } from '../../application'; -import type { TickerMode } from './type'; -import { STATUS } from './type'; -import { RAFTickHandler } from './raf-tick-handler'; -import { TimeOutTickHandler } from './timeout-tick-handler'; - -export class DefaultTicker extends EventEmitter implements ITicker { - protected interval: number; - protected tickerHandler: ITickHandler; - protected _mode: TickerMode; - protected status: STATUS; - protected lastFrameTime: number; - protected tickCounts: number; - protected timelines: ITimeline[]; - autoStop: boolean; - - set mode(m: TickerMode) { - if (this._mode === m) { - return; - } - this._mode = m; - this.setupTickHandler(); - } - get mode(): TickerMode { - return this._mode; - } - - constructor(timelines: ITimeline[] = []) { - super(); - this.init(); - this.lastFrameTime = -1; - this.tickCounts = 0; - this.timelines = timelines; - this.autoStop = true; - } - - init() { - this.interval = NaN; - this.status = STATUS.INITIAL; - application.global.hooks.onSetEnv.tap('default-ticker', () => { - this.initHandler(); - }); - if (application.global.env) { - this.initHandler(); - } - } - - addTimeline(timeline: ITimeline) { - this.timelines.push(timeline); - } - remTimeline(timeline: ITimeline) { - this.timelines = this.timelines.filter(t => t !== timeline); - } - getTimelines(): ITimeline[] { - return this.timelines; - } - - protected initHandler(): ITickHandler | null { - if (this._mode) { - return null; - } - const ticks: { mode: TickerMode; cons: ITickerHandlerStatic }[] = [ - { mode: 'raf', cons: RAFTickHandler }, - { mode: 'timeout', cons: TimeOutTickHandler } - ]; - for (let i = 0; i < ticks.length; i++) { - if (ticks[i].cons.Avaliable()) { - this.mode = ticks[i].mode; - break; - } - } - return null; - } - - /** - * 设置tickHandler - * @returns 返回true表示设置成功,false表示设置失败 - */ - protected setupTickHandler(): boolean { - let handler: ITickHandler; - // 创建下一个tickHandler - switch (this._mode) { - case 'raf': - handler = new RAFTickHandler(); - break; - case 'timeout': - handler = new TimeOutTickHandler(); - break; - // case 'manual': - // handler = new ManualTickHandler(); - // break; - default: - Logger.getInstance().warn('非法的计时器模式'); - handler = new RAFTickHandler(); - break; - } - if (!handler.avaliable()) { - return false; - } - - // 销毁上一个tickerHandler - if (this.tickerHandler) { - this.tickerHandler.release(); - } - this.tickerHandler = handler; - return true; - } - - setInterval(interval: number) { - this.interval = interval; - } - getInterval(): number { - return this.interval; - } - - setFPS(fps: number): void { - this.setInterval(1000 / fps); - } - getFPS(): number { - return 1000 / this.interval; - } - tick(interval: number): void { - this.tickerHandler.tick(interval, (handler: ITickHandler) => { - this.handleTick(handler, { once: true }); - }); - } - tickTo(t: number): void { - if (!this.tickerHandler.tickTo) { - return; - } - this.tickerHandler.tickTo(t, (handler: ITickHandler) => { - this.handleTick(handler, { once: true }); - }); - } - pause(): boolean { - if (this.status === STATUS.INITIAL) { - return false; - } - this.status = STATUS.PAUSE; - return true; - } - resume(): boolean { - if (this.status === STATUS.INITIAL) { - return false; - } - this.status = STATUS.RUNNING; - return true; - } - - ifCanStop(): boolean { - if (this.autoStop) { - if (!this.timelines.length) { - return true; - } - if (this.timelines.reduce((a, b) => a + b.animateCount, 0) === 0) { - return true; - } - } - return false; - } - - start(force: boolean = false): boolean { - if (this.status === STATUS.RUNNING) { - return false; - } - if (!this.tickerHandler) { - return false; - } - // 如果不需要start,那就不start - if (!force) { - // 暂停状态不执行 - if (this.status === STATUS.PAUSE) { - return false; - } - if (!this.timelines.length) { - return false; - } - if (this.timelines.reduce((a, b) => a + b.animateCount, 0) === 0) { - return false; - } - } - this.status = STATUS.RUNNING; - this.tickerHandler.tick(0, this.handleTick); - return true; - } - stop(): void { - // 重新设置tickHandler - this.status = STATUS.INITIAL; - this.setupTickHandler(); - this.lastFrameTime = -1; - } - - protected handleTick = (handler: ITickHandler, params?: { once?: boolean }) => { - const { once = false } = params ?? {}; - // 尝试停止 - if (this.ifCanStop()) { - this.stop(); - return; - } - this._handlerTick(); - if (!once) { - handler.tick(this.interval, this.handleTick); - } - }; - - protected _handlerTick = () => { - // 具体执行函数 - const tickerHandler = this.tickerHandler; - const time = tickerHandler.getTime(); - // 上一帧经过的时间 - let delta = 0; - if (this.lastFrameTime >= 0) { - delta = time - this.lastFrameTime; - } - this.lastFrameTime = time; - - if (this.status !== STATUS.RUNNING) { - return; - } - this.tickCounts++; - - this.timelines.forEach(t => { - t.tick(delta); - }); - this.emit('tick'); - }; - - release(): void { - this.stop(); - this.timelines = []; - this.tickerHandler.release(); - this.emit('afterTick'); - } - - /** - * 同步tick状态,需要手动触发tick执行,保证属性为走完动画的属性 - * 【注】grammar会设置属性到最终值,然后调用render,这时候需要VRender手动触发tick,保证属性为走完动画的属性,而不是Grammar设置上的属性 - */ - trySyncTickStatus() { - if (this.status === STATUS.RUNNING) { - this._handlerTick(); - } - } -} diff --git a/packages/vrender-core/src/animate/Ticker/index.ts b/packages/vrender-core/src/animate/Ticker/index.ts deleted file mode 100644 index d90a0239e..000000000 --- a/packages/vrender-core/src/animate/Ticker/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './default-ticker'; -export * from './manual-ticker'; -export * from './manual-ticker-handler'; -export * from './raf-tick-handler'; -export * from './timeout-tick-handler'; diff --git a/packages/vrender-core/src/animate/Ticker/manual-ticker-handler.ts b/packages/vrender-core/src/animate/Ticker/manual-ticker-handler.ts deleted file mode 100644 index d4b815c7b..000000000 --- a/packages/vrender-core/src/animate/Ticker/manual-ticker-handler.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { ITickHandler } from '../../interface/animate'; - -export class ManualTickHandler implements ITickHandler { - protected timerId: number; - protected time: number = 0; - - static Avaliable(): boolean { - return true; - } - - avaliable(): boolean { - return ManualTickHandler.Avaliable(); - } - - tick(interval: number, cb: (handler: ITickHandler, params?: { once: boolean }) => void): void { - this.time = Math.max(0, interval + this.time); - cb(this, { once: true }); - } - - tickTo(t: number, cb: (handler: ITickHandler, params?: { once: boolean }) => void): void { - this.time = Math.max(0, t); - cb(this, { once: true }); - } - - release() { - if (this.timerId > 0) { - // clearTimeout(this.timerId); - this.timerId = -1; - } - } - - getTime() { - return this.time; - } -} diff --git a/packages/vrender-core/src/animate/Ticker/manual-ticker.ts b/packages/vrender-core/src/animate/Ticker/manual-ticker.ts deleted file mode 100644 index 1e7945524..000000000 --- a/packages/vrender-core/src/animate/Ticker/manual-ticker.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { ITicker, ITickHandler, ITimeline } from '../../interface/animate'; -import { DefaultTicker } from './default-ticker'; -import { ManualTickHandler } from './manual-ticker-handler'; -import type { STATUS, TickerMode } from './type'; - -export class ManualTicker extends DefaultTicker implements ITicker { - protected declare interval: number; - protected declare tickerHandler: ITickHandler; - protected declare _mode: TickerMode; - protected declare status: STATUS; - protected declare lastFrameTime: number; - protected declare tickCounts: number; - protected declare timelines: ITimeline[]; - declare autoStop: boolean; - - set mode(m: TickerMode) { - m = 'manual'; - this.setupTickHandler(); - } - get mode(): TickerMode { - return this._mode; - } - - protected initHandler(): ITickHandler | null { - this.mode = 'manual'; - return null; - } - - /** - * 设置tickHandler - * @returns 返回true表示设置成功,false表示设置失败 - */ - protected setupTickHandler(): boolean { - const handler: ITickHandler = new ManualTickHandler(); - this._mode = 'manual'; - - // 销毁上一个tickerHandler - if (this.tickerHandler) { - this.tickerHandler.release(); - } - this.tickerHandler = handler; - return true; - } - - tickAt(time: number) { - this.tickerHandler.tick(time - Math.max(this.lastFrameTime, 0), (handler: ITickHandler) => { - this.handleTick(handler, { once: true }); - }); - } - - ifCanStop(): boolean { - return false; - } -} diff --git a/packages/vrender-core/src/animate/Ticker/raf-tick-handler.ts b/packages/vrender-core/src/animate/Ticker/raf-tick-handler.ts deleted file mode 100644 index 2f39214b1..000000000 --- a/packages/vrender-core/src/animate/Ticker/raf-tick-handler.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { application } from '../../application'; -import type { ITickHandler } from '../../interface/animate'; - -export class RAFTickHandler implements ITickHandler { - protected released: boolean; - - static Avaliable(): boolean { - return !!application.global.getRequestAnimationFrame(); - } - avaliable(): boolean { - return RAFTickHandler.Avaliable(); - } - - tick(interval: number, cb: (handler: ITickHandler) => void): void { - const raf = application.global.getRequestAnimationFrame(); - raf(() => { - if (this.released) { - return; - } - cb(this); - }); - } - - release() { - this.released = true; - } - getTime() { - return Date.now(); - } -} diff --git a/packages/vrender-core/src/animate/Ticker/timeout-tick-handler.ts b/packages/vrender-core/src/animate/Ticker/timeout-tick-handler.ts deleted file mode 100644 index c6c13c183..000000000 --- a/packages/vrender-core/src/animate/Ticker/timeout-tick-handler.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { ITickHandler } from '../../interface/animate'; - -export class TimeOutTickHandler implements ITickHandler { - protected timerId: number; - - static Avaliable(): boolean { - return true; - } - - avaliable(): boolean { - return TimeOutTickHandler.Avaliable(); - } - - tick(interval: number, cb: (handler: ITickHandler) => void): void { - this.timerId = setTimeout(() => { - cb(this); - }, interval) as unknown as number; - } - - release() { - if (this.timerId > 0) { - clearTimeout(this.timerId); - this.timerId = -1; - } - } - getTime() { - return Date.now(); - } -} diff --git a/packages/vrender-core/src/animate/Ticker/type.ts b/packages/vrender-core/src/animate/Ticker/type.ts deleted file mode 100644 index 97afaea54..000000000 --- a/packages/vrender-core/src/animate/Ticker/type.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type TickerMode = 'raf' | 'timeout' | 'manual'; - -export enum STATUS { - INITIAL = 0, // initial表示初始状态 - RUNNING = 1, // running表示正在执行 - PAUSE = 2 // PULSE表示tick还是继续,只是不执行函数了 -} diff --git a/packages/vrender-core/src/animate/animate.ts b/packages/vrender-core/src/animate/animate.ts deleted file mode 100644 index 57087397a..000000000 --- a/packages/vrender-core/src/animate/animate.ts +++ /dev/null @@ -1,1308 +0,0 @@ -import type { - EasingType, - EasingTypeFunc, - IAnimate, - IAnimateStepType, - IAnimateTarget, - ICustomAnimate, - IGraphic, - IStep, - IStepConfig, - ISubAnimate, - ITimeline -} from '../interface'; -import { AnimateMode, AnimateStatus, AnimateStepType, AttributeUpdateType } from '../common/enums'; -import { Easing } from './easing'; -import { Logger, max } from '@visactor/vutils'; -import { defaultTimeline } from './timeline'; -import { Generator } from '../common/generator'; - -// 参考TweenJS -// https://github.com/CreateJS/TweenJS/tree/master/src/tweenjs -/** - * The MIT License (MIT) - - Copyright (c) 2014 gskinner.com, inc. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ -export abstract class ACustomAnimate implements ICustomAnimate { - declare from: T; - declare to: T; - declare duration: number; - declare easing: EasingType; - declare params: any; - declare target: IAnimateTarget; - declare updateCount: number; - declare subAnimate: ISubAnimate; - declare step?: IStep; - declare mode?: AnimateMode; - - // 用于判断是否一致 - declare _endProps?: any; - declare _mergedEndProps?: any; - - constructor(from: T, to: T, duration: number, easing: EasingType, params?: any) { - this.from = from; - this.to = to; - this.duration = duration; - this.easing = easing; - this.params = params; - this.updateCount = 0; - } - - bind(target: IAnimateTarget, subAni: ISubAnimate) { - this.target = target; - this.subAnimate = subAni; - this.onBind(); - } - - // 在第一次调用的时候触发 - onBind() { - return; - } - - // 第一次执行的时候调用 - onFirstRun() { - return; - } - - // 开始执行的时候调用(如果有循环,那每个周期都会调用) - onStart() { - return; - } - - // 结束执行的时候调用(如果有循环,那每个周期都会调用) - onEnd() { - return; - } - - getEndProps(): Record | void { - return this.to; - } - - getFromProps(): Record | void { - return this.from; - } - - getMergedEndProps(): Record | void { - const thisEndProps = this.getEndProps(); - if (thisEndProps) { - if (this._endProps === thisEndProps) { - return this._mergedEndProps; - } - this._endProps = thisEndProps; - this._mergedEndProps = Object.assign({}, this.step.prev.getLastProps() ?? {}, thisEndProps); - return; - } - return this.step.prev ? this.step.prev.getLastProps() : thisEndProps; - } - - // abstract getFromValue(key: string): any; - // abstract getToValue(key: string): any; - - abstract onUpdate(end: boolean, ratio: number, out: Record): void; - - update(end: boolean, ratio: number, out: Record): void { - if (this.updateCount === 0) { - this.onFirstRun(); - // out添加之前的props - const props = this.step.getLastProps(); - Object.keys(props).forEach(k => { - if (this.subAnimate.animate.validAttr(k)) { - out[k] = props[k]; - } - }); - } - this.updateCount += 1; - this.onUpdate(end, ratio, out); - if (end) { - this.onEnd(); - } - } -} - -export class CbAnimate extends ACustomAnimate { - cb: () => void; - - constructor(cb: () => void) { - super(null, null, 0, 'linear'); - this.cb = cb; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - return; - } - - onStart(): void { - this.cb(); - } -} - -type InterpolateFunc = ( - key: string, - ratio: number, - from: any, - to: any, - target: IAnimateTarget, - out: Record -) => boolean; - -export class Animate implements IAnimate { - static mode: AnimateMode = AnimateMode.NORMAL; - declare target: IAnimateTarget; - declare timeline: ITimeline; - declare nextAnimate?: IAnimate; - declare prevAnimate?: IAnimate; - // 当前Animate的状态,正常,暂停,结束 - declare status: AnimateStatus; - declare readonly id: string | number; - // 开始时间 - protected declare _startTime: number; - protected declare _duringTime: number; - declare subAnimates: SubAnimate[]; - declare tailAnimate: SubAnimate; - - // 绝对的位置 - declare rawPosition: number; - // 时间倍速缩放 - declare timeScale: number; - - declare interpolateFunc: (key: string, ratio: number, from: any, to: any, nextAttributes: any) => boolean; - - declare _onStart?: (() => void)[]; - declare _onFrame?: ((step: IStep, ratio: number) => void)[]; - declare _onEnd?: (() => void)[]; - declare _onRemove?: (() => void)[]; - declare _preventAttrs?: Set; - static interpolateMap: Map = new Map(); - slience?: boolean; - - constructor( - id: string | number = Generator.GenAutoIncrementId(), - timeline: ITimeline = defaultTimeline, - slience?: boolean - ) { - this.id = id; - this.timeline = timeline || defaultTimeline; - this.status = AnimateStatus.INITIAL; - this.tailAnimate = new SubAnimate(this); - this.subAnimates = [this.tailAnimate]; - this.timeScale = 1; - this.rawPosition = -1; - this._startTime = 0; - this._duringTime = 0; - this.timeline.addAnimate(this); - this.slience = slience; - } - - setTimeline(timeline: ITimeline) { - if (timeline === this.timeline) { - return; - } - this.timeline.removeAnimate(this, false); - timeline.addAnimate(this); - } - - getStartTime(): number { - return this._startTime; - } - - getDuration(): number { - return this.subAnimates.reduce((t, subAnimate) => t + subAnimate.totalDuration, 0); - } - - after(animate: IAnimate) { - const t = animate.getDuration(); - this._startTime = t; - return this; - } - - afterAll(list: IAnimate[]) { - let maxT = -Infinity; - list.forEach(a => { - maxT = max(a.getDuration(), maxT); - }); - this._startTime = maxT; - return this; - } - - parallel(animate: IAnimate) { - this._startTime = animate.getStartTime(); - return this; - } - - static AddInterpolate(name: string, cb: InterpolateFunc) { - Animate.interpolateMap.set(name, cb); - } - - play(customAnimate: ICustomAnimate) { - this.tailAnimate.play(customAnimate); - // todo: 考虑使用绑定的ticker执行 - if (this.target) { - const stage = (this.target as IGraphic).stage; - stage && stage.renderNextFrame(); - } - if (this.subAnimates.length === 1 && this.tailAnimate.totalDuration === customAnimate.duration) { - this.trySetAttribute(customAnimate.getFromProps(), customAnimate.mode); - } - return this; - } - - trySetAttribute(attr: Record | void, mode: AnimateMode = Animate.mode) { - if (attr && mode & AnimateMode.SET_ATTR_IMMEDIATELY) { - (this.target as any).setAttributes && - (this.target as any).setAttributes(attr, false, { type: AttributeUpdateType.ANIMATE_PLAY }); - } - } - - runCb(cb: (a: IAnimate, step: IStep) => void) { - // this.tailAnimate.runCb(cb); - const customAnimate = new CbAnimate(() => { - cb(this, customAnimate.step.prev); - }); - this.tailAnimate.play(customAnimate); - return this; - } - - /** - * 自定义插值,返回false表示没有匹配上 - * @param key - * @param from - * @param to - */ - customInterpolate( - key: string, - ratio: number, - from: any, - to: any, - target: IAnimateTarget, - ret: Record - ): boolean { - const func = Animate.interpolateMap.get(key) || Animate.interpolateMap.get(''); - if (!func) { - return false; - } - return func(key, ratio, from, to, target, ret); - } - - pause() { - if (this.status === AnimateStatus.RUNNING) { - this.status = AnimateStatus.PAUSED; - } - } - - resume() { - if (this.status === AnimateStatus.PAUSED) { - this.status = AnimateStatus.RUNNING; - } - } - - to(props: Record, duration: number, easing: EasingType, params?: IStepConfig) { - this.tailAnimate.to(props, duration, easing, params); - // 默认开始动画 - // todo: 考虑使用绑定的ticker执行 - if (this.target) { - const stage = (this.target as IGraphic).stage; - stage && stage.renderNextFrame(); - } - // if (this.subAnimates.length === 1 && this.tailAnimate.duration === duration) { - // this.trySetAttribute(props); - // } - return this; - } - from(props: Record, duration: number, easing: EasingType, params?: IStepConfig) { - this.tailAnimate.from(props, duration, easing, params); - // todo: 考虑使用绑定的ticker执行 - if (this.target) { - const stage = (this.target as IGraphic).stage; - stage && stage.renderNextFrame(); - } - return this; - } - wait(duration: number) { - this.tailAnimate.wait(duration); - // todo: 考虑使用绑定的ticker执行 - if (this.target) { - const stage = (this.target as IGraphic).stage; - stage && stage.renderNextFrame(); - } - return this; - } - startAt(t: number) { - this.tailAnimate.startAt(t); - // todo: 考虑使用绑定的ticker执行 - if (this.target) { - const stage = (this.target as IGraphic).stage; - stage && stage.renderNextFrame(); - } - return this; - } - - loop(l: number) { - this.tailAnimate.loop = l; - // todo: 考虑使用绑定的ticker执行 - if (this.target) { - const stage = (this.target as IGraphic).stage; - stage && stage.renderNextFrame(); - } - return this; - } - - reversed(r: boolean) { - this.tailAnimate.reversed = r; - // todo: 考虑使用绑定的ticker执行 - if (this.target) { - const stage = (this.target as IGraphic).stage; - stage && stage.renderNextFrame(); - } - return this; - } - - bounce(b: boolean) { - this.tailAnimate.bounce = b; - // todo: 考虑使用绑定的ticker执行 - if (this.target) { - const stage = (this.target as IGraphic).stage; - stage && stage.renderNextFrame(); - } - return this; - } - - subAnimate() { - const sa = new SubAnimate(this, this.tailAnimate); - this.tailAnimate = sa; - this.subAnimates.push(sa); - sa.bind(this.target); - return this; - } - - getStartProps(): Record { - return this.subAnimates[0].getStartProps(); - } - - getEndProps(): Record { - return this.tailAnimate.getEndProps(); - } - - depreventAttr(key: string) { - if (!this._preventAttrs) { - return; - } - this._preventAttrs.delete(key); - } - preventAttr(key: string) { - if (!this._preventAttrs) { - this._preventAttrs = new Set(); - } - this._preventAttrs.add(key); - } - preventAttrs(keys: string[]) { - keys.forEach(key => this.preventAttr(key)); - } - validAttr(key: string): boolean { - if (!this._preventAttrs) { - return true; - } - return !this._preventAttrs.has(key); - } - - bind(target: IAnimateTarget) { - this.target = target; - - if (this.target.onAnimateBind && !this.slience) { - this.target.onAnimateBind(this); - } - - this.subAnimates.forEach(sa => { - sa.bind(target); - }); - return this; - } - - advance(delta: number) { - // startTime之前的时间不计入耗时 - if (this._duringTime < this._startTime) { - if (this._duringTime + delta * this.timeScale < this._startTime) { - this._duringTime += delta * this.timeScale; - return; - } - delta = this._duringTime + delta * this.timeScale - this._startTime; - this._duringTime = this._startTime; - } - // 执行advance - if (this.status === AnimateStatus.INITIAL) { - this.status = AnimateStatus.RUNNING; - this._onStart && this._onStart.forEach(cb => cb()); - } - const end = this.setPosition(Math.max(this.rawPosition, 0) + delta * this.timeScale); - if (end && this.status === AnimateStatus.RUNNING) { - this.status = AnimateStatus.END; - this._onEnd && this._onEnd.forEach(cb => cb()); - } - } - - setPosition(rawPosition: number): boolean { - let d = 0; - let sa: SubAnimate | undefined; - const prevRawPos = this.rawPosition; - const maxRawPos = this.subAnimates.reduce((a, b) => a + b.totalDuration, 0); - - if (rawPosition < 0) { - rawPosition = 0; - } - - const end = rawPosition >= maxRawPos; - - if (end) { - rawPosition = maxRawPos; - } - - if (rawPosition === prevRawPos) { - return end; - } - - // 查找对应的subAnimate - for (let i = 0; i < this.subAnimates.length; i++) { - sa = this.subAnimates[i]; - if (d + sa.totalDuration >= rawPosition) { - break; - } else { - d += sa.totalDuration; - sa = undefined; - } - } - this.rawPosition = rawPosition; - sa.setPosition(rawPosition - d); - - return end; - } - - onStart(cb: () => void) { - if (!this._onStart) { - this._onStart = []; - } - this._onStart.push(cb); - } - onEnd(cb: () => void) { - if (!this._onEnd) { - this._onEnd = []; - } - this._onEnd.push(cb); - } - onRemove(cb: () => void) { - if (!this._onRemove) { - this._onRemove = []; - } - this._onRemove.push(cb); - } - onFrame(cb: (step: IStep, ratio: number) => void) { - if (!this._onFrame) { - this._onFrame = []; - } - this._onFrame.push(cb); - } - release() { - this.status = AnimateStatus.END; - return; - } - - stop(nextVal?: 'start' | 'end' | Record) { - if (!nextVal) { - this.target.onStop(); - } - if (nextVal === 'start') { - this.target.onStop(this.getStartProps()); - } else if (nextVal === 'end') { - this.target.onStop(this.getEndProps()); - } else { - this.target.onStop(nextVal); - } - this.release(); - } -} - -// Animate.mode |= AnimateMode.SET_ATTR_IMMEDIATELY; - -export class SubAnimate implements ISubAnimate { - declare target: IAnimateTarget; - declare animate: IAnimate; - // 默认的初始step,一定存在,且stepHead的props一定保存整个subAnimate阶段所有属性的最初 - protected declare stepHead: Step; - protected declare stepTail: Step; - // 结束时反转动画 - declare bounce: boolean; - // 是否reverse - declare reversed: boolean; - // 循环次数,0为执行一次,1为执行两次,Infinity为无限循环 - declare loop: number; - // 持续时间,不包括循环 - declare duration: number; - // 位置,在[0, duration]之间 - declare position: number; - // 绝对的位置,在[0, loops * duration]之间 - declare rawPosition: number; - declare dirty: boolean; - - declare _totalDuration: number; - declare _startAt: number; - declare _lastStep: IStep; - declare _deltaPosition: number; - - get totalDuration(): number { - this.calcAttr(); - return this._totalDuration + this._startAt; - } - - constructor(animate: IAnimate, lastSubAnimate?: SubAnimate) { - this.rawPosition = -1; - this.position = 0; - this.loop = 0; - this.duration = 0; - this.animate = animate; - if (lastSubAnimate) { - this.stepHead = new Step(0, 0, Object.assign({}, lastSubAnimate.stepTail.props)); - } else { - this.stepHead = new Step(0, 0, {}); - } - this.stepTail = this.stepHead; - this.dirty = true; - this._startAt = 0; - } - - // 计算按需计算的属性 - protected calcAttr() { - if (!this.dirty) { - return; - } - - this._totalDuration = this.duration * (this.loop + 1); - } - - bind(target: IAnimateTarget) { - this.target = target; - return this; - } - - play(customAnimate: ICustomAnimate) { - let duration = customAnimate.duration; - if (duration == null || duration < 0) { - duration = 0; - } - const easing = customAnimate.easing; - const easingFunc = typeof easing === 'string' ? Easing[easing] : easing; - const step = this._addStep(duration, null, easingFunc); - step.type = AnimateStepType.customAnimate; - this._appendProps(customAnimate.getEndProps(), step, false); - this._appendCustomAnimate(customAnimate, step); - return this; - } - - // _appendPlayProps(step: IStep) { - - // return; - // } - - to(props: Record, duration: number, easing: EasingType, params?: IStepConfig) { - if (duration == null || duration < 0) { - duration = 0; - } - - const easingFunc = typeof easing === 'string' ? Easing[easing] : easing; - - const step = this._addStep(duration, null, easingFunc); - step.type = AnimateStepType.to; - this._appendProps(props, step, params ? params.tempProps : false); - // this._appendPlayProps(step); - - if (!step.propKeys) { - step.propKeys = Object.keys(step.props); - } - if (!(params && params.noPreventAttrs)) { - this.target.animates && - this.target.animates.forEach(a => { - if (a.id !== this.animate.id) { - a.preventAttrs(step.propKeys); - } - }); - } - return this; - } - - from(props: Record, duration: number, easing: EasingType, params?: IStepConfig) { - this.to(props, 0, easing, params); - const toProps = {}; - if (!this.stepTail.propKeys) { - this.stepTail.propKeys = Object.keys(this.stepTail.props); - } - this.stepTail.propKeys.forEach(k => { - toProps[k] = this.getLastPropByName(k, this.stepTail); - }); - this.to(toProps, duration, easing, params); - this.stepTail.type = AnimateStepType.from; - } - - startAt(t: number) { - if (t < 0) { - t = 0; - } - this._startAt = t; - return this; - } - - getStartProps() { - return this.stepHead?.props; - } - - getEndProps() { - return this.stepTail.props; - } - - getLastStep() { - return this._lastStep; - } - - wait(duration: number) { - if (duration > 0) { - const step = this._addStep(+duration, null); - - step.type = AnimateStepType.wait; - // TODO 这里如果跳帧的话会存在bug - if (step.prev.customAnimate) { - step.props = step.prev.customAnimate.getEndProps(); - } else { - step.props = step.prev.props; - } - if (this.target.onAddStep) { - this.target.onAddStep(step); - } - // this._appendPlayProps(step); - } - return this; - } - - protected _addStep(duration: number, props: any, easingFunc?: EasingTypeFunc) { - const step = new Step(this.duration, duration, props, easingFunc); - this.duration += duration; - this.stepTail.append(step); - this.stepTail = step; - return step; - } - - protected _appendProps(props: any, step: Step, tempProps?: boolean) { - if (tempProps) { - step.props = props; - } else { - // todo: 是否需要深拷贝props - step.props = Object.assign({}, props); - } - let lastStep = step.prev; - const _props = step.props; - // 将undefined的属性设置到默认值 - if (!step.propKeys) { - step.propKeys = Object.keys(step.props); - } - step.propKeys.forEach(k => { - if (step.props[k] === undefined) { - step.props[k] = this.target.getDefaultAttribute(k); - } - }); - // 拷贝之前的step阶段属性 - while (lastStep.prev) { - if (lastStep.props) { - if (!lastStep.propKeys) { - lastStep.propKeys = Object.keys(lastStep.props); - } - lastStep.propKeys.forEach(key => { - if (_props[key] === undefined) { - _props[key] = lastStep.props[key]; - } - }); - } - // 重置propKeys - step.propKeys = Object.keys(step.props); - lastStep = lastStep.prev; - } - - // 设置最初的props属性 - const initProps = this.stepHead.props; - if (!step.propKeys) { - step.propKeys = Object.keys(_props); - } - step.propKeys.forEach(key => { - if (initProps[key] === undefined) { - const parentAnimateInitProps = this.animate.getStartProps(); - initProps[key] = parentAnimateInitProps[key] = this.target.getComputedAttribute(key); - } - }); - - if (this.target.onAddStep) { - this.target.onAddStep(step); - } - } - - protected _appendCustomAnimate(customAnimate: ICustomAnimate, step: Step) { - step.customAnimate = customAnimate; - customAnimate.step = step; - customAnimate.bind(this.target, this); - } - - setPosition(rawPosition: number) { - const d = this.duration; - const loopCount = this.loop; - const prevRawPos = this.rawPosition; - let end = false; - let loop: number; // 当前是第几次循环 - let position: number; // 当前周期的时间 - const startAt = this._startAt ?? 0; - - if (rawPosition < 0) { - rawPosition = 0; - } - if (rawPosition < startAt) { - this.rawPosition = rawPosition; - return false; - } - rawPosition = rawPosition - startAt; - if (d <= 0) { - // 如果不用执行,跳过 - end = true; - // 小于0的话,直接return,如果等于0,那还是得走动画逻辑,将end属性设置上去 - if (d < 0) { - return end; - } - } - loop = Math.floor(rawPosition / d); - position = rawPosition - loop * d; - - // 计算rawPosition - end = rawPosition >= loopCount * d + d; - // 如果结束,跳过 - if (end) { - position = d; - loop = loopCount; - rawPosition = position * loop + d; - } - - if (rawPosition === prevRawPos) { - return end; - } - - // reverse动画 - const rev = !this.reversed !== !(this.bounce && loop % 2); - if (rev) { - position = d - position; - } - - this._deltaPosition = position - this.position; - this.position = position; - this.rawPosition = rawPosition + startAt; - - this.updatePosition(end, rev); - - return end; - } - - protected updatePosition(end: boolean, rev: boolean) { - if (!this.stepHead) { - return; - } - let step = this.stepHead.next; - const position = this.position; - const duration = this.duration; - if (this.target && step) { - let stepNext = step.next; - while (stepNext && stepNext.position <= position) { - step = stepNext; - stepNext = step.next; - } - let ratio = end ? (duration === 0 ? 1 : position / duration) : (position - step.position) / step.duration; // TODO: revisit this. - if (step.easing) { - ratio = step.easing(ratio); - } - // 判断这次和上次过程中是否经历了自定义step,如果跳过了自定义step那么执行自定义step的onEnd - this.tryCallCustomAnimateLifeCycle(step, this._lastStep || (rev ? this.stepTail : this.stepHead), rev); - // if (step !== this._lastStep) { - // if (this._deltaPosition > 0) { - // let _step = step.prev; - // while (_step && _step !== this._lastStep) { - // if (_step.customAnimate) { - // _step.customAnimate.onEnd(); - // } - // _step = _step.prev; - // } - // if (_step && _step.customAnimate) { - // _step.customAnimate.onEnd(); - // } - // } else if (this._deltaPosition < 0) { - // let _step = step.next; - // while (_step && _step !== this._lastStep) { - // if (_step.customAnimate) { - // _step.customAnimate.onEnd(); - // } - // _step = _step.next; - // } - // if (_step && _step.customAnimate) { - // _step.customAnimate.onEnd(); - // } - // } - // } - this.updateTarget(step, ratio, end); - this._lastStep = step; - - this.animate._onFrame && this.animate._onFrame.forEach(cb => cb(step, ratio)); - } - } - - // 如果动画卡顿跳过了自定义动画,那么尝试执行自定义动画的生命周期 - tryCallCustomAnimateLifeCycle(step: IStep, lastStep: IStep, rev: boolean) { - if (step === lastStep) { - return; - } - if (rev) { - let _step = lastStep.prev; - while (_step && _step !== step) { - if (_step.customAnimate) { - _step.customAnimate.onStart && _step.customAnimate.onStart(); - _step.customAnimate.onEnd && _step.customAnimate.onEnd(); - } - _step = step.prev; - } - // 执行lastStep的onEnd和currentStep的onStart - if (lastStep && lastStep.customAnimate) { - lastStep.customAnimate.onEnd && lastStep.customAnimate.onEnd(); - } - if (step && step.customAnimate) { - step.customAnimate.onStart && step.customAnimate.onStart(); - } - } else { - let _step = lastStep.next; - while (_step && _step !== step) { - if (_step.customAnimate) { - _step.customAnimate.onStart && _step.customAnimate.onStart(); - _step.customAnimate.onEnd && _step.customAnimate.onEnd(); - } - _step = _step.next; - } - // 执行lastStep的onEnd和currentStep的onStart - if (lastStep && lastStep.customAnimate) { - lastStep.customAnimate.onEnd && lastStep.customAnimate.onEnd(); - } - if (step && step.customAnimate) { - step.customAnimate.onStart && step.customAnimate.onStart(); - } - } - } - - /** - * 获取这个属性的上一个值 - * @param name - * @param step - * @returns - */ - getLastPropByName(name: string, step: Step): any { - let lastStep = step.prev; - while (lastStep) { - if (lastStep.props && lastStep.props[name] !== undefined) { - return lastStep.props[name]; - } else if (lastStep.customAnimate) { - const val = lastStep.customAnimate.getEndProps()[name]; - if (val !== undefined) { - return val; - } - } - lastStep = lastStep.prev; - } - - Logger.getInstance().warn('未知错误,step中找不到属性'); - return step.props[name]; - } - - protected updateTarget(step: Step, ratio: number, end: boolean) { - if (step.props == null && step.customAnimate == null) { - return; - } - this.target.onStep(this, this.animate, step, ratio, end); - } -} - -// export class Animate implements IAnimate { -// declare target: IAnimateTarget; -// declare timeline: ITimeline; -// protected declare stepHead: Step; -// protected declare stepTail: Step; -// declare nextAnimate?: Animate; -// declare prevAnimate?: Animate; -// // 结束时反转动画 -// declare bounce: boolean; -// // 是否reverse -// declare reversed: boolean; -// // 循环次数,0为执行一次,1为执行两次,Infinity为无限循环 -// declare loop: number; -// // 持续时间,不包括循环 -// declare duration: number; -// // 当前Animate的状态,正常,暂停,结束 -// declare status: AnimateStatus; -// // 位置,在[0, duration]之间 -// declare position: number; -// // 绝对的位置,在[0, loops * duration]之间 -// declare rawPosition: number; -// // 开始时间 -// protected declare _startAt: number; -// // 时间的缩放,例如2表示2倍速 -// declare timeScale: number; -// declare props: Record; -// declare readonly id: string | number; - -// protected declare _onStart?: (() => void)[]; -// protected declare _onFrame?: ((step: IStep, ratio: number) => void)[]; -// protected declare _onEnd?: (() => void)[]; -// declare _onRemove?: (() => void)[]; -// declare _preventAttrs?: Set; - -// constructor(id: string | number = Generator.GenAutoIncrementId(), timeline: ITimeline = defaultTimeline) { -// this.timeline = timeline; -// this.status = AnimateStatus.INITIAL; -// this.rawPosition = -1; -// this.position = 0; -// this.loop = 0; -// this.timeline.addAnimate(this); -// this.timeScale = 1; -// this.id = id; -// this.props = {}; -// this.stepHead = new Step(0, 0, {}); -// this.stepTail = this.stepHead; -// } - -// preventAttr(key: string) { -// if (!this._preventAttrs) { -// this._preventAttrs = new Set(); -// } -// this._preventAttrs.add(key); -// } -// preventAttrs(keys: string[]) { -// keys.forEach(key => this.preventAttr(key)); -// } -// validAttr(key: string): boolean { -// if (!this._preventAttrs) { -// return true; -// } -// return !this._preventAttrs.has(key); -// } - -// getLastPropByName(name: string, step: Step): any { -// let lastStep = step.prev; -// while (lastStep) { -// if (lastStep.props && lastStep.props[name] !== undefined) { -// return lastStep.props[name]; -// } -// lastStep = lastStep.prev; -// } -// let val = this.props[name]; -// if (!val) { -// console.warn('未知错误,step中找不到属性'); -// val = this.target.getComputedAttribute(name); -// this.props[name] = val; -// } - -// return val; -// } - -// bind(target: IAnimateTarget) { -// this.target = target; -// this.duration = 0; -// return this; -// } - -// startAt(t: number) { -// if (t < 0) { -// return this; -// } -// this._startAt = t; -// return this; -// } - -// to(props: Record, duration: number, easing: EasingType, params?: IStepConfig) { -// if (duration == null || duration < 0) { -// duration = 0; -// } - -// const easingFunc = typeof easing === 'string' ? Easing[easing] : easing; - -// const step = this._addStep(duration, null, easingFunc); -// this._appendProps(props, step, params ? params.tempProps : false); -// return this; -// } - -// wait(duration: number) { -// if (duration > 0) { -// const step = this._addStep(+duration, null); -// if (step.prev) { -// step.props = step.prev.props; -// } -// if (this.target.onAddStep) { -// this.target.onAddStep(step); -// } -// } -// return this; -// } - -// protected _addStep(duration: number, props: any, easingFunc?: EasingTypeFunc) { -// const step = new Step(this.duration, duration, props, easingFunc); -// this.duration += duration; -// this.stepTail.append(step); -// this.stepTail = step; -// return step; -// } - -// protected _appendProps(props: any, step: Step, tempProps?: boolean) { -// if (tempProps) { -// step.props = props; -// } else { -// // todo: 是否需要深拷贝props -// step.props = Object.assign({}, props); -// } -// let lastStep = step.prev; -// const _props = step.props; -// // 拷贝之前的step阶段属性 -// while (lastStep.prev) { -// if (lastStep.props) { -// if (!lastStep.propKeys) { -// lastStep.propKeys = Object.keys(lastStep.props); -// } -// lastStep.propKeys.forEach(key => { -// if (_props[key] === undefined) { -// _props[key] = lastStep.props[key]; -// } -// }); -// } -// lastStep = lastStep.prev; -// } - -// // 设置最初的props属性 -// const initProps = this.stepHead.props; -// if (!step.propKeys) { -// step.propKeys = Object.keys(_props); -// step.propKeys.forEach(key => { -// initProps[key] = this.target.getComputedAttribute(key); -// }); -// } - -// if (this.target.onAddStep) { -// this.target.onAddStep(step); -// } -// } - -// advance(delta: number) { -// if (this.status === AnimateStatus.INITIAL) { -// this.status = AnimateStatus.RUNNING; -// this._onStart && this._onStart.forEach(cb => cb()); -// } -// const end = this.setPosition(this.rawPosition + delta * this.timeScale); -// if (end && this.status === AnimateStatus.RUNNING) { -// this.status = AnimateStatus.END; -// this._onEnd && this._onEnd.forEach(cb => cb()); -// } -// } - -// setPosition(rawPosition: number) { -// const d = this.duration; -// const loopCount = this.loop; -// const prevRawPos = this.rawPosition; -// let end = false; -// let loop: number; // 当前是第几次循环 -// let position: number; // 当前周期的时间 -// const startAt = this._startAt ?? 0; - -// if (rawPosition < 0) { -// rawPosition = 0; -// } -// if (rawPosition < startAt) { -// this.rawPosition = rawPosition; -// return false; -// } -// rawPosition = rawPosition - startAt; -// if (d <= 0) { -// // 如果不用执行,跳过 -// end = true; -// return end; -// } -// loop = Math.floor(rawPosition / d); -// position = rawPosition - loop * d; - -// // 计算rawPosition -// end = rawPosition >= loopCount * d + d; -// // 如果结束,跳过 -// if (end) { -// position = d; -// loop = loopCount; -// rawPosition = position * loop + d; -// } - -// if (rawPosition === prevRawPos) { -// return end; -// } - -// // reverse动画 -// const rev = !this.reversed !== !(this.bounce && loop % 2); -// if (rev) { -// position = d - position; -// } - -// this.position = position; -// this.rawPosition = rawPosition + startAt; - -// this.updatePosition(end); - -// return end; -// } - -// protected updatePosition(end: boolean) { -// if (!this.stepHead) { -// return; -// } -// let step = this.stepHead; -// const position = this.position; -// const duration = this.duration; -// if (this.target && step) { -// let stepNext = step.next; -// while (stepNext && stepNext.position <= position) { -// step = step.next; -// stepNext = step.next; -// } -// let ratio = end ? (duration === 0 ? 1 : position / duration) : (position - step.position) / step.duration; // TODO: revisit this. -// if (step.easing) { -// ratio = step.easing(ratio); -// } -// this.updateTarget(step, ratio, end); -// this._onFrame && this._onFrame.forEach(cb => cb(step, ratio)); -// } -// } - -// protected updateTarget(step: Step, ratio: number, end: boolean) { -// if (step.props == null) { -// return; -// } -// this.target.onStep(this, step, ratio, end); -// } - -// onStart(cb: () => void) { -// if (!this._onStart) { -// this._onStart = []; -// } -// this._onStart.push(cb); -// } -// onEnd(cb: () => void) { -// if (!this._onEnd) { -// this._onEnd = []; -// } -// this._onEnd.push(cb); -// } -// onRemove(cb: () => void) { -// if (!this._onRemove) { -// this._onRemove = []; -// } -// this._onRemove.push(cb); -// } -// onFrame(cb: (step: IStep, ratio: number) => void) { -// if (!this._onFrame) { -// this._onFrame = []; -// } -// this._onFrame.push(cb); -// } - -// getStartProps() { -// return this.stepHead?.props; -// } - -// getEndProps(target: Record = {}) { -// let step = this.stepHead; -// while (step) { -// if (step.props) { -// Object.assign(target, step.props); -// } -// step = step.next; -// } - -// return target; -// } - -// stop(nextVal?: 'start' | 'end' | Record) { -// this.status = AnimateStatus.END; -// if (!nextVal) { -// this.target.onStop(); -// } -// if (nextVal === 'start') { -// this.target.onStop(this.getStartProps()); -// } else if (nextVal === 'end') { -// this.target.onStop(this.getEndProps()); -// } else { -// this.target.onStop(nextVal); -// } -// } - -// release() { -// this.status = AnimateStatus.END; -// return; -// } -// } - -class Step implements IStep { - declare prev?: Step; - // 持续时间 - declare duration: number; - // 在animate中的位置 - declare position: number; - declare next?: Step; - declare props: any; - // 保存解析后的props,用于性能优化 - declare parsedProps?: any; - declare propKeys?: string[]; - declare easing?: EasingTypeFunc; - declare customAnimate?: ICustomAnimate; - // passive: boolean; - // index: number; - type: IAnimateStepType; - - constructor(position: number, duration: number, props?: any, easing?: EasingTypeFunc) { - this.duration = duration; - this.position = position; - this.props = props; - this.easing = easing; - } - - append(step: Step) { - step.prev = this; - step.next = this.next; - this.next = step; - } - - getLastProps() { - let step = this.prev; - while (step) { - if (step.props) { - return step.props; - } else if (step.customAnimate) { - return step.customAnimate.getMergedEndProps(); - } - step = step.prev as any; - } - return null; - } -} diff --git a/packages/vrender-core/src/animate/config.ts b/packages/vrender-core/src/animate/config.ts deleted file mode 100644 index 4cd0b74c3..000000000 --- a/packages/vrender-core/src/animate/config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { IAnimateConfig } from './../interface/graphic'; - -export const DefaultStateAnimateConfig: IAnimateConfig = { - duration: 200, - easing: 'cubicOut' -}; - -export const DefaultMorphingAnimateConfig: IAnimateConfig = { - duration: 1000, - easing: 'quadInOut' -}; diff --git a/packages/vrender-core/src/animate/custom-animate.ts b/packages/vrender-core/src/animate/custom-animate.ts deleted file mode 100644 index 5c0c04f99..000000000 --- a/packages/vrender-core/src/animate/custom-animate.ts +++ /dev/null @@ -1,1364 +0,0 @@ -import type { IPoint, IPointLike } from '@visactor/vutils'; -import { - clamp, - getDecimalPlaces, - isArray, - isNumber, - isValidNumber, - pi, - pi2, - Point, - PointService -} from '@visactor/vutils'; -import { application } from '../application'; -import { AttributeUpdateType } from '../common/enums'; -import { CustomPath2D } from '../common/custom-path2d'; -import type { - EasingType, - IArcGraphicAttribute, - IArea, - IAreaCacheItem, - ICubicBezierCurve, - ICurve, - ICustomPath2D, - IGraphic, - IGroup, - ILine, - ILineAttribute, - ILinearGradient, - IRect, - IRectAttribute, - IRectGraphicAttribute, - ISegment, - IShadowRoot -} from '../interface'; -import { ACustomAnimate } from './animate'; -import { Easing } from './easing'; -import { pointInterpolation } from '../common/utils'; -import { divideCubic } from '../common/segment/curve/cubic-bezier'; - -export class IncreaseCount extends ACustomAnimate<{ text: string | number }> { - declare valid: boolean; - - private fromNumber: number; - private toNumber: number; - private decimalLength: number; - - constructor( - from: { text: string | number }, - to: { text: string | number }, - duration: number, - easing: EasingType, - params?: { fixed?: boolean } - ) { - super(from, to, duration, easing, params); - } - - getEndProps(): Record | void { - if (this.valid === false) { - return {}; - } - return { - text: this.to - }; - } - - onBind(): void { - this.fromNumber = isNumber(this.from?.text) ? this.from?.text : Number.parseFloat(this.from?.text); - this.toNumber = isNumber(this.to?.text) ? this.to?.text : Number.parseFloat(this.to?.text); - if (!Number.isFinite(this.toNumber)) { - this.fromNumber = 0; - } - if (!Number.isFinite(this.toNumber)) { - this.valid = false; - } - if (this.valid !== false) { - this.decimalLength = - this.params?.fixed ?? Math.max(getDecimalPlaces(this.fromNumber), getDecimalPlaces(this.toNumber)); - } - } - - onEnd(): void { - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - if (this.valid === false) { - return; - } - if (end) { - out.text = this.to?.text; - } else { - out.text = (this.fromNumber + (this.toNumber - this.fromNumber) * ratio).toFixed(this.decimalLength); - } - } -} - -enum Direction { - LEFT_TO_RIGHT = 0, - RIGHT_TO_LEFT = 1, - TOP_TO_BOTTOM = 2, - BOTTOM_TO_TOP = 3, - STROKE = 4 -} -export class FadeInPlus extends ACustomAnimate { - declare direction: number; - declare toFill: string; - declare toStroke: string; - declare fillGradient: ILinearGradient; - declare strokeGradient: ILinearGradient; - declare fill: boolean; - declare stroke: boolean; - constructor( - from: any, - to: any, - duration: number, - easing: EasingType, - params?: { direction?: number; fill?: boolean; stroke?: boolean } - ) { - super(from, to, duration, easing, params); - const { direction = Direction.LEFT_TO_RIGHT, fill = true, stroke = true } = params || {}; - this.direction = direction; - this.fill = fill; - this.stroke = stroke; - this.fillGradient = { - gradient: 'linear', - stops: [] - }; - this.strokeGradient = { - gradient: 'linear', - stops: [] - }; - } - - getEndProps(): Record { - return { - fill: this.toFill, - stroke: this.toStroke - }; - } - - onBind(): void { - // this.to = parseFloat(this.target.getAnimatePropByName('text')); - this.toFill = this.target.getComputedAttribute('fill'); - this.toStroke = this.target.getComputedAttribute('stroke'); - } - - onEnd(): void { - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - if (!this.toFill) { - return; - } - if (!this.toStroke) { - return; - } - switch (this.direction) { - case Direction.RIGHT_TO_LEFT: - this.rightToLeft(end, ratio, out); - break; - case Direction.TOP_TO_BOTTOM: - this.topToBottom(end, ratio, out); - break; - case Direction.BOTTOM_TO_TOP: - this.bottomToTop(end, ratio, out); - break; - case Direction.STROKE: - this.strokePath(end, ratio, out); - break; - default: - this.leftToRight(end, ratio, out); - break; - } - } - - leftToRight(end: boolean, ratio: number, out: Record) { - if (this.fill) { - const toFillColor = this.toFill; - this.fillGradient.x0 = 0; - this.fillGradient.y0 = 0; - this.fillGradient.x1 = 1; - this.fillGradient.y1 = 0; - this.fillGradient.stops = [ - { offset: 0, color: toFillColor }, - { offset: ratio, color: toFillColor }, - { offset: Math.min(1, ratio * 2), color: 'transparent' } - ]; - out.fill = this.fillGradient; - } - if (this.stroke) { - const toStrokeColor = this.toStroke; - this.strokeGradient.x0 = 0; - this.strokeGradient.y0 = 0; - this.strokeGradient.x1 = 1; - this.strokeGradient.y1 = 0; - this.strokeGradient.stops = [ - { offset: 0, color: toStrokeColor }, - { offset: ratio, color: toStrokeColor }, - { offset: Math.min(1, ratio * 6), color: 'transparent' } - ]; - out.stroke = this.strokeGradient; - // const dashLen = 300; - // const offset = ratio * dashLen; - // out.lineDash = [offset, dashLen - offset]; - } - return; - } - - strokePath(end: boolean, ratio: number, out: Record) { - if (this.fill) { - const toFillColor = this.toFill; - this.fillGradient.x0 = 0; - this.fillGradient.y0 = 0; - this.fillGradient.x1 = 1; - this.fillGradient.y1 = 0; - this.fillGradient.stops = [ - { offset: 0, color: toFillColor }, - { offset: ratio, color: toFillColor }, - { offset: Math.min(1, ratio * 2), color: 'transparent' } - ]; - out.fill = this.fillGradient; - } - if (this.stroke) { - const dashLen = 300; - const offset = ratio * dashLen; - out.lineDash = [offset, dashLen - offset]; - } - return; - } - rightToLeft(end: boolean, ratio: number, out: Record) { - return; - } - topToBottom(end: boolean, ratio: number, out: Record) { - return; - } - bottomToTop(end: boolean, ratio: number, out: Record) { - return; - } -} - -export class InputText extends ACustomAnimate<{ text: string }> { - declare valid: boolean; - declare target: IGraphic; - - private fromText: string = ''; - private toText: string | string[] = ''; - - getEndProps(): Record { - if (this.valid === false) { - return {}; - } - return { - text: this.to - }; - } - - onBind(): void { - this.fromText = this.from?.text ?? ''; - this.toText = this.to?.text || ''; - if (!this.toText || (isArray(this.toText) && this.toText.length === 0)) { - this.valid = false; - } - if (isArray(this.toText)) { - this.toText = this.toText.map(item => (item || '').toString()); - } - // else { - // this.toText = this.toText.toString(); - // // const root = this.target.attachShadow(); - // // const line = application.graphicService.creator.line({ - // // x: 0, - // // y: 0, - // // points: [ - // // { x: 0, y: 0 }, - // // { x: 0, y: this.target.getComputedAttribute('fontSize') } - // // ], - // // stroke: 'black', - // // lineWidth: 1 - // // }); - // // root.add(line); - // } - } - - onEnd(): void { - this.target.detachShadow(); - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - if (this.valid === false) { - return; - } - // update text - const fromCount = this.fromText.length; - const toTextIsArray = isArray(this.toText); - const toCount = toTextIsArray - ? (this.toText as unknown as string[]).reduce((c, t) => c + (t || '').length, 0) - : this.toText.length; - const count = Math.ceil(fromCount + (toCount - fromCount) * ratio); - - if (toTextIsArray) { - out.text = []; - let len = 0; - (this.toText as unknown as string[]).forEach(t => { - if (len + t.length > count) { - out.text.push(t.substr(0, count - len)); - len = count; - } else { - out.text.push(t); - len += t.length; - } - }); - } else { - out.text = (this.toText as string).substr(0, count); - } - // console.log(out.text) - - // update line position - // const line = this.target.shadowRoot?.at(0) as IGraphic; - // const endX = (this.target as any).clipedWidth + 2; - // line.setAttribute('x', endX); - } -} - -export class StreamLight extends ACustomAnimate { - declare valid: boolean; - declare target: IGraphic; - - declare rect: IRect; - declare line: ILine; - declare area: IArea; - constructor( - from: any, - to: any, - duration: number, - easing: EasingType, - params?: { attribute?: Partial; streamLength?: number; isHorizontal?: boolean } - ) { - super(from, to, duration, easing, params); - } - - getEndProps(): Record { - return {}; - } - - onStart(): void { - if (!this.target) { - return; - } - if (this.target.type === 'rect') { - this.onStartRect(); - } else if (this.target.type === 'line') { - this.onStartLineOrArea('line'); - } else if (this.target.type === 'area') { - this.onStartLineOrArea('area'); - } - } - - onStartLineOrArea(type: 'line' | 'area') { - const root = this.target.attachShadow(); - const line = application.graphicService.creator[type]({ - ...this.params?.attribute - }); - this[type] = line; - line.pathProxy = new CustomPath2D(); - root.add(line); - } - - onStartRect(): void { - const root = this.target.attachShadow(); - - const isHorizontal = this.params?.isHorizontal ?? true; - const sizeAttr = isHorizontal ? 'height' : 'width'; - const otherSizeAttr = isHorizontal ? 'width' : 'height'; - const size = this.target.AABBBounds[sizeAttr](); - const y = isHorizontal ? 0 : this.target.AABBBounds.y1; - - const rect = application.graphicService.creator.rect({ - [sizeAttr]: size, - fill: '#bcdeff', - shadowBlur: 30, - shadowColor: '#bcdeff', - ...this.params?.attribute, - x: 0, - y, - [otherSizeAttr]: 0 - }); - this.rect = rect; - root.add(rect); - } - - onBind(): void { - return; - } - - onEnd(): void { - this.target.detachShadow(); - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - if (this.rect) { - return this.onUpdateRect(end, ratio, out); - } else if (this.line || this.area) { - return this.onUpdateLineOrArea(end, ratio, out); - } - } - - protected onUpdateRect(end: boolean, ratio: number, out: Record): void { - const isHorizontal = this.params?.isHorizontal ?? true; - const parentAttr = (this.target as any).attribute; - if (isHorizontal) { - const parentWidth = parentAttr.width ?? Math.abs(parentAttr.x1 - parentAttr.x) ?? 250; - const streamLength = this.params?.streamLength ?? parentWidth; - const maxLength = this.params?.attribute?.width ?? 60; - // 起点,rect x右端点 对齐 parent左端点 - // 如果parent.x1 < parent.x, 需要把rect属性移到parent x1的位置上, 因为初始 rect.x = parent.x - const startX = -maxLength; - // 插值 - const currentX = startX + (streamLength - startX) * ratio; - // 位置限定 > 0 - const x = Math.max(currentX, 0); - // 宽度计算 - const w = Math.min(Math.min(currentX + maxLength, maxLength), streamLength - currentX); - // 如果 rect右端点 超出 parent右端点, 宽度动态调整 - const width = w + x > parentWidth ? Math.max(parentWidth - x, 0) : w; - this.rect.setAttributes( - { - x, - width, - dx: Math.min(parentAttr.x1 - parentAttr.x, 0) - } as any, - false, - { - type: AttributeUpdateType.ANIMATE_PLAY, - animationState: { - ratio, - end - } - } - ); - } else { - const parentHeight = parentAttr.height ?? Math.abs(parentAttr.y1 - parentAttr.y) ?? 250; - const streamLength = this.params?.streamLength ?? parentHeight; - const maxLength = this.params?.attribute?.height ?? 60; - // 起点,y上端点 对齐 parent下端点 - const startY = parentHeight; - // 插值 - const currentY = startY - (streamLength + maxLength) * ratio; - // 位置限定 < parentHeight - let y = Math.min(currentY, parentHeight); - // 高度最小值 - const h = Math.min(parentHeight - currentY, maxLength); - // 如果 rect上端点=y 超出 parent上端点 = 0, 则高度不断变小 - let height; - if (y <= 0) { - // 必须先得到高度再将y置为0, 顺序很重要 - height = Math.max(y + h, 0); - y = 0; - } else { - height = h; - } - this.rect.setAttributes( - { - y, - height, - dy: Math.min(parentAttr.y1 - parentAttr.y, 0) - } as any, - false, - { - type: AttributeUpdateType.ANIMATE_PLAY, - animationState: { - ratio, - end - } - } - ); - } - } - - protected onUpdateLineOrArea(end: boolean, ratio: number, out: Record) { - const target = this.line || this.area; - if (!target) { - return; - } - const customPath = target.pathProxy as ICustomPath2D; - const targetLine = this.target as ILine | IArea; - if (targetLine.cache || targetLine.cacheArea) { - this._onUpdateLineOrAreaWithCache(customPath, targetLine, end, ratio, out); - } else { - this._onUpdateLineWithoutCache(customPath, targetLine, end, ratio, out); - } - const targetAttrs = targetLine.attribute; - target.setAttributes({ - stroke: targetAttrs.stroke, - ...target.attribute - }); - target.addUpdateBoundTag(); - } - - // 针对有cache的linear - protected _onUpdateLineOrAreaWithCache( - customPath: ICustomPath2D, - g: ILine | IArea, - end: boolean, - ratio: number, - out: Record - ) { - customPath.clear(); - if (g.type === 'line') { - let cache = g.cache; - if (!Array.isArray(cache)) { - cache = [cache]; - } - const totalLen = cache.reduce((l: any, c: any) => l + c.getLength(), 0); - const curves: ICurve[] = []; - cache.forEach((c: any) => { - c.curves.forEach((ci: any) => curves.push(ci)); - }); - return this._updateCurves(customPath, curves, totalLen, ratio); - } else if (g.type === 'area' && g.cacheArea?.top?.curves) { - const cache = g.cacheArea as IAreaCacheItem; - const totalLen = cache.top.curves.reduce((a, b) => a + b.getLength(), 0); - return this._updateCurves(customPath, cache.top.curves, totalLen, ratio); - } - } - - protected _updateCurves(customPath: ICustomPath2D, curves: ICurve[], totalLen: number, ratio: number) { - const startLen = totalLen * ratio; - const endLen = Math.min(startLen + this.params?.streamLength ?? 10, totalLen); - let lastLen = 0; - let start = false; - for (let i = 0; i < curves.length; i++) { - if (curves[i].defined !== false) { - const curveItem = curves[i]; - const len = curveItem.getLength(); - const startPercent = 1 - (lastLen + len - startLen) / len; - let endPercent = 1 - (lastLen + len - endLen) / len; - let curveForStart: ICubicBezierCurve; - if (lastLen < startLen && lastLen + len > startLen) { - start = true; - if (curveItem.p2 && curveItem.p3) { - const [_, curve2] = divideCubic(curveItem as ICubicBezierCurve, startPercent); - customPath.moveTo(curve2.p0.x, curve2.p0.y); - curveForStart = curve2; - // console.log(curve2.p0.x, curve2.p0.y); - } else { - const p = curveItem.getPointAt(startPercent); - customPath.moveTo(p.x, p.y); - } - } - if (lastLen < endLen && lastLen + len > endLen) { - if (curveItem.p2 && curveItem.p3) { - if (curveForStart) { - endPercent = (endLen - startLen) / curveForStart.getLength(); - } - const [curve1] = divideCubic(curveForStart || (curveItem as ICubicBezierCurve), endPercent); - customPath.bezierCurveTo(curve1.p1.x, curve1.p1.y, curve1.p2.x, curve1.p2.y, curve1.p3.x, curve1.p3.y); - } else { - const p = curveItem.getPointAt(endPercent); - customPath.lineTo(p.x, p.y); - } - break; - } else if (start) { - if (curveItem.p2 && curveItem.p3) { - const curve = curveForStart || curveItem; - customPath.bezierCurveTo(curve.p1.x, curve.p1.y, curve.p2.x, curve.p2.y, curve.p3.x, curve.p3.y); - } else { - customPath.lineTo(curveItem.p1.x, curveItem.p1.y); - } - } - lastLen += len; - } - } - } - - // 只针对最简单的linear - protected _onUpdateLineWithoutCache( - customPath: ICustomPath2D, - line: ILine, - end: boolean, - ratio: number, - out: Record - ) { - const { points, curveType } = line.attribute; - if (!points || points.length < 2 || curveType !== 'linear') { - return; - } - let totalLen = 0; - for (let i = 1; i < points.length; i++) { - totalLen += PointService.distancePP(points[i], points[i - 1]); - } - const startLen = totalLen * ratio; - const endLen = Math.min(startLen + this.params?.streamLength ?? 10, totalLen); - const nextPoints = []; - let lastLen = 0; - for (let i = 1; i < points.length; i++) { - const len = PointService.distancePP(points[i], points[i - 1]); - if (lastLen < startLen && lastLen + len > startLen) { - nextPoints.push(PointService.pointAtPP(points[i - 1], points[i], 1 - (lastLen + len - startLen) / len)); - } - if (lastLen < endLen && lastLen + len > endLen) { - nextPoints.push(PointService.pointAtPP(points[i - 1], points[i], 1 - (lastLen + len - endLen) / len)); - break; - } else if (nextPoints.length) { - nextPoints.push(points[i]); - } - lastLen += len; - } - - if (!nextPoints.length || nextPoints.length < 2) { - return; - } - customPath.clear(); - customPath.moveTo(nextPoints[0].x, nextPoints[0].y); - for (let i = 1; i < nextPoints.length; i++) { - customPath.lineTo(nextPoints[i].x, nextPoints[i].y); - } - } -} - -export class Meteor extends ACustomAnimate { - declare size: number; - declare target: IGraphic; - declare root: IShadowRoot; - declare posList: IPoint[]; - - get lastPos(): IPoint { - return this.posList[this.posList.length - 1]; - } - - constructor(size: number, duration: number, easing: EasingType, params?: any) { - super(null, null, duration, easing, params); - this.size = size; - this.posList = []; - } - - onBind(): void { - const root = this.target.attachShadow(); - this.root = root; - for (let i = 0; i < this.size; i++) { - const g = this.target.clone(); - const scale = Math.min(((this.size - i) / this.size) * 3, 1); - const opacity = Math.min(0.2 + 0.7 / this.size); - g.setAttributes({ x: 0, y: 0, dx: 0, dy: 0, scaleX: scale, scaleY: scale, opacity }, false, { - type: AttributeUpdateType.ANIMATE_BIND - }); - root.add(g); - } - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - if (end) { - this.target.detachShadow(); - this.posList.length = 0; - return; - } - - const x = this.target.getComputedAttribute('x'); - const y = this.target.getComputedAttribute('y'); - - const nextPos = new Point(x, y); - if (!this.posList.length) { - this.posList.push(nextPos); - return; - } - - this.target.shadowRoot.forEachChildren((g: IGraphic, i) => { - const pos = this.posList[Math.max(this.posList.length - i - 1, 0)]; - g.setAttributes( - { - x: pos.x - x, - y: pos.y - y - }, - false - ); - }); - - this.posList.push(nextPos); - } -} - -export class MotionPath extends ACustomAnimate { - declare valid: boolean; - declare pathLength: number; - declare path: CustomPath2D; - declare distance: number; - declare initAngle: number; - declare changeAngle: boolean; - declare cb?: (from: any, to: any, ratio: number, target: IGraphic) => void; - constructor( - from: any, - to: any, - duration: number, - easing: EasingType, - params?: { - path: CustomPath2D; - distance: number; - cb?: (from: any, to: any, ratio: number, target: IGraphic) => void; - initAngle?: number; - changeAngle?: boolean; - } - ) { - super(from, to, duration, easing, params); - if (params) { - this.pathLength = params.path.getLength(); - this.path = params.path; - this.distance = params.distance; - this.to = params.distance * this.pathLength; - this.initAngle = params.initAngle ?? 0; - this.changeAngle = !!params.changeAngle; - this.cb = params.cb; - } - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - // 计算位置 - const at = this.to * ratio; - const { pos, angle } = this.path.getAttrAt(at); - out.x = pos.x; - out.y = pos.y; - if (this.changeAngle) { - out.angle = angle + this.initAngle; - } - this.cb && this.cb(this.from, this.to, ratio, this.target as IGraphic); - // out.angle = angle + this.initAngle; - } -} - -export class TagPointsUpdate extends ACustomAnimate<{ points?: IPointLike[]; segments?: ISegment[] }> { - protected fromPoints: IPointLike[]; - protected toPoints: IPointLike[]; - protected points: IPointLike[]; - protected interpolatePoints: [IPointLike, IPointLike][]; - protected newPointAnimateType: 'grow' | 'appear' | 'clip'; - protected clipRange: number; - protected shrinkClipRange: number; - protected clipRangeByDimension: 'x' | 'y'; - protected segmentsCache: number[]; - - constructor( - from: any, - to: any, - duration: number, - easing: EasingType, - params?: { newPointAnimateType?: 'grow' | 'appear' | 'clip'; clipRangeByDimension?: 'x' | 'y' } - ) { - super(from, to, duration, easing, params); - this.newPointAnimateType = params?.newPointAnimateType ?? 'grow'; - this.clipRangeByDimension = params?.clipRangeByDimension ?? 'x'; - } - - private getPoints(attribute: typeof this.from, cache = false): IPointLike[] { - if (attribute.points) { - return attribute.points; - } - - if (attribute.segments) { - const points = [] as IPointLike[]; - if (!this.segmentsCache) { - this.segmentsCache = []; - } - attribute.segments.map(segment => { - if (segment.points) { - points.push(...segment.points); - } - if (cache) { - this.segmentsCache.push(segment.points?.length ?? 0); - } - }); - return points; - } - return []; - } - - onBind(): void { - const originFromPoints = this.getPoints(this.from); - const originToPoints = this.getPoints(this.to, true); - this.fromPoints = !originFromPoints ? [] : !Array.isArray(originFromPoints) ? [originFromPoints] : originFromPoints; - this.toPoints = !originToPoints ? [] : !Array.isArray(originToPoints) ? [originToPoints] : originToPoints; - - const tagMap = new Map(); - this.fromPoints.forEach(point => { - if (point.context) { - tagMap.set(point.context, point); - } - }); - let firstMatchedIndex = Infinity; - let lastMatchedIndex = -Infinity; - let firstMatchedPoint: IPointLike; - let lastMatchedPoint: IPointLike; - for (let i = 0; i < this.toPoints.length; i += 1) { - if (tagMap.has(this.toPoints[i].context)) { - firstMatchedIndex = i; - firstMatchedPoint = tagMap.get(this.toPoints[i].context); - break; - } - } - for (let i = this.toPoints.length - 1; i >= 0; i -= 1) { - if (tagMap.has(this.toPoints[i].context)) { - lastMatchedIndex = i; - lastMatchedPoint = tagMap.get(this.toPoints[i].context); - break; - } - } - - if (this.newPointAnimateType === 'clip') { - if (this.toPoints.length !== 0) { - if (Number.isFinite(lastMatchedIndex)) { - this.clipRange = - this.toPoints[lastMatchedIndex][this.clipRangeByDimension] / - this.toPoints[this.toPoints.length - 1][this.clipRangeByDimension]; - if (this.clipRange === 1) { - this.shrinkClipRange = - this.toPoints[lastMatchedIndex][this.clipRangeByDimension] / - this.fromPoints[this.fromPoints.length - 1][this.clipRangeByDimension]; - } - if (!isValidNumber(this.clipRange)) { - this.clipRange = 0; - } else { - this.clipRange = clamp(this.clipRange, 0, 1); - } - } else { - this.clipRange = 0; - } - } - } - // TODO: shrink removed points - // if no point is matched, animation should start from toPoint[0] - let prevMatchedPoint = this.toPoints[0]; - this.interpolatePoints = this.toPoints.map((point, index) => { - const matchedPoint = tagMap.get(point.context); - if (matchedPoint) { - prevMatchedPoint = matchedPoint; - return [matchedPoint, point]; - } - // appear new point - if (this.newPointAnimateType === 'appear' || this.newPointAnimateType === 'clip') { - return [point, point]; - } - // grow new point - if (index < firstMatchedIndex && firstMatchedPoint) { - return [firstMatchedPoint, point]; - } else if (index > lastMatchedIndex && lastMatchedPoint) { - return [lastMatchedPoint, point]; - } - return [prevMatchedPoint, point]; - }); - this.points = this.interpolatePoints.map(interpolate => { - const fromPoint = interpolate[0]; - const toPoint = interpolate[1]; - const newPoint = new Point(fromPoint.x, fromPoint.y, fromPoint.x1, fromPoint.y1); - newPoint.defined = toPoint.defined; - newPoint.context = toPoint.context; - return newPoint; - }); - } - - onFirstRun(): void { - const lastClipRange = this.target.attribute.clipRange; - if (isValidNumber(lastClipRange * this.clipRange)) { - this.clipRange *= lastClipRange; - } - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - // if not create new points, multi points animation might not work well. - this.points = this.points.map((point, index) => { - const newPoint = pointInterpolation(this.interpolatePoints[index][0], this.interpolatePoints[index][1], ratio); - newPoint.context = point.context; - return newPoint; - }); - if (this.clipRange) { - if (this.shrinkClipRange) { - // 折线变短 - if (!end) { - out.points = this.fromPoints; - out.clipRange = this.clipRange - (this.clipRange - this.shrinkClipRange) * ratio; - } else { - out.points = this.toPoints; - out.clipRange = 1; - } - return; - } - out.clipRange = this.clipRange + (1 - this.clipRange) * ratio; - } - if (this.segmentsCache && this.to.segments) { - let start = 0; - out.segments = this.to.segments.map((segment, index) => { - const end = start + this.segmentsCache[index]; - const points = this.points.slice(start, end); - start = end; - return { - ...segment, - points - }; - }); - } else { - out.points = this.points; - } - } -} - -export class GraphicAnimate extends ACustomAnimate { - graphic: IGraphic; - - constructor(from: any, to: any, duration: number, easing: EasingType, params?: { graphic: IGraphic }) { - super(from, to, duration, easing, params); - this.graphic = params?.graphic; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - if (!this.graphic) { - return; - } - Object.keys(this.from).forEach(k => { - out[k] = this.from[k] + (this.to[k] - this.from[k]) * ratio; - }); - } -} - -export class ClipGraphicAnimate extends ACustomAnimate { - private _group?: IGroup; - private _clipGraphic?: IGraphic; - protected clipFromAttribute?: any; - protected clipToAttribute?: any; - - private _lastClip?: boolean; - private _lastPath?: IGraphic[]; - - constructor( - from: any, - to: any, - duration: number, - easing: EasingType, - params: { group: IGroup; clipGraphic: IGraphic } - ) { - super(null, null, duration, easing, params); - this.clipFromAttribute = from; - this.clipToAttribute = to; - this._group = params?.group; - this._clipGraphic = params?.clipGraphic; - } - - onBind() { - if (this._group && this._clipGraphic) { - this._lastClip = this._group.attribute.clip; - this._lastPath = this._group.attribute.path; - this._group.setAttributes( - { - clip: true, - path: [this._clipGraphic] - }, - false, - { type: AttributeUpdateType.ANIMATE_BIND } - ); - } - } - - onEnd() { - if (this._group) { - this._group.setAttributes( - { - clip: this._lastClip, - path: this._lastPath - }, - false, - { type: AttributeUpdateType.ANIMATE_END } - ); - } - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - if (!this._clipGraphic) { - return; - } - const res: any = {}; - Object.keys(this.clipFromAttribute).forEach(k => { - res[k] = this.clipFromAttribute[k] + (this.clipToAttribute[k] - this.clipFromAttribute[k]) * ratio; - }); - this._clipGraphic.setAttributes(res, false, { - type: AttributeUpdateType.ANIMATE_UPDATE, - animationState: { ratio, end } - }); - } -} - -export class ClipAngleAnimate extends ClipGraphicAnimate { - constructor( - from: any, - to: any, - duration: number, - easing: EasingType, - params: { - group: IGroup; - center?: { x: number; y: number }; - startAngle?: number; - radius?: number; - orient?: 'clockwise' | 'anticlockwise'; - animationType?: 'in' | 'out'; - } - ) { - const groupAttribute = params?.group?.attribute ?? {}; - const width = groupAttribute.width ?? 0; - const height = groupAttribute.height ?? 0; - - const animationType = params?.animationType ?? 'in'; - const startAngle = params?.startAngle ?? 0; - const orient = params?.orient ?? 'clockwise'; - - let arcStartAngle = 0; - let arcEndAngle = 0; - if (orient === 'anticlockwise') { - arcEndAngle = animationType === 'in' ? startAngle + Math.PI * 2 : startAngle; - arcEndAngle = startAngle + Math.PI * 2; - } else { - arcStartAngle = startAngle; - arcEndAngle = animationType === 'out' ? startAngle + Math.PI * 2 : startAngle; - } - const arc = application.graphicService.creator.arc({ - x: params?.center?.x ?? width / 2, - y: params?.center?.y ?? height / 2, - outerRadius: params?.radius ?? (width + height) / 2, - innerRadius: 0, - startAngle: arcStartAngle, - endAngle: arcEndAngle, - fill: true - }); - let fromAttributes: Partial; - let toAttributes: Partial; - if (orient === 'anticlockwise') { - fromAttributes = { startAngle: startAngle + Math.PI * 2 }; - toAttributes = { startAngle: startAngle }; - } else { - fromAttributes = { endAngle: startAngle }; - toAttributes = { endAngle: startAngle + Math.PI * 2 }; - } - super( - animationType === 'in' ? fromAttributes : toAttributes, - animationType === 'in' ? toAttributes : fromAttributes, - duration, - easing, - { group: params?.group, clipGraphic: arc } - ); - } -} - -export class ClipRadiusAnimate extends ClipGraphicAnimate { - constructor( - from: any, - to: any, - duration: number, - easing: EasingType, - params: { - group: IGroup; - center?: { x: number; y: number }; - startRadius?: number; - endRadius?: number; - animationType?: 'in' | 'out'; - } - ) { - const groupAttribute = params?.group?.attribute ?? {}; - const width = groupAttribute.width ?? 0; - const height = groupAttribute.height ?? 0; - - const animationType = params?.animationType ?? 'in'; - const startRadius = params?.startRadius ?? 0; - const endRadius = params?.endRadius ?? Math.sqrt((width / 2) ** 2 + (height / 2) ** 2); - - const arc = application.graphicService.creator.arc({ - x: params?.center?.x ?? width / 2, - y: params?.center?.y ?? height / 2, - outerRadius: animationType === 'out' ? endRadius : startRadius, - innerRadius: 0, - startAngle: 0, - endAngle: Math.PI * 2, - fill: true - }); - const fromAttributes: Partial = { outerRadius: startRadius }; - const toAttributes: Partial = { outerRadius: endRadius }; - super( - animationType === 'in' ? fromAttributes : toAttributes, - animationType === 'in' ? toAttributes : fromAttributes, - duration, - easing, - { group: params?.group, clipGraphic: arc } - ); - } -} - -export class ClipDirectionAnimate extends ClipGraphicAnimate { - constructor( - from: any, - to: any, - duration: number, - easing: EasingType, - params: { - group: IGroup; - direction?: 'x' | 'y'; - orient?: 'positive' | 'negative'; - width?: number; - height?: number; - animationType?: 'in' | 'out'; - } - ) { - const groupAttribute = params?.group?.attribute ?? {}; - const width = params?.width ?? groupAttribute.width ?? 0; - const height = params?.height ?? groupAttribute.height ?? 0; - - const animationType = params?.animationType ?? 'in'; - const direction = params?.direction ?? 'x'; - const orient = params?.orient ?? 'positive'; - - const rect = application.graphicService.creator.rect({ - x: 0, - y: 0, - width: animationType === 'in' && direction === 'x' ? 0 : width, - height: animationType === 'in' && direction === 'y' ? 0 : height, - fill: true - }); - let fromAttributes: Partial = {}; - let toAttributes: Partial = {}; - if (direction === 'y') { - if (orient === 'negative') { - fromAttributes = { y: height, height: 0 }; - toAttributes = { y: 0, height: height }; - } else { - fromAttributes = { height: 0 }; - toAttributes = { height: height }; - } - } else { - if (orient === 'negative') { - fromAttributes = { x: width, width: 0 }; - toAttributes = { x: 0, width: width }; - } else { - fromAttributes = { width: 0 }; - toAttributes = { width: width }; - } - } - super( - animationType === 'in' ? fromAttributes : toAttributes, - animationType === 'in' ? toAttributes : fromAttributes, - duration, - easing, - { group: params?.group, clipGraphic: rect } - ); - } -} - -type RotateSphereParams = - | { - center: { x: number; y: number; z: number }; - r: number; - cb?: (out: any) => void; - } - | (() => any); - -export class RotateBySphereAnimate extends ACustomAnimate { - declare params: RotateSphereParams; - declare theta: number; - declare phi: number; - - onStart(): void { - const { center, r } = typeof this.params === 'function' ? this.params() : this.params; - const startX = this.target.getComputedAttribute('x'); - const startY = this.target.getComputedAttribute('y'); - const startZ = this.target.getComputedAttribute('z'); - const phi = Math.acos((startY - center.y) / r); - let theta = Math.acos((startX - center.x) / r / Math.sin(phi)); - if (startZ - center.z < 0) { - theta = pi2 - theta; - } - this.theta = theta; - this.phi = phi; - } - - onBind() { - return; - } - - onEnd() { - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - if (this.phi == null || this.theta == null) { - return; - } - const { center, r, cb } = typeof this.params === 'function' ? this.params() : this.params; - const deltaAngle = Math.PI * 2 * ratio; - const theta = this.theta + deltaAngle; - const phi = this.phi; - const x = r * Math.sin(phi) * Math.cos(theta) + center.x; - const y = r * Math.cos(phi) + center.y; - const z = r * Math.sin(phi) * Math.sin(theta) + center.z; - out.x = x; - out.y = y; - out.z = z; - // out.beta = phi; - out.alpha = theta + pi / 2; - while (out.alpha > pi2) { - out.alpha -= pi2; - } - out.alpha = pi2 - out.alpha; - - out.zIndex = out.z * -10000; - - cb && cb(out); - } -} - -export class AttributeAnimate extends ACustomAnimate { - declare target: IGroup; - - constructor(to: Record, duration: number, easing: EasingType) { - super({}, to, duration, easing); - } - - getEndProps(): Record { - return this.to; - } - - onBind(): void { - Object.keys(this.to).forEach(k => { - this.from[k] = this.target.getComputedAttribute(k); - }); - return; - } - - onEnd(): void { - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - this.target.stepInterpolate( - this.subAnimate, - this.subAnimate.animate, - out, - this.step, - ratio, - end, - this.to, - this.from - ); - } -} - -export class AnimateGroup extends ACustomAnimate { - declare customAnimates: ACustomAnimate[]; - declare updating: boolean; - - constructor(duration: number, customAnimates: ACustomAnimate[]) { - super(null, null, duration, 'linear'); - this.customAnimates = customAnimates; - } - - initAnimates() { - this.customAnimates.forEach(a => { - a.step = this.step; - a.subAnimate = this.subAnimate; - a.target = this.target; - }); - } - - getEndProps(): Record { - const props = {}; - this.customAnimates.forEach(a => { - Object.assign(props, a.getEndProps()); - }); - return props; - } - - onBind(): void { - this.initAnimates(); - this.customAnimates.forEach(a => { - a.onBind(); - }); - return; - } - - onEnd(): void { - this.customAnimates.forEach(a => { - a.onEnd(); - }); - return; - } - - onStart(): void { - this.customAnimates.forEach(a => { - a.onStart(); - }); - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - if (this.updating) { - return; - } - this.updating = true; - this.customAnimates.forEach(a => { - const easing = a.easing; - const easingFunc = typeof easing === 'string' ? Easing[easing] : easing; - ratio = easingFunc(ratio); - a.onUpdate(end, ratio, out); - }); - this.updating = false; - return; - } -} - -export class AnimateGroup1 extends ACustomAnimate { - declare customAnimates: ACustomAnimate[]; - declare updating: boolean; - - constructor(duration: number, customAnimates: ACustomAnimate[]) { - super(null, null, duration, 'linear'); - this.customAnimates = customAnimates; - } - - initAnimates() { - this.customAnimates.forEach(a => { - a.step = this.step; - a.subAnimate = this.subAnimate; - a.target = this.target; - }); - } - - getEndProps(): Record { - const props = {}; - this.customAnimates.forEach(a => { - Object.assign(props, a.getEndProps()); - }); - return props; - } - - onBind(): void { - this.initAnimates(); - this.customAnimates.forEach(a => { - a.onBind(); - }); - return; - } - - onEnd(): void { - this.customAnimates.forEach(a => { - a.onEnd(); - }); - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - if (this.updating) { - return; - } - this.updating = true; - this.customAnimates.forEach(a => { - const easing = a.easing; - const easingFunc = typeof easing === 'string' ? Easing[easing] : easing; - ratio = easingFunc(ratio); - a.onUpdate(end, ratio, out); - }); - this.updating = false; - return; - } -} diff --git a/packages/vrender-core/src/animate/default-ticker.ts b/packages/vrender-core/src/animate/default-ticker.ts deleted file mode 100644 index 80d217394..000000000 --- a/packages/vrender-core/src/animate/default-ticker.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { DefaultTicker } from './Ticker/default-ticker'; -import { defaultTimeline } from './timeline'; - -export const defaultTicker = new DefaultTicker(); -defaultTicker.addTimeline(defaultTimeline); -const TICKER_FPS = 60; -defaultTicker.setFPS(TICKER_FPS); diff --git a/packages/vrender-core/src/animate/easing-func.ts b/packages/vrender-core/src/animate/easing-func.ts deleted file mode 100644 index 42c0dfb8a..000000000 --- a/packages/vrender-core/src/animate/easing-func.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { CustomPath2D } from '../common/custom-path2d'; -import { CurveContext } from '../common/segment'; - -export function generatorPathEasingFunc(path: string) { - const customPath = new CustomPath2D(); - customPath.setCtx(new CurveContext(customPath)); - customPath.fromString(path, 0, 0, 1, 1); - - return (x: number) => { - return customPath.getYAt(x); - }; -} diff --git a/packages/vrender-core/src/animate/easing.ts b/packages/vrender-core/src/animate/easing.ts deleted file mode 100644 index f50472f1d..000000000 --- a/packages/vrender-core/src/animate/easing.ts +++ /dev/null @@ -1,273 +0,0 @@ -/** - * The MIT License (MIT) - - Copyright (c) 2014 gskinner.com, inc. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -// 参考TweenJS -// https://github.com/CreateJS/TweenJS/tree/master/src/tweenjs -import { pi2 } from '@visactor/vutils'; - -/** - * 代码迁移自createjs - * 部分缓动函数参考https://easings.net/ - */ -export class Easing { - private constructor() { - // do nothing - } - - static linear(t: number): number { - return t; - } - - static none() { - return this.linear; - } - - /** - * 获取缓动函数,amount指示这个缓动函数的插值方式 - * @param amount - * @returns - */ - static get(amount: number) { - if (amount < -1) { - amount = -1; - } else if (amount > 1) { - amount = 1; - } - - return function (t: number) { - if (amount === 0) { - return t; - } - if (amount < 0) { - return t * (t * -amount + 1 + amount); - } - return t * ((2 - t) * amount + (1 - amount)); - }; - } - - /* 语法糖 */ - static getPowIn(pow: number) { - return function (t: number) { - return Math.pow(t, pow); - }; - } - - static getPowOut(pow: number) { - return function (t: number) { - return 1 - Math.pow(1 - t, pow); - }; - } - - static getPowInOut(pow: number) { - return function (t: number) { - if ((t *= 2) < 1) { - return 0.5 * Math.pow(t, pow); - } - return 1 - 0.5 * Math.abs(Math.pow(2 - t, pow)); - }; - } - - // 插值函数 - static quadIn = Easing.getPowIn(2); - static quadOut = Easing.getPowOut(2); - - static quadInOut = Easing.getPowInOut(2); - static cubicIn = Easing.getPowIn(3); - static cubicOut = Easing.getPowOut(3); - static cubicInOut = Easing.getPowInOut(3); - static quartIn = Easing.getPowIn(4); - static quartOut = Easing.getPowOut(4); - static quartInOut = Easing.getPowInOut(4); - static quintIn = Easing.getPowIn(5); - static quintOut = Easing.getPowOut(5); - static quintInOut = Easing.getPowInOut(5); - - /* 语法糖 */ - static getBackIn(amount: number) { - return function (t: number) { - return t * t * ((amount + 1) * t - amount); - }; - } - static getBackOut(amount: number) { - return function (t: number) { - return --t * t * ((amount + 1) * t + amount) + 1; - }; - } - static getBackInOut(amount: number) { - amount *= 1.525; - return function (t: number) { - if ((t *= 2) < 1) { - return 0.5 * (t * t * ((amount + 1) * t - amount)); - } - return 0.5 * ((t -= 2) * t * ((amount + 1) * t + amount) + 2); - }; - } - - // 插值函数 - static backIn = Easing.getBackIn(1.7); - static backOut = Easing.getBackOut(1.7); - static backInOut = Easing.getBackInOut(1.7); - - static sineIn(t: number): number { - return 1 - Math.cos((t * Math.PI) / 2); - } - - static sineOut(t: number): number { - return Math.sin((t * Math.PI) / 2); - } - - static sineInOut(t: number): number { - return -(Math.cos(Math.PI * t) - 1) / 2; - } - - static expoIn(t: number): number { - return t === 0 ? 0 : Math.pow(2, 10 * t - 10); - } - - static expoOut(t: number): number { - return t === 1 ? 1 : 1 - Math.pow(2, -10 * t); - } - - static expoInOut(t: number): number { - return t === 0 ? 0 : t === 1 ? 1 : t < 0.5 ? Math.pow(2, 20 * t - 10) / 2 : (2 - Math.pow(2, -20 * t + 10)) / 2; - } - - // 插值函数 - static circIn(t: number) { - return -(Math.sqrt(1 - t * t) - 1); - } - - static circOut(t: number) { - return Math.sqrt(1 - --t * t); - } - - static circInOut(t: number) { - if ((t *= 2) < 1) { - return -0.5 * (Math.sqrt(1 - t * t) - 1); - } - return 0.5 * (Math.sqrt(1 - (t -= 2) * t) + 1); - } - static bounceOut(t: number) { - if (t < 1 / 2.75) { - return 7.5625 * t * t; - } else if (t < 2 / 2.75) { - return 7.5625 * (t -= 1.5 / 2.75) * t + 0.75; - } else if (t < 2.5 / 2.75) { - return 7.5625 * (t -= 2.25 / 2.75) * t + 0.9375; - } - return 7.5625 * (t -= 2.625 / 2.75) * t + 0.984375; - } - static bounceIn(t: number) { - return 1 - Easing.bounceOut(1 - t); - } - - static bounceInOut(t: number) { - if (t < 0.5) { - return Easing.bounceIn(t * 2) * 0.5; - } - return Easing.bounceOut(t * 2 - 1) * 0.5 + 0.5; - } - - /* 语法糖 */ - static getElasticIn(amplitude: number, period: number) { - return function (t: number) { - if (t === 0 || t === 1) { - return t; - } - const s = (period / pi2) * Math.asin(1 / amplitude); - return -(amplitude * Math.pow(2, 10 * (t -= 1)) * Math.sin(((t - s) * pi2) / period)); - }; - } - static getElasticOut(amplitude: number, period: number) { - return function (t: number) { - if (t === 0 || t === 1) { - return t; - } - const s = (period / pi2) * Math.asin(1 / amplitude); - return amplitude * Math.pow(2, -10 * t) * Math.sin(((t - s) * pi2) / period) + 1; - }; - } - static getElasticInOut(amplitude: number, period: number) { - return function (t: number) { - const s = (period / pi2) * Math.asin(1 / amplitude); - if ((t *= 2) < 1) { - return -0.5 * (amplitude * Math.pow(2, 10 * (t -= 1)) * Math.sin(((t - s) * pi2) / period)); - } - return amplitude * Math.pow(2, -10 * (t -= 1)) * Math.sin(((t - s) * pi2) / period) * 0.5 + 1; - }; - } - - // 插值函数 - static elasticIn = Easing.getElasticIn(1, 0.3); - static elasticOut = Easing.getElasticOut(1, 0.3); - static elasticInOut = Easing.getElasticInOut(1, 0.3 * 1.5); - - static easeInOutQuad = (t: number) => { - if ((t /= 0.5) < 1) { - return 0.5 * Math.pow(t, 2); - } - return -0.5 * ((t -= 2) * t - 2); - }; - - static easeOutElastic = (x: number) => { - const c4 = (2 * Math.PI) / 3; - - return x === 0 ? 0 : x === 1 ? 1 : Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1; - }; - - static easeInOutElastic = (x: number) => { - const c5 = (2 * Math.PI) / 4.5; - - return x === 0 - ? 0 - : x === 1 - ? 1 - : x < 0.5 - ? -(Math.pow(2, 20 * x - 10) * Math.sin((20 * x - 11.125) * c5)) / 2 - : (Math.pow(2, -20 * x + 10) * Math.sin((20 * x - 11.125) * c5)) / 2 + 1; - }; - static registerFunc(name: string, func: (t: number) => number) { - (Easing as any)[name] = func; - } -} - -function flicker(t: number, n: number) { - const step = 1 / n; - let flag = 1; - while (t > step) { - t -= step; - flag *= -1; - } - const v = (flag * t) / step; - return v > 0 ? v : 1 + v; -} - -// 注册flicker -for (let i = 0; i < 10; i++) { - (Easing as any)[`flicker${i}`] = (t: number) => flicker(t, i); -} - -for (let i = 2; i < 10; i++) { - (Easing as any)[`aIn${i}`] = (t: number) => i * t * t + (1 - i) * t; -} diff --git a/packages/vrender-core/src/animate/group-fade.ts b/packages/vrender-core/src/animate/group-fade.ts deleted file mode 100644 index 838179d85..000000000 --- a/packages/vrender-core/src/animate/group-fade.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { IGroup } from '../interface/graphic/group'; -import { ACustomAnimate } from './animate'; - -export class GroupFadeIn extends ACustomAnimate { - declare target: IGroup; - - getEndProps(): Record { - return {}; - } - - onBind(): void { - this.target.setTheme({ - common: { - opacity: 0 - } - }); - return; - } - - onEnd(): void { - this.target.setTheme({ - common: { - opacity: 1 - } - }); - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - this.target.setTheme({ - common: { - opacity: ratio - } - }); - } -} - -export class GroupFadeOut extends ACustomAnimate { - declare target: IGroup; - - getEndProps(): Record { - return {}; - } - - onBind(): void { - this.target.setTheme({ - common: { - opacity: 1 - } - }); - return; - } - - onEnd(): void { - this.target.setTheme({ - common: { - opacity: 0 - } - }); - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - this.target.setTheme({ - common: { - opacity: 1 - ratio - } - }); - } -} diff --git a/packages/vrender-core/src/animate/index.ts b/packages/vrender-core/src/animate/index.ts deleted file mode 100644 index b04ca57b3..000000000 --- a/packages/vrender-core/src/animate/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from './Ticker'; -export * from './animate'; -export * from './config'; -export * from './custom-animate'; -export * from './morphing'; -export * from './timeline'; -export * from './group-fade'; -export * from './easing'; diff --git a/packages/vrender-core/src/animate/morphing.ts b/packages/vrender-core/src/animate/morphing.ts deleted file mode 100644 index 312aaaf8a..000000000 --- a/packages/vrender-core/src/animate/morphing.ts +++ /dev/null @@ -1,680 +0,0 @@ -import { - splitArc, - splitCircle, - splitLine, - splitRect, - splitPolygon, - splitArea, - splitPath -} from './../common/split-path'; -import type { - ICustomPath2D, - IGraphic, - MorphingAnimateConfig, - IRect, - EasingType, - MultiMorphingAnimateConfig, - IArc, - ICircle, - IGraphicAttribute, - ILine, - IPolygon, - IArea, - IPath -} from './../interface'; -import { CustomPath2D } from '../common/custom-path2d'; -import { ACustomAnimate } from './animate'; -import { - alignBezierCurves, - applyTransformOnBezierCurves, - findBestMorphingRotation, - pathToBezierCurves -} from '../common/morphing-utils'; -import { application } from '../application'; -import type { IMatrix } from '@visactor/vutils'; -import { isNil } from '@visactor/vutils'; -import { interpolateColor } from '../color-string/interpolate'; -import { ColorStore, ColorType } from '../color-string'; -import { DefaultMorphingAnimateConfig } from './config'; -import { isTransformKey } from '../common/utils'; -import { AttributeUpdateType } from '../common/enums'; - -declare const __DEV__: boolean; - -interface MorphingDataItem { - from: number[]; - to: number[]; - fromCp: number[]; - toCp: number[]; - rotation: number; -} - -interface OtherAttrItem { - from: any; - to: any; - key: string; -} - -const interpolateOtherAttrs = (attrs: OtherAttrItem[], out: any, ratio: number) => { - attrs.forEach(entry => { - if (Number.isFinite(entry.to)) { - out[entry.key] = entry.from + (entry.to - entry.from) * ratio; - } else if (entry.key === 'fill' || entry.key === 'stroke') { - // 保存解析的结果到step - const color = interpolateColor(entry.from, entry.to, ratio, false); - if (color) { - out[entry.key] = color; - } - } - }); -}; - -/* Adapted from zrender by ecomfe - * https://github.com/ecomfe/zrender - * Licensed under the BSD-3-Clause - - * url: https://github.com/ecomfe/zrender/blob/master/src/tool/morphPath.ts - * License: https://github.com/ecomfe/zrender/blob/master/LICENSE - * @license - */ -const interpolateMorphingData = (morphingData: MorphingDataItem[], path: ICustomPath2D, ratio: number) => { - const tmpArr: number[] = []; - const newCp: number[] = []; - path.clear(); - - for (let i = 0; i < morphingData.length; i++) { - const item = morphingData[i]; - const from = item.from; - const to = item.to; - const angle = item.rotation * ratio; - const fromCp = item.fromCp; - const toCp = item.toCp; - const sa = Math.sin(angle); - const ca = Math.cos(angle); - - newCp[0] = fromCp[0] + (toCp[0] - fromCp[0]) * ratio; - newCp[1] = fromCp[1] + (toCp[1] - fromCp[1]) * ratio; - - for (let m = 0; m < from.length; m += 2) { - const x0 = from[m]; - const y0 = from[m + 1]; - const x1 = to[m]; - const y1 = to[m + 1]; - - const x = x0 * (1 - ratio) + x1 * ratio; - const y = y0 * (1 - ratio) + y1 * ratio; - - tmpArr[m] = x * ca - y * sa + newCp[0]; - tmpArr[m + 1] = x * sa + y * ca + newCp[1]; - } - - let x0 = tmpArr[0]; - let y0 = tmpArr[1]; - - path.moveTo(x0, y0); - - for (let m = 2; m < from.length; m += 6) { - const x1 = tmpArr[m]; - const y1 = tmpArr[m + 1]; - const x2 = tmpArr[m + 2]; - const y2 = tmpArr[m + 3]; - const x3 = tmpArr[m + 4]; - const y3 = tmpArr[m + 5]; - - // Is a line. - if (x0 === x1 && y0 === y1 && x2 === x3 && y2 === y3) { - path.lineTo(x3, y3); - } else { - path.bezierCurveTo(x1, y1, x2, y2, x3, y3); - } - x0 = x3; - y0 = y3; - } - } -}; - -const parseMorphingData = ( - fromPath: ICustomPath2D | null, - toPath: ICustomPath2D, - config?: { - fromTransform?: IMatrix; - toTransfrom: IMatrix; - } -) => { - const fromBezier = fromPath ? pathToBezierCurves(fromPath) : []; - const toBezier = pathToBezierCurves(toPath); - - if (config && fromBezier) { - config.fromTransform && applyTransformOnBezierCurves(fromBezier, config.fromTransform.clone().getInverse()); - applyTransformOnBezierCurves(fromBezier, config.toTransfrom); - // applyTransformOnBezierCurves(toBezier, config.toTransfrom.clone().getInverse()); - } - - const [fromBezierCurves, toBezierCurves] = alignBezierCurves(fromBezier, toBezier); - - return fromPath - ? findBestMorphingRotation(fromBezierCurves, toBezierCurves, 10, Math.PI) - : toBezierCurves.map((to, index) => { - return { - from: fromBezierCurves[index], - to, - fromCp: [0, 0], - toCp: [0, 0], - rotation: 0 - }; - }); -}; - -const validateOtherAttrs = [ - 'fill', - 'fillOpacity', - 'shadowBlur', - 'shadowColor', - 'shadowOffsetX', - 'shadowOffsetY', - 'stroke', - 'strokeOpacity', - 'lineDashOffset' - // 'lineWidth' -]; - -const parseOtherAnimateAttrs = ( - fromAttrs: Partial | null, - toAttrs: Partial | null -) => { - if (!fromAttrs || !toAttrs) { - return null; - } - const res: OtherAttrItem[] = []; - let hasAttr = false; - - Object.keys(fromAttrs).forEach(fromKey => { - if (!validateOtherAttrs.includes(fromKey)) { - return; - } - - const toValue = toAttrs[fromKey]; - if (!isNil(toValue) && !isNil(fromAttrs[fromKey]) && toValue !== fromAttrs[fromKey]) { - if (fromKey === 'fill' || fromKey === 'stroke') { - res.push({ - from: - typeof fromAttrs[fromKey] === 'string' - ? ColorStore.Get(fromAttrs[fromKey] as unknown as string, ColorType.Color255) - : fromAttrs[fromKey], - to: typeof toValue === 'string' ? ColorStore.Get(toValue as string, ColorType.Color255) : toValue, - key: fromKey - }); - } else { - res.push({ from: fromAttrs[fromKey], to: toValue, key: fromKey }); - } - - hasAttr = true; - } - }); - - return hasAttr ? res : null; -}; - -export class MorphingPath extends ACustomAnimate { - declare path: CustomPath2D; - - saveOnEnd?: boolean; - otherAttrs?: OtherAttrItem[]; - - constructor( - config: { morphingData: MorphingDataItem[]; otherAttrs?: OtherAttrItem[]; saveOnEnd?: boolean }, - duration: number, - easing: EasingType - ) { - super(0, 1, duration, easing); - this.morphingData = config.morphingData; - this.otherAttrs = config.otherAttrs; - this.saveOnEnd = config.saveOnEnd; - } - - private morphingData?: MorphingDataItem[]; - - getEndProps(): Record { - return {}; - } - - onBind(): void { - (this.target as IGraphic).createPathProxy(); - this.onUpdate(false, 0, (this.target as IGraphic).attribute); - } - - onEnd(): void { - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - const target = this.target as IGraphic; - const pathProxy = typeof target.pathProxy === 'function' ? target.pathProxy(target.attribute) : target.pathProxy; - interpolateMorphingData(this.morphingData, pathProxy, ratio); - if (this.otherAttrs && this.otherAttrs.length) { - interpolateOtherAttrs(this.otherAttrs, out, ratio); - } - // 计算位置 - if (end && !this.saveOnEnd) { - (this.target as IGraphic).pathProxy = null; - } - } -} - -export const morphPath = ( - fromGraphic: IGraphic | null, - toGraphic: IGraphic, - animationConfig?: MorphingAnimateConfig, - fromGraphicTransform?: IMatrix -) => { - if (fromGraphic && (!fromGraphic.valid || !fromGraphic.toCustomPath)) { - if (__DEV__) { - console.error(fromGraphic, ' is not validate'); - } - return null; - } - - if (!toGraphic.valid || !toGraphic.toCustomPath) { - if (__DEV__) { - console.error(toGraphic, ' is not validate'); - } - return null; - } - - let fromTransform = fromGraphic?.globalTransMatrix; - - if (fromGraphicTransform && fromTransform) { - fromTransform = fromGraphicTransform - .clone() - .multiply(fromTransform.a, fromTransform.b, fromTransform.c, fromTransform.d, fromTransform.e, fromTransform.f); - } - const morphingData = parseMorphingData(fromGraphic?.toCustomPath?.(), toGraphic.toCustomPath(), { - fromTransform, - toTransfrom: toGraphic.globalTransMatrix - }); - - const attrs = parseOtherAnimateAttrs(fromGraphic?.attribute, toGraphic.attribute); - const animate = toGraphic.animate(animationConfig); - - if (animationConfig?.delay) { - animate.wait(animationConfig.delay); - } - - animate.play( - new MorphingPath( - { morphingData, otherAttrs: attrs }, - animationConfig?.duration ?? DefaultMorphingAnimateConfig.duration, - animationConfig?.easing ?? DefaultMorphingAnimateConfig.easing - ) - ); - - return animate; -}; - -export const oneToMultiMorph = ( - fromGraphic: IGraphic, - toGraphics: IGraphic[], - animationConfig?: MultiMorphingAnimateConfig -) => { - const validateToGraphics = toGraphics.filter(graphic => graphic && graphic.toCustomPath && graphic.valid); - if (!validateToGraphics.length) { - if (__DEV__) { - console.error(validateToGraphics, ' is not validate'); - } - } - - if (!fromGraphic.valid || !fromGraphic.toCustomPath) { - if (__DEV__) { - console.error(fromGraphic, ' is not validate'); - } - } - - const childGraphics: IGraphic[] = ( - animationConfig?.splitPath === 'clone' ? cloneGraphic : animationConfig?.splitPath ?? splitGraphic - )(fromGraphic, validateToGraphics.length, false); - - const oldOnEnd = animationConfig?.onEnd; - let count = validateToGraphics.length; - const onEachEnd = () => { - count--; - if (count === 0 && oldOnEnd) { - oldOnEnd(); - } - }; - - validateToGraphics.forEach((toChild, index) => { - const fromChild = childGraphics[index]; - const delay = - (animationConfig?.delay ?? 0) + - (animationConfig?.individualDelay - ? animationConfig.individualDelay(index, validateToGraphics.length, fromChild, toChild) - : 0); - morphPath( - fromChild, - toChild, - Object.assign({}, animationConfig, { onEnd: onEachEnd, delay }), - fromGraphic.globalTransMatrix - ); - }); -}; - -export class MultiToOneMorphingPath extends ACustomAnimate { - declare path: CustomPath2D; - - otherAttrs?: OtherAttrItem[][]; - - constructor( - config: { morphingData: MorphingDataItem[][]; otherAttrs?: OtherAttrItem[][] }, - duration: number, - easing: EasingType - ) { - super(0, 1, duration, easing); - this.morphingData = config.morphingData; - this.otherAttrs = config.otherAttrs; - } - - private morphingData?: MorphingDataItem[][]; - - getEndProps(): Record { - return {}; - } - - onBind(): void { - this.addPathProxy(); - } - - private addPathProxy() { - const shadowRoot = (this.target as IGraphic).shadowRoot; - - shadowRoot.forEachChildren(child => { - (child as IGraphic).createPathProxy(); - }); - - this.onUpdate(false, 0, (this.target as IGraphic).attribute); - } - - private clearPathProxy() { - const shadowRoot = (this.target as IGraphic).shadowRoot; - - shadowRoot.forEachChildren(child => { - (child as IGraphic).pathProxy = null; - }); - } - - onEnd(): void { - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - const shadowRoot = (this.target as IGraphic).shadowRoot; - - shadowRoot.forEachChildren((child: IGraphic, index) => { - interpolateMorphingData( - this.morphingData[index], - typeof child.pathProxy === 'function' ? child.pathProxy(child.attribute) : child.pathProxy, - ratio - ); - - if (this.otherAttrs?.[index] && this.otherAttrs[index].length) { - interpolateOtherAttrs(this.otherAttrs[index], child.attribute, ratio); - } - }); - - // 计算位置 - if (end) { - this.clearPathProxy(); - this.morphingData = null; - } - } -} - -const parseShadowChildAttrs = (graphicAttrs: Partial) => { - const attrs: Partial = {}; - - Object.keys(graphicAttrs).forEach(key => { - if (!isTransformKey(key)) { - attrs[key] = graphicAttrs[key]; - } - }); - - // if (attrs.fill == null) { - // attrs.fill = !!attrs.fillColor; - // } - // if (attrs.stroke == null) { - // attrs.stroke = !!attrs.strokeColor; - // } - - return attrs; -}; - -const appendShadowChildrenToGraphic = (graphic: IGraphic, children: IGraphic[], count: number) => { - const childAttrs = parseShadowChildAttrs(graphic.attribute); - const shadowRoot = graphic.attachShadow(); - - if (children.length) { - shadowRoot.setTheme({ - [children[0].type]: childAttrs - }); - children.forEach(element => { - element.setAttributes({ pickable: false }); - shadowRoot.appendChild(element); - }); - } else { - const box = graphic.AABBBounds; - const width = box.width(); - const height = box.height(); - - shadowRoot.setTheme({ - rect: childAttrs - }); - new Array(count).fill(0).forEach(el => { - const child = application.graphicService.creator.rect({ - x: 0, - y: 0, - width, - height: height, - pickable: false - }); - shadowRoot.appendChild(child); - children.push(child); - }); - } -}; - -export const cloneGraphic = (graphic: IGraphic, count: number, needAppend?: boolean) => { - const children: IGraphic[] = []; - const childAttrs = needAppend ? null : parseShadowChildAttrs(graphic.attribute); - const path = graphic.toCustomPath(); - - for (let i = 0; i < count; i++) { - const element = { - path: new CustomPath2D().fromCustomPath2D(path) - }; - - children.push( - application.graphicService.creator.path(needAppend ? element : Object.assign({}, childAttrs, element)) - ); - } - - if (needAppend) { - appendShadowChildrenToGraphic(graphic, children, count); - } - - return children; -}; - -export const splitGraphic = (graphic: IGraphic, count: number, needAppend?: boolean) => { - const children: IGraphic[] = []; - const childAttrs = needAppend ? null : parseShadowChildAttrs(graphic.attribute); - - if (graphic.type === 'rect') { - const childrenAttrs = splitRect(graphic as IRect, count); - childrenAttrs.forEach(element => { - children.push( - application.graphicService.creator.rect(needAppend ? element : Object.assign({}, childAttrs, element)) - ); - }); - } else if (graphic.type === 'arc') { - const childrenAttrs = splitArc(graphic as IArc, count); - childrenAttrs.forEach(element => { - children.push( - application.graphicService.creator.arc(needAppend ? element : Object.assign({}, childAttrs, element)) - ); - }); - } else if (graphic.type === 'circle') { - const childrenAttrs = splitCircle(graphic as ICircle, count); - childrenAttrs.forEach(element => { - children.push( - application.graphicService.creator.arc(needAppend ? element : Object.assign({}, childAttrs, element)) - ); - }); - } else if (graphic.type === 'line') { - const childrenAttrs = splitLine(graphic as ILine, count); - const defaultSymbol = { size: 10, symbolType: 'circle' }; - - childrenAttrs.forEach(element => { - children.push( - application.graphicService.creator.symbol( - needAppend ? Object.assign({}, element, defaultSymbol) : Object.assign({}, childAttrs, element, defaultSymbol) - ) - ); - }); - } else if (graphic.type === 'polygon') { - const childrenAttrs = splitPolygon(graphic as IPolygon, count); - childrenAttrs.forEach(element => { - children.push( - application.graphicService.creator.polygon(needAppend ? element : Object.assign({}, childAttrs, element)) - ); - }); - } else if (graphic.type === 'area') { - const childrenAttrs = splitArea(graphic as IArea, count); - childrenAttrs.forEach(element => { - children.push( - application.graphicService.creator.polygon(needAppend ? element : Object.assign({}, childAttrs, element)) - ); - }); - } else if (graphic.type === 'path') { - const childrenAttrs = splitPath(graphic as IPath, count); - childrenAttrs.forEach(element => { - if ('path' in element) { - children.push( - application.graphicService.creator.path(needAppend ? element : Object.assign({}, childAttrs, element)) - ); - } else { - children.push( - application.graphicService.creator.polygon(needAppend ? element : Object.assign({}, childAttrs, element)) - ); - } - }); - } - - if (needAppend) { - appendShadowChildrenToGraphic(graphic, children, count); - } - - return children; -}; - -/** - * 多对一动画 - * @param fromGraphics - * @param toGraphic - * @param animationConfig - */ -export const multiToOneMorph = ( - fromGraphics: IGraphic[], - toGraphic: IGraphic, - animationConfig?: MultiMorphingAnimateConfig -) => { - const validateFromGraphics = fromGraphics.filter(graphic => graphic.toCustomPath && graphic.valid); - if (!validateFromGraphics.length) { - if (__DEV__) { - console.error(fromGraphics, ' is not validate'); - } - } - - if (!toGraphic.valid || !toGraphic.toCustomPath) { - if (__DEV__) { - console.error(toGraphic, ' is not validate'); - } - } - - const childGraphics: IGraphic[] = ( - animationConfig?.splitPath === 'clone' ? cloneGraphic : animationConfig?.splitPath ?? splitGraphic - )(toGraphic, validateFromGraphics.length, true); - - const toAttrs = toGraphic.attribute; - toGraphic.setAttribute('visible', false); - - const morphingData = validateFromGraphics.map((graphic, index) => { - return parseMorphingData(graphic.toCustomPath(), childGraphics[index].toCustomPath(), { - fromTransform: graphic.globalTransMatrix, - toTransfrom: childGraphics[index].globalTransMatrix - }); - }); - const otherAttrs = validateFromGraphics.map((graphic, index) => { - return parseOtherAnimateAttrs(graphic.attribute, toAttrs); - }); - - if (animationConfig?.individualDelay) { - const oldOnEnd = animationConfig.onEnd; - let count = validateFromGraphics.length; - const onEachEnd = () => { - count--; - if (count === 0) { - toGraphic.setAttributes({ visible: true, ratio: null } as any, false, { - type: AttributeUpdateType.ANIMATE_END - }); - toGraphic.detachShadow(); - if (oldOnEnd) { - oldOnEnd(); - } - } - }; - childGraphics.forEach((to, index) => { - const delay = - (animationConfig.delay ?? 0) + - animationConfig.individualDelay(index, validateFromGraphics.length, fromGraphics[index], to); - const animate = to.animate(Object.assign({}, animationConfig, { onEnd: onEachEnd })); - animate.wait(delay); - - animate.play( - new MorphingPath( - { - morphingData: morphingData[index], - saveOnEnd: true, - otherAttrs: otherAttrs[index] - }, - animationConfig.duration ?? DefaultMorphingAnimateConfig.duration, - animationConfig.easing ?? DefaultMorphingAnimateConfig.easing - ) - ); - }); - } else { - const oldOnEnd = animationConfig?.onEnd; - const config = animationConfig ? Object.assign({}, animationConfig) : {}; - - config.onEnd = () => { - toGraphic.setAttribute('visible', true, false, { type: AttributeUpdateType.ANIMATE_END }); - toGraphic.detachShadow(); - - if (oldOnEnd) { - oldOnEnd(); - } - }; - - const animate = toGraphic.animate(config); - - if (animationConfig?.delay) { - animate.wait(animationConfig.delay); - } - - animate.play( - new MultiToOneMorphingPath( - { morphingData, otherAttrs }, - animationConfig?.duration ?? DefaultMorphingAnimateConfig.duration, - animationConfig?.easing ?? DefaultMorphingAnimateConfig.easing - ) - ); - } -}; diff --git a/packages/vrender-core/src/animate/timeline.ts b/packages/vrender-core/src/animate/timeline.ts deleted file mode 100644 index 463e8b684..000000000 --- a/packages/vrender-core/src/animate/timeline.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { AnimateStatus } from '../common/enums'; -import { Generator } from '../common/generator'; -import type { IAnimate, ITimeline } from '../interface'; - -// 管理一组动画 -export class DefaultTimeline implements ITimeline { - declare id: number; - protected declare animateHead: IAnimate | null; - protected declare animateTail: IAnimate | null; - protected declare ticker: any; - declare animateCount: number; - protected declare paused: boolean; - - constructor() { - this.id = Generator.GenAutoIncrementId(); - this.animateHead = null; - this.animateTail = null; - this.animateCount = 0; - this.paused = false; - } - - addAnimate(animate: IAnimate) { - if (!this.animateTail) { - this.animateHead = animate; - this.animateTail = animate; - } else { - this.animateTail.nextAnimate = animate; - animate.prevAnimate = this.animateTail; - this.animateTail = animate; - animate.nextAnimate = null; - } - this.animateCount++; - } - - pause() { - this.paused = true; - } - resume() { - this.paused = false; - } - - tick(delta: number) { - if (this.paused) { - return; - } - let animate = this.animateHead; - this.animateCount = 0; - while (animate) { - if (animate.status === AnimateStatus.END) { - this.removeAnimate(animate); - } else if (animate.status === AnimateStatus.RUNNING || animate.status === AnimateStatus.INITIAL) { - this.animateCount++; - animate.advance(delta); - } else if (animate.status === AnimateStatus.PAUSED) { - // 暂停 - this.animateCount++; - } - animate = animate.nextAnimate; - } - } - - clear() { - let animate = this.animateHead; - while (animate) { - animate.release(); - animate = animate.nextAnimate; - } - this.animateHead = null; - this.animateTail = null; - this.animateCount = 0; - } - - removeAnimate(animate: IAnimate, release: boolean = true) { - animate._onRemove && animate._onRemove.forEach(cb => cb()); - if (animate === this.animateHead) { - this.animateHead = animate.nextAnimate; - if (animate === this.animateTail) { - // 只有一个元素 - this.animateTail = null; - } else { - // 有多个元素 - this.animateHead.prevAnimate = null; - } - } else if (animate === this.animateTail) { - // 有多个元素 - this.animateTail = animate.prevAnimate; - this.animateTail.nextAnimate = null; - // animate.prevAnimate = null; - } else { - animate.prevAnimate.nextAnimate = animate.nextAnimate; - animate.nextAnimate.prevAnimate = animate.prevAnimate; - // animate不支持二次复用,不需要重置 - // animate.prevAnimate = null; - // animate.nextAnimate = null; - } - release && animate.release(); - - return; - } -} - -export const defaultTimeline = new DefaultTimeline(); diff --git a/packages/vrender-core/src/common/performance-raf.ts b/packages/vrender-core/src/common/performance-raf.ts new file mode 100644 index 000000000..a0669aca2 --- /dev/null +++ b/packages/vrender-core/src/common/performance-raf.ts @@ -0,0 +1,30 @@ +import { vglobal } from '../modules'; + +/** + * 性能优化,将requestAnimationFrame的回调函数存储起来,在下一帧执行 + */ +export class PerformanceRAF { + nextAnimationFrameCbs: FrameRequestCallback[] = []; + + addAnimationFrameCb(callback: FrameRequestCallback) { + this.nextAnimationFrameCbs.push(callback); + // 下一帧执行nextAnimationFrameCbs + this.tryRunAnimationFrameNextFrame(); + return this.nextAnimationFrameCbs.length - 1; + } + + protected runAnimationFrame = (time: number) => { + const cbs = this.nextAnimationFrameCbs; + this.nextAnimationFrameCbs = []; + for (let i = 0; i < cbs.length; i++) { + cbs[i](time); + } + }; + + protected tryRunAnimationFrameNextFrame = () => { + if (!(this.nextAnimationFrameCbs && this.nextAnimationFrameCbs.length === 1)) { + return; + } + vglobal.getRequestAnimationFrame()(this.runAnimationFrame); + }; +} diff --git a/packages/vrender-core/src/core/stage.ts b/packages/vrender-core/src/core/stage.ts index e16c43d50..e4b009a81 100644 --- a/packages/vrender-core/src/core/stage.ts +++ b/packages/vrender-core/src/core/stage.ts @@ -290,7 +290,9 @@ export class Stage extends Group implements IStage { this.ticker.addTimeline(this.timeline); this.timeline.pause(); if (!params.optimize) { - params.optimize = {}; + params.optimize = { + animateMode: 'performance' + }; } this.optmize(params.optimize); // 如果背景是图片,触发加载图片操作 diff --git a/packages/vrender-core/src/index.ts b/packages/vrender-core/src/index.ts index 042131874..1f15501d5 100644 --- a/packages/vrender-core/src/index.ts +++ b/packages/vrender-core/src/index.ts @@ -45,6 +45,7 @@ export * from './common/morphing-utils'; export * from './common/split-path'; export * from './common/enums'; export * from './common/generator'; +export * from './common/performance-raf'; export * from './plugins/constants'; export * from './plugins/builtin-plugin/richtext-edit-plugin'; export * from './allocator/matrix-allocate'; diff --git a/packages/vrender-core/src/interface/stage.ts b/packages/vrender-core/src/interface/stage.ts index c439cfe7a..110b333f2 100644 --- a/packages/vrender-core/src/interface/stage.ts +++ b/packages/vrender-core/src/interface/stage.ts @@ -107,6 +107,10 @@ export type IOptimizeType = { disableCheckGraphicWidthOutRange?: boolean; // tick渲染模式,effect会在tick之后立刻执行render,保证动画效果正常。performance模式中tick和render均是RAF,属性可能会被篡改 tickRenderMode?: 'effect' | 'performance'; + // 是否开启高性能动画,默认开启 + // 开启后不会执行某些安全校验,比如跳帧处理 + // 开启后会自动降帧,最高60fps + animateMode?: 'effect' | 'performance'; }; export interface IOption3D { diff --git a/packages/vrender-core/src/plugins/builtin-plugin/richtext-edit-plugin-old.ts b/packages/vrender-core/src/plugins/builtin-plugin/richtext-edit-plugin-old.ts deleted file mode 100644 index 5594309da..000000000 --- a/packages/vrender-core/src/plugins/builtin-plugin/richtext-edit-plugin-old.ts +++ /dev/null @@ -1,671 +0,0 @@ -// import type { IPointLike } from '@visactor/vutils'; -// import { isObject, isString, max, merge } from '@visactor/vutils'; -// import { Generator } from '../../common/generator'; -// import { createGroup, createLine, createRect } from '../../graphic'; -// import type { -// IGroup, -// ILine, -// IPlugin, -// IPluginService, -// IRect, -// IRichText, -// IRichTextCharacter, -// IRichTextFrame, -// IRichTextIcon, -// IRichTextLine, -// IRichTextParagraph, -// IRichTextParagraphCharacter, -// ITicker, -// ITimeline -// } from '../../interface'; -// import { EditModule, findCursorIndexIgnoreLinebreak } from './edit-module'; -// import { Animate, DefaultTicker, DefaultTimeline } from '../../animate'; - -// type UpdateType = 'input' | 'change' | 'onfocus' | 'defocus' | 'selection' | 'dispatch'; - -// class Selection { -// cacheSelectionStartCursorIdx: number; -// cacheCurCursorIdx: number; -// selectionStartCursorIdx: number; -// curCursorIdx: number; -// rt: IRichText; - -// constructor( -// cacheSelectionStartCursorIdx: number, -// cacheCurCursorIdx: number, -// selectionStartCursorIdx: number, -// curCursorIdx: number, -// rt: IRichText -// ) { -// this.curCursorIdx = curCursorIdx; -// this.selectionStartCursorIdx = selectionStartCursorIdx; -// this.cacheCurCursorIdx = cacheCurCursorIdx; -// this.cacheSelectionStartCursorIdx = cacheSelectionStartCursorIdx; -// this.rt = rt; -// } - -// isEmpty(): boolean { -// return this.selectionStartCursorIdx === this.curCursorIdx; -// } - -// hasFormat(key: string): boolean { -// return this.getFormat(key) != null; -// } - -// /** -// * 获取第idx中key的值 -// * @param key -// * @param idx cursor左侧字符的值,如果idx为-1则认为是特殊情况,为右侧字符的值 -// */ -// _getFormat(key: string, idx: number) { -// if (!this.rt) { -// return null; -// } -// const config = this.rt.attribute.textConfig as any; -// if (idx < 0) { -// idx = 0; -// } -// if (idx >= config.length) { -// return null; -// } -// return config[idx][key] ?? (this.rt.attribute as any)[key]; -// } -// getFormat(key: string): any { -// return this.getAllFormat(key)[0]; -// } - -// getAllFormat(key: string): any { -// const valSet = new Set(); -// let minCursorIdx = Math.min(this.selectionStartCursorIdx, this.curCursorIdx); -// let maxCursorIdx = Math.max(this.selectionStartCursorIdx, this.curCursorIdx); -// if (minCursorIdx === maxCursorIdx) { -// return [this._getFormat(key, minCursorIdx)]; -// } -// minCursorIdx++; -// maxCursorIdx++; -// const maxConfigIdx = this.rt.attribute.textConfig.length - 1; -// if (minCursorIdx > maxConfigIdx) { -// minCursorIdx = maxConfigIdx; -// } -// if (maxCursorIdx > maxConfigIdx) { -// maxCursorIdx = maxConfigIdx; -// } -// for (let i = minCursorIdx; i < maxCursorIdx; i++) { -// const val = this._getFormat(key, i); -// val && valSet.add(val); -// } -// return Array.from(valSet.values()); -// } -// } - -// export const FORMAT_TEXT_COMMAND = 'FORMAT_TEXT_COMMAND'; -// export const FORMAT_ELEMENT_COMMAND = 'FORMAT_ELEMENT_COMMAND'; -// export class RichTextEditPlugin implements IPlugin { -// name: 'RichTextEditPlugin' = 'RichTextEditPlugin'; -// activeEvent: 'onRegister' = 'onRegister'; -// pluginService: IPluginService; -// _uid: number = Generator.GenAutoIncrementId(); -// key: string = this.name + this._uid; -// editing: boolean = false; -// editLine: ILine; -// editBg: IGroup; -// pointerDown: boolean = false; -// // 用于selection中保存上一次click时候的位置 -// lastPoint?: IPointLike; -// editModule: EditModule; -// currRt: IRichText; - -// // 当前的cursor信息 -// // 0.1为第一个字符右侧, -0.1为第一个字符左侧 -// // 1.1为第二个字符右侧,0.9为第二个字符左侧 -// curCursorIdx: number; -// selectionStartCursorIdx: number; - -// commandCbs: Map void>>; -// updateCbs: Array<(type: UpdateType, p: RichTextEditPlugin) => void>; - -// ticker: ITicker; -// timeline: ITimeline; - -// // 富文本有align或者baseline的时候,需要对光标做偏移 -// protected declare deltaX: number; -// protected declare deltaY: number; - -// constructor() { -// this.commandCbs = new Map(); -// this.commandCbs.set(FORMAT_TEXT_COMMAND, [this.formatTextCommandCb]); -// this.updateCbs = []; -// this.timeline = new DefaultTimeline(); -// this.ticker = new DefaultTicker([this.timeline]); -// this.deltaX = 0; -// this.deltaY = 0; -// } - -// static CreateSelection(rt: IRichText) { -// if (!rt) { -// return null; -// } -// const { textConfig = [] } = rt.attribute; -// return new Selection( -// -1, -// textConfig.length - 1, -// findCursorIndexIgnoreLinebreak(textConfig, -1), -// findCursorIndexIgnoreLinebreak(textConfig, textConfig.length - 1), -// rt -// ); -// } - -// /** -// * 获取当前选择的区间范围 -// * @param defaultAll 如果force为true,又没有选择,则认为选择了所有然后进行匹配,如果为false,则认为什么都没有选择,返回null -// * @returns -// */ -// getSelection(defaultAll: boolean = false) { -// if (!this.currRt) { -// return null; -// } -// if ( -// this.selectionStartCursorIdx != null && -// this.curCursorIdx != null -// // this.selectionStartCursorIdx !== this.curCursorIdx && -// ) { -// return new Selection( -// this.selectionStartCursorIdx, -// this.curCursorIdx, -// findCursorIndexIgnoreLinebreak(this.currRt.attribute.textConfig, this.selectionStartCursorIdx), -// findCursorIndexIgnoreLinebreak(this.currRt.attribute.textConfig, this.curCursorIdx), -// this.currRt -// ); -// } else if (defaultAll) { -// return RichTextEditPlugin.CreateSelection(this.currRt); -// } -// return null; -// } - -// /* command */ -// formatTextCommandCb(payload: string, p: RichTextEditPlugin) { -// const rt = p.currRt; -// if (!rt) { -// return; -// } -// const selectionData = p.getSelection(); -// if (!selectionData) { -// return; -// } -// const { selectionStartCursorIdx, curCursorIdx } = selectionData; -// const minCursorIdx = Math.min(selectionStartCursorIdx, curCursorIdx); -// const maxCursorIdx = Math.max(selectionStartCursorIdx, curCursorIdx); -// const config = rt.attribute.textConfig.slice(minCursorIdx + 1, maxCursorIdx + 1); -// if (payload === 'bold') { -// config.forEach((item: IRichTextParagraphCharacter) => (item.fontWeight = 'bold')); -// } else if (payload === 'italic') { -// config.forEach((item: IRichTextParagraphCharacter) => (item.fontStyle = 'italic')); -// } else if (payload === 'underline') { -// config.forEach((item: IRichTextParagraphCharacter) => (item.underline = true)); -// } else if (payload === 'lineThrough') { -// config.forEach((item: IRichTextParagraphCharacter) => (item.lineThrough = true)); -// } else if (isObject(payload)) { -// config.forEach((item: IRichTextParagraphCharacter) => merge(item, payload)); -// } -// rt.setAttributes(rt.attribute); -// } - -// dispatchCommand(command: string, payload: any) { -// const cbs = this.commandCbs.get(command); -// cbs && cbs.forEach(cb => cb(payload, this)); -// this.updateCbs.forEach(cb => cb('dispatch', this)); -// } - -// registerCommand(command: string, cb: (payload: any, p: RichTextEditPlugin) => void) { -// const cbs: Array<(payload: any, p: RichTextEditPlugin) => void> = this.commandCbs.get(command) || []; -// cbs.push(cb); -// } - -// registerUpdateListener(cb: (type: UpdateType, p: RichTextEditPlugin) => void) { -// const cbs = this.updateCbs || []; -// cbs.push(cb); -// } - -// activate(context: IPluginService): void { -// this.pluginService = context; -// this.editModule = new EditModule(); -// // context.stage.on('click', this.handleClick); -// context.stage.on('pointermove', this.handleMove); -// context.stage.on('pointerdown', this.handlePointerDown); -// context.stage.on('pointerup', this.handlePointerUp); -// context.stage.on('pointerleave', this.handlePointerUp); - -// this.editModule.onInput(this.handleInput); -// this.editModule.onChange(this.handleChange); -// } - -// handleInput = (text: string, isComposing: boolean, cursorIdx: number, rt: IRichText, orient: 'left' | 'right') => { -// // 修改cursor的位置,但并不同步,因为这可能是临时的 -// const p = this.getPointByColumnIdx(cursorIdx, rt, orient); -// this.hideSelection(); -// this.setCursor(p.x, p.y1, p.y2); -// this.updateCbs.forEach(cb => cb('input', this)); -// }; -// handleChange = (text: string, isComposing: boolean, cursorIdx: number, rt: IRichText, orient: 'left' | 'right') => { -// // 修改cursor的位置,并同步到editModule -// const p = this.getPointByColumnIdx(cursorIdx, rt, orient); -// this.curCursorIdx = cursorIdx; -// this.selectionStartCursorIdx = cursorIdx; -// this.setCursorAndTextArea(p.x, p.y1, p.y2, rt); -// this.hideSelection(); -// this.updateCbs.forEach(cb => cb('change', this)); -// }; - -// handleMove = (e: PointerEvent) => { -// if (!this.isRichtext(e)) { -// return; -// } -// this.currRt = e.target as IRichText; -// this.handleEnter(e); -// (e.target as any).once('pointerleave', this.handleLeave); - -// this.showSelection(e); -// }; - -// showSelection(e: PointerEvent) { -// const cache = (e.target as IRichText).getFrameCache(); -// if (!(cache && this.editBg)) { -// return; -// } -// if (this.pointerDown) { -// let p0 = this.lastPoint; -// // 计算p1在字符中的位置 -// let p1 = this.getEventPosition(e); -// let line1Info = this.getLineByPoint(cache, p1); -// if (!line1Info) { -// return; -// } -// const column1 = this.getColumnByLinePoint(line1Info, p1); -// const y1 = line1Info.top; -// const y2 = line1Info.top + line1Info.height; -// let x = column1.left + column1.width; -// let cursorIndex = this.getColumnIndex(cache, column1); -// if (p1.x < column1.left + column1.width / 2) { -// x = column1.left; -// cursorIndex -= 1; -// } -// p1.x = x; -// p1.y = (y1 + y2) / 2; -// let line0Info = this.getLineByPoint(cache, p0); -// if (p0.y > p1.y || (p0.y === p1.y && p0.x > p1.x)) { -// [p0, p1] = [p1, p0]; -// [line1Info, line0Info] = [line0Info, line1Info]; -// } - -// this.editBg.removeAllChild(); -// if (line0Info === line1Info) { -// // const column0 = this.getColumnByLinePoint(line0Info, p0); -// this.editBg.setAttributes({ -// x: p0.x, -// y: line0Info.top, -// width: p1.x - p0.x, -// height: line0Info.height, -// fill: '#336df4', -// fillOpacity: 0.2 -// }); -// } else { -// this.editBg.setAttributes({ x: 0, y: line0Info.top, width: 0, height: 0 }); -// const startIdx = cache.lines.findIndex(item => item === line0Info); -// const endIdx = cache.lines.findIndex(item => item === line1Info); -// let y = 0; -// for (let i = startIdx; i <= endIdx; i++) { -// const line = cache.lines[i]; -// if (i === startIdx) { -// const p = line.paragraphs[line.paragraphs.length - 1]; -// this.editBg.add( -// createRect({ -// x: p0.x, -// y, -// width: p.left + p.width - p0.x, -// height: line.height, -// fill: '#336df4', -// fillOpacity: 0.2 -// }) -// ); -// } else if (i === endIdx) { -// const p = line.paragraphs[0]; -// this.editBg.add( -// createRect({ -// x: p.left, -// y, -// width: p1.x - p.left, -// height: line.height, -// fill: '#336df4', -// fillOpacity: 0.2 -// }) -// ); -// } else { -// const p0 = line.paragraphs[0]; -// const p1 = line.paragraphs[line.paragraphs.length - 1]; -// this.editBg.add( -// createRect({ -// x: p0.left, -// y, -// width: p1.left + p1.width - p0.left, -// height: line.height, -// fill: '#336df4', -// fillOpacity: 0.2 -// }) -// ); -// } -// y += line.height; -// } -// } - -// this.curCursorIdx = cursorIndex; -// this.setCursorAndTextArea(x, y1 + 2, y2 - 2, e.target as IRichText); - -// this.applyUpdate(); -// this.updateCbs.forEach(cb => cb('selection', this)); -// } -// } - -// hideSelection() { -// if (this.editBg) { -// this.editBg.removeAllChild(); -// this.editBg.setAttributes({ fill: 'transparent' }); -// } -// } - -// handlePointerDown = (e: PointerEvent) => { -// if (this.editing) { -// this.onFocus(e); -// } else { -// this.deFocus(e); -// } -// this.applyUpdate(); -// this.pointerDown = true; -// this.updateCbs.forEach(cb => cb(this.editing ? 'onfocus' : 'defocus', this)); -// console.log(this.selectionStartCursorIdx); -// }; -// handlePointerUp = (e: PointerEvent) => { -// this.pointerDown = false; -// }; - -// forceFocus(e: PointerEvent) { -// this.handleEnter(e); -// this.handlePointerDown(e); -// this.handlePointerUp(e); -// } - -// // 鼠标进入 -// handleEnter = (e: PointerEvent) => { -// this.editing = true; -// this.pluginService.stage.setCursor('text'); -// }; - -// // 鼠标离开 -// handleLeave = (e: PointerEvent) => { -// this.editing = false; -// this.pluginService.stage.setCursor('default'); -// }; - -// isRichtext(e: PointerEvent) { -// return !!(e.target && (e.target as any).type === 'richtext' && (e.target as any).attribute.editable); -// } - -// protected getEventPosition(e: PointerEvent): IPointLike { -// const p = this.pluginService.stage.eventPointTransform(e); - -// const p1 = { x: 0, y: 0 }; -// (e.target as IRichText).globalTransMatrix.transformPoint(p, p1); -// p1.x -= this.deltaX; -// p1.y -= this.deltaY; -// return p1; -// } - -// protected getLineByPoint(cache: IRichTextFrame, p1: IPointLike): IRichTextLine { -// let lineInfo = cache.lines[0]; -// for (let i = 0; i < cache.lines.length; i++) { -// if (lineInfo.top <= p1.y && lineInfo.top + lineInfo.height >= p1.y) { -// break; -// } -// lineInfo = cache.lines[i + 1]; -// } - -// return lineInfo; -// } -// protected getColumnByLinePoint(lineInfo: IRichTextLine, p1: IPointLike): IRichTextParagraph | IRichTextIcon { -// let columnInfo = lineInfo.paragraphs[0]; -// for (let i = 0; i < lineInfo.paragraphs.length; i++) { -// if (columnInfo.left <= p1.x && columnInfo.left + columnInfo.width >= p1.x) { -// break; -// } -// columnInfo = lineInfo.paragraphs[i]; -// } - -// return columnInfo; -// } - -// onFocus(e: PointerEvent) { -// this.deFocus(e); -// this.currRt = e.target as IRichText; - -// // 添加shadowGraphic -// const target = e.target as IRichText; -// RichTextEditPlugin.tryUpdateRichtext(target); -// const shadowRoot = target.attachShadow(); -// const cache = target.getFrameCache(); -// if (!cache) { -// return; -// } - -// this.deltaX = 0; -// this.deltaY = 0; -// const height = cache.actualHeight; -// const width = cache.lines.reduce((w, item) => Math.max(w, item.actualWidth), 0); -// if (cache.globalAlign === 'center') { -// this.deltaX = -width / 2; -// } else if (cache.globalAlign === 'right') { -// this.deltaX = -width; -// } -// if (cache.globalBaseline === 'middle') { -// this.deltaY = -height / 2; -// } else if (cache.globalBaseline === 'bottom') { -// this.deltaY = -height; -// } - -// shadowRoot.setAttributes({ shadowRootIdx: -1, x: this.deltaX, y: this.deltaY }); -// if (!this.editLine) { -// const line = createLine({ x: 0, y: 0, lineWidth: 1, stroke: 'black' }); -// // 不使用stage的Ticker,避免影响其他的动画以及受到其他动画影响 -// const animate = line.animate(); -// animate.setTimeline(this.timeline); -// animate.to({ opacity: 1 }, 10, 'linear').wait(700).to({ opacity: 0 }, 10, 'linear').wait(700).loop(Infinity); -// this.editLine = line; -// this.ticker.start(true); - -// const g = createGroup({ x: 0, y: 0, width: 0, height: 0 }); -// this.editBg = g; -// shadowRoot.add(this.editLine); -// shadowRoot.add(this.editBg); -// } - -// const p1 = this.getEventPosition(e); - -// const lineInfo = this.getLineByPoint(cache, p1); - -// if (lineInfo) { -// const columnInfo = this.getColumnByLinePoint(lineInfo, p1); -// if (!columnInfo) { -// return; -// } - -// let y1 = lineInfo.top; -// let y2 = lineInfo.top + lineInfo.height; -// let x = columnInfo.left + columnInfo.width; -// y1 += 2; -// y2 -= 2; -// let cursorIndex = this.getColumnIndex(cache, columnInfo); -// if (p1.x < columnInfo.left + columnInfo.width / 2) { -// x = columnInfo.left; -// cursorIndex -= 1; -// } - -// this.lastPoint = { x, y: (y1 + y2) / 2 }; - -// this.curCursorIdx = cursorIndex; -// this.selectionStartCursorIdx = cursorIndex; -// this.setCursorAndTextArea(x, y1, y2, target); -// } -// } - -// protected getPointByColumnIdx(idx: number, rt: IRichText, orient: 'left' | 'right') { -// const cache = rt.getFrameCache(); -// const column = this.getColumnByIndex(cache, idx); -// const height = rt.attribute.fontSize ?? (rt.attribute.textConfig?.[0] as any)?.fontSize; -// if (!column) { -// return { -// x: 0, -// y1: 0, -// y2: height -// }; -// } -// const { lineInfo, columnInfo } = column; -// let y1 = lineInfo.top; -// let y2 = lineInfo.top + lineInfo.height; -// const x = columnInfo.left + (orient === 'left' ? 0 : columnInfo.width); -// y1 += 2; -// y2 -= 2; - -// return { x, y1, y2 }; -// } - -// protected getColumnIndex(cache: IRichTextFrame, cInfo: IRichTextParagraph | IRichTextIcon) { -// // TODO 认为都是单个字符拆分的 -// let inputIndex = -1; -// for (let i = 0; i < cache.lines.length; i++) { -// const line = cache.lines[i]; -// for (let j = 0; j < line.paragraphs.length; j++) { -// inputIndex++; -// if (cInfo === line.paragraphs[j]) { -// return inputIndex; -// } -// } -// } -// return -1; -// } -// protected getColumnByIndex( -// cache: IRichTextFrame, -// index: number -// ): { -// lineInfo: IRichTextLine; -// columnInfo: IRichTextParagraph | IRichTextIcon; -// } | null { -// // TODO 认为都是单个字符拆分的 -// let inputIndex = -1; -// for (let i = 0; i < cache.lines.length; i++) { -// const lineInfo = cache.lines[i]; -// for (let j = 0; j < lineInfo.paragraphs.length; j++) { -// const columnInfo = lineInfo.paragraphs[j]; -// inputIndex++; -// if (inputIndex === index) { -// return { -// lineInfo, -// columnInfo -// }; -// } -// } -// } -// return null; -// } - -// protected setCursorAndTextArea(x: number, y1: number, y2: number, rt: IRichText) { -// this.editLine.setAttributes({ -// points: [ -// { x, y: y1 }, -// { x, y: y2 } -// ] -// }); -// const out = { x: 0, y: 0 }; -// rt.globalTransMatrix.getInverse().transformPoint({ x, y: y1 }, out); -// // TODO 考虑stage变换 -// const { left, top } = this.pluginService.stage.window.getBoundingClientRect(); -// out.x += left; -// out.y += top; - -// this.editModule.moveTo(out.x, out.y, rt, this.curCursorIdx, this.selectionStartCursorIdx); -// } -// protected setCursor(x: number, y1: number, y2: number) { -// this.editLine.setAttributes({ -// points: [ -// { x, y: y1 }, -// { x, y: y2 } -// ] -// }); -// } - -// applyUpdate() { -// this.pluginService.stage.renderNextFrame(); -// } -// deFocus(e: PointerEvent) { -// const target = this.currRt as IRichText; -// if (!target) { -// return; -// } -// target.detachShadow(); -// this.currRt = null; -// if (this.editLine) { -// this.editLine.parent.removeChild(this.editLine); -// this.editLine.release(); -// this.editLine = null; - -// this.editBg.parent.removeChild(this.editBg); -// this.editBg.release(); -// this.editBg = null; -// } -// } - -// static splitText(text: string) { -// // 😁这种emoji长度算两个,所以得处理一下 -// return Array.from(text); -// } - -// static tryUpdateRichtext(richtext: IRichText) { -// const cache = richtext.getFrameCache(); -// if ( -// !cache.lines.every(line => -// line.paragraphs.every( -// item => !(item.text && isString(item.text) && RichTextEditPlugin.splitText(item.text).length > 1) -// ) -// ) -// ) { -// const tc: IRichTextCharacter[] = []; -// richtext.attribute.textConfig.forEach((item: IRichTextParagraphCharacter) => { -// const textList = RichTextEditPlugin.splitText(item.text.toString()); -// if (isString(item.text) && textList.length > 1) { -// // 拆分 -// for (let i = 0; i < textList.length; i++) { -// const t = textList[i]; -// tc.push({ ...item, text: t }); -// } -// } else { -// tc.push(item); -// } -// }); -// richtext.setAttributes({ textConfig: tc }); -// richtext.doUpdateFrameCache(tc); -// } -// } - -// onSelect() { -// return; -// } - -// deactivate(context: IPluginService): void { -// // context.stage.off('pointerdown', this.handleClick); -// context.stage.off('pointermove', this.handleMove); -// context.stage.off('pointerdown', this.handlePointerDown); -// context.stage.off('pointerup', this.handlePointerUp); -// context.stage.off('pointerleave', this.handlePointerUp); -// } - -// release() { -// this.editModule.release(); -// } -// } diff --git a/packages/vrender/package.json b/packages/vrender/package.json index ee5d4ffad..82b4cda62 100644 --- a/packages/vrender/package.json +++ b/packages/vrender/package.json @@ -25,7 +25,8 @@ }, "dependencies": { "@visactor/vrender-core": "workspace:0.22.8", - "@visactor/vrender-kits": "workspace:0.22.8" + "@visactor/vrender-kits": "workspace:0.22.8", + "@visactor/vrender-animate": "workspace:0.22.8" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/rush.json b/rush.json index 00fb58500..82285353f 100644 --- a/rush.json +++ b/rush.json @@ -40,6 +40,13 @@ "shouldPublish": true, "versionPolicyName": "vrenderMain" }, + { + "packageName": "@visactor/vrender-animate", + "projectFolder": "packages/vrender-animate", + "tags": ["package"], + "shouldPublish": true, + "versionPolicyName": "vrenderMain" + }, { "packageName": "@visactor/vrender-kits", "projectFolder": "packages/vrender-kits", From ad2e54553fb8e7d9dbc48788ac34366089c1cad2 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 14 Mar 2025 15:45:50 +0800 Subject: [PATCH 031/179] feat: complete base of vrender-animate --- packages/vrender-animate/src/animate.ts | 565 +++++++ packages/vrender-animate/src/index.ts | 15 + .../vrender-animate/src/interpolate/number.ts | 3 + .../vrender-animate/src/interpolate/store.ts | 211 +++ .../vrender-animate/src/intreface/animate.ts | 33 +- .../vrender-animate/src/intreface/ticker.ts | 55 + .../vrender-animate/src/intreface/timeline.ts | 18 +- packages/vrender-animate/src/step.ts | 208 +++ .../src/ticker/default-ticker.ts | 245 +++ .../src/ticker/manual-ticker.ts | 43 + packages/vrender-animate/src/timeline.ts | 148 ++ .../vrender-animate/src/utils/easing-func.ts | 11 + packages/vrender-animate/src/utils/easing.ts | 273 ++++ .../src/animate/Ticker/default-ticker.ts | 246 +++ .../vrender-core/src/animate/Ticker/index.ts | 5 + .../animate/Ticker/manual-ticker-handler.ts | 35 + .../src/animate/Ticker/manual-ticker.ts | 54 + .../src/animate/Ticker/raf-tick-handler.ts | 30 + .../animate/Ticker/timeout-tick-handler.ts | 29 + .../vrender-core/src/animate/Ticker/type.ts | 7 + packages/vrender-core/src/animate/animate.ts | 1307 ++++++++++++++++ packages/vrender-core/src/animate/config.ts | 11 + .../src/animate/custom-animate.ts | 1364 +++++++++++++++++ .../src/animate/default-ticker.ts | 7 + .../vrender-core/src/animate/easing-func.ts | 12 + packages/vrender-core/src/animate/easing.ts | 273 ++++ .../vrender-core/src/animate/group-fade.ts | 70 + packages/vrender-core/src/animate/index.ts | 8 + packages/vrender-core/src/animate/morphing.ts | 680 ++++++++ packages/vrender-core/src/animate/timeline.ts | 102 ++ .../src/color-string/interpolate.ts | 10 + .../browser/src/pages/animate-next.ts | 101 ++ .../__tests__/browser/src/pages/animate.ts | 25 +- .../__tests__/browser/src/pages/index.ts | 4 + .../vrender/__tests__/browser/vite.config.ts | 1 + 35 files changed, 6174 insertions(+), 35 deletions(-) create mode 100644 packages/vrender-animate/src/animate.ts create mode 100644 packages/vrender-animate/src/interpolate/number.ts create mode 100644 packages/vrender-animate/src/interpolate/store.ts create mode 100644 packages/vrender-animate/src/intreface/ticker.ts create mode 100644 packages/vrender-animate/src/step.ts create mode 100644 packages/vrender-animate/src/ticker/default-ticker.ts create mode 100644 packages/vrender-animate/src/ticker/manual-ticker.ts create mode 100644 packages/vrender-animate/src/timeline.ts create mode 100644 packages/vrender-animate/src/utils/easing-func.ts create mode 100644 packages/vrender-animate/src/utils/easing.ts create mode 100644 packages/vrender-core/src/animate/Ticker/default-ticker.ts create mode 100644 packages/vrender-core/src/animate/Ticker/index.ts create mode 100644 packages/vrender-core/src/animate/Ticker/manual-ticker-handler.ts create mode 100644 packages/vrender-core/src/animate/Ticker/manual-ticker.ts create mode 100644 packages/vrender-core/src/animate/Ticker/raf-tick-handler.ts create mode 100644 packages/vrender-core/src/animate/Ticker/timeout-tick-handler.ts create mode 100644 packages/vrender-core/src/animate/Ticker/type.ts create mode 100644 packages/vrender-core/src/animate/animate.ts create mode 100644 packages/vrender-core/src/animate/config.ts create mode 100644 packages/vrender-core/src/animate/custom-animate.ts create mode 100644 packages/vrender-core/src/animate/default-ticker.ts create mode 100644 packages/vrender-core/src/animate/easing-func.ts create mode 100644 packages/vrender-core/src/animate/easing.ts create mode 100644 packages/vrender-core/src/animate/group-fade.ts create mode 100644 packages/vrender-core/src/animate/index.ts create mode 100644 packages/vrender-core/src/animate/morphing.ts create mode 100644 packages/vrender-core/src/animate/timeline.ts create mode 100644 packages/vrender/__tests__/browser/src/pages/animate-next.ts diff --git a/packages/vrender-animate/src/animate.ts b/packages/vrender-animate/src/animate.ts new file mode 100644 index 000000000..d99f4e4aa --- /dev/null +++ b/packages/vrender-animate/src/animate.ts @@ -0,0 +1,565 @@ +import type { IAnimate, IStep } from './intreface/animate'; +import type { EasingType } from './intreface/easing'; +import { AnimateStatus, AnimateStepType } from './intreface/type'; +import { Step } from './step'; +import type { ITimeline } from './intreface/timeline'; +import type { ICustomAnimate, IGraphic } from '@visactor/vrender-core'; + +let uniqueId = 0; + +export class Animate implements IAnimate { + readonly id: string | number; + status: AnimateStatus = AnimateStatus.INITIAL; + target: IGraphic; + + // 回调函数列表 + _onStart?: (() => void)[] = []; + _onFrame?: ((step: IStep, ratio: number) => void)[] = []; + _onEnd?: (() => void)[] = []; + _onRemove?: (() => void)[] = []; + + // 时间控制 + private _timeline: ITimeline; + private _startTime: number = 0; + private _duration: number = 0; + private _totalDuration: number = 0; + + // 动画控制 + private _reversed: boolean = false; + private _loopCount: number = 0; + private _bounce: boolean = false; + + // 链表头节点和尾节点 + private _firstStep: IStep | null = null; + private _lastStep: IStep | null = null; + + // 初始属性和屏蔽的属性 + private _startProps: Record = {}; + private _endProps: Record = {}; + private _preventAttrs: Set = new Set(); + + protected currentTime: number = 0; + + constructor(id: string | number = uniqueId++) { + this.id = id; + } + + /** + * 获取开始属性 + */ + getStartProps(): Record { + return this._startProps; + } + + /** + * 获取结束属性 + */ + getEndProps(): Record { + return this._endProps; + } + + /** + * 设置时间线 + */ + setTimeline(timeline: ITimeline): void { + this._timeline = timeline; + } + + /** + * 获取时间线 + */ + getTimeline(): ITimeline { + return this._timeline; + } + + /** + * 时间线属性访问器 + */ + get timeline(): ITimeline { + return this._timeline; + } + + /** + * 绑定目标图形 + */ + bind(target: IGraphic): this { + this.target = target; + return this; + } + + /** + * 动画步骤:to + * 添加一个to步骤,这会在当前状态到指定状态间进行插值 + */ + to(props: Record, duration: number = 300, easing: EasingType = 'linear'): this { + // 创建新的step + const step = new Step(AnimateStepType.to, props, duration, easing, this); + + // 如果是第一个step + if (!this._firstStep) { + this._firstStep = step; + this._lastStep = step; + } else { + // 添加到链表末尾 + this._lastStep.append(step); + this._lastStep = step; + } + + // 保存最终属性 + step.propKeys = step.propKeys || Object.keys(step.props); + step.propKeys.forEach(key => { + this._endProps[key] = step.props[key]; + }); + + this.updateDuration(); + + return this; + } + + /** + * 动画步骤:from + * 添加一个from步骤,这会将目标属性先设置为指定值,然后过渡到当前状态 + * 【注意】这可能会导致动画跳变,请谨慎使用 + */ + from(props: Record, duration: number = 300, easing: EasingType = 'linear'): this { + // 创建新的step + const step = new Step(AnimateStepType.from, props, duration, easing, this); + + // 如果是第一个step + if (!this._firstStep) { + this._firstStep = step; + this._lastStep = step; + } else { + // 添加到链表末尾 + this._lastStep.append(step); + this._lastStep = step; + } + + this.updateDuration(); + + return this; + } + + /** + * 暂停动画 + */ + pause(): void { + if (this.status === AnimateStatus.RUNNING) { + this.status = AnimateStatus.PAUSED; + } + } + + /** + * 恢复动画 + */ + resume(): void { + if (this.status === AnimateStatus.PAUSED) { + this.status = AnimateStatus.RUNNING; + } + } + + /** + * 注册开始回调 + */ + onStart(cb?: () => void): void { + if (cb) { + if (!this._onStart) { + this._onStart = []; + } + this._onStart.push(cb); + } else { + this._onStart.forEach(cb => cb()); + // 设置开始属性,Animate不会重复执行start所以不需要判断firstStart + Object.keys(this._endProps).forEach(key => { + this._startProps[key] = this.target.getComputedAttribute(key); + }); + } + } + + /** + * 注册结束回调 + */ + onEnd(cb?: () => void): void { + if (cb) { + if (!this._onEnd) { + this._onEnd = []; + } + this._onEnd.push(cb); + } else { + this._onEnd.forEach(cb => cb()); + } + } + + /** + * 注册帧回调 + */ + onFrame(cb?: (step: IStep, ratio: number) => void): void { + if (cb) { + if (!this._onFrame) { + this._onFrame = []; + } + this._onFrame.push(cb); + } + } + + /** + * 屏蔽单个属性 + */ + preventAttr(key: string): void { + this._preventAttrs.add(key); + } + + /** + * 屏蔽多个属性 + */ + preventAttrs(keys: string[]): void { + keys.forEach(key => this._preventAttrs.add(key)); + } + + /** + * 检查属性是否合法(未被屏蔽) + */ + validAttr(key: string): boolean { + return !this._preventAttrs.has(key); + } + + /** + * 运行自定义回调 + */ + runCb(cb: (a: IAnimate, step: IStep) => void): IAnimate { + this._lastStep?.onEnd(cb); + return this; + } + + /** + * 设置动画开始时间 + */ + startAt(t: number): this { + this._startTime = t; + + return this; + } + + /** + * 自定义插值函数,返回false表示没有匹配上 + */ + customInterpolate( + key: string, + ratio: number, + from: any, + to: any, + target: IGraphic, + ret: Record + ): boolean { + // 默认无自定义插值,可由子类重写 + return false; + } + + /** + * 自定义动画 + */ + play(customAnimate: ICustomAnimate): this { + // 创建新的step + const step = new Step(AnimateStepType.customAnimate, { customAnimate }, 0, 'linear', this); + + // 如果是第一个step + if (!this._firstStep) { + this._firstStep = step; + this._lastStep = step; + } else { + // 添加到链表末尾 + this._lastStep.append(step); + this._lastStep = step; + } + + this.updateDuration(); + + return this; + } + + /** + * 获取起始值,该起始值为animate的起始值,并不一定为step的起始值 + */ + getFromValue(): Record { + return this._startProps; + } + + /** + * 获取结束值 + */ + getToValue(): Record { + return this._endProps; + } + + /** + * 停止动画 + */ + stop(type?: 'start' | 'end' | Record): void { + if (this.status !== AnimateStatus.RUNNING) { + return; + } + + this.status = AnimateStatus.END; + + if (!this.target) { + return; + } + + if (type === 'start') { + // 设置为开始状态 + this.target.setAttributes(this._startProps); + } else if (type === 'end') { + // 设置为结束状态 + this.target.setAttributes(this._endProps); + } else if (type) { + // 设置为自定义状态 + this.target.setAttributes(type); + } + } + + /** + * 释放动画资源 + */ + release(): void { + this.status = AnimateStatus.END; + + // 触发移除回调 + if (this._onRemove) { + this._onRemove.forEach(cb => cb()); + } + + // 清空回调 + this._onStart = []; + this._onFrame = []; + this._onEnd = []; + this._onRemove = []; + } + + /** + * 获取动画持续时间 + */ + getDuration(): number { + return this._duration; + } + + /** + * 获取动画开始时间 + */ + getStartTime(): number { + return this._startTime; + } + + /** + * 等待延迟 + */ + wait(delay: number): this { + // 创建新的wait step + const step = new Step(AnimateStepType.wait, {}, delay, 'linear', this); + + // 如果是第一个step + if (!this._firstStep) { + this._firstStep = step; + this._lastStep = step; + } else { + // 添加到链表末尾 + this._lastStep.append(step); + this._lastStep = step; + } + + this.updateDuration(); + + return this; + } + + /** + * 在所有动画完成后执行 + */ + afterAll(list: IAnimate[]): this { + if (!list || list.length === 0) { + return this; + } + + // 计算所有动画结束的最大时间点 + let maxEndTime = 0; + list.forEach(animate => { + const endTime = animate.getStartTime() + animate.getDuration(); + maxEndTime = Math.max(maxEndTime, endTime); + }); + + // 设置当前动画的开始时间为最大结束时间 + return this.startAt(maxEndTime); + } + + /** + * 在指定动画完成后执行 + */ + after(animate: IAnimate): this { + if (!animate) { + return this; + } + + // 计算指定动画结束的时间点 + const endTime = animate.getStartTime() + animate.getDuration(); + + // 设置当前动画的开始时间为结束时间 + return this.startAt(endTime); + } + + /** + * 并行执行动画 + */ + parallel(animate: IAnimate): this { + if (!animate) { + return this; + } + + // 设置指定动画的开始时间为当前动画的开始时间 + this.startAt(animate.getStartTime()); + + return this; + } + + /** + * 设置动画是否反转 + */ + reversed(r: boolean): this { + this._reversed = r; + return this; + } + + /** + * 设置动画循环次数 + */ + loop(n: number): this { + this._loopCount = n; + return this; + } + + /** + * 设置动画是否反弹 + */ + bounce(b: boolean): this { + this._bounce = b; + return this; + } + + /** + * 推进动画 + */ + advance(delta: number): void { + const nextTime = this.currentTime + delta; + + // 如果还没开始,直接return + if (nextTime < this._startTime) { + return; + } + // 如果已经结束,设置状态后return + if (nextTime >= this._startTime + this._totalDuration) { + this.onEnd(); + this.status = AnimateStatus.END; + return; + } + + this.status = AnimateStatus.RUNNING; + + const deltaFotStep = nextTime - this._startTime; + + // 如果是第一次运行,触发开始回调 + if (this.currentTime <= this._startTime) { + this.onStart(); + } + this.currentTime = nextTime; + + let cycleTime = deltaFotStep % this._duration; + + // 如果是反转动画,需要反转周期内的时间 + if (this._reversed) { + cycleTime = this._duration - cycleTime; + } + + // 选择起始步骤和遍历方向 + let targetStep: IStep | null = null; + + if (this._lastStep === this._firstStep) { + targetStep = this._firstStep; + } else { + let currentStep: IStep | null = null; + if (this._reversed) { + // 反转时从最后一个步骤开始查找 + currentStep = this._lastStep; + + // 从后向前寻找当前时间所在的step + while (currentStep) { + const stepEndTime = currentStep.getStartTime() + currentStep.getDuration(); + + // 反转时,我们需要从动画结束时间向开始时间查找 + if (cycleTime <= stepEndTime && cycleTime > currentStep.getStartTime()) { + targetStep = currentStep; + break; + } + + currentStep = currentStep.prev; + } + } else { + // 正常顺序从第一个步骤开始查找 + currentStep = this._firstStep; + + // 从前向后寻找当前时间所在的step + while (currentStep) { + const stepStartTime = currentStep.getStartTime(); + const stepDuration = currentStep.getDuration(); + const stepEndTime = stepStartTime + stepDuration; + + // 找到当前周期时间所在的step + if (cycleTime >= stepStartTime && cycleTime < stepEndTime) { + targetStep = currentStep; + break; + } + + currentStep = currentStep.next; + } + } + } + + // 如果没找到目标step(可能是所有step都执行完了,但整体动画还没结束,这正常是不存在的) + if (!targetStep) { + this.currentTime = nextTime; + console.warn('动画出现问题'); + return; + } + + // 计算当前step的进度比例(基于当前step内的相对时间) + const stepStartTime = targetStep.getStartTime(); + const stepDuration = targetStep.getDuration(); + + const ratio = this._reversed + ? (stepStartTime + stepDuration - cycleTime) / stepDuration + : (cycleTime - stepStartTime) / stepDuration; + // // 限制ratio在0-1之间 + // ratio = Math.max(0, Math.min(1, ratio)); + + const isEnd = ratio >= 1; + targetStep.onUpdate(isEnd, ratio, {}); + + // 如果step执行完毕 + if (isEnd) { + targetStep.onEnd(); + // 不立即调用onFinish,让动画系统来决定何时结束 + } + + // 触发帧回调 + // if (this._onFrame) { + // this._onFrame.forEach(cb => cb(targetStep, ratio)); + // } + } + + protected updateDuration(): void { + if (!this._lastStep) { + this._duration = 0; + return; + } + + this._duration = this._lastStep.getStartTime() + this._lastStep.getDuration(); + this._totalDuration = this._duration * (this._loopCount + 1); + } +} diff --git a/packages/vrender-animate/src/index.ts b/packages/vrender-animate/src/index.ts index e69de29bb..bf0043130 100644 --- a/packages/vrender-animate/src/index.ts +++ b/packages/vrender-animate/src/index.ts @@ -0,0 +1,15 @@ +// 导出接口 +export * from './intreface/animate'; +export * from './intreface/timeline'; +export * from './intreface/easing'; +export * from './intreface/type'; + +// 导出实现 +export { Animate } from './animate'; +export { DefaultTimeline } from './timeline'; +export { ManualTicker } from './ticker/manual-ticker'; +export { DefaultTicker } from './ticker/default-ticker'; +export { Step } from './step'; + +// 导出工具函数 +export * from './utils/easing-func'; diff --git a/packages/vrender-animate/src/interpolate/number.ts b/packages/vrender-animate/src/interpolate/number.ts new file mode 100644 index 000000000..6abc9faaf --- /dev/null +++ b/packages/vrender-animate/src/interpolate/number.ts @@ -0,0 +1,3 @@ +export function interpolateNumber(from: number, to: number, ratio: number): number { + return from + (to - from) * ratio; +} diff --git a/packages/vrender-animate/src/interpolate/store.ts b/packages/vrender-animate/src/interpolate/store.ts new file mode 100644 index 000000000..fc9c87fef --- /dev/null +++ b/packages/vrender-animate/src/interpolate/store.ts @@ -0,0 +1,211 @@ +import type { IGraphic } from '@visactor/vrender-core'; +import { interpolateColor, interpolatePureColorArray, interpolatePureColorArrayToStr } from '@visactor/vrender-core'; +import { interpolateNumber } from './number'; +import type { IStep } from '../intreface/animate'; + +// 直接设置,触发 隐藏类(Hidden Class)优化: +/** + * +const a = { type: 1 }; +const ITERATIONS = 1e7; // 测试次数 + +// 动态生成 keys 数组(确保引擎无法静态推断 key) +const keys = []; +for (let i = 0; i < ITERATIONS; i++) { + // 通过条件确保 key 动态变化(但实际始终为 'type',避免属性缺失的开销) + keys.push(Math.random() < 0 ? 'other' : 'type'); +} + +// 测试字面量访问 +function testLiteral() { + let sum = 0; + for (let i = 0; i < ITERATIONS; i++) { + const key = keys[i]; // 读取 key(与动态测试完全一致) + sum += a.type; // 差异仅在此处:使用字面量访问 + } + return sum; +} + +// 测试变量动态访问 +function testDynamic() { + let sum = 0; + for (let i = 0; i < ITERATIONS; i++) { + const key = keys[i]; // 读取 key(与字面量测试完全一致) + sum += a[key]; // 差异仅在此处:使用变量访问 + } + return sum; +} + +// 预热(避免 JIT 编译影响) +testLiteral(); +testDynamic(); + +// 正式测试 +console.time('literal'); +testLiteral(); +console.timeEnd('literal'); + +console.time('dynamic'); +testDynamic(); +console.timeEnd('dynamic'); + + +// out: +// literal: 7.1259765625 ms +// dynamic: 9.322998046875 ms + */ + +export class InterpolateUpdateStore { + opacity = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + target.attribute.opacity = interpolateNumber(from, to, ratio); + }; + fillOpacity = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + target.attribute.fillOpacity = interpolateNumber(from, to, ratio); + }; + strokeOpacity = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + target.attribute.strokeOpacity = interpolateNumber(from, to, ratio); + }; + zIndex = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + target.attribute.zIndex = interpolateNumber(from, to, ratio); + }; + backgroundOpacity = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + target.attribute.backgroundOpacity = interpolateNumber(from, to, ratio); + }; + shadowOffsetX = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + target.attribute.shadowOffsetX = interpolateNumber(from, to, ratio); + }; + shadowOffsetY = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + target.attribute.shadowOffsetY = interpolateNumber(from, to, ratio); + }; + shadowBlur = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + target.attribute.shadowBlur = interpolateNumber(from, to, ratio); + }; + fill = ( + key: string, + from: [number, number, number, number], + to: [number, number, number, number], + ratio: number, + step: IStep, + target: IGraphic + ) => { + target.attribute.fill = interpolateColor(from, to, ratio, false) as any; + }; + fillPure = ( + key: string, + from: [number, number, number, number], + to: [number, number, number, number], + ratio: number, + step: IStep, + target: IGraphic + ) => { + target.attribute.fill = interpolatePureColorArrayToStr( + step.fromParsedProps.fill, + step.toParsedProps.fill, + ratio + ) as any; + }; + stroke = ( + key: string, + from: [number, number, number, number], + to: [number, number, number, number], + ratio: number, + step: IStep, + target: IGraphic + ) => { + target.attribute.stroke = interpolateColor(from, to, ratio, false); + }; + strokePure = ( + key: string, + from: [number, number, number, number], + to: [number, number, number, number], + ratio: number, + step: IStep, + target: IGraphic + ) => { + target.attribute.stroke = interpolatePureColorArrayToStr( + step.fromParsedProps.stroke, + step.toParsedProps.stroke, + ratio + ) as any; + }; + + // 需要更新Bounds + width = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + (target.attribute as any).width = interpolateNumber(from, to, ratio); + target.addUpdateBoundTag(); + }; + height = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + (target.attribute as any).height = interpolateNumber(from, to, ratio); + target.addUpdateBoundTag(); + }; + x = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + target.attribute.x = interpolateNumber(from, to, ratio); + target.addUpdateBoundTag(); + }; + y = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + target.attribute.y = interpolateNumber(from, to, ratio); + target.addUpdateBoundTag(); + }; + angle = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + target.attribute.angle = interpolateNumber(from, to, ratio); + target.addUpdateBoundTag(); + }; + scaleX = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + target.attribute.scaleX = interpolateNumber(from, to, ratio); + target.addUpdateBoundTag(); + }; + scaleY = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + target.attribute.scaleY = interpolateNumber(from, to, ratio); + target.addUpdateBoundTag(); + }; + lineWidth = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + target.attribute.lineWidth = interpolateNumber(from, to, ratio); + target.addUpdateBoundTag(); + }; + startAngle = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + (target.attribute as any).startAngle = interpolateNumber(from, to, ratio); + target.addUpdateBoundTag(); + }; + endAngle = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + (target.attribute as any).endAngle = interpolateNumber(from, to, ratio); + target.addUpdateBoundTag(); + }; + radius = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + (target.attribute as any).radius = interpolateNumber(from, to, ratio); + target.addUpdateBoundTag(); + }; + outerRadius = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + (target.attribute as any).outerRadius = interpolateNumber(from, to, ratio); + target.addUpdateBoundTag(); + }; + innerRadius = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + (target.attribute as any).innerRadius = interpolateNumber(from, to, ratio); + target.addUpdateBoundTag(); + }; +} + +export const interpolateUpdateStore = new InterpolateUpdateStore(); + +export function commonInterpolateUpdate(key: string, from: any, to: any, ratio: number, step: IStep, target: IGraphic) { + if (Number.isFinite(to) && Number.isFinite(from)) { + target.attribute = from + (to - from) * ratio; + return true; + } else if (Array.isArray(to) && Array.isArray(from) && to.length === from.length) { + const nextList = []; + let valid = true; + for (let i = 0; i < to.length; i++) { + const v = from[i]; + const val = v + (to[i] - v) * ratio; + if (!Number.isFinite(val)) { + valid = false; + break; + } + nextList.push(val); + } + if (valid) { + (target.attribute as any)[key] = nextList; + } + return true; + } + return false; +} diff --git a/packages/vrender-animate/src/intreface/animate.ts b/packages/vrender-animate/src/intreface/animate.ts index a5b618a73..ff2ebf96a 100644 --- a/packages/vrender-animate/src/intreface/animate.ts +++ b/packages/vrender-animate/src/intreface/animate.ts @@ -1,20 +1,20 @@ -import type { ICustomAnimate, IGraphic, ITimeline } from '@visactor/vrender-core'; +import type { ICustomAnimate, IGraphic } from '@visactor/vrender-core'; import type { EasingType, EasingTypeFunc } from './easing'; import type { AnimateStatus, IAnimateStepType } from './type'; +import type { ITimeline } from './timeline'; export interface IStep { type: IAnimateStepType; prev?: IStep; // 持续时间 duration: number; - // 在animate中的位置 - position: number; // 链表,下一个 next?: IStep; // 属性 props?: Record; // 解析后的属性(用于性能优化,避免每次tick都解析) - parsedProps?: any; + fromParsedProps?: Record; + toParsedProps?: Record; // 解析后的属性列表(用于性能优化,避免每次tick都解析) propKeys?: string[]; // 缓动函数 @@ -28,16 +28,18 @@ export interface IStep { animate: IAnimate; // 设置持续时间 - setDuration: (duration: number) => void; + setDuration: (duration: number, updateDownstream?: boolean) => void; // 获取持续时间 getDuration: () => number; - // 确定插值函数(在开始的时候就确定,避免每次tick都解析) - determineInterpolationFunction: () => void; - // 确定更新函数(在开始的时候就确定,避免每次tick都解析) - determineUpdateFunction: () => void; + // // 确定插值函数(在开始的时候就确定,避免每次tick都解析) + // determineInterpolationFunction: () => void; + // // 确定更新函数(在开始的时候就确定,避免每次tick都解析) + // determineUpdateFunction: () => void; + // 确定插值更新函数(在开始的时候就确定,避免每次tick都解析) + determineInterpolateUpdateFunction: () => void; // 设置开始时间 - setStartTime: (time: number) => void; + setStartTime: (time: number, updateDownstream?: boolean) => void; // 获取开始时间 getStartTime: () => number; @@ -48,11 +50,9 @@ export interface IStep { // 开始执行的时候调用(如果有循环,那每个周期都会调用) onStart: () => void; // 结束执行的时候调用(如果有循环,那每个周期都会调用) - onEnd: () => void; + onEnd: (cb?: (animate: IAnimate, step: IStep) => void) => void; // 更新执行的时候调用(如果有循环,那每个周期都会调用) onUpdate: (end: boolean, ratio: number, out: Record) => void; - // 结束的时候调用(如果有循环,会在整个循环最终结束的时候调用) - onFinish: () => void; } export interface IAnimate { @@ -60,8 +60,6 @@ export interface IAnimate { status: AnimateStatus; target: IGraphic; - interpolateFunc: (key: string, ratio: number, from: any, to: any, nextAttributes: any) => boolean; - _onStart?: (() => void)[]; _onFrame?: ((step: IStep, ratio: number) => void)[]; _onEnd?: (() => void)[]; @@ -132,11 +130,6 @@ export interface IAnimate { // 反弹动画 bounce: (b: boolean) => IAnimate; - // 下一个动画指针 - nextAnimate?: IAnimate; - // 上一个动画指针 - prevAnimate?: IAnimate; - advance: (delta: number) => void; // 设置开始时间(startAt之前是完全不会进入动画生命周期的) diff --git a/packages/vrender-animate/src/intreface/ticker.ts b/packages/vrender-animate/src/intreface/ticker.ts new file mode 100644 index 000000000..c6c43ca34 --- /dev/null +++ b/packages/vrender-animate/src/intreface/ticker.ts @@ -0,0 +1,55 @@ +/** + * Ticker Types for Animation Graph + */ + +import type { EventEmitter } from '@visactor/vutils'; +import type { ITimeline } from './timeline'; + +export type TickerMode = 'raf' | 'timeout' | 'manual'; + +export enum STATUS { + INITIAL = 0, // initial represents initial state + RUNNING = 1, // running represents executing + PAUSE = 2 // PAUSE represents tick continues but functions are not executed +} + +export interface ITickHandler { + /** + * Start executing tick + * @param interval Delay in ms + * @param cb Callback to execute + */ + tick: (interval: number, cb: (handler: ITickHandler) => void) => void; + tickTo?: (t: number, cb: (handler: ITickHandler, params?: { once: boolean }) => void) => void; + getTime: () => number; // Get current time + release: () => void; +} + +export interface ITickerHandlerStatic { + new (): ITickHandler; +} + +export interface ITicker extends EventEmitter { + setFPS?: (fps: number) => void; + setInterval?: (interval: number) => void; + getFPS?: () => number; + getInterval?: () => number; + tick: (interval: number) => void; + tickAt?: (time: number) => void; + pause: () => boolean; + resume: () => boolean; + /** + * Start ticking, if force is true, start regardless; + * otherwise, don't start if timeline is empty + */ + start: (force?: boolean) => boolean; + stop: () => void; + addTimeline: (timeline: ITimeline) => void; + remTimeline: (timeline: ITimeline) => void; + trySyncTickStatus: () => void; + getTimelines: () => ITimeline[]; + release: () => void; + + // Whether to automatically stop, default is true + autoStop: boolean; +} diff --git a/packages/vrender-animate/src/intreface/timeline.ts b/packages/vrender-animate/src/intreface/timeline.ts index b9c0e1bc4..14d628beb 100644 --- a/packages/vrender-animate/src/intreface/timeline.ts +++ b/packages/vrender-animate/src/intreface/timeline.ts @@ -16,24 +16,14 @@ export interface ITimeline { pause: () => void; // 恢复动画 resume: () => void; - // 设置开始时间 - setStartTime: (time: number) => void; - // 获取开始时间 - getStartTime: () => number; - // 获取当前时间 - getCurrentTime: () => number; - // 设置当前时间 - setCurrentTime: (time: number) => void; // 获取动画总时长 getTotalDuration: () => number; - // 获取动画的播放状态 - getPlayState: () => 'playing' | 'paused' | 'stopped'; // 获取动画的播放速度 getPlaySpeed: () => number; // 设置动画的播放速度 setPlaySpeed: (speed: number) => void; - // 获取动画的播放方向 - getPlayDirection: () => 'forward' | 'backward'; - // 设置动画的播放方向 - setPlayDirection: (direction: 'forward' | 'backward') => void; + // 获取动画的播放状态 + getPlayState: () => 'playing' | 'paused' | 'stopped'; + // 获取动画是否正在运行 + isRunning: () => boolean; } diff --git a/packages/vrender-animate/src/step.ts b/packages/vrender-animate/src/step.ts new file mode 100644 index 000000000..7dd556608 --- /dev/null +++ b/packages/vrender-animate/src/step.ts @@ -0,0 +1,208 @@ +import { ColorStore, ColorType, type IGraphic } from '@visactor/vrender-core'; +import type { IAnimate, IStep } from './intreface/animate'; +import type { EasingType, EasingTypeFunc } from './intreface/easing'; +import type { IAnimateStepType } from './intreface/type'; +import { Easing } from './utils/easing'; +import { commonInterpolateUpdate, interpolateUpdateStore } from './interpolate/store'; +import { isString } from '@visactor/vutils'; + +function noop() { + //... +} + +export class Step implements IStep { + type: IAnimateStepType; + prev?: IStep; + duration: number; + next?: IStep; + props?: Record; + propKeys?: string[]; + interpolateUpdateFunctions?: (( + key: string, + from: number, + to: number, + ratio: number, + step: IStep, + target: IGraphic + ) => void)[]; + easing?: EasingTypeFunc; + animate: IAnimate; + target: IGraphic; + fromProps: Record; + fromParsedProps: Record; + toParsedProps: Record; + + // 内部状态 + protected _startTime: number = 0; + _hasFirstRun: boolean = false; + + protected _endCb?: (animate: IAnimate, step: IStep) => void; + + constructor( + type: IAnimateStepType, + props: Record, + duration: number, + easing: EasingType, + animate: IAnimate + ) { + this.type = type; + this.props = props; + this.duration = duration; + this.animate = animate; + this.target = animate.target; + // 设置缓动函数 + if (easing) { + this.easing = typeof easing === 'function' ? easing : Easing[easing]; + } + + this.onBind(); + if (type === 'wait') { + this.onUpdate = noop; + } + } + + append(step: IStep): void { + this.next = step; + step.prev = this; + + // 更新绝对时间 + step.setStartTime(this.getStartTime() + this.duration, false); + } + + // 更新下游节点的开始时间 + private updateDownstreamStartTimes(): void { + let currentStep = this.next; + let currentStartTime = this._startTime + this.duration; + + while (currentStep) { + currentStep.setStartTime(currentStartTime, false); + currentStartTime += currentStep.duration; + currentStep = currentStep.next; + } + } + + getLastProps(): any { + if (this.prev) { + return this.prev.props || {}; + } + return this.animate.getStartProps(); + } + + setDuration(duration: number, updateDownstream: boolean = true): void { + this.duration = duration; + + // 如果有后续节点,更新所有后续节点的开始时间 + if (updateDownstream) { + this.updateDownstreamStartTimes(); + } + } + + getDuration(): number { + return this.duration; + } + + determineInterpolateUpdateFunction(): void { + // 根据属性类型确定插值更新函数 + // 这里可以进行优化,例如缓存不同类型属性的插值更新函数 + if (!this.props) { + return; + } + + this.propKeys = Object.keys(this.props); + const funcs: ((key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => void)[] = []; + this.propKeys.forEach(key => { + // 普通颜色特殊处理,需要提前解析成number[] + if (key === 'fill' || key === 'stroke') { + const from = this.getLastProps()[key]; + const to = this.props[key]; + if (isString(from) && isString(to)) { + const fromArray = ColorStore.Get(from, ColorType.Color255); + const toArray = ColorStore.Get(to, ColorType.Color255); + if (!this.fromParsedProps) { + this.fromParsedProps = {}; + } + if (!this.toParsedProps) { + this.toParsedProps = {}; + } + this.fromParsedProps[key] = fromArray; + this.toParsedProps[key] = toArray; + } + funcs.push((interpolateUpdateStore as any)[key === 'fill' ? 'fillPure' : 'strokePure']); + } else if ((interpolateUpdateStore as any)[key]) { + funcs.push((interpolateUpdateStore as any)[key]); + } else { + funcs.push(commonInterpolateUpdate); + } + }); + this.interpolateUpdateFunctions = funcs; + } + + setStartTime(time: number, updateDownstream: boolean = true): void { + this._startTime = time; + if (updateDownstream) { + this.updateDownstreamStartTimes(); + } + } + + getStartTime(): number { + return this._startTime; + } + + onBind(): void { + // 在第一次绑定到Animate的时候触发 + } + + /** + * 首次运行逻辑 + * 如果跳帧了就不一定会执行 + */ + onFirstRun(): void { + // 首次运行逻辑 + } + + /** + * 开始执行的时候调用 + * 如果跳帧了就不一定会执行 + */ + onStart(): void { + if (!this._hasFirstRun) { + this._hasFirstRun = true; + this.onFirstRun(); + // 获取上一步的属性值作为起始值 + this.fromProps = this.getLastProps(); + this.determineInterpolateUpdateFunction(); + } + } + + /** + * 更新执行的时候调用 + * 如果跳帧了就不一定会执行 + */ + onUpdate = (end: boolean, ratio: number, out: Record): void => { + this.onStart(); + if (!this.props || !this.propKeys) { + return; + } + // 应用缓动函数 + const easedRatio = this.easing(ratio); + this.interpolateUpdateFunctions.forEach((func, index) => { + const key = this.propKeys[index]; + const fromValue = this.fromProps[key]; + const toValue = this.props[key]; + func(key, fromValue, toValue, easedRatio, this, this.target); + }); + }; + + /** + * 结束执行的时候调用 + * 如果跳帧了就不一定会执行 + */ + onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { + if (cb) { + this._endCb = cb; + } else if (this._endCb) { + this.target.setAttributes(this.props); + this._endCb(this.animate, this); + } + } +} diff --git a/packages/vrender-animate/src/ticker/default-ticker.ts b/packages/vrender-animate/src/ticker/default-ticker.ts new file mode 100644 index 000000000..095ba59cb --- /dev/null +++ b/packages/vrender-animate/src/ticker/default-ticker.ts @@ -0,0 +1,245 @@ +import { EventEmitter } from '@visactor/vutils'; +import type { IStage } from '@visactor/vrender-core'; +import { application, PerformanceRAF } from '@visactor/vrender-core'; +import { type ITickHandler, type ITicker, STATUS } from '../intreface/ticker'; +import type { ITimeline } from '../intreface/timeline'; + +const performanceRAF = new PerformanceRAF(); + +class RAFTickHandler implements ITickHandler { + protected released: boolean = false; + + tick(interval: number, cb: (handler: ITickHandler) => void): void { + performanceRAF.addAnimationFrameCb(() => { + if (this.released) { + return; + } + cb(this); + }); + } + + release(): void { + this.released = true; + } + + getTime(): number { + return Date.now(); + } +} + +/** + * Graph-based Ticker implementation + * This ticker works directly with GraphManager instances without needing timeline adapters + */ +export class DefaultTicker extends EventEmitter implements ITicker { + protected interval: number = 16; + protected tickerHandler: ITickHandler; + protected status: STATUS; + protected lastFrameTime: number = -1; + protected tickCounts: number = 0; + protected stage: IStage; + timelines: ITimeline[] = []; + autoStop: boolean = true; + + constructor(stage: IStage) { + super(); + this.init(); + this.lastFrameTime = -1; + this.tickCounts = 0; + this.stage = stage; + this.autoStop = true; + } + + init(): void { + this.interval = 16; + this.status = STATUS.INITIAL; + application.global.hooks.onSetEnv.tap('graph-ticker', () => { + this.initHandler(); + }); + if (application.global.env) { + this.initHandler(); + } + } + + addTimeline(timeline: ITimeline): void { + this.timelines.push(timeline); + } + + remTimeline(timeline: ITimeline): void { + this.timelines = this.timelines.filter(t => t !== timeline); + } + + getTimelines(): ITimeline[] { + return this.timelines; + } + + protected initHandler() { + this.setupTickHandler(); + } + + /** + * Set up the tick handler + * @returns true if setup was successful, false otherwise + */ + protected setupTickHandler(): boolean { + const handler: ITickHandler = new RAFTickHandler(); + + // Destroy the previous tick handler + if (this.tickerHandler) { + this.tickerHandler.release(); + } + + this.tickerHandler = handler; + return true; + } + + setInterval(interval: number): void { + this.interval = interval; + } + + getInterval(): number { + return this.interval; + } + + setFPS(fps: number): void { + this.setInterval(1000 / fps); + } + + getFPS(): number { + return 1000 / this.interval; + } + + tick(interval: number): void { + this.tickerHandler.tick(interval, (handler: ITickHandler) => { + this.handleTick(handler, { once: true }); + }); + } + + tickTo(t: number): void { + if (!this.tickerHandler.tickTo) { + return; + } + this.tickerHandler.tickTo(t, (handler: ITickHandler) => { + this.handleTick(handler, { once: true }); + }); + } + + pause(): boolean { + if (this.status === STATUS.INITIAL) { + return false; + } + this.status = STATUS.PAUSE; + return true; + } + + resume(): boolean { + if (this.status === STATUS.INITIAL) { + return false; + } + this.status = STATUS.RUNNING; + return true; + } + + ifCanStop(): boolean { + if (this.autoStop) { + if (!this.timelines.length) { + return true; + } + if (this.timelines.every(timeline => !timeline.isRunning())) { + return true; + } + } + return false; + } + + start(force: boolean = false): boolean { + if (this.status === STATUS.RUNNING) { + return false; + } + if (!this.tickerHandler) { + return false; + } + + // 暂停中、或者应该停止的时候,就不执行 + if (!force) { + if (this.status === STATUS.PAUSE) { + return false; + } + if (this.ifCanStop()) { + return false; + } + } + + this.status = STATUS.RUNNING; + this.tickerHandler.tick(0, this.handleTick); + return true; + } + + stop(): void { + // Reset the tick handler + this.status = STATUS.INITIAL; + this.setupTickHandler(); + this.lastFrameTime = -1; + } + + /** + * 用于自动启动或停止 + * 基于当前的graph managers检查是否需要启动或停止 + */ + trySyncTickStatus(): void { + if (this.status === STATUS.INITIAL && this.timelines.some(timeline => timeline.isRunning())) { + this.start(); + } else if (this.status === STATUS.RUNNING && this.timelines.every(timeline => !timeline.isRunning())) { + this.stop(); + } + } + + release(): void { + this.stop(); + this.timelines = []; + this.tickerHandler?.release(); + this.tickerHandler = null; + } + + protected handleTick = (handler: ITickHandler, params?: { once?: boolean }): void => { + const { once = false } = params ?? {}; + + // 尝试停止 + if (this.ifCanStop()) { + this.stop(); + return; + } + + this._handlerTick(); + + if (!once) { + handler.tick(this.interval, this.handleTick); + } + }; + + protected _handlerTick = (): void => { + // Specific execution function + const tickerHandler = this.tickerHandler; + const time = tickerHandler.getTime(); + + // Time passed since last frame + let delta = 0; + if (this.lastFrameTime >= 0) { + delta = time - this.lastFrameTime; + } + this.lastFrameTime = time; + + if (this.status !== STATUS.RUNNING) { + return; + } + + this.tickCounts++; + + // Update all graph managers + this.timelines.forEach(timeline => { + timeline.tick(delta); + }); + + this.emit('tick', delta); + }; +} diff --git a/packages/vrender-animate/src/ticker/manual-ticker.ts b/packages/vrender-animate/src/ticker/manual-ticker.ts new file mode 100644 index 000000000..97fda967b --- /dev/null +++ b/packages/vrender-animate/src/ticker/manual-ticker.ts @@ -0,0 +1,43 @@ +import type { ITickHandler } from '../intreface/ticker'; +import { type ITicker } from '../intreface/ticker'; +import { DefaultTicker } from './default-ticker'; + +class ManualTickHandler implements ITickHandler { + protected released: boolean = false; + protected startTime: number = -1; + protected currentTime: number = -1; + + tick(interval: number, cb: (handler: ITickHandler) => void): void { + if (this.startTime < 0) { + this.startTime = 0; + } + this.currentTime = this.startTime + interval; + cb(this); + } + + release(): void { + this.released = true; + } + + getTime(): number { + return this.currentTime; + } +} + +export class ManualTicker extends DefaultTicker implements ITicker { + protected setupTickHandler(): boolean { + const handler: ITickHandler = new ManualTickHandler(); + + // Destroy the previous tick handler + if (this.tickerHandler) { + this.tickerHandler.release(); + } + + this.tickerHandler = handler; + return true; + } + + getTime(): number { + return this.tickerHandler.getTime(); + } +} diff --git a/packages/vrender-animate/src/timeline.ts b/packages/vrender-animate/src/timeline.ts new file mode 100644 index 000000000..bf088dc29 --- /dev/null +++ b/packages/vrender-animate/src/timeline.ts @@ -0,0 +1,148 @@ +import { AnimateStatus, Generator } from '@visactor/vrender-core'; +import type { IAnimate } from './intreface/animate'; +import type { ITimeline } from './intreface/timeline'; + +export class DefaultTimeline implements ITimeline { + declare id: number; + protected animates: IAnimate[] = []; + protected declare ticker: any; + protected declare paused: boolean; + + // 添加必要的属性 + protected _playSpeed: number = 1; + protected _totalDuration: number = 0; + protected _startTime: number = 0; + protected _currentTime: number = 0; + + // 0 ... _endAnimatePtr ... animates.length + // [0, _endAnimatePtr] 表示正在运行的动画 + // (_endAnimatePtr, animates.length) 表示已经结束的动画 + protected _endAnimatePtr: number = -1; + + get animateCount() { + return this.animates.length; + } + + constructor() { + this.id = Generator.GenAutoIncrementId(); + this.animates = []; + this.paused = false; + } + + isRunning() { + return !this.paused && this._endAnimatePtr >= 0; + } + + forEachAccessAnimate(cb: (animate: IAnimate, index: number) => void) { + for (let i = 0; i <= this._endAnimatePtr; i++) { + cb(this.animates[i], i); + } + } + + addAnimate(animate: IAnimate) { + this.animates.push(animate); + // 交换位置 + this._endAnimatePtr++; + this.animates[this.animates.length - 1] = this.animates[this._endAnimatePtr]; + this.animates[this._endAnimatePtr] = animate; + // 更新总时长 + this._totalDuration = Math.max(this._totalDuration, animate.getStartTime() + animate.getDuration()); + } + + pause() { + this.paused = true; + } + + resume() { + this.paused = false; + } + + tick(delta: number) { + if (this.paused) { + return; + } + + // 应用播放速度 + const scaledDelta = delta * this._playSpeed; + + // 更新当前时间 + this._currentTime += scaledDelta; + + this.forEachAccessAnimate((animate, i) => { + if (animate.status === AnimateStatus.END) { + this.removeAnimate(animate, true, i); + } else if (animate.status === AnimateStatus.RUNNING || animate.status === AnimateStatus.INITIAL) { + animate.advance(scaledDelta); + } + }); + } + + clear() { + this.forEachAccessAnimate(animate => { + animate.release(); + }); + this.animates = []; + this._totalDuration = 0; + } + + removeAnimate(animate: IAnimate, release: boolean = true, index?: number) { + if (this._endAnimatePtr < 0) { + return; + } + animate._onRemove && animate._onRemove.forEach(cb => cb()); + release && animate.release(); + + index = index ?? this.animates.indexOf(animate); + // 交换位置 + this.animates[index] = this.animates[this._endAnimatePtr]; + this._endAnimatePtr--; + return; + } + + // 重新计算总时长 + protected recalculateTotalDuration() { + this._totalDuration = 0; + this.animates.forEach(animate => { + this._totalDuration = Math.max(this._totalDuration, animate.getStartTime() + animate.getDuration()); + }); + } + + getTotalDuration() { + return this._totalDuration; + } + + getPlaySpeed() { + return this._playSpeed; + } + + setPlaySpeed(speed: number) { + this._playSpeed = speed; + } + + // 实现ITimeline接口所需的其他方法 + getPlayState(): 'playing' | 'paused' | 'stopped' { + if (this.paused) { + return 'paused'; + } + if (this.animateCount === 0) { + return 'stopped'; + } + return 'playing'; + } + + setStartTime(time: number) { + this._startTime = time; + } + + getStartTime() { + return this._startTime; + } + + getCurrentTime() { + return this._currentTime; + } + + setCurrentTime(time: number) { + this._currentTime = time; + } +} diff --git a/packages/vrender-animate/src/utils/easing-func.ts b/packages/vrender-animate/src/utils/easing-func.ts new file mode 100644 index 000000000..078c83b42 --- /dev/null +++ b/packages/vrender-animate/src/utils/easing-func.ts @@ -0,0 +1,11 @@ +import { CurveContext, CustomPath2D } from '@visactor/vrender-core'; + +export function generatorPathEasingFunc(path: string) { + const customPath = new CustomPath2D(); + customPath.setCtx(new CurveContext(customPath)); + customPath.fromString(path, 0, 0, 1, 1); + + return (x: number) => { + return customPath.getYAt(x); + }; +} diff --git a/packages/vrender-animate/src/utils/easing.ts b/packages/vrender-animate/src/utils/easing.ts new file mode 100644 index 000000000..f50472f1d --- /dev/null +++ b/packages/vrender-animate/src/utils/easing.ts @@ -0,0 +1,273 @@ +/** + * The MIT License (MIT) + + Copyright (c) 2014 gskinner.com, inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +// 参考TweenJS +// https://github.com/CreateJS/TweenJS/tree/master/src/tweenjs +import { pi2 } from '@visactor/vutils'; + +/** + * 代码迁移自createjs + * 部分缓动函数参考https://easings.net/ + */ +export class Easing { + private constructor() { + // do nothing + } + + static linear(t: number): number { + return t; + } + + static none() { + return this.linear; + } + + /** + * 获取缓动函数,amount指示这个缓动函数的插值方式 + * @param amount + * @returns + */ + static get(amount: number) { + if (amount < -1) { + amount = -1; + } else if (amount > 1) { + amount = 1; + } + + return function (t: number) { + if (amount === 0) { + return t; + } + if (amount < 0) { + return t * (t * -amount + 1 + amount); + } + return t * ((2 - t) * amount + (1 - amount)); + }; + } + + /* 语法糖 */ + static getPowIn(pow: number) { + return function (t: number) { + return Math.pow(t, pow); + }; + } + + static getPowOut(pow: number) { + return function (t: number) { + return 1 - Math.pow(1 - t, pow); + }; + } + + static getPowInOut(pow: number) { + return function (t: number) { + if ((t *= 2) < 1) { + return 0.5 * Math.pow(t, pow); + } + return 1 - 0.5 * Math.abs(Math.pow(2 - t, pow)); + }; + } + + // 插值函数 + static quadIn = Easing.getPowIn(2); + static quadOut = Easing.getPowOut(2); + + static quadInOut = Easing.getPowInOut(2); + static cubicIn = Easing.getPowIn(3); + static cubicOut = Easing.getPowOut(3); + static cubicInOut = Easing.getPowInOut(3); + static quartIn = Easing.getPowIn(4); + static quartOut = Easing.getPowOut(4); + static quartInOut = Easing.getPowInOut(4); + static quintIn = Easing.getPowIn(5); + static quintOut = Easing.getPowOut(5); + static quintInOut = Easing.getPowInOut(5); + + /* 语法糖 */ + static getBackIn(amount: number) { + return function (t: number) { + return t * t * ((amount + 1) * t - amount); + }; + } + static getBackOut(amount: number) { + return function (t: number) { + return --t * t * ((amount + 1) * t + amount) + 1; + }; + } + static getBackInOut(amount: number) { + amount *= 1.525; + return function (t: number) { + if ((t *= 2) < 1) { + return 0.5 * (t * t * ((amount + 1) * t - amount)); + } + return 0.5 * ((t -= 2) * t * ((amount + 1) * t + amount) + 2); + }; + } + + // 插值函数 + static backIn = Easing.getBackIn(1.7); + static backOut = Easing.getBackOut(1.7); + static backInOut = Easing.getBackInOut(1.7); + + static sineIn(t: number): number { + return 1 - Math.cos((t * Math.PI) / 2); + } + + static sineOut(t: number): number { + return Math.sin((t * Math.PI) / 2); + } + + static sineInOut(t: number): number { + return -(Math.cos(Math.PI * t) - 1) / 2; + } + + static expoIn(t: number): number { + return t === 0 ? 0 : Math.pow(2, 10 * t - 10); + } + + static expoOut(t: number): number { + return t === 1 ? 1 : 1 - Math.pow(2, -10 * t); + } + + static expoInOut(t: number): number { + return t === 0 ? 0 : t === 1 ? 1 : t < 0.5 ? Math.pow(2, 20 * t - 10) / 2 : (2 - Math.pow(2, -20 * t + 10)) / 2; + } + + // 插值函数 + static circIn(t: number) { + return -(Math.sqrt(1 - t * t) - 1); + } + + static circOut(t: number) { + return Math.sqrt(1 - --t * t); + } + + static circInOut(t: number) { + if ((t *= 2) < 1) { + return -0.5 * (Math.sqrt(1 - t * t) - 1); + } + return 0.5 * (Math.sqrt(1 - (t -= 2) * t) + 1); + } + static bounceOut(t: number) { + if (t < 1 / 2.75) { + return 7.5625 * t * t; + } else if (t < 2 / 2.75) { + return 7.5625 * (t -= 1.5 / 2.75) * t + 0.75; + } else if (t < 2.5 / 2.75) { + return 7.5625 * (t -= 2.25 / 2.75) * t + 0.9375; + } + return 7.5625 * (t -= 2.625 / 2.75) * t + 0.984375; + } + static bounceIn(t: number) { + return 1 - Easing.bounceOut(1 - t); + } + + static bounceInOut(t: number) { + if (t < 0.5) { + return Easing.bounceIn(t * 2) * 0.5; + } + return Easing.bounceOut(t * 2 - 1) * 0.5 + 0.5; + } + + /* 语法糖 */ + static getElasticIn(amplitude: number, period: number) { + return function (t: number) { + if (t === 0 || t === 1) { + return t; + } + const s = (period / pi2) * Math.asin(1 / amplitude); + return -(amplitude * Math.pow(2, 10 * (t -= 1)) * Math.sin(((t - s) * pi2) / period)); + }; + } + static getElasticOut(amplitude: number, period: number) { + return function (t: number) { + if (t === 0 || t === 1) { + return t; + } + const s = (period / pi2) * Math.asin(1 / amplitude); + return amplitude * Math.pow(2, -10 * t) * Math.sin(((t - s) * pi2) / period) + 1; + }; + } + static getElasticInOut(amplitude: number, period: number) { + return function (t: number) { + const s = (period / pi2) * Math.asin(1 / amplitude); + if ((t *= 2) < 1) { + return -0.5 * (amplitude * Math.pow(2, 10 * (t -= 1)) * Math.sin(((t - s) * pi2) / period)); + } + return amplitude * Math.pow(2, -10 * (t -= 1)) * Math.sin(((t - s) * pi2) / period) * 0.5 + 1; + }; + } + + // 插值函数 + static elasticIn = Easing.getElasticIn(1, 0.3); + static elasticOut = Easing.getElasticOut(1, 0.3); + static elasticInOut = Easing.getElasticInOut(1, 0.3 * 1.5); + + static easeInOutQuad = (t: number) => { + if ((t /= 0.5) < 1) { + return 0.5 * Math.pow(t, 2); + } + return -0.5 * ((t -= 2) * t - 2); + }; + + static easeOutElastic = (x: number) => { + const c4 = (2 * Math.PI) / 3; + + return x === 0 ? 0 : x === 1 ? 1 : Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1; + }; + + static easeInOutElastic = (x: number) => { + const c5 = (2 * Math.PI) / 4.5; + + return x === 0 + ? 0 + : x === 1 + ? 1 + : x < 0.5 + ? -(Math.pow(2, 20 * x - 10) * Math.sin((20 * x - 11.125) * c5)) / 2 + : (Math.pow(2, -20 * x + 10) * Math.sin((20 * x - 11.125) * c5)) / 2 + 1; + }; + static registerFunc(name: string, func: (t: number) => number) { + (Easing as any)[name] = func; + } +} + +function flicker(t: number, n: number) { + const step = 1 / n; + let flag = 1; + while (t > step) { + t -= step; + flag *= -1; + } + const v = (flag * t) / step; + return v > 0 ? v : 1 + v; +} + +// 注册flicker +for (let i = 0; i < 10; i++) { + (Easing as any)[`flicker${i}`] = (t: number) => flicker(t, i); +} + +for (let i = 2; i < 10; i++) { + (Easing as any)[`aIn${i}`] = (t: number) => i * t * t + (1 - i) * t; +} diff --git a/packages/vrender-core/src/animate/Ticker/default-ticker.ts b/packages/vrender-core/src/animate/Ticker/default-ticker.ts new file mode 100644 index 000000000..4bf64ece7 --- /dev/null +++ b/packages/vrender-core/src/animate/Ticker/default-ticker.ts @@ -0,0 +1,246 @@ +import { EventEmitter, Logger } from '@visactor/vutils'; +import type { ITickHandler, ITickerHandlerStatic, ITimeline, ITicker } from '../../interface'; +import { application } from '../../application'; +import type { TickerMode } from './type'; +import { STATUS } from './type'; +import { RAFTickHandler } from './raf-tick-handler'; +import { TimeOutTickHandler } from './timeout-tick-handler'; + +export class DefaultTicker extends EventEmitter implements ITicker { + protected interval: number; + protected tickerHandler: ITickHandler; + protected _mode: TickerMode; + protected status: STATUS; + protected lastFrameTime: number; + protected tickCounts: number; + protected timelines: ITimeline[]; + autoStop: boolean; + + set mode(m: TickerMode) { + if (this._mode === m) { + return; + } + this._mode = m; + this.setupTickHandler(); + } + get mode(): TickerMode { + return this._mode; + } + + constructor(timelines: ITimeline[] = []) { + super(); + this.init(); + this.lastFrameTime = -1; + this.tickCounts = 0; + this.timelines = timelines; + this.autoStop = true; + } + + init() { + this.interval = NaN; + this.status = STATUS.INITIAL; + application.global.hooks.onSetEnv.tap('default-ticker', () => { + this.initHandler(); + }); + if (application.global.env) { + this.initHandler(); + } + } + + addTimeline(timeline: ITimeline) { + this.timelines.push(timeline); + } + remTimeline(timeline: ITimeline) { + this.timelines = this.timelines.filter(t => t !== timeline); + } + getTimelines(): ITimeline[] { + return this.timelines; + } + + protected initHandler(): ITickHandler | null { + if (this._mode) { + return null; + } + const ticks: { mode: TickerMode; cons: ITickerHandlerStatic }[] = [ + { mode: 'raf', cons: RAFTickHandler }, + { mode: 'timeout', cons: TimeOutTickHandler } + ]; + for (let i = 0; i < ticks.length; i++) { + if (ticks[i].cons.Avaliable()) { + this.mode = ticks[i].mode; + break; + } + } + return null; + } + + /** + * 设置tickHandler + * @returns 返回true表示设置成功,false表示设置失败 + */ + protected setupTickHandler(): boolean { + let handler: ITickHandler; + // 创建下一个tickHandler + switch (this._mode) { + case 'raf': + handler = new RAFTickHandler(); + break; + case 'timeout': + handler = new TimeOutTickHandler(); + break; + // case 'manual': + // handler = new ManualTickHandler(); + // break; + default: + Logger.getInstance().warn('非法的计时器模式'); + handler = new RAFTickHandler(); + break; + } + if (!handler.avaliable()) { + return false; + } + + // 销毁上一个tickerHandler + if (this.tickerHandler) { + this.tickerHandler.release(); + } + this.tickerHandler = handler; + return true; + } + + setInterval(interval: number) { + this.interval = interval; + } + getInterval(): number { + return this.interval; + } + + setFPS(fps: number): void { + this.setInterval(1000 / fps); + } + getFPS(): number { + return 1000 / this.interval; + } + tick(interval: number): void { + this.tickerHandler.tick(interval, (handler: ITickHandler) => { + this.handleTick(handler, { once: true }); + }); + } + tickTo(t: number): void { + if (!this.tickerHandler.tickTo) { + return; + } + this.tickerHandler.tickTo(t, (handler: ITickHandler) => { + this.handleTick(handler, { once: true }); + }); + } + pause(): boolean { + if (this.status === STATUS.INITIAL) { + return false; + } + this.status = STATUS.PAUSE; + return true; + } + resume(): boolean { + if (this.status === STATUS.INITIAL) { + return false; + } + this.status = STATUS.RUNNING; + return true; + } + + ifCanStop(): boolean { + if (this.autoStop) { + if (!this.timelines.length) { + return true; + } + if (this.timelines.reduce((a, b) => a + b.animateCount, 0) === 0) { + return true; + } + } + return false; + } + + start(force: boolean = false): boolean { + if (this.status === STATUS.RUNNING) { + return false; + } + if (!this.tickerHandler) { + return false; + } + // 如果不需要start,那就不start + if (!force) { + // 暂停状态不执行 + if (this.status === STATUS.PAUSE) { + return false; + } + if (!this.timelines.length) { + return false; + } + if (this.timelines.reduce((a, b) => a + b.animateCount, 0) === 0) { + return false; + } + } + this.status = STATUS.RUNNING; + this.tickerHandler.tick(0, this.handleTick); + return true; + } + stop(): void { + // 重新设置tickHandler + this.status = STATUS.INITIAL; + this.setupTickHandler(); + this.lastFrameTime = -1; + } + + protected handleTick = (handler: ITickHandler, params?: { once?: boolean }) => { + const { once = false } = params ?? {}; + // 尝试停止 + if (this.ifCanStop()) { + this.stop(); + return; + } + this._handlerTick(); + if (!once) { + handler.tick(this.interval, this.handleTick); + } + }; + + protected _handlerTick = () => { + // 具体执行函数 + const tickerHandler = this.tickerHandler; + const time = tickerHandler.getTime(); + // 上一帧经过的时间 + let delta = 0; + if (this.lastFrameTime >= 0) { + delta = time - this.lastFrameTime; + } + this.lastFrameTime = time; + + if (this.status !== STATUS.RUNNING) { + return; + } + this.tickCounts++; + + this.timelines.forEach(t => { + t.tick(delta); + }); + this.emit('tick'); + }; + + release(): void { + this.stop(); + this.timelines = []; + this.tickerHandler.release(); + this.emit('afterTick'); + } + + /** + * 同步tick状态,需要手动触发tick执行,保证属性为走完动画的属性 + * 【注】grammar会设置属性到最终值,然后调用render,这时候需要VRender手动触发tick,保证属性为走完动画的属性,而不是Grammar设置上的属性 + */ + trySyncTickStatus() { + if (this.status === STATUS.RUNNING) { + this._handlerTick(); + } + } +} diff --git a/packages/vrender-core/src/animate/Ticker/index.ts b/packages/vrender-core/src/animate/Ticker/index.ts new file mode 100644 index 000000000..d90a0239e --- /dev/null +++ b/packages/vrender-core/src/animate/Ticker/index.ts @@ -0,0 +1,5 @@ +export * from './default-ticker'; +export * from './manual-ticker'; +export * from './manual-ticker-handler'; +export * from './raf-tick-handler'; +export * from './timeout-tick-handler'; diff --git a/packages/vrender-core/src/animate/Ticker/manual-ticker-handler.ts b/packages/vrender-core/src/animate/Ticker/manual-ticker-handler.ts new file mode 100644 index 000000000..d4b815c7b --- /dev/null +++ b/packages/vrender-core/src/animate/Ticker/manual-ticker-handler.ts @@ -0,0 +1,35 @@ +import type { ITickHandler } from '../../interface/animate'; + +export class ManualTickHandler implements ITickHandler { + protected timerId: number; + protected time: number = 0; + + static Avaliable(): boolean { + return true; + } + + avaliable(): boolean { + return ManualTickHandler.Avaliable(); + } + + tick(interval: number, cb: (handler: ITickHandler, params?: { once: boolean }) => void): void { + this.time = Math.max(0, interval + this.time); + cb(this, { once: true }); + } + + tickTo(t: number, cb: (handler: ITickHandler, params?: { once: boolean }) => void): void { + this.time = Math.max(0, t); + cb(this, { once: true }); + } + + release() { + if (this.timerId > 0) { + // clearTimeout(this.timerId); + this.timerId = -1; + } + } + + getTime() { + return this.time; + } +} diff --git a/packages/vrender-core/src/animate/Ticker/manual-ticker.ts b/packages/vrender-core/src/animate/Ticker/manual-ticker.ts new file mode 100644 index 000000000..1e7945524 --- /dev/null +++ b/packages/vrender-core/src/animate/Ticker/manual-ticker.ts @@ -0,0 +1,54 @@ +import type { ITicker, ITickHandler, ITimeline } from '../../interface/animate'; +import { DefaultTicker } from './default-ticker'; +import { ManualTickHandler } from './manual-ticker-handler'; +import type { STATUS, TickerMode } from './type'; + +export class ManualTicker extends DefaultTicker implements ITicker { + protected declare interval: number; + protected declare tickerHandler: ITickHandler; + protected declare _mode: TickerMode; + protected declare status: STATUS; + protected declare lastFrameTime: number; + protected declare tickCounts: number; + protected declare timelines: ITimeline[]; + declare autoStop: boolean; + + set mode(m: TickerMode) { + m = 'manual'; + this.setupTickHandler(); + } + get mode(): TickerMode { + return this._mode; + } + + protected initHandler(): ITickHandler | null { + this.mode = 'manual'; + return null; + } + + /** + * 设置tickHandler + * @returns 返回true表示设置成功,false表示设置失败 + */ + protected setupTickHandler(): boolean { + const handler: ITickHandler = new ManualTickHandler(); + this._mode = 'manual'; + + // 销毁上一个tickerHandler + if (this.tickerHandler) { + this.tickerHandler.release(); + } + this.tickerHandler = handler; + return true; + } + + tickAt(time: number) { + this.tickerHandler.tick(time - Math.max(this.lastFrameTime, 0), (handler: ITickHandler) => { + this.handleTick(handler, { once: true }); + }); + } + + ifCanStop(): boolean { + return false; + } +} diff --git a/packages/vrender-core/src/animate/Ticker/raf-tick-handler.ts b/packages/vrender-core/src/animate/Ticker/raf-tick-handler.ts new file mode 100644 index 000000000..2f39214b1 --- /dev/null +++ b/packages/vrender-core/src/animate/Ticker/raf-tick-handler.ts @@ -0,0 +1,30 @@ +import { application } from '../../application'; +import type { ITickHandler } from '../../interface/animate'; + +export class RAFTickHandler implements ITickHandler { + protected released: boolean; + + static Avaliable(): boolean { + return !!application.global.getRequestAnimationFrame(); + } + avaliable(): boolean { + return RAFTickHandler.Avaliable(); + } + + tick(interval: number, cb: (handler: ITickHandler) => void): void { + const raf = application.global.getRequestAnimationFrame(); + raf(() => { + if (this.released) { + return; + } + cb(this); + }); + } + + release() { + this.released = true; + } + getTime() { + return Date.now(); + } +} diff --git a/packages/vrender-core/src/animate/Ticker/timeout-tick-handler.ts b/packages/vrender-core/src/animate/Ticker/timeout-tick-handler.ts new file mode 100644 index 000000000..c6c13c183 --- /dev/null +++ b/packages/vrender-core/src/animate/Ticker/timeout-tick-handler.ts @@ -0,0 +1,29 @@ +import type { ITickHandler } from '../../interface/animate'; + +export class TimeOutTickHandler implements ITickHandler { + protected timerId: number; + + static Avaliable(): boolean { + return true; + } + + avaliable(): boolean { + return TimeOutTickHandler.Avaliable(); + } + + tick(interval: number, cb: (handler: ITickHandler) => void): void { + this.timerId = setTimeout(() => { + cb(this); + }, interval) as unknown as number; + } + + release() { + if (this.timerId > 0) { + clearTimeout(this.timerId); + this.timerId = -1; + } + } + getTime() { + return Date.now(); + } +} diff --git a/packages/vrender-core/src/animate/Ticker/type.ts b/packages/vrender-core/src/animate/Ticker/type.ts new file mode 100644 index 000000000..97afaea54 --- /dev/null +++ b/packages/vrender-core/src/animate/Ticker/type.ts @@ -0,0 +1,7 @@ +export type TickerMode = 'raf' | 'timeout' | 'manual'; + +export enum STATUS { + INITIAL = 0, // initial表示初始状态 + RUNNING = 1, // running表示正在执行 + PAUSE = 2 // PULSE表示tick还是继续,只是不执行函数了 +} diff --git a/packages/vrender-core/src/animate/animate.ts b/packages/vrender-core/src/animate/animate.ts new file mode 100644 index 000000000..8e32b97f4 --- /dev/null +++ b/packages/vrender-core/src/animate/animate.ts @@ -0,0 +1,1307 @@ +import type { + EasingType, + EasingTypeFunc, + IAnimate, + IAnimateStepType, + IAnimateTarget, + ICustomAnimate, + IGraphic, + IStep, + IStepConfig, + ISubAnimate, + ITimeline +} from '../interface'; +import { AnimateMode, AnimateStatus, AnimateStepType, AttributeUpdateType } from '../common/enums'; +import { Easing } from './easing'; +import { Logger, max } from '@visactor/vutils'; +import { defaultTimeline } from './timeline'; +import { Generator } from '../common/generator'; + +// 参考TweenJS +// https://github.com/CreateJS/TweenJS/tree/master/src/tweenjs +/** + * The MIT License (MIT) + + Copyright (c) 2014 gskinner.com, inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ +export abstract class ACustomAnimate implements ICustomAnimate { + declare from: T; + declare to: T; + declare duration: number; + declare easing: EasingType; + declare params: any; + declare target: IAnimateTarget; + declare updateCount: number; + declare subAnimate: ISubAnimate; + declare step?: IStep; + declare mode?: AnimateMode; + + // 用于判断是否一致 + declare _endProps?: any; + declare _mergedEndProps?: any; + + constructor(from: T, to: T, duration: number, easing: EasingType, params?: any) { + this.from = from; + this.to = to; + this.duration = duration; + this.easing = easing; + this.params = params; + this.updateCount = 0; + } + + bind(target: IAnimateTarget, subAni: ISubAnimate) { + this.target = target; + this.subAnimate = subAni; + this.onBind(); + } + + // 在第一次调用的时候触发 + onBind() { + return; + } + + // 第一次执行的时候调用 + onFirstRun() { + return; + } + + // 开始执行的时候调用(如果有循环,那每个周期都会调用) + onStart() { + return; + } + + // 结束执行的时候调用(如果有循环,那每个周期都会调用) + onEnd() { + return; + } + + getEndProps(): Record | void { + return this.to; + } + + getFromProps(): Record | void { + return this.from; + } + + getMergedEndProps(): Record | void { + const thisEndProps = this.getEndProps(); + if (thisEndProps) { + if (this._endProps === thisEndProps) { + return this._mergedEndProps; + } + this._endProps = thisEndProps; + this._mergedEndProps = Object.assign({}, this.step.prev.getLastProps() ?? {}, thisEndProps); + return; + } + return this.step.prev ? this.step.prev.getLastProps() : thisEndProps; + } + + // abstract getFromValue(key: string): any; + // abstract getToValue(key: string): any; + + abstract onUpdate(end: boolean, ratio: number, out: Record): void; + + update(end: boolean, ratio: number, out: Record): void { + if (this.updateCount === 0) { + this.onFirstRun(); + // out添加之前的props + const props = this.step.getLastProps(); + Object.keys(props).forEach(k => { + if (this.subAnimate.animate.validAttr(k)) { + out[k] = props[k]; + } + }); + } + this.updateCount += 1; + this.onUpdate(end, ratio, out); + if (end) { + this.onEnd(); + } + } +} + +export class CbAnimate extends ACustomAnimate { + cb: () => void; + + constructor(cb: () => void) { + super(null, null, 0, 'linear'); + this.cb = cb; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + return; + } + + onStart(): void { + this.cb(); + } +} + +type InterpolateFunc = ( + key: string, + ratio: number, + from: any, + to: any, + target: IAnimateTarget, + out: Record +) => boolean; + +export class Animate implements IAnimate { + static mode: AnimateMode = AnimateMode.NORMAL; + declare target: IAnimateTarget; + declare timeline: ITimeline; + declare nextAnimate?: IAnimate; + declare prevAnimate?: IAnimate; + // 当前Animate的状态,正常,暂停,结束 + declare status: AnimateStatus; + declare readonly id: string | number; + // 开始时间 + protected declare _startTime: number; + protected declare _duringTime: number; + declare subAnimates: SubAnimate[]; + declare tailAnimate: SubAnimate; + + // 绝对的位置 + declare rawPosition: number; + // 时间倍速缩放 + declare timeScale: number; + + declare interpolateFunc: (key: string, ratio: number, from: any, to: any, nextAttributes: any) => boolean; + + declare _onStart?: (() => void)[]; + declare _onFrame?: ((step: IStep, ratio: number) => void)[]; + declare _onEnd?: (() => void)[]; + declare _onRemove?: (() => void)[]; + declare _preventAttrs?: Set; + static interpolateMap: Map = new Map(); + slience?: boolean; + + constructor( + id: string | number = Generator.GenAutoIncrementId(), + timeline: ITimeline = defaultTimeline, + slience?: boolean + ) { + this.id = id; + this.timeline = timeline || defaultTimeline; + this.status = AnimateStatus.INITIAL; + this.tailAnimate = new SubAnimate(this); + this.subAnimates = [this.tailAnimate]; + this.timeScale = 1; + this.rawPosition = -1; + this._startTime = 0; + this._duringTime = 0; + this.timeline.addAnimate(this); + this.slience = slience; + } + + setTimeline(timeline: ITimeline) { + if (timeline === this.timeline) { + return; + } + timeline.addAnimate(this); + } + + getStartTime(): number { + return this._startTime; + } + + getDuration(): number { + return this.subAnimates.reduce((t, subAnimate) => t + subAnimate.totalDuration, 0); + } + + after(animate: IAnimate) { + const t = animate.getDuration(); + this._startTime = t; + return this; + } + + afterAll(list: IAnimate[]) { + let maxT = -Infinity; + list.forEach(a => { + maxT = max(a.getDuration(), maxT); + }); + this._startTime = maxT; + return this; + } + + parallel(animate: IAnimate) { + this._startTime = animate.getStartTime(); + return this; + } + + static AddInterpolate(name: string, cb: InterpolateFunc) { + Animate.interpolateMap.set(name, cb); + } + + play(customAnimate: ICustomAnimate) { + this.tailAnimate.play(customAnimate); + // todo: 考虑使用绑定的ticker执行 + if (this.target) { + const stage = (this.target as IGraphic).stage; + stage && stage.renderNextFrame(); + } + if (this.subAnimates.length === 1 && this.tailAnimate.totalDuration === customAnimate.duration) { + this.trySetAttribute(customAnimate.getFromProps(), customAnimate.mode); + } + return this; + } + + trySetAttribute(attr: Record | void, mode: AnimateMode = Animate.mode) { + if (attr && mode & AnimateMode.SET_ATTR_IMMEDIATELY) { + (this.target as any).setAttributes && + (this.target as any).setAttributes(attr, false, { type: AttributeUpdateType.ANIMATE_PLAY }); + } + } + + runCb(cb: (a: IAnimate, step: IStep) => void) { + // this.tailAnimate.runCb(cb); + const customAnimate = new CbAnimate(() => { + cb(this, customAnimate.step.prev); + }); + this.tailAnimate.play(customAnimate); + return this; + } + + /** + * 自定义插值,返回false表示没有匹配上 + * @param key + * @param from + * @param to + */ + customInterpolate( + key: string, + ratio: number, + from: any, + to: any, + target: IAnimateTarget, + ret: Record + ): boolean { + const func = Animate.interpolateMap.get(key) || Animate.interpolateMap.get(''); + if (!func) { + return false; + } + return func(key, ratio, from, to, target, ret); + } + + pause() { + if (this.status === AnimateStatus.RUNNING) { + this.status = AnimateStatus.PAUSED; + } + } + + resume() { + if (this.status === AnimateStatus.PAUSED) { + this.status = AnimateStatus.RUNNING; + } + } + + to(props: Record, duration: number, easing: EasingType, params?: IStepConfig) { + this.tailAnimate.to(props, duration, easing, params); + // 默认开始动画 + // todo: 考虑使用绑定的ticker执行 + if (this.target) { + const stage = (this.target as IGraphic).stage; + stage && stage.renderNextFrame(); + } + // if (this.subAnimates.length === 1 && this.tailAnimate.duration === duration) { + // this.trySetAttribute(props); + // } + return this; + } + from(props: Record, duration: number, easing: EasingType, params?: IStepConfig) { + this.tailAnimate.from(props, duration, easing, params); + // todo: 考虑使用绑定的ticker执行 + if (this.target) { + const stage = (this.target as IGraphic).stage; + stage && stage.renderNextFrame(); + } + return this; + } + wait(duration: number) { + this.tailAnimate.wait(duration); + // todo: 考虑使用绑定的ticker执行 + if (this.target) { + const stage = (this.target as IGraphic).stage; + stage && stage.renderNextFrame(); + } + return this; + } + startAt(t: number) { + this.tailAnimate.startAt(t); + // todo: 考虑使用绑定的ticker执行 + if (this.target) { + const stage = (this.target as IGraphic).stage; + stage && stage.renderNextFrame(); + } + return this; + } + + loop(l: number) { + this.tailAnimate.loop = l; + // todo: 考虑使用绑定的ticker执行 + if (this.target) { + const stage = (this.target as IGraphic).stage; + stage && stage.renderNextFrame(); + } + return this; + } + + reversed(r: boolean) { + this.tailAnimate.reversed = r; + // todo: 考虑使用绑定的ticker执行 + if (this.target) { + const stage = (this.target as IGraphic).stage; + stage && stage.renderNextFrame(); + } + return this; + } + + bounce(b: boolean) { + this.tailAnimate.bounce = b; + // todo: 考虑使用绑定的ticker执行 + if (this.target) { + const stage = (this.target as IGraphic).stage; + stage && stage.renderNextFrame(); + } + return this; + } + + subAnimate() { + const sa = new SubAnimate(this, this.tailAnimate); + this.tailAnimate = sa; + this.subAnimates.push(sa); + sa.bind(this.target); + return this; + } + + getStartProps(): Record { + return this.subAnimates[0].getStartProps(); + } + + getEndProps(): Record { + return this.tailAnimate.getEndProps(); + } + + depreventAttr(key: string) { + if (!this._preventAttrs) { + return; + } + this._preventAttrs.delete(key); + } + preventAttr(key: string) { + if (!this._preventAttrs) { + this._preventAttrs = new Set(); + } + this._preventAttrs.add(key); + } + preventAttrs(keys: string[]) { + keys.forEach(key => this.preventAttr(key)); + } + validAttr(key: string): boolean { + if (!this._preventAttrs) { + return true; + } + return !this._preventAttrs.has(key); + } + + bind(target: IAnimateTarget) { + this.target = target; + + if (this.target.onAnimateBind && !this.slience) { + this.target.onAnimateBind(this); + } + + this.subAnimates.forEach(sa => { + sa.bind(target); + }); + return this; + } + + advance(delta: number) { + // startTime之前的时间不计入耗时 + if (this._duringTime < this._startTime) { + if (this._duringTime + delta * this.timeScale < this._startTime) { + this._duringTime += delta * this.timeScale; + return; + } + delta = this._duringTime + delta * this.timeScale - this._startTime; + this._duringTime = this._startTime; + } + // 执行advance + if (this.status === AnimateStatus.INITIAL) { + this.status = AnimateStatus.RUNNING; + this._onStart && this._onStart.forEach(cb => cb()); + } + const end = this.setPosition(Math.max(this.rawPosition, 0) + delta * this.timeScale); + if (end && this.status === AnimateStatus.RUNNING) { + this.status = AnimateStatus.END; + this._onEnd && this._onEnd.forEach(cb => cb()); + } + } + + setPosition(rawPosition: number): boolean { + let d = 0; + let sa: SubAnimate | undefined; + const prevRawPos = this.rawPosition; + const maxRawPos = this.subAnimates.reduce((a, b) => a + b.totalDuration, 0); + + if (rawPosition < 0) { + rawPosition = 0; + } + + const end = rawPosition >= maxRawPos; + + if (end) { + rawPosition = maxRawPos; + } + + if (rawPosition === prevRawPos) { + return end; + } + + // 查找对应的subAnimate + for (let i = 0; i < this.subAnimates.length; i++) { + sa = this.subAnimates[i]; + if (d + sa.totalDuration >= rawPosition) { + break; + } else { + d += sa.totalDuration; + sa = undefined; + } + } + this.rawPosition = rawPosition; + sa.setPosition(rawPosition - d); + + return end; + } + + onStart(cb: () => void) { + if (!this._onStart) { + this._onStart = []; + } + this._onStart.push(cb); + } + onEnd(cb: () => void) { + if (!this._onEnd) { + this._onEnd = []; + } + this._onEnd.push(cb); + } + onRemove(cb: () => void) { + if (!this._onRemove) { + this._onRemove = []; + } + this._onRemove.push(cb); + } + onFrame(cb: (step: IStep, ratio: number) => void) { + if (!this._onFrame) { + this._onFrame = []; + } + this._onFrame.push(cb); + } + release() { + this.status = AnimateStatus.END; + return; + } + + stop(nextVal?: 'start' | 'end' | Record) { + if (!nextVal) { + this.target.onStop(); + } + if (nextVal === 'start') { + this.target.onStop(this.getStartProps()); + } else if (nextVal === 'end') { + this.target.onStop(this.getEndProps()); + } else { + this.target.onStop(nextVal); + } + this.release(); + } +} + +// Animate.mode |= AnimateMode.SET_ATTR_IMMEDIATELY; + +export class SubAnimate implements ISubAnimate { + declare target: IAnimateTarget; + declare animate: IAnimate; + // 默认的初始step,一定存在,且stepHead的props一定保存整个subAnimate阶段所有属性的最初 + protected declare stepHead: Step; + protected declare stepTail: Step; + // 结束时反转动画 + declare bounce: boolean; + // 是否reverse + declare reversed: boolean; + // 循环次数,0为执行一次,1为执行两次,Infinity为无限循环 + declare loop: number; + // 持续时间,不包括循环 + declare duration: number; + // 位置,在[0, duration]之间 + declare position: number; + // 绝对的位置,在[0, loops * duration]之间 + declare rawPosition: number; + declare dirty: boolean; + + declare _totalDuration: number; + declare _startAt: number; + declare _lastStep: IStep; + declare _deltaPosition: number; + + get totalDuration(): number { + this.calcAttr(); + return this._totalDuration + this._startAt; + } + + constructor(animate: IAnimate, lastSubAnimate?: SubAnimate) { + this.rawPosition = -1; + this.position = 0; + this.loop = 0; + this.duration = 0; + this.animate = animate; + if (lastSubAnimate) { + this.stepHead = new Step(0, 0, Object.assign({}, lastSubAnimate.stepTail.props)); + } else { + this.stepHead = new Step(0, 0, {}); + } + this.stepTail = this.stepHead; + this.dirty = true; + this._startAt = 0; + } + + // 计算按需计算的属性 + protected calcAttr() { + if (!this.dirty) { + return; + } + + this._totalDuration = this.duration * (this.loop + 1); + } + + bind(target: IAnimateTarget) { + this.target = target; + return this; + } + + play(customAnimate: ICustomAnimate) { + let duration = customAnimate.duration; + if (duration == null || duration < 0) { + duration = 0; + } + const easing = customAnimate.easing; + const easingFunc = typeof easing === 'string' ? Easing[easing] : easing; + const step = this._addStep(duration, null, easingFunc); + step.type = AnimateStepType.customAnimate; + this._appendProps(customAnimate.getEndProps(), step, false); + this._appendCustomAnimate(customAnimate, step); + return this; + } + + // _appendPlayProps(step: IStep) { + + // return; + // } + + to(props: Record, duration: number, easing: EasingType, params?: IStepConfig) { + if (duration == null || duration < 0) { + duration = 0; + } + + const easingFunc = typeof easing === 'string' ? Easing[easing] : easing; + + const step = this._addStep(duration, null, easingFunc); + step.type = AnimateStepType.to; + this._appendProps(props, step, params ? params.tempProps : false); + // this._appendPlayProps(step); + + if (!step.propKeys) { + step.propKeys = Object.keys(step.props); + } + if (!(params && params.noPreventAttrs)) { + this.target.animates && + this.target.animates.forEach(a => { + if (a.id !== this.animate.id) { + a.preventAttrs(step.propKeys); + } + }); + } + return this; + } + + from(props: Record, duration: number, easing: EasingType, params?: IStepConfig) { + this.to(props, 0, easing, params); + const toProps = {}; + if (!this.stepTail.propKeys) { + this.stepTail.propKeys = Object.keys(this.stepTail.props); + } + this.stepTail.propKeys.forEach(k => { + toProps[k] = this.getLastPropByName(k, this.stepTail); + }); + this.to(toProps, duration, easing, params); + this.stepTail.type = AnimateStepType.from; + } + + startAt(t: number) { + if (t < 0) { + t = 0; + } + this._startAt = t; + return this; + } + + getStartProps() { + return this.stepHead?.props; + } + + getEndProps() { + return this.stepTail.props; + } + + getLastStep() { + return this._lastStep; + } + + wait(duration: number) { + if (duration > 0) { + const step = this._addStep(+duration, null); + + step.type = AnimateStepType.wait; + // TODO 这里如果跳帧的话会存在bug + if (step.prev.customAnimate) { + step.props = step.prev.customAnimate.getEndProps(); + } else { + step.props = step.prev.props; + } + if (this.target.onAddStep) { + this.target.onAddStep(step); + } + // this._appendPlayProps(step); + } + return this; + } + + protected _addStep(duration: number, props: any, easingFunc?: EasingTypeFunc) { + const step = new Step(this.duration, duration, props, easingFunc); + this.duration += duration; + this.stepTail.append(step); + this.stepTail = step; + return step; + } + + protected _appendProps(props: any, step: Step, tempProps?: boolean) { + if (tempProps) { + step.props = props; + } else { + // todo: 是否需要深拷贝props + step.props = Object.assign({}, props); + } + let lastStep = step.prev; + const _props = step.props; + // 将undefined的属性设置到默认值 + if (!step.propKeys) { + step.propKeys = Object.keys(step.props); + } + step.propKeys.forEach(k => { + if (step.props[k] === undefined) { + step.props[k] = this.target.getDefaultAttribute(k); + } + }); + // 拷贝之前的step阶段属性 + while (lastStep.prev) { + if (lastStep.props) { + if (!lastStep.propKeys) { + lastStep.propKeys = Object.keys(lastStep.props); + } + lastStep.propKeys.forEach(key => { + if (_props[key] === undefined) { + _props[key] = lastStep.props[key]; + } + }); + } + // 重置propKeys + step.propKeys = Object.keys(step.props); + lastStep = lastStep.prev; + } + + // 设置最初的props属性 + const initProps = this.stepHead.props; + if (!step.propKeys) { + step.propKeys = Object.keys(_props); + } + step.propKeys.forEach(key => { + if (initProps[key] === undefined) { + const parentAnimateInitProps = this.animate.getStartProps(); + initProps[key] = parentAnimateInitProps[key] = this.target.getComputedAttribute(key); + } + }); + + if (this.target.onAddStep) { + this.target.onAddStep(step); + } + } + + protected _appendCustomAnimate(customAnimate: ICustomAnimate, step: Step) { + step.customAnimate = customAnimate; + customAnimate.step = step; + customAnimate.bind(this.target, this); + } + + setPosition(rawPosition: number) { + const d = this.duration; + const loopCount = this.loop; + const prevRawPos = this.rawPosition; + let end = false; + let loop: number; // 当前是第几次循环 + let position: number; // 当前周期的时间 + const startAt = this._startAt ?? 0; + + if (rawPosition < 0) { + rawPosition = 0; + } + if (rawPosition < startAt) { + this.rawPosition = rawPosition; + return false; + } + rawPosition = rawPosition - startAt; + if (d <= 0) { + // 如果不用执行,跳过 + end = true; + // 小于0的话,直接return,如果等于0,那还是得走动画逻辑,将end属性设置上去 + if (d < 0) { + return end; + } + } + loop = Math.floor(rawPosition / d); + position = rawPosition - loop * d; + + // 计算rawPosition + end = rawPosition >= loopCount * d + d; + // 如果结束,跳过 + if (end) { + position = d; + loop = loopCount; + rawPosition = position * loop + d; + } + + if (rawPosition === prevRawPos) { + return end; + } + + // reverse动画 + const rev = !this.reversed !== !(this.bounce && loop % 2); + if (rev) { + position = d - position; + } + + this._deltaPosition = position - this.position; + this.position = position; + this.rawPosition = rawPosition + startAt; + + this.updatePosition(end, rev); + + return end; + } + + protected updatePosition(end: boolean, rev: boolean) { + if (!this.stepHead) { + return; + } + let step = this.stepHead.next; + const position = this.position; + const duration = this.duration; + if (this.target && step) { + let stepNext = step.next; + while (stepNext && stepNext.position <= position) { + step = stepNext; + stepNext = step.next; + } + let ratio = end ? (duration === 0 ? 1 : position / duration) : (position - step.position) / step.duration; // TODO: revisit this. + if (step.easing) { + ratio = step.easing(ratio); + } + // 判断这次和上次过程中是否经历了自定义step,如果跳过了自定义step那么执行自定义step的onEnd + this.tryCallCustomAnimateLifeCycle(step, this._lastStep || (rev ? this.stepTail : this.stepHead), rev); + // if (step !== this._lastStep) { + // if (this._deltaPosition > 0) { + // let _step = step.prev; + // while (_step && _step !== this._lastStep) { + // if (_step.customAnimate) { + // _step.customAnimate.onEnd(); + // } + // _step = _step.prev; + // } + // if (_step && _step.customAnimate) { + // _step.customAnimate.onEnd(); + // } + // } else if (this._deltaPosition < 0) { + // let _step = step.next; + // while (_step && _step !== this._lastStep) { + // if (_step.customAnimate) { + // _step.customAnimate.onEnd(); + // } + // _step = _step.next; + // } + // if (_step && _step.customAnimate) { + // _step.customAnimate.onEnd(); + // } + // } + // } + this.updateTarget(step, ratio, end); + this._lastStep = step; + + this.animate._onFrame && this.animate._onFrame.forEach(cb => cb(step, ratio)); + } + } + + // 如果动画卡顿跳过了自定义动画,那么尝试执行自定义动画的生命周期 + tryCallCustomAnimateLifeCycle(step: IStep, lastStep: IStep, rev: boolean) { + if (step === lastStep) { + return; + } + if (rev) { + let _step = lastStep.prev; + while (_step && _step !== step) { + if (_step.customAnimate) { + _step.customAnimate.onStart && _step.customAnimate.onStart(); + _step.customAnimate.onEnd && _step.customAnimate.onEnd(); + } + _step = step.prev; + } + // 执行lastStep的onEnd和currentStep的onStart + if (lastStep && lastStep.customAnimate) { + lastStep.customAnimate.onEnd && lastStep.customAnimate.onEnd(); + } + if (step && step.customAnimate) { + step.customAnimate.onStart && step.customAnimate.onStart(); + } + } else { + let _step = lastStep.next; + while (_step && _step !== step) { + if (_step.customAnimate) { + _step.customAnimate.onStart && _step.customAnimate.onStart(); + _step.customAnimate.onEnd && _step.customAnimate.onEnd(); + } + _step = _step.next; + } + // 执行lastStep的onEnd和currentStep的onStart + if (lastStep && lastStep.customAnimate) { + lastStep.customAnimate.onEnd && lastStep.customAnimate.onEnd(); + } + if (step && step.customAnimate) { + step.customAnimate.onStart && step.customAnimate.onStart(); + } + } + } + + /** + * 获取这个属性的上一个值 + * @param name + * @param step + * @returns + */ + getLastPropByName(name: string, step: Step): any { + let lastStep = step.prev; + while (lastStep) { + if (lastStep.props && lastStep.props[name] !== undefined) { + return lastStep.props[name]; + } else if (lastStep.customAnimate) { + const val = lastStep.customAnimate.getEndProps()[name]; + if (val !== undefined) { + return val; + } + } + lastStep = lastStep.prev; + } + + Logger.getInstance().warn('未知错误,step中找不到属性'); + return step.props[name]; + } + + protected updateTarget(step: Step, ratio: number, end: boolean) { + if (step.props == null && step.customAnimate == null) { + return; + } + this.target.onStep(this, this.animate, step, ratio, end); + } +} + +// export class Animate implements IAnimate { +// declare target: IAnimateTarget; +// declare timeline: ITimeline; +// protected declare stepHead: Step; +// protected declare stepTail: Step; +// declare nextAnimate?: Animate; +// declare prevAnimate?: Animate; +// // 结束时反转动画 +// declare bounce: boolean; +// // 是否reverse +// declare reversed: boolean; +// // 循环次数,0为执行一次,1为执行两次,Infinity为无限循环 +// declare loop: number; +// // 持续时间,不包括循环 +// declare duration: number; +// // 当前Animate的状态,正常,暂停,结束 +// declare status: AnimateStatus; +// // 位置,在[0, duration]之间 +// declare position: number; +// // 绝对的位置,在[0, loops * duration]之间 +// declare rawPosition: number; +// // 开始时间 +// protected declare _startAt: number; +// // 时间的缩放,例如2表示2倍速 +// declare timeScale: number; +// declare props: Record; +// declare readonly id: string | number; + +// protected declare _onStart?: (() => void)[]; +// protected declare _onFrame?: ((step: IStep, ratio: number) => void)[]; +// protected declare _onEnd?: (() => void)[]; +// declare _onRemove?: (() => void)[]; +// declare _preventAttrs?: Set; + +// constructor(id: string | number = Generator.GenAutoIncrementId(), timeline: ITimeline = defaultTimeline) { +// this.timeline = timeline; +// this.status = AnimateStatus.INITIAL; +// this.rawPosition = -1; +// this.position = 0; +// this.loop = 0; +// this.timeline.addAnimate(this); +// this.timeScale = 1; +// this.id = id; +// this.props = {}; +// this.stepHead = new Step(0, 0, {}); +// this.stepTail = this.stepHead; +// } + +// preventAttr(key: string) { +// if (!this._preventAttrs) { +// this._preventAttrs = new Set(); +// } +// this._preventAttrs.add(key); +// } +// preventAttrs(keys: string[]) { +// keys.forEach(key => this.preventAttr(key)); +// } +// validAttr(key: string): boolean { +// if (!this._preventAttrs) { +// return true; +// } +// return !this._preventAttrs.has(key); +// } + +// getLastPropByName(name: string, step: Step): any { +// let lastStep = step.prev; +// while (lastStep) { +// if (lastStep.props && lastStep.props[name] !== undefined) { +// return lastStep.props[name]; +// } +// lastStep = lastStep.prev; +// } +// let val = this.props[name]; +// if (!val) { +// console.warn('未知错误,step中找不到属性'); +// val = this.target.getComputedAttribute(name); +// this.props[name] = val; +// } + +// return val; +// } + +// bind(target: IAnimateTarget) { +// this.target = target; +// this.duration = 0; +// return this; +// } + +// startAt(t: number) { +// if (t < 0) { +// return this; +// } +// this._startAt = t; +// return this; +// } + +// to(props: Record, duration: number, easing: EasingType, params?: IStepConfig) { +// if (duration == null || duration < 0) { +// duration = 0; +// } + +// const easingFunc = typeof easing === 'string' ? Easing[easing] : easing; + +// const step = this._addStep(duration, null, easingFunc); +// this._appendProps(props, step, params ? params.tempProps : false); +// return this; +// } + +// wait(duration: number) { +// if (duration > 0) { +// const step = this._addStep(+duration, null); +// if (step.prev) { +// step.props = step.prev.props; +// } +// if (this.target.onAddStep) { +// this.target.onAddStep(step); +// } +// } +// return this; +// } + +// protected _addStep(duration: number, props: any, easingFunc?: EasingTypeFunc) { +// const step = new Step(this.duration, duration, props, easingFunc); +// this.duration += duration; +// this.stepTail.append(step); +// this.stepTail = step; +// return step; +// } + +// protected _appendProps(props: any, step: Step, tempProps?: boolean) { +// if (tempProps) { +// step.props = props; +// } else { +// // todo: 是否需要深拷贝props +// step.props = Object.assign({}, props); +// } +// let lastStep = step.prev; +// const _props = step.props; +// // 拷贝之前的step阶段属性 +// while (lastStep.prev) { +// if (lastStep.props) { +// if (!lastStep.propKeys) { +// lastStep.propKeys = Object.keys(lastStep.props); +// } +// lastStep.propKeys.forEach(key => { +// if (_props[key] === undefined) { +// _props[key] = lastStep.props[key]; +// } +// }); +// } +// lastStep = lastStep.prev; +// } + +// // 设置最初的props属性 +// const initProps = this.stepHead.props; +// if (!step.propKeys) { +// step.propKeys = Object.keys(_props); +// step.propKeys.forEach(key => { +// initProps[key] = this.target.getComputedAttribute(key); +// }); +// } + +// if (this.target.onAddStep) { +// this.target.onAddStep(step); +// } +// } + +// advance(delta: number) { +// if (this.status === AnimateStatus.INITIAL) { +// this.status = AnimateStatus.RUNNING; +// this._onStart && this._onStart.forEach(cb => cb()); +// } +// const end = this.setPosition(this.rawPosition + delta * this.timeScale); +// if (end && this.status === AnimateStatus.RUNNING) { +// this.status = AnimateStatus.END; +// this._onEnd && this._onEnd.forEach(cb => cb()); +// } +// } + +// setPosition(rawPosition: number) { +// const d = this.duration; +// const loopCount = this.loop; +// const prevRawPos = this.rawPosition; +// let end = false; +// let loop: number; // 当前是第几次循环 +// let position: number; // 当前周期的时间 +// const startAt = this._startAt ?? 0; + +// if (rawPosition < 0) { +// rawPosition = 0; +// } +// if (rawPosition < startAt) { +// this.rawPosition = rawPosition; +// return false; +// } +// rawPosition = rawPosition - startAt; +// if (d <= 0) { +// // 如果不用执行,跳过 +// end = true; +// return end; +// } +// loop = Math.floor(rawPosition / d); +// position = rawPosition - loop * d; + +// // 计算rawPosition +// end = rawPosition >= loopCount * d + d; +// // 如果结束,跳过 +// if (end) { +// position = d; +// loop = loopCount; +// rawPosition = position * loop + d; +// } + +// if (rawPosition === prevRawPos) { +// return end; +// } + +// // reverse动画 +// const rev = !this.reversed !== !(this.bounce && loop % 2); +// if (rev) { +// position = d - position; +// } + +// this.position = position; +// this.rawPosition = rawPosition + startAt; + +// this.updatePosition(end); + +// return end; +// } + +// protected updatePosition(end: boolean) { +// if (!this.stepHead) { +// return; +// } +// let step = this.stepHead; +// const position = this.position; +// const duration = this.duration; +// if (this.target && step) { +// let stepNext = step.next; +// while (stepNext && stepNext.position <= position) { +// step = step.next; +// stepNext = step.next; +// } +// let ratio = end ? (duration === 0 ? 1 : position / duration) : (position - step.position) / step.duration; // TODO: revisit this. +// if (step.easing) { +// ratio = step.easing(ratio); +// } +// this.updateTarget(step, ratio, end); +// this._onFrame && this._onFrame.forEach(cb => cb(step, ratio)); +// } +// } + +// protected updateTarget(step: Step, ratio: number, end: boolean) { +// if (step.props == null) { +// return; +// } +// this.target.onStep(this, step, ratio, end); +// } + +// onStart(cb: () => void) { +// if (!this._onStart) { +// this._onStart = []; +// } +// this._onStart.push(cb); +// } +// onEnd(cb: () => void) { +// if (!this._onEnd) { +// this._onEnd = []; +// } +// this._onEnd.push(cb); +// } +// onRemove(cb: () => void) { +// if (!this._onRemove) { +// this._onRemove = []; +// } +// this._onRemove.push(cb); +// } +// onFrame(cb: (step: IStep, ratio: number) => void) { +// if (!this._onFrame) { +// this._onFrame = []; +// } +// this._onFrame.push(cb); +// } + +// getStartProps() { +// return this.stepHead?.props; +// } + +// getEndProps(target: Record = {}) { +// let step = this.stepHead; +// while (step) { +// if (step.props) { +// Object.assign(target, step.props); +// } +// step = step.next; +// } + +// return target; +// } + +// stop(nextVal?: 'start' | 'end' | Record) { +// this.status = AnimateStatus.END; +// if (!nextVal) { +// this.target.onStop(); +// } +// if (nextVal === 'start') { +// this.target.onStop(this.getStartProps()); +// } else if (nextVal === 'end') { +// this.target.onStop(this.getEndProps()); +// } else { +// this.target.onStop(nextVal); +// } +// } + +// release() { +// this.status = AnimateStatus.END; +// return; +// } +// } + +class Step implements IStep { + declare prev?: Step; + // 持续时间 + declare duration: number; + // 在animate中的位置 + declare position: number; + declare next?: Step; + declare props: any; + // 保存解析后的props,用于性能优化 + declare parsedProps?: any; + declare propKeys?: string[]; + declare easing?: EasingTypeFunc; + declare customAnimate?: ICustomAnimate; + // passive: boolean; + // index: number; + type: IAnimateStepType; + + constructor(position: number, duration: number, props?: any, easing?: EasingTypeFunc) { + this.duration = duration; + this.position = position; + this.props = props; + this.easing = easing; + } + + append(step: Step) { + step.prev = this; + step.next = this.next; + this.next = step; + } + + getLastProps() { + let step = this.prev; + while (step) { + if (step.props) { + return step.props; + } else if (step.customAnimate) { + return step.customAnimate.getMergedEndProps(); + } + step = step.prev as any; + } + return null; + } +} diff --git a/packages/vrender-core/src/animate/config.ts b/packages/vrender-core/src/animate/config.ts new file mode 100644 index 000000000..4cd0b74c3 --- /dev/null +++ b/packages/vrender-core/src/animate/config.ts @@ -0,0 +1,11 @@ +import type { IAnimateConfig } from './../interface/graphic'; + +export const DefaultStateAnimateConfig: IAnimateConfig = { + duration: 200, + easing: 'cubicOut' +}; + +export const DefaultMorphingAnimateConfig: IAnimateConfig = { + duration: 1000, + easing: 'quadInOut' +}; diff --git a/packages/vrender-core/src/animate/custom-animate.ts b/packages/vrender-core/src/animate/custom-animate.ts new file mode 100644 index 000000000..5c0c04f99 --- /dev/null +++ b/packages/vrender-core/src/animate/custom-animate.ts @@ -0,0 +1,1364 @@ +import type { IPoint, IPointLike } from '@visactor/vutils'; +import { + clamp, + getDecimalPlaces, + isArray, + isNumber, + isValidNumber, + pi, + pi2, + Point, + PointService +} from '@visactor/vutils'; +import { application } from '../application'; +import { AttributeUpdateType } from '../common/enums'; +import { CustomPath2D } from '../common/custom-path2d'; +import type { + EasingType, + IArcGraphicAttribute, + IArea, + IAreaCacheItem, + ICubicBezierCurve, + ICurve, + ICustomPath2D, + IGraphic, + IGroup, + ILine, + ILineAttribute, + ILinearGradient, + IRect, + IRectAttribute, + IRectGraphicAttribute, + ISegment, + IShadowRoot +} from '../interface'; +import { ACustomAnimate } from './animate'; +import { Easing } from './easing'; +import { pointInterpolation } from '../common/utils'; +import { divideCubic } from '../common/segment/curve/cubic-bezier'; + +export class IncreaseCount extends ACustomAnimate<{ text: string | number }> { + declare valid: boolean; + + private fromNumber: number; + private toNumber: number; + private decimalLength: number; + + constructor( + from: { text: string | number }, + to: { text: string | number }, + duration: number, + easing: EasingType, + params?: { fixed?: boolean } + ) { + super(from, to, duration, easing, params); + } + + getEndProps(): Record | void { + if (this.valid === false) { + return {}; + } + return { + text: this.to + }; + } + + onBind(): void { + this.fromNumber = isNumber(this.from?.text) ? this.from?.text : Number.parseFloat(this.from?.text); + this.toNumber = isNumber(this.to?.text) ? this.to?.text : Number.parseFloat(this.to?.text); + if (!Number.isFinite(this.toNumber)) { + this.fromNumber = 0; + } + if (!Number.isFinite(this.toNumber)) { + this.valid = false; + } + if (this.valid !== false) { + this.decimalLength = + this.params?.fixed ?? Math.max(getDecimalPlaces(this.fromNumber), getDecimalPlaces(this.toNumber)); + } + } + + onEnd(): void { + return; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + if (this.valid === false) { + return; + } + if (end) { + out.text = this.to?.text; + } else { + out.text = (this.fromNumber + (this.toNumber - this.fromNumber) * ratio).toFixed(this.decimalLength); + } + } +} + +enum Direction { + LEFT_TO_RIGHT = 0, + RIGHT_TO_LEFT = 1, + TOP_TO_BOTTOM = 2, + BOTTOM_TO_TOP = 3, + STROKE = 4 +} +export class FadeInPlus extends ACustomAnimate { + declare direction: number; + declare toFill: string; + declare toStroke: string; + declare fillGradient: ILinearGradient; + declare strokeGradient: ILinearGradient; + declare fill: boolean; + declare stroke: boolean; + constructor( + from: any, + to: any, + duration: number, + easing: EasingType, + params?: { direction?: number; fill?: boolean; stroke?: boolean } + ) { + super(from, to, duration, easing, params); + const { direction = Direction.LEFT_TO_RIGHT, fill = true, stroke = true } = params || {}; + this.direction = direction; + this.fill = fill; + this.stroke = stroke; + this.fillGradient = { + gradient: 'linear', + stops: [] + }; + this.strokeGradient = { + gradient: 'linear', + stops: [] + }; + } + + getEndProps(): Record { + return { + fill: this.toFill, + stroke: this.toStroke + }; + } + + onBind(): void { + // this.to = parseFloat(this.target.getAnimatePropByName('text')); + this.toFill = this.target.getComputedAttribute('fill'); + this.toStroke = this.target.getComputedAttribute('stroke'); + } + + onEnd(): void { + return; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + if (!this.toFill) { + return; + } + if (!this.toStroke) { + return; + } + switch (this.direction) { + case Direction.RIGHT_TO_LEFT: + this.rightToLeft(end, ratio, out); + break; + case Direction.TOP_TO_BOTTOM: + this.topToBottom(end, ratio, out); + break; + case Direction.BOTTOM_TO_TOP: + this.bottomToTop(end, ratio, out); + break; + case Direction.STROKE: + this.strokePath(end, ratio, out); + break; + default: + this.leftToRight(end, ratio, out); + break; + } + } + + leftToRight(end: boolean, ratio: number, out: Record) { + if (this.fill) { + const toFillColor = this.toFill; + this.fillGradient.x0 = 0; + this.fillGradient.y0 = 0; + this.fillGradient.x1 = 1; + this.fillGradient.y1 = 0; + this.fillGradient.stops = [ + { offset: 0, color: toFillColor }, + { offset: ratio, color: toFillColor }, + { offset: Math.min(1, ratio * 2), color: 'transparent' } + ]; + out.fill = this.fillGradient; + } + if (this.stroke) { + const toStrokeColor = this.toStroke; + this.strokeGradient.x0 = 0; + this.strokeGradient.y0 = 0; + this.strokeGradient.x1 = 1; + this.strokeGradient.y1 = 0; + this.strokeGradient.stops = [ + { offset: 0, color: toStrokeColor }, + { offset: ratio, color: toStrokeColor }, + { offset: Math.min(1, ratio * 6), color: 'transparent' } + ]; + out.stroke = this.strokeGradient; + // const dashLen = 300; + // const offset = ratio * dashLen; + // out.lineDash = [offset, dashLen - offset]; + } + return; + } + + strokePath(end: boolean, ratio: number, out: Record) { + if (this.fill) { + const toFillColor = this.toFill; + this.fillGradient.x0 = 0; + this.fillGradient.y0 = 0; + this.fillGradient.x1 = 1; + this.fillGradient.y1 = 0; + this.fillGradient.stops = [ + { offset: 0, color: toFillColor }, + { offset: ratio, color: toFillColor }, + { offset: Math.min(1, ratio * 2), color: 'transparent' } + ]; + out.fill = this.fillGradient; + } + if (this.stroke) { + const dashLen = 300; + const offset = ratio * dashLen; + out.lineDash = [offset, dashLen - offset]; + } + return; + } + rightToLeft(end: boolean, ratio: number, out: Record) { + return; + } + topToBottom(end: boolean, ratio: number, out: Record) { + return; + } + bottomToTop(end: boolean, ratio: number, out: Record) { + return; + } +} + +export class InputText extends ACustomAnimate<{ text: string }> { + declare valid: boolean; + declare target: IGraphic; + + private fromText: string = ''; + private toText: string | string[] = ''; + + getEndProps(): Record { + if (this.valid === false) { + return {}; + } + return { + text: this.to + }; + } + + onBind(): void { + this.fromText = this.from?.text ?? ''; + this.toText = this.to?.text || ''; + if (!this.toText || (isArray(this.toText) && this.toText.length === 0)) { + this.valid = false; + } + if (isArray(this.toText)) { + this.toText = this.toText.map(item => (item || '').toString()); + } + // else { + // this.toText = this.toText.toString(); + // // const root = this.target.attachShadow(); + // // const line = application.graphicService.creator.line({ + // // x: 0, + // // y: 0, + // // points: [ + // // { x: 0, y: 0 }, + // // { x: 0, y: this.target.getComputedAttribute('fontSize') } + // // ], + // // stroke: 'black', + // // lineWidth: 1 + // // }); + // // root.add(line); + // } + } + + onEnd(): void { + this.target.detachShadow(); + return; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + if (this.valid === false) { + return; + } + // update text + const fromCount = this.fromText.length; + const toTextIsArray = isArray(this.toText); + const toCount = toTextIsArray + ? (this.toText as unknown as string[]).reduce((c, t) => c + (t || '').length, 0) + : this.toText.length; + const count = Math.ceil(fromCount + (toCount - fromCount) * ratio); + + if (toTextIsArray) { + out.text = []; + let len = 0; + (this.toText as unknown as string[]).forEach(t => { + if (len + t.length > count) { + out.text.push(t.substr(0, count - len)); + len = count; + } else { + out.text.push(t); + len += t.length; + } + }); + } else { + out.text = (this.toText as string).substr(0, count); + } + // console.log(out.text) + + // update line position + // const line = this.target.shadowRoot?.at(0) as IGraphic; + // const endX = (this.target as any).clipedWidth + 2; + // line.setAttribute('x', endX); + } +} + +export class StreamLight extends ACustomAnimate { + declare valid: boolean; + declare target: IGraphic; + + declare rect: IRect; + declare line: ILine; + declare area: IArea; + constructor( + from: any, + to: any, + duration: number, + easing: EasingType, + params?: { attribute?: Partial; streamLength?: number; isHorizontal?: boolean } + ) { + super(from, to, duration, easing, params); + } + + getEndProps(): Record { + return {}; + } + + onStart(): void { + if (!this.target) { + return; + } + if (this.target.type === 'rect') { + this.onStartRect(); + } else if (this.target.type === 'line') { + this.onStartLineOrArea('line'); + } else if (this.target.type === 'area') { + this.onStartLineOrArea('area'); + } + } + + onStartLineOrArea(type: 'line' | 'area') { + const root = this.target.attachShadow(); + const line = application.graphicService.creator[type]({ + ...this.params?.attribute + }); + this[type] = line; + line.pathProxy = new CustomPath2D(); + root.add(line); + } + + onStartRect(): void { + const root = this.target.attachShadow(); + + const isHorizontal = this.params?.isHorizontal ?? true; + const sizeAttr = isHorizontal ? 'height' : 'width'; + const otherSizeAttr = isHorizontal ? 'width' : 'height'; + const size = this.target.AABBBounds[sizeAttr](); + const y = isHorizontal ? 0 : this.target.AABBBounds.y1; + + const rect = application.graphicService.creator.rect({ + [sizeAttr]: size, + fill: '#bcdeff', + shadowBlur: 30, + shadowColor: '#bcdeff', + ...this.params?.attribute, + x: 0, + y, + [otherSizeAttr]: 0 + }); + this.rect = rect; + root.add(rect); + } + + onBind(): void { + return; + } + + onEnd(): void { + this.target.detachShadow(); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + if (this.rect) { + return this.onUpdateRect(end, ratio, out); + } else if (this.line || this.area) { + return this.onUpdateLineOrArea(end, ratio, out); + } + } + + protected onUpdateRect(end: boolean, ratio: number, out: Record): void { + const isHorizontal = this.params?.isHorizontal ?? true; + const parentAttr = (this.target as any).attribute; + if (isHorizontal) { + const parentWidth = parentAttr.width ?? Math.abs(parentAttr.x1 - parentAttr.x) ?? 250; + const streamLength = this.params?.streamLength ?? parentWidth; + const maxLength = this.params?.attribute?.width ?? 60; + // 起点,rect x右端点 对齐 parent左端点 + // 如果parent.x1 < parent.x, 需要把rect属性移到parent x1的位置上, 因为初始 rect.x = parent.x + const startX = -maxLength; + // 插值 + const currentX = startX + (streamLength - startX) * ratio; + // 位置限定 > 0 + const x = Math.max(currentX, 0); + // 宽度计算 + const w = Math.min(Math.min(currentX + maxLength, maxLength), streamLength - currentX); + // 如果 rect右端点 超出 parent右端点, 宽度动态调整 + const width = w + x > parentWidth ? Math.max(parentWidth - x, 0) : w; + this.rect.setAttributes( + { + x, + width, + dx: Math.min(parentAttr.x1 - parentAttr.x, 0) + } as any, + false, + { + type: AttributeUpdateType.ANIMATE_PLAY, + animationState: { + ratio, + end + } + } + ); + } else { + const parentHeight = parentAttr.height ?? Math.abs(parentAttr.y1 - parentAttr.y) ?? 250; + const streamLength = this.params?.streamLength ?? parentHeight; + const maxLength = this.params?.attribute?.height ?? 60; + // 起点,y上端点 对齐 parent下端点 + const startY = parentHeight; + // 插值 + const currentY = startY - (streamLength + maxLength) * ratio; + // 位置限定 < parentHeight + let y = Math.min(currentY, parentHeight); + // 高度最小值 + const h = Math.min(parentHeight - currentY, maxLength); + // 如果 rect上端点=y 超出 parent上端点 = 0, 则高度不断变小 + let height; + if (y <= 0) { + // 必须先得到高度再将y置为0, 顺序很重要 + height = Math.max(y + h, 0); + y = 0; + } else { + height = h; + } + this.rect.setAttributes( + { + y, + height, + dy: Math.min(parentAttr.y1 - parentAttr.y, 0) + } as any, + false, + { + type: AttributeUpdateType.ANIMATE_PLAY, + animationState: { + ratio, + end + } + } + ); + } + } + + protected onUpdateLineOrArea(end: boolean, ratio: number, out: Record) { + const target = this.line || this.area; + if (!target) { + return; + } + const customPath = target.pathProxy as ICustomPath2D; + const targetLine = this.target as ILine | IArea; + if (targetLine.cache || targetLine.cacheArea) { + this._onUpdateLineOrAreaWithCache(customPath, targetLine, end, ratio, out); + } else { + this._onUpdateLineWithoutCache(customPath, targetLine, end, ratio, out); + } + const targetAttrs = targetLine.attribute; + target.setAttributes({ + stroke: targetAttrs.stroke, + ...target.attribute + }); + target.addUpdateBoundTag(); + } + + // 针对有cache的linear + protected _onUpdateLineOrAreaWithCache( + customPath: ICustomPath2D, + g: ILine | IArea, + end: boolean, + ratio: number, + out: Record + ) { + customPath.clear(); + if (g.type === 'line') { + let cache = g.cache; + if (!Array.isArray(cache)) { + cache = [cache]; + } + const totalLen = cache.reduce((l: any, c: any) => l + c.getLength(), 0); + const curves: ICurve[] = []; + cache.forEach((c: any) => { + c.curves.forEach((ci: any) => curves.push(ci)); + }); + return this._updateCurves(customPath, curves, totalLen, ratio); + } else if (g.type === 'area' && g.cacheArea?.top?.curves) { + const cache = g.cacheArea as IAreaCacheItem; + const totalLen = cache.top.curves.reduce((a, b) => a + b.getLength(), 0); + return this._updateCurves(customPath, cache.top.curves, totalLen, ratio); + } + } + + protected _updateCurves(customPath: ICustomPath2D, curves: ICurve[], totalLen: number, ratio: number) { + const startLen = totalLen * ratio; + const endLen = Math.min(startLen + this.params?.streamLength ?? 10, totalLen); + let lastLen = 0; + let start = false; + for (let i = 0; i < curves.length; i++) { + if (curves[i].defined !== false) { + const curveItem = curves[i]; + const len = curveItem.getLength(); + const startPercent = 1 - (lastLen + len - startLen) / len; + let endPercent = 1 - (lastLen + len - endLen) / len; + let curveForStart: ICubicBezierCurve; + if (lastLen < startLen && lastLen + len > startLen) { + start = true; + if (curveItem.p2 && curveItem.p3) { + const [_, curve2] = divideCubic(curveItem as ICubicBezierCurve, startPercent); + customPath.moveTo(curve2.p0.x, curve2.p0.y); + curveForStart = curve2; + // console.log(curve2.p0.x, curve2.p0.y); + } else { + const p = curveItem.getPointAt(startPercent); + customPath.moveTo(p.x, p.y); + } + } + if (lastLen < endLen && lastLen + len > endLen) { + if (curveItem.p2 && curveItem.p3) { + if (curveForStart) { + endPercent = (endLen - startLen) / curveForStart.getLength(); + } + const [curve1] = divideCubic(curveForStart || (curveItem as ICubicBezierCurve), endPercent); + customPath.bezierCurveTo(curve1.p1.x, curve1.p1.y, curve1.p2.x, curve1.p2.y, curve1.p3.x, curve1.p3.y); + } else { + const p = curveItem.getPointAt(endPercent); + customPath.lineTo(p.x, p.y); + } + break; + } else if (start) { + if (curveItem.p2 && curveItem.p3) { + const curve = curveForStart || curveItem; + customPath.bezierCurveTo(curve.p1.x, curve.p1.y, curve.p2.x, curve.p2.y, curve.p3.x, curve.p3.y); + } else { + customPath.lineTo(curveItem.p1.x, curveItem.p1.y); + } + } + lastLen += len; + } + } + } + + // 只针对最简单的linear + protected _onUpdateLineWithoutCache( + customPath: ICustomPath2D, + line: ILine, + end: boolean, + ratio: number, + out: Record + ) { + const { points, curveType } = line.attribute; + if (!points || points.length < 2 || curveType !== 'linear') { + return; + } + let totalLen = 0; + for (let i = 1; i < points.length; i++) { + totalLen += PointService.distancePP(points[i], points[i - 1]); + } + const startLen = totalLen * ratio; + const endLen = Math.min(startLen + this.params?.streamLength ?? 10, totalLen); + const nextPoints = []; + let lastLen = 0; + for (let i = 1; i < points.length; i++) { + const len = PointService.distancePP(points[i], points[i - 1]); + if (lastLen < startLen && lastLen + len > startLen) { + nextPoints.push(PointService.pointAtPP(points[i - 1], points[i], 1 - (lastLen + len - startLen) / len)); + } + if (lastLen < endLen && lastLen + len > endLen) { + nextPoints.push(PointService.pointAtPP(points[i - 1], points[i], 1 - (lastLen + len - endLen) / len)); + break; + } else if (nextPoints.length) { + nextPoints.push(points[i]); + } + lastLen += len; + } + + if (!nextPoints.length || nextPoints.length < 2) { + return; + } + customPath.clear(); + customPath.moveTo(nextPoints[0].x, nextPoints[0].y); + for (let i = 1; i < nextPoints.length; i++) { + customPath.lineTo(nextPoints[i].x, nextPoints[i].y); + } + } +} + +export class Meteor extends ACustomAnimate { + declare size: number; + declare target: IGraphic; + declare root: IShadowRoot; + declare posList: IPoint[]; + + get lastPos(): IPoint { + return this.posList[this.posList.length - 1]; + } + + constructor(size: number, duration: number, easing: EasingType, params?: any) { + super(null, null, duration, easing, params); + this.size = size; + this.posList = []; + } + + onBind(): void { + const root = this.target.attachShadow(); + this.root = root; + for (let i = 0; i < this.size; i++) { + const g = this.target.clone(); + const scale = Math.min(((this.size - i) / this.size) * 3, 1); + const opacity = Math.min(0.2 + 0.7 / this.size); + g.setAttributes({ x: 0, y: 0, dx: 0, dy: 0, scaleX: scale, scaleY: scale, opacity }, false, { + type: AttributeUpdateType.ANIMATE_BIND + }); + root.add(g); + } + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + if (end) { + this.target.detachShadow(); + this.posList.length = 0; + return; + } + + const x = this.target.getComputedAttribute('x'); + const y = this.target.getComputedAttribute('y'); + + const nextPos = new Point(x, y); + if (!this.posList.length) { + this.posList.push(nextPos); + return; + } + + this.target.shadowRoot.forEachChildren((g: IGraphic, i) => { + const pos = this.posList[Math.max(this.posList.length - i - 1, 0)]; + g.setAttributes( + { + x: pos.x - x, + y: pos.y - y + }, + false + ); + }); + + this.posList.push(nextPos); + } +} + +export class MotionPath extends ACustomAnimate { + declare valid: boolean; + declare pathLength: number; + declare path: CustomPath2D; + declare distance: number; + declare initAngle: number; + declare changeAngle: boolean; + declare cb?: (from: any, to: any, ratio: number, target: IGraphic) => void; + constructor( + from: any, + to: any, + duration: number, + easing: EasingType, + params?: { + path: CustomPath2D; + distance: number; + cb?: (from: any, to: any, ratio: number, target: IGraphic) => void; + initAngle?: number; + changeAngle?: boolean; + } + ) { + super(from, to, duration, easing, params); + if (params) { + this.pathLength = params.path.getLength(); + this.path = params.path; + this.distance = params.distance; + this.to = params.distance * this.pathLength; + this.initAngle = params.initAngle ?? 0; + this.changeAngle = !!params.changeAngle; + this.cb = params.cb; + } + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + // 计算位置 + const at = this.to * ratio; + const { pos, angle } = this.path.getAttrAt(at); + out.x = pos.x; + out.y = pos.y; + if (this.changeAngle) { + out.angle = angle + this.initAngle; + } + this.cb && this.cb(this.from, this.to, ratio, this.target as IGraphic); + // out.angle = angle + this.initAngle; + } +} + +export class TagPointsUpdate extends ACustomAnimate<{ points?: IPointLike[]; segments?: ISegment[] }> { + protected fromPoints: IPointLike[]; + protected toPoints: IPointLike[]; + protected points: IPointLike[]; + protected interpolatePoints: [IPointLike, IPointLike][]; + protected newPointAnimateType: 'grow' | 'appear' | 'clip'; + protected clipRange: number; + protected shrinkClipRange: number; + protected clipRangeByDimension: 'x' | 'y'; + protected segmentsCache: number[]; + + constructor( + from: any, + to: any, + duration: number, + easing: EasingType, + params?: { newPointAnimateType?: 'grow' | 'appear' | 'clip'; clipRangeByDimension?: 'x' | 'y' } + ) { + super(from, to, duration, easing, params); + this.newPointAnimateType = params?.newPointAnimateType ?? 'grow'; + this.clipRangeByDimension = params?.clipRangeByDimension ?? 'x'; + } + + private getPoints(attribute: typeof this.from, cache = false): IPointLike[] { + if (attribute.points) { + return attribute.points; + } + + if (attribute.segments) { + const points = [] as IPointLike[]; + if (!this.segmentsCache) { + this.segmentsCache = []; + } + attribute.segments.map(segment => { + if (segment.points) { + points.push(...segment.points); + } + if (cache) { + this.segmentsCache.push(segment.points?.length ?? 0); + } + }); + return points; + } + return []; + } + + onBind(): void { + const originFromPoints = this.getPoints(this.from); + const originToPoints = this.getPoints(this.to, true); + this.fromPoints = !originFromPoints ? [] : !Array.isArray(originFromPoints) ? [originFromPoints] : originFromPoints; + this.toPoints = !originToPoints ? [] : !Array.isArray(originToPoints) ? [originToPoints] : originToPoints; + + const tagMap = new Map(); + this.fromPoints.forEach(point => { + if (point.context) { + tagMap.set(point.context, point); + } + }); + let firstMatchedIndex = Infinity; + let lastMatchedIndex = -Infinity; + let firstMatchedPoint: IPointLike; + let lastMatchedPoint: IPointLike; + for (let i = 0; i < this.toPoints.length; i += 1) { + if (tagMap.has(this.toPoints[i].context)) { + firstMatchedIndex = i; + firstMatchedPoint = tagMap.get(this.toPoints[i].context); + break; + } + } + for (let i = this.toPoints.length - 1; i >= 0; i -= 1) { + if (tagMap.has(this.toPoints[i].context)) { + lastMatchedIndex = i; + lastMatchedPoint = tagMap.get(this.toPoints[i].context); + break; + } + } + + if (this.newPointAnimateType === 'clip') { + if (this.toPoints.length !== 0) { + if (Number.isFinite(lastMatchedIndex)) { + this.clipRange = + this.toPoints[lastMatchedIndex][this.clipRangeByDimension] / + this.toPoints[this.toPoints.length - 1][this.clipRangeByDimension]; + if (this.clipRange === 1) { + this.shrinkClipRange = + this.toPoints[lastMatchedIndex][this.clipRangeByDimension] / + this.fromPoints[this.fromPoints.length - 1][this.clipRangeByDimension]; + } + if (!isValidNumber(this.clipRange)) { + this.clipRange = 0; + } else { + this.clipRange = clamp(this.clipRange, 0, 1); + } + } else { + this.clipRange = 0; + } + } + } + // TODO: shrink removed points + // if no point is matched, animation should start from toPoint[0] + let prevMatchedPoint = this.toPoints[0]; + this.interpolatePoints = this.toPoints.map((point, index) => { + const matchedPoint = tagMap.get(point.context); + if (matchedPoint) { + prevMatchedPoint = matchedPoint; + return [matchedPoint, point]; + } + // appear new point + if (this.newPointAnimateType === 'appear' || this.newPointAnimateType === 'clip') { + return [point, point]; + } + // grow new point + if (index < firstMatchedIndex && firstMatchedPoint) { + return [firstMatchedPoint, point]; + } else if (index > lastMatchedIndex && lastMatchedPoint) { + return [lastMatchedPoint, point]; + } + return [prevMatchedPoint, point]; + }); + this.points = this.interpolatePoints.map(interpolate => { + const fromPoint = interpolate[0]; + const toPoint = interpolate[1]; + const newPoint = new Point(fromPoint.x, fromPoint.y, fromPoint.x1, fromPoint.y1); + newPoint.defined = toPoint.defined; + newPoint.context = toPoint.context; + return newPoint; + }); + } + + onFirstRun(): void { + const lastClipRange = this.target.attribute.clipRange; + if (isValidNumber(lastClipRange * this.clipRange)) { + this.clipRange *= lastClipRange; + } + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + // if not create new points, multi points animation might not work well. + this.points = this.points.map((point, index) => { + const newPoint = pointInterpolation(this.interpolatePoints[index][0], this.interpolatePoints[index][1], ratio); + newPoint.context = point.context; + return newPoint; + }); + if (this.clipRange) { + if (this.shrinkClipRange) { + // 折线变短 + if (!end) { + out.points = this.fromPoints; + out.clipRange = this.clipRange - (this.clipRange - this.shrinkClipRange) * ratio; + } else { + out.points = this.toPoints; + out.clipRange = 1; + } + return; + } + out.clipRange = this.clipRange + (1 - this.clipRange) * ratio; + } + if (this.segmentsCache && this.to.segments) { + let start = 0; + out.segments = this.to.segments.map((segment, index) => { + const end = start + this.segmentsCache[index]; + const points = this.points.slice(start, end); + start = end; + return { + ...segment, + points + }; + }); + } else { + out.points = this.points; + } + } +} + +export class GraphicAnimate extends ACustomAnimate { + graphic: IGraphic; + + constructor(from: any, to: any, duration: number, easing: EasingType, params?: { graphic: IGraphic }) { + super(from, to, duration, easing, params); + this.graphic = params?.graphic; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + if (!this.graphic) { + return; + } + Object.keys(this.from).forEach(k => { + out[k] = this.from[k] + (this.to[k] - this.from[k]) * ratio; + }); + } +} + +export class ClipGraphicAnimate extends ACustomAnimate { + private _group?: IGroup; + private _clipGraphic?: IGraphic; + protected clipFromAttribute?: any; + protected clipToAttribute?: any; + + private _lastClip?: boolean; + private _lastPath?: IGraphic[]; + + constructor( + from: any, + to: any, + duration: number, + easing: EasingType, + params: { group: IGroup; clipGraphic: IGraphic } + ) { + super(null, null, duration, easing, params); + this.clipFromAttribute = from; + this.clipToAttribute = to; + this._group = params?.group; + this._clipGraphic = params?.clipGraphic; + } + + onBind() { + if (this._group && this._clipGraphic) { + this._lastClip = this._group.attribute.clip; + this._lastPath = this._group.attribute.path; + this._group.setAttributes( + { + clip: true, + path: [this._clipGraphic] + }, + false, + { type: AttributeUpdateType.ANIMATE_BIND } + ); + } + } + + onEnd() { + if (this._group) { + this._group.setAttributes( + { + clip: this._lastClip, + path: this._lastPath + }, + false, + { type: AttributeUpdateType.ANIMATE_END } + ); + } + return; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + if (!this._clipGraphic) { + return; + } + const res: any = {}; + Object.keys(this.clipFromAttribute).forEach(k => { + res[k] = this.clipFromAttribute[k] + (this.clipToAttribute[k] - this.clipFromAttribute[k]) * ratio; + }); + this._clipGraphic.setAttributes(res, false, { + type: AttributeUpdateType.ANIMATE_UPDATE, + animationState: { ratio, end } + }); + } +} + +export class ClipAngleAnimate extends ClipGraphicAnimate { + constructor( + from: any, + to: any, + duration: number, + easing: EasingType, + params: { + group: IGroup; + center?: { x: number; y: number }; + startAngle?: number; + radius?: number; + orient?: 'clockwise' | 'anticlockwise'; + animationType?: 'in' | 'out'; + } + ) { + const groupAttribute = params?.group?.attribute ?? {}; + const width = groupAttribute.width ?? 0; + const height = groupAttribute.height ?? 0; + + const animationType = params?.animationType ?? 'in'; + const startAngle = params?.startAngle ?? 0; + const orient = params?.orient ?? 'clockwise'; + + let arcStartAngle = 0; + let arcEndAngle = 0; + if (orient === 'anticlockwise') { + arcEndAngle = animationType === 'in' ? startAngle + Math.PI * 2 : startAngle; + arcEndAngle = startAngle + Math.PI * 2; + } else { + arcStartAngle = startAngle; + arcEndAngle = animationType === 'out' ? startAngle + Math.PI * 2 : startAngle; + } + const arc = application.graphicService.creator.arc({ + x: params?.center?.x ?? width / 2, + y: params?.center?.y ?? height / 2, + outerRadius: params?.radius ?? (width + height) / 2, + innerRadius: 0, + startAngle: arcStartAngle, + endAngle: arcEndAngle, + fill: true + }); + let fromAttributes: Partial; + let toAttributes: Partial; + if (orient === 'anticlockwise') { + fromAttributes = { startAngle: startAngle + Math.PI * 2 }; + toAttributes = { startAngle: startAngle }; + } else { + fromAttributes = { endAngle: startAngle }; + toAttributes = { endAngle: startAngle + Math.PI * 2 }; + } + super( + animationType === 'in' ? fromAttributes : toAttributes, + animationType === 'in' ? toAttributes : fromAttributes, + duration, + easing, + { group: params?.group, clipGraphic: arc } + ); + } +} + +export class ClipRadiusAnimate extends ClipGraphicAnimate { + constructor( + from: any, + to: any, + duration: number, + easing: EasingType, + params: { + group: IGroup; + center?: { x: number; y: number }; + startRadius?: number; + endRadius?: number; + animationType?: 'in' | 'out'; + } + ) { + const groupAttribute = params?.group?.attribute ?? {}; + const width = groupAttribute.width ?? 0; + const height = groupAttribute.height ?? 0; + + const animationType = params?.animationType ?? 'in'; + const startRadius = params?.startRadius ?? 0; + const endRadius = params?.endRadius ?? Math.sqrt((width / 2) ** 2 + (height / 2) ** 2); + + const arc = application.graphicService.creator.arc({ + x: params?.center?.x ?? width / 2, + y: params?.center?.y ?? height / 2, + outerRadius: animationType === 'out' ? endRadius : startRadius, + innerRadius: 0, + startAngle: 0, + endAngle: Math.PI * 2, + fill: true + }); + const fromAttributes: Partial = { outerRadius: startRadius }; + const toAttributes: Partial = { outerRadius: endRadius }; + super( + animationType === 'in' ? fromAttributes : toAttributes, + animationType === 'in' ? toAttributes : fromAttributes, + duration, + easing, + { group: params?.group, clipGraphic: arc } + ); + } +} + +export class ClipDirectionAnimate extends ClipGraphicAnimate { + constructor( + from: any, + to: any, + duration: number, + easing: EasingType, + params: { + group: IGroup; + direction?: 'x' | 'y'; + orient?: 'positive' | 'negative'; + width?: number; + height?: number; + animationType?: 'in' | 'out'; + } + ) { + const groupAttribute = params?.group?.attribute ?? {}; + const width = params?.width ?? groupAttribute.width ?? 0; + const height = params?.height ?? groupAttribute.height ?? 0; + + const animationType = params?.animationType ?? 'in'; + const direction = params?.direction ?? 'x'; + const orient = params?.orient ?? 'positive'; + + const rect = application.graphicService.creator.rect({ + x: 0, + y: 0, + width: animationType === 'in' && direction === 'x' ? 0 : width, + height: animationType === 'in' && direction === 'y' ? 0 : height, + fill: true + }); + let fromAttributes: Partial = {}; + let toAttributes: Partial = {}; + if (direction === 'y') { + if (orient === 'negative') { + fromAttributes = { y: height, height: 0 }; + toAttributes = { y: 0, height: height }; + } else { + fromAttributes = { height: 0 }; + toAttributes = { height: height }; + } + } else { + if (orient === 'negative') { + fromAttributes = { x: width, width: 0 }; + toAttributes = { x: 0, width: width }; + } else { + fromAttributes = { width: 0 }; + toAttributes = { width: width }; + } + } + super( + animationType === 'in' ? fromAttributes : toAttributes, + animationType === 'in' ? toAttributes : fromAttributes, + duration, + easing, + { group: params?.group, clipGraphic: rect } + ); + } +} + +type RotateSphereParams = + | { + center: { x: number; y: number; z: number }; + r: number; + cb?: (out: any) => void; + } + | (() => any); + +export class RotateBySphereAnimate extends ACustomAnimate { + declare params: RotateSphereParams; + declare theta: number; + declare phi: number; + + onStart(): void { + const { center, r } = typeof this.params === 'function' ? this.params() : this.params; + const startX = this.target.getComputedAttribute('x'); + const startY = this.target.getComputedAttribute('y'); + const startZ = this.target.getComputedAttribute('z'); + const phi = Math.acos((startY - center.y) / r); + let theta = Math.acos((startX - center.x) / r / Math.sin(phi)); + if (startZ - center.z < 0) { + theta = pi2 - theta; + } + this.theta = theta; + this.phi = phi; + } + + onBind() { + return; + } + + onEnd() { + return; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + if (this.phi == null || this.theta == null) { + return; + } + const { center, r, cb } = typeof this.params === 'function' ? this.params() : this.params; + const deltaAngle = Math.PI * 2 * ratio; + const theta = this.theta + deltaAngle; + const phi = this.phi; + const x = r * Math.sin(phi) * Math.cos(theta) + center.x; + const y = r * Math.cos(phi) + center.y; + const z = r * Math.sin(phi) * Math.sin(theta) + center.z; + out.x = x; + out.y = y; + out.z = z; + // out.beta = phi; + out.alpha = theta + pi / 2; + while (out.alpha > pi2) { + out.alpha -= pi2; + } + out.alpha = pi2 - out.alpha; + + out.zIndex = out.z * -10000; + + cb && cb(out); + } +} + +export class AttributeAnimate extends ACustomAnimate { + declare target: IGroup; + + constructor(to: Record, duration: number, easing: EasingType) { + super({}, to, duration, easing); + } + + getEndProps(): Record { + return this.to; + } + + onBind(): void { + Object.keys(this.to).forEach(k => { + this.from[k] = this.target.getComputedAttribute(k); + }); + return; + } + + onEnd(): void { + return; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + this.target.stepInterpolate( + this.subAnimate, + this.subAnimate.animate, + out, + this.step, + ratio, + end, + this.to, + this.from + ); + } +} + +export class AnimateGroup extends ACustomAnimate { + declare customAnimates: ACustomAnimate[]; + declare updating: boolean; + + constructor(duration: number, customAnimates: ACustomAnimate[]) { + super(null, null, duration, 'linear'); + this.customAnimates = customAnimates; + } + + initAnimates() { + this.customAnimates.forEach(a => { + a.step = this.step; + a.subAnimate = this.subAnimate; + a.target = this.target; + }); + } + + getEndProps(): Record { + const props = {}; + this.customAnimates.forEach(a => { + Object.assign(props, a.getEndProps()); + }); + return props; + } + + onBind(): void { + this.initAnimates(); + this.customAnimates.forEach(a => { + a.onBind(); + }); + return; + } + + onEnd(): void { + this.customAnimates.forEach(a => { + a.onEnd(); + }); + return; + } + + onStart(): void { + this.customAnimates.forEach(a => { + a.onStart(); + }); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + if (this.updating) { + return; + } + this.updating = true; + this.customAnimates.forEach(a => { + const easing = a.easing; + const easingFunc = typeof easing === 'string' ? Easing[easing] : easing; + ratio = easingFunc(ratio); + a.onUpdate(end, ratio, out); + }); + this.updating = false; + return; + } +} + +export class AnimateGroup1 extends ACustomAnimate { + declare customAnimates: ACustomAnimate[]; + declare updating: boolean; + + constructor(duration: number, customAnimates: ACustomAnimate[]) { + super(null, null, duration, 'linear'); + this.customAnimates = customAnimates; + } + + initAnimates() { + this.customAnimates.forEach(a => { + a.step = this.step; + a.subAnimate = this.subAnimate; + a.target = this.target; + }); + } + + getEndProps(): Record { + const props = {}; + this.customAnimates.forEach(a => { + Object.assign(props, a.getEndProps()); + }); + return props; + } + + onBind(): void { + this.initAnimates(); + this.customAnimates.forEach(a => { + a.onBind(); + }); + return; + } + + onEnd(): void { + this.customAnimates.forEach(a => { + a.onEnd(); + }); + return; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + if (this.updating) { + return; + } + this.updating = true; + this.customAnimates.forEach(a => { + const easing = a.easing; + const easingFunc = typeof easing === 'string' ? Easing[easing] : easing; + ratio = easingFunc(ratio); + a.onUpdate(end, ratio, out); + }); + this.updating = false; + return; + } +} diff --git a/packages/vrender-core/src/animate/default-ticker.ts b/packages/vrender-core/src/animate/default-ticker.ts new file mode 100644 index 000000000..80d217394 --- /dev/null +++ b/packages/vrender-core/src/animate/default-ticker.ts @@ -0,0 +1,7 @@ +import { DefaultTicker } from './Ticker/default-ticker'; +import { defaultTimeline } from './timeline'; + +export const defaultTicker = new DefaultTicker(); +defaultTicker.addTimeline(defaultTimeline); +const TICKER_FPS = 60; +defaultTicker.setFPS(TICKER_FPS); diff --git a/packages/vrender-core/src/animate/easing-func.ts b/packages/vrender-core/src/animate/easing-func.ts new file mode 100644 index 000000000..42c0dfb8a --- /dev/null +++ b/packages/vrender-core/src/animate/easing-func.ts @@ -0,0 +1,12 @@ +import { CustomPath2D } from '../common/custom-path2d'; +import { CurveContext } from '../common/segment'; + +export function generatorPathEasingFunc(path: string) { + const customPath = new CustomPath2D(); + customPath.setCtx(new CurveContext(customPath)); + customPath.fromString(path, 0, 0, 1, 1); + + return (x: number) => { + return customPath.getYAt(x); + }; +} diff --git a/packages/vrender-core/src/animate/easing.ts b/packages/vrender-core/src/animate/easing.ts new file mode 100644 index 000000000..f50472f1d --- /dev/null +++ b/packages/vrender-core/src/animate/easing.ts @@ -0,0 +1,273 @@ +/** + * The MIT License (MIT) + + Copyright (c) 2014 gskinner.com, inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +// 参考TweenJS +// https://github.com/CreateJS/TweenJS/tree/master/src/tweenjs +import { pi2 } from '@visactor/vutils'; + +/** + * 代码迁移自createjs + * 部分缓动函数参考https://easings.net/ + */ +export class Easing { + private constructor() { + // do nothing + } + + static linear(t: number): number { + return t; + } + + static none() { + return this.linear; + } + + /** + * 获取缓动函数,amount指示这个缓动函数的插值方式 + * @param amount + * @returns + */ + static get(amount: number) { + if (amount < -1) { + amount = -1; + } else if (amount > 1) { + amount = 1; + } + + return function (t: number) { + if (amount === 0) { + return t; + } + if (amount < 0) { + return t * (t * -amount + 1 + amount); + } + return t * ((2 - t) * amount + (1 - amount)); + }; + } + + /* 语法糖 */ + static getPowIn(pow: number) { + return function (t: number) { + return Math.pow(t, pow); + }; + } + + static getPowOut(pow: number) { + return function (t: number) { + return 1 - Math.pow(1 - t, pow); + }; + } + + static getPowInOut(pow: number) { + return function (t: number) { + if ((t *= 2) < 1) { + return 0.5 * Math.pow(t, pow); + } + return 1 - 0.5 * Math.abs(Math.pow(2 - t, pow)); + }; + } + + // 插值函数 + static quadIn = Easing.getPowIn(2); + static quadOut = Easing.getPowOut(2); + + static quadInOut = Easing.getPowInOut(2); + static cubicIn = Easing.getPowIn(3); + static cubicOut = Easing.getPowOut(3); + static cubicInOut = Easing.getPowInOut(3); + static quartIn = Easing.getPowIn(4); + static quartOut = Easing.getPowOut(4); + static quartInOut = Easing.getPowInOut(4); + static quintIn = Easing.getPowIn(5); + static quintOut = Easing.getPowOut(5); + static quintInOut = Easing.getPowInOut(5); + + /* 语法糖 */ + static getBackIn(amount: number) { + return function (t: number) { + return t * t * ((amount + 1) * t - amount); + }; + } + static getBackOut(amount: number) { + return function (t: number) { + return --t * t * ((amount + 1) * t + amount) + 1; + }; + } + static getBackInOut(amount: number) { + amount *= 1.525; + return function (t: number) { + if ((t *= 2) < 1) { + return 0.5 * (t * t * ((amount + 1) * t - amount)); + } + return 0.5 * ((t -= 2) * t * ((amount + 1) * t + amount) + 2); + }; + } + + // 插值函数 + static backIn = Easing.getBackIn(1.7); + static backOut = Easing.getBackOut(1.7); + static backInOut = Easing.getBackInOut(1.7); + + static sineIn(t: number): number { + return 1 - Math.cos((t * Math.PI) / 2); + } + + static sineOut(t: number): number { + return Math.sin((t * Math.PI) / 2); + } + + static sineInOut(t: number): number { + return -(Math.cos(Math.PI * t) - 1) / 2; + } + + static expoIn(t: number): number { + return t === 0 ? 0 : Math.pow(2, 10 * t - 10); + } + + static expoOut(t: number): number { + return t === 1 ? 1 : 1 - Math.pow(2, -10 * t); + } + + static expoInOut(t: number): number { + return t === 0 ? 0 : t === 1 ? 1 : t < 0.5 ? Math.pow(2, 20 * t - 10) / 2 : (2 - Math.pow(2, -20 * t + 10)) / 2; + } + + // 插值函数 + static circIn(t: number) { + return -(Math.sqrt(1 - t * t) - 1); + } + + static circOut(t: number) { + return Math.sqrt(1 - --t * t); + } + + static circInOut(t: number) { + if ((t *= 2) < 1) { + return -0.5 * (Math.sqrt(1 - t * t) - 1); + } + return 0.5 * (Math.sqrt(1 - (t -= 2) * t) + 1); + } + static bounceOut(t: number) { + if (t < 1 / 2.75) { + return 7.5625 * t * t; + } else if (t < 2 / 2.75) { + return 7.5625 * (t -= 1.5 / 2.75) * t + 0.75; + } else if (t < 2.5 / 2.75) { + return 7.5625 * (t -= 2.25 / 2.75) * t + 0.9375; + } + return 7.5625 * (t -= 2.625 / 2.75) * t + 0.984375; + } + static bounceIn(t: number) { + return 1 - Easing.bounceOut(1 - t); + } + + static bounceInOut(t: number) { + if (t < 0.5) { + return Easing.bounceIn(t * 2) * 0.5; + } + return Easing.bounceOut(t * 2 - 1) * 0.5 + 0.5; + } + + /* 语法糖 */ + static getElasticIn(amplitude: number, period: number) { + return function (t: number) { + if (t === 0 || t === 1) { + return t; + } + const s = (period / pi2) * Math.asin(1 / amplitude); + return -(amplitude * Math.pow(2, 10 * (t -= 1)) * Math.sin(((t - s) * pi2) / period)); + }; + } + static getElasticOut(amplitude: number, period: number) { + return function (t: number) { + if (t === 0 || t === 1) { + return t; + } + const s = (period / pi2) * Math.asin(1 / amplitude); + return amplitude * Math.pow(2, -10 * t) * Math.sin(((t - s) * pi2) / period) + 1; + }; + } + static getElasticInOut(amplitude: number, period: number) { + return function (t: number) { + const s = (period / pi2) * Math.asin(1 / amplitude); + if ((t *= 2) < 1) { + return -0.5 * (amplitude * Math.pow(2, 10 * (t -= 1)) * Math.sin(((t - s) * pi2) / period)); + } + return amplitude * Math.pow(2, -10 * (t -= 1)) * Math.sin(((t - s) * pi2) / period) * 0.5 + 1; + }; + } + + // 插值函数 + static elasticIn = Easing.getElasticIn(1, 0.3); + static elasticOut = Easing.getElasticOut(1, 0.3); + static elasticInOut = Easing.getElasticInOut(1, 0.3 * 1.5); + + static easeInOutQuad = (t: number) => { + if ((t /= 0.5) < 1) { + return 0.5 * Math.pow(t, 2); + } + return -0.5 * ((t -= 2) * t - 2); + }; + + static easeOutElastic = (x: number) => { + const c4 = (2 * Math.PI) / 3; + + return x === 0 ? 0 : x === 1 ? 1 : Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1; + }; + + static easeInOutElastic = (x: number) => { + const c5 = (2 * Math.PI) / 4.5; + + return x === 0 + ? 0 + : x === 1 + ? 1 + : x < 0.5 + ? -(Math.pow(2, 20 * x - 10) * Math.sin((20 * x - 11.125) * c5)) / 2 + : (Math.pow(2, -20 * x + 10) * Math.sin((20 * x - 11.125) * c5)) / 2 + 1; + }; + static registerFunc(name: string, func: (t: number) => number) { + (Easing as any)[name] = func; + } +} + +function flicker(t: number, n: number) { + const step = 1 / n; + let flag = 1; + while (t > step) { + t -= step; + flag *= -1; + } + const v = (flag * t) / step; + return v > 0 ? v : 1 + v; +} + +// 注册flicker +for (let i = 0; i < 10; i++) { + (Easing as any)[`flicker${i}`] = (t: number) => flicker(t, i); +} + +for (let i = 2; i < 10; i++) { + (Easing as any)[`aIn${i}`] = (t: number) => i * t * t + (1 - i) * t; +} diff --git a/packages/vrender-core/src/animate/group-fade.ts b/packages/vrender-core/src/animate/group-fade.ts new file mode 100644 index 000000000..838179d85 --- /dev/null +++ b/packages/vrender-core/src/animate/group-fade.ts @@ -0,0 +1,70 @@ +import type { IGroup } from '../interface/graphic/group'; +import { ACustomAnimate } from './animate'; + +export class GroupFadeIn extends ACustomAnimate { + declare target: IGroup; + + getEndProps(): Record { + return {}; + } + + onBind(): void { + this.target.setTheme({ + common: { + opacity: 0 + } + }); + return; + } + + onEnd(): void { + this.target.setTheme({ + common: { + opacity: 1 + } + }); + return; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + this.target.setTheme({ + common: { + opacity: ratio + } + }); + } +} + +export class GroupFadeOut extends ACustomAnimate { + declare target: IGroup; + + getEndProps(): Record { + return {}; + } + + onBind(): void { + this.target.setTheme({ + common: { + opacity: 1 + } + }); + return; + } + + onEnd(): void { + this.target.setTheme({ + common: { + opacity: 0 + } + }); + return; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + this.target.setTheme({ + common: { + opacity: 1 - ratio + } + }); + } +} diff --git a/packages/vrender-core/src/animate/index.ts b/packages/vrender-core/src/animate/index.ts new file mode 100644 index 000000000..b04ca57b3 --- /dev/null +++ b/packages/vrender-core/src/animate/index.ts @@ -0,0 +1,8 @@ +export * from './Ticker'; +export * from './animate'; +export * from './config'; +export * from './custom-animate'; +export * from './morphing'; +export * from './timeline'; +export * from './group-fade'; +export * from './easing'; diff --git a/packages/vrender-core/src/animate/morphing.ts b/packages/vrender-core/src/animate/morphing.ts new file mode 100644 index 000000000..312aaaf8a --- /dev/null +++ b/packages/vrender-core/src/animate/morphing.ts @@ -0,0 +1,680 @@ +import { + splitArc, + splitCircle, + splitLine, + splitRect, + splitPolygon, + splitArea, + splitPath +} from './../common/split-path'; +import type { + ICustomPath2D, + IGraphic, + MorphingAnimateConfig, + IRect, + EasingType, + MultiMorphingAnimateConfig, + IArc, + ICircle, + IGraphicAttribute, + ILine, + IPolygon, + IArea, + IPath +} from './../interface'; +import { CustomPath2D } from '../common/custom-path2d'; +import { ACustomAnimate } from './animate'; +import { + alignBezierCurves, + applyTransformOnBezierCurves, + findBestMorphingRotation, + pathToBezierCurves +} from '../common/morphing-utils'; +import { application } from '../application'; +import type { IMatrix } from '@visactor/vutils'; +import { isNil } from '@visactor/vutils'; +import { interpolateColor } from '../color-string/interpolate'; +import { ColorStore, ColorType } from '../color-string'; +import { DefaultMorphingAnimateConfig } from './config'; +import { isTransformKey } from '../common/utils'; +import { AttributeUpdateType } from '../common/enums'; + +declare const __DEV__: boolean; + +interface MorphingDataItem { + from: number[]; + to: number[]; + fromCp: number[]; + toCp: number[]; + rotation: number; +} + +interface OtherAttrItem { + from: any; + to: any; + key: string; +} + +const interpolateOtherAttrs = (attrs: OtherAttrItem[], out: any, ratio: number) => { + attrs.forEach(entry => { + if (Number.isFinite(entry.to)) { + out[entry.key] = entry.from + (entry.to - entry.from) * ratio; + } else if (entry.key === 'fill' || entry.key === 'stroke') { + // 保存解析的结果到step + const color = interpolateColor(entry.from, entry.to, ratio, false); + if (color) { + out[entry.key] = color; + } + } + }); +}; + +/* Adapted from zrender by ecomfe + * https://github.com/ecomfe/zrender + * Licensed under the BSD-3-Clause + + * url: https://github.com/ecomfe/zrender/blob/master/src/tool/morphPath.ts + * License: https://github.com/ecomfe/zrender/blob/master/LICENSE + * @license + */ +const interpolateMorphingData = (morphingData: MorphingDataItem[], path: ICustomPath2D, ratio: number) => { + const tmpArr: number[] = []; + const newCp: number[] = []; + path.clear(); + + for (let i = 0; i < morphingData.length; i++) { + const item = morphingData[i]; + const from = item.from; + const to = item.to; + const angle = item.rotation * ratio; + const fromCp = item.fromCp; + const toCp = item.toCp; + const sa = Math.sin(angle); + const ca = Math.cos(angle); + + newCp[0] = fromCp[0] + (toCp[0] - fromCp[0]) * ratio; + newCp[1] = fromCp[1] + (toCp[1] - fromCp[1]) * ratio; + + for (let m = 0; m < from.length; m += 2) { + const x0 = from[m]; + const y0 = from[m + 1]; + const x1 = to[m]; + const y1 = to[m + 1]; + + const x = x0 * (1 - ratio) + x1 * ratio; + const y = y0 * (1 - ratio) + y1 * ratio; + + tmpArr[m] = x * ca - y * sa + newCp[0]; + tmpArr[m + 1] = x * sa + y * ca + newCp[1]; + } + + let x0 = tmpArr[0]; + let y0 = tmpArr[1]; + + path.moveTo(x0, y0); + + for (let m = 2; m < from.length; m += 6) { + const x1 = tmpArr[m]; + const y1 = tmpArr[m + 1]; + const x2 = tmpArr[m + 2]; + const y2 = tmpArr[m + 3]; + const x3 = tmpArr[m + 4]; + const y3 = tmpArr[m + 5]; + + // Is a line. + if (x0 === x1 && y0 === y1 && x2 === x3 && y2 === y3) { + path.lineTo(x3, y3); + } else { + path.bezierCurveTo(x1, y1, x2, y2, x3, y3); + } + x0 = x3; + y0 = y3; + } + } +}; + +const parseMorphingData = ( + fromPath: ICustomPath2D | null, + toPath: ICustomPath2D, + config?: { + fromTransform?: IMatrix; + toTransfrom: IMatrix; + } +) => { + const fromBezier = fromPath ? pathToBezierCurves(fromPath) : []; + const toBezier = pathToBezierCurves(toPath); + + if (config && fromBezier) { + config.fromTransform && applyTransformOnBezierCurves(fromBezier, config.fromTransform.clone().getInverse()); + applyTransformOnBezierCurves(fromBezier, config.toTransfrom); + // applyTransformOnBezierCurves(toBezier, config.toTransfrom.clone().getInverse()); + } + + const [fromBezierCurves, toBezierCurves] = alignBezierCurves(fromBezier, toBezier); + + return fromPath + ? findBestMorphingRotation(fromBezierCurves, toBezierCurves, 10, Math.PI) + : toBezierCurves.map((to, index) => { + return { + from: fromBezierCurves[index], + to, + fromCp: [0, 0], + toCp: [0, 0], + rotation: 0 + }; + }); +}; + +const validateOtherAttrs = [ + 'fill', + 'fillOpacity', + 'shadowBlur', + 'shadowColor', + 'shadowOffsetX', + 'shadowOffsetY', + 'stroke', + 'strokeOpacity', + 'lineDashOffset' + // 'lineWidth' +]; + +const parseOtherAnimateAttrs = ( + fromAttrs: Partial | null, + toAttrs: Partial | null +) => { + if (!fromAttrs || !toAttrs) { + return null; + } + const res: OtherAttrItem[] = []; + let hasAttr = false; + + Object.keys(fromAttrs).forEach(fromKey => { + if (!validateOtherAttrs.includes(fromKey)) { + return; + } + + const toValue = toAttrs[fromKey]; + if (!isNil(toValue) && !isNil(fromAttrs[fromKey]) && toValue !== fromAttrs[fromKey]) { + if (fromKey === 'fill' || fromKey === 'stroke') { + res.push({ + from: + typeof fromAttrs[fromKey] === 'string' + ? ColorStore.Get(fromAttrs[fromKey] as unknown as string, ColorType.Color255) + : fromAttrs[fromKey], + to: typeof toValue === 'string' ? ColorStore.Get(toValue as string, ColorType.Color255) : toValue, + key: fromKey + }); + } else { + res.push({ from: fromAttrs[fromKey], to: toValue, key: fromKey }); + } + + hasAttr = true; + } + }); + + return hasAttr ? res : null; +}; + +export class MorphingPath extends ACustomAnimate { + declare path: CustomPath2D; + + saveOnEnd?: boolean; + otherAttrs?: OtherAttrItem[]; + + constructor( + config: { morphingData: MorphingDataItem[]; otherAttrs?: OtherAttrItem[]; saveOnEnd?: boolean }, + duration: number, + easing: EasingType + ) { + super(0, 1, duration, easing); + this.morphingData = config.morphingData; + this.otherAttrs = config.otherAttrs; + this.saveOnEnd = config.saveOnEnd; + } + + private morphingData?: MorphingDataItem[]; + + getEndProps(): Record { + return {}; + } + + onBind(): void { + (this.target as IGraphic).createPathProxy(); + this.onUpdate(false, 0, (this.target as IGraphic).attribute); + } + + onEnd(): void { + return; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + const target = this.target as IGraphic; + const pathProxy = typeof target.pathProxy === 'function' ? target.pathProxy(target.attribute) : target.pathProxy; + interpolateMorphingData(this.morphingData, pathProxy, ratio); + if (this.otherAttrs && this.otherAttrs.length) { + interpolateOtherAttrs(this.otherAttrs, out, ratio); + } + // 计算位置 + if (end && !this.saveOnEnd) { + (this.target as IGraphic).pathProxy = null; + } + } +} + +export const morphPath = ( + fromGraphic: IGraphic | null, + toGraphic: IGraphic, + animationConfig?: MorphingAnimateConfig, + fromGraphicTransform?: IMatrix +) => { + if (fromGraphic && (!fromGraphic.valid || !fromGraphic.toCustomPath)) { + if (__DEV__) { + console.error(fromGraphic, ' is not validate'); + } + return null; + } + + if (!toGraphic.valid || !toGraphic.toCustomPath) { + if (__DEV__) { + console.error(toGraphic, ' is not validate'); + } + return null; + } + + let fromTransform = fromGraphic?.globalTransMatrix; + + if (fromGraphicTransform && fromTransform) { + fromTransform = fromGraphicTransform + .clone() + .multiply(fromTransform.a, fromTransform.b, fromTransform.c, fromTransform.d, fromTransform.e, fromTransform.f); + } + const morphingData = parseMorphingData(fromGraphic?.toCustomPath?.(), toGraphic.toCustomPath(), { + fromTransform, + toTransfrom: toGraphic.globalTransMatrix + }); + + const attrs = parseOtherAnimateAttrs(fromGraphic?.attribute, toGraphic.attribute); + const animate = toGraphic.animate(animationConfig); + + if (animationConfig?.delay) { + animate.wait(animationConfig.delay); + } + + animate.play( + new MorphingPath( + { morphingData, otherAttrs: attrs }, + animationConfig?.duration ?? DefaultMorphingAnimateConfig.duration, + animationConfig?.easing ?? DefaultMorphingAnimateConfig.easing + ) + ); + + return animate; +}; + +export const oneToMultiMorph = ( + fromGraphic: IGraphic, + toGraphics: IGraphic[], + animationConfig?: MultiMorphingAnimateConfig +) => { + const validateToGraphics = toGraphics.filter(graphic => graphic && graphic.toCustomPath && graphic.valid); + if (!validateToGraphics.length) { + if (__DEV__) { + console.error(validateToGraphics, ' is not validate'); + } + } + + if (!fromGraphic.valid || !fromGraphic.toCustomPath) { + if (__DEV__) { + console.error(fromGraphic, ' is not validate'); + } + } + + const childGraphics: IGraphic[] = ( + animationConfig?.splitPath === 'clone' ? cloneGraphic : animationConfig?.splitPath ?? splitGraphic + )(fromGraphic, validateToGraphics.length, false); + + const oldOnEnd = animationConfig?.onEnd; + let count = validateToGraphics.length; + const onEachEnd = () => { + count--; + if (count === 0 && oldOnEnd) { + oldOnEnd(); + } + }; + + validateToGraphics.forEach((toChild, index) => { + const fromChild = childGraphics[index]; + const delay = + (animationConfig?.delay ?? 0) + + (animationConfig?.individualDelay + ? animationConfig.individualDelay(index, validateToGraphics.length, fromChild, toChild) + : 0); + morphPath( + fromChild, + toChild, + Object.assign({}, animationConfig, { onEnd: onEachEnd, delay }), + fromGraphic.globalTransMatrix + ); + }); +}; + +export class MultiToOneMorphingPath extends ACustomAnimate { + declare path: CustomPath2D; + + otherAttrs?: OtherAttrItem[][]; + + constructor( + config: { morphingData: MorphingDataItem[][]; otherAttrs?: OtherAttrItem[][] }, + duration: number, + easing: EasingType + ) { + super(0, 1, duration, easing); + this.morphingData = config.morphingData; + this.otherAttrs = config.otherAttrs; + } + + private morphingData?: MorphingDataItem[][]; + + getEndProps(): Record { + return {}; + } + + onBind(): void { + this.addPathProxy(); + } + + private addPathProxy() { + const shadowRoot = (this.target as IGraphic).shadowRoot; + + shadowRoot.forEachChildren(child => { + (child as IGraphic).createPathProxy(); + }); + + this.onUpdate(false, 0, (this.target as IGraphic).attribute); + } + + private clearPathProxy() { + const shadowRoot = (this.target as IGraphic).shadowRoot; + + shadowRoot.forEachChildren(child => { + (child as IGraphic).pathProxy = null; + }); + } + + onEnd(): void { + return; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + const shadowRoot = (this.target as IGraphic).shadowRoot; + + shadowRoot.forEachChildren((child: IGraphic, index) => { + interpolateMorphingData( + this.morphingData[index], + typeof child.pathProxy === 'function' ? child.pathProxy(child.attribute) : child.pathProxy, + ratio + ); + + if (this.otherAttrs?.[index] && this.otherAttrs[index].length) { + interpolateOtherAttrs(this.otherAttrs[index], child.attribute, ratio); + } + }); + + // 计算位置 + if (end) { + this.clearPathProxy(); + this.morphingData = null; + } + } +} + +const parseShadowChildAttrs = (graphicAttrs: Partial) => { + const attrs: Partial = {}; + + Object.keys(graphicAttrs).forEach(key => { + if (!isTransformKey(key)) { + attrs[key] = graphicAttrs[key]; + } + }); + + // if (attrs.fill == null) { + // attrs.fill = !!attrs.fillColor; + // } + // if (attrs.stroke == null) { + // attrs.stroke = !!attrs.strokeColor; + // } + + return attrs; +}; + +const appendShadowChildrenToGraphic = (graphic: IGraphic, children: IGraphic[], count: number) => { + const childAttrs = parseShadowChildAttrs(graphic.attribute); + const shadowRoot = graphic.attachShadow(); + + if (children.length) { + shadowRoot.setTheme({ + [children[0].type]: childAttrs + }); + children.forEach(element => { + element.setAttributes({ pickable: false }); + shadowRoot.appendChild(element); + }); + } else { + const box = graphic.AABBBounds; + const width = box.width(); + const height = box.height(); + + shadowRoot.setTheme({ + rect: childAttrs + }); + new Array(count).fill(0).forEach(el => { + const child = application.graphicService.creator.rect({ + x: 0, + y: 0, + width, + height: height, + pickable: false + }); + shadowRoot.appendChild(child); + children.push(child); + }); + } +}; + +export const cloneGraphic = (graphic: IGraphic, count: number, needAppend?: boolean) => { + const children: IGraphic[] = []; + const childAttrs = needAppend ? null : parseShadowChildAttrs(graphic.attribute); + const path = graphic.toCustomPath(); + + for (let i = 0; i < count; i++) { + const element = { + path: new CustomPath2D().fromCustomPath2D(path) + }; + + children.push( + application.graphicService.creator.path(needAppend ? element : Object.assign({}, childAttrs, element)) + ); + } + + if (needAppend) { + appendShadowChildrenToGraphic(graphic, children, count); + } + + return children; +}; + +export const splitGraphic = (graphic: IGraphic, count: number, needAppend?: boolean) => { + const children: IGraphic[] = []; + const childAttrs = needAppend ? null : parseShadowChildAttrs(graphic.attribute); + + if (graphic.type === 'rect') { + const childrenAttrs = splitRect(graphic as IRect, count); + childrenAttrs.forEach(element => { + children.push( + application.graphicService.creator.rect(needAppend ? element : Object.assign({}, childAttrs, element)) + ); + }); + } else if (graphic.type === 'arc') { + const childrenAttrs = splitArc(graphic as IArc, count); + childrenAttrs.forEach(element => { + children.push( + application.graphicService.creator.arc(needAppend ? element : Object.assign({}, childAttrs, element)) + ); + }); + } else if (graphic.type === 'circle') { + const childrenAttrs = splitCircle(graphic as ICircle, count); + childrenAttrs.forEach(element => { + children.push( + application.graphicService.creator.arc(needAppend ? element : Object.assign({}, childAttrs, element)) + ); + }); + } else if (graphic.type === 'line') { + const childrenAttrs = splitLine(graphic as ILine, count); + const defaultSymbol = { size: 10, symbolType: 'circle' }; + + childrenAttrs.forEach(element => { + children.push( + application.graphicService.creator.symbol( + needAppend ? Object.assign({}, element, defaultSymbol) : Object.assign({}, childAttrs, element, defaultSymbol) + ) + ); + }); + } else if (graphic.type === 'polygon') { + const childrenAttrs = splitPolygon(graphic as IPolygon, count); + childrenAttrs.forEach(element => { + children.push( + application.graphicService.creator.polygon(needAppend ? element : Object.assign({}, childAttrs, element)) + ); + }); + } else if (graphic.type === 'area') { + const childrenAttrs = splitArea(graphic as IArea, count); + childrenAttrs.forEach(element => { + children.push( + application.graphicService.creator.polygon(needAppend ? element : Object.assign({}, childAttrs, element)) + ); + }); + } else if (graphic.type === 'path') { + const childrenAttrs = splitPath(graphic as IPath, count); + childrenAttrs.forEach(element => { + if ('path' in element) { + children.push( + application.graphicService.creator.path(needAppend ? element : Object.assign({}, childAttrs, element)) + ); + } else { + children.push( + application.graphicService.creator.polygon(needAppend ? element : Object.assign({}, childAttrs, element)) + ); + } + }); + } + + if (needAppend) { + appendShadowChildrenToGraphic(graphic, children, count); + } + + return children; +}; + +/** + * 多对一动画 + * @param fromGraphics + * @param toGraphic + * @param animationConfig + */ +export const multiToOneMorph = ( + fromGraphics: IGraphic[], + toGraphic: IGraphic, + animationConfig?: MultiMorphingAnimateConfig +) => { + const validateFromGraphics = fromGraphics.filter(graphic => graphic.toCustomPath && graphic.valid); + if (!validateFromGraphics.length) { + if (__DEV__) { + console.error(fromGraphics, ' is not validate'); + } + } + + if (!toGraphic.valid || !toGraphic.toCustomPath) { + if (__DEV__) { + console.error(toGraphic, ' is not validate'); + } + } + + const childGraphics: IGraphic[] = ( + animationConfig?.splitPath === 'clone' ? cloneGraphic : animationConfig?.splitPath ?? splitGraphic + )(toGraphic, validateFromGraphics.length, true); + + const toAttrs = toGraphic.attribute; + toGraphic.setAttribute('visible', false); + + const morphingData = validateFromGraphics.map((graphic, index) => { + return parseMorphingData(graphic.toCustomPath(), childGraphics[index].toCustomPath(), { + fromTransform: graphic.globalTransMatrix, + toTransfrom: childGraphics[index].globalTransMatrix + }); + }); + const otherAttrs = validateFromGraphics.map((graphic, index) => { + return parseOtherAnimateAttrs(graphic.attribute, toAttrs); + }); + + if (animationConfig?.individualDelay) { + const oldOnEnd = animationConfig.onEnd; + let count = validateFromGraphics.length; + const onEachEnd = () => { + count--; + if (count === 0) { + toGraphic.setAttributes({ visible: true, ratio: null } as any, false, { + type: AttributeUpdateType.ANIMATE_END + }); + toGraphic.detachShadow(); + if (oldOnEnd) { + oldOnEnd(); + } + } + }; + childGraphics.forEach((to, index) => { + const delay = + (animationConfig.delay ?? 0) + + animationConfig.individualDelay(index, validateFromGraphics.length, fromGraphics[index], to); + const animate = to.animate(Object.assign({}, animationConfig, { onEnd: onEachEnd })); + animate.wait(delay); + + animate.play( + new MorphingPath( + { + morphingData: morphingData[index], + saveOnEnd: true, + otherAttrs: otherAttrs[index] + }, + animationConfig.duration ?? DefaultMorphingAnimateConfig.duration, + animationConfig.easing ?? DefaultMorphingAnimateConfig.easing + ) + ); + }); + } else { + const oldOnEnd = animationConfig?.onEnd; + const config = animationConfig ? Object.assign({}, animationConfig) : {}; + + config.onEnd = () => { + toGraphic.setAttribute('visible', true, false, { type: AttributeUpdateType.ANIMATE_END }); + toGraphic.detachShadow(); + + if (oldOnEnd) { + oldOnEnd(); + } + }; + + const animate = toGraphic.animate(config); + + if (animationConfig?.delay) { + animate.wait(animationConfig.delay); + } + + animate.play( + new MultiToOneMorphingPath( + { morphingData, otherAttrs }, + animationConfig?.duration ?? DefaultMorphingAnimateConfig.duration, + animationConfig?.easing ?? DefaultMorphingAnimateConfig.easing + ) + ); + } +}; diff --git a/packages/vrender-core/src/animate/timeline.ts b/packages/vrender-core/src/animate/timeline.ts new file mode 100644 index 000000000..463e8b684 --- /dev/null +++ b/packages/vrender-core/src/animate/timeline.ts @@ -0,0 +1,102 @@ +import { AnimateStatus } from '../common/enums'; +import { Generator } from '../common/generator'; +import type { IAnimate, ITimeline } from '../interface'; + +// 管理一组动画 +export class DefaultTimeline implements ITimeline { + declare id: number; + protected declare animateHead: IAnimate | null; + protected declare animateTail: IAnimate | null; + protected declare ticker: any; + declare animateCount: number; + protected declare paused: boolean; + + constructor() { + this.id = Generator.GenAutoIncrementId(); + this.animateHead = null; + this.animateTail = null; + this.animateCount = 0; + this.paused = false; + } + + addAnimate(animate: IAnimate) { + if (!this.animateTail) { + this.animateHead = animate; + this.animateTail = animate; + } else { + this.animateTail.nextAnimate = animate; + animate.prevAnimate = this.animateTail; + this.animateTail = animate; + animate.nextAnimate = null; + } + this.animateCount++; + } + + pause() { + this.paused = true; + } + resume() { + this.paused = false; + } + + tick(delta: number) { + if (this.paused) { + return; + } + let animate = this.animateHead; + this.animateCount = 0; + while (animate) { + if (animate.status === AnimateStatus.END) { + this.removeAnimate(animate); + } else if (animate.status === AnimateStatus.RUNNING || animate.status === AnimateStatus.INITIAL) { + this.animateCount++; + animate.advance(delta); + } else if (animate.status === AnimateStatus.PAUSED) { + // 暂停 + this.animateCount++; + } + animate = animate.nextAnimate; + } + } + + clear() { + let animate = this.animateHead; + while (animate) { + animate.release(); + animate = animate.nextAnimate; + } + this.animateHead = null; + this.animateTail = null; + this.animateCount = 0; + } + + removeAnimate(animate: IAnimate, release: boolean = true) { + animate._onRemove && animate._onRemove.forEach(cb => cb()); + if (animate === this.animateHead) { + this.animateHead = animate.nextAnimate; + if (animate === this.animateTail) { + // 只有一个元素 + this.animateTail = null; + } else { + // 有多个元素 + this.animateHead.prevAnimate = null; + } + } else if (animate === this.animateTail) { + // 有多个元素 + this.animateTail = animate.prevAnimate; + this.animateTail.nextAnimate = null; + // animate.prevAnimate = null; + } else { + animate.prevAnimate.nextAnimate = animate.nextAnimate; + animate.nextAnimate.prevAnimate = animate.prevAnimate; + // animate不支持二次复用,不需要重置 + // animate.prevAnimate = null; + // animate.nextAnimate = null; + } + release && animate.release(); + + return; + } +} + +export const defaultTimeline = new DefaultTimeline(); diff --git a/packages/vrender-core/src/color-string/interpolate.ts b/packages/vrender-core/src/color-string/interpolate.ts index a1b2c5a87..4a009e293 100644 --- a/packages/vrender-core/src/color-string/interpolate.ts +++ b/packages/vrender-core/src/color-string/interpolate.ts @@ -186,6 +186,16 @@ export function interpolatePureColorArray( from[3] + (to[3] - from[3]) * ratio ]; } +export function interpolatePureColorArrayToStr( + from: [number, number, number, number], + to: [number, number, number, number], + ratio: number +): string { + // eslint-disable-next-line max-len + return `rgba(${from[0] + (to[0] - from[0]) * ratio},${from[1] + (to[1] - from[1]) * ratio},${ + from[2] + (to[2] - from[2]) * ratio + },${from[3] + (to[3] - from[3]) * ratio})`; +} const _fromColorRGB: [number, number, number, number] = [0, 0, 0, 0]; const _toColorRGB: [number, number, number, number] = [0, 0, 0, 0]; diff --git a/packages/vrender/__tests__/browser/src/pages/animate-next.ts b/packages/vrender/__tests__/browser/src/pages/animate-next.ts new file mode 100644 index 000000000..c388ed06d --- /dev/null +++ b/packages/vrender/__tests__/browser/src/pages/animate-next.ts @@ -0,0 +1,101 @@ +import { DefaultTicker, DefaultTimeline, Animate } from '@visactor/vrender-animate'; +import { + container, + createRect, + createStage, + createSymbol, + IGraphic, + vglobal, + createCircle, + createText +} from '@visactor/vrender'; +// container.load(roughModule); + +vglobal.setEnv('browser'); + +let stage: any; + +function addCase(name: string, container: HTMLElement, cb: (stage: any) => void) { + const button = document.createElement('button'); + button.innerText = name; + button.style.height = '26px'; + container.appendChild(button); + button.addEventListener('click', () => { + stage && stage.release(); + stage = createStage({ + canvas: 'main', + width: 900, + height: 600, + background: 'pink', + disableDirtyBounds: false, + canvasControled: false, + autoRender: true + }); + cb(stage); + }); +} + +export const page = () => { + const btnContainer = document.createElement('div'); + btnContainer.style.width = '80%'; + btnContainer.style.background = '#cecece'; + btnContainer.style.display = 'flex'; + btnContainer.style.flexDirection = 'row'; + btnContainer.style.gap = '3px'; + btnContainer.style.flexWrap = 'wrap'; + btnContainer.style.height = '60px'; + const canvas = document.getElementById('main'); + // 将btnContainer添加到canvas之前 + canvas.parentNode.insertBefore(btnContainer, canvas); + // ========== Performance Example ========== + addCase('Ticker Performance Example', btnContainer, stage => { + let count = 0; + for (let i = 0; i < 1000; i++) { + const rect = createRect({ + x: 100, + y: 100, + width: 100, + height: 100, + fill: 'red' + }); + stage.defaultLayer.add(rect); + const ticker = new DefaultTicker(stage); + const timeline = new DefaultTimeline(); + ticker.addTimeline(timeline); + const animate = new Animate(); + animate.bind(rect); + animate.to({ x: 200 }, 1000000, 'linear'); + timeline.addAnimate(animate); + + ticker.start(); + ticker.on('tick', () => { + count++; + }); + } + }); + addCase('Animate Performance Example', btnContainer, stage => { + const rect = createRect({ + x: 100, + y: 100, + width: 100, + height: 100, + fill: 'red' + }); + stage.defaultLayer.add(rect); + const ticker = new DefaultTicker(stage); + const timeline = new DefaultTimeline(); + ticker.addTimeline(timeline); + + for (let i = 0; i < 200000; i++) { + const animate = new Animate(); + animate.bind(rect); + animate.to({ x: 2000, fill: 'blue' }, 100000, 'linear'); + timeline.addAnimate(animate); + } + + ticker.start(); + ticker.on('tick', () => { + stage.render(); + }); + }); +}; diff --git a/packages/vrender/__tests__/browser/src/pages/animate.ts b/packages/vrender/__tests__/browser/src/pages/animate.ts index 8f7ce82cf..29649e2d7 100644 --- a/packages/vrender/__tests__/browser/src/pages/animate.ts +++ b/packages/vrender/__tests__/browser/src/pages/animate.ts @@ -21,7 +21,9 @@ import { Meteor, AttributeUpdateType, IStage, - Easing + Easing, + DefaultTicker, + DefaultTimeline } from '@visactor/vrender'; import { addShapesToStage, colorPools } from '../utils'; @@ -50,6 +52,27 @@ export const page = () => { const container = document.querySelector('#container')!; const br = document.createElement('br'); container.appendChild(br); + addCase('Animate Performance Example', container, stage => { + const rect = createRect({ + x: 100, + y: 100, + width: 100, + height: 100, + fill: 'red' + }); + const ticker = new DefaultTicker([]); + const timeline = new DefaultTimeline(); + ticker.addTimeline(timeline); + + console.time('animate'); + for (let i = 0; i < 2; i++) { + const animate = rect.animate().to({ fill: 'green' }, 100000, 'linear'); + timeline.addAnimate(animate); + } + + ticker.start(); + console.timeEnd('animate'); + }); addCase('text', container, stage => { stage.background = 'black'; const g = createGroup({}); diff --git a/packages/vrender/__tests__/browser/src/pages/index.ts b/packages/vrender/__tests__/browser/src/pages/index.ts index fc3029595..f3d155024 100644 --- a/packages/vrender/__tests__/browser/src/pages/index.ts +++ b/packages/vrender/__tests__/browser/src/pages/index.ts @@ -19,6 +19,10 @@ export const pages = [ name: '性能测试2', path: 'performance' }, + { + name: 'animate-next', + path: 'animate-next' + }, { name: '内存', path: 'memory' diff --git a/packages/vrender/__tests__/browser/vite.config.ts b/packages/vrender/__tests__/browser/vite.config.ts index 2d7ff256a..6bc1783fa 100644 --- a/packages/vrender/__tests__/browser/vite.config.ts +++ b/packages/vrender/__tests__/browser/vite.config.ts @@ -13,6 +13,7 @@ export default defineConfig({ '@visactor/vrender': path.resolve(__dirname, '../../../vrender/src/index.ts'), '@visactor/vrender-core': path.resolve(__dirname, '../../../vrender-core/src/index.ts'), '@visactor/vrender-kits': path.resolve(__dirname, '../../../vrender-kits/src/index.ts'), + '@visactor/vrender-animate': path.resolve(__dirname, '../../../vrender-animate/src/index.ts'), '@visactor/vrender-components': path.resolve(__dirname, '../../../vrender-components/src/index.ts'), util: 'rollup-plugin-node-polyfills/polyfills/util' } From bdfded419b7e6a1ea66b28d33004208b9752fdc8 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 14 Mar 2025 15:58:57 +0800 Subject: [PATCH 032/179] feat: ticker support fps config --- .../vrender-animate/src/ticker/default-ticker.ts | 15 ++++++++++++++- .../__tests__/browser/src/pages/animate-next.ts | 7 +++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/vrender-animate/src/ticker/default-ticker.ts b/packages/vrender-animate/src/ticker/default-ticker.ts index 095ba59cb..db38707cf 100644 --- a/packages/vrender-animate/src/ticker/default-ticker.ts +++ b/packages/vrender-animate/src/ticker/default-ticker.ts @@ -36,6 +36,7 @@ export class DefaultTicker extends EventEmitter implements ITicker { protected tickerHandler: ITickHandler; protected status: STATUS; protected lastFrameTime: number = -1; + protected lastExecutionTime: number = -1; // Track the last time we actually executed a frame protected tickCounts: number = 0; protected stage: IStage; timelines: ITimeline[] = []; @@ -45,6 +46,7 @@ export class DefaultTicker extends EventEmitter implements ITicker { super(); this.init(); this.lastFrameTime = -1; + this.lastExecutionTime = -1; this.tickCounts = 0; this.stage = stage; this.autoStop = true; @@ -180,6 +182,7 @@ export class DefaultTicker extends EventEmitter implements ITicker { this.status = STATUS.INITIAL; this.setupTickHandler(); this.lastFrameTime = -1; + this.lastExecutionTime = -1; } /** @@ -199,6 +202,7 @@ export class DefaultTicker extends EventEmitter implements ITicker { this.timelines = []; this.tickerHandler?.release(); this.tickerHandler = null; + this.lastExecutionTime = -1; } protected handleTick = (handler: ITickHandler, params?: { once?: boolean }): void => { @@ -210,7 +214,16 @@ export class DefaultTicker extends EventEmitter implements ITicker { return; } - this._handlerTick(); + const currentTime = handler.getTime(); + + // Check if enough time has passed since last execution based on the interval (FPS limit) + const timeFromLastExecution = this.lastExecutionTime < 0 ? this.interval : currentTime - this.lastExecutionTime; + + // Only execute the frame if enough time has passed according to our interval/FPS setting + if (timeFromLastExecution >= this.interval) { + this._handlerTick(); + this.lastExecutionTime = currentTime; + } if (!once) { handler.tick(this.interval, this.handleTick); diff --git a/packages/vrender/__tests__/browser/src/pages/animate-next.ts b/packages/vrender/__tests__/browser/src/pages/animate-next.ts index c388ed06d..bbcbb6836 100644 --- a/packages/vrender/__tests__/browser/src/pages/animate-next.ts +++ b/packages/vrender/__tests__/browser/src/pages/animate-next.ts @@ -83,6 +83,7 @@ export const page = () => { }); stage.defaultLayer.add(rect); const ticker = new DefaultTicker(stage); + ticker.setFPS(10); const timeline = new DefaultTimeline(); ticker.addTimeline(timeline); @@ -97,5 +98,11 @@ export const page = () => { ticker.on('tick', () => { stage.render(); }); + function run() { + requestAnimationFrame(() => { + run(); + }); + } + run(); }); }; From 439e64627af6ef0e02796d8414572bdc4d778374 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 14 Mar 2025 18:40:38 +0800 Subject: [PATCH 033/179] feat: access to vrender-core --- packages/vrender-animate/src/animate.ts | 181 ++- packages/vrender-animate/src/index.ts | 1 + .../vrender-animate/src/interpolate/store.ts | 2 +- .../vrender-animate/src/intreface/animate.ts | 8 +- .../vrender-animate/src/intreface/timeline.ts | 1 + packages/vrender-animate/src/register.ts | 22 + packages/vrender-animate/src/step.ts | 1 - packages/vrender-animate/src/timeline.ts | 5 + .../src/animate/Ticker/default-ticker.ts | 246 --- .../vrender-core/src/animate/Ticker/index.ts | 5 - .../animate/Ticker/manual-ticker-handler.ts | 35 - .../src/animate/Ticker/manual-ticker.ts | 54 - .../src/animate/Ticker/raf-tick-handler.ts | 30 - .../animate/Ticker/timeout-tick-handler.ts | 29 - .../vrender-core/src/animate/Ticker/type.ts | 7 - packages/vrender-core/src/animate/animate.ts | 1307 ---------------- .../src/animate/custom-animate.ts | 1364 ----------------- .../src/animate/default-ticker.ts | 7 - .../vrender-core/src/animate/easing-func.ts | 12 - packages/vrender-core/src/animate/easing.ts | 273 ---- .../vrender-core/src/animate/group-fade.ts | 70 - packages/vrender-core/src/animate/index.ts | 8 - packages/vrender-core/src/animate/morphing.ts | 680 -------- packages/vrender-core/src/animate/timeline.ts | 102 -- packages/vrender-core/src/core/stage.ts | 44 +- packages/vrender-core/src/graphic/graphic.ts | 15 +- packages/vrender-core/src/index.ts | 3 - .../vrender-core/src/interface/animate.ts | 8 + .../builtin-plugin/richtext-edit-plugin.ts | 13 +- .../browser/src/pages/animate-next.ts | 47 +- 30 files changed, 230 insertions(+), 4350 deletions(-) create mode 100644 packages/vrender-animate/src/register.ts delete mode 100644 packages/vrender-core/src/animate/Ticker/default-ticker.ts delete mode 100644 packages/vrender-core/src/animate/Ticker/index.ts delete mode 100644 packages/vrender-core/src/animate/Ticker/manual-ticker-handler.ts delete mode 100644 packages/vrender-core/src/animate/Ticker/manual-ticker.ts delete mode 100644 packages/vrender-core/src/animate/Ticker/raf-tick-handler.ts delete mode 100644 packages/vrender-core/src/animate/Ticker/timeout-tick-handler.ts delete mode 100644 packages/vrender-core/src/animate/Ticker/type.ts delete mode 100644 packages/vrender-core/src/animate/animate.ts delete mode 100644 packages/vrender-core/src/animate/custom-animate.ts delete mode 100644 packages/vrender-core/src/animate/default-ticker.ts delete mode 100644 packages/vrender-core/src/animate/easing-func.ts delete mode 100644 packages/vrender-core/src/animate/easing.ts delete mode 100644 packages/vrender-core/src/animate/group-fade.ts delete mode 100644 packages/vrender-core/src/animate/index.ts delete mode 100644 packages/vrender-core/src/animate/morphing.ts delete mode 100644 packages/vrender-core/src/animate/timeline.ts diff --git a/packages/vrender-animate/src/animate.ts b/packages/vrender-animate/src/animate.ts index d99f4e4aa..076433882 100644 --- a/packages/vrender-animate/src/animate.ts +++ b/packages/vrender-animate/src/animate.ts @@ -3,45 +3,67 @@ import type { EasingType } from './intreface/easing'; import { AnimateStatus, AnimateStepType } from './intreface/type'; import { Step } from './step'; import type { ITimeline } from './intreface/timeline'; -import type { ICustomAnimate, IGraphic } from '@visactor/vrender-core'; - -let uniqueId = 0; +import { Generator, type ICustomAnimate, type IGraphic } from '@visactor/vrender-core'; +import { defaultTimeline } from './timeline'; export class Animate implements IAnimate { readonly id: string | number; - status: AnimateStatus = AnimateStatus.INITIAL; + status: AnimateStatus; target: IGraphic; // 回调函数列表 - _onStart?: (() => void)[] = []; - _onFrame?: ((step: IStep, ratio: number) => void)[] = []; - _onEnd?: (() => void)[] = []; - _onRemove?: (() => void)[] = []; + _onStart?: (() => void)[]; + _onFrame?: ((step: IStep, ratio: number) => void)[]; + _onEnd?: (() => void)[]; + _onRemove?: (() => void)[]; // 时间控制 private _timeline: ITimeline; - private _startTime: number = 0; - private _duration: number = 0; - private _totalDuration: number = 0; + private _startTime: number; + private _duration: number; + private _totalDuration: number; // 动画控制 - private _reversed: boolean = false; - private _loopCount: number = 0; - private _bounce: boolean = false; + // private _reversed: boolean; + private _loopCount: number; + private _currentLoop: number; + private _bounce: boolean; // 链表头节点和尾节点 - private _firstStep: IStep | null = null; - private _lastStep: IStep | null = null; + private _firstStep: IStep | null; + private _lastStep: IStep | null; // 初始属性和屏蔽的属性 - private _startProps: Record = {}; - private _endProps: Record = {}; - private _preventAttrs: Set = new Set(); - - protected currentTime: number = 0; - - constructor(id: string | number = uniqueId++) { + private _startProps: Record; + private _endProps: Record; + private _preventAttrs: Set; + + protected currentTime: number; + slience?: boolean; + + constructor( + id: string | number = Generator.GenAutoIncrementId(), + timeline: ITimeline = defaultTimeline, + slience?: boolean + ) { this.id = id; + this.status = AnimateStatus.INITIAL; + this._timeline = timeline; + timeline.addAnimate(this); + this.slience = slience; + this._startTime = 0; + this._duration = 0; + this._totalDuration = 0; + // this._reversed = false; + this._loopCount = 0; + this._currentLoop = 0; + this._bounce = false; + this._firstStep = null; + this._lastStep = null; + this._startProps = {}; + this._endProps = {}; + this._preventAttrs = new Set(); + this.currentTime = 0; } /** @@ -105,11 +127,23 @@ export class Animate implements IAnimate { this._lastStep = step; } - // 保存最终属性 + /* 预设置step的属性,基于性能考虑,实现比较复杂 */ + // step.propKeys为真实的props属性的key step.propKeys = step.propKeys || Object.keys(step.props); + // step.props为包含前序step的props的最终props,用于跳帧等场景,可以直接设置 + Object.keys(this._endProps).forEach(key => { + step.props[key] = step.props[key] ?? this._endProps[key]; + }); + // 将最终的props设置到step.props中 step.propKeys.forEach(key => { this._endProps[key] = step.props[key]; }); + // 给step的props的原型链上绑定Animate的_startProps + // 下一个step在查找上一个step.props(也就是找到它的fromProps)的时候,就能拿到初始的props了 + // 比如: + // rect.animate().to({ x: 100 }, 1000, 'linear').to({ y: 100 }, 1000, 'linear'); + // 在第二个step查找fromProps的时候,就能拿到第一个step的endProps中的y值(在原型链上) + Object.setPrototypeOf(step.props, this._startProps); this.updateDuration(); @@ -168,7 +202,7 @@ export class Animate implements IAnimate { } this._onStart.push(cb); } else { - this._onStart.forEach(cb => cb()); + this._onStart?.forEach(cb => cb()); // 设置开始属性,Animate不会重复执行start所以不需要判断firstStart Object.keys(this._endProps).forEach(key => { this._startProps[key] = this.target.getComputedAttribute(key); @@ -186,7 +220,7 @@ export class Animate implements IAnimate { } this._onEnd.push(cb); } else { - this._onEnd.forEach(cb => cb()); + this._onEnd?.forEach(cb => cb()); } } @@ -202,6 +236,20 @@ export class Animate implements IAnimate { } } + /** + * 注册移除回调 + */ + onRemove(cb?: () => void): void { + if (cb) { + if (!this._onRemove) { + this._onRemove = []; + } + this._onRemove.push(cb); + } else { + this._onRemove?.forEach(cb => cb()); + } + } + /** * 屏蔽单个属性 */ @@ -419,19 +467,20 @@ export class Animate implements IAnimate { return this; } - /** - * 设置动画是否反转 - */ - reversed(r: boolean): this { - this._reversed = r; - return this; - } + // /** + // * 设置动画是否反转 + // */ + // reversed(r: boolean): this { + // this._reversed = r; + // return this; + // } /** * 设置动画循环次数 */ loop(n: number): this { this._loopCount = n; + this.updateDuration(); return this; } @@ -462,19 +511,24 @@ export class Animate implements IAnimate { this.status = AnimateStatus.RUNNING; - const deltaFotStep = nextTime - this._startTime; - // 如果是第一次运行,触发开始回调 if (this.currentTime <= this._startTime) { this.onStart(); } this.currentTime = nextTime; - let cycleTime = deltaFotStep % this._duration; + let cycleTime = nextTime - this._startTime; + let newLoop = false; + if (this._loopCount > 0) { + cycleTime = cycleTime % this._duration; + const currentLoop = Math.floor(nextTime / this._duration); + newLoop = currentLoop > this._currentLoop; + this._currentLoop = currentLoop; + } // 如果是反转动画,需要反转周期内的时间 - if (this._reversed) { - cycleTime = this._duration - cycleTime; + if (newLoop) { + this.target.setAttributes(this._startProps); } // 选择起始步骤和遍历方向 @@ -483,41 +537,20 @@ export class Animate implements IAnimate { if (this._lastStep === this._firstStep) { targetStep = this._firstStep; } else { - let currentStep: IStep | null = null; - if (this._reversed) { - // 反转时从最后一个步骤开始查找 - currentStep = this._lastStep; - - // 从后向前寻找当前时间所在的step - while (currentStep) { - const stepEndTime = currentStep.getStartTime() + currentStep.getDuration(); - - // 反转时,我们需要从动画结束时间向开始时间查找 - if (cycleTime <= stepEndTime && cycleTime > currentStep.getStartTime()) { - targetStep = currentStep; - break; - } - - currentStep = currentStep.prev; - } - } else { - // 正常顺序从第一个步骤开始查找 - currentStep = this._firstStep; - - // 从前向后寻找当前时间所在的step - while (currentStep) { - const stepStartTime = currentStep.getStartTime(); - const stepDuration = currentStep.getDuration(); - const stepEndTime = stepStartTime + stepDuration; - - // 找到当前周期时间所在的step - if (cycleTime >= stepStartTime && cycleTime < stepEndTime) { - targetStep = currentStep; - break; - } - - currentStep = currentStep.next; + let currentStep: IStep = this._firstStep; + // 从前向后寻找当前时间所在的step + while (currentStep) { + const stepStartTime = currentStep.getStartTime(); + const stepDuration = currentStep.getDuration(); + const stepEndTime = stepStartTime + stepDuration; + + // 找到当前周期时间所在的step + if (cycleTime >= stepStartTime && cycleTime < stepEndTime) { + targetStep = currentStep; + break; } + + currentStep = currentStep.next; } } @@ -532,9 +565,7 @@ export class Animate implements IAnimate { const stepStartTime = targetStep.getStartTime(); const stepDuration = targetStep.getDuration(); - const ratio = this._reversed - ? (stepStartTime + stepDuration - cycleTime) / stepDuration - : (cycleTime - stepStartTime) / stepDuration; + const ratio = (cycleTime - stepStartTime) / stepDuration; // // 限制ratio在0-1之间 // ratio = Math.max(0, Math.min(1, ratio)); diff --git a/packages/vrender-animate/src/index.ts b/packages/vrender-animate/src/index.ts index bf0043130..a19f5bb64 100644 --- a/packages/vrender-animate/src/index.ts +++ b/packages/vrender-animate/src/index.ts @@ -13,3 +13,4 @@ export { Step } from './step'; // 导出工具函数 export * from './utils/easing-func'; +export { registerAnimate } from './register'; diff --git a/packages/vrender-animate/src/interpolate/store.ts b/packages/vrender-animate/src/interpolate/store.ts index fc9c87fef..43dd968fc 100644 --- a/packages/vrender-animate/src/interpolate/store.ts +++ b/packages/vrender-animate/src/interpolate/store.ts @@ -1,5 +1,5 @@ import type { IGraphic } from '@visactor/vrender-core'; -import { interpolateColor, interpolatePureColorArray, interpolatePureColorArrayToStr } from '@visactor/vrender-core'; +import { interpolateColor, interpolatePureColorArrayToStr } from '@visactor/vrender-core'; import { interpolateNumber } from './number'; import type { IStep } from '../intreface/animate'; diff --git a/packages/vrender-animate/src/intreface/animate.ts b/packages/vrender-animate/src/intreface/animate.ts index ff2ebf96a..dcfac8007 100644 --- a/packages/vrender-animate/src/intreface/animate.ts +++ b/packages/vrender-animate/src/intreface/animate.ts @@ -15,6 +15,7 @@ export interface IStep { // 解析后的属性(用于性能优化,避免每次tick都解析) fromParsedProps?: Record; toParsedProps?: Record; + fromProps?: Record; // 解析后的属性列表(用于性能优化,避免每次tick都解析) propKeys?: string[]; // 缓动函数 @@ -79,9 +80,10 @@ export interface IAnimate { from: (props: Record, duration: number, easing: EasingType) => this; pause: () => void; resume: () => void; - onStart: (cb: () => void) => void; - onEnd: (cb: () => void) => void; + onStart: (cb?: () => void) => void; + onEnd: (cb?: () => void) => void; onFrame: (cb: (step: IStep, ratio: number) => void) => void; + onRemove: (cb?: () => void) => void; // 屏蔽属性 preventAttr: (key: string) => void; // 屏蔽属性 @@ -124,7 +126,7 @@ export interface IAnimate { parallel: (animate: IAnimate) => this; // 反转动画 - reversed: (r: boolean) => IAnimate; + // reversed: (r: boolean) => IAnimate; // 循环动画 loop: (n: number) => IAnimate; // 反弹动画 diff --git a/packages/vrender-animate/src/intreface/timeline.ts b/packages/vrender-animate/src/intreface/timeline.ts index 14d628beb..2c4e1b391 100644 --- a/packages/vrender-animate/src/intreface/timeline.ts +++ b/packages/vrender-animate/src/intreface/timeline.ts @@ -2,6 +2,7 @@ import type { IAnimate } from './animate'; export interface ITimeline { id: number; + isGlobal?: boolean; // 包含的动画数量(animate数组的数量),包含所有动画 animateCount: number; // 添加动画 diff --git a/packages/vrender-animate/src/register.ts b/packages/vrender-animate/src/register.ts new file mode 100644 index 000000000..ec35eef44 --- /dev/null +++ b/packages/vrender-animate/src/register.ts @@ -0,0 +1,22 @@ +import { Graphic } from '@visactor/vrender-core'; +import { Animate } from './animate'; +import { defaultTimeline, DefaultTimeline } from './timeline'; +import { DefaultTicker } from './ticker/default-ticker'; + +export function registerAnimate() { + if (!(Graphic as any).Animate) { + (Graphic as any).Animate = Animate; + } + if (!(Graphic as any).Timeline) { + (Graphic as any).Timeline = DefaultTimeline; + } + if (!(Graphic as any).Ticker) { + (Graphic as any).Ticker = DefaultTicker; + } + if (!(Graphic as any).defaultTicker) { + (Graphic as any).defaultTicker = DefaultTicker; + } + if (!(Graphic as any).defaultTimeline) { + (Graphic as any).defaultTimeline = defaultTimeline; + } +} diff --git a/packages/vrender-animate/src/step.ts b/packages/vrender-animate/src/step.ts index 7dd556608..2889a92bc 100644 --- a/packages/vrender-animate/src/step.ts +++ b/packages/vrender-animate/src/step.ts @@ -108,7 +108,6 @@ export class Step implements IStep { return; } - this.propKeys = Object.keys(this.props); const funcs: ((key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => void)[] = []; this.propKeys.forEach(key => { // 普通颜色特殊处理,需要提前解析成number[] diff --git a/packages/vrender-animate/src/timeline.ts b/packages/vrender-animate/src/timeline.ts index bf088dc29..8384e1c7b 100644 --- a/packages/vrender-animate/src/timeline.ts +++ b/packages/vrender-animate/src/timeline.ts @@ -19,6 +19,8 @@ export class DefaultTimeline implements ITimeline { // (_endAnimatePtr, animates.length) 表示已经结束的动画 protected _endAnimatePtr: number = -1; + declare isGlobal?: boolean; + get animateCount() { return this.animates.length; } @@ -146,3 +148,6 @@ export class DefaultTimeline implements ITimeline { this._currentTime = time; } } + +export const defaultTimeline = new DefaultTimeline(); +defaultTimeline.isGlobal = true; diff --git a/packages/vrender-core/src/animate/Ticker/default-ticker.ts b/packages/vrender-core/src/animate/Ticker/default-ticker.ts deleted file mode 100644 index 4bf64ece7..000000000 --- a/packages/vrender-core/src/animate/Ticker/default-ticker.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { EventEmitter, Logger } from '@visactor/vutils'; -import type { ITickHandler, ITickerHandlerStatic, ITimeline, ITicker } from '../../interface'; -import { application } from '../../application'; -import type { TickerMode } from './type'; -import { STATUS } from './type'; -import { RAFTickHandler } from './raf-tick-handler'; -import { TimeOutTickHandler } from './timeout-tick-handler'; - -export class DefaultTicker extends EventEmitter implements ITicker { - protected interval: number; - protected tickerHandler: ITickHandler; - protected _mode: TickerMode; - protected status: STATUS; - protected lastFrameTime: number; - protected tickCounts: number; - protected timelines: ITimeline[]; - autoStop: boolean; - - set mode(m: TickerMode) { - if (this._mode === m) { - return; - } - this._mode = m; - this.setupTickHandler(); - } - get mode(): TickerMode { - return this._mode; - } - - constructor(timelines: ITimeline[] = []) { - super(); - this.init(); - this.lastFrameTime = -1; - this.tickCounts = 0; - this.timelines = timelines; - this.autoStop = true; - } - - init() { - this.interval = NaN; - this.status = STATUS.INITIAL; - application.global.hooks.onSetEnv.tap('default-ticker', () => { - this.initHandler(); - }); - if (application.global.env) { - this.initHandler(); - } - } - - addTimeline(timeline: ITimeline) { - this.timelines.push(timeline); - } - remTimeline(timeline: ITimeline) { - this.timelines = this.timelines.filter(t => t !== timeline); - } - getTimelines(): ITimeline[] { - return this.timelines; - } - - protected initHandler(): ITickHandler | null { - if (this._mode) { - return null; - } - const ticks: { mode: TickerMode; cons: ITickerHandlerStatic }[] = [ - { mode: 'raf', cons: RAFTickHandler }, - { mode: 'timeout', cons: TimeOutTickHandler } - ]; - for (let i = 0; i < ticks.length; i++) { - if (ticks[i].cons.Avaliable()) { - this.mode = ticks[i].mode; - break; - } - } - return null; - } - - /** - * 设置tickHandler - * @returns 返回true表示设置成功,false表示设置失败 - */ - protected setupTickHandler(): boolean { - let handler: ITickHandler; - // 创建下一个tickHandler - switch (this._mode) { - case 'raf': - handler = new RAFTickHandler(); - break; - case 'timeout': - handler = new TimeOutTickHandler(); - break; - // case 'manual': - // handler = new ManualTickHandler(); - // break; - default: - Logger.getInstance().warn('非法的计时器模式'); - handler = new RAFTickHandler(); - break; - } - if (!handler.avaliable()) { - return false; - } - - // 销毁上一个tickerHandler - if (this.tickerHandler) { - this.tickerHandler.release(); - } - this.tickerHandler = handler; - return true; - } - - setInterval(interval: number) { - this.interval = interval; - } - getInterval(): number { - return this.interval; - } - - setFPS(fps: number): void { - this.setInterval(1000 / fps); - } - getFPS(): number { - return 1000 / this.interval; - } - tick(interval: number): void { - this.tickerHandler.tick(interval, (handler: ITickHandler) => { - this.handleTick(handler, { once: true }); - }); - } - tickTo(t: number): void { - if (!this.tickerHandler.tickTo) { - return; - } - this.tickerHandler.tickTo(t, (handler: ITickHandler) => { - this.handleTick(handler, { once: true }); - }); - } - pause(): boolean { - if (this.status === STATUS.INITIAL) { - return false; - } - this.status = STATUS.PAUSE; - return true; - } - resume(): boolean { - if (this.status === STATUS.INITIAL) { - return false; - } - this.status = STATUS.RUNNING; - return true; - } - - ifCanStop(): boolean { - if (this.autoStop) { - if (!this.timelines.length) { - return true; - } - if (this.timelines.reduce((a, b) => a + b.animateCount, 0) === 0) { - return true; - } - } - return false; - } - - start(force: boolean = false): boolean { - if (this.status === STATUS.RUNNING) { - return false; - } - if (!this.tickerHandler) { - return false; - } - // 如果不需要start,那就不start - if (!force) { - // 暂停状态不执行 - if (this.status === STATUS.PAUSE) { - return false; - } - if (!this.timelines.length) { - return false; - } - if (this.timelines.reduce((a, b) => a + b.animateCount, 0) === 0) { - return false; - } - } - this.status = STATUS.RUNNING; - this.tickerHandler.tick(0, this.handleTick); - return true; - } - stop(): void { - // 重新设置tickHandler - this.status = STATUS.INITIAL; - this.setupTickHandler(); - this.lastFrameTime = -1; - } - - protected handleTick = (handler: ITickHandler, params?: { once?: boolean }) => { - const { once = false } = params ?? {}; - // 尝试停止 - if (this.ifCanStop()) { - this.stop(); - return; - } - this._handlerTick(); - if (!once) { - handler.tick(this.interval, this.handleTick); - } - }; - - protected _handlerTick = () => { - // 具体执行函数 - const tickerHandler = this.tickerHandler; - const time = tickerHandler.getTime(); - // 上一帧经过的时间 - let delta = 0; - if (this.lastFrameTime >= 0) { - delta = time - this.lastFrameTime; - } - this.lastFrameTime = time; - - if (this.status !== STATUS.RUNNING) { - return; - } - this.tickCounts++; - - this.timelines.forEach(t => { - t.tick(delta); - }); - this.emit('tick'); - }; - - release(): void { - this.stop(); - this.timelines = []; - this.tickerHandler.release(); - this.emit('afterTick'); - } - - /** - * 同步tick状态,需要手动触发tick执行,保证属性为走完动画的属性 - * 【注】grammar会设置属性到最终值,然后调用render,这时候需要VRender手动触发tick,保证属性为走完动画的属性,而不是Grammar设置上的属性 - */ - trySyncTickStatus() { - if (this.status === STATUS.RUNNING) { - this._handlerTick(); - } - } -} diff --git a/packages/vrender-core/src/animate/Ticker/index.ts b/packages/vrender-core/src/animate/Ticker/index.ts deleted file mode 100644 index d90a0239e..000000000 --- a/packages/vrender-core/src/animate/Ticker/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './default-ticker'; -export * from './manual-ticker'; -export * from './manual-ticker-handler'; -export * from './raf-tick-handler'; -export * from './timeout-tick-handler'; diff --git a/packages/vrender-core/src/animate/Ticker/manual-ticker-handler.ts b/packages/vrender-core/src/animate/Ticker/manual-ticker-handler.ts deleted file mode 100644 index d4b815c7b..000000000 --- a/packages/vrender-core/src/animate/Ticker/manual-ticker-handler.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { ITickHandler } from '../../interface/animate'; - -export class ManualTickHandler implements ITickHandler { - protected timerId: number; - protected time: number = 0; - - static Avaliable(): boolean { - return true; - } - - avaliable(): boolean { - return ManualTickHandler.Avaliable(); - } - - tick(interval: number, cb: (handler: ITickHandler, params?: { once: boolean }) => void): void { - this.time = Math.max(0, interval + this.time); - cb(this, { once: true }); - } - - tickTo(t: number, cb: (handler: ITickHandler, params?: { once: boolean }) => void): void { - this.time = Math.max(0, t); - cb(this, { once: true }); - } - - release() { - if (this.timerId > 0) { - // clearTimeout(this.timerId); - this.timerId = -1; - } - } - - getTime() { - return this.time; - } -} diff --git a/packages/vrender-core/src/animate/Ticker/manual-ticker.ts b/packages/vrender-core/src/animate/Ticker/manual-ticker.ts deleted file mode 100644 index 1e7945524..000000000 --- a/packages/vrender-core/src/animate/Ticker/manual-ticker.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { ITicker, ITickHandler, ITimeline } from '../../interface/animate'; -import { DefaultTicker } from './default-ticker'; -import { ManualTickHandler } from './manual-ticker-handler'; -import type { STATUS, TickerMode } from './type'; - -export class ManualTicker extends DefaultTicker implements ITicker { - protected declare interval: number; - protected declare tickerHandler: ITickHandler; - protected declare _mode: TickerMode; - protected declare status: STATUS; - protected declare lastFrameTime: number; - protected declare tickCounts: number; - protected declare timelines: ITimeline[]; - declare autoStop: boolean; - - set mode(m: TickerMode) { - m = 'manual'; - this.setupTickHandler(); - } - get mode(): TickerMode { - return this._mode; - } - - protected initHandler(): ITickHandler | null { - this.mode = 'manual'; - return null; - } - - /** - * 设置tickHandler - * @returns 返回true表示设置成功,false表示设置失败 - */ - protected setupTickHandler(): boolean { - const handler: ITickHandler = new ManualTickHandler(); - this._mode = 'manual'; - - // 销毁上一个tickerHandler - if (this.tickerHandler) { - this.tickerHandler.release(); - } - this.tickerHandler = handler; - return true; - } - - tickAt(time: number) { - this.tickerHandler.tick(time - Math.max(this.lastFrameTime, 0), (handler: ITickHandler) => { - this.handleTick(handler, { once: true }); - }); - } - - ifCanStop(): boolean { - return false; - } -} diff --git a/packages/vrender-core/src/animate/Ticker/raf-tick-handler.ts b/packages/vrender-core/src/animate/Ticker/raf-tick-handler.ts deleted file mode 100644 index 2f39214b1..000000000 --- a/packages/vrender-core/src/animate/Ticker/raf-tick-handler.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { application } from '../../application'; -import type { ITickHandler } from '../../interface/animate'; - -export class RAFTickHandler implements ITickHandler { - protected released: boolean; - - static Avaliable(): boolean { - return !!application.global.getRequestAnimationFrame(); - } - avaliable(): boolean { - return RAFTickHandler.Avaliable(); - } - - tick(interval: number, cb: (handler: ITickHandler) => void): void { - const raf = application.global.getRequestAnimationFrame(); - raf(() => { - if (this.released) { - return; - } - cb(this); - }); - } - - release() { - this.released = true; - } - getTime() { - return Date.now(); - } -} diff --git a/packages/vrender-core/src/animate/Ticker/timeout-tick-handler.ts b/packages/vrender-core/src/animate/Ticker/timeout-tick-handler.ts deleted file mode 100644 index c6c13c183..000000000 --- a/packages/vrender-core/src/animate/Ticker/timeout-tick-handler.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { ITickHandler } from '../../interface/animate'; - -export class TimeOutTickHandler implements ITickHandler { - protected timerId: number; - - static Avaliable(): boolean { - return true; - } - - avaliable(): boolean { - return TimeOutTickHandler.Avaliable(); - } - - tick(interval: number, cb: (handler: ITickHandler) => void): void { - this.timerId = setTimeout(() => { - cb(this); - }, interval) as unknown as number; - } - - release() { - if (this.timerId > 0) { - clearTimeout(this.timerId); - this.timerId = -1; - } - } - getTime() { - return Date.now(); - } -} diff --git a/packages/vrender-core/src/animate/Ticker/type.ts b/packages/vrender-core/src/animate/Ticker/type.ts deleted file mode 100644 index 97afaea54..000000000 --- a/packages/vrender-core/src/animate/Ticker/type.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type TickerMode = 'raf' | 'timeout' | 'manual'; - -export enum STATUS { - INITIAL = 0, // initial表示初始状态 - RUNNING = 1, // running表示正在执行 - PAUSE = 2 // PULSE表示tick还是继续,只是不执行函数了 -} diff --git a/packages/vrender-core/src/animate/animate.ts b/packages/vrender-core/src/animate/animate.ts deleted file mode 100644 index 8e32b97f4..000000000 --- a/packages/vrender-core/src/animate/animate.ts +++ /dev/null @@ -1,1307 +0,0 @@ -import type { - EasingType, - EasingTypeFunc, - IAnimate, - IAnimateStepType, - IAnimateTarget, - ICustomAnimate, - IGraphic, - IStep, - IStepConfig, - ISubAnimate, - ITimeline -} from '../interface'; -import { AnimateMode, AnimateStatus, AnimateStepType, AttributeUpdateType } from '../common/enums'; -import { Easing } from './easing'; -import { Logger, max } from '@visactor/vutils'; -import { defaultTimeline } from './timeline'; -import { Generator } from '../common/generator'; - -// 参考TweenJS -// https://github.com/CreateJS/TweenJS/tree/master/src/tweenjs -/** - * The MIT License (MIT) - - Copyright (c) 2014 gskinner.com, inc. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ -export abstract class ACustomAnimate implements ICustomAnimate { - declare from: T; - declare to: T; - declare duration: number; - declare easing: EasingType; - declare params: any; - declare target: IAnimateTarget; - declare updateCount: number; - declare subAnimate: ISubAnimate; - declare step?: IStep; - declare mode?: AnimateMode; - - // 用于判断是否一致 - declare _endProps?: any; - declare _mergedEndProps?: any; - - constructor(from: T, to: T, duration: number, easing: EasingType, params?: any) { - this.from = from; - this.to = to; - this.duration = duration; - this.easing = easing; - this.params = params; - this.updateCount = 0; - } - - bind(target: IAnimateTarget, subAni: ISubAnimate) { - this.target = target; - this.subAnimate = subAni; - this.onBind(); - } - - // 在第一次调用的时候触发 - onBind() { - return; - } - - // 第一次执行的时候调用 - onFirstRun() { - return; - } - - // 开始执行的时候调用(如果有循环,那每个周期都会调用) - onStart() { - return; - } - - // 结束执行的时候调用(如果有循环,那每个周期都会调用) - onEnd() { - return; - } - - getEndProps(): Record | void { - return this.to; - } - - getFromProps(): Record | void { - return this.from; - } - - getMergedEndProps(): Record | void { - const thisEndProps = this.getEndProps(); - if (thisEndProps) { - if (this._endProps === thisEndProps) { - return this._mergedEndProps; - } - this._endProps = thisEndProps; - this._mergedEndProps = Object.assign({}, this.step.prev.getLastProps() ?? {}, thisEndProps); - return; - } - return this.step.prev ? this.step.prev.getLastProps() : thisEndProps; - } - - // abstract getFromValue(key: string): any; - // abstract getToValue(key: string): any; - - abstract onUpdate(end: boolean, ratio: number, out: Record): void; - - update(end: boolean, ratio: number, out: Record): void { - if (this.updateCount === 0) { - this.onFirstRun(); - // out添加之前的props - const props = this.step.getLastProps(); - Object.keys(props).forEach(k => { - if (this.subAnimate.animate.validAttr(k)) { - out[k] = props[k]; - } - }); - } - this.updateCount += 1; - this.onUpdate(end, ratio, out); - if (end) { - this.onEnd(); - } - } -} - -export class CbAnimate extends ACustomAnimate { - cb: () => void; - - constructor(cb: () => void) { - super(null, null, 0, 'linear'); - this.cb = cb; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - return; - } - - onStart(): void { - this.cb(); - } -} - -type InterpolateFunc = ( - key: string, - ratio: number, - from: any, - to: any, - target: IAnimateTarget, - out: Record -) => boolean; - -export class Animate implements IAnimate { - static mode: AnimateMode = AnimateMode.NORMAL; - declare target: IAnimateTarget; - declare timeline: ITimeline; - declare nextAnimate?: IAnimate; - declare prevAnimate?: IAnimate; - // 当前Animate的状态,正常,暂停,结束 - declare status: AnimateStatus; - declare readonly id: string | number; - // 开始时间 - protected declare _startTime: number; - protected declare _duringTime: number; - declare subAnimates: SubAnimate[]; - declare tailAnimate: SubAnimate; - - // 绝对的位置 - declare rawPosition: number; - // 时间倍速缩放 - declare timeScale: number; - - declare interpolateFunc: (key: string, ratio: number, from: any, to: any, nextAttributes: any) => boolean; - - declare _onStart?: (() => void)[]; - declare _onFrame?: ((step: IStep, ratio: number) => void)[]; - declare _onEnd?: (() => void)[]; - declare _onRemove?: (() => void)[]; - declare _preventAttrs?: Set; - static interpolateMap: Map = new Map(); - slience?: boolean; - - constructor( - id: string | number = Generator.GenAutoIncrementId(), - timeline: ITimeline = defaultTimeline, - slience?: boolean - ) { - this.id = id; - this.timeline = timeline || defaultTimeline; - this.status = AnimateStatus.INITIAL; - this.tailAnimate = new SubAnimate(this); - this.subAnimates = [this.tailAnimate]; - this.timeScale = 1; - this.rawPosition = -1; - this._startTime = 0; - this._duringTime = 0; - this.timeline.addAnimate(this); - this.slience = slience; - } - - setTimeline(timeline: ITimeline) { - if (timeline === this.timeline) { - return; - } - timeline.addAnimate(this); - } - - getStartTime(): number { - return this._startTime; - } - - getDuration(): number { - return this.subAnimates.reduce((t, subAnimate) => t + subAnimate.totalDuration, 0); - } - - after(animate: IAnimate) { - const t = animate.getDuration(); - this._startTime = t; - return this; - } - - afterAll(list: IAnimate[]) { - let maxT = -Infinity; - list.forEach(a => { - maxT = max(a.getDuration(), maxT); - }); - this._startTime = maxT; - return this; - } - - parallel(animate: IAnimate) { - this._startTime = animate.getStartTime(); - return this; - } - - static AddInterpolate(name: string, cb: InterpolateFunc) { - Animate.interpolateMap.set(name, cb); - } - - play(customAnimate: ICustomAnimate) { - this.tailAnimate.play(customAnimate); - // todo: 考虑使用绑定的ticker执行 - if (this.target) { - const stage = (this.target as IGraphic).stage; - stage && stage.renderNextFrame(); - } - if (this.subAnimates.length === 1 && this.tailAnimate.totalDuration === customAnimate.duration) { - this.trySetAttribute(customAnimate.getFromProps(), customAnimate.mode); - } - return this; - } - - trySetAttribute(attr: Record | void, mode: AnimateMode = Animate.mode) { - if (attr && mode & AnimateMode.SET_ATTR_IMMEDIATELY) { - (this.target as any).setAttributes && - (this.target as any).setAttributes(attr, false, { type: AttributeUpdateType.ANIMATE_PLAY }); - } - } - - runCb(cb: (a: IAnimate, step: IStep) => void) { - // this.tailAnimate.runCb(cb); - const customAnimate = new CbAnimate(() => { - cb(this, customAnimate.step.prev); - }); - this.tailAnimate.play(customAnimate); - return this; - } - - /** - * 自定义插值,返回false表示没有匹配上 - * @param key - * @param from - * @param to - */ - customInterpolate( - key: string, - ratio: number, - from: any, - to: any, - target: IAnimateTarget, - ret: Record - ): boolean { - const func = Animate.interpolateMap.get(key) || Animate.interpolateMap.get(''); - if (!func) { - return false; - } - return func(key, ratio, from, to, target, ret); - } - - pause() { - if (this.status === AnimateStatus.RUNNING) { - this.status = AnimateStatus.PAUSED; - } - } - - resume() { - if (this.status === AnimateStatus.PAUSED) { - this.status = AnimateStatus.RUNNING; - } - } - - to(props: Record, duration: number, easing: EasingType, params?: IStepConfig) { - this.tailAnimate.to(props, duration, easing, params); - // 默认开始动画 - // todo: 考虑使用绑定的ticker执行 - if (this.target) { - const stage = (this.target as IGraphic).stage; - stage && stage.renderNextFrame(); - } - // if (this.subAnimates.length === 1 && this.tailAnimate.duration === duration) { - // this.trySetAttribute(props); - // } - return this; - } - from(props: Record, duration: number, easing: EasingType, params?: IStepConfig) { - this.tailAnimate.from(props, duration, easing, params); - // todo: 考虑使用绑定的ticker执行 - if (this.target) { - const stage = (this.target as IGraphic).stage; - stage && stage.renderNextFrame(); - } - return this; - } - wait(duration: number) { - this.tailAnimate.wait(duration); - // todo: 考虑使用绑定的ticker执行 - if (this.target) { - const stage = (this.target as IGraphic).stage; - stage && stage.renderNextFrame(); - } - return this; - } - startAt(t: number) { - this.tailAnimate.startAt(t); - // todo: 考虑使用绑定的ticker执行 - if (this.target) { - const stage = (this.target as IGraphic).stage; - stage && stage.renderNextFrame(); - } - return this; - } - - loop(l: number) { - this.tailAnimate.loop = l; - // todo: 考虑使用绑定的ticker执行 - if (this.target) { - const stage = (this.target as IGraphic).stage; - stage && stage.renderNextFrame(); - } - return this; - } - - reversed(r: boolean) { - this.tailAnimate.reversed = r; - // todo: 考虑使用绑定的ticker执行 - if (this.target) { - const stage = (this.target as IGraphic).stage; - stage && stage.renderNextFrame(); - } - return this; - } - - bounce(b: boolean) { - this.tailAnimate.bounce = b; - // todo: 考虑使用绑定的ticker执行 - if (this.target) { - const stage = (this.target as IGraphic).stage; - stage && stage.renderNextFrame(); - } - return this; - } - - subAnimate() { - const sa = new SubAnimate(this, this.tailAnimate); - this.tailAnimate = sa; - this.subAnimates.push(sa); - sa.bind(this.target); - return this; - } - - getStartProps(): Record { - return this.subAnimates[0].getStartProps(); - } - - getEndProps(): Record { - return this.tailAnimate.getEndProps(); - } - - depreventAttr(key: string) { - if (!this._preventAttrs) { - return; - } - this._preventAttrs.delete(key); - } - preventAttr(key: string) { - if (!this._preventAttrs) { - this._preventAttrs = new Set(); - } - this._preventAttrs.add(key); - } - preventAttrs(keys: string[]) { - keys.forEach(key => this.preventAttr(key)); - } - validAttr(key: string): boolean { - if (!this._preventAttrs) { - return true; - } - return !this._preventAttrs.has(key); - } - - bind(target: IAnimateTarget) { - this.target = target; - - if (this.target.onAnimateBind && !this.slience) { - this.target.onAnimateBind(this); - } - - this.subAnimates.forEach(sa => { - sa.bind(target); - }); - return this; - } - - advance(delta: number) { - // startTime之前的时间不计入耗时 - if (this._duringTime < this._startTime) { - if (this._duringTime + delta * this.timeScale < this._startTime) { - this._duringTime += delta * this.timeScale; - return; - } - delta = this._duringTime + delta * this.timeScale - this._startTime; - this._duringTime = this._startTime; - } - // 执行advance - if (this.status === AnimateStatus.INITIAL) { - this.status = AnimateStatus.RUNNING; - this._onStart && this._onStart.forEach(cb => cb()); - } - const end = this.setPosition(Math.max(this.rawPosition, 0) + delta * this.timeScale); - if (end && this.status === AnimateStatus.RUNNING) { - this.status = AnimateStatus.END; - this._onEnd && this._onEnd.forEach(cb => cb()); - } - } - - setPosition(rawPosition: number): boolean { - let d = 0; - let sa: SubAnimate | undefined; - const prevRawPos = this.rawPosition; - const maxRawPos = this.subAnimates.reduce((a, b) => a + b.totalDuration, 0); - - if (rawPosition < 0) { - rawPosition = 0; - } - - const end = rawPosition >= maxRawPos; - - if (end) { - rawPosition = maxRawPos; - } - - if (rawPosition === prevRawPos) { - return end; - } - - // 查找对应的subAnimate - for (let i = 0; i < this.subAnimates.length; i++) { - sa = this.subAnimates[i]; - if (d + sa.totalDuration >= rawPosition) { - break; - } else { - d += sa.totalDuration; - sa = undefined; - } - } - this.rawPosition = rawPosition; - sa.setPosition(rawPosition - d); - - return end; - } - - onStart(cb: () => void) { - if (!this._onStart) { - this._onStart = []; - } - this._onStart.push(cb); - } - onEnd(cb: () => void) { - if (!this._onEnd) { - this._onEnd = []; - } - this._onEnd.push(cb); - } - onRemove(cb: () => void) { - if (!this._onRemove) { - this._onRemove = []; - } - this._onRemove.push(cb); - } - onFrame(cb: (step: IStep, ratio: number) => void) { - if (!this._onFrame) { - this._onFrame = []; - } - this._onFrame.push(cb); - } - release() { - this.status = AnimateStatus.END; - return; - } - - stop(nextVal?: 'start' | 'end' | Record) { - if (!nextVal) { - this.target.onStop(); - } - if (nextVal === 'start') { - this.target.onStop(this.getStartProps()); - } else if (nextVal === 'end') { - this.target.onStop(this.getEndProps()); - } else { - this.target.onStop(nextVal); - } - this.release(); - } -} - -// Animate.mode |= AnimateMode.SET_ATTR_IMMEDIATELY; - -export class SubAnimate implements ISubAnimate { - declare target: IAnimateTarget; - declare animate: IAnimate; - // 默认的初始step,一定存在,且stepHead的props一定保存整个subAnimate阶段所有属性的最初 - protected declare stepHead: Step; - protected declare stepTail: Step; - // 结束时反转动画 - declare bounce: boolean; - // 是否reverse - declare reversed: boolean; - // 循环次数,0为执行一次,1为执行两次,Infinity为无限循环 - declare loop: number; - // 持续时间,不包括循环 - declare duration: number; - // 位置,在[0, duration]之间 - declare position: number; - // 绝对的位置,在[0, loops * duration]之间 - declare rawPosition: number; - declare dirty: boolean; - - declare _totalDuration: number; - declare _startAt: number; - declare _lastStep: IStep; - declare _deltaPosition: number; - - get totalDuration(): number { - this.calcAttr(); - return this._totalDuration + this._startAt; - } - - constructor(animate: IAnimate, lastSubAnimate?: SubAnimate) { - this.rawPosition = -1; - this.position = 0; - this.loop = 0; - this.duration = 0; - this.animate = animate; - if (lastSubAnimate) { - this.stepHead = new Step(0, 0, Object.assign({}, lastSubAnimate.stepTail.props)); - } else { - this.stepHead = new Step(0, 0, {}); - } - this.stepTail = this.stepHead; - this.dirty = true; - this._startAt = 0; - } - - // 计算按需计算的属性 - protected calcAttr() { - if (!this.dirty) { - return; - } - - this._totalDuration = this.duration * (this.loop + 1); - } - - bind(target: IAnimateTarget) { - this.target = target; - return this; - } - - play(customAnimate: ICustomAnimate) { - let duration = customAnimate.duration; - if (duration == null || duration < 0) { - duration = 0; - } - const easing = customAnimate.easing; - const easingFunc = typeof easing === 'string' ? Easing[easing] : easing; - const step = this._addStep(duration, null, easingFunc); - step.type = AnimateStepType.customAnimate; - this._appendProps(customAnimate.getEndProps(), step, false); - this._appendCustomAnimate(customAnimate, step); - return this; - } - - // _appendPlayProps(step: IStep) { - - // return; - // } - - to(props: Record, duration: number, easing: EasingType, params?: IStepConfig) { - if (duration == null || duration < 0) { - duration = 0; - } - - const easingFunc = typeof easing === 'string' ? Easing[easing] : easing; - - const step = this._addStep(duration, null, easingFunc); - step.type = AnimateStepType.to; - this._appendProps(props, step, params ? params.tempProps : false); - // this._appendPlayProps(step); - - if (!step.propKeys) { - step.propKeys = Object.keys(step.props); - } - if (!(params && params.noPreventAttrs)) { - this.target.animates && - this.target.animates.forEach(a => { - if (a.id !== this.animate.id) { - a.preventAttrs(step.propKeys); - } - }); - } - return this; - } - - from(props: Record, duration: number, easing: EasingType, params?: IStepConfig) { - this.to(props, 0, easing, params); - const toProps = {}; - if (!this.stepTail.propKeys) { - this.stepTail.propKeys = Object.keys(this.stepTail.props); - } - this.stepTail.propKeys.forEach(k => { - toProps[k] = this.getLastPropByName(k, this.stepTail); - }); - this.to(toProps, duration, easing, params); - this.stepTail.type = AnimateStepType.from; - } - - startAt(t: number) { - if (t < 0) { - t = 0; - } - this._startAt = t; - return this; - } - - getStartProps() { - return this.stepHead?.props; - } - - getEndProps() { - return this.stepTail.props; - } - - getLastStep() { - return this._lastStep; - } - - wait(duration: number) { - if (duration > 0) { - const step = this._addStep(+duration, null); - - step.type = AnimateStepType.wait; - // TODO 这里如果跳帧的话会存在bug - if (step.prev.customAnimate) { - step.props = step.prev.customAnimate.getEndProps(); - } else { - step.props = step.prev.props; - } - if (this.target.onAddStep) { - this.target.onAddStep(step); - } - // this._appendPlayProps(step); - } - return this; - } - - protected _addStep(duration: number, props: any, easingFunc?: EasingTypeFunc) { - const step = new Step(this.duration, duration, props, easingFunc); - this.duration += duration; - this.stepTail.append(step); - this.stepTail = step; - return step; - } - - protected _appendProps(props: any, step: Step, tempProps?: boolean) { - if (tempProps) { - step.props = props; - } else { - // todo: 是否需要深拷贝props - step.props = Object.assign({}, props); - } - let lastStep = step.prev; - const _props = step.props; - // 将undefined的属性设置到默认值 - if (!step.propKeys) { - step.propKeys = Object.keys(step.props); - } - step.propKeys.forEach(k => { - if (step.props[k] === undefined) { - step.props[k] = this.target.getDefaultAttribute(k); - } - }); - // 拷贝之前的step阶段属性 - while (lastStep.prev) { - if (lastStep.props) { - if (!lastStep.propKeys) { - lastStep.propKeys = Object.keys(lastStep.props); - } - lastStep.propKeys.forEach(key => { - if (_props[key] === undefined) { - _props[key] = lastStep.props[key]; - } - }); - } - // 重置propKeys - step.propKeys = Object.keys(step.props); - lastStep = lastStep.prev; - } - - // 设置最初的props属性 - const initProps = this.stepHead.props; - if (!step.propKeys) { - step.propKeys = Object.keys(_props); - } - step.propKeys.forEach(key => { - if (initProps[key] === undefined) { - const parentAnimateInitProps = this.animate.getStartProps(); - initProps[key] = parentAnimateInitProps[key] = this.target.getComputedAttribute(key); - } - }); - - if (this.target.onAddStep) { - this.target.onAddStep(step); - } - } - - protected _appendCustomAnimate(customAnimate: ICustomAnimate, step: Step) { - step.customAnimate = customAnimate; - customAnimate.step = step; - customAnimate.bind(this.target, this); - } - - setPosition(rawPosition: number) { - const d = this.duration; - const loopCount = this.loop; - const prevRawPos = this.rawPosition; - let end = false; - let loop: number; // 当前是第几次循环 - let position: number; // 当前周期的时间 - const startAt = this._startAt ?? 0; - - if (rawPosition < 0) { - rawPosition = 0; - } - if (rawPosition < startAt) { - this.rawPosition = rawPosition; - return false; - } - rawPosition = rawPosition - startAt; - if (d <= 0) { - // 如果不用执行,跳过 - end = true; - // 小于0的话,直接return,如果等于0,那还是得走动画逻辑,将end属性设置上去 - if (d < 0) { - return end; - } - } - loop = Math.floor(rawPosition / d); - position = rawPosition - loop * d; - - // 计算rawPosition - end = rawPosition >= loopCount * d + d; - // 如果结束,跳过 - if (end) { - position = d; - loop = loopCount; - rawPosition = position * loop + d; - } - - if (rawPosition === prevRawPos) { - return end; - } - - // reverse动画 - const rev = !this.reversed !== !(this.bounce && loop % 2); - if (rev) { - position = d - position; - } - - this._deltaPosition = position - this.position; - this.position = position; - this.rawPosition = rawPosition + startAt; - - this.updatePosition(end, rev); - - return end; - } - - protected updatePosition(end: boolean, rev: boolean) { - if (!this.stepHead) { - return; - } - let step = this.stepHead.next; - const position = this.position; - const duration = this.duration; - if (this.target && step) { - let stepNext = step.next; - while (stepNext && stepNext.position <= position) { - step = stepNext; - stepNext = step.next; - } - let ratio = end ? (duration === 0 ? 1 : position / duration) : (position - step.position) / step.duration; // TODO: revisit this. - if (step.easing) { - ratio = step.easing(ratio); - } - // 判断这次和上次过程中是否经历了自定义step,如果跳过了自定义step那么执行自定义step的onEnd - this.tryCallCustomAnimateLifeCycle(step, this._lastStep || (rev ? this.stepTail : this.stepHead), rev); - // if (step !== this._lastStep) { - // if (this._deltaPosition > 0) { - // let _step = step.prev; - // while (_step && _step !== this._lastStep) { - // if (_step.customAnimate) { - // _step.customAnimate.onEnd(); - // } - // _step = _step.prev; - // } - // if (_step && _step.customAnimate) { - // _step.customAnimate.onEnd(); - // } - // } else if (this._deltaPosition < 0) { - // let _step = step.next; - // while (_step && _step !== this._lastStep) { - // if (_step.customAnimate) { - // _step.customAnimate.onEnd(); - // } - // _step = _step.next; - // } - // if (_step && _step.customAnimate) { - // _step.customAnimate.onEnd(); - // } - // } - // } - this.updateTarget(step, ratio, end); - this._lastStep = step; - - this.animate._onFrame && this.animate._onFrame.forEach(cb => cb(step, ratio)); - } - } - - // 如果动画卡顿跳过了自定义动画,那么尝试执行自定义动画的生命周期 - tryCallCustomAnimateLifeCycle(step: IStep, lastStep: IStep, rev: boolean) { - if (step === lastStep) { - return; - } - if (rev) { - let _step = lastStep.prev; - while (_step && _step !== step) { - if (_step.customAnimate) { - _step.customAnimate.onStart && _step.customAnimate.onStart(); - _step.customAnimate.onEnd && _step.customAnimate.onEnd(); - } - _step = step.prev; - } - // 执行lastStep的onEnd和currentStep的onStart - if (lastStep && lastStep.customAnimate) { - lastStep.customAnimate.onEnd && lastStep.customAnimate.onEnd(); - } - if (step && step.customAnimate) { - step.customAnimate.onStart && step.customAnimate.onStart(); - } - } else { - let _step = lastStep.next; - while (_step && _step !== step) { - if (_step.customAnimate) { - _step.customAnimate.onStart && _step.customAnimate.onStart(); - _step.customAnimate.onEnd && _step.customAnimate.onEnd(); - } - _step = _step.next; - } - // 执行lastStep的onEnd和currentStep的onStart - if (lastStep && lastStep.customAnimate) { - lastStep.customAnimate.onEnd && lastStep.customAnimate.onEnd(); - } - if (step && step.customAnimate) { - step.customAnimate.onStart && step.customAnimate.onStart(); - } - } - } - - /** - * 获取这个属性的上一个值 - * @param name - * @param step - * @returns - */ - getLastPropByName(name: string, step: Step): any { - let lastStep = step.prev; - while (lastStep) { - if (lastStep.props && lastStep.props[name] !== undefined) { - return lastStep.props[name]; - } else if (lastStep.customAnimate) { - const val = lastStep.customAnimate.getEndProps()[name]; - if (val !== undefined) { - return val; - } - } - lastStep = lastStep.prev; - } - - Logger.getInstance().warn('未知错误,step中找不到属性'); - return step.props[name]; - } - - protected updateTarget(step: Step, ratio: number, end: boolean) { - if (step.props == null && step.customAnimate == null) { - return; - } - this.target.onStep(this, this.animate, step, ratio, end); - } -} - -// export class Animate implements IAnimate { -// declare target: IAnimateTarget; -// declare timeline: ITimeline; -// protected declare stepHead: Step; -// protected declare stepTail: Step; -// declare nextAnimate?: Animate; -// declare prevAnimate?: Animate; -// // 结束时反转动画 -// declare bounce: boolean; -// // 是否reverse -// declare reversed: boolean; -// // 循环次数,0为执行一次,1为执行两次,Infinity为无限循环 -// declare loop: number; -// // 持续时间,不包括循环 -// declare duration: number; -// // 当前Animate的状态,正常,暂停,结束 -// declare status: AnimateStatus; -// // 位置,在[0, duration]之间 -// declare position: number; -// // 绝对的位置,在[0, loops * duration]之间 -// declare rawPosition: number; -// // 开始时间 -// protected declare _startAt: number; -// // 时间的缩放,例如2表示2倍速 -// declare timeScale: number; -// declare props: Record; -// declare readonly id: string | number; - -// protected declare _onStart?: (() => void)[]; -// protected declare _onFrame?: ((step: IStep, ratio: number) => void)[]; -// protected declare _onEnd?: (() => void)[]; -// declare _onRemove?: (() => void)[]; -// declare _preventAttrs?: Set; - -// constructor(id: string | number = Generator.GenAutoIncrementId(), timeline: ITimeline = defaultTimeline) { -// this.timeline = timeline; -// this.status = AnimateStatus.INITIAL; -// this.rawPosition = -1; -// this.position = 0; -// this.loop = 0; -// this.timeline.addAnimate(this); -// this.timeScale = 1; -// this.id = id; -// this.props = {}; -// this.stepHead = new Step(0, 0, {}); -// this.stepTail = this.stepHead; -// } - -// preventAttr(key: string) { -// if (!this._preventAttrs) { -// this._preventAttrs = new Set(); -// } -// this._preventAttrs.add(key); -// } -// preventAttrs(keys: string[]) { -// keys.forEach(key => this.preventAttr(key)); -// } -// validAttr(key: string): boolean { -// if (!this._preventAttrs) { -// return true; -// } -// return !this._preventAttrs.has(key); -// } - -// getLastPropByName(name: string, step: Step): any { -// let lastStep = step.prev; -// while (lastStep) { -// if (lastStep.props && lastStep.props[name] !== undefined) { -// return lastStep.props[name]; -// } -// lastStep = lastStep.prev; -// } -// let val = this.props[name]; -// if (!val) { -// console.warn('未知错误,step中找不到属性'); -// val = this.target.getComputedAttribute(name); -// this.props[name] = val; -// } - -// return val; -// } - -// bind(target: IAnimateTarget) { -// this.target = target; -// this.duration = 0; -// return this; -// } - -// startAt(t: number) { -// if (t < 0) { -// return this; -// } -// this._startAt = t; -// return this; -// } - -// to(props: Record, duration: number, easing: EasingType, params?: IStepConfig) { -// if (duration == null || duration < 0) { -// duration = 0; -// } - -// const easingFunc = typeof easing === 'string' ? Easing[easing] : easing; - -// const step = this._addStep(duration, null, easingFunc); -// this._appendProps(props, step, params ? params.tempProps : false); -// return this; -// } - -// wait(duration: number) { -// if (duration > 0) { -// const step = this._addStep(+duration, null); -// if (step.prev) { -// step.props = step.prev.props; -// } -// if (this.target.onAddStep) { -// this.target.onAddStep(step); -// } -// } -// return this; -// } - -// protected _addStep(duration: number, props: any, easingFunc?: EasingTypeFunc) { -// const step = new Step(this.duration, duration, props, easingFunc); -// this.duration += duration; -// this.stepTail.append(step); -// this.stepTail = step; -// return step; -// } - -// protected _appendProps(props: any, step: Step, tempProps?: boolean) { -// if (tempProps) { -// step.props = props; -// } else { -// // todo: 是否需要深拷贝props -// step.props = Object.assign({}, props); -// } -// let lastStep = step.prev; -// const _props = step.props; -// // 拷贝之前的step阶段属性 -// while (lastStep.prev) { -// if (lastStep.props) { -// if (!lastStep.propKeys) { -// lastStep.propKeys = Object.keys(lastStep.props); -// } -// lastStep.propKeys.forEach(key => { -// if (_props[key] === undefined) { -// _props[key] = lastStep.props[key]; -// } -// }); -// } -// lastStep = lastStep.prev; -// } - -// // 设置最初的props属性 -// const initProps = this.stepHead.props; -// if (!step.propKeys) { -// step.propKeys = Object.keys(_props); -// step.propKeys.forEach(key => { -// initProps[key] = this.target.getComputedAttribute(key); -// }); -// } - -// if (this.target.onAddStep) { -// this.target.onAddStep(step); -// } -// } - -// advance(delta: number) { -// if (this.status === AnimateStatus.INITIAL) { -// this.status = AnimateStatus.RUNNING; -// this._onStart && this._onStart.forEach(cb => cb()); -// } -// const end = this.setPosition(this.rawPosition + delta * this.timeScale); -// if (end && this.status === AnimateStatus.RUNNING) { -// this.status = AnimateStatus.END; -// this._onEnd && this._onEnd.forEach(cb => cb()); -// } -// } - -// setPosition(rawPosition: number) { -// const d = this.duration; -// const loopCount = this.loop; -// const prevRawPos = this.rawPosition; -// let end = false; -// let loop: number; // 当前是第几次循环 -// let position: number; // 当前周期的时间 -// const startAt = this._startAt ?? 0; - -// if (rawPosition < 0) { -// rawPosition = 0; -// } -// if (rawPosition < startAt) { -// this.rawPosition = rawPosition; -// return false; -// } -// rawPosition = rawPosition - startAt; -// if (d <= 0) { -// // 如果不用执行,跳过 -// end = true; -// return end; -// } -// loop = Math.floor(rawPosition / d); -// position = rawPosition - loop * d; - -// // 计算rawPosition -// end = rawPosition >= loopCount * d + d; -// // 如果结束,跳过 -// if (end) { -// position = d; -// loop = loopCount; -// rawPosition = position * loop + d; -// } - -// if (rawPosition === prevRawPos) { -// return end; -// } - -// // reverse动画 -// const rev = !this.reversed !== !(this.bounce && loop % 2); -// if (rev) { -// position = d - position; -// } - -// this.position = position; -// this.rawPosition = rawPosition + startAt; - -// this.updatePosition(end); - -// return end; -// } - -// protected updatePosition(end: boolean) { -// if (!this.stepHead) { -// return; -// } -// let step = this.stepHead; -// const position = this.position; -// const duration = this.duration; -// if (this.target && step) { -// let stepNext = step.next; -// while (stepNext && stepNext.position <= position) { -// step = step.next; -// stepNext = step.next; -// } -// let ratio = end ? (duration === 0 ? 1 : position / duration) : (position - step.position) / step.duration; // TODO: revisit this. -// if (step.easing) { -// ratio = step.easing(ratio); -// } -// this.updateTarget(step, ratio, end); -// this._onFrame && this._onFrame.forEach(cb => cb(step, ratio)); -// } -// } - -// protected updateTarget(step: Step, ratio: number, end: boolean) { -// if (step.props == null) { -// return; -// } -// this.target.onStep(this, step, ratio, end); -// } - -// onStart(cb: () => void) { -// if (!this._onStart) { -// this._onStart = []; -// } -// this._onStart.push(cb); -// } -// onEnd(cb: () => void) { -// if (!this._onEnd) { -// this._onEnd = []; -// } -// this._onEnd.push(cb); -// } -// onRemove(cb: () => void) { -// if (!this._onRemove) { -// this._onRemove = []; -// } -// this._onRemove.push(cb); -// } -// onFrame(cb: (step: IStep, ratio: number) => void) { -// if (!this._onFrame) { -// this._onFrame = []; -// } -// this._onFrame.push(cb); -// } - -// getStartProps() { -// return this.stepHead?.props; -// } - -// getEndProps(target: Record = {}) { -// let step = this.stepHead; -// while (step) { -// if (step.props) { -// Object.assign(target, step.props); -// } -// step = step.next; -// } - -// return target; -// } - -// stop(nextVal?: 'start' | 'end' | Record) { -// this.status = AnimateStatus.END; -// if (!nextVal) { -// this.target.onStop(); -// } -// if (nextVal === 'start') { -// this.target.onStop(this.getStartProps()); -// } else if (nextVal === 'end') { -// this.target.onStop(this.getEndProps()); -// } else { -// this.target.onStop(nextVal); -// } -// } - -// release() { -// this.status = AnimateStatus.END; -// return; -// } -// } - -class Step implements IStep { - declare prev?: Step; - // 持续时间 - declare duration: number; - // 在animate中的位置 - declare position: number; - declare next?: Step; - declare props: any; - // 保存解析后的props,用于性能优化 - declare parsedProps?: any; - declare propKeys?: string[]; - declare easing?: EasingTypeFunc; - declare customAnimate?: ICustomAnimate; - // passive: boolean; - // index: number; - type: IAnimateStepType; - - constructor(position: number, duration: number, props?: any, easing?: EasingTypeFunc) { - this.duration = duration; - this.position = position; - this.props = props; - this.easing = easing; - } - - append(step: Step) { - step.prev = this; - step.next = this.next; - this.next = step; - } - - getLastProps() { - let step = this.prev; - while (step) { - if (step.props) { - return step.props; - } else if (step.customAnimate) { - return step.customAnimate.getMergedEndProps(); - } - step = step.prev as any; - } - return null; - } -} diff --git a/packages/vrender-core/src/animate/custom-animate.ts b/packages/vrender-core/src/animate/custom-animate.ts deleted file mode 100644 index 5c0c04f99..000000000 --- a/packages/vrender-core/src/animate/custom-animate.ts +++ /dev/null @@ -1,1364 +0,0 @@ -import type { IPoint, IPointLike } from '@visactor/vutils'; -import { - clamp, - getDecimalPlaces, - isArray, - isNumber, - isValidNumber, - pi, - pi2, - Point, - PointService -} from '@visactor/vutils'; -import { application } from '../application'; -import { AttributeUpdateType } from '../common/enums'; -import { CustomPath2D } from '../common/custom-path2d'; -import type { - EasingType, - IArcGraphicAttribute, - IArea, - IAreaCacheItem, - ICubicBezierCurve, - ICurve, - ICustomPath2D, - IGraphic, - IGroup, - ILine, - ILineAttribute, - ILinearGradient, - IRect, - IRectAttribute, - IRectGraphicAttribute, - ISegment, - IShadowRoot -} from '../interface'; -import { ACustomAnimate } from './animate'; -import { Easing } from './easing'; -import { pointInterpolation } from '../common/utils'; -import { divideCubic } from '../common/segment/curve/cubic-bezier'; - -export class IncreaseCount extends ACustomAnimate<{ text: string | number }> { - declare valid: boolean; - - private fromNumber: number; - private toNumber: number; - private decimalLength: number; - - constructor( - from: { text: string | number }, - to: { text: string | number }, - duration: number, - easing: EasingType, - params?: { fixed?: boolean } - ) { - super(from, to, duration, easing, params); - } - - getEndProps(): Record | void { - if (this.valid === false) { - return {}; - } - return { - text: this.to - }; - } - - onBind(): void { - this.fromNumber = isNumber(this.from?.text) ? this.from?.text : Number.parseFloat(this.from?.text); - this.toNumber = isNumber(this.to?.text) ? this.to?.text : Number.parseFloat(this.to?.text); - if (!Number.isFinite(this.toNumber)) { - this.fromNumber = 0; - } - if (!Number.isFinite(this.toNumber)) { - this.valid = false; - } - if (this.valid !== false) { - this.decimalLength = - this.params?.fixed ?? Math.max(getDecimalPlaces(this.fromNumber), getDecimalPlaces(this.toNumber)); - } - } - - onEnd(): void { - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - if (this.valid === false) { - return; - } - if (end) { - out.text = this.to?.text; - } else { - out.text = (this.fromNumber + (this.toNumber - this.fromNumber) * ratio).toFixed(this.decimalLength); - } - } -} - -enum Direction { - LEFT_TO_RIGHT = 0, - RIGHT_TO_LEFT = 1, - TOP_TO_BOTTOM = 2, - BOTTOM_TO_TOP = 3, - STROKE = 4 -} -export class FadeInPlus extends ACustomAnimate { - declare direction: number; - declare toFill: string; - declare toStroke: string; - declare fillGradient: ILinearGradient; - declare strokeGradient: ILinearGradient; - declare fill: boolean; - declare stroke: boolean; - constructor( - from: any, - to: any, - duration: number, - easing: EasingType, - params?: { direction?: number; fill?: boolean; stroke?: boolean } - ) { - super(from, to, duration, easing, params); - const { direction = Direction.LEFT_TO_RIGHT, fill = true, stroke = true } = params || {}; - this.direction = direction; - this.fill = fill; - this.stroke = stroke; - this.fillGradient = { - gradient: 'linear', - stops: [] - }; - this.strokeGradient = { - gradient: 'linear', - stops: [] - }; - } - - getEndProps(): Record { - return { - fill: this.toFill, - stroke: this.toStroke - }; - } - - onBind(): void { - // this.to = parseFloat(this.target.getAnimatePropByName('text')); - this.toFill = this.target.getComputedAttribute('fill'); - this.toStroke = this.target.getComputedAttribute('stroke'); - } - - onEnd(): void { - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - if (!this.toFill) { - return; - } - if (!this.toStroke) { - return; - } - switch (this.direction) { - case Direction.RIGHT_TO_LEFT: - this.rightToLeft(end, ratio, out); - break; - case Direction.TOP_TO_BOTTOM: - this.topToBottom(end, ratio, out); - break; - case Direction.BOTTOM_TO_TOP: - this.bottomToTop(end, ratio, out); - break; - case Direction.STROKE: - this.strokePath(end, ratio, out); - break; - default: - this.leftToRight(end, ratio, out); - break; - } - } - - leftToRight(end: boolean, ratio: number, out: Record) { - if (this.fill) { - const toFillColor = this.toFill; - this.fillGradient.x0 = 0; - this.fillGradient.y0 = 0; - this.fillGradient.x1 = 1; - this.fillGradient.y1 = 0; - this.fillGradient.stops = [ - { offset: 0, color: toFillColor }, - { offset: ratio, color: toFillColor }, - { offset: Math.min(1, ratio * 2), color: 'transparent' } - ]; - out.fill = this.fillGradient; - } - if (this.stroke) { - const toStrokeColor = this.toStroke; - this.strokeGradient.x0 = 0; - this.strokeGradient.y0 = 0; - this.strokeGradient.x1 = 1; - this.strokeGradient.y1 = 0; - this.strokeGradient.stops = [ - { offset: 0, color: toStrokeColor }, - { offset: ratio, color: toStrokeColor }, - { offset: Math.min(1, ratio * 6), color: 'transparent' } - ]; - out.stroke = this.strokeGradient; - // const dashLen = 300; - // const offset = ratio * dashLen; - // out.lineDash = [offset, dashLen - offset]; - } - return; - } - - strokePath(end: boolean, ratio: number, out: Record) { - if (this.fill) { - const toFillColor = this.toFill; - this.fillGradient.x0 = 0; - this.fillGradient.y0 = 0; - this.fillGradient.x1 = 1; - this.fillGradient.y1 = 0; - this.fillGradient.stops = [ - { offset: 0, color: toFillColor }, - { offset: ratio, color: toFillColor }, - { offset: Math.min(1, ratio * 2), color: 'transparent' } - ]; - out.fill = this.fillGradient; - } - if (this.stroke) { - const dashLen = 300; - const offset = ratio * dashLen; - out.lineDash = [offset, dashLen - offset]; - } - return; - } - rightToLeft(end: boolean, ratio: number, out: Record) { - return; - } - topToBottom(end: boolean, ratio: number, out: Record) { - return; - } - bottomToTop(end: boolean, ratio: number, out: Record) { - return; - } -} - -export class InputText extends ACustomAnimate<{ text: string }> { - declare valid: boolean; - declare target: IGraphic; - - private fromText: string = ''; - private toText: string | string[] = ''; - - getEndProps(): Record { - if (this.valid === false) { - return {}; - } - return { - text: this.to - }; - } - - onBind(): void { - this.fromText = this.from?.text ?? ''; - this.toText = this.to?.text || ''; - if (!this.toText || (isArray(this.toText) && this.toText.length === 0)) { - this.valid = false; - } - if (isArray(this.toText)) { - this.toText = this.toText.map(item => (item || '').toString()); - } - // else { - // this.toText = this.toText.toString(); - // // const root = this.target.attachShadow(); - // // const line = application.graphicService.creator.line({ - // // x: 0, - // // y: 0, - // // points: [ - // // { x: 0, y: 0 }, - // // { x: 0, y: this.target.getComputedAttribute('fontSize') } - // // ], - // // stroke: 'black', - // // lineWidth: 1 - // // }); - // // root.add(line); - // } - } - - onEnd(): void { - this.target.detachShadow(); - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - if (this.valid === false) { - return; - } - // update text - const fromCount = this.fromText.length; - const toTextIsArray = isArray(this.toText); - const toCount = toTextIsArray - ? (this.toText as unknown as string[]).reduce((c, t) => c + (t || '').length, 0) - : this.toText.length; - const count = Math.ceil(fromCount + (toCount - fromCount) * ratio); - - if (toTextIsArray) { - out.text = []; - let len = 0; - (this.toText as unknown as string[]).forEach(t => { - if (len + t.length > count) { - out.text.push(t.substr(0, count - len)); - len = count; - } else { - out.text.push(t); - len += t.length; - } - }); - } else { - out.text = (this.toText as string).substr(0, count); - } - // console.log(out.text) - - // update line position - // const line = this.target.shadowRoot?.at(0) as IGraphic; - // const endX = (this.target as any).clipedWidth + 2; - // line.setAttribute('x', endX); - } -} - -export class StreamLight extends ACustomAnimate { - declare valid: boolean; - declare target: IGraphic; - - declare rect: IRect; - declare line: ILine; - declare area: IArea; - constructor( - from: any, - to: any, - duration: number, - easing: EasingType, - params?: { attribute?: Partial; streamLength?: number; isHorizontal?: boolean } - ) { - super(from, to, duration, easing, params); - } - - getEndProps(): Record { - return {}; - } - - onStart(): void { - if (!this.target) { - return; - } - if (this.target.type === 'rect') { - this.onStartRect(); - } else if (this.target.type === 'line') { - this.onStartLineOrArea('line'); - } else if (this.target.type === 'area') { - this.onStartLineOrArea('area'); - } - } - - onStartLineOrArea(type: 'line' | 'area') { - const root = this.target.attachShadow(); - const line = application.graphicService.creator[type]({ - ...this.params?.attribute - }); - this[type] = line; - line.pathProxy = new CustomPath2D(); - root.add(line); - } - - onStartRect(): void { - const root = this.target.attachShadow(); - - const isHorizontal = this.params?.isHorizontal ?? true; - const sizeAttr = isHorizontal ? 'height' : 'width'; - const otherSizeAttr = isHorizontal ? 'width' : 'height'; - const size = this.target.AABBBounds[sizeAttr](); - const y = isHorizontal ? 0 : this.target.AABBBounds.y1; - - const rect = application.graphicService.creator.rect({ - [sizeAttr]: size, - fill: '#bcdeff', - shadowBlur: 30, - shadowColor: '#bcdeff', - ...this.params?.attribute, - x: 0, - y, - [otherSizeAttr]: 0 - }); - this.rect = rect; - root.add(rect); - } - - onBind(): void { - return; - } - - onEnd(): void { - this.target.detachShadow(); - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - if (this.rect) { - return this.onUpdateRect(end, ratio, out); - } else if (this.line || this.area) { - return this.onUpdateLineOrArea(end, ratio, out); - } - } - - protected onUpdateRect(end: boolean, ratio: number, out: Record): void { - const isHorizontal = this.params?.isHorizontal ?? true; - const parentAttr = (this.target as any).attribute; - if (isHorizontal) { - const parentWidth = parentAttr.width ?? Math.abs(parentAttr.x1 - parentAttr.x) ?? 250; - const streamLength = this.params?.streamLength ?? parentWidth; - const maxLength = this.params?.attribute?.width ?? 60; - // 起点,rect x右端点 对齐 parent左端点 - // 如果parent.x1 < parent.x, 需要把rect属性移到parent x1的位置上, 因为初始 rect.x = parent.x - const startX = -maxLength; - // 插值 - const currentX = startX + (streamLength - startX) * ratio; - // 位置限定 > 0 - const x = Math.max(currentX, 0); - // 宽度计算 - const w = Math.min(Math.min(currentX + maxLength, maxLength), streamLength - currentX); - // 如果 rect右端点 超出 parent右端点, 宽度动态调整 - const width = w + x > parentWidth ? Math.max(parentWidth - x, 0) : w; - this.rect.setAttributes( - { - x, - width, - dx: Math.min(parentAttr.x1 - parentAttr.x, 0) - } as any, - false, - { - type: AttributeUpdateType.ANIMATE_PLAY, - animationState: { - ratio, - end - } - } - ); - } else { - const parentHeight = parentAttr.height ?? Math.abs(parentAttr.y1 - parentAttr.y) ?? 250; - const streamLength = this.params?.streamLength ?? parentHeight; - const maxLength = this.params?.attribute?.height ?? 60; - // 起点,y上端点 对齐 parent下端点 - const startY = parentHeight; - // 插值 - const currentY = startY - (streamLength + maxLength) * ratio; - // 位置限定 < parentHeight - let y = Math.min(currentY, parentHeight); - // 高度最小值 - const h = Math.min(parentHeight - currentY, maxLength); - // 如果 rect上端点=y 超出 parent上端点 = 0, 则高度不断变小 - let height; - if (y <= 0) { - // 必须先得到高度再将y置为0, 顺序很重要 - height = Math.max(y + h, 0); - y = 0; - } else { - height = h; - } - this.rect.setAttributes( - { - y, - height, - dy: Math.min(parentAttr.y1 - parentAttr.y, 0) - } as any, - false, - { - type: AttributeUpdateType.ANIMATE_PLAY, - animationState: { - ratio, - end - } - } - ); - } - } - - protected onUpdateLineOrArea(end: boolean, ratio: number, out: Record) { - const target = this.line || this.area; - if (!target) { - return; - } - const customPath = target.pathProxy as ICustomPath2D; - const targetLine = this.target as ILine | IArea; - if (targetLine.cache || targetLine.cacheArea) { - this._onUpdateLineOrAreaWithCache(customPath, targetLine, end, ratio, out); - } else { - this._onUpdateLineWithoutCache(customPath, targetLine, end, ratio, out); - } - const targetAttrs = targetLine.attribute; - target.setAttributes({ - stroke: targetAttrs.stroke, - ...target.attribute - }); - target.addUpdateBoundTag(); - } - - // 针对有cache的linear - protected _onUpdateLineOrAreaWithCache( - customPath: ICustomPath2D, - g: ILine | IArea, - end: boolean, - ratio: number, - out: Record - ) { - customPath.clear(); - if (g.type === 'line') { - let cache = g.cache; - if (!Array.isArray(cache)) { - cache = [cache]; - } - const totalLen = cache.reduce((l: any, c: any) => l + c.getLength(), 0); - const curves: ICurve[] = []; - cache.forEach((c: any) => { - c.curves.forEach((ci: any) => curves.push(ci)); - }); - return this._updateCurves(customPath, curves, totalLen, ratio); - } else if (g.type === 'area' && g.cacheArea?.top?.curves) { - const cache = g.cacheArea as IAreaCacheItem; - const totalLen = cache.top.curves.reduce((a, b) => a + b.getLength(), 0); - return this._updateCurves(customPath, cache.top.curves, totalLen, ratio); - } - } - - protected _updateCurves(customPath: ICustomPath2D, curves: ICurve[], totalLen: number, ratio: number) { - const startLen = totalLen * ratio; - const endLen = Math.min(startLen + this.params?.streamLength ?? 10, totalLen); - let lastLen = 0; - let start = false; - for (let i = 0; i < curves.length; i++) { - if (curves[i].defined !== false) { - const curveItem = curves[i]; - const len = curveItem.getLength(); - const startPercent = 1 - (lastLen + len - startLen) / len; - let endPercent = 1 - (lastLen + len - endLen) / len; - let curveForStart: ICubicBezierCurve; - if (lastLen < startLen && lastLen + len > startLen) { - start = true; - if (curveItem.p2 && curveItem.p3) { - const [_, curve2] = divideCubic(curveItem as ICubicBezierCurve, startPercent); - customPath.moveTo(curve2.p0.x, curve2.p0.y); - curveForStart = curve2; - // console.log(curve2.p0.x, curve2.p0.y); - } else { - const p = curveItem.getPointAt(startPercent); - customPath.moveTo(p.x, p.y); - } - } - if (lastLen < endLen && lastLen + len > endLen) { - if (curveItem.p2 && curveItem.p3) { - if (curveForStart) { - endPercent = (endLen - startLen) / curveForStart.getLength(); - } - const [curve1] = divideCubic(curveForStart || (curveItem as ICubicBezierCurve), endPercent); - customPath.bezierCurveTo(curve1.p1.x, curve1.p1.y, curve1.p2.x, curve1.p2.y, curve1.p3.x, curve1.p3.y); - } else { - const p = curveItem.getPointAt(endPercent); - customPath.lineTo(p.x, p.y); - } - break; - } else if (start) { - if (curveItem.p2 && curveItem.p3) { - const curve = curveForStart || curveItem; - customPath.bezierCurveTo(curve.p1.x, curve.p1.y, curve.p2.x, curve.p2.y, curve.p3.x, curve.p3.y); - } else { - customPath.lineTo(curveItem.p1.x, curveItem.p1.y); - } - } - lastLen += len; - } - } - } - - // 只针对最简单的linear - protected _onUpdateLineWithoutCache( - customPath: ICustomPath2D, - line: ILine, - end: boolean, - ratio: number, - out: Record - ) { - const { points, curveType } = line.attribute; - if (!points || points.length < 2 || curveType !== 'linear') { - return; - } - let totalLen = 0; - for (let i = 1; i < points.length; i++) { - totalLen += PointService.distancePP(points[i], points[i - 1]); - } - const startLen = totalLen * ratio; - const endLen = Math.min(startLen + this.params?.streamLength ?? 10, totalLen); - const nextPoints = []; - let lastLen = 0; - for (let i = 1; i < points.length; i++) { - const len = PointService.distancePP(points[i], points[i - 1]); - if (lastLen < startLen && lastLen + len > startLen) { - nextPoints.push(PointService.pointAtPP(points[i - 1], points[i], 1 - (lastLen + len - startLen) / len)); - } - if (lastLen < endLen && lastLen + len > endLen) { - nextPoints.push(PointService.pointAtPP(points[i - 1], points[i], 1 - (lastLen + len - endLen) / len)); - break; - } else if (nextPoints.length) { - nextPoints.push(points[i]); - } - lastLen += len; - } - - if (!nextPoints.length || nextPoints.length < 2) { - return; - } - customPath.clear(); - customPath.moveTo(nextPoints[0].x, nextPoints[0].y); - for (let i = 1; i < nextPoints.length; i++) { - customPath.lineTo(nextPoints[i].x, nextPoints[i].y); - } - } -} - -export class Meteor extends ACustomAnimate { - declare size: number; - declare target: IGraphic; - declare root: IShadowRoot; - declare posList: IPoint[]; - - get lastPos(): IPoint { - return this.posList[this.posList.length - 1]; - } - - constructor(size: number, duration: number, easing: EasingType, params?: any) { - super(null, null, duration, easing, params); - this.size = size; - this.posList = []; - } - - onBind(): void { - const root = this.target.attachShadow(); - this.root = root; - for (let i = 0; i < this.size; i++) { - const g = this.target.clone(); - const scale = Math.min(((this.size - i) / this.size) * 3, 1); - const opacity = Math.min(0.2 + 0.7 / this.size); - g.setAttributes({ x: 0, y: 0, dx: 0, dy: 0, scaleX: scale, scaleY: scale, opacity }, false, { - type: AttributeUpdateType.ANIMATE_BIND - }); - root.add(g); - } - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - if (end) { - this.target.detachShadow(); - this.posList.length = 0; - return; - } - - const x = this.target.getComputedAttribute('x'); - const y = this.target.getComputedAttribute('y'); - - const nextPos = new Point(x, y); - if (!this.posList.length) { - this.posList.push(nextPos); - return; - } - - this.target.shadowRoot.forEachChildren((g: IGraphic, i) => { - const pos = this.posList[Math.max(this.posList.length - i - 1, 0)]; - g.setAttributes( - { - x: pos.x - x, - y: pos.y - y - }, - false - ); - }); - - this.posList.push(nextPos); - } -} - -export class MotionPath extends ACustomAnimate { - declare valid: boolean; - declare pathLength: number; - declare path: CustomPath2D; - declare distance: number; - declare initAngle: number; - declare changeAngle: boolean; - declare cb?: (from: any, to: any, ratio: number, target: IGraphic) => void; - constructor( - from: any, - to: any, - duration: number, - easing: EasingType, - params?: { - path: CustomPath2D; - distance: number; - cb?: (from: any, to: any, ratio: number, target: IGraphic) => void; - initAngle?: number; - changeAngle?: boolean; - } - ) { - super(from, to, duration, easing, params); - if (params) { - this.pathLength = params.path.getLength(); - this.path = params.path; - this.distance = params.distance; - this.to = params.distance * this.pathLength; - this.initAngle = params.initAngle ?? 0; - this.changeAngle = !!params.changeAngle; - this.cb = params.cb; - } - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - // 计算位置 - const at = this.to * ratio; - const { pos, angle } = this.path.getAttrAt(at); - out.x = pos.x; - out.y = pos.y; - if (this.changeAngle) { - out.angle = angle + this.initAngle; - } - this.cb && this.cb(this.from, this.to, ratio, this.target as IGraphic); - // out.angle = angle + this.initAngle; - } -} - -export class TagPointsUpdate extends ACustomAnimate<{ points?: IPointLike[]; segments?: ISegment[] }> { - protected fromPoints: IPointLike[]; - protected toPoints: IPointLike[]; - protected points: IPointLike[]; - protected interpolatePoints: [IPointLike, IPointLike][]; - protected newPointAnimateType: 'grow' | 'appear' | 'clip'; - protected clipRange: number; - protected shrinkClipRange: number; - protected clipRangeByDimension: 'x' | 'y'; - protected segmentsCache: number[]; - - constructor( - from: any, - to: any, - duration: number, - easing: EasingType, - params?: { newPointAnimateType?: 'grow' | 'appear' | 'clip'; clipRangeByDimension?: 'x' | 'y' } - ) { - super(from, to, duration, easing, params); - this.newPointAnimateType = params?.newPointAnimateType ?? 'grow'; - this.clipRangeByDimension = params?.clipRangeByDimension ?? 'x'; - } - - private getPoints(attribute: typeof this.from, cache = false): IPointLike[] { - if (attribute.points) { - return attribute.points; - } - - if (attribute.segments) { - const points = [] as IPointLike[]; - if (!this.segmentsCache) { - this.segmentsCache = []; - } - attribute.segments.map(segment => { - if (segment.points) { - points.push(...segment.points); - } - if (cache) { - this.segmentsCache.push(segment.points?.length ?? 0); - } - }); - return points; - } - return []; - } - - onBind(): void { - const originFromPoints = this.getPoints(this.from); - const originToPoints = this.getPoints(this.to, true); - this.fromPoints = !originFromPoints ? [] : !Array.isArray(originFromPoints) ? [originFromPoints] : originFromPoints; - this.toPoints = !originToPoints ? [] : !Array.isArray(originToPoints) ? [originToPoints] : originToPoints; - - const tagMap = new Map(); - this.fromPoints.forEach(point => { - if (point.context) { - tagMap.set(point.context, point); - } - }); - let firstMatchedIndex = Infinity; - let lastMatchedIndex = -Infinity; - let firstMatchedPoint: IPointLike; - let lastMatchedPoint: IPointLike; - for (let i = 0; i < this.toPoints.length; i += 1) { - if (tagMap.has(this.toPoints[i].context)) { - firstMatchedIndex = i; - firstMatchedPoint = tagMap.get(this.toPoints[i].context); - break; - } - } - for (let i = this.toPoints.length - 1; i >= 0; i -= 1) { - if (tagMap.has(this.toPoints[i].context)) { - lastMatchedIndex = i; - lastMatchedPoint = tagMap.get(this.toPoints[i].context); - break; - } - } - - if (this.newPointAnimateType === 'clip') { - if (this.toPoints.length !== 0) { - if (Number.isFinite(lastMatchedIndex)) { - this.clipRange = - this.toPoints[lastMatchedIndex][this.clipRangeByDimension] / - this.toPoints[this.toPoints.length - 1][this.clipRangeByDimension]; - if (this.clipRange === 1) { - this.shrinkClipRange = - this.toPoints[lastMatchedIndex][this.clipRangeByDimension] / - this.fromPoints[this.fromPoints.length - 1][this.clipRangeByDimension]; - } - if (!isValidNumber(this.clipRange)) { - this.clipRange = 0; - } else { - this.clipRange = clamp(this.clipRange, 0, 1); - } - } else { - this.clipRange = 0; - } - } - } - // TODO: shrink removed points - // if no point is matched, animation should start from toPoint[0] - let prevMatchedPoint = this.toPoints[0]; - this.interpolatePoints = this.toPoints.map((point, index) => { - const matchedPoint = tagMap.get(point.context); - if (matchedPoint) { - prevMatchedPoint = matchedPoint; - return [matchedPoint, point]; - } - // appear new point - if (this.newPointAnimateType === 'appear' || this.newPointAnimateType === 'clip') { - return [point, point]; - } - // grow new point - if (index < firstMatchedIndex && firstMatchedPoint) { - return [firstMatchedPoint, point]; - } else if (index > lastMatchedIndex && lastMatchedPoint) { - return [lastMatchedPoint, point]; - } - return [prevMatchedPoint, point]; - }); - this.points = this.interpolatePoints.map(interpolate => { - const fromPoint = interpolate[0]; - const toPoint = interpolate[1]; - const newPoint = new Point(fromPoint.x, fromPoint.y, fromPoint.x1, fromPoint.y1); - newPoint.defined = toPoint.defined; - newPoint.context = toPoint.context; - return newPoint; - }); - } - - onFirstRun(): void { - const lastClipRange = this.target.attribute.clipRange; - if (isValidNumber(lastClipRange * this.clipRange)) { - this.clipRange *= lastClipRange; - } - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - // if not create new points, multi points animation might not work well. - this.points = this.points.map((point, index) => { - const newPoint = pointInterpolation(this.interpolatePoints[index][0], this.interpolatePoints[index][1], ratio); - newPoint.context = point.context; - return newPoint; - }); - if (this.clipRange) { - if (this.shrinkClipRange) { - // 折线变短 - if (!end) { - out.points = this.fromPoints; - out.clipRange = this.clipRange - (this.clipRange - this.shrinkClipRange) * ratio; - } else { - out.points = this.toPoints; - out.clipRange = 1; - } - return; - } - out.clipRange = this.clipRange + (1 - this.clipRange) * ratio; - } - if (this.segmentsCache && this.to.segments) { - let start = 0; - out.segments = this.to.segments.map((segment, index) => { - const end = start + this.segmentsCache[index]; - const points = this.points.slice(start, end); - start = end; - return { - ...segment, - points - }; - }); - } else { - out.points = this.points; - } - } -} - -export class GraphicAnimate extends ACustomAnimate { - graphic: IGraphic; - - constructor(from: any, to: any, duration: number, easing: EasingType, params?: { graphic: IGraphic }) { - super(from, to, duration, easing, params); - this.graphic = params?.graphic; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - if (!this.graphic) { - return; - } - Object.keys(this.from).forEach(k => { - out[k] = this.from[k] + (this.to[k] - this.from[k]) * ratio; - }); - } -} - -export class ClipGraphicAnimate extends ACustomAnimate { - private _group?: IGroup; - private _clipGraphic?: IGraphic; - protected clipFromAttribute?: any; - protected clipToAttribute?: any; - - private _lastClip?: boolean; - private _lastPath?: IGraphic[]; - - constructor( - from: any, - to: any, - duration: number, - easing: EasingType, - params: { group: IGroup; clipGraphic: IGraphic } - ) { - super(null, null, duration, easing, params); - this.clipFromAttribute = from; - this.clipToAttribute = to; - this._group = params?.group; - this._clipGraphic = params?.clipGraphic; - } - - onBind() { - if (this._group && this._clipGraphic) { - this._lastClip = this._group.attribute.clip; - this._lastPath = this._group.attribute.path; - this._group.setAttributes( - { - clip: true, - path: [this._clipGraphic] - }, - false, - { type: AttributeUpdateType.ANIMATE_BIND } - ); - } - } - - onEnd() { - if (this._group) { - this._group.setAttributes( - { - clip: this._lastClip, - path: this._lastPath - }, - false, - { type: AttributeUpdateType.ANIMATE_END } - ); - } - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - if (!this._clipGraphic) { - return; - } - const res: any = {}; - Object.keys(this.clipFromAttribute).forEach(k => { - res[k] = this.clipFromAttribute[k] + (this.clipToAttribute[k] - this.clipFromAttribute[k]) * ratio; - }); - this._clipGraphic.setAttributes(res, false, { - type: AttributeUpdateType.ANIMATE_UPDATE, - animationState: { ratio, end } - }); - } -} - -export class ClipAngleAnimate extends ClipGraphicAnimate { - constructor( - from: any, - to: any, - duration: number, - easing: EasingType, - params: { - group: IGroup; - center?: { x: number; y: number }; - startAngle?: number; - radius?: number; - orient?: 'clockwise' | 'anticlockwise'; - animationType?: 'in' | 'out'; - } - ) { - const groupAttribute = params?.group?.attribute ?? {}; - const width = groupAttribute.width ?? 0; - const height = groupAttribute.height ?? 0; - - const animationType = params?.animationType ?? 'in'; - const startAngle = params?.startAngle ?? 0; - const orient = params?.orient ?? 'clockwise'; - - let arcStartAngle = 0; - let arcEndAngle = 0; - if (orient === 'anticlockwise') { - arcEndAngle = animationType === 'in' ? startAngle + Math.PI * 2 : startAngle; - arcEndAngle = startAngle + Math.PI * 2; - } else { - arcStartAngle = startAngle; - arcEndAngle = animationType === 'out' ? startAngle + Math.PI * 2 : startAngle; - } - const arc = application.graphicService.creator.arc({ - x: params?.center?.x ?? width / 2, - y: params?.center?.y ?? height / 2, - outerRadius: params?.radius ?? (width + height) / 2, - innerRadius: 0, - startAngle: arcStartAngle, - endAngle: arcEndAngle, - fill: true - }); - let fromAttributes: Partial; - let toAttributes: Partial; - if (orient === 'anticlockwise') { - fromAttributes = { startAngle: startAngle + Math.PI * 2 }; - toAttributes = { startAngle: startAngle }; - } else { - fromAttributes = { endAngle: startAngle }; - toAttributes = { endAngle: startAngle + Math.PI * 2 }; - } - super( - animationType === 'in' ? fromAttributes : toAttributes, - animationType === 'in' ? toAttributes : fromAttributes, - duration, - easing, - { group: params?.group, clipGraphic: arc } - ); - } -} - -export class ClipRadiusAnimate extends ClipGraphicAnimate { - constructor( - from: any, - to: any, - duration: number, - easing: EasingType, - params: { - group: IGroup; - center?: { x: number; y: number }; - startRadius?: number; - endRadius?: number; - animationType?: 'in' | 'out'; - } - ) { - const groupAttribute = params?.group?.attribute ?? {}; - const width = groupAttribute.width ?? 0; - const height = groupAttribute.height ?? 0; - - const animationType = params?.animationType ?? 'in'; - const startRadius = params?.startRadius ?? 0; - const endRadius = params?.endRadius ?? Math.sqrt((width / 2) ** 2 + (height / 2) ** 2); - - const arc = application.graphicService.creator.arc({ - x: params?.center?.x ?? width / 2, - y: params?.center?.y ?? height / 2, - outerRadius: animationType === 'out' ? endRadius : startRadius, - innerRadius: 0, - startAngle: 0, - endAngle: Math.PI * 2, - fill: true - }); - const fromAttributes: Partial = { outerRadius: startRadius }; - const toAttributes: Partial = { outerRadius: endRadius }; - super( - animationType === 'in' ? fromAttributes : toAttributes, - animationType === 'in' ? toAttributes : fromAttributes, - duration, - easing, - { group: params?.group, clipGraphic: arc } - ); - } -} - -export class ClipDirectionAnimate extends ClipGraphicAnimate { - constructor( - from: any, - to: any, - duration: number, - easing: EasingType, - params: { - group: IGroup; - direction?: 'x' | 'y'; - orient?: 'positive' | 'negative'; - width?: number; - height?: number; - animationType?: 'in' | 'out'; - } - ) { - const groupAttribute = params?.group?.attribute ?? {}; - const width = params?.width ?? groupAttribute.width ?? 0; - const height = params?.height ?? groupAttribute.height ?? 0; - - const animationType = params?.animationType ?? 'in'; - const direction = params?.direction ?? 'x'; - const orient = params?.orient ?? 'positive'; - - const rect = application.graphicService.creator.rect({ - x: 0, - y: 0, - width: animationType === 'in' && direction === 'x' ? 0 : width, - height: animationType === 'in' && direction === 'y' ? 0 : height, - fill: true - }); - let fromAttributes: Partial = {}; - let toAttributes: Partial = {}; - if (direction === 'y') { - if (orient === 'negative') { - fromAttributes = { y: height, height: 0 }; - toAttributes = { y: 0, height: height }; - } else { - fromAttributes = { height: 0 }; - toAttributes = { height: height }; - } - } else { - if (orient === 'negative') { - fromAttributes = { x: width, width: 0 }; - toAttributes = { x: 0, width: width }; - } else { - fromAttributes = { width: 0 }; - toAttributes = { width: width }; - } - } - super( - animationType === 'in' ? fromAttributes : toAttributes, - animationType === 'in' ? toAttributes : fromAttributes, - duration, - easing, - { group: params?.group, clipGraphic: rect } - ); - } -} - -type RotateSphereParams = - | { - center: { x: number; y: number; z: number }; - r: number; - cb?: (out: any) => void; - } - | (() => any); - -export class RotateBySphereAnimate extends ACustomAnimate { - declare params: RotateSphereParams; - declare theta: number; - declare phi: number; - - onStart(): void { - const { center, r } = typeof this.params === 'function' ? this.params() : this.params; - const startX = this.target.getComputedAttribute('x'); - const startY = this.target.getComputedAttribute('y'); - const startZ = this.target.getComputedAttribute('z'); - const phi = Math.acos((startY - center.y) / r); - let theta = Math.acos((startX - center.x) / r / Math.sin(phi)); - if (startZ - center.z < 0) { - theta = pi2 - theta; - } - this.theta = theta; - this.phi = phi; - } - - onBind() { - return; - } - - onEnd() { - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - if (this.phi == null || this.theta == null) { - return; - } - const { center, r, cb } = typeof this.params === 'function' ? this.params() : this.params; - const deltaAngle = Math.PI * 2 * ratio; - const theta = this.theta + deltaAngle; - const phi = this.phi; - const x = r * Math.sin(phi) * Math.cos(theta) + center.x; - const y = r * Math.cos(phi) + center.y; - const z = r * Math.sin(phi) * Math.sin(theta) + center.z; - out.x = x; - out.y = y; - out.z = z; - // out.beta = phi; - out.alpha = theta + pi / 2; - while (out.alpha > pi2) { - out.alpha -= pi2; - } - out.alpha = pi2 - out.alpha; - - out.zIndex = out.z * -10000; - - cb && cb(out); - } -} - -export class AttributeAnimate extends ACustomAnimate { - declare target: IGroup; - - constructor(to: Record, duration: number, easing: EasingType) { - super({}, to, duration, easing); - } - - getEndProps(): Record { - return this.to; - } - - onBind(): void { - Object.keys(this.to).forEach(k => { - this.from[k] = this.target.getComputedAttribute(k); - }); - return; - } - - onEnd(): void { - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - this.target.stepInterpolate( - this.subAnimate, - this.subAnimate.animate, - out, - this.step, - ratio, - end, - this.to, - this.from - ); - } -} - -export class AnimateGroup extends ACustomAnimate { - declare customAnimates: ACustomAnimate[]; - declare updating: boolean; - - constructor(duration: number, customAnimates: ACustomAnimate[]) { - super(null, null, duration, 'linear'); - this.customAnimates = customAnimates; - } - - initAnimates() { - this.customAnimates.forEach(a => { - a.step = this.step; - a.subAnimate = this.subAnimate; - a.target = this.target; - }); - } - - getEndProps(): Record { - const props = {}; - this.customAnimates.forEach(a => { - Object.assign(props, a.getEndProps()); - }); - return props; - } - - onBind(): void { - this.initAnimates(); - this.customAnimates.forEach(a => { - a.onBind(); - }); - return; - } - - onEnd(): void { - this.customAnimates.forEach(a => { - a.onEnd(); - }); - return; - } - - onStart(): void { - this.customAnimates.forEach(a => { - a.onStart(); - }); - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - if (this.updating) { - return; - } - this.updating = true; - this.customAnimates.forEach(a => { - const easing = a.easing; - const easingFunc = typeof easing === 'string' ? Easing[easing] : easing; - ratio = easingFunc(ratio); - a.onUpdate(end, ratio, out); - }); - this.updating = false; - return; - } -} - -export class AnimateGroup1 extends ACustomAnimate { - declare customAnimates: ACustomAnimate[]; - declare updating: boolean; - - constructor(duration: number, customAnimates: ACustomAnimate[]) { - super(null, null, duration, 'linear'); - this.customAnimates = customAnimates; - } - - initAnimates() { - this.customAnimates.forEach(a => { - a.step = this.step; - a.subAnimate = this.subAnimate; - a.target = this.target; - }); - } - - getEndProps(): Record { - const props = {}; - this.customAnimates.forEach(a => { - Object.assign(props, a.getEndProps()); - }); - return props; - } - - onBind(): void { - this.initAnimates(); - this.customAnimates.forEach(a => { - a.onBind(); - }); - return; - } - - onEnd(): void { - this.customAnimates.forEach(a => { - a.onEnd(); - }); - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - if (this.updating) { - return; - } - this.updating = true; - this.customAnimates.forEach(a => { - const easing = a.easing; - const easingFunc = typeof easing === 'string' ? Easing[easing] : easing; - ratio = easingFunc(ratio); - a.onUpdate(end, ratio, out); - }); - this.updating = false; - return; - } -} diff --git a/packages/vrender-core/src/animate/default-ticker.ts b/packages/vrender-core/src/animate/default-ticker.ts deleted file mode 100644 index 80d217394..000000000 --- a/packages/vrender-core/src/animate/default-ticker.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { DefaultTicker } from './Ticker/default-ticker'; -import { defaultTimeline } from './timeline'; - -export const defaultTicker = new DefaultTicker(); -defaultTicker.addTimeline(defaultTimeline); -const TICKER_FPS = 60; -defaultTicker.setFPS(TICKER_FPS); diff --git a/packages/vrender-core/src/animate/easing-func.ts b/packages/vrender-core/src/animate/easing-func.ts deleted file mode 100644 index 42c0dfb8a..000000000 --- a/packages/vrender-core/src/animate/easing-func.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { CustomPath2D } from '../common/custom-path2d'; -import { CurveContext } from '../common/segment'; - -export function generatorPathEasingFunc(path: string) { - const customPath = new CustomPath2D(); - customPath.setCtx(new CurveContext(customPath)); - customPath.fromString(path, 0, 0, 1, 1); - - return (x: number) => { - return customPath.getYAt(x); - }; -} diff --git a/packages/vrender-core/src/animate/easing.ts b/packages/vrender-core/src/animate/easing.ts deleted file mode 100644 index f50472f1d..000000000 --- a/packages/vrender-core/src/animate/easing.ts +++ /dev/null @@ -1,273 +0,0 @@ -/** - * The MIT License (MIT) - - Copyright (c) 2014 gskinner.com, inc. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -// 参考TweenJS -// https://github.com/CreateJS/TweenJS/tree/master/src/tweenjs -import { pi2 } from '@visactor/vutils'; - -/** - * 代码迁移自createjs - * 部分缓动函数参考https://easings.net/ - */ -export class Easing { - private constructor() { - // do nothing - } - - static linear(t: number): number { - return t; - } - - static none() { - return this.linear; - } - - /** - * 获取缓动函数,amount指示这个缓动函数的插值方式 - * @param amount - * @returns - */ - static get(amount: number) { - if (amount < -1) { - amount = -1; - } else if (amount > 1) { - amount = 1; - } - - return function (t: number) { - if (amount === 0) { - return t; - } - if (amount < 0) { - return t * (t * -amount + 1 + amount); - } - return t * ((2 - t) * amount + (1 - amount)); - }; - } - - /* 语法糖 */ - static getPowIn(pow: number) { - return function (t: number) { - return Math.pow(t, pow); - }; - } - - static getPowOut(pow: number) { - return function (t: number) { - return 1 - Math.pow(1 - t, pow); - }; - } - - static getPowInOut(pow: number) { - return function (t: number) { - if ((t *= 2) < 1) { - return 0.5 * Math.pow(t, pow); - } - return 1 - 0.5 * Math.abs(Math.pow(2 - t, pow)); - }; - } - - // 插值函数 - static quadIn = Easing.getPowIn(2); - static quadOut = Easing.getPowOut(2); - - static quadInOut = Easing.getPowInOut(2); - static cubicIn = Easing.getPowIn(3); - static cubicOut = Easing.getPowOut(3); - static cubicInOut = Easing.getPowInOut(3); - static quartIn = Easing.getPowIn(4); - static quartOut = Easing.getPowOut(4); - static quartInOut = Easing.getPowInOut(4); - static quintIn = Easing.getPowIn(5); - static quintOut = Easing.getPowOut(5); - static quintInOut = Easing.getPowInOut(5); - - /* 语法糖 */ - static getBackIn(amount: number) { - return function (t: number) { - return t * t * ((amount + 1) * t - amount); - }; - } - static getBackOut(amount: number) { - return function (t: number) { - return --t * t * ((amount + 1) * t + amount) + 1; - }; - } - static getBackInOut(amount: number) { - amount *= 1.525; - return function (t: number) { - if ((t *= 2) < 1) { - return 0.5 * (t * t * ((amount + 1) * t - amount)); - } - return 0.5 * ((t -= 2) * t * ((amount + 1) * t + amount) + 2); - }; - } - - // 插值函数 - static backIn = Easing.getBackIn(1.7); - static backOut = Easing.getBackOut(1.7); - static backInOut = Easing.getBackInOut(1.7); - - static sineIn(t: number): number { - return 1 - Math.cos((t * Math.PI) / 2); - } - - static sineOut(t: number): number { - return Math.sin((t * Math.PI) / 2); - } - - static sineInOut(t: number): number { - return -(Math.cos(Math.PI * t) - 1) / 2; - } - - static expoIn(t: number): number { - return t === 0 ? 0 : Math.pow(2, 10 * t - 10); - } - - static expoOut(t: number): number { - return t === 1 ? 1 : 1 - Math.pow(2, -10 * t); - } - - static expoInOut(t: number): number { - return t === 0 ? 0 : t === 1 ? 1 : t < 0.5 ? Math.pow(2, 20 * t - 10) / 2 : (2 - Math.pow(2, -20 * t + 10)) / 2; - } - - // 插值函数 - static circIn(t: number) { - return -(Math.sqrt(1 - t * t) - 1); - } - - static circOut(t: number) { - return Math.sqrt(1 - --t * t); - } - - static circInOut(t: number) { - if ((t *= 2) < 1) { - return -0.5 * (Math.sqrt(1 - t * t) - 1); - } - return 0.5 * (Math.sqrt(1 - (t -= 2) * t) + 1); - } - static bounceOut(t: number) { - if (t < 1 / 2.75) { - return 7.5625 * t * t; - } else if (t < 2 / 2.75) { - return 7.5625 * (t -= 1.5 / 2.75) * t + 0.75; - } else if (t < 2.5 / 2.75) { - return 7.5625 * (t -= 2.25 / 2.75) * t + 0.9375; - } - return 7.5625 * (t -= 2.625 / 2.75) * t + 0.984375; - } - static bounceIn(t: number) { - return 1 - Easing.bounceOut(1 - t); - } - - static bounceInOut(t: number) { - if (t < 0.5) { - return Easing.bounceIn(t * 2) * 0.5; - } - return Easing.bounceOut(t * 2 - 1) * 0.5 + 0.5; - } - - /* 语法糖 */ - static getElasticIn(amplitude: number, period: number) { - return function (t: number) { - if (t === 0 || t === 1) { - return t; - } - const s = (period / pi2) * Math.asin(1 / amplitude); - return -(amplitude * Math.pow(2, 10 * (t -= 1)) * Math.sin(((t - s) * pi2) / period)); - }; - } - static getElasticOut(amplitude: number, period: number) { - return function (t: number) { - if (t === 0 || t === 1) { - return t; - } - const s = (period / pi2) * Math.asin(1 / amplitude); - return amplitude * Math.pow(2, -10 * t) * Math.sin(((t - s) * pi2) / period) + 1; - }; - } - static getElasticInOut(amplitude: number, period: number) { - return function (t: number) { - const s = (period / pi2) * Math.asin(1 / amplitude); - if ((t *= 2) < 1) { - return -0.5 * (amplitude * Math.pow(2, 10 * (t -= 1)) * Math.sin(((t - s) * pi2) / period)); - } - return amplitude * Math.pow(2, -10 * (t -= 1)) * Math.sin(((t - s) * pi2) / period) * 0.5 + 1; - }; - } - - // 插值函数 - static elasticIn = Easing.getElasticIn(1, 0.3); - static elasticOut = Easing.getElasticOut(1, 0.3); - static elasticInOut = Easing.getElasticInOut(1, 0.3 * 1.5); - - static easeInOutQuad = (t: number) => { - if ((t /= 0.5) < 1) { - return 0.5 * Math.pow(t, 2); - } - return -0.5 * ((t -= 2) * t - 2); - }; - - static easeOutElastic = (x: number) => { - const c4 = (2 * Math.PI) / 3; - - return x === 0 ? 0 : x === 1 ? 1 : Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1; - }; - - static easeInOutElastic = (x: number) => { - const c5 = (2 * Math.PI) / 4.5; - - return x === 0 - ? 0 - : x === 1 - ? 1 - : x < 0.5 - ? -(Math.pow(2, 20 * x - 10) * Math.sin((20 * x - 11.125) * c5)) / 2 - : (Math.pow(2, -20 * x + 10) * Math.sin((20 * x - 11.125) * c5)) / 2 + 1; - }; - static registerFunc(name: string, func: (t: number) => number) { - (Easing as any)[name] = func; - } -} - -function flicker(t: number, n: number) { - const step = 1 / n; - let flag = 1; - while (t > step) { - t -= step; - flag *= -1; - } - const v = (flag * t) / step; - return v > 0 ? v : 1 + v; -} - -// 注册flicker -for (let i = 0; i < 10; i++) { - (Easing as any)[`flicker${i}`] = (t: number) => flicker(t, i); -} - -for (let i = 2; i < 10; i++) { - (Easing as any)[`aIn${i}`] = (t: number) => i * t * t + (1 - i) * t; -} diff --git a/packages/vrender-core/src/animate/group-fade.ts b/packages/vrender-core/src/animate/group-fade.ts deleted file mode 100644 index 838179d85..000000000 --- a/packages/vrender-core/src/animate/group-fade.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { IGroup } from '../interface/graphic/group'; -import { ACustomAnimate } from './animate'; - -export class GroupFadeIn extends ACustomAnimate { - declare target: IGroup; - - getEndProps(): Record { - return {}; - } - - onBind(): void { - this.target.setTheme({ - common: { - opacity: 0 - } - }); - return; - } - - onEnd(): void { - this.target.setTheme({ - common: { - opacity: 1 - } - }); - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - this.target.setTheme({ - common: { - opacity: ratio - } - }); - } -} - -export class GroupFadeOut extends ACustomAnimate { - declare target: IGroup; - - getEndProps(): Record { - return {}; - } - - onBind(): void { - this.target.setTheme({ - common: { - opacity: 1 - } - }); - return; - } - - onEnd(): void { - this.target.setTheme({ - common: { - opacity: 0 - } - }); - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - this.target.setTheme({ - common: { - opacity: 1 - ratio - } - }); - } -} diff --git a/packages/vrender-core/src/animate/index.ts b/packages/vrender-core/src/animate/index.ts deleted file mode 100644 index b04ca57b3..000000000 --- a/packages/vrender-core/src/animate/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from './Ticker'; -export * from './animate'; -export * from './config'; -export * from './custom-animate'; -export * from './morphing'; -export * from './timeline'; -export * from './group-fade'; -export * from './easing'; diff --git a/packages/vrender-core/src/animate/morphing.ts b/packages/vrender-core/src/animate/morphing.ts deleted file mode 100644 index 312aaaf8a..000000000 --- a/packages/vrender-core/src/animate/morphing.ts +++ /dev/null @@ -1,680 +0,0 @@ -import { - splitArc, - splitCircle, - splitLine, - splitRect, - splitPolygon, - splitArea, - splitPath -} from './../common/split-path'; -import type { - ICustomPath2D, - IGraphic, - MorphingAnimateConfig, - IRect, - EasingType, - MultiMorphingAnimateConfig, - IArc, - ICircle, - IGraphicAttribute, - ILine, - IPolygon, - IArea, - IPath -} from './../interface'; -import { CustomPath2D } from '../common/custom-path2d'; -import { ACustomAnimate } from './animate'; -import { - alignBezierCurves, - applyTransformOnBezierCurves, - findBestMorphingRotation, - pathToBezierCurves -} from '../common/morphing-utils'; -import { application } from '../application'; -import type { IMatrix } from '@visactor/vutils'; -import { isNil } from '@visactor/vutils'; -import { interpolateColor } from '../color-string/interpolate'; -import { ColorStore, ColorType } from '../color-string'; -import { DefaultMorphingAnimateConfig } from './config'; -import { isTransformKey } from '../common/utils'; -import { AttributeUpdateType } from '../common/enums'; - -declare const __DEV__: boolean; - -interface MorphingDataItem { - from: number[]; - to: number[]; - fromCp: number[]; - toCp: number[]; - rotation: number; -} - -interface OtherAttrItem { - from: any; - to: any; - key: string; -} - -const interpolateOtherAttrs = (attrs: OtherAttrItem[], out: any, ratio: number) => { - attrs.forEach(entry => { - if (Number.isFinite(entry.to)) { - out[entry.key] = entry.from + (entry.to - entry.from) * ratio; - } else if (entry.key === 'fill' || entry.key === 'stroke') { - // 保存解析的结果到step - const color = interpolateColor(entry.from, entry.to, ratio, false); - if (color) { - out[entry.key] = color; - } - } - }); -}; - -/* Adapted from zrender by ecomfe - * https://github.com/ecomfe/zrender - * Licensed under the BSD-3-Clause - - * url: https://github.com/ecomfe/zrender/blob/master/src/tool/morphPath.ts - * License: https://github.com/ecomfe/zrender/blob/master/LICENSE - * @license - */ -const interpolateMorphingData = (morphingData: MorphingDataItem[], path: ICustomPath2D, ratio: number) => { - const tmpArr: number[] = []; - const newCp: number[] = []; - path.clear(); - - for (let i = 0; i < morphingData.length; i++) { - const item = morphingData[i]; - const from = item.from; - const to = item.to; - const angle = item.rotation * ratio; - const fromCp = item.fromCp; - const toCp = item.toCp; - const sa = Math.sin(angle); - const ca = Math.cos(angle); - - newCp[0] = fromCp[0] + (toCp[0] - fromCp[0]) * ratio; - newCp[1] = fromCp[1] + (toCp[1] - fromCp[1]) * ratio; - - for (let m = 0; m < from.length; m += 2) { - const x0 = from[m]; - const y0 = from[m + 1]; - const x1 = to[m]; - const y1 = to[m + 1]; - - const x = x0 * (1 - ratio) + x1 * ratio; - const y = y0 * (1 - ratio) + y1 * ratio; - - tmpArr[m] = x * ca - y * sa + newCp[0]; - tmpArr[m + 1] = x * sa + y * ca + newCp[1]; - } - - let x0 = tmpArr[0]; - let y0 = tmpArr[1]; - - path.moveTo(x0, y0); - - for (let m = 2; m < from.length; m += 6) { - const x1 = tmpArr[m]; - const y1 = tmpArr[m + 1]; - const x2 = tmpArr[m + 2]; - const y2 = tmpArr[m + 3]; - const x3 = tmpArr[m + 4]; - const y3 = tmpArr[m + 5]; - - // Is a line. - if (x0 === x1 && y0 === y1 && x2 === x3 && y2 === y3) { - path.lineTo(x3, y3); - } else { - path.bezierCurveTo(x1, y1, x2, y2, x3, y3); - } - x0 = x3; - y0 = y3; - } - } -}; - -const parseMorphingData = ( - fromPath: ICustomPath2D | null, - toPath: ICustomPath2D, - config?: { - fromTransform?: IMatrix; - toTransfrom: IMatrix; - } -) => { - const fromBezier = fromPath ? pathToBezierCurves(fromPath) : []; - const toBezier = pathToBezierCurves(toPath); - - if (config && fromBezier) { - config.fromTransform && applyTransformOnBezierCurves(fromBezier, config.fromTransform.clone().getInverse()); - applyTransformOnBezierCurves(fromBezier, config.toTransfrom); - // applyTransformOnBezierCurves(toBezier, config.toTransfrom.clone().getInverse()); - } - - const [fromBezierCurves, toBezierCurves] = alignBezierCurves(fromBezier, toBezier); - - return fromPath - ? findBestMorphingRotation(fromBezierCurves, toBezierCurves, 10, Math.PI) - : toBezierCurves.map((to, index) => { - return { - from: fromBezierCurves[index], - to, - fromCp: [0, 0], - toCp: [0, 0], - rotation: 0 - }; - }); -}; - -const validateOtherAttrs = [ - 'fill', - 'fillOpacity', - 'shadowBlur', - 'shadowColor', - 'shadowOffsetX', - 'shadowOffsetY', - 'stroke', - 'strokeOpacity', - 'lineDashOffset' - // 'lineWidth' -]; - -const parseOtherAnimateAttrs = ( - fromAttrs: Partial | null, - toAttrs: Partial | null -) => { - if (!fromAttrs || !toAttrs) { - return null; - } - const res: OtherAttrItem[] = []; - let hasAttr = false; - - Object.keys(fromAttrs).forEach(fromKey => { - if (!validateOtherAttrs.includes(fromKey)) { - return; - } - - const toValue = toAttrs[fromKey]; - if (!isNil(toValue) && !isNil(fromAttrs[fromKey]) && toValue !== fromAttrs[fromKey]) { - if (fromKey === 'fill' || fromKey === 'stroke') { - res.push({ - from: - typeof fromAttrs[fromKey] === 'string' - ? ColorStore.Get(fromAttrs[fromKey] as unknown as string, ColorType.Color255) - : fromAttrs[fromKey], - to: typeof toValue === 'string' ? ColorStore.Get(toValue as string, ColorType.Color255) : toValue, - key: fromKey - }); - } else { - res.push({ from: fromAttrs[fromKey], to: toValue, key: fromKey }); - } - - hasAttr = true; - } - }); - - return hasAttr ? res : null; -}; - -export class MorphingPath extends ACustomAnimate { - declare path: CustomPath2D; - - saveOnEnd?: boolean; - otherAttrs?: OtherAttrItem[]; - - constructor( - config: { morphingData: MorphingDataItem[]; otherAttrs?: OtherAttrItem[]; saveOnEnd?: boolean }, - duration: number, - easing: EasingType - ) { - super(0, 1, duration, easing); - this.morphingData = config.morphingData; - this.otherAttrs = config.otherAttrs; - this.saveOnEnd = config.saveOnEnd; - } - - private morphingData?: MorphingDataItem[]; - - getEndProps(): Record { - return {}; - } - - onBind(): void { - (this.target as IGraphic).createPathProxy(); - this.onUpdate(false, 0, (this.target as IGraphic).attribute); - } - - onEnd(): void { - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - const target = this.target as IGraphic; - const pathProxy = typeof target.pathProxy === 'function' ? target.pathProxy(target.attribute) : target.pathProxy; - interpolateMorphingData(this.morphingData, pathProxy, ratio); - if (this.otherAttrs && this.otherAttrs.length) { - interpolateOtherAttrs(this.otherAttrs, out, ratio); - } - // 计算位置 - if (end && !this.saveOnEnd) { - (this.target as IGraphic).pathProxy = null; - } - } -} - -export const morphPath = ( - fromGraphic: IGraphic | null, - toGraphic: IGraphic, - animationConfig?: MorphingAnimateConfig, - fromGraphicTransform?: IMatrix -) => { - if (fromGraphic && (!fromGraphic.valid || !fromGraphic.toCustomPath)) { - if (__DEV__) { - console.error(fromGraphic, ' is not validate'); - } - return null; - } - - if (!toGraphic.valid || !toGraphic.toCustomPath) { - if (__DEV__) { - console.error(toGraphic, ' is not validate'); - } - return null; - } - - let fromTransform = fromGraphic?.globalTransMatrix; - - if (fromGraphicTransform && fromTransform) { - fromTransform = fromGraphicTransform - .clone() - .multiply(fromTransform.a, fromTransform.b, fromTransform.c, fromTransform.d, fromTransform.e, fromTransform.f); - } - const morphingData = parseMorphingData(fromGraphic?.toCustomPath?.(), toGraphic.toCustomPath(), { - fromTransform, - toTransfrom: toGraphic.globalTransMatrix - }); - - const attrs = parseOtherAnimateAttrs(fromGraphic?.attribute, toGraphic.attribute); - const animate = toGraphic.animate(animationConfig); - - if (animationConfig?.delay) { - animate.wait(animationConfig.delay); - } - - animate.play( - new MorphingPath( - { morphingData, otherAttrs: attrs }, - animationConfig?.duration ?? DefaultMorphingAnimateConfig.duration, - animationConfig?.easing ?? DefaultMorphingAnimateConfig.easing - ) - ); - - return animate; -}; - -export const oneToMultiMorph = ( - fromGraphic: IGraphic, - toGraphics: IGraphic[], - animationConfig?: MultiMorphingAnimateConfig -) => { - const validateToGraphics = toGraphics.filter(graphic => graphic && graphic.toCustomPath && graphic.valid); - if (!validateToGraphics.length) { - if (__DEV__) { - console.error(validateToGraphics, ' is not validate'); - } - } - - if (!fromGraphic.valid || !fromGraphic.toCustomPath) { - if (__DEV__) { - console.error(fromGraphic, ' is not validate'); - } - } - - const childGraphics: IGraphic[] = ( - animationConfig?.splitPath === 'clone' ? cloneGraphic : animationConfig?.splitPath ?? splitGraphic - )(fromGraphic, validateToGraphics.length, false); - - const oldOnEnd = animationConfig?.onEnd; - let count = validateToGraphics.length; - const onEachEnd = () => { - count--; - if (count === 0 && oldOnEnd) { - oldOnEnd(); - } - }; - - validateToGraphics.forEach((toChild, index) => { - const fromChild = childGraphics[index]; - const delay = - (animationConfig?.delay ?? 0) + - (animationConfig?.individualDelay - ? animationConfig.individualDelay(index, validateToGraphics.length, fromChild, toChild) - : 0); - morphPath( - fromChild, - toChild, - Object.assign({}, animationConfig, { onEnd: onEachEnd, delay }), - fromGraphic.globalTransMatrix - ); - }); -}; - -export class MultiToOneMorphingPath extends ACustomAnimate { - declare path: CustomPath2D; - - otherAttrs?: OtherAttrItem[][]; - - constructor( - config: { morphingData: MorphingDataItem[][]; otherAttrs?: OtherAttrItem[][] }, - duration: number, - easing: EasingType - ) { - super(0, 1, duration, easing); - this.morphingData = config.morphingData; - this.otherAttrs = config.otherAttrs; - } - - private morphingData?: MorphingDataItem[][]; - - getEndProps(): Record { - return {}; - } - - onBind(): void { - this.addPathProxy(); - } - - private addPathProxy() { - const shadowRoot = (this.target as IGraphic).shadowRoot; - - shadowRoot.forEachChildren(child => { - (child as IGraphic).createPathProxy(); - }); - - this.onUpdate(false, 0, (this.target as IGraphic).attribute); - } - - private clearPathProxy() { - const shadowRoot = (this.target as IGraphic).shadowRoot; - - shadowRoot.forEachChildren(child => { - (child as IGraphic).pathProxy = null; - }); - } - - onEnd(): void { - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - const shadowRoot = (this.target as IGraphic).shadowRoot; - - shadowRoot.forEachChildren((child: IGraphic, index) => { - interpolateMorphingData( - this.morphingData[index], - typeof child.pathProxy === 'function' ? child.pathProxy(child.attribute) : child.pathProxy, - ratio - ); - - if (this.otherAttrs?.[index] && this.otherAttrs[index].length) { - interpolateOtherAttrs(this.otherAttrs[index], child.attribute, ratio); - } - }); - - // 计算位置 - if (end) { - this.clearPathProxy(); - this.morphingData = null; - } - } -} - -const parseShadowChildAttrs = (graphicAttrs: Partial) => { - const attrs: Partial = {}; - - Object.keys(graphicAttrs).forEach(key => { - if (!isTransformKey(key)) { - attrs[key] = graphicAttrs[key]; - } - }); - - // if (attrs.fill == null) { - // attrs.fill = !!attrs.fillColor; - // } - // if (attrs.stroke == null) { - // attrs.stroke = !!attrs.strokeColor; - // } - - return attrs; -}; - -const appendShadowChildrenToGraphic = (graphic: IGraphic, children: IGraphic[], count: number) => { - const childAttrs = parseShadowChildAttrs(graphic.attribute); - const shadowRoot = graphic.attachShadow(); - - if (children.length) { - shadowRoot.setTheme({ - [children[0].type]: childAttrs - }); - children.forEach(element => { - element.setAttributes({ pickable: false }); - shadowRoot.appendChild(element); - }); - } else { - const box = graphic.AABBBounds; - const width = box.width(); - const height = box.height(); - - shadowRoot.setTheme({ - rect: childAttrs - }); - new Array(count).fill(0).forEach(el => { - const child = application.graphicService.creator.rect({ - x: 0, - y: 0, - width, - height: height, - pickable: false - }); - shadowRoot.appendChild(child); - children.push(child); - }); - } -}; - -export const cloneGraphic = (graphic: IGraphic, count: number, needAppend?: boolean) => { - const children: IGraphic[] = []; - const childAttrs = needAppend ? null : parseShadowChildAttrs(graphic.attribute); - const path = graphic.toCustomPath(); - - for (let i = 0; i < count; i++) { - const element = { - path: new CustomPath2D().fromCustomPath2D(path) - }; - - children.push( - application.graphicService.creator.path(needAppend ? element : Object.assign({}, childAttrs, element)) - ); - } - - if (needAppend) { - appendShadowChildrenToGraphic(graphic, children, count); - } - - return children; -}; - -export const splitGraphic = (graphic: IGraphic, count: number, needAppend?: boolean) => { - const children: IGraphic[] = []; - const childAttrs = needAppend ? null : parseShadowChildAttrs(graphic.attribute); - - if (graphic.type === 'rect') { - const childrenAttrs = splitRect(graphic as IRect, count); - childrenAttrs.forEach(element => { - children.push( - application.graphicService.creator.rect(needAppend ? element : Object.assign({}, childAttrs, element)) - ); - }); - } else if (graphic.type === 'arc') { - const childrenAttrs = splitArc(graphic as IArc, count); - childrenAttrs.forEach(element => { - children.push( - application.graphicService.creator.arc(needAppend ? element : Object.assign({}, childAttrs, element)) - ); - }); - } else if (graphic.type === 'circle') { - const childrenAttrs = splitCircle(graphic as ICircle, count); - childrenAttrs.forEach(element => { - children.push( - application.graphicService.creator.arc(needAppend ? element : Object.assign({}, childAttrs, element)) - ); - }); - } else if (graphic.type === 'line') { - const childrenAttrs = splitLine(graphic as ILine, count); - const defaultSymbol = { size: 10, symbolType: 'circle' }; - - childrenAttrs.forEach(element => { - children.push( - application.graphicService.creator.symbol( - needAppend ? Object.assign({}, element, defaultSymbol) : Object.assign({}, childAttrs, element, defaultSymbol) - ) - ); - }); - } else if (graphic.type === 'polygon') { - const childrenAttrs = splitPolygon(graphic as IPolygon, count); - childrenAttrs.forEach(element => { - children.push( - application.graphicService.creator.polygon(needAppend ? element : Object.assign({}, childAttrs, element)) - ); - }); - } else if (graphic.type === 'area') { - const childrenAttrs = splitArea(graphic as IArea, count); - childrenAttrs.forEach(element => { - children.push( - application.graphicService.creator.polygon(needAppend ? element : Object.assign({}, childAttrs, element)) - ); - }); - } else if (graphic.type === 'path') { - const childrenAttrs = splitPath(graphic as IPath, count); - childrenAttrs.forEach(element => { - if ('path' in element) { - children.push( - application.graphicService.creator.path(needAppend ? element : Object.assign({}, childAttrs, element)) - ); - } else { - children.push( - application.graphicService.creator.polygon(needAppend ? element : Object.assign({}, childAttrs, element)) - ); - } - }); - } - - if (needAppend) { - appendShadowChildrenToGraphic(graphic, children, count); - } - - return children; -}; - -/** - * 多对一动画 - * @param fromGraphics - * @param toGraphic - * @param animationConfig - */ -export const multiToOneMorph = ( - fromGraphics: IGraphic[], - toGraphic: IGraphic, - animationConfig?: MultiMorphingAnimateConfig -) => { - const validateFromGraphics = fromGraphics.filter(graphic => graphic.toCustomPath && graphic.valid); - if (!validateFromGraphics.length) { - if (__DEV__) { - console.error(fromGraphics, ' is not validate'); - } - } - - if (!toGraphic.valid || !toGraphic.toCustomPath) { - if (__DEV__) { - console.error(toGraphic, ' is not validate'); - } - } - - const childGraphics: IGraphic[] = ( - animationConfig?.splitPath === 'clone' ? cloneGraphic : animationConfig?.splitPath ?? splitGraphic - )(toGraphic, validateFromGraphics.length, true); - - const toAttrs = toGraphic.attribute; - toGraphic.setAttribute('visible', false); - - const morphingData = validateFromGraphics.map((graphic, index) => { - return parseMorphingData(graphic.toCustomPath(), childGraphics[index].toCustomPath(), { - fromTransform: graphic.globalTransMatrix, - toTransfrom: childGraphics[index].globalTransMatrix - }); - }); - const otherAttrs = validateFromGraphics.map((graphic, index) => { - return parseOtherAnimateAttrs(graphic.attribute, toAttrs); - }); - - if (animationConfig?.individualDelay) { - const oldOnEnd = animationConfig.onEnd; - let count = validateFromGraphics.length; - const onEachEnd = () => { - count--; - if (count === 0) { - toGraphic.setAttributes({ visible: true, ratio: null } as any, false, { - type: AttributeUpdateType.ANIMATE_END - }); - toGraphic.detachShadow(); - if (oldOnEnd) { - oldOnEnd(); - } - } - }; - childGraphics.forEach((to, index) => { - const delay = - (animationConfig.delay ?? 0) + - animationConfig.individualDelay(index, validateFromGraphics.length, fromGraphics[index], to); - const animate = to.animate(Object.assign({}, animationConfig, { onEnd: onEachEnd })); - animate.wait(delay); - - animate.play( - new MorphingPath( - { - morphingData: morphingData[index], - saveOnEnd: true, - otherAttrs: otherAttrs[index] - }, - animationConfig.duration ?? DefaultMorphingAnimateConfig.duration, - animationConfig.easing ?? DefaultMorphingAnimateConfig.easing - ) - ); - }); - } else { - const oldOnEnd = animationConfig?.onEnd; - const config = animationConfig ? Object.assign({}, animationConfig) : {}; - - config.onEnd = () => { - toGraphic.setAttribute('visible', true, false, { type: AttributeUpdateType.ANIMATE_END }); - toGraphic.detachShadow(); - - if (oldOnEnd) { - oldOnEnd(); - } - }; - - const animate = toGraphic.animate(config); - - if (animationConfig?.delay) { - animate.wait(animationConfig.delay); - } - - animate.play( - new MultiToOneMorphingPath( - { morphingData, otherAttrs }, - animationConfig?.duration ?? DefaultMorphingAnimateConfig.duration, - animationConfig?.easing ?? DefaultMorphingAnimateConfig.easing - ) - ); - } -}; diff --git a/packages/vrender-core/src/animate/timeline.ts b/packages/vrender-core/src/animate/timeline.ts deleted file mode 100644 index 463e8b684..000000000 --- a/packages/vrender-core/src/animate/timeline.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { AnimateStatus } from '../common/enums'; -import { Generator } from '../common/generator'; -import type { IAnimate, ITimeline } from '../interface'; - -// 管理一组动画 -export class DefaultTimeline implements ITimeline { - declare id: number; - protected declare animateHead: IAnimate | null; - protected declare animateTail: IAnimate | null; - protected declare ticker: any; - declare animateCount: number; - protected declare paused: boolean; - - constructor() { - this.id = Generator.GenAutoIncrementId(); - this.animateHead = null; - this.animateTail = null; - this.animateCount = 0; - this.paused = false; - } - - addAnimate(animate: IAnimate) { - if (!this.animateTail) { - this.animateHead = animate; - this.animateTail = animate; - } else { - this.animateTail.nextAnimate = animate; - animate.prevAnimate = this.animateTail; - this.animateTail = animate; - animate.nextAnimate = null; - } - this.animateCount++; - } - - pause() { - this.paused = true; - } - resume() { - this.paused = false; - } - - tick(delta: number) { - if (this.paused) { - return; - } - let animate = this.animateHead; - this.animateCount = 0; - while (animate) { - if (animate.status === AnimateStatus.END) { - this.removeAnimate(animate); - } else if (animate.status === AnimateStatus.RUNNING || animate.status === AnimateStatus.INITIAL) { - this.animateCount++; - animate.advance(delta); - } else if (animate.status === AnimateStatus.PAUSED) { - // 暂停 - this.animateCount++; - } - animate = animate.nextAnimate; - } - } - - clear() { - let animate = this.animateHead; - while (animate) { - animate.release(); - animate = animate.nextAnimate; - } - this.animateHead = null; - this.animateTail = null; - this.animateCount = 0; - } - - removeAnimate(animate: IAnimate, release: boolean = true) { - animate._onRemove && animate._onRemove.forEach(cb => cb()); - if (animate === this.animateHead) { - this.animateHead = animate.nextAnimate; - if (animate === this.animateTail) { - // 只有一个元素 - this.animateTail = null; - } else { - // 有多个元素 - this.animateHead.prevAnimate = null; - } - } else if (animate === this.animateTail) { - // 有多个元素 - this.animateTail = animate.prevAnimate; - this.animateTail.nextAnimate = null; - // animate.prevAnimate = null; - } else { - animate.prevAnimate.nextAnimate = animate.nextAnimate; - animate.nextAnimate.prevAnimate = animate.prevAnimate; - // animate不支持二次复用,不需要重置 - // animate.prevAnimate = null; - // animate.nextAnimate = null; - } - release && animate.release(); - - return; - } -} - -export const defaultTimeline = new DefaultTimeline(); diff --git a/packages/vrender-core/src/core/stage.ts b/packages/vrender-core/src/core/stage.ts index e4b009a81..7f4ba50d9 100644 --- a/packages/vrender-core/src/core/stage.ts +++ b/packages/vrender-core/src/core/stage.ts @@ -39,13 +39,12 @@ import { AutoRenderPlugin } from '../plugins/builtin-plugin/auto-render-plugin'; import { AutoRefreshPlugin } from '../plugins/builtin-plugin/auto-refresh-plugin'; import { IncrementalAutoRenderPlugin } from '../plugins/builtin-plugin/incremental-auto-render-plugin'; import { DirtyBoundsPlugin } from '../plugins/builtin-plugin/dirty-bounds-plugin'; -import { defaultTicker } from '../animate/default-ticker'; import { SyncHook } from '../tapable'; import { LayerService } from './constants'; -import { DefaultTimeline } from '../animate'; import { application } from '../application'; import { isBrowserEnv } from '../env-check'; import { Factory } from '../factory'; +import { Graphic } from '../graphic'; const DefaultConfig = { WIDTH: 500, @@ -284,11 +283,7 @@ export class Stage extends Group implements IStage { this.hooks.afterRender.tap('constructor', this.afterRender); this._beforeRender = params.beforeRender; this._afterRender = params.afterRender; - this.ticker = params.ticker || defaultTicker; this.supportInteractiveLayer = params.interactiveLayer !== false; - this.timeline = new DefaultTimeline(); - this.ticker.addTimeline(this.timeline); - this.timeline.pause(); if (!params.optimize) { params.optimize = { animateMode: 'performance' @@ -299,7 +294,24 @@ export class Stage extends Group implements IStage { if (params.background && isString(this._background) && this._background.includes('/')) { this.setAttributes({ background: this._background }); } - this.ticker.on('afterTick', this.afterTickCb); + + this.initAnimate(params); + } + + initAnimate(params: Partial) { + if (Graphic.Ticker && Graphic.Timeline) { + this.ticker = params.ticker || new Graphic.Ticker(this); + this.timeline = new Graphic.Timeline(); + this.ticker.addTimeline(this.timeline); + this.ticker.on('tick', this.afterTickCb); + } + } + + startAnimate() { + if (this.ticker && this.timeline) { + this.ticker.start(); + this.timeline.resume(); + } } pauseRender(sr: number = -1) { @@ -725,14 +737,9 @@ export class Stage extends Group implements IStage { if (this.releaseStatus === 'released') { return; } - this.ticker.start(); - this.timeline.resume(); + this.startAnimate(); const state = this.state; this.state = 'rendering'; - // 判断是否需要手动执行tick - if (!this.tickedBeforeRender) { - this.ticker.trySyncTickStatus(); - } this.layerService.prepareStageLayer(this); if (!this._skipRender) { this.lastRenderparams = params; @@ -801,8 +808,7 @@ export class Stage extends Group implements IStage { if (this.releaseStatus === 'released') { return; } - this.timeline.resume(); - this.ticker.start(); + this.startAnimate(); const state = this.state; this.state = 'rendering'; this.layerService.prepareStageLayer(this); @@ -961,10 +967,6 @@ export class Stage extends Group implements IStage { return false; } - // 动画相关 - startAnimate(t: number): void { - throw new Error('暂不支持'); - } setToFrame(t: number): void { throw new Error('暂不支持'); } @@ -990,8 +992,8 @@ export class Stage extends Group implements IStage { this.interactiveLayer.release(); } this.window.release(); - this.ticker.remTimeline(this.timeline); - this.ticker.removeListener('afterTick', this.afterTickCb); + this.ticker?.remTimeline(this?.timeline); + this.ticker?.removeListener('afterTick', this.afterTickCb); this.renderService.renderTreeRoots = []; } diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index b4239bf63..519df965e 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -27,6 +27,7 @@ import type { import { Node } from './node-tree'; import type { IAnimate, + IAnimateConstructor, IAnimateTarget, IGlyphGraphicAttribute, IGroup, @@ -36,12 +37,13 @@ import type { IStage, IStep, ISubAnimate, - ISymbolClass + ISymbolClass, + ITickerConstructor, + ITimelineConstructor } from '../interface'; import { EventTarget, CustomEvent } from '../event'; import { DefaultTransform } from './config'; import { application } from '../application'; -import { Animate, DefaultStateAnimateConfig, defaultTimeline } from '../animate'; import { interpolateColor } from '../color-string/interpolate'; import { CustomPath2D } from '../common/custom-path2d'; import { ResourceLoader } from '../resource-loader/loader'; @@ -52,6 +54,7 @@ import { parsePadding } from '../common/utils'; import { builtinSymbolsMap, builtInSymbolStrMap, CustomSymbolClass } from './builtin-symbol'; import { isSvg, XMLParser } from '../common/xml'; import { SVG_PARSE_ATTRIBUTE_MAP, SVG_PARSE_ATTRIBUTE_MAP_KEYS } from './constants'; +import { DefaultStateAnimateConfig } from '../animate/config'; const _tempBounds = new AABBBounds(); /** @@ -166,6 +169,10 @@ export abstract class Graphic = Partial, IAnimateTarget { + static Animate: IAnimateConstructor; + static Timeline: ITimelineConstructor; + static Ticker: ITickerConstructor; + static defaultTimeline: ITimelineConstructor; /** * Mixes all enumerable properties and methods from a source object to Element. * @param source - The source of properties and methods to mix in. @@ -910,7 +917,7 @@ export abstract class Graphic = Partial = Partial { - if (a.timeline === defaultTimeline) { + if (a.timeline.isGlobal) { a.setTimeline(timeline); } }); diff --git a/packages/vrender-core/src/index.ts b/packages/vrender-core/src/index.ts index 1f15501d5..6b0d09ef6 100644 --- a/packages/vrender-core/src/index.ts +++ b/packages/vrender-core/src/index.ts @@ -15,7 +15,6 @@ export * from './core'; export * from './core/light'; export * from './core/camera'; export * from './picker'; -export * from './animate'; export * from './resource-loader/loader'; export * from './color-string'; export * from './factory'; @@ -52,7 +51,6 @@ export * from './allocator/matrix-allocate'; export * from './allocator/canvas-allocate'; export * from './allocator/graphic-allocate'; -export * from './animate/default-ticker'; export { wrapCanvas, wrapContext } from './canvas/util'; export * from './common/xml'; export * from './common/inversify-lite'; @@ -103,5 +101,4 @@ export * from './plugins/builtin-plugin/react-attribute-plugin'; export * from './plugins/builtin-plugin/3dview-transform-plugin'; export * from './plugins/builtin-plugin/flex-layout-plugin'; -export * from './animate/easing-func'; export * from './plugins/builtin-plugin/edit-module'; diff --git a/packages/vrender-core/src/interface/animate.ts b/packages/vrender-core/src/interface/animate.ts index 85104bda2..4fc220968 100644 --- a/packages/vrender-core/src/interface/animate.ts +++ b/packages/vrender-core/src/interface/animate.ts @@ -181,6 +181,8 @@ export interface ICustomAnimate { getMergedEndProps: () => Record | void; } +export type IAnimateConstructor = new (...args: any[]) => IAnimate; + // 每一个animate绑定一个graphic,用于描述这个graphic的动画内容 // 在timeline层面,animate相当于是一段timeslice export interface IAnimate { @@ -209,6 +211,7 @@ export interface IAnimate { onStart: (cb: () => void) => void; onEnd: (cb: () => void) => void; onFrame: (cb: (step: IStep, ratio: number) => void) => void; + onRemove: (cb: () => void) => void; // 屏蔽属性 preventAttr: (key: string) => void; // 屏蔽属性 @@ -309,6 +312,7 @@ export interface MultiMorphingAnimateConfig extends MorphingAnimateConfig { export interface ITimeline { id: number; animateCount: number; + isGlobal: boolean; addAnimate: (animate: IAnimate) => void; removeAnimate: (animate: IAnimate, release?: boolean) => void; tick: (delta: number) => void; @@ -317,6 +321,10 @@ export interface ITimeline { resume: () => void; } +export type ITimelineConstructor = new (...args: any[]) => ITimeline; + +export type ITickerConstructor = new (...args: any[]) => ITicker; + export interface ITickHandler extends Releaseable { avaliable: () => boolean; /** diff --git a/packages/vrender-core/src/plugins/builtin-plugin/richtext-edit-plugin.ts b/packages/vrender-core/src/plugins/builtin-plugin/richtext-edit-plugin.ts index ba20f03f4..29eefa459 100644 --- a/packages/vrender-core/src/plugins/builtin-plugin/richtext-edit-plugin.ts +++ b/packages/vrender-core/src/plugins/builtin-plugin/richtext-edit-plugin.ts @@ -8,6 +8,7 @@ import { createRichText, createText, getRichTextBounds, + Graphic, RichText } from '../../graphic'; import type { @@ -27,7 +28,6 @@ import type { ITicker, ITimeline } from '../../interface'; -import { Animate, DefaultTicker, DefaultTimeline } from '../../animate'; import { EditModule, findConfigIndexByCursorIdx, getDefaultCharacterConfig } from './edit-module'; import { application } from '../../application'; import { getWordStartEndIdx } from '../../graphic/richtext/utils'; @@ -148,8 +148,8 @@ export class RichTextEditPlugin implements IPlugin { shadowPlaceHolder: IRichText; shadowBounds: IRect; - ticker: ITicker; - timeline: ITimeline; + ticker?: ITicker; + timeline?: ITimeline; currRt: IRichText; @@ -208,8 +208,8 @@ export class RichTextEditPlugin implements IPlugin { this.commandCbs.set(FORMAT_TEXT_COMMAND, [this.formatTextCommandCb]); this.commandCbs.set(FORMAT_ALL_TEXT_COMMAND, [this.formatAllTextCommandCb]); this.updateCbs = []; - this.timeline = new DefaultTimeline(); - this.ticker = new DefaultTicker([this.timeline]); + this.timeline = Graphic.Timeline && new Graphic.Timeline(); + this.ticker = Graphic.Ticker && new Graphic.Ticker([this.timeline]); this.deltaX = 0; this.deltaY = 0; } @@ -933,6 +933,9 @@ export class RichTextEditPlugin implements IPlugin { } protected addAnimateToLine(line: ILine) { + if (!Graphic.Animate) { + return; + } line.setAttributes({ opacity: 1 }); line.animates && line.animates.forEach(animate => { diff --git a/packages/vrender/__tests__/browser/src/pages/animate-next.ts b/packages/vrender/__tests__/browser/src/pages/animate-next.ts index bbcbb6836..52b721fcc 100644 --- a/packages/vrender/__tests__/browser/src/pages/animate-next.ts +++ b/packages/vrender/__tests__/browser/src/pages/animate-next.ts @@ -1,4 +1,4 @@ -import { DefaultTicker, DefaultTimeline, Animate } from '@visactor/vrender-animate'; +import { DefaultTicker, DefaultTimeline, Animate, registerAnimate } from '@visactor/vrender-animate'; import { container, createRect, @@ -13,6 +13,8 @@ import { vglobal.setEnv('browser'); +registerAnimate(); + let stage: any; function addCase(name: string, container: HTMLElement, cb: (stage: any) => void) { @@ -83,7 +85,7 @@ export const page = () => { }); stage.defaultLayer.add(rect); const ticker = new DefaultTicker(stage); - ticker.setFPS(10); + ticker.setFPS(30); const timeline = new DefaultTimeline(); ticker.addTimeline(timeline); @@ -98,11 +100,40 @@ export const page = () => { ticker.on('tick', () => { stage.render(); }); - function run() { - requestAnimationFrame(() => { - run(); - }); - } - run(); + }); + + addCase('Animate chain', btnContainer, stage => { + const rect = createRect({ + x: 100, + y: 100, + width: 100, + height: 100, + fill: 'red' + }); + stage.defaultLayer.add(rect); + + rect.animate().to({ x: 300 }, 1000, 'linear').to({ y: 300 }, 1000, 'linear').to({ fill: 'blue' }, 1000, 'linear'); + // 中途设置值没问题,它会从orange开始 + rect.setAttribute('fill', 'orange'); + }); + addCase('Animate chain loop', btnContainer, stage => { + const rect = createRect({ + x: 100, + y: 100, + width: 100, + height: 100, + fill: 'red' + }); + stage.defaultLayer.add(rect); + + rect + .animate() + .to({ x: 300 }, 1000, 'linear') + .to({ y: 300 }, 1000, 'linear') + .to({ fill: 'blue' }, 1000, 'linear') + .loop(2) + .bounce(true); + // 中途设置值没问题,它会从orange开始 + rect.setAttribute('fill', 'purple'); }); }; From d5c66e3350ac7392ef397ab8ae5f8b19693c9a71 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 14 Mar 2025 18:46:40 +0800 Subject: [PATCH 034/179] feat: support bounce --- packages/vrender-animate/src/animate.ts | 14 ++++++--- .../browser/src/pages/animate-next.ts | 31 +++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/packages/vrender-animate/src/animate.ts b/packages/vrender-animate/src/animate.ts index 076433882..e167c4ce7 100644 --- a/packages/vrender-animate/src/animate.ts +++ b/packages/vrender-animate/src/animate.ts @@ -519,15 +519,21 @@ export class Animate implements IAnimate { let cycleTime = nextTime - this._startTime; let newLoop = false; + let bounceTime = false; if (this._loopCount > 0) { - cycleTime = cycleTime % this._duration; - const currentLoop = Math.floor(nextTime / this._duration); + cycleTime = (nextTime - this._startTime) % this._duration; + const currentLoop = Math.floor((nextTime - this._startTime) / this._duration); newLoop = currentLoop > this._currentLoop; this._currentLoop = currentLoop; + + bounceTime = this._bounce && currentLoop % 2 === 1; + if (bounceTime) { + cycleTime = this._duration - cycleTime; + } } - // 如果是反转动画,需要反转周期内的时间 - if (newLoop) { + // 如果是新的循环,重置为初始状态 + if (newLoop && !bounceTime) { this.target.setAttributes(this._startProps); } diff --git a/packages/vrender/__tests__/browser/src/pages/animate-next.ts b/packages/vrender/__tests__/browser/src/pages/animate-next.ts index 52b721fcc..01a2488fa 100644 --- a/packages/vrender/__tests__/browser/src/pages/animate-next.ts +++ b/packages/vrender/__tests__/browser/src/pages/animate-next.ts @@ -136,4 +136,35 @@ export const page = () => { // 中途设置值没问题,它会从orange开始 rect.setAttribute('fill', 'purple'); }); + + addCase('Bounce Demo', btnContainer, stage => { + const rect = createRect({ + x: 100, + y: 100, + width: 100, + height: 100, + fill: 'green' + }); + stage.defaultLayer.add(rect); + + // Create a bouncing animation that moves right, then down, then changes color + // With bounce enabled, it will play forward then backward + rect + .animate() + .to({ x: 400 }, 1000, 'linear') + .to({ y: 400 }, 1000, 'linear') + .to({ fill: 'yellow' }, 1000, 'linear') + .loop(3) // Play the animation 3 times + .bounce(true); // Enable bounce so it goes forward and backward + + // Add explanatory text + const text = createText({ + x: 100, + y: 50, + text: 'Bounce Demo: Animation plays forward then backward with bounce(true)', + fontSize: 16, + fill: 'black' + }); + stage.defaultLayer.add(text); + }); }; From 1e2a39eda16cf99a26a54edede29489fdd1bdec0 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 14 Mar 2025 20:07:10 +0800 Subject: [PATCH 035/179] fix: fix issue with add animate --- packages/vrender-animate/src/animate.ts | 10 ++- .../vrender-animate/src/intreface/animate.ts | 1 + packages/vrender-animate/src/register.ts | 8 +-- packages/vrender-animate/src/timeline.ts | 7 +- packages/vrender-core/src/graphic/graphic.ts | 5 +- .../browser/src/pages/animate-next.ts | 72 ++++++++++++++++++- 6 files changed, 89 insertions(+), 14 deletions(-) diff --git a/packages/vrender-animate/src/animate.ts b/packages/vrender-animate/src/animate.ts index e167c4ce7..1d2437404 100644 --- a/packages/vrender-animate/src/animate.ts +++ b/packages/vrender-animate/src/animate.ts @@ -430,7 +430,7 @@ export class Animate implements IAnimate { // 计算所有动画结束的最大时间点 let maxEndTime = 0; list.forEach(animate => { - const endTime = animate.getStartTime() + animate.getDuration(); + const endTime = animate.getStartTime() + animate.getTotalDuration(); maxEndTime = Math.max(maxEndTime, endTime); }); @@ -447,7 +447,7 @@ export class Animate implements IAnimate { } // 计算指定动画结束的时间点 - const endTime = animate.getStartTime() + animate.getDuration(); + const endTime = animate.getStartTime() + animate.getTotalDuration(); // 设置当前动画的开始时间为结束时间 return this.startAt(endTime); @@ -497,9 +497,9 @@ export class Animate implements IAnimate { */ advance(delta: number): void { const nextTime = this.currentTime + delta; - // 如果还没开始,直接return if (nextTime < this._startTime) { + this.currentTime = nextTime; return; } // 如果已经结束,设置状态后return @@ -599,4 +599,8 @@ export class Animate implements IAnimate { this._duration = this._lastStep.getStartTime() + this._lastStep.getDuration(); this._totalDuration = this._duration * (this._loopCount + 1); } + + getTotalDuration(): number { + return this._totalDuration; + } } diff --git a/packages/vrender-animate/src/intreface/animate.ts b/packages/vrender-animate/src/intreface/animate.ts index dcfac8007..cd69c3603 100644 --- a/packages/vrender-animate/src/intreface/animate.ts +++ b/packages/vrender-animate/src/intreface/animate.ts @@ -112,6 +112,7 @@ export interface IAnimate { release: () => void; // 获取持续的时长 getDuration: () => number; + getTotalDuration: () => number; // 获取动画开始时间(注意并不是子动画的startAt) getStartTime: () => number; // 等待delay diff --git a/packages/vrender-animate/src/register.ts b/packages/vrender-animate/src/register.ts index ec35eef44..53b25629f 100644 --- a/packages/vrender-animate/src/register.ts +++ b/packages/vrender-animate/src/register.ts @@ -1,6 +1,6 @@ import { Graphic } from '@visactor/vrender-core'; import { Animate } from './animate'; -import { defaultTimeline, DefaultTimeline } from './timeline'; +import { DefaultTimeline } from './timeline'; import { DefaultTicker } from './ticker/default-ticker'; export function registerAnimate() { @@ -13,10 +13,4 @@ export function registerAnimate() { if (!(Graphic as any).Ticker) { (Graphic as any).Ticker = DefaultTicker; } - if (!(Graphic as any).defaultTicker) { - (Graphic as any).defaultTicker = DefaultTicker; - } - if (!(Graphic as any).defaultTimeline) { - (Graphic as any).defaultTimeline = defaultTimeline; - } } diff --git a/packages/vrender-animate/src/timeline.ts b/packages/vrender-animate/src/timeline.ts index 8384e1c7b..6ce0189ec 100644 --- a/packages/vrender-animate/src/timeline.ts +++ b/packages/vrender-animate/src/timeline.ts @@ -91,8 +91,10 @@ export class DefaultTimeline implements ITimeline { if (this._endAnimatePtr < 0) { return; } - animate._onRemove && animate._onRemove.forEach(cb => cb()); - release && animate.release(); + if (release) { + animate._onRemove && animate._onRemove.forEach(cb => cb()); + animate.release(); + } index = index ?? this.animates.indexOf(animate); // 交换位置 @@ -149,5 +151,6 @@ export class DefaultTimeline implements ITimeline { } } +// 不会使用,存粹做临时存储用,请一定要放置到stage中才行 export const defaultTimeline = new DefaultTimeline(); defaultTimeline.isGlobal = true; diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index 519df965e..8ca151780 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -172,7 +172,6 @@ export abstract class Graphic = Partial = Partial = Partial { if (a.timeline.isGlobal) { a.setTimeline(timeline); + timeline.addAnimate(a); } }); } diff --git a/packages/vrender/__tests__/browser/src/pages/animate-next.ts b/packages/vrender/__tests__/browser/src/pages/animate-next.ts index 01a2488fa..d5543e41c 100644 --- a/packages/vrender/__tests__/browser/src/pages/animate-next.ts +++ b/packages/vrender/__tests__/browser/src/pages/animate-next.ts @@ -13,7 +13,7 @@ import { vglobal.setEnv('browser'); -registerAnimate(); +// registerAnimate(); let stage: any; @@ -167,4 +167,74 @@ export const page = () => { }); stage.defaultLayer.add(text); }); + addCase('Animate Schedule', btnContainer, stage => { + const rect = createRect({ + x: 100, + y: 100, + width: 100, + height: 100, + fill: 'green' + }); + stage.defaultLayer.add(rect); + + // Create a bouncing animation that moves right, then down, then changes color + // With bounce enabled, it will play forward then backward + const rectAnimate = rect + .animate() + .to({ x: 400 }, 1000, 'linear') + .to({ y: 400 }, 1000, 'linear') + .to({ fill: 'yellow' }, 1000, 'linear') + .loop(3) // Play the animation 3 times + .bounce(true); // Enable bounce so it goes forward and backward + + // Add explanatory text + const text = createText({ + x: 300, + y: 50, + text: 'Animate Schedule', + fontSize: 16, + fill: 'black', + textAlign: 'center', + opacity: 0 + }); + const textAnimate = text.animate().to({ opacity: 1 }, 1000, 'linear'); + textAnimate.after(rectAnimate); + stage.defaultLayer.add(text); + }); + + addCase('startAt', btnContainer, stage => { + const rect = createRect({ + x: 100, + y: 100, + width: 100, + height: 100, + fill: 'green' + }); + stage.defaultLayer.add(rect); + + // Create a bouncing animation that moves right, then down, then changes color + // With bounce enabled, it will play forward then backward + const rectAnimate = rect + .animate() + .startAt(2000) + .to({ x: 400 }, 1000, 'linear') + .to({ y: 400 }, 1000, 'linear') + .to({ fill: 'yellow' }, 1000, 'linear') + .loop(3) // Play the animation 3 times + .bounce(true); // Enable bounce so it goes forward and backward + + // Add explanatory text + const text = createText({ + x: 300, + y: 50, + text: 'Animate Schedule', + fontSize: 16, + fill: 'black', + textAlign: 'center', + opacity: 0 + }); + const textAnimate = text.animate().to({ opacity: 1 }, 1000, 'linear'); + textAnimate.after(rectAnimate); + stage.defaultLayer.add(text); + }); }; From 7d801f374e5a85f525dcf9bc8ec511990eb537a4 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Mon, 17 Mar 2025 12:04:40 +0800 Subject: [PATCH 036/179] feat: support custom animate --- packages/vrender-animate/src/animate.ts | 107 ++++++++++++------ .../src/custom/custom-animate.ts | 21 ++++ packages/vrender-animate/src/index.ts | 1 + .../vrender-animate/src/intreface/animate.ts | 22 +++- packages/vrender-animate/src/step.ts | 51 +++++++-- .../browser/src/pages/animate-next.ts | 5 +- 6 files changed, 155 insertions(+), 52 deletions(-) create mode 100644 packages/vrender-animate/src/custom/custom-animate.ts diff --git a/packages/vrender-animate/src/animate.ts b/packages/vrender-animate/src/animate.ts index 1d2437404..85a1de241 100644 --- a/packages/vrender-animate/src/animate.ts +++ b/packages/vrender-animate/src/animate.ts @@ -1,9 +1,9 @@ -import type { IAnimate, IStep } from './intreface/animate'; +import type { IAnimate, IStep, ICustomAnimate } from './intreface/animate'; import type { EasingType } from './intreface/easing'; import { AnimateStatus, AnimateStepType } from './intreface/type'; import { Step } from './step'; import type { ITimeline } from './intreface/timeline'; -import { Generator, type ICustomAnimate, type IGraphic } from '@visactor/vrender-core'; +import { Generator, type IGraphic } from '@visactor/vrender-core'; import { defaultTimeline } from './timeline'; export class Animate implements IAnimate { @@ -115,8 +115,16 @@ export class Animate implements IAnimate { */ to(props: Record, duration: number = 300, easing: EasingType = 'linear'): this { // 创建新的step - const step = new Step(AnimateStepType.to, props, duration, easing, this); + const step = new Step(AnimateStepType.to, props, duration, easing); + this.updateStepAfterAppend(step); + + step.bind(this.target, this); + + return this; + } + + protected updateStepAfterAppend(step: IStep): void { // 如果是第一个step if (!this._firstStep) { this._firstStep = step; @@ -127,6 +135,23 @@ export class Animate implements IAnimate { this._lastStep = step; } + this.parseStepProps(step); + + this.updateDuration(); + } + + /** + * 解析step的props + * 1. 预先获取step的propKeys并保存 + * 2. 将截止目前的最新props设置到step.props中,这样该props上的属性就是最终的属性了,跳帧时直接设置即可 + * 3. 同步到_endProps中,保存这个Animate实例的最终props + * 4. 给step的props的原型链上绑定Animate的_startProps,这样在下一个step查找fromProps的时候,一定能拿得到值 + */ + parseStepProps(step: IStep) { + if (!this._lastStep) { + return; + } + /* 预设置step的属性,基于性能考虑,实现比较复杂 */ // step.propKeys为真实的props属性的key step.propKeys = step.propKeys || Object.keys(step.props); @@ -144,10 +169,39 @@ export class Animate implements IAnimate { // rect.animate().to({ x: 100 }, 1000, 'linear').to({ y: 100 }, 1000, 'linear'); // 在第二个step查找fromProps的时候,就能拿到第一个step的endProps中的y值(在原型链上) Object.setPrototypeOf(step.props, this._startProps); + } - this.updateDuration(); - - return this; + /** + * 重新同步和计算props,用于内部某些step发生了变更后,重新计算自身 + * 性能较差,不要频繁调用 + * @returns + */ + reSyncProps() { + if (!this._lastStep) { + return; + } + this._endProps = {}; + let currentStep: IStep = this._firstStep; + // 从前向后寻找当前时间所在的step + while (currentStep) { + // step.props为包含前序step的props的最终props,用于跳帧等场景,可以直接设置 + // eslint-disable-next-line no-loop-func + Object.keys(this._endProps).forEach(key => { + currentStep.props[key] = currentStep.props[key] ?? this._endProps[key]; + }); + // 将最终的props设置到step.props中 + // eslint-disable-next-line no-loop-func + currentStep.propKeys.forEach(key => { + this._endProps[key] = currentStep.props[key]; + }); + // 给step的props的原型链上绑定Animate的_startProps + // 下一个step在查找上一个step.props(也就是找到它的fromProps)的时候,就能拿到初始的props了 + // 比如: + // rect.animate().to({ x: 100 }, 1000, 'linear').to({ y: 100 }, 1000, 'linear'); + // 在第二个step查找fromProps的时候,就能拿到第一个step的endProps中的y值(在原型链上) + Object.setPrototypeOf(currentStep.props, this._startProps); + currentStep = currentStep.next; + } } /** @@ -157,7 +211,7 @@ export class Animate implements IAnimate { */ from(props: Record, duration: number = 300, easing: EasingType = 'linear'): this { // 创建新的step - const step = new Step(AnimateStepType.from, props, duration, easing, this); + const step = new Step(AnimateStepType.from, props, duration, easing); // 如果是第一个step if (!this._firstStep) { @@ -174,6 +228,17 @@ export class Animate implements IAnimate { return this; } + /** + * 自定义动画 + */ + play(customAnimate: ICustomAnimate): this { + this.updateStepAfterAppend(customAnimate); + + customAnimate.bind(this.target, this); + + return this; + } + /** * 暂停动画 */ @@ -303,28 +368,6 @@ export class Animate implements IAnimate { return false; } - /** - * 自定义动画 - */ - play(customAnimate: ICustomAnimate): this { - // 创建新的step - const step = new Step(AnimateStepType.customAnimate, { customAnimate }, 0, 'linear', this); - - // 如果是第一个step - if (!this._firstStep) { - this._firstStep = step; - this._lastStep = step; - } else { - // 添加到链表末尾 - this._lastStep.append(step); - this._lastStep = step; - } - - this.updateDuration(); - - return this; - } - /** * 获取起始值,该起始值为animate的起始值,并不一定为step的起始值 */ @@ -402,7 +445,7 @@ export class Animate implements IAnimate { */ wait(delay: number): this { // 创建新的wait step - const step = new Step(AnimateStepType.wait, {}, delay, 'linear', this); + const step = new Step(AnimateStepType.wait, {}, delay, 'linear'); // 如果是第一个step if (!this._firstStep) { @@ -576,7 +619,7 @@ export class Animate implements IAnimate { // ratio = Math.max(0, Math.min(1, ratio)); const isEnd = ratio >= 1; - targetStep.onUpdate(isEnd, ratio, {}); + targetStep.update(isEnd, ratio, {}); // 如果step执行完毕 if (isEnd) { @@ -590,7 +633,7 @@ export class Animate implements IAnimate { // } } - protected updateDuration(): void { + updateDuration(): void { if (!this._lastStep) { this._duration = 0; return; diff --git a/packages/vrender-animate/src/custom/custom-animate.ts b/packages/vrender-animate/src/custom/custom-animate.ts new file mode 100644 index 000000000..45ac3a89a --- /dev/null +++ b/packages/vrender-animate/src/custom/custom-animate.ts @@ -0,0 +1,21 @@ +import type { ICustomAnimate } from '../intreface/animate'; +import type { IAnimateStepType } from '../intreface/type'; +import { Step } from '../step'; + +export abstract class ACustomAnimate extends Step implements ICustomAnimate { + type: IAnimateStepType = 'customAnimate'; + + get from() { + return this.getFromProps(); + } + + get to() { + return this.getEndProps(); + } + + protected setProps(props: Record) { + this.props = props; + this.propKeys = Object.keys(props); + this.animate.reSyncProps(); + } +} diff --git a/packages/vrender-animate/src/index.ts b/packages/vrender-animate/src/index.ts index a19f5bb64..015a2168a 100644 --- a/packages/vrender-animate/src/index.ts +++ b/packages/vrender-animate/src/index.ts @@ -14,3 +14,4 @@ export { Step } from './step'; // 导出工具函数 export * from './utils/easing-func'; export { registerAnimate } from './register'; +export { ACustomAnimate } from './custom/custom-animate'; diff --git a/packages/vrender-animate/src/intreface/animate.ts b/packages/vrender-animate/src/intreface/animate.ts index cd69c3603..781da095a 100644 --- a/packages/vrender-animate/src/intreface/animate.ts +++ b/packages/vrender-animate/src/intreface/animate.ts @@ -1,8 +1,12 @@ -import type { ICustomAnimate, IGraphic } from '@visactor/vrender-core'; +import type { IGraphic } from '@visactor/vrender-core'; import type { EasingType, EasingTypeFunc } from './easing'; import type { AnimateStatus, IAnimateStepType } from './type'; import type { ITimeline } from './timeline'; +export interface ICustomAnimate extends IStep { + type: IAnimateStepType; +} + export interface IStep { type: IAnimateStepType; prev?: IStep; @@ -32,10 +36,6 @@ export interface IStep { setDuration: (duration: number, updateDownstream?: boolean) => void; // 获取持续时间 getDuration: () => number; - // // 确定插值函数(在开始的时候就确定,避免每次tick都解析) - // determineInterpolationFunction: () => void; - // // 确定更新函数(在开始的时候就确定,避免每次tick都解析) - // determineUpdateFunction: () => void; // 确定插值更新函数(在开始的时候就确定,避免每次tick都解析) determineInterpolateUpdateFunction: () => void; @@ -44,6 +44,7 @@ export interface IStep { // 获取开始时间 getStartTime: () => number; + bind: (target: IGraphic, animate: IAnimate) => void; // 在第一次绑定到Animate的时候触发 onBind: () => void; // 第一次执行的时候调用 @@ -53,7 +54,12 @@ export interface IStep { // 结束执行的时候调用(如果有循环,那每个周期都会调用) onEnd: (cb?: (animate: IAnimate, step: IStep) => void) => void; // 更新执行的时候调用(如果有循环,那每个周期都会调用) + update: (end: boolean, ratio: number, out: Record) => void; onUpdate: (end: boolean, ratio: number, out: Record) => void; + + getEndProps: () => Record | void; + getFromProps: () => Record | void; + getMergedEndProps: () => Record | void; } export interface IAnimate { @@ -138,4 +144,10 @@ export interface IAnimate { // 设置开始时间(startAt之前是完全不会进入动画生命周期的) // 它和wait不一样,如果调用的是wait,wait过程中还算是一个动画阶段,只是空的阶段,而startAt之前是完全不会进入动画生命周期的 startAt: (t: number) => IAnimate; + + // 重新同步和计算props,用于内部某些step发生了变更后,重新计算自身 + reSyncProps: () => void; + + // 更新duration + updateDuration: () => void; } diff --git a/packages/vrender-animate/src/step.ts b/packages/vrender-animate/src/step.ts index 2889a92bc..841a70f29 100644 --- a/packages/vrender-animate/src/step.ts +++ b/packages/vrender-animate/src/step.ts @@ -38,29 +38,25 @@ export class Step implements IStep { protected _endCb?: (animate: IAnimate, step: IStep) => void; - constructor( - type: IAnimateStepType, - props: Record, - duration: number, - easing: EasingType, - animate: IAnimate - ) { + constructor(type: IAnimateStepType, props: Record, duration: number, easing: EasingType) { this.type = type; this.props = props; this.duration = duration; - this.animate = animate; - this.target = animate.target; // 设置缓动函数 if (easing) { this.easing = typeof easing === 'function' ? easing : Easing[easing]; } - - this.onBind(); if (type === 'wait') { this.onUpdate = noop; } } + bind(target: IGraphic, animate: IAnimate): void { + this.target = target; + this.animate = animate; + this.onBind(); + } + append(step: IStep): void { this.next = step; step.prev = this; @@ -79,6 +75,7 @@ export class Step implements IStep { currentStartTime += currentStep.duration; currentStep = currentStep.next; } + this.animate.updateDuration(); } getLastProps(): any { @@ -177,11 +174,13 @@ export class Step implements IStep { * 更新执行的时候调用 * 如果跳帧了就不一定会执行 */ - onUpdate = (end: boolean, ratio: number, out: Record): void => { + update = (end: boolean, ratio: number, out: Record): void => { + // TODO 需要修复,只有在开始的时候才调用 this.onStart(); if (!this.props || !this.propKeys) { return; } + this.onUpdate(end, ratio, out); // 应用缓动函数 const easedRatio = this.easing(ratio); this.interpolateUpdateFunctions.forEach((func, index) => { @@ -192,6 +191,10 @@ export class Step implements IStep { }); }; + onUpdate = (end: boolean, ratio: number, out: Record): void => { + // ... + }; + /** * 结束执行的时候调用 * 如果跳帧了就不一定会执行 @@ -204,4 +207,28 @@ export class Step implements IStep { this._endCb(this.animate, this); } } + + /** + * 获取结束的属性,包含前序的终值,是merge过的 + * @returns + */ + getEndProps(): Record | void { + return this.props; + } + + /** + * 获取开始的属性,是前序的终值 + * @returns + */ + getFromProps(): Record | void { + return this.fromProps; + } + + /** + * 获取结束的属性,包含前序的终值,是merge过的,同getEndProps + * @returns + */ + getMergedEndProps(): Record | void { + return this.getEndProps(); + } } diff --git a/packages/vrender/__tests__/browser/src/pages/animate-next.ts b/packages/vrender/__tests__/browser/src/pages/animate-next.ts index d5543e41c..8580ac3d5 100644 --- a/packages/vrender/__tests__/browser/src/pages/animate-next.ts +++ b/packages/vrender/__tests__/browser/src/pages/animate-next.ts @@ -13,7 +13,7 @@ import { vglobal.setEnv('browser'); -// registerAnimate(); +registerAnimate(); let stage: any; @@ -131,8 +131,7 @@ export const page = () => { .to({ x: 300 }, 1000, 'linear') .to({ y: 300 }, 1000, 'linear') .to({ fill: 'blue' }, 1000, 'linear') - .loop(2) - .bounce(true); + .loop(2); // 中途设置值没问题,它会从orange开始 rect.setAttribute('fill', 'purple'); }); From dfcd3d176be204c6dd3e33b428141bbe11ab33d0 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Mon, 17 Mar 2025 16:15:44 +0800 Subject: [PATCH 037/179] feat: support IncreaseCount animate --- abc.html | 64 ++++- packages/vrender-animate/src/animate.ts | 1 + .../src/custom/custom-animate.ts | 27 +- packages/vrender-animate/src/custom/number.ts | 241 ++++++++++++++++++ packages/vrender-animate/src/index.ts | 1 + packages/vrender-animate/src/step.ts | 16 +- .../browser/src/pages/animate-next.ts | 23 +- 7 files changed, 359 insertions(+), 14 deletions(-) create mode 100644 packages/vrender-animate/src/custom/number.ts diff --git a/abc.html b/abc.html index f4a45e536..21fe282ad 100644 --- a/abc.html +++ b/abc.html @@ -6,8 +6,66 @@ Document - + + diff --git a/packages/vrender-animate/src/animate.ts b/packages/vrender-animate/src/animate.ts index 85a1de241..a52db00b1 100644 --- a/packages/vrender-animate/src/animate.ts +++ b/packages/vrender-animate/src/animate.ts @@ -547,6 +547,7 @@ export class Animate implements IAnimate { } // 如果已经结束,设置状态后return if (nextTime >= this._startTime + this._totalDuration) { + this._lastStep?.onEnd(); this.onEnd(); this.status = AnimateStatus.END; return; diff --git a/packages/vrender-animate/src/custom/custom-animate.ts b/packages/vrender-animate/src/custom/custom-animate.ts index 45ac3a89a..fbb7ba5a7 100644 --- a/packages/vrender-animate/src/custom/custom-animate.ts +++ b/packages/vrender-animate/src/custom/custom-animate.ts @@ -1,9 +1,21 @@ import type { ICustomAnimate } from '../intreface/animate'; +import type { EasingType } from '../intreface/easing'; import type { IAnimateStepType } from '../intreface/type'; import { Step } from '../step'; -export abstract class ACustomAnimate extends Step implements ICustomAnimate { +export abstract class ACustomAnimate extends Step implements ICustomAnimate { type: IAnimateStepType = 'customAnimate'; + declare customFrom: T; + declare params?: any; + declare props?: T; + + // 为了兼容旧的api,from和to是可选的,且尽量不需要From,因为为了避免突变,From都应该从当前位置开始 + // 所以From并不会真正设置到fromProps中,而是作为customFrom参数 + constructor(customFrom: T, customTo: T, duration: number, easing: EasingType, params?: any) { + super('customAnimate', customTo, duration, easing); + this.customFrom = customFrom; + this.params = params; + } get from() { return this.getFromProps(); @@ -13,7 +25,18 @@ export abstract class ACustomAnimate extends Step implements ICustomAnimate { return this.getEndProps(); } - protected setProps(props: Record) { + update(end: boolean, ratio: number, out: Record): void { + // TODO 需要修复,只有在开始的时候才调用 + this.onStart(); + if (!this.props || !this.propKeys) { + return; + } + // 应用缓动函数 + const easedRatio = this.easing(ratio); + this.onUpdate(end, easedRatio, out); + } + + protected setProps(props: T) { this.props = props; this.propKeys = Object.keys(props); this.animate.reSyncProps(); diff --git a/packages/vrender-animate/src/custom/number.ts b/packages/vrender-animate/src/custom/number.ts new file mode 100644 index 000000000..92cc26265 --- /dev/null +++ b/packages/vrender-animate/src/custom/number.ts @@ -0,0 +1,241 @@ +import type { IAnimate, IStep } from '../intreface/animate'; +import type { EasingType } from '../intreface/easing'; +import { ACustomAnimate } from './custom-animate'; + +/** + * 数字增加动画,支持string; number; xx%; xx,xxx; xxx.xx% + * 也支持通过formatTemplate参数指定格式化模板,如 "{{var}}m"、"${{var}}" + * format和formatTemplate可以同时生效,先应用format再应用模板 + */ +export class IncreaseCount extends ACustomAnimate<{ text: string | number }> { + declare valid: boolean; + + private fromNumber: number; + private toNumber: number; + private decimalLength: number; + private format: string; + private formatTemplate: string | null = null; + + constructor( + from: { text: string | number }, + to: { text: string | number }, + duration: number, + easing: EasingType, + // 支持外部控制小数位数以及格式化 + // format控制数字本身的格式化方式 + // formatTemplate可以定义模板字符串如 "{{var}}m"、"${{var}}",两者可以同时使用 + params?: { + decimalLength?: number; + format?: 'percent' | 'thousandth' | 'none'; + formatTemplate?: string; + } + ) { + super(from, to, duration, easing, params); + this.decimalLength = params?.decimalLength; + + // 检查是否提供了格式化模板 + if (params?.formatTemplate && params.formatTemplate.includes('{{var}}')) { + this.formatTemplate = params.formatTemplate; + } + } + + onFirstRun(): void { + const fromProps = this.getLastProps(); + const toProps = this.getEndProps(); + const fromText = fromProps.text ?? 0; + const toText = toProps.text ?? 0; + + // 初始化解析结果 + this.valid = true; + let fromNum = 0; + let toNum = 0; + let fromFormat = ''; + let toFormat = ''; + let maxDecimalLength = 0; + + // 解析fromText + if (typeof fromText === 'number') { + fromNum = fromText; + const str = fromText.toString(); + const decimalPart = str.split('.')[1] || ''; + maxDecimalLength = Math.max(maxDecimalLength, decimalPart.length); + } else if (typeof fromText === 'string') { + // 检查是否是百分比 + if (fromText.endsWith('%')) { + fromFormat = '%'; + const numStr = fromText.substring(0, fromText.length - 1); + // 去除可能的千分位逗号 + const cleanNumStr = numStr.replace(/,/g, ''); + fromNum = parseFloat(cleanNumStr) / 100; + if (isNaN(fromNum)) { + this.valid = false; + return; + } + const decimalPart = cleanNumStr.split('.')[1] || ''; + maxDecimalLength = Math.max(maxDecimalLength, decimalPart.length + 2); // 百分比需要加2 + } else { + // 处理普通数字或带千分位逗号的数字 + const cleanNumStr = fromText.replace(/,/g, ''); + fromNum = parseFloat(cleanNumStr); + if (isNaN(fromNum)) { + this.valid = false; + return; + } + // 检查是否有千分位 + if (fromText.includes(',')) { + fromFormat = ','; + } + const decimalPart = cleanNumStr.split('.')[1] || ''; + maxDecimalLength = Math.max(maxDecimalLength, decimalPart.length); + } + } else { + this.valid = false; + return; + } + + // 解析toText + if (typeof toText === 'number') { + toNum = toText; + const str = toText.toString(); + const decimalPart = str.split('.')[1] || ''; + maxDecimalLength = Math.max(maxDecimalLength, decimalPart.length); + } else if (typeof toText === 'string') { + // 检查是否是百分比 + if (toText.endsWith('%')) { + toFormat = '%'; + const numStr = toText.substring(0, toText.length - 1); + // 去除可能的千分位逗号 + const cleanNumStr = numStr.replace(/,/g, ''); + toNum = parseFloat(cleanNumStr) / 100; + if (isNaN(toNum)) { + this.valid = false; + return; + } + const decimalPart = cleanNumStr.split('.')[1] || ''; + maxDecimalLength = Math.max(maxDecimalLength, decimalPart.length + 2); // 百分比需要加2 + } else { + // 处理普通数字或带千分位逗号的数字 + const cleanNumStr = toText.replace(/,/g, ''); + toNum = parseFloat(cleanNumStr); + if (isNaN(toNum)) { + this.valid = false; + return; + } + // 检查是否有千分位 + if (toText.includes(',')) { + toFormat = ','; + } + const decimalPart = cleanNumStr.split('.')[1] || ''; + maxDecimalLength = Math.max(maxDecimalLength, decimalPart.length); + } + } else { + this.valid = false; + return; + } + + // 设置最终格式 + // 检查是否有外部传入的格式 + if (this.params?.format) { + // 使用外部传入的格式,将外部格式映射到内部格式 + switch (this.params.format) { + case 'percent': + this.format = '%'; + break; + case 'thousandth': + this.format = ','; + break; + case 'none': + this.format = ''; + break; + default: + // 如果传入了未知格式,则使用自动检测的格式 + this.format = toFormat || fromFormat; + } + + // 如果外部指定了百分比格式,但输入不是百分比,需要适配 + if (this.format === '%' && toFormat !== '%' && fromFormat !== '%') { + // 不需要除以100,因为输入不是百分比 + if (this.decimalLength === undefined) { + // 默认百分比显示2位小数 + this.decimalLength = 2; + } + } + + // 如果外部指定了不用百分比格式,但输入是百分比,需要适配 + if (this.format !== '%' && (toFormat === '%' || fromFormat === '%')) { + // 需要乘以100,因为输入是百分比但不显示为百分比 + fromNum = fromNum * 100; + toNum = toNum * 100; + } + } else { + // 自动检测格式,优先使用toFormat,如果to没有特殊格式则使用fromFormat + this.format = toFormat || fromFormat; + } + + // 设置fromNumber和toNumber + this.fromNumber = fromNum; + this.toNumber = toNum; + + // 如果没有传入decimalLength,则根据输入格式设置 + if (this.decimalLength === undefined) { + this.decimalLength = maxDecimalLength; + } + } + + onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { + super.onEnd(cb); + if (!cb) { + this.target.setAttributes(this.to); + } + } + + onUpdate = (end: boolean, ratio: number, out: Record): void => { + if (!this.valid) { + return; + } + // 插值计算当前数值 + const currentNumber = this.fromNumber + (this.toNumber - this.fromNumber) * ratio; + + // 根据格式和小数位格式化数字 + let formattedText: string | number = ''; + const format = this.format; + + // 首先格式化数字值(保留小数位) + // 对于百分比,乘以100 + const adjustedNumber = format === '%' ? currentNumber * 100 : currentNumber; + // 保留指定小数位 + const numberWithDecimals = adjustedNumber.toFixed(this.decimalLength); + // 如果小数位全是0,转为整数 + let formattedNumber: string | number = numberWithDecimals; + if (parseFloat(numberWithDecimals) === Math.floor(parseFloat(numberWithDecimals))) { + formattedNumber = Math.floor(parseFloat(numberWithDecimals)); + } + + // 应用基本格式(百分比、千分位) + let formattedWithBasicFormat: string | number; + if (format === '%') { + // 百分比格式 + formattedWithBasicFormat = `${formattedNumber}%`; + } else if (format === ',') { + // 千分位格式 + const parts = formattedNumber.toString().split('.'); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); + formattedWithBasicFormat = parts.join('.'); + } else { + // 普通数字格式 + formattedWithBasicFormat = formattedNumber; + } + + // 应用模板(如果存在) + if (this.formatTemplate) { + // 使用模板格式化 + formattedText = this.formatTemplate.replace('{{var}}', formattedWithBasicFormat.toString()); + } else { + // 不使用模板,直接使用基本格式的结果 + formattedText = formattedWithBasicFormat; + } + + // 更新图形的text属性 + this.target.setAttribute('text', formattedText); + }; +} diff --git a/packages/vrender-animate/src/index.ts b/packages/vrender-animate/src/index.ts index 015a2168a..3b09eb9d2 100644 --- a/packages/vrender-animate/src/index.ts +++ b/packages/vrender-animate/src/index.ts @@ -15,3 +15,4 @@ export { Step } from './step'; export * from './utils/easing-func'; export { registerAnimate } from './register'; export { ACustomAnimate } from './custom/custom-animate'; +export { IncreaseCount } from './custom/number'; diff --git a/packages/vrender-animate/src/step.ts b/packages/vrender-animate/src/step.ts index 841a70f29..b09ac26fc 100644 --- a/packages/vrender-animate/src/step.ts +++ b/packages/vrender-animate/src/step.ts @@ -25,7 +25,7 @@ export class Step implements IStep { step: IStep, target: IGraphic ) => void)[]; - easing?: EasingTypeFunc; + easing: EasingTypeFunc; animate: IAnimate; target: IGraphic; fromProps: Record; @@ -45,6 +45,8 @@ export class Step implements IStep { // 设置缓动函数 if (easing) { this.easing = typeof easing === 'function' ? easing : Easing[easing]; + } else { + this.easing = Easing.linear; } if (type === 'wait') { this.onUpdate = noop; @@ -163,10 +165,10 @@ export class Step implements IStep { onStart(): void { if (!this._hasFirstRun) { this._hasFirstRun = true; - this.onFirstRun(); // 获取上一步的属性值作为起始值 this.fromProps = this.getLastProps(); this.determineInterpolateUpdateFunction(); + this.onFirstRun(); } } @@ -174,13 +176,12 @@ export class Step implements IStep { * 更新执行的时候调用 * 如果跳帧了就不一定会执行 */ - update = (end: boolean, ratio: number, out: Record): void => { + update(end: boolean, ratio: number, out: Record): void { // TODO 需要修复,只有在开始的时候才调用 this.onStart(); if (!this.props || !this.propKeys) { return; } - this.onUpdate(end, ratio, out); // 应用缓动函数 const easedRatio = this.easing(ratio); this.interpolateUpdateFunctions.forEach((func, index) => { @@ -189,7 +190,8 @@ export class Step implements IStep { const toValue = this.props[key]; func(key, fromValue, toValue, easedRatio, this, this.target); }); - }; + this.onUpdate(end, easedRatio, out); + } onUpdate = (end: boolean, ratio: number, out: Record): void => { // ... @@ -212,7 +214,7 @@ export class Step implements IStep { * 获取结束的属性,包含前序的终值,是merge过的 * @returns */ - getEndProps(): Record | void { + getEndProps(): Record { return this.props; } @@ -220,7 +222,7 @@ export class Step implements IStep { * 获取开始的属性,是前序的终值 * @returns */ - getFromProps(): Record | void { + getFromProps(): Record { return this.fromProps; } diff --git a/packages/vrender/__tests__/browser/src/pages/animate-next.ts b/packages/vrender/__tests__/browser/src/pages/animate-next.ts index 8580ac3d5..19aa16d35 100644 --- a/packages/vrender/__tests__/browser/src/pages/animate-next.ts +++ b/packages/vrender/__tests__/browser/src/pages/animate-next.ts @@ -1,4 +1,4 @@ -import { DefaultTicker, DefaultTimeline, Animate, registerAnimate } from '@visactor/vrender-animate'; +import { DefaultTicker, DefaultTimeline, Animate, registerAnimate, IncreaseCount } from '@visactor/vrender-animate'; import { container, createRect, @@ -39,7 +39,7 @@ function addCase(name: string, container: HTMLElement, cb: (stage: any) => void) export const page = () => { const btnContainer = document.createElement('div'); - btnContainer.style.width = '80%'; + btnContainer.style.width = '1000px'; btnContainer.style.background = '#cecece'; btnContainer.style.display = 'flex'; btnContainer.style.flexDirection = 'row'; @@ -236,4 +236,23 @@ export const page = () => { textAnimate.after(rectAnimate); stage.defaultLayer.add(text); }); + addCase('custom IncreaseCount', btnContainer, stage => { + // Add explanatory text + const text = createText({ + x: 300, + y: 50, + text: '0%咿呀呀', + fontSize: 16, + fill: 'black', + textAlign: 'center', + opacity: 1 + }); + const customAnimate = new IncreaseCount(null, { text: '12,345,678%咿呀呀' }, 1000, 'linear', { + decimalLength: 0, + format: 'thousandth', + formatTemplate: '{{var}}%咿呀呀' + }); + text.animate().play(customAnimate); + stage.defaultLayer.add(text); + }); }; From 75e3848da796b1f460b27ac9a4300bb6d29b24a0 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 18 Mar 2025 16:53:24 +0800 Subject: [PATCH 038/179] feat: support part custom animate --- common/config/rush/pnpm-lock.yaml | 3 + .../src/custom/clip-graphic.ts | 233 ++++++++++++++++++ .../vrender-animate/src/custom/group-fade.ts | 70 ++++++ .../vrender-animate/src/custom/input-text.ts | 147 +++++++++++ packages/vrender-animate/src/custom/number.ts | 4 +- packages/vrender-animate/src/custom/sphere.ts | 64 +++++ .../vrender-animate/src/custom/tag-points.ts | 180 ++++++++++++++ packages/vrender-animate/src/index.ts | 5 + packages/vrender-animate/src/step.ts | 4 +- packages/vrender-core/package.json | 3 +- packages/vrender-core/src/index.ts | 23 ++ packages/vrender-core/src/interface/index.ts | 2 +- .../browser/src/pages/animate-next.ts | 25 +- .../__tests__/browser/src/pages/state.ts | 18 +- 14 files changed, 759 insertions(+), 22 deletions(-) create mode 100644 packages/vrender-animate/src/custom/clip-graphic.ts create mode 100644 packages/vrender-animate/src/custom/group-fade.ts create mode 100644 packages/vrender-animate/src/custom/input-text.ts create mode 100644 packages/vrender-animate/src/custom/sphere.ts create mode 100644 packages/vrender-animate/src/custom/tag-points.ts diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 2bb7d8699..3bf009c20 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -396,6 +396,9 @@ importers: ../../packages/vrender-core: dependencies: + '@visactor/vrender-animate': + specifier: workspace:0.22.8 + version: link:../vrender-animate '@visactor/vutils': specifier: ~0.19.5 version: 0.19.5 diff --git a/packages/vrender-animate/src/custom/clip-graphic.ts b/packages/vrender-animate/src/custom/clip-graphic.ts new file mode 100644 index 000000000..f57f15de0 --- /dev/null +++ b/packages/vrender-animate/src/custom/clip-graphic.ts @@ -0,0 +1,233 @@ +import type { IArcGraphicAttribute, IGraphic, IGroup, IRectGraphicAttribute } from '@visactor/vrender-core'; +import { application, AttributeUpdateType } from '@visactor/vrender-core'; +import { ACustomAnimate } from './custom-animate'; +import type { EasingType } from '../intreface/easing'; + +export class ClipGraphicAnimate extends ACustomAnimate { + private _group?: IGroup; + private _clipGraphic?: IGraphic; + protected clipFromAttribute?: any; + protected clipToAttribute?: any; + + private _lastClip?: boolean; + private _lastPath?: IGraphic[]; + + constructor( + from: any, + to: any, + duration: number, + easing: EasingType, + params: { group: IGroup; clipGraphic: IGraphic } + ) { + super(null, null, duration, easing, params); + this.clipFromAttribute = from; + this.clipToAttribute = to; + this._group = params?.group; + this._clipGraphic = params?.clipGraphic; + } + + onBind() { + if (this._group && this._clipGraphic) { + this._lastClip = this._group.attribute.clip; + this._lastPath = this._group.attribute.path; + this._group.setAttributes( + { + clip: true, + path: [this._clipGraphic] + }, + false, + { type: AttributeUpdateType.ANIMATE_BIND } + ); + } + } + + onEnd() { + if (this._group) { + this._group.setAttributes( + { + clip: this._lastClip, + path: this._lastPath + }, + false, + { type: AttributeUpdateType.ANIMATE_END } + ); + } + return; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + if (!this._clipGraphic) { + return; + } + const res: any = {}; + Object.keys(this.clipFromAttribute).forEach(k => { + res[k] = this.clipFromAttribute[k] + (this.clipToAttribute[k] - this.clipFromAttribute[k]) * ratio; + }); + this._clipGraphic.setAttributes(res, false, { + type: AttributeUpdateType.ANIMATE_UPDATE, + animationState: { ratio, end } + }); + } +} + +export class ClipAngleAnimate extends ClipGraphicAnimate { + constructor( + from: any, + to: any, + duration: number, + easing: EasingType, + params: { + group: IGroup; + center?: { x: number; y: number }; + startAngle?: number; + radius?: number; + orient?: 'clockwise' | 'anticlockwise'; + animationType?: 'in' | 'out'; + } + ) { + const groupAttribute = params?.group?.attribute ?? {}; + const width = groupAttribute.width ?? 0; + const height = groupAttribute.height ?? 0; + + const animationType = params?.animationType ?? 'in'; + const startAngle = params?.startAngle ?? 0; + const orient = params?.orient ?? 'clockwise'; + + let arcStartAngle = 0; + let arcEndAngle = 0; + if (orient === 'anticlockwise') { + arcEndAngle = animationType === 'in' ? startAngle + Math.PI * 2 : startAngle; + arcEndAngle = startAngle + Math.PI * 2; + } else { + arcStartAngle = startAngle; + arcEndAngle = animationType === 'out' ? startAngle + Math.PI * 2 : startAngle; + } + const arc = application.graphicService.creator.arc({ + x: params?.center?.x ?? width / 2, + y: params?.center?.y ?? height / 2, + outerRadius: params?.radius ?? (width + height) / 2, + innerRadius: 0, + startAngle: arcStartAngle, + endAngle: arcEndAngle, + fill: true + }); + let fromAttributes: Partial; + let toAttributes: Partial; + if (orient === 'anticlockwise') { + fromAttributes = { startAngle: startAngle + Math.PI * 2 }; + toAttributes = { startAngle: startAngle }; + } else { + fromAttributes = { endAngle: startAngle }; + toAttributes = { endAngle: startAngle + Math.PI * 2 }; + } + super( + animationType === 'in' ? fromAttributes : toAttributes, + animationType === 'in' ? toAttributes : fromAttributes, + duration, + easing, + { group: params?.group, clipGraphic: arc } + ); + } +} + +export class ClipRadiusAnimate extends ClipGraphicAnimate { + constructor( + from: any, + to: any, + duration: number, + easing: EasingType, + params: { + group: IGroup; + center?: { x: number; y: number }; + startRadius?: number; + endRadius?: number; + animationType?: 'in' | 'out'; + } + ) { + const groupAttribute = params?.group?.attribute ?? {}; + const width = groupAttribute.width ?? 0; + const height = groupAttribute.height ?? 0; + + const animationType = params?.animationType ?? 'in'; + const startRadius = params?.startRadius ?? 0; + const endRadius = params?.endRadius ?? Math.sqrt((width / 2) ** 2 + (height / 2) ** 2); + + const arc = application.graphicService.creator.arc({ + x: params?.center?.x ?? width / 2, + y: params?.center?.y ?? height / 2, + outerRadius: animationType === 'out' ? endRadius : startRadius, + innerRadius: 0, + startAngle: 0, + endAngle: Math.PI * 2, + fill: true + }); + const fromAttributes: Partial = { outerRadius: startRadius }; + const toAttributes: Partial = { outerRadius: endRadius }; + super( + animationType === 'in' ? fromAttributes : toAttributes, + animationType === 'in' ? toAttributes : fromAttributes, + duration, + easing, + { group: params?.group, clipGraphic: arc } + ); + } +} + +export class ClipDirectionAnimate extends ClipGraphicAnimate { + constructor( + from: any, + to: any, + duration: number, + easing: EasingType, + params: { + group: IGroup; + direction?: 'x' | 'y'; + orient?: 'positive' | 'negative'; + width?: number; + height?: number; + animationType?: 'in' | 'out'; + } + ) { + const groupAttribute = params?.group?.attribute ?? {}; + const width = params?.width ?? groupAttribute.width ?? 0; + const height = params?.height ?? groupAttribute.height ?? 0; + + const animationType = params?.animationType ?? 'in'; + const direction = params?.direction ?? 'x'; + const orient = params?.orient ?? 'positive'; + + const rect = application.graphicService.creator.rect({ + x: 0, + y: 0, + width: animationType === 'in' && direction === 'x' ? 0 : width, + height: animationType === 'in' && direction === 'y' ? 0 : height, + fill: true + }); + let fromAttributes: Partial = {}; + let toAttributes: Partial = {}; + if (direction === 'y') { + if (orient === 'negative') { + fromAttributes = { y: height, height: 0 }; + toAttributes = { y: 0, height: height }; + } else { + fromAttributes = { height: 0 }; + toAttributes = { height: height }; + } + } else { + if (orient === 'negative') { + fromAttributes = { x: width, width: 0 }; + toAttributes = { x: 0, width: width }; + } else { + fromAttributes = { width: 0 }; + toAttributes = { width: width }; + } + } + super( + animationType === 'in' ? fromAttributes : toAttributes, + animationType === 'in' ? toAttributes : fromAttributes, + duration, + easing, + { group: params?.group, clipGraphic: rect } + ); + } +} diff --git a/packages/vrender-animate/src/custom/group-fade.ts b/packages/vrender-animate/src/custom/group-fade.ts new file mode 100644 index 000000000..b44b689d9 --- /dev/null +++ b/packages/vrender-animate/src/custom/group-fade.ts @@ -0,0 +1,70 @@ +import type { IGroup } from '@visactor/vrender-core'; +import { ACustomAnimate } from './custom-animate'; + +export class GroupFadeIn extends ACustomAnimate { + declare target: IGroup; + + getEndProps(): Record { + return {}; + } + + onBind(): void { + this.target.setTheme({ + common: { + opacity: 0 + } + }); + return; + } + + onEnd(): void { + this.target.setTheme({ + common: { + opacity: 1 + } + }); + return; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + this.target.setTheme({ + common: { + opacity: ratio + } + }); + } +} + +export class GroupFadeOut extends ACustomAnimate { + declare target: IGroup; + + getEndProps(): Record { + return {}; + } + + onBind(): void { + this.target.setTheme({ + common: { + opacity: 1 + } + }); + return; + } + + onEnd(): void { + this.target.setTheme({ + common: { + opacity: 0 + } + }); + return; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + this.target.setTheme({ + common: { + opacity: 1 - ratio + } + }); + } +} diff --git a/packages/vrender-animate/src/custom/input-text.ts b/packages/vrender-animate/src/custom/input-text.ts new file mode 100644 index 000000000..3954a4b46 --- /dev/null +++ b/packages/vrender-animate/src/custom/input-text.ts @@ -0,0 +1,147 @@ +import type { IAnimate, IStep } from '../intreface/animate'; +import type { EasingType } from '../intreface/easing'; +import { ACustomAnimate } from './custom-animate'; + +/** + * 文本输入动画,实现类似打字机的字符逐个显示效果 + * 支持通过beforeText和afterText参数添加前缀和后缀 + * 支持通过showCursor参数显示光标,cursorChar自定义光标字符 + */ +export class InputText extends ACustomAnimate<{ text: string }> { + declare valid: boolean; + + private fromText: string = ''; + private toText: string = ''; + private showCursor: boolean = false; + private cursorChar: string = '|'; + private blinkCursor: boolean = true; + private beforeText: string = ''; + private afterText: string = ''; + + constructor( + from: { text: string }, + to: { text: string }, + duration: number, + easing: EasingType, + params?: { + showCursor?: boolean; + cursorChar?: string; + blinkCursor?: boolean; + beforeText?: string; + afterText?: string; + } + ) { + super(from, to, duration, easing, params); + + // 配置光标相关选项 + if (params?.showCursor !== undefined) { + this.showCursor = params.showCursor; + } + if (params?.cursorChar !== undefined) { + this.cursorChar = params.cursorChar; + } + if (params?.blinkCursor !== undefined) { + this.blinkCursor = params.blinkCursor; + } + + // 配置前缀和后缀文本 + if (params?.beforeText !== undefined) { + this.beforeText = params.beforeText; + } + if (params?.afterText !== undefined) { + this.afterText = params.afterText; + } + } + + onFirstRun(): void { + const fromProps = this.getLastProps(); + const toProps = this.getEndProps(); + const fromText = fromProps.text ?? ''; + const toText = toProps.text ?? ''; + + // 初始化解析结果 + this.valid = true; + + // 存储文本用于动画 + this.fromText = fromText.toString(); + this.toText = toText.toString(); + + // 确保to不为空 + if (!this.toText && this.toText !== '') { + this.valid = false; + return; + } + } + + onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { + super.onEnd(cb); + if (!cb) { + // 动画结束时,显示完整文本(不带闪烁光标) + if (this.showCursor && !this.blinkCursor) { + // 如果有光标但不闪烁,保留光标 + this.target.setAttribute('text', this.beforeText + this.toText + this.cursorChar + this.afterText); + } else { + // 不显示光标 + this.target.setAttribute('text', this.beforeText + this.toText + this.afterText); + } + } + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + if (!this.valid) { + return; + } + + // 计算当前应该显示的字符数量 + const totalChars = this.toText.length; + const fromChars = this.fromText.length; + + // 如果fromText比toText长,则是删除动画 + // 否则是添加动画 + let currentLength: number; + let currentText: string; + + if (fromChars > totalChars) { + // 删除文本动画(从多到少) + currentLength = Math.round(fromChars - (fromChars - totalChars) * ratio); + currentText = this.fromText.substring(0, currentLength); + } else { + // 添加文本动画(从少到多) + currentLength = Math.round(fromChars + (totalChars - fromChars) * ratio); + + // 如果fromText是toText的前缀,则直接使用toText的子串 + if (this.toText.startsWith(this.fromText)) { + currentText = this.toText.substring(0, currentLength); + } else { + // 否则需要在fromText和toText之间进行过渡 + if (currentLength <= fromChars) { + currentText = this.fromText.substring(0, currentLength); + } else { + currentText = this.toText.substring(0, currentLength - fromChars + Math.min(fromChars, currentLength)); + } + } + } + + // 构建最终显示的文本 + let displayText = this.beforeText + currentText + this.afterText; + + // 添加光标效果 + if (this.showCursor) { + if (this.blinkCursor) { + // 闪烁效果:在动画期间,光标每半个周期闪烁一次 + const blinkRate = 0.1; // 光标闪烁频率(每10%动画进度闪烁一次) + const showCursorNow = Math.floor(ratio / blinkRate) % 2 === 0; + + if (showCursorNow) { + displayText = this.beforeText + currentText + this.cursorChar + this.afterText; + } + } else { + // 固定光标(不闪烁) + displayText = this.beforeText + currentText + this.cursorChar + this.afterText; + } + } + + // 更新图形的text属性 + this.target.setAttribute('text', displayText); + } +} diff --git a/packages/vrender-animate/src/custom/number.ts b/packages/vrender-animate/src/custom/number.ts index 92cc26265..58514295b 100644 --- a/packages/vrender-animate/src/custom/number.ts +++ b/packages/vrender-animate/src/custom/number.ts @@ -189,7 +189,7 @@ export class IncreaseCount extends ACustomAnimate<{ text: string | number }> { } } - onUpdate = (end: boolean, ratio: number, out: Record): void => { + onUpdate(end: boolean, ratio: number, out: Record): void { if (!this.valid) { return; } @@ -237,5 +237,5 @@ export class IncreaseCount extends ACustomAnimate<{ text: string | number }> { // 更新图形的text属性 this.target.setAttribute('text', formattedText); - }; + } } diff --git a/packages/vrender-animate/src/custom/sphere.ts b/packages/vrender-animate/src/custom/sphere.ts new file mode 100644 index 000000000..a8c7d72c3 --- /dev/null +++ b/packages/vrender-animate/src/custom/sphere.ts @@ -0,0 +1,64 @@ +import { pi, pi2 } from '@visactor/vutils'; +import { ACustomAnimate } from './custom-animate'; + +type RotateSphereParams = + | { + center: { x: number; y: number; z: number }; + r: number; + cb?: (out: any) => void; + } + | (() => any); + +export class RotateBySphereAnimate extends ACustomAnimate { + declare params: RotateSphereParams; + declare theta: number; + declare phi: number; + + onStart(): void { + const { center, r } = typeof this.params === 'function' ? this.params() : this.params; + const startX = this.target.getComputedAttribute('x'); + const startY = this.target.getComputedAttribute('y'); + const startZ = this.target.getComputedAttribute('z'); + const phi = Math.acos((startY - center.y) / r); + let theta = Math.acos((startX - center.x) / r / Math.sin(phi)); + if (startZ - center.z < 0) { + theta = pi2 - theta; + } + this.theta = theta; + this.phi = phi; + } + + onBind() { + return; + } + + onEnd() { + return; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + if (this.phi == null || this.theta == null) { + return; + } + const { center, r, cb } = typeof this.params === 'function' ? this.params() : this.params; + const deltaAngle = Math.PI * 2 * ratio; + const theta = this.theta + deltaAngle; + const phi = this.phi; + const x = r * Math.sin(phi) * Math.cos(theta) + center.x; + const y = r * Math.cos(phi) + center.y; + const z = r * Math.sin(phi) * Math.sin(theta) + center.z; + out.x = x; + out.y = y; + out.z = z; + // out.beta = phi; + out.alpha = theta + pi / 2; + while (out.alpha > pi2) { + out.alpha -= pi2; + } + out.alpha = pi2 - out.alpha; + + out.zIndex = out.z * -10000; + + cb && cb(out); + } +} diff --git a/packages/vrender-animate/src/custom/tag-points.ts b/packages/vrender-animate/src/custom/tag-points.ts new file mode 100644 index 000000000..660025b43 --- /dev/null +++ b/packages/vrender-animate/src/custom/tag-points.ts @@ -0,0 +1,180 @@ +import type { IPointLike } from '@visactor/vutils'; +import { clamp, isValidNumber, Point } from '@visactor/vutils'; +import { ACustomAnimate } from './custom-animate'; +import type { ISegment } from '@visactor/vrender-core'; +import { pointInterpolation } from '@visactor/vrender-core'; +import type { EasingType } from '../intreface/easing'; + +export class TagPointsUpdate extends ACustomAnimate<{ points?: IPointLike[]; segments?: ISegment[] }> { + protected fromPoints: IPointLike[]; + protected toPoints: IPointLike[]; + protected points: IPointLike[]; + protected interpolatePoints: [IPointLike, IPointLike][]; + protected newPointAnimateType: 'grow' | 'appear' | 'clip'; + protected clipRange: number; + protected shrinkClipRange: number; + protected clipRangeByDimension: 'x' | 'y'; + protected segmentsCache: number[]; + + constructor( + from: any, + to: any, + duration: number, + easing: EasingType, + params?: { newPointAnimateType?: 'grow' | 'appear' | 'clip'; clipRangeByDimension?: 'x' | 'y' } + ) { + super(from, to, duration, easing, params); + this.newPointAnimateType = params?.newPointAnimateType ?? 'grow'; + this.clipRangeByDimension = params?.clipRangeByDimension ?? 'x'; + } + + private getPoints(attribute: typeof this.from, cache = false): IPointLike[] { + if (attribute.points) { + return attribute.points; + } + + if (attribute.segments) { + const points = [] as IPointLike[]; + if (!this.segmentsCache) { + this.segmentsCache = []; + } + attribute.segments.map((segment: any) => { + if (segment.points) { + points.push(...segment.points); + } + if (cache) { + this.segmentsCache.push(segment.points?.length ?? 0); + } + }); + return points; + } + return []; + } + + onBind(): void { + const originFromPoints = this.getPoints(this.from); + const originToPoints = this.getPoints(this.to, true); + this.fromPoints = !originFromPoints ? [] : !Array.isArray(originFromPoints) ? [originFromPoints] : originFromPoints; + this.toPoints = !originToPoints ? [] : !Array.isArray(originToPoints) ? [originToPoints] : originToPoints; + + const tagMap = new Map(); + this.fromPoints.forEach(point => { + if (point.context) { + tagMap.set(point.context, point); + } + }); + let firstMatchedIndex = Infinity; + let lastMatchedIndex = -Infinity; + let firstMatchedPoint: IPointLike; + let lastMatchedPoint: IPointLike; + for (let i = 0; i < this.toPoints.length; i += 1) { + if (tagMap.has(this.toPoints[i].context)) { + firstMatchedIndex = i; + firstMatchedPoint = tagMap.get(this.toPoints[i].context); + break; + } + } + for (let i = this.toPoints.length - 1; i >= 0; i -= 1) { + if (tagMap.has(this.toPoints[i].context)) { + lastMatchedIndex = i; + lastMatchedPoint = tagMap.get(this.toPoints[i].context); + break; + } + } + + if (this.newPointAnimateType === 'clip') { + if (this.toPoints.length !== 0) { + if (Number.isFinite(lastMatchedIndex)) { + this.clipRange = + this.toPoints[lastMatchedIndex][this.clipRangeByDimension] / + this.toPoints[this.toPoints.length - 1][this.clipRangeByDimension]; + if (this.clipRange === 1) { + this.shrinkClipRange = + this.toPoints[lastMatchedIndex][this.clipRangeByDimension] / + this.fromPoints[this.fromPoints.length - 1][this.clipRangeByDimension]; + } + if (!isValidNumber(this.clipRange)) { + this.clipRange = 0; + } else { + this.clipRange = clamp(this.clipRange, 0, 1); + } + } else { + this.clipRange = 0; + } + } + } + // TODO: shrink removed points + // if no point is matched, animation should start from toPoint[0] + let prevMatchedPoint = this.toPoints[0]; + this.interpolatePoints = this.toPoints.map((point, index) => { + const matchedPoint = tagMap.get(point.context); + if (matchedPoint) { + prevMatchedPoint = matchedPoint; + return [matchedPoint, point]; + } + // appear new point + if (this.newPointAnimateType === 'appear' || this.newPointAnimateType === 'clip') { + return [point, point]; + } + // grow new point + if (index < firstMatchedIndex && firstMatchedPoint) { + return [firstMatchedPoint, point]; + } else if (index > lastMatchedIndex && lastMatchedPoint) { + return [lastMatchedPoint, point]; + } + return [prevMatchedPoint, point]; + }); + this.points = this.interpolatePoints.map(interpolate => { + const fromPoint = interpolate[0]; + const toPoint = interpolate[1]; + const newPoint = new Point(fromPoint.x, fromPoint.y, fromPoint.x1, fromPoint.y1); + newPoint.defined = toPoint.defined; + newPoint.context = toPoint.context; + return newPoint; + }); + } + + onFirstRun(): void { + const lastClipRange = (this.target.attribute as any).clipRange; + if (isValidNumber(lastClipRange * this.clipRange)) { + this.clipRange *= lastClipRange; + } + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + // if not create new points, multi points animation might not work well. + this.points = this.points.map((point, index) => { + const newPoint = pointInterpolation(this.interpolatePoints[index][0], this.interpolatePoints[index][1], ratio); + newPoint.context = point.context; + return newPoint; + }); + if (this.clipRange) { + if (this.shrinkClipRange) { + // 折线变短 + if (!end) { + out.points = this.fromPoints; + out.clipRange = this.clipRange - (this.clipRange - this.shrinkClipRange) * ratio; + } else { + out.points = this.toPoints; + out.clipRange = 1; + } + return; + } + out.clipRange = this.clipRange + (1 - this.clipRange) * ratio; + } + if (this.segmentsCache && this.to.segments) { + let start = 0; + out.segments = this.to.segments.map((segment: any, index: any) => { + const end = start + this.segmentsCache[index]; + const points = this.points.slice(start, end); + start = end; + return { + ...segment, + points + }; + }); + } else { + out.points = this.points; + } + } +} diff --git a/packages/vrender-animate/src/index.ts b/packages/vrender-animate/src/index.ts index 3b09eb9d2..ef01e769c 100644 --- a/packages/vrender-animate/src/index.ts +++ b/packages/vrender-animate/src/index.ts @@ -16,3 +16,8 @@ export * from './utils/easing-func'; export { registerAnimate } from './register'; export { ACustomAnimate } from './custom/custom-animate'; export { IncreaseCount } from './custom/number'; +export { InputText } from './custom/input-text'; +export { ClipGraphicAnimate, ClipAngleAnimate, ClipRadiusAnimate, ClipDirectionAnimate } from './custom/clip-graphic'; +export { TagPointsUpdate } from './custom/tag-points'; +export { GroupFadeIn, GroupFadeOut } from './custom/group-fade'; +export { RotateBySphereAnimate } from './custom/sphere'; diff --git a/packages/vrender-animate/src/step.ts b/packages/vrender-animate/src/step.ts index b09ac26fc..2809c73b3 100644 --- a/packages/vrender-animate/src/step.ts +++ b/packages/vrender-animate/src/step.ts @@ -193,9 +193,9 @@ export class Step implements IStep { this.onUpdate(end, easedRatio, out); } - onUpdate = (end: boolean, ratio: number, out: Record): void => { + onUpdate(end: boolean, ratio: number, out: Record): void { // ... - }; + } /** * 结束执行的时候调用 diff --git a/packages/vrender-core/package.json b/packages/vrender-core/package.json index 33b3a7e1a..1aaeba72a 100644 --- a/packages/vrender-core/package.json +++ b/packages/vrender-core/package.json @@ -30,7 +30,8 @@ }, "dependencies": { "color-convert": "2.0.1", - "@visactor/vutils": "~0.19.5" + "@visactor/vutils": "~0.19.5", + "@visactor/vrender-animate": "workspace:0.22.8" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-core/src/index.ts b/packages/vrender-core/src/index.ts index 6b0d09ef6..27220af6f 100644 --- a/packages/vrender-core/src/index.ts +++ b/packages/vrender-core/src/index.ts @@ -102,3 +102,26 @@ export * from './plugins/builtin-plugin/3dview-transform-plugin'; export * from './plugins/builtin-plugin/flex-layout-plugin'; export * from './plugins/builtin-plugin/edit-module'; +export { + registerAnimate, + ACustomAnimate, + InputText, + IncreaseCount, + Animate, + DefaultTicker, + DefaultTimeline, + TagPointsUpdate, + ClipGraphicAnimate, + ClipAngleAnimate, + ClipRadiusAnimate, + ClipDirectionAnimate, + GroupFadeIn, + GroupFadeOut, + RotateBySphereAnimate +} from '@visactor/vrender-animate'; + +// TODO VChart那块非要引用vgrammar-core,这里先临时导出,后面删除 +export const AnimateGroup = {}; +export const oneToMultiMorph = {}; +export const multiToOneMorph = {}; +export const morphPath = {}; diff --git a/packages/vrender-core/src/interface/index.ts b/packages/vrender-core/src/interface/index.ts index 24126c1eb..67ea4971f 100644 --- a/packages/vrender-core/src/interface/index.ts +++ b/packages/vrender-core/src/interface/index.ts @@ -57,7 +57,7 @@ export * from './context'; export * from './path'; export * from './color'; export * from './common'; -export * from './animate'; +// export * from './animate'; export * from './camera'; export * from './matrix'; export * from './light'; diff --git a/packages/vrender/__tests__/browser/src/pages/animate-next.ts b/packages/vrender/__tests__/browser/src/pages/animate-next.ts index 19aa16d35..5809c5261 100644 --- a/packages/vrender/__tests__/browser/src/pages/animate-next.ts +++ b/packages/vrender/__tests__/browser/src/pages/animate-next.ts @@ -1,4 +1,11 @@ -import { DefaultTicker, DefaultTimeline, Animate, registerAnimate, IncreaseCount } from '@visactor/vrender-animate'; +import { + DefaultTicker, + DefaultTimeline, + Animate, + registerAnimate, + IncreaseCount, + InputText +} from '@visactor/vrender-animate'; import { container, createRect, @@ -255,4 +262,20 @@ export const page = () => { text.animate().play(customAnimate); stage.defaultLayer.add(text); }); + addCase('custom InputText', btnContainer, stage => { + // Add explanatory text + const text = createText({ + x: 300, + y: 50, + text: '', + fontSize: 16, + fill: 'black', + textAlign: 'center', + opacity: 1 + }); + // Terminal-style animation with prompt + const terminalAnimation = new InputText({ text: '' }, { text: '这是一段文本内容' }, 1000, 'linear'); + text.animate().play(terminalAnimation); + stage.defaultLayer.add(text); + }); }; diff --git a/packages/vrender/__tests__/browser/src/pages/state.ts b/packages/vrender/__tests__/browser/src/pages/state.ts index 91672b935..262a8c3dd 100644 --- a/packages/vrender/__tests__/browser/src/pages/state.ts +++ b/packages/vrender/__tests__/browser/src/pages/state.ts @@ -1,13 +1,8 @@ -import { - createStage, - createCircle, - FederatedEvent, - DefaultTicker, - defaultTimeline, - defaultTicker -} from '@visactor/vrender'; +import { createStage, createCircle, FederatedEvent } from '@visactor/vrender'; +import { registerAnimate } from '@visactor/vrender-animate'; import { addShapesToStage, colorPools } from '../utils'; +registerAnimate(); // container.load(roughModule); export const page = () => { @@ -154,13 +149,6 @@ export const page = () => { viewHeight: 600 }); - setTimeout(() => { - defaultTicker.pause(); - setTimeout(() => { - defaultTicker.resume(); - }, 2000); - }, 2000); - (window as any).stage = stage; addShapesToStage(stage, shapes as any, true); stage.render(); From 6da8549df7abbbcd9818b11499a015a658ea1108 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 19 Mar 2025 11:39:02 +0800 Subject: [PATCH 039/179] feat: support animate-executor --- packages/vrender-animate/src/animate.ts | 39 +- .../src/executor/animate-executor.ts | 481 ++++++++++++++++++ .../vrender-animate/src/executor/executor.ts | 147 ++++++ packages/vrender-animate/src/index.ts | 1 + .../vrender-animate/src/interpolate/store.ts | 6 +- packages/vrender-animate/src/step.ts | 15 + packages/vrender-core/src/graphic/graphic.ts | 3 + packages/vrender-core/src/index.ts | 2 +- .../vrender-core/src/interface/graphic.ts | 5 +- packages/vrender-core/src/interface/index.ts | 1 - .../browser/src/pages/animate-next.ts | 335 +++++++++++- 11 files changed, 1003 insertions(+), 32 deletions(-) create mode 100644 packages/vrender-animate/src/executor/animate-executor.ts create mode 100644 packages/vrender-animate/src/executor/executor.ts diff --git a/packages/vrender-animate/src/animate.ts b/packages/vrender-animate/src/animate.ts index a52db00b1..63f8cad10 100644 --- a/packages/vrender-animate/src/animate.ts +++ b/packages/vrender-animate/src/animate.ts @@ -1,7 +1,7 @@ import type { IAnimate, IStep, ICustomAnimate } from './intreface/animate'; import type { EasingType } from './intreface/easing'; import { AnimateStatus, AnimateStepType } from './intreface/type'; -import { Step } from './step'; +import { Step, WaitStep } from './step'; import type { ITimeline } from './intreface/timeline'; import { Generator, type IGraphic } from '@visactor/vrender-core'; import { defaultTimeline } from './timeline'; @@ -124,6 +124,21 @@ export class Animate implements IAnimate { return this; } + /** + * 等待延迟 + */ + wait(delay: number): this { + // 创建新的wait step + const step = new WaitStep(AnimateStepType.wait, {}, delay, 'linear'); + + // 如果是第一个step + this.updateStepAfterAppend(step); + + step.bind(this.target, this); + + return this; + } + protected updateStepAfterAppend(step: IStep): void { // 如果是第一个step if (!this._firstStep) { @@ -440,28 +455,6 @@ export class Animate implements IAnimate { return this._startTime; } - /** - * 等待延迟 - */ - wait(delay: number): this { - // 创建新的wait step - const step = new Step(AnimateStepType.wait, {}, delay, 'linear'); - - // 如果是第一个step - if (!this._firstStep) { - this._firstStep = step; - this._lastStep = step; - } else { - // 添加到链表末尾 - this._lastStep.append(step); - this._lastStep = step; - } - - this.updateDuration(); - - return this; - } - /** * 在所有动画完成后执行 */ diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts new file mode 100644 index 000000000..868ee9e30 --- /dev/null +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -0,0 +1,481 @@ +import type { IGraphic, IGroup } from '@visactor/vrender-core'; +import type { + IAnimationConfig, + IAnimationTimeline, + IAnimationTypeConfig, + MarkFunctionCallback, + MarkFunctionValueType, + IAnimationTimeSlice, + IAnimationChannelAttrs, + IAnimationChannelAttributes, + IAnimationCustomConstructor, + IAnimationChannelInterpolator +} from './executor'; +import { ACustomAnimate } from '../custom/custom-animate'; +import type { EasingType } from '../intreface/easing'; +import type { IAnimate } from '../intreface/animate'; +import { cloneDeep, isArray, scale } from '@visactor/vutils'; + +export class AnimateExecutor { + declare _target: IGroup; + + constructor(target: IGroup) { + this._target = target; + } + + /** + * 执行动画,针对图元组 + * 1. oneByOne 为 true 时,内部图元依次执行动画,每个图元通过(duration,delay,delayAfter)来控制自身动画的执行 + * 2. totalTime为总时长(如果有oneByOne的话,从开始到最后一个图元动画结束的时长为totalTime),如果配置了totalTime,那么duration,delay,delayAfter会根据totalTime进行等比缩放 + * 3. 如果配置了partitioner,则根据partitioner进行筛选子图元再执行动画 + */ + execute(params: IAnimationConfig) { + // 判断是否为timeline配置 + const isTimeline = 'timeSlices' in params; + + // 获取子图元 + if (this._target.count <= 1) { + return; + } + + // 筛选符合条件的子图元 + let filteredChildren: IGraphic[]; + + // 如果设置了partitioner,则进行筛选 + if (isTimeline && params.partitioner) { + filteredChildren = (filteredChildren ?? (this._target.getChildren() as IGraphic[])).filter(child => { + return (params as IAnimationTimeline).partitioner((child.context as any)?.data, child, {}); + }); + } + + // 如果需要排序,则进行排序 + if (isTimeline && (params as IAnimationTimeline).sort) { + filteredChildren = filteredChildren ?? (this._target.getChildren() as IGraphic[]); + filteredChildren.sort((a, b) => { + return (params as IAnimationTimeline).sort((a.context as any)?.data, (b.context as any)?.data, a, b, {}); + }); + } + + const totalTime = this.resolveValue(params.totalTime, undefined, undefined); + const startTime = this.resolveValue(params.startTime, undefined, 0); + + // execute只在mark层面调用,所以性能影响可以忽略 + // TODO 如果后续调用频繁,需要重新修改 + const parsedParams = cloneDeep(params); + parsedParams.oneByOneDelay = 0; + parsedParams.startTime = startTime; + parsedParams.totalTime = totalTime; + + const oneByOne = this.resolveValue(params.oneByOne, undefined, false); + + if (isTimeline) { + const timeSlices = (parsedParams as IAnimationTimeline).timeSlices; + if (!isArray(timeSlices)) { + (parsedParams as IAnimationTimeline).timeSlices = [timeSlices]; + } + let sliceTime = 0; + ((parsedParams as IAnimationTimeline).timeSlices as IAnimationTimeSlice[]).forEach(slice => { + slice.delay = this.resolveValue(slice.delay, undefined, 0); + slice.delayAfter = this.resolveValue(slice.delayAfter, undefined, 0); + slice.duration = this.resolveValue(slice.duration, undefined, 300); + sliceTime += slice.delay + slice.duration + slice.delayAfter; + }); + let oneByOneDelay = 0; + let oneByOneTime = 0; + if (oneByOne) { + oneByOneTime = Number(oneByOne); + oneByOneDelay = oneByOneTime; + } + parsedParams.oneByOne = oneByOneTime; + parsedParams.oneByOneDelay = oneByOneDelay; + + if (totalTime) { + const _totalTime = sliceTime + oneByOneDelay * (this._target.count - 2); + const scale = totalTime ? totalTime / _totalTime : 1; + ((parsedParams as IAnimationTimeline).timeSlices as IAnimationTimeSlice[]).forEach(slice => { + slice.delay = (slice.delay as number) * scale; + slice.delayAfter = (slice.delayAfter as number) * scale; + slice.duration = (slice.duration as number) * scale; + }); + parsedParams.oneByOne = oneByOneTime * scale; + parsedParams.oneByOneDelay = oneByOneDelay * scale; + (parsedParams as IAnimationTimeline).startTime = startTime * scale; + } + } else { + const delay = this.resolveValue(params.delay, undefined, 0); + const delayAfter = this.resolveValue(params.delayAfter, undefined, 0); + const duration = this.resolveValue(params.duration, undefined, 300); + + let oneByOneDelay = 0; + let oneByOneTime = 0; + if (oneByOne) { + oneByOneTime = Number(oneByOne); + oneByOneDelay = duration + oneByOneTime; + } + parsedParams.oneByOne = oneByOneTime; + parsedParams.oneByOneDelay = oneByOneDelay; + + if (totalTime) { + const _totalTime = delay + delayAfter + duration + oneByOneDelay * (this._target.count - 2); + const scale = totalTime ? totalTime / _totalTime : 1; + parsedParams.delay = delay * scale; + parsedParams.delayAfter = delayAfter * scale; + parsedParams.duration = duration * scale; + parsedParams.oneByOne = oneByOneTime * scale; + parsedParams.oneByOneDelay = oneByOneDelay * scale; + (parsedParams as IAnimationTypeConfig).startTime = startTime; + } + } + + // const duration = this.resolveValue(isTimeline ? 0 : (params as IAnimationTypeConfig).duration, undefined, 300); + + // const animates: IAnimate[] = []; + + const cb = isTimeline + ? (child: IGraphic, index: number) => { + // 执行单个图元的timeline动画 + this.executeTimelineItem(parsedParams as IAnimationTimeline, child, index); + } + : (child: IGraphic, index: number) => { + // 执行单个图元的config动画 + this.executeTypeConfigItem(parsedParams as IAnimationTypeConfig, child, index); + }; + + // 执行每个图元的动画 + if (filteredChildren) { + filteredChildren.forEach(cb); + } else { + this._target.forEachChildren(cb); + } + + return; + } + + /** + * 执行 TypeConfig 类型的动画 + */ + private executeTypeConfigItem(params: IAnimationTypeConfig, graphic: IGraphic, index: number): IAnimate { + const { + type = 'to', + channel, + custom, + customParameters, + easing = 'linear', + delay = 0, + delayAfter = 0, + duration = 300, + startTime = 0, + oneByOneDelay = 0, + loop, + options, + controlOptions + } = params as any; + + // 创建动画实例 + const animate = graphic.animate() as unknown as IAnimate; + + const delayValue = delay as number; + + // 设置开始时间 + animate.startAt((startTime as number) + index * oneByOneDelay); + + // 添加延迟 + if (delayValue > 0) { + animate.wait(delayValue); + } + + // 根据 channel 配置创建属性对象 + const props = this.createPropsFromChannel(channel, graphic); + + if (type === 'to') { + animate.to(props, duration as number, easing); + } else if (type === 'from') { + animate.from(props, duration as number, easing); + } else if (custom) { + // 处理自定义动画 + const customParams = this.resolveValue(customParameters, graphic, {}); + + if (typeof custom === 'function') { + // 自定义插值器 - 创建自定义插值动画 + this.createCustomInterpolatorAnimation( + animate, + custom as IAnimationChannelInterpolator, + props, + duration as number, + easing, + customParams + ); + } else { + // 自定义动画构造器 - 创建自定义动画类 + this.createCustomAnimation( + animate, + custom as IAnimationCustomConstructor, + props, + duration as number, + easing, + customParams + ); + } + } + + // 添加后延迟 + if ((delayAfter as number) > 0) { + animate.wait(delayAfter as number); + } + + // 设置循环 + if (loop && (loop as number) > 0) { + animate.loop(loop as number); + } + + return animate; + } + + /** + * 执行 Timeline 类型的动画 + */ + private executeTimelineItem(params: IAnimationTimeline, graphic: IGraphic, index: number): IAnimate { + const { timeSlices, startTime = 0, loop, oneByOneDelay, controlOptions } = params as any; + + // 创建动画实例 + const animate = graphic.animate() as unknown as IAnimate; + + // 设置开始时间 + animate.startAt((startTime as number) + index * oneByOneDelay); + + // 设置循环 + if (loop && (loop as number) > 0) { + animate.loop(loop as number); + } + + // 处理时间切片 + const slices = Array.isArray(timeSlices) ? timeSlices : [timeSlices]; + + slices.forEach(slice => { + this.applyTimeSliceToAnimate(slice, animate, graphic); + }); + + return animate; + } + + /** + * 将时间切片应用到动画实例 + */ + private applyTimeSliceToAnimate(slice: IAnimationTimeSlice, animate: IAnimate, graphic: IGraphic) { + const { effects, duration = 300, delay = 0, delayAfter = 0 } = slice; + + // 解析时间参数 + const durationValue = duration as number; + const delayValue = delay as number; + const delayAfterValue = delayAfter as number; + + // 添加延迟 + if (delayValue > 0) { + animate.wait(delayValue); + } + + // 处理动画效果 + const effectsArray = Array.isArray(effects) ? effects : [effects]; + + effectsArray.forEach(effect => { + const { type = 'to', channel, custom, customParameters, easing = 'linear', options } = effect; + + // 根据 channel 配置创建属性对象 + const props = this.createPropsFromChannel(channel, graphic); + + if (type === 'to') { + animate.to(props, durationValue, easing); + } else if (type === 'from') { + animate.from(props, durationValue, easing); + } else if (custom) { + // 处理自定义动画 + const customParams = this.resolveValue(customParameters, graphic, {}); + + if (typeof custom === 'function') { + // 自定义插值器 - 创建自定义插值动画 + this.createCustomInterpolatorAnimation( + animate, + custom as IAnimationChannelInterpolator, + props, + durationValue, + easing, + customParams + ); + } else { + // 自定义动画构造器 - 创建自定义动画类 + this.createCustomAnimation( + animate, + custom as IAnimationCustomConstructor, + props, + durationValue, + easing, + customParams + ); + } + } + }); + + // 添加后延迟 + if (delayAfterValue > 0) { + animate.wait(delayAfterValue); + } + } + + /** + * 创建自定义插值器动画 + */ + private createCustomInterpolatorAnimation( + animate: IAnimate, + interpolator: IAnimationChannelInterpolator, + props: Record, + duration: number, + easing: EasingType, + customParams: any + ) { + // 获取动画目标的当前属性作为起始值 + const from: Record = {}; + const to = props; + + // 为每个属性填充起始值 + Object.keys(to).forEach(key => { + from[key] = animate.target.getComputedAttribute(key); + }); + + // 创建自定义动画步骤 + // 注意:这里需要设计一个特殊的自定义动画类来处理插值器函数 + class CustomInterpolatorAnimate extends ACustomAnimate> { + interpolator: IAnimationChannelInterpolator; + parameters: any; + + constructor( + from: Record, + to: Record, + duration: number, + easing: EasingType, + interpolator: IAnimationChannelInterpolator, + parameters: any + ) { + super(from, to, duration, easing, parameters); + this.interpolator = interpolator; + this.parameters = parameters; + this.setProps(to); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + // 调用插值器函数进行自定义插值 + this.interpolator( + ratio, + this.customFrom, + this.props, + out, + (this.target.context as any)?.data, + this.target, + this.parameters + ); + } + } + + // 创建并添加自定义插值动画 + const customAnimate = new CustomInterpolatorAnimate(from, to, duration, easing, interpolator, customParams); + + animate.play(customAnimate); + } + + /** + * 创建自定义动画类 + */ + private createCustomAnimation( + animate: IAnimate, + CustomAnimateConstructor: IAnimationCustomConstructor, + props: Record, + duration: number, + easing: EasingType, + customParams: any + ) { + // 获取动画目标的当前属性作为起始值 + const from: Record = {}; + const to = props; + + // 为每个属性填充起始值 + Object.keys(to).forEach(key => { + from[key] = animate.target.getComputedAttribute(key); + }); + + // 实例化自定义动画类 + const customAnimate = new CustomAnimateConstructor(from, to, duration, easing, customParams); + + // 播放自定义动画 + animate.play(customAnimate); + } + + /** + * 从 channel 配置创建属性对象 + */ + private createPropsFromChannel( + channel: IAnimationChannelAttrs | IAnimationChannelAttributes | undefined, + graphic: IGraphic + ): Record { + const props: Record = {}; + + if (!channel) { + return props; + } + + if (Array.isArray(channel)) { + // 如果是属性数组,使用当前的属性值 + channel.forEach(attr => { + props[attr] = graphic.getComputedAttribute(attr); + }); + } else { + // 如果是对象,解析 from/to 配置 + Object.entries(channel).forEach(([key, config]) => { + if (config.to !== undefined) { + if (typeof config.to === 'function') { + props[key] = config.to((graphic.context as any)?.data, graphic, {}); + } else { + props[key] = config.to; + } + } + }); + } + + return props; + } + + /** + * 解析函数或值类型的配置项 + */ + private resolveValue(value: MarkFunctionValueType | undefined, graphic?: IGraphic, defaultValue?: T): T { + if (value === undefined) { + return defaultValue as T; + } + + if (typeof value === 'function' && graphic) { + return (value as MarkFunctionCallback)((graphic.context as any)?.data, graphic, {}); + } + + return value as T; + } + + /** + * 执行动画(具体执行到内部的单个图元) + */ + executeItem(params: IAnimationConfig, graphic: IGraphic, index: number): IAnimate | null { + if (!graphic) { + return null; + } + + const isTimeline = 'timeSlices' in params; + let animate: IAnimate | null = null; + + if (isTimeline) { + // 处理 Timeline 类型的动画配置 + animate = this.executeTimelineItem(params as IAnimationTimeline, graphic, index); + } else { + // 处理 TypeConfig 类型的动画配置 + animate = this.executeTypeConfigItem(params as IAnimationTypeConfig, graphic, index); + } + + return animate; + } +} diff --git a/packages/vrender-animate/src/executor/executor.ts b/packages/vrender-animate/src/executor/executor.ts new file mode 100644 index 000000000..4cd4bdb7d --- /dev/null +++ b/packages/vrender-animate/src/executor/executor.ts @@ -0,0 +1,147 @@ +import type { IGraphic } from '@visactor/vrender-core'; +import type { EasingType } from '../intreface/easing'; +import type { ACustomAnimate } from '../custom/custom-animate'; + +export type MarkFunctionCallback = (datum: any, graphic: IGraphic, parameters: any) => T; +export type MarkFunctionValueType = MarkFunctionCallback | T; + +interface IAnimationParameters { + [key: string]: any; +} + +/** + * 动画 channel 配置 + */ +export type IAnimationChannelFunction = (datum: any, element: IGraphic, parameters: IAnimationParameters) => any; + +/** + * 动画 channel 属性配置 + */ +export type IAnimationChannelAttrs = Record< + string, + { + from?: any | IAnimationChannelFunction; + to?: any | IAnimationChannelFunction; + } +>; +export type IAnimationChannelAttributes = string[]; + +/** + * 动画 channel 插值器 + */ +export type IAnimationChannelInterpolator = ( + ratio: number, + from: any, + to: any, + nextAttributes: any, + datum: any, + element: IGraphic, + parameters: IAnimationParameters +) => boolean | void; + +/** + * 动画 custom 构造器 + */ +export interface IAnimationCustomConstructor { + new (from: any, to: any, duration: number, ease: EasingType, parameters?: any): ACustomAnimate; +} + +export interface IAnimationEffect { + /** 动画类型 */ + type?: string; + /** 动画 channel 配置 */ + channel?: IAnimationChannelAttrs | IAnimationChannelAttributes; + /** 动画 自定义插值 配置 */ + custom?: IAnimationChannelInterpolator | IAnimationCustomConstructor; + /** 动画 custom 参数配置 */ + customParameters?: MarkFunctionValueType; + /** 动画 easing 配置 */ + easing?: EasingType; + /** options暂时没有处理 */ + options?: MarkFunctionValueType; +} + +export interface IAnimationTimeSlice { + /** 动画效果 */ + effects: IAnimationEffect | IAnimationEffect[]; + /** 动画时长 */ + duration?: MarkFunctionValueType; + /** 延迟delay后执行动画 */ + delay?: MarkFunctionValueType; + /** effect动画后再延迟delayAfter结束这个周期 */ + delayAfter?: MarkFunctionValueType; +} + +export interface IAnimationControlOptions { + /** 当动画状态变更时清空动画 */ + stopWhenStateChange?: boolean; + /** 是否立即应用动画初始状态 */ + immediatelyApply?: boolean; + /** encode 计算图元最终状态时是否忽略循环动画 */ + ignoreLoopFinalAttributes?: boolean; +} + +/** + * 动画 config 简化配置 + */ +export interface IAnimationTypeConfig { + /** 动画类型 */ + type?: string; + /** 动画 channel 配置 */ + channel?: IAnimationChannelAttrs | IAnimationChannelAttributes; + /** 动画 自定义插值 配置 */ + custom?: IAnimationChannelInterpolator | IAnimationCustomConstructor; + /** 动画 custom 参数配置 */ + customParameters?: MarkFunctionValueType; + /** 动画 easing 配置 */ + easing?: EasingType; + /** 动画 delay 配置 */ + delay?: MarkFunctionValueType; + /** 动画 delayAfter 配置 */ + delayAfter?: MarkFunctionValueType; + /** 动画 duration 配置 */ + duration?: MarkFunctionValueType; + /** 动画 oneByOne 配置(是否依次执行) */ + oneByOne?: MarkFunctionValueType; + /** 动画 startTime 配置 */ + startTime?: MarkFunctionValueType; + /** 动画 totalTime 配置(如果有循环,只算一个周期) */ + totalTime?: MarkFunctionValueType; + /** loop: true 无限循环; loop: 正整数,表示循环的次数 */ + loop?: boolean | number; + /** 动画 effect 配置项 */ + options?: MarkFunctionValueType; + /** 动画执行相关控制配置项 */ + controlOptions?: IAnimationControlOptions; +} + +/** + * 动画 timeline 完整配置,一条时间线内的动画单元只能串行 + * 多个timeline是可以并行的 + * 考虑到同一图元不能在多个timeline上,所以timeline不应该提供数组配置的能力 + */ +export interface IAnimationTimeline { + /** 为了方便动画编排,用户可以设置 id 用于识别时间线 */ + id?: string; + /** 时间切片 */ + timeSlices: IAnimationTimeSlice | IAnimationTimeSlice[]; + /** 动画开始的相对时间,可以为负数 */ + startTime?: MarkFunctionValueType; + /** 动画时长 */ + totalTime?: MarkFunctionValueType; + /** 动画依次执行的延迟 */ + oneByOne?: MarkFunctionValueType; + /** loop: true 无限循环; loop: 正整数,表示循环的次数 */ + loop?: MarkFunctionValueType; + /** 对图元元素进行划分,和过滤类似,但是不同时间线不能同时作用在相同的元素上 */ + partitioner?: MarkFunctionCallback; + /** 对同一时间线上的元素进行排序 */ + sort?: (datumA: any, datumB: any, elementA: IGraphic, elementB: IGraphic, parameters: any) => number; + /** 动画执行相关控制配置项 */ + controlOptions?: IAnimationControlOptions; +} + +/** + * 动画配置 + */ +export type IAnimationConfig = IAnimationTimeline | IAnimationTypeConfig; diff --git a/packages/vrender-animate/src/index.ts b/packages/vrender-animate/src/index.ts index ef01e769c..2cd2a77f1 100644 --- a/packages/vrender-animate/src/index.ts +++ b/packages/vrender-animate/src/index.ts @@ -21,3 +21,4 @@ export { ClipGraphicAnimate, ClipAngleAnimate, ClipRadiusAnimate, ClipDirectionA export { TagPointsUpdate } from './custom/tag-points'; export { GroupFadeIn, GroupFadeOut } from './custom/group-fade'; export { RotateBySphereAnimate } from './custom/sphere'; +export { AnimateExecutor } from './executor/animate-executor'; diff --git a/packages/vrender-animate/src/interpolate/store.ts b/packages/vrender-animate/src/interpolate/store.ts index 43dd968fc..179f9fa28 100644 --- a/packages/vrender-animate/src/interpolate/store.ts +++ b/packages/vrender-animate/src/interpolate/store.ts @@ -182,13 +182,17 @@ export class InterpolateUpdateStore { (target.attribute as any).innerRadius = interpolateNumber(from, to, ratio); target.addUpdateBoundTag(); }; + size = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + (target.attribute as any).size = interpolateNumber(from, to, ratio); + target.addUpdateBoundTag(); + }; } export const interpolateUpdateStore = new InterpolateUpdateStore(); export function commonInterpolateUpdate(key: string, from: any, to: any, ratio: number, step: IStep, target: IGraphic) { if (Number.isFinite(to) && Number.isFinite(from)) { - target.attribute = from + (to - from) * ratio; + (target.attribute as any)[key] = from + (to - from) * ratio; return true; } else if (Array.isArray(to) && Array.isArray(from) && to.length === from.length) { const nextList = []; diff --git a/packages/vrender-animate/src/step.ts b/packages/vrender-animate/src/step.ts index 2809c73b3..207eae2ad 100644 --- a/packages/vrender-animate/src/step.ts +++ b/packages/vrender-animate/src/step.ts @@ -234,3 +234,18 @@ export class Step implements IStep { return this.getEndProps(); } } + +export class WaitStep extends Step { + constructor(type: IAnimateStepType, props: Record, duration: number, easing: EasingType) { + super(type, props, duration, easing); + } + + update(end: boolean, ratio: number, out: Record): void { + this.onStart(); + // 其他的不执行 + } + + determineInterpolateUpdateFunction(): void { + return; + } +} diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index 8ca151780..f07c1bbec 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -198,6 +198,9 @@ export abstract class Graphic = Partial; + static userSymbolMap: Record = {}; declare onBeforeAttributeUpdate?: ( diff --git a/packages/vrender-core/src/index.ts b/packages/vrender-core/src/index.ts index 27220af6f..e253ad412 100644 --- a/packages/vrender-core/src/index.ts +++ b/packages/vrender-core/src/index.ts @@ -120,7 +120,7 @@ export { RotateBySphereAnimate } from '@visactor/vrender-animate'; -// TODO VChart那块非要引用vgrammar-core,这里先临时导出,后面删除 +// TODO VChart那块要引用vgrammar-core,这里先临时导出,后面删除 export const AnimateGroup = {}; export const oneToMultiMorph = {}; export const multiToOneMorph = {}; diff --git a/packages/vrender-core/src/interface/graphic.ts b/packages/vrender-core/src/interface/graphic.ts index cce026232..3ace1c526 100644 --- a/packages/vrender-core/src/interface/graphic.ts +++ b/packages/vrender-core/src/interface/graphic.ts @@ -692,7 +692,10 @@ export interface IGraphic = Partial; backgroundImg?: boolean; attachedThemeGraphic?: IGraphic; - + /** + * 保存语法上下文 + */ + context?: Record; bindDom?: Map< string | HTMLElement, { container: HTMLElement | string; dom: HTMLElement | any; wrapGroup: HTMLDivElement | any; root?: any } diff --git a/packages/vrender-core/src/interface/index.ts b/packages/vrender-core/src/interface/index.ts index 67ea4971f..fb1e91221 100644 --- a/packages/vrender-core/src/interface/index.ts +++ b/packages/vrender-core/src/interface/index.ts @@ -57,7 +57,6 @@ export * from './context'; export * from './path'; export * from './color'; export * from './common'; -// export * from './animate'; export * from './camera'; export * from './matrix'; export * from './light'; diff --git a/packages/vrender/__tests__/browser/src/pages/animate-next.ts b/packages/vrender/__tests__/browser/src/pages/animate-next.ts index 5809c5261..6df2bf781 100644 --- a/packages/vrender/__tests__/browser/src/pages/animate-next.ts +++ b/packages/vrender/__tests__/browser/src/pages/animate-next.ts @@ -4,7 +4,8 @@ import { Animate, registerAnimate, IncreaseCount, - InputText + InputText, + AnimateExecutor } from '@visactor/vrender-animate'; import { container, @@ -14,7 +15,8 @@ import { IGraphic, vglobal, createCircle, - createText + createText, + createGroup } from '@visactor/vrender'; // container.load(roughModule); @@ -52,7 +54,7 @@ export const page = () => { btnContainer.style.flexDirection = 'row'; btnContainer.style.gap = '3px'; btnContainer.style.flexWrap = 'wrap'; - btnContainer.style.height = '60px'; + btnContainer.style.height = '90px'; const canvas = document.getElementById('main'); // 将btnContainer添加到canvas之前 canvas.parentNode.insertBefore(btnContainer, canvas); @@ -259,7 +261,8 @@ export const page = () => { format: 'thousandth', formatTemplate: '{{var}}%咿呀呀' }); - text.animate().play(customAnimate); + // Use cast to avoid type errors + text.animate().play(customAnimate as any); stage.defaultLayer.add(text); }); addCase('custom InputText', btnContainer, stage => { @@ -275,7 +278,329 @@ export const page = () => { }); // Terminal-style animation with prompt const terminalAnimation = new InputText({ text: '' }, { text: '这是一段文本内容' }, 1000, 'linear'); - text.animate().play(terminalAnimation); + // Use cast to avoid type errors + text.animate().play(terminalAnimation as any); + stage.defaultLayer.add(text); + }); + addCase('AnimateExecutor Basic', btnContainer, stage => { + // Add explanatory text + const group = createGroup({ + x: 100, + y: 100 + }); + + for (let i = 0; i < 6; i++) { + const rect = createRect({ + x: i * 100, + y: 100, + width: 80, + height: 100, + fill: 'black' + }); + group.add(rect); + } + const executor = new AnimateExecutor(group); + + // Basic animation - all elements fade and change color simultaneously + executor.execute({ + type: 'to', + channel: { + fill: { + to: 'red' + }, + opacity: { + to: 0.5 + } + }, + duration: 1000, + easing: 'linear' + }); + + // Add title + const text = createText({ + x: 300, + y: 50, + text: 'Basic AnimateExecutor - Simultaneous Animation', + fontSize: 16, + fill: 'black', + textAlign: 'center' + }); + stage.defaultLayer.add(group); + stage.defaultLayer.add(text); + }); + + addCase('AnimateExecutor oneByOne', btnContainer, stage => { + // Create a group with multiple rects + const group = createGroup({ + x: 100, + y: 100 + }); + + for (let i = 0; i < 6; i++) { + const rect = createRect({ + x: i * 100, + y: 0, + width: 80, + height: 100, + fill: 'blue', + opacity: 0.3 + }); + group.add(rect); + } + const executor = new AnimateExecutor(group); + + // Sequential animation - elements animate one after another + executor.execute({ + type: 'to', + channel: { + y: { + to: 200 + }, + fill: { + to: 'green' + }, + opacity: { + to: 1 + } + }, + oneByOne: true, // Enable sequential animation + duration: 500, + easing: 'quadOut' + }); + + // Add title + const text = createText({ + x: 300, + y: 50, + text: 'AnimateExecutor with oneByOne - Sequential Animation', + fontSize: 16, + fill: 'black', + textAlign: 'center' + }); + stage.defaultLayer.add(group); + stage.defaultLayer.add(text); + }); + + addCase('AnimateExecutor totalTime', btnContainer, stage => { + // Create a group with multiple elements + const group = createGroup({ + x: 50, + y: 150 + }); + + for (let i = 0; i < 10; i++) { + const circle = createCircle({ + x: i * 80, + y: 0, + radius: 30, + fill: 'purple', + opacity: 0.5 + }); + group.add(circle); + } + const executor = new AnimateExecutor(group); + + // Sequential animation with fixed total time + executor.execute({ + type: 'to', + channel: { + y: { + to: (datum: any, graphic: IGraphic, params: any) => { + // Alternate between up and down + return (graphic as any).idx % 2 === 0 ? 100 : -100; + } + }, + radius: { + to: 50 + }, + opacity: { + to: 1 + } + }, + oneByOne: true, // Enable sequential animation + totalTime: 600, // Entire animation sequence takes exactly 3 seconds + duration: 500, // Base duration before scaling + easing: 'bounceOut' + }); + + // Add title + const text = createText({ + x: 400, + y: 50, + text: 'AnimateExecutor with totalTime - Fixed Duration Animation', + fontSize: 16, + fill: 'black', + textAlign: 'center' + }); + stage.defaultLayer.add(group); + stage.defaultLayer.add(text); + }); + + addCase('AnimateExecutor Timeline', btnContainer, stage => { + // Create a group with elements + const group = createGroup({ + x: 200, + y: 200 + }); + + // Create 8 shapes arranged in a circle + for (let i = 0; i < 8; i++) { + const angle = (i / 8) * Math.PI * 2; + const x = Math.cos(angle) * 150; + const y = Math.sin(angle) * 150; + + // Create a symbol for variety + const symbol = createSymbol({ + x: x, + y: y, + size: 40, + symbolType: i % 4 === 0 ? 'circle' : i % 4 === 1 ? 'square' : i % 4 === 2 ? 'triangle' : 'diamond', + fill: 'orange', + stroke: 'black', + lineWidth: 2, + angle: 0 + }); + group.add(symbol); + } + + const executor = new AnimateExecutor(group); + + // Complex timeline animation + executor.execute({ + // Use a timeline configuration with multiple time slices + timeSlices: [ + { + // First slice - scale up + effects: { + type: 'to', + channel: { + size: { + to: 60 + }, + opacity: { + to: 0.7 + } + }, + easing: 'quadIn' + }, + duration: 500 + }, + { + // Second slice - rotate + effects: { + type: 'to', + channel: { + angle: { + to: Math.PI * 2 + } + }, + easing: 'linear' + }, + duration: 1000 + }, + { + // Third slice - change color and scale down + effects: [ + { + type: 'to', + channel: { + fill: { + to: 'red' + } + } + }, + { + type: 'to', + channel: { + size: { + to: 40 + } + } + } + ], + duration: 500 + } + ], + oneByOne: 100, // Sequential with 100ms interval + loop: 2 // Repeat twice + }); + + // Add title + const text = createText({ + x: 400, + y: 50, + text: 'AnimateExecutor with Timeline - Complex Animation Sequence', + fontSize: 16, + fill: 'black', + textAlign: 'center' + }); + stage.defaultLayer.add(group); + stage.defaultLayer.add(text); + }); + + addCase('AnimateExecutor Partitioner', btnContainer, stage => { + // Create a group with a grid of rectangles + const group = createGroup({ + x: 100, + y: 150 + }); + + // Create a 6x4 grid of rectangles + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 6; col++) { + const rect = createRect({ + x: col * 100, + y: row * 100, + width: 80, + height: 80, + fill: 'gray' + }); + rect.context = { + data: [{ row, col, even: (row + col) % 2 === 0 }] + }; + group.add(rect); + } + } + + const executor = new AnimateExecutor(group); + + // Apply animation only to elements where row + col is even + executor.execute({ + timeSlices: { + effects: { + type: 'to', + channel: { + fill: { + to: 'blue' + }, + width: { + to: 90 + }, + height: { + to: 90 + } + }, + easing: 'elasticOut' + }, + duration: 1000 + }, + // Partitioner function to filter elements + partitioner: (datum: any, graphic: IGraphic, params: any) => { + return datum && datum.length && datum[0].even === true; + }, + oneByOne: 50 + }); + + // Add title + const text = createText({ + x: 400, + y: 50, + text: 'AnimateExecutor with Partitioner - Filtered Animation', + fontSize: 16, + fill: 'black', + textAlign: 'center' + }); + stage.defaultLayer.add(group); stage.defaultLayer.add(text); }); }; From ab36a336c1652535779ad682f3b5b6cd1d7ff8ed Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 19 Mar 2025 13:16:54 +0800 Subject: [PATCH 040/179] feat: support custom animate for executor --- packages/vrender-animate/src/animate.ts | 14 +- .../src/executor/animate-executor.ts | 101 +++----- .../vrender-animate/src/intreface/animate.ts | 6 +- packages/vrender-animate/src/step.ts | 14 +- .../browser/src/pages/animate-next.ts | 218 +++++++++++++++++- 5 files changed, 276 insertions(+), 77 deletions(-) diff --git a/packages/vrender-animate/src/animate.ts b/packages/vrender-animate/src/animate.ts index 63f8cad10..10dd85b2b 100644 --- a/packages/vrender-animate/src/animate.ts +++ b/packages/vrender-animate/src/animate.ts @@ -41,6 +41,10 @@ export class Animate implements IAnimate { protected currentTime: number; slience?: boolean; + interpolateUpdateFunction: + | ((from: Record, to: Record, ratio: number, step: IStep, target: IGraphic) => void) + | null; + constructor( id: string | number = Generator.GenAutoIncrementId(), timeline: ITimeline = defaultTimeline, @@ -64,6 +68,7 @@ export class Animate implements IAnimate { this._endProps = {}; this._preventAttrs = new Set(); this.currentTime = 0; + this.interpolateUpdateFunction = null; } /** @@ -512,9 +517,14 @@ export class Animate implements IAnimate { // } /** - * 设置动画循环次数 + * 设置动画循环次数,如果传入true,则无限循环,如果传入false,则不循环 */ - loop(n: number): this { + loop(n: number | boolean): this { + if (n === true) { + n = Infinity; + } else if (n === false) { + n = 0; + } this._loopCount = n; this.updateDuration(); return this; diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index 868ee9e30..1ab736a4d 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -14,7 +14,7 @@ import type { import { ACustomAnimate } from '../custom/custom-animate'; import type { EasingType } from '../intreface/easing'; import type { IAnimate } from '../intreface/animate'; -import { cloneDeep, isArray, scale } from '@visactor/vutils'; +import { cloneDeep, isArray, isFunction, scale } from '@visactor/vutils'; export class AnimateExecutor { declare _target: IGroup; @@ -187,35 +187,37 @@ export class AnimateExecutor { // 根据 channel 配置创建属性对象 const props = this.createPropsFromChannel(channel, graphic); - if (type === 'to') { - animate.to(props, duration as number, easing); - } else if (type === 'from') { - animate.from(props, duration as number, easing); - } else if (custom) { - // 处理自定义动画 + // 处理自定义动画 + if (custom) { const customParams = this.resolveValue(customParameters, graphic, {}); - if (typeof custom === 'function') { - // 自定义插值器 - 创建自定义插值动画 - this.createCustomInterpolatorAnimation( - animate, - custom as IAnimationChannelInterpolator, - props, - duration as number, - easing, - customParams - ); - } else { - // 自定义动画构造器 - 创建自定义动画类 - this.createCustomAnimation( - animate, - custom as IAnimationCustomConstructor, - props, - duration as number, - easing, - customParams - ); + if (isFunction(custom)) { + if (/^class\s/.test(Function.prototype.toString.call(custom))) { + // 自定义动画构造器 - 创建自定义动画类 + this.createCustomAnimation( + animate, + custom as IAnimationCustomConstructor, + props, + duration as number, + easing, + customParams + ); + } else { + // 自定义插值器 - 创建自定义插值动画 + this.createCustomInterpolatorAnimation( + animate, + custom as IAnimationChannelInterpolator, + props, + duration as number, + easing, + customParams + ); + } } + } else if (type === 'to') { + animate.to(props, duration as number, easing); + } else if (type === 'from') { + animate.from(props, duration as number, easing); } // 添加后延迟 @@ -291,7 +293,7 @@ export class AnimateExecutor { // 处理自定义动画 const customParams = this.resolveValue(customParameters, graphic, {}); - if (typeof custom === 'function') { + if (isFunction(custom)) { // 自定义插值器 - 创建自定义插值动画 this.createCustomInterpolatorAnimation( animate, @@ -341,44 +343,11 @@ export class AnimateExecutor { from[key] = animate.target.getComputedAttribute(key); }); - // 创建自定义动画步骤 - // 注意:这里需要设计一个特殊的自定义动画类来处理插值器函数 - class CustomInterpolatorAnimate extends ACustomAnimate> { - interpolator: IAnimationChannelInterpolator; - parameters: any; - - constructor( - from: Record, - to: Record, - duration: number, - easing: EasingType, - interpolator: IAnimationChannelInterpolator, - parameters: any - ) { - super(from, to, duration, easing, parameters); - this.interpolator = interpolator; - this.parameters = parameters; - this.setProps(to); - } + animate.interpolateUpdateFunction = (from, to, ratio, step, target) => { + interpolator(ratio, from, to, step, target, animate.target, customParams); + }; - onUpdate(end: boolean, ratio: number, out: Record): void { - // 调用插值器函数进行自定义插值 - this.interpolator( - ratio, - this.customFrom, - this.props, - out, - (this.target.context as any)?.data, - this.target, - this.parameters - ); - } - } - - // 创建并添加自定义插值动画 - const customAnimate = new CustomInterpolatorAnimate(from, to, duration, easing, interpolator, customParams); - - animate.play(customAnimate); + animate.to(props, duration, easing); } /** @@ -460,7 +429,7 @@ export class AnimateExecutor { /** * 执行动画(具体执行到内部的单个图元) */ - executeItem(params: IAnimationConfig, graphic: IGraphic, index: number): IAnimate | null { + executeItem(params: IAnimationConfig, graphic: IGraphic, index: number = 0): IAnimate | null { if (!graphic) { return null; } diff --git a/packages/vrender-animate/src/intreface/animate.ts b/packages/vrender-animate/src/intreface/animate.ts index 781da095a..78d8a4b88 100644 --- a/packages/vrender-animate/src/intreface/animate.ts +++ b/packages/vrender-animate/src/intreface/animate.ts @@ -67,6 +67,10 @@ export interface IAnimate { status: AnimateStatus; target: IGraphic; + interpolateUpdateFunction: + | ((from: Record, to: Record, ratio: number, step: IStep, target: IGraphic) => void) + | null; + _onStart?: (() => void)[]; _onFrame?: ((step: IStep, ratio: number) => void)[]; _onEnd?: (() => void)[]; @@ -135,7 +139,7 @@ export interface IAnimate { // 反转动画 // reversed: (r: boolean) => IAnimate; // 循环动画 - loop: (n: number) => IAnimate; + loop: (n: number | boolean) => IAnimate; // 反弹动画 bounce: (b: boolean) => IAnimate; diff --git a/packages/vrender-animate/src/step.ts b/packages/vrender-animate/src/step.ts index 207eae2ad..cf4a77dee 100644 --- a/packages/vrender-animate/src/step.ts +++ b/packages/vrender-animate/src/step.ts @@ -184,12 +184,14 @@ export class Step implements IStep { } // 应用缓动函数 const easedRatio = this.easing(ratio); - this.interpolateUpdateFunctions.forEach((func, index) => { - const key = this.propKeys[index]; - const fromValue = this.fromProps[key]; - const toValue = this.props[key]; - func(key, fromValue, toValue, easedRatio, this, this.target); - }); + this.animate.interpolateUpdateFunction + ? this.animate.interpolateUpdateFunction(this.fromProps, this.props, easedRatio, this, this.target) + : this.interpolateUpdateFunctions.forEach((func, index) => { + const key = this.propKeys[index]; + const fromValue = this.fromProps[key]; + const toValue = this.props[key]; + func(key, fromValue, toValue, easedRatio, this, this.target); + }); this.onUpdate(end, easedRatio, out); } diff --git a/packages/vrender/__tests__/browser/src/pages/animate-next.ts b/packages/vrender/__tests__/browser/src/pages/animate-next.ts index 6df2bf781..c0204df84 100644 --- a/packages/vrender/__tests__/browser/src/pages/animate-next.ts +++ b/packages/vrender/__tests__/browser/src/pages/animate-next.ts @@ -5,7 +5,8 @@ import { registerAnimate, IncreaseCount, InputText, - AnimateExecutor + AnimateExecutor, + ACustomAnimate } from '@visactor/vrender-animate'; import { container, @@ -16,8 +17,11 @@ import { vglobal, createCircle, createText, - createGroup + createGroup, + createLine, + createPath } from '@visactor/vrender'; +import type { EasingType } from '@visactor/vrender-animate'; // container.load(roughModule); vglobal.setEnv('browser'); @@ -46,6 +50,41 @@ function addCase(name: string, container: HTMLElement, cb: (stage: any) => void) }); } +// Custom rainbow color interpolator function +function rainbowColorInterpolator( + ratio: number, + from: any, + to: any, + out: Record, + datum: any, + target: IGraphic, + params: any +) { + // Create rainbow effect using HSL colors + const hue = (ratio * 360) % 360; + target.attribute.fill = `hsl(${hue}, 80%, 60%)`; +} + +// Path tracing interpolator function +function pathTracingInterpolator( + ratio: number, + from: any, + to: any, + out: Record, + datum: any, + target: IGraphic, + params: any +) { + const path = params.path || 'M0,0 L100,0 L100,100 L0,100 Z'; + const length = 100 || params.length || 1000; // Estimate of path length + + // Set stroke-dasharray and stroke-dashoffset to create tracing effect + // out.strokeDasharray = length; + // out.strokeDashoffset = length * (1 - ratio); + target.attribute.lineDash = [length, length]; + target.attribute.lineDashOffset = length * (1 - ratio); +} + export const page = () => { const btnContainer = document.createElement('div'); btnContainer.style.width = '1000px'; @@ -603,4 +642,179 @@ export const page = () => { stage.defaultLayer.add(group); stage.defaultLayer.add(text); }); + + // New test cases for custom animations + addCase('AnimateExecutor Custom Interpolator', btnContainer, stage => { + // Create a group to hold all elements + const group = createGroup({ + x: 100, + y: 100 + }); + + // Add a title + const title = createText({ + x: 400, + y: 30, + text: 'AnimateExecutor with Custom Interpolator Function', + fontSize: 18, + fill: 'black', + textAlign: 'center' + }); + + // Create a row of rectangles for the rainbow effect + const rectangles = []; + for (let i = 0; i < 10; i++) { + const rect = createRect({ + x: i * 70, + y: 80, + width: 60, + height: 60, + fill: 'gray', + cornerRadius: 5 // Using cornerRadius instead of radius + }); + rectangles.push(rect); + group.add(rect); + } + + // Create path for the path tracing demo + const pathElement = createPath({ + path: 'M50,250 C150,150 250,350 350,250 S550,150 650,250', + stroke: 'black', + lineWidth: 5, + fill: 'transparent' + // strokeDasharray and strokeDashoffset will be set by the animation + }); + group.add(pathElement); + + // Create a text label for the path tracing demo + const pathLabel = createText({ + x: 350, + y: 210, + text: 'Path Tracing Animation', + fontSize: 14, + fill: 'black', + textAlign: 'center' + }); + group.add(pathLabel); + + // Create executor + const executor = new AnimateExecutor(group); + + // Rainbow color animation using custom interpolator + executor.execute({ + type: 'to', + custom: rainbowColorInterpolator, + channel: { + fill: { to: 'transparent' } // Placeholder, actual color will be set by interpolator + }, + oneByOne: 50, + duration: 2000, + loop: Infinity + }); + + // Path tracing animation using custom interpolator + executor.executeItem( + { + type: 'to', + custom: pathTracingInterpolator, + customParameters: { + path: 'M50,250 C150,150 250,350 350,250 S550,150 650,250', + length: 800 // Approximate length of the path + }, + channel: { + strokeDasharray: { to: 800 }, + strokeDashoffset: { to: 0 } + }, + duration: 3000, + loop: Infinity + }, + pathElement + ); + + stage.defaultLayer.add(group); + stage.defaultLayer.add(title); + }); + + addCase('AnimateExecutor Custom Animation Class', btnContainer, stage => { + // Create a group to hold all elements + const group = createGroup({ + x: 100, + y: 100 + }); + + // Add a title + const title = createText({ + x: 400, + y: 30, + text: 'AnimateExecutor with Custom Animation Classes', + fontSize: 18, + fill: 'black', + textAlign: 'center' + }); + + // Create text element for typewriter effect + const typewriterText = createText({ + x: 350, + y: 100, + text: '', + fontSize: 20, + fill: 'blue', + textAlign: 'center' + }); + group.add(typewriterText); + + // Create a row of circles for wave animation + const circles = []; + for (let i = 0; i < 12; i++) { + const circle = createCircle({ + x: 50 + i * 60, + y: 200, + radius: 15, + fill: 'purple', + opacity: 0.6 + }); + circles.push(circle); + group.add(circle); + } + + // Create executor + const executor = new AnimateExecutor(group); + + // Apply typewriter animation + executor.executeItem( + { + type: 'to', + custom: InputText, + channel: { + text: { to: 'This is a custom typewriter animation effect!' } + }, + duration: 3000, + loop: true + }, + typewriterText + ); + + // Apply wave animation to circles + // circles.forEach((circle, index) => { + // executor.executeItem( + // { + // type: 'to', + // custom: WaveAnimate, + // customParameters: { + // amplitude: 30, + // frequency: 2 + index * 0.2 // Different frequency for each circle + // }, + // channel: { + // y: { to: 200 } // This will be replaced by the wave animation + // }, + // duration: 3000, + // loop: true + // }, + // circle + // ); + // }); + + stage.defaultLayer.add(group); + stage.defaultLayer.add(title); + }); }; From 444d4f3e37107c4eb480adeb52787600854d1846 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 20 Mar 2025 15:27:06 +0800 Subject: [PATCH 041/179] feat: support base function for state animate --- packages/vrender-animate/src/animate.ts | 16 ++- .../src/executor/animate-executor.ts | 130 ++++++++++++++++-- packages/vrender-animate/src/index.ts | 3 + .../vrender-animate/src/intreface/animate.ts | 3 + packages/vrender-animate/src/register.ts | 5 + .../src/state/animation-state.ts | 116 ++++++++++++++++ .../src/state/animation-states-registry.ts | 128 +++++++++++++++++ .../src/state/graphic-extension.ts | 63 +++++++++ packages/vrender-animate/src/state/index.ts | 3 + packages/vrender-animate/src/state/types.ts | 39 ++++++ packages/vrender-animate/src/step.ts | 46 +++++++ 11 files changed, 540 insertions(+), 12 deletions(-) create mode 100644 packages/vrender-animate/src/state/animation-state.ts create mode 100644 packages/vrender-animate/src/state/animation-states-registry.ts create mode 100644 packages/vrender-animate/src/state/graphic-extension.ts create mode 100644 packages/vrender-animate/src/state/index.ts create mode 100644 packages/vrender-animate/src/state/types.ts diff --git a/packages/vrender-animate/src/animate.ts b/packages/vrender-animate/src/animate.ts index 10dd85b2b..6a6da6be6 100644 --- a/packages/vrender-animate/src/animate.ts +++ b/packages/vrender-animate/src/animate.ts @@ -188,7 +188,8 @@ export class Animate implements IAnimate { // 比如: // rect.animate().to({ x: 100 }, 1000, 'linear').to({ y: 100 }, 1000, 'linear'); // 在第二个step查找fromProps的时候,就能拿到第一个step的endProps中的y值(在原型链上) - Object.setPrototypeOf(step.props, this._startProps); + // TODO 由于会有其他animate的干扰,所以不能直接设置原型链 + // Object.setPrototypeOf(step.props, this._startProps); } /** @@ -219,7 +220,8 @@ export class Animate implements IAnimate { // 比如: // rect.animate().to({ x: 100 }, 1000, 'linear').to({ y: 100 }, 1000, 'linear'); // 在第二个step查找fromProps的时候,就能拿到第一个step的endProps中的y值(在原型链上) - Object.setPrototypeOf(currentStep.props, this._startProps); + // TODO 由于会有其他animate的干扰,所以不能直接设置原型链 + // Object.setPrototypeOf(currentStep.props, this._startProps); currentStep = currentStep.next; } } @@ -340,6 +342,14 @@ export class Animate implements IAnimate { */ preventAttr(key: string): void { this._preventAttrs.add(key); + // 从所有step中移除该属性,并从自身的_startProps和_endProps中移除该属性 + delete this._startProps[key]; + delete this._endProps[key]; + let step = this._firstStep; + while (step) { + step.deleteSelfAttr(key); + step = step.next; + } } /** @@ -412,6 +422,8 @@ export class Animate implements IAnimate { this.status = AnimateStatus.END; + this.onEnd(); + if (!this.target) { return; } diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index 1ab736a4d..5307032c7 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -11,23 +11,102 @@ import type { IAnimationCustomConstructor, IAnimationChannelInterpolator } from './executor'; -import { ACustomAnimate } from '../custom/custom-animate'; import type { EasingType } from '../intreface/easing'; import type { IAnimate } from '../intreface/animate'; import { cloneDeep, isArray, isFunction, scale } from '@visactor/vutils'; -export class AnimateExecutor { - declare _target: IGroup; +interface IAnimateExecutor { + execute: (params: IAnimationConfig) => void; + executeItem: (params: IAnimationConfig, graphic: IGraphic, index?: number) => IAnimate | null; + onStart: (cb?: () => void) => void; + onEnd: (cb?: () => void) => void; +} + +export class AnimateExecutor implements IAnimateExecutor { + declare _target: IGraphic; + + // 所有动画实例 + private _animates: IAnimate[] = []; + + // 动画开始回调 + private _startCallbacks: (() => void)[] = []; + // 动画结束回调 + private _endCallbacks: (() => void)[] = []; + + // 是否已经开始动画 + private _started: boolean = false; + + // 当前正在运行的动画数量 + private _activeCount: number = 0; - constructor(target: IGroup) { + constructor(target: IGraphic) { this._target = target; } /** - * 执行动画,针对图元组 - * 1. oneByOne 为 true 时,内部图元依次执行动画,每个图元通过(duration,delay,delayAfter)来控制自身动画的执行 - * 2. totalTime为总时长(如果有oneByOne的话,从开始到最后一个图元动画结束的时长为totalTime),如果配置了totalTime,那么duration,delay,delayAfter会根据totalTime进行等比缩放 - * 3. 如果配置了partitioner,则根据partitioner进行筛选子图元再执行动画 + * 注册一个回调,当动画开始时调用 + */ + onStart(cb?: () => void): void { + if (cb) { + this._startCallbacks.push(cb); + + // 如果动画已经开始,立即调用回调 + if (this._started && this._activeCount > 0) { + cb(); + } + } else { + this._startCallbacks.forEach(cb => { + cb(); + }); + } + } + + /** + * 注册一个回调,当所有动画结束时调用 + */ + onEnd(cb?: () => void): void { + if (cb) { + this._endCallbacks.push(cb); + } else { + this._endCallbacks.forEach(cb => { + cb(); + }); + } + } + + /** + * 跟踪动画并附加生命周期钩子 + */ + private _trackAnimation(animate: IAnimate): void { + this._animates.push(animate); + this._activeCount++; + + // 如果这是第一个正在运行的动画,触发onStart回调 + if (this._activeCount === 1 && !this._started) { + this._started = true; + this.onStart(); + } + + // 处理动画完成 + animate.onEnd(() => { + this._activeCount--; + + // 从跟踪的动画中移除 + const index = this._animates.indexOf(animate); + if (index >= 0) { + this._animates.splice(index, 1); + } + + // 如果所有动画都已完成,触发onEnd回调 + if (this._activeCount === 0 && this._started) { + this._started = false; + this.onEnd(); + } + }); + } + + /** + * 执行动画,针对一组元素 */ execute(params: IAnimationConfig) { // 判断是否为timeline配置 @@ -35,6 +114,7 @@ export class AnimateExecutor { // 获取子图元 if (this._target.count <= 1) { + this.executeItem(params, this._target, 0); return; } @@ -134,11 +214,17 @@ export class AnimateExecutor { const cb = isTimeline ? (child: IGraphic, index: number) => { // 执行单个图元的timeline动画 - this.executeTimelineItem(parsedParams as IAnimationTimeline, child, index); + const animate = this.executeTimelineItem(parsedParams as IAnimationTimeline, child, index); + if (animate) { + this._trackAnimation(animate); + } } : (child: IGraphic, index: number) => { // 执行单个图元的config动画 - this.executeTypeConfigItem(parsedParams as IAnimationTypeConfig, child, index); + const animate = this.executeTypeConfigItem(parsedParams as IAnimationTypeConfig, child, index); + if (animate) { + this._trackAnimation(animate); + } }; // 执行每个图元的动画 @@ -445,6 +531,30 @@ export class AnimateExecutor { animate = this.executeTypeConfigItem(params as IAnimationTypeConfig, graphic, index); } + // 跟踪动画以进行生命周期管理 + if (animate) { + this._trackAnimation(animate); + } + return animate; } + + /** + * 停止所有由该执行器管理的动画 + */ + stop(): void { + this._animates.forEach(animate => { + animate.stop(); + }); + + // 清空动画实例数组 + this._animates = []; + this._activeCount = 0; + + // 如果动画正在运行,触发结束回调 + if (this._started) { + this._started = false; + this.onEnd(); + } + } } diff --git a/packages/vrender-animate/src/index.ts b/packages/vrender-animate/src/index.ts index 2cd2a77f1..9bd09b652 100644 --- a/packages/vrender-animate/src/index.ts +++ b/packages/vrender-animate/src/index.ts @@ -22,3 +22,6 @@ export { TagPointsUpdate } from './custom/tag-points'; export { GroupFadeIn, GroupFadeOut } from './custom/group-fade'; export { RotateBySphereAnimate } from './custom/sphere'; export { AnimateExecutor } from './executor/animate-executor'; + +// Export animation state modules +export * from './state'; diff --git a/packages/vrender-animate/src/intreface/animate.ts b/packages/vrender-animate/src/intreface/animate.ts index 78d8a4b88..16090bec6 100644 --- a/packages/vrender-animate/src/intreface/animate.ts +++ b/packages/vrender-animate/src/intreface/animate.ts @@ -60,6 +60,9 @@ export interface IStep { getEndProps: () => Record | void; getFromProps: () => Record | void; getMergedEndProps: () => Record | void; + + // 屏蔽自身属性,会直接从props等内容里删除掉 + deleteSelfAttr: (key: string) => void; } export interface IAnimate { diff --git a/packages/vrender-animate/src/register.ts b/packages/vrender-animate/src/register.ts index 53b25629f..1dfc36171 100644 --- a/packages/vrender-animate/src/register.ts +++ b/packages/vrender-animate/src/register.ts @@ -2,6 +2,8 @@ import { Graphic } from '@visactor/vrender-core'; import { Animate } from './animate'; import { DefaultTimeline } from './timeline'; import { DefaultTicker } from './ticker/default-ticker'; +import { mixin } from '@visactor/vutils'; +import { GraphicStateExtension } from './state/graphic-extension'; export function registerAnimate() { if (!(Graphic as any).Animate) { @@ -13,4 +15,7 @@ export function registerAnimate() { if (!(Graphic as any).Ticker) { (Graphic as any).Ticker = DefaultTicker; } + + // Mix in animation state methods to Graphic prototype + mixin(Graphic.prototype, new GraphicStateExtension()); } diff --git a/packages/vrender-animate/src/state/animation-state.ts b/packages/vrender-animate/src/state/animation-state.ts new file mode 100644 index 000000000..5780c97b1 --- /dev/null +++ b/packages/vrender-animate/src/state/animation-state.ts @@ -0,0 +1,116 @@ +import type { IGraphic } from '@visactor/vrender-core'; +import type { IAnimationState } from './types'; +import { AnimationTransitionRegistry } from './animation-states-registry'; +import type { IAnimationConfig } from '../executor/executor'; +import { AnimateExecutor } from '../executor/animate-executor'; + +export class AnimationStateStore { + graphic: IGraphic; + + constructor(graphic: IGraphic) { + this.graphic = graphic; + } + + // 动画状态配置 + // 并不是所有图元都有(只有mark才有),所以在应用状态的时候,需要额外传入 + states?: Map; + + registerState(state: IAnimationState): void { + if (!this.states) { + this.states = new Map(); + } + this.states.set(state.name, state); + } + + clearStates(): void { + this.states?.clear(); + } +} + +// 一个状态对应一个执行器,每个图元都有一一对应 +interface IStateInfo { + state: string; + animationConfig: IAnimationConfig; + executor: AnimateExecutor; +} + +export class AnimationStateManager { + protected graphic: IGraphic; + + // 当前状态 + // TODO(注意,这里无法了解动画的顺序,既有串行也有并行,具体在执行的时候确定,执行之后就无法获取串行或并行配置了) + stateList: IStateInfo[] | null = null; + + constructor(graphic: IGraphic) { + this.graphic = graphic; + } + + // TODO 这里因为只有状态变更才会调用,所以代码写的比较宽松,如果有性能问题需要优化 + /** + * 应用状态 + * @param nextState 下一个状态数组,如果传入数组,那么状态是串行的。但是每次applyState都会立即执行动画,也就是applyState和applyState之间是并行 + * @param animationConfig 动画配置 + */ + applyState(nextState: string[], animationConfig: IAnimationState[]): void { + const registry = AnimationTransitionRegistry.getInstance(); + + // TODO 这里指判断第一个状态,后续如果需要的话要循环判断 + const _stateList = this.stateList[0]; + // 检查是否需要停止当前状态,以及下一个状态是否需要应用 + const shouldStopState: IStateInfo[] = []; + const shouldApplyState: IStateInfo[] = []; + if (!_stateList) { + nextState.forEach((state, index) => { + shouldApplyState.push({ + state, + animationConfig: animationConfig[index].animation, + executor: new AnimateExecutor(this.graphic) + }); + }); + } else { + nextState.forEach((state, index) => { + const result = registry.isTransitionAllowed(_stateList.state, state, this.graphic); + if (result.allowTransition) { + shouldApplyState.push({ + state, + animationConfig: animationConfig[index].animation, + executor: new AnimateExecutor(this.graphic) + }); + } + if (result.stopOriginalTransition) { + shouldStopState.push(_stateList); + } + }); + } + + // 停止动画 + shouldStopState.forEach(state => { + state.executor.stop(); + }); + + // 立即应用动画,串行的应用 + for (let i = 0; i < shouldApplyState.length; i++) { + shouldApplyState[i].executor.execute(shouldApplyState[i].animationConfig); + // 如果下一个状态存在,那么下一个状态的动画在当前状态动画结束后立即执行 + const nextState = shouldApplyState[i + 1]; + shouldApplyState[i].executor.onEnd(() => { + if (nextState) { + nextState.executor.execute(nextState.animationConfig); + } + // 删除这个状态 + this.stateList = this.stateList.filter(state => state !== shouldApplyState[i]); + }); + } + + this.stateList = this.stateList.filter(state => !shouldStopState.includes(state)); + this.stateList.push(...shouldApplyState); + } + + clearState(): void { + this.stateList = null; + } + + // getstateList(): string[] | null { + // return this.stateList; + // } +} diff --git a/packages/vrender-animate/src/state/animation-states-registry.ts b/packages/vrender-animate/src/state/animation-states-registry.ts new file mode 100644 index 000000000..3b49566af --- /dev/null +++ b/packages/vrender-animate/src/state/animation-states-registry.ts @@ -0,0 +1,128 @@ +import type { IGraphic } from '@visactor/vrender-core'; + +interface ITransitionResult { + allowTransition: boolean; + stopOriginalTransition: boolean; +} + +/** + * 注册动画状态切换的转换函数 + */ +export type TransitionFunction = (graphic: IGraphic, fromState: string) => ITransitionResult; + +/** + * 动画状态切换的注册表 + * 管理所有图形的动画状态切换逻辑 + */ +export class AnimationTransitionRegistry { + private static instance: AnimationTransitionRegistry; + + // 源状态到目标状态的映射,每个目标状态都有一个转换函数 + private transitions: Map> = new Map(); + + constructor() { + this.registerDefaultTransitions(); + } + + /** + * 获取注册表的单例实例 + */ + static getInstance(): AnimationTransitionRegistry { + if (!AnimationTransitionRegistry.instance) { + AnimationTransitionRegistry.instance = new AnimationTransitionRegistry(); + } + return AnimationTransitionRegistry.instance; + } + + /** + * 注册默认的转换规则 + */ + private registerDefaultTransitions(): void { + // 设置默认的转换规则 + // 退出动画不能被中断,除非是进入动画 + this.registerTransition('exit', 'enter', () => ({ + allowTransition: true, + stopOriginalTransition: true + })); + this.registerTransition('exit', '*', () => ({ + allowTransition: false, + stopOriginalTransition: false + })); + + // 进入动画可以被任何动画中断 + this.registerTransition('enter', '*', () => ({ + allowTransition: true, + stopOriginalTransition: true + })); + + // Disappear 是一个退出动画,遵循相同的规则 + this.registerTransition('disappear', 'enter', () => ({ + allowTransition: true, + stopOriginalTransition: true + })); + this.registerTransition('disappear', 'appear', () => ({ + allowTransition: true, + stopOriginalTransition: true + })); + this.registerTransition('disappear', '*', () => ({ + allowTransition: false, + stopOriginalTransition: false + })); + + // Appear 是一个进入动画,可以被任何动画中断 + this.registerTransition('appear', '*', () => ({ + allowTransition: true, + stopOriginalTransition: true + })); + } + + /** + * 检查两个状态之间是否允许转换 + */ + isTransitionAllowed(fromState: string, toState: string, graphic: IGraphic): ITransitionResult { + // 直接转换规则 + if (this.transitions.get(fromState)?.has(toState)) { + return this.transitions.get(fromState).get(toState)(graphic, fromState); + } + + // 状态到通配符 + if (this.transitions.get(fromState)?.has('*')) { + return this.transitions.get(fromState).get('*')(graphic, fromState); + } + + // 通配符到状态 + if (this.transitions.get('*')?.has(toState)) { + return this.transitions.get('*').get(toState)(graphic, fromState); + } + + // 通配符到通配符 + if (this.transitions.get('*')?.has('*')) { + return this.transitions.get('*').get('*')(graphic, fromState); + } + + // 默认允许转换 + return { + allowTransition: true, + stopOriginalTransition: true + }; + } + + /** + * 注册两个状态之间的转换 + */ + registerTransition(fromState: string, toState: string, transition: TransitionFunction): void { + let fromStateMap = this.transitions.get(fromState); + + if (!fromStateMap) { + fromStateMap = new Map(); + this.transitions.set(fromState, fromStateMap); + } + + fromStateMap.set(toState, transition); + } +} + +// 初始化单例转换注册表 +const transitionRegistry = AnimationTransitionRegistry.getInstance(); + +export { transitionRegistry }; diff --git a/packages/vrender-animate/src/state/graphic-extension.ts b/packages/vrender-animate/src/state/graphic-extension.ts new file mode 100644 index 000000000..454349a78 --- /dev/null +++ b/packages/vrender-animate/src/state/graphic-extension.ts @@ -0,0 +1,63 @@ +import type { IGraphic } from '@visactor/vrender-core'; +import type { IAnimationState } from './types'; +import { AnimationStateManager, AnimationStateStore } from './animation-state'; + +/** + * 将动画状态方法作为混入扩展 Graphic 的类 + */ +export class GraphicStateExtension { + _getAnimationStateManager(graphic: IGraphic): AnimationStateManager { + if (!(graphic as any)._animationStateManager) { + // Create the appropriate manager type based on whether this is a group + (graphic as any)._animationStateManager = new AnimationStateManager(graphic); + } + return (graphic as any)._animationStateManager; + } + _getAnimationStateStore(graphic: IGraphic): AnimationStateStore { + if (!(graphic as any)._animationStateStore) { + // Create the appropriate manager type based on whether this is a group + (graphic as any)._animationStateStore = new AnimationStateStore(graphic); + } + return (graphic as any)._animationStateStore; + } + + /** + * 注册一个动画状态 + */ + registerAnimationState(state: IAnimationState): this { + this._getAnimationStateStore(this as unknown as IGraphic).registerState(state); + return this; + } + + /** + * 应用一个动画状态到图形 + */ + applyAnimationState(state: string[], animationConfig: IAnimationState[]): this { + this._getAnimationStateManager(this as unknown as IGraphic).applyState(state, animationConfig); + return this; + } + + /** + * 清除图形上的所有动画状态 + */ + clearAnimationStates(): this { + this._getAnimationStateManager(this as unknown as IGraphic).clearState(); + return this; + } + + // /** + // * 获取图形当前的动画状态 + // */ + // getCurrentAnimationState(): string[] | null { + // return this._getAnimationStateManager(this as unknown as IGraphic).getCurrentState(); + // } + + /** + * 继承 + */ + static extend(graphic: IGraphic): IGraphic { + const extension = new GraphicStateExtension(); + extension._getAnimationStateManager(graphic); + return graphic; + } +} diff --git a/packages/vrender-animate/src/state/index.ts b/packages/vrender-animate/src/state/index.ts new file mode 100644 index 000000000..980a74dc0 --- /dev/null +++ b/packages/vrender-animate/src/state/index.ts @@ -0,0 +1,3 @@ +export * from './animation-state'; +export * from './graphic-extension'; +export * from './animation-states-registry'; diff --git a/packages/vrender-animate/src/state/types.ts b/packages/vrender-animate/src/state/types.ts new file mode 100644 index 000000000..8c474a9eb --- /dev/null +++ b/packages/vrender-animate/src/state/types.ts @@ -0,0 +1,39 @@ +import type { IGraphic } from '@visactor/vrender-core'; +import type { IAnimationConfig } from '../executor/executor'; + +export interface IAnimationState { + /** + * 状态名称 + */ + name: string; + + /** + * 动画配置 + */ + animation: IAnimationConfig; +} + +/** + * Animation state manager for a graphic + */ +export interface IAnimationStateManager { + /** + * Register a state for the graphic + */ + registerState: (state: IAnimationState) => void; + + /** + * Apply a state to the graphic + */ + applyState: (state: string | string[]) => void; + + /** + * Clear all states from the graphic + */ + clearStates: () => void; + + /** + * Get the current state of the graphic + */ + getCurrentState: () => string | string[] | null; +} diff --git a/packages/vrender-animate/src/step.ts b/packages/vrender-animate/src/step.ts index cf4a77dee..5d0a88d74 100644 --- a/packages/vrender-animate/src/step.ts +++ b/packages/vrender-animate/src/step.ts @@ -169,9 +169,51 @@ export class Step implements IStep { this.fromProps = this.getLastProps(); this.determineInterpolateUpdateFunction(); this.onFirstRun(); + this.tryPreventConflict(); + this.trySyncStartProps(); } } + protected tryPreventConflict(): void { + // 屏蔽掉之前动画冲突的属性 + const animate = this.animate; + const target = this.target; + target.animates.forEach(a => { + if (a.id === animate.id) { + return; + } + const fromProps = a.getStartProps(); + this.propKeys.forEach(key => { + if (fromProps[key] != null) { + a.preventAttr(key); + } + }); + }); + } + + /** + * 删除自身属性,会直接从props等内容里删除掉 + */ + deleteSelfAttr(key: string): void { + delete this.props[key]; + delete this.fromProps[key]; + const index = this.propKeys.indexOf(key); + if (index !== -1) { + this.propKeys.splice(index, 1); + this.interpolateUpdateFunctions?.splice(index, 1); + } + } + + /** + * 尝试同步startProps,因为当前animate的startProps仅包含当前animate的信息,不排除过程中有其他animate的干扰 + * 所以为了避免属性突变,需要确保startProps的属性值是最新的 + */ + trySyncStartProps(): void { + this.propKeys.forEach(key => { + this.fromProps[key] = this.animate.target.getComputedAttribute(key); + }); + } + /** * 更新执行的时候调用 * 如果跳帧了就不一定会执行 @@ -187,6 +229,10 @@ export class Step implements IStep { this.animate.interpolateUpdateFunction ? this.animate.interpolateUpdateFunction(this.fromProps, this.props, easedRatio, this, this.target) : this.interpolateUpdateFunctions.forEach((func, index) => { + // 如果这个属性被屏蔽了,直接跳过 + if (!this.animate.validAttr(this.propKeys[index])) { + return; + } const key = this.propKeys[index]; const fromValue = this.fromProps[key]; const toValue = this.props[key]; From da0f4da7030ae74e2b458a982451249a9f7b5a49 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 20 Mar 2025 20:05:09 +0800 Subject: [PATCH 042/179] feat: support status schedule --- packages/vrender-animate/src/animate.ts | 9 +- .../src/executor/animate-executor.ts | 24 +- .../vrender-animate/src/executor/executor.ts | 4 + .../vrender-animate/src/intreface/animate.ts | 2 +- packages/vrender-animate/src/register.ts | 2 +- .../src/state/animation-state.ts | 47 +- .../src/state/animation-states-registry.ts | 93 +- packages/vrender-animate/src/step.ts | 4 +- packages/vrender-core/src/graphic/graphic.ts | 3 + .../browser/src/pages/animate-next.ts | 231 ++++- .../browser/src/pages/animate-state.ts | 981 ++++++++++++++++++ .../__tests__/browser/src/pages/index.ts | 4 + .../browser/src/pages/performance.ts | 202 ++-- 13 files changed, 1411 insertions(+), 195 deletions(-) create mode 100644 packages/vrender/__tests__/browser/src/pages/animate-state.ts diff --git a/packages/vrender-animate/src/animate.ts b/packages/vrender-animate/src/animate.ts index 6a6da6be6..c206671b2 100644 --- a/packages/vrender-animate/src/animate.ts +++ b/packages/vrender-animate/src/animate.ts @@ -37,6 +37,8 @@ export class Animate implements IAnimate { private _startProps: Record; private _endProps: Record; private _preventAttrs: Set; + // 优先级,用于判定是否能被后续的动画preventAttr + declare priority: number; protected currentTime: number; slience?: boolean; @@ -69,6 +71,7 @@ export class Animate implements IAnimate { this._preventAttrs = new Set(); this.currentTime = 0; this.interpolateUpdateFunction = null; + this.priority = 0; } /** @@ -610,7 +613,7 @@ export class Animate implements IAnimate { const stepEndTime = stepStartTime + stepDuration; // 找到当前周期时间所在的step - if (cycleTime >= stepStartTime && cycleTime < stepEndTime) { + if (cycleTime >= stepStartTime && cycleTime <= stepEndTime) { targetStep = currentStep; break; } @@ -621,8 +624,8 @@ export class Animate implements IAnimate { // 如果没找到目标step(可能是所有step都执行完了,但整体动画还没结束,这正常是不存在的) if (!targetStep) { - this.currentTime = nextTime; - console.warn('动画出现问题'); + // this.currentTime = nextTime; + // console.warn('动画出现问题'); return; } diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index 5307032c7..9e75dfb80 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -112,12 +112,6 @@ export class AnimateExecutor implements IAnimateExecutor { // 判断是否为timeline配置 const isTimeline = 'timeSlices' in params; - // 获取子图元 - if (this._target.count <= 1) { - this.executeItem(params, this._target, 0); - return; - } - // 筛选符合条件的子图元 let filteredChildren: IGraphic[]; @@ -230,6 +224,8 @@ export class AnimateExecutor implements IAnimateExecutor { // 执行每个图元的动画 if (filteredChildren) { filteredChildren.forEach(cb); + } else if (this._target.count <= 1) { + cb(this._target, 0); } else { this._target.forEachChildren(cb); } @@ -253,12 +249,15 @@ export class AnimateExecutor implements IAnimateExecutor { startTime = 0, oneByOneDelay = 0, loop, + bounce, + priority = 0, options, controlOptions } = params as any; // 创建动画实例 const animate = graphic.animate() as unknown as IAnimate; + animate.priority = priority; const delayValue = delay as number; @@ -316,6 +315,11 @@ export class AnimateExecutor implements IAnimateExecutor { animate.loop(loop as number); } + // 设置反弹 + if (bounce) { + animate.bounce(true); + } + return animate; } @@ -323,10 +327,11 @@ export class AnimateExecutor implements IAnimateExecutor { * 执行 Timeline 类型的动画 */ private executeTimelineItem(params: IAnimationTimeline, graphic: IGraphic, index: number): IAnimate { - const { timeSlices, startTime = 0, loop, oneByOneDelay, controlOptions } = params as any; + const { timeSlices, startTime = 0, loop, bounce, oneByOneDelay, priority, controlOptions } = params as any; // 创建动画实例 const animate = graphic.animate() as unknown as IAnimate; + animate.priority = priority; // 设置开始时间 animate.startAt((startTime as number) + index * oneByOneDelay); @@ -336,6 +341,11 @@ export class AnimateExecutor implements IAnimateExecutor { animate.loop(loop as number); } + // 设置反弹 + if (bounce) { + animate.bounce(true); + } + // 处理时间切片 const slices = Array.isArray(timeSlices) ? timeSlices : [timeSlices]; diff --git a/packages/vrender-animate/src/executor/executor.ts b/packages/vrender-animate/src/executor/executor.ts index 4cd4bdb7d..3e03f3a50 100644 --- a/packages/vrender-animate/src/executor/executor.ts +++ b/packages/vrender-animate/src/executor/executor.ts @@ -113,6 +113,8 @@ export interface IAnimationTypeConfig { options?: MarkFunctionValueType; /** 动画执行相关控制配置项 */ controlOptions?: IAnimationControlOptions; + /** 动画优先级 */ + priority?: number; } /** @@ -139,6 +141,8 @@ export interface IAnimationTimeline { sort?: (datumA: any, datumB: any, elementA: IGraphic, elementB: IGraphic, parameters: any) => number; /** 动画执行相关控制配置项 */ controlOptions?: IAnimationControlOptions; + /** 动画优先级 */ + priority?: number; } /** diff --git a/packages/vrender-animate/src/intreface/animate.ts b/packages/vrender-animate/src/intreface/animate.ts index 16090bec6..8145ccc4c 100644 --- a/packages/vrender-animate/src/intreface/animate.ts +++ b/packages/vrender-animate/src/intreface/animate.ts @@ -69,7 +69,7 @@ export interface IAnimate { readonly id: string | number; status: AnimateStatus; target: IGraphic; - + priority: number; interpolateUpdateFunction: | ((from: Record, to: Record, ratio: number, step: IStep, target: IGraphic) => void) | null; diff --git a/packages/vrender-animate/src/register.ts b/packages/vrender-animate/src/register.ts index 1dfc36171..04595cdcf 100644 --- a/packages/vrender-animate/src/register.ts +++ b/packages/vrender-animate/src/register.ts @@ -17,5 +17,5 @@ export function registerAnimate() { } // Mix in animation state methods to Graphic prototype - mixin(Graphic.prototype, new GraphicStateExtension()); + mixin(Graphic, GraphicStateExtension); } diff --git a/packages/vrender-animate/src/state/animation-state.ts b/packages/vrender-animate/src/state/animation-state.ts index 5780c97b1..38b178474 100644 --- a/packages/vrender-animate/src/state/animation-state.ts +++ b/packages/vrender-animate/src/state/animation-state.ts @@ -55,11 +55,10 @@ export class AnimationStateManager { const registry = AnimationTransitionRegistry.getInstance(); // TODO 这里指判断第一个状态,后续如果需要的话要循环判断 - const _stateList = this.stateList[0]; // 检查是否需要停止当前状态,以及下一个状态是否需要应用 const shouldStopState: IStateInfo[] = []; const shouldApplyState: IStateInfo[] = []; - if (!_stateList) { + if (!(this.stateList && this.stateList.length)) { nextState.forEach((state, index) => { shouldApplyState.push({ state, @@ -68,8 +67,18 @@ export class AnimationStateManager { }); }); } else { + const _stateList = this.stateList[0]; nextState.forEach((state, index) => { - const result = registry.isTransitionAllowed(_stateList.state, state, this.graphic); + // 遍历this.stateList,获取result,只要有一个是false,那这个result就是false + const result: { allowTransition: boolean; stopOriginalTransition: boolean } = { + allowTransition: true, + stopOriginalTransition: true + }; + this.stateList.forEach(currState => { + const _result = registry.isTransitionAllowed(currState.state, state, this.graphic); + result.allowTransition = result.allowTransition && _result.allowTransition; + result.stopOriginalTransition = result.stopOriginalTransition && _result.stopOriginalTransition; + }); if (result.allowTransition) { shouldApplyState.push({ state, @@ -89,24 +98,34 @@ export class AnimationStateManager { }); // 立即应用动画,串行的应用 - for (let i = 0; i < shouldApplyState.length; i++) { - shouldApplyState[i].executor.execute(shouldApplyState[i].animationConfig); + if (shouldApplyState.length) { + shouldApplyState[0].executor.execute(shouldApplyState[0].animationConfig); // 如果下一个状态存在,那么下一个状态的动画在当前状态动画结束后立即执行 - const nextState = shouldApplyState[i + 1]; - shouldApplyState[i].executor.onEnd(() => { - if (nextState) { - nextState.executor.execute(nextState.animationConfig); - } - // 删除这个状态 - this.stateList = this.stateList.filter(state => state !== shouldApplyState[i]); - }); + for (let i = 1; i < shouldApplyState.length; i++) { + const nextState = shouldApplyState[i]; + shouldApplyState[i - 1].executor.onEnd(() => { + if (nextState) { + nextState.executor.execute(nextState.animationConfig); + } + // 删除这个状态 + this.stateList = this.stateList.filter(state => state !== shouldApplyState[i]); + }); + } } - this.stateList = this.stateList.filter(state => !shouldStopState.includes(state)); + if (this.stateList) { + this.stateList = this.stateList.filter(state => !shouldStopState.includes(state)); + } else { + this.stateList = []; + } this.stateList.push(...shouldApplyState); } clearState(): void { + // 清空状态 + this.stateList?.forEach(state => { + state.executor.stop(); + }); this.stateList = null; } diff --git a/packages/vrender-animate/src/state/animation-states-registry.ts b/packages/vrender-animate/src/state/animation-states-registry.ts index 3b49566af..b8cde7c49 100644 --- a/packages/vrender-animate/src/state/animation-states-registry.ts +++ b/packages/vrender-animate/src/state/animation-states-registry.ts @@ -38,39 +38,90 @@ export class AnimationTransitionRegistry { * 注册默认的转换规则 */ private registerDefaultTransitions(): void { - // 设置默认的转换规则 - // 退出动画不能被中断,除非是进入动画 - this.registerTransition('exit', 'enter', () => ({ + // appear动画,可以被任何动画覆盖,但不会停止(disappear、exit除外) + this.registerTransition('appear', '*', () => ({ + allowTransition: true, + stopOriginalTransition: false + })); + // appear 动画碰到appear动画,什么都不会发生 + this.registerTransition('appear', 'appear', () => ({ + allowTransition: false, + stopOriginalTransition: false + })); + this.registerTransition('appear', 'disappear', () => ({ allowTransition: true, stopOriginalTransition: true })); - this.registerTransition('exit', '*', () => ({ + this.registerTransition('appear', 'exit', () => ({ + allowTransition: true, + stopOriginalTransition: true + })); + + // 循环动画(normal),可以被任何动画覆盖,但不会停止(disappear、exit除外) + this.registerTransition('normal', '*', () => ({ + allowTransition: true, + stopOriginalTransition: false + })); + this.registerTransition('normal', 'normal', () => ({ allowTransition: false, stopOriginalTransition: false })); + this.registerTransition('normal', 'disappear', () => ({ + allowTransition: true, + stopOriginalTransition: true + })); + this.registerTransition('normal', 'exit', () => ({ + allowTransition: true, + stopOriginalTransition: true + })); - // 进入动画可以被任何动画中断 - this.registerTransition('enter', '*', () => ({ + // 退出动画不能被覆盖或停止(disappear除外) + this.registerTransition('exit', '*', () => ({ + allowTransition: false, + stopOriginalTransition: false + })); + this.registerTransition('exit', 'disappear', () => ({ allowTransition: true, stopOriginalTransition: true })); + // 退出动画碰到enter动画,会立即停止 + this.registerTransition('exit', 'enter', () => ({ + allowTransition: true, + stopOriginalTransition: true + })); + // 退出动画碰到退出,什么都不会发生 + this.registerTransition('exit', 'exit', () => ({ + allowTransition: false, + stopOriginalTransition: false + })); - // Disappear 是一个退出动画,遵循相同的规则 - this.registerTransition('disappear', 'enter', () => ({ + // enter 动画可以被任何动画覆盖,但不会停止(exit、disappear除外) + this.registerTransition('enter', '*', () => ({ + allowTransition: true, + stopOriginalTransition: false + })); + // enter 动画碰到enter动画,什么都不会发生 + this.registerTransition('enter', 'enter', () => ({ + allowTransition: false, + stopOriginalTransition: false + })); + this.registerTransition('enter', 'disappear', () => ({ allowTransition: true, stopOriginalTransition: true })); - this.registerTransition('disappear', 'appear', () => ({ + this.registerTransition('enter', 'exit', () => ({ allowTransition: true, stopOriginalTransition: true })); + + // disappear 动画碰到任何动画,什么都不会发生(appear除外) this.registerTransition('disappear', '*', () => ({ allowTransition: false, stopOriginalTransition: false })); - // Appear 是一个进入动画,可以被任何动画中断 - this.registerTransition('appear', '*', () => ({ + // disappear 动画碰到appear动画,会立即停止 + this.registerTransition('disappear', 'appear', () => ({ allowTransition: true, stopOriginalTransition: true })); @@ -81,23 +132,27 @@ export class AnimationTransitionRegistry { */ isTransitionAllowed(fromState: string, toState: string, graphic: IGraphic): ITransitionResult { // 直接转换规则 - if (this.transitions.get(fromState)?.has(toState)) { - return this.transitions.get(fromState).get(toState)(graphic, fromState); + let func = this.transitions.get(fromState)?.get(toState); + if (func) { + return func(graphic, fromState); } // 状态到通配符 - if (this.transitions.get(fromState)?.has('*')) { - return this.transitions.get(fromState).get('*')(graphic, fromState); + func = this.transitions.get(fromState)?.get('*'); + if (func) { + return func(graphic, fromState); } // 通配符到状态 - if (this.transitions.get('*')?.has(toState)) { - return this.transitions.get('*').get(toState)(graphic, fromState); + func = this.transitions.get('*')?.get(toState); + if (func) { + return func(graphic, fromState); } // 通配符到通配符 - if (this.transitions.get('*')?.has('*')) { - return this.transitions.get('*').get('*')(graphic, fromState); + func = this.transitions.get('*')?.get('*'); + if (func) { + return func(graphic, fromState); } // 默认允许转换 diff --git a/packages/vrender-animate/src/step.ts b/packages/vrender-animate/src/step.ts index 5d0a88d74..e6283acfa 100644 --- a/packages/vrender-animate/src/step.ts +++ b/packages/vrender-animate/src/step.ts @@ -178,8 +178,8 @@ export class Step implements IStep { // 屏蔽掉之前动画冲突的属性 const animate = this.animate; const target = this.target; - target.animates.forEach(a => { - if (a.id === animate.id) { + target.animates.forEach((a: any) => { + if (a === animate || a.priority > animate.priority) { return; } const fromProps = a.getStartProps(); diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index f07c1bbec..fea6890d8 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -943,6 +943,9 @@ export abstract class Graphic = Partial { btnContainer.style.flexDirection = 'row'; btnContainer.style.gap = '3px'; btnContainer.style.flexWrap = 'wrap'; - btnContainer.style.height = '90px'; + btnContainer.style.height = '120px'; const canvas = document.getElementById('main'); // 将btnContainer添加到canvas之前 canvas.parentNode.insertBefore(btnContainer, canvas); @@ -164,6 +165,21 @@ export const page = () => { // 中途设置值没问题,它会从orange开始 rect.setAttribute('fill', 'orange'); }); + addCase('Animate conflict', btnContainer, stage => { + const rect = createRect({ + x: 100, + y: 100, + width: 100, + height: 100, + fill: 'red' + }); + stage.defaultLayer.add(rect); + + rect.animate().to({ x: 600, y: 300 }, 6000, 'linear'); + setTimeout(() => { + rect.animate().to({ fill: 'orange' }, 1000, 'linear').to({ x: 0 }, 2000, 'linear'); + }, 1000); + }); addCase('Animate chain loop', btnContainer, stage => { const rect = createRect({ x: 100, @@ -352,7 +368,7 @@ export const page = () => { } }, duration: 1000, - easing: 'linear' + easing: 'elasticOut' }); // Add title @@ -367,6 +383,48 @@ export const page = () => { stage.defaultLayer.add(group); stage.defaultLayer.add(text); }); + addCase('AnimateExecutor Item', btnContainer, stage => { + // Add explanatory text + + for (let i = 0; i < 6; i++) { + const rect = createRect({ + x: i * 100, + y: 100, + width: 80, + height: 100, + fill: 'black' + }); + const executor = new AnimateExecutor(rect); + + // Basic animation - all elements fade and change color simultaneously + executor.execute({ + type: 'to', + channel: { + fill: { + to: 'red' + }, + opacity: { + to: 0.5 + } + }, + duration: 1000, + easing: 'elasticOut' + }); + stage.defaultLayer.add(rect); + } + + // Add title + const text = createText({ + x: 300, + y: 50, + text: 'Basic AnimateExecutor - Simultaneous Animation', + fontSize: 16, + fill: 'black', + textAlign: 'center' + }); + + stage.defaultLayer.add(text); + }); addCase('AnimateExecutor oneByOne', btnContainer, stage => { // Create a group with multiple rects @@ -603,6 +661,73 @@ export const page = () => { const executor = new AnimateExecutor(group); + // Apply animation only to elements where row + col is even + executor.execute({ + timeSlices: [ + { + effects: { + type: 'to', + channel: { + fill: { + to: 'blue' + }, + width: { + to: 90 + }, + height: { + to: 90 + } + }, + easing: 'elasticOut' + }, + duration: 1000 + } + ], + // Partitioner function to filter elements + partitioner: (datum: any, graphic: IGraphic, params: any) => { + return datum && datum.length && datum[0].even === true; + }, + oneByOne: 50 + }); + + // Add title + const text = createText({ + x: 400, + y: 50, + text: 'AnimateExecutor with Partitioner - Filtered Animation', + fontSize: 16, + fill: 'black', + textAlign: 'center' + }); + stage.defaultLayer.add(group); + stage.defaultLayer.add(text); + }); + addCase('AnimateExecutor lifecycle', btnContainer, stage => { + // Create a group with a grid of rectangles + const group = createGroup({ + x: 100, + y: 150 + }); + + // Create a 6x4 grid of rectangles + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 6; col++) { + const rect = createRect({ + x: col * 100, + y: row * 100, + width: 80, + height: 80, + fill: 'gray' + }); + rect.context = { + data: [{ row, col, even: (row + col) % 2 === 0 }] + }; + group.add(rect); + } + } + + const executor = new AnimateExecutor(group); + // Apply animation only to elements where row + col is even executor.execute({ timeSlices: { @@ -623,6 +748,7 @@ export const page = () => { }, duration: 1000 }, + loop: 2, // Partitioner function to filter elements partitioner: (datum: any, graphic: IGraphic, params: any) => { return datum && datum.length && datum[0].even === true; @@ -634,11 +760,19 @@ export const page = () => { const text = createText({ x: 400, y: 50, - text: 'AnimateExecutor with Partitioner - Filtered Animation', + text: 'AnimateExecutor with lifecycle', fontSize: 16, fill: 'black', textAlign: 'center' }); + + executor.onStart(() => { + console.log('onStart'); + }); + executor.onEnd(() => { + console.log('onEnd'); + alert('完成'); + }); stage.defaultLayer.add(group); stage.defaultLayer.add(text); }); @@ -681,7 +815,7 @@ export const page = () => { path: 'M50,250 C150,150 250,350 350,250 S550,150 650,250', stroke: 'black', lineWidth: 5, - fill: 'transparent' + fill: 'orange' // strokeDasharray and strokeDashoffset will be set by the animation }); group.add(pathElement); @@ -704,9 +838,7 @@ export const page = () => { executor.execute({ type: 'to', custom: rainbowColorInterpolator, - channel: { - fill: { to: 'transparent' } // Placeholder, actual color will be set by interpolator - }, + channel: {}, oneByOne: 50, duration: 2000, loop: Infinity @@ -817,4 +949,87 @@ export const page = () => { stage.defaultLayer.add(group); stage.defaultLayer.add(title); }); + + addCase('AnimateExecutor conflict', btnContainer, stage => { + // Create a group to hold all elements + const group = createGroup({ + x: 100, + y: 100 + }); + + // Add a title + const title = createText({ + x: 400, + y: 30, + text: 'AnimateExecutor with Custom Animation Classes', + fontSize: 18, + fill: 'black', + textAlign: 'center' + }); + + // Create text element for typewriter effect + const typewriterText = createText({ + x: 350, + y: 100, + text: '', + fontSize: 20, + fill: 'blue', + textAlign: 'center' + }); + group.add(typewriterText); + + // Create a row of circles for wave animation + const circles = []; + for (let i = 0; i < 12; i++) { + const circle = createCircle({ + x: 50 + i * 60, + y: 200, + radius: 15, + fill: 'purple', + opacity: 0.6 + }); + circles.push(circle); + group.add(circle); + } + + // Create executor + const executor = new AnimateExecutor(group); + + // Apply typewriter animation + executor.executeItem( + { + type: 'to', + custom: InputText, + channel: { + text: { to: 'This is a custom typewriter animation effect!' } + }, + duration: 3000, + loop: true + }, + typewriterText + ); + + // Apply wave animation to circles + // circles.forEach((circle, index) => { + // executor.executeItem( + // { + // type: 'to', + // custom: WaveAnimate, + // customParameters: { + // amplitude: 30, + // frequency: 2 + index * 0.2 // Different frequency for each circle + // }, + // channel: { + // y: { to: 200 } // This will be replaced by the wave animation + // }, + // duration: 3000, + // loop: true + // }, + // circle + // ); + // }); + + stage.defaultLayer.add(group); + stage.defaultLayer.add(title); + }); }; diff --git a/packages/vrender/__tests__/browser/src/pages/animate-state.ts b/packages/vrender/__tests__/browser/src/pages/animate-state.ts new file mode 100644 index 000000000..7699545da --- /dev/null +++ b/packages/vrender/__tests__/browser/src/pages/animate-state.ts @@ -0,0 +1,981 @@ +import { + DefaultTicker, + DefaultTimeline, + Animate, + registerAnimate, + IncreaseCount, + InputText, + AnimateExecutor, + ACustomAnimate +} from '@visactor/vrender-animate'; +import { + container, + createRect, + createStage, + createSymbol, + IGraphic, + vglobal, + createCircle, + createText, + createGroup, + createLine, + createPath +} from '@visactor/vrender'; +import type { EasingType } from '@visactor/vrender-animate'; +// container.load(roughModule); + +vglobal.setEnv('browser'); + +registerAnimate(); + +let stage: any; + +function addCase(name: string, container: HTMLElement, cb: (stage: any) => void) { + const button = document.createElement('button'); + button.innerText = name; + button.style.height = '26px'; + container.appendChild(button); + button.addEventListener('click', () => { + stage && stage.release(); + stage = createStage({ + canvas: 'main', + width: 900, + height: 600, + background: 'pink', + disableDirtyBounds: false, + canvasControled: false, + autoRender: true + }); + cb(stage); + }); +} + +export const page = () => { + const btnContainer = document.createElement('div'); + btnContainer.style.width = '1000px'; + btnContainer.style.background = '#cecece'; + btnContainer.style.display = 'flex'; + btnContainer.style.flexDirection = 'row'; + btnContainer.style.gap = '3px'; + btnContainer.style.flexWrap = 'wrap'; + btnContainer.style.height = '120px'; + const canvas = document.getElementById('main'); + // 将btnContainer添加到canvas之前 + canvas.parentNode.insertBefore(btnContainer, canvas); + + // Basic animation state registration and application + addCase('Basic Animation States', btnContainer, stage => { + // Create a title + const title = createText({ + x: 450, + y: 50, + text: 'Basic Animation States', + fontSize: 20, + fontWeight: 'bold', + fill: 'black', + textAlign: 'center' + }); + + const instructions = createText({ + x: 450, + y: 80, + text: 'Click the buttons below to apply different animation states', + fontSize: 14, + fill: 'black', + textAlign: 'center' + }); + + // Create a rectangle to animate + const rect = createRect({ + x: 300, + y: 200, + width: 100, + height: 100, + fill: 'blue' + }); + rect.context = { id: 'rect1' }; + + console.log(rect); + + // Register animation states + rect.registerAnimationState({ + name: 'pulse', + animation: { + timeSlices: [ + { + duration: 500, + effects: { + type: 'to', + channel: { + scaleX: { to: 1.2 }, + scaleY: { to: 1.2 } + }, + easing: 'linear' + } + }, + { + duration: 500, + effects: { + type: 'to', + channel: { + scaleX: { to: 1 }, + scaleY: { to: 1 } + }, + easing: 'linear' + } + } + ], + loop: true + } + }); + + rect.registerAnimationState({ + name: 'spin', + animation: { + type: 'to', + channel: { + angle: { to: 360 } + }, + duration: 2000, + easing: 'linear', + loop: true + } + }); + + rect.registerAnimationState({ + name: 'highlight', + animation: { + type: 'to', + channel: { + fill: { to: 'orange' }, + strokeWidth: { to: 3 }, + stroke: { to: 'red' } + }, + duration: 300, + easing: 'sineOut' + } + }); + + // Create control buttons + const createControlButton = (x: number, y: number, label: string, action: () => void) => { + const buttonGroup = createGroup({ + x, + y + }); + + const buttonBg = createRect({ + x: 0, + y: 0, + width: 120, + height: 30, + fill: 'lightgray', + stroke: 'gray', + lineWidth: 1, + cornerRadius: 5 + }); + + const buttonText = createText({ + x: 60, + y: 15, + text: label, + fontSize: 14, + fill: 'black', + textAlign: 'center', + textBaseline: 'middle' + }); + + buttonGroup.add(buttonBg); + buttonGroup.add(buttonText); + + // Add click handler + buttonGroup.addEventListener('click', action); + + return buttonGroup; + }; + + // Add control buttons + const buttonsGroup = createGroup({ + x: 0, + y: 350 + }); + + buttonsGroup.add( + createControlButton(200, 0, 'Pulse', () => { + rect.applyAnimationState( + ['pulse'], + [ + { + name: 'pulse', + animation: { + timeSlices: [ + { + duration: 500, + effects: { + type: 'to', + channel: { + scaleX: { to: 1.2 }, + scaleY: { to: 1.2 } + }, + easing: 'linear' + } + }, + { + duration: 500, + effects: { + type: 'to', + channel: { + scaleX: { to: 1 }, + scaleY: { to: 1 } + }, + easing: 'linear' + } + } + ], + loop: true + } + } + ] + ); + }) + ); + + buttonsGroup.add( + createControlButton(340, 0, 'Spin', () => { + rect.applyAnimationState( + ['spin'], + [ + { + name: 'spin', + animation: { + type: 'to', + channel: { + angle: { to: 360 } + }, + duration: 2000, + easing: 'linear', + loop: true + } + } + ] + ); + }) + ); + + buttonsGroup.add( + createControlButton(480, 0, 'Highlight', () => { + rect.applyAnimationState( + ['highlight'], + [ + { + name: 'highlight', + animation: { + type: 'to', + channel: { + fill: { to: 'orange' }, + strokeWidth: { to: 3 }, + stroke: { to: 'red' } + }, + duration: 300, + easing: 'sineOut' + } + } + ] + ); + }) + ); + + buttonsGroup.add( + createControlButton(620, 0, 'Clear', () => { + rect.clearAnimationStates(); + }) + ); + + // Add to stage + stage.defaultLayer.add(title); + stage.defaultLayer.add(instructions); + stage.defaultLayer.add(rect); + stage.defaultLayer.add(buttonsGroup); + }); + + // Animation state transitions + addCase('Animation State Transitions', btnContainer, stage => { + // Create a title + const title = createText({ + x: 450, + y: 50, + text: 'Animation State Transitions', + fontSize: 20, + fontWeight: 'bold', + fill: 'black', + textAlign: 'center' + }); + + const instructions = createText({ + x: 450, + y: 80, + text: 'Apply states in sequence to see transition behavior', + fontSize: 14, + fill: 'black', + textAlign: 'center' + }); + + // Create a circle to animate + const circle = createCircle({ + x: 450, + y: 250, + radius: 50, + fill: 'green' + }); + circle.context = { id: 'circle1' }; + + // Create control buttons + const createControlButton = (x: number, y: number, label: string, action: () => void) => { + const buttonGroup = createGroup({ + x, + y + }); + + const buttonBg = createRect({ + x: 0, + y: 0, + width: 120, + height: 30, + fill: 'lightgray', + stroke: 'gray', + lineWidth: 1, + cornerRadius: 5 + }); + + const buttonText = createText({ + x: 60, + y: 15, + text: label, + fontSize: 14, + fill: 'black', + textAlign: 'center', + textBaseline: 'middle' + }); + + buttonGroup.add(buttonBg); + buttonGroup.add(buttonText); + + // Add click handler + buttonGroup.addEventListener('click', action); + + return buttonGroup; + }; + + // Add transition explanations + const statusText = createText({ + x: 450, + y: 350, + text: 'Current state: None', + fontSize: 16, + fill: 'black', + textAlign: 'center' + }); + + const ruleExplanation = createText({ + x: 450, + y: 380, + text: 'Try different state sequences to see transition rules in action', + fontSize: 14, + fill: 'darkgray', + textAlign: 'center' + }); + + // Add control buttons + const buttonsGroup = createGroup({ + x: 0, + y: 430 + }); + + // First reset circle to simplify state + circle.opacity = 0; + circle.scaleX = 0; + circle.scaleY = 0; + + // Helper to update status text + const updateStatus = (state: string) => { + statusText.setAttribute('text', `Current state: ${state}`); + }; + console.log(circle); + + buttonsGroup.add( + createControlButton(200, 0, 'Appear', () => { + circle.applyAnimationState( + ['appear'], + [ + { + name: 'appear', + animation: { + type: 'to', + channel: { + opacity: { from: 0, to: 1 }, + scaleX: { from: 0, to: 1 }, + scaleY: { from: 0, to: 1 }, + fill: { from: 'green', to: 'blue' } + }, + duration: 3000, + easing: 'linear' + } + } + ] + ); + updateStatus('appear (enter)'); + ruleExplanation.attribute.text = 'Enter animations can be interrupted by any animation'; + }) + ); + + buttonsGroup.add( + createControlButton(340, 0, 'normal', () => { + circle.applyAnimationState( + ['normal'], + [ + { + name: 'normal', + animation: { + timeSlices: [ + { + duration: 600, + effects: { + type: 'to', + channel: { + y: { to: 200 }, + fill: { to: 'red' } + }, + easing: 'linear' + } + }, + { + duration: 600, + effects: { + type: 'to', + channel: { + y: { to: 300 } + }, + easing: 'linear' + } + } + ], + priority: 1, + loop: true, + bounce: true + } + } + ] + ); + updateStatus('normal'); + ruleExplanation.attribute.text = 'normal animations can be overridden by other animations'; + }) + ); + + buttonsGroup.add( + createControlButton(480, 0, 'Disappear', () => { + circle.applyAnimationState( + ['disappear'], + [ + { + name: 'disappear', + animation: { + type: 'to', + channel: { + opacity: { from: 1, to: 0 } + }, + duration: 800, + easing: 'sineIn' + } + } + ] + ); + updateStatus('disappear (exit)'); + ruleExplanation.attribute.text = 'Exit animations cannot be interrupted except by enter animations'; + }) + ); + + buttonsGroup.add( + createControlButton(620, 0, 'Clear', () => { + circle.clearAnimationStates(); + updateStatus('None'); + ruleExplanation.attribute.text = 'Try different state sequences to see transition rules in action'; + }) + ); + + // for (let i = 0; i < 3; i++) { + // const rect = createRect({ + // x: i * 100, + // y: 100, + // width: 80, + // height: 100, + // fill: 'black' + // }); + // setTimeout(() => { + // const executor = new AnimateExecutor(rect); + + // // Basic animation - all elements fade and change color simultaneously + // executor.execute({ + // type: 'to', + // channel: { + // fill: { + // to: 'red' + // }, + // opacity: { + // to: 0.5 + // } + // }, + // duration: 1000, + // easing: 'elasticOut' + // }); + // }, 2000); + // stage.defaultLayer.add(rect); + // } + + // Add to stage + stage.defaultLayer.add(title); + stage.defaultLayer.add(instructions); + stage.defaultLayer.add(circle); + stage.defaultLayer.add(statusText); + stage.defaultLayer.add(ruleExplanation); + stage.defaultLayer.add(buttonsGroup); + }); + + // Multiple state sequences + addCase('State Sequences', btnContainer, stage => { + // Create a title + const title = createText({ + x: 450, + y: 50, + text: 'Animation State Sequences', + fontSize: 20, + fontWeight: 'bold', + fill: 'black', + textAlign: 'center' + }); + + const instructions = createText({ + x: 450, + y: 80, + text: 'Apply state sequences to see chained animations', + fontSize: 14, + fill: 'black', + textAlign: 'center' + }); + + // Create a path to animate + const path = createPath({ + path: 'M250,200 C300,100 400,100 450,200 S600,300 650,200', + stroke: 'blue', + lineWidth: 3, + fill: 'none' + }); + path.context = { id: 'path1' }; + + // Register animation states + path.registerAnimationState({ + name: 'draw', + animation: { + type: 'to', + channel: { + lineDash: { from: [1000, 1000], to: [0, 0] }, + lineDashOffset: { from: 1000, to: 0 } + }, + duration: 2000, + easing: 'linear' + } + }); + + path.registerAnimationState({ + name: 'fill', + animation: { + type: 'to', + channel: { + fill: { from: 'none', to: 'rgba(0, 0, 255, 0.2)' } + }, + duration: 1000, + easing: 'sineOut' + } + }); + + path.registerAnimationState({ + name: 'pulse', + animation: { + type: 'to', + channel: { + lineWidth: { from: 3, to: 6 } + }, + duration: 500, + easing: 'linear' + } + }); + + path.registerAnimationState({ + name: 'reset', + animation: { + type: 'to', + channel: { + lineDash: { to: [1000, 1000] }, + lineDashOffset: { to: 1000 }, + fill: { to: 'none' }, + lineWidth: { to: 3 } + }, + duration: 1000, + easing: 'sineIn' + } + }); + + // Create control buttons + const createControlButton = (x: number, y: number, label: string, action: () => void) => { + const buttonGroup = createGroup({ + x, + y + }); + + const buttonBg = createRect({ + x: 0, + y: 0, + width: 160, + height: 30, + fill: 'lightgray', + stroke: 'gray', + lineWidth: 1, + cornerRadius: 5 + }); + + const buttonText = createText({ + x: 80, + y: 15, + text: label, + fontSize: 14, + fill: 'black', + textAlign: 'center', + textBaseline: 'middle' + }); + + buttonGroup.add(buttonBg); + buttonGroup.add(buttonText); + + // Add click handler + buttonGroup.addEventListener('click', action); + + return buttonGroup; + }; + + // Add control buttons + const buttonsGroup = createGroup({ + x: 0, + y: 350 + }); + + buttonsGroup.add( + createControlButton(200, 0, 'Draw → Fill → Pulse', () => { + debugger; + path.applyAnimationState( + ['draw', 'fill', 'pulse'], + [ + { + name: 'draw', + animation: { + type: 'to', + channel: { + stroke: { from: 'none', to: 'orange' } + }, + duration: 2000, + easing: 'linear' + } + }, + { + name: 'fill', + animation: { + type: 'to', + channel: { + fill: { from: 'none', to: 'rgba(0, 0, 255, 0.2)' } + }, + duration: 1000, + easing: 'sineOut' + } + }, + { + name: 'pulse', + animation: { + type: 'to', + channel: { + lineWidth: { from: 3, to: 6 } + }, + duration: 500, + easing: 'linear' + } + } + ] + ); + }) + ); + + buttonsGroup.add( + createControlButton(380, 0, 'Reset', () => { + path.applyAnimationState( + ['reset'], + [ + { + name: 'reset', + animation: { + type: 'to', + channel: { + lineDash: { to: [1000, 1000] }, + lineDashOffset: { to: 1000 }, + fill: { to: 'none' }, + lineWidth: { to: 3 } + }, + duration: 1000, + easing: 'sineIn' + } + } + ] + ); + }) + ); + + buttonsGroup.add( + createControlButton(560, 0, 'Clear', () => { + path.clearAnimationStates(); + }) + ); + + // Add to stage + stage.defaultLayer.add(title); + stage.defaultLayer.add(instructions); + stage.defaultLayer.add(path); + stage.defaultLayer.add(buttonsGroup); + }); + + // Group animation states + addCase('Group Animation States', btnContainer, stage => { + // Create a title + const title = createText({ + x: 450, + y: 50, + text: 'Group Animation States', + fontSize: 20, + fontWeight: 'bold', + fill: 'black', + textAlign: 'center' + }); + + const instructions = createText({ + x: 450, + y: 80, + text: 'Apply states to a group and see the effects', + fontSize: 14, + fill: 'black', + textAlign: 'center' + }); + + // Create a group with multiple elements + const group = createGroup({ + x: 300, + y: 200 + }); + group.context = { id: 'animationGroup' }; + + const rect1 = createRect({ + x: 0, + y: 0, + width: 60, + height: 60, + fill: 'red' + }); + rect1.context = { id: 'rect1' }; + + const rect2 = createRect({ + x: 100, + y: 0, + width: 60, + height: 60, + fill: 'green' + }); + rect2.context = { id: 'rect2' }; + + const rect3 = createRect({ + x: 200, + y: 0, + width: 60, + height: 60, + fill: 'blue' + }); + rect3.context = { id: 'rect3' }; + + group.add(rect1); + group.add(rect2); + group.add(rect3); + + // Register animation states + group.registerAnimationState({ + name: 'appear', + animation: { + type: 'to', + channel: { + opacity: { from: 0, to: 1 }, + scaleX: { from: 0.5, to: 1 }, + scaleY: { from: 0.5, to: 1 } + }, + duration: 1000, + easing: 'elasticOut' + } + }); + + group.registerAnimationState({ + name: 'shuffle', + animation: { + type: 'to', + channel: { + x: { to: 200 } + }, + duration: 1000, + easing: 'linear' + } + }); + + group.registerAnimationState({ + name: 'disappear', + animation: { + type: 'to', + channel: { + opacity: { to: 0 }, + y: { to: 250 } + }, + duration: 800, + easing: 'sineIn' + } + }); + + // Create control buttons + const createControlButton = (x: number, y: number, label: string, action: () => void) => { + const buttonGroup = createGroup({ + x, + y + }); + + const buttonBg = createRect({ + x: 0, + y: 0, + width: 120, + height: 30, + fill: 'lightgray', + stroke: 'gray', + lineWidth: 1, + cornerRadius: 5 + }); + + const buttonText = createText({ + x: 60, + y: 15, + text: label, + fontSize: 14, + fill: 'black', + textAlign: 'center', + textBaseline: 'middle' + }); + + buttonGroup.add(buttonBg); + buttonGroup.add(buttonText); + + // Add click handler + buttonGroup.addEventListener('click', action); + + return buttonGroup; + }; + + // Initialize group to be invisible + group.opacity = 0; + group.scaleX = 0.5; + group.scaleY = 0.5; + + // Add control buttons + const buttonsGroup = createGroup({ + x: 0, + y: 350 + }); + + buttonsGroup.add( + createControlButton(200, 0, 'Appear', () => { + group.applyAnimationState( + ['appear'], + [ + { + name: 'appear', + animation: { + type: 'to', + channel: { + opacity: { from: 0, to: 1 }, + scaleX: { from: 0.5, to: 1 }, + scaleY: { from: 0.5, to: 1 } + }, + duration: 1000, + easing: 'elasticOut' + } + } + ] + ); + }) + ); + + buttonsGroup.add( + createControlButton(340, 0, 'Shuffle', () => { + group.applyAnimationState( + ['shuffle'], + [ + { + name: 'shuffle', + animation: { + type: 'to', + channel: { + x: { to: group.attribute.x === 300 ? 100 : 300 } + }, + duration: 1000, + easing: 'linear' + } + } + ] + ); + }) + ); + + buttonsGroup.add( + createControlButton(480, 0, 'Disappear', () => { + group.applyAnimationState( + ['disappear'], + [ + { + name: 'disappear', + animation: { + type: 'to', + channel: { + opacity: { to: 0 }, + y: { to: 250 } + }, + duration: 800, + easing: 'sineIn' + } + } + ] + ); + }) + ); + + buttonsGroup.add( + createControlButton(620, 0, 'Reset', () => { + group.clearAnimationStates(); + group.opacity = 0; + group.scaleX = 0.5; + group.scaleY = 0.5; + group.x = 300; + group.y = 200; + }) + ); + + // Add to stage + stage.defaultLayer.add(title); + stage.defaultLayer.add(instructions); + stage.defaultLayer.add(group); + stage.defaultLayer.add(buttonsGroup); + }); +}; diff --git a/packages/vrender/__tests__/browser/src/pages/index.ts b/packages/vrender/__tests__/browser/src/pages/index.ts index f3d155024..2a6566bef 100644 --- a/packages/vrender/__tests__/browser/src/pages/index.ts +++ b/packages/vrender/__tests__/browser/src/pages/index.ts @@ -23,6 +23,10 @@ export const pages = [ name: 'animate-next', path: 'animate-next' }, + { + name: 'animate-state', + path: 'animate-state' + }, { name: '内存', path: 'memory' diff --git a/packages/vrender/__tests__/browser/src/pages/performance.ts b/packages/vrender/__tests__/browser/src/pages/performance.ts index 0624430d1..daea0aab2 100644 --- a/packages/vrender/__tests__/browser/src/pages/performance.ts +++ b/packages/vrender/__tests__/browser/src/pages/performance.ts @@ -38,152 +38,74 @@ export const page = () => { } console.timeEnd('setFont'); }); - addTest('array map set', () => { - const list = new Array(100000).fill(0).map(item => createArc({})); - - let array = []; - let map = new Map(); - let set = new Set(); - function createArray() { - list.forEach(item => { - array.push(item); - }); - } - - function createMap() { - list.forEach(item => { - map.set(item._uid, item); - }); - } - - function createSet() { - list.forEach(item => { - set.add(item); - }); - } - - function forEachArray() { - let id = 0; - array.forEach(item => (id += item._uid)); - return id; - } - - function forEachMap() { - let id = 0; - map.forEach(item => (id += item._uid)); - return id; - } - - function forEachSet() { - let id = 0; - set.forEach(item => (id += item._uid)); - return id; - } - - function deleteArray() { - list.forEach(item => { - array.push(item); - }); - } - - function delteMap() { - list.forEach(item => { - map.delete(item._uid); - }); - } - - function deletSet() { - list.forEach(item => { - set.delete(item); - }); - } - - console.time('array'); - createArray(); - console.timeEnd('array'); - - console.time('map'); - createMap(); - console.timeEnd('map'); - - console.time('set'); - createSet(); - console.timeEnd('set'); - - console.time('array foreach'); - const arrayCount = forEachArray(); - console.timeEnd('array foreach'); - - console.time('map foreach'); - const mapCount = forEachMap(); - console.timeEnd('map foreach'); - - console.time('set foreach'); - const setCount = forEachSet(); - console.timeEnd('set foreach'); - - console.time('map delete'); - delteMap(); - console.timeEnd('map delete'); - - console.time('set delete'); - deletSet(); - console.timeEnd('set delete'); - - console.log(list, map, set, arrayCount, mapCount, setCount); - }); - addTest('raf calls', () => { - const createRun = () => { - let i = 0; - function run() { - requestAnimationFrame(run); - i++; - } - run(); - }; - - for (let i = 0; i < 600; i++) { - createRun(); - } - }); - addTest('mock raf', () => { - class PerformanceRAF { - nextAnimationFrameCbs: FrameRequestCallback[] = []; - - addAnimationFrameCb(callback: FrameRequestCallback) { - this.nextAnimationFrameCbs.push(callback); - // 下一帧执行nextAnimationFrameCbs - this.tryRunAnimationFrameNextFrame(); - return this.nextAnimationFrameCbs.length - 1; + addTest('state map', () => { + class AAA { + transitions: Map> = new Map(); + constructor(public from: string, public to: string) {} + /** + * 注册默认的转换规则 + */ + registerDefaultTransitions(): void { + // 设置默认的转换规则 + // 退出动画不能被中断,除非是进入动画 + this.registerTransition('exit', 'enter', () => ({ + allowTransition: true, + stopOriginalTransition: true + })); + this.registerTransition('exit', '*', () => ({ + allowTransition: false, + stopOriginalTransition: false + })); + + // 进入动画可以被任何动画中断 + this.registerTransition('enter', '*', () => ({ + allowTransition: true, + stopOriginalTransition: true + })); + + // Disappear 是一个退出动画,遵循相同的规则 + this.registerTransition('disappear', 'enter', () => ({ + allowTransition: true, + stopOriginalTransition: true + })); + this.registerTransition('disappear', 'appear', () => ({ + allowTransition: true, + stopOriginalTransition: true + })); + this.registerTransition('disappear', '*', () => ({ + allowTransition: false, + stopOriginalTransition: false + })); + + // Appear 是一个进入动画,可以被任何动画中断 + this.registerTransition('appear', '*', () => ({ + allowTransition: true, + stopOriginalTransition: true + })); } - protected runAnimationFrame = (time: number) => { - const cbs = this.nextAnimationFrameCbs; - this.nextAnimationFrameCbs = []; - for (let i = 0; i < cbs.length; i++) { - cbs[i](time); + registerTransition(fromState: string, toState: string, transition: any): void { + if (!this.transitions.has(fromState)) { + this.transitions.set(fromState, new Map()); } - }; - protected tryRunAnimationFrameNextFrame = () => { - if (!(this.nextAnimationFrameCbs && this.nextAnimationFrameCbs.length === 1)) { - return; - } - requestAnimationFrame(this.runAnimationFrame); - }; - } - const performanceRAF = new PerformanceRAF(); - const createRun = () => { - let i = 0; - function run() { - performanceRAF.addAnimationFrameCb(run); - i++; + this.transitions.get(fromState)!.set(toState, transition); } - run(); - }; + } - for (let i = 0; i < 600; i++) { - createRun(); + const aaa = new AAA('from', 'to'); + aaa.registerDefaultTransitions(); + aaa.registerTransition('from', 'to', () => ({ + allowTransition: true, + stopOriginalTransition: true + })); + + const fromList = new Array(10000).fill(0).map((_, i) => ['from', 'to', 'appear', 'disappear'][i % 4]); + const toList = new Array(10000).fill(0).map((_, i) => ['from', 'to', 'appear', 'disappear'][i % 4]); + console.time('transitions'); + for (let i = 0; i < 10000; i++) { + aaa.transitions.get(fromList[i])?.get(toList[i])?.transition(); } + console.timeEnd('transitions'); }); }; From fce33f92d9f5de78ee38d164983d811e69a30dee Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Mon, 24 Mar 2025 11:48:58 +0800 Subject: [PATCH 043/179] feat: support base function for vrender-animate --- .../vrender-animate/src/animate-extension.ts | 73 +++++ packages/vrender-animate/src/animate.ts | 19 +- packages/vrender-animate/src/custom/clip.ts | 24 ++ packages/vrender-animate/src/custom/common.ts | 86 ++++++ .../src/custom/custom-animate.ts | 10 +- packages/vrender-animate/src/custom/fade.ts | 24 ++ .../vrender-animate/src/custom/growCenter.ts | 256 ++++++++++++++++++ .../vrender-animate/src/custom/growHeight.ts | 219 +++++++++++++++ .../vrender-animate/src/custom/growWidth.ts | 217 +++++++++++++++ packages/vrender-animate/src/custom/number.ts | 2 +- .../vrender-animate/src/custom/register.ts | 27 ++ packages/vrender-animate/src/custom/scale.ts | 105 +++++++ packages/vrender-animate/src/custom/state.ts | 45 +++ packages/vrender-animate/src/custom/update.ts | 48 ++++ .../src/executor/animate-executor.ts | 252 ++++++++++------- .../vrender-animate/src/executor/executor.ts | 2 + packages/vrender-animate/src/index.ts | 6 +- .../vrender-animate/src/interpolate/store.ts | 5 + .../vrender-animate/src/intreface/animate.ts | 2 + packages/vrender-animate/src/register.ts | 7 +- .../src/state/animation-state.ts | 41 ++- .../src/state/animation-states-registry.ts | 1 + .../src/state/graphic-extension.ts | 12 +- packages/vrender-animate/src/step.ts | 5 +- packages/vrender-core/src/graphic/graphic.ts | 69 ++--- packages/vrender-core/src/index.ts | 4 +- .../vrender-core/src/interface/graphic.ts | 4 +- .../builtin-plugin/richtext-edit-plugin.ts | 4 +- .../browser/src/pages/animate-next.ts | 80 +++++- .../browser/src/pages/animate-state.ts | 95 +++---- .../browser/src/pages/richtext-editor.ts | 26 +- 31 files changed, 1544 insertions(+), 226 deletions(-) create mode 100644 packages/vrender-animate/src/animate-extension.ts create mode 100644 packages/vrender-animate/src/custom/clip.ts create mode 100644 packages/vrender-animate/src/custom/common.ts create mode 100644 packages/vrender-animate/src/custom/fade.ts create mode 100644 packages/vrender-animate/src/custom/growCenter.ts create mode 100644 packages/vrender-animate/src/custom/growHeight.ts create mode 100644 packages/vrender-animate/src/custom/growWidth.ts create mode 100644 packages/vrender-animate/src/custom/register.ts create mode 100644 packages/vrender-animate/src/custom/scale.ts create mode 100644 packages/vrender-animate/src/custom/state.ts create mode 100644 packages/vrender-animate/src/custom/update.ts diff --git a/packages/vrender-animate/src/animate-extension.ts b/packages/vrender-animate/src/animate-extension.ts new file mode 100644 index 000000000..a2108c7db --- /dev/null +++ b/packages/vrender-animate/src/animate-extension.ts @@ -0,0 +1,73 @@ +// 1. 支持animate函数 +// 2. 支持animates map +// 2. 支持animatedAttribute 为所有动画的最终结果(loop为INFINITY的动画不算) +// 3. 支持finalAttribute 为所有动画的最终结果(animatedAttribute 合并了 graphic.attribute之后的最终结果) +// 3. 重载Graphic的getAttributes方法,根据参数getAttributes(final = true)返回finalAttribute = {}; merge(finalAttribute, graphic.attribute, animatedAttribute), +// animatedAttribute为所有动画的最终结果(loop为INFINITY的动画不算) + +import type { IGraphicAnimateParams } from '@visactor/vrender-core'; +import type { IAnimate } from './intreface/animate'; +import { Animate } from './animate'; +import { defaultTimeline } from './timeline'; + +// 基于性能考虑,每次调用animate函数,都会设置animatedAttribute为null,每次getAttributes(true)会根据animatedAttribute属性判断是否需要重新计算animatedAttribute。 +export class AnimateExtension { + animatedAttribute: Record | null = null; + + declare animates: Map; + + getAttributes(final: boolean = false) { + if (final) { + this.computeAnimatedAttribute(); + return this.getFinalAttribute(); + } + return (this as any).attribute; + } + + animate(params?: IGraphicAnimateParams) { + if (!this.animates) { + this.animates = new Map(); + } + const animate = new Animate( + params?.id, + params?.timeline ?? ((this as any).stage && (this as any).stage.getTimeline()) ?? defaultTimeline, + params?.slience + ); + + animate.bind(this as any); + if (params) { + const { onStart, onEnd, onRemove } = params; + onStart != null && animate.onStart(onStart); + onEnd != null && animate.onEnd(onEnd); + onRemove != null && animate.onRemove(onRemove); + } + this.animates.set(animate.id, animate); + animate.onRemove(() => { + animate.stop(); + this.animates.delete(animate.id); + }); + + // TODO 考虑性能问题 + (this as any).stage?.ticker.start(); + + return animate; + } + + protected computeAnimatedAttribute() { + if (!this.animatedAttribute) { + this.animatedAttribute = {}; + + this.animates.forEach(animate => { + if (animate.getLoop() !== Infinity) { + Object.assign(this.animatedAttribute, animate.getEndProps()); + } + }); + } + } + + protected getFinalAttribute() { + const finalAttribute = {}; + Object.assign(finalAttribute, (this as any).attribute, this.animatedAttribute); + return finalAttribute; + } +} diff --git a/packages/vrender-animate/src/animate.ts b/packages/vrender-animate/src/animate.ts index c206671b2..4f73529d1 100644 --- a/packages/vrender-animate/src/animate.ts +++ b/packages/vrender-animate/src/animate.ts @@ -114,6 +114,10 @@ export class Animate implements IAnimate { */ bind(target: IGraphic): this { this.target = target; + // 添加一个animationAttribute属性,用于存储动画过程中的属性 + if (!this.target.animationAttribute) { + this.target.animationAttribute = {}; + } return this; } @@ -419,9 +423,10 @@ export class Animate implements IAnimate { * 停止动画 */ stop(type?: 'start' | 'end' | Record): void { - if (this.status !== AnimateStatus.RUNNING) { - return; - } + // TODO 有些动画可能一添加就被删除 + // if (this.status === AnimateStatus.END) { + // return; + // } this.status = AnimateStatus.END; @@ -557,6 +562,10 @@ export class Animate implements IAnimate { * 推进动画 */ advance(delta: number): void { + if (this.status === AnimateStatus.END) { + console.warn('aaa 动画已经结束,不能推进'); + return; + } const nextTime = this.currentTime + delta; // 如果还没开始,直接return if (nextTime < this._startTime) { @@ -665,4 +674,8 @@ export class Animate implements IAnimate { getTotalDuration(): number { return this._totalDuration; } + + getLoop(): number { + return this._loopCount; + } } diff --git a/packages/vrender-animate/src/custom/clip.ts b/packages/vrender-animate/src/custom/clip.ts new file mode 100644 index 000000000..f3a25bc00 --- /dev/null +++ b/packages/vrender-animate/src/custom/clip.ts @@ -0,0 +1,24 @@ +import type { EasingType } from '../intreface/easing'; +import { CommonIn, CommonOut } from './common'; + +export interface IScaleAnimationOptions { + direction?: 'x' | 'y' | 'xy'; +} + +export class ClipIn extends CommonIn { + declare valid: boolean; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: IScaleAnimationOptions) { + super(from, to, duration, easing, params); + this.keys = ['clipRange']; + } +} + +export class ClipOut extends CommonOut { + declare valid: boolean; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: IScaleAnimationOptions) { + super(from, to, duration, easing, params); + this.keys = ['clipRange']; + } +} diff --git a/packages/vrender-animate/src/custom/common.ts b/packages/vrender-animate/src/custom/common.ts new file mode 100644 index 000000000..4e44be4a4 --- /dev/null +++ b/packages/vrender-animate/src/custom/common.ts @@ -0,0 +1,86 @@ +import type { IAnimate, IStep } from '../intreface/animate'; +import type { EasingType } from '../intreface/easing'; +import { ACustomAnimate } from './custom-animate'; + +export interface IScaleAnimationOptions { + direction?: 'x' | 'y' | 'xy'; +} + +export class CommonIn extends ACustomAnimate> { + declare valid: boolean; + + keys: string[]; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: IScaleAnimationOptions) { + super(from, to, duration, easing, params); + } + + onBind(): void { + const attrs = this.target.getFinalAttribute(); + const fromAttrs = this.target.context.lastAttrs ?? {}; + + const to: Record = {}; + const from: Record = {}; + this.keys.forEach(key => { + to[key] = attrs?.[key] ?? 1; + from[key] = fromAttrs?.[key] ?? 0; + }); + + this.props = to; + this.propKeys = this.keys; + this.animate.reSyncProps(); + this.from = from; + this.to = to; + this.target.setAttributes(from as any); + } + + onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { + super.onEnd(cb); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + this.propKeys.forEach(key => { + out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.setAttributes(out); + } +} + +export class CommonOut extends ACustomAnimate> { + declare valid: boolean; + + keys: string[]; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: IScaleAnimationOptions) { + super(from, to, duration, easing, params); + } + + onBind(): void { + const attrs = this.target.getFinalAttribute(); + + const to: Record = {}; + const from: Record = {}; + this.keys.forEach(key => { + to[key] = 0; + from[key] = attrs?.[key] ?? 1; + }); + + this.props = to; + this.propKeys = this.keys; + this.animate.reSyncProps(); + this.from = from; + this.to = to; + this.target.setAttributes(from as any); + } + + onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { + super.onEnd(cb); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + this.propKeys.forEach(key => { + out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.setAttributes(out); + } +} diff --git a/packages/vrender-animate/src/custom/custom-animate.ts b/packages/vrender-animate/src/custom/custom-animate.ts index fbb7ba5a7..70c854fa4 100644 --- a/packages/vrender-animate/src/custom/custom-animate.ts +++ b/packages/vrender-animate/src/custom/custom-animate.ts @@ -8,6 +8,8 @@ export abstract class ACustomAnimate extends Step implements ICustomAnimate { declare customFrom: T; declare params?: any; declare props?: T; + declare from?: T; + declare to?: T; // 为了兼容旧的api,from和to是可选的,且尽量不需要From,因为为了避免突变,From都应该从当前位置开始 // 所以From并不会真正设置到fromProps中,而是作为customFrom参数 @@ -17,14 +19,6 @@ export abstract class ACustomAnimate extends Step implements ICustomAnimate { this.params = params; } - get from() { - return this.getFromProps(); - } - - get to() { - return this.getEndProps(); - } - update(end: boolean, ratio: number, out: Record): void { // TODO 需要修复,只有在开始的时候才调用 this.onStart(); diff --git a/packages/vrender-animate/src/custom/fade.ts b/packages/vrender-animate/src/custom/fade.ts new file mode 100644 index 000000000..83c237b72 --- /dev/null +++ b/packages/vrender-animate/src/custom/fade.ts @@ -0,0 +1,24 @@ +import type { EasingType } from '../intreface/easing'; +import { CommonIn, CommonOut } from './common'; + +export interface IScaleAnimationOptions { + direction?: 'x' | 'y' | 'xy'; +} + +export class FadeIn extends CommonIn { + declare valid: boolean; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: IScaleAnimationOptions) { + super(from, to, duration, easing, params); + this.keys = ['opacity']; + } +} + +export class FadeOut extends CommonOut { + declare valid: boolean; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: IScaleAnimationOptions) { + super(from, to, duration, easing, params); + this.keys = ['opacity']; + } +} diff --git a/packages/vrender-animate/src/custom/growCenter.ts b/packages/vrender-animate/src/custom/growCenter.ts new file mode 100644 index 000000000..35f0bf72e --- /dev/null +++ b/packages/vrender-animate/src/custom/growCenter.ts @@ -0,0 +1,256 @@ +import type { IGraphic, IGroup } from '@visactor/vrender-core'; +import { isNil, isNumber, isValid } from '@visactor/vutils'; +import type { IAnimate, IStep } from '../intreface/animate'; +import type { EasingType } from '../intreface/easing'; +import { ACustomAnimate } from './custom-animate'; + +interface IGrowCartesianAnimationOptions { + orient?: 'positive' | 'negative'; + overall?: boolean | number; + direction?: 'x' | 'y' | 'xy'; +} + +interface IAnimationParameters { + width: number; + height: number; + group: IGroup; + elementIndex: number; + elementCount: number; + view: any; +} + +type TypeAnimation = ( + graphic: T, + options: any, + animationParameters: IAnimationParameters +) => { from?: { [channel: string]: any }; to?: { [channel: string]: any } }; + +const growCenterIn: TypeAnimation = ( + graphic: IGraphic, + options: IGrowCartesianAnimationOptions, + animationParameters: IAnimationParameters +) => { + const attrs = graphic.getFinalAttribute(); + switch (options?.direction) { + case 'x': { + const x = attrs.x; + const x1 = attrs.x1; + const width = attrs.width; + + return { + from: isValid(width) + ? { + x: x + width / 2, + x1: undefined, + width: 0 + } + : { + x: (x + x1) / 2, + x1: (x + x1) / 2, + width: undefined + }, + to: { x, x1, width } + }; + } + case 'y': { + const y = attrs.y; + const y1 = attrs.y1; + const height = attrs.height; + + return { + from: isValid(height) + ? { + y: y + height / 2, + y1: undefined, + height: 0 + } + : { + y: (y + y1) / 2, + y1: (y + y1) / 2, + height: undefined + }, + to: { y, y1, height } + }; + } + case 'xy': + default: { + const x = attrs.x; + const x1 = attrs.x1; + const width = attrs.width; + const y = attrs.y; + const y1 = attrs.y1; + const height = attrs.height; + const from: any = {}; + + if (isValid(width)) { + from.x = x + width / 2; + from.width = 0; + from.x1 = undefined; + } else { + from.x = (x + x1) / 2; + from.x1 = (x + x1) / 2; + from.width = undefined; + } + + if (isValid(height)) { + from.y = y + height / 2; + from.height = 0; + from.y1 = undefined; + } else { + from.y = (y + y1) / 2; + from.y1 = (y + y1) / 2; + from.height = undefined; + } + + return { + from, + to: { x, y, x1, y1, width, height } + }; + } + } +}; + +const growCenterOut: TypeAnimation = ( + graphic: IGraphic, + options: IGrowCartesianAnimationOptions, + animationParameters: IAnimationParameters +) => { + const attrs = graphic.getFinalAttribute(); + switch (options?.direction) { + case 'x': { + const x = attrs.x; + const x1 = attrs.x1; + const width = attrs.width; + + return { + to: isValid(width) + ? { + x: x + width / 2, + x1: undefined, + width: 0 + } + : { + x: (x + x1) / 2, + x1: (x + x1) / 2, + width: undefined + } + }; + } + case 'y': { + const y = attrs.y; + const y1 = attrs.y1; + const height = attrs.height; + + return { + to: isValid(height) + ? { + y: y + height / 2, + y1: undefined, + height: 0 + } + : { + y: (y + y1) / 2, + y1: (y + y1) / 2, + height: undefined + } + }; + } + case 'xy': + default: { + const x = attrs.x; + const y = attrs.y; + const x1 = attrs.x1; + const y1 = attrs.y1; + const width = attrs.width; + const height = attrs.height; + const to: any = {}; + + if (isValid(width)) { + to.x = x + width / 2; + to.width = 0; + to.x1 = undefined; + } else { + to.x = (x + x1) / 2; + to.x1 = (x + x1) / 2; + to.width = undefined; + } + + if (isValid(height)) { + to.y = y + height / 2; + to.height = 0; + to.y1 = undefined; + } else { + to.y = (y + y1) / 2; + to.y1 = (y + y1) / 2; + to.height = undefined; + } + + return { + to + }; + } + } +}; + +/** + * 增长渐入 + */ +export class GrowCenterIn extends ACustomAnimate> { + declare valid: boolean; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: any) { + super(from, to, duration, easing, params); + } + + onBind(): void { + const { from, to } = growCenterIn(this.target, this.params.options, this.params); + const fromAttrs = this.target.context.lastAttrs ?? from; + this.props = to; + this.propKeys = Object.keys(to).filter(key => to[key] != null); + this.animate.reSyncProps(); + this.from = fromAttrs; + this.to = to; + this.target.setAttributes(fromAttrs); + } + + onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { + super.onEnd(cb); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + this.propKeys.forEach(key => { + out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.setAttributes(out); + } +} + +export class GrowCenterOut extends ACustomAnimate> { + declare valid: boolean; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: any) { + super(from, to, duration, easing, params); + } + + onBind(): void { + const attrs = this.target.getFinalAttribute(); + const { from, to } = growCenterOut(this.target, this.params.options, this.params); + this.props = to; + this.propKeys = Object.keys(to).filter(key => to[key] != null); + this.animate.reSyncProps(); + this.from = from || attrs; + this.to = to; + // this.target.setAttributes(from); + } + + onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { + super.onEnd(cb); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + this.propKeys.forEach(key => { + out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.setAttributes(out); + } +} diff --git a/packages/vrender-animate/src/custom/growHeight.ts b/packages/vrender-animate/src/custom/growHeight.ts new file mode 100644 index 000000000..32cdb4d73 --- /dev/null +++ b/packages/vrender-animate/src/custom/growHeight.ts @@ -0,0 +1,219 @@ +import type { IGraphic, IGroup } from '@visactor/vrender-core'; +import { isNil, isNumber, isValid } from '@visactor/vutils'; +import type { IAnimate, IStep } from '../intreface/animate'; +import type { EasingType } from '../intreface/easing'; +import { ACustomAnimate } from './custom-animate'; + +interface IGrowCartesianAnimationOptions { + orient?: 'positive' | 'negative'; + overall?: boolean | number; + direction?: 'x' | 'y' | 'xy'; +} + +interface IAnimationParameters { + width: number; + height: number; + group: IGroup; + elementIndex: number; + elementCount: number; + view: any; +} + +type TypeAnimation = ( + graphic: T, + options: any, + animationParameters: IAnimationParameters +) => { from?: { [channel: string]: any }; to?: { [channel: string]: any } }; + +function growHeightInIndividual( + graphic: IGraphic, + options: IGrowCartesianAnimationOptions, + animationParameters: IAnimationParameters +) { + const attrs = graphic.getFinalAttribute(); + const y = attrs.y; + const y1 = attrs.y1; + const height = attrs.height; + + if (options && options.orient === 'negative') { + const computedY1 = isValid(height) ? Math.max(y, y + height) : Math.max(y, y1); + return { + from: { y: computedY1, y1: isNil(y1) ? undefined : computedY1, height: isNil(height) ? undefined : 0 }, + to: { y: y, y1: y1, height: height } + }; + } + + const computedY = isValid(height) ? Math.min(y, y + height) : Math.min(y, y1); + return { + from: { y: computedY, y1: isNil(y1) ? undefined : computedY, height: isNil(height) ? undefined : 0 }, + to: { y: y, y1: y1, height: height } + }; +} + +function growHeightInOverall( + graphic: IGraphic, + options: IGrowCartesianAnimationOptions, + animationParameters: IAnimationParameters +) { + const attrs = graphic.getFinalAttribute(); + const y = attrs.y; + const y1 = attrs.y1; + const height = attrs.height; + + let overallValue: number; + if (options && options.orient === 'negative') { + if (isNumber(options.overall)) { + overallValue = options.overall; + } else if (animationParameters.group) { + overallValue = (animationParameters as any).groupHeight ?? animationParameters.group.getBounds().height(); + + (animationParameters as any).groupHeight = overallValue; + } else { + overallValue = animationParameters.height; + } + } else { + overallValue = isNumber(options?.overall) ? options.overall : 0; + } + return { + from: { y: overallValue, y1: isNil(y1) ? undefined : overallValue, height: isNil(height) ? undefined : 0 }, + to: { y: y, y1: y1, height: height } + }; +} + +const growHeightIn: TypeAnimation = ( + graphic: IGraphic, + options: IGrowCartesianAnimationOptions, + animationParameters: IAnimationParameters +) => { + return (options?.overall ?? false) !== false + ? growHeightInOverall(graphic, options, animationParameters) + : growHeightInIndividual(graphic, options, animationParameters); +}; + +/** + * 增长渐入 + */ +export class GrowHeightIn extends ACustomAnimate> { + declare valid: boolean; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: any) { + super(from, to, duration, easing, params); + } + + onBind(): void { + const { from, to } = growHeightIn(this.target, this.params.options, this.params); + const fromAttrs = this.target.context.lastAttrs ?? from; + this.props = to; + this.propKeys = Object.keys(to).filter(key => to[key] != null); + this.animate.reSyncProps(); + this.from = fromAttrs; + this.to = to; + this.target.setAttributes(fromAttrs); + } + + onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { + super.onEnd(cb); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + this.propKeys.forEach(key => { + out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.setAttributes(out); + } +} + +function growHeightOutIndividual( + graphic: IGraphic, + options: IGrowCartesianAnimationOptions, + animationParameters: IAnimationParameters +) { + const attrs = graphic.getFinalAttribute(); + const y = attrs.y; + const y1 = attrs.y1; + const height = attrs.height; + + if (options && options.orient === 'negative') { + const computedY1 = isValid(height) ? Math.max(y, y + height) : Math.max(y, y1); + + return { + to: { y: computedY1, y1: isNil(y1) ? undefined : computedY1, height: isNil(height) ? undefined : 0 } + }; + } + + const computedY = isValid(height) ? Math.min(y, y + height) : Math.min(y, y1); + return { + to: { y: computedY, y1: isNil(y1) ? undefined : computedY, height: isNil(height) ? undefined : 0 } + }; +} + +function growHeightOutOverall( + graphic: IGraphic, + options: IGrowCartesianAnimationOptions, + animationParameters: IAnimationParameters +) { + const attrs = graphic.getFinalAttribute(); + const y1 = attrs.y1; + const height = attrs.height; + + let overallValue: number; + if (options && options.orient === 'negative') { + if (isNumber(options.overall)) { + overallValue = options.overall; + } else if (animationParameters.group) { + overallValue = (animationParameters as any).groupHeight ?? animationParameters.group.getBounds().height(); + + (animationParameters as any).groupHeight = overallValue; + } else { + overallValue = animationParameters.height; + } + } else { + overallValue = isNumber(options?.overall) ? options.overall : 0; + } + return { + to: { y: overallValue, y1: isNil(y1) ? undefined : overallValue, height: isNil(height) ? undefined : 0 } + }; +} + +/** + * 增长渐出 + */ +export const growHeightOut: TypeAnimation = ( + graphic: IGraphic, + options: IGrowCartesianAnimationOptions, + animationParameters: IAnimationParameters +) => { + return (options?.overall ?? false) !== false + ? growHeightOutOverall(graphic, options, animationParameters) + : growHeightOutIndividual(graphic, options, animationParameters); +}; + +export class GrowHeightOut extends ACustomAnimate> { + declare valid: boolean; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: any) { + super(from, to, duration, easing, params); + } + + onBind(): void { + const attrs = this.target.getFinalAttribute(); + const { from, to } = growHeightOut(this.target, this.params.options, this.params); + this.props = to; + this.propKeys = Object.keys(to).filter(key => to[key] != null); + this.animate.reSyncProps(); + this.from = from || attrs; + this.to = to; + // this.target.setAttributes(from); + } + + onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { + super.onEnd(cb); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + this.propKeys.forEach(key => { + out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.setAttributes(out); + } +} diff --git a/packages/vrender-animate/src/custom/growWidth.ts b/packages/vrender-animate/src/custom/growWidth.ts new file mode 100644 index 000000000..1dc1a1084 --- /dev/null +++ b/packages/vrender-animate/src/custom/growWidth.ts @@ -0,0 +1,217 @@ +import type { IGraphic, IGroup } from '@visactor/vrender-core'; +import { isNil, isNumber, isValid } from '@visactor/vutils'; +import type { IAnimate, IStep } from '../intreface/animate'; +import type { EasingType } from '../intreface/easing'; +import { ACustomAnimate } from './custom-animate'; + +interface IGrowCartesianAnimationOptions { + orient?: 'positive' | 'negative'; + overall?: boolean | number; + direction?: 'x' | 'y' | 'xy'; +} + +interface IAnimationParameters { + width: number; + height: number; + group: IGroup; + elementIndex: number; + elementCount: number; + view: any; +} + +type TypeAnimation = ( + graphic: T, + options: any, + animationParameters: IAnimationParameters +) => { from?: { [channel: string]: any }; to?: { [channel: string]: any } }; + +function growWidthInIndividual( + graphic: IGraphic, + options: IGrowCartesianAnimationOptions, + animationParameters: IAnimationParameters +) { + const attrs = graphic.getFinalAttribute(); + const x = attrs.x; + const x1 = attrs.x1; + const width = attrs.width; + + if (options && options.orient === 'negative') { + const computedX1 = isValid(width) ? Math.max(x, x + width) : Math.max(x, x1); + + return { + from: { x: computedX1, x1: isNil(x1) ? undefined : computedX1, width: isNil(width) ? undefined : 0 }, + to: { x: x, x1: x1, width: width } + }; + } + + const computedX = isValid(width) ? Math.min(x, x + width) : Math.min(x, x1); + return { + from: { x: computedX, x1: isNil(x1) ? undefined : computedX, width: isNil(width) ? undefined : 0 }, + to: { x: x, x1: x1, width: width } + }; +} + +function growWidthInOverall( + graphic: IGraphic, + options: IGrowCartesianAnimationOptions, + animationParameters: IAnimationParameters +) { + const attrs = graphic.getFinalAttribute(); + // no need to handle the situation where x > x1 + const x = attrs.x; + const x1 = attrs.x1; + const width = attrs.width; + let overallValue: number; + if (options && options.orient === 'negative') { + if (isNumber(options.overall)) { + overallValue = options.overall; + } else if (animationParameters.group) { + overallValue = (animationParameters as any).groupWidth ?? animationParameters.group.getBounds().width(); + + (animationParameters as any).groupWidth = overallValue; + } else { + overallValue = animationParameters.width; + } + } else { + overallValue = isNumber(options?.overall) ? options?.overall : 0; + } + return { + from: { x: overallValue, x1: isNil(x1) ? undefined : overallValue, width: isNil(width) ? undefined : 0 }, + to: { x: x, x1: x1, width: width } + }; +} + +const growWidthIn: TypeAnimation = ( + graphic: IGraphic, + options: IGrowCartesianAnimationOptions, + animationParameters: IAnimationParameters +) => { + return (options?.overall ?? false) !== false + ? growWidthInOverall(graphic, options, animationParameters) + : growWidthInIndividual(graphic, options, animationParameters); +}; + +function growWidthOutIndividual( + graphic: IGraphic, + options: IGrowCartesianAnimationOptions, + animationParameters: IAnimationParameters +) { + const attrs = graphic.getFinalAttribute(); + const x = attrs.x; + const x1 = attrs.x1; + const width = attrs.width; + + if (options && options.orient === 'negative') { + const computedX1 = isValid(width) ? Math.max(x, x + width) : Math.max(x, x1); + + return { + to: { x: computedX1, x1: isNil(x1) ? undefined : computedX1, width: isNil(width) ? undefined : 0 } + }; + } + + const computedX = isValid(width) ? Math.min(x, x + width) : Math.min(x, x1); + return { + to: { x: computedX, x1: isNil(x1) ? undefined : computedX, width: isNil(width) ? undefined : 0 } + }; +} + +function growWidthOutOverall( + graphic: IGraphic, + options: IGrowCartesianAnimationOptions, + animationParameters: IAnimationParameters +) { + const attrs = graphic.getFinalAttribute(); + const x1 = attrs.x1; + const width = attrs.width; + + let overallValue: number; + if (options && options.orient === 'negative') { + if (isNumber(options.overall)) { + overallValue = options.overall; + } else if (animationParameters.group) { + overallValue = (animationParameters as any).groupWidth ?? animationParameters.group.getBounds().width(); + + (animationParameters as any).groupWidth = overallValue; + } else { + overallValue = animationParameters.width; + } + } else { + overallValue = isNumber(options?.overall) ? options.overall : 0; + } + return { + to: { x: overallValue, x1: isNil(x1) ? undefined : overallValue, width: isNil(width) ? undefined : 0 } + }; +} + +export const growWidthOut: TypeAnimation = ( + graphic: IGraphic, + options: IGrowCartesianAnimationOptions, + animationParameters: IAnimationParameters +) => { + return (options?.overall ?? false) !== false + ? growWidthOutOverall(graphic, options, animationParameters) + : growWidthOutIndividual(graphic, options, animationParameters); +}; + +/** + * 增长渐入 + */ +export class GrowWidthIn extends ACustomAnimate> { + declare valid: boolean; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: any) { + super(from, to, duration, easing, params); + } + + onBind(): void { + const { from, to } = growWidthIn(this.target, this.params.options, this.params); + const fromAttrs = this.target.context.lastAttrs ?? from; + this.props = to; + this.propKeys = Object.keys(to).filter(key => to[key] != null); + this.animate.reSyncProps(); + this.from = fromAttrs; + this.to = to; + this.target.setAttributes(fromAttrs); + } + + onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { + super.onEnd(cb); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + this.propKeys.forEach(key => { + out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.setAttributes(out); + } +} + +export class GrowWidthOut extends ACustomAnimate> { + declare valid: boolean; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: any) { + super(from, to, duration, easing, params); + } + + onBind(): void { + const attrs = this.target.getFinalAttribute(); + const { from, to } = growWidthOut(this.target, this.params.options, this.params); + this.props = to; + this.propKeys = Object.keys(to).filter(key => to[key] != null); + this.animate.reSyncProps(); + this.from = from || attrs; + this.to = to; + // this.target.setAttributes(from); + } + + onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { + super.onEnd(cb); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + this.propKeys.forEach(key => { + out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.setAttributes(out); + } +} diff --git a/packages/vrender-animate/src/custom/number.ts b/packages/vrender-animate/src/custom/number.ts index 58514295b..18970efba 100644 --- a/packages/vrender-animate/src/custom/number.ts +++ b/packages/vrender-animate/src/custom/number.ts @@ -185,7 +185,7 @@ export class IncreaseCount extends ACustomAnimate<{ text: string | number }> { onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { super.onEnd(cb); if (!cb) { - this.target.setAttributes(this.to); + this.props && this.target.setAttributes(this.props as any); } } diff --git a/packages/vrender-animate/src/custom/register.ts b/packages/vrender-animate/src/custom/register.ts new file mode 100644 index 000000000..66f1e1a13 --- /dev/null +++ b/packages/vrender-animate/src/custom/register.ts @@ -0,0 +1,27 @@ +import { AnimateExecutor } from '../executor/animate-executor'; +import { ClipIn, ClipOut } from './clip'; +import { FadeIn, FadeOut } from './fade'; +import { GrowCenterIn, GrowCenterOut } from './growCenter'; +import { GrowHeightIn, GrowHeightOut } from './growHeight'; +import { GrowWidthIn, GrowWidthOut } from './growWidth'; +import { ScaleIn, ScaleOut } from './scale'; +import { State } from './state'; +import { Update } from './update'; + +export const registerCustomAnimate = () => { + AnimateExecutor.registerBuiltInAnimate('scaleIn', ScaleIn); + AnimateExecutor.registerBuiltInAnimate('scaleOut', ScaleOut); + AnimateExecutor.registerBuiltInAnimate('growHeightIn', GrowHeightIn); + AnimateExecutor.registerBuiltInAnimate('growHeightOut', GrowHeightOut); + AnimateExecutor.registerBuiltInAnimate('growWidthIn', GrowWidthIn); + AnimateExecutor.registerBuiltInAnimate('growWidthOut', GrowWidthOut); + AnimateExecutor.registerBuiltInAnimate('growCenterIn', GrowCenterIn); + AnimateExecutor.registerBuiltInAnimate('growCenterOut', GrowCenterOut); + AnimateExecutor.registerBuiltInAnimate('clipIn', ClipIn); + AnimateExecutor.registerBuiltInAnimate('clipOut', ClipOut); + AnimateExecutor.registerBuiltInAnimate('fadeIn', FadeIn); + AnimateExecutor.registerBuiltInAnimate('fadeOut', FadeOut); + // state和update共用一个自定义动画类 + AnimateExecutor.registerBuiltInAnimate('update', Update); + AnimateExecutor.registerBuiltInAnimate('state', State); +}; diff --git a/packages/vrender-animate/src/custom/scale.ts b/packages/vrender-animate/src/custom/scale.ts new file mode 100644 index 000000000..90dc184b7 --- /dev/null +++ b/packages/vrender-animate/src/custom/scale.ts @@ -0,0 +1,105 @@ +import type { IAnimate, IStep } from '../intreface/animate'; +import type { EasingType } from '../intreface/easing'; +import { ACustomAnimate } from './custom-animate'; + +export interface IScaleAnimationOptions { + direction?: 'x' | 'y' | 'xy'; +} + +export class ScaleIn extends ACustomAnimate> { + declare valid: boolean; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: IScaleAnimationOptions) { + super(from, to, duration, easing, params); + } + + onBind(): void { + let from: Record; + let to: Record; + const attrs = this.target.getFinalAttribute(); + const fromAttrs = this.target.context.lastAttrs ?? {}; + + switch (this.params?.direction) { + case 'x': + from = { scaleX: fromAttrs.scaleX ?? 0 }; + to = { scaleX: attrs?.scaleX ?? 1 }; + break; + case 'y': + from = { scaleY: fromAttrs.scaleY ?? 0 }; + to = { scaleY: attrs?.scaleY ?? 1 }; + break; + case 'xy': + default: + from = { scaleX: fromAttrs.scaleX ?? 0, scaleY: fromAttrs.scaleY ?? 0 }; + to = { + scaleX: attrs?.scaleX ?? 1, + scaleY: attrs?.scaleY ?? 1 + }; + } + this.props = to; + this.propKeys = Object.keys(to); + this.animate.reSyncProps(); + this.from = from; + this.to = to; + this.target.setAttributes(from); + } + + onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { + super.onEnd(cb); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + this.propKeys.forEach(key => { + out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.setAttributes(out); + } +} + +export class ScaleOut extends ACustomAnimate> { + declare valid: boolean; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: IScaleAnimationOptions) { + super(from, to, duration, easing, params); + } + + onBind(): void { + let from: Record; + let to: Record; + // 获取当前的数据 + const attrs = this.target.getFinalAttribute(); + switch (this.params?.direction) { + case 'x': + from = { scaleX: attrs?.scaleX ?? 1 }; + to = { scaleX: 0 }; + break; + case 'y': + from = { scaleY: attrs?.scaleY ?? 1 }; + to = { scaleY: 0 }; + break; + case 'xy': + default: + from = { scaleX: attrs?.scaleX ?? 1, scaleY: attrs?.scaleY ?? 1 }; + to = { + scaleX: 0, + scaleY: 0 + }; + } + this.props = to; + this.propKeys = Object.keys(to); + this.animate.reSyncProps(); + this.from = from; + this.to = to; + } + + onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { + super.onEnd(cb); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + this.propKeys.forEach(key => { + out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.setAttributes(out); + } +} diff --git a/packages/vrender-animate/src/custom/state.ts b/packages/vrender-animate/src/custom/state.ts new file mode 100644 index 000000000..785dd199f --- /dev/null +++ b/packages/vrender-animate/src/custom/state.ts @@ -0,0 +1,45 @@ +import type { IAnimate, IStep } from '../intreface/animate'; +import type { EasingType } from '../intreface/easing'; +import { ACustomAnimate } from './custom-animate'; + +export interface IUpdateAnimationOptions { + diffAttrs: Record; + animationState: string; + diffState: string; + data: Record[]; +} + +/** + * 文本输入动画,实现类似打字机的字符逐个显示效果 + * 支持通过beforeText和afterText参数添加前缀和后缀 + * 支持通过showCursor参数显示光标,cursorChar自定义光标字符 + */ +export class State extends ACustomAnimate> { + declare valid: boolean; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: IUpdateAnimationOptions) { + super(from, to, duration, easing, params); + } + + update(end: boolean, ratio: number, out: Record): void { + this.onStart(); + if (!this.props || !this.propKeys) { + return; + } + // 应用缓动函数 + const easedRatio = this.easing(ratio); + this.animate.interpolateUpdateFunction + ? this.animate.interpolateUpdateFunction(this.fromProps, this.props, easedRatio, this, this.target) + : this.interpolateUpdateFunctions.forEach((func, index) => { + // 如果这个属性被屏蔽了,直接跳过 + if (!this.animate.validAttr(this.propKeys[index])) { + return; + } + const key = this.propKeys[index]; + const fromValue = this.fromProps[key]; + const toValue = this.props[key]; + func(key, fromValue, toValue, easedRatio, this, this.target); + }); + this.onUpdate(end, easedRatio, out); + } +} diff --git a/packages/vrender-animate/src/custom/update.ts b/packages/vrender-animate/src/custom/update.ts new file mode 100644 index 000000000..d8fe22f94 --- /dev/null +++ b/packages/vrender-animate/src/custom/update.ts @@ -0,0 +1,48 @@ +import type { IAnimate, IStep } from '../intreface/animate'; +import type { EasingType } from '../intreface/easing'; +import { ACustomAnimate } from './custom-animate'; + +export interface IUpdateAnimationOptions { + diffAttrs: Record; + animationState: string; + diffState: string; + data: Record[]; +} + +/** + * 文本输入动画,实现类似打字机的字符逐个显示效果 + * 支持通过beforeText和afterText参数添加前缀和后缀 + * 支持通过showCursor参数显示光标,cursorChar自定义光标字符 + */ +export class Update extends ACustomAnimate> { + declare valid: boolean; + params: IUpdateAnimationOptions; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: IUpdateAnimationOptions) { + const { diffAttrs = {} } = params; + super(from, diffAttrs, duration, easing, params); + this.params = params; + } + + update(end: boolean, ratio: number, out: Record): void { + this.onStart(); + if (!this.props || !this.propKeys) { + return; + } + // 应用缓动函数 + const easedRatio = this.easing(ratio); + this.animate.interpolateUpdateFunction + ? this.animate.interpolateUpdateFunction(this.fromProps, this.props, easedRatio, this, this.target) + : this.interpolateUpdateFunctions.forEach((func, index) => { + // 如果这个属性被屏蔽了,直接跳过 + if (!this.animate.validAttr(this.propKeys[index])) { + return; + } + const key = this.propKeys[index]; + const fromValue = this.fromProps[key]; + const toValue = this.props[key]; + func(key, fromValue, toValue, easedRatio, this, this.target); + }); + this.onUpdate(end, easedRatio, out); + } +} diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index 9e75dfb80..0be836b8c 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -23,6 +23,12 @@ interface IAnimateExecutor { } export class AnimateExecutor implements IAnimateExecutor { + static builtInAnimateMap: Record = {}; + + static registerBuiltInAnimate(name: string, animate: IAnimationCustomConstructor) { + AnimateExecutor.builtInAnimateMap[name] = animate; + } + declare _target: IGraphic; // 所有动画实例 @@ -105,31 +111,9 @@ export class AnimateExecutor implements IAnimateExecutor { }); } - /** - * 执行动画,针对一组元素 - */ - execute(params: IAnimationConfig) { - // 判断是否为timeline配置 + parseParams(params: IAnimationConfig): IAnimationConfig { const isTimeline = 'timeSlices' in params; - // 筛选符合条件的子图元 - let filteredChildren: IGraphic[]; - - // 如果设置了partitioner,则进行筛选 - if (isTimeline && params.partitioner) { - filteredChildren = (filteredChildren ?? (this._target.getChildren() as IGraphic[])).filter(child => { - return (params as IAnimationTimeline).partitioner((child.context as any)?.data, child, {}); - }); - } - - // 如果需要排序,则进行排序 - if (isTimeline && (params as IAnimationTimeline).sort) { - filteredChildren = filteredChildren ?? (this._target.getChildren() as IGraphic[]); - filteredChildren.sort((a, b) => { - return (params as IAnimationTimeline).sort((a.context as any)?.data, (b.context as any)?.data, a, b, {}); - }); - } - const totalTime = this.resolveValue(params.totalTime, undefined, undefined); const startTime = this.resolveValue(params.startTime, undefined, 0); @@ -201,21 +185,47 @@ export class AnimateExecutor implements IAnimateExecutor { } } - // const duration = this.resolveValue(isTimeline ? 0 : (params as IAnimationTypeConfig).duration, undefined, 300); + return parsedParams; + } - // const animates: IAnimate[] = []; + /** + * 执行动画,针对一组元素 + */ + execute(params: IAnimationConfig) { + // 判断是否为timeline配置 + const isTimeline = 'timeSlices' in params; + + // 筛选符合条件的子图元 + let filteredChildren: IGraphic[]; + + // 如果设置了partitioner,则进行筛选 + if (isTimeline && params.partitioner) { + filteredChildren = (filteredChildren ?? (this._target.getChildren() as IGraphic[])).filter(child => { + return (params as IAnimationTimeline).partitioner((child.context as any)?.data, child, {}); + }); + } + + // 如果需要排序,则进行排序 + if (isTimeline && (params as IAnimationTimeline).sort) { + filteredChildren = filteredChildren ?? (this._target.getChildren() as IGraphic[]); + filteredChildren.sort((a, b) => { + return (params as IAnimationTimeline).sort((a.context as any)?.data, (b.context as any)?.data, a, b, {}); + }); + } + + const parsedParams = this.parseParams(params); const cb = isTimeline - ? (child: IGraphic, index: number) => { + ? (child: IGraphic, index: number, count: number) => { // 执行单个图元的timeline动画 - const animate = this.executeTimelineItem(parsedParams as IAnimationTimeline, child, index); + const animate = this.executeTimelineItem(parsedParams as IAnimationTimeline, child, index, count); if (animate) { this._trackAnimation(animate); } } - : (child: IGraphic, index: number) => { + : (child: IGraphic, index: number, count: number) => { // 执行单个图元的config动画 - const animate = this.executeTypeConfigItem(parsedParams as IAnimationTypeConfig, child, index); + const animate = this.executeTypeConfigItem(parsedParams as IAnimationTypeConfig, child, index, count); if (animate) { this._trackAnimation(animate); } @@ -223,11 +233,11 @@ export class AnimateExecutor implements IAnimateExecutor { // 执行每个图元的动画 if (filteredChildren) { - filteredChildren.forEach(cb); + filteredChildren.forEach((child, index) => cb(child, index, filteredChildren.length)); } else if (this._target.count <= 1) { - cb(this._target, 0); + cb(this._target, 0, 1); } else { - this._target.forEachChildren(cb); + this._target.forEachChildren((child, index) => cb(child as IGraphic, index, this._target.count - 1)); } return; @@ -236,11 +246,15 @@ export class AnimateExecutor implements IAnimateExecutor { /** * 执行 TypeConfig 类型的动画 */ - private executeTypeConfigItem(params: IAnimationTypeConfig, graphic: IGraphic, index: number): IAnimate { + private executeTypeConfigItem( + params: IAnimationTypeConfig, + graphic: IGraphic, + index: number, + count: number + ): IAnimate { const { - type = 'to', + type, channel, - custom, customParameters, easing = 'linear', delay = 0, @@ -254,6 +268,7 @@ export class AnimateExecutor implements IAnimateExecutor { options, controlOptions } = params as any; + const custom = params.custom ?? AnimateExecutor.builtInAnimateMap[type]; // 创建动画实例 const animate = graphic.animate() as unknown as IAnimate; @@ -261,8 +276,16 @@ export class AnimateExecutor implements IAnimateExecutor { const delayValue = delay as number; + // 如果设置了indexKey,则使用indexKey作为index + const datum = graphic.context?.data?.[0]; + const indexKey = graphic.context?.indexKey; + if (datum && indexKey) { + index = datum[indexKey] ?? index; + } + // 设置开始时间 - animate.startAt((startTime as number) + index * oneByOneDelay); + animate.startAt(startTime as number); + animate.wait(index * oneByOneDelay); // 添加延迟 if (delayValue > 0) { @@ -270,12 +293,60 @@ export class AnimateExecutor implements IAnimateExecutor { } // 根据 channel 配置创建属性对象 - const props = this.createPropsFromChannel(channel, graphic); + const props = params.to ?? this.createPropsFromChannel(channel, graphic); + + this._handleRunAnimate( + animate, + custom, + props, + duration as number, + easing, + customParameters, + options, + type, + graphic + ); + if (oneByOneDelay) { + animate.wait(oneByOneDelay * (count - index - 1)); + } + + // 添加后延迟 + if ((delayAfter as number) > 0) { + animate.wait(delayAfter as number); + } + + // 设置循环 + if (loop && (loop as number) > 0) { + animate.loop(loop as number); + } + + // 设置反弹 + if (bounce) { + animate.bounce(true); + } + + return animate; + } + + private _handleRunAnimate( + animate: IAnimate, + custom: IAnimationCustomConstructor | IAnimationChannelInterpolator, + props: Record, + duration: number, + easing: EasingType, + customParameters: any, + options: any, + type: string, + graphic: IGraphic + ) { // 处理自定义动画 if (custom) { const customParams = this.resolveValue(customParameters, graphic, {}); - + const objOptions = isFunction(options) + ? options.call(null, customParameters.data && customParameters.data[0], graphic, customParameters) + : options; + customParams.options = objOptions; if (isFunction(custom)) { if (/^class\s/.test(Function.prototype.toString.call(custom))) { // 自定义动画构造器 - 创建自定义动画类 @@ -304,37 +375,28 @@ export class AnimateExecutor implements IAnimateExecutor { } else if (type === 'from') { animate.from(props, duration as number, easing); } - - // 添加后延迟 - if ((delayAfter as number) > 0) { - animate.wait(delayAfter as number); - } - - // 设置循环 - if (loop && (loop as number) > 0) { - animate.loop(loop as number); - } - - // 设置反弹 - if (bounce) { - animate.bounce(true); - } - - return animate; } /** * 执行 Timeline 类型的动画 */ - private executeTimelineItem(params: IAnimationTimeline, graphic: IGraphic, index: number): IAnimate { + private executeTimelineItem(params: IAnimationTimeline, graphic: IGraphic, index: number, count: number): IAnimate { const { timeSlices, startTime = 0, loop, bounce, oneByOneDelay, priority, controlOptions } = params as any; + // 如果设置了indexKey,则使用indexKey作为index + const datum = graphic.context?.data?.[0]; + const indexKey = graphic.context?.indexKey; + if (datum && indexKey) { + index = datum[indexKey] ?? index; + } + // 创建动画实例 const animate = graphic.animate() as unknown as IAnimate; animate.priority = priority; // 设置开始时间 - animate.startAt((startTime as number) + index * oneByOneDelay); + animate.startAt(startTime as number); + animate.wait(index * oneByOneDelay); // 设置循环 if (loop && (loop as number) > 0) { @@ -353,6 +415,11 @@ export class AnimateExecutor implements IAnimateExecutor { this.applyTimeSliceToAnimate(slice, animate, graphic); }); + // 后等待 + if (oneByOneDelay) { + animate.wait(oneByOneDelay * (count - index - 1)); + } + return animate; } @@ -363,7 +430,7 @@ export class AnimateExecutor implements IAnimateExecutor { const { effects, duration = 300, delay = 0, delayAfter = 0 } = slice; // 解析时间参数 - const durationValue = duration as number; + // const durationValue = duration as number; const delayValue = delay as number; const delayAfterValue = delayAfter as number; @@ -376,41 +443,23 @@ export class AnimateExecutor implements IAnimateExecutor { const effectsArray = Array.isArray(effects) ? effects : [effects]; effectsArray.forEach(effect => { - const { type = 'to', channel, custom, customParameters, easing = 'linear', options } = effect; + const { type = 'to', channel, customParameters, easing = 'linear', options } = effect; + + const custom = effect.custom ?? AnimateExecutor.builtInAnimateMap[type]; // 根据 channel 配置创建属性对象 const props = this.createPropsFromChannel(channel, graphic); - - if (type === 'to') { - animate.to(props, durationValue, easing); - } else if (type === 'from') { - animate.from(props, durationValue, easing); - } else if (custom) { - // 处理自定义动画 - const customParams = this.resolveValue(customParameters, graphic, {}); - - if (isFunction(custom)) { - // 自定义插值器 - 创建自定义插值动画 - this.createCustomInterpolatorAnimation( - animate, - custom as IAnimationChannelInterpolator, - props, - durationValue, - easing, - customParams - ); - } else { - // 自定义动画构造器 - 创建自定义动画类 - this.createCustomAnimation( - animate, - custom as IAnimationCustomConstructor, - props, - durationValue, - easing, - customParams - ); - } - } + this._handleRunAnimate( + animate, + custom, + props, + duration as number, + easing, + customParameters, + options, + type, + graphic + ); }); // 添加后延迟 @@ -486,12 +535,7 @@ export class AnimateExecutor implements IAnimateExecutor { return props; } - if (Array.isArray(channel)) { - // 如果是属性数组,使用当前的属性值 - channel.forEach(attr => { - props[attr] = graphic.getComputedAttribute(attr); - }); - } else { + if (!Array.isArray(channel)) { // 如果是对象,解析 from/to 配置 Object.entries(channel).forEach(([key, config]) => { if (config.to !== undefined) { @@ -525,7 +569,7 @@ export class AnimateExecutor implements IAnimateExecutor { /** * 执行动画(具体执行到内部的单个图元) */ - executeItem(params: IAnimationConfig, graphic: IGraphic, index: number = 0): IAnimate | null { + executeItem(params: IAnimationConfig, graphic: IGraphic, index: number = 0, count: number = 1): IAnimate | null { if (!graphic) { return null; } @@ -535,10 +579,10 @@ export class AnimateExecutor implements IAnimateExecutor { if (isTimeline) { // 处理 Timeline 类型的动画配置 - animate = this.executeTimelineItem(params as IAnimationTimeline, graphic, index); + animate = this.executeTimelineItem(params as IAnimationTimeline, graphic, index, count); } else { // 处理 TypeConfig 类型的动画配置 - animate = this.executeTypeConfigItem(params as IAnimationTypeConfig, graphic, index); + animate = this.executeTypeConfigItem(params as IAnimationTypeConfig, graphic, index, count); } // 跟踪动画以进行生命周期管理 @@ -552,10 +596,12 @@ export class AnimateExecutor implements IAnimateExecutor { /** * 停止所有由该执行器管理的动画 */ - stop(): void { - this._animates.forEach(animate => { - animate.stop(); - }); + stop(type?: 'start' | 'end' | Record): void { + // animate.stop会从数组里删除,所以需要while循环,不能forEach + while (this._animates.length > 0) { + const animate = this._animates.pop(); + animate?.stop(type); + } // 清空动画实例数组 this._animates = []; diff --git a/packages/vrender-animate/src/executor/executor.ts b/packages/vrender-animate/src/executor/executor.ts index 3e03f3a50..b716b287c 100644 --- a/packages/vrender-animate/src/executor/executor.ts +++ b/packages/vrender-animate/src/executor/executor.ts @@ -89,6 +89,8 @@ export interface IAnimationTypeConfig { type?: string; /** 动画 channel 配置 */ channel?: IAnimationChannelAttrs | IAnimationChannelAttributes; + /** 动画 to 配置(和channel互斥,如果同时设置,以to为准) */ + to?: Record; /** 动画 自定义插值 配置 */ custom?: IAnimationChannelInterpolator | IAnimationCustomConstructor; /** 动画 custom 参数配置 */ diff --git a/packages/vrender-animate/src/index.ts b/packages/vrender-animate/src/index.ts index 9bd09b652..7b9b660f6 100644 --- a/packages/vrender-animate/src/index.ts +++ b/packages/vrender-animate/src/index.ts @@ -22,6 +22,10 @@ export { TagPointsUpdate } from './custom/tag-points'; export { GroupFadeIn, GroupFadeOut } from './custom/group-fade'; export { RotateBySphereAnimate } from './custom/sphere'; export { AnimateExecutor } from './executor/animate-executor'; - +export { registerCustomAnimate } from './custom/register'; // Export animation state modules export * from './state'; +export { AnimationTransitionRegistry } from './state/animation-states-registry'; +export { transitionRegistry } from './state/animation-states-registry'; +export { AnimationStateManager } from './state/animation-state'; +export { AnimationStateStore } from './state/animation-state'; diff --git a/packages/vrender-animate/src/interpolate/store.ts b/packages/vrender-animate/src/interpolate/store.ts index 179f9fa28..b60b0b79e 100644 --- a/packages/vrender-animate/src/interpolate/store.ts +++ b/packages/vrender-animate/src/interpolate/store.ts @@ -141,22 +141,27 @@ export class InterpolateUpdateStore { x = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { target.attribute.x = interpolateNumber(from, to, ratio); target.addUpdateBoundTag(); + target.addUpdatePositionTag(); }; y = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { target.attribute.y = interpolateNumber(from, to, ratio); target.addUpdateBoundTag(); + target.addUpdatePositionTag(); }; angle = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { target.attribute.angle = interpolateNumber(from, to, ratio); target.addUpdateBoundTag(); + target.addUpdatePositionTag(); }; scaleX = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { target.attribute.scaleX = interpolateNumber(from, to, ratio); target.addUpdateBoundTag(); + target.addUpdatePositionTag(); }; scaleY = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { target.attribute.scaleY = interpolateNumber(from, to, ratio); target.addUpdateBoundTag(); + target.addUpdatePositionTag(); }; lineWidth = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { target.attribute.lineWidth = interpolateNumber(from, to, ratio); diff --git a/packages/vrender-animate/src/intreface/animate.ts b/packages/vrender-animate/src/intreface/animate.ts index 8145ccc4c..900b6cab9 100644 --- a/packages/vrender-animate/src/intreface/animate.ts +++ b/packages/vrender-animate/src/intreface/animate.ts @@ -139,6 +139,8 @@ export interface IAnimate { // 并行执行 parallel: (animate: IAnimate) => this; + getLoop: () => number; + // 反转动画 // reversed: (r: boolean) => IAnimate; // 循环动画 diff --git a/packages/vrender-animate/src/register.ts b/packages/vrender-animate/src/register.ts index 04595cdcf..640c2b193 100644 --- a/packages/vrender-animate/src/register.ts +++ b/packages/vrender-animate/src/register.ts @@ -1,9 +1,10 @@ import { Graphic } from '@visactor/vrender-core'; import { Animate } from './animate'; -import { DefaultTimeline } from './timeline'; +import { defaultTimeline, DefaultTimeline } from './timeline'; import { DefaultTicker } from './ticker/default-ticker'; import { mixin } from '@visactor/vutils'; import { GraphicStateExtension } from './state/graphic-extension'; +import { AnimateExtension } from './animate-extension'; export function registerAnimate() { if (!(Graphic as any).Animate) { @@ -12,10 +13,14 @@ export function registerAnimate() { if (!(Graphic as any).Timeline) { (Graphic as any).Timeline = DefaultTimeline; } + if (!(Graphic as any).defaultTimeline) { + (Graphic as any).defaultTimeline = defaultTimeline; + } if (!(Graphic as any).Ticker) { (Graphic as any).Ticker = DefaultTicker; } // Mix in animation state methods to Graphic prototype mixin(Graphic, GraphicStateExtension); + mixin(Graphic, AnimateExtension); } diff --git a/packages/vrender-animate/src/state/animation-state.ts b/packages/vrender-animate/src/state/animation-state.ts index 38b178474..59f1ab76c 100644 --- a/packages/vrender-animate/src/state/animation-state.ts +++ b/packages/vrender-animate/src/state/animation-state.ts @@ -50,8 +50,9 @@ export class AnimationStateManager { * 应用状态 * @param nextState 下一个状态数组,如果传入数组,那么状态是串行的。但是每次applyState都会立即执行动画,也就是applyState和applyState之间是并行 * @param animationConfig 动画配置 + * @param callback 动画结束后的回调函数,参数empty为true表示没有动画需要执行直接调的回调 */ - applyState(nextState: string[], animationConfig: IAnimationState[]): void { + applyState(nextState: string[], animationConfig: IAnimationState[], callback?: (empty?: boolean) => void): void { const registry = AnimationTransitionRegistry.getInstance(); // TODO 这里指判断第一个状态,后续如果需要的话要循环判断 @@ -67,7 +68,7 @@ export class AnimationStateManager { }); }); } else { - const _stateList = this.stateList[0]; + // const _stateList = this.stateList[0]; nextState.forEach((state, index) => { // 遍历this.stateList,获取result,只要有一个是false,那这个result就是false const result: { allowTransition: boolean; stopOriginalTransition: boolean } = { @@ -77,17 +78,21 @@ export class AnimationStateManager { this.stateList.forEach(currState => { const _result = registry.isTransitionAllowed(currState.state, state, this.graphic); result.allowTransition = result.allowTransition && _result.allowTransition; - result.stopOriginalTransition = result.stopOriginalTransition && _result.stopOriginalTransition; }); + // 所有状态都允许过渡,则添加到shouldApplyState if (result.allowTransition) { shouldApplyState.push({ state, animationConfig: animationConfig[index].animation, executor: new AnimateExecutor(this.graphic) }); - } - if (result.stopOriginalTransition) { - shouldStopState.push(_stateList); + // 允许过渡的话,需要重新遍历this.stateList,获取stopOriginalTransition + this.stateList.forEach(currState => { + const _result = registry.isTransitionAllowed(currState.state, state, this.graphic); + if (_result.stopOriginalTransition) { + shouldStopState.push(currState); + } + }); } }); } @@ -101,16 +106,25 @@ export class AnimationStateManager { if (shouldApplyState.length) { shouldApplyState[0].executor.execute(shouldApplyState[0].animationConfig); // 如果下一个状态存在,那么下一个状态的动画在当前状态动画结束后立即执行 - for (let i = 1; i < shouldApplyState.length; i++) { - const nextState = shouldApplyState[i]; - shouldApplyState[i - 1].executor.onEnd(() => { + for (let i = 0; i < shouldApplyState.length; i++) { + const nextState = shouldApplyState[i + 1]; + const currentState = shouldApplyState[i]; + currentState.executor.onEnd(() => { if (nextState) { nextState.executor.execute(nextState.animationConfig); } // 删除这个状态 - this.stateList = this.stateList.filter(state => state !== shouldApplyState[i]); + this.stateList = this.stateList.filter(state => state !== currentState); + + // 如果是最后一个状态且有回调,则调用回调 + if (i === shouldApplyState.length - 1 && callback) { + callback(false); + } }); } + } else if (callback) { + // 如果没有需要应用的动画状态,直接调用回调 + callback(true); } if (this.stateList) { @@ -121,6 +135,13 @@ export class AnimationStateManager { this.stateList.push(...shouldApplyState); } + stopState(state: string, type?: 'start' | 'end' | Record): void { + const stateInfo = this.stateList?.find(stateInfo => stateInfo.state === state); + if (stateInfo) { + stateInfo.executor.stop(type); + } + } + clearState(): void { // 清空状态 this.stateList?.forEach(state => { diff --git a/packages/vrender-animate/src/state/animation-states-registry.ts b/packages/vrender-animate/src/state/animation-states-registry.ts index b8cde7c49..d3905da11 100644 --- a/packages/vrender-animate/src/state/animation-states-registry.ts +++ b/packages/vrender-animate/src/state/animation-states-registry.ts @@ -62,6 +62,7 @@ export class AnimationTransitionRegistry { allowTransition: true, stopOriginalTransition: false })); + // 循环动画碰到循环动画,什么都不会发生 this.registerTransition('normal', 'normal', () => ({ allowTransition: false, stopOriginalTransition: false diff --git a/packages/vrender-animate/src/state/graphic-extension.ts b/packages/vrender-animate/src/state/graphic-extension.ts index 454349a78..83c4fc09a 100644 --- a/packages/vrender-animate/src/state/graphic-extension.ts +++ b/packages/vrender-animate/src/state/graphic-extension.ts @@ -32,8 +32,16 @@ export class GraphicStateExtension { /** * 应用一个动画状态到图形 */ - applyAnimationState(state: string[], animationConfig: IAnimationState[]): this { - this._getAnimationStateManager(this as unknown as IGraphic).applyState(state, animationConfig); + applyAnimationState(state: string[], animationConfig: IAnimationState[], callback?: (empty?: boolean) => void): this { + this._getAnimationStateManager(this as unknown as IGraphic).applyState(state, animationConfig, callback); + return this; + } + + /** + * 停止一个动画状态 + */ + stopAnimationState(state: string, type?: 'start' | 'end' | Record): this { + this._getAnimationStateManager(this as unknown as IGraphic).stopState(state, type); return this; } diff --git a/packages/vrender-animate/src/step.ts b/packages/vrender-animate/src/step.ts index e6283acfa..3699e73cf 100644 --- a/packages/vrender-animate/src/step.ts +++ b/packages/vrender-animate/src/step.ts @@ -168,9 +168,9 @@ export class Step implements IStep { // 获取上一步的属性值作为起始值 this.fromProps = this.getLastProps(); this.determineInterpolateUpdateFunction(); - this.onFirstRun(); this.tryPreventConflict(); this.trySyncStartProps(); + this.onFirstRun(); } } @@ -196,7 +196,8 @@ export class Step implements IStep { */ deleteSelfAttr(key: string): void { delete this.props[key]; - delete this.fromProps[key]; + // fromProps在动画开始时才会计算,这时可能不在 + this.fromProps && delete this.fromProps[key]; const index = this.propKeys.indexOf(key); if (index !== -1) { this.propKeys.splice(index, 1); diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index fea6890d8..8047d8805 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -55,6 +55,7 @@ import { builtinSymbolsMap, builtInSymbolStrMap, CustomSymbolClass } from './bui import { isSvg, XMLParser } from '../common/xml'; import { SVG_PARSE_ATTRIBUTE_MAP, SVG_PARSE_ATTRIBUTE_MAP_KEYS } from './constants'; import { DefaultStateAnimateConfig } from '../animate/config'; +import type { ITimeline } from '../interface/animate'; const _tempBounds = new AABBBounds(); /** @@ -172,6 +173,7 @@ export abstract class Graphic = Partial = Partial = Partial { - animate.stop(); - this.animates.delete(animate.id); - }); - - // TODO 考虑性能问题 - this.stage?.ticker.start(); - - return animate; - } - onAttributeUpdate(context?: ISetAttributeContext) { if (context && context.skipUpdateCallback) { return; @@ -1007,13 +979,30 @@ export abstract class Graphic = Partial = Partial void; // animate - animate: (params?: IGraphicAnimateParams) => IAnimate; + animate?: (params?: IGraphicAnimateParams) => IAnimate; // 语法糖,可有可无,有的为了首屏性能考虑做成get方法,有的由外界直接托管,内部不赋值 name?: string; @@ -832,6 +832,8 @@ export interface IGraphic = Partial void; getNoWorkAnimateAttr: () => Record; getGraphicTheme: () => T; + + getAttributes: () => Partial; } export interface IRoot extends IGraphic { diff --git a/packages/vrender-core/src/plugins/builtin-plugin/richtext-edit-plugin.ts b/packages/vrender-core/src/plugins/builtin-plugin/richtext-edit-plugin.ts index 29eefa459..afab6bf36 100644 --- a/packages/vrender-core/src/plugins/builtin-plugin/richtext-edit-plugin.ts +++ b/packages/vrender-core/src/plugins/builtin-plugin/richtext-edit-plugin.ts @@ -793,7 +793,7 @@ export class RichTextEditPlugin implements IPlugin { // 不使用stage的Ticker,避免影响其他的动画以及受到其他动画影响 this.addAnimateToLine(line); this.editLine = line; - this.ticker.start(true); + this.ticker && this.ticker.start(true); const g = createGroup({ x: 0, y: 0, width: 0, height: 0 }); this.editBg = g; @@ -933,7 +933,7 @@ export class RichTextEditPlugin implements IPlugin { } protected addAnimateToLine(line: ILine) { - if (!Graphic.Animate) { + if (!line.animate) { return; } line.setAttributes({ opacity: 1 }); diff --git a/packages/vrender/__tests__/browser/src/pages/animate-next.ts b/packages/vrender/__tests__/browser/src/pages/animate-next.ts index 06d8ae73e..96ab18d18 100644 --- a/packages/vrender/__tests__/browser/src/pages/animate-next.ts +++ b/packages/vrender/__tests__/browser/src/pages/animate-next.ts @@ -6,7 +6,8 @@ import { IncreaseCount, InputText, AnimateExecutor, - ACustomAnimate + ACustomAnimate, + registerCustomAnimate } from '@visactor/vrender-animate'; import { container, @@ -27,6 +28,7 @@ import type { EasingType } from '@visactor/vrender-animate'; vglobal.setEnv('browser'); registerAnimate(); +registerCustomAnimate(); let stage: any; @@ -151,6 +153,20 @@ export const page = () => { }); }); + addCase('Animate basic', btnContainer, stage => { + const rect = createRect({ + x: 100, + y: 100, + width: 100, + height: 100, + fill: 'red' + }); + stage.defaultLayer.add(rect); + + rect.animate().to({ x: 300 }, 1000, 'linear'); + // 中途设置值没问题,它会从orange开始 + rect.setAttribute('fill', 'orange'); + }); addCase('Animate chain', btnContainer, stage => { const rect = createRect({ x: 100, @@ -776,6 +792,68 @@ export const page = () => { stage.defaultLayer.add(group); stage.defaultLayer.add(text); }); + addCase('AnimateExecutor builtInAnimate', btnContainer, stage => { + // Create a group with a grid of rectangles + const group = createGroup({ + x: 100, + y: 150 + }); + + // Create a 6x4 grid of rectangles + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 6; col++) { + const rect = createRect({ + x: col * 100, + y: row * 100, + width: 80, + height: 80, + fill: 'gray' + }); + rect.context = { + data: [{ row, col, even: (row + col) % 2 === 0 }] + }; + group.add(rect); + } + } + + const executor = new AnimateExecutor(group); + + // Apply animation only to elements where row + col is even + executor.execute({ + timeSlices: { + effects: { + type: 'scaleIn', + easing: 'elasticOut' + }, + duration: 1000 + }, + // Partitioner function to filter elements + partitioner: (datum: any, graphic: IGraphic, params: any) => { + return datum && datum.length && datum[0].even === true; + }, + oneByOne: 50 + }); + + // Add title + const text = createText({ + x: 400, + y: 50, + text: 'AnimateExecutor with lifecycle', + fontSize: 16, + fill: 'black', + textAlign: 'center' + }); + + executor.onStart(() => { + console.log('onStart'); + }); + executor.onEnd(() => { + console.log('onEnd'); + alert('完成'); + }); + stage.defaultLayer.add(group); + stage.defaultLayer.add(text); + }); // New test cases for custom animations addCase('AnimateExecutor Custom Interpolator', btnContainer, stage => { diff --git a/packages/vrender/__tests__/browser/src/pages/animate-state.ts b/packages/vrender/__tests__/browser/src/pages/animate-state.ts index 7699545da..f3fd841bd 100644 --- a/packages/vrender/__tests__/browser/src/pages/animate-state.ts +++ b/packages/vrender/__tests__/browser/src/pages/animate-state.ts @@ -6,7 +6,8 @@ import { IncreaseCount, InputText, AnimateExecutor, - ACustomAnimate + ACustomAnimate, + registerCustomAnimate } from '@visactor/vrender-animate'; import { container, @@ -27,6 +28,7 @@ import type { EasingType } from '@visactor/vrender-animate'; vglobal.setEnv('browser'); registerAnimate(); +registerCustomAnimate(); let stage: any; @@ -807,46 +809,6 @@ export const page = () => { group.add(rect2); group.add(rect3); - // Register animation states - group.registerAnimationState({ - name: 'appear', - animation: { - type: 'to', - channel: { - opacity: { from: 0, to: 1 }, - scaleX: { from: 0.5, to: 1 }, - scaleY: { from: 0.5, to: 1 } - }, - duration: 1000, - easing: 'elasticOut' - } - }); - - group.registerAnimationState({ - name: 'shuffle', - animation: { - type: 'to', - channel: { - x: { to: 200 } - }, - duration: 1000, - easing: 'linear' - } - }); - - group.registerAnimationState({ - name: 'disappear', - animation: { - type: 'to', - channel: { - opacity: { to: 0 }, - y: { to: 250 } - }, - duration: 800, - easing: 'sineIn' - } - }); - // Create control buttons const createControlButton = (x: number, y: number, label: string, action: () => void) => { const buttonGroup = createGroup({ @@ -897,24 +859,65 @@ export const page = () => { buttonsGroup.add( createControlButton(200, 0, 'Appear', () => { + const normalAnimation = { + name: 'normal', + animation: { + loop: true, + startTime: 100, + oneByOne: 100, + priority: 1, + timeSlices: [ + { + delay: 1000, + effects: { + channel: { + fillOpacity: { + to: 0.5 + } + }, + easing: 'linear' + }, + duration: 500 + }, + { + effects: { + channel: { + fillOpacity: { + to: 1 + } + }, + easing: 'linear' + }, + duration: 500 + } + ], + customParameters: null + } + }; group.applyAnimationState( - ['appear'], + ['appear', 'normal'], [ { name: 'appear', animation: { type: 'to', channel: { - opacity: { from: 0, to: 1 }, - scaleX: { from: 0.5, to: 1 }, - scaleY: { from: 0.5, to: 1 } + scaleX: { from: 0, to: 1.6 }, + scaleY: { from: 0, to: 1.6 } }, duration: 1000, easing: 'elasticOut' } - } + }, + normalAnimation ] ); + + setTimeout(() => { + group.stopAnimationState('normal'); + group.applyAnimationState(['normal'], [normalAnimation]); + console.log(group); + }, 3000); }) ); diff --git a/packages/vrender/__tests__/browser/src/pages/richtext-editor.ts b/packages/vrender/__tests__/browser/src/pages/richtext-editor.ts index 0dd451c3d..a350b9aca 100644 --- a/packages/vrender/__tests__/browser/src/pages/richtext-editor.ts +++ b/packages/vrender/__tests__/browser/src/pages/richtext-editor.ts @@ -64,10 +64,28 @@ export const page = () => { fontWeight: 'normal', fontFamily: 'D-Din', lineHeight: '150%', - text: 'fkdlajfldsfjlsaa', - isComposing: false, - dy: 5, - space: 6 + text: 'a', + isComposing: false + }, + { + fill: '#1F2329', + stroke: false, + fontSize: 16, + fontWeight: 'normal', + fontFamily: 'D-Din', + lineHeight: '150%', + text: 'b', + isComposing: false + }, + { + fill: '#1F2329', + stroke: false, + fontSize: 16, + fontWeight: 'normal', + fontFamily: 'D-Din', + lineHeight: '150%', + text: 'c', + isComposing: false } ], upgradeAttrs: { From 3b0fad4010e3aaa1fe15998b014d6a100fbc90bc Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 25 Mar 2025 20:18:43 +0800 Subject: [PATCH 044/179] feat: add custom animate from vgrammar --- packages/vrender-animate/src/animate.ts | 4 + .../vrender-animate/src/custom/growAngle.ts | 190 ++++++++++ .../vrender-animate/src/custom/growCenter.ts | 2 +- .../vrender-animate/src/custom/growPoints.ts | 331 ++++++++++++++++++ .../vrender-animate/src/custom/growRadius.ts | 167 +++++++++ .../vrender-animate/src/custom/register.ts | 20 ++ 6 files changed, 713 insertions(+), 1 deletion(-) create mode 100644 packages/vrender-animate/src/custom/growAngle.ts create mode 100644 packages/vrender-animate/src/custom/growPoints.ts create mode 100644 packages/vrender-animate/src/custom/growRadius.ts diff --git a/packages/vrender-animate/src/animate.ts b/packages/vrender-animate/src/animate.ts index 4f73529d1..d32f63a10 100644 --- a/packages/vrender-animate/src/animate.ts +++ b/packages/vrender-animate/src/animate.ts @@ -114,6 +114,10 @@ export class Animate implements IAnimate { */ bind(target: IGraphic): this { this.target = target; + + if (this.target.onAnimateBind && !this.slience) { + this.target.onAnimateBind(this as any); + } // 添加一个animationAttribute属性,用于存储动画过程中的属性 if (!this.target.animationAttribute) { this.target.animationAttribute = {}; diff --git a/packages/vrender-animate/src/custom/growAngle.ts b/packages/vrender-animate/src/custom/growAngle.ts new file mode 100644 index 000000000..69b1ca270 --- /dev/null +++ b/packages/vrender-animate/src/custom/growAngle.ts @@ -0,0 +1,190 @@ +import { type IGraphic, type IGroup } from '@visactor/vrender-core'; +import type { EasingType } from '../intreface/easing'; +import { ACustomAnimate } from './custom-animate'; +import { isNumber } from '@visactor/vutils'; + +interface IAnimationParameters { + width: number; + height: number; + group: IGroup; + elementIndex: number; + elementCount: number; + view: any; +} + +type TypeAnimation = ( + graphic: T, + options: any, + animationParameters: IAnimationParameters +) => { from?: { [channel: string]: any }; to?: { [channel: string]: any } }; + +export interface IGrowAngleAnimationOptions { + orient?: 'clockwise' | 'anticlockwise'; + overall?: boolean | number; +} + +const growAngleInIndividual = ( + graphic: IGraphic, + options: IGrowAngleAnimationOptions, + animationParameters: IAnimationParameters +) => { + const attrs = graphic.getFinalAttribute(); + if (options && options.orient === 'anticlockwise') { + return { + from: { startAngle: attrs?.endAngle }, + to: { startAngle: attrs?.startAngle } + }; + } + return { + from: { endAngle: attrs?.startAngle }, + to: { endAngle: attrs?.endAngle } + }; +}; + +const growAngleInOverall = ( + graphic: IGraphic, + options: IGrowAngleAnimationOptions, + animationParameters: IAnimationParameters +) => { + const attrs = graphic.getFinalAttribute(); + + if (options && options.orient === 'anticlockwise') { + const overallValue = isNumber(options.overall) ? options.overall : Math.PI * 2; + return { + from: { + startAngle: overallValue, + endAngle: overallValue + }, + to: { + startAngle: attrs?.startAngle, + endAngle: attrs?.endAngle + } + }; + } + const overallValue = isNumber(options?.overall) ? options.overall : 0; + return { + from: { + startAngle: overallValue, + endAngle: overallValue + }, + to: { + startAngle: attrs?.startAngle, + endAngle: attrs?.endAngle + } + }; +}; + +export const growAngleIn: TypeAnimation = ( + graphic: IGraphic, + options: IGrowAngleAnimationOptions, + animationParameters: IAnimationParameters +) => { + return (options?.overall ?? false) !== false + ? growAngleInOverall(graphic, options, animationParameters) + : growAngleInIndividual(graphic, options, animationParameters); +}; + +const growAngleOutIndividual = ( + graphic: IGraphic, + options: IGrowAngleAnimationOptions, + animationParameters: IAnimationParameters +) => { + const attrs = graphic.getFinalAttribute(); + + if (options && options.orient === 'anticlockwise') { + return { + from: { startAngle: attrs.startAngle }, + to: { startAngle: attrs?.endAngle } + }; + } + return { + from: { endAngle: attrs.endAngle }, + to: { endAngle: attrs?.startAngle } + }; +}; + +const growAngleOutOverall = ( + graphic: IGraphic, + options: IGrowAngleAnimationOptions, + animationParameters: IAnimationParameters +) => { + const attrs = graphic.getFinalAttribute(); + if (options && options.orient === 'anticlockwise') { + const overallValue = isNumber(options.overall) ? options.overall : Math.PI * 2; + return { + from: { + startAngle: attrs.startAngle, + endAngle: attrs.endAngle + }, + to: { + startAngle: overallValue, + endAngle: overallValue + } + }; + } + const overallValue = isNumber(options?.overall) ? options.overall : 0; + return { + from: { + startAngle: attrs.startAngle, + endAngle: attrs.endAngle + }, + to: { + startAngle: overallValue, + endAngle: overallValue + } + }; +}; + +export const growAngleOut: TypeAnimation = ( + graphic: IGraphic, + options: IGrowAngleAnimationOptions, + animationParameters: IAnimationParameters +) => { + return (options?.overall ?? false) !== false + ? growAngleOutOverall(graphic, options, animationParameters) + : growAngleOutIndividual(graphic, options, animationParameters); +}; + +export class GworPointsBase extends ACustomAnimate> { + declare valid: boolean; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: any) { + super(from, to, duration, easing, params); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + this.propKeys.forEach(key => { + out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.setAttributes(out); + } +} + +/** + * 增长渐入 + */ +export class GrowAngleIn extends GworPointsBase { + onBind(): void { + const { from, to } = growAngleIn(this.target, this.params.options, this.params); + const fromAttrs = this.target.context.lastAttrs ?? from; + this.props = to; + this.propKeys = Object.keys(to).filter(key => to[key] != null); + this.animate.reSyncProps(); + this.from = fromAttrs; + this.to = to; + this.target.setAttributes(fromAttrs); + } +} + +export class GrowAngleOut extends GworPointsBase { + onBind(): void { + const { from, to } = growAngleOut(this.target, this.params.options, this.params); + const fromAttrs = this.target.context.lastAttrs ?? from; + this.props = to; + this.propKeys = Object.keys(to).filter(key => to[key] != null); + this.animate.reSyncProps(); + this.from = fromAttrs; + this.to = to; + this.target.setAttributes(fromAttrs); + } +} diff --git a/packages/vrender-animate/src/custom/growCenter.ts b/packages/vrender-animate/src/custom/growCenter.ts index 35f0bf72e..694a4b8a4 100644 --- a/packages/vrender-animate/src/custom/growCenter.ts +++ b/packages/vrender-animate/src/custom/growCenter.ts @@ -1,5 +1,5 @@ import type { IGraphic, IGroup } from '@visactor/vrender-core'; -import { isNil, isNumber, isValid } from '@visactor/vutils'; +import { isValid } from '@visactor/vutils'; import type { IAnimate, IStep } from '../intreface/animate'; import type { EasingType } from '../intreface/easing'; import { ACustomAnimate } from './custom-animate'; diff --git a/packages/vrender-animate/src/custom/growPoints.ts b/packages/vrender-animate/src/custom/growPoints.ts new file mode 100644 index 000000000..f73f93375 --- /dev/null +++ b/packages/vrender-animate/src/custom/growPoints.ts @@ -0,0 +1,331 @@ +import { pointInterpolation, type IGraphic, type IGroup } from '@visactor/vrender-core'; +import type { IPointLike } from '@visactor/vutils'; +import { isValidNumber } from '@visactor/vutils'; +import type { IAnimate, IStep } from '../intreface/animate'; +import type { EasingType } from '../intreface/easing'; +import { ACustomAnimate } from './custom-animate'; + +interface IAnimationParameters { + width: number; + height: number; + group: IGroup; + elementIndex: number; + elementCount: number; + view: any; +} + +type TypeAnimation = ( + graphic: T, + options: any, + animationParameters: IAnimationParameters +) => { from?: { [channel: string]: any }; to?: { [channel: string]: any } }; + +export interface IGrowPointsAnimationOptions { + orient?: 'positive' | 'negative'; +} + +export interface IGrowPointsOverallAnimationOptions extends IGrowPointsAnimationOptions { + center?: IPointLike; +} + +const getCenterPoints = ( + graphic: IGraphic, + options: IGrowPointsOverallAnimationOptions, + animationParameters: IAnimationParameters +) => { + const attrs = graphic.getFinalAttribute(); + const points: IPointLike[] = attrs.points; + const center: IPointLike = { x: 0, y: 0 }; + points.forEach(point => { + center.x += point.x; + center.y += point.y; + }); + center.x /= points.length; + center.y /= points.length; + + if (options && options.center) { + if (isValidNumber(options.center.x)) { + center.x = options.center.x; + } + if (isValidNumber(options.center.y)) { + center.y = options.center.y; + } + } + + if (graphic.type === 'area') { + center.x1 = center.x; + center.y1 = center.y; + } + + return points.map(point => Object.assign({}, point, center)); +}; + +export const growPointsIn: TypeAnimation = ( + graphic: IGraphic, + options: IGrowPointsOverallAnimationOptions, + animationParameters: IAnimationParameters +) => { + const attrs = graphic.getFinalAttribute(); + return { + from: { points: getCenterPoints(graphic, options, animationParameters) }, + to: { points: attrs.points } + }; +}; + +export const growPointsOut: TypeAnimation = ( + graphic: IGraphic, + options: IGrowPointsOverallAnimationOptions, + animationParameters: IAnimationParameters +) => { + const attrs = graphic.getFinalAttribute(); + return { + from: { points: attrs.points }, + to: { points: getCenterPoints(graphic, options, animationParameters) } + }; +}; + +export class GworPointsBase extends ACustomAnimate> { + declare valid: boolean; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: any) { + super(from, to, duration, easing, params); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + const fromPoints = this.from?.points as unknown as IPointLike[]; + const toPoints = this.to?.points as unknown as IPointLike[]; + if (!fromPoints || !toPoints) { + return; + } + + out.points = fromPoints.map((point, index) => { + const newPoint = pointInterpolation(fromPoints[index], toPoints[index], ratio); + return newPoint; + }); + this.target.setAttributes(out); + } +} + +/** + * 增长渐入 + */ +export class GrowPointsIn extends GworPointsBase { + onBind(): void { + if (['area', 'line'].includes(this.target.type)) { + const { from, to } = growPointsIn(this.target, this.params.options, this.params); + const fromAttrs = this.target.context.lastAttrs ?? from; + this.props = to; + this.propKeys = Object.keys(to).filter(key => to[key] != null); + this.animate.reSyncProps(); + this.from = fromAttrs; + this.to = to; + this.target.setAttributes(fromAttrs); + } else { + this.valid = false; + } + } +} + +export class GrowPointsOut extends GworPointsBase { + onBind(): void { + if (['area', 'line'].includes(this.target.type)) { + const attrs = this.target.getFinalAttribute(); + const { from, to } = growPointsOut(this.target, this.params.options, this.params); + this.props = to; + this.propKeys = Object.keys(to).filter(key => to[key] != null); + this.animate.reSyncProps(); + this.from = from || attrs; + this.to = to; + } else { + this.valid = false; + } + } +} + +const changePointsX = ( + graphic: IGraphic, + options: IGrowPointsAnimationOptions, + animationParameters: IAnimationParameters +) => { + const attrs = graphic.getFinalAttribute(); + const points = attrs.points; + return points.map((point: IPointLike) => { + if (options && options.orient === 'negative') { + let groupRight = animationParameters.width; + + if (animationParameters.group) { + groupRight = (animationParameters as any).groupWidth ?? animationParameters.group.getBounds().width(); + + (animationParameters as any).groupWidth = groupRight; + } + + return { + ...point, + x: groupRight, + y: point.y, + x1: groupRight, + y1: point.y1, + defined: point.defined !== false + } as IPointLike; + } + return { + ...point, + x: 0, + y: point.y, + x1: 0, + y1: point.y1, + defined: point.defined !== false + } as IPointLike; + }); +}; + +const growPointsXIn: TypeAnimation = ( + graphic: IGraphic, + options: IGrowPointsAnimationOptions, + animationParameters: IAnimationParameters +) => { + const attrs = graphic.getFinalAttribute(); + return { + from: { points: changePointsX(graphic, options, animationParameters) }, + to: { points: attrs.points } + }; +}; + +const growPointsXOut: TypeAnimation = ( + graphic: IGraphic, + options: IGrowPointsAnimationOptions, + animationParameters: IAnimationParameters +) => { + const attrs = graphic.getFinalAttribute(); + return { + from: { points: attrs.points }, + to: { points: changePointsX(graphic, options, animationParameters) } + }; +}; + +export class GrowPointsXIn extends GworPointsBase { + onBind(): void { + if (['area', 'line'].includes(this.target.type)) { + const { from, to } = growPointsXIn(this.target, this.params.options, this.params); + const fromAttrs = this.target.context.lastAttrs ?? from; + this.props = to; + this.propKeys = Object.keys(to).filter(key => to[key] != null); + this.animate.reSyncProps(); + this.from = fromAttrs; + this.to = to; + this.target.setAttributes(fromAttrs); + } else { + this.valid = false; + } + } +} + +export class GrowPointsXOut extends GworPointsBase { + onBind(): void { + if (['area', 'line'].includes(this.target.type)) { + const attrs = this.target.getFinalAttribute(); + const { from, to } = growPointsXOut(this.target, this.params.options, this.params); + this.props = to; + this.propKeys = Object.keys(to).filter(key => to[key] != null); + this.animate.reSyncProps(); + this.from = from || attrs; + this.to = to; + } else { + this.valid = false; + } + } +} + +const changePointsY = ( + graphic: IGraphic, + options: IGrowPointsAnimationOptions, + animationParameters: IAnimationParameters +) => { + const attrs = graphic.getFinalAttribute(); + const points = attrs.points; + return points.map((point: IPointLike) => { + if (options && options.orient === 'negative') { + let groupBottom = animationParameters.height; + + if (animationParameters.group) { + groupBottom = (animationParameters as any).groupHeight ?? animationParameters.group.getBounds().height(); + + (animationParameters as any).groupHeight = groupBottom; + } + + return { + ...point, + x: point.x, + y: groupBottom, + x1: point.x1, + y1: groupBottom, + defined: point.defined !== false + } as IPointLike; + } + return { + ...point, + x: point.x, + y: 0, + x1: point.x1, + y1: 0, + defined: point.defined !== false + } as IPointLike; + }); +}; + +const growPointsYIn: TypeAnimation = ( + graphic: IGraphic, + options: IGrowPointsAnimationOptions, + animationParameters: IAnimationParameters +) => { + const attrs = graphic.getFinalAttribute(); + return { + from: { points: changePointsY(graphic, options, animationParameters) }, + to: { points: attrs.points } + }; +}; + +const growPointsYOut: TypeAnimation = ( + graphic: IGraphic, + options: IGrowPointsAnimationOptions, + animationParameters: IAnimationParameters +) => { + const attrs = graphic.getFinalAttribute(); + return { + from: { points: attrs.points }, + to: { points: changePointsY(graphic, options, animationParameters) } + }; +}; + +export class GrowPointsYIn extends GworPointsBase { + onBind(): void { + if (['area', 'line'].includes(this.target.type)) { + const { from, to } = growPointsYIn(this.target, this.params.options, this.params); + const fromAttrs = this.target.context.lastAttrs ?? from; + this.props = to; + this.propKeys = Object.keys(to).filter(key => to[key] != null); + this.animate.reSyncProps(); + this.from = fromAttrs; + this.to = to; + this.target.setAttributes(fromAttrs); + } else { + this.valid = false; + } + } +} + +export class GrowPointsYOut extends GworPointsBase { + onBind(): void { + if (['area', 'line'].includes(this.target.type)) { + const attrs = this.target.getFinalAttribute(); + const { from, to } = growPointsYOut(this.target, this.params.options, this.params); + this.props = to; + this.propKeys = Object.keys(to).filter(key => to[key] != null); + this.animate.reSyncProps(); + this.from = from || attrs; + this.to = to; + } else { + this.valid = false; + } + } +} diff --git a/packages/vrender-animate/src/custom/growRadius.ts b/packages/vrender-animate/src/custom/growRadius.ts new file mode 100644 index 000000000..480170fb9 --- /dev/null +++ b/packages/vrender-animate/src/custom/growRadius.ts @@ -0,0 +1,167 @@ +import { type IGraphic, type IGroup } from '@visactor/vrender-core'; +import type { EasingType } from '../intreface/easing'; +import { ACustomAnimate } from './custom-animate'; +import { isNumber } from '@visactor/vutils'; + +interface IAnimationParameters { + width: number; + height: number; + group: IGroup; + elementIndex: number; + elementCount: number; + view: any; +} + +type TypeAnimation = ( + graphic: T, + options: any, + animationParameters: IAnimationParameters +) => { from?: { [channel: string]: any }; to?: { [channel: string]: any } }; + +export interface IGrowAngleAnimationOptions { + orient?: 'clockwise' | 'anticlockwise'; + overall?: boolean | number; +} + +export interface IGrowRadiusAnimationOptions { + orient?: 'inside' | 'outside'; + overall?: boolean | number; +} + +const growRadiusInIndividual = ( + graphic: IGraphic, + options: IGrowRadiusAnimationOptions, + animationParameters: IAnimationParameters +) => { + const attrs = graphic.getFinalAttribute(); + + if (options && options.orient === 'inside') { + return { + from: { innerRadius: attrs?.outerRadius }, + to: { innerRadius: attrs?.innerRadius } + }; + } + return { + from: { outerRadius: attrs?.innerRadius }, + to: { outerRadius: attrs?.outerRadius } + }; +}; + +const growRadiusInOverall = ( + graphic: IGraphic, + options: IGrowRadiusAnimationOptions, + animationParameters: IAnimationParameters +) => { + const attrs = graphic.getFinalAttribute(); + const overallValue = isNumber(options?.overall) ? options.overall : 0; + return { + from: { + innerRadius: overallValue, + outerRadius: overallValue + }, + to: { + innerRadius: attrs?.innerRadius, + outerRadius: attrs?.outerRadius + } + }; +}; + +export const growRadiusIn: TypeAnimation = ( + graphic: IGraphic, + options: IGrowRadiusAnimationOptions, + animationParameters: IAnimationParameters +) => { + return (options?.overall ?? false) !== false + ? growRadiusInOverall(graphic, options, animationParameters) + : growRadiusInIndividual(graphic, options, animationParameters); +}; + +const growRadiusOutIndividual = ( + graphic: IGraphic, + options: IGrowRadiusAnimationOptions, + animationParameters: IAnimationParameters +) => { + const attrs = graphic.getFinalAttribute(); + if (options && options.orient === 'inside') { + return { + from: { innerRadius: graphic.getGraphicAttribute('innerRadius', true) }, + to: { innerRadius: attrs?.outerRadius } + }; + } + return { + from: { outerRadius: graphic.getGraphicAttribute('outerRadius', true) }, + to: { outerRadius: attrs?.innerRadius } + }; +}; + +const growRadiusOutOverall = ( + graphic: IGraphic, + options: IGrowRadiusAnimationOptions, + animationParameters: IAnimationParameters +) => { + const overallValue = isNumber(options?.overall) ? options.overall : 0; + return { + from: { + innerRadius: graphic.getGraphicAttribute('innerRadius', true), + outerRadius: graphic.getGraphicAttribute('outerRadius', true) + }, + to: { + innerRadius: overallValue, + outerRadius: overallValue + } + }; +}; + +export const growRadiusOut: TypeAnimation = ( + graphic: IGraphic, + options: IGrowRadiusAnimationOptions, + animationParameters: IAnimationParameters +) => { + return (options?.overall ?? false) !== false + ? growRadiusOutOverall(graphic, options, animationParameters) + : growRadiusOutIndividual(graphic, options, animationParameters); +}; + +export class GworPointsBase extends ACustomAnimate> { + declare valid: boolean; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: any) { + super(from, to, duration, easing, params); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + this.propKeys.forEach(key => { + out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.setAttributes(out); + } +} + +/** + * 增长渐入 + */ +export class GrowRadiusIn extends GworPointsBase { + onBind(): void { + const { from, to } = growRadiusIn(this.target, this.params.options, this.params); + const fromAttrs = this.target.context.lastAttrs ?? from; + this.props = to; + this.propKeys = Object.keys(to).filter(key => to[key] != null); + this.animate.reSyncProps(); + this.from = fromAttrs; + this.to = to; + this.target.setAttributes(fromAttrs); + } +} + +export class GrowRadiusOut extends GworPointsBase { + onBind(): void { + const { from, to } = growRadiusOut(this.target, this.params.options, this.params); + const fromAttrs = this.target.context.lastAttrs ?? from; + this.props = to; + this.propKeys = Object.keys(to).filter(key => to[key] != null); + this.animate.reSyncProps(); + this.from = fromAttrs; + this.to = to; + this.target.setAttributes(fromAttrs); + } +} diff --git a/packages/vrender-animate/src/custom/register.ts b/packages/vrender-animate/src/custom/register.ts index 66f1e1a13..8d68d0a4b 100644 --- a/packages/vrender-animate/src/custom/register.ts +++ b/packages/vrender-animate/src/custom/register.ts @@ -1,8 +1,18 @@ import { AnimateExecutor } from '../executor/animate-executor'; import { ClipIn, ClipOut } from './clip'; import { FadeIn, FadeOut } from './fade'; +import { GrowAngleIn, GrowAngleOut } from './growAngle'; import { GrowCenterIn, GrowCenterOut } from './growCenter'; import { GrowHeightIn, GrowHeightOut } from './growHeight'; +import { + GrowPointsIn, + GrowPointsOut, + GrowPointsXIn, + GrowPointsXOut, + GrowPointsYIn, + GrowPointsYOut +} from './growPoints'; +import { GrowRadiusIn, GrowRadiusOut } from './growRadius'; import { GrowWidthIn, GrowWidthOut } from './growWidth'; import { ScaleIn, ScaleOut } from './scale'; import { State } from './state'; @@ -21,6 +31,16 @@ export const registerCustomAnimate = () => { AnimateExecutor.registerBuiltInAnimate('clipOut', ClipOut); AnimateExecutor.registerBuiltInAnimate('fadeIn', FadeIn); AnimateExecutor.registerBuiltInAnimate('fadeOut', FadeOut); + AnimateExecutor.registerBuiltInAnimate('growPointsIn', GrowPointsIn); + AnimateExecutor.registerBuiltInAnimate('growPointsOut', GrowPointsOut); + AnimateExecutor.registerBuiltInAnimate('growPointsXIn', GrowPointsXIn); + AnimateExecutor.registerBuiltInAnimate('growPointsXOut', GrowPointsXOut); + AnimateExecutor.registerBuiltInAnimate('growPointsYIn', GrowPointsYIn); + AnimateExecutor.registerBuiltInAnimate('growPointsYOut', GrowPointsYOut); + AnimateExecutor.registerBuiltInAnimate('growAngleIn', GrowAngleIn); + AnimateExecutor.registerBuiltInAnimate('growAngleOut', GrowAngleOut); + AnimateExecutor.registerBuiltInAnimate('growRadiusIn', GrowRadiusIn); + AnimateExecutor.registerBuiltInAnimate('growRadiusOut', GrowRadiusOut); // state和update共用一个自定义动画类 AnimateExecutor.registerBuiltInAnimate('update', Update); AnimateExecutor.registerBuiltInAnimate('state', State); From d573b45f80be396812cea4e8f861ed8818ac7154 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 25 Mar 2025 20:22:38 +0800 Subject: [PATCH 045/179] fix: remove temp export from vrender-core --- packages/vrender-core/src/index.ts | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/packages/vrender-core/src/index.ts b/packages/vrender-core/src/index.ts index 823418345..6b0d09ef6 100644 --- a/packages/vrender-core/src/index.ts +++ b/packages/vrender-core/src/index.ts @@ -102,28 +102,3 @@ export * from './plugins/builtin-plugin/3dview-transform-plugin'; export * from './plugins/builtin-plugin/flex-layout-plugin'; export * from './plugins/builtin-plugin/edit-module'; -export { - registerAnimate, - ACustomAnimate, - InputText, - IncreaseCount, - Animate, - DefaultTicker, - DefaultTimeline, - TagPointsUpdate, - ClipGraphicAnimate, - ClipAngleAnimate, - ClipRadiusAnimate, - ClipDirectionAnimate, - GroupFadeIn, - GroupFadeOut, - RotateBySphereAnimate, - registerCustomAnimate, - AnimationTransitionRegistry -} from '@visactor/vrender-animate'; - -// TODO VChart那块要引用vgrammar-core,这里先临时导出,后面删除 -export const AnimateGroup = {}; -export const oneToMultiMorph = {}; -export const multiToOneMorph = {}; -export const morphPath = {}; From dc16e95f406d50ac9416db576950d7b29bfc4a8f Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 25 Mar 2025 20:46:44 +0800 Subject: [PATCH 046/179] fix: fix issue with type --- common/config/rush/pnpm-lock.yaml | 3 +++ .../vrender-animate/src/animate-extension.ts | 11 ++++++++++- .../vrender-animate/src/intreface/animate.ts | 5 +++++ packages/vrender-components/package.json | 3 ++- .../src/axis/animate/group-transition.ts | 2 +- .../src/label-item/label-item.ts | 6 +++--- .../src/label/animate/animate.ts | 4 ++-- .../vrender-components/src/poptip/poptip.ts | 18 +++++++++++++++--- .../src/weather/weather-box.ts | 5 +++-- packages/vrender-core/src/core/stage.ts | 6 +++--- packages/vrender-core/src/graphic/graphic.ts | 6 ++---- packages/vrender-core/src/interface/index.ts | 1 + .../builtin-plugin/richtext-edit-plugin.ts | 5 +++-- 13 files changed, 53 insertions(+), 22 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 3bf009c20..b7b1f2b83 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -341,6 +341,9 @@ importers: ../../packages/vrender-components: dependencies: + '@visactor/vrender-animate': + specifier: workspace:0.22.8 + version: link:../vrender-animate '@visactor/vrender-core': specifier: workspace:0.22.8 version: link:../vrender-core diff --git a/packages/vrender-animate/src/animate-extension.ts b/packages/vrender-animate/src/animate-extension.ts index a2108c7db..27326a62a 100644 --- a/packages/vrender-animate/src/animate-extension.ts +++ b/packages/vrender-animate/src/animate-extension.ts @@ -8,7 +8,8 @@ import type { IGraphicAnimateParams } from '@visactor/vrender-core'; import type { IAnimate } from './intreface/animate'; import { Animate } from './animate'; -import { defaultTimeline } from './timeline'; +import { DefaultTimeline, defaultTimeline } from './timeline'; +import { DefaultTicker } from './ticker/default-ticker'; // 基于性能考虑,每次调用animate函数,都会设置animatedAttribute为null,每次getAttributes(true)会根据animatedAttribute属性判断是否需要重新计算animatedAttribute。 export class AnimateExtension { @@ -65,6 +66,14 @@ export class AnimateExtension { } } + createTimeline() { + return new DefaultTimeline(); + } + + createTicker(stage: any) { + return new DefaultTicker(stage); + } + protected getFinalAttribute() { const finalAttribute = {}; Object.assign(finalAttribute, (this as any).attribute, this.animatedAttribute); diff --git a/packages/vrender-animate/src/intreface/animate.ts b/packages/vrender-animate/src/intreface/animate.ts index 900b6cab9..832f39462 100644 --- a/packages/vrender-animate/src/intreface/animate.ts +++ b/packages/vrender-animate/src/intreface/animate.ts @@ -160,3 +160,8 @@ export interface IAnimate { // 更新duration updateDuration: () => void; } + +export enum AnimateMode { + NORMAL = 0b0000, + SET_ATTR_IMMEDIATELY = 0b0001 +} diff --git a/packages/vrender-components/package.json b/packages/vrender-components/package.json index ea13ed173..2bd3090e4 100644 --- a/packages/vrender-components/package.json +++ b/packages/vrender-components/package.json @@ -28,7 +28,8 @@ "@visactor/vutils": "~0.19.5", "@visactor/vscale": "~0.19.5", "@visactor/vrender-core": "workspace:0.22.8", - "@visactor/vrender-kits": "workspace:0.22.8" + "@visactor/vrender-kits": "workspace:0.22.8", + "@visactor/vrender-animate": "workspace:0.22.8" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-components/src/axis/animate/group-transition.ts b/packages/vrender-components/src/axis/animate/group-transition.ts index 7590cd6f6..8425c9e76 100644 --- a/packages/vrender-components/src/axis/animate/group-transition.ts +++ b/packages/vrender-components/src/axis/animate/group-transition.ts @@ -1,5 +1,5 @@ import type { EasingType, IGraphic, IGroup } from '@visactor/vrender-core'; -import { ACustomAnimate, AnimateMode } from '@visactor/vrender-core'; +import { ACustomAnimate, AnimateMode } from '@visactor/vrender-animate'; import type { Dict } from '@visactor/vutils'; import { cloneDeep, interpolateString, isEqual, isValidNumber } from '@visactor/vutils'; import { traverseGroup } from '../../util'; diff --git a/packages/vrender-components/src/label-item/label-item.ts b/packages/vrender-components/src/label-item/label-item.ts index 34655db36..a3b22146b 100644 --- a/packages/vrender-components/src/label-item/label-item.ts +++ b/packages/vrender-components/src/label-item/label-item.ts @@ -7,7 +7,7 @@ import type { ISymbolGraphicAttribute, IText } from '@visactor/vrender-core'; -import { ILineAttribute, InputText, ISymbolAttribute } from '@visactor/vrender-core'; +import { InputText } from '@visactor/vrender-animate'; import { AbstractComponent } from '../core/base'; import type { IStoryLabelItemAttrs } from './type'; import type { ComponentOptions } from '../interface'; @@ -277,14 +277,14 @@ export class StoryLabelItem extends AbstractComponent> { this.titleShape && this.titleShape .animate() - .play(new InputText({ text: '' }, { text: this.titleShape.attribute.text as string }, duration, easing as any)); + .play( + new InputText( + { text: '' }, + { text: this.titleShape.attribute.text as string }, + duration, + easing as any + ) as any + ); this.contentShape && this.contentShape .animate() .play( - new InputText({ text: '' }, { text: this.contentShape.attribute.text as string }, duration, easing as any) + new InputText( + { text: '' }, + { text: this.contentShape.attribute.text as string }, + duration, + easing as any + ) as any ); // 摇摆 diff --git a/packages/vrender-components/src/weather/weather-box.ts b/packages/vrender-components/src/weather/weather-box.ts index 722a40c64..2811e4e66 100644 --- a/packages/vrender-components/src/weather/weather-box.ts +++ b/packages/vrender-components/src/weather/weather-box.ts @@ -2,7 +2,8 @@ import { AbstractComponent } from '../core/base'; import type { IWeatherBoxAttrs } from './type'; import type { ComponentOptions } from '../interface'; import { merge } from '@visactor/vutils'; -import { Animate, DefaultTimeline, type IGroup, type ISymbol, type ITimeline } from '@visactor/vrender-core'; +import type { IGroup, ISymbol } from '@visactor/vrender-core'; +import { Animate, DefaultTimeline, type ITimeline } from '@visactor/vrender-animate'; // todo 后续可能做成有随机数种子的伪随机,这样可以保证每次都生成一样的随机数 function random() { @@ -65,7 +66,7 @@ export class WeatherBox extends AbstractComponent> { constructor(attributes: IWeatherBoxAttrs, options?: ComponentOptions) { super(options?.skipDefault ? attributes : merge({}, WeatherBox.defaultAttributes, attributes)); - this.timeline = options?.timeline ?? new DefaultTimeline(); + this.timeline = options?.timeline ?? (new DefaultTimeline() as any); } protected render(): void { diff --git a/packages/vrender-core/src/core/stage.ts b/packages/vrender-core/src/core/stage.ts index 7f4ba50d9..9f14a9343 100644 --- a/packages/vrender-core/src/core/stage.ts +++ b/packages/vrender-core/src/core/stage.ts @@ -299,9 +299,9 @@ export class Stage extends Group implements IStage { } initAnimate(params: Partial) { - if (Graphic.Ticker && Graphic.Timeline) { - this.ticker = params.ticker || new Graphic.Ticker(this); - this.timeline = new Graphic.Timeline(); + if ((this as any).createTicker && (this as any).createTimeline) { + this.ticker = params.ticker || (this as any).createTicker(this); + this.timeline = (this as any).createTimeline(); this.ticker.addTimeline(this.timeline); this.ticker.on('tick', this.afterTickCb); } diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index 8047d8805..437e2be12 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -170,10 +170,6 @@ export abstract class Graphic = Partial, IAnimateTarget { - static Animate: IAnimateConstructor; - static Timeline: ITimelineConstructor; - static Ticker: ITickerConstructor; - static defaultTimeline: ITimeline; /** * Mixes all enumerable properties and methods from a source object to Element. * @param source - The source of properties and methods to mix in. @@ -285,6 +281,8 @@ export abstract class Graphic = Partial T; declare animates: Map; + declare animate?: () => IAnimate; + declare nextAttrs?: T; declare prevAttrs?: T; declare finalAttrs?: T; diff --git a/packages/vrender-core/src/interface/index.ts b/packages/vrender-core/src/interface/index.ts index fb1e91221..cf3aeb200 100644 --- a/packages/vrender-core/src/interface/index.ts +++ b/packages/vrender-core/src/interface/index.ts @@ -73,3 +73,4 @@ export * from './plugin'; export * from './picker'; export * from './text'; export * from './window'; +export * from './animate'; diff --git a/packages/vrender-core/src/plugins/builtin-plugin/richtext-edit-plugin.ts b/packages/vrender-core/src/plugins/builtin-plugin/richtext-edit-plugin.ts index afab6bf36..b37593641 100644 --- a/packages/vrender-core/src/plugins/builtin-plugin/richtext-edit-plugin.ts +++ b/packages/vrender-core/src/plugins/builtin-plugin/richtext-edit-plugin.ts @@ -208,8 +208,6 @@ export class RichTextEditPlugin implements IPlugin { this.commandCbs.set(FORMAT_TEXT_COMMAND, [this.formatTextCommandCb]); this.commandCbs.set(FORMAT_ALL_TEXT_COMMAND, [this.formatAllTextCommandCb]); this.updateCbs = []; - this.timeline = Graphic.Timeline && new Graphic.Timeline(); - this.ticker = Graphic.Ticker && new Graphic.Ticker([this.timeline]); this.deltaX = 0; this.deltaY = 0; } @@ -310,6 +308,9 @@ export class RichTextEditPlugin implements IPlugin { this.editModule.onInput(this.handleInput); this.editModule.onChange(this.handleChange); this.editModule.onFocusOut(this.handleFocusOut); + + this.timeline = (this as any).createTimeline && (this as any).createTimeline(); + this.ticker = (this as any).createTicker && (this as any).createTicker(context.stage); } copyToClipboard(e: KeyboardEvent): boolean { From 42bb4d846870b2063d615cfe5602f8252fa2b3eb Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 26 Mar 2025 21:45:23 +0800 Subject: [PATCH 047/179] feat: final attribute seted by vchart --- .../vrender-animate/src/animate-extension.ts | 32 ++++++++----------- packages/vrender-animate/src/custom/common.ts | 6 +++- .../vrender-animate/src/custom/growAngle.ts | 4 +++ .../vrender-animate/src/custom/growCenter.ts | 4 +++ .../vrender-animate/src/custom/growHeight.ts | 3 ++ .../vrender-animate/src/custom/growPoints.ts | 10 ++++-- .../vrender-animate/src/custom/growRadius.ts | 13 +++++--- .../vrender-animate/src/custom/growWidth.ts | 4 +++ packages/vrender-animate/src/custom/scale.ts | 4 +++ .../vrender-animate/src/interpolate/store.ts | 23 ++++++------- packages/vrender-animate/src/step.ts | 15 +++++++-- packages/vrender-core/src/index.ts | 25 +++++++++++++++ .../vrender-core/src/interface/graphic.ts | 2 +- .../browser/src/pages/animate-next.ts | 2 +- .../__tests__/browser/src/pages/rect.ts | 20 ++++++------ 15 files changed, 114 insertions(+), 53 deletions(-) diff --git a/packages/vrender-animate/src/animate-extension.ts b/packages/vrender-animate/src/animate-extension.ts index 27326a62a..f8843b68d 100644 --- a/packages/vrender-animate/src/animate-extension.ts +++ b/packages/vrender-animate/src/animate-extension.ts @@ -13,14 +13,13 @@ import { DefaultTicker } from './ticker/default-ticker'; // 基于性能考虑,每次调用animate函数,都会设置animatedAttribute为null,每次getAttributes(true)会根据animatedAttribute属性判断是否需要重新计算animatedAttribute。 export class AnimateExtension { - animatedAttribute: Record | null = null; + declare finalAttribute: Record; declare animates: Map; getAttributes(final: boolean = false) { if (final) { - this.computeAnimatedAttribute(); - return this.getFinalAttribute(); + return this.finalAttribute; } return (this as any).attribute; } @@ -54,18 +53,6 @@ export class AnimateExtension { return animate; } - protected computeAnimatedAttribute() { - if (!this.animatedAttribute) { - this.animatedAttribute = {}; - - this.animates.forEach(animate => { - if (animate.getLoop() !== Infinity) { - Object.assign(this.animatedAttribute, animate.getEndProps()); - } - }); - } - } - createTimeline() { return new DefaultTimeline(); } @@ -74,9 +61,18 @@ export class AnimateExtension { return new DefaultTicker(stage); } + setFinalAttribute(finalAttribute: Record) { + if (!this.finalAttribute) { + this.finalAttribute = {}; + } + Object.assign(this.finalAttribute, finalAttribute); + } + + initFinalAttribute(finalAttribute: Record) { + this.finalAttribute = finalAttribute; + } + protected getFinalAttribute() { - const finalAttribute = {}; - Object.assign(finalAttribute, (this as any).attribute, this.animatedAttribute); - return finalAttribute; + return this.finalAttribute; } } diff --git a/packages/vrender-animate/src/custom/common.ts b/packages/vrender-animate/src/custom/common.ts index 4e44be4a4..c05d51b09 100644 --- a/packages/vrender-animate/src/custom/common.ts +++ b/packages/vrender-animate/src/custom/common.ts @@ -16,7 +16,11 @@ export class CommonIn extends ACustomAnimate> { } onBind(): void { - const attrs = this.target.getFinalAttribute(); + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + if (this.params?.diffAttrs) { + this.target.setAttributes(this.params.diffAttrs); + } + const attrs = this.target.getAttributes(true); const fromAttrs = this.target.context.lastAttrs ?? {}; const to: Record = {}; diff --git a/packages/vrender-animate/src/custom/growAngle.ts b/packages/vrender-animate/src/custom/growAngle.ts index 69b1ca270..5ac50ae22 100644 --- a/packages/vrender-animate/src/custom/growAngle.ts +++ b/packages/vrender-animate/src/custom/growAngle.ts @@ -165,6 +165,10 @@ export class GworPointsBase extends ACustomAnimate> { */ export class GrowAngleIn extends GworPointsBase { onBind(): void { + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + if (this.params?.diffAttrs) { + this.target.setAttributes(this.params.diffAttrs); + } const { from, to } = growAngleIn(this.target, this.params.options, this.params); const fromAttrs = this.target.context.lastAttrs ?? from; this.props = to; diff --git a/packages/vrender-animate/src/custom/growCenter.ts b/packages/vrender-animate/src/custom/growCenter.ts index 694a4b8a4..f062be1ce 100644 --- a/packages/vrender-animate/src/custom/growCenter.ts +++ b/packages/vrender-animate/src/custom/growCenter.ts @@ -203,6 +203,10 @@ export class GrowCenterIn extends ACustomAnimate> { } onBind(): void { + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + if (this.params?.diffAttrs) { + this.target.setAttributes(this.params.diffAttrs); + } const { from, to } = growCenterIn(this.target, this.params.options, this.params); const fromAttrs = this.target.context.lastAttrs ?? from; this.props = to; diff --git a/packages/vrender-animate/src/custom/growHeight.ts b/packages/vrender-animate/src/custom/growHeight.ts index 32cdb4d73..cf8420c2f 100644 --- a/packages/vrender-animate/src/custom/growHeight.ts +++ b/packages/vrender-animate/src/custom/growHeight.ts @@ -101,6 +101,9 @@ export class GrowHeightIn extends ACustomAnimate> { } onBind(): void { + if (this.params?.diffAttrs) { + this.target.setAttributes(this.params.diffAttrs); + } const { from, to } = growHeightIn(this.target, this.params.options, this.params); const fromAttrs = this.target.context.lastAttrs ?? from; this.props = to; diff --git a/packages/vrender-animate/src/custom/growPoints.ts b/packages/vrender-animate/src/custom/growPoints.ts index f73f93375..b9d254203 100644 --- a/packages/vrender-animate/src/custom/growPoints.ts +++ b/packages/vrender-animate/src/custom/growPoints.ts @@ -111,7 +111,7 @@ export class GworPointsBase extends ACustomAnimate> { */ export class GrowPointsIn extends GworPointsBase { onBind(): void { - if (['area', 'line'].includes(this.target.type)) { + if (['area', 'line', 'polygon'].includes(this.target.type)) { const { from, to } = growPointsIn(this.target, this.params.options, this.params); const fromAttrs = this.target.context.lastAttrs ?? from; this.props = to; @@ -205,7 +205,7 @@ const growPointsXOut: TypeAnimation = ( export class GrowPointsXIn extends GworPointsBase { onBind(): void { - if (['area', 'line'].includes(this.target.type)) { + if (['area', 'line', 'polygon'].includes(this.target.type)) { const { from, to } = growPointsXIn(this.target, this.params.options, this.params); const fromAttrs = this.target.context.lastAttrs ?? from; this.props = to; @@ -299,7 +299,11 @@ const growPointsYOut: TypeAnimation = ( export class GrowPointsYIn extends GworPointsBase { onBind(): void { - if (['area', 'line'].includes(this.target.type)) { + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + if (this.params?.diffAttrs) { + this.target.setAttributes(this.params.diffAttrs); + } + if (['area', 'line', 'polygon'].includes(this.target.type)) { const { from, to } = growPointsYIn(this.target, this.params.options, this.params); const fromAttrs = this.target.context.lastAttrs ?? from; this.props = to; diff --git a/packages/vrender-animate/src/custom/growRadius.ts b/packages/vrender-animate/src/custom/growRadius.ts index 480170fb9..bee737ae0 100644 --- a/packages/vrender-animate/src/custom/growRadius.ts +++ b/packages/vrender-animate/src/custom/growRadius.ts @@ -84,12 +84,12 @@ const growRadiusOutIndividual = ( const attrs = graphic.getFinalAttribute(); if (options && options.orient === 'inside') { return { - from: { innerRadius: graphic.getGraphicAttribute('innerRadius', true) }, + from: { innerRadius: attrs?.innerRadius }, to: { innerRadius: attrs?.outerRadius } }; } return { - from: { outerRadius: graphic.getGraphicAttribute('outerRadius', true) }, + from: { outerRadius: attrs?.outerRadius }, to: { outerRadius: attrs?.innerRadius } }; }; @@ -99,11 +99,12 @@ const growRadiusOutOverall = ( options: IGrowRadiusAnimationOptions, animationParameters: IAnimationParameters ) => { + const attrs = graphic.getFinalAttribute(); const overallValue = isNumber(options?.overall) ? options.overall : 0; return { from: { - innerRadius: graphic.getGraphicAttribute('innerRadius', true), - outerRadius: graphic.getGraphicAttribute('outerRadius', true) + innerRadius: attrs?.innerRadius, + outerRadius: attrs?.outerRadius }, to: { innerRadius: overallValue, @@ -142,6 +143,10 @@ export class GworPointsBase extends ACustomAnimate> { */ export class GrowRadiusIn extends GworPointsBase { onBind(): void { + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + if (this.params?.diffAttrs) { + this.target.setAttributes(this.params.diffAttrs); + } const { from, to } = growRadiusIn(this.target, this.params.options, this.params); const fromAttrs = this.target.context.lastAttrs ?? from; this.props = to; diff --git a/packages/vrender-animate/src/custom/growWidth.ts b/packages/vrender-animate/src/custom/growWidth.ts index 1dc1a1084..b15339e00 100644 --- a/packages/vrender-animate/src/custom/growWidth.ts +++ b/packages/vrender-animate/src/custom/growWidth.ts @@ -164,6 +164,10 @@ export class GrowWidthIn extends ACustomAnimate> { } onBind(): void { + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + if (this.params?.diffAttrs) { + this.target.setAttributes(this.params.diffAttrs); + } const { from, to } = growWidthIn(this.target, this.params.options, this.params); const fromAttrs = this.target.context.lastAttrs ?? from; this.props = to; diff --git a/packages/vrender-animate/src/custom/scale.ts b/packages/vrender-animate/src/custom/scale.ts index 90dc184b7..62a9d63db 100644 --- a/packages/vrender-animate/src/custom/scale.ts +++ b/packages/vrender-animate/src/custom/scale.ts @@ -14,6 +14,10 @@ export class ScaleIn extends ACustomAnimate> { } onBind(): void { + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + if (this.params?.diffAttrs) { + this.target.setAttributes(this.params.diffAttrs); + } let from: Record; let to: Record; const attrs = this.target.getFinalAttribute(); diff --git a/packages/vrender-animate/src/interpolate/store.ts b/packages/vrender-animate/src/interpolate/store.ts index b60b0b79e..f2cc36450 100644 --- a/packages/vrender-animate/src/interpolate/store.ts +++ b/packages/vrender-animate/src/interpolate/store.ts @@ -1,7 +1,8 @@ import type { IGraphic } from '@visactor/vrender-core'; -import { interpolateColor, interpolatePureColorArrayToStr } from '@visactor/vrender-core'; +import { interpolateColor, interpolatePureColorArrayToStr, pointsInterpolation } from '@visactor/vrender-core'; import { interpolateNumber } from './number'; import type { IStep } from '../intreface/animate'; +import type { IPointLike } from '@visactor/vutils'; // 直接设置,触发 隐藏类(Hidden Class)优化: /** @@ -98,11 +99,9 @@ export class InterpolateUpdateStore { step: IStep, target: IGraphic ) => { - target.attribute.fill = interpolatePureColorArrayToStr( - step.fromParsedProps.fill, - step.toParsedProps.fill, - ratio - ) as any; + target.attribute.fill = step.fromParsedProps.fill + ? (interpolatePureColorArrayToStr(step.fromParsedProps.fill, step.toParsedProps.fill, ratio) as any) + : step.toParsedProps.fill; }; stroke = ( key: string, @@ -122,11 +121,9 @@ export class InterpolateUpdateStore { step: IStep, target: IGraphic ) => { - target.attribute.stroke = interpolatePureColorArrayToStr( - step.fromParsedProps.stroke, - step.toParsedProps.stroke, - ratio - ) as any; + target.attribute.stroke = step.fromParsedProps.stroke + ? (interpolatePureColorArrayToStr(step.fromParsedProps.stroke, step.toParsedProps.stroke, ratio) as any) + : step.toParsedProps.stroke; }; // 需要更新Bounds @@ -191,6 +188,10 @@ export class InterpolateUpdateStore { (target.attribute as any).size = interpolateNumber(from, to, ratio); target.addUpdateBoundTag(); }; + points = (key: string, from: IPointLike[], to: IPointLike[], ratio: number, step: IStep, target: IGraphic) => { + (target.attribute as any).points = pointsInterpolation(from, to, ratio); + target.addUpdateBoundTag(); + }; } export const interpolateUpdateStore = new InterpolateUpdateStore(); diff --git a/packages/vrender-animate/src/step.ts b/packages/vrender-animate/src/step.ts index 3699e73cf..3a5fe88c2 100644 --- a/packages/vrender-animate/src/step.ts +++ b/packages/vrender-animate/src/step.ts @@ -111,7 +111,7 @@ export class Step implements IStep { this.propKeys.forEach(key => { // 普通颜色特殊处理,需要提前解析成number[] if (key === 'fill' || key === 'stroke') { - const from = this.getLastProps()[key]; + const from = this.fromProps[key]; const to = this.props[key]; if (isString(from) && isString(to)) { const fromArray = ColorStore.Get(from, ColorType.Color255); @@ -124,8 +124,12 @@ export class Step implements IStep { } this.fromParsedProps[key] = fromArray; this.toParsedProps[key] = toArray; + funcs.push((interpolateUpdateStore as any)[key === 'fill' ? 'fillPure' : 'strokePure']); + } else if ((interpolateUpdateStore as any)[key]) { + funcs.push((interpolateUpdateStore as any)[key]); + } else { + funcs.push(commonInterpolateUpdate); } - funcs.push((interpolateUpdateStore as any)[key === 'fill' ? 'fillPure' : 'strokePure']); } else if ((interpolateUpdateStore as any)[key]) { funcs.push((interpolateUpdateStore as any)[key]); } else { @@ -167,6 +171,11 @@ export class Step implements IStep { this._hasFirstRun = true; // 获取上一步的属性值作为起始值 this.fromProps = this.getLastProps(); + const startProps = this.animate.getStartProps(); + this.propKeys && + this.propKeys.forEach(key => { + this.fromProps[key] = this.fromProps[key] ?? startProps[key]; + }); this.determineInterpolateUpdateFunction(); this.tryPreventConflict(); this.trySyncStartProps(); @@ -251,10 +260,10 @@ export class Step implements IStep { * 如果跳帧了就不一定会执行 */ onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { + this.target.setAttributes(this.props); if (cb) { this._endCb = cb; } else if (this._endCb) { - this.target.setAttributes(this.props); this._endCb(this.animate, this); } } diff --git a/packages/vrender-core/src/index.ts b/packages/vrender-core/src/index.ts index 6b0d09ef6..823418345 100644 --- a/packages/vrender-core/src/index.ts +++ b/packages/vrender-core/src/index.ts @@ -102,3 +102,28 @@ export * from './plugins/builtin-plugin/3dview-transform-plugin'; export * from './plugins/builtin-plugin/flex-layout-plugin'; export * from './plugins/builtin-plugin/edit-module'; +export { + registerAnimate, + ACustomAnimate, + InputText, + IncreaseCount, + Animate, + DefaultTicker, + DefaultTimeline, + TagPointsUpdate, + ClipGraphicAnimate, + ClipAngleAnimate, + ClipRadiusAnimate, + ClipDirectionAnimate, + GroupFadeIn, + GroupFadeOut, + RotateBySphereAnimate, + registerCustomAnimate, + AnimationTransitionRegistry +} from '@visactor/vrender-animate'; + +// TODO VChart那块要引用vgrammar-core,这里先临时导出,后面删除 +export const AnimateGroup = {}; +export const oneToMultiMorph = {}; +export const multiToOneMorph = {}; +export const morphPath = {}; diff --git a/packages/vrender-core/src/interface/graphic.ts b/packages/vrender-core/src/interface/graphic.ts index afeb99ee5..7873fb7a3 100644 --- a/packages/vrender-core/src/interface/graphic.ts +++ b/packages/vrender-core/src/interface/graphic.ts @@ -833,7 +833,7 @@ export interface IGraphic = Partial Record; getGraphicTheme: () => T; - getAttributes: () => Partial; + getAttributes: (final?: boolean) => Partial; } export interface IRoot extends IGraphic { diff --git a/packages/vrender/__tests__/browser/src/pages/animate-next.ts b/packages/vrender/__tests__/browser/src/pages/animate-next.ts index 96ab18d18..8596be638 100644 --- a/packages/vrender/__tests__/browser/src/pages/animate-next.ts +++ b/packages/vrender/__tests__/browser/src/pages/animate-next.ts @@ -163,7 +163,7 @@ export const page = () => { }); stage.defaultLayer.add(rect); - rect.animate().to({ x: 300 }, 1000, 'linear'); + rect.animate().to({ x: 300 }, 1000, 'linear').to({ y: 300 }, 1000, 'linear'); // 中途设置值没问题,它会从orange开始 rect.setAttribute('fill', 'orange'); }); diff --git a/packages/vrender/__tests__/browser/src/pages/rect.ts b/packages/vrender/__tests__/browser/src/pages/rect.ts index 01a7284f1..93f71d824 100644 --- a/packages/vrender/__tests__/browser/src/pages/rect.ts +++ b/packages/vrender/__tests__/browser/src/pages/rect.ts @@ -18,17 +18,15 @@ export const page = () => { // ); const rect = createRect({ - x: 20, - y: 20, - width: 101.55555555555556, - height: 30, - cornerRadius: -4, - background: - 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzFfMTUxNjkpIj4KPHBhdGggZD0iTTEwIDIwQzE1LjUyMjggMjAgMjAgMTUuNTIyOCAyMCAxMEMyMCA0LjQ3NzE1IDE1LjUyMjggMCAxMCAwQzQuNDc3MTUgMCAwIDQuNDc3MTUgMCAxMEMwIDE1LjUyMjggNC40NzcxNSAyMCAxMCAyMFoiIGZpbGw9IiNGMEYwRjAiLz4KPHBhdGggZD0iTTIwIDkuOTk5OTZDMjAgNS43MDAzMSAxNy4yODYzIDIuMDM0ODggMTMuNDc4MyAwLjYyMTk0OFYxOS4zNzhDMTcuMjg2MyAxNy45NjUgMjAgMTQuMjk5NiAyMCA5Ljk5OTk2WiIgZmlsbD0iI0Q4MDAyNyIvPgo8cGF0aCBkPSJNMCA5Ljk5OTk2QzAgMTQuMjk5NiAyLjcxMzc1IDE3Ljk2NSA2LjUyMTc2IDE5LjM3OFYwLjYyMTk0OEMyLjcxMzc1IDIuMDM0ODggMCA1LjcwMDMxIDAgOS45OTk5NloiIGZpbGw9IiM2REE1NDQiLz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMF8xXzE1MTY5Ij4KPHJlY3Qgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=', - fill: 'rgba(0,0,0,0.3)', - backgroundMode: 'repeat-x', - boundsPadding: [2, 2, 2, 2], - pickMode: 'imprecise' + visible: true, + lineWidth: 0, + fill: '#FF8A00', + stroke: '#FF8A00', + x: 207.40897089999999, + y: 148.53125, + width: NaN, + x1: 49.113898, + height: 381.9375 }); rect.states = { From 56e05a9485b3905abe423dd0cc0322bdb5bc8005 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 27 Mar 2025 15:07:40 +0800 Subject: [PATCH 048/179] fix: fix issue with getAttributes, optmize performance for scale and executor --- .../vrender-animate/src/animate-extension.ts | 2 +- packages/vrender-animate/src/animate.ts | 12 +- packages/vrender-animate/src/custom/common.ts | 2 +- packages/vrender-animate/src/custom/scale.ts | 39 +++++-- packages/vrender-animate/src/custom/update.ts | 5 +- .../src/executor/animate-executor.ts | 106 +++++++++++------- 6 files changed, 105 insertions(+), 61 deletions(-) diff --git a/packages/vrender-animate/src/animate-extension.ts b/packages/vrender-animate/src/animate-extension.ts index f8843b68d..e9f285f1b 100644 --- a/packages/vrender-animate/src/animate-extension.ts +++ b/packages/vrender-animate/src/animate-extension.ts @@ -18,7 +18,7 @@ export class AnimateExtension { declare animates: Map; getAttributes(final: boolean = false) { - if (final) { + if (final && this.finalAttribute) { return this.finalAttribute; } return (this as any).attribute; diff --git a/packages/vrender-animate/src/animate.ts b/packages/vrender-animate/src/animate.ts index d32f63a10..bd4069755 100644 --- a/packages/vrender-animate/src/animate.ts +++ b/packages/vrender-animate/src/animate.ts @@ -133,10 +133,10 @@ export class Animate implements IAnimate { // 创建新的step const step = new Step(AnimateStepType.to, props, duration, easing); - this.updateStepAfterAppend(step); - step.bind(this.target, this); + this.updateStepAfterAppend(step); + return this; } @@ -147,11 +147,10 @@ export class Animate implements IAnimate { // 创建新的wait step const step = new WaitStep(AnimateStepType.wait, {}, delay, 'linear'); - // 如果是第一个step - this.updateStepAfterAppend(step); - step.bind(this.target, this); + this.updateStepAfterAppend(step); + return this; } @@ -265,9 +264,8 @@ export class Animate implements IAnimate { * 自定义动画 */ play(customAnimate: ICustomAnimate): this { - this.updateStepAfterAppend(customAnimate); - customAnimate.bind(this.target, this); + this.updateStepAfterAppend(customAnimate); return this; } diff --git a/packages/vrender-animate/src/custom/common.ts b/packages/vrender-animate/src/custom/common.ts index c05d51b09..412f38087 100644 --- a/packages/vrender-animate/src/custom/common.ts +++ b/packages/vrender-animate/src/custom/common.ts @@ -20,7 +20,7 @@ export class CommonIn extends ACustomAnimate> { if (this.params?.diffAttrs) { this.target.setAttributes(this.params.diffAttrs); } - const attrs = this.target.getAttributes(true); + const attrs = (this.target as any).getAttributes(true); const fromAttrs = this.target.context.lastAttrs ?? {}; const to: Record = {}; diff --git a/packages/vrender-animate/src/custom/scale.ts b/packages/vrender-animate/src/custom/scale.ts index 62a9d63db..be10017ef 100644 --- a/packages/vrender-animate/src/custom/scale.ts +++ b/packages/vrender-animate/src/custom/scale.ts @@ -13,10 +13,12 @@ export class ScaleIn extends ACustomAnimate> { super(from, to, duration, easing, params); } + declare _updateFunction: (ratio: number) => void; + onBind(): void { // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) if (this.params?.diffAttrs) { - this.target.setAttributes(this.params.diffAttrs); + Object.assign(this.target.attribute, this.params.diffAttrs); } let from: Record; let to: Record; @@ -27,10 +29,14 @@ export class ScaleIn extends ACustomAnimate> { case 'x': from = { scaleX: fromAttrs.scaleX ?? 0 }; to = { scaleX: attrs?.scaleX ?? 1 }; + this.propKeys = ['scaleX']; + this._updateFunction = this.updateX; break; case 'y': from = { scaleY: fromAttrs.scaleY ?? 0 }; to = { scaleY: attrs?.scaleY ?? 1 }; + this.propKeys = ['scaleY']; + this._updateFunction = this.updateY; break; case 'xy': default: @@ -39,24 +45,41 @@ export class ScaleIn extends ACustomAnimate> { scaleX: attrs?.scaleX ?? 1, scaleY: attrs?.scaleY ?? 1 }; + this.propKeys = ['scaleX', 'scaleY']; + this._updateFunction = this.updateXY; } + this.props = to; - this.propKeys = Object.keys(to); - this.animate.reSyncProps(); + // 性能消耗,不用reSyncProps + // this.animate.reSyncProps(); this.from = from; this.to = to; - this.target.setAttributes(from); + // 性能优化,不需要setAttributes + Object.assign(this.target.attribute, from); + this.target.addUpdatePositionTag(); + this.target.addUpdateBoundTag(); } onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { super.onEnd(cb); } + updateX(ratio: number): void { + this.target.attribute.scaleX = this.from.scaleX + (this.to.scaleX - this.from.scaleX) * ratio; + } + + updateY(ratio: number): void { + this.target.attribute.scaleY = this.from.scaleY + (this.to.scaleY - this.from.scaleY) * ratio; + } + + updateXY(ratio: number): void { + this.updateX(ratio); + this.updateY(ratio); + } + onUpdate(end: boolean, ratio: number, out: Record): void { - this.propKeys.forEach(key => { - out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; - }); - this.target.setAttributes(out); + this._updateFunction(ratio); + this.target.addUpdatePositionTag(); } } diff --git a/packages/vrender-animate/src/custom/update.ts b/packages/vrender-animate/src/custom/update.ts index d8fe22f94..83d3f39b1 100644 --- a/packages/vrender-animate/src/custom/update.ts +++ b/packages/vrender-animate/src/custom/update.ts @@ -1,4 +1,3 @@ -import type { IAnimate, IStep } from '../intreface/animate'; import type { EasingType } from '../intreface/easing'; import { ACustomAnimate } from './custom-animate'; @@ -16,12 +15,12 @@ export interface IUpdateAnimationOptions { */ export class Update extends ACustomAnimate> { declare valid: boolean; - params: IUpdateAnimationOptions; + // params: IUpdateAnimationOptions; constructor(from: null, to: null, duration: number, easing: EasingType, params?: IUpdateAnimationOptions) { const { diffAttrs = {} } = params; super(from, diffAttrs, duration, easing, params); - this.params = params; + // this.params = params; } update(end: boolean, ratio: number, out: Record): void { diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index 0be836b8c..e1857fb75 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -111,9 +111,7 @@ export class AnimateExecutor implements IAnimateExecutor { }); } - parseParams(params: IAnimationConfig): IAnimationConfig { - const isTimeline = 'timeSlices' in params; - + parseParams(params: IAnimationConfig, isTimeline: boolean): IAnimationConfig { const totalTime = this.resolveValue(params.totalTime, undefined, undefined); const startTime = this.resolveValue(params.startTime, undefined, 0); @@ -154,15 +152,28 @@ export class AnimateExecutor implements IAnimateExecutor { slice.delay = (slice.delay as number) * scale; slice.delayAfter = (slice.delayAfter as number) * scale; slice.duration = (slice.duration as number) * scale; + if (!Array.isArray(slice.effects)) { + slice.effects = [slice.effects]; + } + slice.effects.forEach(effect => { + effect.custom = effect.custom ?? AnimateExecutor.builtInAnimateMap[effect.type as any]; + const customType = + effect.custom && isFunction(effect.custom) + ? /^class\s/.test(Function.prototype.toString.call(effect.custom)) + ? 1 + : 2 + : 0; + (effect as any).customType = customType; + }); }); parsedParams.oneByOne = oneByOneTime * scale; parsedParams.oneByOneDelay = oneByOneDelay * scale; (parsedParams as IAnimationTimeline).startTime = startTime * scale; } } else { - const delay = this.resolveValue(params.delay, undefined, 0); - const delayAfter = this.resolveValue(params.delayAfter, undefined, 0); - const duration = this.resolveValue(params.duration, undefined, 300); + const delay = this.resolveValue((params as IAnimationTypeConfig).delay, undefined, 0); + const delayAfter = this.resolveValue((params as IAnimationTypeConfig).delayAfter, undefined, 0); + const duration = this.resolveValue((params as IAnimationTypeConfig).duration, undefined, 300); let oneByOneDelay = 0; let oneByOneTime = 0; @@ -172,6 +183,17 @@ export class AnimateExecutor implements IAnimateExecutor { } parsedParams.oneByOne = oneByOneTime; parsedParams.oneByOneDelay = oneByOneDelay; + parsedParams.custom = + (params as IAnimationTypeConfig).custom ?? + AnimateExecutor.builtInAnimateMap[(params as IAnimationTypeConfig).type]; + + const customType = + parsedParams.custom && isFunction(parsedParams.custom) + ? /^class\s/.test(Function.prototype.toString.call(parsedParams.custom)) + ? 1 + : 2 + : 0; + parsedParams.customType = customType; if (totalTime) { const _totalTime = delay + delayAfter + duration + oneByOneDelay * (this._target.count - 2); @@ -213,7 +235,7 @@ export class AnimateExecutor implements IAnimateExecutor { }); } - const parsedParams = this.parseParams(params); + const parsedParams = this.parseParams(params, isTimeline); const cb = isTimeline ? (child: IGraphic, index: number, count: number) => { @@ -266,9 +288,10 @@ export class AnimateExecutor implements IAnimateExecutor { bounce, priority = 0, options, + custom, + customType, // 0: undefined, 1: class, 2: function controlOptions } = params as any; - const custom = params.custom ?? AnimateExecutor.builtInAnimateMap[type]; // 创建动画实例 const animate = graphic.animate() as unknown as IAnimate; @@ -285,7 +308,8 @@ export class AnimateExecutor implements IAnimateExecutor { // 设置开始时间 animate.startAt(startTime as number); - animate.wait(index * oneByOneDelay); + const wait = index * oneByOneDelay; + wait > 0 && animate.wait(wait); // 添加延迟 if (delayValue > 0) { @@ -298,6 +322,7 @@ export class AnimateExecutor implements IAnimateExecutor { this._handleRunAnimate( animate, custom, + customType, props, duration as number, easing, @@ -332,6 +357,7 @@ export class AnimateExecutor implements IAnimateExecutor { private _handleRunAnimate( animate: IAnimate, custom: IAnimationCustomConstructor | IAnimationChannelInterpolator, + customType: number, // 0: undefined, 1: class, 2: function props: Record, duration: number, easing: EasingType, @@ -341,34 +367,32 @@ export class AnimateExecutor implements IAnimateExecutor { graphic: IGraphic ) { // 处理自定义动画 - if (custom) { + if (custom && customType) { const customParams = this.resolveValue(customParameters, graphic, {}); const objOptions = isFunction(options) ? options.call(null, customParameters.data && customParameters.data[0], graphic, customParameters) : options; customParams.options = objOptions; - if (isFunction(custom)) { - if (/^class\s/.test(Function.prototype.toString.call(custom))) { - // 自定义动画构造器 - 创建自定义动画类 - this.createCustomAnimation( - animate, - custom as IAnimationCustomConstructor, - props, - duration as number, - easing, - customParams - ); - } else { - // 自定义插值器 - 创建自定义插值动画 - this.createCustomInterpolatorAnimation( - animate, - custom as IAnimationChannelInterpolator, - props, - duration as number, - easing, - customParams - ); - } + if (customType === 1) { + // 自定义动画构造器 - 创建自定义动画类 + this.createCustomAnimation( + animate, + custom as IAnimationCustomConstructor, + props, + duration as number, + easing, + customParams + ); + } else if (customType === 2) { + // 自定义插值器 - 创建自定义插值动画 + this.createCustomInterpolatorAnimation( + animate, + custom as IAnimationChannelInterpolator, + props, + duration as number, + easing, + customParams + ); } } else if (type === 'to') { animate.to(props, duration as number, easing); @@ -445,13 +469,12 @@ export class AnimateExecutor implements IAnimateExecutor { effectsArray.forEach(effect => { const { type = 'to', channel, customParameters, easing = 'linear', options } = effect; - const custom = effect.custom ?? AnimateExecutor.builtInAnimateMap[type]; - // 根据 channel 配置创建属性对象 const props = this.createPropsFromChannel(channel, graphic); this._handleRunAnimate( animate, - custom, + effect.custom, + (effect as any).customType, props, duration as number, easing, @@ -507,16 +530,17 @@ export class AnimateExecutor implements IAnimateExecutor { customParams: any ) { // 获取动画目标的当前属性作为起始值 - const from: Record = {}; + // const from: Record = {}; const to = props; - // 为每个属性填充起始值 - Object.keys(to).forEach(key => { - from[key] = animate.target.getComputedAttribute(key); - }); + // // 为每个属性填充起始值 + // Object.keys(to).forEach(key => { + // from[key] = animate.target.getComputedAttribute(key); + // }); // 实例化自定义动画类 - const customAnimate = new CustomAnimateConstructor(from, to, duration, easing, customParams); + // 自定义动画自己去计算from + const customAnimate = new CustomAnimateConstructor(null, to, duration, easing, customParams); // 播放自定义动画 animate.play(customAnimate); From 450ebe38a9f5684f539b0952441cc4ae4fb6570b Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 27 Mar 2025 19:57:21 +0800 Subject: [PATCH 049/179] feat: optmize performance for animate update value --- packages/vrender-animate/src/custom/common.ts | 12 ++-- .../vrender-animate/src/custom/growAngle.ts | 58 +++++++++++++++---- .../vrender-animate/src/custom/growCenter.ts | 12 ++-- .../vrender-animate/src/custom/growHeight.ts | 20 ++++--- .../vrender-animate/src/custom/growPoints.ts | 5 +- .../vrender-animate/src/custom/growRadius.ts | 6 +- .../vrender-animate/src/custom/growWidth.ts | 12 ++-- packages/vrender-animate/src/custom/scale.ts | 5 +- .../src/common/performance-raf.ts | 19 +++++- packages/vrender-core/src/core/global.ts | 47 +++++++++++++++ packages/vrender-core/src/core/stage.ts | 13 ++--- packages/vrender-core/src/interface/global.ts | 2 + 12 files changed, 165 insertions(+), 46 deletions(-) diff --git a/packages/vrender-animate/src/custom/common.ts b/packages/vrender-animate/src/custom/common.ts index 412f38087..c359587d2 100644 --- a/packages/vrender-animate/src/custom/common.ts +++ b/packages/vrender-animate/src/custom/common.ts @@ -43,10 +43,12 @@ export class CommonIn extends ACustomAnimate> { } onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; this.propKeys.forEach(key => { - out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + attribute[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; }); - this.target.setAttributes(out); + this.target.addUpdatePositionTag(); + this.target.addUpdateShapeAndBoundsTag(); } } @@ -82,9 +84,11 @@ export class CommonOut extends ACustomAnimate> { } onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; this.propKeys.forEach(key => { - out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + attribute[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; }); - this.target.setAttributes(out); + this.target.addUpdatePositionTag(); + this.target.addUpdateShapeAndBoundsTag(); } } diff --git a/packages/vrender-animate/src/custom/growAngle.ts b/packages/vrender-animate/src/custom/growAngle.ts index 5ac50ae22..bdc2809ce 100644 --- a/packages/vrender-animate/src/custom/growAngle.ts +++ b/packages/vrender-animate/src/custom/growAngle.ts @@ -145,42 +145,74 @@ export const growAngleOut: TypeAnimation = ( : growAngleOutIndividual(graphic, options, animationParameters); }; -export class GworPointsBase extends ACustomAnimate> { +export class GrowAngleBase extends ACustomAnimate> { declare valid: boolean; + declare _updateFunction: (ratio: number) => void; + constructor(from: null, to: null, duration: number, easing: EasingType, params?: any) { super(from, to, duration, easing, params); } + determineUpdateFunction(): void { + if (!this.propKeys) { + this.valid = false; + } else if (this.propKeys && this.propKeys.length > 1) { + this._updateFunction = this.updateAngle; + } else if (this.propKeys[0] === 'startAngle') { + this._updateFunction = this.updateStartAngle; + } else if (this.propKeys[0] === 'endAngle') { + this._updateFunction = this.updateEndAngle; + } else { + this.valid = false; + } + } + + updateStartAngle(ratio: number): void { + (this.target.attribute as any).startAngle = + this.from.startAngle + (this.to.startAngle - this.from.startAngle) * ratio; + } + + updateEndAngle(ratio: number): void { + (this.target.attribute as any).endAngle = this.from.endAngle + (this.to.endAngle - this.from.endAngle) * ratio; + } + + updateAngle(ratio: number): void { + this.updateStartAngle(ratio); + this.updateEndAngle(ratio); + } + onUpdate(end: boolean, ratio: number, out: Record): void { - this.propKeys.forEach(key => { - out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; - }); - this.target.setAttributes(out); + this._updateFunction(ratio); + this.target.addUpdatePositionTag(); + this.target.addUpdateShapeAndBoundsTag(); } } /** * 增长渐入 */ -export class GrowAngleIn extends GworPointsBase { +export class GrowAngleIn extends GrowAngleBase { onBind(): void { // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) if (this.params?.diffAttrs) { - this.target.setAttributes(this.params.diffAttrs); + Object.assign(this.target.attribute, this.params.diffAttrs); } const { from, to } = growAngleIn(this.target, this.params.options, this.params); const fromAttrs = this.target.context.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); - this.animate.reSyncProps(); this.from = fromAttrs; this.to = to; - this.target.setAttributes(fromAttrs); + // 性能优化,不需要setAttributes + Object.assign(this.target.attribute, fromAttrs); + this.target.addUpdatePositionTag(); + this.target.addUpdateBoundTag(); + this.determineUpdateFunction(); } } -export class GrowAngleOut extends GworPointsBase { +export class GrowAngleOut extends GrowAngleBase { onBind(): void { const { from, to } = growAngleOut(this.target, this.params.options, this.params); const fromAttrs = this.target.context.lastAttrs ?? from; @@ -189,6 +221,10 @@ export class GrowAngleOut extends GworPointsBase { this.animate.reSyncProps(); this.from = fromAttrs; this.to = to; - this.target.setAttributes(fromAttrs); + // 性能优化,不需要setAttributes + Object.assign(this.target.attribute, fromAttrs); + this.target.addUpdatePositionTag(); + this.target.addUpdateBoundTag(); + this.determineUpdateFunction(); } } diff --git a/packages/vrender-animate/src/custom/growCenter.ts b/packages/vrender-animate/src/custom/growCenter.ts index f062be1ce..1fbcd9be8 100644 --- a/packages/vrender-animate/src/custom/growCenter.ts +++ b/packages/vrender-animate/src/custom/growCenter.ts @@ -222,10 +222,12 @@ export class GrowCenterIn extends ACustomAnimate> { } onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; this.propKeys.forEach(key => { - out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + attribute[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; }); - this.target.setAttributes(out); + this.target.addUpdatePositionTag(); + this.target.addUpdateShapeAndBoundsTag(); } } @@ -252,9 +254,11 @@ export class GrowCenterOut extends ACustomAnimate> { } onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; this.propKeys.forEach(key => { - out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + attribute[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; }); - this.target.setAttributes(out); + this.target.addUpdatePositionTag(); + this.target.addUpdateShapeAndBoundsTag(); } } diff --git a/packages/vrender-animate/src/custom/growHeight.ts b/packages/vrender-animate/src/custom/growHeight.ts index cf8420c2f..254fe25ba 100644 --- a/packages/vrender-animate/src/custom/growHeight.ts +++ b/packages/vrender-animate/src/custom/growHeight.ts @@ -102,16 +102,18 @@ export class GrowHeightIn extends ACustomAnimate> { onBind(): void { if (this.params?.diffAttrs) { - this.target.setAttributes(this.params.diffAttrs); + Object.assign(this.target.attribute, this.params.diffAttrs); } const { from, to } = growHeightIn(this.target, this.params.options, this.params); const fromAttrs = this.target.context.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); - this.animate.reSyncProps(); this.from = fromAttrs; this.to = to; - this.target.setAttributes(fromAttrs); + // 性能优化,不需要setAttributes + Object.assign(this.target.attribute, fromAttrs); + this.target.addUpdatePositionTag(); + this.target.addUpdateBoundTag(); } onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { @@ -119,10 +121,12 @@ export class GrowHeightIn extends ACustomAnimate> { } onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; this.propKeys.forEach(key => { - out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + attribute[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; }); - this.target.setAttributes(out); + this.target.addUpdatePositionTag(); + this.target.addUpdateShapeAndBoundsTag(); } } @@ -214,9 +218,11 @@ export class GrowHeightOut extends ACustomAnimate> { } onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; this.propKeys.forEach(key => { - out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + attribute[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; }); - this.target.setAttributes(out); + this.target.addUpdatePositionTag(); + this.target.addUpdateShapeAndBoundsTag(); } } diff --git a/packages/vrender-animate/src/custom/growPoints.ts b/packages/vrender-animate/src/custom/growPoints.ts index b9d254203..9127be6d3 100644 --- a/packages/vrender-animate/src/custom/growPoints.ts +++ b/packages/vrender-animate/src/custom/growPoints.ts @@ -98,11 +98,12 @@ export class GworPointsBase extends ACustomAnimate> { return; } - out.points = fromPoints.map((point, index) => { + (this.target.attribute as any).points = fromPoints.map((point, index) => { const newPoint = pointInterpolation(fromPoints[index], toPoints[index], ratio); return newPoint; }); - this.target.setAttributes(out); + this.target.addUpdatePositionTag(); + this.target.addUpdateShapeAndBoundsTag(); } } diff --git a/packages/vrender-animate/src/custom/growRadius.ts b/packages/vrender-animate/src/custom/growRadius.ts index bee737ae0..345de89c1 100644 --- a/packages/vrender-animate/src/custom/growRadius.ts +++ b/packages/vrender-animate/src/custom/growRadius.ts @@ -131,10 +131,12 @@ export class GworPointsBase extends ACustomAnimate> { } onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; this.propKeys.forEach(key => { - out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + attribute[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; }); - this.target.setAttributes(out); + this.target.addUpdatePositionTag(); + this.target.addUpdateShapeAndBoundsTag(); } } diff --git a/packages/vrender-animate/src/custom/growWidth.ts b/packages/vrender-animate/src/custom/growWidth.ts index b15339e00..9c72fa059 100644 --- a/packages/vrender-animate/src/custom/growWidth.ts +++ b/packages/vrender-animate/src/custom/growWidth.ts @@ -183,10 +183,12 @@ export class GrowWidthIn extends ACustomAnimate> { } onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; this.propKeys.forEach(key => { - out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + attribute[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; }); - this.target.setAttributes(out); + this.target.addUpdatePositionTag(); + this.target.addUpdateShapeAndBoundsTag(); } } @@ -213,9 +215,11 @@ export class GrowWidthOut extends ACustomAnimate> { } onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; this.propKeys.forEach(key => { - out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + attribute[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; }); - this.target.setAttributes(out); + this.target.addUpdatePositionTag(); + this.target.addUpdateShapeAndBoundsTag(); } } diff --git a/packages/vrender-animate/src/custom/scale.ts b/packages/vrender-animate/src/custom/scale.ts index be10017ef..ab615b651 100644 --- a/packages/vrender-animate/src/custom/scale.ts +++ b/packages/vrender-animate/src/custom/scale.ts @@ -124,9 +124,10 @@ export class ScaleOut extends ACustomAnimate> { } onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; this.propKeys.forEach(key => { - out[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + attribute[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; }); - this.target.setAttributes(out); + this.target.addUpdatePositionTag(); } } diff --git a/packages/vrender-core/src/common/performance-raf.ts b/packages/vrender-core/src/common/performance-raf.ts index a0669aca2..460cbc986 100644 --- a/packages/vrender-core/src/common/performance-raf.ts +++ b/packages/vrender-core/src/common/performance-raf.ts @@ -5,6 +5,7 @@ import { vglobal } from '../modules'; */ export class PerformanceRAF { nextAnimationFrameCbs: FrameRequestCallback[] = []; + private _rafHandle: number | null = null; addAnimationFrameCb(callback: FrameRequestCallback) { this.nextAnimationFrameCbs.push(callback); @@ -13,18 +14,30 @@ export class PerformanceRAF { return this.nextAnimationFrameCbs.length - 1; } + removeAnimationFrameCb(index: number): boolean { + if (index >= 0 && index < this.nextAnimationFrameCbs.length) { + // Set to null instead of empty function to avoid linter error + this.nextAnimationFrameCbs[index] = null; + return true; + } + return false; + } + protected runAnimationFrame = (time: number) => { + this._rafHandle = null; const cbs = this.nextAnimationFrameCbs; this.nextAnimationFrameCbs = []; for (let i = 0; i < cbs.length; i++) { - cbs[i](time); + if (cbs[i]) { + cbs[i](time); + } } }; protected tryRunAnimationFrameNextFrame = () => { - if (!(this.nextAnimationFrameCbs && this.nextAnimationFrameCbs.length === 1)) { + if (this._rafHandle !== null || this.nextAnimationFrameCbs.length === 0) { return; } - vglobal.getRequestAnimationFrame()(this.runAnimationFrame); + this._rafHandle = vglobal.getRequestAnimationFrame()(this.runAnimationFrame); }; } diff --git a/packages/vrender-core/src/core/global.ts b/packages/vrender-core/src/core/global.ts index 21fa87ad0..987d83976 100644 --- a/packages/vrender-core/src/core/global.ts +++ b/packages/vrender-core/src/core/global.ts @@ -16,6 +16,7 @@ import { EnvContribution } from '../constants'; import type { IAABBBoundsLike } from '@visactor/vutils'; import { container } from '../container'; import { Generator } from '../common/generator'; +import { PerformanceRAF } from '../common/performance-raf'; const defaultEnv: EnvType = 'browser'; @injectable() @@ -25,6 +26,8 @@ export class DefaultGlobal implements IGlobal { private _isSafari?: boolean; private _isChrome?: boolean; private _isImageAnonymous?: boolean = true; + private _performanceRAFList: PerformanceRAF[] = []; + get env(): EnvType { return this._env; } @@ -274,6 +277,50 @@ export class DefaultGlobal implements IGlobal { return this.envContribution.getRequestAnimationFrame(); } + /** + * 获取特定的requestAnimationFrame,同一个id底层共用一个原生的requestAnimationFrame + * @param id 唯一标识,用于区分不同的requestAnimationFrame,请使用数字,不要太大,因为底层使用的是数组索引 + */ + getSpecifiedRequestAnimationFrame(id: number) { + if (!this._env) { + this.setEnv(defaultEnv); + } + + // Check if PerformanceRAF instance exists for this id + if (!this._performanceRAFList[id]) { + this._performanceRAFList[id] = new PerformanceRAF(); + } + + const performanceRAF = this._performanceRAFList[id]; + + // Return a function that adds the callback to the specific PerformanceRAF instance + return (callback: FrameRequestCallback): number => { + return performanceRAF.addAnimationFrameCb(callback); + }; + } + + /** + * 获取特定的cancelAnimationFrame,用于取消特定id的requestAnimationFrame + * @param id + */ + getSpecifiedCancelAnimationFrame(id: number) { + if (!this._env) { + this.setEnv(defaultEnv); + } + + // Return no-op if no PerformanceRAF instance exists for this id + if (!this._performanceRAFList[id]) { + return () => false; + } + + const performanceRAF = this._performanceRAFList[id]; + + // Return a function that removes the callback from the specific PerformanceRAF instance + return (handle: number): boolean => { + return performanceRAF.removeAnimationFrameCb(handle); + }; + } + getCancelAnimationFrame() { if (!this._env) { this.setEnv(defaultEnv); diff --git a/packages/vrender-core/src/core/stage.ts b/packages/vrender-core/src/core/stage.ts index 9f14a9343..3b83ed7e6 100644 --- a/packages/vrender-core/src/core/stage.ts +++ b/packages/vrender-core/src/core/stage.ts @@ -200,6 +200,9 @@ export class Stage extends Group implements IStage { // 第一次render不需要强行走动画 protected tickedBeforeRender: boolean = true; + // 随机分配一个rafId + readonly rafId: number; + /** * 所有属性都具有默认值。 * Canvas为字符串或者Canvas元素,那么默认图层就会绑定到这个Canvas上 @@ -296,6 +299,7 @@ export class Stage extends Group implements IStage { } this.initAnimate(params); + this.rafId = Math.floor(Math.random() * 3); } initAnimate(params: Partial) { @@ -477,12 +481,7 @@ export class Stage extends Group implements IStage { protected afterTickCb = () => { this.tickedBeforeRender = true; // 性能模式不用立刻渲染 - if (this.params.optimize?.tickRenderMode === 'performance') { - // do nothing - } else { - // 不是rendering的时候,render - this.state !== 'rendering' && this.render(); - } + this.state !== 'rendering' && this.render(); }; setBeforeRender(cb: (stage: IStage) => void) { @@ -798,7 +797,7 @@ export class Stage extends Group implements IStage { } if (!this.willNextFrameRender) { this.willNextFrameRender = true; - this.global.getRequestAnimationFrame()(() => { + this.global.getSpecifiedRequestAnimationFrame(this.rafId)(() => { this._doRenderInThisFrame(), (this.willNextFrameRender = false); }); } diff --git a/packages/vrender-core/src/interface/global.ts b/packages/vrender-core/src/interface/global.ts index 3675c02da..55ddcf6aa 100644 --- a/packages/vrender-core/src/interface/global.ts +++ b/packages/vrender-core/src/interface/global.ts @@ -254,6 +254,8 @@ export interface IGlobal extends Omit null | ((callback: FrameRequestCallback) => number); getCancelAnimationFrame: () => null | ((h: number) => void); + getSpecifiedRequestAnimationFrame: (id: number) => (callback: FrameRequestCallback) => number; + getSpecifiedCancelAnimationFrame: (id: number) => (h: number) => void; /** * 将窗口坐标转换为画布坐标,小程序/小组件环境需要兼容 From c9edc60264963cef58b4b02c6d3d04b191b11a87 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 27 Mar 2025 20:03:31 +0800 Subject: [PATCH 050/179] feat: support font performance cache --- .../canvas/contributions/browser/context.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/vrender-kits/src/canvas/contributions/browser/context.ts b/packages/vrender-kits/src/canvas/contributions/browser/context.ts index 3e5fef416..62faa76a0 100644 --- a/packages/vrender-kits/src/canvas/contributions/browser/context.ts +++ b/packages/vrender-kits/src/canvas/contributions/browser/context.ts @@ -125,6 +125,8 @@ export class BrowserContext2d implements IContext2d { declare fontFamily: string; declare fontSize: number; declare _clearMatrix: IMatrix; + declare _font?: string; + // 属性代理 set fillStyle(d: string | CanvasGradient | CanvasPattern) { this.nativeContext.fillStyle = d; @@ -133,10 +135,14 @@ export class BrowserContext2d implements IContext2d { return this.nativeContext.fillStyle; } set font(d: string) { + if (d === this._font) { + return; + } + this._font = d; this.nativeContext.font = d; } get font(): string { - return this.nativeContext.font; + return this._font ?? this.nativeContext.font; } set globalAlpha(d: number) { this.nativeContext.globalAlpha = d * this.baseGlobalAlpha; @@ -1167,13 +1173,9 @@ export class BrowserContext2d implements IContext2d { } const { scaleIn3d = defaultParams.scaleIn3d } = params; if (params.font) { - _context.font = params.font; + this.font = params.font; } else { - _context.font = getContextFont( - params, - defaultParams, - scaleIn3d && this.camera && this.camera.getProjectionScale(z) - ); + this.font = getContextFont(params, defaultParams, scaleIn3d && this.camera && this.camera.getProjectionScale(z)); } const { fontFamily = defaultParams.fontFamily, fontSize = defaultParams.fontSize } = params; this.fontFamily = fontFamily; @@ -1190,9 +1192,9 @@ export class BrowserContext2d implements IContext2d { defaultParams = this.textAttributes; } if (params.font) { - _context.font = params.font; + this.font = params.font; } else { - _context.font = getContextFont(params, defaultParams, this.camera && this.camera.getProjectionScale(z)); + this.font = getContextFont(params, defaultParams, this.camera && this.camera.getProjectionScale(z)); } const { fontFamily = defaultParams.fontFamily, fontSize = defaultParams.fontSize } = params; this.fontFamily = fontFamily; From f436b8c040a80c57be4a954635ae9caa5f16825f Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 27 Mar 2025 21:11:43 +0800 Subject: [PATCH 051/179] feat: optmize the performance for rect/arc/symbol render --- .../vrender-animate/src/custom/growAngle.ts | 1 - .../vrender-core/src/interface/graphic.ts | 1 + .../render/contributions/render/arc-render.ts | 112 ++++++++---- .../contributions/render/base-render.ts | 8 +- .../contributions/render/draw-interceptor.ts | 9 + .../contributions/render/group-render.ts | 25 ++- .../contributions/render/rect-render.ts | 115 +++++++++---- .../contributions/render/symbol-render.ts | 160 ++++++++++++++---- .../canvas/contributions/browser/context.ts | 15 +- 9 files changed, 334 insertions(+), 112 deletions(-) diff --git a/packages/vrender-animate/src/custom/growAngle.ts b/packages/vrender-animate/src/custom/growAngle.ts index bdc2809ce..dae1bd4ac 100644 --- a/packages/vrender-animate/src/custom/growAngle.ts +++ b/packages/vrender-animate/src/custom/growAngle.ts @@ -184,7 +184,6 @@ export class GrowAngleBase extends ACustomAnimate> { onUpdate(end: boolean, ratio: number, out: Record): void { this._updateFunction(ratio); - this.target.addUpdatePositionTag(); this.target.addUpdateShapeAndBoundsTag(); } } diff --git a/packages/vrender-core/src/interface/graphic.ts b/packages/vrender-core/src/interface/graphic.ts index 7873fb7a3..7d23a4c64 100644 --- a/packages/vrender-core/src/interface/graphic.ts +++ b/packages/vrender-core/src/interface/graphic.ts @@ -501,6 +501,7 @@ export type IGraphicStyle = ILayout & * 设置图形对应的鼠标样式 */ cursor: Cursor | null; + // @deprecated 用处少废弃,后续考虑新设计API filter: string; renderStyle?: 'default' | 'rough' | any; roughStyle?: IRoughStyle | null; diff --git a/packages/vrender-core/src/render/contributions/render/arc-render.ts b/packages/vrender-core/src/render/contributions/render/arc-render.ts index 574fe0956..a2005da62 100644 --- a/packages/vrender-core/src/render/contributions/render/arc-render.ts +++ b/packages/vrender-core/src/render/contributions/render/arc-render.ts @@ -19,7 +19,8 @@ import type { IGraphicRender, IGraphicRenderDrawParams, IContributionProvider, - IConicalGradient + IConicalGradient, + IArcGraphicAttribute } from '../../../interface'; import { cornerTangents, drawArcPath, fillVisible } from './utils'; import { getConicGradientAt } from '../../../canvas/conical-gradient'; @@ -208,10 +209,11 @@ export class DefaultCanvasArcRender extends BaseRender implements IGraphic ctx: IContext2d, markAttribute: Partial, themeAttribute: IThemeAttribute - ) => boolean + ) => boolean, + arcAttribute?: Required ) { // const arcAttribute = graphicService.themeService.getCurrentTheme().arcAttribute; - const arcAttribute = getTheme(arc, params?.theme).arc; + // const arcAttribute = getTheme(arc, params?.theme).arc; const { fill = arcAttribute.fill, stroke = arcAttribute.stroke, @@ -282,34 +284,35 @@ export class DefaultCanvasArcRender extends BaseRender implements IGraphic strokeCb ); - const _runFill = () => { - if (doFill) { - if (fillCb) { - fillCb(context, arc.attribute, arcAttribute); - } else if (fVisible) { - context.setCommonStyle(arc, arc.attribute, originX - x, originY - y, arcAttribute); - context.fill(); - } - } - }; - - const _runStroke = () => { - if (doStroke && isFullStroke) { - if (strokeCb) { - strokeCb(context, arc.attribute, arcAttribute); - } else if (sVisible) { - context.setStrokeStyle(arc, arc.attribute, originX - x, originY - y, arcAttribute); - context.stroke(); - } - } - }; + // 内联的函数性能差 + // const _runFill = () => { + // if (doFill) { + // if (fillCb) { + // fillCb(context, arc.attribute, arcAttribute); + // } else if (fVisible) { + // context.setCommonStyle(arc, arc.attribute, originX - x, originY - y, arcAttribute); + // context.fill(); + // } + // } + // }; + + // const _runStroke = () => { + // if (doStroke && isFullStroke) { + // if (strokeCb) { + // strokeCb(context, arc.attribute, arcAttribute); + // } else if (sVisible) { + // context.setStrokeStyle(arc, arc.attribute, originX - x, originY - y, arcAttribute); + // context.stroke(); + // } + // } + // }; if (!fillStrokeOrder) { - _runFill(); - _runStroke(); + this._runFill(arc, context, x, y, arcAttribute, doFill, fVisible, originX, originY, fillCb); + this._runStroke(arc, context, x, y, arcAttribute, doStroke, sVisible, strokeCb); } else { - _runStroke(); - _runFill(); + this._runStroke(arc, context, x, y, arcAttribute, doStroke, sVisible, strokeCb); + this._runFill(arc, context, x, y, arcAttribute, doFill, fVisible, originX, originY, fillCb); } } @@ -434,8 +437,59 @@ export class DefaultCanvasArcRender extends BaseRender implements IGraphic } } + private _runFill( + arc: IArc, + context: IContext2d, + x: number, + y: number, + arcAttribute: Required, + doFill: boolean, + fVisible: boolean, + originX: number, + originY: number, + fillCb?: ( + ctx: IContext2d, + markAttribute: Partial, + themeAttribute: IThemeAttribute + ) => boolean + ) { + if (doFill) { + if (fillCb) { + fillCb(context, arc.attribute, arcAttribute); + } else if (fVisible) { + context.setCommonStyle(arc, arc.attribute, originX - x, originY - y, arcAttribute); + context.fill(); + } + } + } + + private _runStroke( + arc: IArc, + context: IContext2d, + x: number, + y: number, + arcAttribute: Required, + doStroke: boolean, + sVisible: boolean, + strokeCb?: ( + ctx: IContext2d, + markAttribute: Partial, + themeAttribute: IThemeAttribute + ) => boolean + ) { + if (doStroke) { + if (strokeCb) { + // fillCb(context, arc.attribute, arcAttribute); + } else if (sVisible) { + context.setStrokeStyle(arc, arc.attribute, x, y, arcAttribute); + // context.strokeStyle = 'red'; + context.stroke(); + } + } + } + draw(arc: IArc, renderService: IRenderService, drawContext: IDrawContext, params?: IGraphicRenderDrawParams) { const arcAttribute = getTheme(arc, params?.theme).arc; - this._draw(arc, arcAttribute, false, drawContext, params); + this._draw(arc, arcAttribute, false, drawContext, params, arcAttribute); } } diff --git a/packages/vrender-core/src/render/contributions/render/base-render.ts b/packages/vrender-core/src/render/contributions/render/base-render.ts index 535fc7a57..d82c767dd 100644 --- a/packages/vrender-core/src/render/contributions/render/base-render.ts +++ b/packages/vrender-core/src/render/contributions/render/base-render.ts @@ -505,7 +505,8 @@ export abstract class BaseRender { defaultAttr: IGraphicAttribute, computed3dMatrix: boolean, drawContext: IDrawContext, - params?: IGraphicRenderDrawParams + params?: IGraphicRenderDrawParams, + themeAttribute?: IGraphicAttribute ) { const { context } = drawContext; if (!context) { @@ -528,7 +529,7 @@ export abstract class BaseRender { return; } - this.drawShape(graphic, context, x, y, drawContext, params); + this.drawShape(graphic, context, x, y, drawContext, params, null, null, themeAttribute); this.z = 0; if (context.modelMatrix !== lastModelMatrix) { @@ -555,7 +556,8 @@ export abstract class BaseRender { ctx: IContext2d, markAttribute: Partial, themeAttribute: IThemeAttribute - ) => boolean + ) => boolean, + themeAttribute?: IGraphicAttribute ): void; // abstract drawShape( diff --git a/packages/vrender-core/src/render/contributions/render/draw-interceptor.ts b/packages/vrender-core/src/render/contributions/render/draw-interceptor.ts index 71f34908d..a3bd7ad16 100644 --- a/packages/vrender-core/src/render/contributions/render/draw-interceptor.ts +++ b/packages/vrender-core/src/render/contributions/render/draw-interceptor.ts @@ -187,6 +187,15 @@ export class CommonDrawItemInterceptorContribution implements IDrawItemIntercept drawContribution: IDrawContribution, params?: IGraphicRenderDrawParams ): boolean { + // 【性能方案】判定写在外层,减少遍历判断耗时,10000条数据减少1ms + if ( + (!graphic.in3dMode || drawContext.in3dInterceptor) && + !graphic.shadowRoot && + !(graphic.baseGraphic || graphic.attribute.globalZIndex || graphic.interactiveGraphic) + ) { + return false; + } + for (let i = 0; i < this.interceptors.length; i++) { if ( this.interceptors[i].afterDrawItem && diff --git a/packages/vrender-core/src/render/contributions/render/group-render.ts b/packages/vrender-core/src/render/contributions/render/group-render.ts index ec44d6450..52aeb6328 100644 --- a/packages/vrender-core/src/render/contributions/render/group-render.ts +++ b/packages/vrender-core/src/render/contributions/render/group-render.ts @@ -10,7 +10,8 @@ import type { IRenderService, IGraphicRender, IGraphicRenderDrawParams, - IContributionProvider + IContributionProvider, + IGroupGraphicAttribute } from '../../../interface'; import { getTheme } from '../../../graphic/theme'; import { getModelMatrix } from '../../../graphic/graphic-service/graphic-service'; @@ -56,18 +57,25 @@ export class DefaultCanvasGroupRender implements IGraphicRender { ctx: IContext2d, markAttribute: Partial, themeAttribute: IThemeAttribute - ) => boolean + ) => boolean, + groupAttribute?: Required ) { - // const groupAttribute = graphicService.themeService.getCurrentTheme().groupAttribute; - const groupAttribute = getTheme(group, params?.theme).group; + // 提前判定,否则每次都要获取一堆属性 const { + clip = groupAttribute.clip, fill = groupAttribute.fill, - background, stroke = groupAttribute.stroke, + background + } = group.attribute; + + if (!(clip || fill || stroke || background)) { + return; + } + + const { opacity = groupAttribute.opacity, width = groupAttribute.width, height = groupAttribute.height, - clip = groupAttribute.clip, fillOpacity = groupAttribute.fillOpacity, strokeOpacity = groupAttribute.strokeOpacity, cornerRadius = groupAttribute.cornerRadius, @@ -321,10 +329,11 @@ export class DefaultCanvasGroupRender implements IGraphicRender { drawContext, params, () => false, - () => false + () => false, + groupAttribute ); } else { - this.drawShape(group, context, 0, 0, drawContext); + this.drawShape(group, context, 0, 0, drawContext, null, null, null, groupAttribute); } // 绘制子元素的时候要添加scroll diff --git a/packages/vrender-core/src/render/contributions/render/rect-render.ts b/packages/vrender-core/src/render/contributions/render/rect-render.ts index c0e6f6bfc..a627e5eaa 100644 --- a/packages/vrender-core/src/render/contributions/render/rect-render.ts +++ b/packages/vrender-core/src/render/contributions/render/rect-render.ts @@ -64,9 +64,9 @@ export class DefaultCanvasRectRender extends BaseRender implements IGraph ctx: IContext2d, markAttribute: Partial, themeAttribute: IThemeAttribute - ) => boolean + ) => boolean, + rectAttribute?: Required ) { - const rectAttribute = this.tempTheme ?? getTheme(rect, params?.theme).rect; const { fill = rectAttribute.fill, background, @@ -142,35 +142,36 @@ export class DefaultCanvasRectRender extends BaseRender implements IGraph doFillOrStroke ); - const _runFill = () => { - if (doFillOrStroke.doFill) { - if (fillCb) { - fillCb(context, rect.attribute, rectAttribute); - } else if (fVisible) { - // 存在fill - context.setCommonStyle(rect, rect.attribute, originX - x, originY - y, rectAttribute); - context.fill(); - } - } - }; - const _runStroke = () => { - if (doFillOrStroke.doStroke) { - if (strokeCb) { - strokeCb(context, rect.attribute, rectAttribute); - } else if (sVisible) { - // 存在stroke - context.setStrokeStyle(rect, rect.attribute, originX - x, originY - y, rectAttribute); - context.stroke(); - } - } - }; + // 内联的函数性能差 + // const _runFill = () => { + // if (doFillOrStroke.doFill) { + // if (fillCb) { + // fillCb(context, rect.attribute, rectAttribute); + // } else if (fVisible) { + // // 存在fill + // context.setCommonStyle(rect, rect.attribute, originX - x, originY - y, rectAttribute); + // context.fill(); + // } + // } + // }; + // const _runStroke = () => { + // if (doFillOrStroke.doStroke) { + // if (strokeCb) { + // strokeCb(context, rect.attribute, rectAttribute); + // } else if (sVisible) { + // // 存在stroke + // context.setStrokeStyle(rect, rect.attribute, originX - x, originY - y, rectAttribute); + // context.stroke(); + // } + // } + // }; if (!fillStrokeOrder) { - _runFill(); - _runStroke(); + this._runFill(rect, context, x, y, rectAttribute, doFillOrStroke, fVisible, originX, originY, fillCb); + this._runStroke(rect, context, x, y, rectAttribute, doFillOrStroke, sVisible, originX, originY, strokeCb); } else { - _runStroke(); - _runFill(); + this._runStroke(rect, context, x, y, rectAttribute, doFillOrStroke, sVisible, originX, originY, strokeCb); + this._runFill(rect, context, x, y, rectAttribute, doFillOrStroke, fVisible, originX, originY, fillCb); } this.afterRenderStep( @@ -189,10 +190,62 @@ export class DefaultCanvasRectRender extends BaseRender implements IGraph ); } + private _runFill( + rect: IRect, + context: IContext2d, + x: number, + y: number, + rectAttribute: Required, + doFillOrStroke: { doFill: boolean; doStroke: boolean }, + fVisible: boolean, + originX: number, + originY: number, + fillCb?: ( + ctx: IContext2d, + markAttribute: Partial, + themeAttribute: IThemeAttribute + ) => boolean + ) { + if (doFillOrStroke.doFill) { + if (fillCb) { + fillCb(context, rect.attribute, rectAttribute); + } else if (fVisible) { + // 存在fill + context.setCommonStyle(rect, rect.attribute, originX - x, originY - y, rectAttribute); + context.fill(); + } + } + } + + private _runStroke( + rect: IRect, + context: IContext2d, + x: number, + y: number, + rectAttribute: Required, + doFillOrStroke: { doFill: boolean; doStroke: boolean }, + sVisible: boolean, + originX: number, + originY: number, + strokeCb?: ( + ctx: IContext2d, + markAttribute: Partial, + themeAttribute: IThemeAttribute + ) => boolean + ) { + if (doFillOrStroke.doStroke) { + if (strokeCb) { + strokeCb(context, rect.attribute, rectAttribute); + } else if (sVisible) { + // 存在stroke + context.setStrokeStyle(rect, rect.attribute, originX - x, originY - y, rectAttribute); + context.stroke(); + } + } + } + draw(rect: IRect, renderService: IRenderService, drawContext: IDrawContext, params?: IGraphicRenderDrawParams) { const rectAttribute = getTheme(rect, params?.theme).rect; - this.tempTheme = rectAttribute; - this._draw(rect, rectAttribute, false, drawContext, params); - this.tempTheme = null; + this._draw(rect, rectAttribute, false, drawContext, params, rectAttribute); } } diff --git a/packages/vrender-core/src/render/contributions/render/symbol-render.ts b/packages/vrender-core/src/render/contributions/render/symbol-render.ts index b3429f70c..601dfd11a 100644 --- a/packages/vrender-core/src/render/contributions/render/symbol-render.ts +++ b/packages/vrender-core/src/render/contributions/render/symbol-render.ts @@ -15,7 +15,8 @@ import type { IGraphicRender, IGraphicRenderDrawParams, IContributionProvider, - ICustomPath2D + ICustomPath2D, + ISymbolGraphicAttribute } from '../../../interface'; import type {} from '../../render-service'; import { BaseRender } from './base-render'; @@ -180,40 +181,71 @@ export class DefaultCanvasSymbolRender extends BaseRender implements IG // } // svg就不用fill和stroke了 - const _runFill = () => { - if (doFill && !parsedPath.isSvg) { - if (fillCb) { - fillCb(context, symbol.attribute, symbolAttribute); - } else if (fVisible) { - context.setCommonStyle(symbol, symbol.attribute, originX - x, originY - y, symbolAttribute); - context.fill(); - } - } - }; - const _runStroke = () => { - if (doStroke && !parsedPath.isSvg) { - if (strokeCb) { - strokeCb(context, symbol.attribute, symbolAttribute); - } else if (sVisible && clipRange >= 1) { - // 如果clipRange < 1,就需要靠afterRender进行绘制了 - context.setStrokeStyle( - symbol, - symbol.attribute, - (originX - x) / scaleX, - (originY - y) / scaleY, - symbolAttribute - ); - context.stroke(); - } - } - }; + // 内联的函数性能差 + // const _runFill = () => { + // if (doFill && !parsedPath.isSvg) { + // if (fillCb) { + // fillCb(context, symbol.attribute, symbolAttribute); + // } else if (fVisible) { + // context.setCommonStyle(symbol, symbol.attribute, originX - x, originY - y, symbolAttribute); + // context.fill(); + // } + // } + // }; + // const _runStroke = () => { + // if (doStroke && !parsedPath.isSvg) { + // if (strokeCb) { + // strokeCb(context, symbol.attribute, symbolAttribute); + // } else if (sVisible && clipRange >= 1) { + // // 如果clipRange < 1,就需要靠afterRender进行绘制了 + // context.setStrokeStyle( + // symbol, + // symbol.attribute, + // (originX - x) / scaleX, + // (originY - y) / scaleY, + // symbolAttribute + // ); + // context.stroke(); + // } + // } + // }; if (!fillStrokeOrder) { - _runFill(); - _runStroke(); + this._runFill(symbol, context, x, y, symbolAttribute, doFill, fVisible, originX, originY, parsedPath, fillCb); + this._runStroke( + symbol, + context, + x, + y, + symbolAttribute, + doStroke, + sVisible, + originX, + originY, + parsedPath, + clipRange, + scaleX, + scaleY, + strokeCb + ); } else { - _runStroke(); - _runFill(); + this._runStroke( + symbol, + context, + x, + y, + symbolAttribute, + doStroke, + sVisible, + originX, + originY, + parsedPath, + clipRange, + scaleX, + scaleY, + strokeCb + ); + this._runFill(symbol, context, x, y, symbolAttribute, doFill, fVisible, originX, originY, parsedPath, fillCb); } this.afterRenderStep( @@ -232,6 +264,70 @@ export class DefaultCanvasSymbolRender extends BaseRender implements IG ); } + private _runFill( + symbol: ISymbol, + context: IContext2d, + x: number, + y: number, + symbolAttribute: Required, + doFill: boolean, + fVisible: boolean, + originX: number, + originY: number, + parsedPath: any, + fillCb?: ( + ctx: IContext2d, + markAttribute: Partial, + themeAttribute: IThemeAttribute + ) => boolean + ) { + if (doFill && !parsedPath.isSvg) { + if (fillCb) { + fillCb(context, symbol.attribute, symbolAttribute); + } else if (fVisible) { + context.setCommonStyle(symbol, symbol.attribute, originX - x, originY - y, symbolAttribute); + context.fill(); + } + } + } + + private _runStroke( + symbol: ISymbol, + context: IContext2d, + x: number, + y: number, + symbolAttribute: Required, + doStroke: boolean, + sVisible: boolean, + originX: number, + originY: number, + parsedPath: any, + clipRange: number, + scaleX: number, + scaleY: number, + strokeCb?: ( + ctx: IContext2d, + markAttribute: Partial, + themeAttribute: IThemeAttribute + ) => boolean + ) { + if (doStroke && !parsedPath.isSvg) { + if (strokeCb) { + strokeCb(context, symbol.attribute, symbolAttribute); + } else if (sVisible && clipRange >= 1) { + // 如果clipRange < 1,就需要靠afterRender进行绘制了 + context.setStrokeStyle( + symbol, + symbol.attribute, + (originX - x) / scaleX, + (originY - y) / scaleY, + symbolAttribute + ); + context.stroke(); + } + } + } + draw(symbol: ISymbol, renderService: IRenderService, drawContext: IDrawContext, params?: IGraphicRenderDrawParams) { const symbolAttribute = getTheme(symbol, params?.theme).symbol; this._draw(symbol, symbolAttribute, false, drawContext, params); diff --git a/packages/vrender-kits/src/canvas/contributions/browser/context.ts b/packages/vrender-kits/src/canvas/contributions/browser/context.ts index 62faa76a0..931481c9e 100644 --- a/packages/vrender-kits/src/canvas/contributions/browser/context.ts +++ b/packages/vrender-kits/src/canvas/contributions/browser/context.ts @@ -1059,17 +1059,19 @@ export class BrowserContext2d implements IContext2d { const { opacity = defaultParams.opacity, shadowBlur = defaultParams.shadowBlur, - shadowColor = defaultParams.shadowColor, - shadowOffsetX = defaultParams.shadowOffsetX, - shadowOffsetY = defaultParams.shadowOffsetY, blur = defaultParams.blur, - filter = defaultParams.filter, globalCompositeOperation = defaultParams.globalCompositeOperation } = attribute; if (opacity <= 1e-12) { return; } - if (shadowBlur || shadowOffsetX || shadowOffsetY) { + if (shadowBlur) { + const { + shadowColor = defaultParams.shadowColor, + shadowOffsetX = defaultParams.shadowOffsetX, + shadowOffsetY = defaultParams.shadowOffsetY + } = attribute; + // canvas的shadow不支持dpr,这里手动设置 _context.shadowBlur = shadowBlur * this.dpr; _context.shadowColor = shadowColor; @@ -1086,9 +1088,6 @@ export class BrowserContext2d implements IContext2d { if (blur) { _context.filter = `blur(${blur}px)`; this._clearFilterStyle = true; - } else if (filter) { - _context.filter = filter; - this._clearFilterStyle = true; } else if (this._clearFilterStyle) { _context.filter = 'blur(0px)'; this._clearFilterStyle = false; From 261d8d3d8d22cfbfac9d3932c6b989856bf32002 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 27 Mar 2025 21:20:11 +0800 Subject: [PATCH 052/179] feat: remove extra save/restore, optmize setTransform call --- .../render/contributions/render/draw-contribution.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/vrender-core/src/render/contributions/render/draw-contribution.ts b/packages/vrender-core/src/render/contributions/render/draw-contribution.ts index 9344c465d..d6cb36291 100644 --- a/packages/vrender-core/src/render/contributions/render/draw-contribution.ts +++ b/packages/vrender-core/src/render/contributions/render/draw-contribution.ts @@ -132,8 +132,8 @@ export class DefaultDrawContribution implements IDrawContribution { context.inuse = true; context.setClearMatrix(transMatrix.a, transMatrix.b, transMatrix.c, transMatrix.d, transMatrix.e, transMatrix.f); // 初始化context - context.clearMatrix(); - context.setTransformForCurrent(true); + context.clearMatrix(false); + // context.setTransformForCurrent(true); // const drawInArea = // dirtyBounds.width() * context.dpr < context.canvas.width || @@ -160,7 +160,7 @@ export class DefaultDrawContribution implements IDrawContribution { // // 设置translate // context.translate(x, y, true); - context.save(); + // context.save(); renderService.renderTreeRoots .sort((a, b) => { return (a.attribute.zIndex ?? DefaultAttribute.zIndex) - (b.attribute.zIndex ?? DefaultAttribute.zIndex); @@ -172,8 +172,8 @@ export class DefaultDrawContribution implements IDrawContribution { }); // context.restore(); - context.restore(); - context.setClearMatrix(1, 0, 0, 1, 0, 0); + // context.restore(); + // context.setClearMatrix(1, 0, 0, 1, 0, 0); // this.break = false; context.inuse = false; context.draw(); From 16293dc2538e8f208b95cd252148c0f59dd07037 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 27 Mar 2025 21:32:07 +0800 Subject: [PATCH 053/179] fix: fix issue with rect/arc/symbol pick issue --- .../src/render/contributions/render/arc-render.ts | 2 +- .../src/render/contributions/render/rect-render.ts | 1 + .../src/render/contributions/render/symbol-render.ts | 5 +++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/vrender-core/src/render/contributions/render/arc-render.ts b/packages/vrender-core/src/render/contributions/render/arc-render.ts index a2005da62..6e4ccff6c 100644 --- a/packages/vrender-core/src/render/contributions/render/arc-render.ts +++ b/packages/vrender-core/src/render/contributions/render/arc-render.ts @@ -213,7 +213,7 @@ export class DefaultCanvasArcRender extends BaseRender implements IGraphic arcAttribute?: Required ) { // const arcAttribute = graphicService.themeService.getCurrentTheme().arcAttribute; - // const arcAttribute = getTheme(arc, params?.theme).arc; + arcAttribute = arcAttribute ?? getTheme(arc, params?.theme).arc; const { fill = arcAttribute.fill, stroke = arcAttribute.stroke, diff --git a/packages/vrender-core/src/render/contributions/render/rect-render.ts b/packages/vrender-core/src/render/contributions/render/rect-render.ts index a627e5eaa..966bf8b8f 100644 --- a/packages/vrender-core/src/render/contributions/render/rect-render.ts +++ b/packages/vrender-core/src/render/contributions/render/rect-render.ts @@ -67,6 +67,7 @@ export class DefaultCanvasRectRender extends BaseRender implements IGraph ) => boolean, rectAttribute?: Required ) { + rectAttribute = rectAttribute ?? getTheme(rect, params?.theme).rect; const { fill = rectAttribute.fill, background, diff --git a/packages/vrender-core/src/render/contributions/render/symbol-render.ts b/packages/vrender-core/src/render/contributions/render/symbol-render.ts index 601dfd11a..54430fd94 100644 --- a/packages/vrender-core/src/render/contributions/render/symbol-render.ts +++ b/packages/vrender-core/src/render/contributions/render/symbol-render.ts @@ -65,10 +65,11 @@ export class DefaultCanvasSymbolRender extends BaseRender implements IG ctx: IContext2d, markAttribute: Partial, themeAttribute: IThemeAttribute - ) => boolean + ) => boolean, + symbolAttribute?: Required ) { // const symbolAttribute = graphicService.themeService.getCurrentTheme().symbolAttribute; - const symbolAttribute = getTheme(symbol, params?.theme).symbol; + symbolAttribute = symbolAttribute ?? getTheme(symbol, params?.theme).symbol; const { size = symbolAttribute.size, From e13ebb358618c0c6e718266bc21cfde47ef0c6c4 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 27 Mar 2025 21:57:38 +0800 Subject: [PATCH 054/179] feat: optmize performance for scroll check --- packages/vrender-core/src/interface/render.ts | 2 ++ .../contributions/render/draw-contribution.ts | 26 +++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/vrender-core/src/interface/render.ts b/packages/vrender-core/src/interface/render.ts index 8a2da061a..3a5e3b848 100644 --- a/packages/vrender-core/src/interface/render.ts +++ b/packages/vrender-core/src/interface/render.ts @@ -53,6 +53,8 @@ export interface IDrawContext extends IRenderServiceDrawParams { drawContribution?: IDrawContribution; // hack内容 hack_pieFace?: 'inside' | 'bottom' | 'top' | 'outside'; + // group是否有旋转,每一个renderGroup都会更新,用于在renderItem的时候给子节点使用 + isGroupScroll?: boolean; } export interface IDrawContribution { diff --git a/packages/vrender-core/src/render/contributions/render/draw-contribution.ts b/packages/vrender-core/src/render/contributions/render/draw-contribution.ts index d6cb36291..d966fd686 100644 --- a/packages/vrender-core/src/render/contributions/render/draw-contribution.ts +++ b/packages/vrender-core/src/render/contributions/render/draw-contribution.ts @@ -228,6 +228,8 @@ export class DefaultDrawContribution implements IDrawContribution { this.dirtyBounds.copy(this.backupDirtyBounds).transformWithMatrix(nextM.getInverse()); } + drawContext.isGroupScroll = !!(group.attribute.scrollX || group.attribute.scrollY); + this.renderItem(group, drawContext, { drawingCb: () => { skipSort @@ -356,19 +358,27 @@ export class DefaultDrawContribution implements IDrawContribution { return; } - let retrans: boolean = this.scrollMatrix && (this.scrollMatrix.e !== 0 || this.scrollMatrix.f !== 0); + let retrans: boolean = false; let tempBounds: IBounds; - if (graphic.parent) { + if (drawContext.isGroupScroll) { const { scrollX = 0, scrollY = 0 } = graphic.parent.attribute; - if (!!(scrollX || scrollY)) { - retrans = true; - if (!this.scrollMatrix) { - this.scrollMatrix = matrixAllocate.allocate(1, 0, 0, 1, 0, 0); - } - this.scrollMatrix.translate(-scrollX, -scrollY); + retrans = true; + if (!this.scrollMatrix) { + this.scrollMatrix = matrixAllocate.allocate(1, 0, 0, 1, 0, 0); } + this.scrollMatrix.translate(-scrollX, -scrollY); } + // if (graphic.parent) { + // const { scrollX = 0, scrollY = 0 } = graphic.parent.attribute; + // if (!!(scrollX || scrollY)) { + // retrans = true; + // if (!this.scrollMatrix) { + // this.scrollMatrix = matrixAllocate.allocate(1, 0, 0, 1, 0, 0); + // } + // this.scrollMatrix.translate(-scrollX, -scrollY); + // } + // } // 需要二次变化,那就重新算一个变换后的Bounds if (retrans) { tempBounds = this.dirtyBounds.clone().transformWithMatrix(this.scrollMatrix); From a8d604577b9f4c8f3a6b3ff66620d4e22afb28f1 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 27 Mar 2025 22:15:34 +0800 Subject: [PATCH 055/179] feat: optmize the performance for group.drawingCb --- packages/vrender-core/src/interface/render.ts | 8 +++ .../contributions/render/draw-contribution.ts | 64 ++++++++++--------- .../contributions/render/group-render.ts | 4 +- 3 files changed, 45 insertions(+), 31 deletions(-) diff --git a/packages/vrender-core/src/interface/render.ts b/packages/vrender-core/src/interface/render.ts index 3a5e3b848..c3451700f 100644 --- a/packages/vrender-core/src/interface/render.ts +++ b/packages/vrender-core/src/interface/render.ts @@ -80,6 +80,14 @@ export interface IGraphicRenderDrawParams { drawingCb?: () => void; skipDraw?: boolean; theme?: IFullThemeSpec; + // TODO 这里是为了性能优化,之前使用匿名函数的方式闭包等逻辑会影响性能,现在直接将函数显示定义,将参数传入提升性能,就是牺牲了代码可读性 + // 用于在group中进行递归渲染的参数 + renderInGroupParams?: { + skipSort?: boolean; + nextM?: IMatrixLike; + }; + // 用于在group中进行递归渲染的函数 + renderInGroup?: (skipSort: boolean, group: IGroup, drawContext: IDrawContext, nextM: IMatrixLike) => void; } export interface IGraphicRender { diff --git a/packages/vrender-core/src/render/contributions/render/draw-contribution.ts b/packages/vrender-core/src/render/contributions/render/draw-contribution.ts index d966fd686..4d4df2b45 100644 --- a/packages/vrender-core/src/render/contributions/render/draw-contribution.ts +++ b/packages/vrender-core/src/render/contributions/render/draw-contribution.ts @@ -231,35 +231,11 @@ export class DefaultDrawContribution implements IDrawContribution { drawContext.isGroupScroll = !!(group.attribute.scrollX || group.attribute.scrollY); this.renderItem(group, drawContext, { - drawingCb: () => { - skipSort - ? group.forEachChildren((item: IGraphic) => { - if (drawContext.break) { - return; - } - if (item.isContainer) { - this.renderGroup(item as IGroup, drawContext, nextM); - } else { - this.renderItem(item, drawContext); - } - }) - : foreach( - group, - DefaultAttribute.zIndex, - (item: IGraphic) => { - if (drawContext.break) { - return; - } - if (item.isContainer) { - this.renderGroup(item as IGroup, drawContext, nextM); - } else { - this.renderItem(item, drawContext); - } - }, - false, - !!drawContext.context?.camera - ); - } + renderInGroupParams: { + skipSort, + nextM + }, + renderInGroup: this._renderInGroup }); if (this.useDirtyBounds) { @@ -269,6 +245,36 @@ export class DefaultDrawContribution implements IDrawContribution { } } + _renderInGroup = (skipSort: boolean, group: IGroup, drawContext: IDrawContext, nextM: IMatrix) => { + skipSort + ? group.forEachChildren((item: IGraphic) => { + if (drawContext.break) { + return; + } + if (item.isContainer) { + this.renderGroup(item as IGroup, drawContext, nextM); + } else { + this.renderItem(item, drawContext); + } + }) + : foreach( + group, + DefaultAttribute.zIndex, + (item: IGraphic) => { + if (drawContext.break) { + return; + } + if (item.isContainer) { + this.renderGroup(item as IGroup, drawContext, nextM); + } else { + this.renderItem(item, drawContext); + } + }, + false, + !!drawContext.context?.camera + ); + }; + protected _increaseRender(group: IGroup, drawContext: IDrawContext) { const { layer, stage } = drawContext; const { subLayers } = layer; diff --git a/packages/vrender-core/src/render/contributions/render/group-render.ts b/packages/vrender-core/src/render/contributions/render/group-render.ts index 52aeb6328..35925813c 100644 --- a/packages/vrender-core/src/render/contributions/render/group-render.ts +++ b/packages/vrender-core/src/render/contributions/render/group-render.ts @@ -342,8 +342,8 @@ export class DefaultCanvasGroupRender implements IGraphicRender { context.translate(scrollX, scrollY); } let p: any; - if (params && params.drawingCb) { - p = params.drawingCb(); + if (params && params.renderInGroup) { + p = params.renderInGroup(params.skipDraw, group, drawContext, params.renderInGroupParams?.nextM); } if (context.modelMatrix !== lastModelMatrix) { From 1000f0cdb3ff9264de7233a547341e1952d9e56c Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 28 Mar 2025 15:25:40 +0800 Subject: [PATCH 056/179] feat: ticker are randomly and evenly distributed in the timeline --- .../src/ticker/default-ticker.ts | 80 +++++++++++-------- .../vrender-core/src/canvas/empty-context.ts | 6 ++ packages/vrender-core/src/core/stage.ts | 5 +- .../vrender-core/src/interface/context.ts | 2 + packages/vrender-core/src/interface/stage.ts | 6 +- .../contributions/render/draw-contribution.ts | 7 +- .../contributions/render/group-render.ts | 24 +++--- .../canvas/contributions/browser/context.ts | 4 +- 8 files changed, 81 insertions(+), 53 deletions(-) diff --git a/packages/vrender-animate/src/ticker/default-ticker.ts b/packages/vrender-animate/src/ticker/default-ticker.ts index db38707cf..61330f76c 100644 --- a/packages/vrender-animate/src/ticker/default-ticker.ts +++ b/packages/vrender-animate/src/ticker/default-ticker.ts @@ -9,12 +9,12 @@ const performanceRAF = new PerformanceRAF(); class RAFTickHandler implements ITickHandler { protected released: boolean = false; - tick(interval: number, cb: (handler: ITickHandler) => void): void { + tick(interval: number, cb: (handler: ITickHandler) => void | boolean): void { performanceRAF.addAnimationFrameCb(() => { if (this.released) { return; } - cb(this); + return cb(this); }); } @@ -32,24 +32,37 @@ class RAFTickHandler implements ITickHandler { * This ticker works directly with GraphManager instances without needing timeline adapters */ export class DefaultTicker extends EventEmitter implements ITicker { - protected interval: number = 16; + protected interval: number; protected tickerHandler: ITickHandler; protected status: STATUS; - protected lastFrameTime: number = -1; - protected lastExecutionTime: number = -1; // Track the last time we actually executed a frame - protected tickCounts: number = 0; + protected lastFrameTime: number; + protected tickCounts: number; protected stage: IStage; timelines: ITimeline[] = []; - autoStop: boolean = true; + autoStop: boolean; + // 随机扰动(每次都对interval进行随机的扰动,避免所有tick都发生在同一帧) + protected _jitter: number; + protected timeOffset: number; + declare _lastTickTime: number; + protected frameTimeHistory: number[] = []; constructor(stage: IStage) { super(); this.init(); this.lastFrameTime = -1; - this.lastExecutionTime = -1; this.tickCounts = 0; this.stage = stage; this.autoStop = true; + this.interval = 16; + this.computeTimeOffsetAndJitter(); + } + + /** + * 计算时间偏移和随机扰动 + */ + computeTimeOffsetAndJitter(): void { + this.timeOffset = Math.floor(Math.random() * this.interval); + this._jitter = Math.min(Math.max(this.interval * 0.2, 6), this.interval * 0.7); } init(): void { @@ -97,6 +110,7 @@ export class DefaultTicker extends EventEmitter implements ITicker { setInterval(interval: number): void { this.interval = interval; + this.computeTimeOffsetAndJitter(); } getInterval(): number { @@ -104,7 +118,7 @@ export class DefaultTicker extends EventEmitter implements ITicker { } setFPS(fps: number): void { - this.setInterval(1000 / fps); + this.setInterval(Math.floor(1000 / fps)); } getFPS(): number { @@ -113,7 +127,7 @@ export class DefaultTicker extends EventEmitter implements ITicker { tick(interval: number): void { this.tickerHandler.tick(interval, (handler: ITickHandler) => { - this.handleTick(handler, { once: true }); + return this.handleTick(handler, { once: true }); }); } @@ -182,7 +196,6 @@ export class DefaultTicker extends EventEmitter implements ITicker { this.status = STATUS.INITIAL; this.setupTickHandler(); this.lastFrameTime = -1; - this.lastExecutionTime = -1; } /** @@ -202,46 +215,49 @@ export class DefaultTicker extends EventEmitter implements ITicker { this.timelines = []; this.tickerHandler?.release(); this.tickerHandler = null; - this.lastExecutionTime = -1; + this.lastFrameTime = -1; } - protected handleTick = (handler: ITickHandler, params?: { once?: boolean }): void => { + protected checkSkip = (delta: number): boolean => { + // 随机扰动(每次都对interval进行随机的扰动,避免所有tick都发生在同一帧) + return delta < this.interval + (Math.random() - 0.5) * 2 * this._jitter; + }; + + protected handleTick = (handler: ITickHandler, params?: { once?: boolean }): boolean => { const { once = false } = params ?? {}; // 尝试停止 if (this.ifCanStop()) { this.stop(); - return; + return false; } const currentTime = handler.getTime(); + this._lastTickTime = currentTime; + + if (this.lastFrameTime < 0) { + this.lastFrameTime = currentTime - this.interval + this.timeOffset; + this.frameTimeHistory.push(this.lastFrameTime); + } + + const delta = currentTime - this.lastFrameTime; - // Check if enough time has passed since last execution based on the interval (FPS limit) - const timeFromLastExecution = this.lastExecutionTime < 0 ? this.interval : currentTime - this.lastExecutionTime; + const skip = this.checkSkip(delta); - // Only execute the frame if enough time has passed according to our interval/FPS setting - if (timeFromLastExecution >= this.interval) { - this._handlerTick(); - this.lastExecutionTime = currentTime; + if (!skip) { + this._handlerTick(delta); + this.lastFrameTime = currentTime; + this.frameTimeHistory.push(this.lastFrameTime); } if (!once) { handler.tick(this.interval, this.handleTick); } - }; - - protected _handlerTick = (): void => { - // Specific execution function - const tickerHandler = this.tickerHandler; - const time = tickerHandler.getTime(); - // Time passed since last frame - let delta = 0; - if (this.lastFrameTime >= 0) { - delta = time - this.lastFrameTime; - } - this.lastFrameTime = time; + return !skip; + }; + protected _handlerTick = (delta: number): void => { if (this.status !== STATUS.RUNNING) { return; } diff --git a/packages/vrender-core/src/canvas/empty-context.ts b/packages/vrender-core/src/canvas/empty-context.ts index c64eee7bd..a143b94bd 100644 --- a/packages/vrender-core/src/canvas/empty-context.ts +++ b/packages/vrender-core/src/canvas/empty-context.ts @@ -126,6 +126,12 @@ export class EmptyContext2d implements IContext2d { this.restore(); } + reset() { + this.matrix.setValue(1, 0, 0, 1, 0, 0); + this.applyedMatrix = new Matrix(1, 0, 0, 1, 0, 0); + this.stack.length = 0; + } + restore() { if (this.stack.length > 0) { matrixAllocate.free(this.matrix); diff --git a/packages/vrender-core/src/core/stage.ts b/packages/vrender-core/src/core/stage.ts index 3b83ed7e6..39579d582 100644 --- a/packages/vrender-core/src/core/stage.ts +++ b/packages/vrender-core/src/core/stage.ts @@ -299,12 +299,15 @@ export class Stage extends Group implements IStage { } this.initAnimate(params); - this.rafId = Math.floor(Math.random() * 3); + this.rafId = params.rafId ?? Math.floor(Math.random() * 6); } initAnimate(params: Partial) { if ((this as any).createTicker && (this as any).createTimeline) { this.ticker = params.ticker || (this as any).createTicker(this); + if (this.params.optimize?.tickRenderMode !== 'effect') { + this.ticker.setFPS(30); + } this.timeline = (this as any).createTimeline(); this.ticker.addTimeline(this.timeline); this.ticker.on('tick', this.afterTickCb); diff --git a/packages/vrender-core/src/interface/context.ts b/packages/vrender-core/src/interface/context.ts index dc51a3736..06a92d7ca 100644 --- a/packages/vrender-core/src/interface/context.ts +++ b/packages/vrender-core/src/interface/context.ts @@ -100,6 +100,8 @@ export interface IContext2d extends Releaseable { getContext: () => any; + reset: (setTransform?: boolean) => void; + /** * 设置当前ctx 的transform信息 */ diff --git a/packages/vrender-core/src/interface/stage.ts b/packages/vrender-core/src/interface/stage.ts index 110b333f2..ea99f3e90 100644 --- a/packages/vrender-core/src/interface/stage.ts +++ b/packages/vrender-core/src/interface/stage.ts @@ -85,6 +85,9 @@ export interface IStageParams { supportsPointerEvents?: boolean; context?: IStageCreateContext; + + // 被分配的rafId,用于renderNextFrame,避免使用大量原生的RAF + rafId?: number; } export type EventConfig = { @@ -106,11 +109,10 @@ export type IOptimizeType = { // 如果有dirtyBounds那么该配置不生效 disableCheckGraphicWidthOutRange?: boolean; // tick渲染模式,effect会在tick之后立刻执行render,保证动画效果正常。performance模式中tick和render均是RAF,属性可能会被篡改 - tickRenderMode?: 'effect' | 'performance'; // 是否开启高性能动画,默认开启 // 开启后不会执行某些安全校验,比如跳帧处理 // 开启后会自动降帧,最高60fps - animateMode?: 'effect' | 'performance'; + tickRenderMode?: 'effect' | 'performance'; }; export interface IOption3D { diff --git a/packages/vrender-core/src/render/contributions/render/draw-contribution.ts b/packages/vrender-core/src/render/contributions/render/draw-contribution.ts index 4d4df2b45..871436b9d 100644 --- a/packages/vrender-core/src/render/contributions/render/draw-contribution.ts +++ b/packages/vrender-core/src/render/contributions/render/draw-contribution.ts @@ -129,7 +129,9 @@ export class DefaultDrawContribution implements IDrawContribution { dirtyBounds.y2 = Math.ceil(dirtyBounds.y2 * context.dpr) / context.dpr; } this.backupDirtyBounds.copy(dirtyBounds); - context.inuse = true; + // TODO:不需要设置context.transform,后续translate会设置 + context.save(); + context.reset(false); context.setClearMatrix(transMatrix.a, transMatrix.b, transMatrix.c, transMatrix.d, transMatrix.e, transMatrix.f); // 初始化context context.clearMatrix(false); @@ -175,7 +177,8 @@ export class DefaultDrawContribution implements IDrawContribution { // context.restore(); // context.setClearMatrix(1, 0, 0, 1, 0, 0); // this.break = false; - context.inuse = false; + // context.inuse = false; + context.restore(); context.draw(); } diff --git a/packages/vrender-core/src/render/contributions/render/group-render.ts b/packages/vrender-core/src/render/contributions/render/group-render.ts index 35925813c..b914a6eb4 100644 --- a/packages/vrender-core/src/render/contributions/render/group-render.ts +++ b/packages/vrender-core/src/render/contributions/render/group-render.ts @@ -61,17 +61,14 @@ export class DefaultCanvasGroupRender implements IGraphicRender { groupAttribute?: Required ) { // 提前判定,否则每次都要获取一堆属性 - const { - clip = groupAttribute.clip, - fill = groupAttribute.fill, - stroke = groupAttribute.stroke, - background - } = group.attribute; + const { clip, fill, stroke, background } = group.attribute; if (!(clip || fill || stroke || background)) { return; } + groupAttribute = groupAttribute ?? getTheme(group, params?.theme).group; + const { opacity = groupAttribute.opacity, width = groupAttribute.width, @@ -230,12 +227,12 @@ export class DefaultCanvasGroupRender implements IGraphicRender { return; } - // debugger; - const { clip, baseOpacity = 1, drawMode, x, y, width, height } = group.attribute; + const { clip, baseOpacity = 1, drawMode } = group.attribute; const lastNativeContext = context.nativeContext; const lastNativeCanvas = context.canvas.nativeCanvas; if (drawMode > 0) { + const { x, y, width, height } = group.attribute; // 绘制到新的Canvas上,然后再绘制回来 const canvas = context.canvas; const newCanvas = vglobal.createCanvas({ width: canvas.width, height: canvas.height, dpr: 1 }); @@ -278,8 +275,6 @@ export class DefaultCanvasGroupRender implements IGraphicRender { const baseGlobalAlpha = context.baseGlobalAlpha; context.baseGlobalAlpha *= baseOpacity; - const groupAttribute = getTheme(group, params?.theme).group; - // const lastMatrix = context.modelMatrix; // if (context.camera) { // const m = group.transMatrix; @@ -301,6 +296,7 @@ export class DefaultCanvasGroupRender implements IGraphicRender { const lastModelMatrix = context.modelMatrix; const camera = context.camera; if (camera) { + const groupAttribute = getTheme(group, params?.theme).group; const nextModelMatrix = mat4Allocate.allocate(); // 计算模型矩阵 const modelMatrix = mat4Allocate.allocate(); @@ -329,15 +325,14 @@ export class DefaultCanvasGroupRender implements IGraphicRender { drawContext, params, () => false, - () => false, - groupAttribute + () => false ); } else { - this.drawShape(group, context, 0, 0, drawContext, null, null, null, groupAttribute); + this.drawShape(group, context, 0, 0, drawContext, null, null, null); } // 绘制子元素的时候要添加scroll - const { scrollX = groupAttribute.scrollX, scrollY = groupAttribute.scrollY } = group.attribute; + const { scrollX, scrollY } = group.attribute; if (scrollX || scrollY) { context.translate(scrollX, scrollY); } @@ -354,6 +349,7 @@ export class DefaultCanvasGroupRender implements IGraphicRender { context.baseGlobalAlpha = baseGlobalAlpha; if (drawMode > 0) { + const { x, y, width, height } = group.attribute; // 将原始的context和canvas恢复,另外将newCanvas上的内容绘制到lastCanvas上 const newContext = context.nativeContext; const newCanvas = context.canvas.nativeCanvas; diff --git a/packages/vrender-kits/src/canvas/contributions/browser/context.ts b/packages/vrender-kits/src/canvas/contributions/browser/context.ts index 931481c9e..2218c3049 100644 --- a/packages/vrender-kits/src/canvas/contributions/browser/context.ts +++ b/packages/vrender-kits/src/canvas/contributions/browser/context.ts @@ -257,14 +257,14 @@ export class BrowserContext2d implements IContext2d { this.baseGlobalAlpha = 1; } - reset() { + reset(setTransform: boolean = true) { if (this.stack.length) { Logger.getInstance().warn('可能存在bug,matrix没有清空'); } this.matrix.setValue(1, 0, 0, 1, 0, 0); this.applyedMatrix = new Matrix(1, 0, 0, 1, 0, 0); this.stack.length = 0; - this.nativeContext.setTransform(1, 0, 0, 1, 0, 0); + setTransform && this.nativeContext.setTransform(1, 0, 0, 1, 0, 0); } getCanvas() { From 322cdfb4afbf9b97511d58f04d7938d35d351190 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 28 Mar 2025 17:47:40 +0800 Subject: [PATCH 057/179] fix: fix issue with arc label --- packages/vrender-components/src/label/arc.ts | 20 +++++++++---------- packages/vrender-components/src/label/base.ts | 15 +++++++------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/vrender-components/src/label/arc.ts b/packages/vrender-components/src/label/arc.ts index 3d0879474..49b14d068 100644 --- a/packages/vrender-components/src/label/arc.ts +++ b/packages/vrender-components/src/label/arc.ts @@ -276,7 +276,7 @@ export class ArcLabel extends LabelBase { data.forEach((d, index) => { const currentMark = this._idToGraphic.get(d.id); - const graphicAttribute = currentMark.attribute as IArcGraphicAttribute; + const graphicAttribute = currentMark.getAttributes(true) as IArcGraphicAttribute; const center = { x: graphicAttribute?.x ?? 0, y: graphicAttribute?.y ?? 0 }; if (!isNil(data[index]) && !isNil(textBoundsArray[index])) { const item = data[index] ? data[index] : null; @@ -502,8 +502,8 @@ export class ArcLabel extends LabelBase { let maxRadius = 0; currentMarks.forEach((currentMark: IGraphic) => { - if ((currentMark.attribute as IArcGraphicAttribute).outerRadius > maxRadius) { - maxRadius = (currentMark.attribute as IArcGraphicAttribute).outerRadius; + if ((currentMark.getAttributes(true) as IArcGraphicAttribute).outerRadius > maxRadius) { + maxRadius = (currentMark.getAttributes(true) as IArcGraphicAttribute).outerRadius; } }); @@ -767,8 +767,8 @@ export class ArcLabel extends LabelBase { let maxRadius = 0; currentMarks.forEach((currentMark: IGraphic) => { - if ((currentMark.attribute as IArcGraphicAttribute).outerRadius > maxRadius) { - maxRadius = (currentMark.attribute as IArcGraphicAttribute).outerRadius; + if ((currentMark.getAttributes(true) as IArcGraphicAttribute).outerRadius > maxRadius) { + maxRadius = (currentMark.getAttributes(true) as IArcGraphicAttribute).outerRadius; } }); @@ -821,8 +821,8 @@ export class ArcLabel extends LabelBase { let maxRadius = 0; currentMarks.forEach((currentMark: IGraphic) => { - if ((currentMark.attribute as IArcGraphicAttribute).outerRadius > maxRadius) { - maxRadius = (currentMark.attribute as IArcGraphicAttribute).outerRadius; + if ((currentMark.getAttributes(true) as IArcGraphicAttribute).outerRadius > maxRadius) { + maxRadius = (currentMark.getAttributes(true) as IArcGraphicAttribute).outerRadius; } }); @@ -902,8 +902,8 @@ export class ArcLabel extends LabelBase { let maxRadius = 0; currentMarks.forEach((currentMark: IGraphic) => { - if ((currentMark.attribute as IArcGraphicAttribute).outerRadius > maxRadius) { - maxRadius = (currentMark.attribute as IArcGraphicAttribute).outerRadius; + if ((currentMark.getAttributes(true) as IArcGraphicAttribute).outerRadius > maxRadius) { + maxRadius = (currentMark.getAttributes(true) as IArcGraphicAttribute).outerRadius; } }); @@ -1017,7 +1017,7 @@ export class ArcLabel extends LabelBase { } if (baseMark.type === 'arc3d' && baseMark) { - const { beta, x, y } = baseMark.attribute; + const { beta, x, y } = baseMark.getAttributes(true); lineGraphic.setAttributes({ beta, anchor3d: [x, y] diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index 3352e17ad..51d4c2cd3 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -159,8 +159,8 @@ export class LabelBase extends AbstractComponent { }; } - if (baseMark && baseMark.attribute.fill) { - lineGraphic.setAttribute('stroke', baseMark.attribute.fill); + if (baseMark && baseMark.getAttributes(true).fill) { + lineGraphic.setAttribute('stroke', baseMark.getAttributes(true).fill); } if (this.attribute.line && !isEmpty(this.attribute.line.style)) { @@ -453,12 +453,13 @@ export class LabelBase extends AbstractComponent { continue; } + const graphicAttribute = baseMark.getAttributes(true); const labelAttribute = { fill: this._isCollectionBase - ? isArray(baseMark.attribute.stroke) - ? baseMark.attribute.stroke.find(entry => !!entry && entry !== true) - : baseMark.attribute.stroke - : baseMark.attribute.fill, + ? isArray(graphicAttribute.stroke) + ? graphicAttribute.stroke.find(entry => !!entry && entry !== true) + : graphicAttribute.stroke + : graphicAttribute.fill, ...textStyle, ...textData }; @@ -1048,7 +1049,7 @@ export class LabelBase extends AbstractComponent { * similarBase(智能反色的补色), * null(不执行智能反色,保持fill设置的颜色) * */ - let backgroundColor = baseMark.attribute.fill as IColor; + let backgroundColor = baseMark.getAttributes(true).fill as IColor; let foregroundColor = label.attribute.fill as IColor; if (isObject(backgroundColor) && backgroundColor.gradient) { From 3ae73bc0afe99dc3362e75ad46b436d7a1072cb4 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 28 Mar 2025 18:47:18 +0800 Subject: [PATCH 058/179] feat: support setAttributesAndPreventAnimate --- packages/vrender-animate/src/animate-extension.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/vrender-animate/src/animate-extension.ts b/packages/vrender-animate/src/animate-extension.ts index e9f285f1b..5517be8bb 100644 --- a/packages/vrender-animate/src/animate-extension.ts +++ b/packages/vrender-animate/src/animate-extension.ts @@ -17,6 +17,16 @@ export class AnimateExtension { declare animates: Map; + setAttributesAndPreventAnimate(attributes: Record) { + (this as any).setAttributes(attributes); + this.animates && + this.animates.forEach(animate => { + Object.keys(attributes).forEach(key => { + animate.preventAttr(key); + }); + }); + } + getAttributes(final: boolean = false) { if (final && this.finalAttribute) { return this.finalAttribute; From 9a32825dda4aece56c5d634b01c0d73c7c52f2af Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Mon, 31 Mar 2025 19:57:57 +0800 Subject: [PATCH 059/179] fix: fix issue with canvas font set --- .../src/executor/animate-executor.ts | 4 ++-- packages/vrender-animate/src/register.ts | 16 ---------------- .../src/canvas/contributions/browser/context.ts | 3 +++ 3 files changed, 5 insertions(+), 18 deletions(-) diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index e1857fb75..003f1d0b0 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -1,4 +1,4 @@ -import type { IGraphic, IGroup } from '@visactor/vrender-core'; +import type { IGraphic } from '@visactor/vrender-core'; import type { IAnimationConfig, IAnimationTimeline, @@ -13,7 +13,7 @@ import type { } from './executor'; import type { EasingType } from '../intreface/easing'; import type { IAnimate } from '../intreface/animate'; -import { cloneDeep, isArray, isFunction, scale } from '@visactor/vutils'; +import { cloneDeep, isArray, isFunction } from '@visactor/vutils'; interface IAnimateExecutor { execute: (params: IAnimationConfig) => void; diff --git a/packages/vrender-animate/src/register.ts b/packages/vrender-animate/src/register.ts index 640c2b193..fc7266b38 100644 --- a/packages/vrender-animate/src/register.ts +++ b/packages/vrender-animate/src/register.ts @@ -1,25 +1,9 @@ import { Graphic } from '@visactor/vrender-core'; -import { Animate } from './animate'; -import { defaultTimeline, DefaultTimeline } from './timeline'; -import { DefaultTicker } from './ticker/default-ticker'; import { mixin } from '@visactor/vutils'; import { GraphicStateExtension } from './state/graphic-extension'; import { AnimateExtension } from './animate-extension'; export function registerAnimate() { - if (!(Graphic as any).Animate) { - (Graphic as any).Animate = Animate; - } - if (!(Graphic as any).Timeline) { - (Graphic as any).Timeline = DefaultTimeline; - } - if (!(Graphic as any).defaultTimeline) { - (Graphic as any).defaultTimeline = defaultTimeline; - } - if (!(Graphic as any).Ticker) { - (Graphic as any).Ticker = DefaultTicker; - } - // Mix in animation state methods to Graphic prototype mixin(Graphic, GraphicStateExtension); mixin(Graphic, AnimateExtension); diff --git a/packages/vrender-kits/src/canvas/contributions/browser/context.ts b/packages/vrender-kits/src/canvas/contributions/browser/context.ts index 2218c3049..9a77d233a 100644 --- a/packages/vrender-kits/src/canvas/contributions/browser/context.ts +++ b/packages/vrender-kits/src/canvas/contributions/browser/context.ts @@ -331,6 +331,9 @@ export class BrowserContext2d implements IContext2d { this.matrix = this.stack.pop() as Matrix; this.setTransformForCurrent(true); } + this.font = ''; + this._clearFilterStyle = false; + this._clearShadowStyle = false; } highPerformanceRestore() { if (this.stack.length > 0) { From 3c924070a92c7b4491a852e8a2a91981c68e6940 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 1 Apr 2025 20:04:35 +0800 Subject: [PATCH 060/179] feat: support component animator --- .../vrender-animate/src/animate-extension.ts | 33 +++ packages/vrender-animate/src/animate.ts | 6 + .../component/component-animate-extension.ts | 122 ++++++++++ .../src/component/component-animator.ts | 212 ++++++++++++++++ .../vrender-animate/src/component/index.ts | 2 + .../src/custom/custom-animate.ts | 13 + .../src/custom/label-item-animate.ts | 227 ++++++++++++++++++ .../src/custom/poptip-animate.ts | 107 +++++++++ .../vrender-animate/src/custom/register.ts | 8 + .../src/executor/animate-executor.ts | 12 +- .../vrender-animate/src/executor/executor.ts | 6 + packages/vrender-animate/src/index.ts | 4 + .../vrender-animate/src/intreface/animate.ts | 3 + .../src/state/animation-state.ts | 72 ++++++ .../src/state/graphic-extension.ts | 51 ++++ packages/vrender-animate/src/step.ts | 4 + .../src/ticker/default-ticker.ts | 3 +- .../__tests__/browser/examples/poptip.ts | 22 +- .../browser/examples/story-label-item.ts | 28 ++- .../__tests__/browser/vite.config.ts | 3 +- .../src/label-item/label-item.ts | 124 +--------- .../vrender-components/src/poptip/poptip.ts | 48 ---- packages/vrender-core/src/graphic/graphic.ts | 6 +- packages/vrender-core/src/index.ts | 25 -- 24 files changed, 932 insertions(+), 209 deletions(-) create mode 100644 packages/vrender-animate/src/component/component-animate-extension.ts create mode 100644 packages/vrender-animate/src/component/component-animator.ts create mode 100644 packages/vrender-animate/src/component/index.ts create mode 100644 packages/vrender-animate/src/custom/label-item-animate.ts create mode 100644 packages/vrender-animate/src/custom/poptip-animate.ts diff --git a/packages/vrender-animate/src/animate-extension.ts b/packages/vrender-animate/src/animate-extension.ts index 5517be8bb..112f857b8 100644 --- a/packages/vrender-animate/src/animate-extension.ts +++ b/packages/vrender-animate/src/animate-extension.ts @@ -10,10 +10,13 @@ import type { IAnimate } from './intreface/animate'; import { Animate } from './animate'; import { DefaultTimeline, defaultTimeline } from './timeline'; import { DefaultTicker } from './ticker/default-ticker'; +import type { IAnimationConfig } from './executor/executor'; +import { AnimateExecutor } from './executor/animate-executor'; // 基于性能考虑,每次调用animate函数,都会设置animatedAttribute为null,每次getAttributes(true)会根据animatedAttribute属性判断是否需要重新计算animatedAttribute。 export class AnimateExtension { declare finalAttribute: Record; + _animateExecutor: AnimateExecutor | null; declare animates: Map; @@ -82,6 +85,36 @@ export class AnimateExtension { this.finalAttribute = finalAttribute; } + initAnimateExecutor(): void { + if (!this._animateExecutor) { + this._animateExecutor = new AnimateExecutor(this as any); + } + } + + /** + * Apply animation configuration to the component + * @param config Animation configuration + * @returns This component instance + */ + executeAnimation(config: IAnimationConfig): this { + this.initAnimateExecutor(); + this._animateExecutor.execute(config); + return this; + } + + /** + * Apply animations to multiple components + * @param configs Animation configurations + * @returns This component instance + */ + executeAnimations(configs: IAnimationConfig[]): this { + this.initAnimateExecutor(); + configs.forEach(config => { + this._animateExecutor.execute(config); + }); + return this; + } + protected getFinalAttribute() { return this.finalAttribute; } diff --git a/packages/vrender-animate/src/animate.ts b/packages/vrender-animate/src/animate.ts index bd4069755..a9944eebb 100644 --- a/packages/vrender-animate/src/animate.ts +++ b/packages/vrender-animate/src/animate.ts @@ -429,6 +429,12 @@ export class Animate implements IAnimate { // if (this.status === AnimateStatus.END) { // return; // } + // 遍历step,调用其stop + let step = this._firstStep; + while (step) { + step.stop(); + step = step.next; + } this.status = AnimateStatus.END; diff --git a/packages/vrender-animate/src/component/component-animate-extension.ts b/packages/vrender-animate/src/component/component-animate-extension.ts new file mode 100644 index 000000000..abb7b55f9 --- /dev/null +++ b/packages/vrender-animate/src/component/component-animate-extension.ts @@ -0,0 +1,122 @@ +// import type { IGraphic } from '@visactor/vrender-core'; +// import type { IAnimationConfig } from '../executor/executor'; +// import { ComponentAnimator } from './component-animator'; + +// /** +// * Component animation extension that can be mixed in to component classes +// */ +// export class ComponentAnimateExtension { +// private _componentAnimator: ComponentAnimator; + +// /** +// * Get the component animator for this component +// * @returns The ComponentAnimator instance +// */ +// getComponentAnimator(): ComponentAnimator { +// if (!this._componentAnimator) { +// this._componentAnimator = new ComponentAnimator(this as unknown as IGraphic); +// } +// return this._componentAnimator; +// } + +// /** +// * Create a new animation sequence for this component +// * @returns A new ComponentAnimator instance +// */ +// createAnimationSequence(): ComponentAnimator { +// return new ComponentAnimator(this as unknown as IGraphic); +// } + +// /** +// * Create an animation for the component with the given preset +// * @param preset Animation preset name ('appear', 'disappear', etc.) +// * @param options Animation options +// * @returns The ComponentAnimator instance +// */ +// animate(preset: string, options?: Record): ComponentAnimator { +// const animator = this.getComponentAnimator(); + +// // Call the appropriate animation setup method based on preset +// switch (preset) { +// case 'appear': +// this.setupAppearAnimation(animator, options); +// break; +// case 'disappear': +// this.setupDisappearAnimation(animator, options); +// break; +// default: +// throw new Error(`Unknown animation preset: ${preset}`); +// } + +// // Start the animation immediately +// animator.start(); + +// return animator; +// } + +// /** +// * Create an appear animation +// * @param options Animation options +// * @returns The ComponentAnimator instance (not started) +// */ +// createAppearAnimation(options?: Record): ComponentAnimator { +// const animator = this.createAnimationSequence(); +// this.setupAppearAnimation(animator, options); +// return animator; +// } + +// /** +// * Create a disappear animation +// * @param options Animation options +// * @returns The ComponentAnimator instance (not started) +// */ +// createDisappearAnimation(options?: Record): ComponentAnimator { +// const animator = this.createAnimationSequence(); +// this.setupDisappearAnimation(animator, options); +// return animator; +// } + +// /** +// * Execute an animation with the given config directly on the component +// * @param config Animation configuration +// * @returns This component +// */ +// executeAnimation(config: IAnimationConfig): this { +// this.getComponentAnimator() +// .animate(this as unknown as IGraphic, config) +// .start(); +// return this; +// } + +// /** +// * Set up appear animation for this component +// * This is a placeholder method that component classes should override +// * @param animator The ComponentAnimator to set up +// * @param options Animation options +// */ +// protected setupAppearAnimation(animator: ComponentAnimator, options?: Record): void { +// // To be overridden by concrete component classes +// console.warn('setupAppearAnimation not implemented for this component'); +// } + +// /** +// * Set up disappear animation for this component +// * This is a placeholder method that component classes should override +// * @param animator The ComponentAnimator to set up +// * @param options Animation options +// */ +// protected setupDisappearAnimation(animator: ComponentAnimator, options?: Record): void { +// // To be overridden by concrete component classes +// console.warn('setupDisappearAnimation not implemented for this component'); +// } +// } + +// /** +// * Type for components that can be animated +// */ +// export interface IAnimatableComponent { +// animate: (preset: string, options?: Record) => ComponentAnimator; +// createAppearAnimation: (options?: Record) => ComponentAnimator; +// createDisappearAnimation: (options?: Record) => ComponentAnimator; +// executeAnimation: (config: IAnimationConfig) => IAnimatableComponent; +// } diff --git a/packages/vrender-animate/src/component/component-animator.ts b/packages/vrender-animate/src/component/component-animator.ts new file mode 100644 index 000000000..a23fe57fc --- /dev/null +++ b/packages/vrender-animate/src/component/component-animator.ts @@ -0,0 +1,212 @@ +import type { IAnimate } from '../intreface/animate'; +import type { IGraphic } from '@visactor/vrender-core'; +import { AnimateExecutor } from '../executor/animate-executor'; +import type { IAnimationConfig, IAnimationTypeConfig, IAnimationTimeline } from '../executor/executor'; + +/** + * Animation task that contains information about a scheduled animation + */ +interface IAnimationTask { + graphic: IGraphic; + config: IAnimationConfig; + delay: number; + animate?: IAnimate; +} + +/** + * ComponentAnimator provides a way to orchestrate animations across child elements + * with centralized lifecycle management + */ +export class ComponentAnimator { + private component: IGraphic; + private tasks: IAnimationTask[] = []; + private started: boolean = false; + private completed: number = 0; + private totalDuration: number = 0; + private onStartCallbacks: (() => void)[] = []; + private onEndCallbacks: (() => void)[] = []; + private onUpdateCallbacks: ((progress: number) => void)[] = []; + + /** + * Creates a new ComponentAnimator + * @param component The component or group containing elements to animate + */ + constructor(component: IGraphic) { + this.component = component; + } + + /** + * Add animation for a specific graphic element + * @param graphic The graphic element to animate + * @param config Animation configuration + * @param delay Optional delay before starting this animation (in ms) + * @returns This ComponentAnimator for chaining + */ + animate(graphic: IGraphic, config: IAnimationConfig, delay: number = 0): ComponentAnimator { + if (this.started) { + console.warn('Cannot add animations after animation has started'); + return this; + } + + this.tasks.push({ + graphic, + config, + delay + }); + + // Calculate total duration including delay + let configDuration = 300; // Default duration + let configDelay = 0; // Default delay + + // Extract duration and delay based on config type + if ('duration' in config) { + // TypeConfig case + const typeConfig = config as IAnimationTypeConfig; + configDuration = typeof typeConfig.duration === 'number' ? typeConfig.duration : 300; + configDelay = typeof typeConfig.delay === 'number' ? typeConfig.delay : 0; + } else if ('timeSlices' in config) { + // Timeline case - calculate total duration from all time slices + const timelineConfig = config as IAnimationTimeline; + const timeSlices = Array.isArray(timelineConfig.timeSlices) + ? timelineConfig.timeSlices + : [timelineConfig.timeSlices]; + + let totalTimeSliceDuration = 0; + timeSlices.forEach(slice => { + const sliceDuration = typeof slice.duration === 'number' ? slice.duration : 300; + const sliceDelay = typeof slice.delay === 'number' ? slice.delay : 0; + const sliceDelayAfter = typeof slice.delayAfter === 'number' ? slice.delayAfter : 0; + totalTimeSliceDuration += sliceDuration + sliceDelay + sliceDelayAfter; + }); + + configDuration = totalTimeSliceDuration; + } + + const duration = configDuration + configDelay + delay; + + if (duration > this.totalDuration) { + this.totalDuration = duration; + } + + return this; + } + + /** + * Add a callback to be called when animation starts + * @param callback Function to call when animation starts + * @returns This ComponentAnimator for chaining + */ + onStart(callback: () => void): ComponentAnimator { + this.onStartCallbacks.push(callback); + return this; + } + + /** + * Add a callback to be called when animation ends + * @param callback Function to call when animation ends + * @returns This ComponentAnimator for chaining + */ + onEnd(callback: () => void): ComponentAnimator { + this.onEndCallbacks.push(callback); + return this; + } + + /** + * Add a callback to be called when animation updates + * @param callback Function to call when animation updates (receives progress from 0 to 1) + * @returns This ComponentAnimator for chaining + */ + onUpdate(callback: (progress: number) => void): ComponentAnimator { + this.onUpdateCallbacks.push(callback); + return this; + } + + /** + * Start all animations in this component animation + * @returns This ComponentAnimator + */ + start(): ComponentAnimator { + if (this.started) { + console.warn('Animation has already started'); + return this; + } + + this.started = true; + this.completed = 0; + + // Call onStart callbacks + this.onStartCallbacks.forEach(callback => callback()); + + // Empty animation case + if (this.tasks.length === 0) { + setTimeout(() => { + this.onEndCallbacks.forEach(callback => callback()); + }, 0); + return this; + } + + // Start all animations with their specified delays + this.tasks.forEach(task => { + const executor = new AnimateExecutor(task.graphic); + + // Set up callbacks to track completion + executor.onEnd(() => { + this.completed++; + if (this.completed === this.tasks.length) { + this.onEndCallbacks.forEach(callback => callback()); + } + }); + + // Start animation after delay + if (task.delay > 0) { + setTimeout(() => { + const animate = executor.executeItem(task.config, task.graphic); + task.animate = animate; + }, task.delay); + } else { + const animate = executor.executeItem(task.config, task.graphic); + task.animate = animate; + } + }); + + return this; + } + + /** + * Stop all animations in this component animation + * @param jumpToEnd Whether to jump to the end state (true) or start state (false) + * @returns This ComponentAnimator + */ + stop(jumpToEnd: boolean = true): ComponentAnimator { + this.tasks.forEach(task => { + if (task.animate) { + task.animate.stop(jumpToEnd ? 'end' : 'start'); + } + }); + + // If not already completed, call end callbacks + if (this.started && this.completed !== this.tasks.length) { + this.onEndCallbacks.forEach(callback => callback()); + this.completed = this.tasks.length; + } + + return this; + } + + /** + * Get total duration of all animations including delays + * @returns Total duration in milliseconds + */ + getDuration(): number { + return this.totalDuration; + } +} + +/** + * Factory function to create a ComponentAnimator for a component + * @param component The component or group to animate + * @returns A new ComponentAnimator instance + */ +export function createComponentAnimator(component: IGraphic): ComponentAnimator { + return new ComponentAnimator(component); +} diff --git a/packages/vrender-animate/src/component/index.ts b/packages/vrender-animate/src/component/index.ts new file mode 100644 index 000000000..c67017eb1 --- /dev/null +++ b/packages/vrender-animate/src/component/index.ts @@ -0,0 +1,2 @@ +export * from './component-animator'; +// export * from './component-animate-extension'; diff --git a/packages/vrender-animate/src/custom/custom-animate.ts b/packages/vrender-animate/src/custom/custom-animate.ts index 70c854fa4..b8be2ef0e 100644 --- a/packages/vrender-animate/src/custom/custom-animate.ts +++ b/packages/vrender-animate/src/custom/custom-animate.ts @@ -1,3 +1,4 @@ +import type { ComponentAnimator } from '../component'; import type { ICustomAnimate } from '../intreface/animate'; import type { EasingType } from '../intreface/easing'; import type { IAnimateStepType } from '../intreface/type'; @@ -36,3 +37,15 @@ export abstract class ACustomAnimate extends Step implements ICustomAnimate { this.animate.reSyncProps(); } } + +export abstract class AComponentAnimate extends ACustomAnimate { + protected _animator: ComponentAnimator; + + onFirstRun(): void { + this._animator && this._animator.start(); + } + + stop(): void { + this._animator && this._animator.stop(); + } +} diff --git a/packages/vrender-animate/src/custom/label-item-animate.ts b/packages/vrender-animate/src/custom/label-item-animate.ts new file mode 100644 index 000000000..527e6ec07 --- /dev/null +++ b/packages/vrender-animate/src/custom/label-item-animate.ts @@ -0,0 +1,227 @@ +import { AComponentAnimate } from './custom-animate'; +import { createComponentAnimator } from '../component'; +import { InputText } from './input-text'; + +/** + * LabelItemAppear class handles the appear animation for StoryLabelItem components + */ +export class LabelItemAppear extends AComponentAnimate { + onBind(): void { + const animator = createComponentAnimator(this.target); + this._animator = animator; + const duration = this.duration; + const easing = this.easing; + const target = this.target as any; + + const { symbolStartOuterType = 'scale', titleType = 'typewriter', titlePanelType = 'scale' } = this.params; + + const symbolTime = duration / 10; + target._symbolStart.setAttributes({ scaleX: 0, scaleY: 0 }); + + animator.animate(target._symbolStart, { + type: 'to', + to: { scaleX: 1, scaleY: 1 }, + duration: symbolTime * 5, + easing + }); + + let symbolStartOuterFrom: any; + let symbolStartOuterTo: any; + if (symbolStartOuterType === 'scale') { + symbolStartOuterFrom = { scaleX: 0, scaleY: 0 }; + symbolStartOuterTo = { scaleX: 1, scaleY: 1 }; + } else { + symbolStartOuterFrom = { clipRange: 0 }; + symbolStartOuterTo = { clipRange: 1 }; + } + target._symbolStartOuter.setAttributes(symbolStartOuterFrom); + + animator.animate(target._symbolStartOuter, { + type: 'to', + to: symbolStartOuterTo, + duration: symbolTime * 5, + easing + }); + + target._symbolEnd.setAttributes({ scaleX: 0, scaleY: 0 }); + + animator.animate(target._symbolEnd, { + type: 'to', + to: { scaleX: 1, scaleY: 1 }, + duration: symbolTime * 2, + delay: symbolTime * 8, + easing + }); + + target._line.setAttributes({ clipRange: 0 }); + + animator.animate(target._line, { + type: 'to', + to: { clipRange: 1 }, + duration: symbolTime * 9, + easing + }); + + if (titleType === 'typewriter') { + const titleTopText = target._titleTop.attribute.text as string; + target._titleTop.setAttributes({ text: '' }); + + animator.animate(target._titleTop, { + type: 'custom', + delay: symbolTime * 5, + duration: symbolTime * 4, + easing: 'linear', + to: { text: titleTopText }, + custom: InputText + }); + + const titleBottomText = target._titleBottom.attribute.text as string; + target._titleBottom.setAttributes({ text: '' }); + + animator.animate(target._titleBottom, { + type: 'custom', + delay: symbolTime * 5, + duration: symbolTime * 4, + easing: 'linear', + to: { text: titleBottomText }, + custom: InputText + }); + } else { + target._titleTop.setAttributes({ dy: target._titleTop.AABBBounds.height() + 10 }); + + animator.animate(target._titleTop, { + type: 'to', + to: { + dy: 0 + }, + delay: symbolTime * 5, + duration: symbolTime * 4, + easing: 'linear' + }); + + target._titleBottom.setAttributes({ dy: -(10 + target._titleBottom.AABBBounds.height()) }); + + animator.animate(target._titleBottom, { + type: 'to', + to: { + dy: 0 + }, + delay: symbolTime * 5, + duration: symbolTime * 4, + easing: 'linear' + }); + } + + if (titlePanelType === 'scale') { + [target._titleTopPanel, target._titleBottomPanel].forEach(panel => { + const scaleX = panel.attribute.scaleX ?? 1; + panel.setAttributes({ scaleX: 0 }); + animator.animate(panel, { + type: 'to', + to: { + scaleX + }, + duration, + easing + }); + }); + } else if (titlePanelType === 'stroke') { + [target._titleTopPanel, target._titleBottomPanel].forEach(panel => { + const b = panel.AABBBounds; + const totalLen = (b.width() + b.height()) * 2; + panel.setAttributes({ lineDash: [0, totalLen * 10] }); + animator.animate(panel, { + type: 'to', + to: { + lineDash: [totalLen, totalLen * 10] + }, + duration, + easing + }); + }); + } + } +} + +/** + * LabelItemDisappear class handles the disappear animation for StoryLabelItem components + */ +export class LabelItemDisappear extends AComponentAnimate { + onBind(): void { + const animator = createComponentAnimator(this.target); + this._animator = animator; + + const duration = this.duration; + const easing = this.easing; + const target = this.target as any; + + const { mode } = this.params; + + if (mode === 'scale') { + animator.animate(target._symbolStart, { + type: 'to', + to: { scaleX: 0, scaleY: 0 }, + duration, + easing + }); + } else { + animator.animate(target._line, { + type: 'to', + to: { clipRange: 0 }, + duration, + easing + }); + + animator.animate(target._symbolStart, { + type: 'to', + to: { scaleX: 0, scaleY: 0 }, + duration: duration / 2, + delay: duration / 2, + easing + }); + + animator.animate(target._symbolEnd, { + type: 'to', + to: { scaleX: 0, scaleY: 0 }, + duration, + easing + }); + + animator.animate(target._titleTop, { + type: 'to', + to: { dy: target._titleTop.AABBBounds.height() + 10 }, + duration: duration / 2, + easing + }); + + animator.animate(target._titleBottom, { + type: 'to', + to: { dy: -(10 + target._titleBottom.AABBBounds.height()) }, + duration: duration / 2, + easing + }); + + animator.animate(target._symbolStartOuter, { + type: 'to', + to: { clipRange: 0 }, + duration: duration / 2, + delay: duration / 2, + easing + }); + + animator.animate(target._titleTopPanel, { + type: 'to', + to: { scaleX: 0 }, + duration, + easing + }); + + animator.animate(target._titleBottomPanel, { + type: 'to', + to: { scaleX: 0 }, + duration, + easing + }); + } + } +} diff --git a/packages/vrender-animate/src/custom/poptip-animate.ts b/packages/vrender-animate/src/custom/poptip-animate.ts new file mode 100644 index 000000000..87b6739c7 --- /dev/null +++ b/packages/vrender-animate/src/custom/poptip-animate.ts @@ -0,0 +1,107 @@ +import { AComponentAnimate } from './custom-animate'; +import { createComponentAnimator } from '../component'; +import { InputText } from './input-text'; + +/** + * PoptipAppear class handles the appear animation for Poptip components + */ +export class PoptipAppear extends AComponentAnimate { + onBind(): void { + const animator = createComponentAnimator(this.target); + this._animator = animator; + + const duration = this.duration; + const easing = this.easing; + const target = this.target as any; + + const { wave } = this.params; + + target.setAttributes({ scaleX: 0, scaleY: 0 }); + + animator.animate(target, { + type: 'to', + to: { scaleX: 1, scaleY: 1 }, + duration: (duration / 3) * 2, + easing + }); + + target.titleShape && + animator.animate(target.titleShape, { + type: 'custom', + to: { text: target.titleShape.attribute.text as string }, + duration, + easing, + custom: InputText + }); + + target.contentShape && + animator.animate(target.contentShape, { + type: 'custom', + to: { text: target.contentShape.attribute.text as string }, + duration, + easing, + custom: InputText + }); + + if (wave) { + const dur = duration / 6; + animator.animate(target.group, { + timeSlices: [ + { + duration: dur, + effects: { + type: 'to', + to: { angle: wave }, + easing + } + }, + { + duration: dur * 2, + effects: { + type: 'to', + to: { angle: -wave }, + easing + } + }, + { + duration: dur * 2, + effects: { + type: 'to', + to: { angle: wave }, + easing + } + }, + { + duration: dur, + effects: { + type: 'to', + to: { angle: 0 }, + easing + } + } + ] + }); + } + } +} + +/** + * PoptipDisappear class handles the disappear animation for Poptip components + */ +export class PoptipDisappear extends AComponentAnimate { + onBind(): void { + const animator = createComponentAnimator(this.target); + this._animator = animator; + + const duration = this.duration; + const easing = this.easing; + const target = this.target as any; + + animator.animate(target, { + type: 'to', + to: { scaleX: 0, scaleY: 0 }, + duration, + easing + }); + } +} diff --git a/packages/vrender-animate/src/custom/register.ts b/packages/vrender-animate/src/custom/register.ts index 8d68d0a4b..d598adfe4 100644 --- a/packages/vrender-animate/src/custom/register.ts +++ b/packages/vrender-animate/src/custom/register.ts @@ -14,6 +14,8 @@ import { } from './growPoints'; import { GrowRadiusIn, GrowRadiusOut } from './growRadius'; import { GrowWidthIn, GrowWidthOut } from './growWidth'; +import { LabelItemAppear, LabelItemDisappear } from './label-item-animate'; +import { PoptipAppear, PoptipDisappear } from './poptip-animate'; import { ScaleIn, ScaleOut } from './scale'; import { State } from './state'; import { Update } from './update'; @@ -44,4 +46,10 @@ export const registerCustomAnimate = () => { // state和update共用一个自定义动画类 AnimateExecutor.registerBuiltInAnimate('update', Update); AnimateExecutor.registerBuiltInAnimate('state', State); + // Label item animations + AnimateExecutor.registerBuiltInAnimate('labelItemAppear', LabelItemAppear); + AnimateExecutor.registerBuiltInAnimate('labelItemDisappear', LabelItemDisappear); + // Poptip animations + AnimateExecutor.registerBuiltInAnimate('poptipAppear', PoptipAppear); + AnimateExecutor.registerBuiltInAnimate('poptipDisappear', PoptipDisappear); }; diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index 003f1d0b0..bdc3f0aeb 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -214,6 +214,10 @@ export class AnimateExecutor implements IAnimateExecutor { * 执行动画,针对一组元素 */ execute(params: IAnimationConfig) { + if (params.selfOnly) { + return this.executeItem(params, this._target, 0, 1); + } + // 判断是否为timeline配置 const isTimeline = 'timeSlices' in params; @@ -470,7 +474,7 @@ export class AnimateExecutor implements IAnimateExecutor { const { type = 'to', channel, customParameters, easing = 'linear', options } = effect; // 根据 channel 配置创建属性对象 - const props = this.createPropsFromChannel(channel, graphic); + const props = effect.to ?? this.createPropsFromChannel(channel, graphic); this._handleRunAnimate( animate, effect.custom, @@ -601,12 +605,14 @@ export class AnimateExecutor implements IAnimateExecutor { const isTimeline = 'timeSlices' in params; let animate: IAnimate | null = null; + const parsedParams = this.parseParams(params, isTimeline); + if (isTimeline) { // 处理 Timeline 类型的动画配置 - animate = this.executeTimelineItem(params as IAnimationTimeline, graphic, index, count); + animate = this.executeTimelineItem(parsedParams as IAnimationTimeline, graphic, index, count); } else { // 处理 TypeConfig 类型的动画配置 - animate = this.executeTypeConfigItem(params as IAnimationTypeConfig, graphic, index, count); + animate = this.executeTypeConfigItem(parsedParams as IAnimationTypeConfig, graphic, index, count); } // 跟踪动画以进行生命周期管理 diff --git a/packages/vrender-animate/src/executor/executor.ts b/packages/vrender-animate/src/executor/executor.ts index b716b287c..30d6d8431 100644 --- a/packages/vrender-animate/src/executor/executor.ts +++ b/packages/vrender-animate/src/executor/executor.ts @@ -51,6 +51,8 @@ export interface IAnimationEffect { type?: string; /** 动画 channel 配置 */ channel?: IAnimationChannelAttrs | IAnimationChannelAttributes; + /** 动画 to 配置(和channel互斥,如果同时设置,以to为准) */ + to?: Record; /** 动画 自定义插值 配置 */ custom?: IAnimationChannelInterpolator | IAnimationCustomConstructor; /** 动画 custom 参数配置 */ @@ -117,6 +119,8 @@ export interface IAnimationTypeConfig { controlOptions?: IAnimationControlOptions; /** 动画优先级 */ priority?: number; + /** 该动画是否需要忽略子图元 */ + selfOnly?: boolean; } /** @@ -145,6 +149,8 @@ export interface IAnimationTimeline { controlOptions?: IAnimationControlOptions; /** 动画优先级 */ priority?: number; + /** 该动画是否需要忽略子图元 */ + selfOnly?: boolean; } /** diff --git a/packages/vrender-animate/src/index.ts b/packages/vrender-animate/src/index.ts index 7b9b660f6..91d926a29 100644 --- a/packages/vrender-animate/src/index.ts +++ b/packages/vrender-animate/src/index.ts @@ -22,6 +22,7 @@ export { TagPointsUpdate } from './custom/tag-points'; export { GroupFadeIn, GroupFadeOut } from './custom/group-fade'; export { RotateBySphereAnimate } from './custom/sphere'; export { AnimateExecutor } from './executor/animate-executor'; +export type { IAnimationConfig } from './executor/executor'; export { registerCustomAnimate } from './custom/register'; // Export animation state modules export * from './state'; @@ -29,3 +30,6 @@ export { AnimationTransitionRegistry } from './state/animation-states-registry'; export { transitionRegistry } from './state/animation-states-registry'; export { AnimationStateManager } from './state/animation-state'; export { AnimationStateStore } from './state/animation-state'; + +// Export component animation modules +export * from './component'; diff --git a/packages/vrender-animate/src/intreface/animate.ts b/packages/vrender-animate/src/intreface/animate.ts index 832f39462..ea5bb6fce 100644 --- a/packages/vrender-animate/src/intreface/animate.ts +++ b/packages/vrender-animate/src/intreface/animate.ts @@ -63,6 +63,9 @@ export interface IStep { // 屏蔽自身属性,会直接从props等内容里删除掉 deleteSelfAttr: (key: string) => void; + + // 停止 + stop: () => void; } export interface IAnimate { diff --git a/packages/vrender-animate/src/state/animation-state.ts b/packages/vrender-animate/src/state/animation-state.ts index 59f1ab76c..314e7b6d5 100644 --- a/packages/vrender-animate/src/state/animation-state.ts +++ b/packages/vrender-animate/src/state/animation-state.ts @@ -4,6 +4,21 @@ import { AnimationTransitionRegistry } from './animation-states-registry'; import type { IAnimationConfig } from '../executor/executor'; import { AnimateExecutor } from '../executor/animate-executor'; +// Standard animation state names +export const AnimationStates = { + APPEAR: 'appear', + DISAPPEAR: 'disappear', + UPDATE: 'update', + HIGHLIGHT: 'highlight', + UNHIGHLIGHT: 'unhighlight', + SELECT: 'select', + UNSELECT: 'unselect', + HOVER: 'hover', + UNHOVER: 'unhover', + ACTIVE: 'active', + INACTIVE: 'inactive' +}; + export class AnimationStateStore { graphic: IGraphic; @@ -135,6 +150,63 @@ export class AnimationStateManager { this.stateList.push(...shouldApplyState); } + /** + * Apply a standard appear animation to the graphic + * @param animationConfig Animation configuration + * @param callback Callback to be called when animation ends + */ + applyAppearState(animationConfig: IAnimationConfig, callback?: () => void): void { + this.applyState([AnimationStates.APPEAR], [{ name: AnimationStates.APPEAR, animation: animationConfig }], callback); + } + + /** + * Apply a standard disappear animation to the graphic + * @param animationConfig Animation configuration + * @param callback Callback to be called when animation ends + */ + applyDisappearState(animationConfig: IAnimationConfig, callback?: () => void): void { + this.applyState( + [AnimationStates.DISAPPEAR], + [{ name: AnimationStates.DISAPPEAR, animation: animationConfig }], + callback + ); + } + + /** + * Apply a standard update animation to the graphic + * @param animationConfig Animation configuration + * @param callback Callback to be called when animation ends + */ + applyUpdateState(animationConfig: IAnimationConfig, callback?: () => void): void { + this.applyState([AnimationStates.UPDATE], [{ name: AnimationStates.UPDATE, animation: animationConfig }], callback); + } + + /** + * Apply a standard highlight animation to the graphic + * @param animationConfig Animation configuration + * @param callback Callback to be called when animation ends + */ + applyHighlightState(animationConfig: IAnimationConfig, callback?: () => void): void { + this.applyState( + [AnimationStates.HIGHLIGHT], + [{ name: AnimationStates.HIGHLIGHT, animation: animationConfig }], + callback + ); + } + + /** + * Apply a standard unhighlight animation to the graphic + * @param animationConfig Animation configuration + * @param callback Callback to be called when animation ends + */ + applyUnhighlightState(animationConfig: IAnimationConfig, callback?: () => void): void { + this.applyState( + [AnimationStates.UNHIGHLIGHT], + [{ name: AnimationStates.UNHIGHLIGHT, animation: animationConfig }], + callback + ); + } + stopState(state: string, type?: 'start' | 'end' | Record): void { const stateInfo = this.stateList?.find(stateInfo => stateInfo.state === state); if (stateInfo) { diff --git a/packages/vrender-animate/src/state/graphic-extension.ts b/packages/vrender-animate/src/state/graphic-extension.ts index 83c4fc09a..eca71d734 100644 --- a/packages/vrender-animate/src/state/graphic-extension.ts +++ b/packages/vrender-animate/src/state/graphic-extension.ts @@ -1,6 +1,7 @@ import type { IGraphic } from '@visactor/vrender-core'; import type { IAnimationState } from './types'; import { AnimationStateManager, AnimationStateStore } from './animation-state'; +import type { IAnimationConfig } from '../executor/executor'; /** * 将动画状态方法作为混入扩展 Graphic 的类 @@ -37,6 +38,56 @@ export class GraphicStateExtension { return this; } + /** + * 应用出现动画状态 + * @param animationConfig 动画配置 + * @param callback 动画结束回调 + */ + applyAppearState(animationConfig: IAnimationConfig, callback?: () => void): this { + this._getAnimationStateManager(this as unknown as IGraphic).applyAppearState(animationConfig, callback); + return this; + } + + /** + * 应用消失动画状态 + * @param animationConfig 动画配置 + * @param callback 动画结束回调 + */ + applyDisappearState(animationConfig: IAnimationConfig, callback?: () => void): this { + this._getAnimationStateManager(this as unknown as IGraphic).applyDisappearState(animationConfig, callback); + return this; + } + + /** + * 应用更新动画状态 + * @param animationConfig 动画配置 + * @param callback 动画结束回调 + */ + applyUpdateState(animationConfig: IAnimationConfig, callback?: () => void): this { + this._getAnimationStateManager(this as unknown as IGraphic).applyUpdateState(animationConfig, callback); + return this; + } + + /** + * 应用高亮动画状态 + * @param animationConfig 动画配置 + * @param callback 动画结束回调 + */ + applyHighlightState(animationConfig: IAnimationConfig, callback?: () => void): this { + this._getAnimationStateManager(this as unknown as IGraphic).applyHighlightState(animationConfig, callback); + return this; + } + + /** + * 应用取消高亮动画状态 + * @param animationConfig 动画配置 + * @param callback 动画结束回调 + */ + applyUnhighlightState(animationConfig: IAnimationConfig, callback?: () => void): this { + this._getAnimationStateManager(this as unknown as IGraphic).applyUnhighlightState(animationConfig, callback); + return this; + } + /** * 停止一个动画状态 */ diff --git a/packages/vrender-animate/src/step.ts b/packages/vrender-animate/src/step.ts index 3a5fe88c2..edcdd5cc8 100644 --- a/packages/vrender-animate/src/step.ts +++ b/packages/vrender-animate/src/step.ts @@ -291,6 +291,10 @@ export class Step implements IStep { getMergedEndProps(): Record | void { return this.getEndProps(); } + + stop(): void { + // ... + } } export class WaitStep extends Step { diff --git a/packages/vrender-animate/src/ticker/default-ticker.ts b/packages/vrender-animate/src/ticker/default-ticker.ts index 61330f76c..34d0c6fcf 100644 --- a/packages/vrender-animate/src/ticker/default-ticker.ts +++ b/packages/vrender-animate/src/ticker/default-ticker.ts @@ -220,7 +220,8 @@ export class DefaultTicker extends EventEmitter implements ITicker { protected checkSkip = (delta: number): boolean => { // 随机扰动(每次都对interval进行随机的扰动,避免所有tick都发生在同一帧) - return delta < this.interval + (Math.random() - 0.5) * 2 * this._jitter; + const skip = delta < this.interval + (Math.random() - 0.5) * 2 * this._jitter; + return skip; }; protected handleTick = (handler: ITickHandler, params?: { once?: boolean }): boolean => { diff --git a/packages/vrender-components/__tests__/browser/examples/poptip.ts b/packages/vrender-components/__tests__/browser/examples/poptip.ts index 105888532..604aa865c 100644 --- a/packages/vrender-components/__tests__/browser/examples/poptip.ts +++ b/packages/vrender-components/__tests__/browser/examples/poptip.ts @@ -3,6 +3,11 @@ import '@visactor/vrender'; import { createLine, createText, IText } from '@visactor/vrender'; import render from '../../util/render'; import { PopTip, loadPoptip } from '../../../src'; +import { registerAnimate, registerCustomAnimate } from '@visactor/vrender-animate'; + +registerAnimate(); +registerCustomAnimate(); + // import { initBrowserEnv } from '@visactor/vrender-kits'; // initBrowserEnv(); loadPoptip(); @@ -246,9 +251,22 @@ export function run() { ); stage.render(); poptipList.forEach(poptip => { - poptip.appearAnimate({ duration: 300, easing: 'quadOut', wave: 0.3 }); + poptip.applyAppearState({ + type: 'poptipAppear', + duration: 300, + easing: 'quadOut', + selfOnly: true, + customParameters: { + wave: 0.3 + } + }); + setTimeout(() => { - poptip.disappearAnimate({ duration: 300, easing: 'aIn3' }); + poptip.applyDisappearState({ + type: 'poptipDisappear', + duration: 300, + easing: 'aIn3' + }); }, 2000); }); diff --git a/packages/vrender-components/__tests__/browser/examples/story-label-item.ts b/packages/vrender-components/__tests__/browser/examples/story-label-item.ts index 72fdd58b5..457805436 100644 --- a/packages/vrender-components/__tests__/browser/examples/story-label-item.ts +++ b/packages/vrender-components/__tests__/browser/examples/story-label-item.ts @@ -1,8 +1,10 @@ import '@visactor/vrender'; -import { IPointLike } from '@visactor/vutils'; import render from '../../util/render'; import { StoryLabelItem } from '../../../src'; -import { createLine } from '@visactor/vrender-core'; +import { registerAnimate, registerCustomAnimate } from '@visactor/vrender-animate'; + +registerAnimate(); +registerCustomAnimate(); export function run() { const labels: StoryLabelItem[] = []; @@ -97,22 +99,32 @@ export function run() { }) ); - const stage = render(labels, 'main', 'black'); + const stage = render(labels, 'main', { background: 'black' }); stage.render(); labels.forEach((label, index) => { - label.appearAnimate({ + label.applyAppearState({ + type: 'labelItemAppear', duration: 1000, easing: 'cubicIn', - symbolStartOuterType: 'scale', - titleType: 'move', - titlePanelType: index > 3 ? 'stroke' : 'scale' + selfOnly: true, + customParameters: { + symbolStartOuterType: 'scale', + titleType: 'typewriter', + titlePanelType: index > 3 ? 'stroke' : 'scale' + } }); }); setTimeout(() => { labels.forEach(label => { - label.disappearAnimate({ duration: 1000, easing: 'cubicIn' }); + label.applyDisappearState({ + type: 'labelItemDisappear', + duration: 1000, + easing: 'cubicIn', + selfOnly: true, + customParameters: {} + }); }); }, 3000); } diff --git a/packages/vrender-components/__tests__/browser/vite.config.ts b/packages/vrender-components/__tests__/browser/vite.config.ts index 4f393b723..576b76192 100644 --- a/packages/vrender-components/__tests__/browser/vite.config.ts +++ b/packages/vrender-components/__tests__/browser/vite.config.ts @@ -16,7 +16,8 @@ export default defineConfig({ alias: { '@visactor/vrender-core': path.resolve(__dirname, '../../../vrender-core/src/index.ts'), '@visactor/vrender': path.resolve(__dirname, '../../../vrender/src/index.ts'), - '@visactor/vrender-kits': path.resolve(__dirname, '../../../vrender-kits/src/index.ts') + '@visactor/vrender-kits': path.resolve(__dirname, '../../../vrender-kits/src/index.ts'), + '@visactor/vrender-animate': path.resolve(__dirname, '../../../vrender-animate/src/index.ts') } } }); diff --git a/packages/vrender-components/src/label-item/label-item.ts b/packages/vrender-components/src/label-item/label-item.ts index a3b22146b..7a0cede72 100644 --- a/packages/vrender-components/src/label-item/label-item.ts +++ b/packages/vrender-components/src/label-item/label-item.ts @@ -15,14 +15,14 @@ import { max, merge } from '@visactor/vutils'; export class StoryLabelItem extends AbstractComponent> { name: 'labelItem'; - private _line?: ILine; - private _symbolStart: ISymbol; - private _symbolEnd: ISymbol; - private _symbolStartOuter: ISymbol; - private _titleTop: IText; - private _titleBottom: IText; - private _titleTopPanel: IRect; - private _titleBottomPanel: IRect; + _line?: ILine; + _symbolStart: ISymbol; + _symbolEnd: ISymbol; + _symbolStartOuter: ISymbol; + _titleTop: IText; + _titleBottom: IText; + _titleTopPanel: IRect; + _titleBottomPanel: IRect; static defaultAttributes: Partial = { // 内容在X上的偏移量 @@ -231,112 +231,4 @@ export class StoryLabelItem extends AbstractComponent { - const scaleX = panel.attribute.scaleX; - panel.setAttributes({ scaleX: 0 }); - panel.animate().to({ scaleX }, duration, 'circInOut'); - }); - } else if (titlePanelType === 'stroke') { - [this._titleTopPanel, this._titleBottomPanel].forEach(panel => { - const b = panel.AABBBounds; - const totalLen = (b.width() + b.height()) * 2; - panel.setAttributes({ lineDash: [0, totalLen * 10] }); - panel.animate().to({ lineDash: [totalLen, totalLen * 10] }, duration, 'quadOut'); - }); - } - } - - disappearAnimate(animateConfig: { duration?: number; easing?: string; mode?: 'scale' | 'default' }) { - if (animateConfig.mode === 'scale') { - const { duration = 1000, easing = 'quadOut' } = animateConfig; - this.animate().to({ scaleX: 0, scaleY: 0 }, duration, easing as any); - } else { - const { duration = 1000, easing = 'quadOut' } = animateConfig; - this._line.animate().to({ clipRange: 0 }, duration, easing as any); - this._symbolStart - .animate() - .wait(duration / 2) - .to({ scaleX: 0, scaleY: 0 }, duration / 2, easing as any); - this._symbolEnd.animate().to({ scaleX: 0, scaleY: 0 }, duration, easing as any); - this._titleTop.animate().to({ dy: this._titleTop.AABBBounds.height() + 10 }, duration / 2, easing as any); - this._titleBottom - .animate() - .to({ dy: -(10 + this._titleBottom.AABBBounds.height()) }, duration / 2, easing as any); - this._symbolStartOuter - .animate() - .wait(duration / 2) - .to({ clipRange: 0 }, duration / 2, easing as any); - this._titleTopPanel.animate().to({ scaleX: 0 }, duration, 'circInOut'); - this._titleBottomPanel.animate().to({ scaleX: 0 }, duration, 'circInOut'); - } - } } diff --git a/packages/vrender-components/src/poptip/poptip.ts b/packages/vrender-components/src/poptip/poptip.ts index 582f44f0f..d708fc299 100644 --- a/packages/vrender-components/src/poptip/poptip.ts +++ b/packages/vrender-components/src/poptip/poptip.ts @@ -560,52 +560,4 @@ export class PopTip extends AbstractComponent> { }; } } - - appearAnimate(animateConfig: { duration?: number; easing?: string; wave?: number }) { - // 基准时间,line[0, 500], point[100, 600] 100 onebyone, pointNormal[600, 1000] 90+90 onebyone, activeLine[500, 700] - // line和activeLine的clipRange - const { duration = 1000, easing = 'quadOut' } = animateConfig; - this.setAttributes({ scaleX: 0, scaleY: 0 }); - this.animate().to({ scaleX: 1, scaleY: 1 }, (duration / 3) * 2, easing as any); - this.titleShape && - this.titleShape - .animate() - .play( - new InputText( - { text: '' }, - { text: this.titleShape.attribute.text as string }, - duration, - easing as any - ) as any - ); - this.contentShape && - this.contentShape - .animate() - .play( - new InputText( - { text: '' }, - { text: this.contentShape.attribute.text as string }, - duration, - easing as any - ) as any - ); - - // 摇摆 - if (animateConfig.wave) { - const dur = duration / 6; - this.group - .animate() - .to({ angle: animateConfig.wave }, dur, easing as any) - .to({ angle: -animateConfig.wave }, dur * 2, easing as any) - .to({ angle: animateConfig.wave }, dur * 2, easing as any) - .to({ angle: 0 }, dur, easing as any); - } - } - - disappearAnimate(animateConfig: { duration?: number; easing?: string }) { - // 基准时间,line[0, 500], point[100, 600] 100 onebyone, pointNormal[600, 1000] 90+90 onebyone, activeLine[500, 700] - // line和activeLine的clipRange - const { duration = 1000, easing = 'quadOut' } = animateConfig; - this.animate().to({ scaleX: 0, scaleY: 0 }, duration, easing as any); - } } diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index 437e2be12..57e26429a 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -996,11 +996,7 @@ export abstract class Graphic = Partial Date: Wed, 2 Apr 2025 10:22:56 +0800 Subject: [PATCH 061/179] feat: support part function for label animate refactor --- packages/vrender-animate/src/custom/common.ts | 2 +- .../vrender-animate/src/custom/growAngle.ts | 4 +- .../vrender-animate/src/custom/growCenter.ts | 2 +- .../vrender-animate/src/custom/growHeight.ts | 2 +- .../vrender-animate/src/custom/growPoints.ts | 6 +- .../vrender-animate/src/custom/growRadius.ts | 4 +- .../vrender-animate/src/custom/growWidth.ts | 2 +- .../src/custom/label-animate.ts | 50 +++ .../vrender-animate/src/custom/register.ts | 7 + packages/vrender-animate/src/custom/scale.ts | 2 +- .../src/executor/animate-executor.ts | 42 +- packages/vrender-components/src/label/base.ts | 382 +++++++++++------- packages/vrender-core/src/core/stage.ts | 2 +- 13 files changed, 332 insertions(+), 175 deletions(-) create mode 100644 packages/vrender-animate/src/custom/label-animate.ts diff --git a/packages/vrender-animate/src/custom/common.ts b/packages/vrender-animate/src/custom/common.ts index c359587d2..e0884ee5f 100644 --- a/packages/vrender-animate/src/custom/common.ts +++ b/packages/vrender-animate/src/custom/common.ts @@ -21,7 +21,7 @@ export class CommonIn extends ACustomAnimate> { this.target.setAttributes(this.params.diffAttrs); } const attrs = (this.target as any).getAttributes(true); - const fromAttrs = this.target.context.lastAttrs ?? {}; + const fromAttrs = this.target.context?.lastAttrs ?? {}; const to: Record = {}; const from: Record = {}; diff --git a/packages/vrender-animate/src/custom/growAngle.ts b/packages/vrender-animate/src/custom/growAngle.ts index dae1bd4ac..12a5643a4 100644 --- a/packages/vrender-animate/src/custom/growAngle.ts +++ b/packages/vrender-animate/src/custom/growAngle.ts @@ -198,7 +198,7 @@ export class GrowAngleIn extends GrowAngleBase { Object.assign(this.target.attribute, this.params.diffAttrs); } const { from, to } = growAngleIn(this.target, this.params.options, this.params); - const fromAttrs = this.target.context.lastAttrs ?? from; + const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); this.from = fromAttrs; @@ -214,7 +214,7 @@ export class GrowAngleIn extends GrowAngleBase { export class GrowAngleOut extends GrowAngleBase { onBind(): void { const { from, to } = growAngleOut(this.target, this.params.options, this.params); - const fromAttrs = this.target.context.lastAttrs ?? from; + const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); this.animate.reSyncProps(); diff --git a/packages/vrender-animate/src/custom/growCenter.ts b/packages/vrender-animate/src/custom/growCenter.ts index 1fbcd9be8..c497470fb 100644 --- a/packages/vrender-animate/src/custom/growCenter.ts +++ b/packages/vrender-animate/src/custom/growCenter.ts @@ -208,7 +208,7 @@ export class GrowCenterIn extends ACustomAnimate> { this.target.setAttributes(this.params.diffAttrs); } const { from, to } = growCenterIn(this.target, this.params.options, this.params); - const fromAttrs = this.target.context.lastAttrs ?? from; + const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); this.animate.reSyncProps(); diff --git a/packages/vrender-animate/src/custom/growHeight.ts b/packages/vrender-animate/src/custom/growHeight.ts index 254fe25ba..fa0c9a44a 100644 --- a/packages/vrender-animate/src/custom/growHeight.ts +++ b/packages/vrender-animate/src/custom/growHeight.ts @@ -105,7 +105,7 @@ export class GrowHeightIn extends ACustomAnimate> { Object.assign(this.target.attribute, this.params.diffAttrs); } const { from, to } = growHeightIn(this.target, this.params.options, this.params); - const fromAttrs = this.target.context.lastAttrs ?? from; + const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); this.from = fromAttrs; diff --git a/packages/vrender-animate/src/custom/growPoints.ts b/packages/vrender-animate/src/custom/growPoints.ts index 9127be6d3..29f01cd3f 100644 --- a/packages/vrender-animate/src/custom/growPoints.ts +++ b/packages/vrender-animate/src/custom/growPoints.ts @@ -114,7 +114,7 @@ export class GrowPointsIn extends GworPointsBase { onBind(): void { if (['area', 'line', 'polygon'].includes(this.target.type)) { const { from, to } = growPointsIn(this.target, this.params.options, this.params); - const fromAttrs = this.target.context.lastAttrs ?? from; + const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); this.animate.reSyncProps(); @@ -208,7 +208,7 @@ export class GrowPointsXIn extends GworPointsBase { onBind(): void { if (['area', 'line', 'polygon'].includes(this.target.type)) { const { from, to } = growPointsXIn(this.target, this.params.options, this.params); - const fromAttrs = this.target.context.lastAttrs ?? from; + const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); this.animate.reSyncProps(); @@ -306,7 +306,7 @@ export class GrowPointsYIn extends GworPointsBase { } if (['area', 'line', 'polygon'].includes(this.target.type)) { const { from, to } = growPointsYIn(this.target, this.params.options, this.params); - const fromAttrs = this.target.context.lastAttrs ?? from; + const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); this.animate.reSyncProps(); diff --git a/packages/vrender-animate/src/custom/growRadius.ts b/packages/vrender-animate/src/custom/growRadius.ts index 345de89c1..6314955ea 100644 --- a/packages/vrender-animate/src/custom/growRadius.ts +++ b/packages/vrender-animate/src/custom/growRadius.ts @@ -150,7 +150,7 @@ export class GrowRadiusIn extends GworPointsBase { this.target.setAttributes(this.params.diffAttrs); } const { from, to } = growRadiusIn(this.target, this.params.options, this.params); - const fromAttrs = this.target.context.lastAttrs ?? from; + const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); this.animate.reSyncProps(); @@ -163,7 +163,7 @@ export class GrowRadiusIn extends GworPointsBase { export class GrowRadiusOut extends GworPointsBase { onBind(): void { const { from, to } = growRadiusOut(this.target, this.params.options, this.params); - const fromAttrs = this.target.context.lastAttrs ?? from; + const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); this.animate.reSyncProps(); diff --git a/packages/vrender-animate/src/custom/growWidth.ts b/packages/vrender-animate/src/custom/growWidth.ts index 9c72fa059..07d922cd1 100644 --- a/packages/vrender-animate/src/custom/growWidth.ts +++ b/packages/vrender-animate/src/custom/growWidth.ts @@ -169,7 +169,7 @@ export class GrowWidthIn extends ACustomAnimate> { this.target.setAttributes(this.params.diffAttrs); } const { from, to } = growWidthIn(this.target, this.params.options, this.params); - const fromAttrs = this.target.context.lastAttrs ?? from; + const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); this.animate.reSyncProps(); diff --git a/packages/vrender-animate/src/custom/label-animate.ts b/packages/vrender-animate/src/custom/label-animate.ts new file mode 100644 index 000000000..bf15e9029 --- /dev/null +++ b/packages/vrender-animate/src/custom/label-animate.ts @@ -0,0 +1,50 @@ +import { AComponentAnimate } from './custom-animate'; +import { createComponentAnimator } from '../component'; + +/** + * LabelUpdate class handles the update animation for Label components + */ +export class LabelUpdate extends AComponentAnimate { + onBind(): void { + const animator = createComponentAnimator(this.target); + this._animator = animator; + const duration = this.duration; + const easing = this.easing; + + const { prevText, curText, prevLabelLine, curLabelLine } = this.params; + const diff: Record = {}; + + for (const key in curText.attribute) { + if (prevText.attribute[key] !== curText.attribute[key]) { + diff[key] = curText.attribute[key]; + } + } + + const { text, ...rest } = diff; + + animator.animate(prevText, { + type: 'to', + to: rest, + duration, + easing + }); + + animator.animate(prevText, { + type: 'increaseCount', + to: { + text: curText.attribute.text + }, + duration, + easing + }); + + if (prevLabelLine) { + animator.animate(prevLabelLine, { + type: 'to', + to: curLabelLine.attribute, + duration, + easing + }); + } + } +} diff --git a/packages/vrender-animate/src/custom/register.ts b/packages/vrender-animate/src/custom/register.ts index d598adfe4..2d295b6c0 100644 --- a/packages/vrender-animate/src/custom/register.ts +++ b/packages/vrender-animate/src/custom/register.ts @@ -14,13 +14,18 @@ import { } from './growPoints'; import { GrowRadiusIn, GrowRadiusOut } from './growRadius'; import { GrowWidthIn, GrowWidthOut } from './growWidth'; +import { LabelUpdate } from './label-animate'; import { LabelItemAppear, LabelItemDisappear } from './label-item-animate'; +import { IncreaseCount } from './number'; import { PoptipAppear, PoptipDisappear } from './poptip-animate'; import { ScaleIn, ScaleOut } from './scale'; import { State } from './state'; import { Update } from './update'; export const registerCustomAnimate = () => { + // 基础动画 + AnimateExecutor.registerBuiltInAnimate('increaseCount', IncreaseCount); + AnimateExecutor.registerBuiltInAnimate('scaleIn', ScaleIn); AnimateExecutor.registerBuiltInAnimate('scaleOut', ScaleOut); AnimateExecutor.registerBuiltInAnimate('growHeightIn', GrowHeightIn); @@ -52,4 +57,6 @@ export const registerCustomAnimate = () => { // Poptip animations AnimateExecutor.registerBuiltInAnimate('poptipAppear', PoptipAppear); AnimateExecutor.registerBuiltInAnimate('poptipDisappear', PoptipDisappear); + // Label update animation + AnimateExecutor.registerBuiltInAnimate('labelUpdate', LabelUpdate); }; diff --git a/packages/vrender-animate/src/custom/scale.ts b/packages/vrender-animate/src/custom/scale.ts index ab615b651..c739ef992 100644 --- a/packages/vrender-animate/src/custom/scale.ts +++ b/packages/vrender-animate/src/custom/scale.ts @@ -23,7 +23,7 @@ export class ScaleIn extends ACustomAnimate> { let from: Record; let to: Record; const attrs = this.target.getFinalAttribute(); - const fromAttrs = this.target.context.lastAttrs ?? {}; + const fromAttrs = this.target.context?.lastAttrs ?? {}; switch (this.params?.direction) { case 'x': diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index bdc3f0aeb..d1805654f 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -116,8 +116,8 @@ export class AnimateExecutor implements IAnimateExecutor { const startTime = this.resolveValue(params.startTime, undefined, 0); // execute只在mark层面调用,所以性能影响可以忽略 - // TODO 如果后续调用频繁,需要重新修改 - const parsedParams = cloneDeep(params); + // TODO 存在性能问题,如果后续调用频繁,需要重新修改 + const parsedParams: Record = { ...params }; parsedParams.oneByOneDelay = 0; parsedParams.startTime = startTime; parsedParams.totalTime = totalTime; @@ -148,23 +148,29 @@ export class AnimateExecutor implements IAnimateExecutor { if (totalTime) { const _totalTime = sliceTime + oneByOneDelay * (this._target.count - 2); const scale = totalTime ? totalTime / _totalTime : 1; - ((parsedParams as IAnimationTimeline).timeSlices as IAnimationTimeSlice[]).forEach(slice => { - slice.delay = (slice.delay as number) * scale; - slice.delayAfter = (slice.delayAfter as number) * scale; - slice.duration = (slice.duration as number) * scale; - if (!Array.isArray(slice.effects)) { - slice.effects = [slice.effects]; + ((parsedParams as IAnimationTimeline).timeSlices as IAnimationTimeSlice[]) = ( + (parsedParams as IAnimationTimeline).timeSlices as IAnimationTimeSlice[] + ).map(slice => { + let effects = slice.effects; + if (!Array.isArray(effects)) { + effects = [effects]; } - slice.effects.forEach(effect => { - effect.custom = effect.custom ?? AnimateExecutor.builtInAnimateMap[effect.type as any]; - const customType = - effect.custom && isFunction(effect.custom) - ? /^class\s/.test(Function.prototype.toString.call(effect.custom)) - ? 1 - : 2 - : 0; - (effect as any).customType = customType; - }); + return { + ...slice, + delay: (slice.delay as number) * scale, + delayAfter: (slice.delayAfter as number) * scale, + duration: (slice.duration as number) * scale, + effects: effects.map(effect => { + const custom = effect.custom ?? AnimateExecutor.builtInAnimateMap[effect.type as any]; + const customType = + custom && isFunction(custom) ? (/^class\s/.test(Function.prototype.toString.call(custom)) ? 1 : 2) : 0; + return { + ...effect, + custom, + customType + }; + }) + }; }); parsedParams.oneByOne = oneByOneTime * scale; parsedParams.oneByOneDelay = oneByOneDelay * scale; diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index 51d4c2cd3..4d8a2ef1c 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -807,6 +807,67 @@ export class LabelBase extends AbstractComponent { this._graphicToText = currentTextMap; } + protected runEnterAnimation(text: IText | IRichText) { + if (this._enableAnimation === false || !this._animationConfig.enter) { + return; + } + + const relatedGraphic = this.getRelatedGraphic(text.attribute); + const { enter } = this._animationConfig; + + text.applyAnimationState( + ['enter'], + [ + { + name: 'enter', + animation: { + ...enter, + selfOnly: true, + customParameters: { + relatedGraphic + } + } + } + ] + ); + } + + protected _runUpdateAnimation(prevLabel: LabelContent, currentLabel: LabelContent) { + if (this._enableAnimation === false || !this._animationConfig.update) { + return; + } + + const { text: prevText, labelLine: prevLabelLine } = prevLabel; + const { text: curText, labelLine: curLabelLine } = currentLabel; + + const { duration, easing } = this._animationConfig.update; + + prevText.applyAnimationState( + ['update'], + [ + { + name: 'update', + animation: { + type: 'labelUpdate', + duration, + easing, + customParameters: { + prevText, + curText, + prevLabelLine, + curLabelLine + } + } + } + ] + ); + + // updateAnimation(prevText, curText, this._animationConfig.update); + // if (prevLabelLine && curLabelLine) { + // prevLabel.labelLine.animate().to(curLabelLine.attribute, duration, easing); + // } + } + protected _addLabel( label: LabelContent, texts?: LabelContent['text'][], @@ -815,64 +876,65 @@ export class LabelBase extends AbstractComponent { ) { const { text, labelLine } = label; // TODO: 或许还需要判断关联图元是否有动画? - const relatedGraphic = this.getRelatedGraphic(text.attribute); - this._syncStateWithRelatedGraphic(relatedGraphic); - - if (this._enableAnimation !== false && this._animationConfig.enter !== false) { - if (relatedGraphic) { - const { from, to } = getAnimationAttributes(text.attribute, 'fadeIn'); - if (text) { - this.add(text); - } + // const relatedGraphic = this.getRelatedGraphic(text.attribute); + // this._syncStateWithRelatedGraphic(relatedGraphic); - if (labelLine) { - labelLines.push(labelLine); - this.add(labelLine); - } - - // enter的时长如果不是大于0,那么直接跳过动画 - this._animationConfig.enter.duration > 0 && - relatedGraphic.once('animate-bind', a => { - // text和labelLine共用一个from - text.setAttributes(from); - labelLine && labelLine.setAttributes(from); - const listener = this._afterRelatedGraphicAttributeUpdate( - text, - texts, - labelLine, - labelLines, - index, - relatedGraphic, - to, - this._animationConfig.enter as ILabelEnterAnimation - ); - relatedGraphic.on('afterAttributeUpdate', listener); - }); - } - } else { - if (text) { - this.add(text); - } - if (labelLine) { - this.add(labelLine); - } + if (text) { + this.add(text); } + if (labelLine) { + this.add(labelLine); + } + + this.runEnterAnimation(text); + + // if (this._enableAnimation !== false && this._animationConfig.enter !== false) { + // if (relatedGraphic) { + // const { from, to } = getAnimationAttributes(text.attribute, 'fadeIn'); + // if (text) { + // this.add(text); + // } + + // if (labelLine) { + // labelLines.push(labelLine); + // this.add(labelLine); + // } + + // // enter的时长如果不是大于0,那么直接跳过动画 + // this._animationConfig.enter.duration > 0 && + // relatedGraphic.once('animate-bind', a => { + // // text和labelLine共用一个from + // text.setAttributes(from); + // labelLine && labelLine.setAttributes(from); + // const listener = this._afterRelatedGraphicAttributeUpdate( + // text, + // texts, + // labelLine, + // labelLines, + // index, + // relatedGraphic, + // to, + // this._animationConfig.enter as ILabelEnterAnimation + // ); + // relatedGraphic.on('afterAttributeUpdate', listener); + // }); + // } + // } else { + + // } } protected _updateLabel(prevLabel: LabelContent, currentLabel: LabelContent) { const { text: prevText, labelLine: prevLabelLine } = prevLabel; const { text: curText, labelLine: curLabelLine } = currentLabel; - if (this._enableAnimation !== false && this._animationConfig.update !== false) { - const { duration, easing } = this._animationConfig.update; - updateAnimation(prevText, curText, this._animationConfig.update); - if (prevLabelLine && curLabelLine) { - prevLabel.labelLine.animate().to(curLabelLine.attribute, duration, easing); - } - } else { + + if (this._enableAnimation === false || !this._animationConfig.update) { prevLabel.text.setAttributes(curText.attribute as any); if (prevLabelLine && curLabelLine) { prevLabel.labelLine.setAttributes(curLabelLine.attribute); } + } else { + this._runUpdateAnimation(prevLabel, currentLabel); } } @@ -885,14 +947,46 @@ export class LabelBase extends AbstractComponent { }; if (this._enableAnimation !== false && this._animationConfig.exit !== false) { - const { duration, easing } = this._animationConfig.exit; + // const { duration, easing } = this._animationConfig.exit; + // textMap.forEach(label => { + // label.text + // ?.animate() + // .to(getAnimationAttributes(label.text.attribute, 'fadeOut').to, duration, easing) + // .onEnd(() => { + // removeLabelAndLine(label); + // }); + // }); textMap.forEach(label => { - label.text - ?.animate() - .to(getAnimationAttributes(label.text.attribute, 'fadeOut').to, duration, easing) - .onEnd(() => { + label.text.applyAnimationState( + ['exit'], + [ + { + name: 'exit', + animation: { + type: 'fadeOut', + ...this._animationConfig.exit + } + } + ], + () => { removeLabelAndLine(label); - }); + } + ); + label.labelLine?.applyAnimationState( + ['exit'], + [ + { + name: 'exit', + animation: { + type: 'fadeOut', + ...this._animationConfig.exit + } + } + ], + () => { + // removeLabelAndLine(label); + } + ); }); } else { textMap.forEach(label => { @@ -927,99 +1021,99 @@ export class LabelBase extends AbstractComponent { } }; - protected _syncStateWithRelatedGraphic(relatedGraphic: IGraphic) { - if (this.attribute.syncState && relatedGraphic) { - relatedGraphic.on('afterAttributeUpdate', this._handleRelatedGraphicSetState); - } - } + // protected _syncStateWithRelatedGraphic(relatedGraphic: IGraphic) { + // if (this.attribute.syncState && relatedGraphic) { + // relatedGraphic.on('afterAttributeUpdate', this._handleRelatedGraphicSetState); + // } + // } // 默认labelLine和text共用相同动画属性 - protected _afterRelatedGraphicAttributeUpdate( - text: IText | IRichText, - texts: (IText | IRichText)[], - labelLine: ILine, - labelLines: ILine[], - index: number, - relatedGraphic: IGraphic, - to: any, - { mode, duration, easing, delay }: ILabelAnimation - ) { - // TODO: 跟随动画 - const listener = (event: any) => { - const { detail } = event; - if (!detail) { - return {}; - } - const step = detail.animationState?.step; - const isValidAnimateState = - detail.type === AttributeUpdateType.ANIMATE_UPDATE && - step && - // 不是第一个wait - !(step.type === 'wait' && step.prev?.type == null); - - if (!isValidAnimateState) { - return {}; - } - // const prevStep = step.prev; - // if (prevStep && prevStep.type === 'wait' && prevStep.prev?.type == null) { - // delay = delay ?? step.position; - // } - if (detail.type === AttributeUpdateType.ANIMATE_END) { - text.setAttributes(to); - labelLine && labelLine.setAttributes(to); - return; - } - - const onStart = () => { - if (relatedGraphic) { - relatedGraphic.onAnimateBind = undefined; - relatedGraphic.removeEventListener('afterAttributeUpdate', listener); - } - }; - - switch (mode) { - case 'after': - // 3. 当前关联图元的动画播放结束后 - if (detail.animationState.end) { - text.animate({ onStart }).wait(delay).to(to, duration, easing); - labelLine && labelLine.animate().wait(delay).to(to, duration, easing); - } - break; - case 'after-all': - // 2. 所有完成后才开始; - if (index === texts.length - 1) { - if (detail.animationState.end) { - texts.forEach(t => { - t.animate({ onStart }).wait(delay).to(to, duration, easing); - }); - labelLines.forEach(t => { - t.animate().wait(delay).to(to, duration, easing); - }); - } - } - break; - case 'same-time': - default: - if (this._isCollectionBase) { - const point = this._idToPoint.get((text.attribute as LabelItem).id); - if ( - point && - (!text.animates || !text.animates.has('label-animate')) && - relatedGraphic.containsPoint(point.x, point.y, IContainPointMode.LOCAL, this.stage?.getPickerService()) - ) { - text.animate({ onStart }).wait(delay).to(to, duration, easing); - labelLine && labelLine.animate().wait(delay).to(to, duration, easing); - } - } else if (detail.animationState.isFirstFrameOfStep) { - text.animate({ onStart }).wait(delay).to(to, duration, easing); - labelLine && labelLine.animate().wait(delay).to(to, duration, easing); - } - - break; - } - }; - return listener; - } + // protected _afterRelatedGraphicAttributeUpdate( + // text: IText | IRichText, + // texts: (IText | IRichText)[], + // labelLine: ILine, + // labelLines: ILine[], + // index: number, + // relatedGraphic: IGraphic, + // to: any, + // { mode, duration, easing, delay }: ILabelAnimation + // ) { + // // TODO: 跟随动画 + // const listener = (event: any) => { + // const { detail } = event; + // if (!detail) { + // return {}; + // } + // const step = detail.animationState?.step; + // const isValidAnimateState = + // detail.type === AttributeUpdateType.ANIMATE_UPDATE && + // step && + // // 不是第一个wait + // !(step.type === 'wait' && step.prev?.type == null); + + // if (!isValidAnimateState) { + // return {}; + // } + // // const prevStep = step.prev; + // // if (prevStep && prevStep.type === 'wait' && prevStep.prev?.type == null) { + // // delay = delay ?? step.position; + // // } + // if (detail.type === AttributeUpdateType.ANIMATE_END) { + // text.setAttributes(to); + // labelLine && labelLine.setAttributes(to); + // return; + // } + + // const onStart = () => { + // if (relatedGraphic) { + // relatedGraphic.onAnimateBind = undefined; + // relatedGraphic.removeEventListener('afterAttributeUpdate', listener); + // } + // }; + + // switch (mode) { + // case 'after': + // // 3. 当前关联图元的动画播放结束后 + // if (detail.animationState.end) { + // text.animate({ onStart }).wait(delay).to(to, duration, easing); + // labelLine && labelLine.animate().wait(delay).to(to, duration, easing); + // } + // break; + // case 'after-all': + // // 2. 所有完成后才开始; + // if (index === texts.length - 1) { + // if (detail.animationState.end) { + // texts.forEach(t => { + // t.animate({ onStart }).wait(delay).to(to, duration, easing); + // }); + // labelLines.forEach(t => { + // t.animate().wait(delay).to(to, duration, easing); + // }); + // } + // } + // break; + // case 'same-time': + // default: + // if (this._isCollectionBase) { + // const point = this._idToPoint.get((text.attribute as LabelItem).id); + // if ( + // point && + // (!text.animates || !text.animates.has('label-animate')) && + // relatedGraphic.containsPoint(point.x, point.y, IContainPointMode.LOCAL, this.stage?.getPickerService()) + // ) { + // text.animate({ onStart }).wait(delay).to(to, duration, easing); + // labelLine && labelLine.animate().wait(delay).to(to, duration, easing); + // } + // } else if (detail.animationState.isFirstFrameOfStep) { + // text.animate({ onStart }).wait(delay).to(to, duration, easing); + // labelLine && labelLine.animate().wait(delay).to(to, duration, easing); + // } + + // break; + // } + // }; + // return listener; + // } protected _smartInvert(labels: (IText | IRichText)[]) { const option = (isObject(this.attribute.smartInvert) ? this.attribute.smartInvert : {}) as SmartInvertAttrs; diff --git a/packages/vrender-core/src/core/stage.ts b/packages/vrender-core/src/core/stage.ts index 39579d582..8abeb6ad8 100644 --- a/packages/vrender-core/src/core/stage.ts +++ b/packages/vrender-core/src/core/stage.ts @@ -289,7 +289,7 @@ export class Stage extends Group implements IStage { this.supportInteractiveLayer = params.interactiveLayer !== false; if (!params.optimize) { params.optimize = { - animateMode: 'performance' + tickRenderMode: 'performance' }; } this.optmize(params.optimize); From 283440661bfe5a01cae2bb58364d9da81ede611f Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 2 Apr 2025 17:32:34 +0800 Subject: [PATCH 062/179] feat: support label animate mode, fix issue with delay and delayAfter of function --- .../src/component/component-animator.ts | 53 +---- .../src/custom/custom-animate.ts | 4 +- .../src/custom/label-animate.ts | 50 ----- .../src/custom/label-item-animate.ts | 4 + .../src/custom/poptip-animate.ts | 4 + .../vrender-animate/src/custom/register.ts | 3 - .../src/executor/animate-executor.ts | 22 ++- packages/vrender-animate/src/index.ts | 3 +- .../src/animation/label-animate.ts | 104 ++++++++++ .../src/label/animate/animate.ts | 123 +----------- packages/vrender-components/src/label/base.ts | 184 +++--------------- .../vrender-components/src/label/register.ts | 6 + 12 files changed, 166 insertions(+), 394 deletions(-) delete mode 100644 packages/vrender-animate/src/custom/label-animate.ts create mode 100644 packages/vrender-components/src/animation/label-animate.ts diff --git a/packages/vrender-animate/src/component/component-animator.ts b/packages/vrender-animate/src/component/component-animator.ts index a23fe57fc..21b14edd3 100644 --- a/packages/vrender-animate/src/component/component-animator.ts +++ b/packages/vrender-animate/src/component/component-animator.ts @@ -9,7 +9,6 @@ import type { IAnimationConfig, IAnimationTypeConfig, IAnimationTimeline } from interface IAnimationTask { graphic: IGraphic; config: IAnimationConfig; - delay: number; animate?: IAnimate; } @@ -42,7 +41,7 @@ export class ComponentAnimator { * @param delay Optional delay before starting this animation (in ms) * @returns This ComponentAnimator for chaining */ - animate(graphic: IGraphic, config: IAnimationConfig, delay: number = 0): ComponentAnimator { + animate(graphic: IGraphic, config: IAnimationConfig): ComponentAnimator { if (this.started) { console.warn('Cannot add animations after animation has started'); return this; @@ -50,44 +49,9 @@ export class ComponentAnimator { this.tasks.push({ graphic, - config, - delay + config }); - // Calculate total duration including delay - let configDuration = 300; // Default duration - let configDelay = 0; // Default delay - - // Extract duration and delay based on config type - if ('duration' in config) { - // TypeConfig case - const typeConfig = config as IAnimationTypeConfig; - configDuration = typeof typeConfig.duration === 'number' ? typeConfig.duration : 300; - configDelay = typeof typeConfig.delay === 'number' ? typeConfig.delay : 0; - } else if ('timeSlices' in config) { - // Timeline case - calculate total duration from all time slices - const timelineConfig = config as IAnimationTimeline; - const timeSlices = Array.isArray(timelineConfig.timeSlices) - ? timelineConfig.timeSlices - : [timelineConfig.timeSlices]; - - let totalTimeSliceDuration = 0; - timeSlices.forEach(slice => { - const sliceDuration = typeof slice.duration === 'number' ? slice.duration : 300; - const sliceDelay = typeof slice.delay === 'number' ? slice.delay : 0; - const sliceDelayAfter = typeof slice.delayAfter === 'number' ? slice.delayAfter : 0; - totalTimeSliceDuration += sliceDuration + sliceDelay + sliceDelayAfter; - }); - - configDuration = totalTimeSliceDuration; - } - - const duration = configDuration + configDelay + delay; - - if (duration > this.totalDuration) { - this.totalDuration = duration; - } - return this; } @@ -157,16 +121,9 @@ export class ComponentAnimator { } }); - // Start animation after delay - if (task.delay > 0) { - setTimeout(() => { - const animate = executor.executeItem(task.config, task.graphic); - task.animate = animate; - }, task.delay); - } else { - const animate = executor.executeItem(task.config, task.graphic); - task.animate = animate; - } + const animate = executor.executeItem(task.config, task.graphic); + task.animate = animate; + this.totalDuration = Math.max(this.totalDuration, animate.getStartTime() + animate.getDuration()); }); return this; diff --git a/packages/vrender-animate/src/custom/custom-animate.ts b/packages/vrender-animate/src/custom/custom-animate.ts index b8be2ef0e..590d793bd 100644 --- a/packages/vrender-animate/src/custom/custom-animate.ts +++ b/packages/vrender-animate/src/custom/custom-animate.ts @@ -41,8 +41,10 @@ export abstract class ACustomAnimate extends Step implements ICustomAnimate { export abstract class AComponentAnimate extends ACustomAnimate { protected _animator: ComponentAnimator; - onFirstRun(): void { + completeBind(animator: ComponentAnimator): void { + this.setStartTime(0); this._animator && this._animator.start(); + this.setDuration(animator.getDuration()); } stop(): void { diff --git a/packages/vrender-animate/src/custom/label-animate.ts b/packages/vrender-animate/src/custom/label-animate.ts deleted file mode 100644 index bf15e9029..000000000 --- a/packages/vrender-animate/src/custom/label-animate.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { AComponentAnimate } from './custom-animate'; -import { createComponentAnimator } from '../component'; - -/** - * LabelUpdate class handles the update animation for Label components - */ -export class LabelUpdate extends AComponentAnimate { - onBind(): void { - const animator = createComponentAnimator(this.target); - this._animator = animator; - const duration = this.duration; - const easing = this.easing; - - const { prevText, curText, prevLabelLine, curLabelLine } = this.params; - const diff: Record = {}; - - for (const key in curText.attribute) { - if (prevText.attribute[key] !== curText.attribute[key]) { - diff[key] = curText.attribute[key]; - } - } - - const { text, ...rest } = diff; - - animator.animate(prevText, { - type: 'to', - to: rest, - duration, - easing - }); - - animator.animate(prevText, { - type: 'increaseCount', - to: { - text: curText.attribute.text - }, - duration, - easing - }); - - if (prevLabelLine) { - animator.animate(prevLabelLine, { - type: 'to', - to: curLabelLine.attribute, - duration, - easing - }); - } - } -} diff --git a/packages/vrender-animate/src/custom/label-item-animate.ts b/packages/vrender-animate/src/custom/label-item-animate.ts index 527e6ec07..9ab6b0499 100644 --- a/packages/vrender-animate/src/custom/label-item-animate.ts +++ b/packages/vrender-animate/src/custom/label-item-animate.ts @@ -140,6 +140,8 @@ export class LabelItemAppear extends AComponentAnimate { }); }); } + + this.completeBind(animator); } } @@ -223,5 +225,7 @@ export class LabelItemDisappear extends AComponentAnimate { easing }); } + + this.completeBind(animator); } } diff --git a/packages/vrender-animate/src/custom/poptip-animate.ts b/packages/vrender-animate/src/custom/poptip-animate.ts index 87b6739c7..b226df45b 100644 --- a/packages/vrender-animate/src/custom/poptip-animate.ts +++ b/packages/vrender-animate/src/custom/poptip-animate.ts @@ -82,6 +82,8 @@ export class PoptipAppear extends AComponentAnimate { ] }); } + + this.completeBind(animator); } } @@ -103,5 +105,7 @@ export class PoptipDisappear extends AComponentAnimate { duration, easing }); + + this.completeBind(animator); } } diff --git a/packages/vrender-animate/src/custom/register.ts b/packages/vrender-animate/src/custom/register.ts index 2d295b6c0..cdf3b7c59 100644 --- a/packages/vrender-animate/src/custom/register.ts +++ b/packages/vrender-animate/src/custom/register.ts @@ -14,7 +14,6 @@ import { } from './growPoints'; import { GrowRadiusIn, GrowRadiusOut } from './growRadius'; import { GrowWidthIn, GrowWidthOut } from './growWidth'; -import { LabelUpdate } from './label-animate'; import { LabelItemAppear, LabelItemDisappear } from './label-item-animate'; import { IncreaseCount } from './number'; import { PoptipAppear, PoptipDisappear } from './poptip-animate'; @@ -57,6 +56,4 @@ export const registerCustomAnimate = () => { // Poptip animations AnimateExecutor.registerBuiltInAnimate('poptipAppear', PoptipAppear); AnimateExecutor.registerBuiltInAnimate('poptipDisappear', PoptipDisappear); - // Label update animation - AnimateExecutor.registerBuiltInAnimate('labelUpdate', LabelUpdate); }; diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index d1805654f..949691e83 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -307,7 +307,7 @@ export class AnimateExecutor implements IAnimateExecutor { const animate = graphic.animate() as unknown as IAnimate; animate.priority = priority; - const delayValue = delay as number; + const delayValue = isFunction(delay) ? delay(graphic.context?.data?.[0], graphic, {}) : delay; // 如果设置了indexKey,则使用indexKey作为index const datum = graphic.context?.data?.[0]; @@ -317,14 +317,15 @@ export class AnimateExecutor implements IAnimateExecutor { } // 设置开始时间 - animate.startAt(startTime as number); + animate.startAt((startTime as number) + delayValue); const wait = index * oneByOneDelay; wait > 0 && animate.wait(wait); - // 添加延迟 - if (delayValue > 0) { - animate.wait(delayValue); - } + // 放到startAt中,否则label无法确定主图元何时开始 + // // 添加延迟 + // if (delayValue > 0) { + // animate.wait(delayValue); + // } // 根据 channel 配置创建属性对象 const props = params.to ?? this.createPropsFromChannel(channel, graphic); @@ -347,8 +348,9 @@ export class AnimateExecutor implements IAnimateExecutor { } // 添加后延迟 - if ((delayAfter as number) > 0) { - animate.wait(delayAfter as number); + const delayAfterValue = isFunction(delayAfter) ? delayAfter(graphic.context?.data?.[0], graphic, {}) : delayAfter; + if (delayAfterValue > 0) { + animate.wait(delayAfterValue as number); } // 设置循环 @@ -465,8 +467,8 @@ export class AnimateExecutor implements IAnimateExecutor { // 解析时间参数 // const durationValue = duration as number; - const delayValue = delay as number; - const delayAfterValue = delayAfter as number; + const delayValue = isFunction(delay) ? delay(graphic.context?.data?.[0], graphic, {}) : delay; + const delayAfterValue = isFunction(delayAfter) ? delayAfter(graphic.context?.data?.[0], graphic, {}) : delayAfter; // 添加延迟 if (delayValue > 0) { diff --git a/packages/vrender-animate/src/index.ts b/packages/vrender-animate/src/index.ts index 91d926a29..b1361adb4 100644 --- a/packages/vrender-animate/src/index.ts +++ b/packages/vrender-animate/src/index.ts @@ -14,7 +14,8 @@ export { Step } from './step'; // 导出工具函数 export * from './utils/easing-func'; export { registerAnimate } from './register'; -export { ACustomAnimate } from './custom/custom-animate'; +export { ACustomAnimate, AComponentAnimate } from './custom/custom-animate'; +export { ComponentAnimator } from './component/component-animator'; export { IncreaseCount } from './custom/number'; export { InputText } from './custom/input-text'; export { ClipGraphicAnimate, ClipAngleAnimate, ClipRadiusAnimate, ClipDirectionAnimate } from './custom/clip-graphic'; diff --git a/packages/vrender-components/src/animation/label-animate.ts b/packages/vrender-components/src/animation/label-animate.ts new file mode 100644 index 000000000..f08137032 --- /dev/null +++ b/packages/vrender-components/src/animation/label-animate.ts @@ -0,0 +1,104 @@ +import { AComponentAnimate, AnimateExecutor, createComponentAnimator } from '@visactor/vrender-animate'; + +/** + * LabelUpdate class handles the update animation for Label components + */ +export class LabelUpdate extends AComponentAnimate { + onBind(): void { + const animator = createComponentAnimator(this.target); + this._animator = animator; + const duration = this.duration; + const easing = this.easing; + + const { prevText, curText, prevLabelLine, curLabelLine } = this.params; + const diff: Record = {}; + + for (const key in curText.attribute) { + if (prevText.attribute[key] !== curText.attribute[key]) { + diff[key] = curText.attribute[key]; + } + } + + const { text, ...rest } = diff; + + animator.animate(prevText, { + type: 'to', + to: rest, + duration, + easing + }); + + animator.animate(prevText, { + type: 'increaseCount', + to: { + text: curText.attribute.text + }, + duration, + easing + }); + + if (prevLabelLine) { + animator.animate(prevLabelLine, { + type: 'to', + to: curLabelLine.attribute, + duration, + easing + }); + } + + this._animator && this._animator.start(); + } +} + +export class LabelEnter extends AComponentAnimate { + onBind(): void { + const animator = createComponentAnimator(this.target); + this._animator = animator; + const duration = this.duration; + const easing = this.easing; + + const { relatedGraphic, relatedGraphics, config } = this.params; + const { mode, type = 'fadeIn' } = config; + + const target = this.target; + + let startTime = 0; + + if (mode === 'after') { + relatedGraphic.animates && + relatedGraphic.animates.forEach((animate: any) => { + startTime = Math.max(startTime, animate.getStartTime() + animate.getTotalDuration()); + }); + } else if (mode === 'after-all') { + relatedGraphics && + relatedGraphics.forEach((graphic: any) => { + graphic.animates && + graphic.animates.forEach((animate: any) => { + startTime = Math.max(startTime, animate.getStartTime() + animate.getTotalDuration()); + }); + }); + } else { + // 'same-time' + relatedGraphic.animates && + relatedGraphic.animates.forEach((animate: any) => { + startTime = Math.max(startTime, animate.getStartTime()); + }); + } + + animator.animate(target, { + ...config, + duration, + easing, + startTime, + type + }); + + this.completeBind(animator); + } +} + +export function registerLabelAnimate() { + // Label update animation + AnimateExecutor.registerBuiltInAnimate('labelUpdate', LabelUpdate); + AnimateExecutor.registerBuiltInAnimate('labelEnter', LabelEnter); +} diff --git a/packages/vrender-components/src/label/animate/animate.ts b/packages/vrender-components/src/label/animate/animate.ts index b5dc14e3d..f4d2d0418 100644 --- a/packages/vrender-components/src/label/animate/animate.ts +++ b/packages/vrender-components/src/label/animate/animate.ts @@ -1,124 +1,5 @@ -import type { IText, ITextGraphicAttribute, EasingType } from '@visactor/vrender-core'; -import { IncreaseCount } from '@visactor/vrender-animate'; -import type { ILabelAnimation, ILabelUpdateAnimation, ILabelUpdateChannelAnimation, LabelContent } from '../type'; -import { array, isArray, isEmpty, isValidNumber } from '@visactor/vutils'; - -const fadeIn = (textAttribute: ITextGraphicAttribute = {}) => { - return { - from: { - opacity: 0, - fillOpacity: 0, - strokeOpacity: 0 - }, - to: { - opacity: textAttribute.opacity ?? 1, - fillOpacity: textAttribute.fillOpacity ?? 1, - strokeOpacity: textAttribute.strokeOpacity ?? 1 - } - }; -}; - -const fadeOut = (textAttribute: ITextGraphicAttribute = {}) => { - return { - from: { - opacity: textAttribute.opacity ?? 1, - fillOpacity: textAttribute.fillOpacity ?? 1, - strokeOpacity: textAttribute.strokeOpacity ?? 1 - }, - to: { - opacity: 0, - fillOpacity: 0, - strokeOpacity: 0 - } - }; -}; - -const animationEffects = { fadeIn, fadeOut }; - -export function getAnimationAttributes( - textAttribute: ITextGraphicAttribute, - type: 'fadeIn' | 'fadeOut' -): { - from: any; - to: any; -} { - return animationEffects[type]?.(textAttribute) ?? { from: {}, to: {} }; -} - -export function updateAnimation( - prev: LabelContent['text'], - next: LabelContent['text'], - animationConfig: ILabelUpdateAnimation | ILabelUpdateChannelAnimation[] -) { - const changeAttributes = (prev: LabelContent['text'], next: LabelContent['text']) => { - const changed = {}; - for (const key in next.attribute) { - if (prev.attribute[key] !== next.attribute[key]) { - changed[key] = next.attribute[key]; - } - } - return changed; - }; - - if (!isArray(animationConfig)) { - const { duration, easing, increaseEffect = true } = animationConfig; - - prev.animate().to(changeAttributes(prev, next), duration, easing); - if (increaseEffect && prev.type === 'text' && next.type === 'text') { - playIncreaseCount(prev as IText, next as IText, duration, easing); - } - return; - } - - animationConfig.forEach(cfg => { - const { duration, easing, increaseEffect = true, channel } = cfg; - const { to } = update(prev, next, channel, cfg.options); - if (!isEmpty(to)) { - prev.animate().to(changeAttributes(prev, next), duration, easing); - } - - if (increaseEffect && prev.type === 'text' && next.type === 'text') { - playIncreaseCount(prev as IText, next as IText, duration, easing); - } - }); -} - -export const update = ( - prev: LabelContent['text'], - next: LabelContent['text'], - channel?: string[], - options?: ILabelUpdateChannelAnimation['options'] -) => { - const from = Object.assign({}, prev.attribute); - const to = Object.assign({}, next.attribute); - array(options?.excludeChannels).forEach(key => { - delete to[key]; - }); - Object.keys(to).forEach(key => { - if (channel && !channel.includes(key)) { - delete to[key]; - } - }); - return { from, to }; -}; - -export function playIncreaseCount(prev: IText, next: IText, duration: number, easing: EasingType) { - if ( - prev.attribute.text !== next.attribute.text && - isValidNumber(Number(prev.attribute.text) * Number(next.attribute.text)) - ) { - prev - .animate() - .play( - new IncreaseCount( - { text: prev.attribute.text as string }, - { text: next.attribute.text as string }, - duration, - easing - ) as any - ); - } -} +import type { EasingType } from '@visactor/vrender-core'; +import type { ILabelAnimation } from '../type'; export const DefaultLabelAnimation: ILabelAnimation = { mode: 'same-time', diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index 4d8a2ef1c..adcc5b4a6 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -42,7 +42,6 @@ import { bitmapTool, boundToRange, canPlace, clampText, place } from './overlap' import type { BaseLabelAttrs, OverlapAttrs, - ILabelAnimation, LabelItem, SmartInvertAttrs, ILabelEnterAnimation, @@ -52,7 +51,7 @@ import type { ShiftYStrategy, Strategy } from './type'; -import { DefaultLabelAnimation, getAnimationAttributes, updateAnimation } from './animate/animate'; +import { DefaultLabelAnimation } from './animate/animate'; import { connectLineBetweenBounds, getPointsOfLineArea } from './util'; import type { ComponentOptions } from '../interface'; import { loadLabelComponent } from './register'; @@ -690,7 +689,7 @@ export class LabelBase extends AbstractComponent { hasPlace = place( bmpTool, bitmap, - strategy[j], + (strategy as any)[j], this.attribute, text as Text, this._isCollectionBase @@ -807,7 +806,7 @@ export class LabelBase extends AbstractComponent { this._graphicToText = currentTextMap; } - protected runEnterAnimation(text: IText | IRichText) { + protected runEnterAnimation(text: IText | IRichText, labelLine?: ILine) { if (this._enableAnimation === false || !this._animationConfig.enter) { return; } @@ -815,20 +814,28 @@ export class LabelBase extends AbstractComponent { const relatedGraphic = this.getRelatedGraphic(text.attribute); const { enter } = this._animationConfig; - text.applyAnimationState( - ['enter'], - [ - { - name: 'enter', - animation: { - ...enter, - selfOnly: true, - customParameters: { - relatedGraphic + [text, labelLine].filter(Boolean).forEach(item => + item.applyAnimationState( + ['enter'], + [ + { + name: 'enter', + animation: { + ...enter, + type: 'labelEnter', + selfOnly: true, + customParameters: { + relatedGraphic, + relatedGraphics: this._idToGraphic, + config: { + ...enter, + type: item === text ? enter.type : 'clipIn' + } + } } } - } - ] + ] + ) ); } @@ -861,11 +868,6 @@ export class LabelBase extends AbstractComponent { } ] ); - - // updateAnimation(prevText, curText, this._animationConfig.update); - // if (prevLabelLine && curLabelLine) { - // prevLabel.labelLine.animate().to(curLabelLine.attribute, duration, easing); - // } } protected _addLabel( @@ -886,42 +888,7 @@ export class LabelBase extends AbstractComponent { this.add(labelLine); } - this.runEnterAnimation(text); - - // if (this._enableAnimation !== false && this._animationConfig.enter !== false) { - // if (relatedGraphic) { - // const { from, to } = getAnimationAttributes(text.attribute, 'fadeIn'); - // if (text) { - // this.add(text); - // } - - // if (labelLine) { - // labelLines.push(labelLine); - // this.add(labelLine); - // } - - // // enter的时长如果不是大于0,那么直接跳过动画 - // this._animationConfig.enter.duration > 0 && - // relatedGraphic.once('animate-bind', a => { - // // text和labelLine共用一个from - // text.setAttributes(from); - // labelLine && labelLine.setAttributes(from); - // const listener = this._afterRelatedGraphicAttributeUpdate( - // text, - // texts, - // labelLine, - // labelLines, - // index, - // relatedGraphic, - // to, - // this._animationConfig.enter as ILabelEnterAnimation - // ); - // relatedGraphic.on('afterAttributeUpdate', listener); - // }); - // } - // } else { - - // } + this.runEnterAnimation(text, labelLine); } protected _updateLabel(prevLabel: LabelContent, currentLabel: LabelContent) { @@ -947,15 +914,6 @@ export class LabelBase extends AbstractComponent { }; if (this._enableAnimation !== false && this._animationConfig.exit !== false) { - // const { duration, easing } = this._animationConfig.exit; - // textMap.forEach(label => { - // label.text - // ?.animate() - // .to(getAnimationAttributes(label.text.attribute, 'fadeOut').to, duration, easing) - // .onEnd(() => { - // removeLabelAndLine(label); - // }); - // }); textMap.forEach(label => { label.text.applyAnimationState( ['exit'], @@ -1021,100 +979,6 @@ export class LabelBase extends AbstractComponent { } }; - // protected _syncStateWithRelatedGraphic(relatedGraphic: IGraphic) { - // if (this.attribute.syncState && relatedGraphic) { - // relatedGraphic.on('afterAttributeUpdate', this._handleRelatedGraphicSetState); - // } - // } - - // 默认labelLine和text共用相同动画属性 - // protected _afterRelatedGraphicAttributeUpdate( - // text: IText | IRichText, - // texts: (IText | IRichText)[], - // labelLine: ILine, - // labelLines: ILine[], - // index: number, - // relatedGraphic: IGraphic, - // to: any, - // { mode, duration, easing, delay }: ILabelAnimation - // ) { - // // TODO: 跟随动画 - // const listener = (event: any) => { - // const { detail } = event; - // if (!detail) { - // return {}; - // } - // const step = detail.animationState?.step; - // const isValidAnimateState = - // detail.type === AttributeUpdateType.ANIMATE_UPDATE && - // step && - // // 不是第一个wait - // !(step.type === 'wait' && step.prev?.type == null); - - // if (!isValidAnimateState) { - // return {}; - // } - // // const prevStep = step.prev; - // // if (prevStep && prevStep.type === 'wait' && prevStep.prev?.type == null) { - // // delay = delay ?? step.position; - // // } - // if (detail.type === AttributeUpdateType.ANIMATE_END) { - // text.setAttributes(to); - // labelLine && labelLine.setAttributes(to); - // return; - // } - - // const onStart = () => { - // if (relatedGraphic) { - // relatedGraphic.onAnimateBind = undefined; - // relatedGraphic.removeEventListener('afterAttributeUpdate', listener); - // } - // }; - - // switch (mode) { - // case 'after': - // // 3. 当前关联图元的动画播放结束后 - // if (detail.animationState.end) { - // text.animate({ onStart }).wait(delay).to(to, duration, easing); - // labelLine && labelLine.animate().wait(delay).to(to, duration, easing); - // } - // break; - // case 'after-all': - // // 2. 所有完成后才开始; - // if (index === texts.length - 1) { - // if (detail.animationState.end) { - // texts.forEach(t => { - // t.animate({ onStart }).wait(delay).to(to, duration, easing); - // }); - // labelLines.forEach(t => { - // t.animate().wait(delay).to(to, duration, easing); - // }); - // } - // } - // break; - // case 'same-time': - // default: - // if (this._isCollectionBase) { - // const point = this._idToPoint.get((text.attribute as LabelItem).id); - // if ( - // point && - // (!text.animates || !text.animates.has('label-animate')) && - // relatedGraphic.containsPoint(point.x, point.y, IContainPointMode.LOCAL, this.stage?.getPickerService()) - // ) { - // text.animate({ onStart }).wait(delay).to(to, duration, easing); - // labelLine && labelLine.animate().wait(delay).to(to, duration, easing); - // } - // } else if (detail.animationState.isFirstFrameOfStep) { - // text.animate({ onStart }).wait(delay).to(to, duration, easing); - // labelLine && labelLine.animate().wait(delay).to(to, duration, easing); - // } - - // break; - // } - // }; - // return listener; - // } - protected _smartInvert(labels: (IText | IRichText)[]) { const option = (isObject(this.attribute.smartInvert) ? this.attribute.smartInvert : {}) as SmartInvertAttrs; const { textType, contrastRatiosThreshold, alternativeColors, mode, interactInvertType } = option; diff --git a/packages/vrender-components/src/label/register.ts b/packages/vrender-components/src/label/register.ts index dceb201e5..d0630b15c 100644 --- a/packages/vrender-components/src/label/register.ts +++ b/packages/vrender-components/src/label/register.ts @@ -1,8 +1,14 @@ import { registerGroup, registerLine, registerRichtext, registerText } from '@visactor/vrender-kits'; +import { registerLabelAnimate } from '../animation/label-animate'; export function loadLabelComponent() { registerGroup(); registerText(); registerRichtext(); registerLine(); + registerLabelAnimate(); +} + +export function loadLabelAnimate() { + registerLabelAnimate(); } From 4c6d7b4c1d550cf1471d4d1850a585b220fccb4d Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 2 Apr 2025 21:18:36 +0800 Subject: [PATCH 063/179] feat: support input-richtext --- .../vrender-animate/src/animate-extension.ts | 10 - .../vrender-animate/src/custom/register.ts | 5 + .../src/custom/richtext/input-richtext.ts | 205 ++++++++++++ .../src/custom/richtext/slide-richtext.ts | 0 packages/vrender-core/src/graphic/graphic.ts | 14 +- .../browser/src/pages/custom-animate.ts | 303 ++++++++++++++++++ .../__tests__/browser/src/pages/index.ts | 4 + 7 files changed, 529 insertions(+), 12 deletions(-) create mode 100644 packages/vrender-animate/src/custom/richtext/input-richtext.ts create mode 100644 packages/vrender-animate/src/custom/richtext/slide-richtext.ts create mode 100644 packages/vrender/__tests__/browser/src/pages/custom-animate.ts diff --git a/packages/vrender-animate/src/animate-extension.ts b/packages/vrender-animate/src/animate-extension.ts index 112f857b8..c23d0ddde 100644 --- a/packages/vrender-animate/src/animate-extension.ts +++ b/packages/vrender-animate/src/animate-extension.ts @@ -20,16 +20,6 @@ export class AnimateExtension { declare animates: Map; - setAttributesAndPreventAnimate(attributes: Record) { - (this as any).setAttributes(attributes); - this.animates && - this.animates.forEach(animate => { - Object.keys(attributes).forEach(key => { - animate.preventAttr(key); - }); - }); - } - getAttributes(final: boolean = false) { if (final && this.finalAttribute) { return this.finalAttribute; diff --git a/packages/vrender-animate/src/custom/register.ts b/packages/vrender-animate/src/custom/register.ts index cdf3b7c59..0f752ea61 100644 --- a/packages/vrender-animate/src/custom/register.ts +++ b/packages/vrender-animate/src/custom/register.ts @@ -20,6 +20,8 @@ import { PoptipAppear, PoptipDisappear } from './poptip-animate'; import { ScaleIn, ScaleOut } from './scale'; import { State } from './state'; import { Update } from './update'; +import { InputText } from './input-text'; +import { InputRichText } from './richtext/input-richtext'; export const registerCustomAnimate = () => { // 基础动画 @@ -56,4 +58,7 @@ export const registerCustomAnimate = () => { // Poptip animations AnimateExecutor.registerBuiltInAnimate('poptipAppear', PoptipAppear); AnimateExecutor.registerBuiltInAnimate('poptipDisappear', PoptipDisappear); + // Text input animations + AnimateExecutor.registerBuiltInAnimate('inputText', InputText); + AnimateExecutor.registerBuiltInAnimate('inputRichText', InputRichText); }; diff --git a/packages/vrender-animate/src/custom/richtext/input-richtext.ts b/packages/vrender-animate/src/custom/richtext/input-richtext.ts new file mode 100644 index 000000000..bab12de41 --- /dev/null +++ b/packages/vrender-animate/src/custom/richtext/input-richtext.ts @@ -0,0 +1,205 @@ +import type { IAnimate, IStep } from '../../intreface/animate'; +import type { EasingType } from '../../intreface/easing'; +import { ACustomAnimate } from '../custom-animate'; +import type { IRichTextCharacter, IRichTextParagraphCharacter } from '@visactor/vrender-core'; +import { RichText } from '@visactor/vrender-core'; + +/** + * 富文本输入动画,实现类似打字机的字符逐个显示效果 + * 支持通过beforeText和afterText参数添加前缀和后缀 + * 支持通过showCursor参数显示光标,cursorChar自定义光标字符 + * 支持通过fadeInChars参数开启字符透明度渐变效果 + */ +export class InputRichText extends ACustomAnimate<{ textConfig: IRichTextCharacter[] }> { + declare valid: boolean; + + private fromTextConfig: IRichTextCharacter[] = []; + private toTextConfig: IRichTextCharacter[] = []; + private originalTextConfig: IRichTextCharacter[] = []; + private showCursor: boolean = false; + private cursorChar: string = '|'; + private blinkCursor: boolean = true; + private beforeText: string = ''; + private afterText: string = ''; + private fadeInChars: boolean = false; + private fadeInDuration: number = 0.3; // 透明度渐变持续时间,以动画总时长的比例表示 + + constructor( + from: { textConfig: IRichTextCharacter[] }, + to: { textConfig: IRichTextCharacter[] }, + duration: number, + easing: EasingType, + params?: { + showCursor?: boolean; + cursorChar?: string; + blinkCursor?: boolean; + beforeText?: string; + afterText?: string; + fadeInChars?: boolean; + fadeInDuration?: number; + } + ) { + super(from, to, duration, easing, params); + + // 配置光标相关选项 + if (params?.showCursor !== undefined) { + this.showCursor = params.showCursor; + } + if (params?.cursorChar !== undefined) { + this.cursorChar = params.cursorChar; + } + if (params?.blinkCursor !== undefined) { + this.blinkCursor = params.blinkCursor; + } + + // 配置前缀和后缀文本 + if (params?.beforeText !== undefined) { + this.beforeText = params.beforeText; + } + if (params?.afterText !== undefined) { + this.afterText = params.afterText; + } + + // 配置字符透明度渐变效果 + if (params?.fadeInChars !== undefined) { + this.fadeInChars = params.fadeInChars; + } + if (params?.fadeInDuration !== undefined) { + this.fadeInDuration = params.fadeInDuration; + } + } + + onFirstRun(): void { + const fromProps = this.getLastProps(); + const toProps = this.getEndProps(); + + // 存储原始配置 + this.originalTextConfig = toProps.textConfig ? [...toProps.textConfig] : []; + + // 初始化解析结果 + this.valid = true; + + // 确保to不为空 + if (!this.originalTextConfig || this.originalTextConfig.length === 0) { + this.valid = false; + return; + } + + // 将文本拆分为单个字符,使用RichText的静态方法 + this.fromTextConfig = + fromProps.textConfig && fromProps.textConfig.length > 0 + ? RichText.TransformTextConfig2SingleCharacter(fromProps.textConfig) + : []; + + this.toTextConfig = RichText.TransformTextConfig2SingleCharacter(this.originalTextConfig); + } + + onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { + super.onEnd(cb); + if (!cb) { + // 动画结束时,恢复原始textConfig + this.target.setAttribute('textConfig', this.originalTextConfig); + } + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + if (!this.valid) { + return; + } + + // 计算当前应该显示的字符数量 + const totalItems = this.toTextConfig.length; + const fromItems = this.fromTextConfig.length; + + // 计算文本显示比例上限 - 如果有渐变效果,需要为尾部字符的渐变留出时间 + // 例如,如果fadeInDuration为0.3,则文本显示部分最多占用动画时间的70% + const maxTextShowRatio = this.fadeInChars ? 1 - this.fadeInDuration : 1; + + // 确定当前应该显示多少个项目 + let currentLength: number; + + // 如果fromItems比totalItems长,则是删除动画,否则是添加动画 + if (fromItems > totalItems) { + // 删除文本动画(从多到少) + currentLength = Math.round(fromItems - (fromItems - totalItems) * ratio); + } else { + // 添加文本动画(从少到多)- 需要更快显示字符以便留出时间让最后的字符完成渐变 + if (this.fadeInChars) { + // 当ratio达到maxTextShowRatio时,应该已经显示全部文本 + const adjustedRatio = Math.min(1, ratio / maxTextShowRatio); + currentLength = Math.round(fromItems + (totalItems - fromItems) * adjustedRatio); + } else { + // 无渐变效果时,正常显示 + currentLength = Math.round(fromItems + (totalItems - fromItems) * ratio); + } + } + + // 构建当前要显示的textConfig + let currentTextConfig: IRichTextCharacter[]; + if (fromItems > totalItems) { + // 删除动画:显示from的前currentLength项 + currentTextConfig = this.fromTextConfig.slice(0, currentLength); + } else { + // 添加文本动画:显示to的前currentLength项,可能需要应用透明度 + currentTextConfig = this.toTextConfig.slice(0, currentLength).map((item, index) => { + // 如果启用了透明度渐变效果 + if (this.fadeInChars && 'text' in item) { + // 计算每个字符从出现到结束的渐变进度 + // 字符在特定时间点出现:出现时刻 = (index / totalItems) * maxTextShowRatio + // 当前时刻 = ratio + // 渐变持续时间 = fadeInDuration + // 渐变进度 = (当前时刻 - 出现时刻) / 渐变持续时间 + + const appearTime = (index / totalItems) * maxTextShowRatio; + const fadeProgress = (ratio - appearTime) / this.fadeInDuration; + + // 限制透明度在0-1范围内 + const opacity = Math.max(0, Math.min(1, fadeProgress)); + + // 如果是文本项,添加透明度 + return { + ...item, + opacity: opacity + }; + } + return item; + }); + } + + // 如果启用了光标 + if (this.showCursor && currentLength < totalItems) { + // 判断是否应该显示光标 + let shouldShowCursor = true; + + if (this.blinkCursor) { + // 闪烁效果:在动画期间,光标每半个周期闪烁一次 + const blinkRate = 0.1; // 光标闪烁频率(每10%动画进度闪烁一次) + shouldShowCursor = Math.floor(ratio / blinkRate) % 2 === 0; + } + + if (shouldShowCursor && currentTextConfig.length > 0) { + // 找到最后一个文本项,在其后添加光标 + const lastIndex = currentTextConfig.length - 1; + const lastItem = currentTextConfig[lastIndex]; + + if ('text' in lastItem) { + // 如果最后一项是文本,将光标添加到文本后面 + currentTextConfig[lastIndex] = { + ...lastItem, + text: String(lastItem.text) + this.cursorChar + }; + } else { + // 如果最后一项是非文本(如图片),添加一个只包含光标的新文本项 + const cursorItem: IRichTextParagraphCharacter = { + text: this.cursorChar, + fontSize: 16 // 使用默认字体大小,或者从context获取 + }; + currentTextConfig.push(cursorItem); + } + } + } + + // 更新富文本的textConfig属性 + this.target.setAttribute('textConfig', currentTextConfig); + } +} diff --git a/packages/vrender-animate/src/custom/richtext/slide-richtext.ts b/packages/vrender-animate/src/custom/richtext/slide-richtext.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index 57e26429a..fb804bbe1 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -648,6 +648,15 @@ export abstract class Graphic = Partial, forceUpdateTag: boolean = false, context?: ISetAttributeContext) { + this.setAttributes(params, forceUpdateTag, context); + this.animates && + this.animates.forEach(animate => { + Object.keys(params).forEach(key => { + animate.preventAttr(key); + }); + }); + } setAttributes(params: Partial, forceUpdateTag: boolean = false, context?: ISetAttributeContext) { params = @@ -959,6 +968,7 @@ export abstract class Graphic = Partial, stateNames: string[], hasAnimation?: boolean, isClear?: boolean) { + // 应用状态的时候要停掉动画 if (hasAnimation) { const keys = Object.keys(attrs); const noWorkAttrs = this.getNoWorkAnimateAttr(); @@ -997,10 +1007,10 @@ export abstract class Graphic = Partial void) { + const button = document.createElement('button'); + button.innerText = name; + button.style.height = '26px'; + container.appendChild(button); + button.addEventListener('click', () => { + stage && stage.release(); + stage = createStage({ + canvas: 'main', + width: 900, + height: 600, + background: 'pink', + disableDirtyBounds: false, + canvasControled: false, + autoRender: true + }); + cb(stage); + }); +} + +export const page = () => { + const container = document.createElement('div'); + container.style.width = '1000px'; + container.style.background = '#cecece'; + container.style.display = 'flex'; + container.style.flexDirection = 'row'; + container.style.gap = '3px'; + container.style.flexWrap = 'wrap'; + container.style.height = '120px'; + const canvas = document.getElementById('main'); + // 将container添加到canvas之前 + canvas.parentNode.insertBefore(container, canvas); + + // Test case for InputText animation + addCase('InputText Animation', container, stage => { + const text = createText({ + x: 20, + y: 80, + text: '', + fontSize: 20, + fill: '#000', + textBaseline: 'middle' + }); + + // Create a group and add the text to it + const group = createGroup({}); + group.add(text); + stage.defaultLayer.add(group); + stage.render(); + + // Create an AnimateExecutor and run the animation + const executor = new AnimateExecutor(group); + executor.execute({ + type: 'inputText', + to: { text: 'Hello, this is a typing animation!' }, + customParameters: { + showCursor: true, + cursorChar: '|', + blinkCursor: true + }, + duration: 2000, + easing: 'linear' + }); + }); + + // Test case for InputRichText animation + addCase('InputRichText Animation', container, stage => { + // Create a richText with empty textConfig + const richText = createRichText({ + x: 20, + y: 80, + width: 600, + height: 100, + textConfig: [], + textBaseline: 'middle' + }); + + // Create a group and add the richText to it + const group = createGroup({}); + group.add(richText); + stage.defaultLayer.add(group); + stage.render(); + + // Define the final textConfig with different styles + const finalTextConfig = [ + { + text: 'Rich ', + fontSize: 24, + fill: '#FF5500', + fontWeight: 'bold' + }, + { + text: 'Text ', + fontSize: 24, + fill: '#0055FF', + fontStyle: 'italic' + }, + { + text: 'Typing ', + fontSize: 24, + fill: '#33AA33' + }, + { + text: 'Animation!', + fontSize: 24, + fill: '#AA33AA', + textDecoration: 'underline' + } + ]; + + // Create an AnimateExecutor and run the animation + const executor = new AnimateExecutor(group); + executor.execute({ + type: 'inputRichText', + to: { textConfig: finalTextConfig }, + customParameters: { + showCursor: true, + cursorChar: '|', + blinkCursor: true, + // 启用字符透明度渐变效果 + fadeInChars: true, + fadeInDuration: 0.3 + }, + duration: 3000, + easing: 'linear' + }); + }); + + // Advanced example with a more complex RichText + addCase('Complex InputRichText Animation', container, stage => { + // Create a richText with empty textConfig + const richText = createRichText({ + x: 20, + y: 20, + width: 600, + height: 120, + textConfig: [], + textBaseline: 'top' + }); + + // Create a group and add the richText to it + const group = createGroup({}); + group.add(richText); + stage.defaultLayer.add(group); + stage.render(); + + // Define a more complex final textConfig + const complexTextConfig = [ + { + text: '富文本打字动画', + fontSize: 22, + fill: '#6600CC', + fontWeight: 'bold' + }, + { + text: '\n', // Line break + fontSize: 22 + }, + { + text: 'Supports ', + fontSize: 18, + fill: '#FF6600' + }, + { + text: 'multiple ', + fontSize: 18, + fill: '#0099FF', + fontStyle: 'italic' + }, + { + text: 'styles ', + fontSize: 18, + fill: '#33CC33', + textDecoration: 'underline' + }, + { + text: 'and formats!', + fontSize: 18, + fill: '#CC3366', + fontWeight: 'bold' + } + ]; + + // Create an AnimateExecutor and run the animation + const executor = new AnimateExecutor(group); + executor.execute({ + type: 'inputRichText', + to: { textConfig: complexTextConfig }, + customParameters: { + showCursor: true, + cursorChar: '▎', + blinkCursor: true, + // 启用字符透明度渐变效果,设置较长的渐变时间提供更平滑的效果 + fadeInChars: true, + fadeInDuration: 0.5 + }, + duration: 5000, + easing: 'linear' + }); + }); + + // Comparison demo to show the difference with/without fade effect + addCase('RichText With/Without Fade', container, stage => { + // Create two richTexts for comparison + const richTextNoFade = createRichText({ + x: 20, + y: 50, + width: 600, + height: 100, + textConfig: [], + textBaseline: 'middle' + }); + + const richTextWithFade = createRichText({ + x: 20, + y: 150, + width: 600, + height: 100, + textConfig: [], + textBaseline: 'middle' + }); + + // Add title texts + const titleNoFade = createText({ + x: 20, + y: 20, + text: 'Without Fade Effect:', + fontSize: 16, + fill: '#000' + }); + + const titleWithFade = createText({ + x: 20, + y: 120, + text: 'With Fade Effect:', + fontSize: 16, + fill: '#000' + }); + + // Create a group and add everything to it + const group = createGroup({}); + group.add(richTextNoFade); + group.add(richTextWithFade); + group.add(titleNoFade); + group.add(titleWithFade); + stage.defaultLayer.add(group); + stage.render(); + + // Define the textConfig to use for both examples + const textConfig = [ + { + text: 'Compare these typing animations to see the difference.', + fontSize: 18, + fill: '#333333', + fontWeight: 'bold' + } + ]; + + // Create an AnimateExecutor and run both animations + const executor1 = new AnimateExecutor(richTextNoFade); + const executor2 = new AnimateExecutor(richTextWithFade); + // Animation without fade effect + executor1.execute({ + type: 'inputRichText', + to: { textConfig }, + customParameters: { + showCursor: true, + cursorChar: '|', + blinkCursor: true, + fadeInChars: false + }, + duration: 4000, + easing: 'linear' + }); + + // Animation with fade effect + executor2.execute({ + type: 'inputRichText', + to: { textConfig }, + customParameters: { + showCursor: true, + cursorChar: '|', + blinkCursor: true, + fadeInChars: true, + fadeInDuration: 0.4 + }, + duration: 4000, + easing: 'linear' + }); + }); + + return container; +}; diff --git a/packages/vrender/__tests__/browser/src/pages/index.ts b/packages/vrender/__tests__/browser/src/pages/index.ts index 2a6566bef..6023727e6 100644 --- a/packages/vrender/__tests__/browser/src/pages/index.ts +++ b/packages/vrender/__tests__/browser/src/pages/index.ts @@ -27,6 +27,10 @@ export const pages = [ name: 'animate-state', path: 'animate-state' }, + { + name: 'custom-animate', + path: 'custom-animate' + }, { name: '内存', path: 'memory' From 2dc5d8bd984ce1a3bd5d22f6650ce796ef4188a3 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 3 Apr 2025 14:26:44 +0800 Subject: [PATCH 064/179] feat: support side richtext animate --- .../vrender-animate/src/custom/register.ts | 2 + .../src/custom/richtext/slide-richtext.ts | 331 ++++++++++++++++++ .../src/executor/animate-executor.ts | 6 +- .../__tests__/browser/vite.config.ts | 1 + .../src/graphic/richtext/paragraph.ts | 4 +- .../browser/src/pages/animate-next.ts | 2 +- .../browser/src/pages/custom-animate.ts | 248 +++++++------ .../browser/src/pages/richtext-editor.ts | 3 + 8 files changed, 485 insertions(+), 112 deletions(-) diff --git a/packages/vrender-animate/src/custom/register.ts b/packages/vrender-animate/src/custom/register.ts index 0f752ea61..20ddc29c8 100644 --- a/packages/vrender-animate/src/custom/register.ts +++ b/packages/vrender-animate/src/custom/register.ts @@ -22,6 +22,7 @@ import { State } from './state'; import { Update } from './update'; import { InputText } from './input-text'; import { InputRichText } from './richtext/input-richtext'; +import { SlideRichText } from './richtext/slide-richtext'; export const registerCustomAnimate = () => { // 基础动画 @@ -61,4 +62,5 @@ export const registerCustomAnimate = () => { // Text input animations AnimateExecutor.registerBuiltInAnimate('inputText', InputText); AnimateExecutor.registerBuiltInAnimate('inputRichText', InputRichText); + AnimateExecutor.registerBuiltInAnimate('slideRichText', SlideRichText); }; diff --git a/packages/vrender-animate/src/custom/richtext/slide-richtext.ts b/packages/vrender-animate/src/custom/richtext/slide-richtext.ts index e69de29bb..61991c9be 100644 --- a/packages/vrender-animate/src/custom/richtext/slide-richtext.ts +++ b/packages/vrender-animate/src/custom/richtext/slide-richtext.ts @@ -0,0 +1,331 @@ +import type { IAnimate, IStep } from '../../intreface/animate'; +import type { EasingType } from '../../intreface/easing'; +import { ACustomAnimate } from '../custom-animate'; +import type { IRichTextCharacter, IRichTextParagraphCharacter } from '@visactor/vrender-core'; +import { RichText } from '@visactor/vrender-core'; + +/** + * 滑动富文本动画,结合打字效果和方向滑动效果 + * 文字会从指定方向滑入,同时逐字显示和渐入 + * 支持上、下、左、右四个方向 + * 支持按单词或字符入场 + */ +export class SlideRichText extends ACustomAnimate<{ textConfig: IRichTextCharacter[] }> { + declare valid: boolean; + + private fromTextConfig: IRichTextCharacter[] = []; + private toTextConfig: IRichTextCharacter[] = []; + private originalTextConfig: IRichTextCharacter[] = []; + private singleCharConfig: IRichTextCharacter[] = []; + private fadeInDuration: number = 0.3; // 透明度渐变持续时间,以动画总时长的比例表示 + private slideDirection: 'up' | 'down' | 'left' | 'right' = 'right'; // 滑动方向 + private slideDistance: number = 30; // 滑动距离(像素) + private wordByWord: boolean = false; // 是否按单词为单位进行动画 + // 默认正则表达式: 匹配英文单词(含中间连字符),连续中文字符,数字,以及独立的符号和空格 + private wordRegex: RegExp = /[a-zA-Z]+(-[a-zA-Z]+)*|[\u4e00-\u9fa5]+|[0-9]+|[^\s\w\u4e00-\u9fa5]/g; + private wordGroups: number[][] = []; // 存储单词分组信息,每个数组包含属于同一单词的字符索引 + + constructor( + from: { textConfig: IRichTextCharacter[] }, + to: { textConfig: IRichTextCharacter[] }, + duration: number, + easing: EasingType, + params?: { + fadeInDuration?: number; + slideDirection?: 'up' | 'down' | 'left' | 'right'; + slideDistance?: number; + wordByWord?: boolean; + wordRegex?: RegExp; + } + ) { + super(from, to, duration, easing, params); + + // 配置透明度渐变效果 + if (params?.fadeInDuration !== undefined) { + this.fadeInDuration = params.fadeInDuration; + } + + // 配置滑动方向和距离 + if (params?.slideDirection !== undefined) { + this.slideDirection = params.slideDirection; + } + if (params?.slideDistance !== undefined) { + this.slideDistance = params.slideDistance; + } + + // 配置按单词动画 + if (params?.wordByWord !== undefined) { + this.wordByWord = params.wordByWord; + } + if (params?.wordRegex !== undefined) { + this.wordRegex = params.wordRegex; + } + } + + onFirstRun(): void { + const fromProps = this.getLastProps(); + const toProps = this.getEndProps(); + + // 存储原始配置 + this.originalTextConfig = toProps.textConfig ? [...toProps.textConfig] : []; + + // 初始化解析结果 + this.valid = true; + + // 确保to不为空 + if (!this.originalTextConfig || this.originalTextConfig.length === 0) { + this.valid = false; + return; + } + + // 将文本拆分为单个字符,使用RichText的静态方法 + this.fromTextConfig = + fromProps.textConfig && fromProps.textConfig.length > 0 + ? RichText.TransformTextConfig2SingleCharacter(fromProps.textConfig) + : []; + + this.toTextConfig = RichText.TransformTextConfig2SingleCharacter(this.originalTextConfig); + + // 创建单字符数组,用于动画 + this.singleCharConfig = this.toTextConfig.map(item => { + if ('text' in item) { + // 文本字符初始设置为透明 + return { + ...item, + opacity: 0, + dx: this.getInitialDx(), + dy: this.getInitialDy() + }; + } + return { ...item, opacity: 0 }; + }); + + // 如果启用按单词动画,则计算单词分组 + if (this.wordByWord) { + this.calculateWordGroups(); + } + } + + // 计算单词分组 + private calculateWordGroups(): void { + // 重置单词分组 + this.wordGroups = []; + + // 构建完整文本用于正则匹配 + let fullText = ''; + const charMap: Record = {}; // 映射全文索引到字符配置索引 + let fullTextIndex = 0; + + // 构建全文和映射 + this.toTextConfig.forEach((item, configIndex) => { + if ('text' in item) { + const text = String(item.text); + fullText += text; + // 为每个字符创建映射 + charMap[fullTextIndex] = configIndex; + fullTextIndex++; + } + }); + + // 使用正则表达式查找单词 + let match; + + // 重置正则表达式状态 + this.wordRegex.lastIndex = 0; + + while ((match = this.wordRegex.exec(fullText)) !== null) { + const wordStart = match.index; + const wordEnd = match.index + match[0].length; + + // 找出属于这个单词的所有字符索引 + const wordIndices = []; + + for (let i = wordStart; i < wordEnd; i++) { + if (charMap[i] !== undefined) { + wordIndices.push(charMap[i]); + } + } + + // 添加到单词分组 + if (wordIndices.length > 0) { + this.wordGroups.push(wordIndices); + } + } + + // 处理没有分配到任何单词的字符 + const allocatedIndices = new Set(); + this.wordGroups.forEach(group => { + group.forEach(index => allocatedIndices.add(index)); + }); + + for (let i = 0; i < this.toTextConfig.length; i++) { + if ('text' in this.toTextConfig[i] && !allocatedIndices.has(i)) { + // 单独为每个未分配的字符创建一个"单词" + this.wordGroups.push([i]); + } + } + } + + // 根据滑动方向计算初始x偏移 + private getInitialDx(): number { + switch (this.slideDirection) { + case 'left': + return -this.slideDistance; + case 'right': + return this.slideDistance; + default: + return 0; + } + } + + // 根据滑动方向计算初始y偏移 + private getInitialDy(): number { + switch (this.slideDirection) { + case 'up': + return -this.slideDistance; + case 'down': + return this.slideDistance; + default: + return 0; + } + } + + onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { + super.onEnd(cb); + if (!cb) { + // 动画结束时,恢复原始textConfig + this.target.setAttribute('textConfig', this.originalTextConfig); + } + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + if (!this.valid) { + return; + } + + // 计算文本显示比例上限 - 为尾部字符的渐变和滑动效果留出时间 + const maxTextShowRatio = 1 - this.fadeInDuration; + + let updatedTextConfig: IRichTextCharacter[]; + + if (this.wordByWord && this.wordGroups.length > 0) { + // 按单词动画 + updatedTextConfig = this.updateByWord(ratio, maxTextShowRatio); + } else { + // 按字符动画 + updatedTextConfig = this.updateByCharacter(ratio, maxTextShowRatio); + } + + // 更新富文本的textConfig属性 + this.target.setAttribute('textConfig', updatedTextConfig); + } + + // 按单词更新文本配置 + private updateByWord(ratio: number, maxTextShowRatio: number): IRichTextCharacter[] { + const totalGroups = this.wordGroups.length; + const updatedTextConfig = [...this.singleCharConfig]; + + // 处理单词分组 + for (let groupIndex = 0; groupIndex < this.wordGroups.length; groupIndex++) { + // 计算这个单词组的显示时间点 + let appearTime; + if (this.slideDirection === 'left') { + // 从右到左顺序(最后的单词先出现) + appearTime = ((totalGroups - 1 - groupIndex) / totalGroups) * maxTextShowRatio; + } else { + // 标准顺序(第一个单词先出现) + appearTime = (groupIndex / totalGroups) * maxTextShowRatio; + } + + // 如果当前时间还没到显示这个单词的时间点,保持隐藏状态 + if (ratio < appearTime) { + for (const charIndex of this.wordGroups[groupIndex]) { + const item = updatedTextConfig[charIndex]; + if ('text' in item) { + updatedTextConfig[charIndex] = { + ...item, + opacity: 0, + dx: this.getInitialDx(), + dy: this.getInitialDy() + }; + } + } + continue; + } + + // 计算动画进度(0-1之间) + const animProgress = (ratio - appearTime) / this.fadeInDuration; + const progress = Math.max(0, Math.min(1, animProgress)); + + // 计算当前偏移和透明度 + const dx = this.getInitialDx() * (1 - progress); + const dy = this.getInitialDy() * (1 - progress); + + // 更新这个单词的所有字符 + for (const charIndex of this.wordGroups[groupIndex]) { + const item = updatedTextConfig[charIndex]; + if ('text' in item) { + updatedTextConfig[charIndex] = { + ...item, + opacity: progress, + dx, + dy + }; + } + } + } + + return updatedTextConfig; + } + + // 按字符更新文本配置 + private updateByCharacter(ratio: number, maxTextShowRatio: number): IRichTextCharacter[] { + const totalItems = this.toTextConfig.length; + const updatedTextConfig = [...this.singleCharConfig]; + + // 更新每个字符的状态 + for (let index = 0; index < updatedTextConfig.length; index++) { + const item = updatedTextConfig[index]; + if ('text' in item) { + // 计算每个字符的显示时间点 + // 对于left方向,反转显示顺序(从右到左) + let appearTime; + if (this.slideDirection === 'left') { + // 从右到左的顺序 (最后的字符先出现) + appearTime = ((totalItems - 1 - index) / totalItems) * maxTextShowRatio; + } else { + // 标准顺序 (第一个字符先出现) + appearTime = (index / totalItems) * maxTextShowRatio; + } + + // 如果当前时间还没到显示这个字符的时间点,保持隐藏状态 + if (ratio < appearTime) { + updatedTextConfig[index] = { + ...item, + opacity: 0, + dx: this.getInitialDx(), + dy: this.getInitialDy() + }; + continue; + } + + // 计算动画进度(0-1之间) + const animProgress = (ratio - appearTime) / this.fadeInDuration; + const progress = Math.max(0, Math.min(1, animProgress)); + + // 计算当前偏移和透明度 + const dx = this.getInitialDx() * (1 - progress); + const dy = this.getInitialDy() * (1 - progress); + + updatedTextConfig[index] = { + ...item, + opacity: progress, + dx, + dy + }; + } + } + + return updatedTextConfig; + } +} diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index 949691e83..353c59f28 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -161,7 +161,7 @@ export class AnimateExecutor implements IAnimateExecutor { delayAfter: (slice.delayAfter as number) * scale, duration: (slice.duration as number) * scale, effects: effects.map(effect => { - const custom = effect.custom ?? AnimateExecutor.builtInAnimateMap[effect.type as any]; + const custom = effect.custom ?? AnimateExecutor.builtInAnimateMap[(effect.type as any) ?? 'to']; const customType = custom && isFunction(custom) ? (/^class\s/.test(Function.prototype.toString.call(custom)) ? 1 : 2) : 0; return { @@ -191,7 +191,7 @@ export class AnimateExecutor implements IAnimateExecutor { parsedParams.oneByOneDelay = oneByOneDelay; parsedParams.custom = (params as IAnimationTypeConfig).custom ?? - AnimateExecutor.builtInAnimateMap[(params as IAnimationTypeConfig).type]; + AnimateExecutor.builtInAnimateMap[(params as IAnimationTypeConfig).type ?? 'to']; const customType = parsedParams.custom && isFunction(parsedParams.custom) @@ -285,7 +285,7 @@ export class AnimateExecutor implements IAnimateExecutor { count: number ): IAnimate { const { - type, + type = 'to', channel, customParameters, easing = 'linear', diff --git a/packages/vrender-core/__tests__/browser/vite.config.ts b/packages/vrender-core/__tests__/browser/vite.config.ts index ad1542445..fe23d9382 100644 --- a/packages/vrender-core/__tests__/browser/vite.config.ts +++ b/packages/vrender-core/__tests__/browser/vite.config.ts @@ -31,6 +31,7 @@ export default defineConfig({ '@visactor/vrender-core': path.resolve(__dirname, '../../../vrender-core/src/index.ts'), '@visactor/vrender-kits': path.resolve(__dirname, '../../../vrender-kits/src/index.ts'), '@visactor/vrender-components': path.resolve(__dirname, '../../../vrender-components/src/index.ts'), + '@visactor/vrender-animate': path.resolve(__dirname, '../../../vrender-animate/src/index.ts'), util: 'rollup-plugin-node-polyfills/polyfills/util' } }, diff --git a/packages/vrender-core/src/graphic/richtext/paragraph.ts b/packages/vrender-core/src/graphic/richtext/paragraph.ts index 57daf8d73..2ce869834 100644 --- a/packages/vrender-core/src/graphic/richtext/paragraph.ts +++ b/packages/vrender-core/src/graphic/richtext/paragraph.ts @@ -338,11 +338,11 @@ export default class Paragraph { const { lineWidth = 1 } = this.character; if (this.character.stroke && lineWidth) { - ctx.strokeText(text, left, baseline + this.dy); + ctx.strokeText(text, left + this.dx, baseline + this.dy); } if (this.character.fill) { - ctx.fillText(text, left, baseline + this.dy); + ctx.fillText(text, left + this.dx, baseline + this.dy); } if (this.character.fill) { diff --git a/packages/vrender/__tests__/browser/src/pages/animate-next.ts b/packages/vrender/__tests__/browser/src/pages/animate-next.ts index 8596be638..09b392831 100644 --- a/packages/vrender/__tests__/browser/src/pages/animate-next.ts +++ b/packages/vrender/__tests__/browser/src/pages/animate-next.ts @@ -374,7 +374,7 @@ export const page = () => { // Basic animation - all elements fade and change color simultaneously executor.execute({ - type: 'to', + // type: 'to', channel: { fill: { to: 'red' diff --git a/packages/vrender/__tests__/browser/src/pages/custom-animate.ts b/packages/vrender/__tests__/browser/src/pages/custom-animate.ts index e092d44a0..59f4c6392 100644 --- a/packages/vrender/__tests__/browser/src/pages/custom-animate.ts +++ b/packages/vrender/__tests__/browser/src/pages/custom-animate.ts @@ -127,7 +127,6 @@ export const page = () => { showCursor: true, cursorChar: '|', blinkCursor: true, - // 启用字符透明度渐变效果 fadeInChars: true, fadeInDuration: 0.3 }, @@ -136,16 +135,17 @@ export const page = () => { }); }); - // Advanced example with a more complex RichText - addCase('Complex InputRichText Animation', container, stage => { + // Slide RichText Animation - Right direction (default) + addCase('SlideRichText - Right Direction', container, stage => { // Create a richText with empty textConfig const richText = createRichText({ x: 20, - y: 20, + y: 80, width: 600, - height: 120, + height: 100, textConfig: [], - textBaseline: 'top' + textBaseline: 'middle', + ascentDescentMode: 'font' }); // Create a group and add the richText to it @@ -154,147 +154,183 @@ export const page = () => { stage.defaultLayer.add(group); stage.render(); - // Define a more complex final textConfig - const complexTextConfig = [ - { - text: '富文本打字动画', - fontSize: 22, - fill: '#6600CC', - fontWeight: 'bold' - }, - { - text: '\n', // Line break - fontSize: 22 - }, - { - text: 'Supports ', - fontSize: 18, - fill: '#FF6600' - }, - { - text: 'multiple ', - fontSize: 18, - fill: '#0099FF', - fontStyle: 'italic' - }, - { - text: 'styles ', - fontSize: 18, - fill: '#33CC33', - textDecoration: 'underline' - }, - { - text: 'and formats!', - fontSize: 18, - fill: '#CC3366', + // Define the textConfig + const textConfig = [ + ...Array.from('Slide from Right ').map(item => ({ + text: item, + fontSize: 24, + fill: '#FF5500', fontWeight: 'bold' - } + })), + ...Array.from('- Characters slide in while typing!').map(item => ({ + text: item, + fontSize: 24, + fill: '#0055FF' + })) ]; // Create an AnimateExecutor and run the animation const executor = new AnimateExecutor(group); executor.execute({ - type: 'inputRichText', - to: { textConfig: complexTextConfig }, + type: 'slideRichText', + to: { textConfig }, customParameters: { - showCursor: true, - cursorChar: '▎', - blinkCursor: true, - // 启用字符透明度渐变效果,设置较长的渐变时间提供更平滑的效果 - fadeInChars: true, - fadeInDuration: 0.5 + wordByWord: true, + fadeInDuration: 0.3, + slideDirection: 'right', + slideDistance: 50 }, - duration: 5000, + duration: 3000, easing: 'linear' }); }); - // Comparison demo to show the difference with/without fade effect - addCase('RichText With/Without Fade', container, stage => { - // Create two richTexts for comparison - const richTextNoFade = createRichText({ + // Slide RichText Animation - Left direction + addCase('SlideRichText - Left Direction', container, stage => { + // Create a richText with empty textConfig + const richText = createRichText({ x: 20, - y: 50, + y: 80, width: 600, height: 100, textConfig: [], - textBaseline: 'middle' + textBaseline: 'middle', + ascentDescentMode: 'font' }); - const richTextWithFade = createRichText({ - x: 20, - y: 150, - width: 600, - height: 100, - textConfig: [], - textBaseline: 'middle' - }); + // Create a group and add the richText to it + const group = createGroup({}); + group.add(richText); + stage.defaultLayer.add(group); + stage.render(); - // Add title texts - const titleNoFade = createText({ - x: 20, - y: 20, - text: 'Without Fade Effect:', - fontSize: 16, - fill: '#000' + // Define the textConfig + const textConfig = [ + ...Array.from('Slide from Left ').map(item => ({ + text: item, + fontSize: 24, + fill: '#FF5500', + fontWeight: 'bold' + })), + ...Array.from('- Characters slide in while typing!').map(item => ({ + text: item, + fontSize: 24, + fill: '#0055FF' + })) + ]; + + // Create an AnimateExecutor and run the animation + const executor = new AnimateExecutor(group); + executor.execute({ + type: 'slideRichText', + to: { textConfig }, + customParameters: { + wordByWord: true, + fadeInDuration: 0.3, + slideDirection: 'left', + slideDistance: 50 + }, + duration: 3000, + easing: 'linear' }); + }); - const titleWithFade = createText({ + // Slide RichText Animation - Up direction + addCase('SlideRichText - Up Direction', container, stage => { + // Create a richText with empty textConfig + const richText = createRichText({ x: 20, - y: 120, - text: 'With Fade Effect:', - fontSize: 16, - fill: '#000' + y: 80, + width: 600, + height: 100, + textConfig: [], + textBaseline: 'middle', + ascentDescentMode: 'font' }); - // Create a group and add everything to it + // Create a group and add the richText to it const group = createGroup({}); - group.add(richTextNoFade); - group.add(richTextWithFade); - group.add(titleNoFade); - group.add(titleWithFade); + group.add(richText); stage.defaultLayer.add(group); stage.render(); - // Define the textConfig to use for both examples + // Define the textConfig const textConfig = [ - { - text: 'Compare these typing animations to see the difference.', - fontSize: 18, - fill: '#333333', + ...Array.from('Slide from Bottom ').map(item => ({ + text: item, + fontSize: 24, + fill: '#FF5500', fontWeight: 'bold' - } + })), + ...Array.from('- Characters slide upward while typing!').map(item => ({ + text: item, + fontSize: 24, + fill: '#0055FF' + })) ]; - // Create an AnimateExecutor and run both animations - const executor1 = new AnimateExecutor(richTextNoFade); - const executor2 = new AnimateExecutor(richTextWithFade); - // Animation without fade effect - executor1.execute({ - type: 'inputRichText', + // Create an AnimateExecutor and run the animation + const executor = new AnimateExecutor(richText); + executor.execute({ + type: 'slideRichText', to: { textConfig }, customParameters: { - showCursor: true, - cursorChar: '|', - blinkCursor: true, - fadeInChars: false + wordByWord: true, + fadeInDuration: 0.3, + slideDirection: 'up', + slideDistance: 50 }, - duration: 4000, + duration: 3000, easing: 'linear' }); + }); - // Animation with fade effect - executor2.execute({ - type: 'inputRichText', + // Slide RichText Animation - Down direction + addCase('SlideRichText - Down Direction', container, stage => { + // Create a richText with empty textConfig + const richText = createRichText({ + x: 20, + y: 80, + width: 600, + height: 100, + textConfig: [], + textBaseline: 'middle', + ascentDescentMode: 'font' + }); + + // Create a group and add the richText to it + const group = createGroup({}); + group.add(richText); + stage.defaultLayer.add(group); + stage.render(); + + // Define the textConfig + const textConfig = [ + ...Array.from('从上方').map(item => ({ + text: item, + fontSize: 24, + fill: '#FF5500', + fontWeight: 'bold' + })), + ...Array.from('- 字符向下滑动!').map(item => ({ + text: item, + fontSize: 24, + fill: '#0055FF' + })) + ]; + + // Create an AnimateExecutor and run the animation + const executor = new AnimateExecutor(group); + executor.execute({ + type: 'slideRichText', to: { textConfig }, customParameters: { - showCursor: true, - cursorChar: '|', - blinkCursor: true, - fadeInChars: true, - fadeInDuration: 0.4 + wordByWord: true, + fadeInDuration: 0.3, + slideDirection: 'down', + slideDistance: 50 }, - duration: 4000, + duration: 3000, easing: 'linear' }); }); diff --git a/packages/vrender/__tests__/browser/src/pages/richtext-editor.ts b/packages/vrender/__tests__/browser/src/pages/richtext-editor.ts index a350b9aca..7501b9c0c 100644 --- a/packages/vrender/__tests__/browser/src/pages/richtext-editor.ts +++ b/packages/vrender/__tests__/browser/src/pages/richtext-editor.ts @@ -65,6 +65,7 @@ export const page = () => { fontFamily: 'D-Din', lineHeight: '150%', text: 'a', + dy: 10, isComposing: false }, { @@ -75,6 +76,7 @@ export const page = () => { fontFamily: 'D-Din', lineHeight: '150%', text: 'b', + dy: 20, isComposing: false }, { @@ -85,6 +87,7 @@ export const page = () => { fontFamily: 'D-Din', lineHeight: '150%', text: 'c', + dx: 30, isComposing: false } ], From fad534d7e1556545dd2651fdd264e1c250d8e7dc Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 3 Apr 2025 16:04:53 +0800 Subject: [PATCH 065/179] fix: fix issue with pie angle interpolate with deleteSelfAttr --- .../vrender-animate/src/custom/growAngle.ts | 21 +++++++++++++++++++ packages/vrender-animate/src/step.ts | 4 +++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/vrender-animate/src/custom/growAngle.ts b/packages/vrender-animate/src/custom/growAngle.ts index 12a5643a4..27338daf7 100644 --- a/packages/vrender-animate/src/custom/growAngle.ts +++ b/packages/vrender-animate/src/custom/growAngle.ts @@ -168,6 +168,27 @@ export class GrowAngleBase extends ACustomAnimate> { } } + /** + * 删除自身属性,会直接从props等内容里删除掉 + */ + deleteSelfAttr(key: string): void { + delete this.props[key]; + // fromProps在动画开始时才会计算,这时可能不在 + this.fromProps && delete this.fromProps[key]; + const index = this.propKeys.indexOf(key); + if (index !== -1) { + this.propKeys.splice(index, 1); + } + + if (this.propKeys && this.propKeys.length > 1) { + this._updateFunction = this.updateAngle; + } else if (this.propKeys[0] === 'startAngle') { + this._updateFunction = this.updateStartAngle; + } else if (this.propKeys[0] === 'endAngle') { + this._updateFunction = this.updateEndAngle; + } + } + updateStartAngle(ratio: number): void { (this.target.attribute as any).startAngle = this.from.startAngle + (this.to.startAngle - this.from.startAngle) * ratio; diff --git a/packages/vrender-animate/src/step.ts b/packages/vrender-animate/src/step.ts index edcdd5cc8..63c211f35 100644 --- a/packages/vrender-animate/src/step.ts +++ b/packages/vrender-animate/src/step.ts @@ -1,4 +1,4 @@ -import { ColorStore, ColorType, type IGraphic } from '@visactor/vrender-core'; +import { ColorStore, ColorType, Generator, type IGraphic } from '@visactor/vrender-core'; import type { IAnimate, IStep } from './intreface/animate'; import type { EasingType, EasingTypeFunc } from './intreface/easing'; import type { IAnimateStepType } from './intreface/type'; @@ -11,6 +11,7 @@ function noop() { } export class Step implements IStep { + id: number; type: IAnimateStepType; prev?: IStep; duration: number; @@ -51,6 +52,7 @@ export class Step implements IStep { if (type === 'wait') { this.onUpdate = noop; } + this.id = Generator.GenAutoIncrementId(); } bind(target: IGraphic, animate: IAnimate): void { From 966b08efb9068eebc56e4cdc8581246aaede2dd7 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 3 Apr 2025 16:08:37 +0800 Subject: [PATCH 066/179] fix: fix type issue --- packages/vrender-components/src/label/base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index adcc5b4a6..3e4ee5a8c 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -829,7 +829,7 @@ export class LabelBase extends AbstractComponent { relatedGraphics: this._idToGraphic, config: { ...enter, - type: item === text ? enter.type : 'clipIn' + type: item === text ? (enter as any).type : 'clipIn' } } } From 4b0b16f77e22f5b3e75bb5eda9d81affd0bc74ed Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 3 Apr 2025 19:17:04 +0800 Subject: [PATCH 067/179] feat: support story animate --- .../vrender-animate/src/custom/register.ts | 8 + packages/vrender-animate/src/custom/story.ts | 328 ++++++++++++ .../__tests__/browser/src/pages/index.ts | 4 + .../browser/src/pages/story-animate.ts | 473 ++++++++++++++++++ 4 files changed, 813 insertions(+) create mode 100644 packages/vrender-animate/src/custom/story.ts create mode 100644 packages/vrender/__tests__/browser/src/pages/story-animate.ts diff --git a/packages/vrender-animate/src/custom/register.ts b/packages/vrender-animate/src/custom/register.ts index 20ddc29c8..747d6537d 100644 --- a/packages/vrender-animate/src/custom/register.ts +++ b/packages/vrender-animate/src/custom/register.ts @@ -23,6 +23,7 @@ import { Update } from './update'; import { InputText } from './input-text'; import { InputRichText } from './richtext/input-richtext'; import { SlideRichText } from './richtext/slide-richtext'; +import { SlideIn, GrowIn, SpinIn, MoveScaleIn, MoveRotateIn } from './story'; export const registerCustomAnimate = () => { // 基础动画 @@ -63,4 +64,11 @@ export const registerCustomAnimate = () => { AnimateExecutor.registerBuiltInAnimate('inputText', InputText); AnimateExecutor.registerBuiltInAnimate('inputRichText', InputRichText); AnimateExecutor.registerBuiltInAnimate('slideRichText', SlideRichText); + + // 故事化动画 + AnimateExecutor.registerBuiltInAnimate('slideIn', SlideIn); + AnimateExecutor.registerBuiltInAnimate('growIn', GrowIn); + AnimateExecutor.registerBuiltInAnimate('spinIn', SpinIn); + AnimateExecutor.registerBuiltInAnimate('moveScaleIn', MoveScaleIn); + AnimateExecutor.registerBuiltInAnimate('moveRotateIn', MoveRotateIn); }; diff --git a/packages/vrender-animate/src/custom/story.ts b/packages/vrender-animate/src/custom/story.ts new file mode 100644 index 000000000..b753b66a8 --- /dev/null +++ b/packages/vrender-animate/src/custom/story.ts @@ -0,0 +1,328 @@ +import { FadeIn } from './fade'; +import type { EasingType } from '../intreface/easing'; +import { ACustomAnimate } from './custom-animate'; +import { AnimateExecutor } from '../executor/animate-executor'; + +export class StoryFadeIn extends FadeIn {} + +// 滑动动画的参数接口 +export interface ISlideAnimationOptions { + direction?: 'top' | 'bottom' | 'left' | 'right'; + distance?: number; + fromOpacity?: number; // 透明度初始值,默认0 +} + +// 缩放动画的参数接口 +export interface IGrowAnimationOptions { + fromScale?: number; + direction?: 'x' | 'y' | 'xy'; + fromOpacity?: number; // 透明度初始值,默认0 +} + +// 旋转动画的参数接口 +export interface ISpinAnimationOptions { + fromAngle?: number; + fromScale?: number; + fromOpacity?: number; // 透明度初始值,默认0 +} + +/** + * 滑动入场动画,包括从上到下,从下到上,从左到右,从右到左的位置,以及透明度属性插值 + */ +export class SlideIn extends ACustomAnimate> { + declare valid: boolean; + declare propKeys: string[]; + declare from: Record; + declare to: Record; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: ISlideAnimationOptions) { + super(from, to, duration, easing, params); + } + + onBind(): void { + // 用于入场的时候设置属性 + const attrs = this.target.getAttributes(true); + + const direction = (this.params?.direction as 'top' | 'bottom' | 'left' | 'right') || 'right'; + const distance = this.params?.distance || 50; + const fromOpacity = this.params?.fromOpacity ?? 0; // 使用透明度初始值参数 + + // 初始化from和to对象 + const from: Record = { opacity: fromOpacity }; + const to: Record = { opacity: 1 }; + + // 根据方向设置对应的属性 + if (direction === 'top') { + from.y = (attrs.y ?? 0) - distance; + to.y = attrs.y ?? 0; + this.propKeys = ['opacity', 'y']; + } else if (direction === 'bottom') { + from.y = (attrs.y ?? 0) + distance; + to.y = attrs.y ?? 0; + this.propKeys = ['opacity', 'y']; + } else if (direction === 'left') { + from.x = (attrs.x ?? 0) - distance; + to.x = attrs.x ?? 0; + this.propKeys = ['opacity', 'x']; + } else { + // right + from.x = (attrs.x ?? 0) + distance; + to.x = attrs.x ?? 0; + this.propKeys = ['opacity', 'x']; + } + + this.from = from; + this.to = to; + this.props = to; + + // 将初始属性应用到目标对象 + this.target.setAttributes(from as any); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; + this.propKeys.forEach(key => { + attribute[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.addUpdatePositionTag(); + this.target.addUpdateShapeAndBoundsTag(); + } +} + +/** + * 缩放入场动画,包括scaleX、scaleY属性从某个比例缩放到1,该比例可以小于1也可以大于1,以及透明度属性插值 + */ +export class GrowIn extends ACustomAnimate> { + declare valid: boolean; + declare propKeys: string[]; + declare from: Record; + declare to: Record; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: IGrowAnimationOptions) { + super(from, to, duration, easing, params); + } + + onBind(): void { + // 用于入场的时候设置属性 + const attrs = this.target.getAttributes(true); + + const fromScale = this.params?.fromScale ?? 0; + const direction = this.params?.direction || 'xy'; + const fromOpacity = this.params?.fromOpacity ?? 0; // 使用透明度初始值参数 + + // 初始化from和to对象 + const from: Record = { opacity: fromOpacity }; + const to: Record = { opacity: 1 }; + this.propKeys = ['opacity']; + + // 根据方向设置对应的缩放属性 + if (direction === 'x' || direction === 'xy') { + from.scaleX = fromScale; + to.scaleX = attrs.scaleX ?? 1; + this.propKeys.push('scaleX'); + } + + if (direction === 'y' || direction === 'xy') { + from.scaleY = fromScale; + to.scaleY = attrs.scaleY ?? 1; + this.propKeys.push('scaleY'); + } + + this.from = from; + this.to = to; + this.props = to; + + // 将初始属性应用到目标对象 + this.target.setAttributes(from as any); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; + this.propKeys.forEach(key => { + attribute[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.addUpdatePositionTag(); + this.target.addUpdateShapeAndBoundsTag(); + } +} + +/** + * 旋转入场动画,包括rotate属性从某个角度度旋转到0,以及缩放属性从某个比例缩放到1,该比例可以小于1也可以大于1 + */ +export class SpinIn extends ACustomAnimate> { + declare valid: boolean; + declare propKeys: string[]; + declare from: Record; + declare to: Record; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: ISpinAnimationOptions) { + super(from, to, duration, easing, params); + } + + onBind(): void { + // 用于入场的时候设置属性 + const attrs = this.target.getAttributes(true); + + const fromAngle = this.params?.fromAngle ?? Math.PI * 2; // 默认旋转一圈 + const fromScale = this.params?.fromScale ?? 0; + const fromOpacity = this.params?.fromOpacity ?? 0; // 使用透明度初始值参数 + + // 初始化from和to对象 + const from: Record = { + opacity: fromOpacity, + angle: fromAngle, + scaleX: fromScale, + scaleY: fromScale + }; + + const to: Record = { + opacity: 1, + angle: attrs.angle ?? 0, + scaleX: attrs.scaleX ?? 1, + scaleY: attrs.scaleY ?? 1 + }; + + this.propKeys = ['opacity', 'angle', 'scaleX', 'scaleY']; + this.from = from; + this.to = to; + this.props = to; + + // 将初始属性应用到目标对象 + this.target.setAttributes(from as any); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; + this.propKeys.forEach(key => { + attribute[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.addUpdatePositionTag(); + this.target.addUpdateShapeAndBoundsTag(); + } +} + +// 复合动画的参数接口 +export interface IMoveScaleAnimationOptions { + slideDirection?: 'top' | 'bottom' | 'left' | 'right'; + slideDistance?: number; + fromScale?: number; + scaleDirection?: 'x' | 'y' | 'xy'; + slideRatio?: number; // 滑动动画占总时长的比例,默认0.5 + fromOpacity?: number; // 透明度初始值,默认0 +} + +/** + * 移动+缩放入场动画 + * 先走SlideIn,然后走GrowIn + */ +export class MoveScaleIn extends ACustomAnimate { + declare valid: boolean; + private readonly slideInDuration: number; + private readonly growInDuration: number; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: IMoveScaleAnimationOptions) { + super(from, to, duration, easing, params); + const slideRatio = params?.slideRatio ?? 0.5; + this.slideInDuration = duration * slideRatio; + this.growInDuration = duration * (1 - slideRatio); + } + + onBind(): void { + // 创建AnimateExecutor来运行序列动画 + const executor = new AnimateExecutor(this.target); + + // 第一步:滑动入场(包含透明度变化) + executor.execute({ + type: 'custom', + custom: SlideIn, + customParameters: { + direction: this.params?.slideDirection || 'right', + distance: this.params?.slideDistance || 50, + fromOpacity: this.params?.fromOpacity ?? 0 + }, + duration: this.slideInDuration, + easing: this.easing + }); + + // 第二步:缩放入场(不包含透明度变化) + executor.execute({ + type: 'custom', + custom: GrowIn, + customParameters: { + fromScale: this.params?.fromScale || 0.5, + direction: this.params?.scaleDirection || 'xy', + fromOpacity: 1 // 设置初始透明度为1,使第二阶段不进行透明度插值 + }, + duration: this.growInDuration, + easing: this.easing, + delay: this.slideInDuration // 等第一步完成后再开始 + }); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + // 动画逻辑由子动画处理 + } +} + +// 移动旋转动画的参数接口 +export interface IMoveRotateAnimationOptions { + slideDirection?: 'top' | 'bottom' | 'left' | 'right'; + slideDistance?: number; + fromAngle?: number; + fromScale?: number; + slideRatio?: number; // 滑动动画占总时长的比例,默认0.5 + fromOpacity?: number; // 透明度初始值,默认0 +} + +/** + * 移动+旋转入场动画 + * 先走SlideIn,然后走SpinIn + */ +export class MoveRotateIn extends ACustomAnimate { + declare valid: boolean; + private readonly slideInDuration: number; + private readonly spinInDuration: number; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: IMoveRotateAnimationOptions) { + super(from, to, duration, easing, params); + const slideRatio = params?.slideRatio ?? 0.5; + this.slideInDuration = duration * slideRatio; + this.spinInDuration = duration * (1 - slideRatio); + } + + onBind(): void { + // 创建AnimateExecutor来运行序列动画 + const executor = new AnimateExecutor(this.target); + + // 第一步:滑动入场(包含透明度变化) + executor.execute({ + type: 'custom', + custom: SlideIn, + customParameters: { + direction: this.params?.slideDirection || 'right', + distance: this.params?.slideDistance || 50, + fromOpacity: this.params?.fromOpacity ?? 0 + }, + duration: this.slideInDuration, + easing: this.easing + }); + + // 第二步:旋转入场(不包含透明度变化) + executor.execute({ + type: 'custom', + custom: SpinIn, + customParameters: { + fromAngle: this.params?.fromAngle || Math.PI, + fromScale: this.params?.fromScale || 0.5, + fromOpacity: 1 // 设置初始透明度为1,使第二阶段不进行透明度插值 + }, + duration: this.spinInDuration, + easing: this.easing, + delay: this.slideInDuration // 等第一步完成后再开始 + }); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + // 动画逻辑由子动画处理 + } +} diff --git a/packages/vrender/__tests__/browser/src/pages/index.ts b/packages/vrender/__tests__/browser/src/pages/index.ts index 6023727e6..8ae5dd979 100644 --- a/packages/vrender/__tests__/browser/src/pages/index.ts +++ b/packages/vrender/__tests__/browser/src/pages/index.ts @@ -31,6 +31,10 @@ export const pages = [ name: 'custom-animate', path: 'custom-animate' }, + { + name: 'story-animate', + path: 'story-animate' + }, { name: '内存', path: 'memory' diff --git a/packages/vrender/__tests__/browser/src/pages/story-animate.ts b/packages/vrender/__tests__/browser/src/pages/story-animate.ts new file mode 100644 index 000000000..c9df46752 --- /dev/null +++ b/packages/vrender/__tests__/browser/src/pages/story-animate.ts @@ -0,0 +1,473 @@ +import { + DefaultTicker, + DefaultTimeline, + Animate, + registerAnimate, + IncreaseCount, + InputText, + AnimateExecutor, + ACustomAnimate, + registerCustomAnimate +} from '@visactor/vrender-animate'; +import { + container, + createRect, + createStage, + createSymbol, + IGraphic, + vglobal, + createCircle, + createText, + createGroup, + createLine, + createPath +} from '@visactor/vrender'; +import type { EasingType } from '@visactor/vrender-animate'; +// container.load(roughModule); + +vglobal.setEnv('browser'); + +registerAnimate(); +registerCustomAnimate(); + +let stage: any; + +function addCase(name: string, container: HTMLElement, cb: (stage: any) => void) { + const button = document.createElement('button'); + button.innerText = name; + button.style.height = '26px'; + container.appendChild(button); + button.addEventListener('click', () => { + stage && stage.release(); + stage = createStage({ + canvas: 'main', + width: 900, + height: 600, + background: 'pink', + disableDirtyBounds: false, + canvasControled: false, + autoRender: true + }); + cb(stage); + }); +} + +export const page = () => { + const btnContainer = document.createElement('div'); + btnContainer.style.width = '1000px'; + btnContainer.style.background = '#cecece'; + btnContainer.style.display = 'flex'; + btnContainer.style.flexDirection = 'row'; + btnContainer.style.gap = '3px'; + btnContainer.style.flexWrap = 'wrap'; + btnContainer.style.height = '120px'; + const canvas = document.getElementById('main'); + // 将btnContainer添加到canvas之前 + canvas.parentNode.insertBefore(btnContainer, canvas); + + // SlideIn Animation Demo + addCase('SlideIn - From Right', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#1890FF', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'slideIn', + customParameters: { + direction: 'right', + distance: 200 + }, + duration: 1000, + easing: 'quadOut' + }); + }); + + addCase('SlideIn - From Left', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#FF7A45', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'slideIn', + customParameters: { + direction: 'left', + distance: 200 + }, + duration: 1000, + easing: 'quadOut' + }); + }); + + addCase('SlideIn - From Top', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#52C41A', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'slideIn', + customParameters: { + direction: 'top', + distance: 200 + }, + duration: 1000, + easing: 'quadOut' + }); + }); + + addCase('SlideIn - From Bottom', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#722ED1', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'slideIn', + customParameters: { + direction: 'bottom', + distance: 200 + }, + duration: 1000, + easing: 'quadOut' + }); + }); + + // GrowIn Animation Demo + addCase('GrowIn - XY Scale', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#FA8C16', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'growIn', + customParameters: { + fromScale: 0.2, + direction: 'xy' + }, + duration: 1000, + easing: 'elasticOut' + }); + }); + + addCase('GrowIn - X Scale', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#13C2C2', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'growIn', + customParameters: { + fromScale: 0.2, + direction: 'x' + }, + duration: 1000, + easing: 'elasticOut' + }); + }); + + addCase('GrowIn - Y Scale', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#EB2F96', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'growIn', + customParameters: { + fromScale: 0.2, + direction: 'y' + }, + duration: 1000, + easing: 'elasticOut' + }); + }); + + // SpinIn Animation Demo + addCase('SpinIn - 360 Rotation', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#F5222D', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'spinIn', + customParameters: { + fromAngle: Math.PI * 2, + fromScale: 0.6 + }, + duration: 1000, + easing: 'quadOut' + }); + }); + + addCase('SpinIn - 180 Rotation', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#2F54EB', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'spinIn', + customParameters: { + fromAngle: Math.PI / 3, + fromScale: 0.6 + }, + duration: 500, + easing: 'quadOut' + }); + }); + + // MoveScaleIn Animation Demo + addCase('MoveScaleIn - From Right', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#A0D911', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'moveScaleIn', + customParameters: { + slideDirection: 'right', + slideDistance: 200, + fromScale: 0.6, + scaleDirection: 'xy', + slideRatio: 0.6 + }, + duration: 2000, + easing: 'cubicOut' + }); + }); + + addCase('MoveScaleIn - From Bottom', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#FAAD14', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'moveScaleIn', + customParameters: { + slideDirection: 'bottom', + slideDistance: 200, + fromScale: 0.2, + scaleDirection: 'xy', + slideRatio: 0.6 + }, + duration: 2000, + easing: 'cubicOut' + }); + }); + + // MoveRotateIn Animation Demo + addCase('MoveRotateIn - From Left', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#1D39C4', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'moveRotateIn', + customParameters: { + slideDirection: 'left', + slideDistance: 200, + fromAngle: Math.PI * 2, + fromScale: 0.2, + slideRatio: 0.4 + }, + duration: 2000, + easing: 'quadOut' + }); + }); + + addCase('MoveRotateIn - From Top', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#C41D7F', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'moveRotateIn', + customParameters: { + slideDirection: 'top', + slideDistance: 200, + fromAngle: Math.PI * 2, + fromScale: 0.2, + slideRatio: 0.4 + }, + duration: 2000, + easing: 'quadOut' + }); + }); + + // Complex Animation Demo with Multiple Rectangles + addCase('Story Animation Sequence', btnContainer, stage => { + // Create a group of rectangles in different positions + const positions = [ + { x: 150, y: 150, color: '#FF4D4F' }, + { x: 300, y: 150, color: '#FADB14' }, + { x: 450, y: 150, color: '#52C41A' }, + { x: 600, y: 150, color: '#1890FF' }, + { x: 150, y: 300, color: '#722ED1' }, + { x: 300, y: 300, color: '#EB2F96' }, + { x: 450, y: 300, color: '#FA8C16' }, + { x: 600, y: 300, color: '#13C2C2' } + ]; + + const rects = positions.map((pos, index) => { + const rect = createRect({ + x: pos.x, + y: pos.y, + width: 80, + height: 80, + fill: pos.color, + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + return rect; + }); + + // Apply different animations with staggered delays + rects.forEach((rect, index) => { + const executor = new AnimateExecutor(rect); + + // Choose different animation types based on index + const delay = index * 200; // 200ms stagger + + switch (index % 4) { + case 0: + executor.execute({ + type: 'slideIn', + customParameters: { + direction: 'right', + distance: 150 + }, + duration: 1000, + easing: 'quadOut', + delay + }); + break; + case 1: + executor.execute({ + type: 'growIn', + customParameters: { + fromScale: 0.2, + direction: 'xy' + }, + duration: 1000, + easing: 'elasticOut', + delay + }); + break; + case 2: + executor.execute({ + type: 'spinIn', + customParameters: { + fromAngle: Math.PI * 2, + fromScale: 0.2 + }, + duration: 1000, + easing: 'backOut', + delay + }); + break; + case 3: + executor.execute({ + type: index < 4 ? 'moveScaleIn' : 'moveRotateIn', + customParameters: { + slideDirection: 'bottom', + slideDistance: 150, + fromScale: 0.2, + fromAngle: Math.PI, + slideRatio: 0.5 + }, + duration: 1500, + easing: 'cubicOut', + delay + }); + break; + } + }); + }); +}; From 2cce6679a76bbd75885ea36beabd5e4aba70defd Mon Sep 17 00:00:00 2001 From: xiaoluoHe Date: Mon, 7 Apr 2025 18:32:59 +0800 Subject: [PATCH 068/179] fix: tagPointtsUpdate custom animation --- .../vrender-animate/src/custom/tag-points.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/vrender-animate/src/custom/tag-points.ts b/packages/vrender-animate/src/custom/tag-points.ts index 660025b43..e93f3e166 100644 --- a/packages/vrender-animate/src/custom/tag-points.ts +++ b/packages/vrender-animate/src/custom/tag-points.ts @@ -2,7 +2,7 @@ import type { IPointLike } from '@visactor/vutils'; import { clamp, isValidNumber, Point } from '@visactor/vutils'; import { ACustomAnimate } from './custom-animate'; import type { ISegment } from '@visactor/vrender-core'; -import { pointInterpolation } from '@visactor/vrender-core'; +import { pointInterpolation, ILineAttribute } from '@visactor/vrender-core'; import type { EasingType } from '../intreface/easing'; export class TagPointsUpdate extends ACustomAnimate<{ points?: IPointLike[]; segments?: ISegment[] }> { @@ -52,6 +52,13 @@ export class TagPointsUpdate extends ACustomAnimate<{ points?: IPointLike[]; seg } onBind(): void { + const { points, segments } = this.target.attribute as any; + const { points: pointsTo, segments: segmentsTo } = this.target.getFinalAttribute() as any; + + this.from = { points, segments }; + this.to = { points: pointsTo, segments: segmentsTo }; + this.props = this.to; + const originFromPoints = this.getPoints(this.from); const originToPoints = this.getPoints(this.to, true); this.fromPoints = !originFromPoints ? [] : !Array.isArray(originFromPoints) ? [originFromPoints] : originFromPoints; @@ -160,11 +167,11 @@ export class TagPointsUpdate extends ACustomAnimate<{ points?: IPointLike[]; seg } return; } - out.clipRange = this.clipRange + (1 - this.clipRange) * ratio; + this.target.setAttributes({ clipRange: this.clipRange + (1 - this.clipRange) * ratio }); } if (this.segmentsCache && this.to.segments) { let start = 0; - out.segments = this.to.segments.map((segment: any, index: any) => { + const segments = this.to.segments.map((segment: any, index: any) => { const end = start + this.segmentsCache[index]; const points = this.points.slice(start, end); start = end; @@ -173,8 +180,11 @@ export class TagPointsUpdate extends ACustomAnimate<{ points?: IPointLike[]; seg points }; }); + (this.target.attribute as ILineAttribute).points = segments; } else { - out.points = this.points; + (this.target.attribute as ILineAttribute).points = this.points; } + this.target.addUpdatePositionTag(); + this.target.addUpdateShapeAndBoundsTag(); } } From 140136c9f7ecaa126b34362b3ec91ab6c35ed7a9 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Mon, 7 Apr 2025 21:45:42 +0800 Subject: [PATCH 069/179] feat: support axis animate, fix issue with component animate conflict --- .../src/component/component-animator.ts | 14 +- .../vrender-animate/src/custom/register.ts | 15 - .../src/animation/animate-component.ts | 60 +++ .../src/animation/axis-animate.ts | 112 +++++ .../src/animation/label-animate.ts | 12 +- .../src/axis/animate/config.ts | 7 + packages/vrender-components/src/axis/base.ts | 25 +- packages/vrender-components/src/axis/line.ts | 137 +++++- .../vrender-components/src/axis/register.ts | 2 + packages/vrender-components/src/axis/type.ts | 17 + .../src/label-item/label-item.ts | 3 +- packages/vrender-components/src/label/base.ts | 24 +- .../vrender-components/src/poptip/poptip.ts | 1 - packages/vrender-core/src/common/diff.ts | 38 ++ packages/vrender-core/src/graphic/graphic.ts | 28 ++ packages/vrender-core/src/index.ts | 1 + .../browser/src/pages/story-animate.ts | 406 ------------------ 17 files changed, 445 insertions(+), 457 deletions(-) create mode 100644 packages/vrender-components/src/animation/animate-component.ts create mode 100644 packages/vrender-components/src/animation/axis-animate.ts create mode 100644 packages/vrender-components/src/axis/animate/config.ts create mode 100644 packages/vrender-core/src/common/diff.ts diff --git a/packages/vrender-animate/src/component/component-animator.ts b/packages/vrender-animate/src/component/component-animator.ts index 21b14edd3..2da91e1d1 100644 --- a/packages/vrender-animate/src/component/component-animator.ts +++ b/packages/vrender-animate/src/component/component-animator.ts @@ -129,15 +129,23 @@ export class ComponentAnimator { return this; } + deleteSelfAttr(key: string): void { + this.tasks.forEach(task => { + if (task.animate) { + task.animate.preventAttr(key); + } + }); + } + /** * Stop all animations in this component animation - * @param jumpToEnd Whether to jump to the end state (true) or start state (false) + * @param type Whether to jump to the end state or start state * @returns This ComponentAnimator */ - stop(jumpToEnd: boolean = true): ComponentAnimator { + stop(type?: 'start' | 'end'): ComponentAnimator { this.tasks.forEach(task => { if (task.animate) { - task.animate.stop(jumpToEnd ? 'end' : 'start'); + task.animate.stop(type); } }); diff --git a/packages/vrender-animate/src/custom/register.ts b/packages/vrender-animate/src/custom/register.ts index 747d6537d..cdf3b7c59 100644 --- a/packages/vrender-animate/src/custom/register.ts +++ b/packages/vrender-animate/src/custom/register.ts @@ -20,10 +20,6 @@ import { PoptipAppear, PoptipDisappear } from './poptip-animate'; import { ScaleIn, ScaleOut } from './scale'; import { State } from './state'; import { Update } from './update'; -import { InputText } from './input-text'; -import { InputRichText } from './richtext/input-richtext'; -import { SlideRichText } from './richtext/slide-richtext'; -import { SlideIn, GrowIn, SpinIn, MoveScaleIn, MoveRotateIn } from './story'; export const registerCustomAnimate = () => { // 基础动画 @@ -60,15 +56,4 @@ export const registerCustomAnimate = () => { // Poptip animations AnimateExecutor.registerBuiltInAnimate('poptipAppear', PoptipAppear); AnimateExecutor.registerBuiltInAnimate('poptipDisappear', PoptipDisappear); - // Text input animations - AnimateExecutor.registerBuiltInAnimate('inputText', InputText); - AnimateExecutor.registerBuiltInAnimate('inputRichText', InputRichText); - AnimateExecutor.registerBuiltInAnimate('slideRichText', SlideRichText); - - // 故事化动画 - AnimateExecutor.registerBuiltInAnimate('slideIn', SlideIn); - AnimateExecutor.registerBuiltInAnimate('growIn', GrowIn); - AnimateExecutor.registerBuiltInAnimate('spinIn', SpinIn); - AnimateExecutor.registerBuiltInAnimate('moveScaleIn', MoveScaleIn); - AnimateExecutor.registerBuiltInAnimate('moveRotateIn', MoveRotateIn); }; diff --git a/packages/vrender-components/src/animation/animate-component.ts b/packages/vrender-components/src/animation/animate-component.ts new file mode 100644 index 000000000..c79da835f --- /dev/null +++ b/packages/vrender-components/src/animation/animate-component.ts @@ -0,0 +1,60 @@ +import type { EasingType } from '@visactor/vrender-core'; +import { AbstractComponent } from '../core/base'; +import { isArray, isObject, merge } from '@visactor/vutils'; + +interface AnimateComponentAttribute { + animation?: boolean | any; + animationEnter?: boolean | any; + animationExit?: boolean | any; + animationUpdate?: boolean | any; +} + +/** + * 标签的离场动画配置 + */ +export interface ICommonAnimation { + /** + * 动画执行的时长 + */ + duration?: number; + /** + * 动画延迟的时长 + */ + delay?: number; + /** + * 动画的缓动函数 + */ + easing?: EasingType; +} + +export abstract class AnimateComponent extends AbstractComponent { + // parsed animation config + protected _animationConfig?: { + enter: ICommonAnimation | false; + exit: ICommonAnimation | false; + update: ICommonAnimation | false; + }; + + _prepareAnimate(defaultAnimation: ICommonAnimation) { + if (this.attribute.animation !== false) { + const { animation, animationEnter, animationExit, animationUpdate } = this.attribute; + const animationCfg = isObject(animation) ? animation : {}; + this._animationConfig = { + enter: animationEnter !== false ? merge({}, defaultAnimation, animationCfg, animationEnter ?? {}) : false, + exit: animationExit !== false ? merge({}, defaultAnimation, animationCfg, animationExit ?? {}) : false, + update: + animationUpdate !== false + ? isArray(animationUpdate) + ? animationUpdate + : merge({}, defaultAnimation, animationCfg, animationUpdate ?? {}) + : false + }; + } else { + this._animationConfig = { + enter: false, + exit: false, + update: false + }; + } + } +} diff --git a/packages/vrender-components/src/animation/axis-animate.ts b/packages/vrender-components/src/animation/axis-animate.ts new file mode 100644 index 000000000..17cabdc1f --- /dev/null +++ b/packages/vrender-components/src/animation/axis-animate.ts @@ -0,0 +1,112 @@ +import { AComponentAnimate, AnimateExecutor, createComponentAnimator } from '@visactor/vrender-animate'; + +/** + * AxisEnter class handles the enter animation for Axis components + */ +export class AxisEnter extends AComponentAnimate { + onBind(): void { + const animator = createComponentAnimator(this.target); + this._animator = animator; + const duration = this.duration; + const easing = this.easing; + const { config, updateEls, enterEls } = this.params; + + let ratio = 1; + + if (updateEls && updateEls.length > 1) { + ratio = 0.5; + const oldData1 = updateEls[0].oldEl.data; + const { rawValue: oldRawValue1, value: oldValue1 } = oldData1; + const oldData2 = updateEls[1].oldEl.data; + const { rawValue: oldRawValue2, value: oldValue2 } = oldData2; + const data = this.target.data; + const { rawValue: newRawValue } = data; + // rawValue 是原始值,value是映射出来的值,假设是线性映射,计算一下newRawValue在old阶段的value是什么值 + const oldValue = + oldValue1 + ((oldValue2 - oldValue1) * (newRawValue - oldRawValue1)) / (oldRawValue2 - oldRawValue1); + // 将 x 和 y 做映射 + const oldX1 = updateEls[0].oldEl.attribute.x; + const oldY1 = updateEls[0].oldEl.attribute.y; + const oldX2 = updateEls[1].oldEl.attribute.x; + const oldY2 = updateEls[1].oldEl.attribute.y; + const oldX = oldX1 + ((oldX2 - oldX1) * (oldValue - oldValue1)) / (oldValue2 - oldValue1); + const oldY = oldY1 + ((oldY2 - oldY1) * (oldValue - oldValue1)) / (oldValue2 - oldValue1); + const newX = this.target.attribute.x; + const newY = this.target.attribute.y; + + this.target.setAttributes({ x: oldX, y: oldY }); + animator.animate(this.target, { + type: 'to', + to: { x: newX, y: newY }, + duration, + easing + }); + } + + animator.animate(this.target, { + type: config.type ?? 'fadeIn', + to: config.to, + duration: duration * ratio, + easing + }); + this.completeBind(animator); + } +} + +/** + * AxisUpdate class handles the update animation for Axis components + */ +export class AxisUpdate extends AComponentAnimate { + onBind(): void { + const animator = createComponentAnimator(this.target); + this._animator = animator; + const duration = this.duration; + const easing = this.easing; + const { config, diffAttrs } = this.params; + // this.target.applyAnimationState( + // ['update'], + // [ + // { + // name: 'update', + // animation: { + // type: 'to', + // to: { ...this.props }, + // duration, + // easing, + // customParameters: { + // diffAttrs: { ...this.props } + // } + // } + // } + // ] + // ); + // console.log('this.props', this.props, { ...this.target.attribute }); + animator.animate(this.target, { + type: config.type ?? 'to', + to: { ...diffAttrs }, + duration, + easing, + customParameters: { + diffAttrs: { ...diffAttrs } + } + }); + this.completeBind(animator); + } + + // 轴动画本身没有逻辑,具体通过animator中执行,所以当需要屏蔽自身属性时,需要通过animator中执行 + deleteSelfAttr(key: string): void { + super.deleteSelfAttr(key); + this._animator.deleteSelfAttr(key); + } + + // 轴动画本身没有逻辑,具体通过animator中执行,所以本身不需要屏蔽冲突 + protected tryPreventConflict(): void { + return; + } +} + +export function registerAxisAnimate() { + // Label update animation + AnimateExecutor.registerBuiltInAnimate('axisEnter', AxisEnter); + AnimateExecutor.registerBuiltInAnimate('axisUpdate', AxisUpdate); +} diff --git a/packages/vrender-components/src/animation/label-animate.ts b/packages/vrender-components/src/animation/label-animate.ts index f08137032..488236bf4 100644 --- a/packages/vrender-components/src/animation/label-animate.ts +++ b/packages/vrender-components/src/animation/label-animate.ts @@ -46,7 +46,12 @@ export class LabelUpdate extends AComponentAnimate { }); } - this._animator && this._animator.start(); + this.completeBind(animator); + } + + // 标签动画本身没有逻辑,具体通过animator中执行,所以本身不需要屏蔽冲突 + protected tryPreventConflict(): void { + return; } } @@ -95,6 +100,11 @@ export class LabelEnter extends AComponentAnimate { this.completeBind(animator); } + + // 标签动画本身没有逻辑,具体通过animator中执行,所以本身不需要屏蔽冲突 + protected tryPreventConflict(): void { + return; + } } export function registerLabelAnimate() { diff --git a/packages/vrender-components/src/axis/animate/config.ts b/packages/vrender-components/src/axis/animate/config.ts new file mode 100644 index 000000000..bbcb62ddd --- /dev/null +++ b/packages/vrender-components/src/axis/animate/config.ts @@ -0,0 +1,7 @@ +import type { EasingType } from '@visactor/vrender-core'; + +export const DefaultAxisAnimation = { + type: 'default', + duration: 300, + easing: 'linear' as EasingType +}; diff --git a/packages/vrender-components/src/axis/base.ts b/packages/vrender-components/src/axis/base.ts index 963a541ff..d9f4b8f30 100644 --- a/packages/vrender-components/src/axis/base.ts +++ b/packages/vrender-components/src/axis/base.ts @@ -14,14 +14,14 @@ import type { IText } from '@visactor/vrender-core'; // eslint-disable-next-line no-duplicate-imports -import { graphicCreator } from '@visactor/vrender-core'; +import { graphicCreator, diff } from '@visactor/vrender-core'; import type { Dict } from '@visactor/vutils'; // eslint-disable-next-line no-duplicate-imports -import { abs, cloneDeep, get, isEmpty, isFunction, merge, pi } from '@visactor/vutils'; +import { abs, cloneDeep, get, isArray, isEmpty, isEqual, isFunction, merge, pi } from '@visactor/vutils'; import { AbstractComponent } from '../core/base'; import type { Point } from '../core/type'; import type { TagAttributes } from '../tag'; -import { createTextGraphicByType } from '../util'; +import { createTextGraphicByType, traverseGroup } from '../util'; import { DEFAULT_STATES } from '../constant'; import { AXIS_ELEMENT_NAME } from './constant'; import { DEFAULT_AXIS_THEME } from './config'; @@ -38,8 +38,10 @@ import type { import { Tag } from '../tag/tag'; import { getElMap, getVerticalCoord } from './util'; import { dispatchClickState, dispatchHoverState, dispatchUnHoverState } from '../util/interaction'; +import { AnimateComponent } from '../animation/animate-component'; +import { DefaultAxisAnimation } from './animate/config'; -export abstract class AxisBase extends AbstractComponent> { +export abstract class AxisBase extends AnimateComponent> { name = 'axis'; // TODO: 组件整体统一起来 @@ -74,6 +76,9 @@ export abstract class AxisBase extends AbstractCom private _lastHover: IGraphic; private _lastSelect: IGraphic; + // 用于动画diff + protected _newElementAttrMap: Dict; + protected abstract renderLine(container: IGroup): void; abstract isInValidValue(value: number): boolean; abstract getTickCoord(value: number): Point; @@ -129,14 +134,24 @@ export abstract class AxisBase extends AbstractCom } protected render(): void { + this._prepare(); this._prevInnerView = this._innerView && getElMap(this._innerView); this.removeAllChild(true); this._innerView = graphicCreator.group({ x: 0, y: 0, pickable: false }); this.add(this._innerView); - this._renderInner(this._innerView); this._bindEvent(); + // 尝试执行动画 + this.runAnimation(); + } + + protected runAnimation() { + return; + } + + protected _prepare() { + this._prepareAnimate(DefaultAxisAnimation); } private _bindEvent() { diff --git a/packages/vrender-components/src/axis/line.ts b/packages/vrender-components/src/axis/line.ts index 7f19bd964..7557bb071 100644 --- a/packages/vrender-components/src/axis/line.ts +++ b/packages/vrender-components/src/axis/line.ts @@ -15,9 +15,9 @@ import { mixin, last as peek } from '@visactor/vutils'; -import { graphicCreator } from '@visactor/vrender-core'; +import { diff, graphicCreator } from '@visactor/vrender-core'; // eslint-disable-next-line no-duplicate-imports -import type { TextAlignType, IGroup, INode, IText, TextBaselineType } from '@visactor/vrender-core'; +import type { TextAlignType, IGroup, INode, IText, TextBaselineType, IGraphic } from '@visactor/vrender-core'; import type { SegmentAttributes } from '../segment'; // eslint-disable-next-line no-duplicate-imports import { Segment } from '../segment'; @@ -27,7 +27,7 @@ import type { LineAttributes, LineAxisAttributes, TitleAttributes, AxisItem, Tra import { AxisBase } from './base'; import { DEFAULT_AXIS_THEME } from './config'; import { AXIS_ELEMENT_NAME, DEFAULT_STATES, TopZIndex } from './constant'; -import { measureTextSize } from '../util'; +import { measureTextSize, traverseGroup } from '../util'; import { autoHide as autoHideFunc } from './overlap/auto-hide'; import { autoRotate as autoRotateFunc, getXAxisLabelAlign, getYAxisLabelAlign } from './overlap/auto-rotate'; import { autoLimit as autoLimitFunc } from './overlap/auto-limit'; @@ -659,6 +659,137 @@ export class LineAxis extends AxisBase { return limitLength; } + protected runAnimation() { + if (this.attribute.animation && (this as any).applyAnimationState) { + // @ts-ignore + const currentInnerView = this.getInnerView(); + // @ts-ignore + const prevInnerView = this.getPrevInnerView(); + if (!prevInnerView) { + return; + } + + const animationConfig = this._animationConfig; + + this._newElementAttrMap = {}; + + const updateEls: { oldEl: IGraphic; newEl: IGraphic }[] = []; + const enterEls: { newEl: IGraphic }[] = []; + + // 遍历新的场景树,将新节点属性更新为旧节点 + // TODO: 目前只处理更新场景 + traverseGroup(currentInnerView, (el: IGraphic) => { + if ((el as IGraphic).type !== 'group' && el.id) { + const oldEl = prevInnerView[el.id]; + // 删除旧图元的动画 + el.setFinalAttribute(el.attribute); + if (oldEl) { + oldEl.release(); + // oldEl.stopAnimationState('enter'); + // oldEl.stopAnimationState('update'); + const oldAttrs = (oldEl as IGraphic).attribute; + const finalAttrs = el.getFinalAttribute(); + const diffAttrs: Record = diff(oldAttrs, finalAttrs); + + let hasDiff = Object.keys(diffAttrs).length > 0; + // TODO 如果入场会有fadeIn,则需要处理opacity(后续还需要考虑其他动画情况) + if ('opacity' in oldAttrs && finalAttrs.opacity !== oldAttrs.opacity) { + diffAttrs.opacity = finalAttrs.opacity ?? 1; + hasDiff = true; + } + + if (animationConfig.update && hasDiff) { + this._newElementAttrMap[el.id] = { + state: 'update', + node: el, + attrs: el.attribute + }; + const oldAttrs = (oldEl as IGraphic).attribute; + + (el as IGraphic).setAttributes(oldAttrs); + + // el.applyAnimationState( + // ['update'], + // [ + // { + // name: 'update', + // animation: { + // selfOnly: true, + // ...animationConfig.update, + // type: 'update', + // to: diffAttrs, + // customParameters: { + // config: animationConfig.update, + // diffAttrs + // } + // } + // } + // ] + // ); + + oldEl.type === 'text' && + updateEls.push({ + oldEl: oldEl, + newEl: el + }); + + el.applyAnimationState( + ['update'], + [ + { + name: 'update', + animation: { + selfOnly: true, + ...animationConfig.update, + type: 'axisUpdate', + customParameters: { + config: animationConfig.update, + diffAttrs + } + } + } + ] + ); + } + } else if (animationConfig.enter) { + this._newElementAttrMap[el.id] = { + state: 'enter', + node: el, + attrs: el.attribute + }; + + enterEls.push({ + newEl: el + }); + } + } + }); + + if (enterEls.length) { + enterEls.forEach(el => { + el.newEl.applyAnimationState( + ['enter'], + [ + { + name: 'enter', + animation: { + ...animationConfig.enter, + type: 'axisEnter', + selfOnly: true, + customParameters: { + config: animationConfig.enter, + updateEls, + enterEls + } + } + } + ] + ); + }); + } + } + } + release(): void { super.release(); this._breaks = null; diff --git a/packages/vrender-components/src/axis/register.ts b/packages/vrender-components/src/axis/register.ts index f278d7070..d24518374 100644 --- a/packages/vrender-components/src/axis/register.ts +++ b/packages/vrender-components/src/axis/register.ts @@ -7,12 +7,14 @@ import { registerRichtext, registerText } from '@visactor/vrender-kits'; +import { registerAxisAnimate } from '../animation/axis-animate'; function loadBasicAxis() { registerGroup(); registerLine(); registerRichtext(); registerText(); + registerAxisAnimate(); } export function loadLineAxisComponent() { diff --git a/packages/vrender-components/src/axis/type.ts b/packages/vrender-components/src/axis/type.ts index c61919450..356ce5540 100644 --- a/packages/vrender-components/src/axis/type.ts +++ b/packages/vrender-components/src/axis/type.ts @@ -54,6 +54,23 @@ export type AxisItem = { }; export interface AxisBaseAttributes extends IGroupGraphicAttribute { + /** + * 是否开启动画 + * @default false + */ + animation?: boolean; + /** + * 标签入场动画配置 + */ + animationEnter?: boolean; + /** + * 标签更新动画配置 + */ + animationUpdate?: boolean; + /** + * 标签离场动画配置 + */ + animationExit?: boolean; /** * 是否开启选中交互 * @default false diff --git a/packages/vrender-components/src/label-item/label-item.ts b/packages/vrender-components/src/label-item/label-item.ts index 7a0cede72..7f53b9c59 100644 --- a/packages/vrender-components/src/label-item/label-item.ts +++ b/packages/vrender-components/src/label-item/label-item.ts @@ -7,11 +7,10 @@ import type { ISymbolGraphicAttribute, IText } from '@visactor/vrender-core'; -import { InputText } from '@visactor/vrender-animate'; import { AbstractComponent } from '../core/base'; import type { IStoryLabelItemAttrs } from './type'; import type { ComponentOptions } from '../interface'; -import { max, merge } from '@visactor/vutils'; +import { merge } from '@visactor/vutils'; export class StoryLabelItem extends AbstractComponent> { name: 'labelItem'; diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index 3e4ee5a8c..5ea53edbc 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -56,9 +56,10 @@ import { connectLineBetweenBounds, getPointsOfLineArea } from './util'; import type { ComponentOptions } from '../interface'; import { loadLabelComponent } from './register'; import { shiftY } from './overlap/shiftY'; +import { AnimateComponent } from '../animation/animate-component'; loadLabelComponent(); -export class LabelBase extends AbstractComponent { +export class LabelBase extends AnimateComponent { name = 'label'; protected _baseMarks?: IGraphic[]; @@ -416,26 +417,7 @@ export class LabelBase extends AbstractComponent { } } - if (this.attribute.animation !== false) { - const { animation, animationEnter, animationExit, animationUpdate } = this.attribute; - const animationCfg = isObject(animation) ? animation : {}; - this._animationConfig = { - enter: animationEnter !== false ? merge({}, DefaultLabelAnimation, animationCfg, animationEnter ?? {}) : false, - exit: animationExit !== false ? merge({}, DefaultLabelAnimation, animationCfg, animationExit ?? {}) : false, - update: - animationUpdate !== false - ? isArray(animationUpdate) - ? animationUpdate - : merge({}, DefaultLabelAnimation, animationCfg, animationUpdate ?? {}) - : false - }; - } else { - this._animationConfig = { - enter: false, - exit: false, - update: false - }; - } + this._prepareAnimate(DefaultLabelAnimation); } protected getRelatedGraphic(item: LabelItem) { diff --git a/packages/vrender-components/src/poptip/poptip.ts b/packages/vrender-components/src/poptip/poptip.ts index d708fc299..2bc8873bf 100644 --- a/packages/vrender-components/src/poptip/poptip.ts +++ b/packages/vrender-components/src/poptip/poptip.ts @@ -11,7 +11,6 @@ import { type TextAlignType, type TextBaselineType } from '@visactor/vrender-core'; -import { InputText } from '@visactor/vrender-animate'; import { AABBBounds, Bounds, diff --git a/packages/vrender-core/src/common/diff.ts b/packages/vrender-core/src/common/diff.ts new file mode 100644 index 000000000..e2b045478 --- /dev/null +++ b/packages/vrender-core/src/common/diff.ts @@ -0,0 +1,38 @@ +import { isEqual } from '@visactor/vutils'; + +/** + * 比较两个对象的差异,深比较,返回差异的对象 + * @param oldAttrs 原始对象 + * @param newAttrs 目标对象 + * @param getAttr 获取属性值的函数(在于oldAttrs里有,newAttrs里没有的属性,调用函数获取,可选) + * @returns 差异的对象 + */ +export function diff>( + oldAttrs: T, + newAttrs: T, + getAttr?: (attr: keyof T) => any +): Record { + const diffObj: Record = {}; + + // 处理newAttrs中的属性 + for (const key in newAttrs) { + // 如果oldAttrs不存在该属性或者属性值不同 + if (!(key in oldAttrs) || !isEqual(oldAttrs[key], newAttrs[key])) { + diffObj[key] = newAttrs[key]; + } + } + + // 处理oldAttrs中有但newAttrs中没有的属性 + if (getAttr) { + for (const key in oldAttrs) { + if (!(key in newAttrs)) { + const value = getAttr(key); + if (value !== undefined) { + diffObj[key] = value; + } + } + } + } + + return diffObj; +} diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index fb804bbe1..e1c6d2b94 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -159,6 +159,33 @@ export const NOWORK_ANIMATE_ATTR = { html: 1 }; +// function createTrackableObject(obj: any) { +// // const accessedProperties = new Set(); // 记录被读取的属性 +// // const modifiedProperties = new Set(); // 记录被设置/修改的属性 + +// const handler = { +// get(target: any, property: any) { +// // accessedProperties.add(property); // 记录读取操作 +// return Reflect.get(target, property); +// }, +// set(target: any, property: any, value: any) { +// if (property === 'opacity' && obj.text === 'Nail polish') { +// console.log('set', property, value); +// } +// // modifiedProperties.add(property); // 记录设置/修改操作 +// return Reflect.set(target, property, value); +// } +// }; + +// const proxy = new Proxy(obj, handler); + +// // 提供方法获取被追踪的属性 +// proxy.getAccessedProperties = () => [...accessedProperties]; +// proxy.getModifiedProperties = () => [...modifiedProperties]; + +// return proxy; +// } + /** * globalTransMatrix更新逻辑 * 1. group的transform修改,会下发到所有下层group,将所有下层的tag修改 @@ -306,6 +333,7 @@ export abstract class Graphic = Partial { const canvas = document.getElementById('main'); // 将btnContainer添加到canvas之前 canvas.parentNode.insertBefore(btnContainer, canvas); - - // SlideIn Animation Demo - addCase('SlideIn - From Right', btnContainer, stage => { - const rect = createRect({ - x: 400, - y: 200, - width: 100, - height: 100, - fill: '#1890FF', - cornerRadius: 10 - }); - stage.defaultLayer.add(rect); - - const executor = new AnimateExecutor(rect); - executor.execute({ - type: 'slideIn', - customParameters: { - direction: 'right', - distance: 200 - }, - duration: 1000, - easing: 'quadOut' - }); - }); - - addCase('SlideIn - From Left', btnContainer, stage => { - const rect = createRect({ - x: 400, - y: 200, - width: 100, - height: 100, - fill: '#FF7A45', - cornerRadius: 10 - }); - stage.defaultLayer.add(rect); - - const executor = new AnimateExecutor(rect); - executor.execute({ - type: 'slideIn', - customParameters: { - direction: 'left', - distance: 200 - }, - duration: 1000, - easing: 'quadOut' - }); - }); - - addCase('SlideIn - From Top', btnContainer, stage => { - const rect = createRect({ - x: 400, - y: 200, - width: 100, - height: 100, - fill: '#52C41A', - cornerRadius: 10 - }); - stage.defaultLayer.add(rect); - - const executor = new AnimateExecutor(rect); - executor.execute({ - type: 'slideIn', - customParameters: { - direction: 'top', - distance: 200 - }, - duration: 1000, - easing: 'quadOut' - }); - }); - - addCase('SlideIn - From Bottom', btnContainer, stage => { - const rect = createRect({ - x: 400, - y: 200, - width: 100, - height: 100, - fill: '#722ED1', - cornerRadius: 10 - }); - stage.defaultLayer.add(rect); - - const executor = new AnimateExecutor(rect); - executor.execute({ - type: 'slideIn', - customParameters: { - direction: 'bottom', - distance: 200 - }, - duration: 1000, - easing: 'quadOut' - }); - }); - - // GrowIn Animation Demo - addCase('GrowIn - XY Scale', btnContainer, stage => { - const rect = createRect({ - x: 400, - y: 200, - width: 100, - height: 100, - fill: '#FA8C16', - cornerRadius: 10 - }); - stage.defaultLayer.add(rect); - - const executor = new AnimateExecutor(rect); - executor.execute({ - type: 'growIn', - customParameters: { - fromScale: 0.2, - direction: 'xy' - }, - duration: 1000, - easing: 'elasticOut' - }); - }); - - addCase('GrowIn - X Scale', btnContainer, stage => { - const rect = createRect({ - x: 400, - y: 200, - width: 100, - height: 100, - fill: '#13C2C2', - cornerRadius: 10 - }); - stage.defaultLayer.add(rect); - - const executor = new AnimateExecutor(rect); - executor.execute({ - type: 'growIn', - customParameters: { - fromScale: 0.2, - direction: 'x' - }, - duration: 1000, - easing: 'elasticOut' - }); - }); - - addCase('GrowIn - Y Scale', btnContainer, stage => { - const rect = createRect({ - x: 400, - y: 200, - width: 100, - height: 100, - fill: '#EB2F96', - cornerRadius: 10 - }); - stage.defaultLayer.add(rect); - - const executor = new AnimateExecutor(rect); - executor.execute({ - type: 'growIn', - customParameters: { - fromScale: 0.2, - direction: 'y' - }, - duration: 1000, - easing: 'elasticOut' - }); - }); - - // SpinIn Animation Demo - addCase('SpinIn - 360 Rotation', btnContainer, stage => { - const rect = createRect({ - x: 400, - y: 200, - width: 100, - height: 100, - fill: '#F5222D', - cornerRadius: 10 - }); - stage.defaultLayer.add(rect); - - const executor = new AnimateExecutor(rect); - executor.execute({ - type: 'spinIn', - customParameters: { - fromAngle: Math.PI * 2, - fromScale: 0.6 - }, - duration: 1000, - easing: 'quadOut' - }); - }); - - addCase('SpinIn - 180 Rotation', btnContainer, stage => { - const rect = createRect({ - x: 400, - y: 200, - width: 100, - height: 100, - fill: '#2F54EB', - cornerRadius: 10 - }); - stage.defaultLayer.add(rect); - - const executor = new AnimateExecutor(rect); - executor.execute({ - type: 'spinIn', - customParameters: { - fromAngle: Math.PI / 3, - fromScale: 0.6 - }, - duration: 500, - easing: 'quadOut' - }); - }); - - // MoveScaleIn Animation Demo - addCase('MoveScaleIn - From Right', btnContainer, stage => { - const rect = createRect({ - x: 400, - y: 200, - width: 100, - height: 100, - fill: '#A0D911', - cornerRadius: 10 - }); - stage.defaultLayer.add(rect); - - const executor = new AnimateExecutor(rect); - executor.execute({ - type: 'moveScaleIn', - customParameters: { - slideDirection: 'right', - slideDistance: 200, - fromScale: 0.6, - scaleDirection: 'xy', - slideRatio: 0.6 - }, - duration: 2000, - easing: 'cubicOut' - }); - }); - - addCase('MoveScaleIn - From Bottom', btnContainer, stage => { - const rect = createRect({ - x: 400, - y: 200, - width: 100, - height: 100, - fill: '#FAAD14', - cornerRadius: 10 - }); - stage.defaultLayer.add(rect); - - const executor = new AnimateExecutor(rect); - executor.execute({ - type: 'moveScaleIn', - customParameters: { - slideDirection: 'bottom', - slideDistance: 200, - fromScale: 0.2, - scaleDirection: 'xy', - slideRatio: 0.6 - }, - duration: 2000, - easing: 'cubicOut' - }); - }); - - // MoveRotateIn Animation Demo - addCase('MoveRotateIn - From Left', btnContainer, stage => { - const rect = createRect({ - x: 400, - y: 200, - width: 100, - height: 100, - fill: '#1D39C4', - cornerRadius: 10 - }); - stage.defaultLayer.add(rect); - - const executor = new AnimateExecutor(rect); - executor.execute({ - type: 'moveRotateIn', - customParameters: { - slideDirection: 'left', - slideDistance: 200, - fromAngle: Math.PI * 2, - fromScale: 0.2, - slideRatio: 0.4 - }, - duration: 2000, - easing: 'quadOut' - }); - }); - - addCase('MoveRotateIn - From Top', btnContainer, stage => { - const rect = createRect({ - x: 400, - y: 200, - width: 100, - height: 100, - fill: '#C41D7F', - cornerRadius: 10 - }); - stage.defaultLayer.add(rect); - - const executor = new AnimateExecutor(rect); - executor.execute({ - type: 'moveRotateIn', - customParameters: { - slideDirection: 'top', - slideDistance: 200, - fromAngle: Math.PI * 2, - fromScale: 0.2, - slideRatio: 0.4 - }, - duration: 2000, - easing: 'quadOut' - }); - }); - - // Complex Animation Demo with Multiple Rectangles - addCase('Story Animation Sequence', btnContainer, stage => { - // Create a group of rectangles in different positions - const positions = [ - { x: 150, y: 150, color: '#FF4D4F' }, - { x: 300, y: 150, color: '#FADB14' }, - { x: 450, y: 150, color: '#52C41A' }, - { x: 600, y: 150, color: '#1890FF' }, - { x: 150, y: 300, color: '#722ED1' }, - { x: 300, y: 300, color: '#EB2F96' }, - { x: 450, y: 300, color: '#FA8C16' }, - { x: 600, y: 300, color: '#13C2C2' } - ]; - - const rects = positions.map((pos, index) => { - const rect = createRect({ - x: pos.x, - y: pos.y, - width: 80, - height: 80, - fill: pos.color, - cornerRadius: 10 - }); - stage.defaultLayer.add(rect); - return rect; - }); - - // Apply different animations with staggered delays - rects.forEach((rect, index) => { - const executor = new AnimateExecutor(rect); - - // Choose different animation types based on index - const delay = index * 200; // 200ms stagger - - switch (index % 4) { - case 0: - executor.execute({ - type: 'slideIn', - customParameters: { - direction: 'right', - distance: 150 - }, - duration: 1000, - easing: 'quadOut', - delay - }); - break; - case 1: - executor.execute({ - type: 'growIn', - customParameters: { - fromScale: 0.2, - direction: 'xy' - }, - duration: 1000, - easing: 'elasticOut', - delay - }); - break; - case 2: - executor.execute({ - type: 'spinIn', - customParameters: { - fromAngle: Math.PI * 2, - fromScale: 0.2 - }, - duration: 1000, - easing: 'backOut', - delay - }); - break; - case 3: - executor.execute({ - type: index < 4 ? 'moveScaleIn' : 'moveRotateIn', - customParameters: { - slideDirection: 'bottom', - slideDistance: 150, - fromScale: 0.2, - fromAngle: Math.PI, - slideRatio: 0.5 - }, - duration: 1500, - easing: 'cubicOut', - delay - }); - break; - } - }); - }); }; From be3154d20d7b5a368e8966ab1c11362949ed1141 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 8 Apr 2025 12:50:36 +0800 Subject: [PATCH 070/179] fix: fix issue with story-animate register --- .../vrender-animate/src/custom/register.ts | 16 + .../browser/src/pages/story-animate.ts | 406 ++++++++++++++++++ 2 files changed, 422 insertions(+) diff --git a/packages/vrender-animate/src/custom/register.ts b/packages/vrender-animate/src/custom/register.ts index cdf3b7c59..a2c3e5c0e 100644 --- a/packages/vrender-animate/src/custom/register.ts +++ b/packages/vrender-animate/src/custom/register.ts @@ -14,11 +14,15 @@ import { } from './growPoints'; import { GrowRadiusIn, GrowRadiusOut } from './growRadius'; import { GrowWidthIn, GrowWidthOut } from './growWidth'; +import { InputText } from './input-text'; import { LabelItemAppear, LabelItemDisappear } from './label-item-animate'; import { IncreaseCount } from './number'; import { PoptipAppear, PoptipDisappear } from './poptip-animate'; +import { InputRichText } from './richtext/input-richtext'; +import { SlideRichText } from './richtext/slide-richtext'; import { ScaleIn, ScaleOut } from './scale'; import { State } from './state'; +import { GrowIn, MoveRotateIn, MoveScaleIn, SlideIn, SpinIn } from './story'; import { Update } from './update'; export const registerCustomAnimate = () => { @@ -56,4 +60,16 @@ export const registerCustomAnimate = () => { // Poptip animations AnimateExecutor.registerBuiltInAnimate('poptipAppear', PoptipAppear); AnimateExecutor.registerBuiltInAnimate('poptipDisappear', PoptipDisappear); + + // Text input animations + AnimateExecutor.registerBuiltInAnimate('inputText', InputText); + AnimateExecutor.registerBuiltInAnimate('inputRichText', InputRichText); + AnimateExecutor.registerBuiltInAnimate('slideRichText', SlideRichText); + + // 故事化动画 + AnimateExecutor.registerBuiltInAnimate('slideIn', SlideIn); + AnimateExecutor.registerBuiltInAnimate('growIn', GrowIn); + AnimateExecutor.registerBuiltInAnimate('spinIn', SpinIn); + AnimateExecutor.registerBuiltInAnimate('moveScaleIn', MoveScaleIn); + AnimateExecutor.registerBuiltInAnimate('moveRotateIn', MoveRotateIn); }; diff --git a/packages/vrender/__tests__/browser/src/pages/story-animate.ts b/packages/vrender/__tests__/browser/src/pages/story-animate.ts index b7a7e6c51..c9df46752 100644 --- a/packages/vrender/__tests__/browser/src/pages/story-animate.ts +++ b/packages/vrender/__tests__/browser/src/pages/story-animate.ts @@ -64,4 +64,410 @@ export const page = () => { const canvas = document.getElementById('main'); // 将btnContainer添加到canvas之前 canvas.parentNode.insertBefore(btnContainer, canvas); + + // SlideIn Animation Demo + addCase('SlideIn - From Right', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#1890FF', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'slideIn', + customParameters: { + direction: 'right', + distance: 200 + }, + duration: 1000, + easing: 'quadOut' + }); + }); + + addCase('SlideIn - From Left', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#FF7A45', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'slideIn', + customParameters: { + direction: 'left', + distance: 200 + }, + duration: 1000, + easing: 'quadOut' + }); + }); + + addCase('SlideIn - From Top', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#52C41A', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'slideIn', + customParameters: { + direction: 'top', + distance: 200 + }, + duration: 1000, + easing: 'quadOut' + }); + }); + + addCase('SlideIn - From Bottom', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#722ED1', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'slideIn', + customParameters: { + direction: 'bottom', + distance: 200 + }, + duration: 1000, + easing: 'quadOut' + }); + }); + + // GrowIn Animation Demo + addCase('GrowIn - XY Scale', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#FA8C16', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'growIn', + customParameters: { + fromScale: 0.2, + direction: 'xy' + }, + duration: 1000, + easing: 'elasticOut' + }); + }); + + addCase('GrowIn - X Scale', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#13C2C2', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'growIn', + customParameters: { + fromScale: 0.2, + direction: 'x' + }, + duration: 1000, + easing: 'elasticOut' + }); + }); + + addCase('GrowIn - Y Scale', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#EB2F96', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'growIn', + customParameters: { + fromScale: 0.2, + direction: 'y' + }, + duration: 1000, + easing: 'elasticOut' + }); + }); + + // SpinIn Animation Demo + addCase('SpinIn - 360 Rotation', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#F5222D', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'spinIn', + customParameters: { + fromAngle: Math.PI * 2, + fromScale: 0.6 + }, + duration: 1000, + easing: 'quadOut' + }); + }); + + addCase('SpinIn - 180 Rotation', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#2F54EB', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'spinIn', + customParameters: { + fromAngle: Math.PI / 3, + fromScale: 0.6 + }, + duration: 500, + easing: 'quadOut' + }); + }); + + // MoveScaleIn Animation Demo + addCase('MoveScaleIn - From Right', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#A0D911', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'moveScaleIn', + customParameters: { + slideDirection: 'right', + slideDistance: 200, + fromScale: 0.6, + scaleDirection: 'xy', + slideRatio: 0.6 + }, + duration: 2000, + easing: 'cubicOut' + }); + }); + + addCase('MoveScaleIn - From Bottom', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#FAAD14', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'moveScaleIn', + customParameters: { + slideDirection: 'bottom', + slideDistance: 200, + fromScale: 0.2, + scaleDirection: 'xy', + slideRatio: 0.6 + }, + duration: 2000, + easing: 'cubicOut' + }); + }); + + // MoveRotateIn Animation Demo + addCase('MoveRotateIn - From Left', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#1D39C4', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'moveRotateIn', + customParameters: { + slideDirection: 'left', + slideDistance: 200, + fromAngle: Math.PI * 2, + fromScale: 0.2, + slideRatio: 0.4 + }, + duration: 2000, + easing: 'quadOut' + }); + }); + + addCase('MoveRotateIn - From Top', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#C41D7F', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'moveRotateIn', + customParameters: { + slideDirection: 'top', + slideDistance: 200, + fromAngle: Math.PI * 2, + fromScale: 0.2, + slideRatio: 0.4 + }, + duration: 2000, + easing: 'quadOut' + }); + }); + + // Complex Animation Demo with Multiple Rectangles + addCase('Story Animation Sequence', btnContainer, stage => { + // Create a group of rectangles in different positions + const positions = [ + { x: 150, y: 150, color: '#FF4D4F' }, + { x: 300, y: 150, color: '#FADB14' }, + { x: 450, y: 150, color: '#52C41A' }, + { x: 600, y: 150, color: '#1890FF' }, + { x: 150, y: 300, color: '#722ED1' }, + { x: 300, y: 300, color: '#EB2F96' }, + { x: 450, y: 300, color: '#FA8C16' }, + { x: 600, y: 300, color: '#13C2C2' } + ]; + + const rects = positions.map((pos, index) => { + const rect = createRect({ + x: pos.x, + y: pos.y, + width: 80, + height: 80, + fill: pos.color, + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + return rect; + }); + + // Apply different animations with staggered delays + rects.forEach((rect, index) => { + const executor = new AnimateExecutor(rect); + + // Choose different animation types based on index + const delay = index * 200; // 200ms stagger + + switch (index % 4) { + case 0: + executor.execute({ + type: 'slideIn', + customParameters: { + direction: 'right', + distance: 150 + }, + duration: 1000, + easing: 'quadOut', + delay + }); + break; + case 1: + executor.execute({ + type: 'growIn', + customParameters: { + fromScale: 0.2, + direction: 'xy' + }, + duration: 1000, + easing: 'elasticOut', + delay + }); + break; + case 2: + executor.execute({ + type: 'spinIn', + customParameters: { + fromAngle: Math.PI * 2, + fromScale: 0.2 + }, + duration: 1000, + easing: 'backOut', + delay + }); + break; + case 3: + executor.execute({ + type: index < 4 ? 'moveScaleIn' : 'moveRotateIn', + customParameters: { + slideDirection: 'bottom', + slideDistance: 150, + fromScale: 0.2, + fromAngle: Math.PI, + slideRatio: 0.5 + }, + duration: 1500, + easing: 'cubicOut', + delay + }); + break; + } + }); + }); }; From 9202b0472fb2bcdf5cf01fde088a2d2fe0382bfc Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 8 Apr 2025 13:07:51 +0800 Subject: [PATCH 071/179] feat: support story animate out --- .../vrender-animate/src/custom/register.ts | 22 +- packages/vrender-animate/src/custom/story.ts | 295 ++++++++++ .../browser/src/pages/story-animate.ts | 517 ++++++++++++++++++ 3 files changed, 832 insertions(+), 2 deletions(-) diff --git a/packages/vrender-animate/src/custom/register.ts b/packages/vrender-animate/src/custom/register.ts index a2c3e5c0e..caffc3358 100644 --- a/packages/vrender-animate/src/custom/register.ts +++ b/packages/vrender-animate/src/custom/register.ts @@ -22,7 +22,18 @@ import { InputRichText } from './richtext/input-richtext'; import { SlideRichText } from './richtext/slide-richtext'; import { ScaleIn, ScaleOut } from './scale'; import { State } from './state'; -import { GrowIn, MoveRotateIn, MoveScaleIn, SlideIn, SpinIn } from './story'; +import { + GrowIn, + GrowOut, + MoveRotateIn, + MoveRotateOut, + MoveScaleIn, + MoveScaleOut, + SlideIn, + SlideOut, + SpinIn, + SpinOut +} from './story'; import { Update } from './update'; export const registerCustomAnimate = () => { @@ -66,10 +77,17 @@ export const registerCustomAnimate = () => { AnimateExecutor.registerBuiltInAnimate('inputRichText', InputRichText); AnimateExecutor.registerBuiltInAnimate('slideRichText', SlideRichText); - // 故事化动画 + // 故事化动画 - 入场 AnimateExecutor.registerBuiltInAnimate('slideIn', SlideIn); AnimateExecutor.registerBuiltInAnimate('growIn', GrowIn); AnimateExecutor.registerBuiltInAnimate('spinIn', SpinIn); AnimateExecutor.registerBuiltInAnimate('moveScaleIn', MoveScaleIn); AnimateExecutor.registerBuiltInAnimate('moveRotateIn', MoveRotateIn); + + // 故事化动画 - 出场 + AnimateExecutor.registerBuiltInAnimate('slideOut', SlideOut); + AnimateExecutor.registerBuiltInAnimate('growOut', GrowOut); + AnimateExecutor.registerBuiltInAnimate('spinOut', SpinOut); + AnimateExecutor.registerBuiltInAnimate('moveScaleOut', MoveScaleOut); + AnimateExecutor.registerBuiltInAnimate('moveRotateOut', MoveRotateOut); }; diff --git a/packages/vrender-animate/src/custom/story.ts b/packages/vrender-animate/src/custom/story.ts index b753b66a8..3b46305d6 100644 --- a/packages/vrender-animate/src/custom/story.ts +++ b/packages/vrender-animate/src/custom/story.ts @@ -326,3 +326,298 @@ export class MoveRotateIn extends ACustomAnimate { // 动画逻辑由子动画处理 } } + +/** + * 滑动出场动画,包括从当前位置滑动到指定方向,以及透明度属性插值 + */ +export class SlideOut extends ACustomAnimate> { + declare valid: boolean; + declare propKeys: string[]; + declare from: Record; + declare to: Record; + + constructor( + from: null, + to: null, + duration: number, + easing: EasingType, + params?: ISlideAnimationOptions & { toOpacity?: number } + ) { + super(from, to, duration, easing, params); + } + + onFirstRun(): void { + // 用于出场的时候设置属性 + const attrs = this.target.getAttributes(); + + const direction = (this.params?.direction as 'top' | 'bottom' | 'left' | 'right') || 'right'; + const distance = this.params?.distance || 50; + const fromOpacity = this.params?.fromOpacity ?? 1; // 使用透明度初始值参数 + const toOpacity = this.params?.toOpacity ?? 0; // 使用目标透明度参数 + + // 初始化from和to对象 + const from: Record = { opacity: fromOpacity }; + const to: Record = { opacity: toOpacity }; + + // 根据方向设置对应的属性 + if (direction === 'top') { + from.y = attrs.y ?? 0; + to.y = (attrs.y ?? 0) - distance; + this.propKeys = ['opacity', 'y']; + } else if (direction === 'bottom') { + from.y = attrs.y ?? 0; + to.y = (attrs.y ?? 0) + distance; + this.propKeys = ['opacity', 'y']; + } else if (direction === 'left') { + from.x = attrs.x ?? 0; + to.x = (attrs.x ?? 0) - distance; + this.propKeys = ['opacity', 'x']; + } else { + // right + from.x = attrs.x ?? 0; + to.x = (attrs.x ?? 0) + distance; + this.propKeys = ['opacity', 'x']; + } + + this.from = from; + this.to = to; + this.props = to; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; + this.propKeys.forEach(key => { + attribute[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.addUpdatePositionTag(); + this.target.addUpdateShapeAndBoundsTag(); + } +} + +/** + * 缩放出场动画,包括scaleX、scaleY属性从当前比例缩放到指定比例,以及透明度属性插值 + */ +export class GrowOut extends ACustomAnimate> { + declare valid: boolean; + declare propKeys: string[]; + declare from: Record; + declare to: Record; + + constructor( + from: null, + to: null, + duration: number, + easing: EasingType, + params?: IGrowAnimationOptions & { toOpacity?: number } + ) { + super(from, to, duration, easing, params); + } + + onFirstRun(): void { + // 用于出场的时候设置属性 + const attrs = this.target.getAttributes(); + + const toScale = this.params?.fromScale ?? 0; // 使用fromScale作为目标比例 + const direction = this.params?.direction || 'xy'; + const fromOpacity = this.params?.fromOpacity ?? 1; // 使用透明度初始值参数 + const toOpacity = this.params?.toOpacity ?? 0; // 使用目标透明度参数 + + // 初始化from和to对象 + const from: Record = { opacity: fromOpacity }; + const to: Record = { opacity: toOpacity }; + this.propKeys = ['opacity']; + + // 根据方向设置对应的缩放属性 + if (direction === 'x' || direction === 'xy') { + from.scaleX = attrs.scaleX ?? 1; + to.scaleX = toScale; + this.propKeys.push('scaleX'); + } + + if (direction === 'y' || direction === 'xy') { + from.scaleY = attrs.scaleY ?? 1; + to.scaleY = toScale; + this.propKeys.push('scaleY'); + } + + this.from = from; + this.to = to; + this.props = to; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; + this.propKeys.forEach(key => { + attribute[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.addUpdatePositionTag(); + this.target.addUpdateShapeAndBoundsTag(); + } +} + +/** + * 旋转出场动画,包括rotate属性从当前角度旋转到指定角度,以及缩放属性从当前比例缩放到指定比例 + */ +export class SpinOut extends ACustomAnimate> { + declare valid: boolean; + declare propKeys: string[]; + declare from: Record; + declare to: Record; + + constructor( + from: null, + to: null, + duration: number, + easing: EasingType, + params?: ISpinAnimationOptions & { toOpacity?: number } + ) { + super(from, to, duration, easing, params); + } + + onFirstRun(): void { + // 用于出场的时候设置属性 + const attrs = this.target.getAttributes(); + + const toAngle = this.params?.fromAngle ?? Math.PI * 2; // 默认旋转一圈 + const toScale = this.params?.fromScale ?? 0; + const fromOpacity = this.params?.fromOpacity ?? 1; // 使用透明度初始值参数 + const toOpacity = this.params?.toOpacity ?? 0; // 使用目标透明度参数 + + // 初始化from和to对象 + const from: Record = { + opacity: fromOpacity, + angle: attrs.angle ?? 0, + scaleX: attrs.scaleX ?? 1, + scaleY: attrs.scaleY ?? 1 + }; + + const to: Record = { + opacity: toOpacity, + angle: toAngle, + scaleX: toScale, + scaleY: toScale + }; + + this.propKeys = ['opacity', 'angle', 'scaleX', 'scaleY']; + this.from = from; + this.to = to; + this.props = to; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; + this.propKeys.forEach(key => { + attribute[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.addUpdatePositionTag(); + this.target.addUpdateShapeAndBoundsTag(); + } +} + +/** + * 移动+缩放出场动画 + * 先走GrowOut,然后走SlideOut + */ +export class MoveScaleOut extends ACustomAnimate { + declare valid: boolean; + private readonly growOutDuration: number; + private readonly slideOutDuration: number; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: IMoveScaleAnimationOptions) { + super(from, to, duration, easing, params); + const slideRatio = params?.slideRatio ?? 0.5; + this.growOutDuration = duration * (1 - slideRatio); + this.slideOutDuration = duration * slideRatio; + } + + onFirstRun(): void { + // 创建AnimateExecutor来运行序列动画 + const executor = new AnimateExecutor(this.target); + + // 第一步:缩放出场(不包含透明度变化) + executor.execute({ + type: 'custom', + custom: GrowOut, + customParameters: { + fromScale: this.params?.fromScale || 0.5, + direction: this.params?.scaleDirection || 'xy', + fromOpacity: 1, // 保持透明度为1,不做变化 + toOpacity: 1 // 确保第一阶段不改变透明度 + }, + duration: this.growOutDuration, + easing: this.easing + }); + + // 第二步:滑动出场(包含透明度变化) + executor.execute({ + type: 'custom', + custom: SlideOut, + customParameters: { + direction: this.params?.slideDirection || 'right', + distance: this.params?.slideDistance || 50, + fromOpacity: 1 // 起始透明度为1 + }, + duration: this.slideOutDuration, + easing: this.easing, + delay: this.growOutDuration // 等第一步完成后再开始 + }); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + // 动画逻辑由子动画处理 + } +} + +/** + * 移动+旋转出场动画 + * 先走SpinOut,然后走SlideOut + */ +export class MoveRotateOut extends ACustomAnimate { + declare valid: boolean; + private readonly spinOutDuration: number; + private readonly slideOutDuration: number; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: IMoveRotateAnimationOptions) { + super(from, to, duration, easing, params); + const slideRatio = params?.slideRatio ?? 0.5; + this.spinOutDuration = duration * (1 - slideRatio); + this.slideOutDuration = duration * slideRatio; + } + + onFirstRun(): void { + // 创建AnimateExecutor来运行序列动画 + const executor = new AnimateExecutor(this.target); + + // 第一步:旋转出场(不包含透明度变化) + executor.execute({ + type: 'custom', + custom: SpinOut, + customParameters: { + fromAngle: this.params?.fromAngle || Math.PI, + fromScale: this.params?.fromScale || 0.5, + fromOpacity: 1, // 保持透明度为1,不做变化 + toOpacity: 1 // 确保第一阶段不改变透明度 + }, + duration: this.spinOutDuration, + easing: this.easing + }); + + // 第二步:滑动出场(包含透明度变化) + executor.execute({ + type: 'custom', + custom: SlideOut, + customParameters: { + direction: this.params?.slideDirection || 'right', + distance: this.params?.slideDistance || 50, + fromOpacity: 1 // 起始透明度为1 + }, + duration: this.slideOutDuration, + easing: this.easing, + delay: this.spinOutDuration // 等第一步完成后再开始 + }); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + // 动画逻辑由子动画处理 + } +} diff --git a/packages/vrender/__tests__/browser/src/pages/story-animate.ts b/packages/vrender/__tests__/browser/src/pages/story-animate.ts index c9df46752..447f05dfe 100644 --- a/packages/vrender/__tests__/browser/src/pages/story-animate.ts +++ b/packages/vrender/__tests__/browser/src/pages/story-animate.ts @@ -470,4 +470,521 @@ export const page = () => { } }); }); + + // ====== EXIT ANIMATIONS DEMOS ====== + + // SlideOut Animation Demo + addCase('SlideOut - To Right', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#1890FF', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'slideOut', + customParameters: { + direction: 'right', + distance: 200 + }, + duration: 1000, + easing: 'quadOut' + }); + }); + + addCase('SlideOut - To Left', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#FF7A45', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'slideOut', + customParameters: { + direction: 'left', + distance: 200 + }, + duration: 1000, + easing: 'quadOut' + }); + }); + + addCase('SlideOut - To Top', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#52C41A', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'slideOut', + customParameters: { + direction: 'top', + distance: 200 + }, + duration: 1000, + easing: 'quadOut' + }); + }); + + addCase('SlideOut - To Bottom', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#722ED1', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'slideOut', + customParameters: { + direction: 'bottom', + distance: 200 + }, + duration: 1000, + easing: 'quadOut' + }); + }); + + // GrowOut Animation Demo + addCase('GrowOut - XY Scale', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#FA8C16', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'growOut', + customParameters: { + fromScale: 0.2, + direction: 'xy' + }, + duration: 1000, + easing: 'elasticOut' + }); + }); + + addCase('GrowOut - X Scale', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#13C2C2', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'growOut', + customParameters: { + fromScale: 0.2, + direction: 'x' + }, + duration: 1000, + easing: 'elasticOut' + }); + }); + + addCase('GrowOut - Y Scale', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#EB2F96', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'growOut', + customParameters: { + fromScale: 0.2, + direction: 'y' + }, + duration: 1000, + easing: 'elasticOut' + }); + }); + + // SpinOut Animation Demo + addCase('SpinOut - 360 Rotation', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#F5222D', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'spinOut', + customParameters: { + fromAngle: Math.PI * 2, + fromScale: 0.6 + }, + duration: 1000, + easing: 'quadOut' + }); + }); + + addCase('SpinOut - 180 Rotation', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#2F54EB', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'spinOut', + customParameters: { + fromAngle: Math.PI / 3, + fromScale: 0.6 + }, + duration: 500, + easing: 'quadOut' + }); + }); + + // MoveScaleOut Animation Demo + addCase('MoveScaleOut - To Right', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#A0D911', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'moveScaleOut', + customParameters: { + slideDirection: 'right', + slideDistance: 200, + fromScale: 0.6, + scaleDirection: 'xy', + slideRatio: 0.6 + }, + duration: 2000, + easing: 'cubicOut' + }); + }); + + addCase('MoveScaleOut - To Bottom', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#FAAD14', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'moveScaleOut', + customParameters: { + slideDirection: 'bottom', + slideDistance: 200, + fromScale: 0.2, + scaleDirection: 'xy', + slideRatio: 0.6 + }, + duration: 2000, + easing: 'cubicOut' + }); + }); + + // MoveRotateOut Animation Demo + addCase('MoveRotateOut - To Left', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#1D39C4', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'moveRotateOut', + customParameters: { + slideDirection: 'left', + slideDistance: 200, + fromAngle: Math.PI * 2, + fromScale: 0.2, + slideRatio: 0.4 + }, + duration: 2000, + easing: 'quadOut' + }); + }); + + addCase('MoveRotateOut - To Top', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#C41D7F', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'moveRotateOut', + customParameters: { + slideDirection: 'top', + slideDistance: 200, + fromAngle: Math.PI * 2, + fromScale: 0.2, + slideRatio: 0.4 + }, + duration: 2000, + easing: 'quadOut' + }); + }); + + // In-Out Animation Sequence + addCase('In-Out Animation Sequence', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#FF4D4F', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + + // First perform entrance animation + executor.execute({ + type: 'slideIn', + customParameters: { + direction: 'right', + distance: 200 + }, + duration: 1000, + easing: 'quadOut' + }); + + // Then perform exit animation after delay + executor.execute({ + type: 'slideOut', + customParameters: { + direction: 'left', + distance: 200 + }, + duration: 1000, + easing: 'quadOut', + delay: 2000 // Wait 2 seconds before starting exit animation + }); + }); + + // Opacity Control in Exit Animations Demo + addCase('Exit Animation Opacity Control', btnContainer, stage => { + // Create a row of rectangles + const colors = ['#1890FF', '#13C2C2', '#F5222D', '#A0D911', '#1D39C4']; + const rects = []; + + // Create 5 rectangles in a row + for (let i = 0; i < 5; i++) { + const rect = createRect({ + x: 150 + i * 150, + y: 200, + width: 80, + height: 80, + fill: colors[i], + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + rects.push(rect); + } + + // Standard moveScaleOut + const executor1 = new AnimateExecutor(rects[0]); + executor1.execute({ + type: 'moveScaleOut', + customParameters: { + slideDirection: 'right', + slideDistance: 100, + fromScale: 0.2, + scaleDirection: 'xy', + slideRatio: 0.5 + }, + duration: 2000, + easing: 'cubicOut' + }); + + // GrowOut with no opacity change (toOpacity: 1) + const executor2 = new AnimateExecutor(rects[1]); + executor2.execute({ + type: 'growOut', + customParameters: { + fromScale: 0.2, + direction: 'xy', + toOpacity: 1 // Keep fully visible during scale + }, + duration: 1000, + easing: 'quadOut' + }); + + // SpinOut with no opacity change, followed by fade out + const executor3 = new AnimateExecutor(rects[2]); + // First spin without opacity change + executor3.execute({ + type: 'spinOut', + customParameters: { + fromAngle: Math.PI * 1.5, + fromScale: 0.4, + toOpacity: 1 // Keep fully visible during spin + }, + duration: 1000, + easing: 'quadOut' + }); + // Then fade out + executor3.execute({ + type: 'fadeOut', + duration: 500, + easing: 'quadOut', + delay: 1000 + }); + + // SlideOut with partial opacity + const executor4 = new AnimateExecutor(rects[3]); + executor4.execute({ + type: 'slideOut', + customParameters: { + direction: 'top', + distance: 200, + toOpacity: 0.3 // Fade to 30% opacity rather than fully invisible + }, + duration: 1000, + easing: 'quadOut' + }); + + // Custom two-phase exit animation + const executor5 = new AnimateExecutor(rects[4]); + // Phase 1: Scale down without opacity change + executor5.execute({ + type: 'growOut', + customParameters: { + fromScale: 0.6, + direction: 'xy', + toOpacity: 1 // Maintain visibility + }, + duration: 800, + easing: 'quadOut' + }); + // Phase 2: Move out with opacity change + executor5.execute({ + type: 'slideOut', + customParameters: { + direction: 'right', + distance: 150 + }, + duration: 1000, + easing: 'quadOut', + delay: 800 // Start after first animation + }); + }); + + // Combined Enter-Exit Sequence Demo with Opacity Control + addCase('Enter-Exit Sequence with Opacity Control', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#EB2F96', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + + // First: entrance animation + executor.execute({ + type: 'growIn', + customParameters: { + fromScale: 0.2, + direction: 'xy' + }, + duration: 1000, + easing: 'elasticOut' + }); + + // Middle: wait in visible state + + // Last: custom exit animation - first spin without fading, then slide out with fade + executor.execute({ + type: 'spinOut', + customParameters: { + fromAngle: Math.PI, + fromScale: 0.7, + toOpacity: 1 // Keep fully visible during spin + }, + duration: 800, + easing: 'quadOut', + delay: 1500 // Wait 1.5s after entrance completes + }); + + executor.execute({ + type: 'slideOut', + customParameters: { + direction: 'bottom', + distance: 100 + // Default toOpacity: 0 for complete fade-out + }, + duration: 600, + easing: 'quadOut', + delay: 2300 // Wait for spin to complete + }); + }); }; From 5c9889a4f94dfbcb0e24208fd42904176d0d37f7 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 8 Apr 2025 13:29:24 +0800 Subject: [PATCH 072/179] feat: support text animate out --- .../vrender-animate/src/custom/register.ts | 4 + .../src/custom/richtext/output-richtext.ts | 277 +++++++++++++ .../src/custom/richtext/slide-out-richtext.ts | 371 ++++++++++++++++++ .../browser/src/pages/custom-animate.ts | 356 +++++++++++++++++ 4 files changed, 1008 insertions(+) create mode 100644 packages/vrender-animate/src/custom/richtext/output-richtext.ts create mode 100644 packages/vrender-animate/src/custom/richtext/slide-out-richtext.ts diff --git a/packages/vrender-animate/src/custom/register.ts b/packages/vrender-animate/src/custom/register.ts index caffc3358..9025a3850 100644 --- a/packages/vrender-animate/src/custom/register.ts +++ b/packages/vrender-animate/src/custom/register.ts @@ -19,7 +19,9 @@ import { LabelItemAppear, LabelItemDisappear } from './label-item-animate'; import { IncreaseCount } from './number'; import { PoptipAppear, PoptipDisappear } from './poptip-animate'; import { InputRichText } from './richtext/input-richtext'; +import { OutputRichText } from './richtext/output-richtext'; import { SlideRichText } from './richtext/slide-richtext'; +import { SlideOutRichText } from './richtext/slide-out-richtext'; import { ScaleIn, ScaleOut } from './scale'; import { State } from './state'; import { @@ -75,7 +77,9 @@ export const registerCustomAnimate = () => { // Text input animations AnimateExecutor.registerBuiltInAnimate('inputText', InputText); AnimateExecutor.registerBuiltInAnimate('inputRichText', InputRichText); + AnimateExecutor.registerBuiltInAnimate('outputRichText', OutputRichText); AnimateExecutor.registerBuiltInAnimate('slideRichText', SlideRichText); + AnimateExecutor.registerBuiltInAnimate('slideOutRichText', SlideOutRichText); // 故事化动画 - 入场 AnimateExecutor.registerBuiltInAnimate('slideIn', SlideIn); diff --git a/packages/vrender-animate/src/custom/richtext/output-richtext.ts b/packages/vrender-animate/src/custom/richtext/output-richtext.ts new file mode 100644 index 000000000..17843bd60 --- /dev/null +++ b/packages/vrender-animate/src/custom/richtext/output-richtext.ts @@ -0,0 +1,277 @@ +import type { IAnimate, IStep } from '../../intreface/animate'; +import type { EasingType } from '../../intreface/easing'; +import { ACustomAnimate } from '../custom-animate'; +import type { IRichTextCharacter, IRichTextParagraphCharacter } from '@visactor/vrender-core'; +import { RichText } from '@visactor/vrender-core'; +import { InputRichText } from './input-richtext'; + +/** + * 富文本退出动画,实现类似打字机的字符逐个消失效果 + * 支持通过beforeText和afterText参数添加前缀和后缀 + * 支持通过showCursor参数显示光标,cursorChar自定义光标字符 + * 支持通过fadeOutChars参数开启字符透明度渐变效果 + * 支持通过direction参数控制消失方向(从头到尾或从尾到头) + */ +export class OutputRichText extends ACustomAnimate<{ textConfig: IRichTextCharacter[] }> { + declare valid: boolean; + + private fromTextConfig: IRichTextCharacter[] = []; + private toTextConfig: IRichTextCharacter[] = []; + private originalTextConfig: IRichTextCharacter[] = []; + private showCursor: boolean = false; + private cursorChar: string = '|'; + private blinkCursor: boolean = true; + private beforeText: string = ''; + private afterText: string = ''; + private fadeOutChars: boolean = false; + private fadeOutDuration: number = 0.3; // 透明度渐变持续时间,以动画总时长的比例表示 + private direction: 'forward' | 'backward' = 'backward'; // 字符消失方向,默认从尾到头(backward) + + constructor( + from: { textConfig: IRichTextCharacter[] }, + to: { textConfig: IRichTextCharacter[] }, + duration: number, + easing: EasingType, + params?: { + showCursor?: boolean; + cursorChar?: string; + blinkCursor?: boolean; + beforeText?: string; + afterText?: string; + fadeOutChars?: boolean; + fadeOutDuration?: number; + direction?: 'forward' | 'backward'; + } + ) { + super(from, to, duration, easing, params); + + // 配置光标相关选项 + if (params?.showCursor !== undefined) { + this.showCursor = params.showCursor; + } + if (params?.cursorChar !== undefined) { + this.cursorChar = params.cursorChar; + } + if (params?.blinkCursor !== undefined) { + this.blinkCursor = params.blinkCursor; + } + + // 配置前缀和后缀文本 + if (params?.beforeText !== undefined) { + this.beforeText = params.beforeText; + } + if (params?.afterText !== undefined) { + this.afterText = params.afterText; + } + + // 配置字符透明度渐变效果 + if (params?.fadeOutChars !== undefined) { + this.fadeOutChars = params.fadeOutChars; + } + if (params?.fadeOutDuration !== undefined) { + this.fadeOutDuration = params.fadeOutDuration; + } + + // 配置方向 + if (params?.direction !== undefined) { + this.direction = params.direction; + } + + this.propKeys = ['textConfig']; + } + + onFirstRun(): void { + const fromProps = this.getLastProps(); + const toProps = this.getEndProps(); + + // 存储原始配置(这里是起始状态,显示所有文本) + this.originalTextConfig = fromProps.textConfig ? [...fromProps.textConfig] : []; + + // 初始化解析结果 + this.valid = true; + + // 确保from不为空 + if (!this.originalTextConfig || this.originalTextConfig.length === 0) { + this.valid = false; + return; + } + + // 将文本拆分为单个字符,使用RichText的静态方法 + this.fromTextConfig = RichText.TransformTextConfig2SingleCharacter(this.originalTextConfig); + + // 目标状态是空文本(或指定的目标) + this.toTextConfig = + toProps.textConfig && toProps.textConfig.length > 0 + ? RichText.TransformTextConfig2SingleCharacter(toProps.textConfig) + : []; + } + + onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { + super.onEnd(cb); + if (!cb) { + // 动画结束时,应用最终textConfig(通常是空的或特定的toTextConfig) + if (this.toTextConfig.length > 0) { + this.target.setAttribute('textConfig', this.toTextConfig); + } else { + this.target.setAttribute('textConfig', []); + } + } + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + if (!this.valid) { + return; + } + + // 获取当前应该显示的字符 + const fromItems = this.fromTextConfig.length; + + // 计算文本显示比例上限 - 如果有渐变效果,需要为尾部字符的渐变留出时间 + const maxTextHideRatio = this.fadeOutChars ? 1 - this.fadeOutDuration : 1; + + // 根据方向确定字符消失的顺序 + let displayedLength: number; + + if (this.direction === 'forward') { + // 从前往后消失(类似于正向打字效果) + if (this.fadeOutChars) { + // 当ratio达到maxTextHideRatio时,应该已经隐藏全部文本 + const adjustedRatio = Math.min(1, ratio / maxTextHideRatio); + displayedLength = Math.round(fromItems * (1 - adjustedRatio)); + } else { + // 无渐变效果时,正常隐藏 + displayedLength = Math.round(fromItems * (1 - ratio)); + } + + // 构建从头开始删除的文本配置 + let currentTextConfig = + this.direction === 'forward' + ? this.fromTextConfig.slice(fromItems - displayedLength) // 从头开始隐藏,保留尾部 + : this.fromTextConfig.slice(0, displayedLength); // 从尾开始隐藏,保留头部 + + // 应用透明度渐变效果 + if (this.fadeOutChars) { + currentTextConfig = this.applyFadeEffect(currentTextConfig, ratio, fromItems, displayedLength); + } + + // 如果启用了光标 + if (this.showCursor && displayedLength > 0) { + currentTextConfig = this.addCursor(currentTextConfig, ratio); + } + + // 更新富文本的textConfig属性 + this.target.setAttribute('textConfig', currentTextConfig); + } else { + // 从后往前消失(类似于退格删除效果) + if (this.fadeOutChars) { + // 当ratio达到maxTextHideRatio时,应该已经隐藏全部文本 + const adjustedRatio = Math.min(1, ratio / maxTextHideRatio); + displayedLength = Math.round(fromItems * (1 - adjustedRatio)); + } else { + // 无渐变效果时,正常隐藏 + displayedLength = Math.round(fromItems * (1 - ratio)); + } + + // 构建从尾开始删除的文本配置 + let currentTextConfig = this.fromTextConfig.slice(0, displayedLength); + + // 应用透明度渐变效果 + if (this.fadeOutChars) { + currentTextConfig = this.applyFadeEffect(currentTextConfig, ratio, fromItems, displayedLength); + } + + // 如果启用了光标 + if (this.showCursor && displayedLength > 0) { + currentTextConfig = this.addCursor(currentTextConfig, ratio); + } + + // 更新富文本的textConfig属性 + this.target.setAttribute('textConfig', currentTextConfig); + } + } + + // 应用透明度渐变效果 + private applyFadeEffect( + textConfig: IRichTextCharacter[], + ratio: number, + totalItems: number, + displayedLength: number + ): IRichTextCharacter[] { + // 计算边界字符的索引,这是正在淡出的字符 + let fadeIndex: number; + + if (this.direction === 'forward') { + // 从前往后消失,当前正在淡出的是第displayedLength个字符 + fadeIndex = totalItems - displayedLength; + } else { + // 从后往前消失,当前正在淡出的是第displayedLength个字符 + fadeIndex = displayedLength; + } + + // 计算边界字符的透明度 + const fadeProgress = (ratio - (1 - this.fadeOutDuration)) / this.fadeOutDuration; + const fadeOpacity = Math.max(0, 1 - Math.min(1, fadeProgress)); + + return textConfig.map((item, index) => { + if (this.direction === 'forward') { + // 从前往后消失,第一个字符最先淡出 + if (index === 0 && 'text' in item) { + return { + ...item, + opacity: fadeOpacity + }; + } + } else { + // 从后往前消失,最后一个字符最先淡出 + if (index === textConfig.length - 1 && 'text' in item) { + return { + ...item, + opacity: fadeOpacity + }; + } + } + return item; + }); + } + + // 添加光标 + private addCursor(textConfig: IRichTextCharacter[], ratio: number): IRichTextCharacter[] { + // 判断是否应该显示光标 + let shouldShowCursor = true; + + if (this.blinkCursor) { + // 闪烁效果:在动画期间,光标每半个周期闪烁一次 + const blinkRate = 0.1; // 光标闪烁频率(每10%动画进度闪烁一次) + shouldShowCursor = Math.floor(ratio / blinkRate) % 2 === 0; + } + + if (shouldShowCursor && textConfig.length > 0) { + // 确定光标位置(根据direction) + const cursorIndex = this.direction === 'forward' ? 0 : textConfig.length - 1; + const cursorItem = textConfig[cursorIndex]; + + if ('text' in cursorItem) { + // 复制数组 + const result = [...textConfig]; + + if (this.direction === 'forward') { + // 光标在前面 + result[cursorIndex] = { + ...cursorItem, + text: this.cursorChar + String(cursorItem.text) + }; + } else { + // 光标在后面 + result[cursorIndex] = { + ...cursorItem, + text: String(cursorItem.text) + this.cursorChar + }; + } + + return result; + } + } + + return textConfig; + } +} diff --git a/packages/vrender-animate/src/custom/richtext/slide-out-richtext.ts b/packages/vrender-animate/src/custom/richtext/slide-out-richtext.ts new file mode 100644 index 000000000..0d746a3c6 --- /dev/null +++ b/packages/vrender-animate/src/custom/richtext/slide-out-richtext.ts @@ -0,0 +1,371 @@ +import type { IAnimate, IStep } from '../../intreface/animate'; +import type { EasingType } from '../../intreface/easing'; +import { ACustomAnimate } from '../custom-animate'; +import type { IRichTextCharacter, IRichTextParagraphCharacter } from '@visactor/vrender-core'; +import { RichText } from '@visactor/vrender-core'; + +/** + * 滑动富文本退出动画,文字会向指定方向滑出,同时逐字消失 + * 支持上、下、左、右四个方向 + * 支持按单词或字符退场 + */ +export class SlideOutRichText extends ACustomAnimate<{ textConfig: IRichTextCharacter[] }> { + declare valid: boolean; + + private fromTextConfig: IRichTextCharacter[] = []; + private toTextConfig: IRichTextCharacter[] = []; + private originalTextConfig: IRichTextCharacter[] = []; + private singleCharConfig: IRichTextCharacter[] = []; + private fadeOutDuration: number = 0.3; // 透明度渐变持续时间,以动画总时长的比例表示 + private slideDirection: 'up' | 'down' | 'left' | 'right' = 'right'; // 滑动方向 + private slideDistance: number = 30; // 滑动距离(像素) + private wordByWord: boolean = false; // 是否按单词为单位进行动画 + // 默认正则表达式: 匹配英文单词(含中间连字符),连续中文字符,数字,以及独立的符号和空格 + private wordRegex: RegExp = /[a-zA-Z]+(-[a-zA-Z]+)*|[\u4e00-\u9fa5]+|[0-9]+|[^\s\w\u4e00-\u9fa5]/g; + private wordGroups: number[][] = []; // 存储单词分组信息,每个数组包含属于同一单词的字符索引 + private reverseOrder: boolean = false; // 是否反转字符/单词的消失顺序 + + constructor( + from: { textConfig: IRichTextCharacter[] }, + to: { textConfig: IRichTextCharacter[] }, + duration: number, + easing: EasingType, + params?: { + fadeOutDuration?: number; + slideDirection?: 'up' | 'down' | 'left' | 'right'; + slideDistance?: number; + wordByWord?: boolean; + wordRegex?: RegExp; + reverseOrder?: boolean; + } + ) { + super(from, to, duration, easing, params); + + // 配置透明度渐变效果 + if (params?.fadeOutDuration !== undefined) { + this.fadeOutDuration = params.fadeOutDuration; + } + + // 配置滑动方向和距离 + if (params?.slideDirection !== undefined) { + this.slideDirection = params.slideDirection; + } + if (params?.slideDistance !== undefined) { + this.slideDistance = params.slideDistance; + } + + // 配置按单词动画 + if (params?.wordByWord !== undefined) { + this.wordByWord = params.wordByWord; + } + if (params?.wordRegex !== undefined) { + this.wordRegex = params.wordRegex; + } + + // 配置顺序 + if (params?.reverseOrder !== undefined) { + this.reverseOrder = params.reverseOrder; + } + + this.propKeys = ['textConfig']; + } + + onFirstRun(): void { + const fromProps = this.getLastProps(); + const toProps = this.getEndProps(); + + // 存储原始配置 + this.originalTextConfig = fromProps.textConfig ? [...fromProps.textConfig] : []; + + // 初始化解析结果 + this.valid = true; + + // 确保from不为空 + if (!this.originalTextConfig || this.originalTextConfig.length === 0) { + this.valid = false; + return; + } + + // 将文本拆分为单个字符,使用RichText的静态方法 + this.fromTextConfig = RichText.TransformTextConfig2SingleCharacter(this.originalTextConfig); + + // 目标状态是空文本(或指定的目标) + this.toTextConfig = + toProps.textConfig && toProps.textConfig.length > 0 + ? RichText.TransformTextConfig2SingleCharacter(toProps.textConfig) + : []; + + // 创建单字符数组,用于动画初始状态 + this.singleCharConfig = this.fromTextConfig.map(item => { + if ('text' in item) { + // 文本字符初始设置为完全可见且无偏移 + return { + ...item, + opacity: 1, + dx: 0, + dy: 0 + }; + } + return { ...item, opacity: 1 }; + }); + + // 如果启用按单词动画,则计算单词分组 + if (this.wordByWord) { + this.calculateWordGroups(); + } + } + + // 计算单词分组 + private calculateWordGroups(): void { + // 重置单词分组 + this.wordGroups = []; + + // 构建完整文本用于正则匹配 + let fullText = ''; + const charMap: Record = {}; // 映射全文索引到字符配置索引 + let fullTextIndex = 0; + + // 构建全文和映射 + this.fromTextConfig.forEach((item, configIndex) => { + if ('text' in item) { + const text = String(item.text); + fullText += text; + // 为每个字符创建映射 + charMap[fullTextIndex] = configIndex; + fullTextIndex++; + } + }); + + // 使用正则表达式查找单词 + let match; + + // 重置正则表达式状态 + this.wordRegex.lastIndex = 0; + + while ((match = this.wordRegex.exec(fullText)) !== null) { + const wordStart = match.index; + const wordEnd = match.index + match[0].length; + + // 找出属于这个单词的所有字符索引 + const wordIndices = []; + + for (let i = wordStart; i < wordEnd; i++) { + if (charMap[i] !== undefined) { + wordIndices.push(charMap[i]); + } + } + + // 添加到单词分组 + if (wordIndices.length > 0) { + this.wordGroups.push(wordIndices); + } + } + + // 处理没有分配到任何单词的字符 + const allocatedIndices = new Set(); + this.wordGroups.forEach(group => { + group.forEach(index => allocatedIndices.add(index)); + }); + + for (let i = 0; i < this.fromTextConfig.length; i++) { + if ('text' in this.fromTextConfig[i] && !allocatedIndices.has(i)) { + // 单独为每个未分配的字符创建一个"单词" + this.wordGroups.push([i]); + } + } + } + + // 根据滑动方向计算目标x偏移(最终位置) + private getTargetDx(): number { + switch (this.slideDirection) { + case 'left': + return -this.slideDistance; + case 'right': + return this.slideDistance; + default: + return 0; + } + } + + // 根据滑动方向计算目标y偏移(最终位置) + private getTargetDy(): number { + switch (this.slideDirection) { + case 'up': + return -this.slideDistance; + case 'down': + return this.slideDistance; + default: + return 0; + } + } + + onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { + super.onEnd(cb); + if (!cb) { + // 动画结束时,应用最终textConfig(通常是空的或特定的toTextConfig) + if (this.toTextConfig.length > 0) { + this.target.setAttribute('textConfig', this.toTextConfig); + } else { + this.target.setAttribute('textConfig', []); + } + } + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + if (!this.valid) { + return; + } + + // 计算文本显示时间比例上限 - 为尾部字符的渐变和滑动效果留出时间 + const maxTextShowRatio = 1 - this.fadeOutDuration; + + let updatedTextConfig: IRichTextCharacter[]; + + if (this.wordByWord && this.wordGroups.length > 0) { + // 按单词动画 + updatedTextConfig = this.updateByWord(ratio, maxTextShowRatio); + } else { + // 按字符动画 + updatedTextConfig = this.updateByCharacter(ratio, maxTextShowRatio); + } + + // 更新富文本的textConfig属性 + this.target.setAttribute('textConfig', updatedTextConfig); + } + + // 按单词更新文本配置 + private updateByWord(ratio: number, maxTextShowRatio: number): IRichTextCharacter[] { + const totalGroups = this.wordGroups.length; + const updatedTextConfig = [...this.singleCharConfig]; + + // 处理单词分组 + for (let groupIndex = 0; groupIndex < this.wordGroups.length; groupIndex++) { + // 计算这个单词组的消失时间点 + let disappearTime; + + if (this.reverseOrder) { + // 反转顺序 (与入场顺序相反) + if (this.slideDirection === 'left') { + // 从左到右的顺序 (第一个单词先消失) + disappearTime = (groupIndex / totalGroups) * maxTextShowRatio; + } else { + // 从右到左的顺序 (最后的单词先消失) + disappearTime = ((totalGroups - 1 - groupIndex) / totalGroups) * maxTextShowRatio; + } + } else { + // 标准顺序 (与入场顺序相同) + if (this.slideDirection === 'left') { + // 从右到左的顺序 (最后的单词先消失) + disappearTime = ((totalGroups - 1 - groupIndex) / totalGroups) * maxTextShowRatio; + } else { + // 从左到右的顺序 (第一个单词先消失) + disappearTime = (groupIndex / totalGroups) * maxTextShowRatio; + } + } + + // 如果当前时间还没到显示这个单词的消失时间点,保持可见状态 + if (ratio < disappearTime) { + for (const charIndex of this.wordGroups[groupIndex]) { + const item = updatedTextConfig[charIndex]; + if ('text' in item) { + updatedTextConfig[charIndex] = { + ...item, + opacity: 1, + dx: 0, + dy: 0 + }; + } + } + continue; + } + + // 计算动画进度(0-1之间) + const animProgress = (ratio - disappearTime) / this.fadeOutDuration; + const progress = Math.max(0, Math.min(1, animProgress)); + + // 计算当前偏移和透明度 + const dx = this.getTargetDx() * progress; + const dy = this.getTargetDy() * progress; + const opacity = 1 - progress; + + // 更新这个单词的所有字符 + for (const charIndex of this.wordGroups[groupIndex]) { + const item = updatedTextConfig[charIndex]; + if ('text' in item) { + updatedTextConfig[charIndex] = { + ...item, + opacity, + dx, + dy + }; + } + } + } + + return updatedTextConfig; + } + + // 按字符更新文本配置 + private updateByCharacter(ratio: number, maxTextShowRatio: number): IRichTextCharacter[] { + const totalItems = this.fromTextConfig.length; + const updatedTextConfig = [...this.singleCharConfig]; + + // 更新每个字符的状态 + for (let index = 0; index < updatedTextConfig.length; index++) { + const item = updatedTextConfig[index]; + if ('text' in item) { + // 计算每个字符的消失时间点 + let disappearTime; + + if (this.reverseOrder) { + // 反转入场顺序 + if (this.slideDirection === 'left') { + // 从左到右的顺序 (第一个字符先消失) + disappearTime = (index / totalItems) * maxTextShowRatio; + } else { + // 从右到左的顺序 (最后的字符先消失) + disappearTime = ((totalItems - 1 - index) / totalItems) * maxTextShowRatio; + } + } else { + // 与入场顺序相同 + if (this.slideDirection === 'left') { + // 从右到左的顺序 (最后的字符先消失) + disappearTime = ((totalItems - 1 - index) / totalItems) * maxTextShowRatio; + } else { + // 标准顺序 (第一个字符先消失) + disappearTime = (index / totalItems) * maxTextShowRatio; + } + } + + // 如果当前时间还没到这个字符的消失时间点,保持可见状态 + if (ratio < disappearTime) { + updatedTextConfig[index] = { + ...item, + opacity: 1, + dx: 0, + dy: 0 + }; + continue; + } + + // 计算动画进度(0-1之间) + const animProgress = (ratio - disappearTime) / this.fadeOutDuration; + const progress = Math.max(0, Math.min(1, animProgress)); + + // 计算当前偏移和透明度 + const dx = this.getTargetDx() * progress; + const dy = this.getTargetDy() * progress; + const opacity = 1 - progress; + + updatedTextConfig[index] = { + ...item, + opacity, + dx, + dy + }; + } + } + + return updatedTextConfig; + } +} diff --git a/packages/vrender/__tests__/browser/src/pages/custom-animate.ts b/packages/vrender/__tests__/browser/src/pages/custom-animate.ts index 59f4c6392..44451336a 100644 --- a/packages/vrender/__tests__/browser/src/pages/custom-animate.ts +++ b/packages/vrender/__tests__/browser/src/pages/custom-animate.ts @@ -7,6 +7,58 @@ registerCustomAnimate(); let stage: any; +// 基础富文本配置 +const basicTextConfig = [ + { + text: 'VRender 富文本', + fontSize: 24, + fill: '#3A86FF', + fontWeight: 'bold' + } +]; + +// 带格式的富文本配置 +const formattedTextConfig = [ + { + text: '富文本', + fontSize: 24, + fill: '#3A86FF', + fontWeight: 'bold' + }, + { + text: '退场', + fontSize: 24, + fill: '#FF006E', + fontWeight: 'bold' + }, + { + text: '动画效果', + fontSize: 24, + fill: '#FFBE0B', + fontWeight: 'bold' + } +]; + +// 段落富文本配置 +const paragraphTextConfig = [ + { + text: 'VRender中的富文本动画\n', + fontSize: 24, + fill: '#3A86FF', + fontWeight: 'bold' + }, + { + text: '这是一段用于演示的多行文本,\n', + fontSize: 18, + fill: '#000' + }, + { + text: '支持逐字符退场和逐单词退场效果', + fontSize: 18, + fill: '#FF006E' + } +]; + // Utility function to add test cases to the page function addCase(name: string, container: HTMLElement, cb: (stage: any) => void) { const button = document.createElement('button'); @@ -335,5 +387,309 @@ export const page = () => { }); }); + // 基础退格删除效果 + addCase('OutputRichText - 退格删除', container, stage => { + const richText = createRichText({ + x: 400, + y: 200, + textConfig: basicTextConfig, + textAlign: 'center', + textBaseline: 'middle' + }); + stage.defaultLayer.add(richText); + + // 启动退场动画 + const executor = new AnimateExecutor(richText); + + // 先等待1秒,然后执行退格删除动画 + setTimeout(() => { + executor.execute({ + type: 'outputRichText', + customParameters: { + showCursor: true, + cursorChar: '|', + blinkCursor: true, + fadeOutChars: true, + direction: 'backward' // 从后往前删除(退格效果) + }, + duration: 2000, + easing: 'linear' + }); + }, 1000); + }); + + // 正向删除效果 + addCase('OutputRichText - 正向删除', container, stage => { + const richText = createRichText({ + x: 400, + y: 200, + textConfig: basicTextConfig, + textAlign: 'center', + textBaseline: 'middle' + }); + stage.defaultLayer.add(richText); + + // 启动退场动画 + const executor = new AnimateExecutor(richText); + + // 先等待1秒,然后执行正向删除动画 + setTimeout(() => { + executor.execute({ + type: 'outputRichText', + customParameters: { + showCursor: true, + cursorChar: '|', + blinkCursor: true, + fadeOutChars: true, + direction: 'forward' // 从前往后删除 + }, + duration: 2000, + easing: 'linear' + }); + }, 1000); + }); + + // 无光标的淡出效果 + addCase('OutputRichText - 无光标淡出', container, stage => { + const richText = createRichText({ + x: 400, + y: 200, + textConfig: formattedTextConfig, + textAlign: 'center', + textBaseline: 'middle' + }); + stage.defaultLayer.add(richText); + + // 启动退场动画 + const executor = new AnimateExecutor(richText); + + // 先等待1秒,然后执行无光标淡出动画 + setTimeout(() => { + executor.execute({ + type: 'outputRichText', + customParameters: { + showCursor: false, + fadeOutChars: true, + direction: 'backward' + }, + duration: 2000, + easing: 'linear' + }); + }, 1000); + }); + + // 完整的出入场序列 + addCase('输入后退出序列', container, stage => { + const richText = createRichText({ + x: 400, + y: 200, + textConfig: [], + textAlign: 'center', + textBaseline: 'middle' + }); + stage.defaultLayer.add(richText); + + // 创建动画执行器 + const executor = new AnimateExecutor(richText); + + // 1. 首先执行输入动画 + executor.execute({ + type: 'inputRichText', + to: { + textConfig: formattedTextConfig + }, + customParameters: { + showCursor: true, + cursorChar: '|', + blinkCursor: true, + fadeInChars: true + }, + duration: 2000, + easing: 'linear' + }); + + // 2. 等待2秒,然后执行退出动画 + setTimeout(() => { + executor.execute({ + type: 'outputRichText', + customParameters: { + showCursor: true, + cursorChar: '|', + blinkCursor: true, + fadeOutChars: true, + direction: 'backward' + }, + duration: 2000, + easing: 'linear' + }); + }, 4000); + }); + + // ==== SlideOutRichText演示 ==== + + // 向右滑出 + addCase('SlideOutRichText - 向右滑出', container, stage => { + const richText = createRichText({ + x: 400, + y: 200, + textConfig: formattedTextConfig, + textAlign: 'center', + textBaseline: 'middle' + }); + stage.defaultLayer.add(richText); + + // 启动退场动画 + const executor = new AnimateExecutor(richText); + + // 先等待1秒,然后执行向右滑出动画 + setTimeout(() => { + executor.execute({ + type: 'slideOutRichText', + customParameters: { + slideDirection: 'right', + slideDistance: 100, + fadeOutDuration: 0.3 + }, + duration: 1500, + easing: 'quadOut' + }); + }, 1000); + }); + + // 向上滑出 + addCase('SlideOutRichText - 向上滑出', container, stage => { + const richText = createRichText({ + x: 400, + y: 200, + textConfig: formattedTextConfig, + textAlign: 'center', + textBaseline: 'middle' + }); + stage.defaultLayer.add(richText); + + // 启动退场动画 + const executor = new AnimateExecutor(richText); + + // 先等待1秒,然后执行向上滑出动画 + setTimeout(() => { + executor.execute({ + type: 'slideOutRichText', + customParameters: { + slideDirection: 'up', + slideDistance: 100, + fadeOutDuration: 0.3 + }, + duration: 1500, + easing: 'quadOut' + }); + }, 1000); + }); + + // 按单词滑出 + addCase('SlideOutRichText - 按单词滑出', container, stage => { + const richText = createRichText({ + x: 400, + y: 200, + textConfig: paragraphTextConfig, + textAlign: 'center', + textBaseline: 'middle' + }); + stage.defaultLayer.add(richText); + + // 启动退场动画 + const executor = new AnimateExecutor(richText); + + // 先等待1秒,然后执行按单词滑出动画 + setTimeout(() => { + executor.execute({ + type: 'slideOutRichText', + customParameters: { + slideDirection: 'right', + slideDistance: 80, + fadeOutDuration: 0.3, + wordByWord: true + }, + duration: 2000, + easing: 'quadOut' + }); + }, 1000); + }); + + // 反向顺序滑出 + addCase('SlideOutRichText - 反向顺序滑出', container, stage => { + const richText = createRichText({ + x: 400, + y: 200, + textConfig: formattedTextConfig, + textAlign: 'center', + textBaseline: 'middle' + }); + stage.defaultLayer.add(richText); + + // 启动退场动画 + const executor = new AnimateExecutor(richText); + + // 先等待1秒,然后执行反向顺序滑出动画 + setTimeout(() => { + executor.execute({ + type: 'slideOutRichText', + customParameters: { + slideDirection: 'right', + slideDistance: 100, + fadeOutDuration: 0.3, + reverseOrder: true // 反转顺序 + }, + duration: 1500, + easing: 'quadOut' + }); + }, 1000); + }); + + // 完整的滑动入场和退场序列 + addCase('滑动入场后退场序列', container, stage => { + const richText = createRichText({ + x: 400, + y: 200, + textConfig: [], + textAlign: 'center', + textBaseline: 'middle' + }); + stage.defaultLayer.add(richText); + + // 创建动画执行器 + const executor = new AnimateExecutor(richText); + + // 1. 首先执行滑动入场动画 + executor.execute({ + type: 'slideRichText', + to: { + textConfig: paragraphTextConfig + }, + customParameters: { + slideDirection: 'right', + slideDistance: 100, + fadeInDuration: 0.3, + wordByWord: true + }, + duration: 2000, + easing: 'quadOut' + }); + + // 2. 等待2.5秒,然后执行滑动退场动画(使用相反方向) + setTimeout(() => { + executor.execute({ + type: 'slideOutRichText', + customParameters: { + slideDirection: 'left', // 反方向滑出 + slideDistance: 100, + fadeOutDuration: 0.3, + wordByWord: true + }, + duration: 2000, + easing: 'quadOut' + }); + }, 4500); + }); + return container; }; From bb477a3a20efb8ea6a75a6cb8da0a9622041d81e Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 8 Apr 2025 15:42:41 +0800 Subject: [PATCH 073/179] feat: optmize effect for story animate --- packages/vrender-animate/src/custom/story.ts | 44 ++++++----- .../vrender-animate/src/custom/tag-points.ts | 6 +- .../vrender-animate/src/interpolate/store.ts | 3 + .../src/ticker/default-ticker.ts | 3 + .../browser/src/pages/story-animate.ts | 73 ++++++++++++++++++- 5 files changed, 105 insertions(+), 24 deletions(-) diff --git a/packages/vrender-animate/src/custom/story.ts b/packages/vrender-animate/src/custom/story.ts index 3b46305d6..a948f0632 100644 --- a/packages/vrender-animate/src/custom/story.ts +++ b/packages/vrender-animate/src/custom/story.ts @@ -48,27 +48,27 @@ export class SlideIn extends ACustomAnimate> { const fromOpacity = this.params?.fromOpacity ?? 0; // 使用透明度初始值参数 // 初始化from和to对象 - const from: Record = { opacity: fromOpacity }; - const to: Record = { opacity: 1 }; + const from: Record = { opacity: fromOpacity, baseOpacity: fromOpacity }; + const to: Record = { opacity: 1, baseOpacity: 1 }; // 根据方向设置对应的属性 if (direction === 'top') { from.y = (attrs.y ?? 0) - distance; to.y = attrs.y ?? 0; - this.propKeys = ['opacity', 'y']; + this.propKeys = ['opacity', 'baseOpacity', 'y']; } else if (direction === 'bottom') { from.y = (attrs.y ?? 0) + distance; to.y = attrs.y ?? 0; - this.propKeys = ['opacity', 'y']; + this.propKeys = ['opacity', 'baseOpacity', 'y']; } else if (direction === 'left') { from.x = (attrs.x ?? 0) - distance; to.x = attrs.x ?? 0; - this.propKeys = ['opacity', 'x']; + this.propKeys = ['opacity', 'baseOpacity', 'x']; } else { // right from.x = (attrs.x ?? 0) + distance; to.x = attrs.x ?? 0; - this.propKeys = ['opacity', 'x']; + this.propKeys = ['opacity', 'baseOpacity', 'x']; } this.from = from; @@ -111,9 +111,9 @@ export class GrowIn extends ACustomAnimate> { const fromOpacity = this.params?.fromOpacity ?? 0; // 使用透明度初始值参数 // 初始化from和to对象 - const from: Record = { opacity: fromOpacity }; - const to: Record = { opacity: 1 }; - this.propKeys = ['opacity']; + const from: Record = { opacity: fromOpacity, baseOpacity: fromOpacity }; + const to: Record = { opacity: 1, baseOpacity: 1 }; + this.propKeys = ['opacity', 'baseOpacity']; // 根据方向设置对应的缩放属性 if (direction === 'x' || direction === 'xy') { @@ -170,6 +170,7 @@ export class SpinIn extends ACustomAnimate> { // 初始化from和to对象 const from: Record = { opacity: fromOpacity, + baseOpacity: fromOpacity, angle: fromAngle, scaleX: fromScale, scaleY: fromScale @@ -177,12 +178,13 @@ export class SpinIn extends ACustomAnimate> { const to: Record = { opacity: 1, + baseOpacity: 1, angle: attrs.angle ?? 0, scaleX: attrs.scaleX ?? 1, scaleY: attrs.scaleY ?? 1 }; - this.propKeys = ['opacity', 'angle', 'scaleX', 'scaleY']; + this.propKeys = ['opacity', 'baseOpacity', 'angle', 'scaleX', 'scaleY']; this.from = from; this.to = to; this.props = to; @@ -356,27 +358,27 @@ export class SlideOut extends ACustomAnimate> { const toOpacity = this.params?.toOpacity ?? 0; // 使用目标透明度参数 // 初始化from和to对象 - const from: Record = { opacity: fromOpacity }; - const to: Record = { opacity: toOpacity }; + const from: Record = { opacity: fromOpacity, baseOpacity: fromOpacity }; + const to: Record = { opacity: toOpacity, baseOpacity: toOpacity }; // 根据方向设置对应的属性 if (direction === 'top') { from.y = attrs.y ?? 0; to.y = (attrs.y ?? 0) - distance; - this.propKeys = ['opacity', 'y']; + this.propKeys = ['opacity', 'baseOpacity', 'y']; } else if (direction === 'bottom') { from.y = attrs.y ?? 0; to.y = (attrs.y ?? 0) + distance; - this.propKeys = ['opacity', 'y']; + this.propKeys = ['opacity', 'baseOpacity', 'y']; } else if (direction === 'left') { from.x = attrs.x ?? 0; to.x = (attrs.x ?? 0) - distance; - this.propKeys = ['opacity', 'x']; + this.propKeys = ['opacity', 'baseOpacity', 'x']; } else { // right from.x = attrs.x ?? 0; to.x = (attrs.x ?? 0) + distance; - this.propKeys = ['opacity', 'x']; + this.propKeys = ['opacity', 'baseOpacity', 'x']; } this.from = from; @@ -423,9 +425,9 @@ export class GrowOut extends ACustomAnimate> { const toOpacity = this.params?.toOpacity ?? 0; // 使用目标透明度参数 // 初始化from和to对象 - const from: Record = { opacity: fromOpacity }; - const to: Record = { opacity: toOpacity }; - this.propKeys = ['opacity']; + const from: Record = { opacity: fromOpacity, baseOpacity: fromOpacity }; + const to: Record = { opacity: toOpacity, baseOpacity: toOpacity }; + this.propKeys = ['opacity', 'baseOpacity']; // 根据方向设置对应的缩放属性 if (direction === 'x' || direction === 'xy') { @@ -486,6 +488,7 @@ export class SpinOut extends ACustomAnimate> { // 初始化from和to对象 const from: Record = { opacity: fromOpacity, + baseOpacity: fromOpacity, angle: attrs.angle ?? 0, scaleX: attrs.scaleX ?? 1, scaleY: attrs.scaleY ?? 1 @@ -493,12 +496,13 @@ export class SpinOut extends ACustomAnimate> { const to: Record = { opacity: toOpacity, + baseOpacity: toOpacity, angle: toAngle, scaleX: toScale, scaleY: toScale }; - this.propKeys = ['opacity', 'angle', 'scaleX', 'scaleY']; + this.propKeys = ['opacity', 'baseOpacity', 'angle', 'scaleX', 'scaleY']; this.from = from; this.to = to; this.props = to; diff --git a/packages/vrender-animate/src/custom/tag-points.ts b/packages/vrender-animate/src/custom/tag-points.ts index e93f3e166..c11836220 100644 --- a/packages/vrender-animate/src/custom/tag-points.ts +++ b/packages/vrender-animate/src/custom/tag-points.ts @@ -1,8 +1,8 @@ import type { IPointLike } from '@visactor/vutils'; import { clamp, isValidNumber, Point } from '@visactor/vutils'; import { ACustomAnimate } from './custom-animate'; -import type { ISegment } from '@visactor/vrender-core'; -import { pointInterpolation, ILineAttribute } from '@visactor/vrender-core'; +import type { ISegment, ILineAttribute } from '@visactor/vrender-core'; +import { pointInterpolation } from '@visactor/vrender-core'; import type { EasingType } from '../intreface/easing'; export class TagPointsUpdate extends ACustomAnimate<{ points?: IPointLike[]; segments?: ISegment[] }> { @@ -167,7 +167,7 @@ export class TagPointsUpdate extends ACustomAnimate<{ points?: IPointLike[]; seg } return; } - this.target.setAttributes({ clipRange: this.clipRange + (1 - this.clipRange) * ratio }); + this.target.setAttributes({ clipRange: this.clipRange + (1 - this.clipRange) * ratio } as any); } if (this.segmentsCache && this.to.segments) { let start = 0; diff --git a/packages/vrender-animate/src/interpolate/store.ts b/packages/vrender-animate/src/interpolate/store.ts index f2cc36450..c2e08542d 100644 --- a/packages/vrender-animate/src/interpolate/store.ts +++ b/packages/vrender-animate/src/interpolate/store.ts @@ -60,6 +60,9 @@ export class InterpolateUpdateStore { opacity = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { target.attribute.opacity = interpolateNumber(from, to, ratio); }; + baseOpacity = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + (target.attribute as any).baseOpacity = interpolateNumber(from, to, ratio); + }; fillOpacity = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { target.attribute.fillOpacity = interpolateNumber(from, to, ratio); }; diff --git a/packages/vrender-animate/src/ticker/default-ticker.ts b/packages/vrender-animate/src/ticker/default-ticker.ts index 34d0c6fcf..30af3abd7 100644 --- a/packages/vrender-animate/src/ticker/default-ticker.ts +++ b/packages/vrender-animate/src/ticker/default-ticker.ts @@ -219,6 +219,9 @@ export class DefaultTicker extends EventEmitter implements ITicker { } protected checkSkip = (delta: number): boolean => { + if (this.stage.params.optimize.tickRenderMode === 'performance') { + return false; + } // 随机扰动(每次都对interval进行随机的扰动,避免所有tick都发生在同一帧) const skip = delta < this.interval + (Math.random() - 0.5) * 2 * this._jitter; return skip; diff --git a/packages/vrender/__tests__/browser/src/pages/story-animate.ts b/packages/vrender/__tests__/browser/src/pages/story-animate.ts index 447f05dfe..b2a37b7af 100644 --- a/packages/vrender/__tests__/browser/src/pages/story-animate.ts +++ b/packages/vrender/__tests__/browser/src/pages/story-animate.ts @@ -60,7 +60,7 @@ export const page = () => { btnContainer.style.flexDirection = 'row'; btnContainer.style.gap = '3px'; btnContainer.style.flexWrap = 'wrap'; - btnContainer.style.height = '120px'; + btnContainer.style.height = '160px'; const canvas = document.getElementById('main'); // 将btnContainer添加到canvas之前 canvas.parentNode.insertBefore(btnContainer, canvas); @@ -789,6 +789,77 @@ export const page = () => { }); }); + // Apple iMessage Animation with iosSpringOut + addCase('Apple iMessage (iosSpringOut)', btnContainer, stage => { + // Create a group to hold the message bubble and its components + const messageGroup = createGroup({ + x: 450, + y: 250 + }); + + // Main bubble (rounded rectangle) + const messageBubble = createRect({ + x: 0, + y: 0, + width: 200, + height: 80, + fill: '#34C759', // Updated to match Apple's green color more closely + cornerRadius: 20, + shadowBlur: 5, + shadowColor: 'rgba(0, 0, 0, 0.1)', + shadowOffsetX: 0, + shadowOffsetY: 2 + }); + + // Message text + const messageText = createText({ + x: 20, + y: 40, + text: 'Hello! 👋', + fontSize: 18, + fontFamily: 'SF Pro, -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif', // Apple system fonts + fill: '#FFFFFF', + textAlign: 'left', + textBaseline: 'middle' + }); + + // Add "now" timestamp text + const timestampText = createText({ + x: 200, + y: 90, + text: 'now', + fontSize: 12, + fontFamily: 'SF Pro, -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif', + fill: 'rgba(120, 120, 128, 0.8)', // Light gray color like iOS + textAlign: 'right', + textBaseline: 'middle' + }); + + // Add all elements to the group + messageGroup.add(messageBubble); + messageGroup.add(messageText); + messageGroup.add(timestampText); + + // Add group to stage + stage.defaultLayer.add(messageGroup); + + // Apply animation to the group + const executor = new AnimateExecutor(messageGroup); + + // Use our custom Apple-style animation + executor.execute({ + type: 'growIn', + customParameters: { + fromScale: 0.3, + direction: 'xy', + fromOpacity: 0 + }, + selfOnly: true, + duration: 400, + easing: 'backOut' // Use our new iOS-style easing function + }); + }); + // In-Out Animation Sequence addCase('In-Out Animation Sequence', btnContainer, stage => { const rect = createRect({ From 7c4c271d11d76ef62e2e3d4da8c22d4899b94224 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 8 Apr 2025 17:38:39 +0800 Subject: [PATCH 074/179] feat: animate-executor support array animate, support move and rotate animate --- .../src/component/component-animator.ts | 12 +- packages/vrender-animate/src/custom/move.ts | 170 ++++++++++++++++++ .../vrender-animate/src/custom/register.ts | 6 + packages/vrender-animate/src/custom/rotate.ts | 97 ++++++++++ packages/vrender-animate/src/custom/update.ts | 10 +- .../src/executor/animate-executor.ts | 30 +++- .../vrender-animate/src/executor/executor.ts | 7 +- .../src/state/animation-state.ts | 17 +- .../src/state/graphic-extension.ts | 6 +- .../browser/src/pages/animate-state.ts | 134 +++++++------- 10 files changed, 402 insertions(+), 87 deletions(-) create mode 100644 packages/vrender-animate/src/custom/move.ts create mode 100644 packages/vrender-animate/src/custom/rotate.ts diff --git a/packages/vrender-animate/src/component/component-animator.ts b/packages/vrender-animate/src/component/component-animator.ts index 2da91e1d1..157776c65 100644 --- a/packages/vrender-animate/src/component/component-animator.ts +++ b/packages/vrender-animate/src/component/component-animator.ts @@ -1,7 +1,7 @@ import type { IAnimate } from '../intreface/animate'; import type { IGraphic } from '@visactor/vrender-core'; import { AnimateExecutor } from '../executor/animate-executor'; -import type { IAnimationConfig, IAnimationTypeConfig, IAnimationTimeline } from '../executor/executor'; +import type { IAnimationConfig } from '../executor/executor'; /** * Animation task that contains information about a scheduled animation @@ -9,7 +9,7 @@ import type { IAnimationConfig, IAnimationTypeConfig, IAnimationTimeline } from interface IAnimationTask { graphic: IGraphic; config: IAnimationConfig; - animate?: IAnimate; + animate?: IAnimate[]; } /** @@ -123,7 +123,9 @@ export class ComponentAnimator { const animate = executor.executeItem(task.config, task.graphic); task.animate = animate; - this.totalDuration = Math.max(this.totalDuration, animate.getStartTime() + animate.getDuration()); + animate.forEach(animate => { + this.totalDuration = Math.max(this.totalDuration, animate.getStartTime() + animate.getDuration()); + }); }); return this; @@ -132,7 +134,7 @@ export class ComponentAnimator { deleteSelfAttr(key: string): void { this.tasks.forEach(task => { if (task.animate) { - task.animate.preventAttr(key); + task.animate.forEach(animate => animate.preventAttr(key)); } }); } @@ -145,7 +147,7 @@ export class ComponentAnimator { stop(type?: 'start' | 'end'): ComponentAnimator { this.tasks.forEach(task => { if (task.animate) { - task.animate.stop(type); + task.animate.forEach(animate => animate.stop(type)); } }); diff --git a/packages/vrender-animate/src/custom/move.ts b/packages/vrender-animate/src/custom/move.ts new file mode 100644 index 000000000..31c1a8048 --- /dev/null +++ b/packages/vrender-animate/src/custom/move.ts @@ -0,0 +1,170 @@ +import type { EasingType, IGraphic, IGroup } from '@visactor/vrender-core'; +import { isFunction, isValidNumber } from '@visactor/vutils'; +import { ACustomAnimate } from './custom-animate'; + +export type FunctionCallback = (...args: any[]) => T; + +export interface IMoveAnimationOptions { + direction?: 'x' | 'y' | 'xy'; + orient?: 'positive' | 'negative'; + offset?: number; + point?: { x?: number; y?: number } | FunctionCallback<{ x?: number; y?: number }>; + excludeChannels?: string[]; +} + +interface IAnimationParameters { + width: number; + height: number; + group: IGroup; + elementIndex: number; + elementCount: number; + view: any; +} + +// When user did not provide proper x/y value, move animation will never work properly, +// due to that, default x/y value won't be set. + +export const moveIn = ( + graphic: IGraphic, + options: IMoveAnimationOptions, + animationParameters: IAnimationParameters +) => { + const { offset = 0, orient, direction, point: pointOpt, excludeChannels = [] } = options ?? {}; + let changedX = 0; + let changedY = 0; + + if (orient === 'negative') { + // consider the offset of group + if (animationParameters.group) { + changedX = (animationParameters as any).groupWidth ?? animationParameters.group.getBounds().width(); + changedY = (animationParameters as any).groupHeight ?? animationParameters.group.getBounds().height(); + + (animationParameters as any).groupWidth = changedX; + (animationParameters as any).groupHeight = changedY; + } else { + changedX = animationParameters.width; + changedY = animationParameters.height; + } + } + + changedX += offset; + changedY += offset; + const point = isFunction(pointOpt) ? pointOpt.call(null, graphic.getDatum(), graphic, animationParameters) : pointOpt; + const fromX = point && isValidNumber(point.x) ? point.x : changedX; + const fromY = point && isValidNumber(point.y) ? point.y : changedY; + const finalAttrs = graphic.getFinalAttribute(); + const finalAttrsX = excludeChannels.includes('x') ? graphic.attribute.x : finalAttrs.x; + const finalAttrsY = excludeChannels.includes('y') ? graphic.attribute.y : finalAttrs.y; + + switch (direction) { + case 'x': + return { + from: { x: fromX }, + to: { x: finalAttrsX } + }; + case 'y': + return { + from: { y: fromY }, + to: { y: finalAttrsY } + }; + case 'xy': + default: + return { + from: { x: fromX, y: fromY }, + to: { + x: finalAttrsX, + y: finalAttrsY + } + }; + } +}; + +export const moveOut = ( + graphic: IGraphic, + options: IMoveAnimationOptions, + animationParameters: IAnimationParameters +) => { + const { offset = 0, orient, direction, point: pointOpt } = options ?? {}; + + // consider the offset of group + const groupBounds = animationParameters.group ? animationParameters.group.getBounds() : null; + const groupWidth = groupBounds?.width() ?? animationParameters.width; + const groupHeight = groupBounds?.height() ?? animationParameters.height; + const changedX = (orient === 'negative' ? groupWidth : 0) + offset; + const changedY = (orient === 'negative' ? groupHeight : 0) + offset; + const point = isFunction(pointOpt) ? pointOpt.call(null, graphic.getDatum(), graphic, animationParameters) : pointOpt; + const fromX = point && isValidNumber(point.x) ? point.x : changedX; + const fromY = point && isValidNumber(point.y) ? point.y : changedY; + + switch (direction) { + case 'x': + return { + from: { x: graphic.attribute.x }, + to: { x: fromX } + }; + case 'y': + return { + from: { y: graphic.attribute.y }, + to: { y: fromY } + }; + case 'xy': + default: + return { + from: { + x: graphic.attribute.x, + y: graphic.attribute.y + }, + to: { x: fromX, y: fromY } + }; + } +}; + +export class MoveBase extends ACustomAnimate> { + declare valid: boolean; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: any) { + super(from, to, duration, easing, params); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; + this.propKeys.forEach(key => { + attribute[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.addUpdatePositionTag(); + this.target.addUpdateShapeAndBoundsTag(); + } +} + +/** + * 增长渐入 + */ +export class MoveIn extends MoveBase { + onBind(): void { + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + if (this.params?.diffAttrs) { + this.target.setAttributes(this.params.diffAttrs); + } + const { from, to } = moveIn(this.target, this.params.options, this.params); + const fromAttrs = this.target.context?.lastAttrs ?? from; + this.props = to; + this.propKeys = Object.keys(to).filter(key => (to as any)[key] != null); + this.animate.reSyncProps(); + this.from = fromAttrs; + this.to = to; + this.target.setAttributes(fromAttrs); + } +} + +export class MoveOut extends MoveBase { + onBind(): void { + const { from, to } = moveOut(this.target, this.params.options, this.params); + const fromAttrs = this.target.context?.lastAttrs ?? from; + this.props = to; + this.propKeys = Object.keys(to).filter(key => (to as any)[key] != null); + this.animate.reSyncProps(); + this.from = fromAttrs; + this.to = to; + this.target.setAttributes(fromAttrs); + } +} diff --git a/packages/vrender-animate/src/custom/register.ts b/packages/vrender-animate/src/custom/register.ts index 9025a3850..7b5530d36 100644 --- a/packages/vrender-animate/src/custom/register.ts +++ b/packages/vrender-animate/src/custom/register.ts @@ -37,6 +37,8 @@ import { SpinOut } from './story'; import { Update } from './update'; +import { MoveIn, MoveOut } from './move'; +import { RotateIn, RotateOut } from './rotate'; export const registerCustomAnimate = () => { // 基础动画 @@ -64,6 +66,10 @@ export const registerCustomAnimate = () => { AnimateExecutor.registerBuiltInAnimate('growAngleOut', GrowAngleOut); AnimateExecutor.registerBuiltInAnimate('growRadiusIn', GrowRadiusIn); AnimateExecutor.registerBuiltInAnimate('growRadiusOut', GrowRadiusOut); + AnimateExecutor.registerBuiltInAnimate('moveIn', MoveIn); + AnimateExecutor.registerBuiltInAnimate('moveOut', MoveOut); + AnimateExecutor.registerBuiltInAnimate('rotateIn', RotateIn); + AnimateExecutor.registerBuiltInAnimate('rotateOut', RotateOut); // state和update共用一个自定义动画类 AnimateExecutor.registerBuiltInAnimate('update', Update); AnimateExecutor.registerBuiltInAnimate('state', State); diff --git a/packages/vrender-animate/src/custom/rotate.ts b/packages/vrender-animate/src/custom/rotate.ts new file mode 100644 index 000000000..12bf44363 --- /dev/null +++ b/packages/vrender-animate/src/custom/rotate.ts @@ -0,0 +1,97 @@ +import type { EasingType, IGraphic } from '@visactor/vrender-core'; +import { isNumberClose, isValidNumber } from '@visactor/vutils'; +import { ACustomAnimate } from './custom-animate'; + +export interface IRotateAnimationOptions { + orient?: 'clockwise' | 'anticlockwise'; + angle?: number; +} + +export const rotateIn = (graphic: IGraphic, options: IRotateAnimationOptions) => { + const finalAttrs = graphic.getFinalAttribute(); + const attributeAngle = finalAttrs.angle ?? 0; + + let angle = 0; + if (isNumberClose(attributeAngle / (Math.PI * 2), 0)) { + angle = Math.round(attributeAngle / (Math.PI * 2)) * Math.PI * 2; + } else if (isValidNumber(options?.angle)) { + angle = options.angle; + } else if (options?.orient === 'anticlockwise') { + angle = Math.ceil(attributeAngle / (Math.PI * 2)) * Math.PI * 2; + } else { + angle = Math.floor(attributeAngle / (Math.PI * 2)) * Math.PI * 2; + } + return { + from: { angle }, + to: { angle: attributeAngle } + }; +}; + +export const rotateOut = (graphic: IGraphic, options: IRotateAnimationOptions) => { + const finalAttrs = graphic.getFinalAttribute(); + const finalAngle = finalAttrs.angle ?? 0; + let angle = 0; + if (isNumberClose(finalAngle / (Math.PI * 2), 0)) { + angle = Math.round(finalAngle / (Math.PI * 2)) * Math.PI * 2; + } else if (isValidNumber(options?.angle)) { + angle = options.angle; + } else if (options?.orient === 'anticlockwise') { + angle = Math.ceil(finalAngle / (Math.PI * 2)) * Math.PI * 2; + } else { + angle = Math.floor(finalAngle / (Math.PI * 2)) * Math.PI * 2; + } + return { + from: { angle: finalAngle }, + to: { angle } + }; +}; + +export class RotateBase extends ACustomAnimate> { + declare valid: boolean; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: any) { + super(from, to, duration, easing, params); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; + this.propKeys.forEach(key => { + attribute[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.addUpdatePositionTag(); + this.target.addUpdateShapeAndBoundsTag(); + } +} + +/** + * 增长渐入 + */ +export class RotateIn extends RotateBase { + onBind(): void { + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + if (this.params?.diffAttrs) { + this.target.setAttributes(this.params.diffAttrs); + } + const { from, to } = rotateIn(this.target, this.params.options); + const fromAttrs = this.target.context?.lastAttrs ?? from; + this.props = to; + this.propKeys = Object.keys(to).filter(key => (to as any)[key] != null); + this.animate.reSyncProps(); + this.from = fromAttrs; + this.to = to; + this.target.setAttributes(fromAttrs); + } +} + +export class RotateOut extends RotateBase { + onBind(): void { + const { from, to } = rotateOut(this.target, this.params.options); + const fromAttrs = this.target.context?.lastAttrs ?? from; + this.props = to; + this.propKeys = Object.keys(to).filter(key => (to as any)[key] != null); + this.animate.reSyncProps(); + this.from = fromAttrs; + this.to = to; + this.target.setAttributes(fromAttrs); + } +} diff --git a/packages/vrender-animate/src/custom/update.ts b/packages/vrender-animate/src/custom/update.ts index 83d3f39b1..b967b44f3 100644 --- a/packages/vrender-animate/src/custom/update.ts +++ b/packages/vrender-animate/src/custom/update.ts @@ -18,7 +18,15 @@ export class Update extends ACustomAnimate> { // params: IUpdateAnimationOptions; constructor(from: null, to: null, duration: number, easing: EasingType, params?: IUpdateAnimationOptions) { - const { diffAttrs = {} } = params; + let { diffAttrs = {} } = params as any; + const { options } = params as any; + + diffAttrs = { ...diffAttrs }; + if (options?.excludeChannels?.length) { + options.excludeChannels.forEach((channel: string) => { + delete diffAttrs[channel]; + }); + } super(from, diffAttrs, duration, easing, params); // this.params = params; } diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index 353c59f28..075759772 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -17,7 +17,7 @@ import { cloneDeep, isArray, isFunction } from '@visactor/vutils'; interface IAnimateExecutor { execute: (params: IAnimationConfig) => void; - executeItem: (params: IAnimationConfig, graphic: IGraphic, index?: number) => IAnimate | null; + executeItem: (params: IAnimationConfig, graphic: IGraphic, index?: number) => IAnimate[]; onStart: (cb?: () => void) => void; onEnd: (cb?: () => void) => void; } @@ -216,12 +216,20 @@ export class AnimateExecutor implements IAnimateExecutor { return parsedParams; } + execute(params: IAnimationConfig | IAnimationConfig[]) { + if (Array.isArray(params)) { + params.forEach(param => this._execute(param)); + } else { + this._execute(params); + } + } + /** * 执行动画,针对一组元素 */ - execute(params: IAnimationConfig) { + _execute(params: IAnimationConfig) { if (params.selfOnly) { - return this.executeItem(params, this._target, 0, 1); + return this._executeItem(params, this._target, 0, 1); } // 判断是否为timeline配置 @@ -582,6 +590,13 @@ export class AnimateExecutor implements IAnimateExecutor { } } }); + } else { + channel.forEach(key => { + const value = graphic.context?.diffAttrs?.[key]; + if (value !== undefined) { + props[key] = value; + } + }); } return props; @@ -602,10 +617,17 @@ export class AnimateExecutor implements IAnimateExecutor { return value as T; } + executeItem(params: IAnimationConfig | IAnimationConfig[], graphic: IGraphic, index: number = 0, count: number = 1) { + if (Array.isArray(params)) { + return params.map(param => this._executeItem(param, graphic, index, count)).filter(Boolean); + } + return [this._executeItem(params, graphic, index, count)].filter(Boolean); + } + /** * 执行动画(具体执行到内部的单个图元) */ - executeItem(params: IAnimationConfig, graphic: IGraphic, index: number = 0, count: number = 1): IAnimate | null { + _executeItem(params: IAnimationConfig, graphic: IGraphic, index: number = 0, count: number = 1): IAnimate | null { if (!graphic) { return null; } diff --git a/packages/vrender-animate/src/executor/executor.ts b/packages/vrender-animate/src/executor/executor.ts index 30d6d8431..41b0eb865 100644 --- a/packages/vrender-animate/src/executor/executor.ts +++ b/packages/vrender-animate/src/executor/executor.ts @@ -60,7 +60,12 @@ export interface IAnimationEffect { /** 动画 easing 配置 */ easing?: EasingType; /** options暂时没有处理 */ - options?: MarkFunctionValueType; + options?: + | MarkFunctionValueType + | { + // 忽略的属性 + excludeChannels?: string[]; + }; } export interface IAnimationTimeSlice { diff --git a/packages/vrender-animate/src/state/animation-state.ts b/packages/vrender-animate/src/state/animation-state.ts index 314e7b6d5..d1c99b1f5 100644 --- a/packages/vrender-animate/src/state/animation-state.ts +++ b/packages/vrender-animate/src/state/animation-state.ts @@ -3,6 +3,7 @@ import type { IAnimationState } from './types'; import { AnimationTransitionRegistry } from './animation-states-registry'; import type { IAnimationConfig } from '../executor/executor'; import { AnimateExecutor } from '../executor/animate-executor'; +import { isArray } from '@visactor/vutils'; // Standard animation state names export const AnimationStates = { @@ -45,7 +46,7 @@ export class AnimationStateStore { // 一个状态对应一个执行器,每个图元都有一一对应 interface IStateInfo { state: string; - animationConfig: IAnimationConfig; + animationConfig: IAnimationConfig | IAnimationConfig[]; executor: AnimateExecutor; } @@ -67,7 +68,11 @@ export class AnimationStateManager { * @param animationConfig 动画配置 * @param callback 动画结束后的回调函数,参数empty为true表示没有动画需要执行直接调的回调 */ - applyState(nextState: string[], animationConfig: IAnimationState[], callback?: (empty?: boolean) => void): void { + applyState( + nextState: string[], + animationConfig: (IAnimationState | IAnimationState[])[], + callback?: (empty?: boolean) => void + ): void { const registry = AnimationTransitionRegistry.getInstance(); // TODO 这里指判断第一个状态,后续如果需要的话要循环判断 @@ -78,7 +83,9 @@ export class AnimationStateManager { nextState.forEach((state, index) => { shouldApplyState.push({ state, - animationConfig: animationConfig[index].animation, + animationConfig: isArray(animationConfig[index]) + ? animationConfig[index].map(item => item.animation) + : animationConfig[index].animation, executor: new AnimateExecutor(this.graphic) }); }); @@ -98,7 +105,9 @@ export class AnimationStateManager { if (result.allowTransition) { shouldApplyState.push({ state, - animationConfig: animationConfig[index].animation, + animationConfig: isArray(animationConfig[index]) + ? animationConfig[index].map(item => item.animation) + : animationConfig[index].animation, executor: new AnimateExecutor(this.graphic) }); // 允许过渡的话,需要重新遍历this.stateList,获取stopOriginalTransition diff --git a/packages/vrender-animate/src/state/graphic-extension.ts b/packages/vrender-animate/src/state/graphic-extension.ts index eca71d734..8f272defa 100644 --- a/packages/vrender-animate/src/state/graphic-extension.ts +++ b/packages/vrender-animate/src/state/graphic-extension.ts @@ -33,7 +33,11 @@ export class GraphicStateExtension { /** * 应用一个动画状态到图形 */ - applyAnimationState(state: string[], animationConfig: IAnimationState[], callback?: (empty?: boolean) => void): this { + applyAnimationState( + state: string[], + animationConfig: (IAnimationState | IAnimationState[])[], + callback?: (empty?: boolean) => void + ): this { this._getAnimationStateManager(this as unknown as IGraphic).applyState(state, animationConfig, callback); return this; } diff --git a/packages/vrender/__tests__/browser/src/pages/animate-state.ts b/packages/vrender/__tests__/browser/src/pages/animate-state.ts index f3fd841bd..8397c3024 100644 --- a/packages/vrender/__tests__/browser/src/pages/animate-state.ts +++ b/packages/vrender/__tests__/browser/src/pages/animate-state.ts @@ -99,65 +99,6 @@ export const page = () => { console.log(rect); - // Register animation states - rect.registerAnimationState({ - name: 'pulse', - animation: { - timeSlices: [ - { - duration: 500, - effects: { - type: 'to', - channel: { - scaleX: { to: 1.2 }, - scaleY: { to: 1.2 } - }, - easing: 'linear' - } - }, - { - duration: 500, - effects: { - type: 'to', - channel: { - scaleX: { to: 1 }, - scaleY: { to: 1 } - }, - easing: 'linear' - } - } - ], - loop: true - } - }); - - rect.registerAnimationState({ - name: 'spin', - animation: { - type: 'to', - channel: { - angle: { to: 360 } - }, - duration: 2000, - easing: 'linear', - loop: true - } - }); - - rect.registerAnimationState({ - name: 'highlight', - animation: { - type: 'to', - channel: { - fill: { to: 'orange' }, - strokeWidth: { to: 3 }, - stroke: { to: 'red' } - }, - duration: 300, - easing: 'sineOut' - } - }); - // Create control buttons const createControlButton = (x: number, y: number, label: string, action: () => void) => { const buttonGroup = createGroup({ @@ -268,19 +209,70 @@ export const page = () => { rect.applyAnimationState( ['highlight'], [ - { - name: 'highlight', - animation: { - type: 'to', - channel: { - fill: { to: 'orange' }, - strokeWidth: { to: 3 }, - stroke: { to: 'red' } - }, - duration: 300, - easing: 'sineOut' + [ + { + name: 'pulse', + animation: { + timeSlices: [ + { + duration: 500, + effects: { + type: 'to', + channel: { + scaleX: { to: 1.2 }, + scaleY: { to: 1.2 } + }, + easing: 'linear' + } + }, + { + duration: 500, + effects: { + type: 'to', + channel: { + scaleX: { to: 1 }, + scaleY: { to: 1 } + }, + easing: 'linear' + } + } + ], + loop: true + } + }, + { + name: 'highlight', + animation: { + timeSlices: [ + { + duration: 1000, + effects: { + channel: { + fill: { to: 'orange' }, + strokeWidth: { to: 3 }, + stroke: { to: 'red' } + } + }, + easing: 'sineOut' + }, + { + duration: 1000, + effects: { + channel: { + fill: { to: 'green' }, + strokeWidth: { to: 3 }, + stroke: { to: 'pink' } + } + }, + easing: 'sineOut' + } + ], + loop: true, + duration: 300, + easing: 'sineOut' + } } - } + ] ] ); }) From 76fcae336b8ab11c5b5c83d170a2e8e21f730caa Mon Sep 17 00:00:00 2001 From: xiaoluoHe Date: Wed, 9 Apr 2025 10:43:22 +0800 Subject: [PATCH 075/179] feat: motionPath & export custom Animate class --- .../vrender-animate/src/custom/motionPath.ts | 58 +++++++++++++++++++ .../vrender-animate/src/custom/register.ts | 56 ++++++++++++++++++ packages/vrender-animate/src/index.ts | 2 +- 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 packages/vrender-animate/src/custom/motionPath.ts diff --git a/packages/vrender-animate/src/custom/motionPath.ts b/packages/vrender-animate/src/custom/motionPath.ts new file mode 100644 index 000000000..c169a5a29 --- /dev/null +++ b/packages/vrender-animate/src/custom/motionPath.ts @@ -0,0 +1,58 @@ +import type { EasingType } from '../intreface/easing'; +import type { CustomPath2D, IGraphic } from '@visactor/vrender-core'; +import { ACustomAnimate } from './custom-animate'; + +export class MotionPath extends ACustomAnimate { + declare valid: boolean; + declare pathLength: number; + declare path: CustomPath2D; + declare distance: number; + declare totalLength: number; + declare initAngle: number; + declare changeAngle: boolean; + declare cb?: (from: any, to: any, ratio: number, target: IGraphic) => void; + constructor( + from: any, + to: any, + duration: number, + easing: EasingType, + params?: { + path: CustomPath2D; + distance: number; + cb?: (from: any, to: any, ratio: number, target: IGraphic) => void; + initAngle?: number; + changeAngle?: boolean; + } + ) { + super(from, to, duration, easing, params); + if (params) { + this.pathLength = params.path.getLength(); + this.path = params.path; + this.distance = params.distance; + this.totalLength = this.distance * this.pathLength; + this.initAngle = params.initAngle ?? 0; + this.changeAngle = !!params.changeAngle; + this.cb = params.cb; + } + } + + onBind(): void { + this.from = { x: 0, y: 0 }; + this.to = this.path.getAttrAt(this.totalLength).pos; + this.props = this.to; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + const attrs = {} as any; + // 计算位置 + const at = this.totalLength * ratio; + const { pos, angle } = this.path.getAttrAt(at); + attrs.x = pos.x; + attrs.y = pos.y; + if (this.changeAngle) { + attrs.angle = angle + this.initAngle; + } + this.cb && this.cb(this.from, this.to, ratio, this.target as IGraphic); + this.target.setAttributes(attrs); + } +} diff --git a/packages/vrender-animate/src/custom/register.ts b/packages/vrender-animate/src/custom/register.ts index 7b5530d36..e9377baf1 100644 --- a/packages/vrender-animate/src/custom/register.ts +++ b/packages/vrender-animate/src/custom/register.ts @@ -39,6 +39,7 @@ import { import { Update } from './update'; import { MoveIn, MoveOut } from './move'; import { RotateIn, RotateOut } from './rotate'; +import { MotionPath } from './motionPath'; export const registerCustomAnimate = () => { // 基础动画 @@ -100,4 +101,59 @@ export const registerCustomAnimate = () => { AnimateExecutor.registerBuiltInAnimate('spinOut', SpinOut); AnimateExecutor.registerBuiltInAnimate('moveScaleOut', MoveScaleOut); AnimateExecutor.registerBuiltInAnimate('moveRotateOut', MoveRotateOut); + + // 路径动画 + AnimateExecutor.registerBuiltInAnimate('MotionPath', MotionPath); +}; + +export { + ClipIn, + ClipOut, + FadeIn, + FadeOut, + GrowAngleIn, + GrowAngleOut, + GrowCenterIn, + GrowCenterOut, + GrowHeightIn, + GrowHeightOut, + GrowPointsIn, + GrowPointsOut, + GrowPointsXIn, + GrowPointsXOut, + GrowPointsYIn, + GrowPointsYOut, + GrowRadiusIn, + GrowRadiusOut, + GrowWidthIn, + GrowWidthOut, + IncreaseCount, + PoptipAppear, + PoptipDisappear, + ScaleIn, + ScaleOut, + MoveIn, + MoveOut, + RotateIn, + RotateOut, + State, + Update, + MotionPath, + LabelItemAppear, + LabelItemDisappear, + InputText, + InputRichText, + OutputRichText, + SlideRichText, + SlideOutRichText, + SlideIn, + GrowIn, + SpinIn, + MoveScaleIn, + MoveRotateIn, + SlideOut, + GrowOut, + SpinOut, + MoveScaleOut, + MoveRotateOut }; diff --git a/packages/vrender-animate/src/index.ts b/packages/vrender-animate/src/index.ts index b1361adb4..e9e5c0ff0 100644 --- a/packages/vrender-animate/src/index.ts +++ b/packages/vrender-animate/src/index.ts @@ -24,7 +24,7 @@ export { GroupFadeIn, GroupFadeOut } from './custom/group-fade'; export { RotateBySphereAnimate } from './custom/sphere'; export { AnimateExecutor } from './executor/animate-executor'; export type { IAnimationConfig } from './executor/executor'; -export { registerCustomAnimate } from './custom/register'; +export * from './custom/register'; // Export animation state modules export * from './state'; export { AnimationTransitionRegistry } from './state/animation-states-registry'; From d22f8900432bdea8f6c809d16baaab9918ff8819 Mon Sep 17 00:00:00 2001 From: xiaoluoHe Date: Wed, 9 Apr 2025 11:30:38 +0800 Subject: [PATCH 076/179] fix: customParams value --- packages/vrender-animate/src/executor/animate-executor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index 075759772..beca5bb37 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -390,7 +390,7 @@ export class AnimateExecutor implements IAnimateExecutor { if (custom && customType) { const customParams = this.resolveValue(customParameters, graphic, {}); const objOptions = isFunction(options) - ? options.call(null, customParameters.data && customParameters.data[0], graphic, customParameters) + ? options.call(null, customParams.data && customParams.data[0], graphic, customParams) : options; customParams.options = objOptions; if (customType === 1) { From 868795467af24e2c056eb1ec9ff75d2237e6bb7f Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 8 Apr 2025 20:12:50 +0800 Subject: [PATCH 077/179] feat: support lastScale to get newTick last pos --- .../src/animation/axis-animate.ts | 58 +++++++----- packages/vrender-components/src/axis/base.ts | 2 + packages/vrender-components/src/axis/line.ts | 88 +++++++------------ 3 files changed, 73 insertions(+), 75 deletions(-) diff --git a/packages/vrender-components/src/animation/axis-animate.ts b/packages/vrender-components/src/animation/axis-animate.ts index 17cabdc1f..56963b797 100644 --- a/packages/vrender-components/src/animation/axis-animate.ts +++ b/packages/vrender-components/src/animation/axis-animate.ts @@ -9,32 +9,20 @@ export class AxisEnter extends AComponentAnimate { this._animator = animator; const duration = this.duration; const easing = this.easing; - const { config, updateEls, enterEls } = this.params; + const { config, lastScale, getTickCoord } = this.params; let ratio = 1; - if (updateEls && updateEls.length > 1) { - ratio = 0.5; - const oldData1 = updateEls[0].oldEl.data; - const { rawValue: oldRawValue1, value: oldValue1 } = oldData1; - const oldData2 = updateEls[1].oldEl.data; - const { rawValue: oldRawValue2, value: oldValue2 } = oldData2; - const data = this.target.data; - const { rawValue: newRawValue } = data; - // rawValue 是原始值,value是映射出来的值,假设是线性映射,计算一下newRawValue在old阶段的value是什么值 - const oldValue = - oldValue1 + ((oldValue2 - oldValue1) * (newRawValue - oldRawValue1)) / (oldRawValue2 - oldRawValue1); - // 将 x 和 y 做映射 - const oldX1 = updateEls[0].oldEl.attribute.x; - const oldY1 = updateEls[0].oldEl.attribute.y; - const oldX2 = updateEls[1].oldEl.attribute.x; - const oldY2 = updateEls[1].oldEl.attribute.y; - const oldX = oldX1 + ((oldX2 - oldX1) * (oldValue - oldValue1)) / (oldValue2 - oldValue1); - const oldY = oldY1 + ((oldY2 - oldY1) * (oldValue - oldValue1)) / (oldValue2 - oldValue1); + if (lastScale && getTickCoord) { + ratio = 0.7; + const currData = this.target.data; + + const oldValue = lastScale.scale(currData.rawValue); + const point = getTickCoord(oldValue); const newX = this.target.attribute.x; const newY = this.target.attribute.y; - this.target.setAttributes({ x: oldX, y: oldY }); + this.target.setAttributes({ x: point.x, y: point.y }); animator.animate(this.target, { type: 'to', to: { x: newX, y: newY }, @@ -43,6 +31,36 @@ export class AxisEnter extends AComponentAnimate { }); } + // if (updateEls && updateEls.length > 1) { + // ratio = 0.5; + // const oldData1 = updateEls[0].oldEl.data; + // const { rawValue: oldRawValue1, value: oldValue1 } = oldData1; + // const oldData2 = updateEls[1].oldEl.data; + // const { rawValue: oldRawValue2, value: oldValue2 } = oldData2; + // const data = this.target.data; + // const { rawValue: newRawValue } = data; + // // rawValue 是原始值,value是映射出来的值,假设是线性映射,计算一下newRawValue在old阶段的value是什么值 + // const oldValue = + // oldValue1 + ((oldValue2 - oldValue1) * (newRawValue - oldRawValue1)) / (oldRawValue2 - oldRawValue1); + // // 将 x 和 y 做映射 + // const oldX1 = updateEls[0].oldEl.attribute.x; + // const oldY1 = updateEls[0].oldEl.attribute.y; + // const oldX2 = updateEls[1].oldEl.attribute.x; + // const oldY2 = updateEls[1].oldEl.attribute.y; + // const oldX = oldX1 + ((oldX2 - oldX1) * (oldValue - oldValue1)) / (oldValue2 - oldValue1); + // const oldY = oldY1 + ((oldY2 - oldY1) * (oldValue - oldValue1)) / (oldValue2 - oldValue1); + // const newX = this.target.attribute.x; + // const newY = this.target.attribute.y; + + // this.target.setAttributes({ x: oldX, y: oldY }); + // animator.animate(this.target, { + // type: 'to', + // to: { x: newX, y: newY }, + // duration, + // easing + // }); + // } + animator.animate(this.target, { type: config.type ?? 'fadeIn', to: config.to, diff --git a/packages/vrender-components/src/axis/base.ts b/packages/vrender-components/src/axis/base.ts index d9f4b8f30..cba10310c 100644 --- a/packages/vrender-components/src/axis/base.ts +++ b/packages/vrender-components/src/axis/base.ts @@ -118,6 +118,8 @@ export abstract class AxisBase extends AnimateComp */ getBoundsWithoutRender(attributes: Partial) { const currentAttribute = cloneDeep(this.attribute); + // lastScale 不能拷贝 + currentAttribute.lastScale = (this.attribute as any).lastScale; merge(this.attribute, attributes); const offscreenGroup = graphicCreator.group({ diff --git a/packages/vrender-components/src/axis/line.ts b/packages/vrender-components/src/axis/line.ts index 7557bb071..808562bb8 100644 --- a/packages/vrender-components/src/axis/line.ts +++ b/packages/vrender-components/src/axis/line.ts @@ -46,6 +46,7 @@ export interface LineAxis export class LineAxis extends AxisBase { static defaultAttributes = DEFAULT_AXIS_THEME; + lastScale: any; constructor(attributes: LineAxisAttributes, options?: ComponentOptions) { super(options?.skipDefault ? attributes : merge({}, LineAxis.defaultAttributes, attributes), options); @@ -660,6 +661,18 @@ export class LineAxis extends AxisBase { } protected runAnimation() { + const lastScale = this.lastScale; + if ((this.attribute as any).lastScale) { + const lastScale = (this.attribute as any).lastScale; + this.lastScale = lastScale.clone(); + if (lastScale._niceType) { + this.lastScale._niceType = lastScale._niceType; + this.lastScale._domainValidator = lastScale._domainValidator; + this.lastScale._niceDomain = lastScale._niceDomain.slice(); + this.lastScale.range([0, 1]); + } + } + if (this.attribute.animation && (this as any).applyAnimationState) { // @ts-ignore const currentInnerView = this.getInnerView(); @@ -673,9 +686,6 @@ export class LineAxis extends AxisBase { this._newElementAttrMap = {}; - const updateEls: { oldEl: IGraphic; newEl: IGraphic }[] = []; - const enterEls: { newEl: IGraphic }[] = []; - // 遍历新的场景树,将新节点属性更新为旧节点 // TODO: 目前只处理更新场景 traverseGroup(currentInnerView, (el: IGraphic) => { @@ -708,31 +718,6 @@ export class LineAxis extends AxisBase { (el as IGraphic).setAttributes(oldAttrs); - // el.applyAnimationState( - // ['update'], - // [ - // { - // name: 'update', - // animation: { - // selfOnly: true, - // ...animationConfig.update, - // type: 'update', - // to: diffAttrs, - // customParameters: { - // config: animationConfig.update, - // diffAttrs - // } - // } - // } - // ] - // ); - - oldEl.type === 'text' && - updateEls.push({ - oldEl: oldEl, - newEl: el - }); - el.applyAnimationState( ['update'], [ @@ -744,7 +729,8 @@ export class LineAxis extends AxisBase { type: 'axisUpdate', customParameters: { config: animationConfig.update, - diffAttrs + diffAttrs, + lastScale } } } @@ -758,35 +744,27 @@ export class LineAxis extends AxisBase { attrs: el.attribute }; - enterEls.push({ - newEl: el - }); + el.applyAnimationState( + ['enter'], + [ + { + name: 'enter', + animation: { + ...animationConfig.enter, + type: 'axisEnter', + selfOnly: true, + customParameters: { + config: animationConfig.enter, + lastScale, + getTickCoord: this.getTickCoord.bind(this) + } + } + } + ] + ); } } }); - - if (enterEls.length) { - enterEls.forEach(el => { - el.newEl.applyAnimationState( - ['enter'], - [ - { - name: 'enter', - animation: { - ...animationConfig.enter, - type: 'axisEnter', - selfOnly: true, - customParameters: { - config: animationConfig.enter, - updateEls, - enterEls - } - } - } - ] - ); - }); - } } } From c7baf051f1c712e73cc337a3e88a50a08eeb5d96 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 9 Apr 2025 16:47:58 +0800 Subject: [PATCH 078/179] feat: update vutils, axis support to use lastScale --- packages/vrender-components/src/axis/base.ts | 114 ++++++++++++++++++- packages/vrender-components/src/axis/line.ts | 109 ------------------ 2 files changed, 108 insertions(+), 115 deletions(-) diff --git a/packages/vrender-components/src/axis/base.ts b/packages/vrender-components/src/axis/base.ts index cba10310c..c440fd1f3 100644 --- a/packages/vrender-components/src/axis/base.ts +++ b/packages/vrender-components/src/axis/base.ts @@ -40,10 +40,13 @@ import { getElMap, getVerticalCoord } from './util'; import { dispatchClickState, dispatchHoverState, dispatchUnHoverState } from '../util/interaction'; import { AnimateComponent } from '../animation/animate-component'; import { DefaultAxisAnimation } from './animate/config'; +import type { IBaseScale } from '@visactor/vscale'; export abstract class AxisBase extends AnimateComponent> { name = 'axis'; + lastScale: IBaseScale; + // TODO: 组件整体统一起来 protected _innerView: IGroup; getInnerView() { @@ -118,8 +121,8 @@ export abstract class AxisBase extends AnimateComp */ getBoundsWithoutRender(attributes: Partial) { const currentAttribute = cloneDeep(this.attribute); - // lastScale 不能拷贝 - currentAttribute.lastScale = (this.attribute as any).lastScale; + // scale 不能拷贝 + currentAttribute.scale = (this.attribute as any).scale; merge(this.attribute, attributes); const offscreenGroup = graphicCreator.group({ @@ -148,10 +151,6 @@ export abstract class AxisBase extends AnimateComp this.runAnimation(); } - protected runAnimation() { - return; - } - protected _prepare() { this._prepareAnimate(DefaultAxisAnimation); } @@ -570,6 +569,109 @@ export abstract class AxisBase extends AnimateComp return data; } + protected runAnimation() { + const lastScale = this.lastScale; + if ((this.attribute as any).scale) { + const scale = (this.attribute as any).scale; + this.lastScale = scale.clone(); + this.lastScale.range([0, 1]); + } + + if (this.attribute.animation && (this as any).applyAnimationState) { + // @ts-ignore + const currentInnerView = this.getInnerView(); + // @ts-ignore + const prevInnerView = this.getPrevInnerView(); + if (!prevInnerView) { + return; + } + + const animationConfig = this._animationConfig; + + this._newElementAttrMap = {}; + + // 遍历新的场景树,将新节点属性更新为旧节点 + // TODO: 目前只处理更新场景 + traverseGroup(currentInnerView, (el: IGraphic) => { + if ((el as IGraphic).type !== 'group' && el.id) { + const oldEl = prevInnerView[el.id]; + // 删除旧图元的动画 + el.setFinalAttribute(el.attribute); + if (oldEl) { + oldEl.release(); + // oldEl.stopAnimationState('enter'); + // oldEl.stopAnimationState('update'); + const oldAttrs = (oldEl as IGraphic).attribute; + const finalAttrs = el.getFinalAttribute(); + const diffAttrs: Record = diff(oldAttrs, finalAttrs); + + let hasDiff = Object.keys(diffAttrs).length > 0; + // TODO 如果入场会有fadeIn,则需要处理opacity(后续还需要考虑其他动画情况) + if ('opacity' in oldAttrs && finalAttrs.opacity !== oldAttrs.opacity) { + diffAttrs.opacity = finalAttrs.opacity ?? 1; + hasDiff = true; + } + + if (animationConfig.update && hasDiff) { + this._newElementAttrMap[el.id] = { + state: 'update', + node: el, + attrs: el.attribute + }; + const oldAttrs = (oldEl as IGraphic).attribute; + + (el as IGraphic).setAttributes(oldAttrs); + + el.applyAnimationState( + ['update'], + [ + { + name: 'update', + animation: { + selfOnly: true, + ...animationConfig.update, + type: 'axisUpdate', + customParameters: { + config: animationConfig.update, + diffAttrs, + lastScale + } + } + } + ] + ); + } + } else if (animationConfig.enter) { + this._newElementAttrMap[el.id] = { + state: 'enter', + node: el, + attrs: el.attribute + }; + + el.applyAnimationState( + ['enter'], + [ + { + name: 'enter', + animation: { + ...animationConfig.enter, + type: 'axisEnter', + selfOnly: true, + customParameters: { + config: animationConfig.enter, + lastScale, + getTickCoord: this.getTickCoord.bind(this) + } + } + } + ] + ); + } + } + }); + } + } + release(): void { super.release(); this._prevInnerView = null; diff --git a/packages/vrender-components/src/axis/line.ts b/packages/vrender-components/src/axis/line.ts index 808562bb8..05637ab22 100644 --- a/packages/vrender-components/src/axis/line.ts +++ b/packages/vrender-components/src/axis/line.ts @@ -46,7 +46,6 @@ export interface LineAxis export class LineAxis extends AxisBase { static defaultAttributes = DEFAULT_AXIS_THEME; - lastScale: any; constructor(attributes: LineAxisAttributes, options?: ComponentOptions) { super(options?.skipDefault ? attributes : merge({}, LineAxis.defaultAttributes, attributes), options); @@ -660,114 +659,6 @@ export class LineAxis extends AxisBase { return limitLength; } - protected runAnimation() { - const lastScale = this.lastScale; - if ((this.attribute as any).lastScale) { - const lastScale = (this.attribute as any).lastScale; - this.lastScale = lastScale.clone(); - if (lastScale._niceType) { - this.lastScale._niceType = lastScale._niceType; - this.lastScale._domainValidator = lastScale._domainValidator; - this.lastScale._niceDomain = lastScale._niceDomain.slice(); - this.lastScale.range([0, 1]); - } - } - - if (this.attribute.animation && (this as any).applyAnimationState) { - // @ts-ignore - const currentInnerView = this.getInnerView(); - // @ts-ignore - const prevInnerView = this.getPrevInnerView(); - if (!prevInnerView) { - return; - } - - const animationConfig = this._animationConfig; - - this._newElementAttrMap = {}; - - // 遍历新的场景树,将新节点属性更新为旧节点 - // TODO: 目前只处理更新场景 - traverseGroup(currentInnerView, (el: IGraphic) => { - if ((el as IGraphic).type !== 'group' && el.id) { - const oldEl = prevInnerView[el.id]; - // 删除旧图元的动画 - el.setFinalAttribute(el.attribute); - if (oldEl) { - oldEl.release(); - // oldEl.stopAnimationState('enter'); - // oldEl.stopAnimationState('update'); - const oldAttrs = (oldEl as IGraphic).attribute; - const finalAttrs = el.getFinalAttribute(); - const diffAttrs: Record = diff(oldAttrs, finalAttrs); - - let hasDiff = Object.keys(diffAttrs).length > 0; - // TODO 如果入场会有fadeIn,则需要处理opacity(后续还需要考虑其他动画情况) - if ('opacity' in oldAttrs && finalAttrs.opacity !== oldAttrs.opacity) { - diffAttrs.opacity = finalAttrs.opacity ?? 1; - hasDiff = true; - } - - if (animationConfig.update && hasDiff) { - this._newElementAttrMap[el.id] = { - state: 'update', - node: el, - attrs: el.attribute - }; - const oldAttrs = (oldEl as IGraphic).attribute; - - (el as IGraphic).setAttributes(oldAttrs); - - el.applyAnimationState( - ['update'], - [ - { - name: 'update', - animation: { - selfOnly: true, - ...animationConfig.update, - type: 'axisUpdate', - customParameters: { - config: animationConfig.update, - diffAttrs, - lastScale - } - } - } - ] - ); - } - } else if (animationConfig.enter) { - this._newElementAttrMap[el.id] = { - state: 'enter', - node: el, - attrs: el.attribute - }; - - el.applyAnimationState( - ['enter'], - [ - { - name: 'enter', - animation: { - ...animationConfig.enter, - type: 'axisEnter', - selfOnly: true, - customParameters: { - config: animationConfig.enter, - lastScale, - getTickCoord: this.getTickCoord.bind(this) - } - } - } - ] - ); - } - } - }); - } - } - release(): void { super.release(); this._breaks = null; From 178e3f8191b566c8125ee0db4f16d79b72796ce0 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 8 Apr 2025 20:25:17 +0800 Subject: [PATCH 079/179] fix: fix issue with state animate --- packages/vrender-core/src/graphic/graphic.ts | 27 +++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index e1c6d2b94..c1d2d0d38 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -310,9 +310,9 @@ export abstract class Graphic = Partial IAnimate; - declare nextAttrs?: T; - declare prevAttrs?: T; - declare finalAttrs?: T; + // declare nextAttrs?: T; + // declare prevAttrs?: T; + // declare finalAttrs?: T; declare pathProxy?: ICustomPath2D; // 依附于某个theme,如果该节点不存在parent,那么这个Theme就作为节点的Theme,避免添加到节点前计算属性 @@ -1036,6 +1036,7 @@ export abstract class Graphic = Partial = Partial { - if ((animate as any).stateNames) { - const endProps = animate.getEndProps(); - if (has(endProps, key)) { - value = endProps[key]; - } - } - }); + // this.animates.forEach(animate => { + // if ((animate as any).stateNames) { + // const endProps = animate.getEndProps(); + // if (has(endProps, key)) { + // value = endProps[key]; + // } + // } + // }); + // console.log(this.finalAttrs); + return (this as any).finalAttribute?.[key]; } return value; From be0b7282150da403af85a66c51f7108daf459e46 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 9 Apr 2025 20:14:07 +0800 Subject: [PATCH 080/179] feat: optmize code for custom animate --- packages/vrender-animate/src/custom/common.ts | 30 ++++--- .../vrender-animate/src/custom/group-fade.ts | 73 +++-------------- .../vrender-animate/src/custom/growAngle.ts | 39 ++++----- .../vrender-animate/src/custom/growCenter.ts | 19 +++-- .../vrender-animate/src/custom/growHeight.ts | 19 ++--- .../vrender-animate/src/custom/growPoints.ts | 48 ++++++----- .../vrender-animate/src/custom/growRadius.ts | 26 +++--- .../vrender-animate/src/custom/growWidth.ts | 14 ++-- packages/vrender-animate/src/custom/move.ts | 21 ++--- packages/vrender-animate/src/custom/readme.md | 10 +++ .../vrender-animate/src/custom/register.ts | 4 +- packages/vrender-animate/src/custom/rotate.ts | 27 +++--- packages/vrender-animate/src/custom/scale.ts | 56 ++++++++----- packages/vrender-animate/src/custom/state.ts | 82 +++++++++---------- packages/vrender-animate/src/custom/story.ts | 6 +- packages/vrender-animate/src/custom/update.ts | 15 ++-- packages/vrender-components/src/label/base.ts | 8 +- 17 files changed, 244 insertions(+), 253 deletions(-) create mode 100644 packages/vrender-animate/src/custom/readme.md diff --git a/packages/vrender-animate/src/custom/common.ts b/packages/vrender-animate/src/custom/common.ts index e0884ee5f..f39b5c8a1 100644 --- a/packages/vrender-animate/src/custom/common.ts +++ b/packages/vrender-animate/src/custom/common.ts @@ -17,25 +17,28 @@ export class CommonIn extends ACustomAnimate> { onBind(): void { // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) - if (this.params?.diffAttrs) { - this.target.setAttributes(this.params.diffAttrs); - } - const attrs = (this.target as any).getAttributes(true); - const fromAttrs = this.target.context?.lastAttrs ?? {}; + const attrs = this.target.getFinalAttribute(); + const fromAttrs: Record = this.target.attribute ?? {}; const to: Record = {}; const from: Record = {}; this.keys.forEach(key => { to[key] = attrs?.[key] ?? 1; - from[key] = fromAttrs?.[key] ?? 0; + from[key] = fromAttrs[key] ?? 0; }); + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + const finalAttribute = this.target.getFinalAttribute(); + if (finalAttribute) { + Object.assign(this.target.attribute, finalAttribute); + } + this.props = to; this.propKeys = this.keys; - this.animate.reSyncProps(); this.from = from; this.to = to; - this.target.setAttributes(from as any); + + this.target.setAttributes(from); } onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { @@ -62,21 +65,24 @@ export class CommonOut extends ACustomAnimate> { } onBind(): void { - const attrs = this.target.getFinalAttribute(); + const attrs: Record = this.target.attribute; const to: Record = {}; const from: Record = {}; this.keys.forEach(key => { to[key] = 0; - from[key] = attrs?.[key] ?? 1; + from[key] = attrs[key] ?? 1; }); this.props = to; this.propKeys = this.keys; - this.animate.reSyncProps(); this.from = from; this.to = to; - this.target.setAttributes(from as any); + + Object.assign(this.target.attribute, from); + this.target.addUpdatePositionTag(); + this.target.addUpdateBoundTag(); + // this.target.setAttributes(from as any); } onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { diff --git a/packages/vrender-animate/src/custom/group-fade.ts b/packages/vrender-animate/src/custom/group-fade.ts index b44b689d9..b09ee82ab 100644 --- a/packages/vrender-animate/src/custom/group-fade.ts +++ b/packages/vrender-animate/src/custom/group-fade.ts @@ -1,70 +1,21 @@ -import type { IGroup } from '@visactor/vrender-core'; +import type { EasingType, IGroup } from '@visactor/vrender-core'; import { ACustomAnimate } from './custom-animate'; +import { CommonIn, CommonOut } from './common'; -export class GroupFadeIn extends ACustomAnimate { - declare target: IGroup; +export class GroupFadeIn extends CommonIn { + declare valid: boolean; - getEndProps(): Record { - return {}; - } - - onBind(): void { - this.target.setTheme({ - common: { - opacity: 0 - } - }); - return; - } - - onEnd(): void { - this.target.setTheme({ - common: { - opacity: 1 - } - }); - return; - } - - onUpdate(end: boolean, ratio: number, out: Record): void { - this.target.setTheme({ - common: { - opacity: ratio - } - }); + constructor(from: null, to: null, duration: number, easing: EasingType, params?: any) { + super(from, to, duration, easing, params); + this.keys = ['baseOpacity']; } } -export class GroupFadeOut extends ACustomAnimate { - declare target: IGroup; - - getEndProps(): Record { - return {}; - } - - onBind(): void { - this.target.setTheme({ - common: { - opacity: 1 - } - }); - return; - } - - onEnd(): void { - this.target.setTheme({ - common: { - opacity: 0 - } - }); - return; - } +export class GroupFadeOut extends CommonOut { + declare valid: boolean; - onUpdate(end: boolean, ratio: number, out: Record): void { - this.target.setTheme({ - common: { - opacity: 1 - ratio - } - }); + constructor(from: null, to: null, duration: number, easing: EasingType, params?: any) { + super(from, to, duration, easing, params); + this.keys = ['baseOpacity']; } } diff --git a/packages/vrender-animate/src/custom/growAngle.ts b/packages/vrender-animate/src/custom/growAngle.ts index 27338daf7..6c81502e7 100644 --- a/packages/vrender-animate/src/custom/growAngle.ts +++ b/packages/vrender-animate/src/custom/growAngle.ts @@ -89,7 +89,7 @@ const growAngleOutIndividual = ( options: IGrowAngleAnimationOptions, animationParameters: IAnimationParameters ) => { - const attrs = graphic.getFinalAttribute(); + const attrs = graphic.attribute as any; if (options && options.orient === 'anticlockwise') { return { @@ -108,7 +108,7 @@ const growAngleOutOverall = ( options: IGrowAngleAnimationOptions, animationParameters: IAnimationParameters ) => { - const attrs = graphic.getFinalAttribute(); + const attrs = graphic.attribute as any; if (options && options.orient === 'anticlockwise') { const overallValue = isNumber(options.overall) ? options.overall : Math.PI * 2; return { @@ -186,6 +186,8 @@ export class GrowAngleBase extends ACustomAnimate> { this._updateFunction = this.updateStartAngle; } else if (this.propKeys[0] === 'endAngle') { this._updateFunction = this.updateEndAngle; + } else { + this._updateFunction = null; } } @@ -204,8 +206,10 @@ export class GrowAngleBase extends ACustomAnimate> { } onUpdate(end: boolean, ratio: number, out: Record): void { - this._updateFunction(ratio); - this.target.addUpdateShapeAndBoundsTag(); + if (this._updateFunction) { + this._updateFunction(ratio); + this.target.addUpdateShapeAndBoundsTag(); + } } } @@ -214,20 +218,20 @@ export class GrowAngleBase extends ACustomAnimate> { */ export class GrowAngleIn extends GrowAngleBase { onBind(): void { - // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) - if (this.params?.diffAttrs) { - Object.assign(this.target.attribute, this.params.diffAttrs); - } const { from, to } = growAngleIn(this.target, this.params.options, this.params); const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); this.from = fromAttrs; this.to = to; - // 性能优化,不需要setAttributes - Object.assign(this.target.attribute, fromAttrs); - this.target.addUpdatePositionTag(); - this.target.addUpdateBoundTag(); + + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + const finalAttribute = this.target.getFinalAttribute(); + if (finalAttribute) { + Object.assign(this.target.attribute, finalAttribute); + } + + this.target.setAttributes(fromAttrs); this.determineUpdateFunction(); } } @@ -235,16 +239,13 @@ export class GrowAngleIn extends GrowAngleBase { export class GrowAngleOut extends GrowAngleBase { onBind(): void { const { from, to } = growAngleOut(this.target, this.params.options, this.params); - const fromAttrs = this.target.context?.lastAttrs ?? from; + const fromAttrs = from; this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); - this.animate.reSyncProps(); - this.from = fromAttrs; + this.from = fromAttrs ?? (this.target.attribute as any); this.to = to; - // 性能优化,不需要setAttributes - Object.assign(this.target.attribute, fromAttrs); - this.target.addUpdatePositionTag(); - this.target.addUpdateBoundTag(); + + // this.target.setAttributes(fromAttrs); this.determineUpdateFunction(); } } diff --git a/packages/vrender-animate/src/custom/growCenter.ts b/packages/vrender-animate/src/custom/growCenter.ts index c497470fb..0b1a593f0 100644 --- a/packages/vrender-animate/src/custom/growCenter.ts +++ b/packages/vrender-animate/src/custom/growCenter.ts @@ -115,7 +115,7 @@ const growCenterOut: TypeAnimation = ( options: IGrowCartesianAnimationOptions, animationParameters: IAnimationParameters ) => { - const attrs = graphic.getFinalAttribute(); + const attrs = graphic.attribute as any; switch (options?.direction) { case 'x': { const x = attrs.x; @@ -203,17 +203,19 @@ export class GrowCenterIn extends ACustomAnimate> { } onBind(): void { - // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) - if (this.params?.diffAttrs) { - this.target.setAttributes(this.params.diffAttrs); - } const { from, to } = growCenterIn(this.target, this.params.options, this.params); const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); - this.animate.reSyncProps(); this.from = fromAttrs; this.to = to; + + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + const finalAttribute = this.target.getFinalAttribute(); + if (finalAttribute) { + Object.assign(this.target.attribute, finalAttribute); + } + this.target.setAttributes(fromAttrs); } @@ -239,12 +241,11 @@ export class GrowCenterOut extends ACustomAnimate> { } onBind(): void { - const attrs = this.target.getFinalAttribute(); const { from, to } = growCenterOut(this.target, this.params.options, this.params); this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); - this.animate.reSyncProps(); - this.from = from || attrs; + + this.from = from ?? (this.target.attribute as any); this.to = to; // this.target.setAttributes(from); } diff --git a/packages/vrender-animate/src/custom/growHeight.ts b/packages/vrender-animate/src/custom/growHeight.ts index fa0c9a44a..d557522cd 100644 --- a/packages/vrender-animate/src/custom/growHeight.ts +++ b/packages/vrender-animate/src/custom/growHeight.ts @@ -101,19 +101,20 @@ export class GrowHeightIn extends ACustomAnimate> { } onBind(): void { - if (this.params?.diffAttrs) { - Object.assign(this.target.attribute, this.params.diffAttrs); - } const { from, to } = growHeightIn(this.target, this.params.options, this.params); const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); this.from = fromAttrs; this.to = to; - // 性能优化,不需要setAttributes - Object.assign(this.target.attribute, fromAttrs); - this.target.addUpdatePositionTag(); - this.target.addUpdateBoundTag(); + + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + const finalAttribute = this.target.getFinalAttribute(); + if (finalAttribute) { + Object.assign(this.target.attribute, finalAttribute); + } + + this.target.setAttributes(fromAttrs); } onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { @@ -203,12 +204,10 @@ export class GrowHeightOut extends ACustomAnimate> { } onBind(): void { - const attrs = this.target.getFinalAttribute(); const { from, to } = growHeightOut(this.target, this.params.options, this.params); this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); - this.animate.reSyncProps(); - this.from = from || attrs; + this.from = from ?? (this.target.attribute as any); this.to = to; // this.target.setAttributes(from); } diff --git a/packages/vrender-animate/src/custom/growPoints.ts b/packages/vrender-animate/src/custom/growPoints.ts index 29f01cd3f..213aeafaf 100644 --- a/packages/vrender-animate/src/custom/growPoints.ts +++ b/packages/vrender-animate/src/custom/growPoints.ts @@ -114,13 +114,17 @@ export class GrowPointsIn extends GworPointsBase { onBind(): void { if (['area', 'line', 'polygon'].includes(this.target.type)) { const { from, to } = growPointsIn(this.target, this.params.options, this.params); - const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); - this.animate.reSyncProps(); - this.from = fromAttrs; + this.from = from; this.to = to; - this.target.setAttributes(fromAttrs); + + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + const finalAttribute = this.target.getFinalAttribute(); + if (finalAttribute) { + Object.assign(this.target.attribute, finalAttribute); + } + this.target.setAttributes(from); } else { this.valid = false; } @@ -134,7 +138,6 @@ export class GrowPointsOut extends GworPointsBase { const { from, to } = growPointsOut(this.target, this.params.options, this.params); this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); - this.animate.reSyncProps(); this.from = from || attrs; this.to = to; } else { @@ -208,13 +211,17 @@ export class GrowPointsXIn extends GworPointsBase { onBind(): void { if (['area', 'line', 'polygon'].includes(this.target.type)) { const { from, to } = growPointsXIn(this.target, this.params.options, this.params); - const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); - this.animate.reSyncProps(); - this.from = fromAttrs; + this.from = from; this.to = to; - this.target.setAttributes(fromAttrs); + + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + const finalAttribute = this.target.getFinalAttribute(); + if (finalAttribute) { + Object.assign(this.target.attribute, finalAttribute); + } + this.target.setAttributes(from); } else { this.valid = false; } @@ -228,7 +235,6 @@ export class GrowPointsXOut extends GworPointsBase { const { from, to } = growPointsXOut(this.target, this.params.options, this.params); this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); - this.animate.reSyncProps(); this.from = from || attrs; this.to = to; } else { @@ -300,19 +306,19 @@ const growPointsYOut: TypeAnimation = ( export class GrowPointsYIn extends GworPointsBase { onBind(): void { - // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) - if (this.params?.diffAttrs) { - this.target.setAttributes(this.params.diffAttrs); - } if (['area', 'line', 'polygon'].includes(this.target.type)) { const { from, to } = growPointsYIn(this.target, this.params.options, this.params); - const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); - this.animate.reSyncProps(); - this.from = fromAttrs; + this.from = from; this.to = to; - this.target.setAttributes(fromAttrs); + + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + const finalAttribute = this.target.getFinalAttribute(); + if (finalAttribute) { + Object.assign(this.target.attribute, finalAttribute); + } + this.target.setAttributes(from); } else { this.valid = false; } @@ -321,13 +327,11 @@ export class GrowPointsYIn extends GworPointsBase { export class GrowPointsYOut extends GworPointsBase { onBind(): void { - if (['area', 'line'].includes(this.target.type)) { - const attrs = this.target.getFinalAttribute(); + if (['area', 'line', 'polygon'].includes(this.target.type)) { const { from, to } = growPointsYOut(this.target, this.params.options, this.params); this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); - this.animate.reSyncProps(); - this.from = from || attrs; + this.from = from ?? (this.target.attribute as any); this.to = to; } else { this.valid = false; diff --git a/packages/vrender-animate/src/custom/growRadius.ts b/packages/vrender-animate/src/custom/growRadius.ts index 6314955ea..bf3beaf52 100644 --- a/packages/vrender-animate/src/custom/growRadius.ts +++ b/packages/vrender-animate/src/custom/growRadius.ts @@ -123,7 +123,7 @@ export const growRadiusOut: TypeAnimation = ( : growRadiusOutIndividual(graphic, options, animationParameters); }; -export class GworPointsBase extends ACustomAnimate> { +export class GrowPointsBase extends ACustomAnimate> { declare valid: boolean; constructor(from: null, to: null, duration: number, easing: EasingType, params?: any) { @@ -143,32 +143,32 @@ export class GworPointsBase extends ACustomAnimate> { /** * 增长渐入 */ -export class GrowRadiusIn extends GworPointsBase { +export class GrowRadiusIn extends GrowPointsBase { onBind(): void { - // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) - if (this.params?.diffAttrs) { - this.target.setAttributes(this.params.diffAttrs); - } const { from, to } = growRadiusIn(this.target, this.params.options, this.params); const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); - this.animate.reSyncProps(); this.from = fromAttrs; this.to = to; + + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + const finalAttribute = this.target.getFinalAttribute(); + if (finalAttribute) { + Object.assign(this.target.attribute, finalAttribute); + } this.target.setAttributes(fromAttrs); } } -export class GrowRadiusOut extends GworPointsBase { +export class GrowRadiusOut extends GrowPointsBase { onBind(): void { - const { from, to } = growRadiusOut(this.target, this.params.options, this.params); - const fromAttrs = this.target.context?.lastAttrs ?? from; + const { to } = growRadiusOut(this.target, this.params.options, this.params); this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); - this.animate.reSyncProps(); - this.from = fromAttrs; + + this.from = this.target.attribute as any; this.to = to; - this.target.setAttributes(fromAttrs); + // this.target.setAttributes(fromAttrs); } } diff --git a/packages/vrender-animate/src/custom/growWidth.ts b/packages/vrender-animate/src/custom/growWidth.ts index 07d922cd1..411690098 100644 --- a/packages/vrender-animate/src/custom/growWidth.ts +++ b/packages/vrender-animate/src/custom/growWidth.ts @@ -164,17 +164,17 @@ export class GrowWidthIn extends ACustomAnimate> { } onBind(): void { - // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) - if (this.params?.diffAttrs) { - this.target.setAttributes(this.params.diffAttrs); - } const { from, to } = growWidthIn(this.target, this.params.options, this.params); const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); - this.animate.reSyncProps(); this.from = fromAttrs; this.to = to; + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + const finalAttribute = this.target.getFinalAttribute(); + if (finalAttribute) { + Object.assign(this.target.attribute, finalAttribute); + } this.target.setAttributes(fromAttrs); } @@ -200,12 +200,10 @@ export class GrowWidthOut extends ACustomAnimate> { } onBind(): void { - const attrs = this.target.getFinalAttribute(); const { from, to } = growWidthOut(this.target, this.params.options, this.params); this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); - this.animate.reSyncProps(); - this.from = from || attrs; + this.from = from ?? (this.target.attribute as any); this.to = to; // this.target.setAttributes(from); } diff --git a/packages/vrender-animate/src/custom/move.ts b/packages/vrender-animate/src/custom/move.ts index 31c1a8048..109c0137f 100644 --- a/packages/vrender-animate/src/custom/move.ts +++ b/packages/vrender-animate/src/custom/move.ts @@ -141,30 +141,27 @@ export class MoveBase extends ACustomAnimate> { */ export class MoveIn extends MoveBase { onBind(): void { - // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) - if (this.params?.diffAttrs) { - this.target.setAttributes(this.params.diffAttrs); - } const { from, to } = moveIn(this.target, this.params.options, this.params); - const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => (to as any)[key] != null); - this.animate.reSyncProps(); - this.from = fromAttrs; + this.from = from; this.to = to; - this.target.setAttributes(fromAttrs); + + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + const finalAttribute = this.target.getFinalAttribute(); + if (finalAttribute) { + Object.assign(this.target.attribute, finalAttribute); + } + this.target.setAttributes(from); } } export class MoveOut extends MoveBase { onBind(): void { const { from, to } = moveOut(this.target, this.params.options, this.params); - const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; this.propKeys = Object.keys(to).filter(key => (to as any)[key] != null); - this.animate.reSyncProps(); - this.from = fromAttrs; + this.from = from; this.to = to; - this.target.setAttributes(fromAttrs); } } diff --git a/packages/vrender-animate/src/custom/readme.md b/packages/vrender-animate/src/custom/readme.md new file mode 100644 index 000000000..e5f2efbb7 --- /dev/null +++ b/packages/vrender-animate/src/custom/readme.md @@ -0,0 +1,10 @@ +## 自定义动画背景提示 + +- VChart 如果存在入场动画,将不会设置属性到 attribute,需要在动画中进行设置(从 finalAttribute 中获取,finalAttribute 中保存着一份最新的属性副本)。 +- VChart 如果存在出场动画,通常不需要直接设置属性,否则可能会导致跳帧。 + +## 容易误解的定义 + +1. step 是一个动画的基本单元,自定义动画继承自 step,step 通过 props 属性作为动画的目标属性。 +2. 而自定义动画中通常使用 from 和 to 保存动画的开始和结束状态,作为一一对应,自定义动画中因为插值是自己实现的,所以你定义 fromaaa 和 toccc 都可以,自己在 update 里拿到你保存的开始和结束变量,然后自己计算。 +3. 但是自定义动画中,你自己保存了自定义的 from 和 to 之后,还需要设置一下 props,这样上层的 Animate 才知道这个动画的最终属性是什么(因为 from 和 to 是你自定义的,而 props 属性才是动画系统认可的最终状态)。 diff --git a/packages/vrender-animate/src/custom/register.ts b/packages/vrender-animate/src/custom/register.ts index e9377baf1..261262f4a 100644 --- a/packages/vrender-animate/src/custom/register.ts +++ b/packages/vrender-animate/src/custom/register.ts @@ -23,7 +23,7 @@ import { OutputRichText } from './richtext/output-richtext'; import { SlideRichText } from './richtext/slide-richtext'; import { SlideOutRichText } from './richtext/slide-out-richtext'; import { ScaleIn, ScaleOut } from './scale'; -import { State } from './state'; +// import { State } from './state'; import { GrowIn, GrowOut, @@ -73,7 +73,7 @@ export const registerCustomAnimate = () => { AnimateExecutor.registerBuiltInAnimate('rotateOut', RotateOut); // state和update共用一个自定义动画类 AnimateExecutor.registerBuiltInAnimate('update', Update); - AnimateExecutor.registerBuiltInAnimate('state', State); + // AnimateExecutor.registerBuiltInAnimate('state', State); // Label item animations AnimateExecutor.registerBuiltInAnimate('labelItemAppear', LabelItemAppear); AnimateExecutor.registerBuiltInAnimate('labelItemDisappear', LabelItemDisappear); diff --git a/packages/vrender-animate/src/custom/rotate.ts b/packages/vrender-animate/src/custom/rotate.ts index 12bf44363..c1f4ad888 100644 --- a/packages/vrender-animate/src/custom/rotate.ts +++ b/packages/vrender-animate/src/custom/rotate.ts @@ -69,29 +69,30 @@ export class RotateBase extends ACustomAnimate> { export class RotateIn extends RotateBase { onBind(): void { // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) - if (this.params?.diffAttrs) { - this.target.setAttributes(this.params.diffAttrs); - } const { from, to } = rotateIn(this.target, this.params.options); - const fromAttrs = this.target.context?.lastAttrs ?? from; + this.props = to; - this.propKeys = Object.keys(to).filter(key => (to as any)[key] != null); - this.animate.reSyncProps(); - this.from = fromAttrs; + this.propKeys = ['angle']; + this.from = from; this.to = to; - this.target.setAttributes(fromAttrs); + + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + const finalAttribute = this.target.getFinalAttribute(); + if (finalAttribute) { + Object.assign(this.target.attribute, finalAttribute); + } + + this.target.setAttributes(from); } } export class RotateOut extends RotateBase { onBind(): void { const { from, to } = rotateOut(this.target, this.params.options); - const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; - this.propKeys = Object.keys(to).filter(key => (to as any)[key] != null); - this.animate.reSyncProps(); - this.from = fromAttrs; + this.propKeys = ['angle']; + + this.from = from; this.to = to; - this.target.setAttributes(fromAttrs); } } diff --git a/packages/vrender-animate/src/custom/scale.ts b/packages/vrender-animate/src/custom/scale.ts index c739ef992..9589c5ff5 100644 --- a/packages/vrender-animate/src/custom/scale.ts +++ b/packages/vrender-animate/src/custom/scale.ts @@ -16,26 +16,20 @@ export class ScaleIn extends ACustomAnimate> { declare _updateFunction: (ratio: number) => void; onBind(): void { - // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) - if (this.params?.diffAttrs) { - Object.assign(this.target.attribute, this.params.diffAttrs); - } let from: Record; let to: Record; const attrs = this.target.getFinalAttribute(); - const fromAttrs = this.target.context?.lastAttrs ?? {}; + const fromAttrs = this.target.attribute ?? {}; switch (this.params?.direction) { case 'x': from = { scaleX: fromAttrs.scaleX ?? 0 }; to = { scaleX: attrs?.scaleX ?? 1 }; - this.propKeys = ['scaleX']; this._updateFunction = this.updateX; break; case 'y': from = { scaleY: fromAttrs.scaleY ?? 0 }; to = { scaleY: attrs?.scaleY ?? 1 }; - this.propKeys = ['scaleY']; this._updateFunction = this.updateY; break; case 'xy': @@ -45,19 +39,20 @@ export class ScaleIn extends ACustomAnimate> { scaleX: attrs?.scaleX ?? 1, scaleY: attrs?.scaleY ?? 1 }; - this.propKeys = ['scaleX', 'scaleY']; this._updateFunction = this.updateXY; } + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + const finalAttribute = this.target.getFinalAttribute(); + if (finalAttribute) { + Object.assign(this.target.attribute, finalAttribute); + } + this.props = to; - // 性能消耗,不用reSyncProps - // this.animate.reSyncProps(); this.from = from; this.to = to; - // 性能优化,不需要setAttributes - Object.assign(this.target.attribute, from); - this.target.addUpdatePositionTag(); - this.target.addUpdateBoundTag(); + // 调用次数不多,可以setAttributes + this.target.setAttributes(from); } onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { @@ -77,9 +72,34 @@ export class ScaleIn extends ACustomAnimate> { this.updateY(ratio); } + /** + * 删除自身属性,会直接从props等内容里删除掉 + */ + deleteSelfAttr(key: string): void { + delete this.props[key]; + // fromProps在动画开始时才会计算,这时可能不在 + this.fromProps && delete this.fromProps[key]; + const index = this.propKeys.indexOf(key); + if (index !== -1) { + this.propKeys.splice(index, 1); + } + + if (this.propKeys && this.propKeys.length > 1) { + this._updateFunction = this.updateXY; + } else if (this.propKeys[0] === 'scaleX') { + this._updateFunction = this.updateX; + } else if (this.propKeys[0] === 'scaleY') { + this._updateFunction = this.updateY; + } else { + this._updateFunction = null; + } + } + onUpdate(end: boolean, ratio: number, out: Record): void { - this._updateFunction(ratio); - this.target.addUpdatePositionTag(); + if (this._updateFunction) { + this._updateFunction(ratio); + this.target.addUpdatePositionTag(); + } } } @@ -94,7 +114,7 @@ export class ScaleOut extends ACustomAnimate> { let from: Record; let to: Record; // 获取当前的数据 - const attrs = this.target.getFinalAttribute(); + const attrs = this.target.attribute; switch (this.params?.direction) { case 'x': from = { scaleX: attrs?.scaleX ?? 1 }; @@ -113,8 +133,6 @@ export class ScaleOut extends ACustomAnimate> { }; } this.props = to; - this.propKeys = Object.keys(to); - this.animate.reSyncProps(); this.from = from; this.to = to; } diff --git a/packages/vrender-animate/src/custom/state.ts b/packages/vrender-animate/src/custom/state.ts index 785dd199f..5dff605f2 100644 --- a/packages/vrender-animate/src/custom/state.ts +++ b/packages/vrender-animate/src/custom/state.ts @@ -1,45 +1,45 @@ -import type { IAnimate, IStep } from '../intreface/animate'; -import type { EasingType } from '../intreface/easing'; -import { ACustomAnimate } from './custom-animate'; +// import type { IAnimate, IStep } from '../intreface/animate'; +// import type { EasingType } from '../intreface/easing'; +// import { ACustomAnimate } from './custom-animate'; -export interface IUpdateAnimationOptions { - diffAttrs: Record; - animationState: string; - diffState: string; - data: Record[]; -} +// export interface IUpdateAnimationOptions { +// diffAttrs: Record; +// animationState: string; +// diffState: string; +// data: Record[]; +// } -/** - * 文本输入动画,实现类似打字机的字符逐个显示效果 - * 支持通过beforeText和afterText参数添加前缀和后缀 - * 支持通过showCursor参数显示光标,cursorChar自定义光标字符 - */ -export class State extends ACustomAnimate> { - declare valid: boolean; +// /** +// * 文本输入动画,实现类似打字机的字符逐个显示效果 +// * 支持通过beforeText和afterText参数添加前缀和后缀 +// * 支持通过showCursor参数显示光标,cursorChar自定义光标字符 +// */ +// export class State extends ACustomAnimate> { +// declare valid: boolean; - constructor(from: null, to: null, duration: number, easing: EasingType, params?: IUpdateAnimationOptions) { - super(from, to, duration, easing, params); - } +// constructor(from: null, to: null, duration: number, easing: EasingType, params?: IUpdateAnimationOptions) { +// super(from, to, duration, easing, params); +// } - update(end: boolean, ratio: number, out: Record): void { - this.onStart(); - if (!this.props || !this.propKeys) { - return; - } - // 应用缓动函数 - const easedRatio = this.easing(ratio); - this.animate.interpolateUpdateFunction - ? this.animate.interpolateUpdateFunction(this.fromProps, this.props, easedRatio, this, this.target) - : this.interpolateUpdateFunctions.forEach((func, index) => { - // 如果这个属性被屏蔽了,直接跳过 - if (!this.animate.validAttr(this.propKeys[index])) { - return; - } - const key = this.propKeys[index]; - const fromValue = this.fromProps[key]; - const toValue = this.props[key]; - func(key, fromValue, toValue, easedRatio, this, this.target); - }); - this.onUpdate(end, easedRatio, out); - } -} +// update(end: boolean, ratio: number, out: Record): void { +// this.onStart(); +// if (!this.props || !this.propKeys) { +// return; +// } +// // 应用缓动函数 +// const easedRatio = this.easing(ratio); +// this.animate.interpolateUpdateFunction +// ? this.animate.interpolateUpdateFunction(this.fromProps, this.props, easedRatio, this, this.target) +// : this.interpolateUpdateFunctions.forEach((func, index) => { +// // 如果这个属性被屏蔽了,直接跳过 +// if (!this.animate.validAttr(this.propKeys[index])) { +// return; +// } +// const key = this.propKeys[index]; +// const fromValue = this.fromProps[key]; +// const toValue = this.props[key]; +// func(key, fromValue, toValue, easedRatio, this, this.target); +// }); +// this.onUpdate(end, easedRatio, out); +// } +// } diff --git a/packages/vrender-animate/src/custom/story.ts b/packages/vrender-animate/src/custom/story.ts index a948f0632..9e8bbd327 100644 --- a/packages/vrender-animate/src/custom/story.ts +++ b/packages/vrender-animate/src/custom/story.ts @@ -41,7 +41,7 @@ export class SlideIn extends ACustomAnimate> { onBind(): void { // 用于入场的时候设置属性 - const attrs = this.target.getAttributes(true); + const attrs = this.target.getFinalAttribute(); const direction = (this.params?.direction as 'top' | 'bottom' | 'left' | 'right') || 'right'; const distance = this.params?.distance || 50; @@ -104,7 +104,7 @@ export class GrowIn extends ACustomAnimate> { onBind(): void { // 用于入场的时候设置属性 - const attrs = this.target.getAttributes(true); + const attrs = this.target.getFinalAttribute(); const fromScale = this.params?.fromScale ?? 0; const direction = this.params?.direction || 'xy'; @@ -161,7 +161,7 @@ export class SpinIn extends ACustomAnimate> { onBind(): void { // 用于入场的时候设置属性 - const attrs = this.target.getAttributes(true); + const attrs = this.target.getFinalAttribute(); const fromAngle = this.params?.fromAngle ?? Math.PI * 2; // 默认旋转一圈 const fromScale = this.params?.fromScale ?? 0; diff --git a/packages/vrender-animate/src/custom/update.ts b/packages/vrender-animate/src/custom/update.ts index b967b44f3..e19683d08 100644 --- a/packages/vrender-animate/src/custom/update.ts +++ b/packages/vrender-animate/src/custom/update.ts @@ -18,17 +18,22 @@ export class Update extends ACustomAnimate> { // params: IUpdateAnimationOptions; constructor(from: null, to: null, duration: number, easing: EasingType, params?: IUpdateAnimationOptions) { - let { diffAttrs = {} } = params as any; - const { options } = params as any; + super(from, to, duration, easing, params); + // this.params = params; + } + + onBind() { + let { diffAttrs = {} } = this.target.context ?? ({} as any); + const { options } = this.params as any; - diffAttrs = { ...diffAttrs }; if (options?.excludeChannels?.length) { + diffAttrs = { ...diffAttrs }; options.excludeChannels.forEach((channel: string) => { delete diffAttrs[channel]; }); } - super(from, diffAttrs, duration, easing, params); - // this.params = params; + + this.props = diffAttrs; } update(end: boolean, ratio: number, out: Record): void { diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index 5ea53edbc..7a7f5746e 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -903,8 +903,8 @@ export class LabelBase extends AnimateComponent { { name: 'exit', animation: { - type: 'fadeOut', - ...this._animationConfig.exit + ...this._animationConfig.exit, + type: 'fadeOut' } } ], @@ -918,8 +918,8 @@ export class LabelBase extends AnimateComponent { { name: 'exit', animation: { - type: 'fadeOut', - ...this._animationConfig.exit + ...this._animationConfig.exit, + type: 'fadeOut' } } ], From 4cd1277707c943d261f862425e69efda78735414 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 9 Apr 2025 21:35:35 +0800 Subject: [PATCH 081/179] fix: fix issue with state and support from-to animate type --- .../vrender-animate/src/custom/from-to.ts | 61 ++++++++++++++ .../vrender-animate/src/custom/register.ts | 6 +- packages/vrender-animate/src/custom/state.ts | 82 +++++++++---------- .../src/executor/animate-executor.ts | 71 +++++++++++++--- .../vrender-animate/src/executor/executor.ts | 4 + packages/vrender-core/src/graphic/graphic.ts | 4 +- 6 files changed, 173 insertions(+), 55 deletions(-) create mode 100644 packages/vrender-animate/src/custom/from-to.ts diff --git a/packages/vrender-animate/src/custom/from-to.ts b/packages/vrender-animate/src/custom/from-to.ts new file mode 100644 index 000000000..26830669f --- /dev/null +++ b/packages/vrender-animate/src/custom/from-to.ts @@ -0,0 +1,61 @@ +import type { EasingType } from '@visactor/vrender-core'; +import { ACustomAnimate } from './custom-animate'; +import type { IAnimate, IStep } from '../intreface/animate'; + +export class FromTo extends ACustomAnimate> { + declare valid: boolean; + + keys: string[]; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: any) { + super(from, to, duration, easing, params); + this.from = from; + this.to = to; + } + + onBind(): void { + const finalAttribute = this.target.getFinalAttribute(); + const currAttribute = this.target.attribute; + // 要同步from和to + const fromKeys = Object.keys(this.from); + const toKeys = Object.keys(this.to); + fromKeys.forEach(key => { + if (this.to[key] == null) { + this.to[key] = finalAttribute[key] ?? 0; + } + }); + toKeys.forEach(key => { + if (this.from[key] == null) { + this.from[key] = (currAttribute as any)[key] ?? 0; + } + }); + this.propKeys = Object.keys(this.from); + + // TODO:比较hack + // 如果是入场动画,那么还需要设置属性 + if (this.target.context?.animationState === 'appear') { + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + const finalAttribute = this.target.getFinalAttribute(); + this.target.setAttributes(finalAttribute); + } + } + + update(end: boolean, ratio: number, out: Record): void { + this.onStart(); + if (!this.props || !this.propKeys) { + return; + } + // 应用缓动函数 + const easedRatio = this.easing(ratio); + this.interpolateUpdateFunctions.forEach((func, index) => { + // 如果这个属性被屏蔽了,直接跳过 + if (!this.animate.validAttr(this.propKeys[index])) { + return; + } + const key = this.propKeys[index]; + const fromValue = this.from[key]; + const toValue = this.to[key]; + func(key, fromValue, toValue, easedRatio, this, this.target); + }); + } +} diff --git a/packages/vrender-animate/src/custom/register.ts b/packages/vrender-animate/src/custom/register.ts index 261262f4a..9eac1ada4 100644 --- a/packages/vrender-animate/src/custom/register.ts +++ b/packages/vrender-animate/src/custom/register.ts @@ -23,7 +23,7 @@ import { OutputRichText } from './richtext/output-richtext'; import { SlideRichText } from './richtext/slide-richtext'; import { SlideOutRichText } from './richtext/slide-out-richtext'; import { ScaleIn, ScaleOut } from './scale'; -// import { State } from './state'; +import { State } from './state'; import { GrowIn, GrowOut, @@ -40,11 +40,13 @@ import { Update } from './update'; import { MoveIn, MoveOut } from './move'; import { RotateIn, RotateOut } from './rotate'; import { MotionPath } from './motionPath'; +import { FromTo } from './from-to'; export const registerCustomAnimate = () => { // 基础动画 AnimateExecutor.registerBuiltInAnimate('increaseCount', IncreaseCount); + AnimateExecutor.registerBuiltInAnimate('fromTo', FromTo); AnimateExecutor.registerBuiltInAnimate('scaleIn', ScaleIn); AnimateExecutor.registerBuiltInAnimate('scaleOut', ScaleOut); AnimateExecutor.registerBuiltInAnimate('growHeightIn', GrowHeightIn); @@ -73,7 +75,7 @@ export const registerCustomAnimate = () => { AnimateExecutor.registerBuiltInAnimate('rotateOut', RotateOut); // state和update共用一个自定义动画类 AnimateExecutor.registerBuiltInAnimate('update', Update); - // AnimateExecutor.registerBuiltInAnimate('state', State); + AnimateExecutor.registerBuiltInAnimate('state', State); // Label item animations AnimateExecutor.registerBuiltInAnimate('labelItemAppear', LabelItemAppear); AnimateExecutor.registerBuiltInAnimate('labelItemDisappear', LabelItemDisappear); diff --git a/packages/vrender-animate/src/custom/state.ts b/packages/vrender-animate/src/custom/state.ts index 5dff605f2..785dd199f 100644 --- a/packages/vrender-animate/src/custom/state.ts +++ b/packages/vrender-animate/src/custom/state.ts @@ -1,45 +1,45 @@ -// import type { IAnimate, IStep } from '../intreface/animate'; -// import type { EasingType } from '../intreface/easing'; -// import { ACustomAnimate } from './custom-animate'; +import type { IAnimate, IStep } from '../intreface/animate'; +import type { EasingType } from '../intreface/easing'; +import { ACustomAnimate } from './custom-animate'; -// export interface IUpdateAnimationOptions { -// diffAttrs: Record; -// animationState: string; -// diffState: string; -// data: Record[]; -// } +export interface IUpdateAnimationOptions { + diffAttrs: Record; + animationState: string; + diffState: string; + data: Record[]; +} -// /** -// * 文本输入动画,实现类似打字机的字符逐个显示效果 -// * 支持通过beforeText和afterText参数添加前缀和后缀 -// * 支持通过showCursor参数显示光标,cursorChar自定义光标字符 -// */ -// export class State extends ACustomAnimate> { -// declare valid: boolean; +/** + * 文本输入动画,实现类似打字机的字符逐个显示效果 + * 支持通过beforeText和afterText参数添加前缀和后缀 + * 支持通过showCursor参数显示光标,cursorChar自定义光标字符 + */ +export class State extends ACustomAnimate> { + declare valid: boolean; -// constructor(from: null, to: null, duration: number, easing: EasingType, params?: IUpdateAnimationOptions) { -// super(from, to, duration, easing, params); -// } + constructor(from: null, to: null, duration: number, easing: EasingType, params?: IUpdateAnimationOptions) { + super(from, to, duration, easing, params); + } -// update(end: boolean, ratio: number, out: Record): void { -// this.onStart(); -// if (!this.props || !this.propKeys) { -// return; -// } -// // 应用缓动函数 -// const easedRatio = this.easing(ratio); -// this.animate.interpolateUpdateFunction -// ? this.animate.interpolateUpdateFunction(this.fromProps, this.props, easedRatio, this, this.target) -// : this.interpolateUpdateFunctions.forEach((func, index) => { -// // 如果这个属性被屏蔽了,直接跳过 -// if (!this.animate.validAttr(this.propKeys[index])) { -// return; -// } -// const key = this.propKeys[index]; -// const fromValue = this.fromProps[key]; -// const toValue = this.props[key]; -// func(key, fromValue, toValue, easedRatio, this, this.target); -// }); -// this.onUpdate(end, easedRatio, out); -// } -// } + update(end: boolean, ratio: number, out: Record): void { + this.onStart(); + if (!this.props || !this.propKeys) { + return; + } + // 应用缓动函数 + const easedRatio = this.easing(ratio); + this.animate.interpolateUpdateFunction + ? this.animate.interpolateUpdateFunction(this.fromProps, this.props, easedRatio, this, this.target) + : this.interpolateUpdateFunctions.forEach((func, index) => { + // 如果这个属性被屏蔽了,直接跳过 + if (!this.animate.validAttr(this.propKeys[index])) { + return; + } + const key = this.propKeys[index]; + const fromValue = this.fromProps[key]; + const toValue = this.props[key]; + func(key, fromValue, toValue, easedRatio, this, this.target); + }); + this.onUpdate(end, easedRatio, out); + } +} diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index beca5bb37..8359218a8 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -161,7 +161,7 @@ export class AnimateExecutor implements IAnimateExecutor { delayAfter: (slice.delayAfter as number) * scale, duration: (slice.duration as number) * scale, effects: effects.map(effect => { - const custom = effect.custom ?? AnimateExecutor.builtInAnimateMap[(effect.type as any) ?? 'to']; + const custom = effect.custom ?? AnimateExecutor.builtInAnimateMap[(effect.type as any) ?? 'fromTo']; const customType = custom && isFunction(custom) ? (/^class\s/.test(Function.prototype.toString.call(custom)) ? 1 : 2) : 0; return { @@ -191,7 +191,7 @@ export class AnimateExecutor implements IAnimateExecutor { parsedParams.oneByOneDelay = oneByOneDelay; parsedParams.custom = (params as IAnimationTypeConfig).custom ?? - AnimateExecutor.builtInAnimateMap[(params as IAnimationTypeConfig).type ?? 'to']; + AnimateExecutor.builtInAnimateMap[(params as IAnimationTypeConfig).type ?? 'fromTo']; const customType = parsedParams.custom && isFunction(parsedParams.custom) @@ -293,7 +293,7 @@ export class AnimateExecutor implements IAnimateExecutor { count: number ): IAnimate { const { - type = 'to', + type = 'fromTo', channel, customParameters, easing = 'linear', @@ -336,12 +336,28 @@ export class AnimateExecutor implements IAnimateExecutor { // } // 根据 channel 配置创建属性对象 - const props = params.to ?? this.createPropsFromChannel(channel, graphic); + // 根据 channel 配置创建属性对象 + let parsedFromProps = null; + let props = params.to; + let from = params.from; + if (!props) { + if (!parsedFromProps) { + parsedFromProps = this.createPropsFromChannel(channel, graphic); + } + props = parsedFromProps.props; + } + if (!from) { + if (!parsedFromProps) { + parsedFromProps = this.createPropsFromChannel(channel, graphic); + } + from = parsedFromProps.from; + } this._handleRunAnimate( animate, custom, customType, + from, props, duration as number, easing, @@ -378,6 +394,7 @@ export class AnimateExecutor implements IAnimateExecutor { animate: IAnimate, custom: IAnimationCustomConstructor | IAnimationChannelInterpolator, customType: number, // 0: undefined, 1: class, 2: function + from: Record | null, props: Record, duration: number, easing: EasingType, @@ -398,6 +415,7 @@ export class AnimateExecutor implements IAnimateExecutor { this.createCustomAnimation( animate, custom as IAnimationCustomConstructor, + from, props, duration as number, easing, @@ -487,14 +505,29 @@ export class AnimateExecutor implements IAnimateExecutor { const effectsArray = Array.isArray(effects) ? effects : [effects]; effectsArray.forEach(effect => { - const { type = 'to', channel, customParameters, easing = 'linear', options } = effect; + const { type = 'fromTo', channel, customParameters, easing = 'linear', options } = effect; // 根据 channel 配置创建属性对象 - const props = effect.to ?? this.createPropsFromChannel(channel, graphic); + let parsedFromProps = null; + let props = effect.to; + let from = effect.from; + if (!props) { + if (!parsedFromProps) { + parsedFromProps = this.createPropsFromChannel(channel, graphic); + } + props = parsedFromProps.props; + } + if (!from) { + if (!parsedFromProps) { + parsedFromProps = this.createPropsFromChannel(channel, graphic); + } + from = parsedFromProps.from; + } this._handleRunAnimate( animate, effect.custom, (effect as any).customType, + from, props, duration as number, easing, @@ -544,6 +577,7 @@ export class AnimateExecutor implements IAnimateExecutor { private createCustomAnimation( animate: IAnimate, CustomAnimateConstructor: IAnimationCustomConstructor, + from: Record | null, props: Record, duration: number, easing: EasingType, @@ -560,7 +594,7 @@ export class AnimateExecutor implements IAnimateExecutor { // 实例化自定义动画类 // 自定义动画自己去计算from - const customAnimate = new CustomAnimateConstructor(null, to, duration, easing, customParams); + const customAnimate = new CustomAnimateConstructor(from, to, duration, easing, customParams); // 播放自定义动画 animate.play(customAnimate); @@ -572,11 +606,15 @@ export class AnimateExecutor implements IAnimateExecutor { private createPropsFromChannel( channel: IAnimationChannelAttrs | IAnimationChannelAttributes | undefined, graphic: IGraphic - ): Record { + ): { from: Record | null; props: Record } { const props: Record = {}; + let from: Record | null = null; if (!channel) { - return props; + return { + from, + props + }; } if (!Array.isArray(channel)) { @@ -589,6 +627,16 @@ export class AnimateExecutor implements IAnimateExecutor { props[key] = config.to; } } + if (config.from !== undefined) { + if (!from) { + from = {}; + } + if (typeof config.from === 'function') { + from[key] = config.from((graphic.context as any)?.data, graphic, {}); + } else { + from[key] = config.from; + } + } }); } else { channel.forEach(key => { @@ -599,7 +647,10 @@ export class AnimateExecutor implements IAnimateExecutor { }); } - return props; + return { + from, + props + }; } /** diff --git a/packages/vrender-animate/src/executor/executor.ts b/packages/vrender-animate/src/executor/executor.ts index 41b0eb865..cc595480b 100644 --- a/packages/vrender-animate/src/executor/executor.ts +++ b/packages/vrender-animate/src/executor/executor.ts @@ -53,6 +53,8 @@ export interface IAnimationEffect { channel?: IAnimationChannelAttrs | IAnimationChannelAttributes; /** 动画 to 配置(和channel互斥,如果同时设置,以to为准) */ to?: Record; + /** 动画 from 配置 */ + from?: Record; /** 动画 自定义插值 配置 */ custom?: IAnimationChannelInterpolator | IAnimationCustomConstructor; /** 动画 custom 参数配置 */ @@ -98,6 +100,8 @@ export interface IAnimationTypeConfig { channel?: IAnimationChannelAttrs | IAnimationChannelAttributes; /** 动画 to 配置(和channel互斥,如果同时设置,以to为准) */ to?: Record; + /** 动画 from 配置 */ + from?: Record; /** 动画 自定义插值 配置 */ custom?: IAnimationChannelInterpolator | IAnimationCustomConstructor; /** 动画 custom 参数配置 */ diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index c1d2d0d38..537b337d5 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -1032,7 +1032,7 @@ export abstract class Graphic = Partial = Partial Date: Wed, 9 Apr 2025 21:42:56 +0800 Subject: [PATCH 082/179] fix: fix type error --- .../vrender-animate/src/state/animation-state.ts | 8 ++++---- packages/vrender-animate/src/timeline.ts | 3 ++- packages/vrender-core/src/common/enums.ts | 12 ------------ packages/vrender-core/src/interface/animate.ts | 14 +++++++++++++- packages/vrender/src/index.ts | 1 + 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/vrender-animate/src/state/animation-state.ts b/packages/vrender-animate/src/state/animation-state.ts index d1c99b1f5..2dcd06617 100644 --- a/packages/vrender-animate/src/state/animation-state.ts +++ b/packages/vrender-animate/src/state/animation-state.ts @@ -84,8 +84,8 @@ export class AnimationStateManager { shouldApplyState.push({ state, animationConfig: isArray(animationConfig[index]) - ? animationConfig[index].map(item => item.animation) - : animationConfig[index].animation, + ? (animationConfig[index] as IAnimationState[]).map(item => item.animation) + : (animationConfig[index] as IAnimationState).animation, executor: new AnimateExecutor(this.graphic) }); }); @@ -106,8 +106,8 @@ export class AnimationStateManager { shouldApplyState.push({ state, animationConfig: isArray(animationConfig[index]) - ? animationConfig[index].map(item => item.animation) - : animationConfig[index].animation, + ? (animationConfig[index] as IAnimationState[]).map(item => item.animation) + : (animationConfig[index] as IAnimationState).animation, executor: new AnimateExecutor(this.graphic) }); // 允许过渡的话,需要重新遍历this.stateList,获取stopOriginalTransition diff --git a/packages/vrender-animate/src/timeline.ts b/packages/vrender-animate/src/timeline.ts index 6ce0189ec..913e7bcb4 100644 --- a/packages/vrender-animate/src/timeline.ts +++ b/packages/vrender-animate/src/timeline.ts @@ -1,6 +1,7 @@ -import { AnimateStatus, Generator } from '@visactor/vrender-core'; +import { Generator } from '@visactor/vrender-core'; import type { IAnimate } from './intreface/animate'; import type { ITimeline } from './intreface/timeline'; +import { AnimateStatus } from './intreface/type'; export class DefaultTimeline implements ITimeline { declare id: number; diff --git a/packages/vrender-core/src/common/enums.ts b/packages/vrender-core/src/common/enums.ts index 91ebdd128..c05c743fb 100644 --- a/packages/vrender-core/src/common/enums.ts +++ b/packages/vrender-core/src/common/enums.ts @@ -41,18 +41,6 @@ export enum AttributeUpdateType { ROTATE_TO = 25 } -export enum AnimateStatus { - INITIAL = 0, - RUNNING = 1, - PAUSED = 2, - END = 3 -} - -export enum AnimateMode { - NORMAL = 0b0000, - SET_ATTR_IMMEDIATELY = 0b0001 -} - export enum AnimateStepType { 'wait' = 'wait', 'from' = 'from', diff --git a/packages/vrender-core/src/interface/animate.ts b/packages/vrender-core/src/interface/animate.ts index 4fc220968..9fabf1a28 100644 --- a/packages/vrender-core/src/interface/animate.ts +++ b/packages/vrender-core/src/interface/animate.ts @@ -1,8 +1,20 @@ import type { EventEmitter } from '@visactor/vutils'; -import type { AnimateMode, AnimateStatus, AnimateStepType } from '../common/enums'; +import type { AnimateStepType } from '../common/enums'; import type { Releaseable } from './common'; import type { IGraphic } from './graphic'; +enum AnimateStatus { + INITIAL = 0, + RUNNING = 1, + PAUSED = 2, + END = 3 +} + +enum AnimateMode { + NORMAL = 0b0000, + SET_ATTR_IMMEDIATELY = 0b0001 +} + // export type EasingType = (...args: any) => any; // export declare class Easing { diff --git a/packages/vrender/src/index.ts b/packages/vrender/src/index.ts index 1d770df83..a1449334c 100644 --- a/packages/vrender/src/index.ts +++ b/packages/vrender/src/index.ts @@ -70,3 +70,4 @@ registerDirectionalLight(); registerOrthoCamera(); export * from '@visactor/vrender-core'; export * from '@visactor/vrender-kits'; +export * from '@visactor/vrender-animate'; From 21a9a71e1eb90591ba74806e889de9d99d8b3d84 Mon Sep 17 00:00:00 2001 From: xiaoluoHe Date: Thu, 10 Apr 2025 15:36:02 +0800 Subject: [PATCH 083/179] feat: export animate `FromTo` --- packages/vrender-animate/src/custom/register.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vrender-animate/src/custom/register.ts b/packages/vrender-animate/src/custom/register.ts index 9eac1ada4..695b797c9 100644 --- a/packages/vrender-animate/src/custom/register.ts +++ b/packages/vrender-animate/src/custom/register.ts @@ -157,5 +157,6 @@ export { GrowOut, SpinOut, MoveScaleOut, - MoveRotateOut + MoveRotateOut, + FromTo }; From 7980404855b2b88280d714357a51511171fa73d6 Mon Sep 17 00:00:00 2001 From: xiaoluoHe Date: Fri, 11 Apr 2025 11:45:45 +0800 Subject: [PATCH 084/179] chore: update vrender-animate config --- packages/vrender-components/jest.config.js | 3 ++- packages/vrender-components/tsconfig.json | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/vrender-components/jest.config.js b/packages/vrender-components/jest.config.js index 36f6004e9..316f8523f 100644 --- a/packages/vrender-components/jest.config.js +++ b/packages/vrender-components/jest.config.js @@ -26,6 +26,7 @@ module.exports = { '@visactor/vrender-core': path.resolve(__dirname, '../vrender-core/src/index.ts'), '@visactor/vrender/es/core': path.resolve(__dirname, '../vrender/src/index.ts'), '@visactor/vrender/es/register': path.resolve(__dirname, '../vrender/src/register.ts'), - '@visactor/vrender/es/kits': path.resolve(__dirname, '../vrender/src/kits.ts') + '@visactor/vrender/es/kits': path.resolve(__dirname, '../vrender/src/kits.ts'), + '@visactor/vrender-animate': path.resolve(__dirname, '../vrender-animate/src/index.ts') } }; diff --git a/packages/vrender-components/tsconfig.json b/packages/vrender-components/tsconfig.json index cf04cc05e..2b97d19e4 100644 --- a/packages/vrender-components/tsconfig.json +++ b/packages/vrender-components/tsconfig.json @@ -16,6 +16,9 @@ }, { "path": "../vrender-kits" + }, + { + "path": "../vrender-animate" } ] } From 3487be7c7088a64ca7aece59877c4502d80d879f Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 10 Apr 2025 11:32:39 +0800 Subject: [PATCH 085/179] feat: ignore do not use customParameters.data --- packages/vrender-animate/src/executor/animate-executor.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index 8359218a8..02f9474e6 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -407,7 +407,12 @@ export class AnimateExecutor implements IAnimateExecutor { if (custom && customType) { const customParams = this.resolveValue(customParameters, graphic, {}); const objOptions = isFunction(options) - ? options.call(null, customParams.data && customParams.data[0], graphic, customParams) + ? options.call( + null, + (customParams && customParams.data?.[0]) ?? graphic.context?.data?.[0], + graphic, + customParams + ) : options; customParams.options = objOptions; if (customType === 1) { From 1f7330284bd33f3fe27a29e169338649175f3839 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 10 Apr 2025 11:56:07 +0800 Subject: [PATCH 086/179] feat: move vrender-animate/interface to vrender-core --- .../vrender-animate/src/animate-extension.ts | 3 +- packages/vrender-animate/src/animate.ts | 16 +- .../src/component/component-animator.ts | 3 +- .../src/custom/clip-graphic.ts | 3 +- packages/vrender-animate/src/custom/clip.ts | 2 +- packages/vrender-animate/src/custom/common.ts | 3 +- .../src/custom/custom-animate.ts | 4 +- packages/vrender-animate/src/custom/fade.ts | 2 +- .../vrender-animate/src/custom/from-to.ts | 1 - .../vrender-animate/src/custom/growAngle.ts | 3 +- .../vrender-animate/src/custom/growCenter.ts | 4 +- .../vrender-animate/src/custom/growHeight.ts | 4 +- .../vrender-animate/src/custom/growPoints.ts | 4 +- .../vrender-animate/src/custom/growRadius.ts | 3 +- .../vrender-animate/src/custom/growWidth.ts | 4 +- .../vrender-animate/src/custom/input-text.ts | 3 +- packages/vrender-animate/src/custom/number.ts | 3 +- .../src/custom/richtext/input-richtext.ts | 10 +- .../src/custom/richtext/output-richtext.ts | 5 +- .../src/custom/richtext/slide-out-richtext.ts | 4 +- .../src/custom/richtext/slide-richtext.ts | 4 +- packages/vrender-animate/src/custom/scale.ts | 3 +- packages/vrender-animate/src/custom/state.ts | 3 +- packages/vrender-animate/src/custom/story.ts | 2 +- .../vrender-animate/src/custom/tag-points.ts | 8 +- packages/vrender-animate/src/custom/update.ts | 2 +- .../src/executor/animate-executor.ts | 6 +- .../vrender-animate/src/executor/executor.ts | 3 +- packages/vrender-animate/src/index.ts | 8 +- .../vrender-animate/src/interpolate/store.ts | 3 +- packages/vrender-animate/src/step.ts | 15 +- .../src/ticker/default-ticker.ts | 6 +- .../src/ticker/manual-ticker.ts | 3 +- packages/vrender-animate/src/timeline.ts | 5 +- .../src/axis/animate/group-transition.ts | 3 +- .../src/weather/weather-box.ts | 4 +- packages/vrender-core/src/common/enums.ts | 7 - packages/vrender-core/src/graphic/graphic.ts | 172 +--- .../vrender-core/src/interface/animate.ts | 735 +++++++++--------- .../src/interface/animation}/animate.ts | 13 +- .../src/interface/animation}/easing.ts | 0 .../src/interface/animation/index.ts | 5 + .../src/interface/animation}/ticker.ts | 0 .../src/interface/animation}/timeline.ts | 0 .../src/interface/animation}/type.ts | 0 .../vrender-core/src/interface/graphic.ts | 2 +- packages/vrender-core/src/interface/index.ts | 2 +- packages/vrender-core/src/interface/stage.ts | 2 +- 48 files changed, 448 insertions(+), 652 deletions(-) rename packages/{vrender-animate/src/intreface => vrender-core/src/interface/animation}/animate.ts (93%) rename packages/{vrender-animate/src/intreface => vrender-core/src/interface/animation}/easing.ts (100%) create mode 100644 packages/vrender-core/src/interface/animation/index.ts rename packages/{vrender-animate/src/intreface => vrender-core/src/interface/animation}/ticker.ts (100%) rename packages/{vrender-animate/src/intreface => vrender-core/src/interface/animation}/timeline.ts (100%) rename packages/{vrender-animate/src/intreface => vrender-core/src/interface/animation}/type.ts (100%) diff --git a/packages/vrender-animate/src/animate-extension.ts b/packages/vrender-animate/src/animate-extension.ts index c23d0ddde..de0151d91 100644 --- a/packages/vrender-animate/src/animate-extension.ts +++ b/packages/vrender-animate/src/animate-extension.ts @@ -5,8 +5,7 @@ // 3. 重载Graphic的getAttributes方法,根据参数getAttributes(final = true)返回finalAttribute = {}; merge(finalAttribute, graphic.attribute, animatedAttribute), // animatedAttribute为所有动画的最终结果(loop为INFINITY的动画不算) -import type { IGraphicAnimateParams } from '@visactor/vrender-core'; -import type { IAnimate } from './intreface/animate'; +import type { IGraphicAnimateParams, IAnimate } from '@visactor/vrender-core'; import { Animate } from './animate'; import { DefaultTimeline, defaultTimeline } from './timeline'; import { DefaultTicker } from './ticker/default-ticker'; diff --git a/packages/vrender-animate/src/animate.ts b/packages/vrender-animate/src/animate.ts index a9944eebb..1c2937e24 100644 --- a/packages/vrender-animate/src/animate.ts +++ b/packages/vrender-animate/src/animate.ts @@ -1,9 +1,15 @@ -import type { IAnimate, IStep, ICustomAnimate } from './intreface/animate'; -import type { EasingType } from './intreface/easing'; -import { AnimateStatus, AnimateStepType } from './intreface/type'; import { Step, WaitStep } from './step'; -import type { ITimeline } from './intreface/timeline'; -import { Generator, type IGraphic } from '@visactor/vrender-core'; +import { + Generator, + AnimateStatus, + AnimateStepType, + type IGraphic, + type IAnimate, + type IStep, + type ICustomAnimate, + type EasingType, + type ITimeline +} from '@visactor/vrender-core'; import { defaultTimeline } from './timeline'; export class Animate implements IAnimate { diff --git a/packages/vrender-animate/src/component/component-animator.ts b/packages/vrender-animate/src/component/component-animator.ts index 157776c65..0434feb2c 100644 --- a/packages/vrender-animate/src/component/component-animator.ts +++ b/packages/vrender-animate/src/component/component-animator.ts @@ -1,5 +1,4 @@ -import type { IAnimate } from '../intreface/animate'; -import type { IGraphic } from '@visactor/vrender-core'; +import type { IGraphic, IAnimate } from '@visactor/vrender-core'; import { AnimateExecutor } from '../executor/animate-executor'; import type { IAnimationConfig } from '../executor/executor'; diff --git a/packages/vrender-animate/src/custom/clip-graphic.ts b/packages/vrender-animate/src/custom/clip-graphic.ts index f57f15de0..49c4d7386 100644 --- a/packages/vrender-animate/src/custom/clip-graphic.ts +++ b/packages/vrender-animate/src/custom/clip-graphic.ts @@ -1,7 +1,6 @@ import type { IArcGraphicAttribute, IGraphic, IGroup, IRectGraphicAttribute } from '@visactor/vrender-core'; -import { application, AttributeUpdateType } from '@visactor/vrender-core'; +import { application, AttributeUpdateType, type EasingType } from '@visactor/vrender-core'; import { ACustomAnimate } from './custom-animate'; -import type { EasingType } from '../intreface/easing'; export class ClipGraphicAnimate extends ACustomAnimate { private _group?: IGroup; diff --git a/packages/vrender-animate/src/custom/clip.ts b/packages/vrender-animate/src/custom/clip.ts index f3a25bc00..e29c3a351 100644 --- a/packages/vrender-animate/src/custom/clip.ts +++ b/packages/vrender-animate/src/custom/clip.ts @@ -1,4 +1,4 @@ -import type { EasingType } from '../intreface/easing'; +import type { EasingType } from '@visactor/vrender-core'; import { CommonIn, CommonOut } from './common'; export interface IScaleAnimationOptions { diff --git a/packages/vrender-animate/src/custom/common.ts b/packages/vrender-animate/src/custom/common.ts index f39b5c8a1..c735d0d16 100644 --- a/packages/vrender-animate/src/custom/common.ts +++ b/packages/vrender-animate/src/custom/common.ts @@ -1,5 +1,4 @@ -import type { IAnimate, IStep } from '../intreface/animate'; -import type { EasingType } from '../intreface/easing'; +import type { EasingType, IAnimate, IStep } from '@visactor/vrender-core'; import { ACustomAnimate } from './custom-animate'; export interface IScaleAnimationOptions { diff --git a/packages/vrender-animate/src/custom/custom-animate.ts b/packages/vrender-animate/src/custom/custom-animate.ts index 590d793bd..48dd53d1f 100644 --- a/packages/vrender-animate/src/custom/custom-animate.ts +++ b/packages/vrender-animate/src/custom/custom-animate.ts @@ -1,7 +1,5 @@ import type { ComponentAnimator } from '../component'; -import type { ICustomAnimate } from '../intreface/animate'; -import type { EasingType } from '../intreface/easing'; -import type { IAnimateStepType } from '../intreface/type'; +import type { EasingType, IAnimateStepType, ICustomAnimate } from '@visactor/vrender-core'; import { Step } from '../step'; export abstract class ACustomAnimate extends Step implements ICustomAnimate { diff --git a/packages/vrender-animate/src/custom/fade.ts b/packages/vrender-animate/src/custom/fade.ts index 83c237b72..a23513ad3 100644 --- a/packages/vrender-animate/src/custom/fade.ts +++ b/packages/vrender-animate/src/custom/fade.ts @@ -1,4 +1,4 @@ -import type { EasingType } from '../intreface/easing'; +import type { EasingType } from '@visactor/vrender-core'; import { CommonIn, CommonOut } from './common'; export interface IScaleAnimationOptions { diff --git a/packages/vrender-animate/src/custom/from-to.ts b/packages/vrender-animate/src/custom/from-to.ts index 26830669f..5eb08cb47 100644 --- a/packages/vrender-animate/src/custom/from-to.ts +++ b/packages/vrender-animate/src/custom/from-to.ts @@ -1,6 +1,5 @@ import type { EasingType } from '@visactor/vrender-core'; import { ACustomAnimate } from './custom-animate'; -import type { IAnimate, IStep } from '../intreface/animate'; export class FromTo extends ACustomAnimate> { declare valid: boolean; diff --git a/packages/vrender-animate/src/custom/growAngle.ts b/packages/vrender-animate/src/custom/growAngle.ts index 6c81502e7..bc4877a56 100644 --- a/packages/vrender-animate/src/custom/growAngle.ts +++ b/packages/vrender-animate/src/custom/growAngle.ts @@ -1,5 +1,4 @@ -import { type IGraphic, type IGroup } from '@visactor/vrender-core'; -import type { EasingType } from '../intreface/easing'; +import { type IGraphic, type IGroup, type EasingType } from '@visactor/vrender-core'; import { ACustomAnimate } from './custom-animate'; import { isNumber } from '@visactor/vutils'; diff --git a/packages/vrender-animate/src/custom/growCenter.ts b/packages/vrender-animate/src/custom/growCenter.ts index 0b1a593f0..7b10c05ca 100644 --- a/packages/vrender-animate/src/custom/growCenter.ts +++ b/packages/vrender-animate/src/custom/growCenter.ts @@ -1,7 +1,5 @@ -import type { IGraphic, IGroup } from '@visactor/vrender-core'; +import type { IGraphic, IGroup, IAnimate, IStep, EasingType } from '@visactor/vrender-core'; import { isValid } from '@visactor/vutils'; -import type { IAnimate, IStep } from '../intreface/animate'; -import type { EasingType } from '../intreface/easing'; import { ACustomAnimate } from './custom-animate'; interface IGrowCartesianAnimationOptions { diff --git a/packages/vrender-animate/src/custom/growHeight.ts b/packages/vrender-animate/src/custom/growHeight.ts index d557522cd..8b4185bc5 100644 --- a/packages/vrender-animate/src/custom/growHeight.ts +++ b/packages/vrender-animate/src/custom/growHeight.ts @@ -1,7 +1,5 @@ -import type { IGraphic, IGroup } from '@visactor/vrender-core'; +import type { IGraphic, IGroup, IAnimate, IStep, EasingType } from '@visactor/vrender-core'; import { isNil, isNumber, isValid } from '@visactor/vutils'; -import type { IAnimate, IStep } from '../intreface/animate'; -import type { EasingType } from '../intreface/easing'; import { ACustomAnimate } from './custom-animate'; interface IGrowCartesianAnimationOptions { diff --git a/packages/vrender-animate/src/custom/growPoints.ts b/packages/vrender-animate/src/custom/growPoints.ts index 213aeafaf..ffd76049a 100644 --- a/packages/vrender-animate/src/custom/growPoints.ts +++ b/packages/vrender-animate/src/custom/growPoints.ts @@ -1,8 +1,6 @@ -import { pointInterpolation, type IGraphic, type IGroup } from '@visactor/vrender-core'; +import { pointInterpolation, type IGraphic, type IGroup, type EasingType } from '@visactor/vrender-core'; import type { IPointLike } from '@visactor/vutils'; import { isValidNumber } from '@visactor/vutils'; -import type { IAnimate, IStep } from '../intreface/animate'; -import type { EasingType } from '../intreface/easing'; import { ACustomAnimate } from './custom-animate'; interface IAnimationParameters { diff --git a/packages/vrender-animate/src/custom/growRadius.ts b/packages/vrender-animate/src/custom/growRadius.ts index bf3beaf52..585161ab3 100644 --- a/packages/vrender-animate/src/custom/growRadius.ts +++ b/packages/vrender-animate/src/custom/growRadius.ts @@ -1,5 +1,4 @@ -import { type IGraphic, type IGroup } from '@visactor/vrender-core'; -import type { EasingType } from '../intreface/easing'; +import { type IGraphic, type IGroup, type EasingType } from '@visactor/vrender-core'; import { ACustomAnimate } from './custom-animate'; import { isNumber } from '@visactor/vutils'; diff --git a/packages/vrender-animate/src/custom/growWidth.ts b/packages/vrender-animate/src/custom/growWidth.ts index 411690098..2ccc85842 100644 --- a/packages/vrender-animate/src/custom/growWidth.ts +++ b/packages/vrender-animate/src/custom/growWidth.ts @@ -1,7 +1,5 @@ -import type { IGraphic, IGroup } from '@visactor/vrender-core'; +import type { IGraphic, IGroup, IAnimate, IStep, EasingType } from '@visactor/vrender-core'; import { isNil, isNumber, isValid } from '@visactor/vutils'; -import type { IAnimate, IStep } from '../intreface/animate'; -import type { EasingType } from '../intreface/easing'; import { ACustomAnimate } from './custom-animate'; interface IGrowCartesianAnimationOptions { diff --git a/packages/vrender-animate/src/custom/input-text.ts b/packages/vrender-animate/src/custom/input-text.ts index 3954a4b46..1e4ad70ff 100644 --- a/packages/vrender-animate/src/custom/input-text.ts +++ b/packages/vrender-animate/src/custom/input-text.ts @@ -1,5 +1,4 @@ -import type { IAnimate, IStep } from '../intreface/animate'; -import type { EasingType } from '../intreface/easing'; +import type { EasingType, IAnimate, IStep } from '@visactor/vrender-core'; import { ACustomAnimate } from './custom-animate'; /** diff --git a/packages/vrender-animate/src/custom/number.ts b/packages/vrender-animate/src/custom/number.ts index 18970efba..f68efd596 100644 --- a/packages/vrender-animate/src/custom/number.ts +++ b/packages/vrender-animate/src/custom/number.ts @@ -1,5 +1,4 @@ -import type { IAnimate, IStep } from '../intreface/animate'; -import type { EasingType } from '../intreface/easing'; +import type { EasingType, IAnimate, IStep } from '@visactor/vrender-core'; import { ACustomAnimate } from './custom-animate'; /** diff --git a/packages/vrender-animate/src/custom/richtext/input-richtext.ts b/packages/vrender-animate/src/custom/richtext/input-richtext.ts index bab12de41..b5bfe1528 100644 --- a/packages/vrender-animate/src/custom/richtext/input-richtext.ts +++ b/packages/vrender-animate/src/custom/richtext/input-richtext.ts @@ -1,7 +1,11 @@ -import type { IAnimate, IStep } from '../../intreface/animate'; -import type { EasingType } from '../../intreface/easing'; import { ACustomAnimate } from '../custom-animate'; -import type { IRichTextCharacter, IRichTextParagraphCharacter } from '@visactor/vrender-core'; +import type { + IRichTextCharacter, + IRichTextParagraphCharacter, + IAnimate, + IStep, + EasingType +} from '@visactor/vrender-core'; import { RichText } from '@visactor/vrender-core'; /** diff --git a/packages/vrender-animate/src/custom/richtext/output-richtext.ts b/packages/vrender-animate/src/custom/richtext/output-richtext.ts index 17843bd60..4a2aad98d 100644 --- a/packages/vrender-animate/src/custom/richtext/output-richtext.ts +++ b/packages/vrender-animate/src/custom/richtext/output-richtext.ts @@ -1,9 +1,6 @@ -import type { IAnimate, IStep } from '../../intreface/animate'; -import type { EasingType } from '../../intreface/easing'; import { ACustomAnimate } from '../custom-animate'; -import type { IRichTextCharacter, IRichTextParagraphCharacter } from '@visactor/vrender-core'; +import type { IRichTextCharacter, IAnimate, IStep, EasingType } from '@visactor/vrender-core'; import { RichText } from '@visactor/vrender-core'; -import { InputRichText } from './input-richtext'; /** * 富文本退出动画,实现类似打字机的字符逐个消失效果 diff --git a/packages/vrender-animate/src/custom/richtext/slide-out-richtext.ts b/packages/vrender-animate/src/custom/richtext/slide-out-richtext.ts index 0d746a3c6..56ff838be 100644 --- a/packages/vrender-animate/src/custom/richtext/slide-out-richtext.ts +++ b/packages/vrender-animate/src/custom/richtext/slide-out-richtext.ts @@ -1,7 +1,5 @@ -import type { IAnimate, IStep } from '../../intreface/animate'; -import type { EasingType } from '../../intreface/easing'; import { ACustomAnimate } from '../custom-animate'; -import type { IRichTextCharacter, IRichTextParagraphCharacter } from '@visactor/vrender-core'; +import type { IRichTextCharacter, IAnimate, IStep, EasingType } from '@visactor/vrender-core'; import { RichText } from '@visactor/vrender-core'; /** diff --git a/packages/vrender-animate/src/custom/richtext/slide-richtext.ts b/packages/vrender-animate/src/custom/richtext/slide-richtext.ts index 61991c9be..4652f310b 100644 --- a/packages/vrender-animate/src/custom/richtext/slide-richtext.ts +++ b/packages/vrender-animate/src/custom/richtext/slide-richtext.ts @@ -1,7 +1,5 @@ -import type { IAnimate, IStep } from '../../intreface/animate'; -import type { EasingType } from '../../intreface/easing'; import { ACustomAnimate } from '../custom-animate'; -import type { IRichTextCharacter, IRichTextParagraphCharacter } from '@visactor/vrender-core'; +import type { IRichTextCharacter, IAnimate, IStep, EasingType } from '@visactor/vrender-core'; import { RichText } from '@visactor/vrender-core'; /** diff --git a/packages/vrender-animate/src/custom/scale.ts b/packages/vrender-animate/src/custom/scale.ts index 9589c5ff5..b12f38a70 100644 --- a/packages/vrender-animate/src/custom/scale.ts +++ b/packages/vrender-animate/src/custom/scale.ts @@ -1,5 +1,4 @@ -import type { IAnimate, IStep } from '../intreface/animate'; -import type { EasingType } from '../intreface/easing'; +import type { EasingType, IAnimate, IStep } from '@visactor/vrender-core'; import { ACustomAnimate } from './custom-animate'; export interface IScaleAnimationOptions { diff --git a/packages/vrender-animate/src/custom/state.ts b/packages/vrender-animate/src/custom/state.ts index 785dd199f..de9eb340e 100644 --- a/packages/vrender-animate/src/custom/state.ts +++ b/packages/vrender-animate/src/custom/state.ts @@ -1,5 +1,4 @@ -import type { IAnimate, IStep } from '../intreface/animate'; -import type { EasingType } from '../intreface/easing'; +import type { EasingType, IAnimate, IStep } from '@visactor/vrender-core'; import { ACustomAnimate } from './custom-animate'; export interface IUpdateAnimationOptions { diff --git a/packages/vrender-animate/src/custom/story.ts b/packages/vrender-animate/src/custom/story.ts index 9e8bbd327..8aee4a64c 100644 --- a/packages/vrender-animate/src/custom/story.ts +++ b/packages/vrender-animate/src/custom/story.ts @@ -1,5 +1,5 @@ import { FadeIn } from './fade'; -import type { EasingType } from '../intreface/easing'; +import type { EasingType } from '@visactor/vrender-core'; import { ACustomAnimate } from './custom-animate'; import { AnimateExecutor } from '../executor/animate-executor'; diff --git a/packages/vrender-animate/src/custom/tag-points.ts b/packages/vrender-animate/src/custom/tag-points.ts index c11836220..180288efc 100644 --- a/packages/vrender-animate/src/custom/tag-points.ts +++ b/packages/vrender-animate/src/custom/tag-points.ts @@ -1,9 +1,7 @@ -import type { IPointLike } from '@visactor/vutils'; -import { clamp, isValidNumber, Point } from '@visactor/vutils'; -import { ACustomAnimate } from './custom-animate'; -import type { ISegment, ILineAttribute } from '@visactor/vrender-core'; +import { clamp, isValidNumber, Point, type IPointLike } from '@visactor/vutils'; +import type { ISegment, ILineAttribute, EasingType } from '@visactor/vrender-core'; import { pointInterpolation } from '@visactor/vrender-core'; -import type { EasingType } from '../intreface/easing'; +import { ACustomAnimate } from './custom-animate'; export class TagPointsUpdate extends ACustomAnimate<{ points?: IPointLike[]; segments?: ISegment[] }> { protected fromPoints: IPointLike[]; diff --git a/packages/vrender-animate/src/custom/update.ts b/packages/vrender-animate/src/custom/update.ts index e19683d08..59007716b 100644 --- a/packages/vrender-animate/src/custom/update.ts +++ b/packages/vrender-animate/src/custom/update.ts @@ -1,4 +1,4 @@ -import type { EasingType } from '../intreface/easing'; +import type { EasingType } from '@visactor/vrender-core'; import { ACustomAnimate } from './custom-animate'; export interface IUpdateAnimationOptions { diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index 02f9474e6..87f8cd0c3 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -1,4 +1,4 @@ -import type { IGraphic } from '@visactor/vrender-core'; +import type { IGraphic, EasingType, IAnimate } from '@visactor/vrender-core'; import type { IAnimationConfig, IAnimationTimeline, @@ -11,9 +11,7 @@ import type { IAnimationCustomConstructor, IAnimationChannelInterpolator } from './executor'; -import type { EasingType } from '../intreface/easing'; -import type { IAnimate } from '../intreface/animate'; -import { cloneDeep, isArray, isFunction } from '@visactor/vutils'; +import { isArray, isFunction } from '@visactor/vutils'; interface IAnimateExecutor { execute: (params: IAnimationConfig) => void; diff --git a/packages/vrender-animate/src/executor/executor.ts b/packages/vrender-animate/src/executor/executor.ts index cc595480b..e9a968325 100644 --- a/packages/vrender-animate/src/executor/executor.ts +++ b/packages/vrender-animate/src/executor/executor.ts @@ -1,5 +1,4 @@ -import type { IGraphic } from '@visactor/vrender-core'; -import type { EasingType } from '../intreface/easing'; +import type { IGraphic, EasingType } from '@visactor/vrender-core'; import type { ACustomAnimate } from '../custom/custom-animate'; export type MarkFunctionCallback = (datum: any, graphic: IGraphic, parameters: any) => T; diff --git a/packages/vrender-animate/src/index.ts b/packages/vrender-animate/src/index.ts index e9e5c0ff0..fd8170b1a 100644 --- a/packages/vrender-animate/src/index.ts +++ b/packages/vrender-animate/src/index.ts @@ -1,15 +1,9 @@ -// 导出接口 -export * from './intreface/animate'; -export * from './intreface/timeline'; -export * from './intreface/easing'; -export * from './intreface/type'; - // 导出实现 export { Animate } from './animate'; export { DefaultTimeline } from './timeline'; export { ManualTicker } from './ticker/manual-ticker'; export { DefaultTicker } from './ticker/default-ticker'; -export { Step } from './step'; +export { Step as AnimateStep } from './step'; // 导出工具函数 export * from './utils/easing-func'; diff --git a/packages/vrender-animate/src/interpolate/store.ts b/packages/vrender-animate/src/interpolate/store.ts index c2e08542d..a17451169 100644 --- a/packages/vrender-animate/src/interpolate/store.ts +++ b/packages/vrender-animate/src/interpolate/store.ts @@ -1,7 +1,6 @@ -import type { IGraphic } from '@visactor/vrender-core'; +import type { IGraphic, IStep } from '@visactor/vrender-core'; import { interpolateColor, interpolatePureColorArrayToStr, pointsInterpolation } from '@visactor/vrender-core'; import { interpolateNumber } from './number'; -import type { IStep } from '../intreface/animate'; import type { IPointLike } from '@visactor/vutils'; // 直接设置,触发 隐藏类(Hidden Class)优化: diff --git a/packages/vrender-animate/src/step.ts b/packages/vrender-animate/src/step.ts index 63c211f35..a28e97005 100644 --- a/packages/vrender-animate/src/step.ts +++ b/packages/vrender-animate/src/step.ts @@ -1,7 +1,14 @@ -import { ColorStore, ColorType, Generator, type IGraphic } from '@visactor/vrender-core'; -import type { IAnimate, IStep } from './intreface/animate'; -import type { EasingType, EasingTypeFunc } from './intreface/easing'; -import type { IAnimateStepType } from './intreface/type'; +import { + ColorStore, + ColorType, + Generator, + type IGraphic, + type IAnimate, + type IStep, + type EasingType, + type EasingTypeFunc, + type IAnimateStepType +} from '@visactor/vrender-core'; import { Easing } from './utils/easing'; import { commonInterpolateUpdate, interpolateUpdateStore } from './interpolate/store'; import { isString } from '@visactor/vutils'; diff --git a/packages/vrender-animate/src/ticker/default-ticker.ts b/packages/vrender-animate/src/ticker/default-ticker.ts index 30af3abd7..e277bd26e 100644 --- a/packages/vrender-animate/src/ticker/default-ticker.ts +++ b/packages/vrender-animate/src/ticker/default-ticker.ts @@ -1,8 +1,6 @@ import { EventEmitter } from '@visactor/vutils'; -import type { IStage } from '@visactor/vrender-core'; -import { application, PerformanceRAF } from '@visactor/vrender-core'; -import { type ITickHandler, type ITicker, STATUS } from '../intreface/ticker'; -import type { ITimeline } from '../intreface/timeline'; +import type { IStage, ITimeline } from '@visactor/vrender-core'; +import { application, PerformanceRAF, type ITickHandler, type ITicker, STATUS } from '@visactor/vrender-core'; const performanceRAF = new PerformanceRAF(); diff --git a/packages/vrender-animate/src/ticker/manual-ticker.ts b/packages/vrender-animate/src/ticker/manual-ticker.ts index 97fda967b..78fa9d106 100644 --- a/packages/vrender-animate/src/ticker/manual-ticker.ts +++ b/packages/vrender-animate/src/ticker/manual-ticker.ts @@ -1,5 +1,4 @@ -import type { ITickHandler } from '../intreface/ticker'; -import { type ITicker } from '../intreface/ticker'; +import type { ITickHandler, ITicker } from '@visactor/vrender-core'; import { DefaultTicker } from './default-ticker'; class ManualTickHandler implements ITickHandler { diff --git a/packages/vrender-animate/src/timeline.ts b/packages/vrender-animate/src/timeline.ts index 913e7bcb4..f3ae332f2 100644 --- a/packages/vrender-animate/src/timeline.ts +++ b/packages/vrender-animate/src/timeline.ts @@ -1,7 +1,4 @@ -import { Generator } from '@visactor/vrender-core'; -import type { IAnimate } from './intreface/animate'; -import type { ITimeline } from './intreface/timeline'; -import { AnimateStatus } from './intreface/type'; +import { Generator, type IAnimate, type ITimeline, AnimateStatus } from '@visactor/vrender-core'; export class DefaultTimeline implements ITimeline { declare id: number; diff --git a/packages/vrender-components/src/axis/animate/group-transition.ts b/packages/vrender-components/src/axis/animate/group-transition.ts index 8425c9e76..ade892b17 100644 --- a/packages/vrender-components/src/axis/animate/group-transition.ts +++ b/packages/vrender-components/src/axis/animate/group-transition.ts @@ -1,5 +1,6 @@ +import { AnimateMode } from '@visactor/vrender-core'; import type { EasingType, IGraphic, IGroup } from '@visactor/vrender-core'; -import { ACustomAnimate, AnimateMode } from '@visactor/vrender-animate'; +import { ACustomAnimate } from '@visactor/vrender-animate'; import type { Dict } from '@visactor/vutils'; import { cloneDeep, interpolateString, isEqual, isValidNumber } from '@visactor/vutils'; import { traverseGroup } from '../../util'; diff --git a/packages/vrender-components/src/weather/weather-box.ts b/packages/vrender-components/src/weather/weather-box.ts index 2811e4e66..ffe7ef2e1 100644 --- a/packages/vrender-components/src/weather/weather-box.ts +++ b/packages/vrender-components/src/weather/weather-box.ts @@ -2,8 +2,8 @@ import { AbstractComponent } from '../core/base'; import type { IWeatherBoxAttrs } from './type'; import type { ComponentOptions } from '../interface'; import { merge } from '@visactor/vutils'; -import type { IGroup, ISymbol } from '@visactor/vrender-core'; -import { Animate, DefaultTimeline, type ITimeline } from '@visactor/vrender-animate'; +import type { IGroup, ISymbol, ITimeline } from '@visactor/vrender-core'; +import { Animate, DefaultTimeline } from '@visactor/vrender-animate'; // todo 后续可能做成有随机数种子的伪随机,这样可以保证每次都生成一样的随机数 function random() { diff --git a/packages/vrender-core/src/common/enums.ts b/packages/vrender-core/src/common/enums.ts index c05c743fb..4b1ff531c 100644 --- a/packages/vrender-core/src/common/enums.ts +++ b/packages/vrender-core/src/common/enums.ts @@ -41,13 +41,6 @@ export enum AttributeUpdateType { ROTATE_TO = 25 } -export enum AnimateStepType { - 'wait' = 'wait', - 'from' = 'from', - 'to' = 'to', - 'customAnimate' = 'customAnimate' -} - export enum Direction { ROW = 1, COLUMN = 2 diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index 537b337d5..16d938490 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -27,7 +27,6 @@ import type { import { Node } from './node-tree'; import type { IAnimate, - IAnimateConstructor, IAnimateTarget, IGlyphGraphicAttribute, IGroup, @@ -36,10 +35,7 @@ import type { IShadowRoot, IStage, IStep, - ISubAnimate, - ISymbolClass, - ITickerConstructor, - ITimelineConstructor + ISymbolClass } from '../interface'; import { EventTarget, CustomEvent } from '../event'; import { DefaultTransform } from './config'; @@ -55,7 +51,6 @@ import { builtinSymbolsMap, builtInSymbolStrMap, CustomSymbolClass } from './bui import { isSvg, XMLParser } from '../common/xml'; import { SVG_PARSE_ATTRIBUTE_MAP, SVG_PARSE_ATTRIBUTE_MAP_KEYS } from './constants'; import { DefaultStateAnimateConfig } from '../animate/config'; -import type { ITimeline } from '../interface/animate'; const _tempBounds = new AABBBounds(); /** @@ -364,7 +359,7 @@ export abstract class Graphic = Partial = Partial, - step: IStep, - ratio: number, - end: boolean, - nextProps: Record, - lastProps?: Record, - nextParsedProps?: any, - propKeys?: string[] - ) { - if (!propKeys) { - propKeys = Object.keys(nextProps); - step.propKeys = propKeys; - } - if (end) { - step.propKeys.forEach(key => { - if (!animate.validAttr(key)) { - return; - } - nextAttributes[key] = nextProps[key]; - }); - } else { - propKeys.forEach(key => { - // 如果属性不合法,那直接return - if (!animate.validAttr(key)) { - return; - } - const nextStepVal = nextProps[key]; - const lastStepVal = (lastProps && lastProps[key]) ?? subAnimate.getLastPropByName(key, step); - if (nextStepVal == null || lastStepVal == null || nextStepVal === lastStepVal) { - // 用户直接调用stepInterpolate可能会走进来,如果传入的参数出现null或者undefined,直接赋值最终的值 - nextAttributes[key] = nextStepVal; - return; - } - let match: boolean; - match = - animate.interpolateFunc && animate.interpolateFunc(key, ratio, lastStepVal, nextStepVal, nextAttributes); - if (match) { - return; - } - match = animate.customInterpolate(key, ratio, lastStepVal, nextStepVal, this, nextAttributes); - if (match) { - return; - } - if (!this.defaultInterpolate(nextStepVal, lastStepVal, key, nextAttributes, nextParsedProps, ratio)) { - this._interpolate(key, ratio, lastStepVal, nextStepVal, nextAttributes); - } - }); - } - - step.parsedProps = nextParsedProps; - } - - defaultInterpolate( - nextStepVal: any, - lastStepVal: any, - key: string, - nextAttributes: Record, - nextParsedProps: any, - ratio: number - ) { - if (Number.isFinite(nextStepVal) && Number.isFinite(lastStepVal)) { - nextAttributes[key] = lastStepVal + (nextStepVal - lastStepVal) * ratio; - return true; - } else if (key === 'fill') { - if (!nextParsedProps) { - nextParsedProps = {}; - } - // 保存解析的结果到step - const fillColorArray: [number, number, number, number] = nextParsedProps.fillColorArray; - const color = interpolateColor(lastStepVal, fillColorArray ?? nextStepVal, ratio, false, (fArray, tArray) => { - nextParsedProps.fillColorArray = tArray; - }); - if (color) { - nextAttributes[key] = color; - } - return true; - } else if (key === 'stroke') { - if (!nextParsedProps) { - nextParsedProps = {}; - } - // 保存解析的结果到step - const strokeColorArray: [number, number, number, number] = nextParsedProps.strokeColorArray; - const color = interpolateColor(lastStepVal, strokeColorArray ?? nextStepVal, ratio, false, (fArray, tArray) => { - nextParsedProps.strokeColorArray = tArray; - }); - if (color) { - nextAttributes[key] = color; - } - return true; - } else if (key === 'shadowColor') { - if (!nextParsedProps) { - nextParsedProps = {}; - } - // 保存解析的结果到step - const shadowColorArray: [number, number, number, number] = nextParsedProps.shadowColorArray; - const color = interpolateColor(lastStepVal, shadowColorArray ?? nextStepVal, ratio, true, (fArray, tArray) => { - nextParsedProps.shadowColorArray = tArray; - }); - if (color) { - nextAttributes[key] = color; - } - - return true; - } else if (Array.isArray(nextStepVal) && nextStepVal.length === lastStepVal.length) { - const nextList = []; - let valid = true; - for (let i = 0; i < nextStepVal.length; i++) { - const v = lastStepVal[i]; - const val = v + (nextStepVal[i] - v) * ratio; - if (!Number.isFinite(val)) { - valid = false; - break; - } - nextList.push(val); - } - if (valid) { - nextAttributes[key] = nextList; - } - } - - return false; - } - - protected _interpolate(key: string, ratio: number, lastStepVal: any, nextStepVal: any, nextAttributes: any) { - return; - } getDefaultAttribute(name: string) { return (this.getGraphicTheme() as any)[name]; diff --git a/packages/vrender-core/src/interface/animate.ts b/packages/vrender-core/src/interface/animate.ts index 9fabf1a28..e2d978971 100644 --- a/packages/vrender-core/src/interface/animate.ts +++ b/packages/vrender-core/src/interface/animate.ts @@ -1,380 +1,363 @@ -import type { EventEmitter } from '@visactor/vutils'; -import type { AnimateStepType } from '../common/enums'; -import type { Releaseable } from './common'; -import type { IGraphic } from './graphic'; - -enum AnimateStatus { - INITIAL = 0, - RUNNING = 1, - PAUSED = 2, - END = 3 -} - -enum AnimateMode { - NORMAL = 0b0000, - SET_ATTR_IMMEDIATELY = 0b0001 -} - -// export type EasingType = (...args: any) => any; - -// export declare class Easing { -// static linear(t: number): number; -// static none(): typeof Easing.linear; +// import type { EventEmitter } from '@visactor/vutils'; +// import type { AnimateStepType } from '../common/enums'; +// import type { Releaseable } from './common'; +// import type { IGraphic } from './graphic'; + +// enum AnimateStatus { +// INITIAL = 0, +// RUNNING = 1, +// PAUSED = 2, +// END = 3 +// } + +// enum AnimateMode { +// NORMAL = 0b0000, +// SET_ATTR_IMMEDIATELY = 0b0001 +// } + +// // export type EasingType = (...args: any) => any; + +// // export declare class Easing { +// // static linear(t: number): number; +// // static none(): typeof Easing.linear; +// // /** +// // * 获取缓动函数,amount指示这个缓动函数的插值方式 +// // * @param amount +// // * @returns +// // */ +// // static get(amount: number): (t: number) => number; +// // static getPowIn(pow: number): (t: number) => number; +// // static getPowOut(pow: number): (t: number) => number; +// // static getPowInOut(pow: number): (t: number) => number; +// // static quadIn: (t: number) => number; +// // static quadOut: (t: number) => number; +// // static quadInOut: (t: number) => number; +// // static cubicIn: (t: number) => number; +// // static cubicOut: (t: number) => number; +// // static cubicInOut: (t: number) => number; +// // static quartIn: (t: number) => number; +// // static quartOut: (t: number) => number; +// // static quartInOut: (t: number) => number; +// // static quintIn: (t: number) => number; +// // static quintOut: (t: number) => number; +// // static quintInOut: (t: number) => number; +// // static getBackIn(amount: number): (t: number) => number; +// // static getBackOut(amount: number): (t: number) => number; +// // static getBackInOut(amount: number): (t: number) => number; +// // static backIn: (t: number) => number; +// // static backOut: (t: number) => number; +// // static backInOut: (t: number) => number; +// // static circIn(t: number): number; +// // static circOut(t: number): number; +// // static circInOut(t: number): number; +// // static bounceOut(t: number): number; +// // static bounceIn(t: number): number; +// // static bounceInOut(t: number): number; +// // static getElasticIn(amplitude: number, period: number): (t: number) => number; +// // static getElasticOut(amplitude: number, period: number): (t: number) => number; +// // static getElasticInOut(amplitude: number, period: number): (t: number) => number; +// // static elasticIn: (t: number) => number; +// // static elasticOut: (t: number) => number; +// // static elasticInOut: (t: number) => number; +// // } + +// // timeline管理一堆的animate,多个timeline互不影响 +// // timeline主要作用是基于layer层面的整体管理 +// // 每个layer默认带有一个timeline +// export interface Timeline { +// AnimateList: IAnimate[]; +// } + +// type IStopType = 'end' | 'start' | 'current'; + +// // TODO: 提供options配置可序列化 +// interface AnimateSpecItem { +// type: 'to' | 'delay' | 'stop' | 'any'; +// params: any[]; +// } + +// export type EasingTypeStr = +// | 'linear' +// | 'quadIn' +// | 'quadOut' +// | 'quadInOut' +// | 'quadInOut' +// | 'cubicIn' +// | 'cubicOut' +// | 'cubicInOut' +// | 'quartIn' +// | 'quartOut' +// | 'quartInOut' +// | 'quintIn' +// | 'quintOut' +// | 'quintInOut' +// | 'backIn' +// | 'backOut' +// | 'backInOut' +// | 'circIn' +// | 'circOut' +// | 'circInOut' +// | 'bounceOut' +// | 'bounceIn' +// | 'bounceInOut' +// | 'elasticIn' +// | 'elasticOut' +// | 'elasticInOut' +// | 'sineIn' +// | 'sineOut' +// | 'sineInOut' +// | 'expoIn' +// | 'expoOut' +// | 'expoInOut' +// // @since 0.21.0 +// | 'easeInOutQuad' +// | 'easeOutElastic' +// | 'easeInOutElastic' +// | ''; +// export type EasingTypeFunc = (t: number) => number; + +// export type EasingType = EasingTypeStr | EasingTypeFunc; + +// export type IAnimateStepType = keyof typeof AnimateStepType; + +// export interface IStep { +// type: IAnimateStepType; +// prev?: IStep; +// // 持续时间 +// duration: number; +// // 在animate中的位置 +// position: number; +// next?: IStep; +// props?: any; +// parsedProps?: any; +// propKeys?: string[]; +// easing?: EasingTypeFunc; +// customAnimate?: ICustomAnimate; + +// append: (step: IStep) => void; +// getLastProps: () => any; +// } + +// export interface IStepConfig { +// tempProps?: boolean; // props为临时props,可以直接使用不用拷贝 +// noPreventAttrs?: boolean; +// } + +// export interface IAnimateTarget { +// onAnimateBind?: (animte: IAnimate | ISubAnimate) => void; +// // 获取属性 +// getComputedAttribute: (name: string) => any; +// // 获取默认属性 +// getDefaultAttribute: (name: string) => any; +// onStop: (props?: Record) => void; +// animates: Map; +// [key: string]: any; +// } + +// export interface ICustomAnimate { +// duration: number; +// easing: EasingType; +// step?: IStep; +// mode?: AnimateMode; + +// bind: (target: IAnimateTarget, subAni: ISubAnimate) => void; +// // 在第一次调用的时候触发 +// onBind: () => void; +// // 第一次执行的时候调用 +// onFirstRun: () => void; +// // 开始执行的时候调用(如果有循环,那每个周期都会调用) +// onStart: () => void; +// // 结束执行的时候调用(如果有循环,那每个周期都会调用) +// onEnd: () => void; +// onUpdate: (end: boolean, ratio: number, out: Record) => void; +// update: (end: boolean, ratio: number, out: Record) => void; +// getEndProps: () => Record | void; +// getFromProps: () => Record | void; +// getMergedEndProps: () => Record | void; +// } + +// export type IAnimateConstructor = new (...args: any[]) => IAnimate; + +// // 每一个animate绑定一个graphic,用于描述这个graphic的动画内容 +// // 在timeline层面,animate相当于是一段timeslice +// export interface IAnimate { +// readonly id: string | number; +// status: AnimateStatus; + +// interpolateFunc: (key: string, ratio: number, from: any, to: any, nextAttributes: any) => boolean; + +// _onStart?: (() => void)[]; +// _onFrame?: ((step: IStep, ratio: number) => void)[]; +// _onEnd?: (() => void)[]; +// _onRemove?: (() => void)[]; + +// getStartProps: () => Record; +// getEndProps: () => Record; + +// setTimeline: (timeline: ITimeline) => void; +// // getTimeline: () => ITimeline; +// readonly timeline: ITimeline; + +// bind: (target: IAnimateTarget) => this; +// to: (props: Record, duration: number, easing: EasingType, params?: IStepConfig) => this; +// from: (props: Record, duration: number, easing: EasingType, params?: IStepConfig) => this; +// pause: () => void; +// resume: () => void; +// onStart: (cb: () => void) => void; +// onEnd: (cb: () => void) => void; +// onFrame: (cb: (step: IStep, ratio: number) => void) => void; +// onRemove: (cb: () => void) => void; +// // 屏蔽属性 +// preventAttr: (key: string) => void; +// // 屏蔽属性 +// preventAttrs: (key: string[]) => void; +// // 属性是否合法 +// validAttr: (key: string) => boolean; + +// runCb: (cb: (a: IAnimate, step: IStep) => void) => IAnimate; + +// // 自定义插值,返回false表示没有匹配上 +// customInterpolate: ( +// key: string, +// ratio: number, +// from: any, +// to: any, +// target: IAnimateTarget, +// ret: Record +// ) => boolean; +// // +// play: (customAnimate: ICustomAnimate) => this; + +// // 获取该属性的上一个值 +// // getLastPropByName: (name: string, step: IStep) => any; +// // delay: (duration: number) => IAnimate; +// stop: (type?: 'start' | 'end' | Record) => void; +// /** 打上END标志,下一帧被删除 */ +// release: () => void; +// // 获取持续的时长 +// getDuration: () => number; +// // 获取动画开始时间(注意并不是子动画的startAt) +// getStartTime: () => number; +// // done: (cb: (_: any) => any) => IAnimate; +// // pause: () => IAnimate; +// // spec: (spec: AnimateSpecItem[]) => IAnimate; +// // start: () => void; // 有start方法,避免动画提前开始(VGrammar需要时间处理数据) +// wait: (delay: number) => this; + +// // // 编排 +// afterAll: (list: IAnimate[]) => this; +// after: (animate: IAnimate) => this; +// parallel: (animate: IAnimate) => this; + +// // // timislice (getter) +// // startTime: number; +// // endTime: number; +// // startTimes: number[]; +// // endTimes: number[]; + +// // // 高级参数,frame到frameEnd之间可以进行reverse,loop,bounce效果 +// // frame: () => IAnimate; +// // frameEnd: () => IAnimate; +// reversed: (r: boolean) => IAnimate; +// loop: (n: number) => IAnimate; +// bounce: (b: boolean) => IAnimate; + +// nextAnimate?: IAnimate; +// prevAnimate?: IAnimate; + +// advance: (delta: number) => void; + +// startAt: (t: number) => IAnimate; + +// // // 语法糖 +// // create: (duration: number) => IAnimate; +// // fadeIn: (duration: number) => IAnimate; +// } + +// export interface ISubAnimate { +// getLastStep: () => IStep; +// animate: IAnimate; +// // 获取该属性的上一个值 +// getLastPropByName: (name: string, step: IStep) => any; +// } + +// // rect.animate().abc().to({}, 1000).delay(1000).frame().to().delay().to().frameEnd().loop().bounce() + +// export interface BaseAnimateConfig { +// id?: number | string; +// interpolate?: (key: string, ratio: number, from: any, to: any, nextAttributes: any) => boolean; +// onStart?: () => void; +// onFrame?: (step: IStep, ratio: number) => void; +// onEnd?: () => void; +// onRemove?: () => void; +// } + +// // VGrammar和 vrender命名不一致,好尴尬 +// export interface MorphingAnimateConfig extends Omit { +// duration?: number; +// easing?: EasingType; // 统一到easing +// delay?: number; +// } + +// export interface MultiMorphingAnimateConfig extends MorphingAnimateConfig { +// splitPath?: 'clone' | ((graphic: IGraphic, count: number, needAppend?: boolean) => IGraphic[]); +// individualDelay?: (index: number, count: number, fromGraphic: IGraphic, toGraphic: IGraphic) => number; +// } + +// export interface ITimeline { +// id: number; +// animateCount: number; +// isGlobal: boolean; +// addAnimate: (animate: IAnimate) => void; +// removeAnimate: (animate: IAnimate, release?: boolean) => void; +// tick: (delta: number) => void; +// clear: () => void; +// pause: () => void; +// resume: () => void; +// } + +// export type ITimelineConstructor = new (...args: any[]) => ITimeline; + +// export type ITickerConstructor = new (...args: any[]) => ITicker; + +// export interface ITickHandler extends Releaseable { +// avaliable: () => boolean; // /** -// * 获取缓动函数,amount指示这个缓动函数的插值方式 -// * @param amount -// * @returns +// * 开始执行tick +// * @param interval 延时 ms +// * @param cb 执行的回调 // */ -// static get(amount: number): (t: number) => number; -// static getPowIn(pow: number): (t: number) => number; -// static getPowOut(pow: number): (t: number) => number; -// static getPowInOut(pow: number): (t: number) => number; -// static quadIn: (t: number) => number; -// static quadOut: (t: number) => number; -// static quadInOut: (t: number) => number; -// static cubicIn: (t: number) => number; -// static cubicOut: (t: number) => number; -// static cubicInOut: (t: number) => number; -// static quartIn: (t: number) => number; -// static quartOut: (t: number) => number; -// static quartInOut: (t: number) => number; -// static quintIn: (t: number) => number; -// static quintOut: (t: number) => number; -// static quintInOut: (t: number) => number; -// static getBackIn(amount: number): (t: number) => number; -// static getBackOut(amount: number): (t: number) => number; -// static getBackInOut(amount: number): (t: number) => number; -// static backIn: (t: number) => number; -// static backOut: (t: number) => number; -// static backInOut: (t: number) => number; -// static circIn(t: number): number; -// static circOut(t: number): number; -// static circInOut(t: number): number; -// static bounceOut(t: number): number; -// static bounceIn(t: number): number; -// static bounceInOut(t: number): number; -// static getElasticIn(amplitude: number, period: number): (t: number) => number; -// static getElasticOut(amplitude: number, period: number): (t: number) => number; -// static getElasticInOut(amplitude: number, period: number): (t: number) => number; -// static elasticIn: (t: number) => number; -// static elasticOut: (t: number) => number; -// static elasticInOut: (t: number) => number; +// tick: (interval: number, cb: (handler: ITickHandler) => void) => void; // 开始 +// tickTo?: (t: number, cb: (handler: ITickHandler, params?: { once: boolean }) => void) => void; +// getTime: () => number; // 获取时间 // } -// timeline管理一堆的animate,多个timeline互不影响 -// timeline主要作用是基于layer层面的整体管理 -// 每个layer默认带有一个timeline -export interface Timeline { - AnimateList: IAnimate[]; -} - -type IStopType = 'end' | 'start' | 'current'; - -// TODO: 提供options配置可序列化 -interface AnimateSpecItem { - type: 'to' | 'delay' | 'stop' | 'any'; - params: any[]; -} - -export type EasingTypeStr = - | 'linear' - | 'quadIn' - | 'quadOut' - | 'quadInOut' - | 'quadInOut' - | 'cubicIn' - | 'cubicOut' - | 'cubicInOut' - | 'quartIn' - | 'quartOut' - | 'quartInOut' - | 'quintIn' - | 'quintOut' - | 'quintInOut' - | 'backIn' - | 'backOut' - | 'backInOut' - | 'circIn' - | 'circOut' - | 'circInOut' - | 'bounceOut' - | 'bounceIn' - | 'bounceInOut' - | 'elasticIn' - | 'elasticOut' - | 'elasticInOut' - | 'sineIn' - | 'sineOut' - | 'sineInOut' - | 'expoIn' - | 'expoOut' - | 'expoInOut' - // @since 0.21.0 - | 'easeInOutQuad' - | 'easeOutElastic' - | 'easeInOutElastic' - | ''; -export type EasingTypeFunc = (t: number) => number; - -export type EasingType = EasingTypeStr | EasingTypeFunc; - -export type IAnimateStepType = keyof typeof AnimateStepType; - -export interface IStep { - type: IAnimateStepType; - prev?: IStep; - // 持续时间 - duration: number; - // 在animate中的位置 - position: number; - next?: IStep; - props?: any; - parsedProps?: any; - propKeys?: string[]; - easing?: EasingTypeFunc; - customAnimate?: ICustomAnimate; - - append: (step: IStep) => void; - getLastProps: () => any; -} - -export interface IStepConfig { - tempProps?: boolean; // props为临时props,可以直接使用不用拷贝 - noPreventAttrs?: boolean; -} - -export interface IAnimateTarget { - onAnimateBind?: (animte: IAnimate | ISubAnimate) => void; - // 添加动画step的时候调用 - onAddStep?: (step: IStep) => void; - // step时调用 - onStep: (subAnimate: ISubAnimate, animate: IAnimate, step: IStep, ratio: number, end: boolean) => void; - // 插值函数 - stepInterpolate: ( - subAnimate: ISubAnimate, - animate: IAnimate, - nextAttributes: Record, - step: IStep, - ratio: number, - end: boolean, - nextProps: Record, - lastProps?: Record, - nextParsedProps?: any, - propKeys?: string[] - ) => void; - // 获取属性 - getComputedAttribute: (name: string) => any; - // 获取默认属性 - getDefaultAttribute: (name: string) => any; - onStop: (props?: Record) => void; - animates: Map; - [key: string]: any; -} - -export interface ICustomAnimate { - duration: number; - easing: EasingType; - step?: IStep; - mode?: AnimateMode; - - bind: (target: IAnimateTarget, subAni: ISubAnimate) => void; - // 在第一次调用的时候触发 - onBind: () => void; - // 第一次执行的时候调用 - onFirstRun: () => void; - // 开始执行的时候调用(如果有循环,那每个周期都会调用) - onStart: () => void; - // 结束执行的时候调用(如果有循环,那每个周期都会调用) - onEnd: () => void; - onUpdate: (end: boolean, ratio: number, out: Record) => void; - update: (end: boolean, ratio: number, out: Record) => void; - getEndProps: () => Record | void; - getFromProps: () => Record | void; - getMergedEndProps: () => Record | void; -} - -export type IAnimateConstructor = new (...args: any[]) => IAnimate; - -// 每一个animate绑定一个graphic,用于描述这个graphic的动画内容 -// 在timeline层面,animate相当于是一段timeslice -export interface IAnimate { - readonly id: string | number; - status: AnimateStatus; - - interpolateFunc: (key: string, ratio: number, from: any, to: any, nextAttributes: any) => boolean; - - _onStart?: (() => void)[]; - _onFrame?: ((step: IStep, ratio: number) => void)[]; - _onEnd?: (() => void)[]; - _onRemove?: (() => void)[]; - - getStartProps: () => Record; - getEndProps: () => Record; - - setTimeline: (timeline: ITimeline) => void; - // getTimeline: () => ITimeline; - readonly timeline: ITimeline; - - bind: (target: IAnimateTarget) => this; - to: (props: Record, duration: number, easing: EasingType, params?: IStepConfig) => this; - from: (props: Record, duration: number, easing: EasingType, params?: IStepConfig) => this; - pause: () => void; - resume: () => void; - onStart: (cb: () => void) => void; - onEnd: (cb: () => void) => void; - onFrame: (cb: (step: IStep, ratio: number) => void) => void; - onRemove: (cb: () => void) => void; - // 屏蔽属性 - preventAttr: (key: string) => void; - // 屏蔽属性 - preventAttrs: (key: string[]) => void; - // 属性是否合法 - validAttr: (key: string) => boolean; - - runCb: (cb: (a: IAnimate, step: IStep) => void) => IAnimate; - - // 自定义插值,返回false表示没有匹配上 - customInterpolate: ( - key: string, - ratio: number, - from: any, - to: any, - target: IAnimateTarget, - ret: Record - ) => boolean; - // - play: (customAnimate: ICustomAnimate) => this; - - // 获取该属性的上一个值 - // getLastPropByName: (name: string, step: IStep) => any; - // delay: (duration: number) => IAnimate; - stop: (type?: 'start' | 'end' | Record) => void; - /** 打上END标志,下一帧被删除 */ - release: () => void; - // 获取持续的时长 - getDuration: () => number; - // 获取动画开始时间(注意并不是子动画的startAt) - getStartTime: () => number; - // done: (cb: (_: any) => any) => IAnimate; - // pause: () => IAnimate; - // spec: (spec: AnimateSpecItem[]) => IAnimate; - // start: () => void; // 有start方法,避免动画提前开始(VGrammar需要时间处理数据) - wait: (delay: number) => this; - - // // 编排 - afterAll: (list: IAnimate[]) => this; - after: (animate: IAnimate) => this; - parallel: (animate: IAnimate) => this; - - // // timislice (getter) - // startTime: number; - // endTime: number; - // startTimes: number[]; - // endTimes: number[]; - - // // 高级参数,frame到frameEnd之间可以进行reverse,loop,bounce效果 - // frame: () => IAnimate; - // frameEnd: () => IAnimate; - reversed: (r: boolean) => IAnimate; - loop: (n: number) => IAnimate; - bounce: (b: boolean) => IAnimate; - - nextAnimate?: IAnimate; - prevAnimate?: IAnimate; - - advance: (delta: number) => void; - - startAt: (t: number) => IAnimate; - - // // 语法糖 - // create: (duration: number) => IAnimate; - // fadeIn: (duration: number) => IAnimate; -} - -export interface ISubAnimate { - getLastStep: () => IStep; - animate: IAnimate; - // 获取该属性的上一个值 - getLastPropByName: (name: string, step: IStep) => any; -} - -// rect.animate().abc().to({}, 1000).delay(1000).frame().to().delay().to().frameEnd().loop().bounce() - -export interface BaseAnimateConfig { - id?: number | string; - interpolate?: (key: string, ratio: number, from: any, to: any, nextAttributes: any) => boolean; - onStart?: () => void; - onFrame?: (step: IStep, ratio: number) => void; - onEnd?: () => void; - onRemove?: () => void; -} - -// VGrammar和 vrender命名不一致,好尴尬 -export interface MorphingAnimateConfig extends Omit { - duration?: number; - easing?: EasingType; // 统一到easing - delay?: number; -} - -export interface MultiMorphingAnimateConfig extends MorphingAnimateConfig { - splitPath?: 'clone' | ((graphic: IGraphic, count: number, needAppend?: boolean) => IGraphic[]); - individualDelay?: (index: number, count: number, fromGraphic: IGraphic, toGraphic: IGraphic) => number; -} - -export interface ITimeline { - id: number; - animateCount: number; - isGlobal: boolean; - addAnimate: (animate: IAnimate) => void; - removeAnimate: (animate: IAnimate, release?: boolean) => void; - tick: (delta: number) => void; - clear: () => void; - pause: () => void; - resume: () => void; -} - -export type ITimelineConstructor = new (...args: any[]) => ITimeline; - -export type ITickerConstructor = new (...args: any[]) => ITicker; - -export interface ITickHandler extends Releaseable { - avaliable: () => boolean; - /** - * 开始执行tick - * @param interval 延时 ms - * @param cb 执行的回调 - */ - tick: (interval: number, cb: (handler: ITickHandler) => void) => void; // 开始 - tickTo?: (t: number, cb: (handler: ITickHandler, params?: { once: boolean }) => void) => void; - getTime: () => number; // 获取时间 -} - -export interface ITickerHandlerStatic { - Avaliable: () => boolean; - new (): ITickHandler; -} - -export interface ITicker extends EventEmitter { - setFPS?: (fps: number) => void; - setInterval?: (interval: number) => void; - getFPS?: () => number; - getInterval?: () => number; - tick: (interval: number) => void; - tickAt?: (time: number) => void; - pause: () => boolean; - resume: () => boolean; - /** - * 开启tick,force为true强制开启,否则如果timeline为空则不开启 - */ - start: (force?: boolean) => boolean; - stop: () => void; - addTimeline: (timeline: ITimeline) => void; - remTimeline: (timeline: ITimeline) => void; - trySyncTickStatus: () => void; - getTimelines: () => ITimeline[]; - - release: () => void; - - // 是否自动停止,默认为true - autoStop: boolean; -} +// export interface ITickerHandlerStatic { +// Avaliable: () => boolean; +// new (): ITickHandler; +// } + +// export interface ITicker extends EventEmitter { +// setFPS?: (fps: number) => void; +// setInterval?: (interval: number) => void; +// getFPS?: () => number; +// getInterval?: () => number; +// tick: (interval: number) => void; +// tickAt?: (time: number) => void; +// pause: () => boolean; +// resume: () => boolean; +// /** +// * 开启tick,force为true强制开启,否则如果timeline为空则不开启 +// */ +// start: (force?: boolean) => boolean; +// stop: () => void; +// addTimeline: (timeline: ITimeline) => void; +// remTimeline: (timeline: ITimeline) => void; +// trySyncTickStatus: () => void; +// getTimelines: () => ITimeline[]; + +// release: () => void; + +// // 是否自动停止,默认为true +// autoStop: boolean; +// } diff --git a/packages/vrender-animate/src/intreface/animate.ts b/packages/vrender-core/src/interface/animation/animate.ts similarity index 93% rename from packages/vrender-animate/src/intreface/animate.ts rename to packages/vrender-core/src/interface/animation/animate.ts index ea5bb6fce..42e466ca1 100644 --- a/packages/vrender-animate/src/intreface/animate.ts +++ b/packages/vrender-core/src/interface/animation/animate.ts @@ -1,4 +1,4 @@ -import type { IGraphic } from '@visactor/vrender-core'; +import type { IGraphic } from '../graphic'; import type { EasingType, EasingTypeFunc } from './easing'; import type { AnimateStatus, IAnimateStepType } from './type'; import type { ITimeline } from './timeline'; @@ -168,3 +168,14 @@ export enum AnimateMode { NORMAL = 0b0000, SET_ATTR_IMMEDIATELY = 0b0001 } + +export interface IAnimateTarget { + onAnimateBind?: (animte: IAnimate) => void; + // 获取属性 + getComputedAttribute: (name: string) => any; + // 获取默认属性 + getDefaultAttribute: (name: string) => any; + onStop: (props?: Record) => void; + animates: Map; + [key: string]: any; +} diff --git a/packages/vrender-animate/src/intreface/easing.ts b/packages/vrender-core/src/interface/animation/easing.ts similarity index 100% rename from packages/vrender-animate/src/intreface/easing.ts rename to packages/vrender-core/src/interface/animation/easing.ts diff --git a/packages/vrender-core/src/interface/animation/index.ts b/packages/vrender-core/src/interface/animation/index.ts new file mode 100644 index 000000000..a4b93f563 --- /dev/null +++ b/packages/vrender-core/src/interface/animation/index.ts @@ -0,0 +1,5 @@ +export * from './animate'; +export * from './ticker'; +export * from './timeline'; +export * from './type'; +export * from './easing'; diff --git a/packages/vrender-animate/src/intreface/ticker.ts b/packages/vrender-core/src/interface/animation/ticker.ts similarity index 100% rename from packages/vrender-animate/src/intreface/ticker.ts rename to packages/vrender-core/src/interface/animation/ticker.ts diff --git a/packages/vrender-animate/src/intreface/timeline.ts b/packages/vrender-core/src/interface/animation/timeline.ts similarity index 100% rename from packages/vrender-animate/src/intreface/timeline.ts rename to packages/vrender-core/src/interface/animation/timeline.ts diff --git a/packages/vrender-animate/src/intreface/type.ts b/packages/vrender-core/src/interface/animation/type.ts similarity index 100% rename from packages/vrender-animate/src/intreface/type.ts rename to packages/vrender-core/src/interface/animation/type.ts diff --git a/packages/vrender-core/src/interface/graphic.ts b/packages/vrender-core/src/interface/graphic.ts index 7d23a4c64..0fc4bbba3 100644 --- a/packages/vrender-core/src/interface/graphic.ts +++ b/packages/vrender-core/src/interface/graphic.ts @@ -1,5 +1,5 @@ import type { IAABBBounds, IMatrix, IPointLike, IPoint, BoundsAnchorType, IOBBBounds } from '@visactor/vutils'; -import type { IAnimate, IStep, EasingType, IAnimateTarget, ITimeline } from './animate'; +import type { IAnimate, IStep, EasingType, IAnimateTarget, ITimeline } from './animation'; import type { IColor } from './color'; import type { IGroup } from './graphic/group'; import type { IShadowRoot } from './graphic/shadow-root'; diff --git a/packages/vrender-core/src/interface/index.ts b/packages/vrender-core/src/interface/index.ts index cf3aeb200..3be5a2ecb 100644 --- a/packages/vrender-core/src/interface/index.ts +++ b/packages/vrender-core/src/interface/index.ts @@ -73,4 +73,4 @@ export * from './plugin'; export * from './picker'; export * from './text'; export * from './window'; -export * from './animate'; +export * from './animation'; diff --git a/packages/vrender-core/src/interface/stage.ts b/packages/vrender-core/src/interface/stage.ts index ea99f3e90..deff4d747 100644 --- a/packages/vrender-core/src/interface/stage.ts +++ b/packages/vrender-core/src/interface/stage.ts @@ -7,7 +7,7 @@ import type { vec3 } from './matrix'; import type { IDirectionLight } from './light'; import type { ISyncHook } from './sync-hook'; import type { IDrawContext, IRenderService } from './render'; -import type { ITicker, ITimeline } from './animate'; +import type { ITicker, ITimeline } from './animation'; import type { IPickerService, PickResult } from './picker'; import type { IPlugin, IPluginService } from './plugin'; import type { IWindow } from './window'; From e953893fc0a3b1ad2d78330cc2b9abe7deef93bc Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 10 Apr 2025 16:22:06 +0800 Subject: [PATCH 087/179] feat: support setAttributes when target is glyph --- packages/vrender-animate/src/custom/clip-graphic.ts | 1 + packages/vrender-animate/src/custom/common.ts | 2 ++ .../vrender-animate/src/custom/custom-animate.ts | 1 + packages/vrender-animate/src/custom/from-to.ts | 3 +++ packages/vrender-animate/src/custom/growAngle.ts | 2 ++ packages/vrender-animate/src/custom/growCenter.ts | 2 ++ packages/vrender-animate/src/custom/growHeight.ts | 2 ++ packages/vrender-animate/src/custom/growPoints.ts | 6 ++++++ packages/vrender-animate/src/custom/growRadius.ts | 2 ++ packages/vrender-animate/src/custom/growWidth.ts | 2 ++ .../vrender-animate/src/custom/label-item-animate.ts | 2 ++ packages/vrender-animate/src/custom/move.ts | 2 ++ .../vrender-animate/src/custom/poptip-animate.ts | 2 ++ packages/vrender-animate/src/custom/rotate.ts | 2 ++ packages/vrender-animate/src/custom/scale.ts | 2 ++ packages/vrender-animate/src/custom/sphere.ts | 1 + packages/vrender-animate/src/custom/state.ts | 1 + packages/vrender-animate/src/custom/story.ts | 5 +++++ packages/vrender-animate/src/custom/tag-points.ts | 1 + packages/vrender-animate/src/custom/update.ts | 1 + packages/vrender-animate/src/step.ts | 12 ++++++++++++ packages/vrender-core/src/core/stage.ts | 2 +- packages/vrender-core/src/index.ts | 8 ++++++++ 23 files changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/vrender-animate/src/custom/clip-graphic.ts b/packages/vrender-animate/src/custom/clip-graphic.ts index 49c4d7386..977352c89 100644 --- a/packages/vrender-animate/src/custom/clip-graphic.ts +++ b/packages/vrender-animate/src/custom/clip-graphic.ts @@ -26,6 +26,7 @@ export class ClipGraphicAnimate extends ACustomAnimate { } onBind() { + super.onBind(); if (this._group && this._clipGraphic) { this._lastClip = this._group.attribute.clip; this._lastPath = this._group.attribute.path; diff --git a/packages/vrender-animate/src/custom/common.ts b/packages/vrender-animate/src/custom/common.ts index c735d0d16..81e79ad44 100644 --- a/packages/vrender-animate/src/custom/common.ts +++ b/packages/vrender-animate/src/custom/common.ts @@ -15,6 +15,7 @@ export class CommonIn extends ACustomAnimate> { } onBind(): void { + super.onBind(); // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) const attrs = this.target.getFinalAttribute(); const fromAttrs: Record = this.target.attribute ?? {}; @@ -64,6 +65,7 @@ export class CommonOut extends ACustomAnimate> { } onBind(): void { + super.onBind(); const attrs: Record = this.target.attribute; const to: Record = {}; diff --git a/packages/vrender-animate/src/custom/custom-animate.ts b/packages/vrender-animate/src/custom/custom-animate.ts index 48dd53d1f..747af59a2 100644 --- a/packages/vrender-animate/src/custom/custom-animate.ts +++ b/packages/vrender-animate/src/custom/custom-animate.ts @@ -27,6 +27,7 @@ export abstract class ACustomAnimate extends Step implements ICustomAnimate { // 应用缓动函数 const easedRatio = this.easing(ratio); this.onUpdate(end, easedRatio, out); + this.syncAttributeUpdate(); } protected setProps(props: T) { diff --git a/packages/vrender-animate/src/custom/from-to.ts b/packages/vrender-animate/src/custom/from-to.ts index 5eb08cb47..84d660f04 100644 --- a/packages/vrender-animate/src/custom/from-to.ts +++ b/packages/vrender-animate/src/custom/from-to.ts @@ -13,6 +13,7 @@ export class FromTo extends ACustomAnimate> { } onBind(): void { + super.onBind(); const finalAttribute = this.target.getFinalAttribute(); const currAttribute = this.target.attribute; // 要同步from和to @@ -56,5 +57,7 @@ export class FromTo extends ACustomAnimate> { const toValue = this.to[key]; func(key, fromValue, toValue, easedRatio, this, this.target); }); + this.onUpdate(end, easedRatio, out); + this.syncAttributeUpdate(); } } diff --git a/packages/vrender-animate/src/custom/growAngle.ts b/packages/vrender-animate/src/custom/growAngle.ts index bc4877a56..4ca428b87 100644 --- a/packages/vrender-animate/src/custom/growAngle.ts +++ b/packages/vrender-animate/src/custom/growAngle.ts @@ -217,6 +217,7 @@ export class GrowAngleBase extends ACustomAnimate> { */ export class GrowAngleIn extends GrowAngleBase { onBind(): void { + super.onBind(); const { from, to } = growAngleIn(this.target, this.params.options, this.params); const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; @@ -237,6 +238,7 @@ export class GrowAngleIn extends GrowAngleBase { export class GrowAngleOut extends GrowAngleBase { onBind(): void { + super.onBind(); const { from, to } = growAngleOut(this.target, this.params.options, this.params); const fromAttrs = from; this.props = to; diff --git a/packages/vrender-animate/src/custom/growCenter.ts b/packages/vrender-animate/src/custom/growCenter.ts index 7b10c05ca..f1ac39a2e 100644 --- a/packages/vrender-animate/src/custom/growCenter.ts +++ b/packages/vrender-animate/src/custom/growCenter.ts @@ -201,6 +201,7 @@ export class GrowCenterIn extends ACustomAnimate> { } onBind(): void { + super.onBind(); const { from, to } = growCenterIn(this.target, this.params.options, this.params); const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; @@ -239,6 +240,7 @@ export class GrowCenterOut extends ACustomAnimate> { } onBind(): void { + super.onBind(); const { from, to } = growCenterOut(this.target, this.params.options, this.params); this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); diff --git a/packages/vrender-animate/src/custom/growHeight.ts b/packages/vrender-animate/src/custom/growHeight.ts index 8b4185bc5..53506a630 100644 --- a/packages/vrender-animate/src/custom/growHeight.ts +++ b/packages/vrender-animate/src/custom/growHeight.ts @@ -99,6 +99,7 @@ export class GrowHeightIn extends ACustomAnimate> { } onBind(): void { + super.onBind(); const { from, to } = growHeightIn(this.target, this.params.options, this.params); const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; @@ -202,6 +203,7 @@ export class GrowHeightOut extends ACustomAnimate> { } onBind(): void { + super.onBind(); const { from, to } = growHeightOut(this.target, this.params.options, this.params); this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); diff --git a/packages/vrender-animate/src/custom/growPoints.ts b/packages/vrender-animate/src/custom/growPoints.ts index ffd76049a..c304a2cf0 100644 --- a/packages/vrender-animate/src/custom/growPoints.ts +++ b/packages/vrender-animate/src/custom/growPoints.ts @@ -110,6 +110,7 @@ export class GworPointsBase extends ACustomAnimate> { */ export class GrowPointsIn extends GworPointsBase { onBind(): void { + super.onBind(); if (['area', 'line', 'polygon'].includes(this.target.type)) { const { from, to } = growPointsIn(this.target, this.params.options, this.params); this.props = to; @@ -131,6 +132,7 @@ export class GrowPointsIn extends GworPointsBase { export class GrowPointsOut extends GworPointsBase { onBind(): void { + super.onBind(); if (['area', 'line'].includes(this.target.type)) { const attrs = this.target.getFinalAttribute(); const { from, to } = growPointsOut(this.target, this.params.options, this.params); @@ -207,6 +209,7 @@ const growPointsXOut: TypeAnimation = ( export class GrowPointsXIn extends GworPointsBase { onBind(): void { + super.onBind(); if (['area', 'line', 'polygon'].includes(this.target.type)) { const { from, to } = growPointsXIn(this.target, this.params.options, this.params); this.props = to; @@ -228,6 +231,7 @@ export class GrowPointsXIn extends GworPointsBase { export class GrowPointsXOut extends GworPointsBase { onBind(): void { + super.onBind(); if (['area', 'line'].includes(this.target.type)) { const attrs = this.target.getFinalAttribute(); const { from, to } = growPointsXOut(this.target, this.params.options, this.params); @@ -304,6 +308,7 @@ const growPointsYOut: TypeAnimation = ( export class GrowPointsYIn extends GworPointsBase { onBind(): void { + super.onBind(); if (['area', 'line', 'polygon'].includes(this.target.type)) { const { from, to } = growPointsYIn(this.target, this.params.options, this.params); this.props = to; @@ -325,6 +330,7 @@ export class GrowPointsYIn extends GworPointsBase { export class GrowPointsYOut extends GworPointsBase { onBind(): void { + super.onBind(); if (['area', 'line', 'polygon'].includes(this.target.type)) { const { from, to } = growPointsYOut(this.target, this.params.options, this.params); this.props = to; diff --git a/packages/vrender-animate/src/custom/growRadius.ts b/packages/vrender-animate/src/custom/growRadius.ts index 585161ab3..52a51a690 100644 --- a/packages/vrender-animate/src/custom/growRadius.ts +++ b/packages/vrender-animate/src/custom/growRadius.ts @@ -144,6 +144,7 @@ export class GrowPointsBase extends ACustomAnimate> { */ export class GrowRadiusIn extends GrowPointsBase { onBind(): void { + super.onBind(); const { from, to } = growRadiusIn(this.target, this.params.options, this.params); const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; @@ -162,6 +163,7 @@ export class GrowRadiusIn extends GrowPointsBase { export class GrowRadiusOut extends GrowPointsBase { onBind(): void { + super.onBind(); const { to } = growRadiusOut(this.target, this.params.options, this.params); this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); diff --git a/packages/vrender-animate/src/custom/growWidth.ts b/packages/vrender-animate/src/custom/growWidth.ts index 2ccc85842..e05b59ddd 100644 --- a/packages/vrender-animate/src/custom/growWidth.ts +++ b/packages/vrender-animate/src/custom/growWidth.ts @@ -162,6 +162,7 @@ export class GrowWidthIn extends ACustomAnimate> { } onBind(): void { + super.onBind(); const { from, to } = growWidthIn(this.target, this.params.options, this.params); const fromAttrs = this.target.context?.lastAttrs ?? from; this.props = to; @@ -198,6 +199,7 @@ export class GrowWidthOut extends ACustomAnimate> { } onBind(): void { + super.onBind(); const { from, to } = growWidthOut(this.target, this.params.options, this.params); this.props = to; this.propKeys = Object.keys(to).filter(key => to[key] != null); diff --git a/packages/vrender-animate/src/custom/label-item-animate.ts b/packages/vrender-animate/src/custom/label-item-animate.ts index 9ab6b0499..639be6492 100644 --- a/packages/vrender-animate/src/custom/label-item-animate.ts +++ b/packages/vrender-animate/src/custom/label-item-animate.ts @@ -7,6 +7,7 @@ import { InputText } from './input-text'; */ export class LabelItemAppear extends AComponentAnimate { onBind(): void { + super.onBind(); const animator = createComponentAnimator(this.target); this._animator = animator; const duration = this.duration; @@ -150,6 +151,7 @@ export class LabelItemAppear extends AComponentAnimate { */ export class LabelItemDisappear extends AComponentAnimate { onBind(): void { + super.onBind(); const animator = createComponentAnimator(this.target); this._animator = animator; diff --git a/packages/vrender-animate/src/custom/move.ts b/packages/vrender-animate/src/custom/move.ts index 109c0137f..6d89895bd 100644 --- a/packages/vrender-animate/src/custom/move.ts +++ b/packages/vrender-animate/src/custom/move.ts @@ -141,6 +141,7 @@ export class MoveBase extends ACustomAnimate> { */ export class MoveIn extends MoveBase { onBind(): void { + super.onBind(); const { from, to } = moveIn(this.target, this.params.options, this.params); this.props = to; this.propKeys = Object.keys(to).filter(key => (to as any)[key] != null); @@ -158,6 +159,7 @@ export class MoveIn extends MoveBase { export class MoveOut extends MoveBase { onBind(): void { + super.onBind(); const { from, to } = moveOut(this.target, this.params.options, this.params); this.props = to; this.propKeys = Object.keys(to).filter(key => (to as any)[key] != null); diff --git a/packages/vrender-animate/src/custom/poptip-animate.ts b/packages/vrender-animate/src/custom/poptip-animate.ts index b226df45b..366fd66ed 100644 --- a/packages/vrender-animate/src/custom/poptip-animate.ts +++ b/packages/vrender-animate/src/custom/poptip-animate.ts @@ -7,6 +7,7 @@ import { InputText } from './input-text'; */ export class PoptipAppear extends AComponentAnimate { onBind(): void { + super.onBind(); const animator = createComponentAnimator(this.target); this._animator = animator; @@ -92,6 +93,7 @@ export class PoptipAppear extends AComponentAnimate { */ export class PoptipDisappear extends AComponentAnimate { onBind(): void { + super.onBind(); const animator = createComponentAnimator(this.target); this._animator = animator; diff --git a/packages/vrender-animate/src/custom/rotate.ts b/packages/vrender-animate/src/custom/rotate.ts index c1f4ad888..746229446 100644 --- a/packages/vrender-animate/src/custom/rotate.ts +++ b/packages/vrender-animate/src/custom/rotate.ts @@ -68,6 +68,7 @@ export class RotateBase extends ACustomAnimate> { */ export class RotateIn extends RotateBase { onBind(): void { + super.onBind(); // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) const { from, to } = rotateIn(this.target, this.params.options); @@ -88,6 +89,7 @@ export class RotateIn extends RotateBase { export class RotateOut extends RotateBase { onBind(): void { + super.onBind(); const { from, to } = rotateOut(this.target, this.params.options); this.props = to; this.propKeys = ['angle']; diff --git a/packages/vrender-animate/src/custom/scale.ts b/packages/vrender-animate/src/custom/scale.ts index b12f38a70..1f5262cd8 100644 --- a/packages/vrender-animate/src/custom/scale.ts +++ b/packages/vrender-animate/src/custom/scale.ts @@ -15,6 +15,7 @@ export class ScaleIn extends ACustomAnimate> { declare _updateFunction: (ratio: number) => void; onBind(): void { + super.onBind(); let from: Record; let to: Record; const attrs = this.target.getFinalAttribute(); @@ -110,6 +111,7 @@ export class ScaleOut extends ACustomAnimate> { } onBind(): void { + super.onBind(); let from: Record; let to: Record; // 获取当前的数据 diff --git a/packages/vrender-animate/src/custom/sphere.ts b/packages/vrender-animate/src/custom/sphere.ts index a8c7d72c3..53333ddb7 100644 --- a/packages/vrender-animate/src/custom/sphere.ts +++ b/packages/vrender-animate/src/custom/sphere.ts @@ -29,6 +29,7 @@ export class RotateBySphereAnimate extends ACustomAnimate { } onBind() { + super.onBind(); return; } diff --git a/packages/vrender-animate/src/custom/state.ts b/packages/vrender-animate/src/custom/state.ts index de9eb340e..9c6e03e20 100644 --- a/packages/vrender-animate/src/custom/state.ts +++ b/packages/vrender-animate/src/custom/state.ts @@ -40,5 +40,6 @@ export class State extends ACustomAnimate> { func(key, fromValue, toValue, easedRatio, this, this.target); }); this.onUpdate(end, easedRatio, out); + this.syncAttributeUpdate(); } } diff --git a/packages/vrender-animate/src/custom/story.ts b/packages/vrender-animate/src/custom/story.ts index 8aee4a64c..659f49a92 100644 --- a/packages/vrender-animate/src/custom/story.ts +++ b/packages/vrender-animate/src/custom/story.ts @@ -40,6 +40,7 @@ export class SlideIn extends ACustomAnimate> { } onBind(): void { + super.onBind(); // 用于入场的时候设置属性 const attrs = this.target.getFinalAttribute(); @@ -103,6 +104,7 @@ export class GrowIn extends ACustomAnimate> { } onBind(): void { + super.onBind(); // 用于入场的时候设置属性 const attrs = this.target.getFinalAttribute(); @@ -160,6 +162,7 @@ export class SpinIn extends ACustomAnimate> { } onBind(): void { + super.onBind(); // 用于入场的时候设置属性 const attrs = this.target.getFinalAttribute(); @@ -230,6 +233,7 @@ export class MoveScaleIn extends ACustomAnimate { } onBind(): void { + super.onBind(); // 创建AnimateExecutor来运行序列动画 const executor = new AnimateExecutor(this.target); @@ -293,6 +297,7 @@ export class MoveRotateIn extends ACustomAnimate { } onBind(): void { + super.onBind(); // 创建AnimateExecutor来运行序列动画 const executor = new AnimateExecutor(this.target); diff --git a/packages/vrender-animate/src/custom/tag-points.ts b/packages/vrender-animate/src/custom/tag-points.ts index 180288efc..654271d8d 100644 --- a/packages/vrender-animate/src/custom/tag-points.ts +++ b/packages/vrender-animate/src/custom/tag-points.ts @@ -50,6 +50,7 @@ export class TagPointsUpdate extends ACustomAnimate<{ points?: IPointLike[]; seg } onBind(): void { + super.onBind(); const { points, segments } = this.target.attribute as any; const { points: pointsTo, segments: segmentsTo } = this.target.getFinalAttribute() as any; diff --git a/packages/vrender-animate/src/custom/update.ts b/packages/vrender-animate/src/custom/update.ts index 59007716b..6131bfe02 100644 --- a/packages/vrender-animate/src/custom/update.ts +++ b/packages/vrender-animate/src/custom/update.ts @@ -23,6 +23,7 @@ export class Update extends ACustomAnimate> { } onBind() { + super.onBind(); let { diffAttrs = {} } = this.target.context ?? ({} as any); const { options } = this.params as any; diff --git a/packages/vrender-animate/src/step.ts b/packages/vrender-animate/src/step.ts index a28e97005..a834358a5 100644 --- a/packages/vrender-animate/src/step.ts +++ b/packages/vrender-animate/src/step.ts @@ -46,6 +46,8 @@ export class Step implements IStep { protected _endCb?: (animate: IAnimate, step: IStep) => void; + syncAttributeUpdate: () => void; + constructor(type: IAnimateStepType, props: Record, duration: number, easing: EasingType) { this.type = type; this.props = props; @@ -60,12 +62,14 @@ export class Step implements IStep { this.onUpdate = noop; } this.id = Generator.GenAutoIncrementId(); + this.syncAttributeUpdate = noop; } bind(target: IGraphic, animate: IAnimate): void { this.target = target; this.animate = animate; this.onBind(); + this.syncAttributeUpdate(); } append(step: IStep): void { @@ -161,8 +165,15 @@ export class Step implements IStep { onBind(): void { // 在第一次绑定到Animate的时候触发 + if (this.target.type === 'glyph') { + this.syncAttributeUpdate = this._syncAttributeUpdate; + } } + _syncAttributeUpdate = (): void => { + this.target.setAttributes(this.target.attribute); + }; + /** * 首次运行逻辑 * 如果跳帧了就不一定会执行 @@ -258,6 +269,7 @@ export class Step implements IStep { func(key, fromValue, toValue, easedRatio, this, this.target); }); this.onUpdate(end, easedRatio, out); + this.syncAttributeUpdate(); } onUpdate(end: boolean, ratio: number, out: Record): void { diff --git a/packages/vrender-core/src/core/stage.ts b/packages/vrender-core/src/core/stage.ts index 8abeb6ad8..eb6ea4d70 100644 --- a/packages/vrender-core/src/core/stage.ts +++ b/packages/vrender-core/src/core/stage.ts @@ -305,7 +305,7 @@ export class Stage extends Group implements IStage { initAnimate(params: Partial) { if ((this as any).createTicker && (this as any).createTimeline) { this.ticker = params.ticker || (this as any).createTicker(this); - if (this.params.optimize?.tickRenderMode !== 'effect') { + if (this.params.optimize?.tickRenderMode === 'performance') { this.ticker.setFPS(30); } this.timeline = (this as any).createTimeline(); diff --git a/packages/vrender-core/src/index.ts b/packages/vrender-core/src/index.ts index 0e1830689..f2f10b795 100644 --- a/packages/vrender-core/src/index.ts +++ b/packages/vrender-core/src/index.ts @@ -103,3 +103,11 @@ export * from './plugins/builtin-plugin/3dview-transform-plugin'; export * from './plugins/builtin-plugin/flex-layout-plugin'; export * from './plugins/builtin-plugin/edit-module'; + +export const morphPath = {}; +export const multiToOneMorph = {}; +export const oneToMultiMorph = {}; +export class ACustomAnimate {} +export const AnimateGroup = {}; +export const Animate = {}; +export const defaultTicker = {}; From 2620c36533e6881db8498a9863dee901a5def595 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 10 Apr 2025 17:50:14 +0800 Subject: [PATCH 088/179] fix: fix issue in fromTo and effect custom attribute --- .../vrender-animate/src/custom/from-to.ts | 58 +++++++++++-------- .../src/executor/animate-executor.ts | 8 ++- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/packages/vrender-animate/src/custom/from-to.ts b/packages/vrender-animate/src/custom/from-to.ts index 84d660f04..7d5e8a59f 100644 --- a/packages/vrender-animate/src/custom/from-to.ts +++ b/packages/vrender-animate/src/custom/from-to.ts @@ -8,29 +8,29 @@ export class FromTo extends ACustomAnimate> { constructor(from: null, to: null, duration: number, easing: EasingType, params?: any) { super(from, to, duration, easing, params); - this.from = from; - this.to = to; + this.from = from ?? {}; } onBind(): void { super.onBind(); + const finalAttribute = this.target.getFinalAttribute(); - const currAttribute = this.target.attribute; - // 要同步from和to - const fromKeys = Object.keys(this.from); - const toKeys = Object.keys(this.to); - fromKeys.forEach(key => { - if (this.to[key] == null) { - this.to[key] = finalAttribute[key] ?? 0; - } - }); - toKeys.forEach(key => { - if (this.from[key] == null) { - this.from[key] = (currAttribute as any)[key] ?? 0; + // 如果存在from,不存在to,那么需要设置给props + Object.keys(this.from).forEach(key => { + if (this.props[key] == null) { + this.props[key] = finalAttribute[key]; } }); - this.propKeys = Object.keys(this.from); + } + onFirstRun(): void { + // 获取上一步的属性值作为起始值 + this.from = { ...this.getLastProps(), ...this.from }; + const startProps = this.animate.getStartProps(); + this.propKeys && + this.propKeys.forEach(key => { + this.from[key] = this.from[key] ?? startProps[key]; + }); // TODO:比较hack // 如果是入场动画,那么还需要设置属性 if (this.target.context?.animationState === 'appear') { @@ -38,25 +38,33 @@ export class FromTo extends ACustomAnimate> { const finalAttribute = this.target.getFinalAttribute(); this.target.setAttributes(finalAttribute); } + this.target.setAttributes(this.from); } + /** + * 更新执行的时候调用 + * 如果跳帧了就不一定会执行 + */ update(end: boolean, ratio: number, out: Record): void { + // TODO 需要修复,只有在开始的时候才调用 this.onStart(); if (!this.props || !this.propKeys) { return; } // 应用缓动函数 const easedRatio = this.easing(ratio); - this.interpolateUpdateFunctions.forEach((func, index) => { - // 如果这个属性被屏蔽了,直接跳过 - if (!this.animate.validAttr(this.propKeys[index])) { - return; - } - const key = this.propKeys[index]; - const fromValue = this.from[key]; - const toValue = this.to[key]; - func(key, fromValue, toValue, easedRatio, this, this.target); - }); + this.animate.interpolateUpdateFunction + ? this.animate.interpolateUpdateFunction(this.from, this.props, easedRatio, this, this.target) + : this.interpolateUpdateFunctions.forEach((func, index) => { + // 如果这个属性被屏蔽了,直接跳过 + if (!this.animate.validAttr(this.propKeys[index])) { + return; + } + const key = this.propKeys[index]; + const fromValue = this.from[key]; + const toValue = this.props[key]; + func(key, fromValue, toValue, easedRatio, this, this.target); + }); this.onUpdate(end, easedRatio, out); this.syncAttributeUpdate(); } diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index 87f8cd0c3..a6b955f19 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -526,10 +526,14 @@ export class AnimateExecutor implements IAnimateExecutor { } from = parsedFromProps.from; } + const custom = effect.custom ?? AnimateExecutor.builtInAnimateMap[type]; + const customType = + (effect as any).customType ?? + (custom && isFunction(custom) ? (/^class\s/.test(Function.prototype.toString.call(custom)) ? 1 : 2) : 0); this._handleRunAnimate( animate, - effect.custom, - (effect as any).customType, + custom, + customType, from, props, duration as number, From 6279cee2c4f7756201ac4d9450a060a2d20feccd Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 11 Apr 2025 16:55:36 +0800 Subject: [PATCH 089/179] feat: support export streamlight --- .../src/custom/{from-to.ts => fromTo.ts} | 0 .../custom/{group-fade.ts => groupFade.ts} | 0 .../vrender-animate/src/custom/motionPath.ts | 3 +- .../vrender-animate/src/custom/register.ts | 11 +- .../vrender-animate/src/custom/streamLight.ts | 313 ++++++++++++++++++ packages/vrender-animate/src/index.ts | 2 +- .../vrender-core/src/common/segment/index.ts | 1 + 7 files changed, 325 insertions(+), 5 deletions(-) rename packages/vrender-animate/src/custom/{from-to.ts => fromTo.ts} (100%) rename packages/vrender-animate/src/custom/{group-fade.ts => groupFade.ts} (100%) create mode 100644 packages/vrender-animate/src/custom/streamLight.ts diff --git a/packages/vrender-animate/src/custom/from-to.ts b/packages/vrender-animate/src/custom/fromTo.ts similarity index 100% rename from packages/vrender-animate/src/custom/from-to.ts rename to packages/vrender-animate/src/custom/fromTo.ts diff --git a/packages/vrender-animate/src/custom/group-fade.ts b/packages/vrender-animate/src/custom/groupFade.ts similarity index 100% rename from packages/vrender-animate/src/custom/group-fade.ts rename to packages/vrender-animate/src/custom/groupFade.ts diff --git a/packages/vrender-animate/src/custom/motionPath.ts b/packages/vrender-animate/src/custom/motionPath.ts index c169a5a29..d0a851adc 100644 --- a/packages/vrender-animate/src/custom/motionPath.ts +++ b/packages/vrender-animate/src/custom/motionPath.ts @@ -1,5 +1,4 @@ -import type { EasingType } from '../intreface/easing'; -import type { CustomPath2D, IGraphic } from '@visactor/vrender-core'; +import type { CustomPath2D, IGraphic, EasingType } from '@visactor/vrender-core'; import { ACustomAnimate } from './custom-animate'; export class MotionPath extends ACustomAnimate { diff --git a/packages/vrender-animate/src/custom/register.ts b/packages/vrender-animate/src/custom/register.ts index 695b797c9..fb6876709 100644 --- a/packages/vrender-animate/src/custom/register.ts +++ b/packages/vrender-animate/src/custom/register.ts @@ -40,7 +40,9 @@ import { Update } from './update'; import { MoveIn, MoveOut } from './move'; import { RotateIn, RotateOut } from './rotate'; import { MotionPath } from './motionPath'; -import { FromTo } from './from-to'; +import { FromTo } from './fromTo'; +import { GroupFadeIn, GroupFadeOut } from './groupFade'; +import { StreamLight } from './streamLight'; export const registerCustomAnimate = () => { // 基础动画 @@ -106,6 +108,8 @@ export const registerCustomAnimate = () => { // 路径动画 AnimateExecutor.registerBuiltInAnimate('MotionPath', MotionPath); + // 流光动画 + AnimateExecutor.registerBuiltInAnimate('streamLight', StreamLight); }; export { @@ -158,5 +162,8 @@ export { SpinOut, MoveScaleOut, MoveRotateOut, - FromTo + GroupFadeIn, + GroupFadeOut, + FromTo, + StreamLight }; diff --git a/packages/vrender-animate/src/custom/streamLight.ts b/packages/vrender-animate/src/custom/streamLight.ts new file mode 100644 index 000000000..02865749b --- /dev/null +++ b/packages/vrender-animate/src/custom/streamLight.ts @@ -0,0 +1,313 @@ +import type { + EasingType, + IArea, + IAreaCacheItem, + ICubicBezierCurve, + ICurve, + ICustomPath2D, + IGraphic, + ILine, + ILineAttribute, + IRect, + IRectAttribute +} from '@visactor/vrender-core'; +import { application, AttributeUpdateType, CustomPath2D, divideCubic } from '@visactor/vrender-core'; +import { ACustomAnimate } from './custom-animate'; +import type { IPoint } from '@visactor/vutils'; +import { PointService } from '@visactor/vutils'; + +export class StreamLight extends ACustomAnimate { + declare valid: boolean; + declare target: IGraphic; + + declare rect: IRect; + declare line: ILine; + declare area: IArea; + constructor( + from: any, + to: any, + duration: number, + easing: EasingType, + params?: { attribute?: Partial; streamLength?: number; isHorizontal?: boolean } + ) { + super(from, to, duration, easing, params); + } + + getEndProps(): Record { + return {}; + } + + onStart(): void { + if (!this.target) { + return; + } + if (this.target.type === 'rect') { + this.onStartRect(); + } else if (this.target.type === 'line') { + this.onStartLineOrArea('line'); + } else if (this.target.type === 'area') { + this.onStartLineOrArea('area'); + } + } + + onStartLineOrArea(type: 'line' | 'area') { + const root = this.target.attachShadow(); + const line = application.graphicService.creator[type]({ + ...this.params?.attribute + }); + this[type] = line; + line.pathProxy = new CustomPath2D(); + root.add(line); + } + + onStartRect(): void { + const root = this.target.attachShadow(); + + const isHorizontal = this.params?.isHorizontal ?? true; + const sizeAttr = isHorizontal ? 'height' : 'width'; + const otherSizeAttr = isHorizontal ? 'width' : 'height'; + const size = this.target.AABBBounds[sizeAttr](); + const y = isHorizontal ? 0 : this.target.AABBBounds.y1; + + const rect = application.graphicService.creator.rect({ + [sizeAttr]: size, + fill: '#bcdeff', + shadowBlur: 30, + shadowColor: '#bcdeff', + ...this.params?.attribute, + x: 0, + y, + [otherSizeAttr]: 0 + }); + this.rect = rect; + root.add(rect); + } + + onBind(): void { + return; + } + + onEnd(): void { + this.target.detachShadow(); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + if (this.rect) { + return this.onUpdateRect(end, ratio, out); + } else if (this.line || this.area) { + return this.onUpdateLineOrArea(end, ratio, out); + } + } + + protected onUpdateRect(end: boolean, ratio: number, out: Record): void { + const isHorizontal = this.params?.isHorizontal ?? true; + const parentAttr = (this.target as any).attribute; + if (isHorizontal) { + const parentWidth = parentAttr.width ?? Math.abs(parentAttr.x1 - parentAttr.x) ?? 250; + const streamLength = this.params?.streamLength ?? parentWidth; + const maxLength = this.params?.attribute?.width ?? 60; + // 起点,rect x右端点 对齐 parent左端点 + // 如果parent.x1 < parent.x, 需要把rect属性移到parent x1的位置上, 因为初始 rect.x = parent.x + const startX = -maxLength; + // 插值 + const currentX = startX + (streamLength - startX) * ratio; + // 位置限定 > 0 + const x = Math.max(currentX, 0); + // 宽度计算 + const w = Math.min(Math.min(currentX + maxLength, maxLength), streamLength - currentX); + // 如果 rect右端点 超出 parent右端点, 宽度动态调整 + const width = w + x > parentWidth ? Math.max(parentWidth - x, 0) : w; + this.rect.setAttributes( + { + x, + width, + dx: Math.min(parentAttr.x1 - parentAttr.x, 0) + } as any, + false, + { + type: AttributeUpdateType.ANIMATE_PLAY, + animationState: { + ratio, + end + } + } + ); + } else { + const parentHeight = parentAttr.height ?? Math.abs(parentAttr.y1 - parentAttr.y) ?? 250; + const streamLength = this.params?.streamLength ?? parentHeight; + const maxLength = this.params?.attribute?.height ?? 60; + // 起点,y上端点 对齐 parent下端点 + const startY = parentHeight; + // 插值 + const currentY = startY - (streamLength + maxLength) * ratio; + // 位置限定 < parentHeight + let y = Math.min(currentY, parentHeight); + // 高度最小值 + const h = Math.min(parentHeight - currentY, maxLength); + // 如果 rect上端点=y 超出 parent上端点 = 0, 则高度不断变小 + let height; + if (y <= 0) { + // 必须先得到高度再将y置为0, 顺序很重要 + height = Math.max(y + h, 0); + y = 0; + } else { + height = h; + } + this.rect.setAttributes( + { + y, + height, + dy: Math.min(parentAttr.y1 - parentAttr.y, 0) + } as any, + false, + { + type: AttributeUpdateType.ANIMATE_PLAY, + animationState: { + ratio, + end + } + } + ); + } + } + + protected onUpdateLineOrArea(end: boolean, ratio: number, out: Record) { + const target = this.line || this.area; + if (!target) { + return; + } + const customPath = target.pathProxy as ICustomPath2D; + const targetLine = this.target as ILine | IArea; + if (targetLine.cache || targetLine.cacheArea) { + this._onUpdateLineOrAreaWithCache(customPath, targetLine, end, ratio, out); + } else { + this._onUpdateLineWithoutCache(customPath, targetLine, end, ratio, out); + } + const targetAttrs = targetLine.attribute; + target.setAttributes({ + stroke: targetAttrs.stroke, + ...target.attribute + }); + target.addUpdateBoundTag(); + } + + // 针对有cache的linear + protected _onUpdateLineOrAreaWithCache( + customPath: ICustomPath2D, + g: ILine | IArea, + end: boolean, + ratio: number, + out: Record + ) { + customPath.clear(); + if (g.type === 'line') { + let cache = g.cache; + if (!Array.isArray(cache)) { + cache = [cache]; + } + const totalLen = cache.reduce((l: any, c: any) => l + c.getLength(), 0); + const curves: ICurve[] = []; + cache.forEach((c: any) => { + c.curves.forEach((ci: any) => curves.push(ci)); + }); + return this._updateCurves(customPath, curves, totalLen, ratio); + } else if (g.type === 'area' && g.cacheArea?.top?.curves) { + const cache = g.cacheArea as IAreaCacheItem; + const totalLen = cache.top.curves.reduce((a, b) => a + b.getLength(), 0); + return this._updateCurves(customPath, cache.top.curves, totalLen, ratio); + } + } + + protected _updateCurves(customPath: ICustomPath2D, curves: ICurve[], totalLen: number, ratio: number) { + const startLen = totalLen * ratio; + const endLen = Math.min(startLen + (this.params?.streamLength ?? 10), totalLen); + let lastLen = 0; + let start = false; + for (let i = 0; i < curves.length; i++) { + if (curves[i].defined !== false) { + const curveItem = curves[i]; + const len = curveItem.getLength(); + const startPercent = 1 - (lastLen + len - startLen) / len; + let endPercent = 1 - (lastLen + len - endLen) / len; + let curveForStart: ICubicBezierCurve; + if (lastLen < startLen && lastLen + len > startLen) { + start = true; + if (curveItem.p2 && curveItem.p3) { + const [_, curve2] = divideCubic(curveItem as ICubicBezierCurve, startPercent); + customPath.moveTo(curve2.p0.x, curve2.p0.y); + curveForStart = curve2; + // console.log(curve2.p0.x, curve2.p0.y); + } else { + const p = curveItem.getPointAt(startPercent); + customPath.moveTo(p.x, p.y); + } + } + if (lastLen < endLen && lastLen + len > endLen) { + if (curveItem.p2 && curveItem.p3) { + if (curveForStart) { + endPercent = (endLen - startLen) / curveForStart.getLength(); + } + const [curve1] = divideCubic(curveForStart || (curveItem as ICubicBezierCurve), endPercent); + customPath.bezierCurveTo(curve1.p1.x, curve1.p1.y, curve1.p2.x, curve1.p2.y, curve1.p3.x, curve1.p3.y); + } else { + const p = curveItem.getPointAt(endPercent); + customPath.lineTo(p.x, p.y); + } + break; + } else if (start) { + if (curveItem.p2 && curveItem.p3) { + const curve = curveForStart || curveItem; + customPath.bezierCurveTo(curve.p1.x, curve.p1.y, curve.p2.x, curve.p2.y, curve.p3.x, curve.p3.y); + } else { + customPath.lineTo(curveItem.p1.x, curveItem.p1.y); + } + } + lastLen += len; + } + } + } + + // 只针对最简单的linear + protected _onUpdateLineWithoutCache( + customPath: ICustomPath2D, + line: ILine, + end: boolean, + ratio: number, + out: Record + ) { + const { points, curveType } = line.attribute; + if (!points || points.length < 2 || curveType !== 'linear') { + return; + } + let totalLen = 0; + for (let i = 1; i < points.length; i++) { + totalLen += PointService.distancePP(points[i], points[i - 1]); + } + const startLen = totalLen * ratio; + const endLen = Math.min(startLen + (this.params?.streamLength ?? 10), totalLen); + const nextPoints = []; + let lastLen = 0; + for (let i = 1; i < points.length; i++) { + const len = PointService.distancePP(points[i], points[i - 1]); + if (lastLen < startLen && lastLen + len > startLen) { + nextPoints.push(PointService.pointAtPP(points[i - 1], points[i], 1 - (lastLen + len - startLen) / len)); + } + if (lastLen < endLen && lastLen + len > endLen) { + nextPoints.push(PointService.pointAtPP(points[i - 1], points[i], 1 - (lastLen + len - endLen) / len)); + break; + } else if (nextPoints.length) { + nextPoints.push(points[i]); + } + lastLen += len; + } + + if (!nextPoints.length || nextPoints.length < 2) { + return; + } + customPath.clear(); + customPath.moveTo(nextPoints[0].x, nextPoints[0].y); + for (let i = 1; i < nextPoints.length; i++) { + customPath.lineTo(nextPoints[i].x, nextPoints[i].y); + } + } +} diff --git a/packages/vrender-animate/src/index.ts b/packages/vrender-animate/src/index.ts index fd8170b1a..3b012ffc4 100644 --- a/packages/vrender-animate/src/index.ts +++ b/packages/vrender-animate/src/index.ts @@ -14,7 +14,7 @@ export { IncreaseCount } from './custom/number'; export { InputText } from './custom/input-text'; export { ClipGraphicAnimate, ClipAngleAnimate, ClipRadiusAnimate, ClipDirectionAnimate } from './custom/clip-graphic'; export { TagPointsUpdate } from './custom/tag-points'; -export { GroupFadeIn, GroupFadeOut } from './custom/group-fade'; +export { GroupFadeIn, GroupFadeOut } from './custom/groupFade'; export { RotateBySphereAnimate } from './custom/sphere'; export { AnimateExecutor } from './executor/animate-executor'; export type { IAnimationConfig } from './executor/executor'; diff --git a/packages/vrender-core/src/common/segment/index.ts b/packages/vrender-core/src/common/segment/index.ts index 4c065929d..b8496c6df 100644 --- a/packages/vrender-core/src/common/segment/index.ts +++ b/packages/vrender-core/src/common/segment/index.ts @@ -14,6 +14,7 @@ export * from './basis'; export * from './monotone'; export * from './step'; export * from './curve/curve-context'; +export * from './curve/cubic-bezier'; export function calcLineCache( points: IPointLike[], From 3172899acd48cfb62ed0344121a33778f8b8b42a Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 11 Apr 2025 17:05:05 +0800 Subject: [PATCH 090/179] fix: fix issue with entries --- packages/vrender-animate/src/executor/animate-executor.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index a6b955f19..b3bd961ce 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -626,7 +626,8 @@ export class AnimateExecutor implements IAnimateExecutor { if (!Array.isArray(channel)) { // 如果是对象,解析 from/to 配置 - Object.entries(channel).forEach(([key, config]) => { + Object.keys(channel).forEach(key => { + const config = channel[key]; if (config.to !== undefined) { if (typeof config.to === 'function') { props[key] = config.to((graphic.context as any)?.data, graphic, {}); From 76da5d4d34ad110284328ed9d5b4a32c23422bb1 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Mon, 14 Apr 2025 11:53:54 +0800 Subject: [PATCH 091/179] fix: fix issue with executor get data, and support getGraphicAttribute in graphic --- .../vrender-animate/src/animate-extension.ts | 8 ++ packages/vrender-animate/src/custom/move.ts | 8 +- .../src/executor/animate-executor.ts | 101 ++++++++++-------- packages/vrender-core/src/index.ts | 8 -- 4 files changed, 68 insertions(+), 57 deletions(-) diff --git a/packages/vrender-animate/src/animate-extension.ts b/packages/vrender-animate/src/animate-extension.ts index de0151d91..ff0ef8ecb 100644 --- a/packages/vrender-animate/src/animate-extension.ts +++ b/packages/vrender-animate/src/animate-extension.ts @@ -107,4 +107,12 @@ export class AnimateExtension { protected getFinalAttribute() { return this.finalAttribute; } + + // TODO prev是兼容原本VGrammar函数的一个参数,用于动画中获取上一次属性,目前的逻辑中应该不需要,直接去当前帧的属性即可 + getGraphicAttribute(key: string, prev: boolean = false) { + if (!prev && this.finalAttribute) { + return this.finalAttribute[key]; + } + return (this as any).attribute[key]; + } } diff --git a/packages/vrender-animate/src/custom/move.ts b/packages/vrender-animate/src/custom/move.ts index 6d89895bd..af85c54c4 100644 --- a/packages/vrender-animate/src/custom/move.ts +++ b/packages/vrender-animate/src/custom/move.ts @@ -49,7 +49,9 @@ export const moveIn = ( changedX += offset; changedY += offset; - const point = isFunction(pointOpt) ? pointOpt.call(null, graphic.getDatum(), graphic, animationParameters) : pointOpt; + const point = isFunction(pointOpt) + ? pointOpt.call(null, graphic.context?.data?.[0], graphic, animationParameters) + : pointOpt; const fromX = point && isValidNumber(point.x) ? point.x : changedX; const fromY = point && isValidNumber(point.y) ? point.y : changedY; const finalAttrs = graphic.getFinalAttribute(); @@ -92,7 +94,9 @@ export const moveOut = ( const groupHeight = groupBounds?.height() ?? animationParameters.height; const changedX = (orient === 'negative' ? groupWidth : 0) + offset; const changedY = (orient === 'negative' ? groupHeight : 0) + offset; - const point = isFunction(pointOpt) ? pointOpt.call(null, graphic.getDatum(), graphic, animationParameters) : pointOpt; + const point = isFunction(pointOpt) + ? pointOpt.call(null, graphic.context?.data?.[0], graphic, animationParameters) + : pointOpt; const fromX = point && isValidNumber(point.x) ? point.x : changedX; const fromY = point && isValidNumber(point.y) ? point.y : changedY; diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index b3bd961ce..8dacc4e73 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -11,7 +11,7 @@ import type { IAnimationCustomConstructor, IAnimationChannelInterpolator } from './executor'; -import { isArray, isFunction } from '@visactor/vutils'; +import { cloneDeep, isArray, isFunction } from '@visactor/vutils'; interface IAnimateExecutor { execute: (params: IAnimationConfig) => void; @@ -109,18 +109,18 @@ export class AnimateExecutor implements IAnimateExecutor { }); } - parseParams(params: IAnimationConfig, isTimeline: boolean): IAnimationConfig { + parseParams(params: IAnimationConfig, isTimeline: boolean, child?: IGraphic): IAnimationConfig { const totalTime = this.resolveValue(params.totalTime, undefined, undefined); const startTime = this.resolveValue(params.startTime, undefined, 0); // execute只在mark层面调用,所以性能影响可以忽略 // TODO 存在性能问题,如果后续调用频繁,需要重新修改 - const parsedParams: Record = { ...params }; + const parsedParams: Record = cloneDeep(params); parsedParams.oneByOneDelay = 0; parsedParams.startTime = startTime; parsedParams.totalTime = totalTime; - const oneByOne = this.resolveValue(params.oneByOne, undefined, false); + const oneByOne = this.resolveValue(params.oneByOne, child, false); if (isTimeline) { const timeSlices = (parsedParams as IAnimationTimeline).timeSlices; @@ -129,9 +129,9 @@ export class AnimateExecutor implements IAnimateExecutor { } let sliceTime = 0; ((parsedParams as IAnimationTimeline).timeSlices as IAnimationTimeSlice[]).forEach(slice => { - slice.delay = this.resolveValue(slice.delay, undefined, 0); - slice.delayAfter = this.resolveValue(slice.delayAfter, undefined, 0); - slice.duration = this.resolveValue(slice.duration, undefined, 300); + slice.delay = this.resolveValue(slice.delay, child, 0); + slice.delayAfter = this.resolveValue(slice.delayAfter, child, 0); + slice.duration = this.resolveValue(slice.duration, child, 300); sliceTime += slice.delay + slice.duration + slice.delayAfter; }); let oneByOneDelay = 0; @@ -143,41 +143,42 @@ export class AnimateExecutor implements IAnimateExecutor { parsedParams.oneByOne = oneByOneTime; parsedParams.oneByOneDelay = oneByOneDelay; + let scale = 1; if (totalTime) { const _totalTime = sliceTime + oneByOneDelay * (this._target.count - 2); - const scale = totalTime ? totalTime / _totalTime : 1; - ((parsedParams as IAnimationTimeline).timeSlices as IAnimationTimeSlice[]) = ( - (parsedParams as IAnimationTimeline).timeSlices as IAnimationTimeSlice[] - ).map(slice => { - let effects = slice.effects; - if (!Array.isArray(effects)) { - effects = [effects]; - } - return { - ...slice, - delay: (slice.delay as number) * scale, - delayAfter: (slice.delayAfter as number) * scale, - duration: (slice.duration as number) * scale, - effects: effects.map(effect => { - const custom = effect.custom ?? AnimateExecutor.builtInAnimateMap[(effect.type as any) ?? 'fromTo']; - const customType = - custom && isFunction(custom) ? (/^class\s/.test(Function.prototype.toString.call(custom)) ? 1 : 2) : 0; - return { - ...effect, - custom, - customType - }; - }) - }; - }); - parsedParams.oneByOne = oneByOneTime * scale; - parsedParams.oneByOneDelay = oneByOneDelay * scale; - (parsedParams as IAnimationTimeline).startTime = startTime * scale; + scale = totalTime ? totalTime / _totalTime : 1; } + ((parsedParams as IAnimationTimeline).timeSlices as IAnimationTimeSlice[]) = ( + (parsedParams as IAnimationTimeline).timeSlices as IAnimationTimeSlice[] + ).map(slice => { + let effects = slice.effects; + if (!Array.isArray(effects)) { + effects = [effects]; + } + return { + ...slice, + delay: (slice.delay as number) * scale, + delayAfter: (slice.delayAfter as number) * scale, + duration: (slice.duration as number) * scale, + effects: effects.map(effect => { + const custom = effect.custom ?? AnimateExecutor.builtInAnimateMap[(effect.type as any) ?? 'fromTo']; + const customType = + custom && isFunction(custom) ? (/^class\s/.test(Function.prototype.toString.call(custom)) ? 1 : 2) : 0; + return { + ...effect, + custom, + customType + }; + }) + }; + }); + parsedParams.oneByOne = oneByOneTime * scale; + parsedParams.oneByOneDelay = oneByOneDelay * scale; + (parsedParams as IAnimationTimeline).startTime = startTime * scale; } else { - const delay = this.resolveValue((params as IAnimationTypeConfig).delay, undefined, 0); - const delayAfter = this.resolveValue((params as IAnimationTypeConfig).delayAfter, undefined, 0); - const duration = this.resolveValue((params as IAnimationTypeConfig).duration, undefined, 300); + const delay = this.resolveValue((params as IAnimationTypeConfig).delay, child, 0); + const delayAfter = this.resolveValue((params as IAnimationTypeConfig).delayAfter, child, 0); + const duration = this.resolveValue((params as IAnimationTypeConfig).duration, child, 300); let oneByOneDelay = 0; let oneByOneTime = 0; @@ -239,7 +240,7 @@ export class AnimateExecutor implements IAnimateExecutor { // 如果设置了partitioner,则进行筛选 if (isTimeline && params.partitioner) { filteredChildren = (filteredChildren ?? (this._target.getChildren() as IGraphic[])).filter(child => { - return (params as IAnimationTimeline).partitioner((child.context as any)?.data, child, {}); + return (params as IAnimationTimeline).partitioner((child.context as any)?.data?.[0], child, {}); }); } @@ -247,14 +248,21 @@ export class AnimateExecutor implements IAnimateExecutor { if (isTimeline && (params as IAnimationTimeline).sort) { filteredChildren = filteredChildren ?? (this._target.getChildren() as IGraphic[]); filteredChildren.sort((a, b) => { - return (params as IAnimationTimeline).sort((a.context as any)?.data, (b.context as any)?.data, a, b, {}); + return (params as IAnimationTimeline).sort( + (a.context as any)?.data?.[0], + (b.context as any)?.data?.[0], + a, + b, + {} + ); }); } - const parsedParams = this.parseParams(params, isTimeline); + // const cb = isTimeline ? (child: IGraphic, index: number, count: number) => { + const parsedParams = this.parseParams(params, isTimeline, child); // 执行单个图元的timeline动画 const animate = this.executeTimelineItem(parsedParams as IAnimationTimeline, child, index, count); if (animate) { @@ -262,6 +270,7 @@ export class AnimateExecutor implements IAnimateExecutor { } } : (child: IGraphic, index: number, count: number) => { + const parsedParams = this.parseParams(params, isTimeline, child); // 执行单个图元的config动画 const animate = this.executeTypeConfigItem(parsedParams as IAnimationTypeConfig, child, index, count); if (animate) { @@ -527,9 +536,7 @@ export class AnimateExecutor implements IAnimateExecutor { from = parsedFromProps.from; } const custom = effect.custom ?? AnimateExecutor.builtInAnimateMap[type]; - const customType = - (effect as any).customType ?? - (custom && isFunction(custom) ? (/^class\s/.test(Function.prototype.toString.call(custom)) ? 1 : 2) : 0); + const customType = (effect as any).customType; this._handleRunAnimate( animate, custom, @@ -630,7 +637,7 @@ export class AnimateExecutor implements IAnimateExecutor { const config = channel[key]; if (config.to !== undefined) { if (typeof config.to === 'function') { - props[key] = config.to((graphic.context as any)?.data, graphic, {}); + props[key] = config.to((graphic.context as any)?.data?.[0], graphic, {}); } else { props[key] = config.to; } @@ -640,7 +647,7 @@ export class AnimateExecutor implements IAnimateExecutor { from = {}; } if (typeof config.from === 'function') { - from[key] = config.from((graphic.context as any)?.data, graphic, {}); + from[key] = config.from((graphic.context as any)?.data?.[0], graphic, {}); } else { from[key] = config.from; } @@ -670,7 +677,7 @@ export class AnimateExecutor implements IAnimateExecutor { } if (typeof value === 'function' && graphic) { - return (value as MarkFunctionCallback)((graphic.context as any)?.data, graphic, {}); + return (value as MarkFunctionCallback)((graphic.context as any)?.data?.[0], graphic, {}); } return value as T; diff --git a/packages/vrender-core/src/index.ts b/packages/vrender-core/src/index.ts index f2f10b795..0e1830689 100644 --- a/packages/vrender-core/src/index.ts +++ b/packages/vrender-core/src/index.ts @@ -103,11 +103,3 @@ export * from './plugins/builtin-plugin/3dview-transform-plugin'; export * from './plugins/builtin-plugin/flex-layout-plugin'; export * from './plugins/builtin-plugin/edit-module'; - -export const morphPath = {}; -export const multiToOneMorph = {}; -export const oneToMultiMorph = {}; -export class ACustomAnimate {} -export const AnimateGroup = {}; -export const Animate = {}; -export const defaultTicker = {}; From 517fe80f3e294c178e6dfd4ec6f4cca41ea2c85c Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Mon, 14 Apr 2025 16:59:12 +0800 Subject: [PATCH 092/179] fix: fi x issue with growPoint and move, fix issue with call end at waitStep --- packages/vrender-animate/src/animate.ts | 13 ++++++++++ .../vrender-animate/src/custom/growPoints.ts | 22 ++++++++--------- packages/vrender-animate/src/custom/move.ts | 24 +++++++------------ .../src/executor/animate-executor.ts | 4 ++++ packages/vrender-animate/src/step.ts | 7 ++++++ packages/vrender-core/src/graphic/graphic.ts | 2 +- 6 files changed, 44 insertions(+), 28 deletions(-) diff --git a/packages/vrender-animate/src/animate.ts b/packages/vrender-animate/src/animate.ts index 1c2937e24..dbfbdeca8 100644 --- a/packages/vrender-animate/src/animate.ts +++ b/packages/vrender-animate/src/animate.ts @@ -49,6 +49,9 @@ export class Animate implements IAnimate { protected currentTime: number; slience?: boolean; + // 临时变量 + lastRunStep?: IStep; + interpolateUpdateFunction: | ((from: Record, to: Record, ratio: number, step: IStep, target: IGraphic) => void) | null; @@ -588,6 +591,7 @@ export class Animate implements IAnimate { } // 如果已经结束,设置状态后return if (nextTime >= this._startTime + this._totalDuration) { + this._lastStep?.onUpdate(true, 1, {}); this._lastStep?.onEnd(); this.onEnd(); this.status = AnimateStatus.END; @@ -652,6 +656,14 @@ export class Animate implements IAnimate { return; } + // 如果当前step和上一次执行的step不一样,则调用上一次step的onEnd,确保所有完成的step都调用了结束 + // 如果上一次的step已经调用了onEnd,在下面的onEnd那里会将lastRunStep设置为null + if (targetStep !== this.lastRunStep) { + this.lastRunStep?.onEnd(); + } + + this.lastRunStep = targetStep; + // 计算当前step的进度比例(基于当前step内的相对时间) const stepStartTime = targetStep.getStartTime(); const stepDuration = targetStep.getDuration(); @@ -666,6 +678,7 @@ export class Animate implements IAnimate { // 如果step执行完毕 if (isEnd) { targetStep.onEnd(); + this.lastRunStep = null; // 不立即调用onFinish,让动画系统来决定何时结束 } diff --git a/packages/vrender-animate/src/custom/growPoints.ts b/packages/vrender-animate/src/custom/growPoints.ts index c304a2cf0..79925e615 100644 --- a/packages/vrender-animate/src/custom/growPoints.ts +++ b/packages/vrender-animate/src/custom/growPoints.ts @@ -155,12 +155,10 @@ const changePointsX = ( const points = attrs.points; return points.map((point: IPointLike) => { if (options && options.orient === 'negative') { - let groupRight = animationParameters.width; + let groupRight = graphic.stage.viewWidth; - if (animationParameters.group) { - groupRight = (animationParameters as any).groupWidth ?? animationParameters.group.getBounds().width(); - - (animationParameters as any).groupWidth = groupRight; + if (graphic.parent.parent.parent) { + groupRight = graphic.parent.parent.parent.AABBBounds.width(); } return { @@ -254,12 +252,9 @@ const changePointsY = ( const points = attrs.points; return points.map((point: IPointLike) => { if (options && options.orient === 'negative') { - let groupBottom = animationParameters.height; - - if (animationParameters.group) { - groupBottom = (animationParameters as any).groupHeight ?? animationParameters.group.getBounds().height(); - - (animationParameters as any).groupHeight = groupBottom; + let groupBottom = graphic.stage.viewHeight; + if (graphic.parent.parent.parent) { + groupBottom = graphic.parent.parent.parent.AABBBounds.height(); } return { @@ -321,7 +316,10 @@ export class GrowPointsYIn extends GworPointsBase { if (finalAttribute) { Object.assign(this.target.attribute, finalAttribute); } - this.target.setAttributes(from); + + if (this.params.controlOptions?.immediatelyApply !== false) { + this.target.setAttributes(from); + } } else { this.valid = false; } diff --git a/packages/vrender-animate/src/custom/move.ts b/packages/vrender-animate/src/custom/move.ts index af85c54c4..32e71e49e 100644 --- a/packages/vrender-animate/src/custom/move.ts +++ b/packages/vrender-animate/src/custom/move.ts @@ -34,17 +34,8 @@ export const moveIn = ( let changedY = 0; if (orient === 'negative') { - // consider the offset of group - if (animationParameters.group) { - changedX = (animationParameters as any).groupWidth ?? animationParameters.group.getBounds().width(); - changedY = (animationParameters as any).groupHeight ?? animationParameters.group.getBounds().height(); - - (animationParameters as any).groupWidth = changedX; - (animationParameters as any).groupHeight = changedY; - } else { - changedX = animationParameters.width; - changedY = animationParameters.height; - } + changedX = graphic.stage.viewWidth; + changedY = graphic.stage.viewHeight; } changedX += offset; @@ -89,9 +80,9 @@ export const moveOut = ( const { offset = 0, orient, direction, point: pointOpt } = options ?? {}; // consider the offset of group - const groupBounds = animationParameters.group ? animationParameters.group.getBounds() : null; - const groupWidth = groupBounds?.width() ?? animationParameters.width; - const groupHeight = groupBounds?.height() ?? animationParameters.height; + // const groupBounds = graphic.parent ? graphic.parent.getBounds() : null; + const groupWidth = graphic.stage.viewWidth; + const groupHeight = graphic.stage.viewHeight; const changedX = (orient === 'negative' ? groupWidth : 0) + offset; const changedY = (orient === 'negative' ? groupHeight : 0) + offset; const point = isFunction(pointOpt) @@ -157,7 +148,10 @@ export class MoveIn extends MoveBase { if (finalAttribute) { Object.assign(this.target.attribute, finalAttribute); } - this.target.setAttributes(from); + + if (this.params.controlOptions?.immediatelyApply !== false) { + this.target.setAttributes(from); + } } } diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index 8dacc4e73..2a18bf835 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -369,6 +369,7 @@ export class AnimateExecutor implements IAnimateExecutor { duration as number, easing, customParameters, + controlOptions, options, type, graphic @@ -406,6 +407,7 @@ export class AnimateExecutor implements IAnimateExecutor { duration: number, easing: EasingType, customParameters: any, + controlOptions: any, options: any, type: string, graphic: IGraphic @@ -422,6 +424,7 @@ export class AnimateExecutor implements IAnimateExecutor { ) : options; customParams.options = objOptions; + customParams.controlOptions = controlOptions; if (customType === 1) { // 自定义动画构造器 - 创建自定义动画类 this.createCustomAnimation( @@ -546,6 +549,7 @@ export class AnimateExecutor implements IAnimateExecutor { duration as number, easing, customParameters, + null, options, type, graphic diff --git a/packages/vrender-animate/src/step.ts b/packages/vrender-animate/src/step.ts index a834358a5..63b6179f2 100644 --- a/packages/vrender-animate/src/step.ts +++ b/packages/vrender-animate/src/step.ts @@ -323,6 +323,13 @@ export class WaitStep extends Step { super(type, props, duration, easing); } + onStart(): void { + super.onStart(); + // 设置上一个阶段的props到attribute + const fromProps = this.getFromProps(); + this.target.setAttributes(fromProps); + } + update(end: boolean, ratio: number, out: Record): void { this.onStart(); // 其他的不执行 diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index 16d938490..3b5885e84 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -164,7 +164,7 @@ export const NOWORK_ANIMATE_ATTR = { // return Reflect.get(target, property); // }, // set(target: any, property: any, value: any) { -// if (property === 'opacity' && obj.text === 'Nail polish') { +// if (property === 'points' && value.length === 5 && value.every(item => item.y === 337)) { // console.log('set', property, value); // } // // modifiedProperties.add(property); // 记录设置/修改操作 From ea99b78917f69ee041a12b8a45a74a14a988d89b Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Mon, 14 Apr 2025 17:33:41 +0800 Subject: [PATCH 093/179] fix: fix issue with common enter custom animate --- packages/vrender-animate/src/custom/clip.ts | 1 + packages/vrender-animate/src/custom/common.ts | 12 +++++------- packages/vrender-animate/src/custom/fade.ts | 1 + packages/vrender-animate/src/custom/groupFade.ts | 1 + packages/vrender-animate/src/custom/growPoints.ts | 8 ++++++-- packages/vrender-animate/src/custom/rotate.ts | 4 +++- packages/vrender-animate/src/custom/scale.ts | 5 +++-- packages/vrender-core/src/index.ts | 8 ++++++++ 8 files changed, 28 insertions(+), 12 deletions(-) diff --git a/packages/vrender-animate/src/custom/clip.ts b/packages/vrender-animate/src/custom/clip.ts index e29c3a351..ea79a10fc 100644 --- a/packages/vrender-animate/src/custom/clip.ts +++ b/packages/vrender-animate/src/custom/clip.ts @@ -11,6 +11,7 @@ export class ClipIn extends CommonIn { constructor(from: null, to: null, duration: number, easing: EasingType, params?: IScaleAnimationOptions) { super(from, to, duration, easing, params); this.keys = ['clipRange']; + this.from = { clipRange: 0 }; } } diff --git a/packages/vrender-animate/src/custom/common.ts b/packages/vrender-animate/src/custom/common.ts index 81e79ad44..1228a7778 100644 --- a/packages/vrender-animate/src/custom/common.ts +++ b/packages/vrender-animate/src/custom/common.ts @@ -21,10 +21,10 @@ export class CommonIn extends ACustomAnimate> { const fromAttrs: Record = this.target.attribute ?? {}; const to: Record = {}; - const from: Record = {}; + const from: Record = this.from ?? {}; this.keys.forEach(key => { to[key] = attrs?.[key] ?? 1; - from[key] = fromAttrs[key] ?? 0; + from[key] = from[key] ?? fromAttrs[key] ?? 0; }); // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) @@ -38,11 +38,9 @@ export class CommonIn extends ACustomAnimate> { this.from = from; this.to = to; - this.target.setAttributes(from); - } - - onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { - super.onEnd(cb); + if (this.params.controlOptions?.immediatelyApply !== false) { + this.target.setAttributes(from); + } } onUpdate(end: boolean, ratio: number, out: Record): void { diff --git a/packages/vrender-animate/src/custom/fade.ts b/packages/vrender-animate/src/custom/fade.ts index a23513ad3..64240227f 100644 --- a/packages/vrender-animate/src/custom/fade.ts +++ b/packages/vrender-animate/src/custom/fade.ts @@ -11,6 +11,7 @@ export class FadeIn extends CommonIn { constructor(from: null, to: null, duration: number, easing: EasingType, params?: IScaleAnimationOptions) { super(from, to, duration, easing, params); this.keys = ['opacity']; + this.from = { opacity: 0 }; } } diff --git a/packages/vrender-animate/src/custom/groupFade.ts b/packages/vrender-animate/src/custom/groupFade.ts index b09ee82ab..5af5f96c9 100644 --- a/packages/vrender-animate/src/custom/groupFade.ts +++ b/packages/vrender-animate/src/custom/groupFade.ts @@ -8,6 +8,7 @@ export class GroupFadeIn extends CommonIn { constructor(from: null, to: null, duration: number, easing: EasingType, params?: any) { super(from, to, duration, easing, params); this.keys = ['baseOpacity']; + this.from = { baseOpacity: 0 }; } } diff --git a/packages/vrender-animate/src/custom/growPoints.ts b/packages/vrender-animate/src/custom/growPoints.ts index 79925e615..22085a1e5 100644 --- a/packages/vrender-animate/src/custom/growPoints.ts +++ b/packages/vrender-animate/src/custom/growPoints.ts @@ -123,7 +123,9 @@ export class GrowPointsIn extends GworPointsBase { if (finalAttribute) { Object.assign(this.target.attribute, finalAttribute); } - this.target.setAttributes(from); + if (this.params.controlOptions?.immediatelyApply !== false) { + this.target.setAttributes(from); + } } else { this.valid = false; } @@ -220,7 +222,9 @@ export class GrowPointsXIn extends GworPointsBase { if (finalAttribute) { Object.assign(this.target.attribute, finalAttribute); } - this.target.setAttributes(from); + if (this.params.controlOptions?.immediatelyApply !== false) { + this.target.setAttributes(from); + } } else { this.valid = false; } diff --git a/packages/vrender-animate/src/custom/rotate.ts b/packages/vrender-animate/src/custom/rotate.ts index 746229446..8b79c0e81 100644 --- a/packages/vrender-animate/src/custom/rotate.ts +++ b/packages/vrender-animate/src/custom/rotate.ts @@ -83,7 +83,9 @@ export class RotateIn extends RotateBase { Object.assign(this.target.attribute, finalAttribute); } - this.target.setAttributes(from); + if (this.params.controlOptions?.immediatelyApply !== false) { + this.target.setAttributes(from); + } } } diff --git a/packages/vrender-animate/src/custom/scale.ts b/packages/vrender-animate/src/custom/scale.ts index 1f5262cd8..f27fbac4d 100644 --- a/packages/vrender-animate/src/custom/scale.ts +++ b/packages/vrender-animate/src/custom/scale.ts @@ -51,8 +51,9 @@ export class ScaleIn extends ACustomAnimate> { this.props = to; this.from = from; this.to = to; - // 调用次数不多,可以setAttributes - this.target.setAttributes(from); + if (this.params.controlOptions?.immediatelyApply !== false) { + this.target.setAttributes(from); + } } onEnd(cb?: (animate: IAnimate, step: IStep) => void): void { diff --git a/packages/vrender-core/src/index.ts b/packages/vrender-core/src/index.ts index 0e1830689..f2f10b795 100644 --- a/packages/vrender-core/src/index.ts +++ b/packages/vrender-core/src/index.ts @@ -103,3 +103,11 @@ export * from './plugins/builtin-plugin/3dview-transform-plugin'; export * from './plugins/builtin-plugin/flex-layout-plugin'; export * from './plugins/builtin-plugin/edit-module'; + +export const morphPath = {}; +export const multiToOneMorph = {}; +export const oneToMultiMorph = {}; +export class ACustomAnimate {} +export const AnimateGroup = {}; +export const Animate = {}; +export const defaultTicker = {}; From 3a7dedebd2e0c01cc4bf19a2f493f2c3b3129703 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Mon, 14 Apr 2025 20:44:44 +0800 Subject: [PATCH 094/179] fix: fix issue with fromto animate --- packages/vrender-animate/src/custom/fromTo.ts | 10 ++++++++++ .../src/executor/animate-executor.ts | 13 +++++++++---- packages/vrender-animate/src/step.ts | 4 +++- packages/vrender-core/src/graphic/graphic.ts | 2 +- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/vrender-animate/src/custom/fromTo.ts b/packages/vrender-animate/src/custom/fromTo.ts index 7d5e8a59f..c9c3ba64e 100644 --- a/packages/vrender-animate/src/custom/fromTo.ts +++ b/packages/vrender-animate/src/custom/fromTo.ts @@ -21,6 +21,16 @@ export class FromTo extends ACustomAnimate> { this.props[key] = finalAttribute[key]; } }); + + // 如果入场动画,那么需要设置属性 + if (this.target.context?.animationState === 'appear') { + if (finalAttribute) { + Object.assign(this.target.attribute, finalAttribute); + } + } + if (this.params.controlOptions?.immediatelyApply !== false) { + this.target.setAttributes(this.from); + } } onFirstRun(): void { diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index 2a18bf835..b76b7f6b3 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -332,8 +332,8 @@ export class AnimateExecutor implements IAnimateExecutor { } // 设置开始时间 - animate.startAt((startTime as number) + delayValue); - const wait = index * oneByOneDelay; + animate.startAt(startTime as number); + const wait = index * oneByOneDelay + delayValue; wait > 0 && animate.wait(wait); // 放到startAt中,否则label无法确定主图元何时开始 @@ -375,14 +375,19 @@ export class AnimateExecutor implements IAnimateExecutor { graphic ); + let totalDelay = 0; if (oneByOneDelay) { - animate.wait(oneByOneDelay * (count - index - 1)); + totalDelay = oneByOneDelay * (count - index - 1); } // 添加后延迟 const delayAfterValue = isFunction(delayAfter) ? delayAfter(graphic.context?.data?.[0], graphic, {}) : delayAfter; if (delayAfterValue > 0) { - animate.wait(delayAfterValue as number); + totalDelay += delayAfterValue as number; + } + + if (totalDelay > 0) { + animate.wait(totalDelay); } // 设置循环 diff --git a/packages/vrender-animate/src/step.ts b/packages/vrender-animate/src/step.ts index 63b6179f2..42212f7c5 100644 --- a/packages/vrender-animate/src/step.ts +++ b/packages/vrender-animate/src/step.ts @@ -208,7 +208,7 @@ export class Step implements IStep { const animate = this.animate; const target = this.target; target.animates.forEach((a: any) => { - if (a === animate || a.priority > animate.priority) { + if (a === animate || a.priority > animate.priority || a.priority === Infinity) { return; } const fromProps = a.getStartProps(); @@ -325,6 +325,8 @@ export class WaitStep extends Step { onStart(): void { super.onStart(); + } + onFirstRun(): void { // 设置上一个阶段的props到attribute const fromProps = this.getFromProps(); this.target.setAttributes(fromProps); diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index 3b5885e84..16d938490 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -164,7 +164,7 @@ export const NOWORK_ANIMATE_ATTR = { // return Reflect.get(target, property); // }, // set(target: any, property: any, value: any) { -// if (property === 'points' && value.length === 5 && value.every(item => item.y === 337)) { +// if (property === 'opacity' && obj.text === 'Nail polish') { // console.log('set', property, value); // } // // modifiedProperties.add(property); // 记录设置/修改操作 From 7b93689dba6358e3fc6522b97ed18d4323f96d21 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 15 Apr 2025 20:03:10 +0800 Subject: [PATCH 095/179] fix: fix issue with manual-ticker --- .../src/ticker/manual-ticker.ts | 42 +++++- packages/vrender-animate/src/timeline.ts | 1 - packages/vrender-core/src/core/stage.ts | 31 +++-- .../browser/src/pages/animate-tick.ts | 125 ++++++++++++++++++ .../__tests__/browser/src/pages/index.ts | 4 + 5 files changed, 192 insertions(+), 11 deletions(-) create mode 100644 packages/vrender/__tests__/browser/src/pages/animate-tick.ts diff --git a/packages/vrender-animate/src/ticker/manual-ticker.ts b/packages/vrender-animate/src/ticker/manual-ticker.ts index 78fa9d106..59cdc9bf5 100644 --- a/packages/vrender-animate/src/ticker/manual-ticker.ts +++ b/packages/vrender-animate/src/ticker/manual-ticker.ts @@ -1,4 +1,5 @@ -import type { ITickHandler, ITicker } from '@visactor/vrender-core'; +import type { IStage } from '@visactor/vrender-core'; +import { STATUS, type ITickHandler, type ITicker } from '@visactor/vrender-core'; import { DefaultTicker } from './default-ticker'; class ManualTickHandler implements ITickHandler { @@ -21,9 +22,25 @@ class ManualTickHandler implements ITickHandler { getTime(): number { return this.currentTime; } + + tickTo(time: number, cb: (handler: ITickHandler) => void): void { + if (this.startTime < 0) { + this.startTime = 0; + this.currentTime = 0; + } + const interval = time - this.currentTime; + this.tick(interval, cb); + } } export class ManualTicker extends DefaultTicker implements ITicker { + constructor(stage: IStage) { + super(stage); + // manualTicker 的 lastFrameTime 默认为 0 + // status 默认为 STATUS.RUNNING(不需要启动) + this.lastFrameTime = 0; + this.status = STATUS.RUNNING; + } protected setupTickHandler(): boolean { const handler: ITickHandler = new ManualTickHandler(); @@ -39,4 +56,27 @@ export class ManualTicker extends DefaultTicker implements ITicker { getTime(): number { return this.tickerHandler.getTime(); } + + tickAt(time: number): void { + this.tickTo(time); + } + + start(force = false) { + if (this.status === STATUS.RUNNING) { + return false; + } + if (!this.tickerHandler) { + return false; + } + if (!force) { + if (this.status === STATUS.PAUSE) { + return false; + } + if (this.ifCanStop()) { + return false; + } + } + this.status = STATUS.RUNNING; + return true; + } } diff --git a/packages/vrender-animate/src/timeline.ts b/packages/vrender-animate/src/timeline.ts index f3ae332f2..4a0d72bb8 100644 --- a/packages/vrender-animate/src/timeline.ts +++ b/packages/vrender-animate/src/timeline.ts @@ -3,7 +3,6 @@ import { Generator, type IAnimate, type ITimeline, AnimateStatus } from '@visact export class DefaultTimeline implements ITimeline { declare id: number; protected animates: IAnimate[] = []; - protected declare ticker: any; protected declare paused: boolean; // 添加必要的属性 diff --git a/packages/vrender-core/src/core/stage.ts b/packages/vrender-core/src/core/stage.ts index eb6ea4d70..8e84d0341 100644 --- a/packages/vrender-core/src/core/stage.ts +++ b/packages/vrender-core/src/core/stage.ts @@ -163,7 +163,7 @@ export class Stage extends Group implements IStage { return this.at(0) as unknown as ILayer; } - ticker: ITicker; + protected _ticker: ITicker; autoRender: boolean; autoRefresh: boolean; @@ -203,6 +203,19 @@ export class Stage extends Group implements IStage { // 随机分配一个rafId readonly rafId: number; + get ticker() { + return this._ticker; + } + + set ticker(ticker: ITicker) { + if (this._ticker) { + this._ticker.removeListener('tick', this.afterTickCb); + } + ticker.addTimeline(this.timeline); + this._ticker = ticker; + this._ticker.on('tick', this.afterTickCb); + } + /** * 所有属性都具有默认值。 * Canvas为字符串或者Canvas元素,那么默认图层就会绑定到这个Canvas上 @@ -304,19 +317,19 @@ export class Stage extends Group implements IStage { initAnimate(params: Partial) { if ((this as any).createTicker && (this as any).createTimeline) { - this.ticker = params.ticker || (this as any).createTicker(this); + this._ticker = params.ticker || (this as any).createTicker(this); if (this.params.optimize?.tickRenderMode === 'performance') { - this.ticker.setFPS(30); + this._ticker.setFPS(30); } this.timeline = (this as any).createTimeline(); - this.ticker.addTimeline(this.timeline); - this.ticker.on('tick', this.afterTickCb); + this._ticker.addTimeline(this.timeline); + this._ticker.on('tick', this.afterTickCb); } } startAnimate() { - if (this.ticker && this.timeline) { - this.ticker.start(); + if (this._ticker && this.timeline) { + this._ticker.start(); this.timeline.resume(); } } @@ -994,8 +1007,8 @@ export class Stage extends Group implements IStage { this.interactiveLayer.release(); } this.window.release(); - this.ticker?.remTimeline(this?.timeline); - this.ticker?.removeListener('afterTick', this.afterTickCb); + this._ticker?.remTimeline(this?.timeline); + this._ticker?.removeListener('tick', this.afterTickCb); this.renderService.renderTreeRoots = []; } diff --git a/packages/vrender/__tests__/browser/src/pages/animate-tick.ts b/packages/vrender/__tests__/browser/src/pages/animate-tick.ts new file mode 100644 index 000000000..755311374 --- /dev/null +++ b/packages/vrender/__tests__/browser/src/pages/animate-tick.ts @@ -0,0 +1,125 @@ +import { + DefaultTicker, + DefaultTimeline, + Animate, + registerAnimate, + IncreaseCount, + InputText, + AnimateExecutor, + ACustomAnimate, + registerCustomAnimate, + ManualTicker +} from '@visactor/vrender-animate'; +import { + container, + createRect, + createStage, + createSymbol, + IGraphic, + vglobal, + createCircle, + createText, + createGroup, + createLine, + createPath +} from '@visactor/vrender'; +import type { EasingType } from '@visactor/vrender-animate'; +// container.load(roughModule); + +vglobal.setEnv('browser'); + +registerAnimate(); +registerCustomAnimate(); + +let stage: any; + +function addCase(name: string, container: HTMLElement, cb: (stage: any) => void) { + const button = document.createElement('button'); + button.innerText = name; + button.style.height = '26px'; + container.appendChild(button); + button.addEventListener('click', () => { + stage && stage.release(); + stage = createStage({ + canvas: 'main', + width: 900, + height: 600, + background: 'pink', + disableDirtyBounds: false, + canvasControled: false, + autoRender: true + }); + cb(stage); + }); +} + +export const page = () => { + const btnContainer = document.createElement('div'); + btnContainer.style.width = '1000px'; + btnContainer.style.background = '#cecece'; + btnContainer.style.display = 'flex'; + btnContainer.style.flexDirection = 'row'; + btnContainer.style.gap = '3px'; + btnContainer.style.flexWrap = 'wrap'; + btnContainer.style.height = '120px'; + const canvas = document.getElementById('main'); + // 将btnContainer添加到canvas之前 + canvas.parentNode.insertBefore(btnContainer, canvas); + + // Basic animation state registration and application + addCase('Manual Ticker', btnContainer, stage => { + stage.ticker = new ManualTicker(stage); + // Create a rectangle to animate + const rect = createRect({ + x: 300, + y: 200, + width: 100, + height: 100, + fill: 'blue' + }); + rect.context = { id: 'rect1' }; + + console.log(rect); + + // Create control buttons + rect.applyAnimationState( + ['pulse'], + [ + { + name: 'pulse', + animation: { + timeSlices: [ + { + duration: 500, + effects: { + type: 'to', + channel: { + scaleX: { to: 1.8 }, + scaleY: { to: 1.8 } + }, + easing: 'linear' + } + }, + { + duration: 500, + effects: { + type: 'to', + channel: { + scaleX: { to: 1 }, + scaleY: { to: 1 } + }, + easing: 'linear' + } + } + ], + loop: true + } + } + ] + ); + stage.defaultLayer.add(rect); + + stage.ticker.tickAt(200); + // stage.ticker.tickAt(800); + }); +}; diff --git a/packages/vrender/__tests__/browser/src/pages/index.ts b/packages/vrender/__tests__/browser/src/pages/index.ts index 8ae5dd979..96fd11ae7 100644 --- a/packages/vrender/__tests__/browser/src/pages/index.ts +++ b/packages/vrender/__tests__/browser/src/pages/index.ts @@ -35,6 +35,10 @@ export const pages = [ name: 'story-animate', path: 'story-animate' }, + { + name: 'animate-tick', + path: 'animate-tick' + }, { name: '内存', path: 'memory' From ef35d89bdebad08fcd4bc93dcba7e1063053753a Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 17 Apr 2025 15:04:10 +0800 Subject: [PATCH 096/179] fix: fix issue with manual tick caused by checkSkip --- .../vrender-animate/src/ticker/default-ticker.ts | 10 +++++++--- .../vrender-animate/src/ticker/manual-ticker.ts | 16 +++++++++------- packages/vrender-core/src/core/stage.ts | 2 ++ .../src/interface/animation/ticker.ts | 3 +++ .../__tests__/browser/src/pages/animate-tick.ts | 6 ++++++ 5 files changed, 27 insertions(+), 10 deletions(-) diff --git a/packages/vrender-animate/src/ticker/default-ticker.ts b/packages/vrender-animate/src/ticker/default-ticker.ts index e277bd26e..fcf7778cd 100644 --- a/packages/vrender-animate/src/ticker/default-ticker.ts +++ b/packages/vrender-animate/src/ticker/default-ticker.ts @@ -44,7 +44,7 @@ export class DefaultTicker extends EventEmitter implements ITicker { declare _lastTickTime: number; protected frameTimeHistory: number[] = []; - constructor(stage: IStage) { + constructor(stage?: IStage) { super(); this.init(); this.lastFrameTime = -1; @@ -55,6 +55,10 @@ export class DefaultTicker extends EventEmitter implements ITicker { this.computeTimeOffsetAndJitter(); } + bindStage(stage: IStage): void { + this.stage = stage; + } + /** * 计算时间偏移和随机扰动 */ @@ -216,14 +220,14 @@ export class DefaultTicker extends EventEmitter implements ITicker { this.lastFrameTime = -1; } - protected checkSkip = (delta: number): boolean => { + protected checkSkip(delta: number): boolean { if (this.stage.params.optimize.tickRenderMode === 'performance') { return false; } // 随机扰动(每次都对interval进行随机的扰动,避免所有tick都发生在同一帧) const skip = delta < this.interval + (Math.random() - 0.5) * 2 * this._jitter; return skip; - }; + } protected handleTick = (handler: ITickHandler, params?: { once?: boolean }): boolean => { const { once = false } = params ?? {}; diff --git a/packages/vrender-animate/src/ticker/manual-ticker.ts b/packages/vrender-animate/src/ticker/manual-ticker.ts index 59cdc9bf5..56fc823a5 100644 --- a/packages/vrender-animate/src/ticker/manual-ticker.ts +++ b/packages/vrender-animate/src/ticker/manual-ticker.ts @@ -4,14 +4,13 @@ import { DefaultTicker } from './default-ticker'; class ManualTickHandler implements ITickHandler { protected released: boolean = false; - protected startTime: number = -1; protected currentTime: number = -1; tick(interval: number, cb: (handler: ITickHandler) => void): void { - if (this.startTime < 0) { - this.startTime = 0; + if (this.currentTime < 0) { + this.currentTime = 0; } - this.currentTime = this.startTime + interval; + this.currentTime += interval; cb(this); } @@ -24,8 +23,7 @@ class ManualTickHandler implements ITickHandler { } tickTo(time: number, cb: (handler: ITickHandler) => void): void { - if (this.startTime < 0) { - this.startTime = 0; + if (this.currentTime < 0) { this.currentTime = 0; } const interval = time - this.currentTime; @@ -34,7 +32,7 @@ class ManualTickHandler implements ITickHandler { } export class ManualTicker extends DefaultTicker implements ITicker { - constructor(stage: IStage) { + constructor(stage?: IStage) { super(stage); // manualTicker 的 lastFrameTime 默认为 0 // status 默认为 STATUS.RUNNING(不需要启动) @@ -53,6 +51,10 @@ export class ManualTicker extends DefaultTicker implements ITicker { return true; } + checkSkip(delta: number): boolean { + return false; + } + getTime(): number { return this.tickerHandler.getTime(); } diff --git a/packages/vrender-core/src/core/stage.ts b/packages/vrender-core/src/core/stage.ts index 8e84d0341..b15574f2f 100644 --- a/packages/vrender-core/src/core/stage.ts +++ b/packages/vrender-core/src/core/stage.ts @@ -208,6 +208,7 @@ export class Stage extends Group implements IStage { } set ticker(ticker: ITicker) { + ticker.bindStage(this); if (this._ticker) { this._ticker.removeListener('tick', this.afterTickCb); } @@ -318,6 +319,7 @@ export class Stage extends Group implements IStage { initAnimate(params: Partial) { if ((this as any).createTicker && (this as any).createTimeline) { this._ticker = params.ticker || (this as any).createTicker(this); + this._ticker.bindStage(this); if (this.params.optimize?.tickRenderMode === 'performance') { this._ticker.setFPS(30); } diff --git a/packages/vrender-core/src/interface/animation/ticker.ts b/packages/vrender-core/src/interface/animation/ticker.ts index c6c43ca34..497266498 100644 --- a/packages/vrender-core/src/interface/animation/ticker.ts +++ b/packages/vrender-core/src/interface/animation/ticker.ts @@ -4,6 +4,7 @@ import type { EventEmitter } from '@visactor/vutils'; import type { ITimeline } from './timeline'; +import type { IStage } from '../stage'; export type TickerMode = 'raf' | 'timeout' | 'manual'; @@ -50,6 +51,8 @@ export interface ITicker extends EventEmitter { getTimelines: () => ITimeline[]; release: () => void; + bindStage: (stage: IStage) => void; + // Whether to automatically stop, default is true autoStop: boolean; } diff --git a/packages/vrender/__tests__/browser/src/pages/animate-tick.ts b/packages/vrender/__tests__/browser/src/pages/animate-tick.ts index 755311374..6b71c5d4f 100644 --- a/packages/vrender/__tests__/browser/src/pages/animate-tick.ts +++ b/packages/vrender/__tests__/browser/src/pages/animate-tick.ts @@ -120,6 +120,12 @@ export const page = () => { stage.defaultLayer.add(rect); stage.ticker.tickAt(200); + setTimeout(() => { + stage.ticker.tickAt(300); + setTimeout(() => { + stage.ticker.tickAt(400); + }, 1000); + }, 1000); // stage.ticker.tickAt(800); }); }; From f1b7e52cef394726fb6479ed7beb97999d4995ed Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 17 Apr 2025 17:44:06 +0800 Subject: [PATCH 097/179] fix: fix issue with timeline --- packages/vrender-animate/src/timeline.ts | 105 +++++++++++++++++------ packages/vrender-core/src/core/stage.ts | 2 +- packages/vrender-core/src/index.ts | 8 -- 3 files changed, 81 insertions(+), 34 deletions(-) diff --git a/packages/vrender-animate/src/timeline.ts b/packages/vrender-animate/src/timeline.ts index 4a0d72bb8..70ec0df27 100644 --- a/packages/vrender-animate/src/timeline.ts +++ b/packages/vrender-animate/src/timeline.ts @@ -1,8 +1,18 @@ import { Generator, type IAnimate, type ITimeline, AnimateStatus } from '@visactor/vrender-core'; +// 定义链表节点 +interface AnimateNode { + animate: IAnimate; + next: AnimateNode | null; + prev: AnimateNode | null; +} + export class DefaultTimeline implements ITimeline { declare id: number; - protected animates: IAnimate[] = []; + protected head: AnimateNode | null = null; + protected tail: AnimateNode | null = null; + protected animateMap: Map = new Map(); + protected _animateCount: number = 0; protected declare paused: boolean; // 添加必要的属性 @@ -11,39 +21,57 @@ export class DefaultTimeline implements ITimeline { protected _startTime: number = 0; protected _currentTime: number = 0; - // 0 ... _endAnimatePtr ... animates.length - // [0, _endAnimatePtr] 表示正在运行的动画 - // (_endAnimatePtr, animates.length) 表示已经结束的动画 - protected _endAnimatePtr: number = -1; - declare isGlobal?: boolean; get animateCount() { - return this.animates.length; + return this._animateCount; } constructor() { this.id = Generator.GenAutoIncrementId(); - this.animates = []; this.paused = false; } isRunning() { - return !this.paused && this._endAnimatePtr >= 0; + return !this.paused && this._animateCount > 0; } forEachAccessAnimate(cb: (animate: IAnimate, index: number) => void) { - for (let i = 0; i <= this._endAnimatePtr; i++) { - cb(this.animates[i], i); + let current = this.head; + let index = 0; + + while (current) { + // 保存下一个节点的引用,以防在回调中移除当前节点 + const next = current.next; + cb(current.animate, index); + index++; + current = next; } } addAnimate(animate: IAnimate) { - this.animates.push(animate); - // 交换位置 - this._endAnimatePtr++; - this.animates[this.animates.length - 1] = this.animates[this._endAnimatePtr]; - this.animates[this._endAnimatePtr] = animate; + const newNode: AnimateNode = { + animate, + next: null, + prev: null + }; + + // 添加到链表尾部 + if (!this.head) { + this.head = newNode; + this.tail = newNode; + } else { + if (this.tail) { + this.tail.next = newNode; + newNode.prev = this.tail; + this.tail = newNode; + } + } + + // 在映射中保存节点引用 + this.animateMap.set(animate, newNode); + this._animateCount++; + // 更新总时长 this._totalDuration = Math.max(this._totalDuration, animate.getStartTime() + animate.getDuration()); } @@ -69,7 +97,7 @@ export class DefaultTimeline implements ITimeline { this.forEachAccessAnimate((animate, i) => { if (animate.status === AnimateStatus.END) { - this.removeAnimate(animate, true, i); + this.removeAnimate(animate, true); } else if (animate.status === AnimateStatus.RUNNING || animate.status === AnimateStatus.INITIAL) { animate.advance(scaledDelta); } @@ -80,30 +108,57 @@ export class DefaultTimeline implements ITimeline { this.forEachAccessAnimate(animate => { animate.release(); }); - this.animates = []; + + this.head = null; + this.tail = null; + this.animateMap.clear(); + this._animateCount = 0; this._totalDuration = 0; } - removeAnimate(animate: IAnimate, release: boolean = true, index?: number) { - if (this._endAnimatePtr < 0) { + removeAnimate(animate: IAnimate, release: boolean = true) { + const node = this.animateMap.get(animate); + + if (!node) { return; } + if (release) { animate._onRemove && animate._onRemove.forEach(cb => cb()); animate.release(); } - index = index ?? this.animates.indexOf(animate); - // 交换位置 - this.animates[index] = this.animates[this._endAnimatePtr]; - this._endAnimatePtr--; + // 从链表中移除节点 + if (node.prev) { + node.prev.next = node.next; + } else { + // 节点是头节点 + this.head = node.next; + } + + if (node.next) { + node.next.prev = node.prev; + } else { + // 节点是尾节点 + this.tail = node.prev; + } + + // 从映射中移除 + this.animateMap.delete(animate); + this._animateCount--; + + // 如果移除的是最长时间的动画,应该重新计算总时长 + if (animate.getStartTime() + animate.getDuration() >= this._totalDuration) { + this.recalculateTotalDuration(); + } + return; } // 重新计算总时长 protected recalculateTotalDuration() { this._totalDuration = 0; - this.animates.forEach(animate => { + this.forEachAccessAnimate(animate => { this._totalDuration = Math.max(this._totalDuration, animate.getStartTime() + animate.getDuration()); }); } diff --git a/packages/vrender-core/src/core/stage.ts b/packages/vrender-core/src/core/stage.ts index b15574f2f..7859040c1 100644 --- a/packages/vrender-core/src/core/stage.ts +++ b/packages/vrender-core/src/core/stage.ts @@ -499,7 +499,7 @@ export class Stage extends Group implements IStage { protected afterTickCb = () => { this.tickedBeforeRender = true; // 性能模式不用立刻渲染 - this.state !== 'rendering' && this.render(); + this.state !== 'rendering' && this.renderNextFrame(); }; setBeforeRender(cb: (stage: IStage) => void) { diff --git a/packages/vrender-core/src/index.ts b/packages/vrender-core/src/index.ts index f2f10b795..0e1830689 100644 --- a/packages/vrender-core/src/index.ts +++ b/packages/vrender-core/src/index.ts @@ -103,11 +103,3 @@ export * from './plugins/builtin-plugin/3dview-transform-plugin'; export * from './plugins/builtin-plugin/flex-layout-plugin'; export * from './plugins/builtin-plugin/edit-module'; - -export const morphPath = {}; -export const multiToOneMorph = {}; -export const oneToMultiMorph = {}; -export class ACustomAnimate {} -export const AnimateGroup = {}; -export const Animate = {}; -export const defaultTicker = {}; From 50ec32a386f3c201b2479ae97001fe4ae313b28b Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 17 Apr 2025 21:32:47 +0800 Subject: [PATCH 098/179] feat: support PulseAnimate and add stroke effect for InputRichText --- .../vrender-animate/src/custom/register.ts | 13 +- .../src/custom/richtext/input-richtext.ts | 84 ++- packages/vrender-animate/src/custom/story.ts | 382 ++++++++++- packages/vrender-core/src/index.ts | 8 + .../browser/src/pages/custom-animate.ts | 127 ++++ .../browser/src/pages/story-animate.ts | 597 +++++++++++++++++- 6 files changed, 1177 insertions(+), 34 deletions(-) diff --git a/packages/vrender-animate/src/custom/register.ts b/packages/vrender-animate/src/custom/register.ts index fb6876709..9fbbda35f 100644 --- a/packages/vrender-animate/src/custom/register.ts +++ b/packages/vrender-animate/src/custom/register.ts @@ -31,10 +31,13 @@ import { MoveRotateOut, MoveScaleIn, MoveScaleOut, + PulseAnimate, SlideIn, SlideOut, SpinIn, - SpinOut + SpinOut, + StrokeIn, + StrokeOut } from './story'; import { Update } from './update'; import { MoveIn, MoveOut } from './move'; @@ -98,6 +101,7 @@ export const registerCustomAnimate = () => { AnimateExecutor.registerBuiltInAnimate('spinIn', SpinIn); AnimateExecutor.registerBuiltInAnimate('moveScaleIn', MoveScaleIn); AnimateExecutor.registerBuiltInAnimate('moveRotateIn', MoveRotateIn); + AnimateExecutor.registerBuiltInAnimate('strokeIn', StrokeIn); // 故事化动画 - 出场 AnimateExecutor.registerBuiltInAnimate('slideOut', SlideOut); @@ -105,6 +109,10 @@ export const registerCustomAnimate = () => { AnimateExecutor.registerBuiltInAnimate('spinOut', SpinOut); AnimateExecutor.registerBuiltInAnimate('moveScaleOut', MoveScaleOut); AnimateExecutor.registerBuiltInAnimate('moveRotateOut', MoveRotateOut); + AnimateExecutor.registerBuiltInAnimate('strokeOut', StrokeOut); + + // 特效动画 + AnimateExecutor.registerBuiltInAnimate('pulse', PulseAnimate); // 路径动画 AnimateExecutor.registerBuiltInAnimate('MotionPath', MotionPath); @@ -162,6 +170,9 @@ export { SpinOut, MoveScaleOut, MoveRotateOut, + StrokeIn, + StrokeOut, + PulseAnimate, GroupFadeIn, GroupFadeOut, FromTo, diff --git a/packages/vrender-animate/src/custom/richtext/input-richtext.ts b/packages/vrender-animate/src/custom/richtext/input-richtext.ts index b5bfe1528..aa357d65e 100644 --- a/packages/vrender-animate/src/custom/richtext/input-richtext.ts +++ b/packages/vrender-animate/src/custom/richtext/input-richtext.ts @@ -13,6 +13,7 @@ import { RichText } from '@visactor/vrender-core'; * 支持通过beforeText和afterText参数添加前缀和后缀 * 支持通过showCursor参数显示光标,cursorChar自定义光标字符 * 支持通过fadeInChars参数开启字符透明度渐变效果 + * 支持通过strokeFirst参数开启描边先于填充显示效果,使用文字自身颜色作为描边色 */ export class InputRichText extends ACustomAnimate<{ textConfig: IRichTextCharacter[] }> { declare valid: boolean; @@ -23,10 +24,10 @@ export class InputRichText extends ACustomAnimate<{ textConfig: IRichTextCharact private showCursor: boolean = false; private cursorChar: string = '|'; private blinkCursor: boolean = true; - private beforeText: string = ''; - private afterText: string = ''; private fadeInChars: boolean = false; private fadeInDuration: number = 0.3; // 透明度渐变持续时间,以动画总时长的比例表示 + private strokeFirst: boolean = false; // 是否开启描边先于填充显示效果 + private strokeToFillRatio: number = 0.3; // 描边到填充的过渡比例,占总动画时长的比例 constructor( from: { textConfig: IRichTextCharacter[] }, @@ -41,6 +42,8 @@ export class InputRichText extends ACustomAnimate<{ textConfig: IRichTextCharact afterText?: string; fadeInChars?: boolean; fadeInDuration?: number; + strokeFirst?: boolean; + strokeToFillRatio?: number; } ) { super(from, to, duration, easing, params); @@ -56,14 +59,6 @@ export class InputRichText extends ACustomAnimate<{ textConfig: IRichTextCharact this.blinkCursor = params.blinkCursor; } - // 配置前缀和后缀文本 - if (params?.beforeText !== undefined) { - this.beforeText = params.beforeText; - } - if (params?.afterText !== undefined) { - this.afterText = params.afterText; - } - // 配置字符透明度渐变效果 if (params?.fadeInChars !== undefined) { this.fadeInChars = params.fadeInChars; @@ -71,6 +66,14 @@ export class InputRichText extends ACustomAnimate<{ textConfig: IRichTextCharact if (params?.fadeInDuration !== undefined) { this.fadeInDuration = params.fadeInDuration; } + + // 配置描边先于填充显示效果 + if (params?.strokeFirst !== undefined) { + this.strokeFirst = params.strokeFirst; + } + if (params?.strokeToFillRatio !== undefined) { + this.strokeToFillRatio = params.strokeToFillRatio; + } } onFirstRun(): void { @@ -144,27 +147,48 @@ export class InputRichText extends ACustomAnimate<{ textConfig: IRichTextCharact // 删除动画:显示from的前currentLength项 currentTextConfig = this.fromTextConfig.slice(0, currentLength); } else { - // 添加文本动画:显示to的前currentLength项,可能需要应用透明度 + // 添加文本动画:显示to的前currentLength项,可能需要应用透明度和描边效果 currentTextConfig = this.toTextConfig.slice(0, currentLength).map((item, index) => { - // 如果启用了透明度渐变效果 - if (this.fadeInChars && 'text' in item) { - // 计算每个字符从出现到结束的渐变进度 - // 字符在特定时间点出现:出现时刻 = (index / totalItems) * maxTextShowRatio - // 当前时刻 = ratio - // 渐变持续时间 = fadeInDuration - // 渐变进度 = (当前时刻 - 出现时刻) / 渐变持续时间 - - const appearTime = (index / totalItems) * maxTextShowRatio; - const fadeProgress = (ratio - appearTime) / this.fadeInDuration; - - // 限制透明度在0-1范围内 - const opacity = Math.max(0, Math.min(1, fadeProgress)); - - // 如果是文本项,添加透明度 - return { - ...item, - opacity: opacity - }; + // 如果是文本项并且需要应用效果 + if ('text' in item) { + const newItem = { ...item }; + + // 如果启用了描边优先效果 + if (this.strokeFirst) { + // 计算描边到填充的过渡进度 + // 字符在特定时间点出现:出现时刻 = (index / totalItems) * maxTextShowRatio + const appearTime = (index / totalItems) * maxTextShowRatio; + const itemLifetime = Math.max(0, ratio - appearTime); // 当前字符已经存在的时间 + const maxLifetime = 1 - appearTime; // 当前字符从出现到动画结束的最大时间 + const fillProgress = Math.min(1, itemLifetime / (this.strokeToFillRatio * maxLifetime)); + + // 使用文本自身的填充颜色作为描边颜色 + if ('fill' in newItem && newItem.fill) { + newItem.stroke = newItem.fill; + // 计算描边宽度,基于字体大小 + // const fontSize = newItem.fontSize || 16; + // newItem.lineWidth = Math.max(1, fontSize * 0.05); // 线宽大约为字体大小的5% + + // 如果还没到填充阶段,则将填充色透明度设为0 + if (fillProgress < 1) { + newItem.fillOpacity = fillProgress; + } + } + + // 如果也启用了透明度渐变 + if (this.fadeInChars) { + const fadeProgress = Math.min(1, itemLifetime / (this.fadeInDuration * maxLifetime)); + newItem.opacity = Math.max(0, Math.min(1, fadeProgress)); + } + } + // 只启用了透明度渐变效果,没有启用描边优先 + else if (this.fadeInChars) { + const appearTime = (index / totalItems) * maxTextShowRatio; + const fadeProgress = (ratio - appearTime) / this.fadeInDuration; + newItem.opacity = Math.max(0, Math.min(1, fadeProgress)); + } + + return newItem; } return item; }); diff --git a/packages/vrender-animate/src/custom/story.ts b/packages/vrender-animate/src/custom/story.ts index 659f49a92..75b22200d 100644 --- a/packages/vrender-animate/src/custom/story.ts +++ b/packages/vrender-animate/src/custom/story.ts @@ -1,7 +1,8 @@ import { FadeIn } from './fade'; -import type { EasingType } from '@visactor/vrender-core'; +import type { EasingType, IGraphicAttribute } from '@visactor/vrender-core'; import { ACustomAnimate } from './custom-animate'; import { AnimateExecutor } from '../executor/animate-executor'; +import { ColorStore, ColorType, interpolateColor } from '@visactor/vrender-core'; export class StoryFadeIn extends FadeIn {} @@ -26,6 +27,32 @@ export interface ISpinAnimationOptions { fromOpacity?: number; // 透明度初始值,默认0 } +// 描边动画的参数接口 +export interface IStrokeAnimationOptions { + lineWidth?: number; // 描边宽度,默认2 + strokeColor?: string; // 描边颜色,默认黑色 + fromOpacity?: number; // 透明度初始值,默认1 + dashLength?: number; // 虚线长度,默认为元素周长 + showFill?: boolean; // 是否显示填充,默认false + fillOpacity?: number; // 填充透明度,仅当showFill为true时有效 +} + +// 脉冲/强调动画的参数接口 +export interface IPulseAnimationOptions { + pulseCount?: number; // 脉冲次数 + pulseOpacity?: number; // 脉冲透明度,默认0.3 + pulseScale?: number; // 缩放比例,默认1.05 + pulseColor?: string; // 脉冲颜色,默认为元素自身颜色 + pulseColorIntensity?: number; // 脉冲颜色强度,0-1,默认0.2 + strokeOnly?: boolean; // 是否只应用于描边,默认false + fillOnly?: boolean; // 是否只应用于填充,默认false + useScale?: boolean; // 是否使用缩放效果,默认true + useOpacity?: boolean; // 是否使用透明度效果,默认true + useColor?: boolean; // 是否使用颜色效果,默认false + useStroke?: boolean; // 是否使用描边效果,默认true + useFill?: boolean; // 是否使用填充效果,默认true +} + /** * 滑动入场动画,包括从上到下,从下到上,从左到右,从右到左的位置,以及透明度属性插值 */ @@ -206,6 +233,206 @@ export class SpinIn extends ACustomAnimate> { } } +/** + * 描边入场动画,使用lineDashOffset实现描边效果 + * 通过调整虚线偏移量,创建线条逐渐显示的动画效果 + */ +export class StrokeIn extends ACustomAnimate> { + declare valid: boolean; + declare propKeys: string[]; + declare from: Record; + declare to: Record; + private perimeter: number = 0; + private originalAttributes: Record = {}; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: IStrokeAnimationOptions) { + super(from, to, duration, easing, params); + } + + onBind(): void { + super.onBind(); + // 保存原始属性 + this.originalAttributes = { ...this.target.getAttributes() }; + + // 获取图形周长 + if (this.target.type === 'rect') { + const attr = this.target.attribute as any; + const width = attr.width ?? 100; + const height = attr.height ?? 100; + this.perimeter = 2 * (width + height); + } else if (this.target.type === 'circle') { + const attr = this.target.attribute as any; + const radius = attr.radius ?? 50; + this.perimeter = 2 * Math.PI * radius; + } else if (this.target.type === 'ellipse') { + const attr = this.target.attribute as any; + const radiusX = attr.radiusX ?? 50; + const radiusY = attr.radiusY ?? 50; + // 椭圆周长近似计算 + this.perimeter = 2 * Math.PI * Math.sqrt((radiusX * radiusX + radiusY * radiusY) / 2); + } else { + // 对于其他形状,使用默认值 + this.perimeter = 1000; + } + + const lineWidth = this.params?.lineWidth ?? 2; + const strokeColor = this.params?.strokeColor ?? 'black'; + const fromOpacity = this.params?.fromOpacity ?? 1; + const dashLength = this.params?.dashLength ?? this.perimeter; + const showFill = this.params?.showFill ?? false; + const fillOpacity = this.params?.fillOpacity ?? 0; + + // 设置初始状态 + this.from = { + lineDash: [dashLength, dashLength], + lineDashOffset: dashLength, + lineWidth, + stroke: strokeColor, + strokeOpacity: fromOpacity + }; + + // 设置目标状态 + this.to = { + lineDash: [dashLength, dashLength], + lineDashOffset: 0, + lineWidth, + stroke: strokeColor, + strokeOpacity: fromOpacity + }; + + // 如果需要显示填充,添加填充相关属性 + if (showFill) { + this.from.fillOpacity = fillOpacity; + this.to.fillOpacity = this.originalAttributes.fillOpacity ?? 1; + } else { + this.from.fillOpacity = 0; + this.to.fillOpacity = 0; + } + + this.propKeys = ['lineDash', 'lineDashOffset', 'lineWidth', 'stroke', 'strokeOpacity', 'fillOpacity']; + this.props = this.to; + + // 应用初始属性 + this.target.setAttributes(this.from); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; + + // 更新lineDashOffset + attribute.lineDashOffset = this.from.lineDashOffset + (this.to.lineDashOffset - this.from.lineDashOffset) * ratio; + + // 更新fillOpacity (如果需要显示填充) + if (this.params?.showFill) { + attribute.fillOpacity = this.from.fillOpacity + (this.to.fillOpacity - this.from.fillOpacity) * ratio; + } + } + + onEnd(): void { + super.onEnd(); + // 动画结束后,是否要恢复原始属性 + if (!this.params?.showFill) { + // 如果不显示填充,恢复原始的stroke属性但保持fillOpacity为0 + const originalAttrs = { ...this.originalAttributes }; + originalAttrs.fillOpacity = 0; + this.target.setAttributes(originalAttrs); + } + } +} + +/** + * 描边出场动画,使用lineDashOffset实现描边消失效果 + */ +export class StrokeOut extends ACustomAnimate> { + declare valid: boolean; + declare propKeys: string[]; + declare from: Record; + declare to: Record; + private perimeter: number = 0; + private originalAttributes: Record = {}; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: IStrokeAnimationOptions) { + super(from, to, duration, easing, params); + } + + onFirstRun(): void { + // 保存原始属性 + this.originalAttributes = { ...this.target.getAttributes() }; + + // 获取图形周长 + if (this.target.type === 'rect') { + const attr = this.target.attribute as any; + const width = attr.width ?? 100; + const height = attr.height ?? 100; + this.perimeter = 2 * (width + height); + } else if (this.target.type === 'circle') { + const attr = this.target.attribute as any; + const radius = attr.radius ?? 50; + this.perimeter = 2 * Math.PI * radius; + } else if (this.target.type === 'ellipse') { + const attr = this.target.attribute as any; + const radiusX = attr.radiusX ?? 50; + const radiusY = attr.radiusY ?? 50; + // 椭圆周长近似计算 + this.perimeter = 2 * Math.PI * Math.sqrt((radiusX * radiusX + radiusY * radiusY) / 2); + } else { + // 对于其他形状,使用默认值 + this.perimeter = 1000; + } + + const lineWidth = this.params?.lineWidth ?? 2; + const strokeColor = this.params?.strokeColor ?? 'black'; + const fromOpacity = this.params?.fromOpacity ?? 1; + const dashLength = this.params?.dashLength ?? this.perimeter; + const showFill = this.params?.showFill ?? false; + + // 设置初始状态 - 完全显示的描边 + this.from = { + lineDash: [dashLength, dashLength], + lineDashOffset: 0, + lineWidth, + stroke: strokeColor, + strokeOpacity: fromOpacity + }; + + // 设置目标状态 - 完全消失的描边 + this.to = { + lineDash: [dashLength, dashLength], + lineDashOffset: -dashLength, + lineWidth, + stroke: strokeColor, + strokeOpacity: fromOpacity + }; + + // 处理填充 + if (showFill) { + this.from.fillOpacity = this.originalAttributes.fillOpacity ?? 1; + this.to.fillOpacity = 0; + } else { + this.from.fillOpacity = 0; + this.to.fillOpacity = 0; + } + + this.propKeys = ['lineDash', 'lineDashOffset', 'lineWidth', 'stroke', 'strokeOpacity', 'fillOpacity']; + this.props = this.to; + + // 应用初始属性 + this.target.setAttributes(this.from); + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; + + // 更新lineDashOffset + attribute.lineDashOffset = this.from.lineDashOffset + (this.to.lineDashOffset - this.from.lineDashOffset) * ratio; + + // 更新fillOpacity (如果有) + if (this.params?.showFill) { + attribute.fillOpacity = this.from.fillOpacity + (this.to.fillOpacity - this.from.fillOpacity) * ratio; + } + } +} + // 复合动画的参数接口 export interface IMoveScaleAnimationOptions { slideDirection?: 'top' | 'bottom' | 'left' | 'right'; @@ -630,3 +857,156 @@ export class MoveRotateOut extends ACustomAnimate { // 动画逻辑由子动画处理 } } + +/** + * 脉冲/强调动画,通过循环变化透明度、颜色和缩放来吸引注意力 + */ +export class PulseAnimate extends ACustomAnimate> { + declare valid: boolean; + private originalAttributes: Record = {}; + private pulseCount: number = 3; // 默认3次脉冲 + private pulseOpacity: number = 0.3; + private pulseScale: number = 1.05; + private pulseColor: string | null = null; + private pulseColorIntensity: number = 0.2; + private strokeOnly: boolean = false; + private fillOnly: boolean = false; + private useScale: boolean = true; + private useOpacity: boolean = true; + private useStroke: boolean = true; + private useFill: boolean = true; + private useColor: boolean = false; + private originalFill: string | null = null; + private originalStroke: string | null = null; + + constructor(from: null, to: null, duration: number, easing: EasingType, params?: IPulseAnimationOptions) { + super(from, to, duration, easing, params); + + // 配置脉冲参数 + if (params?.pulseCount !== undefined) { + this.pulseCount = params.pulseCount; + } + if (params?.pulseScale !== undefined) { + this.pulseScale = params.pulseScale; + } + if (params?.pulseColor !== undefined) { + this.pulseColor = params.pulseColor; + } + if (params?.pulseColorIntensity !== undefined) { + this.pulseColorIntensity = params.pulseColorIntensity; + } + if (params?.strokeOnly !== undefined) { + this.strokeOnly = params.strokeOnly; + } + if (params?.fillOnly !== undefined) { + this.fillOnly = params.fillOnly; + } + if (params?.useScale !== undefined) { + this.useScale = params.useScale; + } + if (params?.useOpacity !== undefined) { + this.useOpacity = params.useOpacity; + } + if (params?.useStroke !== undefined) { + this.useStroke = params.useStroke; + } + if (params?.useFill !== undefined) { + this.useFill = params.useFill; + } + if (params?.useColor !== undefined) { + this.useColor = params.useColor; + } + } + + onBind(): void { + super.onBind(); + // 保存原始属性 + this.originalAttributes = { ...this.target.getAttributes() }; + + // 保存颜色相关的属性 + if (this.useColor) { + this.originalFill = this.originalAttributes.fill || null; + this.originalStroke = this.originalAttributes.stroke || null; + + // 如果没有指定脉冲颜色,使用元素自身的颜色 + if (!this.pulseColor) { + if (this.fillOnly && this.originalFill) { + this.pulseColor = this.originalFill; + } else if (this.strokeOnly && this.originalStroke) { + this.pulseColor = this.originalStroke; + } else if (this.originalFill) { + this.pulseColor = this.originalFill; + } else if (this.originalStroke) { + this.pulseColor = this.originalStroke; + } else { + this.pulseColor = '#FFFFFF'; // 默认白色 + } + } + } + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + // 使用ratio计算脉冲效果 + // ratio从0到1表示整个动画的进度 + + // 计算脉冲值:通过将单个进度映射到多个脉冲周期 + // 将0-1的ratio映射到0到pulseCount*2*PI的角度,用于sin函数 + const angle = ratio * Math.PI * this.pulseCount; + // 将sin值(-1到1)映射到0到1的范围 + const pulseValue = Math.abs(Math.sin(angle)); + + // 应用属性 + const attribute: Record = this.target.attribute; + + // 应用透明度 pulse + if (this.useOpacity) { + // 确保即使是最小值也是基于原始透明度的百分比 + const opacity = 1 + (this.pulseOpacity - 1) * pulseValue; + if (this.useStroke) { + attribute.strokeOpacity = (this.originalAttributes.strokeOpacity || 1) * opacity; + } + if (this.useFill) { + attribute.fillOpacity = (this.originalAttributes.fillOpacity || 1) * opacity; + } + } + + // 应用缩放脉冲 + if (this.useScale) { + // 计算缩放比例: 从1到pulseScale之间变化 + const scale = 1 + (this.pulseScale - 1) * pulseValue; + attribute.scaleX = (this.originalAttributes.scaleX || 1) * scale; + attribute.scaleY = (this.originalAttributes.scaleY || 1) * scale; + } + + // 应用颜色脉冲 + if (this.useColor && this.pulseColor) { + this.applyColorPulse(attribute, pulseValue); + } + + // 确保更新渲染 + this.target.addUpdateShapeAndBoundsTag(); + this.target.addUpdatePositionTag(); + } + + // 应用颜色脉冲 + private applyColorPulse(attribute: Record, pulseValue: number): void { + // 根据pulseColorIntensity调整颜色变化强度 + const colorRatio = this.pulseColorIntensity * pulseValue; + + // 应用填充颜色脉冲 + if (this.useFill && this.originalFill && this.pulseColor) { + attribute.fill = interpolateColor(this.originalFill, this.pulseColor, colorRatio, true); + } + + // 应用描边颜色脉冲 + if (this.useStroke && this.originalStroke && this.pulseColor) { + attribute.stroke = interpolateColor(this.originalStroke, this.pulseColor, colorRatio, true); + } + } + + onEnd(): void { + super.onEnd(); + // 恢复原始属性 + this.target.setAttributes(this.originalAttributes); + } +} diff --git a/packages/vrender-core/src/index.ts b/packages/vrender-core/src/index.ts index 0e1830689..f2f10b795 100644 --- a/packages/vrender-core/src/index.ts +++ b/packages/vrender-core/src/index.ts @@ -103,3 +103,11 @@ export * from './plugins/builtin-plugin/3dview-transform-plugin'; export * from './plugins/builtin-plugin/flex-layout-plugin'; export * from './plugins/builtin-plugin/edit-module'; + +export const morphPath = {}; +export const multiToOneMorph = {}; +export const oneToMultiMorph = {}; +export class ACustomAnimate {} +export const AnimateGroup = {}; +export const Animate = {}; +export const defaultTicker = {}; diff --git a/packages/vrender/__tests__/browser/src/pages/custom-animate.ts b/packages/vrender/__tests__/browser/src/pages/custom-animate.ts index 44451336a..f47feec5b 100644 --- a/packages/vrender/__tests__/browser/src/pages/custom-animate.ts +++ b/packages/vrender/__tests__/browser/src/pages/custom-animate.ts @@ -691,5 +691,132 @@ export const page = () => { }, 4500); }); + // Test case for InputRichText with stroke animation + addCase('InputRichText - Stroke First Animation', container, stage => { + // Create a richText with empty textConfig + const richText = createRichText({ + x: 20, + y: 80, + width: 600, + height: 100, + textConfig: [], + textBaseline: 'middle' + }); + + // Create a group and add the richText to it + const group = createGroup({}); + group.add(richText); + stage.defaultLayer.add(group); + stage.render(); + + // Define the final textConfig with different styles + const finalTextConfig = [ + { + text: 'Hello, ', + fontSize: 40, + fill: '#FF5500', + stroke: '#FF5500', + lineWidth: 2, + fontWeight: 'bold' + }, + { + text: 'Manim', + fontSize: 40, + fill: '#0055FF', + stroke: '#0055FF', + lineWidth: 2, + fontStyle: 'italic' + }, + { + text: '!', + fontSize: 40, + fill: '#FF5500', + stroke: '#FF5500', + lineWidth: 2, + fontWeight: 'bold' + } + ]; + + // Create an AnimateExecutor and run the animation + const executor = new AnimateExecutor(group); + executor.execute({ + type: 'inputRichText', + to: { textConfig: finalTextConfig }, + customParameters: { + showCursor: true, + cursorChar: '|', + blinkCursor: true, + strokeFirst: true, + strokeToFillRatio: 0 + }, + duration: 3000, + easing: 'linear' + }); + }); + + // Test case with a more complex stroke first animation + addCase('InputRichText - Complex Stroke Animation', container, stage => { + // Black background to better show the effect + stage.background = '#000000'; + + // Create a richText with empty textConfig + const richText = createRichText({ + x: 250, + y: 200, + width: 600, + height: 200, + textConfig: [], + textAlign: 'center', + textBaseline: 'middle' + }); + + stage.defaultLayer.add(richText); + stage.render(); + + // Define the final textConfig with different styles + const finalTextConfig = [ + { + text: 'Hello, ', + fontSize: 60, + fill: '#FFFFFF', + stroke: '#FFFFFF', + lineWidth: 1, + fontWeight: 'bold' + }, + { + text: 'Manim', + fontSize: 60, + fill: '#3A86FF', + stroke: '#3A86FF', + lineWidth: 1, + fontWeight: 'bold' + }, + { + text: '!', + fontSize: 60, + fill: '#FF006E', + stroke: '#FF006E', + lineWidth: 1, + fontWeight: 'bold' + } + ]; + + // Create an AnimateExecutor and run the animation + const executor = new AnimateExecutor(richText); + executor.execute({ + type: 'inputRichText', + to: { textConfig: finalTextConfig }, + customParameters: { + showCursor: false, + strokeFirst: true, + strokeToFillRatio: 1, + fadeInChars: true, + fadeInDuration: 0.2 + }, + duration: 4000, + easing: 'quadOut' + }); + }); + return container; }; diff --git a/packages/vrender/__tests__/browser/src/pages/story-animate.ts b/packages/vrender/__tests__/browser/src/pages/story-animate.ts index b2a37b7af..5a8881225 100644 --- a/packages/vrender/__tests__/browser/src/pages/story-animate.ts +++ b/packages/vrender/__tests__/browser/src/pages/story-animate.ts @@ -22,7 +22,7 @@ import { createLine, createPath } from '@visactor/vrender'; -import type { EasingType } from '@visactor/vrender-animate'; +import type { EasingType } from '@visactor/vrender-core'; // container.load(roughModule); vglobal.setEnv('browser'); @@ -60,7 +60,7 @@ export const page = () => { btnContainer.style.flexDirection = 'row'; btnContainer.style.gap = '3px'; btnContainer.style.flexWrap = 'wrap'; - btnContainer.style.height = '160px'; + btnContainer.style.height = '230px'; const canvas = document.getElementById('main'); // 将btnContainer添加到canvas之前 canvas.parentNode.insertBefore(btnContainer, canvas); @@ -1058,4 +1058,597 @@ export const page = () => { delay: 2300 // Wait for spin to complete }); }); + + // New: StrokeIn Animation Demo + addCase('StrokeIn - Rectangle', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: 'red', + stroke: '#FF4D4F', + cornerRadius: 10, + fillOpacity: 0 // Start with no fill + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'strokeIn', + customParameters: { + lineWidth: 3, + strokeColor: '#FF4D4F', + showFill: true, + fillOpacity: 0 + }, + duration: 1500, + easing: 'quadOut' + }); + }); + + addCase('StrokeIn - Circle', btnContainer, stage => { + const circle = createCircle({ + x: 400, + y: 200, + radius: 50, + fill: '#52C41A', + fillOpacity: 0 // Start with no fill + }); + stage.defaultLayer.add(circle); + + const executor = new AnimateExecutor(circle); + executor.execute({ + type: 'strokeIn', + customParameters: { + lineWidth: 4, + strokeColor: '#722ED1', + showFill: true + }, + duration: 1500, + easing: 'cubicOut' + }); + }); + + // StrokeOut Animation Demo + addCase('StrokeOut - Rectangle', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#1890FF', + stroke: '#FF4D4F', + lineWidth: 3, + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'strokeOut', + customParameters: { + lineWidth: 3, + strokeColor: '#FF4D4F', + showFill: true + }, + duration: 1500, + easing: 'quadOut' + }); + }); + + // Combined Animations with Stroke + addCase('StrokeIn + GrowIn', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#1890FF', + cornerRadius: 10, + fillOpacity: 0 // Start with no fill + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + // First do the stroke animation + executor.execute({ + type: 'strokeIn', + customParameters: { + lineWidth: 3, + strokeColor: '#FF4D4F' + }, + duration: 1000, + easing: 'quadOut' + }); + + // Then do the fill grow animation + executor.execute({ + type: 'growIn', + customParameters: { + fromScale: 1, + direction: 'xy', + fromOpacity: 0 + }, + duration: 800, + easing: 'quadOut', + delay: 1000 // Start after stroke animation completes + }); + }); + + // Path StrokeIn Animation + addCase('StrokeIn - Path', btnContainer, stage => { + // Create a simple path (e.g., a triangle) + const path = createPath({ + x: 400, + y: 200, + path: 'M 0 -50 L 50 50 L -50 50 Z', + fill: '#FA8C16', + fillOpacity: 0 + }); + stage.defaultLayer.add(path); + + const executor = new AnimateExecutor(path); + executor.execute({ + type: 'strokeIn', + customParameters: { + lineWidth: 4, + strokeColor: '#EB2F96', + dashLength: 300, // Custom dash length for the path + showFill: true + }, + duration: 2000, + easing: 'elasticOut' + }); + }); + + // Text with StrokeIn Animation + addCase('StrokeIn - Text', btnContainer, stage => { + const text = createText({ + x: 400, + y: 200, + text: 'Hello World', + fontSize: 40, + fontWeight: 'bold', + fill: '#1D39C4', + fillOpacity: 0, + textAlign: 'center', + textBaseline: 'middle' + }); + stage.defaultLayer.add(text); + + const executor = new AnimateExecutor(text); + executor.execute({ + type: 'strokeIn', + customParameters: { + lineWidth: 2, + strokeColor: '#1D39C4', + dashLength: 800, // Longer dash length for text + showFill: true + }, + duration: 2000, + easing: 'cubicOut' + }); + }); + + // Sequence demo with multiple shapes using StrokeIn + addCase('Sequence - StrokeIn', btnContainer, stage => { + // Create multiple shapes in a row + const positions = [150, 300, 450, 600]; + const colors = ['#1890FF', '#52C41A', '#FA8C16', '#EB2F96']; + const shapes = []; + + // Create different shapes + shapes.push( + createRect({ + x: positions[0], + y: 200, + width: 80, + height: 80, + fill: colors[0], + fillOpacity: 0, + cornerRadius: 10 + }) + ); + + shapes.push( + createCircle({ + x: positions[1], + y: 200, + radius: 40, + fill: colors[1], + fillOpacity: 0 + }) + ); + + shapes.push( + createRect({ + x: positions[2], + y: 200, + width: 80, + height: 80, + fill: colors[2], + fillOpacity: 0 + }) + ); + + shapes.push( + createCircle({ + x: positions[3], + y: 200, + radius: 40, + fill: colors[3], + fillOpacity: 0 + }) + ); + + // Add all shapes to the stage + shapes.forEach(shape => stage.defaultLayer.add(shape)); + + // Animate each shape with a delay + shapes.forEach((shape, index) => { + const executor = new AnimateExecutor(shape); + executor.execute({ + type: 'strokeIn', + customParameters: { + lineWidth: 3, + strokeColor: colors[index], + showFill: true + }, + duration: 1500, + easing: 'quadOut', + delay: index * 300 // Stagger the animations + }); + }); + }); + + // PULSE ANIMATION DEMOS ========================= + + // Basic Opacity Pulse Animation + addCase('Pulse - Opacity', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + opacity: 0.8, + fill: '#1890FF', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'pulse', + customParameters: { + useOpacity: true, + useScale: false, + useColor: false, + pulseOpacity: 1.3, + pulseCount: 3 // 3 pulses over the duration + }, + duration: 2000, // Duration for all 3 pulses + easing: 'linear' + }); + }); + + // Scale Pulse Animation + addCase('Pulse - Scale', btnContainer, stage => { + const circle = createCircle({ + x: 400, + y: 200, + radius: 50, + fill: '#FA8C16' + }); + stage.defaultLayer.add(circle); + + const executor = new AnimateExecutor(circle); + executor.execute({ + type: 'pulse', + customParameters: { + useOpacity: false, + useScale: true, + useColor: false, + pulseScale: 1.2, // Grow to 1.2x size + pulseCount: 3 // 5 complete pulses + }, + duration: 2000, // 2 seconds for all 5 pulses + easing: 'linear' + }); + }); + + // Color Pulse Animation + addCase('Pulse - Color', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#52C41A', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'pulse', + customParameters: { + useOpacity: false, + useScale: false, + useColor: true, + pulseColor: 'orange', // Pulse to red + pulseColorIntensity: 1, // Strong color shift + pulseCount: 2 // 4 pulses + }, + duration: 1000, + easing: 'linear' + }); + }); + + // Combined Effects Pulse Animation + addCase('Pulse - Combined', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#722ED1', + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'pulse', + customParameters: { + useOpacity: true, + useScale: true, + useColor: true, + opacityMin: 0.6, + opacityMax: 1, + pulseScale: 1.1, + pulseColor: '#EB2F96', // Pulse to pink + pulseColorIntensity: 0.5, + pulseCount: 6 // 6 pulses + }, + duration: 3000, // 3 seconds for all 6 pulses + easing: 'linear' + }); + }); + + // Stroke Only Pulse Animation + addCase('Pulse - Stroke Only', btnContainer, stage => { + const rect = createRect({ + x: 400, + y: 200, + width: 100, + height: 100, + fill: '#1D39C4', + stroke: '#FFC53D', + lineWidth: 4, + cornerRadius: 10 + }); + stage.defaultLayer.add(rect); + + const executor = new AnimateExecutor(rect); + executor.execute({ + type: 'pulse', + customParameters: { + useOpacity: true, + useScale: false, + useColor: true, + opacityMin: 0.2, + opacityMax: 1, + pulseColor: '#FFFFFF', // Pulse to white + pulseColorIntensity: 0.7, + useFill: false, // Only pulse the stroke, not the fill + pulseCount: 3 // 3 pulses + }, + duration: 1500, // 1.5 seconds for all 3 pulses + easing: 'linear' + }); + }); + + // Looping Pulse Animation using repeat + addCase('Pulse - With Repeat', btnContainer, stage => { + const circle = createCircle({ + x: 400, + y: 200, + radius: 50, + fill: '#FF4D4F' + }); + stage.defaultLayer.add(circle); + + const executor = new AnimateExecutor(circle); + executor.execute({ + type: 'pulse', + customParameters: { + useOpacity: true, + useScale: true, + useColor: false, + opacityMin: 0.3, + opacityMax: 1, + pulseScale: 1.3, + pulseCount: 2 // Only 2 pulses per duration cycle + }, + duration: 1000, // 1 second for 2 pulses + easing: 'linear', + loop: 4 // Repeat the animation 4 times (total 8 pulses) + }); + }); + + // Fast pulse animation (heart beat) + addCase('Pulse - Heart Beat', btnContainer, stage => { + // Create a heart shape + const heart = createPath({ + x: 400, + y: 200, + path: 'M 0 -15 C -15 -15 -20 0 0 15 C 20 0 15 -15 0 -15 Z', // Simple heart shape + fill: '#FF4D4F', + scaleX: 3, + scaleY: 3, + lineWidth: 0 + }); + stage.defaultLayer.add(heart); + + const executor = new AnimateExecutor(heart); + executor.execute({ + type: 'pulse', + customParameters: { + useOpacity: false, + useScale: true, + useColor: false, + pulseScale: 1.15, // Subtle scale change + pulseCount: 2 // Two beats per cycle - like a heart rhythm + }, + duration: 1000, // 1 second for normal heart rate + easing: 'linear', + loop: 6 // Continue beating for a while + }); + }); + + // Highlight Attention Animation (realistic use case) + addCase('Highlight Element', btnContainer, stage => { + // Create background element + const background = createRect({ + x: 0, + y: 0, + width: 900, + height: 600, + fill: '#F0F2F5' + }); + stage.defaultLayer.add(background); + + // Create a group of elements to simulate a UI + const group = createGroup({ + x: 100, + y: 100 + }); + + // Create multiple shapes representing UI elements + for (let i = 0; i < 5; i++) { + const rect = createRect({ + x: i * 140, + y: 0, + width: 120, + height: 80, + fill: i === 2 ? '#1890FF' : '#FFFFFF', + stroke: '#D9D9D9', + lineWidth: 1, + cornerRadius: 4 + }); + group.add(rect); + + // Add some text + const text = createText({ + x: i * 140 + 60, + y: 40, + text: `Item ${i + 1}`, + fontSize: 16, + fill: i === 2 ? '#FFFFFF' : '#000000', + textAlign: 'center', + textBaseline: 'middle' + }); + group.add(text); + } + + stage.defaultLayer.add(group); + + // After a short delay, highlight the middle element with pulse animation + setTimeout(() => { + const targetElement = group.children[4]; // Element at index 2 (3rd element) + const executor = new AnimateExecutor(targetElement); + + executor.execute({ + type: 'pulse', + customParameters: { + useOpacity: false, + useScale: true, + useColor: true, + pulseScale: 1.08, + pulseColor: '#1890FF', + pulseColorIntensity: 0.2, + pulseCount: 5 + }, + duration: 2000, // 2 seconds for all 5 pulses + easing: 'quadInOut' + }); + }, 1000); + }); + + // Error State Pulse Animation + addCase('Error State Pulse', btnContainer, stage => { + // Create a form input field + const inputField = createRect({ + x: 300, + y: 200, + width: 300, + height: 40, + fill: '#FFFFFF', + stroke: '#D9D9D9', + lineWidth: 1, + cornerRadius: 4 + }); + + // Add label + const label = createText({ + x: 310, + y: 180, + text: 'Email', + fontSize: 14, + fill: '#000000', + textAlign: 'left', + textBaseline: 'middle' + }); + + // Add placeholder text + const placeholder = createText({ + x: 310, + y: 220, + text: 'example@invalid', + fontSize: 14, + fill: '#BFBFBF', + textAlign: 'left', + textBaseline: 'middle' + }); + + stage.defaultLayer.add(inputField); + stage.defaultLayer.add(label); + stage.defaultLayer.add(placeholder); + + // After a delay, simulate validation error with pulse animation + setTimeout(() => { + // Change border to red + inputField.setAttribute('stroke', '#FF4D4F'); + + // Add error message + const errorMessage = createText({ + x: 310, + y: 250, + text: 'Please enter a valid email address', + fontSize: 12, + fill: '#FF4D4F', + textAlign: 'left', + textBaseline: 'middle' + }); + stage.defaultLayer.add(errorMessage); + + // Pulse animation for the input field + const executor = new AnimateExecutor(inputField); + executor.execute({ + type: 'pulse', + customParameters: { + useOpacity: false, + useScale: false, + useColor: true, + pulseColor: '#FF7875', // Lighter red + pulseColorIntensity: 0.3, + strokeOnly: true, + pulseCount: 3 + }, + duration: 1200, // 1.2 seconds for all 3 pulses + easing: 'quadInOut' + }); + }, 1000); + }); }; From 44ceb799f8001206b33ed4bb2d01f8676b64742a Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 18 Apr 2025 17:14:01 +0800 Subject: [PATCH 099/179] feat: support morphing --- packages/vrender-animate/src/animate.ts | 6 +- .../vrender-animate/src/config/morphing.ts | 6 + .../vrender-animate/src/custom/morphing.ts | 676 ++++++++++++++++++ packages/vrender-animate/src/index.ts | 1 + .../vrender-animate/src/utils/transform.ts | 16 + packages/vrender-core/src/animate/config.ts | 5 - .../vrender-core/src/common/split-path.ts | 2 +- packages/vrender-core/src/common/utils.ts | 17 - packages/vrender-core/src/index.ts | 8 - .../src/interface/animation/animate.ts | 20 + .../__tests__/browser/src/pages/morphing.ts | 79 +- 11 files changed, 763 insertions(+), 73 deletions(-) create mode 100644 packages/vrender-animate/src/config/morphing.ts create mode 100644 packages/vrender-animate/src/custom/morphing.ts create mode 100644 packages/vrender-animate/src/utils/transform.ts diff --git a/packages/vrender-animate/src/animate.ts b/packages/vrender-animate/src/animate.ts index dbfbdeca8..d6af3d033 100644 --- a/packages/vrender-animate/src/animate.ts +++ b/packages/vrender-animate/src/animate.ts @@ -445,9 +445,11 @@ export class Animate implements IAnimate { step = step.next; } - this.status = AnimateStatus.END; + if (this.status !== AnimateStatus.END) { + this.onEnd(); + } - this.onEnd(); + this.status = AnimateStatus.END; if (!this.target) { return; diff --git a/packages/vrender-animate/src/config/morphing.ts b/packages/vrender-animate/src/config/morphing.ts new file mode 100644 index 000000000..8dc724e1d --- /dev/null +++ b/packages/vrender-animate/src/config/morphing.ts @@ -0,0 +1,6 @@ +import type { IAnimateConfig } from '@visactor/vrender-core'; + +export const DefaultMorphingAnimateConfig: IAnimateConfig = { + duration: 1000, + easing: 'quadInOut' +}; diff --git a/packages/vrender-animate/src/custom/morphing.ts b/packages/vrender-animate/src/custom/morphing.ts new file mode 100644 index 000000000..d4cbc81dd --- /dev/null +++ b/packages/vrender-animate/src/custom/morphing.ts @@ -0,0 +1,676 @@ +import { + splitArc, + splitCircle, + splitLine, + splitRect, + splitPolygon, + splitArea, + splitPath, + CustomPath2D, + application, + interpolateColor, + ColorStore, + ColorType, + alignBezierCurves, + applyTransformOnBezierCurves, + findBestMorphingRotation, + pathToBezierCurves, + AttributeUpdateType, + type MorphingAnimateConfig, + type MultiMorphingAnimateConfig, + type ICustomPath2D, + type IGraphic, + type IRect, + type EasingType, + type IArc, + type ICircle, + type IGraphicAttribute, + type ILine, + type IPolygon, + type IArea, + type IPath +} from '@visactor/vrender-core'; +import { isNil, type IMatrix } from '@visactor/vutils'; +import { ACustomAnimate } from './custom-animate'; +import { DefaultMorphingAnimateConfig } from '../config/morphing'; +import { isTransformKey } from '../utils/transform'; + +declare const __DEV__: boolean; + +interface MorphingDataItem { + from: number[]; + to: number[]; + fromCp: number[]; + toCp: number[]; + rotation: number; +} + +interface OtherAttrItem { + from: any; + to: any; + key: string; +} + +const interpolateOtherAttrs = (attrs: OtherAttrItem[], out: any, ratio: number) => { + attrs.forEach(entry => { + if (Number.isFinite(entry.to)) { + out[entry.key] = entry.from + (entry.to - entry.from) * ratio; + } else if (entry.key === 'fill' || entry.key === 'stroke') { + // 保存解析的结果到step + const color = interpolateColor(entry.from, entry.to, ratio, false); + if (color) { + out[entry.key] = color; + } + } + }); +}; + +/* Adapted from zrender by ecomfe + * https://github.com/ecomfe/zrender + * Licensed under the BSD-3-Clause + + * url: https://github.com/ecomfe/zrender/blob/master/src/tool/morphPath.ts + * License: https://github.com/ecomfe/zrender/blob/master/LICENSE + * @license + */ +const interpolateMorphingData = (morphingData: MorphingDataItem[], path: ICustomPath2D, ratio: number) => { + const tmpArr: number[] = []; + const newCp: number[] = []; + path.clear(); + + for (let i = 0; i < morphingData.length; i++) { + const item = morphingData[i]; + const from = item.from; + const to = item.to; + const angle = item.rotation * ratio; + const fromCp = item.fromCp; + const toCp = item.toCp; + const sa = Math.sin(angle); + const ca = Math.cos(angle); + + newCp[0] = fromCp[0] + (toCp[0] - fromCp[0]) * ratio; + newCp[1] = fromCp[1] + (toCp[1] - fromCp[1]) * ratio; + + for (let m = 0; m < from.length; m += 2) { + const x0 = from[m]; + const y0 = from[m + 1]; + const x1 = to[m]; + const y1 = to[m + 1]; + + const x = x0 * (1 - ratio) + x1 * ratio; + const y = y0 * (1 - ratio) + y1 * ratio; + + tmpArr[m] = x * ca - y * sa + newCp[0]; + tmpArr[m + 1] = x * sa + y * ca + newCp[1]; + } + + let x0 = tmpArr[0]; + let y0 = tmpArr[1]; + + path.moveTo(x0, y0); + + for (let m = 2; m < from.length; m += 6) { + const x1 = tmpArr[m]; + const y1 = tmpArr[m + 1]; + const x2 = tmpArr[m + 2]; + const y2 = tmpArr[m + 3]; + const x3 = tmpArr[m + 4]; + const y3 = tmpArr[m + 5]; + + // Is a line. + if (x0 === x1 && y0 === y1 && x2 === x3 && y2 === y3) { + path.lineTo(x3, y3); + } else { + path.bezierCurveTo(x1, y1, x2, y2, x3, y3); + } + x0 = x3; + y0 = y3; + } + } +}; + +const parseMorphingData = ( + fromPath: ICustomPath2D | null, + toPath: ICustomPath2D, + config?: { + fromTransform?: IMatrix; + toTransfrom: IMatrix; + } +) => { + const fromBezier = fromPath ? pathToBezierCurves(fromPath) : []; + const toBezier = pathToBezierCurves(toPath); + + if (config && fromBezier) { + config.fromTransform && applyTransformOnBezierCurves(fromBezier, config.fromTransform.clone().getInverse()); + applyTransformOnBezierCurves(fromBezier, config.toTransfrom); + // applyTransformOnBezierCurves(toBezier, config.toTransfrom.clone().getInverse()); + } + + const [fromBezierCurves, toBezierCurves] = alignBezierCurves(fromBezier, toBezier); + + return fromPath + ? findBestMorphingRotation(fromBezierCurves, toBezierCurves, 10, Math.PI) + : toBezierCurves.map((to, index) => { + return { + from: fromBezierCurves[index], + to, + fromCp: [0, 0], + toCp: [0, 0], + rotation: 0 + }; + }); +}; + +const validateOtherAttrs = [ + 'fill', + 'fillOpacity', + 'shadowBlur', + 'shadowColor', + 'shadowOffsetX', + 'shadowOffsetY', + 'stroke', + 'strokeOpacity', + 'lineDashOffset' + // 'lineWidth' +]; + +const parseOtherAnimateAttrs = ( + fromAttrs: Partial | null, + toAttrs: Partial | null +) => { + if (!fromAttrs || !toAttrs) { + return null; + } + const res: OtherAttrItem[] = []; + let hasAttr = false; + + Object.keys(fromAttrs).forEach(fromKey => { + if (!validateOtherAttrs.includes(fromKey)) { + return; + } + + const toValue = (toAttrs as any)[fromKey]; + if (!isNil(toValue) && !isNil((fromAttrs as any)[fromKey]) && toValue !== (fromAttrs as any)[fromKey]) { + if (fromKey === 'fill' || fromKey === 'stroke') { + res.push({ + from: + typeof fromAttrs[fromKey] === 'string' + ? ColorStore.Get(fromAttrs[fromKey] as unknown as string, ColorType.Color255) + : fromAttrs[fromKey], + to: typeof toValue === 'string' ? ColorStore.Get(toValue as string, ColorType.Color255) : toValue, + key: fromKey + }); + } else { + res.push({ from: (fromAttrs as any)[fromKey], to: toValue, key: fromKey }); + } + + hasAttr = true; + } + }); + + return hasAttr ? res : null; +}; + +export class MorphingPath extends ACustomAnimate> { + declare path: CustomPath2D; + + saveOnEnd?: boolean; + otherAttrs?: OtherAttrItem[]; + + constructor( + config: { morphingData: MorphingDataItem[]; otherAttrs?: OtherAttrItem[]; saveOnEnd?: boolean }, + duration: number, + easing: EasingType + ) { + super({}, {}, duration, easing); + this.morphingData = config.morphingData; + this.otherAttrs = config.otherAttrs; + this.saveOnEnd = config.saveOnEnd; + } + + private morphingData?: MorphingDataItem[]; + + getEndProps(): Record { + return {}; + } + + onBind(): void { + (this.target as IGraphic).createPathProxy(); + this.onUpdate(false, 0, (this.target as IGraphic).attribute); + } + + onEnd(): void { + return; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + const target = this.target as IGraphic; + const pathProxy = typeof target.pathProxy === 'function' ? target.pathProxy(target.attribute) : target.pathProxy; + interpolateMorphingData(this.morphingData, pathProxy, ratio); + if (this.otherAttrs && this.otherAttrs.length) { + interpolateOtherAttrs(this.otherAttrs, out, ratio); + } + this.target.setAttributes(out); + // 计算位置 + if (end && !this.saveOnEnd) { + (this.target as IGraphic).pathProxy = null; + } + } +} + +export const morphPath = ( + fromGraphic: IGraphic | null, + toGraphic: IGraphic, + animationConfig?: MorphingAnimateConfig, + fromGraphicTransform?: IMatrix +) => { + if (fromGraphic && (!fromGraphic.valid || !fromGraphic.toCustomPath)) { + if (__DEV__) { + console.error(fromGraphic, ' is not validate'); + } + return null; + } + + if (!toGraphic.valid || !toGraphic.toCustomPath) { + if (__DEV__) { + console.error(toGraphic, ' is not validate'); + } + return null; + } + + let fromTransform = fromGraphic?.globalTransMatrix; + + if (fromGraphicTransform && fromTransform) { + fromTransform = fromGraphicTransform + .clone() + .multiply(fromTransform.a, fromTransform.b, fromTransform.c, fromTransform.d, fromTransform.e, fromTransform.f); + } + const morphingData = parseMorphingData(fromGraphic?.toCustomPath?.(), toGraphic.toCustomPath(), { + fromTransform, + toTransfrom: toGraphic.globalTransMatrix + }); + + const attrs = parseOtherAnimateAttrs(fromGraphic?.attribute, toGraphic.attribute); + const animate = toGraphic.animate(animationConfig); + + if (animationConfig?.delay) { + animate.wait(animationConfig.delay); + } + + const morphingPath = new MorphingPath( + { morphingData, otherAttrs: attrs }, + animationConfig?.duration ?? DefaultMorphingAnimateConfig.duration, + animationConfig?.easing ?? DefaultMorphingAnimateConfig.easing + ); + animate.play(morphingPath); + + return animate; +}; + +export const oneToMultiMorph = ( + fromGraphic: IGraphic, + toGraphics: IGraphic[], + animationConfig?: MultiMorphingAnimateConfig +) => { + const validateToGraphics = toGraphics.filter(graphic => graphic && graphic.toCustomPath && graphic.valid); + if (!validateToGraphics.length) { + if (__DEV__) { + console.error(validateToGraphics, ' is not validate'); + } + } + + if (!fromGraphic.valid || !fromGraphic.toCustomPath) { + if (__DEV__) { + console.error(fromGraphic, ' is not validate'); + } + } + + const childGraphics: IGraphic[] = ( + animationConfig?.splitPath === 'clone' ? cloneGraphic : animationConfig?.splitPath ?? splitGraphic + )(fromGraphic, validateToGraphics.length, false); + + const oldOnEnd = animationConfig?.onEnd; + let count = validateToGraphics.length; + const onEachEnd = () => { + count--; + if (count === 0 && oldOnEnd) { + oldOnEnd(); + } + }; + + validateToGraphics.forEach((toChild, index) => { + const fromChild = childGraphics[index]; + const delay = + (animationConfig?.delay ?? 0) + + (animationConfig?.individualDelay + ? animationConfig.individualDelay(index, validateToGraphics.length, fromChild, toChild) + : 0); + morphPath( + fromChild, + toChild, + Object.assign({}, animationConfig, { onEnd: onEachEnd, delay }), + fromGraphic.globalTransMatrix + ); + }); +}; + +export class MultiToOneMorphingPath extends ACustomAnimate> { + declare path: CustomPath2D; + + otherAttrs?: OtherAttrItem[][]; + + constructor( + config: { morphingData: MorphingDataItem[][]; otherAttrs?: OtherAttrItem[][] }, + duration: number, + easing: EasingType + ) { + super({}, {}, duration, easing); + this.morphingData = config.morphingData; + this.otherAttrs = config.otherAttrs; + } + + private morphingData?: MorphingDataItem[][]; + + getEndProps(): Record { + return {}; + } + + onBind(): void { + this.addPathProxy(); + } + + private addPathProxy() { + const shadowRoot = (this.target as IGraphic).shadowRoot; + + shadowRoot.forEachChildren(child => { + (child as IGraphic).createPathProxy(); + }); + + this.onUpdate(false, 0, (this.target as IGraphic).attribute); + } + + private clearPathProxy() { + const shadowRoot = (this.target as IGraphic).shadowRoot; + + shadowRoot.forEachChildren(child => { + (child as IGraphic).pathProxy = null; + }); + } + + onEnd(): void { + return; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + const shadowRoot = (this.target as IGraphic).shadowRoot; + + shadowRoot.forEachChildren((child: IGraphic, index) => { + interpolateMorphingData( + this.morphingData[index], + typeof child.pathProxy === 'function' ? child.pathProxy(child.attribute) : child.pathProxy, + ratio + ); + + if (this.otherAttrs?.[index] && this.otherAttrs[index].length) { + interpolateOtherAttrs(this.otherAttrs[index], child.attribute, ratio); + } + }); + + // 计算位置 + if (end) { + this.clearPathProxy(); + this.morphingData = null; + } + } +} + +const parseShadowChildAttrs = (graphicAttrs: Partial) => { + const attrs: Partial = {}; + + Object.keys(graphicAttrs).forEach(key => { + if (!isTransformKey(key)) { + (attrs as any)[key] = (graphicAttrs as any)[key]; + } + }); + + // if (attrs.fill == null) { + // attrs.fill = !!attrs.fillColor; + // } + // if (attrs.stroke == null) { + // attrs.stroke = !!attrs.strokeColor; + // } + + return attrs; +}; + +const appendShadowChildrenToGraphic = (graphic: IGraphic, children: IGraphic[], count: number) => { + const childAttrs = parseShadowChildAttrs(graphic.attribute); + const shadowRoot = graphic.attachShadow(); + + if (children.length) { + shadowRoot.setTheme({ + [children[0].type]: childAttrs + }); + children.forEach(element => { + element.setAttributes({ pickable: false }); + shadowRoot.appendChild(element); + }); + } else { + const box = graphic.AABBBounds; + const width = box.width(); + const height = box.height(); + + shadowRoot.setTheme({ + rect: childAttrs + }); + new Array(count).fill(0).forEach(el => { + const child = application.graphicService.creator.rect({ + x: 0, + y: 0, + width, + height: height, + pickable: false + }); + shadowRoot.appendChild(child); + children.push(child); + }); + } +}; + +export const cloneGraphic = (graphic: IGraphic, count: number, needAppend?: boolean) => { + const children: IGraphic[] = []; + const childAttrs = needAppend ? null : parseShadowChildAttrs(graphic.attribute); + const path = graphic.toCustomPath(); + + for (let i = 0; i < count; i++) { + const element = { + path: new CustomPath2D().fromCustomPath2D(path) + }; + + children.push( + application.graphicService.creator.path(needAppend ? element : Object.assign({}, childAttrs, element)) + ); + } + + if (needAppend) { + appendShadowChildrenToGraphic(graphic, children, count); + } + + return children; +}; + +export const splitGraphic = (graphic: IGraphic, count: number, needAppend?: boolean) => { + const children: IGraphic[] = []; + const childAttrs = needAppend ? null : parseShadowChildAttrs(graphic.attribute); + + if (graphic.type === 'rect') { + const childrenAttrs = splitRect(graphic as IRect, count); + childrenAttrs.forEach(element => { + children.push( + application.graphicService.creator.rect(needAppend ? element : Object.assign({}, childAttrs, element)) + ); + }); + } else if (graphic.type === 'arc') { + const childrenAttrs = splitArc(graphic as IArc, count); + childrenAttrs.forEach(element => { + children.push( + application.graphicService.creator.arc(needAppend ? element : Object.assign({}, childAttrs, element)) + ); + }); + } else if (graphic.type === 'circle') { + const childrenAttrs = splitCircle(graphic as ICircle, count); + childrenAttrs.forEach(element => { + children.push( + application.graphicService.creator.arc(needAppend ? element : Object.assign({}, childAttrs, element)) + ); + }); + } else if (graphic.type === 'line') { + const childrenAttrs = splitLine(graphic as ILine, count); + const defaultSymbol = { size: 10, symbolType: 'circle' }; + + childrenAttrs.forEach(element => { + children.push( + application.graphicService.creator.symbol( + needAppend ? Object.assign({}, element, defaultSymbol) : Object.assign({}, childAttrs, element, defaultSymbol) + ) + ); + }); + } else if (graphic.type === 'polygon') { + const childrenAttrs = splitPolygon(graphic as IPolygon, count); + childrenAttrs.forEach(element => { + children.push( + application.graphicService.creator.polygon(needAppend ? element : Object.assign({}, childAttrs, element)) + ); + }); + } else if (graphic.type === 'area') { + const childrenAttrs = splitArea(graphic as IArea, count); + childrenAttrs.forEach(element => { + children.push( + application.graphicService.creator.polygon(needAppend ? element : Object.assign({}, childAttrs, element)) + ); + }); + } else if (graphic.type === 'path') { + const childrenAttrs = splitPath(graphic as IPath, count); + childrenAttrs.forEach(element => { + if ('path' in element) { + children.push( + application.graphicService.creator.path(needAppend ? element : Object.assign({}, childAttrs, element)) + ); + } else { + children.push( + application.graphicService.creator.polygon(needAppend ? element : Object.assign({}, childAttrs, element)) + ); + } + }); + } + + if (needAppend) { + appendShadowChildrenToGraphic(graphic, children, count); + } + + return children; +}; + +/** + * 多对一动画 + * @param fromGraphics + * @param toGraphic + * @param animationConfig + */ +export const multiToOneMorph = ( + fromGraphics: IGraphic[], + toGraphic: IGraphic, + animationConfig?: MultiMorphingAnimateConfig +) => { + const validateFromGraphics = fromGraphics.filter(graphic => graphic.toCustomPath && graphic.valid); + if (!validateFromGraphics.length) { + if (__DEV__) { + console.error(fromGraphics, ' is not validate'); + } + } + + if (!toGraphic.valid || !toGraphic.toCustomPath) { + if (__DEV__) { + console.error(toGraphic, ' is not validate'); + } + } + + const childGraphics: IGraphic[] = ( + animationConfig?.splitPath === 'clone' ? cloneGraphic : animationConfig?.splitPath ?? splitGraphic + )(toGraphic, validateFromGraphics.length, true); + + const toAttrs = toGraphic.attribute; + toGraphic.setAttribute('visible', false); + + const morphingData = validateFromGraphics.map((graphic, index) => { + return parseMorphingData(graphic.toCustomPath(), childGraphics[index].toCustomPath(), { + fromTransform: graphic.globalTransMatrix, + toTransfrom: childGraphics[index].globalTransMatrix + }); + }); + const otherAttrs = validateFromGraphics.map((graphic, index) => { + return parseOtherAnimateAttrs(graphic.attribute, toAttrs); + }); + + if (animationConfig?.individualDelay) { + const oldOnEnd = animationConfig.onEnd; + let count = validateFromGraphics.length; + const onEachEnd = () => { + count--; + if (count === 0) { + toGraphic.setAttributes({ visible: true, ratio: null } as any, false, { + type: AttributeUpdateType.ANIMATE_END + }); + toGraphic.detachShadow(); + if (oldOnEnd) { + oldOnEnd(); + } + } + }; + childGraphics.forEach((to, index) => { + const delay = + (animationConfig.delay ?? 0) + + animationConfig.individualDelay(index, validateFromGraphics.length, fromGraphics[index], to); + const animate = to.animate(Object.assign({}, animationConfig, { onEnd: onEachEnd })); + animate.wait(delay); + + animate.play( + new MorphingPath( + { + morphingData: morphingData[index], + saveOnEnd: true, + otherAttrs: otherAttrs[index] + }, + animationConfig.duration ?? DefaultMorphingAnimateConfig.duration, + animationConfig.easing ?? DefaultMorphingAnimateConfig.easing + ) + ); + }); + } else { + const oldOnEnd = animationConfig?.onEnd; + const config = animationConfig ? Object.assign({}, animationConfig) : {}; + + config.onEnd = () => { + toGraphic.setAttribute('visible', true, false, { type: AttributeUpdateType.ANIMATE_END }); + toGraphic.detachShadow(); + + if (oldOnEnd) { + oldOnEnd(); + } + }; + + const animate = toGraphic.animate(config); + + if (animationConfig?.delay) { + animate.wait(animationConfig.delay); + } + + animate.play( + new MultiToOneMorphingPath( + { morphingData, otherAttrs }, + animationConfig?.duration ?? DefaultMorphingAnimateConfig.duration, + animationConfig?.easing ?? DefaultMorphingAnimateConfig.easing + ) + ); + } +}; diff --git a/packages/vrender-animate/src/index.ts b/packages/vrender-animate/src/index.ts index 3b012ffc4..2d9bcbfe0 100644 --- a/packages/vrender-animate/src/index.ts +++ b/packages/vrender-animate/src/index.ts @@ -11,6 +11,7 @@ export { registerAnimate } from './register'; export { ACustomAnimate, AComponentAnimate } from './custom/custom-animate'; export { ComponentAnimator } from './component/component-animator'; export { IncreaseCount } from './custom/number'; +export { MorphingPath, MultiToOneMorphingPath, oneToMultiMorph, multiToOneMorph } from './custom/morphing'; export { InputText } from './custom/input-text'; export { ClipGraphicAnimate, ClipAngleAnimate, ClipRadiusAnimate, ClipDirectionAnimate } from './custom/clip-graphic'; export { TagPointsUpdate } from './custom/tag-points'; diff --git a/packages/vrender-animate/src/utils/transform.ts b/packages/vrender-animate/src/utils/transform.ts new file mode 100644 index 000000000..304ada2d1 --- /dev/null +++ b/packages/vrender-animate/src/utils/transform.ts @@ -0,0 +1,16 @@ +export const transformKeys = [ + 'x', + 'y', + 'dx', + 'dy', + 'scaleX', + 'scaleY', + 'angle', + 'anchor', + 'postMatrix', + 'scrollX', + 'scrollY' +]; +export const isTransformKey = (key: string) => { + return transformKeys.includes(key); +}; diff --git a/packages/vrender-core/src/animate/config.ts b/packages/vrender-core/src/animate/config.ts index 4cd0b74c3..5e568db08 100644 --- a/packages/vrender-core/src/animate/config.ts +++ b/packages/vrender-core/src/animate/config.ts @@ -4,8 +4,3 @@ export const DefaultStateAnimateConfig: IAnimateConfig = { duration: 200, easing: 'cubicOut' }; - -export const DefaultMorphingAnimateConfig: IAnimateConfig = { - duration: 1000, - easing: 'quadInOut' -}; diff --git a/packages/vrender-core/src/common/split-path.ts b/packages/vrender-core/src/common/split-path.ts index f3d56539d..3f7e0641d 100644 --- a/packages/vrender-core/src/common/split-path.ts +++ b/packages/vrender-core/src/common/split-path.ts @@ -231,7 +231,7 @@ export const splitArea = (area: IArea, count: number) => { const res: { points: IPointLike[] }[] = []; - recursiveCallBinarySplit(points, count, res); + recursiveCallBinarySplit(allPoints, count, res); return res; }; diff --git a/packages/vrender-core/src/common/utils.ts b/packages/vrender-core/src/common/utils.ts index 7a425a4af..8231abcdf 100644 --- a/packages/vrender-core/src/common/utils.ts +++ b/packages/vrender-core/src/common/utils.ts @@ -289,23 +289,6 @@ export function pointsInterpolation( return points; } -export const transformKeys = [ - 'x', - 'y', - 'dx', - 'dy', - 'scaleX', - 'scaleY', - 'angle', - 'anchor', - 'postMatrix', - 'scrollX', - 'scrollY' -]; -export const isTransformKey = (key: string) => { - return transformKeys.includes(key); -}; - export function getAttributeFromDefaultAttrList(attr: Record | Record[], key: string) { if (isArray(attr)) { let val; diff --git a/packages/vrender-core/src/index.ts b/packages/vrender-core/src/index.ts index f2f10b795..0e1830689 100644 --- a/packages/vrender-core/src/index.ts +++ b/packages/vrender-core/src/index.ts @@ -103,11 +103,3 @@ export * from './plugins/builtin-plugin/3dview-transform-plugin'; export * from './plugins/builtin-plugin/flex-layout-plugin'; export * from './plugins/builtin-plugin/edit-module'; - -export const morphPath = {}; -export const multiToOneMorph = {}; -export const oneToMultiMorph = {}; -export class ACustomAnimate {} -export const AnimateGroup = {}; -export const Animate = {}; -export const defaultTicker = {}; diff --git a/packages/vrender-core/src/interface/animation/animate.ts b/packages/vrender-core/src/interface/animation/animate.ts index 42e466ca1..8673a5a97 100644 --- a/packages/vrender-core/src/interface/animation/animate.ts +++ b/packages/vrender-core/src/interface/animation/animate.ts @@ -179,3 +179,23 @@ export interface IAnimateTarget { animates: Map; [key: string]: any; } + +export interface BaseAnimateConfig { + id?: number | string; + interpolate?: (key: string, ratio: number, from: any, to: any, nextAttributes: any) => boolean; + onStart?: () => void; + onFrame?: (step: IStep, ratio: number) => void; + onEnd?: () => void; + onRemove?: () => void; +} + +export interface MorphingAnimateConfig extends Omit { + duration?: number; + easing?: EasingType; // 统一到easing + delay?: number; +} + +export interface MultiMorphingAnimateConfig extends MorphingAnimateConfig { + splitPath?: 'clone' | ((graphic: IGraphic, count: number, needAppend?: boolean) => IGraphic[]); + individualDelay?: (index: number, count: number, fromGraphic: IGraphic, toGraphic: IGraphic) => number; +} diff --git a/packages/vrender/__tests__/browser/src/pages/morphing.ts b/packages/vrender/__tests__/browser/src/pages/morphing.ts index 93502018e..a12dbd027 100644 --- a/packages/vrender/__tests__/browser/src/pages/morphing.ts +++ b/packages/vrender/__tests__/browser/src/pages/morphing.ts @@ -3,22 +3,20 @@ import { createRect, createLine, createCircle, - MorphingPath, - morphPath, - pathToBezierCurves, createSymbol, - oneToMultiMorph, multiToOneMorph, createPolygon, - splitPolygon, - createText, createArea, createArc, - splitGraphic, - defaultTicker + registerAnimate, + registerCustomAnimate, + oneToMultiMorph } from '@visactor/vrender'; import { colorPools } from '../utils'; +registerAnimate(); +registerCustomAnimate(); + // container.load(roughModule); export const page = () => { @@ -113,13 +111,14 @@ export const page = () => { }); const area = createArea({ - curveType: 'basis', + curveType: 'monotoneX', x: 400, y: 200, points: [ - { x: 0, y: 100, y1: 50 }, - { x: 50, y: 80, y1: 60 }, - { x: 80, y: 150, y1: 20 } + { x: 0, y: 100, y1: 200 }, + { x: 50, y: 80, y1: 200 }, + { x: 80, y: 150, y1: 200 }, + { x: 100, y: 100, y1: 200 } ], fill: colorPools[10], stroke: 'green' @@ -153,7 +152,7 @@ export const page = () => { // rect.pathProxy = pathToBezierCurves(circle.toCustomPath()); const line = createLine({ - x: 800, + x: 300, y: 100, points: [ { x: 0, y: 0 }, @@ -194,34 +193,34 @@ export const page = () => { stage.defaultLayer.appendChild(symbols); } - // stage.defaultLayer.appendChild(arc); + // stage.defaultLayer.appendChild(area); - // oneToMultiMorph(arc, symbolList, { duration: 2000, easing: 'quadIn' }); + oneToMultiMorph(area, symbolList, { duration: 2000, easing: 'quadIn' }); - const fromSymbolList = []; - for (let i = 0; i < 23; i++) { - const symbols = createSymbol({ - x: Math.random() * 500, - y: Math.random() * 500, - symbolType: 'triangleLeft', - size: 5 + Math.floor(Math.random() * 10), - fill: 'green', - // stroke: 'red', - // angle: Math.PI / 4, - lineWidth: 6 - }); - fromSymbolList.push(symbols); - } - stage.defaultLayer.appendChild(rect); + // const fromSymbolList = []; + // for (let i = 0; i < 23; i++) { + // const symbols = createSymbol({ + // x: Math.random() * 500, + // y: Math.random() * 500, + // symbolType: 'triangleLeft', + // size: 5 + Math.floor(Math.random() * 10), + // fill: 'green', + // // stroke: 'red', + // // angle: Math.PI / 4, + // lineWidth: 6 + // }); + // fromSymbolList.push(symbols); + // } + // stage.defaultLayer.appendChild(rect); - multiToOneMorph(fromSymbolList, rect, { - duration: 2000, - easing: 'quadIn', - // splitPath: 'clone', - individualDelay: (index, count, fromGraphic, toGraphic) => { - return index * 100; - } - }); + // multiToOneMorph(fromSymbolList, rect, { + // duration: 2000, + // easing: 'quadIn', + // // splitPath: 'clone', + // individualDelay: (index, count, fromGraphic, toGraphic) => { + // return index * 100; + // } + // }); // morphPath(fromSymbolList[0], polygon, { duration: 2000, easing: 'quadIn' }); const fromSymbolList2 = []; @@ -254,10 +253,10 @@ export const page = () => { stage.on('click', () => { if (isPause) { isPause = false; - defaultTicker.resume(); + // defaultTicker.resume(); } else { isPause = true; - defaultTicker.pause(); + // defaultTicker.pause(); } }); From 12a85b93bc5f87433478e3d749becb911c7a10de Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Mon, 21 Apr 2025 20:21:32 +0800 Subject: [PATCH 100/179] feat: export some for morphing --- packages/vrender-animate/src/index.ts | 2 +- packages/vrender-core/src/interface/render.ts | 1 + packages/vrender-core/src/render/render-service.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/vrender-animate/src/index.ts b/packages/vrender-animate/src/index.ts index 2d9bcbfe0..3df6fb6db 100644 --- a/packages/vrender-animate/src/index.ts +++ b/packages/vrender-animate/src/index.ts @@ -11,7 +11,7 @@ export { registerAnimate } from './register'; export { ACustomAnimate, AComponentAnimate } from './custom/custom-animate'; export { ComponentAnimator } from './component/component-animator'; export { IncreaseCount } from './custom/number'; -export { MorphingPath, MultiToOneMorphingPath, oneToMultiMorph, multiToOneMorph } from './custom/morphing'; +export { MorphingPath, MultiToOneMorphingPath, oneToMultiMorph, multiToOneMorph, morphPath } from './custom/morphing'; export { InputText } from './custom/input-text'; export { ClipGraphicAnimate, ClipAngleAnimate, ClipRadiusAnimate, ClipDirectionAnimate } from './custom/clip-graphic'; export { TagPointsUpdate } from './custom/tag-points'; diff --git a/packages/vrender-core/src/interface/render.ts b/packages/vrender-core/src/interface/render.ts index c3451700f..08c2f3630 100644 --- a/packages/vrender-core/src/interface/render.ts +++ b/packages/vrender-core/src/interface/render.ts @@ -31,6 +31,7 @@ export interface IRenderService { renderTreeRoots: IGraphic[]; // 此次render的数组 renderLists: IGraphic[]; drawParams: IRenderServiceDrawParams; + drawContribution: IDrawContribution; prepare: (updateBounds: boolean) => void; prepareRenderList: () => void; diff --git a/packages/vrender-core/src/render/render-service.ts b/packages/vrender-core/src/render/render-service.ts index f6239386a..23d90f9f1 100644 --- a/packages/vrender-core/src/render/render-service.ts +++ b/packages/vrender-core/src/render/render-service.ts @@ -20,7 +20,7 @@ export class DefaultRenderService implements IRenderService { constructor( @inject(DrawContribution) - private readonly drawContribution: IDrawContribution + public readonly drawContribution: IDrawContribution ) {} // 渲染前准备工作,计算bounds等逻辑 From 65d4e244681e3c827979116f1ce8d366c96aa1b0 Mon Sep 17 00:00:00 2001 From: xuanhun <717532978@qq.com> Date: Tue, 22 Apr 2025 11:19:05 +0800 Subject: [PATCH 101/179] docs: update link --- docs/assets/guide/en/asd/Basic/VRender_basic_tutorial.md | 2 +- docs/assets/guide/zh/asd/Basic/Vrender_basic_tutorial.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/assets/guide/en/asd/Basic/VRender_basic_tutorial.md b/docs/assets/guide/en/asd/Basic/VRender_basic_tutorial.md index 3e1287c66..0ac07c18b 100644 --- a/docs/assets/guide/en/asd/Basic/VRender_basic_tutorial.md +++ b/docs/assets/guide/en/asd/Basic/VRender_basic_tutorial.md @@ -14,7 +14,7 @@ VRender is a relatively low-level rendering library. We will introduce the usage ## Graphic System -This section will quickly introduce the graphic system. For more detailed understanding, please refer to [Graphic System](./Basic_Tutorial/Graphic) +This section will quickly introduce the graphic system. For more detailed understanding, please refer to [Graphic System](../Basic_Tutorial/Graphic) ### Create Graphics diff --git a/docs/assets/guide/zh/asd/Basic/Vrender_basic_tutorial.md b/docs/assets/guide/zh/asd/Basic/Vrender_basic_tutorial.md index 72b9ea8e3..7f98d0cc2 100644 --- a/docs/assets/guide/zh/asd/Basic/Vrender_basic_tutorial.md +++ b/docs/assets/guide/zh/asd/Basic/Vrender_basic_tutorial.md @@ -14,7 +14,7 @@ VRender 是一个比较底层的渲染库,我们将以使用场景作为区分 ## 图元系统 -此章节会快速介绍图元系统,如需详细了解,请参考[图元系统](./Basic_Tutorial/Graphic) +此章节会快速介绍图元系统,如需详细了解,请参考[图元系统](../Basic_Tutorial/Graphic) ### 创建图元 From 3ed8d13a1ae09f745af71dd237df98d86df8d567 Mon Sep 17 00:00:00 2001 From: xiaoluoHe Date: Tue, 22 Apr 2025 14:52:36 +0800 Subject: [PATCH 102/179] fix: incorrect result for TagPointsUpdate animation --- packages/vrender-core/src/animate/custom-animate.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/vrender-core/src/animate/custom-animate.ts b/packages/vrender-core/src/animate/custom-animate.ts index 5c0c04f99..c9152c57e 100644 --- a/packages/vrender-core/src/animate/custom-animate.ts +++ b/packages/vrender-core/src/animate/custom-animate.ts @@ -1,6 +1,7 @@ import type { IPoint, IPointLike } from '@visactor/vutils'; import { clamp, + cloneDeep, getDecimalPlaces, isArray, isNumber, @@ -863,6 +864,12 @@ export class TagPointsUpdate extends ACustomAnimate<{ points?: IPointLike[]; seg } onUpdate(end: boolean, ratio: number, out: Record): void { + if (end) { + Object.keys(this.to).forEach(k => { + out[k] = this.to[k]; + }); + return; + } // if not create new points, multi points animation might not work well. this.points = this.points.map((point, index) => { const newPoint = pointInterpolation(this.interpolatePoints[index][0], this.interpolatePoints[index][1], ratio); From 9dc885c187f78e38c03cbd45e6b0741062fb1b8b Mon Sep 17 00:00:00 2001 From: xiaoluoHe Date: Tue, 22 Apr 2025 14:53:18 +0800 Subject: [PATCH 103/179] docs: update changelog of rush --- .../fix-tagPointsUpdate-end_2025-04-22-06-53.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@visactor/vrender-core/fix-tagPointsUpdate-end_2025-04-22-06-53.json diff --git a/common/changes/@visactor/vrender-core/fix-tagPointsUpdate-end_2025-04-22-06-53.json b/common/changes/@visactor/vrender-core/fix-tagPointsUpdate-end_2025-04-22-06-53.json new file mode 100644 index 000000000..ac5b8402e --- /dev/null +++ b/common/changes/@visactor/vrender-core/fix-tagPointsUpdate-end_2025-04-22-06-53.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-core", + "comment": "fix: incorrect result for TagPointsUpdate animation", + "type": "none" + } + ], + "packageName": "@visactor/vrender-core" +} \ No newline at end of file From ebd073c66e3cb53cddeefb6025b0d2038679118b Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 22 Apr 2025 15:06:56 +0800 Subject: [PATCH 104/179] feat: enhance toCustomPath to support any shape morphing --- .../vrender-animate/src/custom/morphing.ts | 111 +++++- packages/vrender-core/src/application.ts | 3 +- .../src/color-string/interpolate.ts | 6 +- .../vrender-core/src/common/custom-path2d.ts | 18 +- .../vrender-core/src/common/morphing-utils.ts | 351 ++++++------------ packages/vrender-core/src/common/shape/arc.ts | 75 +++- packages/vrender-core/src/graphic/arc.ts | 6 +- packages/vrender-core/src/graphic/area.ts | 6 +- packages/vrender-core/src/graphic/circle.ts | 6 +- packages/vrender-core/src/graphic/graphic.ts | 14 + packages/vrender-core/src/graphic/line.ts | 6 +- packages/vrender-core/src/graphic/rect.ts | 7 +- packages/vrender-core/src/graphic/star.ts | 6 +- packages/vrender-core/src/modules.ts | 5 +- .../src/render/contributions/rough/context.ts | 4 + .../__tests__/browser/src/pages/morphing.ts | 127 ++++--- 16 files changed, 421 insertions(+), 330 deletions(-) diff --git a/packages/vrender-animate/src/custom/morphing.ts b/packages/vrender-animate/src/custom/morphing.ts index d4cbc81dd..7a69c0d78 100644 --- a/packages/vrender-animate/src/custom/morphing.ts +++ b/packages/vrender-animate/src/custom/morphing.ts @@ -30,7 +30,7 @@ import { type IArea, type IPath } from '@visactor/vrender-core'; -import { isNil, type IMatrix } from '@visactor/vutils'; +import { isArray, isNil, type IMatrix } from '@visactor/vutils'; import { ACustomAnimate } from './custom-animate'; import { DefaultMorphingAnimateConfig } from '../config/morphing'; import { isTransformKey } from '../utils/transform'; @@ -51,6 +51,12 @@ interface OtherAttrItem { key: string; } +/** + * 插值计算非路径属性(如颜色、透明度等) + * @param attrs 要插值的属性数组 + * @param out 输出对象 + * @param ratio 插值比例 + */ const interpolateOtherAttrs = (attrs: OtherAttrItem[], out: any, ratio: number) => { attrs.forEach(entry => { if (Number.isFinite(entry.to)) { @@ -73,6 +79,12 @@ const interpolateOtherAttrs = (attrs: OtherAttrItem[], out: any, ratio: number) * License: https://github.com/ecomfe/zrender/blob/master/LICENSE * @license */ +/** + * 根据给定比例插值计算形变数据并应用到路径 + * @param morphingData 形变数据 + * @param path 目标路径对象 + * @param ratio 插值比例 + */ const interpolateMorphingData = (morphingData: MorphingDataItem[], path: ICustomPath2D, ratio: number) => { const tmpArr: number[] = []; const newCp: number[] = []; @@ -129,6 +141,13 @@ const interpolateMorphingData = (morphingData: MorphingDataItem[], path: ICustom } }; +/** + * 解析形变数据,将源路径和目标路径转换为贝塞尔曲线并找到最佳旋转角度 + * @param fromPath 源路径 + * @param toPath 目标路径 + * @param config 变换配置 + * @returns 形变数据数组 + */ const parseMorphingData = ( fromPath: ICustomPath2D | null, toPath: ICustomPath2D, @@ -137,6 +156,7 @@ const parseMorphingData = ( toTransfrom: IMatrix; } ) => { + // [fromPath, toPath] = [toPath, fromPath]; const fromBezier = fromPath ? pathToBezierCurves(fromPath) : []; const toBezier = pathToBezierCurves(toPath); @@ -174,6 +194,12 @@ const validateOtherAttrs = [ // 'lineWidth' ]; +/** + * 解析可动画属性,提取源属性和目标属性的差异 + * @param fromAttrs 源属性 + * @param toAttrs 目标属性 + * @returns 可动画属性数组 + */ const parseOtherAnimateAttrs = ( fromAttrs: Partial | null, toAttrs: Partial | null @@ -192,12 +218,14 @@ const parseOtherAnimateAttrs = ( const toValue = (toAttrs as any)[fromKey]; if (!isNil(toValue) && !isNil((fromAttrs as any)[fromKey]) && toValue !== (fromAttrs as any)[fromKey]) { if (fromKey === 'fill' || fromKey === 'stroke') { + const parseColor = (color: string) => { + return typeof color === 'string' ? ColorStore.Get(color, ColorType.Color255) : color; + }; res.push({ - from: - typeof fromAttrs[fromKey] === 'string' - ? ColorStore.Get(fromAttrs[fromKey] as unknown as string, ColorType.Color255) - : fromAttrs[fromKey], - to: typeof toValue === 'string' ? ColorStore.Get(toValue as string, ColorType.Color255) : toValue, + from: isArray(fromAttrs[fromKey]) + ? fromAttrs[fromKey].map(parseColor) + : parseColor((fromAttrs as any)[fromKey]), + to: isArray(toValue) ? toValue.map(parseColor) : parseColor(toValue), key: fromKey }); } else { @@ -211,6 +239,9 @@ const parseOtherAnimateAttrs = ( return hasAttr ? res : null; }; +/** + * 形变路径动画类,用于处理路径和其他属性的形变 + */ export class MorphingPath extends ACustomAnimate> { declare path: CustomPath2D; @@ -243,6 +274,12 @@ export class MorphingPath extends ACustomAnimate> { return; } + /** + * 更新动画状态 + * @param end 是否结束 + * @param ratio 动画进度比例 + * @param out 输出属性对象 + */ onUpdate(end: boolean, ratio: number, out: Record): void { const target = this.target as IGraphic; const pathProxy = typeof target.pathProxy === 'function' ? target.pathProxy(target.attribute) : target.pathProxy; @@ -258,6 +295,14 @@ export class MorphingPath extends ACustomAnimate> { } } +/** + * 创建从一个图形到另一个图形的形变动画 + * @param fromGraphic 源图形 + * @param toGraphic 目标图形 + * @param animationConfig 动画配置 + * @param fromGraphicTransform 源图形变换矩阵 + * @returns 动画实例 + */ export const morphPath = ( fromGraphic: IGraphic | null, toGraphic: IGraphic, @@ -307,6 +352,12 @@ export const morphPath = ( return animate; }; +/** + * 创建从一个图形到多个图形的形变动画 + * @param fromGraphic 源图形 + * @param toGraphics 目标图形数组 + * @param animationConfig 动画配置 + */ export const oneToMultiMorph = ( fromGraphic: IGraphic, toGraphics: IGraphic[], @@ -354,6 +405,9 @@ export const oneToMultiMorph = ( }); }; +/** + * 多对一形变路径动画类,用于处理多个路径形变为一个目标路径 + */ export class MultiToOneMorphingPath extends ACustomAnimate> { declare path: CustomPath2D; @@ -379,6 +433,9 @@ export class MultiToOneMorphingPath extends ACustomAnimate> this.addPathProxy(); } + /** + * 为每个子图形添加路径代理 + */ private addPathProxy() { const shadowRoot = (this.target as IGraphic).shadowRoot; @@ -389,6 +446,9 @@ export class MultiToOneMorphingPath extends ACustomAnimate> this.onUpdate(false, 0, (this.target as IGraphic).attribute); } + /** + * 清除所有子图形的路径代理 + */ private clearPathProxy() { const shadowRoot = (this.target as IGraphic).shadowRoot; @@ -401,6 +461,12 @@ export class MultiToOneMorphingPath extends ACustomAnimate> return; } + /** + * 更新动画状态 + * @param end 是否结束 + * @param ratio 动画进度比例 + * @param out 输出属性对象 + */ onUpdate(end: boolean, ratio: number, out: Record): void { const shadowRoot = (this.target as IGraphic).shadowRoot; @@ -424,6 +490,11 @@ export class MultiToOneMorphingPath extends ACustomAnimate> } } +/** + * 解析图形的阴影子元素属性(排除变换相关属性) + * @param graphicAttrs 图形属性 + * @returns 阴影子元素属性 + */ const parseShadowChildAttrs = (graphicAttrs: Partial) => { const attrs: Partial = {}; @@ -443,6 +514,12 @@ const parseShadowChildAttrs = (graphicAttrs: Partial) => { return attrs; }; +/** + * 将阴影子元素添加到图形中 + * @param graphic 目标图形 + * @param children 子元素数组 + * @param count 子元素数量 + */ const appendShadowChildrenToGraphic = (graphic: IGraphic, children: IGraphic[], count: number) => { const childAttrs = parseShadowChildAttrs(graphic.attribute); const shadowRoot = graphic.attachShadow(); @@ -477,6 +554,13 @@ const appendShadowChildrenToGraphic = (graphic: IGraphic, children: IGraphic[], } }; +/** + * 克隆图形为多个相同的图形 + * @param graphic 源图形 + * @param count 克隆数量 + * @param needAppend 是否需要添加到源图形中 + * @returns 克隆的图形数组 + */ export const cloneGraphic = (graphic: IGraphic, count: number, needAppend?: boolean) => { const children: IGraphic[] = []; const childAttrs = needAppend ? null : parseShadowChildAttrs(graphic.attribute); @@ -499,6 +583,13 @@ export const cloneGraphic = (graphic: IGraphic, count: number, needAppend?: bool return children; }; +/** + * 将图形分割为多个子图形 + * @param graphic 源图形 + * @param count 分割数量 + * @param needAppend 是否需要添加到源图形中 + * @returns 分割后的图形数组 + */ export const splitGraphic = (graphic: IGraphic, count: number, needAppend?: boolean) => { const children: IGraphic[] = []; const childAttrs = needAppend ? null : parseShadowChildAttrs(graphic.attribute); @@ -572,10 +663,10 @@ export const splitGraphic = (graphic: IGraphic, count: number, needAppend?: bool }; /** - * 多对一动画 - * @param fromGraphics - * @param toGraphic - * @param animationConfig + * 创建从多个图形到一个图形的形变动画 + * @param fromGraphics 源图形数组 + * @param toGraphic 目标图形 + * @param animationConfig 动画配置 */ export const multiToOneMorph = ( fromGraphics: IGraphic[], diff --git a/packages/vrender-core/src/application.ts b/packages/vrender-core/src/application.ts index 920a43b22..55f694eda 100644 --- a/packages/vrender-core/src/application.ts +++ b/packages/vrender-core/src/application.ts @@ -1,11 +1,12 @@ import type { ILayerService } from './interface/core'; import type { IGraphicUtil, ITransformUtil } from './interface/core'; -import type { IGlobal, IGraphicService } from './interface'; +import type { IGlobal, IGraphicService, IRenderService } from './interface'; export class Application { global: IGlobal; graphicUtil: IGraphicUtil; graphicService: IGraphicService; + renderService: IRenderService; transformUtil: ITransformUtil; layerService: ILayerService; } diff --git a/packages/vrender-core/src/color-string/interpolate.ts b/packages/vrender-core/src/color-string/interpolate.ts index 4a009e293..3f045bed2 100644 --- a/packages/vrender-core/src/color-string/interpolate.ts +++ b/packages/vrender-core/src/color-string/interpolate.ts @@ -21,12 +21,12 @@ export function interpolateColor( alphaChannel: boolean, cb?: (fromArray: [number, number, number, number], toArray: [number, number, number, number]) => void ): false | string | IGradientColor | string[] { - if ((Array.isArray(from) && !isNumber(from[0])) || (Array.isArray(to) && !isNumber(to[0]))) { + if (Array.isArray(from) && !isNumber(from[0]) && Array.isArray(to) && !isNumber(to[0])) { // 待性能优化 const out: string[] = new Array(4).fill(0).map((_, index) => { return _interpolateColor( - isArray(from) ? (from[index] as string) : from, - isArray(to) ? (to[index] as string) : to, + isArray(from) ? ((from[index] ?? from[0]) as string) : from, + isArray(to) ? ((to[index] ?? to[0]) as string) : to, ratio, alphaChannel ) as string; diff --git a/packages/vrender-core/src/common/custom-path2d.ts b/packages/vrender-core/src/common/custom-path2d.ts index 33dd884eb..c7dd551d7 100644 --- a/packages/vrender-core/src/common/custom-path2d.ts +++ b/packages/vrender-core/src/common/custom-path2d.ts @@ -139,15 +139,15 @@ export class CustomPath2D extends CurvePath implements ICustomPath2D { list[enumCommandMap.C] = (cmd: CommandType) => `C${cmd[1]} ${cmd[2]} ${cmd[3]} ${cmd[4]} ${cmd[5]} ${cmd[6]}`; list[enumCommandMap.A] = (cmd: CommandType) => { const bezierPathList: number[] = []; - addArcToBezierPath( - bezierPathList, - cmd[4] as number, - cmd[5] as number, - cmd[1] as number, - cmd[2] as number, - cmd[3] as number, - cmd[3] as number - ); + const x = cmd[1] as number; + const y = cmd[2] as number; + const radius = cmd[3] as number; + const startAngle = cmd[4] as number; + const endAngle = cmd[5] as number; + const counterclockwise = cmd[6] as boolean; + + addArcToBezierPath(bezierPathList, startAngle, endAngle, x, y, radius, radius, counterclockwise); + let path = ''; for (let i = 0; i < bezierPathList.length; i += 6) { path += `C${bezierPathList[i]} ${bezierPathList[i + 1]} ${bezierPathList[i + 2]} ${bezierPathList[i + 3]} ${ diff --git a/packages/vrender-core/src/common/morphing-utils.ts b/packages/vrender-core/src/common/morphing-utils.ts index 8bdf92ee2..f10d31ef2 100644 --- a/packages/vrender-core/src/common/morphing-utils.ts +++ b/packages/vrender-core/src/common/morphing-utils.ts @@ -332,263 +332,132 @@ export function alignBezierCurves(array1: number[][], array2: number[][]) { return [newArray1, newArray2]; } -const addLineToBezierPath = (bezierPath: number[], x0: number, y0: number, x1: number, y1: number) => { - if (!(isNumberClose(x0, x1) && isNumberClose(y0, y1))) { - bezierPath.push(x0, y0, x1, y1, x1, y1); - } -}; - +/** + * 将路径转换为贝塞尔曲线数组 + * 通过复用CustomPath2D中的方法,确保处理的一致性 + * @param path 要转换的路径 + * @returns 贝塞尔曲线数组 + */ export function pathToBezierCurves(path: ICustomPath2D): number[][] { - const commandList = path.commandList; - - const bezierArrayGroups: number[][] = []; - let currentSubpath: number[]; - - // end point - let xi: number = 0; - let yi: number = 0; - // start point - let x0: number = 0; - let y0: number = 0; - - const createNewSubpath = (x: number, y: number) => { - // More than one M command - if (currentSubpath && currentSubpath.length > 2) { - bezierArrayGroups.push(currentSubpath); - } - currentSubpath = [x, y]; - }; - - // the first control point - let x1: number; - let y1: number; - // the second control point - let x2: number; - let y2: number; - - for (let i = 0, len = commandList.length; i < len; i++) { - const cmd = commandList[i]; - - const isFirst = i === 0; - - if (isFirst) { - // 如果第一个命令是 L, C, Q - // 则 previous point 同绘制命令的第一个 point - // 第一个命令为 Arc 的情况下会在后面特殊处理 - x0 = xi = cmd[1] as number; - y0 = yi = cmd[2] as number; - - if ([enumCommandMap.L, enumCommandMap.C, enumCommandMap.Q].includes(cmd[0])) { - // Start point - currentSubpath = [x0, y0]; - } - } + // 创建临时路径和临时上下文 + const tempPath = new CustomPath2D(); - switch (cmd[0]) { - case enumCommandMap.M: - // moveTo 命令重新创建一个新的 subpath, 并且更新新的起点 - // 在 closePath 的时候使用 - xi = x0 = cmd[1] as number; - yi = y0 = cmd[2] as number; - - createNewSubpath(x0, y0); - break; - case enumCommandMap.L: - x1 = cmd[1] as number; - y1 = cmd[2] as number; - addLineToBezierPath(currentSubpath, xi, yi, x1, y1); - xi = x1; - yi = y1; - break; - case enumCommandMap.C: - currentSubpath.push( - cmd[1] as number, - cmd[2] as number, - cmd[3] as number, - cmd[4] as number, - (xi = cmd[5] as number), - (yi = cmd[6] as number) - ); - break; - case enumCommandMap.Q: - x1 = cmd[1] as number; - y1 = cmd[2] as number; - x2 = cmd[3] as number; - y2 = cmd[4] as number; - currentSubpath.push( - // Convert quadratic to cubic - xi + (2 / 3) * (x1 - xi), - yi + (2 / 3) * (y1 - yi), - x2 + (2 / 3) * (x1 - x2), - y2 + (2 / 3) * (y1 - y2), - x2, - y2 - ); - xi = x2; - yi = y2; - break; - case enumCommandMap.A: { - const cx = cmd[1] as number; - const cy = cmd[2] as number; - const rx = cmd[3] as number; - const ry = rx; - const startAngle = cmd[4] as number; - const endAngle = cmd[5] as number; - - const counterClockwise = !!(cmd[6] as number); - - x1 = Math.cos(startAngle) * rx + cx; - y1 = Math.sin(startAngle) * rx + cy; - if (isFirst) { - // 直接使用 arc 命令 - // 第一个命令起点还未定义 - x0 = x1; - y0 = y1; - createNewSubpath(x0, y0); - } else { - // Connect a line between current point to arc start point. - addLineToBezierPath(currentSubpath, xi, yi, x1, y1); - } - - xi = Math.cos(endAngle) * rx + cx; - yi = Math.sin(endAngle) * rx + cy; + // 将路径转换为SVG路径字符串,这样可以利用CustomPath2D中的解析能力 + const svgPathString = path.toString(); - const step = ((counterClockwise ? -1 : 1) * Math.PI) / 2; - - for (let angle = startAngle; counterClockwise ? angle > endAngle : angle < endAngle; angle += step) { - const nextAngle = counterClockwise ? Math.max(angle + step, endAngle) : Math.min(angle + step, endAngle); - addArcToBezierPath(currentSubpath, angle, nextAngle, cx, cy, rx, ry); - } - break; - } - case enumCommandMap.E: { - const cx = cmd[1] as number; - const cy = cmd[2] as number; - const rx = cmd[3] as number; - const ry = cmd[4] as number; - const rotate = cmd[5] as number; - const startAngle = cmd[6] as number; - const endAngle = (cmd[7] as number) + startAngle; - - const anticlockwise = !!(cmd[8] as number); - const hasRotate = !isNumberClose(rotate, 0); - const rc = Math.cos(rotate); - const rs = Math.sin(rotate); - - let xTemp = Math.cos(startAngle) * rx; - let yTemp = Math.sin(startAngle) * ry; - - if (hasRotate) { - x1 = xTemp * rc - yTemp * rs + cx; - y1 = xTemp * rs + yTemp * rc + cy; - } else { - x1 = xTemp + cx; - y1 = yTemp + cy; - } - if (isFirst) { - // 直接使用 arc 命令 - // 第一个命令起点还未定义 - x0 = x1; - y0 = y1; - createNewSubpath(x0, y0); - } else { - // Connect a line between current point to arc start point. - addLineToBezierPath(currentSubpath, xi, yi, x1, y1); - } + // 如果路径为空,直接返回空数组 + if (!svgPathString) { + return []; + } - xTemp = Math.cos(endAngle) * rx; - yTemp = Math.sin(endAngle) * ry; - if (hasRotate) { - xi = xTemp * rc - yTemp * rs + cx; - yi = xTemp * rs + yTemp * rc + cy; - } else { - xi = xTemp + cx; - yi = yTemp + cy; - } + // 使用临时路径解析SVG字符串 + tempPath.fromString(svgPathString); - const step = ((anticlockwise ? -1 : 1) * Math.PI) / 2; + // 确保曲线已经构建 + const curves = tempPath.tryBuildCurves(); - for (let angle = startAngle; anticlockwise ? angle > endAngle : angle < endAngle; angle += step) { - const nextAngle = anticlockwise ? Math.max(angle + step, endAngle) : Math.min(angle + step, endAngle); - addArcToBezierPath(currentSubpath, angle, nextAngle, cx, cy, rx, ry); + if (!curves || curves.length === 0) { + return []; + } - if (hasRotate) { - const curLen = currentSubpath.length; + // 用于存储分离的子路径 + const bezierSubpaths: number[][] = []; + let currentSubpath: number[] = null; + + // 初始化当前子路径 + currentSubpath = []; + let firstX = 0; // 记录子路径起点X (用于闭合路径) + let firstY = 0; // 记录子路径起点Y (用于闭合路径) + let lastX = 0; // 记录上一个点的X (用于连续线段) + let lastY = 0; // 记录上一个点的Y (用于连续线段) + let isSubpathStart = true; + let isPathClosed = false; + + for (let i = 0; i < curves.length; i++) { + const curve = curves[i]; + + // 如果是新的子路径开始或者第一个点 + if (isSubpathStart) { + firstX = curve.p0.x; + firstY = curve.p0.y; + lastX = firstX; + lastY = firstY; + currentSubpath = [firstX, firstY]; + bezierSubpaths.push(currentSubpath); + isSubpathStart = false; + } - for (let j = curLen - 6; j <= curLen - 1; j += 2) { - xTemp = currentSubpath[j]; - yTemp = currentSubpath[j + 1]; + // 处理不同类型的曲线 + if (curve.p1 && curve.p2 && curve.p3) { + // 三次贝塞尔曲线 + currentSubpath.push(curve.p1.x, curve.p1.y, curve.p2.x, curve.p2.y, curve.p3.x, curve.p3.y); + lastX = curve.p3.x; + lastY = curve.p3.y; + } else if (curve.p1 && curve.p2) { + // 二次贝塞尔曲线,转换为三次贝塞尔曲线 + const x1 = curve.p1.x; + const y1 = curve.p1.y; + const x2 = curve.p2.x; + const y2 = curve.p2.y; + + currentSubpath.push( + lastX + (2 / 3) * (x1 - lastX), + lastY + (2 / 3) * (y1 - lastY), + x2 + (2 / 3) * (x1 - x2), + y2 + (2 / 3) * (y1 - y2), + x2, + y2 + ); + + lastX = x2; + lastY = y2; + } else if (curve.p1) { + // 直线段,转换为贝塞尔曲线格式 + // 直线的情况,p1就是终点 + const endX = curve.p1.x; + const endY = curve.p1.y; + + // 避免添加长度为0的线段 + if (!(Math.abs(lastX - endX) < 1e-10 && Math.abs(lastY - endY) < 1e-10)) { + // 使用addLineToBezierPath的逻辑:x0,y0, x1,y1, x1,y1 + // 第一个控制点等于起点,第二个控制点等于终点,终点等于终点 + currentSubpath.push( + lastX, + lastY, // 第一个控制点 = 起点 + endX, + endY, // 第二个控制点 = 终点 + endX, + endY // 终点 + ); + } - currentSubpath[j] = (xTemp - cx) * rc - (yTemp - cy) * rs + cx; - currentSubpath[j + 1] = (xTemp - cx) * rs + (yTemp - cy) * rc + cy; - } - } - } + lastX = endX; + lastY = endY; + } - break; - } - case enumCommandMap.R: { - x0 = xi = cmd[1] as number; - y0 = yi = cmd[2] as number; - x1 = x0 + (cmd[3] as number); - y1 = y0 + (cmd[4] as number); - - // rect is an individual path. - createNewSubpath(x1, y0); - addLineToBezierPath(currentSubpath, x1, y0, x1, y1); - addLineToBezierPath(currentSubpath, x1, y1, x0, y1); - addLineToBezierPath(currentSubpath, x0, y1, x0, y0); - addLineToBezierPath(currentSubpath, x0, y0, x1, y0); - break; + // 检查是否是闭合路径(最后一个点回到起点) + if (i === curves.length - 1) { + if (Math.abs(lastX - firstX) < 1e-10 && Math.abs(lastY - firstY) < 1e-10) { + isPathClosed = true; } - case enumCommandMap.AT: { - const tx1 = cmd[1] as number; - const ty1 = cmd[2] as number; - const tx2 = cmd[3] as number; - const ty2 = cmd[4] as number; - const r = cmd[5] as number; - - const dis1 = PointService.distancePP({ x: xi, y: yi }, { x: tx1, y: ty1 }); - const dis2 = PointService.distancePP({ x: tx2, y: ty2 }, { x: tx1, y: ty1 }); - const theta = ((xi - tx1) * (tx2 - tx1) + (yi - ty1) * (ty2 - ty1)) / (dis1 * dis2); - const dis = r / Math.sin(theta / 2); - const midX = (xi + tx2 - 2 * tx1) / 2; - const midY = (yi + ty2 - 2 * ty1) / 2; - const midLen = PointService.distancePP({ x: midX, y: midY }, { x: 0, y: 0 }); - const cx = tx1 + (dis * midX) / midLen; - const cy = tx2 + (dis * midY) / midLen; - const disP = Math.sqrt(dis * dis - r * r); - x0 = tx1 + (disP * (xi - tx1)) / dis1; - y0 = ty1 + (disP * (yi - ty1)) / dis1; - - // Connect a line between current point to arc start point. - addLineToBezierPath(currentSubpath, xi, yi, x0, y0); - - xi = tx1 + (disP * (tx2 - tx1)) / dis2; - yi = ty1 + (disP * (ty2 - ty1)) / dis2; - - const startAngle = getAngleByPoint({ x: cx, y: cy }, { x: x0, y: y0 }); - - const endAngle = getAngleByPoint({ x: cx, y: cy }, { x: xi, y: yi }); - - addArcToBezierPath(currentSubpath, startAngle, endAngle, cx, cy, r, r); - - break; - } - case enumCommandMap.Z: { - currentSubpath && addLineToBezierPath(currentSubpath, xi, yi, x0, y0); - xi = x0; - yi = y0; - break; + } + + // 检查是否需要开始新的子路径 + // 只有在检测到明确的路径中断(不连续的点)时才开始新子路径 + if (i < curves.length - 1) { + const nextCurve = curves[i + 1]; + if (Math.abs(lastX - nextCurve.p0.x) > 1e-10 || Math.abs(lastY - nextCurve.p0.y) > 1e-10) { + // 当前子路径结束,需要创建新的子路径 + isSubpathStart = true; } } } - if (currentSubpath && currentSubpath.length > 2) { - bezierArrayGroups.push(currentSubpath); - } + // 移除空的子路径 + const validSubpaths = bezierSubpaths.filter(subpath => subpath.length > 2); - return bezierArrayGroups; + // 为了保持与原始函数一致,如果只有一个子路径,返回它的数组 + return validSubpaths.length === 1 ? [validSubpaths[0]] : validSubpaths; } export function applyTransformOnBezierCurves(bezierCurves: number[][], martrix: IMatrix) { diff --git a/packages/vrender-core/src/common/shape/arc.ts b/packages/vrender-core/src/common/shape/arc.ts index 2c6d36924..d9e4d432f 100644 --- a/packages/vrender-core/src/common/shape/arc.ts +++ b/packages/vrender-core/src/common/shape/arc.ts @@ -173,6 +173,17 @@ export function drawArc( * @license */ +/** + * 将弧形添加到贝塞尔路径 + * @param bezierPath 贝塞尔路径数组 + * @param startAngle 起始角度 + * @param endAngle 结束角度 + * @param cx 圆心x坐标 + * @param cy 圆心y坐标 + * @param rx x半径 + * @param ry y半径 + * @param counterclockwise 是否逆时针,默认为false(顺时针) + */ export const addArcToBezierPath = ( bezierPath: number[], startAngle: number, @@ -180,19 +191,47 @@ export const addArcToBezierPath = ( cx: number, cy: number, rx: number, - ry: number + ry: number, + counterclockwise: boolean = false ) => { // https://stackoverflow.com/questions/1734745/how-to-create-circle-with-b%C3%A9zier-curves - const delta = Math.abs(endAngle - startAngle); - const count = delta > 0.5 * Math.PI ? Math.ceil((2 * delta) / Math.PI) : 1; - const stepAngle = (endAngle - startAngle) / count; + // 标准化角度到 [0, 2π] 范围 + const PI2 = Math.PI * 2; + const sAngle = ((startAngle % PI2) + PI2) % PI2; + let eAngle = ((endAngle % PI2) + PI2) % PI2; + + // 确定角度差并进行角度调整 + let deltaAngle; + if (counterclockwise) { + // 逆时针时,确保终点角度小于起点角度 + if (eAngle >= sAngle) { + eAngle -= PI2; + } + deltaAngle = eAngle - sAngle; + } else { + // 顺时针时,确保终点角度大于起点角度 + if (eAngle <= sAngle) { + eAngle += PI2; + } + deltaAngle = eAngle - sAngle; + } + + // 计算需要分成的段数,每段不超过90度 + const count = Math.ceil(Math.abs(deltaAngle) / (Math.PI * 0.5)); + // 每段的角度增量 + const stepAngle = deltaAngle / count; + + // 对每段生成贝塞尔曲线 for (let i = 0; i < count; i++) { - const sa = startAngle + stepAngle * i; - const ea = startAngle + stepAngle * (i + 1); - const len = (Math.tan(Math.abs(stepAngle) / 4) * 4) / 3; - const dir = ea < sa ? -1 : 1; + const sa = sAngle + stepAngle * i; + const ea = sAngle + stepAngle * (i + 1); + + // 计算贝塞尔控制点的参数 + // 4/3 * tan(θ/4) 是贝塞尔曲线近似圆弧的最佳比例 + const len = (4 / 3) * Math.tan(Math.abs(stepAngle) / 4); + // 计算起点和终点坐标 const c1 = Math.cos(sa); const s1 = Math.sin(sa); const c2 = Math.cos(ea); @@ -204,17 +243,19 @@ export const addArcToBezierPath = ( const x4 = c2 * rx + cx; const y4 = s2 * ry + cy; - const hx = rx * len * dir; - const hy = ry * len * dir; + // 计算控制点坐标,符号根据方向调整 + const sign = counterclockwise ? -1 : 1; + const hx = rx * len * sign; + const hy = ry * len * sign; + // 将贝塞尔曲线点添加到路径 bezierPath.push( - // Move control points on tangent. - x1 - hx * s1, - y1 + hy * c1, - x4 + hx * s2, - y4 - hy * c2, - x4, - y4 + x1 - hx * s1, // 第一个控制点x + y1 + hy * c1, // 第一个控制点y + x4 + hx * s2, // 第二个控制点x + y4 - hy * c2, // 第二个控制点y + x4, // 终点x + y4 // 终点y ); } }; diff --git a/packages/vrender-core/src/graphic/arc.ts b/packages/vrender-core/src/graphic/arc.ts index 1c81904ae..2e69dce91 100644 --- a/packages/vrender-core/src/graphic/arc.ts +++ b/packages/vrender-core/src/graphic/arc.ts @@ -343,6 +343,10 @@ export class Arc extends Graphic implements IArc { } toCustomPath() { + let path = super.toCustomPath(); + if (path) { + return path; + } const x = 0; const y = 0; @@ -359,7 +363,7 @@ export class Arc extends Graphic implements IArc { innerRadius = temp; } - const path = new CustomPath2D(); + path = new CustomPath2D(); if (outerRadius <= epsilon) { path.moveTo(x, y); diff --git a/packages/vrender-core/src/graphic/area.ts b/packages/vrender-core/src/graphic/area.ts index e617e219a..391da50be 100644 --- a/packages/vrender-core/src/graphic/area.ts +++ b/packages/vrender-core/src/graphic/area.ts @@ -124,7 +124,11 @@ export class Area extends Graphic implements IArea { } toCustomPath() { - const path = new CustomPath2D(); + let path = super.toCustomPath(); + if (path) { + return path; + } + path = new CustomPath2D(); const attribute = this.attribute; const segments = attribute.segments; diff --git a/packages/vrender-core/src/graphic/circle.ts b/packages/vrender-core/src/graphic/circle.ts index 9140f2259..1b07cd925 100644 --- a/packages/vrender-core/src/graphic/circle.ts +++ b/packages/vrender-core/src/graphic/circle.ts @@ -100,6 +100,10 @@ export class Circle extends Graphic implements ICircle } toCustomPath() { + let path = super.toCustomPath(); + if (path) { + return path; + } const x = 0; const y = 0; @@ -108,7 +112,7 @@ export class Circle extends Graphic implements ICircle const startAngle = attribute.startAngle ?? this.getDefaultAttribute('startAngle'); const endAngle = attribute.endAngle ?? this.getDefaultAttribute('endAngle'); - const path = new CustomPath2D(); + path = new CustomPath2D(); path.arc(x, y, radius, startAngle, endAngle); diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index 16d938490..b25e6251c 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -51,6 +51,7 @@ import { builtinSymbolsMap, builtInSymbolStrMap, CustomSymbolClass } from './bui import { isSvg, XMLParser } from '../common/xml'; import { SVG_PARSE_ATTRIBUTE_MAP, SVG_PARSE_ATTRIBUTE_MAP_KEYS } from './constants'; import { DefaultStateAnimateConfig } from '../animate/config'; +import { EmptyContext2d } from '../canvas'; const _tempBounds = new AABBBounds(); /** @@ -1567,6 +1568,19 @@ export abstract class Graphic = Partial; abstract clone(): IGraphic; + + toCustomPath(): ICustomPath2D { + // throw new Error('暂不支持'); + const renderer = (this.stage?.renderService || application.renderService)?.drawContribution?.getRenderContribution( + this + ); + if (renderer) { + const context = new EmptyContext2d(null, 1); + renderer.drawShape(this, context, 0, 0, {} as any, {}); + return context.path; + } + return null; + } } Graphic.mixin(EventTarget); diff --git a/packages/vrender-core/src/graphic/line.ts b/packages/vrender-core/src/graphic/line.ts index d8bfa3f77..b69d6a2c8 100644 --- a/packages/vrender-core/src/graphic/line.ts +++ b/packages/vrender-core/src/graphic/line.ts @@ -117,8 +117,12 @@ export class Line extends Graphic implements ILine { } toCustomPath() { + let path = super.toCustomPath(); + if (path) { + return path; + } const attribute = this.attribute; - const path = new CustomPath2D(); + path = new CustomPath2D(); const segments = attribute.segments; const parsePoints = (points: IPointLike[]) => { diff --git a/packages/vrender-core/src/graphic/rect.ts b/packages/vrender-core/src/graphic/rect.ts index 7b2e4acc6..b586a79d1 100644 --- a/packages/vrender-core/src/graphic/rect.ts +++ b/packages/vrender-core/src/graphic/rect.ts @@ -7,6 +7,7 @@ import { application } from '../application'; import { RECT_NUMBER_TYPE } from './constants'; import { normalizeRectAttributes } from '../common/rect-utils'; import { updateBoundsOfCommonOuterBorder } from './graphic-service/common-outer-boder-bounds'; +import { EmptyContext2d } from '../canvas'; const RECT_UPDATE_TAG_KEY = ['width', 'x1', 'y1', 'height', 'cornerRadius', ...GRAPHIC_UPDATE_TAG_KEY]; @@ -71,10 +72,14 @@ export class Rect extends Graphic implements IRect { toCustomPath(): ICustomPath2D { // throw new Error('暂不支持'); + let path = super.toCustomPath(); + if (path) { + return path; + } const attribute = this.attribute; const { x, y, width, height } = normalizeRectAttributes(attribute); - const path = new CustomPath2D(); + path = new CustomPath2D(); path.moveTo(x, y); path.rect(x, y, width, height); diff --git a/packages/vrender-core/src/graphic/star.ts b/packages/vrender-core/src/graphic/star.ts index 6c601801e..7bfdbefad 100644 --- a/packages/vrender-core/src/graphic/star.ts +++ b/packages/vrender-core/src/graphic/star.ts @@ -127,9 +127,13 @@ export class Star extends Graphic implements IStar { } toCustomPath() { + let path = super.toCustomPath(); + if (path) { + return path; + } const starTheme = this.getGraphicTheme(); const points = this.getStarPoints(this.attribute, starTheme); - const path = new CustomPath2D(); + path = new CustomPath2D(); points.forEach((point, index) => { if (index === 0) { diff --git a/packages/vrender-core/src/modules.ts b/packages/vrender-core/src/modules.ts index acbf798c9..5a91cf26d 100644 --- a/packages/vrender-core/src/modules.ts +++ b/packages/vrender-core/src/modules.ts @@ -11,13 +11,14 @@ import loadRenderContributions from './render/contributions/modules'; import { LayerService } from './core/constants'; // import { IMat4Allocate, IMatrixAllocate, Mat4Allocate, MatrixAllocate } from './allocator/matrix-allocate'; // import { GlobalPickerService } from './picker/constants'; -import type { IGlobal, IGraphicService, IPickerService } from './interface'; +import type { IGlobal, IGraphicService, IPickerService, IRenderService } from './interface'; import { application } from './application'; import type { IGraphicUtil, ILayerService, ITransformUtil } from './interface/core'; import { GraphicService } from './graphic/constants'; import { GraphicUtil, TransformUtil } from './core/constants'; import { container } from './container'; import { VGlobal } from './constants'; +import { RenderService } from './render'; export function preLoadAllModule() { if (preLoadAllModule.__loaded) { @@ -49,6 +50,8 @@ export const transformUtil = container.get(TransformUtil); application.transformUtil = transformUtil; export const graphicService = container.get(GraphicService); application.graphicService = graphicService; +export const renderService = container.get(RenderService); +application.renderService = renderService; // export const matrixAllocate = container.get(MatrixAllocate); // export const mat4Allocate = container.get(Mat4Allocate); // export const canvasAllocate = container.get(CanvasAllocate); diff --git a/packages/vrender-kits/src/render/contributions/rough/context.ts b/packages/vrender-kits/src/render/contributions/rough/context.ts index df1c64b93..680e21276 100644 --- a/packages/vrender-kits/src/render/contributions/rough/context.ts +++ b/packages/vrender-kits/src/render/contributions/rough/context.ts @@ -54,6 +54,10 @@ export class RoughContext2d implements IContext2d { this.customPath = customPath; } + reset(setTransform: boolean = true) { + return this.originContext.reset(setTransform); + } + // Path-related methods that affect both the original context and the custom path beginPath(): void { this.originContext.beginPath(); diff --git a/packages/vrender/__tests__/browser/src/pages/morphing.ts b/packages/vrender/__tests__/browser/src/pages/morphing.ts index a12dbd027..c844e9d5a 100644 --- a/packages/vrender/__tests__/browser/src/pages/morphing.ts +++ b/packages/vrender/__tests__/browser/src/pages/morphing.ts @@ -10,7 +10,9 @@ import { createArc, registerAnimate, registerCustomAnimate, - oneToMultiMorph + oneToMultiMorph, + morphPath, + createGroup } from '@visactor/vrender'; import { colorPools } from '../utils'; @@ -178,65 +180,106 @@ export const page = () => { // morphPath(circle, rect, { duration: 2000, easing: 'quadIn' }); // morphPath(line, rect, { duration: 2000, ease: 'cubicIn' }); - const symbolList = []; - for (let i = 0; i < 21; i++) { + // const symbolList = []; + // for (let i = 0; i < 21; i++) { + // const symbols = createSymbol({ + // x: Math.random() * 500, + // y: Math.random() * 500, + // symbolType: 'arrow', + // size: 10, + // fill: colorPools[2], + // // angle: Math.PI / 4, + // lineWidth: 6 + // }); + // symbolList.push(symbols); + // stage.defaultLayer.appendChild(symbols); + // } + + // // stage.defaultLayer.appendChild(area); + + // oneToMultiMorph(area, symbolList, { duration: 2000, easing: 'quadIn' }); + + const fromSymbolList = []; + for (let i = 0; i < 23; i++) { const symbols = createSymbol({ x: Math.random() * 500, y: Math.random() * 500, - symbolType: 'arrow', - size: 10, - fill: colorPools[2], + symbolType: 'triangleLeft', + size: 5 + Math.floor(Math.random() * 10), + fill: 'green', + // stroke: 'red', // angle: Math.PI / 4, lineWidth: 6 }); - symbolList.push(symbols); - stage.defaultLayer.appendChild(symbols); + fromSymbolList.push(symbols); } + stage.defaultLayer.appendChild(rect); - // stage.defaultLayer.appendChild(area); + multiToOneMorph(fromSymbolList, rect, { + duration: 2000, + easing: 'quadIn', + // splitPath: 'clone', + individualDelay: (index, count, fromGraphic, toGraphic) => { + return index * 100; + } + }); - oneToMultiMorph(area, symbolList, { duration: 2000, easing: 'quadIn' }); + // const r1 = createRect({ + // visible: true, + // lineWidth: 0, + // fillOpacity: 0.8, + // // cornerRadius: 100, + // fill: '#FF8A00', + // stroke: '#FF8A00', + // x: 200, + // y: 200, + // height: 300, + // width: 100 + // }); + + // const a1 = createArc({ + // visible: true, + // lineWidth: 0, + // innerPadding: 0, + // outerPadding: 0, + // fillOpacity: 1, + // fill: '#FF8A00', + // padAngle: 0, + // stroke: '#FF8A00', + // x: 200, + // y: 200, + // startAngle: 0, + // endAngle: Math.PI, + // outerRadius: 200, + // innerRadius: 100, + // cornerRadius: 100 + // }); + // stage.defaultLayer.ad(r1); + // const group = createGroup({ + // x: 300, + // y: 200 + // }); + // group.add(a1); + // stage.defaultLayer.appendChild(a1); + // morphPath(r1, a1, { duration: 2000, easing: 'linear' }); + // stage.defaultLayer.appendChild(r1); + // stage.defaultLayer.appendChild(a1); + // morphPath(a1, r1, { duration: 2000, easing: 'linear' }); - // const fromSymbolList = []; - // for (let i = 0; i < 23; i++) { + // const fromSymbolList2 = []; + // for (let i = 0; i < 20; i++) { // const symbols = createSymbol({ - // x: Math.random() * 500, - // y: Math.random() * 500, + // x: 300 + i * 20, + // y: 300, // symbolType: 'triangleLeft', // size: 5 + Math.floor(Math.random() * 10), // fill: 'green', - // // stroke: 'red', // // angle: Math.PI / 4, // lineWidth: 6 // }); - // fromSymbolList.push(symbols); + // fromSymbolList2.push(symbols); + // // stage.defaultLayer.appendChild(symbols); // } - // stage.defaultLayer.appendChild(rect); - - // multiToOneMorph(fromSymbolList, rect, { - // duration: 2000, - // easing: 'quadIn', - // // splitPath: 'clone', - // individualDelay: (index, count, fromGraphic, toGraphic) => { - // return index * 100; - // } - // }); - // morphPath(fromSymbolList[0], polygon, { duration: 2000, easing: 'quadIn' }); - - const fromSymbolList2 = []; - for (let i = 0; i < 20; i++) { - const symbols = createSymbol({ - x: 300 + i * 20, - y: 300, - symbolType: 'triangleLeft', - size: 5 + Math.floor(Math.random() * 10), - fill: 'green', - // angle: Math.PI / 4, - lineWidth: 6 - }); - fromSymbolList2.push(symbols); - // stage.defaultLayer.appendChild(symbols); - } // stage.defaultLayer.appendChild(rect4); // multiToOneMorph(fromSymbolList2, rect4, { // duration: 2000, From 259a7503f7ebfc0f0ec17ed74d8169c915a44a4f Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 23 Apr 2025 15:53:52 +0800 Subject: [PATCH 105/179] fix: fix issue with clone params crash, fix issue with label get bounds, fix issue with clipin dimension, fix issue with fadeIn opacity --- packages/vrender-animate/src/custom/clip.ts | 8 ++ packages/vrender-animate/src/custom/fade.ts | 6 +- .../vrender-animate/src/custom/morphing.ts | 2 +- .../src/executor/animate-executor.ts | 20 ++-- packages/vrender-components/src/label/base.ts | 6 ++ .../vrender-core/src/graphic/richtext/icon.ts | 2 +- packages/vrender-core/src/index.ts | 8 ++ .../browser/src/pages/animate-ticker.ts | 93 +++++++++++++++++++ .../__tests__/browser/src/pages/index.ts | 4 + 9 files changed, 138 insertions(+), 11 deletions(-) create mode 100644 packages/vrender/__tests__/browser/src/pages/animate-ticker.ts diff --git a/packages/vrender-animate/src/custom/clip.ts b/packages/vrender-animate/src/custom/clip.ts index ea79a10fc..41c5861e8 100644 --- a/packages/vrender-animate/src/custom/clip.ts +++ b/packages/vrender-animate/src/custom/clip.ts @@ -13,6 +13,14 @@ export class ClipIn extends CommonIn { this.keys = ['clipRange']; this.from = { clipRange: 0 }; } + onFirstRun(): void { + super.onFirstRun(); + const { clipDimension } = this.params?.options || {}; + // 需要设置clipRangeByDimension + if (clipDimension) { + (this.target.attribute as any).clipRangeByDimension = clipDimension; + } + } } export class ClipOut extends CommonOut { diff --git a/packages/vrender-animate/src/custom/fade.ts b/packages/vrender-animate/src/custom/fade.ts index 64240227f..20cc9cdfc 100644 --- a/packages/vrender-animate/src/custom/fade.ts +++ b/packages/vrender-animate/src/custom/fade.ts @@ -10,8 +10,8 @@ export class FadeIn extends CommonIn { constructor(from: null, to: null, duration: number, easing: EasingType, params?: IScaleAnimationOptions) { super(from, to, duration, easing, params); - this.keys = ['opacity']; - this.from = { opacity: 0 }; + this.keys = ['opacity', 'fillOpacity', 'strokeOpacity']; + this.from = { opacity: 0, fillOpacity: 0, strokeOpacity: 0 }; } } @@ -20,6 +20,6 @@ export class FadeOut extends CommonOut { constructor(from: null, to: null, duration: number, easing: EasingType, params?: IScaleAnimationOptions) { super(from, to, duration, easing, params); - this.keys = ['opacity']; + this.keys = ['opacity', 'fillOpacity', 'strokeOpacity']; } } diff --git a/packages/vrender-animate/src/custom/morphing.ts b/packages/vrender-animate/src/custom/morphing.ts index 7a69c0d78..e5b65ba64 100644 --- a/packages/vrender-animate/src/custom/morphing.ts +++ b/packages/vrender-animate/src/custom/morphing.ts @@ -223,7 +223,7 @@ const parseOtherAnimateAttrs = ( }; res.push({ from: isArray(fromAttrs[fromKey]) - ? fromAttrs[fromKey].map(parseColor) + ? (fromAttrs[fromKey] as any).map(parseColor) : parseColor((fromAttrs as any)[fromKey]), to: isArray(toValue) ? toValue.map(parseColor) : parseColor(toValue), key: fromKey diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index b76b7f6b3..86ab9a35e 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -115,7 +115,7 @@ export class AnimateExecutor implements IAnimateExecutor { // execute只在mark层面调用,所以性能影响可以忽略 // TODO 存在性能问题,如果后续调用频繁,需要重新修改 - const parsedParams: Record = cloneDeep(params); + const parsedParams: Record = { ...params }; parsedParams.oneByOneDelay = 0; parsedParams.startTime = startTime; parsedParams.totalTime = totalTime; @@ -128,11 +128,19 @@ export class AnimateExecutor implements IAnimateExecutor { (parsedParams as IAnimationTimeline).timeSlices = [timeSlices]; } let sliceTime = 0; - ((parsedParams as IAnimationTimeline).timeSlices as IAnimationTimeSlice[]).forEach(slice => { - slice.delay = this.resolveValue(slice.delay, child, 0); - slice.delayAfter = this.resolveValue(slice.delayAfter, child, 0); - slice.duration = this.resolveValue(slice.duration, child, 300); - sliceTime += slice.delay + slice.duration + slice.delayAfter; + ((parsedParams as IAnimationTimeline).timeSlices as IAnimationTimeSlice[]) = ( + (parsedParams as IAnimationTimeline).timeSlices as IAnimationTimeSlice[] + ).map(slice => { + const delay = this.resolveValue(slice.delay, child, 0); + const delayAfter = this.resolveValue(slice.delayAfter, child, 0); + const duration = this.resolveValue(slice.duration, child, 300); + sliceTime += delay + duration + delayAfter; + return { + ...slice, + delay, + delayAfter, + duration + }; }); let oneByOneDelay = 0; let oneByOneTime = 0; diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index 7a7f5746e..f34a898ce 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -732,6 +732,12 @@ export class LabelBase extends AnimateComponent { ): IBoundsLike { if (graphic) { if (graphic.attribute.visible !== false) { + // TODO (这里有些hack)如果是入场的时候,需要使用finalAttribute + if (graphic.context?.animationState === 'appear') { + const clonedGraphic = graphic.clone(); + Object.assign(clonedGraphic.attribute, graphic.getAttributes(true)); + return clonedGraphic.AABBBounds; + } return graphic.AABBBounds; } const { x, y } = graphic.attribute; diff --git a/packages/vrender-core/src/graphic/richtext/icon.ts b/packages/vrender-core/src/graphic/richtext/icon.ts index 4d801fbcd..5e1c5f639 100644 --- a/packages/vrender-core/src/graphic/richtext/icon.ts +++ b/packages/vrender-core/src/graphic/richtext/icon.ts @@ -53,7 +53,7 @@ export class RichTextIcon extends Image implements IRichTextIcon { } animationBackUps?: { from: Record; to: Record } | undefined; incrementalAt?: number | undefined; - toCustomPath?: (() => ICustomPath2D) | undefined; + toCustomPath: (() => ICustomPath2D) | undefined; get width(): number { return (this.attribute.width ?? 0) + this._marginArray[1] + this._marginArray[3]; diff --git a/packages/vrender-core/src/index.ts b/packages/vrender-core/src/index.ts index 0e1830689..f2f10b795 100644 --- a/packages/vrender-core/src/index.ts +++ b/packages/vrender-core/src/index.ts @@ -103,3 +103,11 @@ export * from './plugins/builtin-plugin/3dview-transform-plugin'; export * from './plugins/builtin-plugin/flex-layout-plugin'; export * from './plugins/builtin-plugin/edit-module'; + +export const morphPath = {}; +export const multiToOneMorph = {}; +export const oneToMultiMorph = {}; +export class ACustomAnimate {} +export const AnimateGroup = {}; +export const Animate = {}; +export const defaultTicker = {}; diff --git a/packages/vrender/__tests__/browser/src/pages/animate-ticker.ts b/packages/vrender/__tests__/browser/src/pages/animate-ticker.ts new file mode 100644 index 000000000..119f34b76 --- /dev/null +++ b/packages/vrender/__tests__/browser/src/pages/animate-ticker.ts @@ -0,0 +1,93 @@ +import { + DefaultTicker, + DefaultTimeline, + Animate, + registerAnimate, + IncreaseCount, + InputText, + AnimateExecutor, + ACustomAnimate, + registerCustomAnimate, + ManualTicker +} from '@visactor/vrender-animate'; +import { + container, + createRect, + createStage, + createSymbol, + IGraphic, + vglobal, + createCircle, + createText, + createGroup, + createLine, + createPath +} from '@visactor/vrender'; +import type { EasingType } from '@visactor/vrender-animate'; +// container.load(roughModule); + +vglobal.setEnv('browser'); + +registerAnimate(); +registerCustomAnimate(); + +let stage: any; +let ticker; + +function addCase(name: string, container: HTMLElement, cb: (stage: any) => void) { + const button = document.createElement('button'); + button.innerText = name; + button.style.height = '26px'; + container.appendChild(button); + button.addEventListener('click', () => { + stage && stage.release(); + stage = createStage({ + canvas: 'main', + width: 900, + height: 600, + background: 'pink', + disableDirtyBounds: false, + canvasControled: false, + autoRender: true + }); + ticker = new ManualTicker(stage); + stage.ticker = ticker; + cb(stage); + }); +} + +export const page = () => { + const btnContainer = document.createElement('div'); + btnContainer.style.width = '1000px'; + btnContainer.style.background = '#cecece'; + btnContainer.style.display = 'flex'; + btnContainer.style.flexDirection = 'row'; + btnContainer.style.gap = '3px'; + btnContainer.style.flexWrap = 'wrap'; + btnContainer.style.height = '120px'; + const canvas = document.getElementById('main'); + // 将btnContainer添加到canvas之前 + canvas.parentNode.insertBefore(btnContainer, canvas); + + // Basic animation state registration and application + addCase('Basic Animation States', btnContainer, stage => { + // Create a rectangle to animate + const rect = createRect({ + x: 300, + y: 200, + width: 100, + height: 100, + fill: 'blue' + }); + rect.context = { id: 'rect1' }; + + console.log(rect); + + rect.animate().to({ x: 800 }, 1000, 'linear'); + + // Add to stage + stage.defaultLayer.add(rect); + + ticker.tickAt(300); + }); +}; diff --git a/packages/vrender/__tests__/browser/src/pages/index.ts b/packages/vrender/__tests__/browser/src/pages/index.ts index 96fd11ae7..2a7ed832d 100644 --- a/packages/vrender/__tests__/browser/src/pages/index.ts +++ b/packages/vrender/__tests__/browser/src/pages/index.ts @@ -27,6 +27,10 @@ export const pages = [ name: 'animate-state', path: 'animate-state' }, + { + name: 'animate-ticker', + path: 'animate-ticker' + }, { name: 'custom-animate', path: 'custom-animate' From 5bcb09c74ce035646720ac19a6b24124b78bb1b4 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 23 Apr 2025 16:35:42 +0800 Subject: [PATCH 106/179] fix: fix issue with axis update animateType --- packages/vrender-components/src/animation/axis-animate.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/vrender-components/src/animation/axis-animate.ts b/packages/vrender-components/src/animation/axis-animate.ts index 56963b797..a9e0ce4be 100644 --- a/packages/vrender-components/src/animation/axis-animate.ts +++ b/packages/vrender-components/src/animation/axis-animate.ts @@ -12,10 +12,9 @@ export class AxisEnter extends AComponentAnimate { const { config, lastScale, getTickCoord } = this.params; let ratio = 1; - - if (lastScale && getTickCoord) { + const currData = this.target.data; + if (lastScale && getTickCoord && currData) { ratio = 0.7; - const currData = this.target.data; const oldValue = lastScale.scale(currData.rawValue); const point = getTickCoord(oldValue); @@ -100,7 +99,7 @@ export class AxisUpdate extends AComponentAnimate { // ); // console.log('this.props', this.props, { ...this.target.attribute }); animator.animate(this.target, { - type: config.type ?? 'to', + type: 'to', to: { ...diffAttrs }, duration, easing, From bab8e27d536b410718da74b178a4f416278c3b82 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 23 Apr 2025 17:10:25 +0800 Subject: [PATCH 107/179] fix: fix scale issue with addUpdateBoundTag --- packages/vrender-animate/src/custom/scale.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/vrender-animate/src/custom/scale.ts b/packages/vrender-animate/src/custom/scale.ts index f27fbac4d..91ae9a4c6 100644 --- a/packages/vrender-animate/src/custom/scale.ts +++ b/packages/vrender-animate/src/custom/scale.ts @@ -100,6 +100,7 @@ export class ScaleIn extends ACustomAnimate> { if (this._updateFunction) { this._updateFunction(ratio); this.target.addUpdatePositionTag(); + this.target.addUpdateBoundTag(); } } } @@ -149,5 +150,6 @@ export class ScaleOut extends ACustomAnimate> { attribute[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; }); this.target.addUpdatePositionTag(); + this.target.addUpdateBoundTag(); } } From c89b74ce917386f5d044212ccaf4c688a3c533c4 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 23 Apr 2025 17:43:11 +0800 Subject: [PATCH 108/179] fix: fix issue with oneByOne --- .../src/executor/animate-executor.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index 86ab9a35e..c08f6abb0 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -143,12 +143,9 @@ export class AnimateExecutor implements IAnimateExecutor { }; }); let oneByOneDelay = 0; - let oneByOneTime = 0; if (oneByOne) { - oneByOneTime = Number(oneByOne); - oneByOneDelay = oneByOneTime; + oneByOneDelay = typeof oneByOne === 'number' ? (oneByOne as number) : oneByOne ? sliceTime : 0; } - parsedParams.oneByOne = oneByOneTime; parsedParams.oneByOneDelay = oneByOneDelay; let scale = 1; @@ -180,21 +177,18 @@ export class AnimateExecutor implements IAnimateExecutor { }) }; }); - parsedParams.oneByOne = oneByOneTime * scale; parsedParams.oneByOneDelay = oneByOneDelay * scale; (parsedParams as IAnimationTimeline).startTime = startTime * scale; } else { const delay = this.resolveValue((params as IAnimationTypeConfig).delay, child, 0); const delayAfter = this.resolveValue((params as IAnimationTypeConfig).delayAfter, child, 0); const duration = this.resolveValue((params as IAnimationTypeConfig).duration, child, 300); + const loopTime = delay + delayAfter + duration; let oneByOneDelay = 0; - let oneByOneTime = 0; if (oneByOne) { - oneByOneTime = Number(oneByOne); - oneByOneDelay = duration + oneByOneTime; + oneByOneDelay = typeof oneByOne === 'number' ? (oneByOne as number) : oneByOne ? loopTime : 0; } - parsedParams.oneByOne = oneByOneTime; parsedParams.oneByOneDelay = oneByOneDelay; parsedParams.custom = (params as IAnimationTypeConfig).custom ?? @@ -214,7 +208,6 @@ export class AnimateExecutor implements IAnimateExecutor { parsedParams.delay = delay * scale; parsedParams.delayAfter = delayAfter * scale; parsedParams.duration = duration * scale; - parsedParams.oneByOne = oneByOneTime * scale; parsedParams.oneByOneDelay = oneByOneDelay * scale; (parsedParams as IAnimationTypeConfig).startTime = startTime; } From 2a3d04120467d0314b971d5e974d103b586320f3 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 23 Apr 2025 19:56:31 +0800 Subject: [PATCH 109/179] fix: fix issue with label while has animate and compute bounds --- packages/vrender-components/src/label/base.ts | 21 ++++++++++++++++--- packages/vrender-core/src/graphic/graphic.ts | 2 +- packages/vrender-core/src/index.ts | 14 ++++++------- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index f34a898ce..321ae0b32 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -176,6 +176,14 @@ export class LabelBase extends AnimateComponent { if (isNil(this._idToGraphic) || (this._isCollectionBase && isNil(this._idToPoint))) { return; } + // 如果有动画的话,需要先设置入场的最终属性,否则无法计算放重叠、反色之类的逻辑 + const markAttributeList: any[] = []; + if (this._enableAnimation !== false) { + this._baseMarks.forEach(mark => { + markAttributeList.push(mark.attribute); + mark.initAttributes(mark.getAttributes(true)); + }); + } const { overlap, smartInvert, dataFilter, customLayoutFunc, customOverlapFunc } = this.attribute; let data = this.attribute.data; @@ -240,6 +248,12 @@ export class LabelBase extends AnimateComponent { } this._renderLabels(labels); + + if (this._enableAnimation !== false) { + this._baseMarks.forEach((mark, index) => { + mark.initAttributes(markAttributeList[index]); + }); + } } private _bindEvent(target: IGraphic) { @@ -732,8 +746,8 @@ export class LabelBase extends AnimateComponent { ): IBoundsLike { if (graphic) { if (graphic.attribute.visible !== false) { - // TODO (这里有些hack)如果是入场的时候,需要使用finalAttribute - if (graphic.context?.animationState === 'appear') { + // TODO 这里有些hack 如果有动画,需要使用finalAttribute + if (graphic.context?.animationState) { const clonedGraphic = graphic.clone(); Object.assign(clonedGraphic.attribute, graphic.getAttributes(true)); return clonedGraphic.AABBBounds; @@ -817,7 +831,8 @@ export class LabelBase extends AnimateComponent { relatedGraphics: this._idToGraphic, config: { ...enter, - type: item === text ? (enter as any).type : 'clipIn' + // 默认fadeIn + type: item === text ? (enter as any).type : 'fadeIn' } } } diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index b25e6251c..444f5769f 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -1012,7 +1012,7 @@ export abstract class Graphic = Partial Date: Fri, 25 Apr 2025 17:01:41 +0800 Subject: [PATCH 110/179] feat: support layoutRect in options --- packages/vrender-animate/src/custom/clip-graphic.ts | 2 +- packages/vrender-animate/src/custom/growHeight.ts | 11 +++++++++-- packages/vrender-animate/src/custom/move.ts | 11 ++++++----- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/vrender-animate/src/custom/clip-graphic.ts b/packages/vrender-animate/src/custom/clip-graphic.ts index 977352c89..ffd05dee9 100644 --- a/packages/vrender-animate/src/custom/clip-graphic.ts +++ b/packages/vrender-animate/src/custom/clip-graphic.ts @@ -18,7 +18,7 @@ export class ClipGraphicAnimate extends ACustomAnimate { easing: EasingType, params: { group: IGroup; clipGraphic: IGraphic } ) { - super(null, null, duration, easing, params); + super(null, {}, duration, easing, params); this.clipFromAttribute = from; this.clipToAttribute = to; this._group = params?.group; diff --git a/packages/vrender-animate/src/custom/growHeight.ts b/packages/vrender-animate/src/custom/growHeight.ts index 53506a630..b1131eba3 100644 --- a/packages/vrender-animate/src/custom/growHeight.ts +++ b/packages/vrender-animate/src/custom/growHeight.ts @@ -6,6 +6,7 @@ interface IGrowCartesianAnimationOptions { orient?: 'positive' | 'negative'; overall?: boolean | number; direction?: 'x' | 'y' | 'xy'; + layoutRect?: { width: number; height: number }; } interface IAnimationParameters { @@ -63,7 +64,10 @@ function growHeightInOverall( if (isNumber(options.overall)) { overallValue = options.overall; } else if (animationParameters.group) { - overallValue = (animationParameters as any).groupHeight ?? animationParameters.group.getBounds().height(); + overallValue = + (animationParameters as any).groupHeight ?? + options.layoutRect?.height ?? + animationParameters.group.getBounds().height(); (animationParameters as any).groupHeight = overallValue; } else { @@ -168,7 +172,10 @@ function growHeightOutOverall( if (isNumber(options.overall)) { overallValue = options.overall; } else if (animationParameters.group) { - overallValue = (animationParameters as any).groupHeight ?? animationParameters.group.getBounds().height(); + overallValue = + (animationParameters as any).groupHeight ?? + options.layoutRect?.height ?? + animationParameters.group.getBounds().height(); (animationParameters as any).groupHeight = overallValue; } else { diff --git a/packages/vrender-animate/src/custom/move.ts b/packages/vrender-animate/src/custom/move.ts index 32e71e49e..be3446ade 100644 --- a/packages/vrender-animate/src/custom/move.ts +++ b/packages/vrender-animate/src/custom/move.ts @@ -10,6 +10,7 @@ export interface IMoveAnimationOptions { offset?: number; point?: { x?: number; y?: number } | FunctionCallback<{ x?: number; y?: number }>; excludeChannels?: string[]; + layoutRect?: { width: number; height: number }; } interface IAnimationParameters { @@ -29,13 +30,13 @@ export const moveIn = ( options: IMoveAnimationOptions, animationParameters: IAnimationParameters ) => { - const { offset = 0, orient, direction, point: pointOpt, excludeChannels = [] } = options ?? {}; + const { offset = 0, orient, direction, point: pointOpt, excludeChannels = [], layoutRect = {} } = options ?? {}; let changedX = 0; let changedY = 0; if (orient === 'negative') { - changedX = graphic.stage.viewWidth; - changedY = graphic.stage.viewHeight; + changedX = layoutRect.width ?? graphic.stage.viewWidth; + changedY = layoutRect.height ?? graphic.stage.viewHeight; } changedX += offset; @@ -81,8 +82,8 @@ export const moveOut = ( // consider the offset of group // const groupBounds = graphic.parent ? graphic.parent.getBounds() : null; - const groupWidth = graphic.stage.viewWidth; - const groupHeight = graphic.stage.viewHeight; + const groupWidth = options.layoutRect?.width ?? graphic.stage.viewWidth; + const groupHeight = options.layoutRect?.height ?? graphic.stage.viewHeight; const changedX = (orient === 'negative' ? groupWidth : 0) + offset; const changedY = (orient === 'negative' ? groupHeight : 0) + offset; const point = isFunction(pointOpt) From 007f5f2fd0e8c248039f8af09ced3347fc6b5521 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 25 Apr 2025 17:22:38 +0800 Subject: [PATCH 111/179] fix: fix issue with controlOptions in timeline --- .../vrender-animate/src/executor/animate-executor.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index c08f6abb0..b288e0e0a 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -495,7 +495,7 @@ export class AnimateExecutor implements IAnimateExecutor { const slices = Array.isArray(timeSlices) ? timeSlices : [timeSlices]; slices.forEach(slice => { - this.applyTimeSliceToAnimate(slice, animate, graphic); + this.applyTimeSliceToAnimate(slice, animate, graphic, controlOptions); }); // 后等待 @@ -509,7 +509,12 @@ export class AnimateExecutor implements IAnimateExecutor { /** * 将时间切片应用到动画实例 */ - private applyTimeSliceToAnimate(slice: IAnimationTimeSlice, animate: IAnimate, graphic: IGraphic) { + private applyTimeSliceToAnimate( + slice: IAnimationTimeSlice, + animate: IAnimate, + graphic: IGraphic, + controlOptions: any + ) { const { effects, duration = 300, delay = 0, delayAfter = 0 } = slice; // 解析时间参数 @@ -555,7 +560,7 @@ export class AnimateExecutor implements IAnimateExecutor { duration as number, easing, customParameters, - null, + controlOptions, options, type, graphic From c4d5762cf5e14d0722f65682d96daff521c5c96e Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 25 Apr 2025 18:40:51 +0800 Subject: [PATCH 112/179] fix: fix type error --- packages/vrender-animate/src/custom/move.ts | 4 ++-- packages/vrender-core/src/color-string/interpolate.ts | 2 +- packages/vrender/__tests__/common/morphing-utils.test.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vrender-animate/src/custom/move.ts b/packages/vrender-animate/src/custom/move.ts index be3446ade..225076dbf 100644 --- a/packages/vrender-animate/src/custom/move.ts +++ b/packages/vrender-animate/src/custom/move.ts @@ -35,8 +35,8 @@ export const moveIn = ( let changedY = 0; if (orient === 'negative') { - changedX = layoutRect.width ?? graphic.stage.viewWidth; - changedY = layoutRect.height ?? graphic.stage.viewHeight; + changedX = (layoutRect as any).width ?? graphic.stage.viewWidth; + changedY = (layoutRect as any).height ?? graphic.stage.viewHeight; } changedX += offset; diff --git a/packages/vrender-core/src/color-string/interpolate.ts b/packages/vrender-core/src/color-string/interpolate.ts index 3f045bed2..fd6d61857 100644 --- a/packages/vrender-core/src/color-string/interpolate.ts +++ b/packages/vrender-core/src/color-string/interpolate.ts @@ -21,7 +21,7 @@ export function interpolateColor( alphaChannel: boolean, cb?: (fromArray: [number, number, number, number], toArray: [number, number, number, number]) => void ): false | string | IGradientColor | string[] { - if (Array.isArray(from) && !isNumber(from[0]) && Array.isArray(to) && !isNumber(to[0])) { + if ((Array.isArray(from) && !isNumber(from[0])) || (Array.isArray(to) && !isNumber(to[0]))) { // 待性能优化 const out: string[] = new Array(4).fill(0).map((_, index) => { return _interpolateColor( diff --git a/packages/vrender/__tests__/common/morphing-utils.test.ts b/packages/vrender/__tests__/common/morphing-utils.test.ts index d05c557be..77694abfb 100644 --- a/packages/vrender/__tests__/common/morphing-utils.test.ts +++ b/packages/vrender/__tests__/common/morphing-utils.test.ts @@ -7,7 +7,7 @@ it('pathToBezierCurves', () => { expect(pathToBezierCurves(path)).toEqual([[100, 100, 100, 100, 200, 200, 200, 200]]); path.fromString('L200,200C50,50,150,150,300,300'); - expect(pathToBezierCurves(path)).toEqual([[200, 200, 50, 50, 150, 150, 300, 300]]); + expect(pathToBezierCurves(path)).toEqual([[0, 0, 0, 0, 200, 200, 200, 200, 50, 50, 150, 150, 300, 300]]); }); it('alignSubpath empty paths', () => { From 67d62b573a17fd4fa196a7d37c48eb044639d779 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 25 Apr 2025 20:00:02 +0800 Subject: [PATCH 113/179] fix: fix issue with dependencies --- common/config/rush/pnpm-lock.yaml | 3 --- packages/vrender-core/package.json | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index b7b1f2b83..ee6b76691 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -399,9 +399,6 @@ importers: ../../packages/vrender-core: dependencies: - '@visactor/vrender-animate': - specifier: workspace:0.22.8 - version: link:../vrender-animate '@visactor/vutils': specifier: ~0.19.5 version: 0.19.5 diff --git a/packages/vrender-core/package.json b/packages/vrender-core/package.json index 1aaeba72a..33b3a7e1a 100644 --- a/packages/vrender-core/package.json +++ b/packages/vrender-core/package.json @@ -30,8 +30,7 @@ }, "dependencies": { "color-convert": "2.0.1", - "@visactor/vutils": "~0.19.5", - "@visactor/vrender-animate": "workspace:0.22.8" + "@visactor/vutils": "~0.19.5" }, "devDependencies": { "@internal/bundler": "workspace:*", From a6b721df15a8e191099de5cc3c6d86ae909818cb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 28 Apr 2025 09:14:22 +0000 Subject: [PATCH 114/179] build: prelease version 0.22.11 --- ...-tagPointsUpdate-end_2025-04-22-06-53.json | 10 ------- common/config/rush/pnpm-lock.yaml | 26 +++++++++---------- common/config/rush/version-policies.json | 2 +- docs/package.json | 2 +- packages/react-vrender-utils/CHANGELOG.json | 6 +++++ packages/react-vrender-utils/CHANGELOG.md | 7 ++++- packages/react-vrender-utils/package.json | 6 ++--- packages/react-vrender/CHANGELOG.json | 6 +++++ packages/react-vrender/CHANGELOG.md | 7 ++++- packages/react-vrender/package.json | 4 +-- packages/vrender-components/CHANGELOG.json | 6 +++++ packages/vrender-components/CHANGELOG.md | 7 ++++- packages/vrender-components/package.json | 6 ++--- packages/vrender-core/CHANGELOG.json | 12 +++++++++ packages/vrender-core/CHANGELOG.md | 9 ++++++- packages/vrender-core/package.json | 2 +- packages/vrender-kits/CHANGELOG.json | 6 +++++ packages/vrender-kits/CHANGELOG.md | 7 ++++- packages/vrender-kits/package.json | 4 +-- packages/vrender/CHANGELOG.json | 6 +++++ packages/vrender/CHANGELOG.md | 7 ++++- packages/vrender/package.json | 6 ++--- tools/bugserver-trigger/package.json | 8 +++--- 23 files changed, 113 insertions(+), 49 deletions(-) delete mode 100644 common/changes/@visactor/vrender-core/fix-tagPointsUpdate-end_2025-04-22-06-53.json diff --git a/common/changes/@visactor/vrender-core/fix-tagPointsUpdate-end_2025-04-22-06-53.json b/common/changes/@visactor/vrender-core/fix-tagPointsUpdate-end_2025-04-22-06-53.json deleted file mode 100644 index ac5b8402e..000000000 --- a/common/changes/@visactor/vrender-core/fix-tagPointsUpdate-end_2025-04-22-06-53.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender-core", - "comment": "fix: incorrect result for TagPointsUpdate animation", - "type": "none" - } - ], - "packageName": "@visactor/vrender-core" -} \ No newline at end of file diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 9da7a082f..a00bd3b3f 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: specifier: ~0.5.7 version: 0.5.7 '@visactor/vrender': - specifier: workspace:0.22.10 + specifier: workspace:0.22.11 version: link:../packages/vrender '@visactor/vutils': specifier: ~0.19.5 @@ -95,7 +95,7 @@ importers: ../../packages/react-vrender: dependencies: '@visactor/vrender': - specifier: workspace:0.22.10 + specifier: workspace:0.22.11 version: link:../vrender '@visactor/vutils': specifier: ~0.19.5 @@ -153,10 +153,10 @@ importers: ../../packages/react-vrender-utils: dependencies: '@visactor/react-vrender': - specifier: workspace:0.22.10 + specifier: workspace:0.22.11 version: link:../react-vrender '@visactor/vrender': - specifier: workspace:0.22.10 + specifier: workspace:0.22.11 version: link:../vrender '@visactor/vutils': specifier: ~0.19.5 @@ -211,10 +211,10 @@ importers: ../../packages/vrender: dependencies: '@visactor/vrender-core': - specifier: workspace:0.22.10 + specifier: workspace:0.22.11 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:0.22.10 + specifier: workspace:0.22.11 version: link:../vrender-kits devDependencies: '@internal/bundler': @@ -281,10 +281,10 @@ importers: ../../packages/vrender-components: dependencies: '@visactor/vrender-core': - specifier: workspace:0.22.10 + specifier: workspace:0.22.11 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:0.22.10 + specifier: workspace:0.22.11 version: link:../vrender-kits '@visactor/vscale': specifier: ~0.19.5 @@ -403,7 +403,7 @@ importers: specifier: 2.4.1 version: 2.4.1 '@visactor/vrender-core': - specifier: workspace:0.22.10 + specifier: workspace:0.22.11 version: link:../vrender-core '@visactor/vutils': specifier: ~0.19.5 @@ -519,16 +519,16 @@ importers: ../../tools/bugserver-trigger: dependencies: '@visactor/vrender': - specifier: workspace:0.22.10 + specifier: workspace:0.22.11 version: link:../../packages/vrender '@visactor/vrender-components': - specifier: workspace:0.22.10 + specifier: workspace:0.22.11 version: link:../../packages/vrender-components '@visactor/vrender-core': - specifier: workspace:0.22.10 + specifier: workspace:0.22.11 version: link:../../packages/vrender-core '@visactor/vrender-kits': - specifier: workspace:0.22.10 + specifier: workspace:0.22.11 version: link:../../packages/vrender-kits devDependencies: '@internal/bundler': diff --git a/common/config/rush/version-policies.json b/common/config/rush/version-policies.json index 24769dfed..7f86412ba 100644 --- a/common/config/rush/version-policies.json +++ b/common/config/rush/version-policies.json @@ -1 +1 @@ -[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"0.22.10","nextBump":"patch"}] +[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"0.22.11","nextBump":"patch"}] diff --git a/docs/package.json b/docs/package.json index 9867285a2..ce86be1c3 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,7 +13,7 @@ "@visactor/vchart": "1.3.0", "@visactor/vutils": "~0.19.5", "@visactor/vgrammar": "~0.5.7", - "@visactor/vrender": "workspace:0.22.10", + "@visactor/vrender": "workspace:0.22.11", "markdown-it": "^13.0.0", "highlight.js": "^11.8.0", "axios": "^1.4.0", diff --git a/packages/react-vrender-utils/CHANGELOG.json b/packages/react-vrender-utils/CHANGELOG.json index ab02c64b8..8b4603931 100644 --- a/packages/react-vrender-utils/CHANGELOG.json +++ b/packages/react-vrender-utils/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/react-vrender-utils", "entries": [ + { + "version": "0.22.11", + "tag": "@visactor/react-vrender-utils_v0.22.11", + "date": "Mon, 28 Apr 2025 09:08:54 GMT", + "comments": {} + }, { "version": "0.22.10", "tag": "@visactor/react-vrender-utils_v0.22.10", diff --git a/packages/react-vrender-utils/CHANGELOG.md b/packages/react-vrender-utils/CHANGELOG.md index 877733226..5c151bb69 100644 --- a/packages/react-vrender-utils/CHANGELOG.md +++ b/packages/react-vrender-utils/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/react-vrender-utils -This log was last generated on Mon, 21 Apr 2025 02:48:16 GMT and should not be manually modified. +This log was last generated on Mon, 28 Apr 2025 09:08:54 GMT and should not be manually modified. + +## 0.22.11 +Mon, 28 Apr 2025 09:08:54 GMT + +_Version update only_ ## 0.22.10 Mon, 21 Apr 2025 02:48:16 GMT diff --git a/packages/react-vrender-utils/package.json b/packages/react-vrender-utils/package.json index e5ee46d87..d9bb4a947 100644 --- a/packages/react-vrender-utils/package.json +++ b/packages/react-vrender-utils/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender-utils", - "version": "0.22.10", + "version": "0.22.11", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -24,8 +24,8 @@ "react-dom": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:0.22.10", - "@visactor/react-vrender": "workspace:0.22.10", + "@visactor/vrender": "workspace:0.22.11", + "@visactor/react-vrender": "workspace:0.22.11", "@visactor/vutils": "~0.19.5", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/react-vrender/CHANGELOG.json b/packages/react-vrender/CHANGELOG.json index e784078db..3b4810a84 100644 --- a/packages/react-vrender/CHANGELOG.json +++ b/packages/react-vrender/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/react-vrender", "entries": [ + { + "version": "0.22.11", + "tag": "@visactor/react-vrender_v0.22.11", + "date": "Mon, 28 Apr 2025 09:08:54 GMT", + "comments": {} + }, { "version": "0.22.10", "tag": "@visactor/react-vrender_v0.22.10", diff --git a/packages/react-vrender/CHANGELOG.md b/packages/react-vrender/CHANGELOG.md index ac8e4dbb1..1f4f6a97e 100644 --- a/packages/react-vrender/CHANGELOG.md +++ b/packages/react-vrender/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/react-vrender -This log was last generated on Mon, 21 Apr 2025 02:48:16 GMT and should not be manually modified. +This log was last generated on Mon, 28 Apr 2025 09:08:54 GMT and should not be manually modified. + +## 0.22.11 +Mon, 28 Apr 2025 09:08:54 GMT + +_Version update only_ ## 0.22.10 Mon, 21 Apr 2025 02:48:16 GMT diff --git a/packages/react-vrender/package.json b/packages/react-vrender/package.json index 4741b0619..592c34c2c 100644 --- a/packages/react-vrender/package.json +++ b/packages/react-vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender", - "version": "0.22.10", + "version": "0.22.11", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -23,7 +23,7 @@ "react": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:0.22.10", + "@visactor/vrender": "workspace:0.22.11", "@visactor/vutils": "~0.19.5", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/vrender-components/CHANGELOG.json b/packages/vrender-components/CHANGELOG.json index 7c3e3516e..45bbae3a4 100644 --- a/packages/vrender-components/CHANGELOG.json +++ b/packages/vrender-components/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender-components", "entries": [ + { + "version": "0.22.11", + "tag": "@visactor/vrender-components_v0.22.11", + "date": "Mon, 28 Apr 2025 09:08:54 GMT", + "comments": {} + }, { "version": "0.22.10", "tag": "@visactor/vrender-components_v0.22.10", diff --git a/packages/vrender-components/CHANGELOG.md b/packages/vrender-components/CHANGELOG.md index 4fa9d4762..28a96e3ac 100644 --- a/packages/vrender-components/CHANGELOG.md +++ b/packages/vrender-components/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender-components -This log was last generated on Mon, 21 Apr 2025 02:48:16 GMT and should not be manually modified. +This log was last generated on Mon, 28 Apr 2025 09:08:54 GMT and should not be manually modified. + +## 0.22.11 +Mon, 28 Apr 2025 09:08:54 GMT + +_Version update only_ ## 0.22.10 Mon, 21 Apr 2025 02:48:16 GMT diff --git a/packages/vrender-components/package.json b/packages/vrender-components/package.json index 60831aa7b..455bc75ed 100644 --- a/packages/vrender-components/package.json +++ b/packages/vrender-components/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-components", - "version": "0.22.10", + "version": "0.22.11", "description": "components library for dp visualization", "sideEffects": false, "main": "cjs/index.js", @@ -27,8 +27,8 @@ "dependencies": { "@visactor/vutils": "~0.19.5", "@visactor/vscale": "~0.19.5", - "@visactor/vrender-core": "workspace:0.22.10", - "@visactor/vrender-kits": "workspace:0.22.10" + "@visactor/vrender-core": "workspace:0.22.11", + "@visactor/vrender-kits": "workspace:0.22.11" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-core/CHANGELOG.json b/packages/vrender-core/CHANGELOG.json index 837f526fd..5f44b43e4 100644 --- a/packages/vrender-core/CHANGELOG.json +++ b/packages/vrender-core/CHANGELOG.json @@ -1,6 +1,18 @@ { "name": "@visactor/vrender-core", "entries": [ + { + "version": "0.22.11", + "tag": "@visactor/vrender-core_v0.22.11", + "date": "Mon, 28 Apr 2025 09:08:54 GMT", + "comments": { + "none": [ + { + "comment": "fix: incorrect result for TagPointsUpdate animation" + } + ] + } + }, { "version": "0.22.10", "tag": "@visactor/vrender-core_v0.22.10", diff --git a/packages/vrender-core/CHANGELOG.md b/packages/vrender-core/CHANGELOG.md index 49c1c6b2a..c154dae4b 100644 --- a/packages/vrender-core/CHANGELOG.md +++ b/packages/vrender-core/CHANGELOG.md @@ -1,6 +1,13 @@ # Change Log - @visactor/vrender-core -This log was last generated on Mon, 21 Apr 2025 02:48:16 GMT and should not be manually modified. +This log was last generated on Mon, 28 Apr 2025 09:08:54 GMT and should not be manually modified. + +## 0.22.11 +Mon, 28 Apr 2025 09:08:54 GMT + +### Updates + +- fix: incorrect result for TagPointsUpdate animation ## 0.22.10 Mon, 21 Apr 2025 02:48:16 GMT diff --git a/packages/vrender-core/package.json b/packages/vrender-core/package.json index 605bec07d..77e2535a8 100644 --- a/packages/vrender-core/package.json +++ b/packages/vrender-core/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-core", - "version": "0.22.10", + "version": "0.22.11", "description": "", "sideEffects": [ "./src/modules.ts", diff --git a/packages/vrender-kits/CHANGELOG.json b/packages/vrender-kits/CHANGELOG.json index 4546344ae..2ec334646 100644 --- a/packages/vrender-kits/CHANGELOG.json +++ b/packages/vrender-kits/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender-kits", "entries": [ + { + "version": "0.22.11", + "tag": "@visactor/vrender-kits_v0.22.11", + "date": "Mon, 28 Apr 2025 09:08:54 GMT", + "comments": {} + }, { "version": "0.22.10", "tag": "@visactor/vrender-kits_v0.22.10", diff --git a/packages/vrender-kits/CHANGELOG.md b/packages/vrender-kits/CHANGELOG.md index b3df28c49..1867599ee 100644 --- a/packages/vrender-kits/CHANGELOG.md +++ b/packages/vrender-kits/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender-kits -This log was last generated on Mon, 21 Apr 2025 02:48:16 GMT and should not be manually modified. +This log was last generated on Mon, 28 Apr 2025 09:08:54 GMT and should not be manually modified. + +## 0.22.11 +Mon, 28 Apr 2025 09:08:54 GMT + +_Version update only_ ## 0.22.10 Mon, 21 Apr 2025 02:48:16 GMT diff --git a/packages/vrender-kits/package.json b/packages/vrender-kits/package.json index 7424ad503..2e9637019 100644 --- a/packages/vrender-kits/package.json +++ b/packages/vrender-kits/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-kits", - "version": "0.22.10", + "version": "0.22.11", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -21,7 +21,7 @@ }, "dependencies": { "@visactor/vutils": "~0.19.5", - "@visactor/vrender-core": "workspace:0.22.10", + "@visactor/vrender-core": "workspace:0.22.11", "@resvg/resvg-js": "2.4.1", "roughjs": "4.5.2", "gifuct-js": "2.1.2", diff --git a/packages/vrender/CHANGELOG.json b/packages/vrender/CHANGELOG.json index 32f8ba4d6..1eccfc4ff 100644 --- a/packages/vrender/CHANGELOG.json +++ b/packages/vrender/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender", "entries": [ + { + "version": "0.22.11", + "tag": "@visactor/vrender_v0.22.11", + "date": "Mon, 28 Apr 2025 09:08:54 GMT", + "comments": {} + }, { "version": "0.22.10", "tag": "@visactor/vrender_v0.22.10", diff --git a/packages/vrender/CHANGELOG.md b/packages/vrender/CHANGELOG.md index 44bfb60cf..195629e90 100644 --- a/packages/vrender/CHANGELOG.md +++ b/packages/vrender/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender -This log was last generated on Mon, 21 Apr 2025 02:48:16 GMT and should not be manually modified. +This log was last generated on Mon, 28 Apr 2025 09:08:54 GMT and should not be manually modified. + +## 0.22.11 +Mon, 28 Apr 2025 09:08:54 GMT + +_Version update only_ ## 0.22.10 Mon, 21 Apr 2025 02:48:16 GMT diff --git a/packages/vrender/package.json b/packages/vrender/package.json index 7a442588b..837931a21 100644 --- a/packages/vrender/package.json +++ b/packages/vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender", - "version": "0.22.10", + "version": "0.22.11", "description": "", "sideEffects": true, "main": "cjs/index.js", @@ -24,8 +24,8 @@ "test-watch": "cross-env DEBUG_MODE=1 jest --watch" }, "dependencies": { - "@visactor/vrender-core": "workspace:0.22.10", - "@visactor/vrender-kits": "workspace:0.22.10" + "@visactor/vrender-core": "workspace:0.22.11", + "@visactor/vrender-kits": "workspace:0.22.11" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/tools/bugserver-trigger/package.json b/tools/bugserver-trigger/package.json index 68c6b42e9..11acabaac 100644 --- a/tools/bugserver-trigger/package.json +++ b/tools/bugserver-trigger/package.json @@ -8,10 +8,10 @@ "ci": "ts-node --transpileOnly --skipProject ./scripts/trigger-test.ts" }, "dependencies": { - "@visactor/vrender": "workspace:0.22.10", - "@visactor/vrender-core": "workspace:0.22.10", - "@visactor/vrender-kits": "workspace:0.22.10", - "@visactor/vrender-components": "workspace:0.22.10" + "@visactor/vrender": "workspace:0.22.11", + "@visactor/vrender-core": "workspace:0.22.11", + "@visactor/vrender-kits": "workspace:0.22.11", + "@visactor/vrender-components": "workspace:0.22.11" }, "devDependencies": { "@rushstack/eslint-patch": "~1.1.4", From b68cfa9365168a5ab2c526c4fbaf31b31a3aa578 Mon Sep 17 00:00:00 2001 From: neuqzxy Date: Mon, 28 Apr 2025 11:02:01 +0000 Subject: [PATCH 115/179] docs: generate changelog of release v0.22.11 --- docs/assets/changelog/en/changelog.md | 13 +++++++++++++ docs/assets/changelog/zh/changelog.md | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/docs/assets/changelog/en/changelog.md b/docs/assets/changelog/en/changelog.md index 1d3a91d51..b4f382f30 100644 --- a/docs/assets/changelog/en/changelog.md +++ b/docs/assets/changelog/en/changelog.md @@ -1,3 +1,16 @@ +# v0.22.11 + +2025-04-28 + + +**🐛 Bug fix** + +- **@visactor/vrender-core**: incorrect result for TagPointsUpdate animation + + + +[more detail about v0.22.11](https://github.com/VisActor/VRender/releases/tag/v0.22.11) + # v0.22.10 2025-04-21 diff --git a/docs/assets/changelog/zh/changelog.md b/docs/assets/changelog/zh/changelog.md index c6a42adcc..a18dc48c9 100644 --- a/docs/assets/changelog/zh/changelog.md +++ b/docs/assets/changelog/zh/changelog.md @@ -1,3 +1,16 @@ +# v0.22.11 + +2025-04-28 + + +**🐛 功能修复** + +- **@visactor/vrender-core**: incorrect result for TagPointsUpdate animation + + + +[更多详情请查看 v0.22.11](https://github.com/VisActor/VRender/releases/tag/v0.22.11) + # v0.22.10 2025-04-21 From 2bd7618d3d3ea01d178de2f7c3d303b1e9ee26bf Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 6 May 2025 19:49:21 +0800 Subject: [PATCH 116/179] fix: fix issue with single graphicService call all stage --- packages/vrender-core/src/core/stage.ts | 9 +++-- .../graphic/graphic-service/graphic-module.ts | 2 +- packages/vrender-core/src/graphic/graphic.ts | 8 ++--- packages/vrender-core/src/graphic/group.ts | 14 ++++---- packages/vrender-core/src/index.ts | 14 ++++---- packages/vrender-core/src/interface/stage.ts | 2 ++ .../builtin-plugin/auto-render-plugin.ts | 22 +++++++----- .../builtin-plugin/dirty-bounds-plugin.ts | 35 +++++++++++-------- .../builtin-plugin/flex-layout-plugin.ts | 33 ++++++++++------- .../builtin-plugin/html-attribute-plugin.ts | 4 +-- .../incremental-auto-render-plugin.ts | 26 +++++++++----- 11 files changed, 102 insertions(+), 67 deletions(-) diff --git a/packages/vrender-core/src/core/stage.ts b/packages/vrender-core/src/core/stage.ts index 7859040c1..d36ea65e8 100644 --- a/packages/vrender-core/src/core/stage.ts +++ b/packages/vrender-core/src/core/stage.ts @@ -24,7 +24,8 @@ import type { IOptimizeType, LayerMode, PickResult, - IPlugin + IPlugin, + IGraphicService } from '../interface'; import { VWindow } from './window'; import type { Layer } from './layer'; @@ -44,7 +45,7 @@ import { LayerService } from './constants'; import { application } from '../application'; import { isBrowserEnv } from '../env-check'; import { Factory } from '../factory'; -import { Graphic } from '../graphic'; +import { Graphic, GraphicService } from '../graphic'; const DefaultConfig = { WIDTH: 500, @@ -178,6 +179,7 @@ export class Stage extends Group implements IStage { protected pickerService?: IPickerService; readonly pluginService: IPluginService; readonly layerService: ILayerService; + readonly graphicService: IGraphicService; private _eventSystem?: EventSystem; private get eventSystem(): EventSystem { return this._eventSystem; @@ -241,6 +243,7 @@ export class Stage extends Group implements IStage { this.renderService = container.get(RenderService); this.pluginService = container.get(PluginService); this.layerService = container.get(LayerService); + this.graphicService = container.get(GraphicService); this.pluginService.active(this, params); this.window.create({ @@ -303,7 +306,7 @@ export class Stage extends Group implements IStage { this.supportInteractiveLayer = params.interactiveLayer !== false; if (!params.optimize) { params.optimize = { - tickRenderMode: 'performance' + tickRenderMode: 'effect' }; } this.optmize(params.optimize); diff --git a/packages/vrender-core/src/graphic/graphic-service/graphic-module.ts b/packages/vrender-core/src/graphic/graphic-service/graphic-module.ts index 4a0eacacb..d46fbfd37 100644 --- a/packages/vrender-core/src/graphic/graphic-service/graphic-module.ts +++ b/packages/vrender-core/src/graphic/graphic-service/graphic-module.ts @@ -6,7 +6,7 @@ import { graphicCreator } from '../graphic-creator'; // import { DefaultThemeService, Theme, ThemeServce } from './theme-service'; export default new ContainerModule(bind => { - bind(GraphicService).to(DefaultGraphicService).inSingletonScope(); + bind(GraphicService).to(DefaultGraphicService); bind(GraphicCreator).toConstantValue(graphicCreator); }); diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index 444f5769f..96919bafb 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -374,10 +374,10 @@ export abstract class Graphic = Partial = Partial = Partial implements IGroup { if (!this.shouldUpdateAABBBounds()) { return this._AABBBounds; } - application.graphicService.beforeUpdateAABBBounds(this, this.stage, true, this._AABBBounds); + this.stage?.graphicService.beforeUpdateAABBBounds(this, this.stage, true, this._AABBBounds); const selfChange = this.shouldSelfChangeUpdateAABBBounds(); const bounds = this.doUpdateAABBBounds(); this.addUpdateLayoutTag(); - application.graphicService.afterUpdateAABBBounds(this, this.stage, this._AABBBounds, this, selfChange); + this.stage?.graphicService.afterUpdateAABBBounds(this, this.stage, this._AABBBounds, this, selfChange); // 直接返回空Bounds,但是前面的流程还是要走 if (this.attribute.boundsMode === 'empty') { bounds.clear(); @@ -259,13 +259,13 @@ export class Group extends Graphic implements IGroup { (data as unknown as this).layer = this.layer; } this.addUpdateBoundTag(); - application.graphicService.onAddIncremental(node as unknown as IGraphic, this, this.stage); + this.stage?.graphicService.onAddIncremental(node as unknown as IGraphic, this, this.stage); return data; } incrementalClearChild(): void { super.removeAllChild(); this.addUpdateBoundTag(); - application.graphicService.onClearIncremental(this, this.stage); + this.stage?.graphicService.onClearIncremental(this, this.stage); return; } @@ -298,14 +298,14 @@ export class Group extends Graphic implements IGroup { removeChild(child: IGraphic): IGraphic { const data = super.removeChild(child); child.stage = null; - application.graphicService.onRemove(child); + this.stage?.graphicService.onRemove(child); this.addUpdateBoundTag(); return data as IGraphic; } removeAllChild(deep: boolean = false): void { this.forEachChildren((child: IGraphic) => { - application.graphicService.onRemove(child); + this.stage?.graphicService.onRemove(child); if (deep && child.isContainer) { child.removeAllChild(deep); } @@ -320,7 +320,7 @@ export class Group extends Graphic implements IGroup { this.layer = layer; this.setStageToShadowRoot(stage, layer); this._onSetStage && this._onSetStage(this, stage, layer); - application.graphicService.onSetStage(this, stage); + this.stage?.graphicService.onSetStage(this, stage); this.forEachChildren(item => { (item as any).setStage(stage, this.layer); }); diff --git a/packages/vrender-core/src/index.ts b/packages/vrender-core/src/index.ts index 541d92f29..f2f10b795 100644 --- a/packages/vrender-core/src/index.ts +++ b/packages/vrender-core/src/index.ts @@ -104,10 +104,10 @@ export * from './plugins/builtin-plugin/flex-layout-plugin'; export * from './plugins/builtin-plugin/edit-module'; -// export const morphPath = {}; -// export const multiToOneMorph = {}; -// export const oneToMultiMorph = {}; -// export class ACustomAnimate {} -// export const AnimateGroup = {}; -// export const Animate = {}; -// export const defaultTicker = {}; +export const morphPath = {}; +export const multiToOneMorph = {}; +export const oneToMultiMorph = {}; +export class ACustomAnimate {} +export const AnimateGroup = {}; +export const Animate = {}; +export const defaultTicker = {}; diff --git a/packages/vrender-core/src/interface/stage.ts b/packages/vrender-core/src/interface/stage.ts index deff4d747..77508e7ca 100644 --- a/packages/vrender-core/src/interface/stage.ts +++ b/packages/vrender-core/src/interface/stage.ts @@ -13,6 +13,7 @@ import type { IPlugin, IPluginService } from './plugin'; import type { IWindow } from './window'; import type { ILayerService } from './core'; import type { IFullThemeSpec } from './graphic/theme'; +import type { IGraphicService } from './graphic-service'; export type IExportType = 'canvas' | 'imageData'; @@ -172,6 +173,7 @@ export interface IStage extends INode { ticker: ITicker; increaseAutoRender: boolean; readonly renderService: IRenderService; + readonly graphicService: IGraphicService; getPickerService: () => IPickerService; readonly pluginService: IPluginService; readonly layerService: ILayerService; diff --git a/packages/vrender-core/src/plugins/builtin-plugin/auto-render-plugin.ts b/packages/vrender-core/src/plugins/builtin-plugin/auto-render-plugin.ts index c47bf877c..758d5854d 100644 --- a/packages/vrender-core/src/plugins/builtin-plugin/auto-render-plugin.ts +++ b/packages/vrender-core/src/plugins/builtin-plugin/auto-render-plugin.ts @@ -20,9 +20,13 @@ export class AutoRenderPlugin implements IPlugin { activate(context: IPluginService): void { this.pluginService = context; - application.graphicService.hooks.onAttributeUpdate.tap(this.key, this.handleChange); - application.graphicService.hooks.onSetStage.tap(this.key, this.handleChange); - application.graphicService.hooks.onRemove.tap(this.key, this.handleChange); + const stage = this.pluginService.stage; + if (!stage) { + return; + } + stage.graphicService.hooks.onAttributeUpdate.tap(this.key, this.handleChange); + stage.graphicService.hooks.onSetStage.tap(this.key, this.handleChange); + stage.graphicService.hooks.onRemove.tap(this.key, this.handleChange); } deactivate(context: IPluginService): void { const filterByName = (taps: FullTap[]) => { @@ -30,11 +34,13 @@ export class AutoRenderPlugin implements IPlugin { return item.name !== this.key; }); }; + const stage = this.pluginService.stage; + if (!stage) { + return; + } - application.graphicService.hooks.onAttributeUpdate.taps = filterByName( - application.graphicService.hooks.onAttributeUpdate.taps - ); - application.graphicService.hooks.onSetStage.taps = filterByName(application.graphicService.hooks.onSetStage.taps); - application.graphicService.hooks.onRemove.taps = filterByName(application.graphicService.hooks.onRemove.taps); + stage.graphicService.hooks.onAttributeUpdate.taps = filterByName(stage.graphicService.hooks.onAttributeUpdate.taps); + stage.graphicService.hooks.onSetStage.taps = filterByName(stage.graphicService.hooks.onSetStage.taps); + stage.graphicService.hooks.onRemove.taps = filterByName(stage.graphicService.hooks.onRemove.taps); } } diff --git a/packages/vrender-core/src/plugins/builtin-plugin/dirty-bounds-plugin.ts b/packages/vrender-core/src/plugins/builtin-plugin/dirty-bounds-plugin.ts index 9e4557724..cec9c9226 100644 --- a/packages/vrender-core/src/plugins/builtin-plugin/dirty-bounds-plugin.ts +++ b/packages/vrender-core/src/plugins/builtin-plugin/dirty-bounds-plugin.ts @@ -21,7 +21,11 @@ export class DirtyBoundsPlugin implements IPlugin { } stage.dirtyBounds.clear(); }); - application.graphicService.hooks.beforeUpdateAABBBounds.tap( + const stage = this.pluginService.stage; + if (!stage) { + return; + } + stage.graphicService.hooks.beforeUpdateAABBBounds.tap( this.key, (graphic: IGraphic, stage: IStage, willUpdate: boolean, bounds: IAABBBounds) => { if (graphic.glyphHost) { @@ -40,7 +44,7 @@ export class DirtyBoundsPlugin implements IPlugin { } } ); - application.graphicService.hooks.afterUpdateAABBBounds.tap( + stage.graphicService.hooks.afterUpdateAABBBounds.tap( this.key, ( graphic: IGraphic, @@ -59,7 +63,7 @@ export class DirtyBoundsPlugin implements IPlugin { stage.dirty(params.globalAABBBounds); } ); - application.graphicService.hooks.clearAABBBounds.tap( + stage.graphicService.hooks.clearAABBBounds.tap( this.key, (graphic: IGraphic, stage: IStage, bounds: IAABBBounds) => { if (!(stage && stage === this.pluginService.stage && stage.renderCount)) { @@ -70,7 +74,7 @@ export class DirtyBoundsPlugin implements IPlugin { } } ); - application.graphicService.hooks.onRemove.tap(this.key, (graphic: IGraphic) => { + stage.graphicService.hooks.onRemove.tap(this.key, (graphic: IGraphic) => { const stage = graphic.stage; if (!(stage && stage === this.pluginService.stage && stage.renderCount)) { return; @@ -81,22 +85,25 @@ export class DirtyBoundsPlugin implements IPlugin { }); } deactivate(context: IPluginService): void { - application.graphicService.hooks.beforeUpdateAABBBounds.taps = - application.graphicService.hooks.beforeUpdateAABBBounds.taps.filter(item => { + const stage = this.pluginService.stage; + if (!stage) { + return; + } + stage.graphicService.hooks.beforeUpdateAABBBounds.taps = + stage.graphicService.hooks.beforeUpdateAABBBounds.taps.filter(item => { return item.name !== this.key; }); - application.graphicService.hooks.afterUpdateAABBBounds.taps = - application.graphicService.hooks.afterUpdateAABBBounds.taps.filter(item => { + stage.graphicService.hooks.afterUpdateAABBBounds.taps = + stage.graphicService.hooks.afterUpdateAABBBounds.taps.filter(item => { return item.name !== this.key; }); - application.graphicService.hooks.clearAABBBounds.taps = - application.graphicService.hooks.clearAABBBounds.taps.filter(item => { - return item.name !== this.key; - }); - context.stage.hooks.afterRender.taps = context.stage.hooks.afterRender.taps.filter(item => { + stage.graphicService.hooks.clearAABBBounds.taps = stage.graphicService.hooks.clearAABBBounds.taps.filter(item => { + return item.name !== this.key; + }); + stage.hooks.afterRender.taps = stage.hooks.afterRender.taps.filter(item => { return item.name !== this.key; }); - application.graphicService.hooks.onRemove.taps = application.graphicService.hooks.onRemove.taps.filter(item => { + stage.graphicService.hooks.onRemove.taps = stage.graphicService.hooks.onRemove.taps.filter(item => { return item.name !== this.key; }); } diff --git a/packages/vrender-core/src/plugins/builtin-plugin/flex-layout-plugin.ts b/packages/vrender-core/src/plugins/builtin-plugin/flex-layout-plugin.ts index d56888e39..752281ea7 100644 --- a/packages/vrender-core/src/plugins/builtin-plugin/flex-layout-plugin.ts +++ b/packages/vrender-core/src/plugins/builtin-plugin/flex-layout-plugin.ts @@ -456,8 +456,12 @@ export class FlexLayoutPlugin implements IPlugin { activate(context: IPluginService): void { this.pluginService = context; + const stage = this.pluginService.stage; + if (!stage) { + return; + } // 属性更新 - application.graphicService.hooks.onAttributeUpdate.tap(this.key, graphic => { + stage.graphicService.hooks.onAttributeUpdate.tap(this.key, graphic => { if (graphic.glyphHost) { graphic = graphic.glyphHost; } @@ -467,7 +471,7 @@ export class FlexLayoutPlugin implements IPlugin { this.tryLayout(graphic, false); }); // 包围盒更新(如果包围盒发生变化,就重新布局 - application.graphicService.hooks.beforeUpdateAABBBounds.tap( + stage.graphicService.hooks.beforeUpdateAABBBounds.tap( this.key, (graphic: IGraphic, stage: IStage, willUpdate: boolean, bounds: IAABBBounds) => { if (graphic.glyphHost) { @@ -482,7 +486,7 @@ export class FlexLayoutPlugin implements IPlugin { _tempBounds.copy(bounds); } ); - application.graphicService.hooks.afterUpdateAABBBounds.tap( + stage.graphicService.hooks.afterUpdateAABBBounds.tap( this.key, ( graphic: IGraphic, @@ -503,7 +507,7 @@ export class FlexLayoutPlugin implements IPlugin { } ); // 添加到场景树 - application.graphicService.hooks.onSetStage.tap(this.key, graphic => { + stage.graphicService.hooks.onSetStage.tap(this.key, graphic => { if (graphic.glyphHost) { graphic = graphic.glyphHost; } @@ -511,19 +515,24 @@ export class FlexLayoutPlugin implements IPlugin { }); } deactivate(context: IPluginService): void { - application.graphicService.hooks.onAttributeUpdate.taps = - application.graphicService.hooks.onAttributeUpdate.taps.filter(item => { + const stage = this.pluginService.stage; + if (!stage) { + return; + } + stage.graphicService.hooks.onAttributeUpdate.taps = stage.graphicService.hooks.onAttributeUpdate.taps.filter( + item => { return item.name !== this.key; - }); - application.graphicService.hooks.beforeUpdateAABBBounds.taps = - application.graphicService.hooks.beforeUpdateAABBBounds.taps.filter(item => { + } + ); + stage.graphicService.hooks.beforeUpdateAABBBounds.taps = + stage.graphicService.hooks.beforeUpdateAABBBounds.taps.filter(item => { return item.name !== this.key; }); - application.graphicService.hooks.afterUpdateAABBBounds.taps = - application.graphicService.hooks.afterUpdateAABBBounds.taps.filter(item => { + stage.graphicService.hooks.afterUpdateAABBBounds.taps = + stage.graphicService.hooks.afterUpdateAABBBounds.taps.filter(item => { return item.name !== this.key; }); - application.graphicService.hooks.onSetStage.taps = application.graphicService.hooks.onSetStage.taps.filter(item => { + stage.graphicService.hooks.onSetStage.taps = stage.graphicService.hooks.onSetStage.taps.filter(item => { return item.name !== this.key; }); } diff --git a/packages/vrender-core/src/plugins/builtin-plugin/html-attribute-plugin.ts b/packages/vrender-core/src/plugins/builtin-plugin/html-attribute-plugin.ts index 261c2a6b4..d0104d318 100644 --- a/packages/vrender-core/src/plugins/builtin-plugin/html-attribute-plugin.ts +++ b/packages/vrender-core/src/plugins/builtin-plugin/html-attribute-plugin.ts @@ -52,8 +52,8 @@ export class HtmlAttributePlugin implements IPlugin { context.stage.hooks.afterRender.taps = context.stage.hooks.afterRender.taps.filter(item => { return item.name !== this.key; }); - application.graphicService.hooks.onRemove.unTap(this.key); - application.graphicService.hooks.onRelease.unTap(this.key); + // application.graphicService.hooks.onRemove.unTap(this.key); + // application.graphicService.hooks.onRelease.unTap(this.key); this.release(); } diff --git a/packages/vrender-core/src/plugins/builtin-plugin/incremental-auto-render-plugin.ts b/packages/vrender-core/src/plugins/builtin-plugin/incremental-auto-render-plugin.ts index aa0c8af05..598569818 100644 --- a/packages/vrender-core/src/plugins/builtin-plugin/incremental-auto-render-plugin.ts +++ b/packages/vrender-core/src/plugins/builtin-plugin/incremental-auto-render-plugin.ts @@ -14,7 +14,11 @@ export class IncrementalAutoRenderPlugin implements IPlugin { activate(context: IPluginService): void { this.pluginService = context; - application.graphicService.hooks.onAddIncremental.tap(this.key, (graphic, group, stage) => { + const stage = this.pluginService.stage; + if (!stage) { + return; + } + stage.graphicService.hooks.onAddIncremental.tap(this.key, (graphic, group, stage) => { if (graphic.glyphHost) { graphic = graphic.glyphHost; } @@ -23,7 +27,7 @@ export class IncrementalAutoRenderPlugin implements IPlugin { this.renderNextFrame(group); } }); - application.graphicService.hooks.onClearIncremental.tap(this.key, (group, stage) => { + stage.graphicService.hooks.onClearIncremental.tap(this.key, (group, stage) => { if (group.stage === context.stage && group.stage != null) { this.nextUserParams.startAtId = group._uid; this.nextUserParams.restartIncremental = true; @@ -32,14 +36,18 @@ export class IncrementalAutoRenderPlugin implements IPlugin { }); } deactivate(context: IPluginService): void { - application.graphicService.hooks.onAddIncremental.taps = - application.graphicService.hooks.onAddIncremental.taps.filter(item => { - return item.name !== this.key; - }); - application.graphicService.hooks.onClearIncremental.taps = - application.graphicService.hooks.onClearIncremental.taps.filter(item => { + const stage = this.pluginService.stage; + if (!stage) { + return; + } + stage.graphicService.hooks.onAddIncremental.taps = stage.graphicService.hooks.onAddIncremental.taps.filter(item => { + return item.name !== this.key; + }); + stage.graphicService.hooks.onClearIncremental.taps = stage.graphicService.hooks.onClearIncremental.taps.filter( + item => { return item.name !== this.key; - }); + } + ); } renderNextFrame(group: IGroup): void { From b28ff626b37556b9b282bb618e982a31bffc1809 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 7 May 2025 11:13:19 +0800 Subject: [PATCH 117/179] feat: support diff attribute of axis to optmize performance --- packages/vrender-components/src/axis/base.ts | 17 +++++++++++++++++ packages/vrender-core/src/index.ts | 14 +++++++------- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/vrender-components/src/axis/base.ts b/packages/vrender-components/src/axis/base.ts index c440fd1f3..4e759cb7c 100644 --- a/packages/vrender-components/src/axis/base.ts +++ b/packages/vrender-components/src/axis/base.ts @@ -47,6 +47,8 @@ export abstract class AxisBase extends AnimateComp lastScale: IBaseScale; + lastAttribute: T; + // TODO: 组件整体统一起来 protected _innerView: IGroup; getInnerView() { @@ -120,9 +122,14 @@ export abstract class AxisBase extends AnimateComp * TODO:后面看情况再抽象为通用的方法 */ getBoundsWithoutRender(attributes: Partial) { + // 如果属性没有变化,则直接返回上一次的包围盒 const currentAttribute = cloneDeep(this.attribute); // scale 不能拷贝 currentAttribute.scale = (this.attribute as any).scale; + // 如果属性没有变化,则直接返回组件的包围盒 + if (isEqual(currentAttribute, this.attribute)) { + return this.AABBBounds; + } merge(this.attribute, attributes); const offscreenGroup = graphicCreator.group({ @@ -139,6 +146,14 @@ export abstract class AxisBase extends AnimateComp } protected render(): void { + // 坐标轴的scale 不用比较 + if (this.lastAttribute) { + // @ts-ignore + this.lastAttribute.scale = this.attribute.scale; + } + if (this.lastAttribute && isEqual(this.lastAttribute, this.attribute)) { + return; + } this._prepare(); this._prevInnerView = this._innerView && getElMap(this._innerView); this.removeAllChild(true); @@ -149,6 +164,8 @@ export abstract class AxisBase extends AnimateComp this._bindEvent(); // 尝试执行动画 this.runAnimation(); + + // this.lastAttribute = cloneDeep(this.attribute); } protected _prepare() { diff --git a/packages/vrender-core/src/index.ts b/packages/vrender-core/src/index.ts index f2f10b795..541d92f29 100644 --- a/packages/vrender-core/src/index.ts +++ b/packages/vrender-core/src/index.ts @@ -104,10 +104,10 @@ export * from './plugins/builtin-plugin/flex-layout-plugin'; export * from './plugins/builtin-plugin/edit-module'; -export const morphPath = {}; -export const multiToOneMorph = {}; -export const oneToMultiMorph = {}; -export class ACustomAnimate {} -export const AnimateGroup = {}; -export const Animate = {}; -export const defaultTicker = {}; +// export const morphPath = {}; +// export const multiToOneMorph = {}; +// export const oneToMultiMorph = {}; +// export class ACustomAnimate {} +// export const AnimateGroup = {}; +// export const Animate = {}; +// export const defaultTicker = {}; From 9a0bcca40d8d9b2262cfce81d46ae89794ac09fb Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 7 May 2025 11:21:43 +0800 Subject: [PATCH 118/179] feat: upgrade vutils --- common/config/rush/pnpm-lock.yaml | 50 +++++++++++------------ docs/demos/package.json | 2 +- docs/package.json | 2 +- packages/react-vrender-utils/package.json | 2 +- packages/react-vrender/package.json | 2 +- packages/vrender-animate/package.json | 2 +- packages/vrender-components/package.json | 6 +-- packages/vrender-core/package.json | 2 +- packages/vrender-kits/package.json | 2 +- packages/vrender/package.json | 2 +- 10 files changed, 36 insertions(+), 36 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index ee6b76691..2b6ca6a0d 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -25,8 +25,8 @@ importers: specifier: workspace:0.22.8 version: link:../packages/vrender '@visactor/vutils': - specifier: ~0.19.5 - version: 0.19.5 + specifier: 1.0.4 + version: 1.0.4 axios: specifier: ^1.4.0 version: 1.8.4 @@ -98,8 +98,8 @@ importers: specifier: workspace:0.22.8 version: link:../vrender '@visactor/vutils': - specifier: ~0.19.5 - version: 0.19.5 + specifier: 1.0.4 + version: 1.0.4 react-reconciler: specifier: ^0.29.0 version: 0.29.2(react@18.3.1) @@ -159,8 +159,8 @@ importers: specifier: workspace:0.22.8 version: link:../vrender '@visactor/vutils': - specifier: ~0.19.5 - version: 0.19.5 + specifier: 1.0.4 + version: 1.0.4 react-reconciler: specifier: ^0.29.0 version: 0.29.2(react@18.3.1) @@ -242,8 +242,8 @@ importers: specifier: ^18.0.0 version: 18.3.5(@types/react@18.3.20) '@visactor/vutils': - specifier: ~0.19.5 - version: 0.19.5 + specifier: 1.0.4 + version: 1.0.4 '@vitejs/plugin-react': specifier: 3.1.0 version: 3.1.0(vite@3.2.6(@types/node@22.13.17)(less@4.1.3)(terser@5.17.1)) @@ -287,8 +287,8 @@ importers: specifier: workspace:0.22.8 version: link:../vrender-core '@visactor/vutils': - specifier: ~0.19.5 - version: 0.19.5 + specifier: 1.0.4 + version: 1.0.4 devDependencies: '@internal/bundler': specifier: workspace:* @@ -351,11 +351,11 @@ importers: specifier: workspace:0.22.8 version: link:../vrender-kits '@visactor/vscale': - specifier: ~0.19.5 - version: 0.19.5 + specifier: 1.0.4 + version: 1.0.4 '@visactor/vutils': - specifier: ~0.19.5 - version: 0.19.5 + specifier: 1.0.4 + version: 1.0.4 devDependencies: '@internal/bundler': specifier: workspace:* @@ -400,8 +400,8 @@ importers: ../../packages/vrender-core: dependencies: '@visactor/vutils': - specifier: ~0.19.5 - version: 0.19.5 + specifier: 1.0.4 + version: 1.0.4 color-convert: specifier: 2.0.1 version: 2.0.1 @@ -470,8 +470,8 @@ importers: specifier: workspace:0.22.8 version: link:../vrender-core '@visactor/vutils': - specifier: ~0.19.5 - version: 0.19.5 + specifier: 1.0.4 + version: 1.0.4 gifuct-js: specifier: 2.1.2 version: 2.1.2 @@ -2192,8 +2192,8 @@ packages: '@visactor/vscale@0.15.14': resolution: {integrity: sha512-ttGdvS49APcO23WFfsbq9zyQ1y767LjqchPPL78KOrd4UjhYQXRCdeqz7K4A57e333R37oLnPfSuIVFz9qJGYw==} - '@visactor/vscale@0.19.5': - resolution: {integrity: sha512-KiXrn184Fh5aJBl/IcOK5irkJr0jwrpNjLPJ/0wfepYSycyEF5z7lDdfnvoJFEcMoljYjDQVg6Fxg9Adozc6vg==} + '@visactor/vscale@1.0.4': + resolution: {integrity: sha512-mXuX0gbQ5dmsU+dOfrDfFT45ijTZrFh1wYeIY44cdMhFo4v+tVdeihN0F+3CEI7oSZiZENbpJ7dXvxnu04xG/g==} '@visactor/vutils@0.13.3': resolution: {integrity: sha512-lCFiuUHwqz/0RCvIYa79ycduCLAILWaXddPOjxEd3VRX9CCoWMUmRtM3gF5JxtK2pK6Mu7hW7LaMSuWFw+0Kkw==} @@ -2201,8 +2201,8 @@ packages: '@visactor/vutils@0.15.14': resolution: {integrity: sha512-mZuJhXdDZqq5arqc/LfEmWOY6l7ErK1MurO8bR3vESxeCaQ18pN36iit15K2IMQVJuKZPnZ2ksw8+a1irXi/8A==} - '@visactor/vutils@0.19.5': - resolution: {integrity: sha512-sSU9Gnmnej7LgkENKkdmVqx1I3ZYVugDbGP0KEzgo8j+txAwrthEQTSeFwZcVS0iYrAvSzpmAVuN0/NRo6+vpg==} + '@visactor/vutils@1.0.4': + resolution: {integrity: sha512-GE149SM5WAc9DMNV7bGtPD4xHP68vbHMRuxGPJ3ndzAGLC/KuXpClteMw6bTY1fRX1vDLY/tQ/GVthgeOx4kDw==} '@vitejs/plugin-react@3.1.0': resolution: {integrity: sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==} @@ -8839,9 +8839,9 @@ snapshots: dependencies: '@visactor/vutils': 0.15.14 - '@visactor/vscale@0.19.5': + '@visactor/vscale@1.0.4': dependencies: - '@visactor/vutils': 0.19.5 + '@visactor/vutils': 1.0.4 '@visactor/vutils@0.13.3': dependencies: @@ -8855,7 +8855,7 @@ snapshots: '@turf/invariant': 6.5.0 eventemitter3: 4.0.7 - '@visactor/vutils@0.19.5': + '@visactor/vutils@1.0.4': dependencies: '@turf/helpers': 6.5.0 '@turf/invariant': 6.5.0 diff --git a/docs/demos/package.json b/docs/demos/package.json index 491b840fd..923ba0c7f 100644 --- a/docs/demos/package.json +++ b/docs/demos/package.json @@ -12,7 +12,7 @@ "@internal/eslint-config": "workspace:*", "@internal/ts-config": "workspace:*", "@visactor/vrender-kits": "workspace:0.14.8", - "@visactor/vutils": "~0.19.5", + "@visactor/vutils": "1.0.4", "d3-scale-chromatic": "^3.0.0", "lodash": "4.17.21", "dat.gui": "^0.7.9", diff --git a/docs/package.json b/docs/package.json index d12fccddb..c1dd777ee 100644 --- a/docs/package.json +++ b/docs/package.json @@ -11,7 +11,7 @@ "dependencies": { "@arco-design/web-react": "2.46.1", "@visactor/vchart": "1.3.0", - "@visactor/vutils": "~0.19.5", + "@visactor/vutils": "1.0.4", "@visactor/vgrammar": "~0.5.7", "@visactor/vrender": "workspace:0.22.8", "markdown-it": "^13.0.0", diff --git a/packages/react-vrender-utils/package.json b/packages/react-vrender-utils/package.json index 65fa50856..18f61bdcc 100644 --- a/packages/react-vrender-utils/package.json +++ b/packages/react-vrender-utils/package.json @@ -26,7 +26,7 @@ "dependencies": { "@visactor/vrender": "workspace:0.22.8", "@visactor/react-vrender": "workspace:0.22.8", - "@visactor/vutils": "~0.19.5", + "@visactor/vutils": "1.0.4", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" }, diff --git a/packages/react-vrender/package.json b/packages/react-vrender/package.json index 6033f1ec7..2aa20c388 100644 --- a/packages/react-vrender/package.json +++ b/packages/react-vrender/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "@visactor/vrender": "workspace:0.22.8", - "@visactor/vutils": "~0.19.5", + "@visactor/vutils": "1.0.4", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" }, diff --git a/packages/vrender-animate/package.json b/packages/vrender-animate/package.json index 2f06734b5..b8b0f2398 100644 --- a/packages/vrender-animate/package.json +++ b/packages/vrender-animate/package.json @@ -20,7 +20,7 @@ "test": "" }, "dependencies": { - "@visactor/vutils": "~0.19.5", + "@visactor/vutils": "1.0.4", "@visactor/vrender-core": "workspace:0.22.8" }, "devDependencies": { diff --git a/packages/vrender-components/package.json b/packages/vrender-components/package.json index 2bd3090e4..09ba1d865 100644 --- a/packages/vrender-components/package.json +++ b/packages/vrender-components/package.json @@ -25,8 +25,8 @@ "build:spec-types": "rm -rf ./spec-types && tsc -p ./tsconfig.spec.json --declaration --emitDeclarationOnly --outDir ./spec-types" }, "dependencies": { - "@visactor/vutils": "~0.19.5", - "@visactor/vscale": "~0.19.5", + "@visactor/vutils": "1.0.4", + "@visactor/vscale": "1.0.4", "@visactor/vrender-core": "workspace:0.22.8", "@visactor/vrender-kits": "workspace:0.22.8", "@visactor/vrender-animate": "workspace:0.22.8" @@ -36,7 +36,7 @@ "@internal/eslint-config": "workspace:*", "@internal/ts-config": "workspace:*", "@rushstack/eslint-patch": "~1.1.4", - "@visactor/vscale": "~0.19.5", + "@visactor/vscale": "1.0.4", "@types/jest": "^26.0.0", "jest": "^26.0.0", "jest-electron": "^0.1.12", diff --git a/packages/vrender-core/package.json b/packages/vrender-core/package.json index 33b3a7e1a..04fe2ddc3 100644 --- a/packages/vrender-core/package.json +++ b/packages/vrender-core/package.json @@ -30,7 +30,7 @@ }, "dependencies": { "color-convert": "2.0.1", - "@visactor/vutils": "~0.19.5" + "@visactor/vutils": "1.0.4" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-kits/package.json b/packages/vrender-kits/package.json index 42f124ffb..d0d219dc3 100644 --- a/packages/vrender-kits/package.json +++ b/packages/vrender-kits/package.json @@ -20,7 +20,7 @@ "test": "" }, "dependencies": { - "@visactor/vutils": "~0.19.5", + "@visactor/vutils": "1.0.4", "@visactor/vrender-core": "workspace:0.22.8", "@resvg/resvg-js": "2.4.1", "roughjs": "4.5.2", diff --git a/packages/vrender/package.json b/packages/vrender/package.json index 82b4cda62..290528e40 100644 --- a/packages/vrender/package.json +++ b/packages/vrender/package.json @@ -33,7 +33,7 @@ "@internal/eslint-config": "workspace:*", "@internal/ts-config": "workspace:*", "@rushstack/eslint-patch": "~1.1.4", - "@visactor/vutils": "~0.19.5", + "@visactor/vutils": "1.0.4", "canvas": "2.11.2", "react": "^18.0.0", "react-dom": "^18.0.0", From 2645daef8e4ee575f40eedf4d9549e79607cbe75 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 9 May 2025 12:32:01 +0800 Subject: [PATCH 119/179] fix: fix issue with axis bounds and render cache --- packages/vrender-components/src/axis/base.ts | 26 ++++++++++++++------ packages/vrender-core/src/graphic/graphic.ts | 6 ++--- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/vrender-components/src/axis/base.ts b/packages/vrender-components/src/axis/base.ts index 4e759cb7c..95782a71f 100644 --- a/packages/vrender-components/src/axis/base.ts +++ b/packages/vrender-components/src/axis/base.ts @@ -15,7 +15,7 @@ import type { } from '@visactor/vrender-core'; // eslint-disable-next-line no-duplicate-imports import { graphicCreator, diff } from '@visactor/vrender-core'; -import type { Dict } from '@visactor/vutils'; +import type { Dict, IBounds } from '@visactor/vutils'; // eslint-disable-next-line no-duplicate-imports import { abs, cloneDeep, get, isArray, isEmpty, isEqual, isFunction, merge, pi } from '@visactor/vutils'; import { AbstractComponent } from '../core/base'; @@ -47,7 +47,11 @@ export abstract class AxisBase extends AnimateComp lastScale: IBaseScale; - lastAttribute: T; + // 上一次执行render的attribute + protected lastAttribute: T; + // 上一次执行BoundsWithoutRender的attribute + protected lastBoundsWithoutRenderAttribute: Partial; + protected lastBoundsWithoutRenderBounds: IBounds; // TODO: 组件整体统一起来 protected _innerView: IGroup; @@ -122,15 +126,20 @@ export abstract class AxisBase extends AnimateComp * TODO:后面看情况再抽象为通用的方法 */ getBoundsWithoutRender(attributes: Partial) { + delete (attributes as any).scale; // 如果属性没有变化,则直接返回上一次的包围盒 const currentAttribute = cloneDeep(this.attribute); - // scale 不能拷贝 + // scale 不能拷贝,它是一个实例,重新设置上去 currentAttribute.scale = (this.attribute as any).scale; + merge(this.attribute, attributes); + // 如果属性没有变化,则直接返回组件的包围盒 - if (isEqual(currentAttribute, this.attribute)) { - return this.AABBBounds; + if (this.lastBoundsWithoutRenderAttribute && isEqual(this.lastBoundsWithoutRenderAttribute, this.attribute)) { + this.attribute = currentAttribute; + return this.lastBoundsWithoutRenderBounds; } - merge(this.attribute, attributes); + // 更新属性 + this.lastBoundsWithoutRenderAttribute = this.attribute; const offscreenGroup = graphicCreator.group({ x: this.attribute.x, @@ -142,7 +151,8 @@ export abstract class AxisBase extends AnimateComp this.removeChild(offscreenGroup); this.attribute = currentAttribute; - return offscreenGroup.AABBBounds; + this.lastBoundsWithoutRenderBounds = offscreenGroup.AABBBounds; + return this.lastBoundsWithoutRenderBounds; } protected render(): void { @@ -165,7 +175,7 @@ export abstract class AxisBase extends AnimateComp // 尝试执行动画 this.runAnimation(); - // this.lastAttribute = cloneDeep(this.attribute); + this.lastAttribute = cloneDeep(this.attribute); } protected _prepare() { diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index 96919bafb..32797c96d 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -165,7 +165,7 @@ export const NOWORK_ANIMATE_ATTR = { // return Reflect.get(target, property); // }, // set(target: any, property: any, value: any) { -// if (property === 'opacity' && obj.text === 'Nail polish') { +// if (property === 'size' && !isFinite(value)) { // console.log('set', property, value); // } // // modifiedProperties.add(property); // 记录设置/修改操作 @@ -176,8 +176,8 @@ export const NOWORK_ANIMATE_ATTR = { // const proxy = new Proxy(obj, handler); // // 提供方法获取被追踪的属性 -// proxy.getAccessedProperties = () => [...accessedProperties]; -// proxy.getModifiedProperties = () => [...modifiedProperties]; +// // proxy.getAccessedProperties = () => [...accessedProperties]; +// // proxy.getModifiedProperties = () => [...modifiedProperties]; // return proxy; // } From a925750aca9365cc2113dff460a51a2c260b79a6 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 9 May 2025 15:26:51 +0800 Subject: [PATCH 120/179] fix: change order of context.reset and context.save --- .../src/render/contributions/render/draw-contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vrender-core/src/render/contributions/render/draw-contribution.ts b/packages/vrender-core/src/render/contributions/render/draw-contribution.ts index 871436b9d..e039bc6bd 100644 --- a/packages/vrender-core/src/render/contributions/render/draw-contribution.ts +++ b/packages/vrender-core/src/render/contributions/render/draw-contribution.ts @@ -130,8 +130,8 @@ export class DefaultDrawContribution implements IDrawContribution { } this.backupDirtyBounds.copy(dirtyBounds); // TODO:不需要设置context.transform,后续translate会设置 - context.save(); context.reset(false); + context.save(); context.setClearMatrix(transMatrix.a, transMatrix.b, transMatrix.c, transMatrix.d, transMatrix.e, transMatrix.f); // 初始化context context.clearMatrix(false); From 6406980640518f6439efa2b497be90186f9c774d Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 9 May 2025 16:45:03 +0800 Subject: [PATCH 121/179] fix: fix issue with TagPointsUpdate for line segment --- packages/vrender-animate/src/custom/tag-points.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vrender-animate/src/custom/tag-points.ts b/packages/vrender-animate/src/custom/tag-points.ts index 654271d8d..418d9fc2f 100644 --- a/packages/vrender-animate/src/custom/tag-points.ts +++ b/packages/vrender-animate/src/custom/tag-points.ts @@ -179,7 +179,7 @@ export class TagPointsUpdate extends ACustomAnimate<{ points?: IPointLike[]; seg points }; }); - (this.target.attribute as ILineAttribute).points = segments; + (this.target.attribute as ILineAttribute).segments = segments; } else { (this.target.attribute as ILineAttribute).points = this.points; } From a0125d54cbfeeb82b9e05ec2a4f376fd52387c77 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 9 May 2025 17:29:34 +0800 Subject: [PATCH 122/179] fix: fix issue with appear animate set attribute when has image attr --- packages/vrender-animate/src/custom/common.ts | 2 +- packages/vrender-animate/src/custom/fromTo.ts | 12 ++++++------ packages/vrender-animate/src/custom/growAngle.ts | 2 +- packages/vrender-animate/src/custom/growCenter.ts | 2 +- packages/vrender-animate/src/custom/growHeight.ts | 2 +- packages/vrender-animate/src/custom/growPoints.ts | 6 +++--- packages/vrender-animate/src/custom/growRadius.ts | 2 +- packages/vrender-animate/src/custom/growWidth.ts | 2 +- packages/vrender-animate/src/custom/move.ts | 2 +- packages/vrender-animate/src/custom/rotate.ts | 2 +- packages/vrender-animate/src/custom/scale.ts | 2 +- 11 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/vrender-animate/src/custom/common.ts b/packages/vrender-animate/src/custom/common.ts index 1228a7778..6c510b152 100644 --- a/packages/vrender-animate/src/custom/common.ts +++ b/packages/vrender-animate/src/custom/common.ts @@ -30,7 +30,7 @@ export class CommonIn extends ACustomAnimate> { // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) const finalAttribute = this.target.getFinalAttribute(); if (finalAttribute) { - Object.assign(this.target.attribute, finalAttribute); + this.target.setAttributes(finalAttribute); } this.props = to; diff --git a/packages/vrender-animate/src/custom/fromTo.ts b/packages/vrender-animate/src/custom/fromTo.ts index c9c3ba64e..4f325e432 100644 --- a/packages/vrender-animate/src/custom/fromTo.ts +++ b/packages/vrender-animate/src/custom/fromTo.ts @@ -25,7 +25,7 @@ export class FromTo extends ACustomAnimate> { // 如果入场动画,那么需要设置属性 if (this.target.context?.animationState === 'appear') { if (finalAttribute) { - Object.assign(this.target.attribute, finalAttribute); + this.target.setAttributes(finalAttribute); } } if (this.params.controlOptions?.immediatelyApply !== false) { @@ -43,11 +43,11 @@ export class FromTo extends ACustomAnimate> { }); // TODO:比较hack // 如果是入场动画,那么还需要设置属性 - if (this.target.context?.animationState === 'appear') { - // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) - const finalAttribute = this.target.getFinalAttribute(); - this.target.setAttributes(finalAttribute); - } + // if (this.target.context?.animationState === 'appear') { + // // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + // const finalAttribute = this.target.getFinalAttribute(); + // this.target.setAttributes(finalAttribute); + // } this.target.setAttributes(this.from); } diff --git a/packages/vrender-animate/src/custom/growAngle.ts b/packages/vrender-animate/src/custom/growAngle.ts index 4ca428b87..f2952f9a0 100644 --- a/packages/vrender-animate/src/custom/growAngle.ts +++ b/packages/vrender-animate/src/custom/growAngle.ts @@ -228,7 +228,7 @@ export class GrowAngleIn extends GrowAngleBase { // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) const finalAttribute = this.target.getFinalAttribute(); if (finalAttribute) { - Object.assign(this.target.attribute, finalAttribute); + this.target.setAttributes(finalAttribute); } this.target.setAttributes(fromAttrs); diff --git a/packages/vrender-animate/src/custom/growCenter.ts b/packages/vrender-animate/src/custom/growCenter.ts index f1ac39a2e..3ecb6cc30 100644 --- a/packages/vrender-animate/src/custom/growCenter.ts +++ b/packages/vrender-animate/src/custom/growCenter.ts @@ -212,7 +212,7 @@ export class GrowCenterIn extends ACustomAnimate> { // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) const finalAttribute = this.target.getFinalAttribute(); if (finalAttribute) { - Object.assign(this.target.attribute, finalAttribute); + this.target.setAttributes(finalAttribute); } this.target.setAttributes(fromAttrs); diff --git a/packages/vrender-animate/src/custom/growHeight.ts b/packages/vrender-animate/src/custom/growHeight.ts index b1131eba3..c2d9a6fe9 100644 --- a/packages/vrender-animate/src/custom/growHeight.ts +++ b/packages/vrender-animate/src/custom/growHeight.ts @@ -114,7 +114,7 @@ export class GrowHeightIn extends ACustomAnimate> { // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) const finalAttribute = this.target.getFinalAttribute(); if (finalAttribute) { - Object.assign(this.target.attribute, finalAttribute); + this.target.setAttributes(finalAttribute); } this.target.setAttributes(fromAttrs); diff --git a/packages/vrender-animate/src/custom/growPoints.ts b/packages/vrender-animate/src/custom/growPoints.ts index 22085a1e5..2dfa37554 100644 --- a/packages/vrender-animate/src/custom/growPoints.ts +++ b/packages/vrender-animate/src/custom/growPoints.ts @@ -121,7 +121,7 @@ export class GrowPointsIn extends GworPointsBase { // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) const finalAttribute = this.target.getFinalAttribute(); if (finalAttribute) { - Object.assign(this.target.attribute, finalAttribute); + this.target.setAttributes(finalAttribute); } if (this.params.controlOptions?.immediatelyApply !== false) { this.target.setAttributes(from); @@ -220,7 +220,7 @@ export class GrowPointsXIn extends GworPointsBase { // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) const finalAttribute = this.target.getFinalAttribute(); if (finalAttribute) { - Object.assign(this.target.attribute, finalAttribute); + this.target.setAttributes(finalAttribute); } if (this.params.controlOptions?.immediatelyApply !== false) { this.target.setAttributes(from); @@ -318,7 +318,7 @@ export class GrowPointsYIn extends GworPointsBase { // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) const finalAttribute = this.target.getFinalAttribute(); if (finalAttribute) { - Object.assign(this.target.attribute, finalAttribute); + this.target.setAttributes(finalAttribute); } if (this.params.controlOptions?.immediatelyApply !== false) { diff --git a/packages/vrender-animate/src/custom/growRadius.ts b/packages/vrender-animate/src/custom/growRadius.ts index 52a51a690..6755fedd0 100644 --- a/packages/vrender-animate/src/custom/growRadius.ts +++ b/packages/vrender-animate/src/custom/growRadius.ts @@ -155,7 +155,7 @@ export class GrowRadiusIn extends GrowPointsBase { // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) const finalAttribute = this.target.getFinalAttribute(); if (finalAttribute) { - Object.assign(this.target.attribute, finalAttribute); + this.target.setAttributes(finalAttribute); } this.target.setAttributes(fromAttrs); } diff --git a/packages/vrender-animate/src/custom/growWidth.ts b/packages/vrender-animate/src/custom/growWidth.ts index e05b59ddd..4c7061684 100644 --- a/packages/vrender-animate/src/custom/growWidth.ts +++ b/packages/vrender-animate/src/custom/growWidth.ts @@ -172,7 +172,7 @@ export class GrowWidthIn extends ACustomAnimate> { // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) const finalAttribute = this.target.getFinalAttribute(); if (finalAttribute) { - Object.assign(this.target.attribute, finalAttribute); + this.target.setAttributes(finalAttribute); } this.target.setAttributes(fromAttrs); } diff --git a/packages/vrender-animate/src/custom/move.ts b/packages/vrender-animate/src/custom/move.ts index 225076dbf..c3e640ada 100644 --- a/packages/vrender-animate/src/custom/move.ts +++ b/packages/vrender-animate/src/custom/move.ts @@ -147,7 +147,7 @@ export class MoveIn extends MoveBase { // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) const finalAttribute = this.target.getFinalAttribute(); if (finalAttribute) { - Object.assign(this.target.attribute, finalAttribute); + this.target.setAttributes(finalAttribute); } if (this.params.controlOptions?.immediatelyApply !== false) { diff --git a/packages/vrender-animate/src/custom/rotate.ts b/packages/vrender-animate/src/custom/rotate.ts index 8b79c0e81..e11fc723c 100644 --- a/packages/vrender-animate/src/custom/rotate.ts +++ b/packages/vrender-animate/src/custom/rotate.ts @@ -80,7 +80,7 @@ export class RotateIn extends RotateBase { // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) const finalAttribute = this.target.getFinalAttribute(); if (finalAttribute) { - Object.assign(this.target.attribute, finalAttribute); + this.target.setAttributes(finalAttribute); } if (this.params.controlOptions?.immediatelyApply !== false) { diff --git a/packages/vrender-animate/src/custom/scale.ts b/packages/vrender-animate/src/custom/scale.ts index 91ae9a4c6..923f0739c 100644 --- a/packages/vrender-animate/src/custom/scale.ts +++ b/packages/vrender-animate/src/custom/scale.ts @@ -45,7 +45,7 @@ export class ScaleIn extends ACustomAnimate> { // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) const finalAttribute = this.target.getFinalAttribute(); if (finalAttribute) { - Object.assign(this.target.attribute, finalAttribute); + this.target.setAttributes(finalAttribute); } this.props = to; From 1840c681a98f9ca087e3817991a1f3f27c1bf027 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 9 May 2025 17:37:31 +0800 Subject: [PATCH 123/179] fix: revert axis performance commit --- packages/vrender-components/src/axis/base.ts | 29 +------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/packages/vrender-components/src/axis/base.ts b/packages/vrender-components/src/axis/base.ts index 95782a71f..baaecfd0e 100644 --- a/packages/vrender-components/src/axis/base.ts +++ b/packages/vrender-components/src/axis/base.ts @@ -47,12 +47,6 @@ export abstract class AxisBase extends AnimateComp lastScale: IBaseScale; - // 上一次执行render的attribute - protected lastAttribute: T; - // 上一次执行BoundsWithoutRender的attribute - protected lastBoundsWithoutRenderAttribute: Partial; - protected lastBoundsWithoutRenderBounds: IBounds; - // TODO: 组件整体统一起来 protected _innerView: IGroup; getInnerView() { @@ -126,21 +120,11 @@ export abstract class AxisBase extends AnimateComp * TODO:后面看情况再抽象为通用的方法 */ getBoundsWithoutRender(attributes: Partial) { - delete (attributes as any).scale; - // 如果属性没有变化,则直接返回上一次的包围盒 const currentAttribute = cloneDeep(this.attribute); // scale 不能拷贝,它是一个实例,重新设置上去 currentAttribute.scale = (this.attribute as any).scale; merge(this.attribute, attributes); - // 如果属性没有变化,则直接返回组件的包围盒 - if (this.lastBoundsWithoutRenderAttribute && isEqual(this.lastBoundsWithoutRenderAttribute, this.attribute)) { - this.attribute = currentAttribute; - return this.lastBoundsWithoutRenderBounds; - } - // 更新属性 - this.lastBoundsWithoutRenderAttribute = this.attribute; - const offscreenGroup = graphicCreator.group({ x: this.attribute.x, y: this.attribute.y @@ -151,19 +135,10 @@ export abstract class AxisBase extends AnimateComp this.removeChild(offscreenGroup); this.attribute = currentAttribute; - this.lastBoundsWithoutRenderBounds = offscreenGroup.AABBBounds; - return this.lastBoundsWithoutRenderBounds; + return offscreenGroup.AABBBounds; } protected render(): void { - // 坐标轴的scale 不用比较 - if (this.lastAttribute) { - // @ts-ignore - this.lastAttribute.scale = this.attribute.scale; - } - if (this.lastAttribute && isEqual(this.lastAttribute, this.attribute)) { - return; - } this._prepare(); this._prevInnerView = this._innerView && getElMap(this._innerView); this.removeAllChild(true); @@ -174,8 +149,6 @@ export abstract class AxisBase extends AnimateComp this._bindEvent(); // 尝试执行动画 this.runAnimation(); - - this.lastAttribute = cloneDeep(this.attribute); } protected _prepare() { From fb0a899d178fb14409c222ca697078f6c0b54754 Mon Sep 17 00:00:00 2001 From: xile611 Date: Fri, 9 May 2025 18:49:32 +0800 Subject: [PATCH 124/179] fix: update `getBoundsWithoutRender` of axis --- packages/vrender-components/src/axis/base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vrender-components/src/axis/base.ts b/packages/vrender-components/src/axis/base.ts index baaecfd0e..9335b4202 100644 --- a/packages/vrender-components/src/axis/base.ts +++ b/packages/vrender-components/src/axis/base.ts @@ -123,7 +123,7 @@ export abstract class AxisBase extends AnimateComp const currentAttribute = cloneDeep(this.attribute); // scale 不能拷贝,它是一个实例,重新设置上去 currentAttribute.scale = (this.attribute as any).scale; - merge(this.attribute, attributes); + this.attribute = attributes; const offscreenGroup = graphicCreator.group({ x: this.attribute.x, From f10b0f5bf2718249b05bbc89d052f2e578eedaad Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 13 May 2025 16:52:48 +0800 Subject: [PATCH 125/179] feat: support afterStateUpdate event, and lable listen afterStateUpdate --- packages/vrender-components/src/label/base.ts | 10 ++++++++-- packages/vrender-core/src/graphic/graphic.ts | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index 321ae0b32..004ad7ac1 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -873,6 +873,12 @@ export class LabelBase extends AnimateComponent { ); } + protected _syncStateWithRelatedGraphic(relatedGraphic: IGraphic) { + if (this.attribute.syncState && relatedGraphic) { + relatedGraphic.on('afterStateUpdate', this._handleRelatedGraphicSetState); + } + } + protected _addLabel( label: LabelContent, texts?: LabelContent['text'][], @@ -881,8 +887,8 @@ export class LabelBase extends AnimateComponent { ) { const { text, labelLine } = label; // TODO: 或许还需要判断关联图元是否有动画? - // const relatedGraphic = this.getRelatedGraphic(text.attribute); - // this._syncStateWithRelatedGraphic(relatedGraphic); + const relatedGraphic = this.getRelatedGraphic(text.attribute); + this._syncStateWithRelatedGraphic(relatedGraphic); if (text) { this.add(text); diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index 32797c96d..836c765a1 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -1037,6 +1037,8 @@ export abstract class Graphic = Partial) { From b1e6f5df328debdc0951ba0fc1d11181677600d8 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 14 May 2025 14:41:36 +0800 Subject: [PATCH 126/179] fix: fix issue with _debug_bounds and 3d sort --- .../src/render/contributions/render/draw-interceptor.ts | 2 ++ .../src/render/contributions/render/group-render.ts | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/vrender-core/src/render/contributions/render/draw-interceptor.ts b/packages/vrender-core/src/render/contributions/render/draw-interceptor.ts index a3bd7ad16..7dbe9ce4b 100644 --- a/packages/vrender-core/src/render/contributions/render/draw-interceptor.ts +++ b/packages/vrender-core/src/render/contributions/render/draw-interceptor.ts @@ -191,6 +191,7 @@ export class CommonDrawItemInterceptorContribution implements IDrawItemIntercept if ( (!graphic.in3dMode || drawContext.in3dInterceptor) && !graphic.shadowRoot && + !graphic.attribute._debug_bounds && !(graphic.baseGraphic || graphic.attribute.globalZIndex || graphic.interactiveGraphic) ) { return false; @@ -218,6 +219,7 @@ export class CommonDrawItemInterceptorContribution implements IDrawItemIntercept if ( (!graphic.in3dMode || drawContext.in3dInterceptor) && !graphic.shadowRoot && + !graphic.attribute._debug_bounds && !(graphic.baseGraphic || graphic.attribute.globalZIndex || graphic.interactiveGraphic) ) { return false; diff --git a/packages/vrender-core/src/render/contributions/render/group-render.ts b/packages/vrender-core/src/render/contributions/render/group-render.ts index b914a6eb4..fa6d3a3f1 100644 --- a/packages/vrender-core/src/render/contributions/render/group-render.ts +++ b/packages/vrender-core/src/render/contributions/render/group-render.ts @@ -338,7 +338,12 @@ export class DefaultCanvasGroupRender implements IGraphicRender { } let p: any; if (params && params.renderInGroup) { - p = params.renderInGroup(params.skipDraw, group, drawContext, params.renderInGroupParams?.nextM); + p = params.renderInGroup( + params.renderInGroupParams?.skipSort, + group, + drawContext, + params.renderInGroupParams?.nextM + ); } if (context.modelMatrix !== lastModelMatrix) { From ad6b8370e16357e9b6e39d901a580dd568cb9090 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 14 May 2025 14:43:29 +0800 Subject: [PATCH 127/179] fix: fix issue with shadow --- .../vrender-kits/src/canvas/contributions/browser/context.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/vrender-kits/src/canvas/contributions/browser/context.ts b/packages/vrender-kits/src/canvas/contributions/browser/context.ts index 9a77d233a..acfc8be43 100644 --- a/packages/vrender-kits/src/canvas/contributions/browser/context.ts +++ b/packages/vrender-kits/src/canvas/contributions/browser/context.ts @@ -1062,13 +1062,15 @@ export class BrowserContext2d implements IContext2d { const { opacity = defaultParams.opacity, shadowBlur = defaultParams.shadowBlur, + shadowOffsetX = defaultParams.shadowOffsetX, + shadowOffsetY = defaultParams.shadowOffsetY, blur = defaultParams.blur, globalCompositeOperation = defaultParams.globalCompositeOperation } = attribute; if (opacity <= 1e-12) { return; } - if (shadowBlur) { + if (shadowOffsetX || shadowOffsetY || shadowBlur) { const { shadowColor = defaultParams.shadowColor, shadowOffsetX = defaultParams.shadowOffsetX, From 1c5783347ae2d8d78d11e43b6f22ed8484b393b2 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 14 May 2025 14:56:57 +0800 Subject: [PATCH 128/179] fix: fix issue with arc stroke --- .../src/render/contributions/render/arc-render.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/vrender-core/src/render/contributions/render/arc-render.ts b/packages/vrender-core/src/render/contributions/render/arc-render.ts index 6e4ccff6c..8954e9bf5 100644 --- a/packages/vrender-core/src/render/contributions/render/arc-render.ts +++ b/packages/vrender-core/src/render/contributions/render/arc-render.ts @@ -309,9 +309,9 @@ export class DefaultCanvasArcRender extends BaseRender implements IGraphic if (!fillStrokeOrder) { this._runFill(arc, context, x, y, arcAttribute, doFill, fVisible, originX, originY, fillCb); - this._runStroke(arc, context, x, y, arcAttribute, doStroke, sVisible, strokeCb); + this._runStroke(arc, context, x, y, arcAttribute, doStroke, isFullStroke, sVisible, strokeCb); } else { - this._runStroke(arc, context, x, y, arcAttribute, doStroke, sVisible, strokeCb); + this._runStroke(arc, context, x, y, arcAttribute, doStroke, isFullStroke, sVisible, strokeCb); this._runFill(arc, context, x, y, arcAttribute, doFill, fVisible, originX, originY, fillCb); } } @@ -470,6 +470,7 @@ export class DefaultCanvasArcRender extends BaseRender implements IGraphic y: number, arcAttribute: Required, doStroke: boolean, + isFullStroke: boolean, sVisible: boolean, strokeCb?: ( ctx: IContext2d, @@ -477,7 +478,7 @@ export class DefaultCanvasArcRender extends BaseRender implements IGraphic themeAttribute: IThemeAttribute ) => boolean ) { - if (doStroke) { + if (doStroke && isFullStroke) { if (strokeCb) { // fillCb(context, arc.attribute, arcAttribute); } else if (sVisible) { From 7f403797de848d279cc160ab39693ee5662a8523 Mon Sep 17 00:00:00 2001 From: xile611 Date: Wed, 14 May 2025 17:22:03 +0800 Subject: [PATCH 129/179] fix: add animaitonend event to timeline --- packages/vrender-animate/src/timeline.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/vrender-animate/src/timeline.ts b/packages/vrender-animate/src/timeline.ts index 70ec0df27..c563dde40 100644 --- a/packages/vrender-animate/src/timeline.ts +++ b/packages/vrender-animate/src/timeline.ts @@ -1,4 +1,5 @@ import { Generator, type IAnimate, type ITimeline, AnimateStatus } from '@visactor/vrender-core'; +import { EventEmitter } from '@visactor/vutils'; // 定义链表节点 interface AnimateNode { @@ -7,7 +8,7 @@ interface AnimateNode { prev: AnimateNode | null; } -export class DefaultTimeline implements ITimeline { +export class DefaultTimeline extends EventEmitter implements ITimeline { declare id: number; protected head: AnimateNode | null = null; protected tail: AnimateNode | null = null; @@ -28,6 +29,7 @@ export class DefaultTimeline implements ITimeline { } constructor() { + super(); this.id = Generator.GenAutoIncrementId(); this.paused = false; } @@ -102,6 +104,10 @@ export class DefaultTimeline implements ITimeline { animate.advance(scaledDelta); } }); + + if (this._animateCount === 0) { + this.emit('animationEnd'); + } } clear() { From 9df5a283c1ecdde74bc5d225d7b4eaa234bd5207 Mon Sep 17 00:00:00 2001 From: xile611 Date: Wed, 14 May 2025 18:52:59 +0800 Subject: [PATCH 130/179] fix: update interface of IGraphic --- packages/vrender-core/src/interface/graphic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vrender-core/src/interface/graphic.ts b/packages/vrender-core/src/interface/graphic.ts index 0fc4bbba3..fc33ce802 100644 --- a/packages/vrender-core/src/interface/graphic.ts +++ b/packages/vrender-core/src/interface/graphic.ts @@ -732,7 +732,7 @@ export interface IGraphic = Partial Partial; findFace?: () => IFace3d; toggleState: (stateName: string, hasAnimation?: boolean) => void; - removeState: (stateName: string, hasAnimation?: boolean) => void; + removeState: (stateName: string | string[], hasAnimation?: boolean) => void; clearStates: (hasAnimation?: boolean) => void; useStates: (states: string[], hasAnimation?: boolean) => void; addState: (stateName: string, keepCurrentStates?: boolean, hasAnimation?: boolean) => void; From bea604b3c2925d7258d3fedaf0dee0749733fd83 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 15 May 2025 16:52:08 +0800 Subject: [PATCH 131/179] fix: fix issue with global graphic service hooks is empty --- .../graphic-service/graphic-service.ts | 3 +-- packages/vrender-core/src/graphic/graphic.ts | 21 +++++++++---------- packages/vrender-core/src/graphic/group.ts | 14 ++++++------- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts b/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts index a184ff5bf..38a4ad720 100644 --- a/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts +++ b/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts @@ -23,7 +23,6 @@ import { BoundsContext } from '../../common/bounds-context'; import { renderCommandList } from '../../common/render-command-list'; import { GraphicCreator } from '../constants'; import { identityMat4, multiplyMat4Mat4, rotateX, rotateY, rotateZ, scaleMat4, translate } from '../../common/matrix'; -import { application } from '../../application'; export function getExtraModelMatrix(dx: number, dy: number, graphic: IGraphic): mat4 | null { const { alpha, beta } = graphic.attribute; @@ -381,7 +380,7 @@ export class DefaultGraphicService implements IGraphicService { // application.graphicService.beforeUpdateAABBBounds(graphic, graphic.stage, true, aabbBounds); if (!aabbBounds.empty()) { graphic.parent && aabbBounds.transformWithMatrix((graphic.parent as IGroup).globalTransMatrix); - application.graphicService.clearAABBBounds(graphic, graphic.stage, aabbBounds); + this.clearAABBBounds(graphic, graphic.stage, aabbBounds); aabbBounds.clear(); } return false; diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index 836c765a1..31403cca3 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -332,6 +332,10 @@ export abstract class Graphic = Partial = Partial = Partial, this._AABBBounds, @@ -607,12 +611,7 @@ export abstract class Graphic = Partial, - this._AABBBounds, - this - ) + this.getGraphicService().validCheck(this.attribute, this.getGraphicTheme() as Required, this._AABBBounds, this) ); } @@ -956,7 +955,7 @@ export abstract class Graphic = Partial = Partial implements IGroup { if (!this.shouldUpdateAABBBounds()) { return this._AABBBounds; } - this.stage?.graphicService.beforeUpdateAABBBounds(this, this.stage, true, this._AABBBounds); + this.getGraphicService().beforeUpdateAABBBounds(this, this.stage, true, this._AABBBounds); const selfChange = this.shouldSelfChangeUpdateAABBBounds(); const bounds = this.doUpdateAABBBounds(); this.addUpdateLayoutTag(); - this.stage?.graphicService.afterUpdateAABBBounds(this, this.stage, this._AABBBounds, this, selfChange); + this.getGraphicService().afterUpdateAABBBounds(this, this.stage, this._AABBBounds, this, selfChange); // 直接返回空Bounds,但是前面的流程还是要走 if (this.attribute.boundsMode === 'empty') { bounds.clear(); @@ -259,13 +259,13 @@ export class Group extends Graphic implements IGroup { (data as unknown as this).layer = this.layer; } this.addUpdateBoundTag(); - this.stage?.graphicService.onAddIncremental(node as unknown as IGraphic, this, this.stage); + this.getGraphicService().onAddIncremental(node as unknown as IGraphic, this, this.stage); return data; } incrementalClearChild(): void { super.removeAllChild(); this.addUpdateBoundTag(); - this.stage?.graphicService.onClearIncremental(this, this.stage); + this.getGraphicService().onClearIncremental(this, this.stage); return; } @@ -298,14 +298,14 @@ export class Group extends Graphic implements IGroup { removeChild(child: IGraphic): IGraphic { const data = super.removeChild(child); child.stage = null; - this.stage?.graphicService.onRemove(child); + this.getGraphicService().onRemove(child); this.addUpdateBoundTag(); return data as IGraphic; } removeAllChild(deep: boolean = false): void { this.forEachChildren((child: IGraphic) => { - this.stage?.graphicService.onRemove(child); + this.getGraphicService().onRemove(child); if (deep && child.isContainer) { child.removeAllChild(deep); } @@ -320,7 +320,7 @@ export class Group extends Graphic implements IGroup { this.layer = layer; this.setStageToShadowRoot(stage, layer); this._onSetStage && this._onSetStage(this, stage, layer); - this.stage?.graphicService.onSetStage(this, stage); + this.getGraphicService().onSetStage(this, stage); this.forEachChildren(item => { (item as any).setStage(stage, this.layer); }); From 6fc553970441836873c1edc9a02a72d645f38143 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Fri, 16 May 2025 16:25:08 +0800 Subject: [PATCH 132/179] feat: remove marker type. close @VisActor/VChart#3782 --- ...t-remove-marker-type_2025-05-16-08-24.json | 10 ++ .../__tests__/browser/examples/mark-point.ts | 132 +++++------------- .../vrender-components/src/marker/point.ts | 48 +++---- .../vrender-components/src/marker/type.ts | 51 ++----- 4 files changed, 76 insertions(+), 165 deletions(-) create mode 100644 common/changes/@visactor/vrender-components/feat-remove-marker-type_2025-05-16-08-24.json diff --git a/common/changes/@visactor/vrender-components/feat-remove-marker-type_2025-05-16-08-24.json b/common/changes/@visactor/vrender-components/feat-remove-marker-type_2025-05-16-08-24.json new file mode 100644 index 000000000..c0dea2719 --- /dev/null +++ b/common/changes/@visactor/vrender-components/feat-remove-marker-type_2025-05-16-08-24.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-components", + "comment": "feat: remove marker type. close @VisActor/VChart#3782", + "type": "none" + } + ], + "packageName": "@visactor/vrender-components" +} \ No newline at end of file diff --git a/packages/vrender-components/__tests__/browser/examples/mark-point.ts b/packages/vrender-components/__tests__/browser/examples/mark-point.ts index 43bcdb68a..73e344773 100644 --- a/packages/vrender-components/__tests__/browser/examples/mark-point.ts +++ b/packages/vrender-components/__tests__/browser/examples/mark-point.ts @@ -62,24 +62,13 @@ export function run() { fill: 'red' } }, - symbol: { + itemContent: { hover: { - stroke: 'red', - fill: 'red' - } - }, - image: { - hover: { - stroke: 'red', - fill: 'red', + stroke: 'blue', + fill: 'blue', width: 200, - height: 200 - } - }, - text: { - hover: { - stroke: 'red', - fill: 'red' + height: 200, + size: 200 } }, textBackground: { @@ -87,18 +76,6 @@ export function run() { stroke: 'red', fill: 'red' } - }, - richText: { - hover: { - stroke: 'red', - fill: 'red' - } - }, - customMark: { - hover: { - stroke: 'red', - fill: 'red' - } } }, @@ -150,85 +127,42 @@ export function run() { confine: true, autoRotate: guiObject.itemAutoRotate, - textStyle: { - // text: 'mark point label text' - type: 'text', + type: 'text', + style: { + type: 'rich', textStyle: { - text: 'Type your annotation text here', + textConfig: [{ text: 'Type your annotation text here' }], // fontWeight: 'bold', fontSize: 12, fill: '#3f51b5', height: 25 // textAlign: 'center' - } - // text: [ - // { - - // }, - // { - // text: '替代方案', - // fontStyle: 'italic', - // textDecoration: 'underline', - // fill: '#3f51b5', - // height: 25 - // } - // ] - }, - richTextStyle: { - textConfig: [ - { - text: 'Mapbox', - fontWeight: 'bold', - fontSize: 10, - fill: '#3f51b5' - }, - { - text: '公司成立于2010年,创立目标是为Google Map提供一个', - - fontSize: 8 - }, + }, + text: [ + {}, { text: '替代方案', fontStyle: 'italic', - + textDecoration: 'underline', fill: '#3f51b5', - fontSize: 8 - }, - { - text: '。在当时,Google Map', - - fontSize: 8 - }, - { - text: '地图', - textDecoration: 'line-through', - - fontSize: 8 - }, - { - text: '[1]', - script: 'super', - - fontSize: 8 - }, - { - text: '几乎垄断了所有线上地图业务,但是在Google Map中,几乎没有定制化的可能,也没有任何工具可以让制图者按照他们的设想来创建地图', - - fontSize: 8 - }, - { - text: '。\n', - - fill: '#30ff05', - fontSize: 8 + height: 25 } ] - }, - imageStyle: { - image: `https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/shape_logo.png` - // width: 400, - // height: 400 } + + // type: 'image', + // style: { + // image: `https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/shape_logo.png`, + // width: 400, + // height: 400 + // }, + + // type: 'symbol', + // style: { + // symbolType: `circle`, + // size: 30, + // fill: 'red' + // } }, targetSymbol: { visible: true, @@ -265,13 +199,12 @@ export function run() { const markPoint2 = new MarkPoint({ position: { - x: 250, + x: 0, y: 250 }, ...(styleAttr as any), itemContent: { - ...styleAttr.itemContent, - type: 'text' + ...styleAttr.itemContent } }); @@ -283,7 +216,7 @@ export function run() { ...(styleAttr as any), itemContent: { ...styleAttr.itemContent, - type: 'richText' + type: 'text' } }); @@ -294,8 +227,7 @@ export function run() { }, ...(styleAttr as any), itemContent: { - ...styleAttr.itemContent, - type: 'image' + ...styleAttr.itemContent } }); diff --git a/packages/vrender-components/src/marker/point.ts b/packages/vrender-components/src/marker/point.ts index 1f77dea05..fb7c7cd6e 100644 --- a/packages/vrender-components/src/marker/point.ts +++ b/packages/vrender-components/src/marker/point.ts @@ -109,11 +109,9 @@ export class MarkPoint extends Marker { refX = 0, refY = 0, refAngle = 0, - textStyle = {}, - richTextStyle = {}, - imageStyle = {}, + style, position: positionType = IMarkPointItemPosition.middle - } = itemContent; + } = itemContent as any; const { state } = this.attribute as MarkPointAttrs; const lineEndAngle = this._line?.getEndAngle() || 0; const itemRefOffsetX = refX * Math.cos(lineEndAngle) + refY * Math.cos(lineEndAngle - Math.PI / 2); @@ -122,8 +120,8 @@ export class MarkPoint extends Marker { const offsetX = newItemPosition.x - newPosition.x; const offsetY = newItemPosition.y - newPosition.y; item.setAttributes({ - ...(textStyle as TagAttributes), - textStyle: { + ...(style as TagAttributes), + style: { ...this.getTextAlignAttr( autoRotate, offsetX, @@ -131,25 +129,25 @@ export class MarkPoint extends Marker { lineEndAngle, itemContent.position ?? ('end' as keyof typeof IMarkPointItemPosition) ), - ...textStyle.textStyle + ...style.textStyle }, state: { panel: merge({}, DEFAULT_STATES, state?.textBackground), - text: merge({}, DEFAULT_STATES, state?.text) + text: merge({}, DEFAULT_STATES, state?.itemContent) } } as any); } else if (itemType === 'richText') { item.setAttributes({ - dx: this.getItemDx(item, positionType, richTextStyle) + (richTextStyle.dx || 0), - dy: this.getItemDy(item, positionType, richTextStyle) + (richTextStyle.dy || 0) + dx: this.getItemDx(item, positionType, style) + (style.dx || 0), + dy: this.getItemDy(item, positionType, style) + (style.dy || 0) }); - item.states = merge({}, DEFAULT_STATES, state?.richText); + item.states = merge({}, DEFAULT_STATES, state?.itemContent); } else if (itemType === 'image') { item.setAttributes({ - dx: this.getItemDx(item, positionType, imageStyle) + (imageStyle.dx || 0), - dy: this.getItemDy(item, positionType, imageStyle) + (imageStyle.dy || 0) + dx: this.getItemDx(item, positionType, style) + (style.dx || 0), + dy: this.getItemDy(item, positionType, style) + (style.dy || 0) }); - item.states = merge({}, DEFAULT_STATES, state?.image); + item.states = merge({}, DEFAULT_STATES, state?.itemContent); } const itemAngle = isPostiveXAxis(lineEndAngle) ? lineEndAngle : lineEndAngle - Math.PI; @@ -193,37 +191,31 @@ export class MarkPoint extends Marker { protected initItem(itemContent: IItemContent, newPosition: Point, newItemPosition: Point) { const { state } = this.attribute as MarkPointAttrs; - const { type = 'text', symbolStyle, richTextStyle, imageStyle, renderCustomCallback } = itemContent; + const { type = 'text', style, renderCustomCallback } = itemContent as any; let item: ISymbol | Tag | IImage | IRichText | IGroup; if (type === 'symbol') { item = graphicCreator.symbol({ ...newItemPosition, - ...symbolStyle + ...style }); - item.states = merge({}, DEFAULT_STATES, state?.symbol); + item.states = merge({}, DEFAULT_STATES, state?.itemContent); } else if (type === 'text') { item = new Tag({ ...newItemPosition, state: { panel: merge({}, DEFAULT_STATES, state?.textBackground), - text: merge({}, DEFAULT_STATES, state?.text) + text: merge({}, DEFAULT_STATES, state?.itemContent) } }); - } else if (type === 'richText') { - item = graphicCreator.richtext({ - ...newItemPosition, - ...richTextStyle - }); - item.states = merge({}, DEFAULT_STATES, state?.richText); } else if (type === 'image') { item = graphicCreator.image({ ...newItemPosition, - ...imageStyle + ...style }); - item.states = merge({}, DEFAULT_STATES, state?.image); + item.states = merge({}, DEFAULT_STATES, state?.itemContent); } else if (type === 'custom' && renderCustomCallback) { item = renderCustomCallback(); - item.states = merge({}, DEFAULT_STATES, state?.customMark); + item.states = merge({}, DEFAULT_STATES, state?.itemContent); } item.name = `mark-point-${type}`; this.setItemAttributes(item, itemContent, newPosition, newItemPosition, type); @@ -461,7 +453,7 @@ export class MarkPoint extends Marker { visible: targetItemvisible = false, size: targetSymbolSize } = targetSymbol; - const targetSize = targetItemvisible ? targetSymbolStyle.size ?? targetSymbolSize ?? 20 : 0; + const targetSize = targetItemvisible ? (targetSymbolStyle.size ?? targetSymbolSize ?? 20) : 0; let targetOffsetAngle; if (itemLine.type === 'type-do') { diff --git a/packages/vrender-components/src/marker/type.ts b/packages/vrender-components/src/marker/type.ts index 5247d265d..af44ea090 100644 --- a/packages/vrender-components/src/marker/type.ts +++ b/packages/vrender-components/src/marker/type.ts @@ -285,34 +285,24 @@ export type MarkPointState = { * 设置线终点图形在特定状态下的样式 */ lineEndSymbol?: State>; - /** - * 设置标注图形在特定状态下的样式 - */ - symbol?: State>; - /** - * 设置标注图形在特定状态下的样式 - */ - image?: State>; - /** - * 设置标签在特定状态下的样式 - */ - text?: State>; /** * 设置标签背景区块在特定状态下的样式 */ textBackground?: State>; - /** - * 设置富文本在特定状态下的样式 - */ - richText?: State>; - /** - * 设置自定义标注图形在特定状态下的样式 - */ - customMark?: State>; /** * 设置目标元素在特定状态下的样式 */ targetItem?: State>; + /** + * 设置content在特定状态下的样式 + * 等价于原来的 symbol | image | text | richText | customMark + */ + itemContent?: State< + | Partial + | Partial + | Partial + | Partial + >; }; export type MarkerLineLabelAttrs = { @@ -494,9 +484,9 @@ export type MarkArcAreaAttrs = MarkerAttrs & { export type IItemContent = IMarkRef & { /** * 标注类型 - * Tips: 保留'richText'与之前的定义做兼容 + * Tips: 不保留'richText', 在vchart层做兼容 */ - type?: 'symbol' | 'text' | 'image' | 'richText' | 'custom'; + type?: 'symbol' | 'text' | 'image' | 'custom'; /** * 设置标注的位置 */ @@ -510,22 +500,9 @@ export type IItemContent = IMarkRef & { */ offsetY?: number; /** - * type为symbol时, symbol的样式 - */ - symbolStyle?: ISymbolGraphicAttribute; - /** - * type为image时, image的样式 - */ - imageStyle?: IImageGraphicAttribute; - /** - * type为text时, text的配置 - * 'text'类型的ItemContent新增三种子类型:'text','rich','html'。配置在textStyle.type上,继承自TagAttributes。 - */ - textStyle?: IMarkLabel; - /** - * type为rich text时, rich text的样式 + * item样式 */ - richTextStyle?: IRichTextGraphicAttribute; + style?: ISymbolGraphicAttribute | IImageGraphicAttribute | IMarkLabel; /** * type为custom时,允许以callback的方式传入需要render的item */ From b910807450aae5b4f0b1c88d1ed0d7a467406053 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Fri, 16 May 2025 17:24:32 +0800 Subject: [PATCH 133/179] fix: solve animate problem --- packages/vrender-components/src/marker/register.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/vrender-components/src/marker/register.ts b/packages/vrender-components/src/marker/register.ts index d27809dee..9c3aaa0c9 100644 --- a/packages/vrender-components/src/marker/register.ts +++ b/packages/vrender-components/src/marker/register.ts @@ -8,29 +8,35 @@ import { } from '@visactor/vrender-kits'; import { loadTagComponent } from '../tag/register'; import { loadArcSegmentComponent, loadSegmentComponent } from '../segment/register'; +import { registerAnimate } from '@visactor/vrender-animate'; function loadBaseMarker() { registerGroup(); loadTagComponent(); + registerAnimate(); } export function loadMarkLineComponent() { loadBaseMarker(); loadSegmentComponent(); + registerAnimate(); } export function loadMarkArcLineComponent() { loadBaseMarker(); loadArcSegmentComponent(); + registerAnimate(); } export function loadMarkAreaComponent() { loadBaseMarker(); registerPolygon(); + registerAnimate(); } export function loadMarkArcAreaComponent() { loadBaseMarker(); registerArc(); + registerAnimate(); } export function loadMarkPointComponent() { @@ -40,4 +46,5 @@ export function loadMarkPointComponent() { registerSymbol(); registerImage(); registerLine(); + registerAnimate(); } From 826cdc081ada5e2ec51ff7c198b32c622531f206 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Fri, 16 May 2025 21:16:44 +0800 Subject: [PATCH 134/179] chore: test error --- packages/vrender-components/__tests__/unit/marker/point.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vrender-components/__tests__/unit/marker/point.test.ts b/packages/vrender-components/__tests__/unit/marker/point.test.ts index 588568805..129f1ae51 100644 --- a/packages/vrender-components/__tests__/unit/marker/point.test.ts +++ b/packages/vrender-components/__tests__/unit/marker/point.test.ts @@ -30,7 +30,7 @@ describe('Marker', () => { refX: 10, refY: 0, refAngle: 0, - textStyle: { + style: { text: 'mark point label text', panel: { visible: true From 7b1a61fe789bbc35039b576a613f982badcb3896 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 20 May 2025 15:54:16 +0800 Subject: [PATCH 135/179] feat: add transition --- .../src/state/animation-states-registry.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/vrender-animate/src/state/animation-states-registry.ts b/packages/vrender-animate/src/state/animation-states-registry.ts index d3905da11..d19600202 100644 --- a/packages/vrender-animate/src/state/animation-states-registry.ts +++ b/packages/vrender-animate/src/state/animation-states-registry.ts @@ -126,6 +126,37 @@ export class AnimationTransitionRegistry { allowTransition: true, stopOriginalTransition: true })); + + this.registerTransition('update', '*', () => ({ + allowTransition: true, + stopOriginalTransition: false + })); + // update动画碰到disappear动画,会停止,也会被覆盖 + this.registerTransition('update', 'disappear', () => ({ + allowTransition: true, + stopOriginalTransition: true + })); + // update动画碰到exit动画,会停止,也会被覆盖 + this.registerTransition('update', 'exit', () => ({ + allowTransition: true, + stopOriginalTransition: true + })); + + // state动画,可以被任何动画覆盖,但不会停止(disappear、exit除外) + this.registerTransition('state', '*', () => ({ + allowTransition: true, + stopOriginalTransition: false + })); + // state动画碰到disappear动画,会停止,也会被覆盖 + this.registerTransition('state', 'disappear', () => ({ + allowTransition: true, + stopOriginalTransition: true + })); + // state动画碰到exit动画,会停止,也会被覆盖 + this.registerTransition('state', 'exit', () => ({ + allowTransition: true, + stopOriginalTransition: true + })); } /** From b7b976fed3f1d4428bb6803dc93281513328a865 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 21 May 2025 15:53:56 +0800 Subject: [PATCH 136/179] fix: fix issue with circular deps --- .../src/common/performance-raf.ts | 4 +-- .../src/common/segment/curve/cubic-bezier.ts | 29 ---------------- .../common/segment/curve/quadratic-bezier.ts | 33 +++++++++++++++++-- .../contributions/render/group-render.ts | 4 +-- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/packages/vrender-core/src/common/performance-raf.ts b/packages/vrender-core/src/common/performance-raf.ts index 460cbc986..e724ec8d6 100644 --- a/packages/vrender-core/src/common/performance-raf.ts +++ b/packages/vrender-core/src/common/performance-raf.ts @@ -1,4 +1,4 @@ -import { vglobal } from '../modules'; +import { application } from '../application'; /** * 性能优化,将requestAnimationFrame的回调函数存储起来,在下一帧执行 @@ -38,6 +38,6 @@ export class PerformanceRAF { if (this._rafHandle !== null || this.nextAnimationFrameCbs.length === 0) { return; } - this._rafHandle = vglobal.getRequestAnimationFrame()(this.runAnimationFrame); + this._rafHandle = application.global.getRequestAnimationFrame()(this.runAnimationFrame); }; } diff --git a/packages/vrender-core/src/common/segment/curve/cubic-bezier.ts b/packages/vrender-core/src/common/segment/curve/cubic-bezier.ts index f3494aed6..c064203a2 100644 --- a/packages/vrender-core/src/common/segment/curve/cubic-bezier.ts +++ b/packages/vrender-core/src/common/segment/curve/cubic-bezier.ts @@ -35,35 +35,6 @@ export function divideCubic(curve: ICubicBezierCurve, t: number): ICubicBezierCu return [curve1, curve2]; } -/** - * 对三次贝塞尔曲线进行分割 - * @param p0 起点 - * @param p1 控制点1 - * @param p2 控制点2 - * @param p3 终点 - * @param t - */ -export function divideQuad(curve: IQuadraticBezierCurve, t: number): IQuadraticBezierCurve[] { - const { p0, p1, p2 } = curve; - - // 划分点 - const pt = quadPointAt(p0, p1, p2, t); - // const xt = pt.x; - // const yt = pt.y; - - // 计算两点之间的差值点 - const c1 = PointService.pointAtPP(p0, p1, t); - const c2 = PointService.pointAtPP(p1, p2, t); - // const c3 = PointService.pointAtPP(p2, p3, t); - // const c12 = PointService.pointAtPP(c1, c2, t); - // const c23 = PointService.pointAtPP(c2, c3, t); - // const direction = p1.x1 ? p1.y > p0.y ? 0 : 1 : p1.x > p0.x ? 0 : 1; - - const curve1 = new QuadraticBezierCurve(p0, c1, pt); - const curve2 = new QuadraticBezierCurve(pt, c2, p2); - - return [curve1, curve2]; -} export class CubicBezierCurve extends Curve implements ICubicBezierCurve { type: number = CurveTypeEnum.CubicBezierCurve; diff --git a/packages/vrender-core/src/common/segment/curve/quadratic-bezier.ts b/packages/vrender-core/src/common/segment/curve/quadratic-bezier.ts index 0d7834350..6313f9323 100644 --- a/packages/vrender-core/src/common/segment/curve/quadratic-bezier.ts +++ b/packages/vrender-core/src/common/segment/curve/quadratic-bezier.ts @@ -2,8 +2,37 @@ import type { IDirection, IPath2D, IQuadraticBezierCurve } from '../../../interf import { quadLength, quadPointAt } from '../../bezier-utils'; import { CurveTypeEnum, Direction } from '../../enums'; import { Curve } from './base'; -import { abs, atan2, max, min, type IPoint, type IPointLike } from '@visactor/vutils'; -import { divideQuad } from './cubic-bezier'; +import { abs, atan2, max, min, PointService, type IPoint, type IPointLike } from '@visactor/vutils'; + +/** + * 对三次贝塞尔曲线进行分割 + * @param p0 起点 + * @param p1 控制点1 + * @param p2 控制点2 + * @param p3 终点 + * @param t + */ +export function divideQuad(curve: IQuadraticBezierCurve, t: number): IQuadraticBezierCurve[] { + const { p0, p1, p2 } = curve; + + // 划分点 + const pt = quadPointAt(p0, p1, p2, t); + // const xt = pt.x; + // const yt = pt.y; + + // 计算两点之间的差值点 + const c1 = PointService.pointAtPP(p0, p1, t); + const c2 = PointService.pointAtPP(p1, p2, t); + // const c3 = PointService.pointAtPP(p2, p3, t); + // const c12 = PointService.pointAtPP(c1, c2, t); + // const c23 = PointService.pointAtPP(c2, c3, t); + // const direction = p1.x1 ? p1.y > p0.y ? 0 : 1 : p1.x > p0.x ? 0 : 1; + + const curve1 = new QuadraticBezierCurve(p0, c1, pt); + const curve2 = new QuadraticBezierCurve(pt, c2, p2); + + return [curve1, curve2]; +} export class QuadraticBezierCurve extends Curve implements IQuadraticBezierCurve { type: number = CurveTypeEnum.QuadraticBezierCurve; diff --git a/packages/vrender-core/src/render/contributions/render/group-render.ts b/packages/vrender-core/src/render/contributions/render/group-render.ts index fa6d3a3f1..f77477365 100644 --- a/packages/vrender-core/src/render/contributions/render/group-render.ts +++ b/packages/vrender-core/src/render/contributions/render/group-render.ts @@ -26,7 +26,7 @@ import { GROUP_NUMBER_TYPE } from '../../../graphic/constants'; import { BaseRenderContributionTime } from '../../../common/enums'; import { defaultGroupBackgroundRenderContribution } from './contributions'; import { multiplyMat4Mat4 } from '../../../common/matrix'; -import { vglobal } from '../../../modules'; +import { application } from '../../../application'; @injectable() export class DefaultCanvasGroupRender implements IGraphicRender { @@ -235,7 +235,7 @@ export class DefaultCanvasGroupRender implements IGraphicRender { const { x, y, width, height } = group.attribute; // 绘制到新的Canvas上,然后再绘制回来 const canvas = context.canvas; - const newCanvas = vglobal.createCanvas({ width: canvas.width, height: canvas.height, dpr: 1 }); + const newCanvas = application.global.createCanvas({ width: canvas.width, height: canvas.height, dpr: 1 }); const newContext = newCanvas.getContext('2d'); const transform = context.nativeContext.getTransform(); // 首先应用transform From e26292c026a5dd6d9b724d5dea691f70fcf60717 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Wed, 21 May 2025 17:51:45 +0800 Subject: [PATCH 137/179] fix: delete-default-value --- ...hore-delete-default-value_2025-05-21-09-51.json | 10 ++++++++++ packages/vrender-components/src/marker/config.ts | 14 +++----------- 2 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 common/changes/@visactor/vrender-components/chore-delete-default-value_2025-05-21-09-51.json diff --git a/common/changes/@visactor/vrender-components/chore-delete-default-value_2025-05-21-09-51.json b/common/changes/@visactor/vrender-components/chore-delete-default-value_2025-05-21-09-51.json new file mode 100644 index 000000000..0decaf859 --- /dev/null +++ b/common/changes/@visactor/vrender-components/chore-delete-default-value_2025-05-21-09-51.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-components", + "comment": "fix: delete-default-value", + "type": "none" + } + ], + "packageName": "@visactor/vrender-components" +} \ No newline at end of file diff --git a/packages/vrender-components/src/marker/config.ts b/packages/vrender-components/src/marker/config.ts index edb5e525c..e07511903 100644 --- a/packages/vrender-components/src/marker/config.ts +++ b/packages/vrender-components/src/marker/config.ts @@ -450,23 +450,15 @@ export const DEFAULT_MARK_POINT_THEME = { type: 'text', position: 'middle', refX: 10, - symbolStyle: { + style: { symbolType: 'star', fill: 'rgb(48, 115, 242)', fillOpacity: 0.8, - size: 20 - }, - textStyle: { + size: 20, dx: 0, - dy: 0 - }, - imageStyle: { + dy: 0, width: 80, height: 80 - }, - richTextStyle: { - width: 100, - height: 100 } } }; From 94b142cc766224b323136d7aa30db118740bbf7e Mon Sep 17 00:00:00 2001 From: skie1997 Date: Wed, 21 May 2025 20:45:18 +0800 Subject: [PATCH 138/179] fix: style error --- packages/vrender-components/src/marker/point.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/vrender-components/src/marker/point.ts b/packages/vrender-components/src/marker/point.ts index fb7c7cd6e..ae5cd5032 100644 --- a/packages/vrender-components/src/marker/point.ts +++ b/packages/vrender-components/src/marker/point.ts @@ -121,7 +121,7 @@ export class MarkPoint extends Marker { const offsetY = newItemPosition.y - newPosition.y; item.setAttributes({ ...(style as TagAttributes), - style: { + textStyle: { ...this.getTextAlignAttr( autoRotate, offsetX, @@ -207,6 +207,13 @@ export class MarkPoint extends Marker { text: merge({}, DEFAULT_STATES, state?.itemContent) } }); + } else if (type === 'richText') { + // 兼容老逻辑 + item = graphicCreator.richtext({ + ...newItemPosition, + ...style + }); + item.states = merge({}, DEFAULT_STATES, state?.itemContent); } else if (type === 'image') { item = graphicCreator.image({ ...newItemPosition, From 1b5d7fe1b1b426d116e2e2fe84efc949e9595d41 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Wed, 21 May 2025 22:32:02 +0800 Subject: [PATCH 139/179] fix: delete default style --- packages/vrender-components/src/marker/config.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/vrender-components/src/marker/config.ts b/packages/vrender-components/src/marker/config.ts index e07511903..d7ed59672 100644 --- a/packages/vrender-components/src/marker/config.ts +++ b/packages/vrender-components/src/marker/config.ts @@ -449,17 +449,7 @@ export const DEFAULT_MARK_POINT_THEME = { itemContent: { type: 'text', position: 'middle', - refX: 10, - style: { - symbolType: 'star', - fill: 'rgb(48, 115, 242)', - fillOpacity: 0.8, - size: 20, - dx: 0, - dy: 0, - width: 80, - height: 80 - } + refX: 10 } }; From cb6b9ac4fad9470aae4c53a8124f71f1243b0e1e Mon Sep 17 00:00:00 2001 From: xile611 Date: Thu, 29 May 2025 17:37:36 +0800 Subject: [PATCH 140/179] fix: polygon should not throw error when points is empty --- packages/vrender-core/src/common/polygon.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/vrender-core/src/common/polygon.ts b/packages/vrender-core/src/common/polygon.ts index cc84518a6..014c5a730 100644 --- a/packages/vrender-core/src/common/polygon.ts +++ b/packages/vrender-core/src/common/polygon.ts @@ -10,6 +10,9 @@ import type { IPath2D } from '../interface'; * @param y */ export function drawPolygon(path: IPath2D, points: IPointLike[], x: number, y: number) { + if (!points || !points.length) { + return; + } path.moveTo(points[0].x + x, points[0].y + y); for (let i = 1; i < points.length; i++) { path.lineTo(points[i].x + x, points[i].y + y); From 4548bc232227839090aaa8f6e8e7e3c3012361ef Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 29 May 2025 18:09:15 +0800 Subject: [PATCH 141/179] fix: performanceRAF id start from 1 --- packages/vrender-core/src/common/performance-raf.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/vrender-core/src/common/performance-raf.ts b/packages/vrender-core/src/common/performance-raf.ts index e724ec8d6..9d0398f1e 100644 --- a/packages/vrender-core/src/common/performance-raf.ts +++ b/packages/vrender-core/src/common/performance-raf.ts @@ -11,13 +11,18 @@ export class PerformanceRAF { this.nextAnimationFrameCbs.push(callback); // 下一帧执行nextAnimationFrameCbs this.tryRunAnimationFrameNextFrame(); - return this.nextAnimationFrameCbs.length - 1; + return this.nextAnimationFrameCbs.length; } + /** + * 移除指定索引的回调函数 + * @param index raf索引,从1开始,相当于内部nextAnimationFrameCbs的idx + 1 + * @returns 是否移除成功 + */ removeAnimationFrameCb(index: number): boolean { - if (index >= 0 && index < this.nextAnimationFrameCbs.length) { + if (index > 0 && index <= this.nextAnimationFrameCbs.length) { // Set to null instead of empty function to avoid linter error - this.nextAnimationFrameCbs[index] = null; + this.nextAnimationFrameCbs[index - 1] = null; return true; } return false; From 8b549e7767900c539356501c9a70549722477d58 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Thu, 29 May 2025 20:56:23 +0800 Subject: [PATCH 142/179] feat: change setFinalAttribute and initFinalAttribute to setFinalAttributes initFinalAttributes, add tutorial md --- VRender-Animate-Guide.md | 531 ++++++++++++++++++ .../vrender-animate/src/animate-extension.ts | 4 +- packages/vrender-animate/src/step.ts | 2 +- packages/vrender-components/src/axis/base.ts | 2 +- 4 files changed, 535 insertions(+), 4 deletions(-) create mode 100644 VRender-Animate-Guide.md diff --git a/VRender-Animate-Guide.md b/VRender-Animate-Guide.md new file mode 100644 index 000000000..5007635d0 --- /dev/null +++ b/VRender-Animate-Guide.md @@ -0,0 +1,531 @@ +# VRender-Animate 使用指南 + +VRender-Animate 是 VRender 图形引擎的动画系统,提供强大的动画能力,支持属性动画、自定义动画、状态管理等功能。 + +## 升级变更 + +### animate 注册机制 + +0.x 的 VRender 默认自带 animate,现在需要手动注册。 + +```typescript +import { registerAnimate, registerCustomAnimate } from '@visactor/vrender-animate'; + +// 注册基本的动画接口 +registerAnimate(); +// 注册自定义动画 +registerCustomAnimate(); +``` + +### Ticker 接口变更 + +0.x 的 VRender 中 Ticker 和 Stage 不对应,所以无需传入 Stage,现在需要传入 Stage。并且 Stage 和 Ticker 一一对应 + +```typescript +import { DefaultTicker } from '@visactor/vrender-animate'; +const ticker = new DefaultTicker(stage); +``` + +## 1. Timeline、Ticker、Animate 核心架构 + +### 1.1 Timeline(时间线管理器) + +Timeline 是动画系统的时间管理核心,负责统一管理所有动画的时间推进。 + +**核心特性:** + +- 基于链表的动画节点管理,支持高效的增删操作 +- 支持播放速度控制 +- 支持暂停/恢复功能 +- 自动计算总时长 +- 事件系统支持 + +**使用示例:** + +```typescript +import { DefaultTimeline } from '@visactor/vrender-animate'; + +// 创建时间线 +const timeline = new DefaultTimeline(); + +// 设置播放速度 +timeline.setPlaySpeed(2.0); // 2倍速播放 + +// 暂停和恢复 +timeline.pause(); +timeline.resume(); + +// 监听动画结束事件 +timeline.on('animationEnd', () => { + console.log('所有动画已完成'); +}); + +// 获取时间线状态 +console.log('动画数量:', timeline.animateCount); +console.log('播放状态:', timeline.getPlayState()); // 'playing' | 'paused' | 'stopped' +console.log('总时长:', timeline.getTotalDuration()); +``` + +**Timeline 主要方法:** + +- `addAnimate(animate)` - 添加动画到时间线 +- `removeAnimate(animate, release)` - 从时间线移除动画 +- `tick(delta)` - 推进时间线(通常由 Ticker 调用) +- `clear()` - 清空所有动画 +- `forEachAccessAnimate(callback)` - 遍历所有可执行动画 + +### 1.2 Ticker(时钟驱动器) + +Ticker 负责驱动时间的推进,有两种实现: + +#### DefaultTicker(默认时钟) + +基于 requestAnimationFrame 的实时时钟,适用于正常的动画播放场景。 + +```typescript +import { DefaultTicker } from '@visactor/vrender-animate'; + +const ticker = new DefaultTicker(stage); + +// 添加时间线 +ticker.addTimeline(timeline); + +// 设置帧率 +ticker.setFPS(60); +ticker.setInterval(16); // 或直接设置间隔(毫秒) + +// 启动时钟 +ticker.start(); + +// 暂停和恢复 +ticker.pause(); +ticker.resume(); + +// 停止时钟 +ticker.stop(); +``` + +**DefaultTicker 特性:** + +- 自动启停:当没有动画时自动停止 +- 帧率控制:支持设置 FPS 或间隔时间 +- 随机扰动:避免所有 tick 发生在同一帧 + +#### ManualTicker(手动时钟) + +手动控制的时钟,适用于测试、录制或精确控制的场景。 + +```typescript +import { ManualTicker } from '@visactor/vrender-animate'; + +const manualTicker = new ManualTicker(stage); +manualTicker.addTimeline(timeline); + +// 手动推进时间 +manualTicker.tick(16.67); // 推进一帧的时间 + +// 跳转到指定时间点 +manualTicker.tickTo(1000); // 跳转到1秒时间点 +manualTicker.tickAt(5000); // 跳转到5s时间点 + +// 获取当前时间 +console.log('当前时间:', manualTicker.getTime()); +``` + +### 1.3 Animate(动画实例) + +Animate 是单个动画的执行实体,支持链式调用的动画定义。 + +**基本使用:** + +```typescript +import { Animate } from '@visactor/vrender-animate'; + +// 创建动画 +const animate = new Animate('moveAnimation', timeline); + +// 绑定目标图形(真正使用时请用myRect.animate()) +animate.bind(myRect); + +if (!myRect.animates) { + myRect.animates = new Map(); +} +myRect.animates.set(animate.id, animate); + +// 定义动画序列 +animate + .to({ x: 100, y: 50 }, 1000, 'easeInOut') // 移动动画 + .wait(500) // 等待500ms + .to({ fill: 'red' }, 800, 'linear') // 颜色变化 + .from({ opacity: 0.5 }, 300, 'easeOut'); // 从指定状态开始 + +// 设置回调 +animate.onStart(() => console.log('动画开始')); +animate.onFrame((step, ratio) => console.log('动画进度:', ratio)); +animate.onEnd(() => console.log('动画结束')); +animate.onRemove(() => console.log('动画被移除')); +``` + +**动画控制方法:** + +```typescript +// 播放控制 +animate.play(); // 开始播放 +animate.pause(); // 暂停 +animate.resume(); // 恢复 +animate.stop('end'); // 停止并跳到结束状态 +animate.stop('start'); // 停止并回到开始状态 +animate.stop({ x: 50, y: 25 }); // 停止并设置为指定状态 + +// 时间控制 +animate.startAt(1000); // 延迟1秒开始 +animate.getDuration(); // 获取总时长 +animate.getStartTime(); // 获取开始时间 + +// 属性控制 +animate.preventAttr('x'); // 阻止x属性被后续动画修改 +animate.preventAttrs(['x', 'y']); // 阻止多个属性 +animate.validAttr('x'); // 检查属性是否可以被修改 +``` + +**高级功能:** + +```typescript +// 循环动画 +animate.loop(3); // 循环3次 +animate.loop(Infinity); // 无限循环 + +// 回弹动画 +animate.bounce(true); // 启用回弹效果 + +// 动画链式依赖 +const anim1 = animate1.to({ x: 100 }, 1000); +const anim2 = animate2.after(anim1).to({ y: 100 }, 1000); // anim2在anim1后执行 +const anim3 = animate3.afterAll([anim1, anim2]).to({ opacity: 0 }, 500); // anim3在所有动画后执行 + +// 并行动画 +const anim4 = animate4.parallel(anim1).to({ opacity: 0.5 }, 1000); // anim4与anim1并行 +``` + +**动画步骤(Step)详解:** + +```typescript +// Step是Animate内部的执行单元 +// 每个to()、wait()、from()调用都会创建一个Step + +// Step的主要属性 +interface IStep { + type: 'to' | 'wait' | 'from'; + duration: number; + props: Record; + easing: EasingTypeFunc; + + // 链表结构 + prev?: IStep; + next?: IStep; + + // 执行方法 + onStart(): void; + update(end: boolean, ratio: number, out: Record): void; + onEnd(): void; +} +``` + +--- + +## 2. AnimationState 使用及 AnimationTransition 管理 + +### 2.1 动画状态概念 + +AnimationState 为图形元素提供状态驱动的动画系统,支持预定义状态之间的平滑过渡。 + +**状态配置接口:** + +```typescript +interface IAnimationState { + name: string; // 状态名称 + animation: IAnimationConfig; // 动画配置 +} + +interface IAnimationConfig { + from?: Record; // 起始属性 + to?: Record; // 目标属性 + duration?: number; // 持续时间 + delay?: number; // 延迟时间 + easing?: EasingType; // 缓动函数 + loop?: boolean | number; // 循环设置 + // ... 更多配置选项 +} +``` + +### 2.2 注册和使用动画状态 + +```typescript +import { AnimationStates } from '@visactor/vrender-animate'; + +// 1. 注册基础动画状态 +myRect.registerAnimationState({ + name: AnimationStates.APPEAR, + animation: { + from: { opacity: 0, scaleX: 0, scaleY: 0 }, + to: { opacity: 1, scaleX: 1, scaleY: 1 }, + duration: 800, + easing: 'bounceOut' + } +}); + +myRect.registerAnimationState({ + name: AnimationStates.HIGHLIGHT, + animation: { + to: { fill: 'yellow', lineWidth: 3 }, + duration: 300, + easing: 'easeInOut' + } +}); + +// 2. 注册自定义状态 +myRect.registerAnimationState({ + name: 'customGlow', + animation: { + to: { + fill: 'gold', + stroke: 'orange', + shadowBlur: 10, + shadowColor: 'yellow' + }, + duration: 500, + easing: 'easeOutQuad' + } +}); + +// 3. 应用状态 +el.applyAnimationState( + ['update'], + [ + { + name: 'update', + animation: { + selfOnly: true, + ...animationConfig.update, + type: 'axisUpdate', + customParameters: { + config: animationConfig.update, + diffAttrs, + lastScale + } + } + } + ] +); +``` + +### 2.3 AnimationTransition 转换规则 + +AnimationTransitionRegistry 管理状态之间的转换规则,决定哪些状态可以互相切换。 + +**内置转换规则:** + +```typescript +// appear动画可以被任何动画覆盖(除了disappear、exit会停止appear) +'appear' -> '*' // 允许转换,不停止原动画 +'appear' -> 'appear' // 不允许转换(避免重复) +'appear' -> 'disappear' // 允许转换,停止原动画 +'appear' -> 'exit' // 允许转换,停止原动画 + +// 循环动画(normal)的转换规则 +'normal' -> '*' // 允许转换,不停止原动画 +'normal' -> 'normal' // 不允许转换 +'normal' -> 'disappear' // 允许转换,停止原动画 +'normal' -> 'exit' // 允许转换,停止原动画 + +// 退出动画不能被覆盖(除了disappear) +'exit' -> '*' // 不允许转换 +'exit' -> 'disappear' // 允许转换,停止原动画 +'exit' -> 'enter' // 允许转换,停止原动画 +'exit' -> 'exit' // 不允许转换 + +// update和state动画的转换规则 +'update' -> '*' // 允许转换,不停止原动画 +'update' -> 'disappear' // 允许转换,停止原动画 +'update' -> 'exit' // 允许转换,停止原动画 + +'state' -> '*' // 允许转换,不停止原动画 +'state' -> 'disappear' // 允许转换,停止原动画 +'state' -> 'exit' // 允许转换,停止原动画 +``` + +**自定义转换规则:** + +```typescript +import { AnimationTransitionRegistry } from '@visactor/vrender-animate'; + +const registry = AnimationTransitionRegistry.getInstance(); + +// 注册自定义转换规则 +registry.registerTransition('customState1', 'customState2', (graphic, fromState) => { + return { + allowTransition: true, // 是否允许转换 + stopOriginalTransition: true // 是否停止原动画 + }; +}); + +// 通配符规则 +registry.registerTransition('*', 'emergency', (graphic, fromState) => { + // 紧急状态可以打断任何动画 + return { + allowTransition: true, + stopOriginalTransition: true + }; +}); +``` + +**转换函数的参数:** + +```typescript +type TransitionFunction = ( + graphic: IGraphic, // 目标图形对象 + fromState: string // 源状态名称 +) => { + allowTransition: boolean; // 是否允许进行状态转换 + stopOriginalTransition: boolean; // 是否停止当前正在执行的动画 +}; +``` + +--- + +## 3. 动画注册机制(registerAnimate) + +### 3.1 注册过程 + +```typescript +import { registerAnimate } from '@visactor/vrender-animate'; + +// 全局注册动画功能到Graphic原型 +registerAnimate(); +``` + +**注册效果:** + +- 所有继承自 Graphic 的图形对象都将具备动画能力 + +### 3.2 扩展后的图形功能 + +注册后,所有图形对象将具备以下动画能力: + +#### 状态管理方法(来自 GraphicStateExtension) + +```typescript +// 状态注册 +myRect.registerAnimationState(state: IAnimationState): this; + +// 状态应用 +myRect.applyAnimationState( + state: string[], + animationConfig: (IAnimationState | IAnimationState[])[], + callback?: (empty?: boolean) => void +): this; + +// 预定义状态方法 +myRect.applyAppearState(config: IAnimationConfig, callback?: () => void): this; +myRect.applyDisappearState(config: IAnimationConfig, callback?: () => void): this; +myRect.applyUpdateState(config: IAnimationConfig, callback?: () => void): this; +myRect.applyHighlightState(config: IAnimationConfig, callback?: () => void): this; +myRect.applyUnhighlightState(config: IAnimationConfig, callback?: () => void): this; + +// 状态控制 +myRect.stopAnimationState(state: string, type?: 'start' | 'end' | Record): this; +myRect.clearAnimationStates(): this; +``` + +#### 动画执行方法(来自 AnimateExtension) + +```typescript +// 基础动画创建 +myRect.animate(params?: IGraphicAnimateParams): IAnimate; + +// 动画执行器 +myRect.executeAnimation(config: IAnimationConfig): this; +myRect.executeAnimations(configs: IAnimationConfig[]): this; + +// 时间线和时钟创建 +myRect.createTimeline(): DefaultTimeline; +myRect.createTicker(stage: any): DefaultTicker; + +// 属性管理 +myRect.getAttributes(final?: boolean): Record; +myRect.setFinalAttributes(finalAttribute: Record): void; +myRect.initFinalAttributes(finalAttribute: Record): void; +myRect.getGraphicAttribute(key: string, prev?: boolean): any; +``` + +### 3.3 完整使用示例 + +```typescript +import { registerAnimate, AnimationStates, DefaultTimeline, DefaultTicker } from '@visactor/vrender-animate'; +import { Rect, Circle, Stage } from '@visactor/vrender-core'; + +// 1. 注册动画系统 +registerAnimate(); + +// 2. 创建舞台和图形 +const stage = new Stage({ + container: 'main', + width: 800, + height: 600 +}); + +const rect = new Rect({ + x: 100, + y: 100, + width: 50, + height: 50, + fill: 'blue' +}); + +stage.defaultLayer.add(rect); + +// 3. 使用链式动画API +rect + .animate() + .to({ x: 200, fill: 'red' }, 1000, 'linear') + .wait(500) + .to({ y: 200, opacity: 0.5 }, 800, 'linear') + .onEnd(() => console.log('矩形动画完成')); + +// 4. 事件驱动的动画 +rect.addEventListener('pointerenter', () => { + rect.applyAnimationState( + ['hover'], + [ + { + name: 'hover', + animation: { + to: { fill: 'yellow', scaleX: 1.2, scaleY: 1.2 }, + duration: 200, + easing: 'easeOutQuad' + } + } + ] + ); +}); + +rect.addEventListener('pointertap', () => { + rect.applyAnimationState( + ['selected'], + [ + { + name: 'selected', + animation: { + to: { + stroke: 'orange', + lineWidth: 3, + shadowBlur: 5, + shadowColor: 'orange' + }, + duration: 300 + } + } + ] + ); +}); +``` diff --git a/packages/vrender-animate/src/animate-extension.ts b/packages/vrender-animate/src/animate-extension.ts index ff0ef8ecb..36c94ca2b 100644 --- a/packages/vrender-animate/src/animate-extension.ts +++ b/packages/vrender-animate/src/animate-extension.ts @@ -63,14 +63,14 @@ export class AnimateExtension { return new DefaultTicker(stage); } - setFinalAttribute(finalAttribute: Record) { + setFinalAttributes(finalAttribute: Record) { if (!this.finalAttribute) { this.finalAttribute = {}; } Object.assign(this.finalAttribute, finalAttribute); } - initFinalAttribute(finalAttribute: Record) { + initFinalAttributes(finalAttribute: Record) { this.finalAttribute = finalAttribute; } diff --git a/packages/vrender-animate/src/step.ts b/packages/vrender-animate/src/step.ts index 42212f7c5..80d3d9214 100644 --- a/packages/vrender-animate/src/step.ts +++ b/packages/vrender-animate/src/step.ts @@ -54,7 +54,7 @@ export class Step implements IStep { this.duration = duration; // 设置缓动函数 if (easing) { - this.easing = typeof easing === 'function' ? easing : Easing[easing]; + this.easing = typeof easing === 'function' ? easing : Easing[easing] ?? Easing.linear; } else { this.easing = Easing.linear; } diff --git a/packages/vrender-components/src/axis/base.ts b/packages/vrender-components/src/axis/base.ts index 9335b4202..aa3460e15 100644 --- a/packages/vrender-components/src/axis/base.ts +++ b/packages/vrender-components/src/axis/base.ts @@ -596,7 +596,7 @@ export abstract class AxisBase extends AnimateComp if ((el as IGraphic).type !== 'group' && el.id) { const oldEl = prevInnerView[el.id]; // 删除旧图元的动画 - el.setFinalAttribute(el.attribute); + el.setFinalAttributes(el.attribute); if (oldEl) { oldEl.release(); // oldEl.stopAnimationState('enter'); From ae0ebe7d777ce7869d28b8598df55c8afb5d705e Mon Sep 17 00:00:00 2001 From: skie1997 Date: Sat, 31 May 2025 22:30:25 +0800 Subject: [PATCH 143/179] fix: brush active problem --- .../fix-brush-problem_2025-05-31-14-30.json | 10 ++++++++ .../vrender-components/src/brush/brush.ts | 25 +++++++++++++------ 2 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 common/changes/@visactor/vrender-components/fix-brush-problem_2025-05-31-14-30.json diff --git a/common/changes/@visactor/vrender-components/fix-brush-problem_2025-05-31-14-30.json b/common/changes/@visactor/vrender-components/fix-brush-problem_2025-05-31-14-30.json new file mode 100644 index 000000000..ef83ae9b5 --- /dev/null +++ b/common/changes/@visactor/vrender-components/fix-brush-problem_2025-05-31-14-30.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-components", + "comment": "fix: brush active problem", + "type": "none" + } + ], + "packageName": "@visactor/vrender-components" +} \ No newline at end of file diff --git a/packages/vrender-components/src/brush/brush.ts b/packages/vrender-components/src/brush/brush.ts index 4da78c53c..27f7acf43 100644 --- a/packages/vrender-components/src/brush/brush.ts +++ b/packages/vrender-components/src/brush/brush.ts @@ -28,6 +28,7 @@ export class Brush extends AbstractComponent> { private _container!: IGroup; // 绘制mask时的相关属性 + private _activeBrushState = false; // 用于标记激活状态 private _activeDrawState = false; // 用于标记绘制状态 private _cacheDrawPoints: IPointLike[] = []; // 用于维护鼠标走过的路径,主要用于绘制mask的点 // 移动mask时的相关属性 @@ -157,12 +158,7 @@ export class Brush extends AbstractComponent> { brushMode === 'single' && this._clearMask(); this._addBrushMask(); this._dispatchBrushEvent(IOperateType.drawStart, e); - // 无论是多选,还是单选 - // 如果这是第一个brush mask - // 证明这第一次绘制, 则触发brushActive事件 - if (Object.keys(this._brushMaskAABBBoundsDict).length === 1) { - this._dispatchBrushEvent(IOperateType.brushActive, e); - } + this._activeBrushState = false; } /** @@ -198,7 +194,7 @@ export class Brush extends AbstractComponent> { */ private _drawing(e: FederatedPointerEvent) { const pos = this.eventPosToStagePos(e); - const { brushType } = this.attribute as BrushAttributes; + const { brushType, sizeThreshold = DEFAULT_SIZE_THRESHOLD } = this.attribute as BrushAttributes; const cacheLength = this._cacheDrawPoints.length; @@ -218,7 +214,20 @@ export class Brush extends AbstractComponent> { // 更新mask形状 const maskPoints = this._computeMaskPoints(); this._operatingMask.setAttribute('points', maskPoints); - this._dispatchBrushEvent(IOperateType.drawing, e); + const { x: x1, y: y1 } = this._startPos; + const { x: x2, y: y2 } = this.eventPosToStagePos(e); + // 绘制大小超过阈值, 才激活brush + if (Math.abs(x2 - x1) > sizeThreshold || Math.abs(y1 - y2) > sizeThreshold) { + // 无论是多选,还是单选 + // 如果这是第一个brush mask + // 证明这第一次绘制, 则触发brushActive事件 + if (Object.keys(this._brushMaskAABBBoundsDict).length === 1 && !this._activeBrushState) { + this._activeBrushState = true; + this._dispatchBrushEvent(IOperateType.brushActive, e); + } else { + this._dispatchBrushEvent(IOperateType.drawing, e); + } + } } /** From 7aac339178429f38b87c2361a9c661aa5e82c4c8 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 3 Jun 2025 10:54:25 +0800 Subject: [PATCH 144/179] fix: clone diffAttrs to avoid recall same animate --- packages/vrender-animate/src/custom/update.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vrender-animate/src/custom/update.ts b/packages/vrender-animate/src/custom/update.ts index 6131bfe02..2e38de631 100644 --- a/packages/vrender-animate/src/custom/update.ts +++ b/packages/vrender-animate/src/custom/update.ts @@ -27,8 +27,8 @@ export class Update extends ACustomAnimate> { let { diffAttrs = {} } = this.target.context ?? ({} as any); const { options } = this.params as any; + diffAttrs = { ...diffAttrs }; if (options?.excludeChannels?.length) { - diffAttrs = { ...diffAttrs }; options.excludeChannels.forEach((channel: string) => { delete diffAttrs[channel]; }); From 07eabe332a779dea499ebb28e467b21a7eeacc0a Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 3 Jun 2025 11:08:33 +0800 Subject: [PATCH 145/179] fix: fix issue with performance raf idx repeat --- .../src/common/performance-raf.ts | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/vrender-core/src/common/performance-raf.ts b/packages/vrender-core/src/common/performance-raf.ts index 9d0398f1e..d9d2bae09 100644 --- a/packages/vrender-core/src/common/performance-raf.ts +++ b/packages/vrender-core/src/common/performance-raf.ts @@ -1,17 +1,19 @@ import { application } from '../application'; +let idx = 0; + /** * 性能优化,将requestAnimationFrame的回调函数存储起来,在下一帧执行 */ export class PerformanceRAF { - nextAnimationFrameCbs: FrameRequestCallback[] = []; + nextAnimationFrameCbs: Map = new Map(); private _rafHandle: number | null = null; addAnimationFrameCb(callback: FrameRequestCallback) { - this.nextAnimationFrameCbs.push(callback); + this.nextAnimationFrameCbs.set(++idx, callback); // 下一帧执行nextAnimationFrameCbs this.tryRunAnimationFrameNextFrame(); - return this.nextAnimationFrameCbs.length; + return idx; } /** @@ -20,9 +22,8 @@ export class PerformanceRAF { * @returns 是否移除成功 */ removeAnimationFrameCb(index: number): boolean { - if (index > 0 && index <= this.nextAnimationFrameCbs.length) { - // Set to null instead of empty function to avoid linter error - this.nextAnimationFrameCbs[index - 1] = null; + if (this.nextAnimationFrameCbs.has(index)) { + this.nextAnimationFrameCbs.delete(index); return true; } return false; @@ -31,16 +32,12 @@ export class PerformanceRAF { protected runAnimationFrame = (time: number) => { this._rafHandle = null; const cbs = this.nextAnimationFrameCbs; - this.nextAnimationFrameCbs = []; - for (let i = 0; i < cbs.length; i++) { - if (cbs[i]) { - cbs[i](time); - } - } + this.nextAnimationFrameCbs = new Map(); + cbs.forEach(cb => cb(time)); }; protected tryRunAnimationFrameNextFrame = () => { - if (this._rafHandle !== null || this.nextAnimationFrameCbs.length === 0) { + if (this._rafHandle !== null || this.nextAnimationFrameCbs.size === 0) { return; } this._rafHandle = application.global.getRequestAnimationFrame()(this.runAnimationFrame); From e07b7ba8355c47a34dfb3b370aa3746c3f8ce0ee Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 3 Jun 2025 16:22:16 +0800 Subject: [PATCH 146/179] fix: fix issue with sphere animate --- packages/vrender-animate/src/custom/sphere.ts | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/packages/vrender-animate/src/custom/sphere.ts b/packages/vrender-animate/src/custom/sphere.ts index 53333ddb7..59a0af2ae 100644 --- a/packages/vrender-animate/src/custom/sphere.ts +++ b/packages/vrender-animate/src/custom/sphere.ts @@ -14,11 +14,29 @@ export class RotateBySphereAnimate extends ACustomAnimate { declare theta: number; declare phi: number; + onBind(): void { + super.onBind(); + + const to: Record = {}; + const from: Record = this.from ?? {}; + + // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + const finalAttribute = this.target.getFinalAttribute(); + if (finalAttribute) { + this.target.setAttributes(finalAttribute); + } + + this.props = to; + this.propKeys = ['x', 'y', 'z', 'alpha', 'zIndex']; + this.from = from; + this.to = to; + } + onStart(): void { const { center, r } = typeof this.params === 'function' ? this.params() : this.params; - const startX = this.target.getComputedAttribute('x'); - const startY = this.target.getComputedAttribute('y'); - const startZ = this.target.getComputedAttribute('z'); + const startX = this.target.finalAttribute.x; + const startY = this.target.finalAttribute.y; + const startZ = this.target.finalAttribute.z; const phi = Math.acos((startY - center.y) / r); let theta = Math.acos((startX - center.x) / r / Math.sin(phi)); if (startZ - center.z < 0) { @@ -28,11 +46,6 @@ export class RotateBySphereAnimate extends ACustomAnimate { this.phi = phi; } - onBind() { - super.onBind(); - return; - } - onEnd() { return; } @@ -41,6 +54,7 @@ export class RotateBySphereAnimate extends ACustomAnimate { if (this.phi == null || this.theta == null) { return; } + const target = this.target; const { center, r, cb } = typeof this.params === 'function' ? this.params() : this.params; const deltaAngle = Math.PI * 2 * ratio; const theta = this.theta + deltaAngle; @@ -48,18 +62,17 @@ export class RotateBySphereAnimate extends ACustomAnimate { const x = r * Math.sin(phi) * Math.cos(theta) + center.x; const y = r * Math.cos(phi) + center.y; const z = r * Math.sin(phi) * Math.sin(theta) + center.z; - out.x = x; - out.y = y; - out.z = z; + target.attribute.x = x; + target.attribute.y = y; + target.attribute.z = z; // out.beta = phi; - out.alpha = theta + pi / 2; - while (out.alpha > pi2) { - out.alpha -= pi2; + target.attribute.alpha = theta + pi / 2; + while (target.attribute.alpha > pi2) { + target.attribute.alpha -= pi2; } - out.alpha = pi2 - out.alpha; - - out.zIndex = out.z * -10000; + target.attribute.alpha = pi2 - target.attribute.alpha; + target.attribute.zIndex = target.attribute.z * -10000; cb && cb(out); } } From d58f66561e211a4dd64859fd9d365db5051aaf14 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 3 Jun 2025 16:29:29 +0800 Subject: [PATCH 147/179] fix: fix issue with RotateBySphereAnimate bind set attribute --- packages/vrender-animate/src/custom/sphere.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/vrender-animate/src/custom/sphere.ts b/packages/vrender-animate/src/custom/sphere.ts index 59a0af2ae..a3c1118f8 100644 --- a/packages/vrender-animate/src/custom/sphere.ts +++ b/packages/vrender-animate/src/custom/sphere.ts @@ -17,22 +17,27 @@ export class RotateBySphereAnimate extends ACustomAnimate { onBind(): void { super.onBind(); - const to: Record = {}; - const from: Record = this.from ?? {}; + // const to: Record = {}; + // const from: Record = this.from ?? {}; // 用于入场的时候设置属性(因为有动画的时候VChart不会再设置属性了) + + // this.props = to; + this.propKeys = ['x', 'y', 'z', 'alpha', 'zIndex']; + // this.from = from; + // this.to = to; + } + + onFirstRun(): void { + super.onFirstRun(); const finalAttribute = this.target.getFinalAttribute(); if (finalAttribute) { this.target.setAttributes(finalAttribute); } - - this.props = to; - this.propKeys = ['x', 'y', 'z', 'alpha', 'zIndex']; - this.from = from; - this.to = to; } onStart(): void { + super.onStart(); const { center, r } = typeof this.params === 'function' ? this.params() : this.params; const startX = this.target.finalAttribute.x; const startY = this.target.finalAttribute.y; From 427695333e95f628bd8a76c3eed97b04cd6f3934 Mon Sep 17 00:00:00 2001 From: xile611 Date: Wed, 4 Jun 2025 10:39:35 +0800 Subject: [PATCH 148/179] fix: fix label aniamtion when update --- .../src/animation/label-animate.ts | 3 ++- packages/vrender-components/src/label/base.ts | 21 +++++++------------ 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/packages/vrender-components/src/animation/label-animate.ts b/packages/vrender-components/src/animation/label-animate.ts index 488236bf4..031d16f83 100644 --- a/packages/vrender-components/src/animation/label-animate.ts +++ b/packages/vrender-components/src/animation/label-animate.ts @@ -1,4 +1,4 @@ -import { AComponentAnimate, AnimateExecutor, createComponentAnimator } from '@visactor/vrender-animate'; +import { AComponentAnimate, AnimateExecutor, createComponentAnimator, IncreaseCount } from '@visactor/vrender-animate'; /** * LabelUpdate class handles the update animation for Label components @@ -108,6 +108,7 @@ export class LabelEnter extends AComponentAnimate { } export function registerLabelAnimate() { + AnimateExecutor.registerBuiltInAnimate('increaseCount', IncreaseCount); // Label update animation AnimateExecutor.registerBuiltInAnimate('labelUpdate', LabelUpdate); AnimateExecutor.registerBuiltInAnimate('labelEnter', LabelEnter); diff --git a/packages/vrender-components/src/label/base.ts b/packages/vrender-components/src/label/base.ts index 004ad7ac1..67f23a849 100644 --- a/packages/vrender-components/src/label/base.ts +++ b/packages/vrender-components/src/label/base.ts @@ -31,7 +31,6 @@ import { isObject, pointInRect } from '@visactor/vutils'; -import { AbstractComponent } from '../core/base'; import type { PointLocationCfg } from '../core/type'; import { labelSmartInvert, contrastAccessibilityChecker, smartInvertStrategy } from '../util/label-smartInvert'; import { createTextGraphicByType, getMarksByName, getNoneGroupMarksByName, traverseGroup } from '../util'; @@ -787,19 +786,20 @@ export class LabelBase extends AnimateComponent { if (showLabelLine) { labelLine = this._createLabelLine(text as IText, relatedGraphic); } + const currentLabel = labelLine ? { text, labelLine } : { text }; if (syncState) { - this.updateStatesOfLabels([labelLine ? { text, labelLine } : { text }], relatedGraphic.currentStates ?? []); + this.updateStatesOfLabels([currentLabel], relatedGraphic.currentStates ?? []); } if (state === 'enter') { texts.push(text); - currentTextMap.set(textKey, labelLine ? { text, labelLine } : { text }); - this._addLabel({ text, labelLine }, texts, labelLines, index); + currentTextMap.set(textKey, currentLabel); + this._addLabel(currentLabel, texts, labelLines, index); } else if (state === 'update') { const prevLabel = prevTextMap.get(textKey); prevTextMap.delete(textKey); currentTextMap.set(textKey, prevLabel); - this._updateLabel(prevLabel, { text, labelLine }); + this._updateLabel(prevLabel, currentLabel); } }); @@ -843,15 +843,9 @@ export class LabelBase extends AnimateComponent { } protected _runUpdateAnimation(prevLabel: LabelContent, currentLabel: LabelContent) { - if (this._enableAnimation === false || !this._animationConfig.update) { - return; - } - const { text: prevText, labelLine: prevLabelLine } = prevLabel; const { text: curText, labelLine: curLabelLine } = currentLabel; - const { duration, easing } = this._animationConfig.update; - prevText.applyAnimationState( ['update'], [ @@ -859,8 +853,7 @@ export class LabelBase extends AnimateComponent { name: 'update', animation: { type: 'labelUpdate', - duration, - easing, + ...this._animationConfig.update, customParameters: { prevText, curText, @@ -904,7 +897,7 @@ export class LabelBase extends AnimateComponent { const { text: prevText, labelLine: prevLabelLine } = prevLabel; const { text: curText, labelLine: curLabelLine } = currentLabel; - if (this._enableAnimation === false || !this._animationConfig.update) { + if (this._enableAnimation === false || this._animationConfig.update === false) { prevLabel.text.setAttributes(curText.attribute as any); if (prevLabelLine && curLabelLine) { prevLabel.labelLine.setAttributes(curLabelLine.attribute); From 74aa5b17baf09aaaf982079e65727bdc929581e4 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Fri, 6 Jun 2025 10:56:58 +0800 Subject: [PATCH 149/179] fix: use removeAllChild to remove brush mask and add log notes --- .../fix-brush-problem_2025-05-31-14-30.json | 2 +- .../fix-brush-problem_2025-06-06-02-53.json | 10 ++++++++++ packages/vrender-components/src/brush/brush.ts | 10 +--------- 3 files changed, 12 insertions(+), 10 deletions(-) create mode 100644 common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-02-53.json diff --git a/common/changes/@visactor/vrender-components/fix-brush-problem_2025-05-31-14-30.json b/common/changes/@visactor/vrender-components/fix-brush-problem_2025-05-31-14-30.json index ef83ae9b5..ba64f9de6 100644 --- a/common/changes/@visactor/vrender-components/fix-brush-problem_2025-05-31-14-30.json +++ b/common/changes/@visactor/vrender-components/fix-brush-problem_2025-05-31-14-30.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@visactor/vrender-components", - "comment": "fix: brush active problem", + "comment": "fix: brush active problem. fix visactor/vchart#4017", "type": "none" } ], diff --git a/common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-02-53.json b/common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-02-53.json new file mode 100644 index 000000000..cce01dfea --- /dev/null +++ b/common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-02-53.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-components", + "comment": "fix: use removeAllChild to remove brush mask. fix visactor/vchart#4017", + "type": "none" + } + ], + "packageName": "@visactor/vrender-components" +} \ No newline at end of file diff --git a/packages/vrender-components/src/brush/brush.ts b/packages/vrender-components/src/brush/brush.ts index 27f7acf43..91be90d9e 100644 --- a/packages/vrender-components/src/brush/brush.ts +++ b/packages/vrender-components/src/brush/brush.ts @@ -68,18 +68,10 @@ export class Brush extends AbstractComponent> { * @description * 1. 判断状态: 如果在brushMask中,则属于移动状态; 否则属于绘制状态 *(移动状态和绘制状态互斥, 且移动状态考虑brushMoved配置, 如果在brush点内但brushMoved为false, 则走绘制状态, 而非两个状态都不响应, 此效果与echarts保持一致) - * 2. 判断坐标是否在有效交互范围内 * 2. 如果是移动状态: 标记移动状态 & 标记正在移动的mask & 初始化mask的dx和dy * 3. 如果是绘制状态: 标记绘制状态 & 标记正在绘制的mask & 清除之前的mask & 添加新的mask */ private _onBrushStart = (e: FederatedPointerEvent) => { - if (this._outOfInteractiveRange(e)) { - if (!this._isEmptyMask()) { - this._clearMask(); - this._dispatchBrushEvent(IOperateType.brushClear, e); - } - return; - } const { updateTrigger = DEFAULT_BRUSH_ATTRIBUTES.updateTrigger, endTrigger = DEFAULT_BRUSH_ATTRIBUTES.endTrigger, @@ -473,7 +465,7 @@ export class Brush extends AbstractComponent> { */ private _clearMask() { this._brushMaskAABBBoundsDict = {}; - this._container.incrementalClearChild(); + this._container.removeAllChild(); this._operatingMask = null; } From 04abb3a840a97125b08756c657c7ed7f77d9dd2f Mon Sep 17 00:00:00 2001 From: skie1997 Date: Fri, 6 Jun 2025 11:57:05 +0800 Subject: [PATCH 150/179] fix: datazoom text render error. fix visactor/vchart#4018 --- .../fix-brush-problem_2025-06-06-03-56.json | 10 ++++++++++ packages/vrender-components/src/data-zoom/data-zoom.ts | 7 +++++-- 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-03-56.json diff --git a/common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-03-56.json b/common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-03-56.json new file mode 100644 index 000000000..f33830f10 --- /dev/null +++ b/common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-03-56.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-components", + "comment": "fix: datazoom text render error. fix visactor/vchart#4018", + "type": "none" + } + ], + "packageName": "@visactor/vrender-components" +} \ No newline at end of file diff --git a/packages/vrender-components/src/data-zoom/data-zoom.ts b/packages/vrender-components/src/data-zoom/data-zoom.ts index eb97228b3..e227c3f2f 100644 --- a/packages/vrender-components/src/data-zoom/data-zoom.ts +++ b/packages/vrender-components/src/data-zoom/data-zoom.ts @@ -613,10 +613,13 @@ export class DataZoom extends AbstractComponent> { // 第三次绘制: 避免startText和endText重叠, 如果重叠了, 对startText做位置调整(考虑到调整的最小化,只单独调整startText而不调整endText) if (new Bounds().set(x1, y1, x2, y2).intersects(endTextBounds)) { const direction = this.attribute.orient === 'bottom' || this.attribute.orient === 'right' ? -1 : 1; + if (this._isHorizontal) { - this._startText.setAttribute('dy', startTextDy + direction * Math.abs(endTextBounds.y1 - endTextBounds.y2)); + const boundsYDiff = Math.abs(endTextBounds.y1 - endTextBounds.y2); // visible: false时, bounds可能是非法的 + this._startText.setAttribute('dy', startTextDy + direction * (Number.isFinite(boundsYDiff) ? boundsYDiff : 0)); } else { - this._startText.setAttribute('dx', startTextDx + direction * Math.abs(endTextBounds.x1 - endTextBounds.x2)); + const boundsXDiff = Math.abs(endTextBounds.x1 - endTextBounds.x2); // visible: false时, bounds可能是非法的 + this._startText.setAttribute('dx', startTextDx + direction * (Number.isFinite(boundsXDiff) ? boundsXDiff : 0)); } } else { if (this._isHorizontal) { From b0340b7739a815d67634d0d494deebf4c920d060 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Wed, 4 Jun 2025 12:05:28 +0800 Subject: [PATCH 151/179] fix: brush event pos problem when stage set scale --- .../fix-brush-problem_2025-06-06-02-36.json | 10 +++++++ .../vrender-components/src/brush/brush.ts | 7 ----- packages/vrender-components/src/core/base.ts | 12 +++++++- .../src/data-zoom/data-zoom.ts | 28 ++++++++----------- 4 files changed, 32 insertions(+), 25 deletions(-) create mode 100644 common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-02-36.json diff --git a/common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-02-36.json b/common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-02-36.json new file mode 100644 index 000000000..504ec2dcc --- /dev/null +++ b/common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-02-36.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-components", + "comment": "fix: brush event pos problem when stage scale", + "type": "none" + } + ], + "packageName": "@visactor/vrender-components" +} \ No newline at end of file diff --git a/packages/vrender-components/src/brush/brush.ts b/packages/vrender-components/src/brush/brush.ts index 91be90d9e..b71dee060 100644 --- a/packages/vrender-components/src/brush/brush.ts +++ b/packages/vrender-components/src/brush/brush.ts @@ -442,13 +442,6 @@ export class Brush extends AbstractComponent> { return false; } - /** - * 事件系统坐标转换为stage坐标 - */ - protected eventPosToStagePos(e: FederatedPointerEvent) { - return this.stage.eventPointTransform(e); - } - /** * 根据操作类型触发对应的事件 */ diff --git a/packages/vrender-components/src/core/base.ts b/packages/vrender-components/src/core/base.ts index 9a6908329..863a4e431 100644 --- a/packages/vrender-components/src/core/base.ts +++ b/packages/vrender-components/src/core/base.ts @@ -1,7 +1,7 @@ /** * @description 组件基类 */ -import type { IGroupGraphicAttribute, ISetAttributeContext } from '@visactor/vrender-core'; +import type { FederatedPointerEvent, IGroupGraphicAttribute, ISetAttributeContext } from '@visactor/vrender-core'; import { Group, CustomEvent } from '@visactor/vrender-core'; import type { Dict } from '@visactor/vutils'; import { merge, isFunction, isPlainObject, isNil } from '@visactor/vutils'; @@ -166,4 +166,14 @@ export abstract class AbstractComponent 内部坐标 + const stagePoints = this.stage?.eventPointTransform(e as any) ?? { x: 0, y: 0 }; // updateSpec过程中交互的话, stage可能为空 + // 2. 内部坐标 -> 组件坐标 (比如: 给layer设置 scale / x / y) + this.globalTransMatrix.transformPoint(stagePoints, result); + return result; + } } diff --git a/packages/vrender-components/src/data-zoom/data-zoom.ts b/packages/vrender-components/src/data-zoom/data-zoom.ts index e227c3f2f..7dbdb8134 100644 --- a/packages/vrender-components/src/data-zoom/data-zoom.ts +++ b/packages/vrender-components/src/data-zoom/data-zoom.ts @@ -232,12 +232,6 @@ export class DataZoom extends AbstractComponent> { shouldRender && this.setAttributes({ start, end }); } - /** 事件系统坐标转换为stage坐标 */ - protected eventPosToStagePos(e: FederatedPointerEvent) { - // updateSpec过程中交互的话, stage可能为空 - return this.stage?.eventPointTransform(e) ?? { x: 0, y: 0 }; - } - private _clearDragEvents() { const evtTarget = vglobal.env === 'browser' ? vglobal : this.stage; const triggers = getEndTriggersOfDrag(); @@ -739,7 +733,7 @@ export class DataZoom extends AbstractComponent> { height, cursor: brushSelect ? 'crosshair' : 'auto', ...backgroundStyle, - pickable: zoomLock ? false : backgroundStyle.pickable ?? true + pickable: zoomLock ? false : (backgroundStyle.pickable ?? true) }, 'rect' ) as IRect; @@ -763,7 +757,7 @@ export class DataZoom extends AbstractComponent> { height: height, cursor: brushSelect ? 'crosshair' : 'move', ...selectedBackgroundStyle, - pickable: zoomLock ? false : (selectedBackgroundChartStyle as any).pickable ?? true + pickable: zoomLock ? false : ((selectedBackgroundChartStyle as any).pickable ?? true) }, 'rect' ) as IRect; @@ -778,7 +772,7 @@ export class DataZoom extends AbstractComponent> { height: (end - start) * height, cursor: brushSelect ? 'crosshair' : 'move', ...selectedBackgroundStyle, - pickable: zoomLock ? false : selectedBackgroundStyle.pickable ?? true + pickable: zoomLock ? false : (selectedBackgroundStyle.pickable ?? true) }, 'rect' ) as IRect; @@ -800,7 +794,7 @@ export class DataZoom extends AbstractComponent> { width: (end - start) * width, height: middleHandlerBackgroundSize, ...middleHandlerStyle.background?.style, - pickable: zoomLock ? false : middleHandlerStyle.background?.style?.pickable ?? true + pickable: zoomLock ? false : (middleHandlerStyle.background?.style?.pickable ?? true) }, 'rect' ) as IRect; @@ -813,7 +807,7 @@ export class DataZoom extends AbstractComponent> { angle: 0, symbolType: middleHandlerStyle.icon?.symbolType ?? 'square', ...middleHandlerStyle.icon, - pickable: zoomLock ? false : middleHandlerStyle.icon.pickable ?? true + pickable: zoomLock ? false : (middleHandlerStyle.icon.pickable ?? true) }, 'symbol' ) as ISymbol; @@ -827,7 +821,7 @@ export class DataZoom extends AbstractComponent> { symbolType: startHandlerStyle.symbolType ?? 'square', ...(DEFAULT_HANDLER_ATTR_MAP.horizontal as any), ...startHandlerStyle, - pickable: zoomLock ? false : startHandlerStyle.pickable ?? true + pickable: zoomLock ? false : (startHandlerStyle.pickable ?? true) }, 'symbol' ) as ISymbol; @@ -840,7 +834,7 @@ export class DataZoom extends AbstractComponent> { symbolType: endHandlerStyle.symbolType ?? 'square', ...(DEFAULT_HANDLER_ATTR_MAP.horizontal as any), ...endHandlerStyle, - pickable: zoomLock ? false : endHandlerStyle.pickable ?? true + pickable: zoomLock ? false : (endHandlerStyle.pickable ?? true) }, 'symbol' ) as ISymbol; @@ -893,7 +887,7 @@ export class DataZoom extends AbstractComponent> { width: middleHandlerBackgroundSize, height: (end - start) * height, ...middleHandlerStyle.background?.style, - pickable: zoomLock ? false : middleHandlerStyle.background?.style?.pickable ?? true + pickable: zoomLock ? false : (middleHandlerStyle.background?.style?.pickable ?? true) }, 'rect' ) as IRect; @@ -910,7 +904,7 @@ export class DataZoom extends AbstractComponent> { symbolType: middleHandlerStyle.icon?.symbolType ?? 'square', strokeBoundsBuffer: 0, ...middleHandlerStyle.icon, - pickable: zoomLock ? false : middleHandlerStyle.icon?.pickable ?? true + pickable: zoomLock ? false : (middleHandlerStyle.icon?.pickable ?? true) }, 'symbol' ) as ISymbol; @@ -924,7 +918,7 @@ export class DataZoom extends AbstractComponent> { symbolType: startHandlerStyle.symbolType ?? 'square', ...(DEFAULT_HANDLER_ATTR_MAP.vertical as any), ...startHandlerStyle, - pickable: zoomLock ? false : startHandlerStyle.pickable ?? true + pickable: zoomLock ? false : (startHandlerStyle.pickable ?? true) }, 'symbol' ) as ISymbol; @@ -938,7 +932,7 @@ export class DataZoom extends AbstractComponent> { symbolType: endHandlerStyle.symbolType ?? 'square', ...(DEFAULT_HANDLER_ATTR_MAP.vertical as any), ...endHandlerStyle, - pickable: zoomLock ? false : endHandlerStyle.pickable ?? true + pickable: zoomLock ? false : (endHandlerStyle.pickable ?? true) }, 'symbol' ) as ISymbol; From 7c81c8bbcd286270c186bfe7c34221778901f062 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 6 Jun 2025 17:35:34 +0800 Subject: [PATCH 152/179] fix: fix issue with build --- .github/workflows/hotfix-release.yml | 3 +++ .github/workflows/release.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/hotfix-release.yml b/.github/workflows/hotfix-release.yml index d5c2ce28b..886f6608c 100644 --- a/.github/workflows/hotfix-release.yml +++ b/.github/workflows/hotfix-release.yml @@ -63,6 +63,9 @@ jobs: - name: Build vrender-kits run: node common/scripts/install-run-rush.js build --only @visactor/vrender-kits + - name: Build vrender-animate + run: node common/scripts/install-run-rush.js build --only @visactor/vrender-animate + - name: Build vrender run: node common/scripts/install-run-rush.js build --only @visactor/vrender diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 711899fce..496d6626e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,6 +60,9 @@ jobs: - name: Build vrender-kits run: node common/scripts/install-run-rush.js build --only @visactor/vrender-kits + - name: Build vrender-animate + run: node common/scripts/install-run-rush.js build --only @visactor/vrender-animate + - name: Build vrender run: node common/scripts/install-run-rush.js build --only @visactor/vrender From 2d415bd5a18bd1082c129734c1c21dd33d2d03c8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 6 Jun 2025 09:42:59 +0000 Subject: [PATCH 153/179] build: prelease version 1.0.0 --- ...delete-default-value_2025-05-21-09-51.json | 10 - ...t-remove-marker-type_2025-05-16-08-24.json | 10 - common/config/rush/pnpm-lock.yaml | 290 ++++++++++++++++-- common/config/rush/version-policies.json | 2 +- docs/package.json | 2 +- packages/react-vrender-utils/CHANGELOG.json | 6 + packages/react-vrender-utils/CHANGELOG.md | 7 +- packages/react-vrender-utils/package.json | 6 +- packages/react-vrender/CHANGELOG.json | 6 + packages/react-vrender/CHANGELOG.md | 7 +- packages/react-vrender/package.json | 4 +- packages/vrender-animate/CHANGELOG.json | 8 +- packages/vrender-animate/CHANGELOG.md | 9 +- packages/vrender-animate/package.json | 4 +- packages/vrender-components/CHANGELOG.json | 15 + packages/vrender-components/CHANGELOG.md | 10 +- packages/vrender-components/package.json | 8 +- packages/vrender-core/CHANGELOG.json | 6 + packages/vrender-core/CHANGELOG.md | 7 +- packages/vrender-core/package.json | 2 +- packages/vrender-kits/CHANGELOG.json | 6 + packages/vrender-kits/CHANGELOG.md | 7 +- packages/vrender-kits/package.json | 4 +- packages/vrender/CHANGELOG.json | 6 + packages/vrender/CHANGELOG.md | 7 +- packages/vrender/package.json | 8 +- tools/bugserver-trigger/package.json | 8 +- 27 files changed, 387 insertions(+), 78 deletions(-) delete mode 100644 common/changes/@visactor/vrender-components/chore-delete-default-value_2025-05-21-09-51.json delete mode 100644 common/changes/@visactor/vrender-components/feat-remove-marker-type_2025-05-16-08-24.json diff --git a/common/changes/@visactor/vrender-components/chore-delete-default-value_2025-05-21-09-51.json b/common/changes/@visactor/vrender-components/chore-delete-default-value_2025-05-21-09-51.json deleted file mode 100644 index 0decaf859..000000000 --- a/common/changes/@visactor/vrender-components/chore-delete-default-value_2025-05-21-09-51.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender-components", - "comment": "fix: delete-default-value", - "type": "none" - } - ], - "packageName": "@visactor/vrender-components" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-components/feat-remove-marker-type_2025-05-16-08-24.json b/common/changes/@visactor/vrender-components/feat-remove-marker-type_2025-05-16-08-24.json deleted file mode 100644 index c0dea2719..000000000 --- a/common/changes/@visactor/vrender-components/feat-remove-marker-type_2025-05-16-08-24.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender-components", - "comment": "feat: remove marker type. close @VisActor/VChart#3782", - "type": "none" - } - ], - "packageName": "@visactor/vrender-components" -} \ No newline at end of file diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 5de8acfe8..3cbe06d17 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: specifier: ~0.5.7 version: 0.5.7 '@visactor/vrender': - specifier: workspace:0.22.11 + specifier: workspace:1.0.0 version: link:../packages/vrender '@visactor/vutils': specifier: 1.0.4 @@ -95,7 +95,7 @@ importers: ../../packages/react-vrender: dependencies: '@visactor/vrender': - specifier: workspace:0.22.11 + specifier: workspace:1.0.0 version: link:../vrender '@visactor/vutils': specifier: 1.0.4 @@ -153,10 +153,10 @@ importers: ../../packages/react-vrender-utils: dependencies: '@visactor/react-vrender': - specifier: workspace:0.22.11 + specifier: workspace:1.0.0 version: link:../react-vrender '@visactor/vrender': - specifier: workspace:0.22.11 + specifier: workspace:1.0.0 version: link:../vrender '@visactor/vutils': specifier: 1.0.4 @@ -211,13 +211,13 @@ importers: ../../packages/vrender: dependencies: '@visactor/vrender-animate': - specifier: workspace:0.22.11 + specifier: workspace:1.0.0 version: link:../vrender-animate '@visactor/vrender-core': - specifier: workspace:0.22.11 + specifier: workspace:1.0.0 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:0.22.11 + specifier: workspace:1.0.0 version: link:../vrender-kits devDependencies: '@internal/bundler': @@ -261,7 +261,7 @@ importers: version: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) jest-electron: specifier: ^0.1.12 - version: 0.1.12(jest@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5))) + version: 0.1.12(jest@26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5))) jest-extended: specifier: ^1.2.1 version: 1.2.1 @@ -273,7 +273,7 @@ importers: version: 18.3.1(react@18.3.1) ts-jest: specifier: ^26.0.0 - version: 26.5.6(jest@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)))(typescript@4.9.5) + version: 26.5.6(jest@26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)))(typescript@4.9.5) typescript: specifier: 4.9.5 version: 4.9.5 @@ -284,7 +284,7 @@ importers: ../../packages/vrender-animate: dependencies: '@visactor/vrender-core': - specifier: workspace:0.22.11 + specifier: workspace:1.0.0 version: link:../vrender-core '@visactor/vutils': specifier: 1.0.4 @@ -342,13 +342,13 @@ importers: ../../packages/vrender-components: dependencies: '@visactor/vrender-animate': - specifier: workspace:0.22.11 + specifier: workspace:1.0.0 version: link:../vrender-animate '@visactor/vrender-core': - specifier: workspace:0.22.11 + specifier: workspace:1.0.0 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:0.22.11 + specifier: workspace:1.0.0 version: link:../vrender-kits '@visactor/vscale': specifier: 1.0.4 @@ -380,7 +380,7 @@ importers: version: 8.18.0 jest: specifier: ^26.0.0 - version: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + version: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) jest-electron: specifier: ^0.1.12 version: 0.1.12(jest@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5))) @@ -438,7 +438,7 @@ importers: version: 8.18.0 jest: specifier: ^26.0.0 - version: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + version: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) jest-electron: specifier: ^0.1.12 version: 0.1.12(jest@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5))) @@ -467,7 +467,7 @@ importers: specifier: 2.4.1 version: 2.4.1 '@visactor/vrender-core': - specifier: workspace:0.22.11 + specifier: workspace:1.0.0 version: link:../vrender-core '@visactor/vutils': specifier: 1.0.4 @@ -573,7 +573,7 @@ importers: version: 26.0.24 jest: specifier: ^26.0.0 - version: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + version: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) typescript: specifier: 4.9.5 version: 4.9.5 @@ -583,16 +583,16 @@ importers: ../../tools/bugserver-trigger: dependencies: '@visactor/vrender': - specifier: workspace:0.22.11 + specifier: workspace:1.0.0 version: link:../../packages/vrender '@visactor/vrender-components': - specifier: workspace:0.22.11 + specifier: workspace:1.0.0 version: link:../../packages/vrender-components '@visactor/vrender-core': - specifier: workspace:0.22.11 + specifier: workspace:1.0.0 version: link:../../packages/vrender-core '@visactor/vrender-kits': - specifier: workspace:0.22.11 + specifier: workspace:1.0.0 version: link:../../packages/vrender-kits devDependencies: '@internal/bundler': @@ -7935,6 +7935,43 @@ snapshots: - ts-node - utf-8-validate + '@jest/core@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5))': + dependencies: + '@jest/console': 26.6.2 + '@jest/reporters': 26.6.2 + '@jest/test-result': 26.6.2 + '@jest/transform': 26.6.2 + '@jest/types': 26.6.2 + '@types/node': 22.13.17 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 26.6.2 + jest-config: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-haste-map: 26.6.2 + jest-message-util: 26.6.2 + jest-regex-util: 26.0.0 + jest-resolve: 26.6.2 + jest-resolve-dependencies: 26.6.3 + jest-runner: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-snapshot: 26.6.2 + jest-util: 26.6.2 + jest-validate: 26.6.2 + jest-watcher: 26.6.2 + micromatch: 4.0.8 + p-each-series: 2.2.0 + rimraf: 3.0.2 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + '@jest/environment@24.9.0': dependencies: '@jest/fake-timers': 24.9.0 @@ -8051,6 +8088,20 @@ snapshots: - ts-node - utf-8-validate + '@jest/test-sequencer@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5))': + dependencies: + '@jest/test-result': 26.6.2 + graceful-fs: 4.2.11 + jest-haste-map: 26.6.2 + jest-runner: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + '@jest/transform@24.9.0': dependencies: '@babel/core': 7.20.12 @@ -11399,6 +11450,28 @@ snapshots: - ts-node - utf-8-validate + jest-cli@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)): + dependencies: + '@jest/core': 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + '@jest/test-result': 26.6.2 + '@jest/types': 26.6.2 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + import-local: 3.2.0 + is-ci: 2.0.0 + jest-config: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-util: 26.6.2 + jest-validate: 26.6.2 + prompts: 2.4.2 + yargs: 15.4.1 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + jest-config@24.9.0: dependencies: '@babel/core': 7.20.12 @@ -11434,7 +11507,35 @@ snapshots: jest-environment-jsdom: 26.6.2(canvas@2.11.2) jest-environment-node: 26.6.2 jest-get-type: 26.3.0 - jest-jasmine2: 26.6.3 + jest-jasmine2: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-regex-util: 26.0.0 + jest-resolve: 26.6.2 + jest-util: 26.6.2 + jest-validate: 26.6.2 + micromatch: 4.0.8 + pretty-format: 26.6.2 + optionalDependencies: + ts-node: 10.9.0(@types/node@22.13.17)(typescript@4.9.5) + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - utf-8-validate + + jest-config@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)): + dependencies: + '@babel/core': 7.20.12 + '@jest/test-sequencer': 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + '@jest/types': 26.6.2 + babel-jest: 26.6.3(@babel/core@7.20.12) + chalk: 4.1.2 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-environment-jsdom: 26.6.2(canvas@2.11.2) + jest-environment-node: 26.6.2 + jest-get-type: 26.3.0 + jest-jasmine2: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) jest-regex-util: 26.0.0 jest-resolve: 26.6.2 jest-util: 26.6.2 @@ -11494,7 +11595,7 @@ snapshots: jest-util: 26.6.2 pretty-format: 26.6.2 - jest-electron@0.1.12(jest@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5))): + jest-electron@0.1.12(jest@26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5))): dependencies: electron: 11.5.0 jest: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) @@ -11510,6 +11611,22 @@ snapshots: transitivePeerDependencies: - supports-color + jest-electron@0.1.12(jest@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5))): + dependencies: + electron: 11.5.0 + jest: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-haste-map: 24.9.0 + jest-message-util: 24.9.0 + jest-mock: 24.9.0 + jest-resolve: 24.9.0 + jest-runner: 24.9.0 + jest-runtime: 24.9.0 + jest-util: 24.9.0 + throat: 5.0.0 + tslib: 1.14.1 + transitivePeerDependencies: + - supports-color + jest-environment-jsdom@24.9.0: dependencies: '@jest/environment': 24.9.0 @@ -11623,7 +11740,7 @@ snapshots: transitivePeerDependencies: - supports-color - jest-jasmine2@26.6.3: + jest-jasmine2@26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)): dependencies: '@babel/traverse': 7.27.0 '@jest/environment': 26.6.2 @@ -11644,7 +11761,38 @@ snapshots: pretty-format: 26.6.2 throat: 5.0.0 transitivePeerDependencies: + - bufferutil + - canvas - supports-color + - ts-node + - utf-8-validate + + jest-jasmine2@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)): + dependencies: + '@babel/traverse': 7.27.0 + '@jest/environment': 26.6.2 + '@jest/source-map': 26.6.2 + '@jest/test-result': 26.6.2 + '@jest/types': 26.6.2 + '@types/node': 22.13.17 + chalk: 4.1.2 + co: 4.6.0 + expect: 26.6.2 + is-generator-fn: 2.1.0 + jest-each: 26.6.2 + jest-matcher-utils: 26.6.2 + jest-message-util: 26.6.2 + jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-snapshot: 26.6.2 + jest-util: 26.6.2 + pretty-format: 26.6.2 + throat: 5.0.0 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate jest-leak-detector@24.9.0: dependencies: @@ -11799,6 +11947,35 @@ snapshots: - ts-node - utf-8-validate + jest-runner@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)): + dependencies: + '@jest/console': 26.6.2 + '@jest/environment': 26.6.2 + '@jest/test-result': 26.6.2 + '@jest/types': 26.6.2 + '@types/node': 22.13.17 + chalk: 4.1.2 + emittery: 0.7.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-docblock: 26.0.0 + jest-haste-map: 26.6.2 + jest-leak-detector: 26.6.2 + jest-message-util: 26.6.2 + jest-resolve: 26.6.2 + jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-util: 26.6.2 + jest-worker: 26.6.2 + source-map-support: 0.5.21 + throat: 5.0.0 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + jest-runtime@24.9.0: dependencies: '@jest/console': 24.9.0 @@ -11863,6 +12040,42 @@ snapshots: - ts-node - utf-8-validate + jest-runtime@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)): + dependencies: + '@jest/console': 26.6.2 + '@jest/environment': 26.6.2 + '@jest/fake-timers': 26.6.2 + '@jest/globals': 26.6.2 + '@jest/source-map': 26.6.2 + '@jest/test-result': 26.6.2 + '@jest/transform': 26.6.2 + '@jest/types': 26.6.2 + '@types/yargs': 15.0.19 + chalk: 4.1.2 + cjs-module-lexer: 0.6.0 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-config: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-haste-map: 26.6.2 + jest-message-util: 26.6.2 + jest-mock: 26.6.2 + jest-regex-util: 26.0.0 + jest-resolve: 26.6.2 + jest-snapshot: 26.6.2 + jest-util: 26.6.2 + jest-validate: 26.6.2 + slash: 3.0.0 + strip-bom: 4.0.0 + yargs: 15.4.1 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + jest-serializer@24.9.0: {} jest-serializer@26.6.2: @@ -11980,6 +12193,18 @@ snapshots: - ts-node - utf-8-validate + jest@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)): + dependencies: + '@jest/core': 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + import-local: 3.2.0 + jest-cli: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + js-binary-schema-parser@2.0.3: {} js-string-escape@1.0.1: {} @@ -13920,7 +14145,7 @@ snapshots: dependencies: punycode: 2.3.1 - ts-jest@26.5.6(jest@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)))(typescript@4.9.5): + ts-jest@26.5.6(jest@26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)))(typescript@4.9.5): dependencies: bs-logger: 0.2.6 buffer-from: 1.1.2 @@ -13935,6 +14160,21 @@ snapshots: typescript: 4.9.5 yargs-parser: 20.2.9 + ts-jest@26.5.6(jest@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)))(typescript@4.9.5): + dependencies: + bs-logger: 0.2.6 + buffer-from: 1.1.2 + fast-json-stable-stringify: 2.1.0 + jest: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-util: 26.6.2 + json5: 2.2.3 + lodash: 4.17.21 + make-error: 1.3.6 + mkdirp: 1.0.4 + semver: 7.3.4 + typescript: 4.9.5 + yargs-parser: 20.2.9 + ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5): dependencies: '@cspotcode/source-map-support': 0.8.1 diff --git a/common/config/rush/version-policies.json b/common/config/rush/version-policies.json index 7f86412ba..73a89a49f 100644 --- a/common/config/rush/version-policies.json +++ b/common/config/rush/version-policies.json @@ -1 +1 @@ -[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"0.22.11","nextBump":"patch"}] +[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"1.0.0","nextBump":"major"}] diff --git a/docs/package.json b/docs/package.json index b677e3ac7..75a327bfa 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,7 +13,7 @@ "@visactor/vchart": "1.3.0", "@visactor/vutils": "1.0.4", "@visactor/vgrammar": "~0.5.7", - "@visactor/vrender": "workspace:0.22.11", + "@visactor/vrender": "workspace:1.0.0", "markdown-it": "^13.0.0", "highlight.js": "^11.8.0", "axios": "^1.4.0", diff --git a/packages/react-vrender-utils/CHANGELOG.json b/packages/react-vrender-utils/CHANGELOG.json index 8b4603931..6c45ad5be 100644 --- a/packages/react-vrender-utils/CHANGELOG.json +++ b/packages/react-vrender-utils/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/react-vrender-utils", "entries": [ + { + "version": "1.0.0", + "tag": "@visactor/react-vrender-utils_v1.0.0", + "date": "Fri, 06 Jun 2025 09:38:19 GMT", + "comments": {} + }, { "version": "0.22.11", "tag": "@visactor/react-vrender-utils_v0.22.11", diff --git a/packages/react-vrender-utils/CHANGELOG.md b/packages/react-vrender-utils/CHANGELOG.md index 5c151bb69..12ac320b1 100644 --- a/packages/react-vrender-utils/CHANGELOG.md +++ b/packages/react-vrender-utils/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/react-vrender-utils -This log was last generated on Mon, 28 Apr 2025 09:08:54 GMT and should not be manually modified. +This log was last generated on Fri, 06 Jun 2025 09:38:19 GMT and should not be manually modified. + +## 1.0.0 +Fri, 06 Jun 2025 09:38:19 GMT + +_Version update only_ ## 0.22.11 Mon, 28 Apr 2025 09:08:54 GMT diff --git a/packages/react-vrender-utils/package.json b/packages/react-vrender-utils/package.json index 86a45509f..38f22b830 100644 --- a/packages/react-vrender-utils/package.json +++ b/packages/react-vrender-utils/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender-utils", - "version": "0.22.11", + "version": "1.0.0", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -24,8 +24,8 @@ "react-dom": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:0.22.11", - "@visactor/react-vrender": "workspace:0.22.11", + "@visactor/vrender": "workspace:1.0.0", + "@visactor/react-vrender": "workspace:1.0.0", "@visactor/vutils": "1.0.4", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/react-vrender/CHANGELOG.json b/packages/react-vrender/CHANGELOG.json index 3b4810a84..d35e79b99 100644 --- a/packages/react-vrender/CHANGELOG.json +++ b/packages/react-vrender/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/react-vrender", "entries": [ + { + "version": "1.0.0", + "tag": "@visactor/react-vrender_v1.0.0", + "date": "Fri, 06 Jun 2025 09:38:19 GMT", + "comments": {} + }, { "version": "0.22.11", "tag": "@visactor/react-vrender_v0.22.11", diff --git a/packages/react-vrender/CHANGELOG.md b/packages/react-vrender/CHANGELOG.md index 1f4f6a97e..2cc3bb0c3 100644 --- a/packages/react-vrender/CHANGELOG.md +++ b/packages/react-vrender/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/react-vrender -This log was last generated on Mon, 28 Apr 2025 09:08:54 GMT and should not be manually modified. +This log was last generated on Fri, 06 Jun 2025 09:38:19 GMT and should not be manually modified. + +## 1.0.0 +Fri, 06 Jun 2025 09:38:19 GMT + +_Version update only_ ## 0.22.11 Mon, 28 Apr 2025 09:08:54 GMT diff --git a/packages/react-vrender/package.json b/packages/react-vrender/package.json index c128184e1..0d46c2853 100644 --- a/packages/react-vrender/package.json +++ b/packages/react-vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender", - "version": "0.22.11", + "version": "1.0.0", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -23,7 +23,7 @@ "react": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:0.22.11", + "@visactor/vrender": "workspace:1.0.0", "@visactor/vutils": "1.0.4", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/vrender-animate/CHANGELOG.json b/packages/vrender-animate/CHANGELOG.json index 78d2e60be..a40ed80c8 100644 --- a/packages/vrender-animate/CHANGELOG.json +++ b/packages/vrender-animate/CHANGELOG.json @@ -1,6 +1,12 @@ { - "name": "@visactor/vrender-kits", + "name": "@visactor/vrender-animate", "entries": [ + { + "version": "1.0.0", + "tag": "@visactor/vrender-animate_v1.0.0", + "date": "Fri, 06 Jun 2025 09:38:19 GMT", + "comments": {} + }, { "version": "0.22.1", "tag": "@visactor/vrender-kits_v0.22.1", diff --git a/packages/vrender-animate/CHANGELOG.md b/packages/vrender-animate/CHANGELOG.md index bf342f138..2ec73f229 100644 --- a/packages/vrender-animate/CHANGELOG.md +++ b/packages/vrender-animate/CHANGELOG.md @@ -1,6 +1,11 @@ -# Change Log - @visactor/vrender-kits +# Change Log - @visactor/vrender-animate -This log was last generated on Tue, 18 Feb 2025 10:14:45 GMT and should not be manually modified. +This log was last generated on Fri, 06 Jun 2025 09:38:19 GMT and should not be manually modified. + +## 1.0.0 +Fri, 06 Jun 2025 09:38:19 GMT + +_Version update only_ ## 0.22.1 Tue, 18 Feb 2025 10:14:45 GMT diff --git a/packages/vrender-animate/package.json b/packages/vrender-animate/package.json index b9adb2718..5780f08a3 100644 --- a/packages/vrender-animate/package.json +++ b/packages/vrender-animate/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-animate", - "version": "0.22.11", + "version": "1.0.0", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -21,7 +21,7 @@ }, "dependencies": { "@visactor/vutils": "1.0.4", - "@visactor/vrender-core": "workspace:0.22.11" + "@visactor/vrender-core": "workspace:1.0.0" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-components/CHANGELOG.json b/packages/vrender-components/CHANGELOG.json index 45bbae3a4..7d6f19b80 100644 --- a/packages/vrender-components/CHANGELOG.json +++ b/packages/vrender-components/CHANGELOG.json @@ -1,6 +1,21 @@ { "name": "@visactor/vrender-components", "entries": [ + { + "version": "1.0.0", + "tag": "@visactor/vrender-components_v1.0.0", + "date": "Fri, 06 Jun 2025 09:38:19 GMT", + "comments": { + "none": [ + { + "comment": "fix: delete-default-value" + }, + { + "comment": "feat: remove marker type. close @VisActor/VChart#3782" + } + ] + } + }, { "version": "0.22.11", "tag": "@visactor/vrender-components_v0.22.11", diff --git a/packages/vrender-components/CHANGELOG.md b/packages/vrender-components/CHANGELOG.md index 28a96e3ac..4a86b028a 100644 --- a/packages/vrender-components/CHANGELOG.md +++ b/packages/vrender-components/CHANGELOG.md @@ -1,6 +1,14 @@ # Change Log - @visactor/vrender-components -This log was last generated on Mon, 28 Apr 2025 09:08:54 GMT and should not be manually modified. +This log was last generated on Fri, 06 Jun 2025 09:38:19 GMT and should not be manually modified. + +## 1.0.0 +Fri, 06 Jun 2025 09:38:19 GMT + +### Updates + +- fix: delete-default-value +- feat: remove marker type. close @VisActor/VChart#3782 ## 0.22.11 Mon, 28 Apr 2025 09:08:54 GMT diff --git a/packages/vrender-components/package.json b/packages/vrender-components/package.json index 547d6ee06..e4dce8656 100644 --- a/packages/vrender-components/package.json +++ b/packages/vrender-components/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-components", - "version": "0.22.11", + "version": "1.0.0", "description": "components library for dp visualization", "sideEffects": false, "main": "cjs/index.js", @@ -27,9 +27,9 @@ "dependencies": { "@visactor/vutils": "1.0.4", "@visactor/vscale": "1.0.4", - "@visactor/vrender-core": "workspace:0.22.11", - "@visactor/vrender-kits": "workspace:0.22.11", - "@visactor/vrender-animate": "workspace:0.22.11" + "@visactor/vrender-core": "workspace:1.0.0", + "@visactor/vrender-kits": "workspace:1.0.0", + "@visactor/vrender-animate": "workspace:1.0.0" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-core/CHANGELOG.json b/packages/vrender-core/CHANGELOG.json index 5f44b43e4..89c55df28 100644 --- a/packages/vrender-core/CHANGELOG.json +++ b/packages/vrender-core/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender-core", "entries": [ + { + "version": "1.0.0", + "tag": "@visactor/vrender-core_v1.0.0", + "date": "Fri, 06 Jun 2025 09:38:19 GMT", + "comments": {} + }, { "version": "0.22.11", "tag": "@visactor/vrender-core_v0.22.11", diff --git a/packages/vrender-core/CHANGELOG.md b/packages/vrender-core/CHANGELOG.md index c154dae4b..7c905a707 100644 --- a/packages/vrender-core/CHANGELOG.md +++ b/packages/vrender-core/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender-core -This log was last generated on Mon, 28 Apr 2025 09:08:54 GMT and should not be manually modified. +This log was last generated on Fri, 06 Jun 2025 09:38:19 GMT and should not be manually modified. + +## 1.0.0 +Fri, 06 Jun 2025 09:38:19 GMT + +_Version update only_ ## 0.22.11 Mon, 28 Apr 2025 09:08:54 GMT diff --git a/packages/vrender-core/package.json b/packages/vrender-core/package.json index 9f7352128..afa409cc2 100644 --- a/packages/vrender-core/package.json +++ b/packages/vrender-core/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-core", - "version": "0.22.11", + "version": "1.0.0", "description": "", "sideEffects": [ "./src/modules.ts", diff --git a/packages/vrender-kits/CHANGELOG.json b/packages/vrender-kits/CHANGELOG.json index 2ec334646..a27347d4b 100644 --- a/packages/vrender-kits/CHANGELOG.json +++ b/packages/vrender-kits/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender-kits", "entries": [ + { + "version": "1.0.0", + "tag": "@visactor/vrender-kits_v1.0.0", + "date": "Fri, 06 Jun 2025 09:38:19 GMT", + "comments": {} + }, { "version": "0.22.11", "tag": "@visactor/vrender-kits_v0.22.11", diff --git a/packages/vrender-kits/CHANGELOG.md b/packages/vrender-kits/CHANGELOG.md index 1867599ee..3a556f0e2 100644 --- a/packages/vrender-kits/CHANGELOG.md +++ b/packages/vrender-kits/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender-kits -This log was last generated on Mon, 28 Apr 2025 09:08:54 GMT and should not be manually modified. +This log was last generated on Fri, 06 Jun 2025 09:38:19 GMT and should not be manually modified. + +## 1.0.0 +Fri, 06 Jun 2025 09:38:19 GMT + +_Version update only_ ## 0.22.11 Mon, 28 Apr 2025 09:08:54 GMT diff --git a/packages/vrender-kits/package.json b/packages/vrender-kits/package.json index b68c959a2..cd93401bf 100644 --- a/packages/vrender-kits/package.json +++ b/packages/vrender-kits/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-kits", - "version": "0.22.11", + "version": "1.0.0", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -21,7 +21,7 @@ }, "dependencies": { "@visactor/vutils": "1.0.4", - "@visactor/vrender-core": "workspace:0.22.11", + "@visactor/vrender-core": "workspace:1.0.0", "@resvg/resvg-js": "2.4.1", "roughjs": "4.5.2", "gifuct-js": "2.1.2", diff --git a/packages/vrender/CHANGELOG.json b/packages/vrender/CHANGELOG.json index 1eccfc4ff..1d4fab82f 100644 --- a/packages/vrender/CHANGELOG.json +++ b/packages/vrender/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender", "entries": [ + { + "version": "1.0.0", + "tag": "@visactor/vrender_v1.0.0", + "date": "Fri, 06 Jun 2025 09:38:19 GMT", + "comments": {} + }, { "version": "0.22.11", "tag": "@visactor/vrender_v0.22.11", diff --git a/packages/vrender/CHANGELOG.md b/packages/vrender/CHANGELOG.md index 195629e90..579dfe133 100644 --- a/packages/vrender/CHANGELOG.md +++ b/packages/vrender/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender -This log was last generated on Mon, 28 Apr 2025 09:08:54 GMT and should not be manually modified. +This log was last generated on Fri, 06 Jun 2025 09:38:19 GMT and should not be manually modified. + +## 1.0.0 +Fri, 06 Jun 2025 09:38:19 GMT + +_Version update only_ ## 0.22.11 Mon, 28 Apr 2025 09:08:54 GMT diff --git a/packages/vrender/package.json b/packages/vrender/package.json index 9a35901b6..3bf69c048 100644 --- a/packages/vrender/package.json +++ b/packages/vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender", - "version": "0.22.11", + "version": "1.0.0", "description": "", "sideEffects": true, "main": "cjs/index.js", @@ -24,9 +24,9 @@ "test-watch": "cross-env DEBUG_MODE=1 jest --watch" }, "dependencies": { - "@visactor/vrender-core": "workspace:0.22.11", - "@visactor/vrender-kits": "workspace:0.22.11", - "@visactor/vrender-animate": "workspace:0.22.11" + "@visactor/vrender-core": "workspace:1.0.0", + "@visactor/vrender-kits": "workspace:1.0.0", + "@visactor/vrender-animate": "workspace:1.0.0" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/tools/bugserver-trigger/package.json b/tools/bugserver-trigger/package.json index 11acabaac..67485981f 100644 --- a/tools/bugserver-trigger/package.json +++ b/tools/bugserver-trigger/package.json @@ -8,10 +8,10 @@ "ci": "ts-node --transpileOnly --skipProject ./scripts/trigger-test.ts" }, "dependencies": { - "@visactor/vrender": "workspace:0.22.11", - "@visactor/vrender-core": "workspace:0.22.11", - "@visactor/vrender-kits": "workspace:0.22.11", - "@visactor/vrender-components": "workspace:0.22.11" + "@visactor/vrender": "workspace:1.0.0", + "@visactor/vrender-core": "workspace:1.0.0", + "@visactor/vrender-kits": "workspace:1.0.0", + "@visactor/vrender-components": "workspace:1.0.0" }, "devDependencies": { "@rushstack/eslint-patch": "~1.1.4", From 57832f2e3edfd58c93b7b18134ef6789a86713ef Mon Sep 17 00:00:00 2001 From: xile611 Date: Fri, 6 Jun 2025 11:10:29 +0800 Subject: [PATCH 154/179] fix: disable sampling when label is rich text --- .../src/axis/tick-data/continuous.ts | 79 +++++++------ .../src/axis/tick-data/discrete/linear.ts | 109 ++++++++++-------- .../src/axis/tick-data/util.ts | 16 ++- 3 files changed, 113 insertions(+), 91 deletions(-) diff --git a/packages/vrender-components/src/axis/tick-data/continuous.ts b/packages/vrender-components/src/axis/tick-data/continuous.ts index 777203125..bd0683ead 100644 --- a/packages/vrender-components/src/axis/tick-data/continuous.ts +++ b/packages/vrender-components/src/axis/tick-data/continuous.ts @@ -166,7 +166,7 @@ export const continuousTicks = (scale: ContinuousScale, op: ITickDataOpt): ITick samplingScaleTicks.push(tick); } }); - items = getCartesianLabelBounds(scale, samplingScaleTicks, op as ICartesianTickDataOpt).map( + items = getCartesianLabelBounds(scale, samplingScaleTicks, op as ICartesianTickDataOpt)?.map( (bounds, i) => ({ AABBBounds: bounds, @@ -174,7 +174,7 @@ export const continuousTicks = (scale: ContinuousScale, op: ITickDataOpt): ITick } as ILabelItem) ); } else { - items = getCartesianLabelBounds(scale, scaleTicks, op as ICartesianTickDataOpt).map( + items = getCartesianLabelBounds(scale, scaleTicks, op as ICartesianTickDataOpt)?.map( (bounds, i) => ({ AABBBounds: bounds, @@ -182,49 +182,52 @@ export const continuousTicks = (scale: ContinuousScale, op: ITickDataOpt): ITick } as ILabelItem) ); } - const firstSourceItem = items[0]; - const lastSourceItem = last(items); - const samplingMethod = breakData && breakData() ? methods.greedy : methods.parity; // 由于轴截断后刻度会存在不均匀的情况,所以不能使用 parity 算法 - while (items.length >= 3 && hasOverlap(items as any, labelGap)) { - items = samplingMethod(items, labelGap); - } - - const checkFirst = op.labelFirstVisible; - let checkLast = op.labelLastVisible; // 这里和 auto-hide 里的逻辑有差异,不根据 length 自动强制显示最后一个(会引起 vtable 较多 badcase)。 + if (items) { + const firstSourceItem = items[0]; + const lastSourceItem = last(items); - if (intersect(firstSourceItem as any, lastSourceItem as any, labelGap)) { - if (items.includes(lastSourceItem) && items.length > 1 && checkFirst && checkLast) { - items.splice(items.indexOf(lastSourceItem), 1); - checkLast = false; + const samplingMethod = breakData && breakData() ? methods.greedy : methods.parity; // 由于轴截断后刻度会存在不均匀的情况,所以不能使用 parity 算法 + while (items.length >= 3 && hasOverlap(items as any, labelGap)) { + items = samplingMethod(items, labelGap); } - } - forceItemVisible(firstSourceItem, items, checkFirst, (item: ILabelItem) => - intersect(item as any, firstSourceItem as any, labelGap) - ); - forceItemVisible( - lastSourceItem, - items, - checkLast, - (item: ILabelItem) => - intersect(item as any, lastSourceItem as any, labelGap) || - (checkFirst && item !== firstSourceItem ? intersect(item as any, firstSourceItem as any, labelGap) : false), - true - ); - - const ticks = items.map(item => item.value); - - if (ticks.length < 3 && labelFlush) { - if (ticks.length > 1) { - ticks.pop(); + const checkFirst = op.labelFirstVisible; + let checkLast = op.labelLastVisible; // 这里和 auto-hide 里的逻辑有差异,不根据 length 自动强制显示最后一个(会引起 vtable 较多 badcase)。 + + if (intersect(firstSourceItem as any, lastSourceItem as any, labelGap)) { + if (items.includes(lastSourceItem) && items.length > 1 && checkFirst && checkLast) { + items.splice(items.indexOf(lastSourceItem), 1); + checkLast = false; + } } - if (last(ticks) !== last(scaleTicks)) { - ticks.push(last(scaleTicks)); + + forceItemVisible(firstSourceItem, items, checkFirst, (item: ILabelItem) => + intersect(item as any, firstSourceItem as any, labelGap) + ); + forceItemVisible( + lastSourceItem, + items, + checkLast, + (item: ILabelItem) => + intersect(item as any, lastSourceItem as any, labelGap) || + (checkFirst && item !== firstSourceItem ? intersect(item as any, firstSourceItem as any, labelGap) : false), + true + ); + + const ticks = items.map(item => item.value); + + if (ticks.length < 3 && labelFlush) { + if (ticks.length > 1) { + ticks.pop(); + } + if (last(ticks) !== last(scaleTicks)) { + ticks.push(last(scaleTicks)); + } } - } - scaleTicks = ticks; + scaleTicks = ticks; + } } } return convertDomainToTickData(scaleTicks); diff --git a/packages/vrender-components/src/axis/tick-data/discrete/linear.ts b/packages/vrender-components/src/axis/tick-data/discrete/linear.ts index 7b73b87f8..d32ac2a8c 100644 --- a/packages/vrender-components/src/axis/tick-data/discrete/linear.ts +++ b/packages/vrender-components/src/axis/tick-data/discrete/linear.ts @@ -1,5 +1,5 @@ import type { BandScale, IBaseScale } from '@visactor/vscale'; -import { isFunction, isValid, maxInArray, minInArray, binaryFuzzySearchInNumberRange } from '@visactor/vutils'; +import { isFunction, isValid, maxInArray, minInArray, binaryFuzzySearchInNumberRange, isNil } from '@visactor/vutils'; import type { ICartesianTickDataOpt, ITickData } from '../../type'; import { convertDomainToTickData, getCartesianLabelBounds, isAxisHorizontal } from '../util'; @@ -13,12 +13,15 @@ const getOneDimensionalLabelBounds = ( isHorizontal: boolean ): OneDimensionalBounds[] => { const labelBoundsList = getCartesianLabelBounds(scale, domain, op); - return labelBoundsList.map(bounds => { - if (isHorizontal) { - return [bounds.x1, bounds.x2, bounds.width()]; - } - return [bounds.y1, bounds.y2, bounds.height()]; - }); + return ( + labelBoundsList && + labelBoundsList.map(bounds => { + if (isHorizontal) { + return [bounds.x1, bounds.x2, bounds.width()]; + } + return [bounds.y1, bounds.y2, bounds.height()]; + }) + ); }; /** 判断两个 bounds 是否有重叠情况 */ @@ -79,63 +82,71 @@ export const linearDiscreteTicks = (scale: BandScale, op: ICartesianTickDataOpt) const rangeEnd = maxInArray(range); if (domain.length <= rangeSize / fontSize) { - const incrementUnit = (rangeEnd - rangeStart) / domain.length; const labelBoundsList = getOneDimensionalLabelBounds(scale, domain, op, isHorizontal); - const minBoundsLength = Math.min(...labelBoundsList.map(bounds => bounds[2])); - - const stepResult = getStep( - domain, - labelBoundsList, - labelGap, - op.labelLastVisible, - Math.floor(minBoundsLength / incrementUnit), // 给step赋上合适的初值,有效改善外层循环次数 - false - ); - - scaleTicks = (scale as BandScale).stepTicks(stepResult.step); - if (op.labelLastVisible) { - if (stepResult.delCount) { - scaleTicks = scaleTicks.slice(0, scaleTicks.length - stepResult.delCount); + + if (labelBoundsList) { + const minBoundsLength = Math.min(...labelBoundsList.map(bounds => bounds[2])); + + const incrementUnit = (rangeEnd - rangeStart) / domain.length; + const stepResult = getStep( + domain, + labelBoundsList, + labelGap, + op.labelLastVisible, + Math.floor(minBoundsLength / incrementUnit), // 给step赋上合适的初值,有效改善外层循环次数 + false + ); + + scaleTicks = (scale as BandScale).stepTicks(stepResult.step); + if (op.labelLastVisible) { + if (stepResult.delCount) { + scaleTicks = scaleTicks.slice(0, scaleTicks.length - stepResult.delCount); + } + scaleTicks.push(domain[domain.length - 1]); } - scaleTicks.push(domain[domain.length - 1]); } } else { // only check first middle last, use the max size to sampling const tempDomain = [domain[0], domain[Math.floor(domain.length / 2)], domain[domain.length - 1]]; const tempList = getOneDimensionalLabelBounds(scale, tempDomain, op, isHorizontal); - let maxBounds: OneDimensionalBounds = null; - tempList.forEach(current => { - if (!maxBounds) { - maxBounds = current; - return; - } - if (maxBounds[2] < current[2]) { - maxBounds = current; - } - }); - const step = - rangeEnd - rangeStart - labelGap > 0 - ? Math.ceil((domain.length * (labelGap + maxBounds[2])) / (rangeEnd - rangeStart - labelGap)) - : domain.length - 1; + if (tempList) { + let maxBounds: OneDimensionalBounds = null; + tempList.forEach(current => { + if (!maxBounds) { + maxBounds = current; + return; + } + if (maxBounds[2] < current[2]) { + maxBounds = current; + } + }); + + const step = + rangeEnd - rangeStart - labelGap > 0 + ? Math.ceil((domain.length * (labelGap + maxBounds[2])) / (rangeEnd - rangeStart - labelGap)) + : domain.length - 1; - scaleTicks = (scale as BandScale).stepTicks(step); + scaleTicks = (scale as BandScale).stepTicks(step); - if ( - op.labelLastVisible && - (!scaleTicks.length || scaleTicks[scaleTicks.length - 1] !== domain[domain.length - 1]) - ) { if ( - scaleTicks.length && - Math.abs(scale.scale(scaleTicks[scaleTicks.length - 1]) - scale.scale(domain[domain.length - 1])) < - maxBounds[2] + op.labelLastVisible && + (!scaleTicks.length || scaleTicks[scaleTicks.length - 1] !== domain[domain.length - 1]) ) { - scaleTicks = scaleTicks.slice(0, -1); + if ( + scaleTicks.length && + Math.abs(scale.scale(scaleTicks[scaleTicks.length - 1]) - scale.scale(domain[domain.length - 1])) < + maxBounds[2] + ) { + scaleTicks = scaleTicks.slice(0, -1); + } + scaleTicks.push(domain[domain.length - 1]); } - scaleTicks.push(domain[domain.length - 1]); } } - } else { + } + + if (isNil(scaleTicks)) { scaleTicks = scale.domain(); } diff --git a/packages/vrender-components/src/axis/tick-data/util.ts b/packages/vrender-components/src/axis/tick-data/util.ts index 651d55353..f5cbbf980 100644 --- a/packages/vrender-components/src/axis/tick-data/util.ts +++ b/packages/vrender-components/src/axis/tick-data/util.ts @@ -1,5 +1,5 @@ import type { IBaseScale } from '@visactor/vscale'; -import { AABBBounds, degreeToRadian } from '@visactor/vutils'; +import { AABBBounds, degreeToRadian, isPlainObject } from '@visactor/vutils'; import type { TextAlignType, TextBaselineType } from '@visactor/vrender-core'; import { initTextMeasure } from '../../util/text'; import type { ICartesianTickDataOpt, IOrientType, ITickData } from '../type'; @@ -74,9 +74,17 @@ export const getCartesianLabelBounds = (scale: IBaseScale, domain: any[], op: IC const textMeasure = initTextMeasure(labelStyle); const range = scale.range(); - const labelBoundsList = domain.map((v: any, i: number) => { + let labelBoundsList: AABBBounds[] = []; + + for (let i = 0; i < domain.length; i++) { + const v = domain[i]; const str = labelFormatter ? labelFormatter(v) : `${v}`; + if (isPlainObject(str)) { + labelBoundsList = undefined; + break; + } + // 估算文本宽高 const { width, height } = textMeasure.quickMeasure(str); const textWidth = Math.max(width, MIN_TICK_GAP); @@ -124,8 +132,8 @@ export const getCartesianLabelBounds = (scale: IBaseScale, domain: any[], op: IC bounds.rotate(labelAngle, baseTextX, baseTextY); } - return bounds; - }); + labelBoundsList.push(bounds); + } return labelBoundsList; }; From 9d13c279fcae3f99ed05bac9a56c34e7019ca676 Mon Sep 17 00:00:00 2001 From: xile611 Date: Fri, 6 Jun 2025 11:14:22 +0800 Subject: [PATCH 155/179] docs: update changlog of rush --- ...abel-sampling-error-for-rich_2025-06-06-03-14.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@visactor/vrender-components/fix-label-sampling-error-for-rich_2025-06-06-03-14.json diff --git a/common/changes/@visactor/vrender-components/fix-label-sampling-error-for-rich_2025-06-06-03-14.json b/common/changes/@visactor/vrender-components/fix-label-sampling-error-for-rich_2025-06-06-03-14.json new file mode 100644 index 000000000..7139a5153 --- /dev/null +++ b/common/changes/@visactor/vrender-components/fix-label-sampling-error-for-rich_2025-06-06-03-14.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: disable sampling when label is rich text\n\n", + "type": "none", + "packageName": "@visactor/vrender-components" + } + ], + "packageName": "@visactor/vrender-components", + "email": "dingling112@gmail.com" +} \ No newline at end of file From 440b81c3542fd1573011a82bd7531a5dd97e4b68 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Mon, 9 Jun 2025 14:16:07 +0800 Subject: [PATCH 156/179] feat: upgrade vutils to 1.0.6 --- common/config/rush/pnpm-lock.yaml | 308 +++++++++++++++++++--- docs/demos/package.json | 2 +- docs/package.json | 2 +- packages/react-vrender-utils/package.json | 2 +- packages/react-vrender/package.json | 2 +- packages/vrender-animate/package.json | 2 +- packages/vrender-components/package.json | 6 +- packages/vrender-core/package.json | 2 +- packages/vrender-kits/package.json | 2 +- packages/vrender/package.json | 2 +- 10 files changed, 285 insertions(+), 45 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 5de8acfe8..cd9fc66f1 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -25,8 +25,8 @@ importers: specifier: workspace:0.22.11 version: link:../packages/vrender '@visactor/vutils': - specifier: 1.0.4 - version: 1.0.4 + specifier: 1.0.6 + version: 1.0.6 axios: specifier: ^1.4.0 version: 1.8.4 @@ -98,8 +98,8 @@ importers: specifier: workspace:0.22.11 version: link:../vrender '@visactor/vutils': - specifier: 1.0.4 - version: 1.0.4 + specifier: 1.0.6 + version: 1.0.6 react-reconciler: specifier: ^0.29.0 version: 0.29.2(react@18.3.1) @@ -159,8 +159,8 @@ importers: specifier: workspace:0.22.11 version: link:../vrender '@visactor/vutils': - specifier: 1.0.4 - version: 1.0.4 + specifier: 1.0.6 + version: 1.0.6 react-reconciler: specifier: ^0.29.0 version: 0.29.2(react@18.3.1) @@ -242,8 +242,8 @@ importers: specifier: ^18.0.0 version: 18.3.5(@types/react@18.3.20) '@visactor/vutils': - specifier: 1.0.4 - version: 1.0.4 + specifier: 1.0.6 + version: 1.0.6 '@vitejs/plugin-react': specifier: 3.1.0 version: 3.1.0(vite@3.2.6(@types/node@22.13.17)(less@4.1.3)(terser@5.17.1)) @@ -261,7 +261,7 @@ importers: version: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) jest-electron: specifier: ^0.1.12 - version: 0.1.12(jest@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5))) + version: 0.1.12(jest@26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5))) jest-extended: specifier: ^1.2.1 version: 1.2.1 @@ -273,7 +273,7 @@ importers: version: 18.3.1(react@18.3.1) ts-jest: specifier: ^26.0.0 - version: 26.5.6(jest@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)))(typescript@4.9.5) + version: 26.5.6(jest@26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)))(typescript@4.9.5) typescript: specifier: 4.9.5 version: 4.9.5 @@ -287,8 +287,8 @@ importers: specifier: workspace:0.22.11 version: link:../vrender-core '@visactor/vutils': - specifier: 1.0.4 - version: 1.0.4 + specifier: 1.0.6 + version: 1.0.6 devDependencies: '@internal/bundler': specifier: workspace:* @@ -351,11 +351,11 @@ importers: specifier: workspace:0.22.11 version: link:../vrender-kits '@visactor/vscale': - specifier: 1.0.4 - version: 1.0.4 + specifier: 1.0.6 + version: 1.0.6 '@visactor/vutils': - specifier: 1.0.4 - version: 1.0.4 + specifier: 1.0.6 + version: 1.0.6 devDependencies: '@internal/bundler': specifier: workspace:* @@ -380,7 +380,7 @@ importers: version: 8.18.0 jest: specifier: ^26.0.0 - version: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + version: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) jest-electron: specifier: ^0.1.12 version: 0.1.12(jest@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5))) @@ -400,8 +400,8 @@ importers: ../../packages/vrender-core: dependencies: '@visactor/vutils': - specifier: 1.0.4 - version: 1.0.4 + specifier: 1.0.6 + version: 1.0.6 color-convert: specifier: 2.0.1 version: 2.0.1 @@ -438,7 +438,7 @@ importers: version: 8.18.0 jest: specifier: ^26.0.0 - version: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + version: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) jest-electron: specifier: ^0.1.12 version: 0.1.12(jest@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5))) @@ -470,8 +470,8 @@ importers: specifier: workspace:0.22.11 version: link:../vrender-core '@visactor/vutils': - specifier: 1.0.4 - version: 1.0.4 + specifier: 1.0.6 + version: 1.0.6 gifuct-js: specifier: 2.1.2 version: 2.1.2 @@ -573,7 +573,7 @@ importers: version: 26.0.24 jest: specifier: ^26.0.0 - version: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + version: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) typescript: specifier: 4.9.5 version: 4.9.5 @@ -2192,8 +2192,8 @@ packages: '@visactor/vscale@0.15.14': resolution: {integrity: sha512-ttGdvS49APcO23WFfsbq9zyQ1y767LjqchPPL78KOrd4UjhYQXRCdeqz7K4A57e333R37oLnPfSuIVFz9qJGYw==} - '@visactor/vscale@1.0.4': - resolution: {integrity: sha512-mXuX0gbQ5dmsU+dOfrDfFT45ijTZrFh1wYeIY44cdMhFo4v+tVdeihN0F+3CEI7oSZiZENbpJ7dXvxnu04xG/g==} + '@visactor/vscale@1.0.6': + resolution: {integrity: sha512-E6ySrzOIyL85luy5dKPpKzaCjf/hkLFF/mAn37Lv8XJWhyxWjYO29GM7cIlqDNCKAY0qsONPnfmgdGX+Hoe5vg==} '@visactor/vutils@0.13.3': resolution: {integrity: sha512-lCFiuUHwqz/0RCvIYa79ycduCLAILWaXddPOjxEd3VRX9CCoWMUmRtM3gF5JxtK2pK6Mu7hW7LaMSuWFw+0Kkw==} @@ -2201,8 +2201,8 @@ packages: '@visactor/vutils@0.15.14': resolution: {integrity: sha512-mZuJhXdDZqq5arqc/LfEmWOY6l7ErK1MurO8bR3vESxeCaQ18pN36iit15K2IMQVJuKZPnZ2ksw8+a1irXi/8A==} - '@visactor/vutils@1.0.4': - resolution: {integrity: sha512-GE149SM5WAc9DMNV7bGtPD4xHP68vbHMRuxGPJ3ndzAGLC/KuXpClteMw6bTY1fRX1vDLY/tQ/GVthgeOx4kDw==} + '@visactor/vutils@1.0.6': + resolution: {integrity: sha512-87/AYLrjY1rtvIT0N/9S+sESialMQUKYv7MDjLjUo37u0hmeL/AwRSGBSvjxdxayKHOmdwUK1BLpQrDIrssKLg==} '@vitejs/plugin-react@3.1.0': resolution: {integrity: sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==} @@ -7935,6 +7935,43 @@ snapshots: - ts-node - utf-8-validate + '@jest/core@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5))': + dependencies: + '@jest/console': 26.6.2 + '@jest/reporters': 26.6.2 + '@jest/test-result': 26.6.2 + '@jest/transform': 26.6.2 + '@jest/types': 26.6.2 + '@types/node': 22.13.17 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 26.6.2 + jest-config: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-haste-map: 26.6.2 + jest-message-util: 26.6.2 + jest-regex-util: 26.0.0 + jest-resolve: 26.6.2 + jest-resolve-dependencies: 26.6.3 + jest-runner: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-snapshot: 26.6.2 + jest-util: 26.6.2 + jest-validate: 26.6.2 + jest-watcher: 26.6.2 + micromatch: 4.0.8 + p-each-series: 2.2.0 + rimraf: 3.0.2 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + '@jest/environment@24.9.0': dependencies: '@jest/fake-timers': 24.9.0 @@ -8051,6 +8088,20 @@ snapshots: - ts-node - utf-8-validate + '@jest/test-sequencer@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5))': + dependencies: + '@jest/test-result': 26.6.2 + graceful-fs: 4.2.11 + jest-haste-map: 26.6.2 + jest-runner: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + '@jest/transform@24.9.0': dependencies: '@babel/core': 7.20.12 @@ -8788,9 +8839,9 @@ snapshots: dependencies: '@visactor/vutils': 0.15.14 - '@visactor/vscale@1.0.4': + '@visactor/vscale@1.0.6': dependencies: - '@visactor/vutils': 1.0.4 + '@visactor/vutils': 1.0.6 '@visactor/vutils@0.13.3': dependencies: @@ -8804,7 +8855,7 @@ snapshots: '@turf/invariant': 6.5.0 eventemitter3: 4.0.7 - '@visactor/vutils@1.0.4': + '@visactor/vutils@1.0.6': dependencies: '@turf/helpers': 6.5.0 '@turf/invariant': 6.5.0 @@ -11399,6 +11450,28 @@ snapshots: - ts-node - utf-8-validate + jest-cli@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)): + dependencies: + '@jest/core': 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + '@jest/test-result': 26.6.2 + '@jest/types': 26.6.2 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + import-local: 3.2.0 + is-ci: 2.0.0 + jest-config: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-util: 26.6.2 + jest-validate: 26.6.2 + prompts: 2.4.2 + yargs: 15.4.1 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + jest-config@24.9.0: dependencies: '@babel/core': 7.20.12 @@ -11434,7 +11507,35 @@ snapshots: jest-environment-jsdom: 26.6.2(canvas@2.11.2) jest-environment-node: 26.6.2 jest-get-type: 26.3.0 - jest-jasmine2: 26.6.3 + jest-jasmine2: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-regex-util: 26.0.0 + jest-resolve: 26.6.2 + jest-util: 26.6.2 + jest-validate: 26.6.2 + micromatch: 4.0.8 + pretty-format: 26.6.2 + optionalDependencies: + ts-node: 10.9.0(@types/node@22.13.17)(typescript@4.9.5) + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - utf-8-validate + + jest-config@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)): + dependencies: + '@babel/core': 7.20.12 + '@jest/test-sequencer': 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + '@jest/types': 26.6.2 + babel-jest: 26.6.3(@babel/core@7.20.12) + chalk: 4.1.2 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-environment-jsdom: 26.6.2(canvas@2.11.2) + jest-environment-node: 26.6.2 + jest-get-type: 26.3.0 + jest-jasmine2: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) jest-regex-util: 26.0.0 jest-resolve: 26.6.2 jest-util: 26.6.2 @@ -11494,7 +11595,7 @@ snapshots: jest-util: 26.6.2 pretty-format: 26.6.2 - jest-electron@0.1.12(jest@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5))): + jest-electron@0.1.12(jest@26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5))): dependencies: electron: 11.5.0 jest: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) @@ -11510,6 +11611,22 @@ snapshots: transitivePeerDependencies: - supports-color + jest-electron@0.1.12(jest@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5))): + dependencies: + electron: 11.5.0 + jest: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-haste-map: 24.9.0 + jest-message-util: 24.9.0 + jest-mock: 24.9.0 + jest-resolve: 24.9.0 + jest-runner: 24.9.0 + jest-runtime: 24.9.0 + jest-util: 24.9.0 + throat: 5.0.0 + tslib: 1.14.1 + transitivePeerDependencies: + - supports-color + jest-environment-jsdom@24.9.0: dependencies: '@jest/environment': 24.9.0 @@ -11623,7 +11740,7 @@ snapshots: transitivePeerDependencies: - supports-color - jest-jasmine2@26.6.3: + jest-jasmine2@26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)): dependencies: '@babel/traverse': 7.27.0 '@jest/environment': 26.6.2 @@ -11644,7 +11761,38 @@ snapshots: pretty-format: 26.6.2 throat: 5.0.0 transitivePeerDependencies: + - bufferutil + - canvas - supports-color + - ts-node + - utf-8-validate + + jest-jasmine2@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)): + dependencies: + '@babel/traverse': 7.27.0 + '@jest/environment': 26.6.2 + '@jest/source-map': 26.6.2 + '@jest/test-result': 26.6.2 + '@jest/types': 26.6.2 + '@types/node': 22.13.17 + chalk: 4.1.2 + co: 4.6.0 + expect: 26.6.2 + is-generator-fn: 2.1.0 + jest-each: 26.6.2 + jest-matcher-utils: 26.6.2 + jest-message-util: 26.6.2 + jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-snapshot: 26.6.2 + jest-util: 26.6.2 + pretty-format: 26.6.2 + throat: 5.0.0 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate jest-leak-detector@24.9.0: dependencies: @@ -11799,6 +11947,35 @@ snapshots: - ts-node - utf-8-validate + jest-runner@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)): + dependencies: + '@jest/console': 26.6.2 + '@jest/environment': 26.6.2 + '@jest/test-result': 26.6.2 + '@jest/types': 26.6.2 + '@types/node': 22.13.17 + chalk: 4.1.2 + emittery: 0.7.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-docblock: 26.0.0 + jest-haste-map: 26.6.2 + jest-leak-detector: 26.6.2 + jest-message-util: 26.6.2 + jest-resolve: 26.6.2 + jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-util: 26.6.2 + jest-worker: 26.6.2 + source-map-support: 0.5.21 + throat: 5.0.0 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + jest-runtime@24.9.0: dependencies: '@jest/console': 24.9.0 @@ -11863,6 +12040,42 @@ snapshots: - ts-node - utf-8-validate + jest-runtime@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)): + dependencies: + '@jest/console': 26.6.2 + '@jest/environment': 26.6.2 + '@jest/fake-timers': 26.6.2 + '@jest/globals': 26.6.2 + '@jest/source-map': 26.6.2 + '@jest/test-result': 26.6.2 + '@jest/transform': 26.6.2 + '@jest/types': 26.6.2 + '@types/yargs': 15.0.19 + chalk: 4.1.2 + cjs-module-lexer: 0.6.0 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-config: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-haste-map: 26.6.2 + jest-message-util: 26.6.2 + jest-mock: 26.6.2 + jest-regex-util: 26.0.0 + jest-resolve: 26.6.2 + jest-snapshot: 26.6.2 + jest-util: 26.6.2 + jest-validate: 26.6.2 + slash: 3.0.0 + strip-bom: 4.0.0 + yargs: 15.4.1 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + jest-serializer@24.9.0: {} jest-serializer@26.6.2: @@ -11980,6 +12193,18 @@ snapshots: - ts-node - utf-8-validate + jest@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)): + dependencies: + '@jest/core': 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + import-local: 3.2.0 + jest-cli: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + js-binary-schema-parser@2.0.3: {} js-string-escape@1.0.1: {} @@ -13920,7 +14145,7 @@ snapshots: dependencies: punycode: 2.3.1 - ts-jest@26.5.6(jest@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)))(typescript@4.9.5): + ts-jest@26.5.6(jest@26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)))(typescript@4.9.5): dependencies: bs-logger: 0.2.6 buffer-from: 1.1.2 @@ -13935,6 +14160,21 @@ snapshots: typescript: 4.9.5 yargs-parser: 20.2.9 + ts-jest@26.5.6(jest@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)))(typescript@4.9.5): + dependencies: + bs-logger: 0.2.6 + buffer-from: 1.1.2 + fast-json-stable-stringify: 2.1.0 + jest: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-util: 26.6.2 + json5: 2.2.3 + lodash: 4.17.21 + make-error: 1.3.6 + mkdirp: 1.0.4 + semver: 7.3.4 + typescript: 4.9.5 + yargs-parser: 20.2.9 + ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5): dependencies: '@cspotcode/source-map-support': 0.8.1 diff --git a/docs/demos/package.json b/docs/demos/package.json index 923ba0c7f..4b5158728 100644 --- a/docs/demos/package.json +++ b/docs/demos/package.json @@ -12,7 +12,7 @@ "@internal/eslint-config": "workspace:*", "@internal/ts-config": "workspace:*", "@visactor/vrender-kits": "workspace:0.14.8", - "@visactor/vutils": "1.0.4", + "@visactor/vutils": "1.0.6", "d3-scale-chromatic": "^3.0.0", "lodash": "4.17.21", "dat.gui": "^0.7.9", diff --git a/docs/package.json b/docs/package.json index b677e3ac7..1aafa788e 100644 --- a/docs/package.json +++ b/docs/package.json @@ -11,7 +11,7 @@ "dependencies": { "@arco-design/web-react": "2.46.1", "@visactor/vchart": "1.3.0", - "@visactor/vutils": "1.0.4", + "@visactor/vutils": "1.0.6", "@visactor/vgrammar": "~0.5.7", "@visactor/vrender": "workspace:0.22.11", "markdown-it": "^13.0.0", diff --git a/packages/react-vrender-utils/package.json b/packages/react-vrender-utils/package.json index 86a45509f..6ce6dab59 100644 --- a/packages/react-vrender-utils/package.json +++ b/packages/react-vrender-utils/package.json @@ -26,7 +26,7 @@ "dependencies": { "@visactor/vrender": "workspace:0.22.11", "@visactor/react-vrender": "workspace:0.22.11", - "@visactor/vutils": "1.0.4", + "@visactor/vutils": "1.0.6", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" }, diff --git a/packages/react-vrender/package.json b/packages/react-vrender/package.json index c128184e1..79b846c61 100644 --- a/packages/react-vrender/package.json +++ b/packages/react-vrender/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "@visactor/vrender": "workspace:0.22.11", - "@visactor/vutils": "1.0.4", + "@visactor/vutils": "1.0.6", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" }, diff --git a/packages/vrender-animate/package.json b/packages/vrender-animate/package.json index b9adb2718..e9051c08e 100644 --- a/packages/vrender-animate/package.json +++ b/packages/vrender-animate/package.json @@ -20,7 +20,7 @@ "test": "" }, "dependencies": { - "@visactor/vutils": "1.0.4", + "@visactor/vutils": "1.0.6", "@visactor/vrender-core": "workspace:0.22.11" }, "devDependencies": { diff --git a/packages/vrender-components/package.json b/packages/vrender-components/package.json index 547d6ee06..08dc4b98f 100644 --- a/packages/vrender-components/package.json +++ b/packages/vrender-components/package.json @@ -25,8 +25,8 @@ "build:spec-types": "rm -rf ./spec-types && tsc -p ./tsconfig.spec.json --declaration --emitDeclarationOnly --outDir ./spec-types" }, "dependencies": { - "@visactor/vutils": "1.0.4", - "@visactor/vscale": "1.0.4", + "@visactor/vutils": "1.0.6", + "@visactor/vscale": "1.0.6", "@visactor/vrender-core": "workspace:0.22.11", "@visactor/vrender-kits": "workspace:0.22.11", "@visactor/vrender-animate": "workspace:0.22.11" @@ -36,7 +36,7 @@ "@internal/eslint-config": "workspace:*", "@internal/ts-config": "workspace:*", "@rushstack/eslint-patch": "~1.1.4", - "@visactor/vscale": "1.0.4", + "@visactor/vscale": "1.0.6", "@types/jest": "^26.0.0", "jest": "^26.0.0", "jest-electron": "^0.1.12", diff --git a/packages/vrender-core/package.json b/packages/vrender-core/package.json index 9f7352128..5af191733 100644 --- a/packages/vrender-core/package.json +++ b/packages/vrender-core/package.json @@ -30,7 +30,7 @@ }, "dependencies": { "color-convert": "2.0.1", - "@visactor/vutils": "1.0.4" + "@visactor/vutils": "1.0.6" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-kits/package.json b/packages/vrender-kits/package.json index b68c959a2..c4946ba2d 100644 --- a/packages/vrender-kits/package.json +++ b/packages/vrender-kits/package.json @@ -20,7 +20,7 @@ "test": "" }, "dependencies": { - "@visactor/vutils": "1.0.4", + "@visactor/vutils": "1.0.6", "@visactor/vrender-core": "workspace:0.22.11", "@resvg/resvg-js": "2.4.1", "roughjs": "4.5.2", diff --git a/packages/vrender/package.json b/packages/vrender/package.json index 9a35901b6..1261d98a6 100644 --- a/packages/vrender/package.json +++ b/packages/vrender/package.json @@ -33,7 +33,7 @@ "@internal/eslint-config": "workspace:*", "@internal/ts-config": "workspace:*", "@rushstack/eslint-patch": "~1.1.4", - "@visactor/vutils": "1.0.4", + "@visactor/vutils": "1.0.6", "canvas": "2.11.2", "react": "^18.0.0", "react-dom": "^18.0.0", From c5415059b0c6c6f4898de86b5308e8c0ebab9e2a Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Mon, 9 Jun 2025 15:09:26 +0800 Subject: [PATCH 157/179] fix: bugserver-trigger add vrender-animate --- common/config/rush/pnpm-lock.yaml | 96 ++++------------------------ tools/bugserver-trigger/package.json | 3 +- tools/bugserver-trigger/src/index.ts | 6 +- 3 files changed, 21 insertions(+), 84 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index cd9fc66f1..3ddb2794f 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -585,6 +585,9 @@ importers: '@visactor/vrender': specifier: workspace:0.22.11 version: link:../../packages/vrender + '@visactor/vrender-animate': + specifier: workspace:0.22.11 + version: link:../../packages/vrender-animate '@visactor/vrender-components': specifier: workspace:0.22.11 version: link:../../packages/vrender-components @@ -7955,7 +7958,7 @@ snapshots: jest-resolve: 26.6.2 jest-resolve-dependencies: 26.6.3 jest-runner: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) - jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-runtime: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) jest-snapshot: 26.6.2 jest-util: 26.6.2 jest-validate: 26.6.2 @@ -8074,27 +8077,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@jest/test-sequencer@26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5))': + '@jest/test-sequencer@26.6.3': dependencies: '@jest/test-result': 26.6.2 graceful-fs: 4.2.11 jest-haste-map: 26.6.2 - jest-runner: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-runner: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) jest-runtime: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) transitivePeerDependencies: - - bufferutil - - canvas - supports-color - - ts-node - - utf-8-validate - '@jest/test-sequencer@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5))': + '@jest/test-sequencer@26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5))': dependencies: '@jest/test-result': 26.6.2 graceful-fs: 4.2.11 jest-haste-map: 26.6.2 - jest-runner: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) - jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-runner: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-runtime: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) transitivePeerDependencies: - bufferutil - canvas @@ -11507,7 +11506,7 @@ snapshots: jest-environment-jsdom: 26.6.2(canvas@2.11.2) jest-environment-node: 26.6.2 jest-get-type: 26.3.0 - jest-jasmine2: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-jasmine2: 26.6.3 jest-regex-util: 26.0.0 jest-resolve: 26.6.2 jest-util: 26.6.2 @@ -11525,7 +11524,7 @@ snapshots: jest-config@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)): dependencies: '@babel/core': 7.20.12 - '@jest/test-sequencer': 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + '@jest/test-sequencer': 26.6.3 '@jest/types': 26.6.2 babel-jest: 26.6.3(@babel/core@7.20.12) chalk: 4.1.2 @@ -11535,7 +11534,7 @@ snapshots: jest-environment-jsdom: 26.6.2(canvas@2.11.2) jest-environment-node: 26.6.2 jest-get-type: 26.3.0 - jest-jasmine2: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-jasmine2: 26.6.3 jest-regex-util: 26.0.0 jest-resolve: 26.6.2 jest-util: 26.6.2 @@ -11740,7 +11739,7 @@ snapshots: transitivePeerDependencies: - supports-color - jest-jasmine2@26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)): + jest-jasmine2@26.6.3: dependencies: '@babel/traverse': 7.27.0 '@jest/environment': 26.6.2 @@ -11761,38 +11760,7 @@ snapshots: pretty-format: 26.6.2 throat: 5.0.0 transitivePeerDependencies: - - bufferutil - - canvas - supports-color - - ts-node - - utf-8-validate - - jest-jasmine2@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)): - dependencies: - '@babel/traverse': 7.27.0 - '@jest/environment': 26.6.2 - '@jest/source-map': 26.6.2 - '@jest/test-result': 26.6.2 - '@jest/types': 26.6.2 - '@types/node': 22.13.17 - chalk: 4.1.2 - co: 4.6.0 - expect: 26.6.2 - is-generator-fn: 2.1.0 - jest-each: 26.6.2 - jest-matcher-utils: 26.6.2 - jest-message-util: 26.6.2 - jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) - jest-snapshot: 26.6.2 - jest-util: 26.6.2 - pretty-format: 26.6.2 - throat: 5.0.0 - transitivePeerDependencies: - - bufferutil - - canvas - - supports-color - - ts-node - - utf-8-validate jest-leak-detector@24.9.0: dependencies: @@ -11964,7 +11932,7 @@ snapshots: jest-leak-detector: 26.6.2 jest-message-util: 26.6.2 jest-resolve: 26.6.2 - jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) + jest-runtime: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) jest-util: 26.6.2 jest-worker: 26.6.2 source-map-support: 0.5.21 @@ -12040,42 +12008,6 @@ snapshots: - ts-node - utf-8-validate - jest-runtime@26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)): - dependencies: - '@jest/console': 26.6.2 - '@jest/environment': 26.6.2 - '@jest/fake-timers': 26.6.2 - '@jest/globals': 26.6.2 - '@jest/source-map': 26.6.2 - '@jest/test-result': 26.6.2 - '@jest/transform': 26.6.2 - '@jest/types': 26.6.2 - '@types/yargs': 15.0.19 - chalk: 4.1.2 - cjs-module-lexer: 0.6.0 - collect-v8-coverage: 1.0.2 - exit: 0.1.2 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-config: 26.6.3(ts-node@10.9.0(@types/node@22.13.17)(typescript@4.9.5)) - jest-haste-map: 26.6.2 - jest-message-util: 26.6.2 - jest-mock: 26.6.2 - jest-regex-util: 26.0.0 - jest-resolve: 26.6.2 - jest-snapshot: 26.6.2 - jest-util: 26.6.2 - jest-validate: 26.6.2 - slash: 3.0.0 - strip-bom: 4.0.0 - yargs: 15.4.1 - transitivePeerDependencies: - - bufferutil - - canvas - - supports-color - - ts-node - - utf-8-validate - jest-serializer@24.9.0: {} jest-serializer@26.6.2: diff --git a/tools/bugserver-trigger/package.json b/tools/bugserver-trigger/package.json index 11acabaac..18cb105d4 100644 --- a/tools/bugserver-trigger/package.json +++ b/tools/bugserver-trigger/package.json @@ -11,7 +11,8 @@ "@visactor/vrender": "workspace:0.22.11", "@visactor/vrender-core": "workspace:0.22.11", "@visactor/vrender-kits": "workspace:0.22.11", - "@visactor/vrender-components": "workspace:0.22.11" + "@visactor/vrender-components": "workspace:0.22.11", + "@visactor/vrender-animate": "workspace:0.22.11" }, "devDependencies": { "@rushstack/eslint-patch": "~1.1.4", diff --git a/tools/bugserver-trigger/src/index.ts b/tools/bugserver-trigger/src/index.ts index dab515bd8..515664071 100644 --- a/tools/bugserver-trigger/src/index.ts +++ b/tools/bugserver-trigger/src/index.ts @@ -2,6 +2,7 @@ import * as VRender from '@visactor/vrender'; import * as VRenderKits from '@visactor/vrender-kits'; import * as VRenderCore from '@visactor/vrender-core'; import * as VRenderComponents from '@visactor/vrender-components'; +import * as VRenderAnimate from '@visactor/vrender-animate'; import { registerArcDataLabel, @@ -18,6 +19,8 @@ window.VRenderComponents = VRenderComponents; window.VRenderCore = VRenderCore; // @ts-ignore window.VRenderKits = VRenderKits; +// @ts-ignore +window.VRenderAnimate = VRenderAnimate; registerSymbolDataLabel(); registerRectDataLabel(); @@ -28,7 +31,8 @@ export default { VRender, VRenderComponents, VRenderKits, - VRenderCore + VRenderCore, + VRenderAnimate }; // export const a = 'a'; From e31e4deacaaaf4bb4cb085ee413fd123cd914bbc Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Mon, 9 Jun 2025 15:16:44 +0800 Subject: [PATCH 158/179] fix: fix issue with component test --- packages/vrender-components/tsconfig.spec.json | 3 +++ packages/vrender-components/tsconfig.test.json | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/vrender-components/tsconfig.spec.json b/packages/vrender-components/tsconfig.spec.json index 2733c803f..4e320df32 100644 --- a/packages/vrender-components/tsconfig.spec.json +++ b/packages/vrender-components/tsconfig.spec.json @@ -17,6 +17,9 @@ }, { "path": "../vrender-kits" + }, + { + "path": "../vrender-animate" } ] } diff --git a/packages/vrender-components/tsconfig.test.json b/packages/vrender-components/tsconfig.test.json index 3ae0f0875..473e2e391 100644 --- a/packages/vrender-components/tsconfig.test.json +++ b/packages/vrender-components/tsconfig.test.json @@ -4,7 +4,8 @@ "paths": { "@visactor/vrender": ["../vrender/src"], "@visactor/vrender-core": ["../vrender-core/src"], - "@visactor/vrender-kits": ["../vrender-kits/src"] + "@visactor/vrender-kits": ["../vrender-kits/src"], + "@visactor/vrender-animate": ["../vrender-animate/src"] } }, "references": [] From 0c87a60850199c9f2e01e54591fa34724946bd5e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 9 Jun 2025 07:33:30 +0000 Subject: [PATCH 159/179] build: prelease version 1.0.1 --- .../fix-brush-problem_2025-05-31-14-30.json | 10 -- .../fix-brush-problem_2025-06-06-02-36.json | 10 -- .../fix-brush-problem_2025-06-06-02-53.json | 10 -- .../fix-brush-problem_2025-06-06-03-56.json | 10 -- common/config/rush/pnpm-lock.yaml | 157 +++++++++++++++--- common/config/rush/version-policies.json | 2 +- docs/package.json | 2 +- packages/react-vrender-utils/CHANGELOG.json | 6 + packages/react-vrender-utils/CHANGELOG.md | 7 +- packages/react-vrender-utils/package.json | 6 +- packages/react-vrender/CHANGELOG.json | 6 + packages/react-vrender/CHANGELOG.md | 7 +- packages/react-vrender/package.json | 4 +- packages/vrender-animate/CHANGELOG.json | 6 + packages/vrender-animate/CHANGELOG.md | 7 +- packages/vrender-animate/package.json | 4 +- packages/vrender-components/CHANGELOG.json | 21 +++ packages/vrender-components/CHANGELOG.md | 12 +- packages/vrender-components/package.json | 8 +- packages/vrender-core/CHANGELOG.json | 6 + packages/vrender-core/CHANGELOG.md | 7 +- packages/vrender-core/package.json | 2 +- packages/vrender-kits/CHANGELOG.json | 6 + packages/vrender-kits/CHANGELOG.md | 7 +- packages/vrender-kits/package.json | 4 +- packages/vrender/CHANGELOG.json | 6 + packages/vrender/CHANGELOG.md | 7 +- packages/vrender/package.json | 8 +- tools/bugserver-trigger/package.json | 10 +- 29 files changed, 262 insertions(+), 96 deletions(-) delete mode 100644 common/changes/@visactor/vrender-components/fix-brush-problem_2025-05-31-14-30.json delete mode 100644 common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-02-36.json delete mode 100644 common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-02-53.json delete mode 100644 common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-03-56.json diff --git a/common/changes/@visactor/vrender-components/fix-brush-problem_2025-05-31-14-30.json b/common/changes/@visactor/vrender-components/fix-brush-problem_2025-05-31-14-30.json deleted file mode 100644 index ba64f9de6..000000000 --- a/common/changes/@visactor/vrender-components/fix-brush-problem_2025-05-31-14-30.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender-components", - "comment": "fix: brush active problem. fix visactor/vchart#4017", - "type": "none" - } - ], - "packageName": "@visactor/vrender-components" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-02-36.json b/common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-02-36.json deleted file mode 100644 index 504ec2dcc..000000000 --- a/common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-02-36.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender-components", - "comment": "fix: brush event pos problem when stage scale", - "type": "none" - } - ], - "packageName": "@visactor/vrender-components" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-02-53.json b/common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-02-53.json deleted file mode 100644 index cce01dfea..000000000 --- a/common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-02-53.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender-components", - "comment": "fix: use removeAllChild to remove brush mask. fix visactor/vchart#4017", - "type": "none" - } - ], - "packageName": "@visactor/vrender-components" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-03-56.json b/common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-03-56.json deleted file mode 100644 index f33830f10..000000000 --- a/common/changes/@visactor/vrender-components/fix-brush-problem_2025-06-06-03-56.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender-components", - "comment": "fix: datazoom text render error. fix visactor/vchart#4018", - "type": "none" - } - ], - "packageName": "@visactor/vrender-components" -} \ No newline at end of file diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index c39570afb..10c2c8f72 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: specifier: ~0.5.7 version: 0.5.7 '@visactor/vrender': - specifier: workspace:1.0.0 + specifier: workspace:1.0.1 version: link:../packages/vrender '@visactor/vutils': specifier: 1.0.6 @@ -95,7 +95,7 @@ importers: ../../packages/react-vrender: dependencies: '@visactor/vrender': - specifier: workspace:1.0.0 + specifier: workspace:1.0.1 version: link:../vrender '@visactor/vutils': specifier: 1.0.6 @@ -153,10 +153,10 @@ importers: ../../packages/react-vrender-utils: dependencies: '@visactor/react-vrender': - specifier: workspace:1.0.0 + specifier: workspace:1.0.1 version: link:../react-vrender '@visactor/vrender': - specifier: workspace:1.0.0 + specifier: workspace:1.0.1 version: link:../vrender '@visactor/vutils': specifier: 1.0.6 @@ -211,13 +211,13 @@ importers: ../../packages/vrender: dependencies: '@visactor/vrender-animate': - specifier: workspace:1.0.0 + specifier: workspace:1.0.1 version: link:../vrender-animate '@visactor/vrender-core': - specifier: workspace:1.0.0 + specifier: workspace:1.0.1 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:1.0.0 + specifier: workspace:1.0.1 version: link:../vrender-kits devDependencies: '@internal/bundler': @@ -284,7 +284,7 @@ importers: ../../packages/vrender-animate: dependencies: '@visactor/vrender-core': - specifier: workspace:1.0.0 + specifier: workspace:1.0.1 version: link:../vrender-core '@visactor/vutils': specifier: 1.0.6 @@ -342,13 +342,13 @@ importers: ../../packages/vrender-components: dependencies: '@visactor/vrender-animate': - specifier: workspace:1.0.0 + specifier: workspace:1.0.1 version: link:../vrender-animate '@visactor/vrender-core': - specifier: workspace:1.0.0 + specifier: workspace:1.0.1 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:1.0.0 + specifier: workspace:1.0.1 version: link:../vrender-kits '@visactor/vscale': specifier: 1.0.6 @@ -467,7 +467,7 @@ importers: specifier: 2.4.1 version: 2.4.1 '@visactor/vrender-core': - specifier: workspace:1.0.0 + specifier: workspace:1.0.1 version: link:../vrender-core '@visactor/vutils': specifier: 1.0.6 @@ -583,19 +583,19 @@ importers: ../../tools/bugserver-trigger: dependencies: '@visactor/vrender': - specifier: workspace:1.0.0 + specifier: workspace:1.0.1 version: link:../../packages/vrender '@visactor/vrender-animate': - specifier: workspace:1.0.0 + specifier: workspace:1.0.1 version: link:../../packages/vrender-animate '@visactor/vrender-components': - specifier: workspace:1.0.0 + specifier: workspace:1.0.1 version: link:../../packages/vrender-components '@visactor/vrender-core': - specifier: workspace:1.0.0 + specifier: workspace:1.0.1 version: link:../../packages/vrender-core '@visactor/vrender-kits': - specifier: workspace:1.0.0 + specifier: workspace:1.0.1 version: link:../../packages/vrender-kits devDependencies: '@internal/bundler': @@ -7950,14 +7950,14 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 26.6.2 - jest-config: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-config: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) jest-haste-map: 26.6.2 jest-message-util: 26.6.2 jest-regex-util: 26.0.0 jest-resolve: 26.6.2 jest-resolve-dependencies: 26.6.3 jest-runner: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) - jest-runtime: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) jest-snapshot: 26.6.2 jest-util: 26.6.2 jest-validate: 26.6.2 @@ -8076,7 +8076,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@jest/test-sequencer@26.6.3': + '@jest/test-sequencer@26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5))': dependencies: '@jest/test-result': 26.6.2 graceful-fs: 4.2.11 @@ -8084,7 +8084,25 @@ snapshots: jest-runner: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) jest-runtime: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) transitivePeerDependencies: + - bufferutil + - canvas - supports-color + - ts-node + - utf-8-validate + + '@jest/test-sequencer@26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5))': + dependencies: + '@jest/test-result': 26.6.2 + graceful-fs: 4.2.11 + jest-haste-map: 26.6.2 + jest-runner: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate '@jest/transform@24.9.0': dependencies: @@ -11452,7 +11470,7 @@ snapshots: graceful-fs: 4.2.11 import-local: 3.2.0 is-ci: 2.0.0 - jest-config: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-config: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) jest-util: 26.6.2 jest-validate: 26.6.2 prompts: 2.4.2 @@ -11489,7 +11507,7 @@ snapshots: jest-config@26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)): dependencies: '@babel/core': 7.20.12 - '@jest/test-sequencer': 26.6.3 + '@jest/test-sequencer': 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) '@jest/types': 26.6.2 babel-jest: 26.6.3(@babel/core@7.20.12) chalk: 4.1.2 @@ -11514,6 +11532,34 @@ snapshots: - supports-color - utf-8-validate + jest-config@26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)): + dependencies: + '@babel/core': 7.20.12 + '@jest/test-sequencer': 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + '@jest/types': 26.6.2 + babel-jest: 26.6.3(@babel/core@7.20.12) + chalk: 4.1.2 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-environment-jsdom: 26.6.2(canvas@2.11.2) + jest-environment-node: 26.6.2 + jest-get-type: 26.3.0 + jest-jasmine2: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-regex-util: 26.0.0 + jest-resolve: 26.6.2 + jest-util: 26.6.2 + jest-validate: 26.6.2 + micromatch: 4.0.8 + pretty-format: 26.6.2 + optionalDependencies: + ts-node: 10.9.0(@types/node@22.15.30)(typescript@4.9.5) + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - utf-8-validate + jest-diff@24.9.0: dependencies: chalk: 2.4.2 @@ -11731,6 +11777,33 @@ snapshots: - ts-node - utf-8-validate + jest-jasmine2@26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)): + dependencies: + '@babel/traverse': 7.27.4 + '@jest/environment': 26.6.2 + '@jest/source-map': 26.6.2 + '@jest/test-result': 26.6.2 + '@jest/types': 26.6.2 + '@types/node': 22.15.30 + chalk: 4.1.2 + co: 4.6.0 + expect: 26.6.2 + is-generator-fn: 2.1.0 + jest-each: 26.6.2 + jest-matcher-utils: 26.6.2 + jest-message-util: 26.6.2 + jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-snapshot: 26.6.2 + jest-util: 26.6.2 + pretty-format: 26.6.2 + throat: 5.0.0 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + jest-leak-detector@24.9.0: dependencies: jest-get-type: 24.9.0 @@ -11895,13 +11968,13 @@ snapshots: emittery: 0.7.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-config: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) jest-docblock: 26.0.0 jest-haste-map: 26.6.2 jest-leak-detector: 26.6.2 jest-message-util: 26.6.2 jest-resolve: 26.6.2 - jest-runtime: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) jest-util: 26.6.2 jest-worker: 26.6.2 source-map-support: 0.5.21 @@ -11977,6 +12050,42 @@ snapshots: - ts-node - utf-8-validate + jest-runtime@26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)): + dependencies: + '@jest/console': 26.6.2 + '@jest/environment': 26.6.2 + '@jest/fake-timers': 26.6.2 + '@jest/globals': 26.6.2 + '@jest/source-map': 26.6.2 + '@jest/test-result': 26.6.2 + '@jest/transform': 26.6.2 + '@jest/types': 26.6.2 + '@types/yargs': 15.0.19 + chalk: 4.1.2 + cjs-module-lexer: 0.6.0 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-config: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-haste-map: 26.6.2 + jest-message-util: 26.6.2 + jest-mock: 26.6.2 + jest-regex-util: 26.0.0 + jest-resolve: 26.6.2 + jest-snapshot: 26.6.2 + jest-util: 26.6.2 + jest-validate: 26.6.2 + slash: 3.0.0 + strip-bom: 4.0.0 + yargs: 15.4.1 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + jest-serializer@24.9.0: {} jest-serializer@26.6.2: diff --git a/common/config/rush/version-policies.json b/common/config/rush/version-policies.json index 73a89a49f..7e5ef3d21 100644 --- a/common/config/rush/version-policies.json +++ b/common/config/rush/version-policies.json @@ -1 +1 @@ -[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"1.0.0","nextBump":"major"}] +[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"1.0.1","nextBump":"patch"}] diff --git a/docs/package.json b/docs/package.json index 191926e6e..d998d806e 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,7 +13,7 @@ "@visactor/vchart": "1.3.0", "@visactor/vutils": "1.0.6", "@visactor/vgrammar": "~0.5.7", - "@visactor/vrender": "workspace:1.0.0", + "@visactor/vrender": "workspace:1.0.1", "markdown-it": "^13.0.0", "highlight.js": "^11.8.0", "axios": "^1.4.0", diff --git a/packages/react-vrender-utils/CHANGELOG.json b/packages/react-vrender-utils/CHANGELOG.json index 6c45ad5be..684abd46d 100644 --- a/packages/react-vrender-utils/CHANGELOG.json +++ b/packages/react-vrender-utils/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/react-vrender-utils", "entries": [ + { + "version": "1.0.1", + "tag": "@visactor/react-vrender-utils_v1.0.1", + "date": "Mon, 09 Jun 2025 07:29:07 GMT", + "comments": {} + }, { "version": "1.0.0", "tag": "@visactor/react-vrender-utils_v1.0.0", diff --git a/packages/react-vrender-utils/CHANGELOG.md b/packages/react-vrender-utils/CHANGELOG.md index 12ac320b1..32cb33035 100644 --- a/packages/react-vrender-utils/CHANGELOG.md +++ b/packages/react-vrender-utils/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/react-vrender-utils -This log was last generated on Fri, 06 Jun 2025 09:38:19 GMT and should not be manually modified. +This log was last generated on Mon, 09 Jun 2025 07:29:07 GMT and should not be manually modified. + +## 1.0.1 +Mon, 09 Jun 2025 07:29:07 GMT + +_Version update only_ ## 1.0.0 Fri, 06 Jun 2025 09:38:19 GMT diff --git a/packages/react-vrender-utils/package.json b/packages/react-vrender-utils/package.json index 413ea2ace..b6aea7f3a 100644 --- a/packages/react-vrender-utils/package.json +++ b/packages/react-vrender-utils/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender-utils", - "version": "1.0.0", + "version": "1.0.1", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -24,8 +24,8 @@ "react-dom": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:1.0.0", - "@visactor/react-vrender": "workspace:1.0.0", + "@visactor/vrender": "workspace:1.0.1", + "@visactor/react-vrender": "workspace:1.0.1", "@visactor/vutils": "1.0.6", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/react-vrender/CHANGELOG.json b/packages/react-vrender/CHANGELOG.json index d35e79b99..880ad1843 100644 --- a/packages/react-vrender/CHANGELOG.json +++ b/packages/react-vrender/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/react-vrender", "entries": [ + { + "version": "1.0.1", + "tag": "@visactor/react-vrender_v1.0.1", + "date": "Mon, 09 Jun 2025 07:29:07 GMT", + "comments": {} + }, { "version": "1.0.0", "tag": "@visactor/react-vrender_v1.0.0", diff --git a/packages/react-vrender/CHANGELOG.md b/packages/react-vrender/CHANGELOG.md index 2cc3bb0c3..5d3417ae4 100644 --- a/packages/react-vrender/CHANGELOG.md +++ b/packages/react-vrender/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/react-vrender -This log was last generated on Fri, 06 Jun 2025 09:38:19 GMT and should not be manually modified. +This log was last generated on Mon, 09 Jun 2025 07:29:07 GMT and should not be manually modified. + +## 1.0.1 +Mon, 09 Jun 2025 07:29:07 GMT + +_Version update only_ ## 1.0.0 Fri, 06 Jun 2025 09:38:19 GMT diff --git a/packages/react-vrender/package.json b/packages/react-vrender/package.json index bab62648c..d13c8ce4e 100644 --- a/packages/react-vrender/package.json +++ b/packages/react-vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender", - "version": "1.0.0", + "version": "1.0.1", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -23,7 +23,7 @@ "react": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:1.0.0", + "@visactor/vrender": "workspace:1.0.1", "@visactor/vutils": "1.0.6", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/vrender-animate/CHANGELOG.json b/packages/vrender-animate/CHANGELOG.json index a40ed80c8..8c58bc0d2 100644 --- a/packages/vrender-animate/CHANGELOG.json +++ b/packages/vrender-animate/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender-animate", "entries": [ + { + "version": "1.0.1", + "tag": "@visactor/vrender-animate_v1.0.1", + "date": "Mon, 09 Jun 2025 07:29:07 GMT", + "comments": {} + }, { "version": "1.0.0", "tag": "@visactor/vrender-animate_v1.0.0", diff --git a/packages/vrender-animate/CHANGELOG.md b/packages/vrender-animate/CHANGELOG.md index 2ec73f229..91b59494b 100644 --- a/packages/vrender-animate/CHANGELOG.md +++ b/packages/vrender-animate/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender-animate -This log was last generated on Fri, 06 Jun 2025 09:38:19 GMT and should not be manually modified. +This log was last generated on Mon, 09 Jun 2025 07:29:07 GMT and should not be manually modified. + +## 1.0.1 +Mon, 09 Jun 2025 07:29:07 GMT + +_Version update only_ ## 1.0.0 Fri, 06 Jun 2025 09:38:19 GMT diff --git a/packages/vrender-animate/package.json b/packages/vrender-animate/package.json index d498c105f..5c2ad6966 100644 --- a/packages/vrender-animate/package.json +++ b/packages/vrender-animate/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-animate", - "version": "1.0.0", + "version": "1.0.1", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -21,7 +21,7 @@ }, "dependencies": { "@visactor/vutils": "1.0.6", - "@visactor/vrender-core": "workspace:1.0.0" + "@visactor/vrender-core": "workspace:1.0.1" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-components/CHANGELOG.json b/packages/vrender-components/CHANGELOG.json index 7d6f19b80..146bdb021 100644 --- a/packages/vrender-components/CHANGELOG.json +++ b/packages/vrender-components/CHANGELOG.json @@ -1,6 +1,27 @@ { "name": "@visactor/vrender-components", "entries": [ + { + "version": "1.0.1", + "tag": "@visactor/vrender-components_v1.0.1", + "date": "Mon, 09 Jun 2025 07:29:07 GMT", + "comments": { + "none": [ + { + "comment": "fix: brush active problem. fix visactor/vchart#4017" + }, + { + "comment": "fix: brush event pos problem when stage scale" + }, + { + "comment": "fix: use removeAllChild to remove brush mask. fix visactor/vchart#4017" + }, + { + "comment": "fix: datazoom text render error. fix visactor/vchart#4018" + } + ] + } + }, { "version": "1.0.0", "tag": "@visactor/vrender-components_v1.0.0", diff --git a/packages/vrender-components/CHANGELOG.md b/packages/vrender-components/CHANGELOG.md index 4a86b028a..e4cebe2cc 100644 --- a/packages/vrender-components/CHANGELOG.md +++ b/packages/vrender-components/CHANGELOG.md @@ -1,6 +1,16 @@ # Change Log - @visactor/vrender-components -This log was last generated on Fri, 06 Jun 2025 09:38:19 GMT and should not be manually modified. +This log was last generated on Mon, 09 Jun 2025 07:29:07 GMT and should not be manually modified. + +## 1.0.1 +Mon, 09 Jun 2025 07:29:07 GMT + +### Updates + +- fix: brush active problem. fix visactor/vchart#4017 +- fix: brush event pos problem when stage scale +- fix: use removeAllChild to remove brush mask. fix visactor/vchart#4017 +- fix: datazoom text render error. fix visactor/vchart#4018 ## 1.0.0 Fri, 06 Jun 2025 09:38:19 GMT diff --git a/packages/vrender-components/package.json b/packages/vrender-components/package.json index 4f0d0ca4c..ed42ee030 100644 --- a/packages/vrender-components/package.json +++ b/packages/vrender-components/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-components", - "version": "1.0.0", + "version": "1.0.1", "description": "components library for dp visualization", "sideEffects": false, "main": "cjs/index.js", @@ -27,9 +27,9 @@ "dependencies": { "@visactor/vutils": "1.0.6", "@visactor/vscale": "1.0.6", - "@visactor/vrender-core": "workspace:1.0.0", - "@visactor/vrender-kits": "workspace:1.0.0", - "@visactor/vrender-animate": "workspace:1.0.0" + "@visactor/vrender-core": "workspace:1.0.1", + "@visactor/vrender-kits": "workspace:1.0.1", + "@visactor/vrender-animate": "workspace:1.0.1" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-core/CHANGELOG.json b/packages/vrender-core/CHANGELOG.json index 89c55df28..022566af3 100644 --- a/packages/vrender-core/CHANGELOG.json +++ b/packages/vrender-core/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender-core", "entries": [ + { + "version": "1.0.1", + "tag": "@visactor/vrender-core_v1.0.1", + "date": "Mon, 09 Jun 2025 07:29:07 GMT", + "comments": {} + }, { "version": "1.0.0", "tag": "@visactor/vrender-core_v1.0.0", diff --git a/packages/vrender-core/CHANGELOG.md b/packages/vrender-core/CHANGELOG.md index 7c905a707..4393be68f 100644 --- a/packages/vrender-core/CHANGELOG.md +++ b/packages/vrender-core/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender-core -This log was last generated on Fri, 06 Jun 2025 09:38:19 GMT and should not be manually modified. +This log was last generated on Mon, 09 Jun 2025 07:29:07 GMT and should not be manually modified. + +## 1.0.1 +Mon, 09 Jun 2025 07:29:07 GMT + +_Version update only_ ## 1.0.0 Fri, 06 Jun 2025 09:38:19 GMT diff --git a/packages/vrender-core/package.json b/packages/vrender-core/package.json index 8ad9c4e0c..26dad3000 100644 --- a/packages/vrender-core/package.json +++ b/packages/vrender-core/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-core", - "version": "1.0.0", + "version": "1.0.1", "description": "", "sideEffects": [ "./src/modules.ts", diff --git a/packages/vrender-kits/CHANGELOG.json b/packages/vrender-kits/CHANGELOG.json index a27347d4b..daa624b6d 100644 --- a/packages/vrender-kits/CHANGELOG.json +++ b/packages/vrender-kits/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender-kits", "entries": [ + { + "version": "1.0.1", + "tag": "@visactor/vrender-kits_v1.0.1", + "date": "Mon, 09 Jun 2025 07:29:07 GMT", + "comments": {} + }, { "version": "1.0.0", "tag": "@visactor/vrender-kits_v1.0.0", diff --git a/packages/vrender-kits/CHANGELOG.md b/packages/vrender-kits/CHANGELOG.md index 3a556f0e2..18320c597 100644 --- a/packages/vrender-kits/CHANGELOG.md +++ b/packages/vrender-kits/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender-kits -This log was last generated on Fri, 06 Jun 2025 09:38:19 GMT and should not be manually modified. +This log was last generated on Mon, 09 Jun 2025 07:29:07 GMT and should not be manually modified. + +## 1.0.1 +Mon, 09 Jun 2025 07:29:07 GMT + +_Version update only_ ## 1.0.0 Fri, 06 Jun 2025 09:38:19 GMT diff --git a/packages/vrender-kits/package.json b/packages/vrender-kits/package.json index 5c2339768..e29632722 100644 --- a/packages/vrender-kits/package.json +++ b/packages/vrender-kits/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-kits", - "version": "1.0.0", + "version": "1.0.1", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -21,7 +21,7 @@ }, "dependencies": { "@visactor/vutils": "1.0.6", - "@visactor/vrender-core": "workspace:1.0.0", + "@visactor/vrender-core": "workspace:1.0.1", "@resvg/resvg-js": "2.4.1", "roughjs": "4.5.2", "gifuct-js": "2.1.2", diff --git a/packages/vrender/CHANGELOG.json b/packages/vrender/CHANGELOG.json index 1d4fab82f..8585ef31c 100644 --- a/packages/vrender/CHANGELOG.json +++ b/packages/vrender/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender", "entries": [ + { + "version": "1.0.1", + "tag": "@visactor/vrender_v1.0.1", + "date": "Mon, 09 Jun 2025 07:29:07 GMT", + "comments": {} + }, { "version": "1.0.0", "tag": "@visactor/vrender_v1.0.0", diff --git a/packages/vrender/CHANGELOG.md b/packages/vrender/CHANGELOG.md index 579dfe133..31cc388ac 100644 --- a/packages/vrender/CHANGELOG.md +++ b/packages/vrender/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender -This log was last generated on Fri, 06 Jun 2025 09:38:19 GMT and should not be manually modified. +This log was last generated on Mon, 09 Jun 2025 07:29:07 GMT and should not be manually modified. + +## 1.0.1 +Mon, 09 Jun 2025 07:29:07 GMT + +_Version update only_ ## 1.0.0 Fri, 06 Jun 2025 09:38:19 GMT diff --git a/packages/vrender/package.json b/packages/vrender/package.json index e0dd2697e..d3c7aeb05 100644 --- a/packages/vrender/package.json +++ b/packages/vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender", - "version": "1.0.0", + "version": "1.0.1", "description": "", "sideEffects": true, "main": "cjs/index.js", @@ -24,9 +24,9 @@ "test-watch": "cross-env DEBUG_MODE=1 jest --watch" }, "dependencies": { - "@visactor/vrender-core": "workspace:1.0.0", - "@visactor/vrender-kits": "workspace:1.0.0", - "@visactor/vrender-animate": "workspace:1.0.0" + "@visactor/vrender-core": "workspace:1.0.1", + "@visactor/vrender-kits": "workspace:1.0.1", + "@visactor/vrender-animate": "workspace:1.0.1" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/tools/bugserver-trigger/package.json b/tools/bugserver-trigger/package.json index 3e9a7bb44..5a5d6dc84 100644 --- a/tools/bugserver-trigger/package.json +++ b/tools/bugserver-trigger/package.json @@ -8,11 +8,11 @@ "ci": "ts-node --transpileOnly --skipProject ./scripts/trigger-test.ts" }, "dependencies": { - "@visactor/vrender": "workspace:1.0.0", - "@visactor/vrender-core": "workspace:1.0.0", - "@visactor/vrender-kits": "workspace:1.0.0", - "@visactor/vrender-components": "workspace:1.0.0", - "@visactor/vrender-animate": "workspace:1.0.0" + "@visactor/vrender": "workspace:1.0.1", + "@visactor/vrender-core": "workspace:1.0.1", + "@visactor/vrender-kits": "workspace:1.0.1", + "@visactor/vrender-components": "workspace:1.0.1", + "@visactor/vrender-animate": "workspace:1.0.1" }, "devDependencies": { "@rushstack/eslint-patch": "~1.1.4", From 102e6a34110266287b4fbddb9286e7a2eb930fe8 Mon Sep 17 00:00:00 2001 From: xile611 Date: Tue, 10 Jun 2025 15:20:02 +0800 Subject: [PATCH 160/179] fix: fix aniamtion of dx, dy and customParameters --- packages/vrender-animate/src/custom/fromTo.ts | 4 ++-- .../vrender-animate/src/executor/animate-executor.ts | 5 ++++- packages/vrender-animate/src/interpolate/store.ts | 10 ++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/vrender-animate/src/custom/fromTo.ts b/packages/vrender-animate/src/custom/fromTo.ts index 4f325e432..c16ed83b5 100644 --- a/packages/vrender-animate/src/custom/fromTo.ts +++ b/packages/vrender-animate/src/custom/fromTo.ts @@ -14,14 +14,14 @@ export class FromTo extends ACustomAnimate> { onBind(): void { super.onBind(); - const finalAttribute = this.target.getFinalAttribute(); // 如果存在from,不存在to,那么需要设置给props Object.keys(this.from).forEach(key => { if (this.props[key] == null) { - this.props[key] = finalAttribute[key]; + this.props[key] = this.target.getGraphicAttribute(key); } }); + const finalAttribute = this.target.getFinalAttribute(); // 如果入场动画,那么需要设置属性 if (this.target.context?.animationState === 'appear') { if (finalAttribute) { diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index b288e0e0a..e3b4270b7 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -420,7 +420,10 @@ export class AnimateExecutor implements IAnimateExecutor { ) { // 处理自定义动画 if (custom && customType) { - const customParams = this.resolveValue(customParameters, graphic, {}); + const customParams = this.resolveValue(customParameters, graphic, { + width: graphic.stage.width, + height: graphic.stage.height + }); const objOptions = isFunction(options) ? options.call( null, diff --git a/packages/vrender-animate/src/interpolate/store.ts b/packages/vrender-animate/src/interpolate/store.ts index a17451169..3d6ddeb20 100644 --- a/packages/vrender-animate/src/interpolate/store.ts +++ b/packages/vrender-animate/src/interpolate/store.ts @@ -147,6 +147,16 @@ export class InterpolateUpdateStore { target.addUpdateBoundTag(); target.addUpdatePositionTag(); }; + dx = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + target.attribute.dx = interpolateNumber(from, to, ratio); + target.addUpdateBoundTag(); + target.addUpdatePositionTag(); + }; + dy = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { + target.attribute.dy = interpolateNumber(from, to, ratio); + target.addUpdateBoundTag(); + target.addUpdatePositionTag(); + }; angle = (key: string, from: number, to: number, ratio: number, step: IStep, target: IGraphic) => { target.attribute.angle = interpolateNumber(from, to, ratio); target.addUpdateBoundTag(); From 5d0e283e1554e79b516588df3ec72b48b3ee7642 Mon Sep 17 00:00:00 2001 From: xile611 Date: Tue, 10 Jun 2025 15:20:37 +0800 Subject: [PATCH 161/179] docs: update changlog of rush --- ...-dx-dy-animator-customParams_2025-06-10-07-20.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@visactor/vrender-animate/fix-dx-dy-animator-customParams_2025-06-10-07-20.json diff --git a/common/changes/@visactor/vrender-animate/fix-dx-dy-animator-customParams_2025-06-10-07-20.json b/common/changes/@visactor/vrender-animate/fix-dx-dy-animator-customParams_2025-06-10-07-20.json new file mode 100644 index 000000000..eec79adeb --- /dev/null +++ b/common/changes/@visactor/vrender-animate/fix-dx-dy-animator-customParams_2025-06-10-07-20.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: fix aniamtion of dx, dy and customParameters\n\n", + "type": "none", + "packageName": "@visactor/vrender-animate" + } + ], + "packageName": "@visactor/vrender-animate", + "email": "dingling112@gmail.com" +} \ No newline at end of file From 6a72a36f0f92d29096acfff66e103c98b9124c64 Mon Sep 17 00:00:00 2001 From: xile611 Date: Tue, 10 Jun 2025 17:12:18 +0800 Subject: [PATCH 162/179] chore: update tsconfig of vrender --- packages/vrender/tsconfig.eslint.json | 3 +++ packages/vrender/tsconfig.json | 3 +++ packages/vrender/tsconfig.test.json | 3 ++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/vrender/tsconfig.eslint.json b/packages/vrender/tsconfig.eslint.json index d5f1b26ba..52c55f957 100644 --- a/packages/vrender/tsconfig.eslint.json +++ b/packages/vrender/tsconfig.eslint.json @@ -17,6 +17,9 @@ }, { "path": "../vrender-kits" + }, + { + "path": "../vrender-aniamte" } ] } diff --git a/packages/vrender/tsconfig.json b/packages/vrender/tsconfig.json index fa6e522b7..95836238d 100644 --- a/packages/vrender/tsconfig.json +++ b/packages/vrender/tsconfig.json @@ -16,6 +16,9 @@ }, { "path": "../vrender-kits" + }, + { + "path": "../vrender-animate" } ] } diff --git a/packages/vrender/tsconfig.test.json b/packages/vrender/tsconfig.test.json index cc04987da..c0f87af17 100644 --- a/packages/vrender/tsconfig.test.json +++ b/packages/vrender/tsconfig.test.json @@ -3,7 +3,8 @@ "compilerOptions": { "paths": { "@visactor/vrender-core": ["../vrender-core/src"], - "@visactor/vrender-kits": ["../vrender-kits/src"] + "@visactor/vrender-kits": ["../vrender-kits/src"], + "@visactor/vrender-animate": ["../vrender-aniamte"] } }, "references": [] From af3de85bbd14612b68876df9587a933a61f55f37 Mon Sep 17 00:00:00 2001 From: xile611 Date: Tue, 10 Jun 2025 17:18:43 +0800 Subject: [PATCH 163/179] chore: update version --- .github/workflows/release.yml | 2 +- common/config/rush/version-policies.json | 2 +- docs/package.json | 2 +- packages/react-vrender-utils/package.json | 6 +++--- packages/react-vrender/package.json | 4 ++-- packages/vrender-animate/package.json | 4 ++-- packages/vrender-components/package.json | 8 ++++---- packages/vrender-core/package.json | 2 +- packages/vrender-kits/package.json | 4 ++-- packages/vrender/package.json | 8 ++++---- tools/bugserver-trigger/package.json | 10 +++++----- 11 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 496d6626e..c2bb0245c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -88,7 +88,7 @@ jobs: env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} NPM_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} - run: node common/scripts/install-run-rush.js publish --publish --include-all + run: node common/scripts/install-run-rush.js publish --publish --include-all --tag latest - name: Update shrinkwrap run: node common/scripts/install-run-rush.js update diff --git a/common/config/rush/version-policies.json b/common/config/rush/version-policies.json index 7e5ef3d21..c042b9410 100644 --- a/common/config/rush/version-policies.json +++ b/common/config/rush/version-policies.json @@ -1 +1 @@ -[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"1.0.1","nextBump":"patch"}] +[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"1.0.2","nextBump":"patch"}] diff --git a/docs/package.json b/docs/package.json index d998d806e..275971a5b 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,7 +13,7 @@ "@visactor/vchart": "1.3.0", "@visactor/vutils": "1.0.6", "@visactor/vgrammar": "~0.5.7", - "@visactor/vrender": "workspace:1.0.1", + "@visactor/vrender": "workspace:1.0.2", "markdown-it": "^13.0.0", "highlight.js": "^11.8.0", "axios": "^1.4.0", diff --git a/packages/react-vrender-utils/package.json b/packages/react-vrender-utils/package.json index b6aea7f3a..7274f5457 100644 --- a/packages/react-vrender-utils/package.json +++ b/packages/react-vrender-utils/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender-utils", - "version": "1.0.1", + "version": "1.0.2", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -24,8 +24,8 @@ "react-dom": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:1.0.1", - "@visactor/react-vrender": "workspace:1.0.1", + "@visactor/vrender": "workspace:1.0.2", + "@visactor/react-vrender": "workspace:1.0.2", "@visactor/vutils": "1.0.6", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/react-vrender/package.json b/packages/react-vrender/package.json index d13c8ce4e..39a6df87a 100644 --- a/packages/react-vrender/package.json +++ b/packages/react-vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender", - "version": "1.0.1", + "version": "1.0.2", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -23,7 +23,7 @@ "react": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:1.0.1", + "@visactor/vrender": "workspace:1.0.2", "@visactor/vutils": "1.0.6", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/vrender-animate/package.json b/packages/vrender-animate/package.json index 5c2ad6966..9a47db220 100644 --- a/packages/vrender-animate/package.json +++ b/packages/vrender-animate/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-animate", - "version": "1.0.1", + "version": "1.0.2", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -21,7 +21,7 @@ }, "dependencies": { "@visactor/vutils": "1.0.6", - "@visactor/vrender-core": "workspace:1.0.1" + "@visactor/vrender-core": "workspace:1.0.2" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-components/package.json b/packages/vrender-components/package.json index ed42ee030..700699fe0 100644 --- a/packages/vrender-components/package.json +++ b/packages/vrender-components/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-components", - "version": "1.0.1", + "version": "1.0.2", "description": "components library for dp visualization", "sideEffects": false, "main": "cjs/index.js", @@ -27,9 +27,9 @@ "dependencies": { "@visactor/vutils": "1.0.6", "@visactor/vscale": "1.0.6", - "@visactor/vrender-core": "workspace:1.0.1", - "@visactor/vrender-kits": "workspace:1.0.1", - "@visactor/vrender-animate": "workspace:1.0.1" + "@visactor/vrender-core": "workspace:1.0.2", + "@visactor/vrender-kits": "workspace:1.0.2", + "@visactor/vrender-animate": "workspace:1.0.2" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-core/package.json b/packages/vrender-core/package.json index 26dad3000..77423dc97 100644 --- a/packages/vrender-core/package.json +++ b/packages/vrender-core/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-core", - "version": "1.0.1", + "version": "1.0.2", "description": "", "sideEffects": [ "./src/modules.ts", diff --git a/packages/vrender-kits/package.json b/packages/vrender-kits/package.json index e29632722..47e7496a7 100644 --- a/packages/vrender-kits/package.json +++ b/packages/vrender-kits/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-kits", - "version": "1.0.1", + "version": "1.0.2", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -21,7 +21,7 @@ }, "dependencies": { "@visactor/vutils": "1.0.6", - "@visactor/vrender-core": "workspace:1.0.1", + "@visactor/vrender-core": "workspace:1.0.2", "@resvg/resvg-js": "2.4.1", "roughjs": "4.5.2", "gifuct-js": "2.1.2", diff --git a/packages/vrender/package.json b/packages/vrender/package.json index d3c7aeb05..c30ff2ef0 100644 --- a/packages/vrender/package.json +++ b/packages/vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender", - "version": "1.0.1", + "version": "1.0.2", "description": "", "sideEffects": true, "main": "cjs/index.js", @@ -24,9 +24,9 @@ "test-watch": "cross-env DEBUG_MODE=1 jest --watch" }, "dependencies": { - "@visactor/vrender-core": "workspace:1.0.1", - "@visactor/vrender-kits": "workspace:1.0.1", - "@visactor/vrender-animate": "workspace:1.0.1" + "@visactor/vrender-core": "workspace:1.0.2", + "@visactor/vrender-kits": "workspace:1.0.2", + "@visactor/vrender-animate": "workspace:1.0.2" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/tools/bugserver-trigger/package.json b/tools/bugserver-trigger/package.json index 5a5d6dc84..3f6e30527 100644 --- a/tools/bugserver-trigger/package.json +++ b/tools/bugserver-trigger/package.json @@ -8,11 +8,11 @@ "ci": "ts-node --transpileOnly --skipProject ./scripts/trigger-test.ts" }, "dependencies": { - "@visactor/vrender": "workspace:1.0.1", - "@visactor/vrender-core": "workspace:1.0.1", - "@visactor/vrender-kits": "workspace:1.0.1", - "@visactor/vrender-components": "workspace:1.0.1", - "@visactor/vrender-animate": "workspace:1.0.1" + "@visactor/vrender": "workspace:1.0.2", + "@visactor/vrender-core": "workspace:1.0.2", + "@visactor/vrender-kits": "workspace:1.0.2", + "@visactor/vrender-components": "workspace:1.0.2", + "@visactor/vrender-animate": "workspace:1.0.2" }, "devDependencies": { "@rushstack/eslint-patch": "~1.1.4", From d674b7aeda335581c95ba0407df7e6721ecfb4ce Mon Sep 17 00:00:00 2001 From: xile611 Date: Tue, 10 Jun 2025 17:22:24 +0800 Subject: [PATCH 164/179] docs: update changelog of rush --- .../release-1.0.3_2025-06-10-09-22.json | 10 ++++++++++ .../react-vrender/release-1.0.3_2025-06-10-09-22.json | 10 ++++++++++ .../release-1.0.3_2025-06-10-09-22.json | 10 ++++++++++ .../release-1.0.3_2025-06-10-09-22.json | 10 ++++++++++ .../vrender-core/release-1.0.3_2025-06-10-09-22.json | 10 ++++++++++ .../vrender-kits/release-1.0.3_2025-06-10-09-22.json | 10 ++++++++++ .../vrender/release-1.0.3_2025-06-10-09-22.json | 10 ++++++++++ 7 files changed, 70 insertions(+) create mode 100644 common/changes/@visactor/react-vrender-utils/release-1.0.3_2025-06-10-09-22.json create mode 100644 common/changes/@visactor/react-vrender/release-1.0.3_2025-06-10-09-22.json create mode 100644 common/changes/@visactor/vrender-animate/release-1.0.3_2025-06-10-09-22.json create mode 100644 common/changes/@visactor/vrender-components/release-1.0.3_2025-06-10-09-22.json create mode 100644 common/changes/@visactor/vrender-core/release-1.0.3_2025-06-10-09-22.json create mode 100644 common/changes/@visactor/vrender-kits/release-1.0.3_2025-06-10-09-22.json create mode 100644 common/changes/@visactor/vrender/release-1.0.3_2025-06-10-09-22.json diff --git a/common/changes/@visactor/react-vrender-utils/release-1.0.3_2025-06-10-09-22.json b/common/changes/@visactor/react-vrender-utils/release-1.0.3_2025-06-10-09-22.json new file mode 100644 index 000000000..c028c4ca0 --- /dev/null +++ b/common/changes/@visactor/react-vrender-utils/release-1.0.3_2025-06-10-09-22.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/react-vrender-utils", + "comment": "fix: fix rele", + "type": "none" + } + ], + "packageName": "@visactor/react-vrender-utils" +} \ No newline at end of file diff --git a/common/changes/@visactor/react-vrender/release-1.0.3_2025-06-10-09-22.json b/common/changes/@visactor/react-vrender/release-1.0.3_2025-06-10-09-22.json new file mode 100644 index 000000000..4d9388066 --- /dev/null +++ b/common/changes/@visactor/react-vrender/release-1.0.3_2025-06-10-09-22.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/react-vrender", + "comment": "fix: fix release version", + "type": "none" + } + ], + "packageName": "@visactor/react-vrender" +} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-animate/release-1.0.3_2025-06-10-09-22.json b/common/changes/@visactor/vrender-animate/release-1.0.3_2025-06-10-09-22.json new file mode 100644 index 000000000..60d9beaff --- /dev/null +++ b/common/changes/@visactor/vrender-animate/release-1.0.3_2025-06-10-09-22.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-animate", + "comment": "fix: fix rele", + "type": "none" + } + ], + "packageName": "@visactor/vrender-animate" +} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-components/release-1.0.3_2025-06-10-09-22.json b/common/changes/@visactor/vrender-components/release-1.0.3_2025-06-10-09-22.json new file mode 100644 index 000000000..231657b23 --- /dev/null +++ b/common/changes/@visactor/vrender-components/release-1.0.3_2025-06-10-09-22.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-components", + "comment": "fix: fix rele", + "type": "none" + } + ], + "packageName": "@visactor/vrender-components" +} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-core/release-1.0.3_2025-06-10-09-22.json b/common/changes/@visactor/vrender-core/release-1.0.3_2025-06-10-09-22.json new file mode 100644 index 000000000..9aa25cf6a --- /dev/null +++ b/common/changes/@visactor/vrender-core/release-1.0.3_2025-06-10-09-22.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-core", + "comment": "fix: fix rele", + "type": "none" + } + ], + "packageName": "@visactor/vrender-core" +} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-kits/release-1.0.3_2025-06-10-09-22.json b/common/changes/@visactor/vrender-kits/release-1.0.3_2025-06-10-09-22.json new file mode 100644 index 000000000..cd075886f --- /dev/null +++ b/common/changes/@visactor/vrender-kits/release-1.0.3_2025-06-10-09-22.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-kits", + "comment": "fix: fix rele", + "type": "none" + } + ], + "packageName": "@visactor/vrender-kits" +} \ No newline at end of file diff --git a/common/changes/@visactor/vrender/release-1.0.3_2025-06-10-09-22.json b/common/changes/@visactor/vrender/release-1.0.3_2025-06-10-09-22.json new file mode 100644 index 000000000..ea97d533b --- /dev/null +++ b/common/changes/@visactor/vrender/release-1.0.3_2025-06-10-09-22.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender", + "comment": "fix: fix rele", + "type": "none" + } + ], + "packageName": "@visactor/vrender" +} \ No newline at end of file From 002c43db6c83305c182b25f95b62c5420f3eb22e Mon Sep 17 00:00:00 2001 From: xile611 Date: Tue, 10 Jun 2025 17:28:55 +0800 Subject: [PATCH 165/179] chore: update pnpm-lock.yaml --- common/config/rush/pnpm-lock.yaml | 127 +++++++----------------------- 1 file changed, 28 insertions(+), 99 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 10c2c8f72..b2518f82d 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: specifier: ~0.5.7 version: 0.5.7 '@visactor/vrender': - specifier: workspace:1.0.1 + specifier: workspace:1.0.2 version: link:../packages/vrender '@visactor/vutils': specifier: 1.0.6 @@ -95,7 +95,7 @@ importers: ../../packages/react-vrender: dependencies: '@visactor/vrender': - specifier: workspace:1.0.1 + specifier: workspace:1.0.2 version: link:../vrender '@visactor/vutils': specifier: 1.0.6 @@ -153,10 +153,10 @@ importers: ../../packages/react-vrender-utils: dependencies: '@visactor/react-vrender': - specifier: workspace:1.0.1 + specifier: workspace:1.0.2 version: link:../react-vrender '@visactor/vrender': - specifier: workspace:1.0.1 + specifier: workspace:1.0.2 version: link:../vrender '@visactor/vutils': specifier: 1.0.6 @@ -211,13 +211,13 @@ importers: ../../packages/vrender: dependencies: '@visactor/vrender-animate': - specifier: workspace:1.0.1 + specifier: workspace:1.0.2 version: link:../vrender-animate '@visactor/vrender-core': - specifier: workspace:1.0.1 + specifier: workspace:1.0.2 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:1.0.1 + specifier: workspace:1.0.2 version: link:../vrender-kits devDependencies: '@internal/bundler': @@ -284,7 +284,7 @@ importers: ../../packages/vrender-animate: dependencies: '@visactor/vrender-core': - specifier: workspace:1.0.1 + specifier: workspace:1.0.2 version: link:../vrender-core '@visactor/vutils': specifier: 1.0.6 @@ -342,13 +342,13 @@ importers: ../../packages/vrender-components: dependencies: '@visactor/vrender-animate': - specifier: workspace:1.0.1 + specifier: workspace:1.0.2 version: link:../vrender-animate '@visactor/vrender-core': - specifier: workspace:1.0.1 + specifier: workspace:1.0.2 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:1.0.1 + specifier: workspace:1.0.2 version: link:../vrender-kits '@visactor/vscale': specifier: 1.0.6 @@ -467,7 +467,7 @@ importers: specifier: 2.4.1 version: 2.4.1 '@visactor/vrender-core': - specifier: workspace:1.0.1 + specifier: workspace:1.0.2 version: link:../vrender-core '@visactor/vutils': specifier: 1.0.6 @@ -583,19 +583,19 @@ importers: ../../tools/bugserver-trigger: dependencies: '@visactor/vrender': - specifier: workspace:1.0.1 + specifier: workspace:1.0.2 version: link:../../packages/vrender '@visactor/vrender-animate': - specifier: workspace:1.0.1 + specifier: workspace:1.0.2 version: link:../../packages/vrender-animate '@visactor/vrender-components': - specifier: workspace:1.0.1 + specifier: workspace:1.0.2 version: link:../../packages/vrender-components '@visactor/vrender-core': - specifier: workspace:1.0.1 + specifier: workspace:1.0.2 version: link:../../packages/vrender-core '@visactor/vrender-kits': - specifier: workspace:1.0.1 + specifier: workspace:1.0.2 version: link:../../packages/vrender-kits devDependencies: '@internal/bundler': @@ -7957,7 +7957,7 @@ snapshots: jest-resolve: 26.6.2 jest-resolve-dependencies: 26.6.3 jest-runner: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) - jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-runtime: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) jest-snapshot: 26.6.2 jest-util: 26.6.2 jest-validate: 26.6.2 @@ -8076,27 +8076,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@jest/test-sequencer@26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5))': + '@jest/test-sequencer@26.6.3': dependencies: '@jest/test-result': 26.6.2 graceful-fs: 4.2.11 jest-haste-map: 26.6.2 - jest-runner: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-runner: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) jest-runtime: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) transitivePeerDependencies: - - bufferutil - - canvas - supports-color - - ts-node - - utf-8-validate - '@jest/test-sequencer@26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5))': + '@jest/test-sequencer@26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5))': dependencies: '@jest/test-result': 26.6.2 graceful-fs: 4.2.11 jest-haste-map: 26.6.2 - jest-runner: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) - jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-runner: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-runtime: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) transitivePeerDependencies: - bufferutil - canvas @@ -11517,7 +11513,7 @@ snapshots: jest-environment-jsdom: 26.6.2(canvas@2.11.2) jest-environment-node: 26.6.2 jest-get-type: 26.3.0 - jest-jasmine2: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-jasmine2: 26.6.3 jest-regex-util: 26.0.0 jest-resolve: 26.6.2 jest-util: 26.6.2 @@ -11535,7 +11531,7 @@ snapshots: jest-config@26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)): dependencies: '@babel/core': 7.20.12 - '@jest/test-sequencer': 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + '@jest/test-sequencer': 26.6.3 '@jest/types': 26.6.2 babel-jest: 26.6.3(@babel/core@7.20.12) chalk: 4.1.2 @@ -11545,7 +11541,7 @@ snapshots: jest-environment-jsdom: 26.6.2(canvas@2.11.2) jest-environment-node: 26.6.2 jest-get-type: 26.3.0 - jest-jasmine2: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-jasmine2: 26.6.3 jest-regex-util: 26.0.0 jest-resolve: 26.6.2 jest-util: 26.6.2 @@ -11750,7 +11746,7 @@ snapshots: transitivePeerDependencies: - supports-color - jest-jasmine2@26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)): + jest-jasmine2@26.6.3: dependencies: '@babel/traverse': 7.27.4 '@jest/environment': 26.6.2 @@ -11771,38 +11767,7 @@ snapshots: pretty-format: 26.6.2 throat: 5.0.0 transitivePeerDependencies: - - bufferutil - - canvas - supports-color - - ts-node - - utf-8-validate - - jest-jasmine2@26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)): - dependencies: - '@babel/traverse': 7.27.4 - '@jest/environment': 26.6.2 - '@jest/source-map': 26.6.2 - '@jest/test-result': 26.6.2 - '@jest/types': 26.6.2 - '@types/node': 22.15.30 - chalk: 4.1.2 - co: 4.6.0 - expect: 26.6.2 - is-generator-fn: 2.1.0 - jest-each: 26.6.2 - jest-matcher-utils: 26.6.2 - jest-message-util: 26.6.2 - jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) - jest-snapshot: 26.6.2 - jest-util: 26.6.2 - pretty-format: 26.6.2 - throat: 5.0.0 - transitivePeerDependencies: - - bufferutil - - canvas - - supports-color - - ts-node - - utf-8-validate jest-leak-detector@24.9.0: dependencies: @@ -11974,7 +11939,7 @@ snapshots: jest-leak-detector: 26.6.2 jest-message-util: 26.6.2 jest-resolve: 26.6.2 - jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-runtime: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) jest-util: 26.6.2 jest-worker: 26.6.2 source-map-support: 0.5.21 @@ -12050,42 +12015,6 @@ snapshots: - ts-node - utf-8-validate - jest-runtime@26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)): - dependencies: - '@jest/console': 26.6.2 - '@jest/environment': 26.6.2 - '@jest/fake-timers': 26.6.2 - '@jest/globals': 26.6.2 - '@jest/source-map': 26.6.2 - '@jest/test-result': 26.6.2 - '@jest/transform': 26.6.2 - '@jest/types': 26.6.2 - '@types/yargs': 15.0.19 - chalk: 4.1.2 - cjs-module-lexer: 0.6.0 - collect-v8-coverage: 1.0.2 - exit: 0.1.2 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-config: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) - jest-haste-map: 26.6.2 - jest-message-util: 26.6.2 - jest-mock: 26.6.2 - jest-regex-util: 26.0.0 - jest-resolve: 26.6.2 - jest-snapshot: 26.6.2 - jest-util: 26.6.2 - jest-validate: 26.6.2 - slash: 3.0.0 - strip-bom: 4.0.0 - yargs: 15.4.1 - transitivePeerDependencies: - - bufferutil - - canvas - - supports-color - - ts-node - - utf-8-validate - jest-serializer@24.9.0: {} jest-serializer@26.6.2: From 9bf6b8b767f3bd8ad9ab47ed681eb6ff48a8adc6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 10 Jun 2025 09:39:51 +0000 Subject: [PATCH 166/179] build: prelease version 1.0.3 --- .../release-1.0.3_2025-06-10-09-22.json | 10 -- .../release-1.0.3_2025-06-10-09-22.json | 10 -- ...nimator-customParams_2025-06-10-07-20.json | 11 -- .../release-1.0.3_2025-06-10-09-22.json | 10 -- ...pling-error-for-rich_2025-06-06-03-14.json | 11 -- .../release-1.0.3_2025-06-10-09-22.json | 10 -- .../release-1.0.3_2025-06-10-09-22.json | 10 -- .../release-1.0.3_2025-06-10-09-22.json | 10 -- .../release-1.0.3_2025-06-10-09-22.json | 10 -- common/config/rush/pnpm-lock.yaml | 127 ++++++++++++++---- common/config/rush/version-policies.json | 2 +- docs/package.json | 2 +- packages/react-vrender-utils/CHANGELOG.json | 12 ++ packages/react-vrender-utils/CHANGELOG.md | 9 +- packages/react-vrender-utils/package.json | 6 +- packages/react-vrender/CHANGELOG.json | 12 ++ packages/react-vrender/CHANGELOG.md | 9 +- packages/react-vrender/package.json | 4 +- packages/vrender-animate/CHANGELOG.json | 15 +++ packages/vrender-animate/CHANGELOG.md | 12 +- packages/vrender-animate/package.json | 4 +- packages/vrender-components/CHANGELOG.json | 15 +++ packages/vrender-components/CHANGELOG.md | 12 +- packages/vrender-components/package.json | 8 +- packages/vrender-core/CHANGELOG.json | 12 ++ packages/vrender-core/CHANGELOG.md | 9 +- packages/vrender-core/package.json | 2 +- packages/vrender-kits/CHANGELOG.json | 12 ++ packages/vrender-kits/CHANGELOG.md | 9 +- packages/vrender-kits/package.json | 4 +- packages/vrender/CHANGELOG.json | 12 ++ packages/vrender/CHANGELOG.md | 9 +- packages/vrender/package.json | 8 +- tools/bugserver-trigger/package.json | 10 +- 34 files changed, 276 insertions(+), 152 deletions(-) delete mode 100644 common/changes/@visactor/react-vrender-utils/release-1.0.3_2025-06-10-09-22.json delete mode 100644 common/changes/@visactor/react-vrender/release-1.0.3_2025-06-10-09-22.json delete mode 100644 common/changes/@visactor/vrender-animate/fix-dx-dy-animator-customParams_2025-06-10-07-20.json delete mode 100644 common/changes/@visactor/vrender-animate/release-1.0.3_2025-06-10-09-22.json delete mode 100644 common/changes/@visactor/vrender-components/fix-label-sampling-error-for-rich_2025-06-06-03-14.json delete mode 100644 common/changes/@visactor/vrender-components/release-1.0.3_2025-06-10-09-22.json delete mode 100644 common/changes/@visactor/vrender-core/release-1.0.3_2025-06-10-09-22.json delete mode 100644 common/changes/@visactor/vrender-kits/release-1.0.3_2025-06-10-09-22.json delete mode 100644 common/changes/@visactor/vrender/release-1.0.3_2025-06-10-09-22.json diff --git a/common/changes/@visactor/react-vrender-utils/release-1.0.3_2025-06-10-09-22.json b/common/changes/@visactor/react-vrender-utils/release-1.0.3_2025-06-10-09-22.json deleted file mode 100644 index c028c4ca0..000000000 --- a/common/changes/@visactor/react-vrender-utils/release-1.0.3_2025-06-10-09-22.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/react-vrender-utils", - "comment": "fix: fix rele", - "type": "none" - } - ], - "packageName": "@visactor/react-vrender-utils" -} \ No newline at end of file diff --git a/common/changes/@visactor/react-vrender/release-1.0.3_2025-06-10-09-22.json b/common/changes/@visactor/react-vrender/release-1.0.3_2025-06-10-09-22.json deleted file mode 100644 index 4d9388066..000000000 --- a/common/changes/@visactor/react-vrender/release-1.0.3_2025-06-10-09-22.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/react-vrender", - "comment": "fix: fix release version", - "type": "none" - } - ], - "packageName": "@visactor/react-vrender" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-animate/fix-dx-dy-animator-customParams_2025-06-10-07-20.json b/common/changes/@visactor/vrender-animate/fix-dx-dy-animator-customParams_2025-06-10-07-20.json deleted file mode 100644 index eec79adeb..000000000 --- a/common/changes/@visactor/vrender-animate/fix-dx-dy-animator-customParams_2025-06-10-07-20.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "changes": [ - { - "comment": "fix: fix aniamtion of dx, dy and customParameters\n\n", - "type": "none", - "packageName": "@visactor/vrender-animate" - } - ], - "packageName": "@visactor/vrender-animate", - "email": "dingling112@gmail.com" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-animate/release-1.0.3_2025-06-10-09-22.json b/common/changes/@visactor/vrender-animate/release-1.0.3_2025-06-10-09-22.json deleted file mode 100644 index 60d9beaff..000000000 --- a/common/changes/@visactor/vrender-animate/release-1.0.3_2025-06-10-09-22.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender-animate", - "comment": "fix: fix rele", - "type": "none" - } - ], - "packageName": "@visactor/vrender-animate" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-components/fix-label-sampling-error-for-rich_2025-06-06-03-14.json b/common/changes/@visactor/vrender-components/fix-label-sampling-error-for-rich_2025-06-06-03-14.json deleted file mode 100644 index 7139a5153..000000000 --- a/common/changes/@visactor/vrender-components/fix-label-sampling-error-for-rich_2025-06-06-03-14.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "changes": [ - { - "comment": "fix: disable sampling when label is rich text\n\n", - "type": "none", - "packageName": "@visactor/vrender-components" - } - ], - "packageName": "@visactor/vrender-components", - "email": "dingling112@gmail.com" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-components/release-1.0.3_2025-06-10-09-22.json b/common/changes/@visactor/vrender-components/release-1.0.3_2025-06-10-09-22.json deleted file mode 100644 index 231657b23..000000000 --- a/common/changes/@visactor/vrender-components/release-1.0.3_2025-06-10-09-22.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender-components", - "comment": "fix: fix rele", - "type": "none" - } - ], - "packageName": "@visactor/vrender-components" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-core/release-1.0.3_2025-06-10-09-22.json b/common/changes/@visactor/vrender-core/release-1.0.3_2025-06-10-09-22.json deleted file mode 100644 index 9aa25cf6a..000000000 --- a/common/changes/@visactor/vrender-core/release-1.0.3_2025-06-10-09-22.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender-core", - "comment": "fix: fix rele", - "type": "none" - } - ], - "packageName": "@visactor/vrender-core" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-kits/release-1.0.3_2025-06-10-09-22.json b/common/changes/@visactor/vrender-kits/release-1.0.3_2025-06-10-09-22.json deleted file mode 100644 index cd075886f..000000000 --- a/common/changes/@visactor/vrender-kits/release-1.0.3_2025-06-10-09-22.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender-kits", - "comment": "fix: fix rele", - "type": "none" - } - ], - "packageName": "@visactor/vrender-kits" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender/release-1.0.3_2025-06-10-09-22.json b/common/changes/@visactor/vrender/release-1.0.3_2025-06-10-09-22.json deleted file mode 100644 index ea97d533b..000000000 --- a/common/changes/@visactor/vrender/release-1.0.3_2025-06-10-09-22.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender", - "comment": "fix: fix rele", - "type": "none" - } - ], - "packageName": "@visactor/vrender" -} \ No newline at end of file diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index b2518f82d..6ec73bf37 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: specifier: ~0.5.7 version: 0.5.7 '@visactor/vrender': - specifier: workspace:1.0.2 + specifier: workspace:1.0.3 version: link:../packages/vrender '@visactor/vutils': specifier: 1.0.6 @@ -95,7 +95,7 @@ importers: ../../packages/react-vrender: dependencies: '@visactor/vrender': - specifier: workspace:1.0.2 + specifier: workspace:1.0.3 version: link:../vrender '@visactor/vutils': specifier: 1.0.6 @@ -153,10 +153,10 @@ importers: ../../packages/react-vrender-utils: dependencies: '@visactor/react-vrender': - specifier: workspace:1.0.2 + specifier: workspace:1.0.3 version: link:../react-vrender '@visactor/vrender': - specifier: workspace:1.0.2 + specifier: workspace:1.0.3 version: link:../vrender '@visactor/vutils': specifier: 1.0.6 @@ -211,13 +211,13 @@ importers: ../../packages/vrender: dependencies: '@visactor/vrender-animate': - specifier: workspace:1.0.2 + specifier: workspace:1.0.3 version: link:../vrender-animate '@visactor/vrender-core': - specifier: workspace:1.0.2 + specifier: workspace:1.0.3 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:1.0.2 + specifier: workspace:1.0.3 version: link:../vrender-kits devDependencies: '@internal/bundler': @@ -284,7 +284,7 @@ importers: ../../packages/vrender-animate: dependencies: '@visactor/vrender-core': - specifier: workspace:1.0.2 + specifier: workspace:1.0.3 version: link:../vrender-core '@visactor/vutils': specifier: 1.0.6 @@ -342,13 +342,13 @@ importers: ../../packages/vrender-components: dependencies: '@visactor/vrender-animate': - specifier: workspace:1.0.2 + specifier: workspace:1.0.3 version: link:../vrender-animate '@visactor/vrender-core': - specifier: workspace:1.0.2 + specifier: workspace:1.0.3 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:1.0.2 + specifier: workspace:1.0.3 version: link:../vrender-kits '@visactor/vscale': specifier: 1.0.6 @@ -467,7 +467,7 @@ importers: specifier: 2.4.1 version: 2.4.1 '@visactor/vrender-core': - specifier: workspace:1.0.2 + specifier: workspace:1.0.3 version: link:../vrender-core '@visactor/vutils': specifier: 1.0.6 @@ -583,19 +583,19 @@ importers: ../../tools/bugserver-trigger: dependencies: '@visactor/vrender': - specifier: workspace:1.0.2 + specifier: workspace:1.0.3 version: link:../../packages/vrender '@visactor/vrender-animate': - specifier: workspace:1.0.2 + specifier: workspace:1.0.3 version: link:../../packages/vrender-animate '@visactor/vrender-components': - specifier: workspace:1.0.2 + specifier: workspace:1.0.3 version: link:../../packages/vrender-components '@visactor/vrender-core': - specifier: workspace:1.0.2 + specifier: workspace:1.0.3 version: link:../../packages/vrender-core '@visactor/vrender-kits': - specifier: workspace:1.0.2 + specifier: workspace:1.0.3 version: link:../../packages/vrender-kits devDependencies: '@internal/bundler': @@ -7957,7 +7957,7 @@ snapshots: jest-resolve: 26.6.2 jest-resolve-dependencies: 26.6.3 jest-runner: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) - jest-runtime: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) jest-snapshot: 26.6.2 jest-util: 26.6.2 jest-validate: 26.6.2 @@ -8076,23 +8076,27 @@ snapshots: transitivePeerDependencies: - supports-color - '@jest/test-sequencer@26.6.3': + '@jest/test-sequencer@26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5))': dependencies: '@jest/test-result': 26.6.2 graceful-fs: 4.2.11 jest-haste-map: 26.6.2 - jest-runner: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-runner: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) jest-runtime: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) transitivePeerDependencies: + - bufferutil + - canvas - supports-color + - ts-node + - utf-8-validate - '@jest/test-sequencer@26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5))': + '@jest/test-sequencer@26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5))': dependencies: '@jest/test-result': 26.6.2 graceful-fs: 4.2.11 jest-haste-map: 26.6.2 - jest-runner: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) - jest-runtime: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-runner: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) transitivePeerDependencies: - bufferutil - canvas @@ -11513,7 +11517,7 @@ snapshots: jest-environment-jsdom: 26.6.2(canvas@2.11.2) jest-environment-node: 26.6.2 jest-get-type: 26.3.0 - jest-jasmine2: 26.6.3 + jest-jasmine2: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) jest-regex-util: 26.0.0 jest-resolve: 26.6.2 jest-util: 26.6.2 @@ -11531,7 +11535,7 @@ snapshots: jest-config@26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)): dependencies: '@babel/core': 7.20.12 - '@jest/test-sequencer': 26.6.3 + '@jest/test-sequencer': 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) '@jest/types': 26.6.2 babel-jest: 26.6.3(@babel/core@7.20.12) chalk: 4.1.2 @@ -11541,7 +11545,7 @@ snapshots: jest-environment-jsdom: 26.6.2(canvas@2.11.2) jest-environment-node: 26.6.2 jest-get-type: 26.3.0 - jest-jasmine2: 26.6.3 + jest-jasmine2: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) jest-regex-util: 26.0.0 jest-resolve: 26.6.2 jest-util: 26.6.2 @@ -11746,7 +11750,7 @@ snapshots: transitivePeerDependencies: - supports-color - jest-jasmine2@26.6.3: + jest-jasmine2@26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)): dependencies: '@babel/traverse': 7.27.4 '@jest/environment': 26.6.2 @@ -11767,7 +11771,38 @@ snapshots: pretty-format: 26.6.2 throat: 5.0.0 transitivePeerDependencies: + - bufferutil + - canvas - supports-color + - ts-node + - utf-8-validate + + jest-jasmine2@26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)): + dependencies: + '@babel/traverse': 7.27.4 + '@jest/environment': 26.6.2 + '@jest/source-map': 26.6.2 + '@jest/test-result': 26.6.2 + '@jest/types': 26.6.2 + '@types/node': 22.15.30 + chalk: 4.1.2 + co: 4.6.0 + expect: 26.6.2 + is-generator-fn: 2.1.0 + jest-each: 26.6.2 + jest-matcher-utils: 26.6.2 + jest-message-util: 26.6.2 + jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-snapshot: 26.6.2 + jest-util: 26.6.2 + pretty-format: 26.6.2 + throat: 5.0.0 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate jest-leak-detector@24.9.0: dependencies: @@ -11939,7 +11974,7 @@ snapshots: jest-leak-detector: 26.6.2 jest-message-util: 26.6.2 jest-resolve: 26.6.2 - jest-runtime: 26.6.3(canvas@2.11.2)(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-runtime: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) jest-util: 26.6.2 jest-worker: 26.6.2 source-map-support: 0.5.21 @@ -12015,6 +12050,42 @@ snapshots: - ts-node - utf-8-validate + jest-runtime@26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)): + dependencies: + '@jest/console': 26.6.2 + '@jest/environment': 26.6.2 + '@jest/fake-timers': 26.6.2 + '@jest/globals': 26.6.2 + '@jest/source-map': 26.6.2 + '@jest/test-result': 26.6.2 + '@jest/transform': 26.6.2 + '@jest/types': 26.6.2 + '@types/yargs': 15.0.19 + chalk: 4.1.2 + cjs-module-lexer: 0.6.0 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-config: 26.6.3(ts-node@10.9.0(@types/node@22.15.30)(typescript@4.9.5)) + jest-haste-map: 26.6.2 + jest-message-util: 26.6.2 + jest-mock: 26.6.2 + jest-regex-util: 26.0.0 + jest-resolve: 26.6.2 + jest-snapshot: 26.6.2 + jest-util: 26.6.2 + jest-validate: 26.6.2 + slash: 3.0.0 + strip-bom: 4.0.0 + yargs: 15.4.1 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + jest-serializer@24.9.0: {} jest-serializer@26.6.2: diff --git a/common/config/rush/version-policies.json b/common/config/rush/version-policies.json index c042b9410..59535845e 100644 --- a/common/config/rush/version-policies.json +++ b/common/config/rush/version-policies.json @@ -1 +1 @@ -[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"1.0.2","nextBump":"patch"}] +[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"1.0.3","nextBump":"patch"}] diff --git a/docs/package.json b/docs/package.json index 275971a5b..3653f84d5 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,7 +13,7 @@ "@visactor/vchart": "1.3.0", "@visactor/vutils": "1.0.6", "@visactor/vgrammar": "~0.5.7", - "@visactor/vrender": "workspace:1.0.2", + "@visactor/vrender": "workspace:1.0.3", "markdown-it": "^13.0.0", "highlight.js": "^11.8.0", "axios": "^1.4.0", diff --git a/packages/react-vrender-utils/CHANGELOG.json b/packages/react-vrender-utils/CHANGELOG.json index 684abd46d..700a0a559 100644 --- a/packages/react-vrender-utils/CHANGELOG.json +++ b/packages/react-vrender-utils/CHANGELOG.json @@ -1,6 +1,18 @@ { "name": "@visactor/react-vrender-utils", "entries": [ + { + "version": "1.0.3", + "tag": "@visactor/react-vrender-utils_v1.0.3", + "date": "Tue, 10 Jun 2025 09:32:32 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix rele" + } + ] + } + }, { "version": "1.0.1", "tag": "@visactor/react-vrender-utils_v1.0.1", diff --git a/packages/react-vrender-utils/CHANGELOG.md b/packages/react-vrender-utils/CHANGELOG.md index 32cb33035..bb242616f 100644 --- a/packages/react-vrender-utils/CHANGELOG.md +++ b/packages/react-vrender-utils/CHANGELOG.md @@ -1,6 +1,13 @@ # Change Log - @visactor/react-vrender-utils -This log was last generated on Mon, 09 Jun 2025 07:29:07 GMT and should not be manually modified. +This log was last generated on Tue, 10 Jun 2025 09:32:32 GMT and should not be manually modified. + +## 1.0.3 +Tue, 10 Jun 2025 09:32:32 GMT + +### Updates + +- fix: fix rele ## 1.0.1 Mon, 09 Jun 2025 07:29:07 GMT diff --git a/packages/react-vrender-utils/package.json b/packages/react-vrender-utils/package.json index 7274f5457..a6973f232 100644 --- a/packages/react-vrender-utils/package.json +++ b/packages/react-vrender-utils/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender-utils", - "version": "1.0.2", + "version": "1.0.3", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -24,8 +24,8 @@ "react-dom": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:1.0.2", - "@visactor/react-vrender": "workspace:1.0.2", + "@visactor/vrender": "workspace:1.0.3", + "@visactor/react-vrender": "workspace:1.0.3", "@visactor/vutils": "1.0.6", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/react-vrender/CHANGELOG.json b/packages/react-vrender/CHANGELOG.json index 880ad1843..63d2cf1d1 100644 --- a/packages/react-vrender/CHANGELOG.json +++ b/packages/react-vrender/CHANGELOG.json @@ -1,6 +1,18 @@ { "name": "@visactor/react-vrender", "entries": [ + { + "version": "1.0.3", + "tag": "@visactor/react-vrender_v1.0.3", + "date": "Tue, 10 Jun 2025 09:32:32 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix release version" + } + ] + } + }, { "version": "1.0.1", "tag": "@visactor/react-vrender_v1.0.1", diff --git a/packages/react-vrender/CHANGELOG.md b/packages/react-vrender/CHANGELOG.md index 5d3417ae4..528a2663f 100644 --- a/packages/react-vrender/CHANGELOG.md +++ b/packages/react-vrender/CHANGELOG.md @@ -1,6 +1,13 @@ # Change Log - @visactor/react-vrender -This log was last generated on Mon, 09 Jun 2025 07:29:07 GMT and should not be manually modified. +This log was last generated on Tue, 10 Jun 2025 09:32:32 GMT and should not be manually modified. + +## 1.0.3 +Tue, 10 Jun 2025 09:32:32 GMT + +### Updates + +- fix: fix release version ## 1.0.1 Mon, 09 Jun 2025 07:29:07 GMT diff --git a/packages/react-vrender/package.json b/packages/react-vrender/package.json index 39a6df87a..61a4f9450 100644 --- a/packages/react-vrender/package.json +++ b/packages/react-vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender", - "version": "1.0.2", + "version": "1.0.3", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -23,7 +23,7 @@ "react": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:1.0.2", + "@visactor/vrender": "workspace:1.0.3", "@visactor/vutils": "1.0.6", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/vrender-animate/CHANGELOG.json b/packages/vrender-animate/CHANGELOG.json index 8c58bc0d2..fc7ba5d83 100644 --- a/packages/vrender-animate/CHANGELOG.json +++ b/packages/vrender-animate/CHANGELOG.json @@ -1,6 +1,21 @@ { "name": "@visactor/vrender-animate", "entries": [ + { + "version": "1.0.3", + "tag": "@visactor/vrender-animate_v1.0.3", + "date": "Tue, 10 Jun 2025 09:32:32 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix aniamtion of dx, dy and customParameters\n\n" + }, + { + "comment": "fix: fix rele" + } + ] + } + }, { "version": "1.0.1", "tag": "@visactor/vrender-animate_v1.0.1", diff --git a/packages/vrender-animate/CHANGELOG.md b/packages/vrender-animate/CHANGELOG.md index 91b59494b..25029b557 100644 --- a/packages/vrender-animate/CHANGELOG.md +++ b/packages/vrender-animate/CHANGELOG.md @@ -1,6 +1,16 @@ # Change Log - @visactor/vrender-animate -This log was last generated on Mon, 09 Jun 2025 07:29:07 GMT and should not be manually modified. +This log was last generated on Tue, 10 Jun 2025 09:32:32 GMT and should not be manually modified. + +## 1.0.3 +Tue, 10 Jun 2025 09:32:32 GMT + +### Updates + +- fix: fix aniamtion of dx, dy and customParameters + + +- fix: fix rele ## 1.0.1 Mon, 09 Jun 2025 07:29:07 GMT diff --git a/packages/vrender-animate/package.json b/packages/vrender-animate/package.json index 9a47db220..cb9b5e550 100644 --- a/packages/vrender-animate/package.json +++ b/packages/vrender-animate/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-animate", - "version": "1.0.2", + "version": "1.0.3", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -21,7 +21,7 @@ }, "dependencies": { "@visactor/vutils": "1.0.6", - "@visactor/vrender-core": "workspace:1.0.2" + "@visactor/vrender-core": "workspace:1.0.3" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-components/CHANGELOG.json b/packages/vrender-components/CHANGELOG.json index 146bdb021..b62c38ab7 100644 --- a/packages/vrender-components/CHANGELOG.json +++ b/packages/vrender-components/CHANGELOG.json @@ -1,6 +1,21 @@ { "name": "@visactor/vrender-components", "entries": [ + { + "version": "1.0.3", + "tag": "@visactor/vrender-components_v1.0.3", + "date": "Tue, 10 Jun 2025 09:32:32 GMT", + "comments": { + "none": [ + { + "comment": "fix: disable sampling when label is rich text\n\n" + }, + { + "comment": "fix: fix rele" + } + ] + } + }, { "version": "1.0.1", "tag": "@visactor/vrender-components_v1.0.1", diff --git a/packages/vrender-components/CHANGELOG.md b/packages/vrender-components/CHANGELOG.md index e4cebe2cc..f3d6061ae 100644 --- a/packages/vrender-components/CHANGELOG.md +++ b/packages/vrender-components/CHANGELOG.md @@ -1,6 +1,16 @@ # Change Log - @visactor/vrender-components -This log was last generated on Mon, 09 Jun 2025 07:29:07 GMT and should not be manually modified. +This log was last generated on Tue, 10 Jun 2025 09:32:32 GMT and should not be manually modified. + +## 1.0.3 +Tue, 10 Jun 2025 09:32:32 GMT + +### Updates + +- fix: disable sampling when label is rich text + + +- fix: fix rele ## 1.0.1 Mon, 09 Jun 2025 07:29:07 GMT diff --git a/packages/vrender-components/package.json b/packages/vrender-components/package.json index 700699fe0..e75dc053c 100644 --- a/packages/vrender-components/package.json +++ b/packages/vrender-components/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-components", - "version": "1.0.2", + "version": "1.0.3", "description": "components library for dp visualization", "sideEffects": false, "main": "cjs/index.js", @@ -27,9 +27,9 @@ "dependencies": { "@visactor/vutils": "1.0.6", "@visactor/vscale": "1.0.6", - "@visactor/vrender-core": "workspace:1.0.2", - "@visactor/vrender-kits": "workspace:1.0.2", - "@visactor/vrender-animate": "workspace:1.0.2" + "@visactor/vrender-core": "workspace:1.0.3", + "@visactor/vrender-kits": "workspace:1.0.3", + "@visactor/vrender-animate": "workspace:1.0.3" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-core/CHANGELOG.json b/packages/vrender-core/CHANGELOG.json index 022566af3..837a01a4b 100644 --- a/packages/vrender-core/CHANGELOG.json +++ b/packages/vrender-core/CHANGELOG.json @@ -1,6 +1,18 @@ { "name": "@visactor/vrender-core", "entries": [ + { + "version": "1.0.3", + "tag": "@visactor/vrender-core_v1.0.3", + "date": "Tue, 10 Jun 2025 09:32:32 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix rele" + } + ] + } + }, { "version": "1.0.1", "tag": "@visactor/vrender-core_v1.0.1", diff --git a/packages/vrender-core/CHANGELOG.md b/packages/vrender-core/CHANGELOG.md index 4393be68f..3b7f3c586 100644 --- a/packages/vrender-core/CHANGELOG.md +++ b/packages/vrender-core/CHANGELOG.md @@ -1,6 +1,13 @@ # Change Log - @visactor/vrender-core -This log was last generated on Mon, 09 Jun 2025 07:29:07 GMT and should not be manually modified. +This log was last generated on Tue, 10 Jun 2025 09:32:32 GMT and should not be manually modified. + +## 1.0.3 +Tue, 10 Jun 2025 09:32:32 GMT + +### Updates + +- fix: fix rele ## 1.0.1 Mon, 09 Jun 2025 07:29:07 GMT diff --git a/packages/vrender-core/package.json b/packages/vrender-core/package.json index 77423dc97..9e6f4b996 100644 --- a/packages/vrender-core/package.json +++ b/packages/vrender-core/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-core", - "version": "1.0.2", + "version": "1.0.3", "description": "", "sideEffects": [ "./src/modules.ts", diff --git a/packages/vrender-kits/CHANGELOG.json b/packages/vrender-kits/CHANGELOG.json index daa624b6d..a70e080aa 100644 --- a/packages/vrender-kits/CHANGELOG.json +++ b/packages/vrender-kits/CHANGELOG.json @@ -1,6 +1,18 @@ { "name": "@visactor/vrender-kits", "entries": [ + { + "version": "1.0.3", + "tag": "@visactor/vrender-kits_v1.0.3", + "date": "Tue, 10 Jun 2025 09:32:32 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix rele" + } + ] + } + }, { "version": "1.0.1", "tag": "@visactor/vrender-kits_v1.0.1", diff --git a/packages/vrender-kits/CHANGELOG.md b/packages/vrender-kits/CHANGELOG.md index 18320c597..30ff49c31 100644 --- a/packages/vrender-kits/CHANGELOG.md +++ b/packages/vrender-kits/CHANGELOG.md @@ -1,6 +1,13 @@ # Change Log - @visactor/vrender-kits -This log was last generated on Mon, 09 Jun 2025 07:29:07 GMT and should not be manually modified. +This log was last generated on Tue, 10 Jun 2025 09:32:32 GMT and should not be manually modified. + +## 1.0.3 +Tue, 10 Jun 2025 09:32:32 GMT + +### Updates + +- fix: fix rele ## 1.0.1 Mon, 09 Jun 2025 07:29:07 GMT diff --git a/packages/vrender-kits/package.json b/packages/vrender-kits/package.json index 47e7496a7..547c884de 100644 --- a/packages/vrender-kits/package.json +++ b/packages/vrender-kits/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-kits", - "version": "1.0.2", + "version": "1.0.3", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -21,7 +21,7 @@ }, "dependencies": { "@visactor/vutils": "1.0.6", - "@visactor/vrender-core": "workspace:1.0.2", + "@visactor/vrender-core": "workspace:1.0.3", "@resvg/resvg-js": "2.4.1", "roughjs": "4.5.2", "gifuct-js": "2.1.2", diff --git a/packages/vrender/CHANGELOG.json b/packages/vrender/CHANGELOG.json index 8585ef31c..45bc695d3 100644 --- a/packages/vrender/CHANGELOG.json +++ b/packages/vrender/CHANGELOG.json @@ -1,6 +1,18 @@ { "name": "@visactor/vrender", "entries": [ + { + "version": "1.0.3", + "tag": "@visactor/vrender_v1.0.3", + "date": "Tue, 10 Jun 2025 09:32:32 GMT", + "comments": { + "none": [ + { + "comment": "fix: fix rele" + } + ] + } + }, { "version": "1.0.1", "tag": "@visactor/vrender_v1.0.1", diff --git a/packages/vrender/CHANGELOG.md b/packages/vrender/CHANGELOG.md index 31cc388ac..cca8a3db2 100644 --- a/packages/vrender/CHANGELOG.md +++ b/packages/vrender/CHANGELOG.md @@ -1,6 +1,13 @@ # Change Log - @visactor/vrender -This log was last generated on Mon, 09 Jun 2025 07:29:07 GMT and should not be manually modified. +This log was last generated on Tue, 10 Jun 2025 09:32:32 GMT and should not be manually modified. + +## 1.0.3 +Tue, 10 Jun 2025 09:32:32 GMT + +### Updates + +- fix: fix rele ## 1.0.1 Mon, 09 Jun 2025 07:29:07 GMT diff --git a/packages/vrender/package.json b/packages/vrender/package.json index c30ff2ef0..03232600d 100644 --- a/packages/vrender/package.json +++ b/packages/vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender", - "version": "1.0.2", + "version": "1.0.3", "description": "", "sideEffects": true, "main": "cjs/index.js", @@ -24,9 +24,9 @@ "test-watch": "cross-env DEBUG_MODE=1 jest --watch" }, "dependencies": { - "@visactor/vrender-core": "workspace:1.0.2", - "@visactor/vrender-kits": "workspace:1.0.2", - "@visactor/vrender-animate": "workspace:1.0.2" + "@visactor/vrender-core": "workspace:1.0.3", + "@visactor/vrender-kits": "workspace:1.0.3", + "@visactor/vrender-animate": "workspace:1.0.3" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/tools/bugserver-trigger/package.json b/tools/bugserver-trigger/package.json index 3f6e30527..ffdad381f 100644 --- a/tools/bugserver-trigger/package.json +++ b/tools/bugserver-trigger/package.json @@ -8,11 +8,11 @@ "ci": "ts-node --transpileOnly --skipProject ./scripts/trigger-test.ts" }, "dependencies": { - "@visactor/vrender": "workspace:1.0.2", - "@visactor/vrender-core": "workspace:1.0.2", - "@visactor/vrender-kits": "workspace:1.0.2", - "@visactor/vrender-components": "workspace:1.0.2", - "@visactor/vrender-animate": "workspace:1.0.2" + "@visactor/vrender": "workspace:1.0.3", + "@visactor/vrender-core": "workspace:1.0.3", + "@visactor/vrender-kits": "workspace:1.0.3", + "@visactor/vrender-components": "workspace:1.0.3", + "@visactor/vrender-animate": "workspace:1.0.3" }, "devDependencies": { "@rushstack/eslint-patch": "~1.1.4", From 216e40f35b9db5cf2fae3d0606c2c33d5c33816d Mon Sep 17 00:00:00 2001 From: xile611 Date: Tue, 10 Jun 2025 20:45:19 +0800 Subject: [PATCH 167/179] chore: update tsconfig.test.json of vrender --- packages/vrender/tsconfig.test.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vrender/tsconfig.test.json b/packages/vrender/tsconfig.test.json index c0f87af17..d2021a0ef 100644 --- a/packages/vrender/tsconfig.test.json +++ b/packages/vrender/tsconfig.test.json @@ -4,7 +4,7 @@ "paths": { "@visactor/vrender-core": ["../vrender-core/src"], "@visactor/vrender-kits": ["../vrender-kits/src"], - "@visactor/vrender-animate": ["../vrender-aniamte"] + "@visactor/vrender-animate": ["../vrender-aniamte/src"] } }, "references": [] From ccc5aded7f8049712bf042f20159a822454da0d4 Mon Sep 17 00:00:00 2001 From: xile611 Date: Tue, 17 Jun 2025 10:05:29 +0800 Subject: [PATCH 168/179] fix: fix default params of animation --- packages/vrender-animate/src/executor/animate-executor.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index e3b4270b7..a466ad754 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -420,10 +420,11 @@ export class AnimateExecutor implements IAnimateExecutor { ) { // 处理自定义动画 if (custom && customType) { - const customParams = this.resolveValue(customParameters, graphic, { + const customParams = { width: graphic.stage.width, - height: graphic.stage.height - }); + height: graphic.stage.height, + ...this.resolveValue(customParameters, graphic) + }; const objOptions = isFunction(options) ? options.call( null, From effe26111c75d2a5f95f28618caf61f1ce825481 Mon Sep 17 00:00:00 2001 From: xile611 Date: Tue, 17 Jun 2025 10:06:54 +0800 Subject: [PATCH 169/179] docs: update changlog of rush --- .../fix-aniamte-custom-params_2025-06-17-02-06.json | 11 +++++++++++ .../fix-aniamte-custom-params_2025-06-17-02-06.json | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 common/changes/@visactor/vrender-animate/fix-aniamte-custom-params_2025-06-17-02-06.json create mode 100644 common/changes/@visactor/vrender/fix-aniamte-custom-params_2025-06-17-02-06.json diff --git a/common/changes/@visactor/vrender-animate/fix-aniamte-custom-params_2025-06-17-02-06.json b/common/changes/@visactor/vrender-animate/fix-aniamte-custom-params_2025-06-17-02-06.json new file mode 100644 index 000000000..7844f7e14 --- /dev/null +++ b/common/changes/@visactor/vrender-animate/fix-aniamte-custom-params_2025-06-17-02-06.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: fix default params of animation\n\n", + "type": "none", + "packageName": "@visactor/vrender-animate" + } + ], + "packageName": "@visactor/vrender-animate", + "email": "dingling112@gmail.com" +} \ No newline at end of file diff --git a/common/changes/@visactor/vrender/fix-aniamte-custom-params_2025-06-17-02-06.json b/common/changes/@visactor/vrender/fix-aniamte-custom-params_2025-06-17-02-06.json new file mode 100644 index 000000000..c785b7022 --- /dev/null +++ b/common/changes/@visactor/vrender/fix-aniamte-custom-params_2025-06-17-02-06.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: fix default params of animation\n\n", + "type": "none", + "packageName": "@visactor/vrender" + } + ], + "packageName": "@visactor/vrender", + "email": "dingling112@gmail.com" +} \ No newline at end of file From 051eb673184385981bfc3f4a53dcf13f077bc86f Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 17 Jun 2025 15:47:22 +0800 Subject: [PATCH 170/179] fix: fix issue with animate interface --- packages/vrender-animate/src/animate-extension.ts | 8 -------- packages/vrender-animate/src/animate.ts | 9 +++++++++ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/vrender-animate/src/animate-extension.ts b/packages/vrender-animate/src/animate-extension.ts index 36c94ca2b..6466febfd 100644 --- a/packages/vrender-animate/src/animate-extension.ts +++ b/packages/vrender-animate/src/animate-extension.ts @@ -27,9 +27,6 @@ export class AnimateExtension { } animate(params?: IGraphicAnimateParams) { - if (!this.animates) { - this.animates = new Map(); - } const animate = new Animate( params?.id, params?.timeline ?? ((this as any).stage && (this as any).stage.getTimeline()) ?? defaultTimeline, @@ -43,11 +40,6 @@ export class AnimateExtension { onEnd != null && animate.onEnd(onEnd); onRemove != null && animate.onRemove(onRemove); } - this.animates.set(animate.id, animate); - animate.onRemove(() => { - animate.stop(); - this.animates.delete(animate.id); - }); // TODO 考虑性能问题 (this as any).stage?.ticker.start(); diff --git a/packages/vrender-animate/src/animate.ts b/packages/vrender-animate/src/animate.ts index d6af3d033..29f3e174b 100644 --- a/packages/vrender-animate/src/animate.ts +++ b/packages/vrender-animate/src/animate.ts @@ -124,6 +124,15 @@ export class Animate implements IAnimate { bind(target: IGraphic): this { this.target = target; + if (!this.target.animates) { + this.target.animates = new Map(); + } + this.target.animates.set(this.id, this); + this.onRemove(() => { + this.stop(); + this.target.animates.delete(this.id); + }); + if (this.target.onAnimateBind && !this.slience) { this.target.onAnimateBind(this as any); } From bf331bb5ea7cd5c196c885b1387e3f6709d6bc35 Mon Sep 17 00:00:00 2001 From: xile611 Date: Tue, 17 Jun 2025 15:49:46 +0800 Subject: [PATCH 171/179] fix: fix move animation and custom params --- packages/vrender-animate/src/custom/move.ts | 7 +++---- packages/vrender-animate/src/executor/animate-executor.ts | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vrender-animate/src/custom/move.ts b/packages/vrender-animate/src/custom/move.ts index c3e640ada..2694cfab5 100644 --- a/packages/vrender-animate/src/custom/move.ts +++ b/packages/vrender-animate/src/custom/move.ts @@ -80,10 +80,9 @@ export const moveOut = ( ) => { const { offset = 0, orient, direction, point: pointOpt } = options ?? {}; - // consider the offset of group - // const groupBounds = graphic.parent ? graphic.parent.getBounds() : null; - const groupWidth = options.layoutRect?.width ?? graphic.stage.viewWidth; - const groupHeight = options.layoutRect?.height ?? graphic.stage.viewHeight; + const groupBounds = animationParameters.group ? animationParameters.group.AABBBounds : null; + const groupWidth = groupBounds.width() ?? animationParameters.width; + const groupHeight = groupBounds.height() ?? animationParameters.height; const changedX = (orient === 'negative' ? groupWidth : 0) + offset; const changedY = (orient === 'negative' ? groupHeight : 0) + offset; const point = isFunction(pointOpt) diff --git a/packages/vrender-animate/src/executor/animate-executor.ts b/packages/vrender-animate/src/executor/animate-executor.ts index a466ad754..2570d74e6 100644 --- a/packages/vrender-animate/src/executor/animate-executor.ts +++ b/packages/vrender-animate/src/executor/animate-executor.ts @@ -423,6 +423,7 @@ export class AnimateExecutor implements IAnimateExecutor { const customParams = { width: graphic.stage.width, height: graphic.stage.height, + group: this._target.parent, ...this.resolveValue(customParameters, graphic) }; const objOptions = isFunction(options) From 8e2df12077891811326a6288de92f368b8c522f2 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 17 Jun 2025 16:03:09 +0800 Subject: [PATCH 172/179] feat: add from and to in ACustomAnimate --- packages/vrender-animate/src/custom/custom-animate.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/vrender-animate/src/custom/custom-animate.ts b/packages/vrender-animate/src/custom/custom-animate.ts index 747af59a2..98c958703 100644 --- a/packages/vrender-animate/src/custom/custom-animate.ts +++ b/packages/vrender-animate/src/custom/custom-animate.ts @@ -16,6 +16,8 @@ export abstract class ACustomAnimate extends Step implements ICustomAnimate { super('customAnimate', customTo, duration, easing); this.customFrom = customFrom; this.params = params; + this.from = customFrom; + this.to = customTo; } update(end: boolean, ratio: number, out: Record): void { From 3df7935f6b7a349c2903c731601f69b1ee4aa8b2 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 17 Jun 2025 17:27:24 +0800 Subject: [PATCH 173/179] fix: fix issue with recall setupTickHandler --- .../fix-ticker-setup_2025-06-17-09-27.json | 10 ++++++++++ .../vrender-animate/src/ticker/default-ticker.ts | 15 +++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 common/changes/@visactor/vrender-animate/fix-ticker-setup_2025-06-17-09-27.json diff --git a/common/changes/@visactor/vrender-animate/fix-ticker-setup_2025-06-17-09-27.json b/common/changes/@visactor/vrender-animate/fix-ticker-setup_2025-06-17-09-27.json new file mode 100644 index 000000000..096eb5b5e --- /dev/null +++ b/common/changes/@visactor/vrender-animate/fix-ticker-setup_2025-06-17-09-27.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-animate", + "comment": "fix: fix issue with recall setupTickHandler", + "type": "none" + } + ], + "packageName": "@visactor/vrender-animate" +} \ No newline at end of file diff --git a/packages/vrender-animate/src/ticker/default-ticker.ts b/packages/vrender-animate/src/ticker/default-ticker.ts index fcf7778cd..da1a6acd3 100644 --- a/packages/vrender-animate/src/ticker/default-ticker.ts +++ b/packages/vrender-animate/src/ticker/default-ticker.ts @@ -71,10 +71,10 @@ export class DefaultTicker extends EventEmitter implements ITicker { this.interval = 16; this.status = STATUS.INITIAL; application.global.hooks.onSetEnv.tap('graph-ticker', () => { - this.initHandler(); + this.initHandler(false); }); if (application.global.env) { - this.initHandler(); + this.initHandler(false); } } @@ -90,15 +90,18 @@ export class DefaultTicker extends EventEmitter implements ITicker { return this.timelines; } - protected initHandler() { - this.setupTickHandler(); + protected initHandler(force: boolean = false) { + this.setupTickHandler(force); } /** * Set up the tick handler * @returns true if setup was successful, false otherwise */ - protected setupTickHandler(): boolean { + protected setupTickHandler(force: boolean = false): boolean { + if (!force && this.tickerHandler) { + return true; + } const handler: ITickHandler = new RAFTickHandler(); // Destroy the previous tick handler @@ -196,7 +199,7 @@ export class DefaultTicker extends EventEmitter implements ITicker { stop(): void { // Reset the tick handler this.status = STATUS.INITIAL; - this.setupTickHandler(); + this.setupTickHandler(true); this.lastFrameTime = -1; } From 92b2fd85f40a8bc30f50527aa2967b0e3117c7b4 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 17 Jun 2025 19:44:34 +0800 Subject: [PATCH 174/179] feat: timeline support animationStart flag --- packages/vrender-animate/src/timeline.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/vrender-animate/src/timeline.ts b/packages/vrender-animate/src/timeline.ts index c563dde40..64759264a 100644 --- a/packages/vrender-animate/src/timeline.ts +++ b/packages/vrender-animate/src/timeline.ts @@ -21,6 +21,7 @@ export class DefaultTimeline extends EventEmitter implements ITimeline { protected _totalDuration: number = 0; protected _startTime: number = 0; protected _currentTime: number = 0; + protected _animationEndFlag: boolean = true; declare isGlobal?: boolean; @@ -90,6 +91,10 @@ export class DefaultTimeline extends EventEmitter implements ITimeline { if (this.paused) { return; } + if (this._animationEndFlag) { + this._animationEndFlag = false; + this.emit('animationStart'); + } // 应用播放速度 const scaledDelta = delta * this._playSpeed; @@ -106,6 +111,7 @@ export class DefaultTimeline extends EventEmitter implements ITimeline { }); if (this._animateCount === 0) { + this._animationEndFlag = true; this.emit('animationEnd'); } } From a0bbc3e36dce08bb65e3aec61d6aae096bc05f9a Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 17 Jun 2025 21:26:55 +0800 Subject: [PATCH 175/179] feat: don't prevent normal animate attribute --- .../vrender-animate/develop_2025-06-17-11-44.json | 10 ++++++++++ .../vrender-animate/develop_2025-06-17-13-27.json | 10 ++++++++++ .../vrender-core/develop_2025-06-17-13-27.json | 10 ++++++++++ .../@visactor/vrender/develop_2025-06-17-11-44.json | 10 ++++++++++ packages/vrender-animate/src/custom/fromTo.ts | 5 +++++ packages/vrender-core/src/graphic/graphic.ts | 11 ++++++++++- 6 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 common/changes/@visactor/vrender-animate/develop_2025-06-17-11-44.json create mode 100644 common/changes/@visactor/vrender-animate/develop_2025-06-17-13-27.json create mode 100644 common/changes/@visactor/vrender-core/develop_2025-06-17-13-27.json create mode 100644 common/changes/@visactor/vrender/develop_2025-06-17-11-44.json diff --git a/common/changes/@visactor/vrender-animate/develop_2025-06-17-11-44.json b/common/changes/@visactor/vrender-animate/develop_2025-06-17-11-44.json new file mode 100644 index 000000000..dff8372ed --- /dev/null +++ b/common/changes/@visactor/vrender-animate/develop_2025-06-17-11-44.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-animate", + "comment": "feat: timeline support animationStart flag", + "type": "none" + } + ], + "packageName": "@visactor/vrender-animate" +} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-animate/develop_2025-06-17-13-27.json b/common/changes/@visactor/vrender-animate/develop_2025-06-17-13-27.json new file mode 100644 index 000000000..1313d1d22 --- /dev/null +++ b/common/changes/@visactor/vrender-animate/develop_2025-06-17-13-27.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-animate", + "comment": "feat: don't prevent normal animate attribute", + "type": "none" + } + ], + "packageName": "@visactor/vrender-animate" +} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-core/develop_2025-06-17-13-27.json b/common/changes/@visactor/vrender-core/develop_2025-06-17-13-27.json new file mode 100644 index 000000000..40d783312 --- /dev/null +++ b/common/changes/@visactor/vrender-core/develop_2025-06-17-13-27.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-core", + "comment": "feat: don't prevent normal animate attribute", + "type": "none" + } + ], + "packageName": "@visactor/vrender-core" +} \ No newline at end of file diff --git a/common/changes/@visactor/vrender/develop_2025-06-17-11-44.json b/common/changes/@visactor/vrender/develop_2025-06-17-11-44.json new file mode 100644 index 000000000..d5fceb975 --- /dev/null +++ b/common/changes/@visactor/vrender/develop_2025-06-17-11-44.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender", + "comment": "feat: timeline support animationStart flag", + "type": "none" + } + ], + "packageName": "@visactor/vrender" +} \ No newline at end of file diff --git a/packages/vrender-animate/src/custom/fromTo.ts b/packages/vrender-animate/src/custom/fromTo.ts index c16ed83b5..138471ab8 100644 --- a/packages/vrender-animate/src/custom/fromTo.ts +++ b/packages/vrender-animate/src/custom/fromTo.ts @@ -51,6 +51,11 @@ export class FromTo extends ACustomAnimate> { this.target.setAttributes(this.from); } + deleteSelfAttr(key: string): void { + super.deleteSelfAttr(key); + delete this.from[key]; + } + /** * 更新执行的时候调用 * 如果跳帧了就不一定会执行 diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index 31403cca3..e2ebce429 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -671,10 +671,19 @@ export abstract class Graphic = Partial, forceUpdateTag: boolean = false, context?: ISetAttributeContext) { + setAttributesAndPreventAnimate( + params: Partial, + forceUpdateTag: boolean = false, + context?: ISetAttributeContext, + ignorePriority?: boolean + ) { this.setAttributes(params, forceUpdateTag, context); this.animates && this.animates.forEach(animate => { + // 优先级最高的动画(一般是循环动画),不屏蔽 + if (animate.priority === Infinity && !ignorePriority) { + return; + } Object.keys(params).forEach(key => { animate.preventAttr(key); }); From 469f783902fe4393cbcdf1cdef614bae2f2736e3 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 18 Jun 2025 13:30:18 +0800 Subject: [PATCH 176/179] fix: set finalAttribute when applyStateAttrs --- ...pplyStateAttrs-finalAttribute_2025-06-18-05-30.json | 10 ++++++++++ packages/vrender-core/src/graphic/graphic.ts | 3 +++ 2 files changed, 13 insertions(+) create mode 100644 common/changes/@visactor/vrender-core/fix-applyStateAttrs-finalAttribute_2025-06-18-05-30.json diff --git a/common/changes/@visactor/vrender-core/fix-applyStateAttrs-finalAttribute_2025-06-18-05-30.json b/common/changes/@visactor/vrender-core/fix-applyStateAttrs-finalAttribute_2025-06-18-05-30.json new file mode 100644 index 000000000..f9b5828fe --- /dev/null +++ b/common/changes/@visactor/vrender-core/fix-applyStateAttrs-finalAttribute_2025-06-18-05-30.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-core", + "comment": "fix: set finalAttribute when applyStateAttrs", + "type": "none" + } + ], + "packageName": "@visactor/vrender-core" +} \ No newline at end of file diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index 31403cca3..4667056de 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -1035,6 +1035,9 @@ export abstract class Graphic = Partial Date: Wed, 18 Jun 2025 05:46:35 +0000 Subject: [PATCH 177/179] build: prelease version 1.0.4 --- .../develop_2025-06-17-11-44.json | 10 ------ .../develop_2025-06-17-13-27.json | 10 ------ ...niamte-custom-params_2025-06-17-02-06.json | 11 ------ .../fix-ticker-setup_2025-06-17-09-27.json | 10 ------ .../develop_2025-06-17-13-27.json | 10 ------ ...Attrs-finalAttribute_2025-06-18-05-30.json | 10 ------ .../vrender/develop_2025-06-17-11-44.json | 10 ------ ...niamte-custom-params_2025-06-17-02-06.json | 11 ------ common/config/rush/pnpm-lock.yaml | 34 +++++++++---------- common/config/rush/version-policies.json | 2 +- docs/package.json | 2 +- packages/react-vrender-utils/CHANGELOG.json | 6 ++++ packages/react-vrender-utils/CHANGELOG.md | 7 +++- packages/react-vrender-utils/package.json | 6 ++-- packages/react-vrender/CHANGELOG.json | 6 ++++ packages/react-vrender/CHANGELOG.md | 7 +++- packages/react-vrender/package.json | 4 +-- packages/vrender-animate/CHANGELOG.json | 21 ++++++++++++ packages/vrender-animate/CHANGELOG.md | 14 +++++++- packages/vrender-animate/package.json | 4 +-- packages/vrender-components/CHANGELOG.json | 6 ++++ packages/vrender-components/CHANGELOG.md | 7 +++- packages/vrender-components/package.json | 8 ++--- packages/vrender-core/CHANGELOG.json | 15 ++++++++ packages/vrender-core/CHANGELOG.md | 10 +++++- packages/vrender-core/package.json | 2 +- packages/vrender-kits/CHANGELOG.json | 6 ++++ packages/vrender-kits/CHANGELOG.md | 7 +++- packages/vrender-kits/package.json | 4 +-- packages/vrender/CHANGELOG.json | 15 ++++++++ packages/vrender/CHANGELOG.md | 12 ++++++- packages/vrender/package.json | 8 ++--- tools/bugserver-trigger/package.json | 10 +++--- 33 files changed, 174 insertions(+), 131 deletions(-) delete mode 100644 common/changes/@visactor/vrender-animate/develop_2025-06-17-11-44.json delete mode 100644 common/changes/@visactor/vrender-animate/develop_2025-06-17-13-27.json delete mode 100644 common/changes/@visactor/vrender-animate/fix-aniamte-custom-params_2025-06-17-02-06.json delete mode 100644 common/changes/@visactor/vrender-animate/fix-ticker-setup_2025-06-17-09-27.json delete mode 100644 common/changes/@visactor/vrender-core/develop_2025-06-17-13-27.json delete mode 100644 common/changes/@visactor/vrender-core/fix-applyStateAttrs-finalAttribute_2025-06-18-05-30.json delete mode 100644 common/changes/@visactor/vrender/develop_2025-06-17-11-44.json delete mode 100644 common/changes/@visactor/vrender/fix-aniamte-custom-params_2025-06-17-02-06.json diff --git a/common/changes/@visactor/vrender-animate/develop_2025-06-17-11-44.json b/common/changes/@visactor/vrender-animate/develop_2025-06-17-11-44.json deleted file mode 100644 index dff8372ed..000000000 --- a/common/changes/@visactor/vrender-animate/develop_2025-06-17-11-44.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender-animate", - "comment": "feat: timeline support animationStart flag", - "type": "none" - } - ], - "packageName": "@visactor/vrender-animate" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-animate/develop_2025-06-17-13-27.json b/common/changes/@visactor/vrender-animate/develop_2025-06-17-13-27.json deleted file mode 100644 index 1313d1d22..000000000 --- a/common/changes/@visactor/vrender-animate/develop_2025-06-17-13-27.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender-animate", - "comment": "feat: don't prevent normal animate attribute", - "type": "none" - } - ], - "packageName": "@visactor/vrender-animate" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-animate/fix-aniamte-custom-params_2025-06-17-02-06.json b/common/changes/@visactor/vrender-animate/fix-aniamte-custom-params_2025-06-17-02-06.json deleted file mode 100644 index 7844f7e14..000000000 --- a/common/changes/@visactor/vrender-animate/fix-aniamte-custom-params_2025-06-17-02-06.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "changes": [ - { - "comment": "fix: fix default params of animation\n\n", - "type": "none", - "packageName": "@visactor/vrender-animate" - } - ], - "packageName": "@visactor/vrender-animate", - "email": "dingling112@gmail.com" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-animate/fix-ticker-setup_2025-06-17-09-27.json b/common/changes/@visactor/vrender-animate/fix-ticker-setup_2025-06-17-09-27.json deleted file mode 100644 index 096eb5b5e..000000000 --- a/common/changes/@visactor/vrender-animate/fix-ticker-setup_2025-06-17-09-27.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender-animate", - "comment": "fix: fix issue with recall setupTickHandler", - "type": "none" - } - ], - "packageName": "@visactor/vrender-animate" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-core/develop_2025-06-17-13-27.json b/common/changes/@visactor/vrender-core/develop_2025-06-17-13-27.json deleted file mode 100644 index 40d783312..000000000 --- a/common/changes/@visactor/vrender-core/develop_2025-06-17-13-27.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender-core", - "comment": "feat: don't prevent normal animate attribute", - "type": "none" - } - ], - "packageName": "@visactor/vrender-core" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-core/fix-applyStateAttrs-finalAttribute_2025-06-18-05-30.json b/common/changes/@visactor/vrender-core/fix-applyStateAttrs-finalAttribute_2025-06-18-05-30.json deleted file mode 100644 index f9b5828fe..000000000 --- a/common/changes/@visactor/vrender-core/fix-applyStateAttrs-finalAttribute_2025-06-18-05-30.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender-core", - "comment": "fix: set finalAttribute when applyStateAttrs", - "type": "none" - } - ], - "packageName": "@visactor/vrender-core" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender/develop_2025-06-17-11-44.json b/common/changes/@visactor/vrender/develop_2025-06-17-11-44.json deleted file mode 100644 index d5fceb975..000000000 --- a/common/changes/@visactor/vrender/develop_2025-06-17-11-44.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vrender", - "comment": "feat: timeline support animationStart flag", - "type": "none" - } - ], - "packageName": "@visactor/vrender" -} \ No newline at end of file diff --git a/common/changes/@visactor/vrender/fix-aniamte-custom-params_2025-06-17-02-06.json b/common/changes/@visactor/vrender/fix-aniamte-custom-params_2025-06-17-02-06.json deleted file mode 100644 index c785b7022..000000000 --- a/common/changes/@visactor/vrender/fix-aniamte-custom-params_2025-06-17-02-06.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "changes": [ - { - "comment": "fix: fix default params of animation\n\n", - "type": "none", - "packageName": "@visactor/vrender" - } - ], - "packageName": "@visactor/vrender", - "email": "dingling112@gmail.com" -} \ No newline at end of file diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 6ec73bf37..4502578bb 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: specifier: ~0.5.7 version: 0.5.7 '@visactor/vrender': - specifier: workspace:1.0.3 + specifier: workspace:1.0.4 version: link:../packages/vrender '@visactor/vutils': specifier: 1.0.6 @@ -95,7 +95,7 @@ importers: ../../packages/react-vrender: dependencies: '@visactor/vrender': - specifier: workspace:1.0.3 + specifier: workspace:1.0.4 version: link:../vrender '@visactor/vutils': specifier: 1.0.6 @@ -153,10 +153,10 @@ importers: ../../packages/react-vrender-utils: dependencies: '@visactor/react-vrender': - specifier: workspace:1.0.3 + specifier: workspace:1.0.4 version: link:../react-vrender '@visactor/vrender': - specifier: workspace:1.0.3 + specifier: workspace:1.0.4 version: link:../vrender '@visactor/vutils': specifier: 1.0.6 @@ -211,13 +211,13 @@ importers: ../../packages/vrender: dependencies: '@visactor/vrender-animate': - specifier: workspace:1.0.3 + specifier: workspace:1.0.4 version: link:../vrender-animate '@visactor/vrender-core': - specifier: workspace:1.0.3 + specifier: workspace:1.0.4 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:1.0.3 + specifier: workspace:1.0.4 version: link:../vrender-kits devDependencies: '@internal/bundler': @@ -284,7 +284,7 @@ importers: ../../packages/vrender-animate: dependencies: '@visactor/vrender-core': - specifier: workspace:1.0.3 + specifier: workspace:1.0.4 version: link:../vrender-core '@visactor/vutils': specifier: 1.0.6 @@ -342,13 +342,13 @@ importers: ../../packages/vrender-components: dependencies: '@visactor/vrender-animate': - specifier: workspace:1.0.3 + specifier: workspace:1.0.4 version: link:../vrender-animate '@visactor/vrender-core': - specifier: workspace:1.0.3 + specifier: workspace:1.0.4 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:1.0.3 + specifier: workspace:1.0.4 version: link:../vrender-kits '@visactor/vscale': specifier: 1.0.6 @@ -467,7 +467,7 @@ importers: specifier: 2.4.1 version: 2.4.1 '@visactor/vrender-core': - specifier: workspace:1.0.3 + specifier: workspace:1.0.4 version: link:../vrender-core '@visactor/vutils': specifier: 1.0.6 @@ -583,19 +583,19 @@ importers: ../../tools/bugserver-trigger: dependencies: '@visactor/vrender': - specifier: workspace:1.0.3 + specifier: workspace:1.0.4 version: link:../../packages/vrender '@visactor/vrender-animate': - specifier: workspace:1.0.3 + specifier: workspace:1.0.4 version: link:../../packages/vrender-animate '@visactor/vrender-components': - specifier: workspace:1.0.3 + specifier: workspace:1.0.4 version: link:../../packages/vrender-components '@visactor/vrender-core': - specifier: workspace:1.0.3 + specifier: workspace:1.0.4 version: link:../../packages/vrender-core '@visactor/vrender-kits': - specifier: workspace:1.0.3 + specifier: workspace:1.0.4 version: link:../../packages/vrender-kits devDependencies: '@internal/bundler': diff --git a/common/config/rush/version-policies.json b/common/config/rush/version-policies.json index 59535845e..daf4cebf5 100644 --- a/common/config/rush/version-policies.json +++ b/common/config/rush/version-policies.json @@ -1 +1 @@ -[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"1.0.3","nextBump":"patch"}] +[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"1.0.4","nextBump":"patch"}] diff --git a/docs/package.json b/docs/package.json index 3653f84d5..ea894342b 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,7 +13,7 @@ "@visactor/vchart": "1.3.0", "@visactor/vutils": "1.0.6", "@visactor/vgrammar": "~0.5.7", - "@visactor/vrender": "workspace:1.0.3", + "@visactor/vrender": "workspace:1.0.4", "markdown-it": "^13.0.0", "highlight.js": "^11.8.0", "axios": "^1.4.0", diff --git a/packages/react-vrender-utils/CHANGELOG.json b/packages/react-vrender-utils/CHANGELOG.json index 700a0a559..7ef65d143 100644 --- a/packages/react-vrender-utils/CHANGELOG.json +++ b/packages/react-vrender-utils/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/react-vrender-utils", "entries": [ + { + "version": "1.0.4", + "tag": "@visactor/react-vrender-utils_v1.0.4", + "date": "Wed, 18 Jun 2025 05:41:49 GMT", + "comments": {} + }, { "version": "1.0.3", "tag": "@visactor/react-vrender-utils_v1.0.3", diff --git a/packages/react-vrender-utils/CHANGELOG.md b/packages/react-vrender-utils/CHANGELOG.md index bb242616f..c01d25dcf 100644 --- a/packages/react-vrender-utils/CHANGELOG.md +++ b/packages/react-vrender-utils/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/react-vrender-utils -This log was last generated on Tue, 10 Jun 2025 09:32:32 GMT and should not be manually modified. +This log was last generated on Wed, 18 Jun 2025 05:41:49 GMT and should not be manually modified. + +## 1.0.4 +Wed, 18 Jun 2025 05:41:49 GMT + +_Version update only_ ## 1.0.3 Tue, 10 Jun 2025 09:32:32 GMT diff --git a/packages/react-vrender-utils/package.json b/packages/react-vrender-utils/package.json index a6973f232..2b6e50df0 100644 --- a/packages/react-vrender-utils/package.json +++ b/packages/react-vrender-utils/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender-utils", - "version": "1.0.3", + "version": "1.0.4", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -24,8 +24,8 @@ "react-dom": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:1.0.3", - "@visactor/react-vrender": "workspace:1.0.3", + "@visactor/vrender": "workspace:1.0.4", + "@visactor/react-vrender": "workspace:1.0.4", "@visactor/vutils": "1.0.6", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/react-vrender/CHANGELOG.json b/packages/react-vrender/CHANGELOG.json index 63d2cf1d1..b544c3288 100644 --- a/packages/react-vrender/CHANGELOG.json +++ b/packages/react-vrender/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/react-vrender", "entries": [ + { + "version": "1.0.4", + "tag": "@visactor/react-vrender_v1.0.4", + "date": "Wed, 18 Jun 2025 05:41:49 GMT", + "comments": {} + }, { "version": "1.0.3", "tag": "@visactor/react-vrender_v1.0.3", diff --git a/packages/react-vrender/CHANGELOG.md b/packages/react-vrender/CHANGELOG.md index 528a2663f..d1d35cab2 100644 --- a/packages/react-vrender/CHANGELOG.md +++ b/packages/react-vrender/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/react-vrender -This log was last generated on Tue, 10 Jun 2025 09:32:32 GMT and should not be manually modified. +This log was last generated on Wed, 18 Jun 2025 05:41:49 GMT and should not be manually modified. + +## 1.0.4 +Wed, 18 Jun 2025 05:41:49 GMT + +_Version update only_ ## 1.0.3 Tue, 10 Jun 2025 09:32:32 GMT diff --git a/packages/react-vrender/package.json b/packages/react-vrender/package.json index 61a4f9450..83a7bca91 100644 --- a/packages/react-vrender/package.json +++ b/packages/react-vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender", - "version": "1.0.3", + "version": "1.0.4", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -23,7 +23,7 @@ "react": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:1.0.3", + "@visactor/vrender": "workspace:1.0.4", "@visactor/vutils": "1.0.6", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/vrender-animate/CHANGELOG.json b/packages/vrender-animate/CHANGELOG.json index fc7ba5d83..ff3133b83 100644 --- a/packages/vrender-animate/CHANGELOG.json +++ b/packages/vrender-animate/CHANGELOG.json @@ -1,6 +1,27 @@ { "name": "@visactor/vrender-animate", "entries": [ + { + "version": "1.0.4", + "tag": "@visactor/vrender-animate_v1.0.4", + "date": "Wed, 18 Jun 2025 05:41:49 GMT", + "comments": { + "none": [ + { + "comment": "feat: timeline support animationStart flag" + }, + { + "comment": "feat: don't prevent normal animate attribute" + }, + { + "comment": "fix: fix default params of animation\n\n" + }, + { + "comment": "fix: fix issue with recall setupTickHandler" + } + ] + } + }, { "version": "1.0.3", "tag": "@visactor/vrender-animate_v1.0.3", diff --git a/packages/vrender-animate/CHANGELOG.md b/packages/vrender-animate/CHANGELOG.md index 25029b557..333d144d7 100644 --- a/packages/vrender-animate/CHANGELOG.md +++ b/packages/vrender-animate/CHANGELOG.md @@ -1,6 +1,18 @@ # Change Log - @visactor/vrender-animate -This log was last generated on Tue, 10 Jun 2025 09:32:32 GMT and should not be manually modified. +This log was last generated on Wed, 18 Jun 2025 05:41:49 GMT and should not be manually modified. + +## 1.0.4 +Wed, 18 Jun 2025 05:41:49 GMT + +### Updates + +- feat: timeline support animationStart flag +- feat: don't prevent normal animate attribute +- fix: fix default params of animation + + +- fix: fix issue with recall setupTickHandler ## 1.0.3 Tue, 10 Jun 2025 09:32:32 GMT diff --git a/packages/vrender-animate/package.json b/packages/vrender-animate/package.json index cb9b5e550..5b1fe9923 100644 --- a/packages/vrender-animate/package.json +++ b/packages/vrender-animate/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-animate", - "version": "1.0.3", + "version": "1.0.4", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -21,7 +21,7 @@ }, "dependencies": { "@visactor/vutils": "1.0.6", - "@visactor/vrender-core": "workspace:1.0.3" + "@visactor/vrender-core": "workspace:1.0.4" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-components/CHANGELOG.json b/packages/vrender-components/CHANGELOG.json index b62c38ab7..b9e23f40e 100644 --- a/packages/vrender-components/CHANGELOG.json +++ b/packages/vrender-components/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender-components", "entries": [ + { + "version": "1.0.4", + "tag": "@visactor/vrender-components_v1.0.4", + "date": "Wed, 18 Jun 2025 05:41:49 GMT", + "comments": {} + }, { "version": "1.0.3", "tag": "@visactor/vrender-components_v1.0.3", diff --git a/packages/vrender-components/CHANGELOG.md b/packages/vrender-components/CHANGELOG.md index f3d6061ae..017ae7949 100644 --- a/packages/vrender-components/CHANGELOG.md +++ b/packages/vrender-components/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender-components -This log was last generated on Tue, 10 Jun 2025 09:32:32 GMT and should not be manually modified. +This log was last generated on Wed, 18 Jun 2025 05:41:49 GMT and should not be manually modified. + +## 1.0.4 +Wed, 18 Jun 2025 05:41:49 GMT + +_Version update only_ ## 1.0.3 Tue, 10 Jun 2025 09:32:32 GMT diff --git a/packages/vrender-components/package.json b/packages/vrender-components/package.json index e75dc053c..f471f078a 100644 --- a/packages/vrender-components/package.json +++ b/packages/vrender-components/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-components", - "version": "1.0.3", + "version": "1.0.4", "description": "components library for dp visualization", "sideEffects": false, "main": "cjs/index.js", @@ -27,9 +27,9 @@ "dependencies": { "@visactor/vutils": "1.0.6", "@visactor/vscale": "1.0.6", - "@visactor/vrender-core": "workspace:1.0.3", - "@visactor/vrender-kits": "workspace:1.0.3", - "@visactor/vrender-animate": "workspace:1.0.3" + "@visactor/vrender-core": "workspace:1.0.4", + "@visactor/vrender-kits": "workspace:1.0.4", + "@visactor/vrender-animate": "workspace:1.0.4" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-core/CHANGELOG.json b/packages/vrender-core/CHANGELOG.json index 837a01a4b..4acaa7b15 100644 --- a/packages/vrender-core/CHANGELOG.json +++ b/packages/vrender-core/CHANGELOG.json @@ -1,6 +1,21 @@ { "name": "@visactor/vrender-core", "entries": [ + { + "version": "1.0.4", + "tag": "@visactor/vrender-core_v1.0.4", + "date": "Wed, 18 Jun 2025 05:41:49 GMT", + "comments": { + "none": [ + { + "comment": "feat: don't prevent normal animate attribute" + }, + { + "comment": "fix: set finalAttribute when applyStateAttrs" + } + ] + } + }, { "version": "1.0.3", "tag": "@visactor/vrender-core_v1.0.3", diff --git a/packages/vrender-core/CHANGELOG.md b/packages/vrender-core/CHANGELOG.md index 3b7f3c586..73de10bf3 100644 --- a/packages/vrender-core/CHANGELOG.md +++ b/packages/vrender-core/CHANGELOG.md @@ -1,6 +1,14 @@ # Change Log - @visactor/vrender-core -This log was last generated on Tue, 10 Jun 2025 09:32:32 GMT and should not be manually modified. +This log was last generated on Wed, 18 Jun 2025 05:41:49 GMT and should not be manually modified. + +## 1.0.4 +Wed, 18 Jun 2025 05:41:49 GMT + +### Updates + +- feat: don't prevent normal animate attribute +- fix: set finalAttribute when applyStateAttrs ## 1.0.3 Tue, 10 Jun 2025 09:32:32 GMT diff --git a/packages/vrender-core/package.json b/packages/vrender-core/package.json index 9e6f4b996..6329adaaf 100644 --- a/packages/vrender-core/package.json +++ b/packages/vrender-core/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-core", - "version": "1.0.3", + "version": "1.0.4", "description": "", "sideEffects": [ "./src/modules.ts", diff --git a/packages/vrender-kits/CHANGELOG.json b/packages/vrender-kits/CHANGELOG.json index a70e080aa..98da9a15d 100644 --- a/packages/vrender-kits/CHANGELOG.json +++ b/packages/vrender-kits/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender-kits", "entries": [ + { + "version": "1.0.4", + "tag": "@visactor/vrender-kits_v1.0.4", + "date": "Wed, 18 Jun 2025 05:41:49 GMT", + "comments": {} + }, { "version": "1.0.3", "tag": "@visactor/vrender-kits_v1.0.3", diff --git a/packages/vrender-kits/CHANGELOG.md b/packages/vrender-kits/CHANGELOG.md index 30ff49c31..6035d8227 100644 --- a/packages/vrender-kits/CHANGELOG.md +++ b/packages/vrender-kits/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender-kits -This log was last generated on Tue, 10 Jun 2025 09:32:32 GMT and should not be manually modified. +This log was last generated on Wed, 18 Jun 2025 05:41:49 GMT and should not be manually modified. + +## 1.0.4 +Wed, 18 Jun 2025 05:41:49 GMT + +_Version update only_ ## 1.0.3 Tue, 10 Jun 2025 09:32:32 GMT diff --git a/packages/vrender-kits/package.json b/packages/vrender-kits/package.json index 547c884de..9f187901a 100644 --- a/packages/vrender-kits/package.json +++ b/packages/vrender-kits/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-kits", - "version": "1.0.3", + "version": "1.0.4", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -21,7 +21,7 @@ }, "dependencies": { "@visactor/vutils": "1.0.6", - "@visactor/vrender-core": "workspace:1.0.3", + "@visactor/vrender-core": "workspace:1.0.4", "@resvg/resvg-js": "2.4.1", "roughjs": "4.5.2", "gifuct-js": "2.1.2", diff --git a/packages/vrender/CHANGELOG.json b/packages/vrender/CHANGELOG.json index 45bc695d3..3dd0ace11 100644 --- a/packages/vrender/CHANGELOG.json +++ b/packages/vrender/CHANGELOG.json @@ -1,6 +1,21 @@ { "name": "@visactor/vrender", "entries": [ + { + "version": "1.0.4", + "tag": "@visactor/vrender_v1.0.4", + "date": "Wed, 18 Jun 2025 05:41:49 GMT", + "comments": { + "none": [ + { + "comment": "feat: timeline support animationStart flag" + }, + { + "comment": "fix: fix default params of animation\n\n" + } + ] + } + }, { "version": "1.0.3", "tag": "@visactor/vrender_v1.0.3", diff --git a/packages/vrender/CHANGELOG.md b/packages/vrender/CHANGELOG.md index cca8a3db2..e73b3744c 100644 --- a/packages/vrender/CHANGELOG.md +++ b/packages/vrender/CHANGELOG.md @@ -1,6 +1,16 @@ # Change Log - @visactor/vrender -This log was last generated on Tue, 10 Jun 2025 09:32:32 GMT and should not be manually modified. +This log was last generated on Wed, 18 Jun 2025 05:41:49 GMT and should not be manually modified. + +## 1.0.4 +Wed, 18 Jun 2025 05:41:49 GMT + +### Updates + +- feat: timeline support animationStart flag +- fix: fix default params of animation + + ## 1.0.3 Tue, 10 Jun 2025 09:32:32 GMT diff --git a/packages/vrender/package.json b/packages/vrender/package.json index 03232600d..feb6ea9e9 100644 --- a/packages/vrender/package.json +++ b/packages/vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender", - "version": "1.0.3", + "version": "1.0.4", "description": "", "sideEffects": true, "main": "cjs/index.js", @@ -24,9 +24,9 @@ "test-watch": "cross-env DEBUG_MODE=1 jest --watch" }, "dependencies": { - "@visactor/vrender-core": "workspace:1.0.3", - "@visactor/vrender-kits": "workspace:1.0.3", - "@visactor/vrender-animate": "workspace:1.0.3" + "@visactor/vrender-core": "workspace:1.0.4", + "@visactor/vrender-kits": "workspace:1.0.4", + "@visactor/vrender-animate": "workspace:1.0.4" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/tools/bugserver-trigger/package.json b/tools/bugserver-trigger/package.json index ffdad381f..3e19f13e4 100644 --- a/tools/bugserver-trigger/package.json +++ b/tools/bugserver-trigger/package.json @@ -8,11 +8,11 @@ "ci": "ts-node --transpileOnly --skipProject ./scripts/trigger-test.ts" }, "dependencies": { - "@visactor/vrender": "workspace:1.0.3", - "@visactor/vrender-core": "workspace:1.0.3", - "@visactor/vrender-kits": "workspace:1.0.3", - "@visactor/vrender-components": "workspace:1.0.3", - "@visactor/vrender-animate": "workspace:1.0.3" + "@visactor/vrender": "workspace:1.0.4", + "@visactor/vrender-core": "workspace:1.0.4", + "@visactor/vrender-kits": "workspace:1.0.4", + "@visactor/vrender-components": "workspace:1.0.4", + "@visactor/vrender-animate": "workspace:1.0.4" }, "devDependencies": { "@rushstack/eslint-patch": "~1.1.4", From f5a7141560ea45b71a21717dd2d44b241fb43da3 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 18 Jun 2025 16:43:04 +0800 Subject: [PATCH 178/179] feat: export Easing, Ticker support none-stage mode --- packages/vrender-animate/src/index.ts | 1 + packages/vrender-animate/src/ticker/default-ticker.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vrender-animate/src/index.ts b/packages/vrender-animate/src/index.ts index 3df6fb6db..98bd44ba9 100644 --- a/packages/vrender-animate/src/index.ts +++ b/packages/vrender-animate/src/index.ts @@ -7,6 +7,7 @@ export { Step as AnimateStep } from './step'; // 导出工具函数 export * from './utils/easing-func'; +export * from './utils/easing'; export { registerAnimate } from './register'; export { ACustomAnimate, AComponentAnimate } from './custom/custom-animate'; export { ComponentAnimator } from './component/component-animator'; diff --git a/packages/vrender-animate/src/ticker/default-ticker.ts b/packages/vrender-animate/src/ticker/default-ticker.ts index da1a6acd3..21908779e 100644 --- a/packages/vrender-animate/src/ticker/default-ticker.ts +++ b/packages/vrender-animate/src/ticker/default-ticker.ts @@ -224,7 +224,7 @@ export class DefaultTicker extends EventEmitter implements ITicker { } protected checkSkip(delta: number): boolean { - if (this.stage.params.optimize.tickRenderMode === 'performance') { + if (this.stage?.params?.optimize?.tickRenderMode === 'performance') { return false; } // 随机扰动(每次都对interval进行随机的扰动,避免所有tick都发生在同一帧) From 8ba616f410f50226f68cc16a8fb6a3b87229f1b7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 18 Jun 2025 10:15:45 +0000 Subject: [PATCH 179/179] build: prelease version 1.0.5 --- common/config/rush/pnpm-lock.yaml | 34 ++++++++++----------- common/config/rush/version-policies.json | 2 +- docs/package.json | 2 +- packages/react-vrender-utils/CHANGELOG.json | 6 ++++ packages/react-vrender-utils/CHANGELOG.md | 7 ++++- packages/react-vrender-utils/package.json | 6 ++-- packages/react-vrender/CHANGELOG.json | 6 ++++ packages/react-vrender/CHANGELOG.md | 7 ++++- packages/react-vrender/package.json | 4 +-- packages/vrender-animate/CHANGELOG.json | 6 ++++ packages/vrender-animate/CHANGELOG.md | 7 ++++- packages/vrender-animate/package.json | 4 +-- packages/vrender-components/CHANGELOG.json | 6 ++++ packages/vrender-components/CHANGELOG.md | 7 ++++- packages/vrender-components/package.json | 8 ++--- packages/vrender-core/CHANGELOG.json | 6 ++++ packages/vrender-core/CHANGELOG.md | 7 ++++- packages/vrender-core/package.json | 2 +- packages/vrender-kits/CHANGELOG.json | 6 ++++ packages/vrender-kits/CHANGELOG.md | 7 ++++- packages/vrender-kits/package.json | 4 +-- packages/vrender/CHANGELOG.json | 6 ++++ packages/vrender/CHANGELOG.md | 7 ++++- packages/vrender/package.json | 8 ++--- tools/bugserver-trigger/package.json | 10 +++--- 25 files changed, 126 insertions(+), 49 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 4502578bb..5320cfba9 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: specifier: ~0.5.7 version: 0.5.7 '@visactor/vrender': - specifier: workspace:1.0.4 + specifier: workspace:1.0.5 version: link:../packages/vrender '@visactor/vutils': specifier: 1.0.6 @@ -95,7 +95,7 @@ importers: ../../packages/react-vrender: dependencies: '@visactor/vrender': - specifier: workspace:1.0.4 + specifier: workspace:1.0.5 version: link:../vrender '@visactor/vutils': specifier: 1.0.6 @@ -153,10 +153,10 @@ importers: ../../packages/react-vrender-utils: dependencies: '@visactor/react-vrender': - specifier: workspace:1.0.4 + specifier: workspace:1.0.5 version: link:../react-vrender '@visactor/vrender': - specifier: workspace:1.0.4 + specifier: workspace:1.0.5 version: link:../vrender '@visactor/vutils': specifier: 1.0.6 @@ -211,13 +211,13 @@ importers: ../../packages/vrender: dependencies: '@visactor/vrender-animate': - specifier: workspace:1.0.4 + specifier: workspace:1.0.5 version: link:../vrender-animate '@visactor/vrender-core': - specifier: workspace:1.0.4 + specifier: workspace:1.0.5 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:1.0.4 + specifier: workspace:1.0.5 version: link:../vrender-kits devDependencies: '@internal/bundler': @@ -284,7 +284,7 @@ importers: ../../packages/vrender-animate: dependencies: '@visactor/vrender-core': - specifier: workspace:1.0.4 + specifier: workspace:1.0.5 version: link:../vrender-core '@visactor/vutils': specifier: 1.0.6 @@ -342,13 +342,13 @@ importers: ../../packages/vrender-components: dependencies: '@visactor/vrender-animate': - specifier: workspace:1.0.4 + specifier: workspace:1.0.5 version: link:../vrender-animate '@visactor/vrender-core': - specifier: workspace:1.0.4 + specifier: workspace:1.0.5 version: link:../vrender-core '@visactor/vrender-kits': - specifier: workspace:1.0.4 + specifier: workspace:1.0.5 version: link:../vrender-kits '@visactor/vscale': specifier: 1.0.6 @@ -467,7 +467,7 @@ importers: specifier: 2.4.1 version: 2.4.1 '@visactor/vrender-core': - specifier: workspace:1.0.4 + specifier: workspace:1.0.5 version: link:../vrender-core '@visactor/vutils': specifier: 1.0.6 @@ -583,19 +583,19 @@ importers: ../../tools/bugserver-trigger: dependencies: '@visactor/vrender': - specifier: workspace:1.0.4 + specifier: workspace:1.0.5 version: link:../../packages/vrender '@visactor/vrender-animate': - specifier: workspace:1.0.4 + specifier: workspace:1.0.5 version: link:../../packages/vrender-animate '@visactor/vrender-components': - specifier: workspace:1.0.4 + specifier: workspace:1.0.5 version: link:../../packages/vrender-components '@visactor/vrender-core': - specifier: workspace:1.0.4 + specifier: workspace:1.0.5 version: link:../../packages/vrender-core '@visactor/vrender-kits': - specifier: workspace:1.0.4 + specifier: workspace:1.0.5 version: link:../../packages/vrender-kits devDependencies: '@internal/bundler': diff --git a/common/config/rush/version-policies.json b/common/config/rush/version-policies.json index daf4cebf5..5973d98cf 100644 --- a/common/config/rush/version-policies.json +++ b/common/config/rush/version-policies.json @@ -1 +1 @@ -[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"1.0.4","nextBump":"patch"}] +[{"definitionName":"lockStepVersion","policyName":"vrenderMain","version":"1.0.5","nextBump":"patch"}] diff --git a/docs/package.json b/docs/package.json index ea894342b..f7ab9f9c6 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,7 +13,7 @@ "@visactor/vchart": "1.3.0", "@visactor/vutils": "1.0.6", "@visactor/vgrammar": "~0.5.7", - "@visactor/vrender": "workspace:1.0.4", + "@visactor/vrender": "workspace:1.0.5", "markdown-it": "^13.0.0", "highlight.js": "^11.8.0", "axios": "^1.4.0", diff --git a/packages/react-vrender-utils/CHANGELOG.json b/packages/react-vrender-utils/CHANGELOG.json index 7ef65d143..2e3153906 100644 --- a/packages/react-vrender-utils/CHANGELOG.json +++ b/packages/react-vrender-utils/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/react-vrender-utils", "entries": [ + { + "version": "1.0.5", + "tag": "@visactor/react-vrender-utils_v1.0.5", + "date": "Wed, 18 Jun 2025 10:09:34 GMT", + "comments": {} + }, { "version": "1.0.4", "tag": "@visactor/react-vrender-utils_v1.0.4", diff --git a/packages/react-vrender-utils/CHANGELOG.md b/packages/react-vrender-utils/CHANGELOG.md index c01d25dcf..afc710e47 100644 --- a/packages/react-vrender-utils/CHANGELOG.md +++ b/packages/react-vrender-utils/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/react-vrender-utils -This log was last generated on Wed, 18 Jun 2025 05:41:49 GMT and should not be manually modified. +This log was last generated on Wed, 18 Jun 2025 10:09:34 GMT and should not be manually modified. + +## 1.0.5 +Wed, 18 Jun 2025 10:09:34 GMT + +_Version update only_ ## 1.0.4 Wed, 18 Jun 2025 05:41:49 GMT diff --git a/packages/react-vrender-utils/package.json b/packages/react-vrender-utils/package.json index 2b6e50df0..e9878cb30 100644 --- a/packages/react-vrender-utils/package.json +++ b/packages/react-vrender-utils/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender-utils", - "version": "1.0.4", + "version": "1.0.5", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -24,8 +24,8 @@ "react-dom": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:1.0.4", - "@visactor/react-vrender": "workspace:1.0.4", + "@visactor/vrender": "workspace:1.0.5", + "@visactor/react-vrender": "workspace:1.0.5", "@visactor/vutils": "1.0.6", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/react-vrender/CHANGELOG.json b/packages/react-vrender/CHANGELOG.json index b544c3288..7f23c25e8 100644 --- a/packages/react-vrender/CHANGELOG.json +++ b/packages/react-vrender/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/react-vrender", "entries": [ + { + "version": "1.0.5", + "tag": "@visactor/react-vrender_v1.0.5", + "date": "Wed, 18 Jun 2025 10:09:34 GMT", + "comments": {} + }, { "version": "1.0.4", "tag": "@visactor/react-vrender_v1.0.4", diff --git a/packages/react-vrender/CHANGELOG.md b/packages/react-vrender/CHANGELOG.md index d1d35cab2..52e676b06 100644 --- a/packages/react-vrender/CHANGELOG.md +++ b/packages/react-vrender/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/react-vrender -This log was last generated on Wed, 18 Jun 2025 05:41:49 GMT and should not be manually modified. +This log was last generated on Wed, 18 Jun 2025 10:09:34 GMT and should not be manually modified. + +## 1.0.5 +Wed, 18 Jun 2025 10:09:34 GMT + +_Version update only_ ## 1.0.4 Wed, 18 Jun 2025 05:41:49 GMT diff --git a/packages/react-vrender/package.json b/packages/react-vrender/package.json index 83a7bca91..b1b38fb5e 100644 --- a/packages/react-vrender/package.json +++ b/packages/react-vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vrender", - "version": "1.0.4", + "version": "1.0.5", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -23,7 +23,7 @@ "react": "^18.2.0" }, "dependencies": { - "@visactor/vrender": "workspace:1.0.4", + "@visactor/vrender": "workspace:1.0.5", "@visactor/vutils": "1.0.6", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" diff --git a/packages/vrender-animate/CHANGELOG.json b/packages/vrender-animate/CHANGELOG.json index ff3133b83..798801745 100644 --- a/packages/vrender-animate/CHANGELOG.json +++ b/packages/vrender-animate/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender-animate", "entries": [ + { + "version": "1.0.5", + "tag": "@visactor/vrender-animate_v1.0.5", + "date": "Wed, 18 Jun 2025 10:09:34 GMT", + "comments": {} + }, { "version": "1.0.4", "tag": "@visactor/vrender-animate_v1.0.4", diff --git a/packages/vrender-animate/CHANGELOG.md b/packages/vrender-animate/CHANGELOG.md index 333d144d7..0d526dee2 100644 --- a/packages/vrender-animate/CHANGELOG.md +++ b/packages/vrender-animate/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender-animate -This log was last generated on Wed, 18 Jun 2025 05:41:49 GMT and should not be manually modified. +This log was last generated on Wed, 18 Jun 2025 10:09:34 GMT and should not be manually modified. + +## 1.0.5 +Wed, 18 Jun 2025 10:09:34 GMT + +_Version update only_ ## 1.0.4 Wed, 18 Jun 2025 05:41:49 GMT diff --git a/packages/vrender-animate/package.json b/packages/vrender-animate/package.json index 5b1fe9923..857965e47 100644 --- a/packages/vrender-animate/package.json +++ b/packages/vrender-animate/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-animate", - "version": "1.0.4", + "version": "1.0.5", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -21,7 +21,7 @@ }, "dependencies": { "@visactor/vutils": "1.0.6", - "@visactor/vrender-core": "workspace:1.0.4" + "@visactor/vrender-core": "workspace:1.0.5" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-components/CHANGELOG.json b/packages/vrender-components/CHANGELOG.json index b9e23f40e..781c7d81d 100644 --- a/packages/vrender-components/CHANGELOG.json +++ b/packages/vrender-components/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender-components", "entries": [ + { + "version": "1.0.5", + "tag": "@visactor/vrender-components_v1.0.5", + "date": "Wed, 18 Jun 2025 10:09:34 GMT", + "comments": {} + }, { "version": "1.0.4", "tag": "@visactor/vrender-components_v1.0.4", diff --git a/packages/vrender-components/CHANGELOG.md b/packages/vrender-components/CHANGELOG.md index 017ae7949..b10163851 100644 --- a/packages/vrender-components/CHANGELOG.md +++ b/packages/vrender-components/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender-components -This log was last generated on Wed, 18 Jun 2025 05:41:49 GMT and should not be manually modified. +This log was last generated on Wed, 18 Jun 2025 10:09:34 GMT and should not be manually modified. + +## 1.0.5 +Wed, 18 Jun 2025 10:09:34 GMT + +_Version update only_ ## 1.0.4 Wed, 18 Jun 2025 05:41:49 GMT diff --git a/packages/vrender-components/package.json b/packages/vrender-components/package.json index f471f078a..67c97ab36 100644 --- a/packages/vrender-components/package.json +++ b/packages/vrender-components/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-components", - "version": "1.0.4", + "version": "1.0.5", "description": "components library for dp visualization", "sideEffects": false, "main": "cjs/index.js", @@ -27,9 +27,9 @@ "dependencies": { "@visactor/vutils": "1.0.6", "@visactor/vscale": "1.0.6", - "@visactor/vrender-core": "workspace:1.0.4", - "@visactor/vrender-kits": "workspace:1.0.4", - "@visactor/vrender-animate": "workspace:1.0.4" + "@visactor/vrender-core": "workspace:1.0.5", + "@visactor/vrender-kits": "workspace:1.0.5", + "@visactor/vrender-animate": "workspace:1.0.5" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-core/CHANGELOG.json b/packages/vrender-core/CHANGELOG.json index 4acaa7b15..0fd9368df 100644 --- a/packages/vrender-core/CHANGELOG.json +++ b/packages/vrender-core/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender-core", "entries": [ + { + "version": "1.0.5", + "tag": "@visactor/vrender-core_v1.0.5", + "date": "Wed, 18 Jun 2025 10:09:34 GMT", + "comments": {} + }, { "version": "1.0.4", "tag": "@visactor/vrender-core_v1.0.4", diff --git a/packages/vrender-core/CHANGELOG.md b/packages/vrender-core/CHANGELOG.md index 73de10bf3..e9273ad7d 100644 --- a/packages/vrender-core/CHANGELOG.md +++ b/packages/vrender-core/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender-core -This log was last generated on Wed, 18 Jun 2025 05:41:49 GMT and should not be manually modified. +This log was last generated on Wed, 18 Jun 2025 10:09:34 GMT and should not be manually modified. + +## 1.0.5 +Wed, 18 Jun 2025 10:09:34 GMT + +_Version update only_ ## 1.0.4 Wed, 18 Jun 2025 05:41:49 GMT diff --git a/packages/vrender-core/package.json b/packages/vrender-core/package.json index 6329adaaf..e57d90f04 100644 --- a/packages/vrender-core/package.json +++ b/packages/vrender-core/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-core", - "version": "1.0.4", + "version": "1.0.5", "description": "", "sideEffects": [ "./src/modules.ts", diff --git a/packages/vrender-kits/CHANGELOG.json b/packages/vrender-kits/CHANGELOG.json index 98da9a15d..3a06118f3 100644 --- a/packages/vrender-kits/CHANGELOG.json +++ b/packages/vrender-kits/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender-kits", "entries": [ + { + "version": "1.0.5", + "tag": "@visactor/vrender-kits_v1.0.5", + "date": "Wed, 18 Jun 2025 10:09:34 GMT", + "comments": {} + }, { "version": "1.0.4", "tag": "@visactor/vrender-kits_v1.0.4", diff --git a/packages/vrender-kits/CHANGELOG.md b/packages/vrender-kits/CHANGELOG.md index 6035d8227..fdab75a59 100644 --- a/packages/vrender-kits/CHANGELOG.md +++ b/packages/vrender-kits/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender-kits -This log was last generated on Wed, 18 Jun 2025 05:41:49 GMT and should not be manually modified. +This log was last generated on Wed, 18 Jun 2025 10:09:34 GMT and should not be manually modified. + +## 1.0.5 +Wed, 18 Jun 2025 10:09:34 GMT + +_Version update only_ ## 1.0.4 Wed, 18 Jun 2025 05:41:49 GMT diff --git a/packages/vrender-kits/package.json b/packages/vrender-kits/package.json index 9f187901a..48b345f13 100644 --- a/packages/vrender-kits/package.json +++ b/packages/vrender-kits/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender-kits", - "version": "1.0.4", + "version": "1.0.5", "description": "", "sideEffects": false, "main": "cjs/index.js", @@ -21,7 +21,7 @@ }, "dependencies": { "@visactor/vutils": "1.0.6", - "@visactor/vrender-core": "workspace:1.0.4", + "@visactor/vrender-core": "workspace:1.0.5", "@resvg/resvg-js": "2.4.1", "roughjs": "4.5.2", "gifuct-js": "2.1.2", diff --git a/packages/vrender/CHANGELOG.json b/packages/vrender/CHANGELOG.json index 3dd0ace11..d3f179333 100644 --- a/packages/vrender/CHANGELOG.json +++ b/packages/vrender/CHANGELOG.json @@ -1,6 +1,12 @@ { "name": "@visactor/vrender", "entries": [ + { + "version": "1.0.5", + "tag": "@visactor/vrender_v1.0.5", + "date": "Wed, 18 Jun 2025 10:09:34 GMT", + "comments": {} + }, { "version": "1.0.4", "tag": "@visactor/vrender_v1.0.4", diff --git a/packages/vrender/CHANGELOG.md b/packages/vrender/CHANGELOG.md index e73b3744c..2dfe3ca07 100644 --- a/packages/vrender/CHANGELOG.md +++ b/packages/vrender/CHANGELOG.md @@ -1,6 +1,11 @@ # Change Log - @visactor/vrender -This log was last generated on Wed, 18 Jun 2025 05:41:49 GMT and should not be manually modified. +This log was last generated on Wed, 18 Jun 2025 10:09:34 GMT and should not be manually modified. + +## 1.0.5 +Wed, 18 Jun 2025 10:09:34 GMT + +_Version update only_ ## 1.0.4 Wed, 18 Jun 2025 05:41:49 GMT diff --git a/packages/vrender/package.json b/packages/vrender/package.json index feb6ea9e9..af055af5f 100644 --- a/packages/vrender/package.json +++ b/packages/vrender/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vrender", - "version": "1.0.4", + "version": "1.0.5", "description": "", "sideEffects": true, "main": "cjs/index.js", @@ -24,9 +24,9 @@ "test-watch": "cross-env DEBUG_MODE=1 jest --watch" }, "dependencies": { - "@visactor/vrender-core": "workspace:1.0.4", - "@visactor/vrender-kits": "workspace:1.0.4", - "@visactor/vrender-animate": "workspace:1.0.4" + "@visactor/vrender-core": "workspace:1.0.5", + "@visactor/vrender-kits": "workspace:1.0.5", + "@visactor/vrender-animate": "workspace:1.0.5" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/tools/bugserver-trigger/package.json b/tools/bugserver-trigger/package.json index 3e19f13e4..d27e2c9bb 100644 --- a/tools/bugserver-trigger/package.json +++ b/tools/bugserver-trigger/package.json @@ -8,11 +8,11 @@ "ci": "ts-node --transpileOnly --skipProject ./scripts/trigger-test.ts" }, "dependencies": { - "@visactor/vrender": "workspace:1.0.4", - "@visactor/vrender-core": "workspace:1.0.4", - "@visactor/vrender-kits": "workspace:1.0.4", - "@visactor/vrender-components": "workspace:1.0.4", - "@visactor/vrender-animate": "workspace:1.0.4" + "@visactor/vrender": "workspace:1.0.5", + "@visactor/vrender-core": "workspace:1.0.5", + "@visactor/vrender-kits": "workspace:1.0.5", + "@visactor/vrender-components": "workspace:1.0.5", + "@visactor/vrender-animate": "workspace:1.0.5" }, "devDependencies": { "@rushstack/eslint-patch": "~1.1.4",