Skip to content

Conversation

q32757468
Copy link
Contributor

@q32757468 q32757468 commented Sep 25, 2025

Description

image

不同动画api的效果图:
录屏2025-09-25 17 47 09
录屏2025-09-25 17 48 55
录屏2025-09-25 17 49 29

Motivation and Context

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Enhancement (changes that improvement of current feature or performance)
  • Refactoring (changes that neither fixes a bug nor adds a feature)
  • Test Case (changes that add missing tests or correct existing tests)
  • Code style optimization (changes that do not affect the meaning of the code)
  • Docs (changes that only update documentation)
  • Chore (changes that don't modify src or test files)

Self Check before Merge

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

Copy link
Contributor

Summary of Changes

Hello @q32757468, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求迁移并优化了动画能力,为图表元素引入了新的动画方法。它提供了灵活的 SVG 动画工具,并将其集成到 CellViewEdgeView 中,同时新增了丰富的示例来展示这些功能的用法和效果,旨在提升图表的交互性和视觉吸引力。

Highlights

  • 新增动画能力: 引入了 animateanimateTransformanimateAlongPathsendToken 等新的动画方法,极大地增强了图表元素的动态表现力。
  • 视图层集成: 这些新的动画能力已集成到 CellViewEdgeView 中,方便开发者在图元素上直接调用和使用,简化了动画的实现过程。
  • 通用动画工具: 在 src/common/util/index.ts 中新增了通用的 SVG 动画工具函数,包括 animateanimateTransformanimateAlongPath,为更底层的动画控制提供了支持。
  • 新增示例: 添加了多个示例页面 (AnimateExample, SendTokenExample, AnimateTransformExample, AnimateAlongPathExample) 来演示这些新的动画功能,帮助用户快速理解和上手。
  • view:render 事件: CellViewEdgeView 新增了 view:render 事件,允许在视图渲染完成后触发动画,确保动画在元素准备就绪后执行。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

Summary of Changes

Hello @q32757468, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求迁移并优化了图表库的动画能力,引入了一系列新的 SVG 动画功能,包括通用属性动画、变换动画和沿路径动画。这些增强功能旨在提升图表元素的视觉动态性和交互性,并提供了在边上动画显示令牌的特定功能,以更好地支持流程和状态的可视化。同时,新增了相应的示例来帮助开发者理解和使用这些新特性。

Highlights

  • 新增动画能力: 引入了 animateanimateTransformanimateAlongPath 函数,用于实现 SVG 元素的通用动画、变换动画和沿路径动画。
  • 边上的令牌动画: 在 EdgeView 中新增了 sendToken 方法,允许在边的路径上动画显示令牌,增强了流程可视化的动态效果。
  • 新增动画示例: 添加了多个新的示例页面 (AnimateExample, AnimateTransformExample, AnimateAlongPathExample, SendTokenExample),以直观展示新引入的动画功能。
  • 视图渲染事件: 在 CellView (包括 NodeViewEdgeView) 中新增了 view:render 事件,允许在视图渲染完成后触发自定义逻辑。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

这个 PR 很好地迁移并优化了 v1 的动画能力,通过在 CellViewEdgeView 上暴露新的动画方法,提供了更强大和灵活的动画功能。代码结构清晰,并且添加了丰富的示例来演示新功能。我发现了一些可以改进的地方:一处是 sendToken 方法中的拼写错误,另一处是 CellView 中动画相关方法的代码重复问题。修正这些问题将有助于提高代码的健壮性和可维护性。整体来说,这是一次很棒的更新。

Comment on lines 2442 to 2492
let duration
let reversed
let selector
let rorate
let timing = 'linear'
if (typeof options === 'object') {
duration = options.duration
reversed = options.reversed === true
selector = options.selector
if (options.rotate === false) {
rorate = ''
} else if (options.rotate === true) {
rorate = 'auto'
} else if (options.rotate != null) {
rorate = `${options.rotate}`
}

if (options.timing) {
timing = options.timing
}
} else {
duration = options
reversed = false
selector = null
}

duration = duration || 1000

const attrs: AnimationOptions = {
dur: `${duration}ms`,
repeatCount: '1',
calcMode: timing,
fill: 'freeze',
}

if (rorate) {
attrs.rotate = rorate
}

if (reversed) {
attrs.keyPoints = '1;0'
attrs.keyTimes = '0;1'
}

if (typeof options === 'object') {
const { duration, reversed, selector, rotate, timing, ...others } =
options
Object.keys(others).forEach((key) => {
attrs[key] = others[key]
})
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

sendToken 方法中,变量 rorate 存在拼写错误,应为 rotate。这个拼写错误导致 rotate 动画选项无法正确传递。请修正该变量及其所有引用。

    let duration
    let reversed
    let selector
    let rotate
    let timing = 'linear'
    if (typeof options === 'object') {
      duration = options.duration
      reversed = options.reversed === true
      selector = options.selector
      if (options.rotate === false) {
        rotate = ''
      } else if (options.rotate === true) {
        rotate = 'auto'
      } else if (options.rotate != null) {
        rotate = `${options.rotate}`
      }

      if (options.timing) {
        timing = options.timing
      }
    } else {
      duration = options
      reversed = false
      selector = null
    }

    duration = duration || 1000

    const attrs: AnimationOptions = {
      dur: `${duration}ms`,
      repeatCount: '1',
      calcMode: timing,
      fill: 'freeze',
    }

    if (rotate) {
      attrs.rotate = rotate
    }

    if (reversed) {
      attrs.keyPoints = '1;0'
      attrs.keyTimes = '0;1'
    }

    if (typeof options === 'object') {
      const { duration, reversed, selector, rotate, timing, ...others } =
        options
      Object.keys(others).forEach((key) => {
        attrs[key] = others[key]
      })
    }

Comment on lines 586 to 686
animate(elem: SVGElement | string, options: Util.AnimationOptions) {
const target = typeof elem === 'string' ? this.findOne(elem) : elem
if (target == null) {
throw new Error('Invalid animation element.')
}

const revert = () => {
if (!target.parentNode) {
Dom.remove(target)
}
}

if (!target.parentNode) {
Dom.appendTo(target, this.graph.view.stage)
}

const onComplete = options.complete
options.complete = (e: Event) => {
revert()

if (onComplete) {
onComplete(e)
}
}

return Util.animate(target as SVGElement, options)
}

animateTransform(elem: SVGElement | string, options: Util.AnimationOptions) {
const target = typeof elem === 'string' ? this.findOne(elem) : elem
if (target == null) {
throw new Error('Invalid animation element.')
}

const revert = () => {
if (!target.parentNode) {
Dom.remove(target)
}
}

if (!target.parentNode) {
Dom.appendTo(target, this.graph.view.stage)
}

const onComplete = options.complete
options.complete = (e: Event) => {
revert()

if (onComplete) {
onComplete(e)
}
}

return Util.animateTransform(target as SVGElement, options)
}

animateAlongPath(
elem: SVGElement | string,
options: {
duration?: number
reversed?: boolean
selector?: string
// https://developer.mozilla.org/zh-CN/docs/Web/SVG/Attribute/rotate
rotate?: boolean | number | 'auto' | 'auto-reverse'
// https://developer.mozilla.org/zh-CN/docs/Web/SVG/Attribute/calcMode
timing?: 'linear' | 'discrete' | 'paced' | 'spline'
} & Util.AnimationOptions,
) {
const target = typeof elem === 'string' ? this.findOne(elem) : elem
if (target == null) {
throw new Error('Invalid animation element.')
}

let path
const { selector } = options

if (typeof selector === 'string') {
path = this.findOne(selector, this.container, this.selectors)
} else {
path = this.container.querySelector('path')
}

if (!(path instanceof SVGPathElement)) {
throw new Error('Token animation requires a valid connection path.')
}

const revert = () => {
if (!target.parentNode) {
Dom.remove(target)
}
}

if (!target.parentNode) {
Dom.appendTo(target, this.graph.view.stage)
}

const onComplete = options.complete
options.complete = (e: Event) => {
revert()

if (onComplete) {
onComplete(e)
}
}

return Util.animateAlongPath(target as SVGElement, options, path)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

animateanimateTransformanimateAlongPath 方法中存在大量重复代码,主要用于准备动画目标元素(查找、附加到 DOM、包装 complete 回调)。为了提高代码的可维护性和减少冗余,建议将这部分通用逻辑提取到一个私有辅助方法中。

  private _prepareAnimation(elem: SVGElement | string, options: Util.AnimationOptions) {
    const target = typeof elem === 'string' ? this.findOne(elem) : elem
    if (target == null) {
      throw new Error('Invalid animation element.')
    }

    const revert = () => {
      if (!target.parentNode) {
        Dom.remove(target)
      }
    }

    if (!target.parentNode) {
      Dom.appendTo(target, this.graph.view.stage)
    }

    const onComplete = options.complete
    options.complete = (e: Event) => {
      revert()

      if (onComplete) {
        onComplete(e)
      }
    }

    return target as SVGElement
  }

  animate(elem: SVGElement | string, options: Util.AnimationOptions) {
    const target = this._prepareAnimation(elem, options)
    return Util.animate(target, options)
  }

  animateTransform(elem: SVGElement | string, options: Util.AnimationOptions) {
    const target = this._prepareAnimation(elem, options)
    return Util.animateTransform(target, options)
  }

  animateAlongPath(
    elem: SVGElement | string,
    options: {
      duration?: number
      reversed?: boolean
      selector?: string
      // https://developer.mozilla.org/zh-CN/docs/Web/SVG/Attribute/rotate
      rotate?: boolean | number | 'auto' | 'auto-reverse'
      // https://developer.mozilla.org/zh-CN/docs/Web/SVG/Attribute/calcMode
      timing?: 'linear' | 'discrete' | 'paced' | 'spline'
    } & Util.AnimationOptions,
  ) {
    const target = this._prepareAnimation(elem, options)

    let path
    const { selector } = options

    if (typeof selector === 'string') {
      path = this.findOne(selector, this.container, this.selectors)
    } else {
      path = this.container.querySelector('path')
    }

    if (!(path instanceof SVGPathElement)) {
      throw new Error('Token animation requires a valid connection path.')
    }

    return Util.animateAlongPath(target, options, path)
  }

@codecov-commenter
Copy link

codecov-commenter commented Sep 25, 2025

Codecov Report

❌ Patch coverage is 4.14938% with 231 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.55%. Comparing base (e90f305) to head (9d1b81f).
⚠️ Report is 4 commits behind head on master.

Files with missing lines Patch % Lines
src/view/edge/index.ts 3.22% 90 Missing ⚠️
src/view/cell/index.ts 3.79% 76 Missing ⚠️
src/common/util/index.ts 4.41% 65 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #4798      +/-   ##
==========================================
- Coverage   82.23%   81.55%   -0.69%     
==========================================
  Files         328      327       -1     
  Lines       37613    37869     +256     
  Branches     7584     7591       +7     
==========================================
- Hits        30930    30883      -47     
- Misses       6659     6962     +303     
  Partials       24       24              
Flag Coverage Δ
x6 81.55% <4.14%> (-0.69%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/view/cell/type.ts 100.00% <ø> (ø)
src/view/node/index.ts 64.76% <100.00%> (+0.03%) ⬆️
src/common/util/index.ts 78.19% <4.41%> (-18.18%) ⬇️
src/view/cell/index.ts 80.35% <3.79%> (-10.03%) ⬇️
src/view/edge/index.ts 57.07% <3.22%> (-2.55%) ⬇️

... and 11 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

这次的 PR 迁移并优化了 v1 的动画能力,引入了 animateanimateTransformanimateAlongPathsendToken 等新的动画功能,并提供了相应的示例,功能强大。实现基于 SVG SMIL,这是一个很好的选择。我的审查主要关注于提高代码质量、可维护性和健壮性。我发现了一些代码重复、可以更清晰实现的逻辑以及可以改进的错误处理等方面的问题。

Comment on lines +435 to +437
} catch (error) {
// pass
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

createAnimation 函数中,try...catch 块静默地捕获了 setupAnimation 可能抛出的错误。这可能导致动画失败时难以调试。建议至少在开发模式下打印错误。此外,当 setupAnimation 失败时,创建的 <animate> 元素没有从 DOM 中移除,可能导致内存泄漏。建议在捕获到错误时移除该元素。

Suggested change
} catch (error) {
// pass
}
} catch (error) {
elem.removeChild(animate)
// pass
}

Comment on lines +586 to +640
animate(elem: SVGElement | string, options: Util.AnimationOptions) {
const target = typeof elem === 'string' ? this.findOne(elem) : elem
if (target == null) {
throw new Error('Invalid animation element.')
}

const revert = () => {
if (!target.parentNode) {
Dom.remove(target)
}
}

if (!target.parentNode) {
Dom.appendTo(target, this.graph.view.stage)
}

const onComplete = options.complete
options.complete = (e: Event) => {
revert()

if (onComplete) {
onComplete(e)
}
}

return Util.animate(target as SVGElement, options)
}

animateTransform(elem: SVGElement | string, options: Util.AnimationOptions) {
const target = typeof elem === 'string' ? this.findOne(elem) : elem
if (target == null) {
throw new Error('Invalid animation element.')
}

const revert = () => {
if (!target.parentNode) {
Dom.remove(target)
}
}

if (!target.parentNode) {
Dom.appendTo(target, this.graph.view.stage)
}

const onComplete = options.complete
options.complete = (e: Event) => {
revert()

if (onComplete) {
onComplete(e)
}
}

return Util.animateTransform(target as SVGElement, options)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

animateanimateTransform 方法的实现几乎完全相同,唯一的区别是它们分别调用了 Util.animateUtil.animateTransform。这造成了代码重复。建议将通用逻辑提取到一个私有方法中,以提高代码的可维护性。

  private createCellAnimation(
    elem: SVGElement | string,
    options: Util.AnimationOptions,
    animateFn: (elem: SVGElement, options: Util.AnimationOptions) => () => void,
  ) {
    const target = typeof elem === 'string' ? this.findOne(elem) : elem
    if (target == null) {
      throw new Error('Invalid animation element.')
    }

    const revert = () => {
      if (!target.parentNode) {
        Dom.remove(target)
      }
    }

    if (!target.parentNode) {
      Dom.appendTo(target, this.graph.view.stage)
    }

    const onComplete = options.complete
    options.complete = (e: Event) => {
      revert()

      if (onComplete) {
        onComplete(e)
      }
    }

    return animateFn(target as SVGElement, options)
  }

  animate(elem: SVGElement | string, options: Util.AnimationOptions) {
    return this.createCellAnimation(elem, options, Util.animate)
  }

  animateTransform(elem: SVGElement | string, options: Util.AnimationOptions) {
    return this.createCellAnimation(elem, options, Util.animateTransform)
  }

if (typeof selector === 'string') {
path = this.findOne(selector, this.container, this.selectors)
} else {
path = this.container.querySelector('path')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

animateAlongPath 方法中,当没有提供 selector 时,会默认使用 this.container.querySelector('path') 来查找路径。如果 container 中存在多个 <path> 元素,这可能会导致选择到错误的路径,使动画行为不符合预期。建议在检测到多个路径时抛出错误或显示警告,以提高鲁棒性。

let duration
let reversed
let selector
let rorate
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

sendToken 方法中,变量 rorate 存在拼写错误,应该是 rotate。这个拼写错误在方法中多处出现,建议统一修正。

Suggested change
let rorate
let rotate

if (typeof selector === 'string') {
path = this.findOne(selector, this.container, this.selectors)
} else {
path = this.container.querySelector('path')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

sendToken 方法中,当没有提供 selector 时,会默认使用 this.container.querySelector('path') 来查找路径。如果 container 中存在多个 <path> 元素,这可能会导致选择到错误的路径,使动画行为不符合预期。建议在检测到多个路径时抛出错误或显示警告,以提高鲁棒性。

})
const view2 = graph.findView(rect2)

view2?.on('view:render', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

用法上感觉怪怪的

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我感觉也和现有的其他使用形式不太搭,但是考虑和参考了很多其他的方式,只有这种改动成本最低最使用最简单了,开会可以再讨论讨论~

// pass
}

return () => {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

是否考虑支持链式调用 .animate().animate()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

好想法,从使用形式上来看支持链式调用在添加多个动画效果时会方便很多,尝试支持一下看看支持链式调用会不会有其他问题

const token = Vector.create('circle', { r: 6, fill: 'green' })

view.on('view:render', () => {
view.sendToken(token.node, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个示例感觉也不合适。

Copy link
Member

@lxfu1 lxfu1 Sep 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

可考虑下配置式实现,默认是节点加载完成开始动画,节点上添加对应的 API,例如 startAnimate
stopAnimate

@q32757468 q32757468 changed the title feat: 迁移并优化v1动画能力 WIP: feat: 迁移并优化v1动画能力 Oct 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants