Skip to content

Commit

Permalink
Proper port position updates during animations (#12179)
Browse files Browse the repository at this point in the history
  • Loading branch information
Frizi authored Feb 3, 2025
1 parent a310865 commit 50c9d53
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 10 deletions.
2 changes: 0 additions & 2 deletions app/gui/src/project-view/components/GraphEditor/GraphNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -599,8 +599,6 @@ onWindowBlur(() => {
align-items: center;
white-space: nowrap;
z-index: 24;
transition: outline 0.2s ease;
outline: 0px solid transparent;
}
.binding {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ const backgroundOffset = computed(() => 200 - props.file.sizePercentage)
white-space: nowrap;
padding: 4px 8px;
z-index: 2;
outline: 0px solid transparent;
background: linear-gradient(to right, #e0e0e0 0%, #e0e0e0 50%, #ffffff 50%, #ffffff 100%);
background-size: 200% 100%;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<script setup lang="ts">
import NodeWidget from '@/components/GraphEditor/NodeWidget.vue'
import { useTransitioning } from '@/composables/animation'
import { useLayoutAnimationsState } from '@/providers/animationCounter'
import { WidgetInput, type WidgetUpdate } from '@/providers/widgetRegistry'
import { WidgetEditHandlerParent } from '@/providers/widgetRegistry/editHandler'
import { provideWidgetTree } from '@/providers/widgetTree'
import { Ast } from '@/util/ast'
import { toRef, watch } from 'vue'
import { computed, toRef, watch } from 'vue'
import { AstId } from 'ydoc-shared/ast'
import { ExternalId } from 'ydoc-shared/yjsModel'
Expand Down Expand Up @@ -37,13 +38,18 @@ const layoutTransitions = useTransitioning(
'height',
]),
)
const layoutAnimations = useLayoutAnimationsState()
const anyLayoutAnimationActive = computed(
() => layoutTransitions.active.value || layoutAnimations.anyAnimationActive,
)
const tree = provideWidgetTree(
toRef(props, 'externalId'),
toRef(props, 'rootElement'),
toRef(props, 'conditionalPorts'),
toRef(props, 'extended'),
layoutTransitions.active,
anyLayoutAnimationActive,
toRef(props, 'potentialSelfArgumentId'),
)
watch(toRef(tree, 'currentEdit'), (edit) => emit('currentEditChanged', edit))
Expand Down
9 changes: 9 additions & 0 deletions app/gui/src/project-view/components/SizeTransition.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
export default {}
</script>
<script setup lang="ts">
import { useLayoutAnimationReporter } from '@/providers/animationCounter'
import { hookBeforeFunctionCall } from '@/util/patching'
import { nextTick } from 'vue'
Expand All @@ -35,6 +36,8 @@ const props = withDefaults(
{ duration: 200, easing: 'ease-out' },
)
const animReporter = useLayoutAnimationReporter()
type Done = (cancelled: boolean) => void
type StyleSnapshot = { width: string; height: string; marginLeft: string; progress: string }
const styleSnapshots = new WeakMap<HTMLElement, StyleSnapshot>()
Expand Down Expand Up @@ -125,13 +128,19 @@ function runAnimation(e: HTMLElement, done: Done, isEnter: boolean) {
cleanup(e)
done(true)
})
animation.addEventListener('remove', () => {
cleanup(e)
done(true)
})
e.dataset['transitioning'] = isEnter ? 'enter' : 'leave'
animReporter.reportAnimationStarted()
animation.play()
animationsMap.set(e, animation)
}
function cleanup(e: HTMLElement) {
delete e.dataset['transitioning']
animReporter.reportAnimationEnded()
}
</script>

Expand Down
16 changes: 11 additions & 5 deletions app/gui/src/project-view/composables/animation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,26 +156,32 @@ function useApproachBase<T>(
return readonly(proxyRefs({ value: current, skip }))
}

/** TODO: Add docs */
/**
* Create `events` to check if any CSS transitions of declared properties
* within a DOM subtree are currently in progress.
*
* The state is reported back using the `active` property.
*/
export function useTransitioning(observedProperties?: Set<string>) {
const hasActiveAnimations = ref(false)
const hasActiveTransitions = ref(false)

let numActiveTransitions = 0
function onTransitionStart(e: TransitionEvent) {
if (!observedProperties || observedProperties.has(e.propertyName)) {
if (numActiveTransitions == 0) hasActiveAnimations.value = true
if (numActiveTransitions == 0) hasActiveTransitions.value = true
numActiveTransitions += 1
}
}

function onTransitionEnd(e: TransitionEvent) {
if (!observedProperties || observedProperties.has(e.propertyName)) {
numActiveTransitions -= 1
if (numActiveTransitions == 0) hasActiveAnimations.value = false
if (numActiveTransitions == 0) hasActiveTransitions.value = false
}
}

return {
active: hasActiveAnimations,
active: readonly(hasActiveTransitions),
events: {
transitionstart: onTransitionStart,
transitionend: onTransitionEnd,
Expand Down
69 changes: 69 additions & 0 deletions app/gui/src/project-view/providers/animationCounter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { computed, onScopeDispose, proxyRefs, ref } from 'vue'
import { createContextStore } from '.'

/**
* A counter context that is counting actively running javascript-based layout animations,
* interfaced with using `useAnimationReporter` and `useAnimationsState` composables.
*
* This context supports nesting. Any animation reports within a child context will be
* also reported back to parent contexts, to make sure that `anyAnimationActive` property
* is correctly representing the whole component subtree, even when nesting is present.
*/
const [provideAnimationCounter, injectAnimationCounter] = createContextStore(
'animation counter',
() => {
const parent = injectAnimationCounter(true)
const reportedCount = ref(0)

let disposed = false
onScopeDispose(() => {
modifyCount(-reportedCount.value)
disposed = true
})

function modifyCount(change: number) {
if (disposed) return
reportedCount.value += change
parent?.modifyCount(change)
}

return {
modifyCount,
anyAnimationActive: computed(() => reportedCount.value > 0),
}
},
)

/**
* A composable that allows reporting information about currently scheduled javascript animations
* that can affect DOM element layout, so that any parent component can temporarily enable extra
* logic necessary to track them.
*
* This is NOT supposed to be used forcounting CSS-driven animations, as those can already be discovered
* using `animationstart` and similar events.
*
* See `useLayoutAnimationsState` for use on the receiver end.
*/
export const useLayoutAnimationReporter = () => {
const counter = injectAnimationCounter(true)
return {
reportAnimationStarted: () => counter?.modifyCount(1),
// RequestAnimationFrame is used to ensure that at least one frame has passed since the animation stop
// was triggered on the JS side. That way the active state is maintained for the full duration of the
// last animation frame, and any tracking logic has a chance to react to the final element position.
reportAnimationEnded: () => requestAnimationFrame(() => counter?.modifyCount(-1)),
}
}

/**
* A composable that receives information about wheter any potentially layout-shifting animations
* are currently playing within any of the child components.
*
* See `useLayoutAnimationReporter` for use on the animation component end.
*/
export const useLayoutAnimationsState = () => {
const counter = provideAnimationCounter()
return proxyRefs({
anyAnimationActive: counter.anyAnimationActive,
})
}

0 comments on commit 50c9d53

Please sign in to comment.