Repository navigation
element animations - #721
barnabasmolnar wants to merge 6 commits into
Conversation
f655a39 to
9dd9383
Compare
3c6f672 to
80f06e5
Compare
8776355 to
a4a4306
Compare
b315901 to
e626358
Compare
075417c to
5735dd2
Compare
80f06e5 to
afa1e11
Compare
afa1e11 to
f1fadfa
Compare
Port of barnabasmolnar/animations-v2 onto latest master, squashed into a single commit. Adds an imperative element animation API to the editor: - animateElements() on the imperative API: fade/fly animations with per-element duration, delay, stagger, and easing (linear, easeOut, easeInOut); returns a batch handle with finished promise plus finish()/cancel()/getStatus() for completion signaling - cancelElementAnimation() and clearElementAnimationOverrides() APIs - resolveRenderOpacity prop for host-controlled render opacity - render caches are preserved during animations: position/opacity overrides are applied at draw time (positionOffset / context translation) instead of mutating elements, keyed by a render animation version - bound text elements animate together with their containers - example app (with-script-in-browser) animation playground and initialData demo Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
f1fadfa to
9b52de8
Compare
Expose atomic render override snapshots in the editor and keep effect timing, visibility, and interruption in the host animator. Add a presentation prototype, regression coverage, and API documentation, and remove the animation example changes.
|
Here're the nits and thoughts from my earlier exploration: Two mechanisms apply the same offset in the renderer. renderElement.ts:150-170 wraps the export branches in a context.translate, while the cached-canvas path threads a positionOffset parameter into drawElementFromCanvas. Exports never receive overrides, so the wrappers in the export branches are dead in practice, and they cause the large re-indent that dominates this file's diff. A single context.translate at the top of renderElement covers every branch, reduces the diff to a few lines, and also fixes the uncropped image ghost which currently gets a hard-coded zero offset. It would also give you one place to generalise to a full transform later if scale or rotation effects are wanted. Copied elements defeat the bounds cache. getRenderElementWithPositionOverride allocates a fresh element per frame for culling in App, frame clipping, link badges and embeds. The bounds cache in bounds.ts:85-92 is a WeakMap keyed by element identity, so every copy is a miss. The Renderer deliberately avoids this by shifting the viewport instead, and its comment notes the copy approach breaks for arrow-bound labels whose coordinates derive from the container. That limitation now silently applies to the link badge and frame-clip paths. Pick the viewport-shift approach everywhere. Culling is duplicated. Renderer.ts:71-124 reimplements getVisibleCanvasElements plus the frame-highlight sort. Factor the visibility predicate to accept an optional offset and reuse it. The memoization keys are sound, and the memoized selectedElements identity from Scene makes the cache hit as intended. Frame clipping expression is duplicated and hard to read. staticScene.ts:341-362 and the second block near line 480 inline a twelve-line boolean. Extract a helper such as shouldClipTranslatedFrameChild. The second block also omits the bound-text check the first one has, which looks accidental. Interaction gap for presentations. Opacity-zero elements stay hittable, hoverable and link-clickable in view mode, and the hover cursor cannot be fixed by the host through onLinkOpen. The snapshot map is the natural place for an interactive: false flag in a follow-up. |
a84db4b to
e9944c4
Compare
150a4a3 to
10a08e5
Compare
cf91ce7 to
aef0851
Compare
63d8bbe to
ddee6e5
Compare
5e7e32b to
f499d71
Compare
Presentation effects need to change how elements are drawn without changing the document. This PR adds transient element render overrides to the editor and demonstrates host-owned animation playback in
excalidraw-app, using E+'s beat/reveal-count and preview flows.The intended upstream scope is the generic editor API, rendering support, documentation and regression tests. The host animator and presentation demo live in the fork to exercise the design before integrating it into E+. An alpha/beta/RC build of the editor package from this branch will be needed for that integration.
How it is split
packages/excalidraw)setElementRenderOverrides(), owns a copied visual snapshot, and applies it through the existing React/canvas rendering path.packages/element,packages/excalidraw/rendererandscene/Renderer.ts)excalidraw-app/animations/ElementAnimator.ts)excalidraw-app/animations/AnimationPrototype.tsxandfixtures.ts)ElementAnimatorlives outside the editor package. Presentation effects do not require a new editor clock or play/pause/seek API. There are no changes underexamples/relative tomaster.How it works
The editor accepts one complete snapshot per mounted editor:
Each submission replaces all previous entries and fields atomically. Missing values use the document's values. The editor defensively copies the map and nested offsets; invalid numeric values reject the whole submission. Opacity is absolute on the existing 0–100 scale, and offsets are in scene units. Other element properties cannot be overridden.
Snapshots affect rendering without changing elements,
AppState, history, collaboration data or exports, and do not emitonChange. Submission requests a React update and may be coalesced before canvas drawing. An explicit visual-update guard preserves the normal lifecycle for real updates batched with it. Memoized visual culling avoids extra static repaints from unrelated App updates while a snapshot remains installed.The host combines steady visibility with active transitions before submitting:
Reveal counts advance immediately. A later request can interrupt overlapping targets, starting from their last submitted visual values. Completion or cancellation removes transient values and returns control to the base snapshot: an outgoing effect needs a hidden base to remain hidden. Passing the new base with the requests makes the handoff atomic, including for zero-duration effects. Host rAF runs only while transitions remain and uses the mounted editor's window.
The helper supports fade/fly, direction, in/out phase, duration, delay, stagger and easing, with
finishedandcancel()handles. It expands containers and bound labels together; group members and frame children are explicit targets. Reversal keeps the requested duration, and fly retains E+'s existingeaseOutbehavior in both directions.Geometry limits: hit testing, selection, editing and bindings use document positions. Hidden elements remain interactive, and connected arrows do not follow translated shapes. Arrow and label offsets should match so the label hole moves with the label. Frame opacity multiplies child opacity; a frame-only fade therefore fades its children. Canvas clipping requires frame clipping to be enabled; live DOM embeds retain their existing clipping limitation.
Full contracts and integration details: editor API and host helper, E+ migration and measurements.
Why keep
AnimationControllerinternalClock delivery and playback policy are separate decisions: keeping effects in E+ does not require the clock to live there forever. For this POC, host rAF keeps the public contract to one snapshot setter, while an editor-provided frame source has no demonstrated benefit yet.
setTimeout(0)according to render throttling. Exposing it directly would make those implementation choices part of the host contract. A supported API would need editor-instance isolation, owner-window handling and automatic teardown.AnimationControllerwould not by itself coordinate React commits with canvas drawing or guarantee removal of the observed extra-frame delay. That requires work on the rendering schedule, not just exposing a clock.If real E+ workloads reveal a scheduling problem, first measure where coordination is missing. If an editor-provided driver then earns its complexity, expose a small per-editor frame subscription with defined lifecycle and ordering relative to rendering, while retaining host-owned beats, effects and interruption semantics. That can be added without changing the snapshot API or exposing the existing
AnimationControllerclass.How to test
From the repository root, start the app if it is not already running:
Open http://localhost:3001/animation-prototype. The route uses fixture data with no persistence or collaboration.
Automated checks:
Validation at
f0e64bb87: 31/31 focused tests pass. Including the renderer/export suites gives 64 passing tests and one known baseline failure: the arrow-label SVG test calls the obsolete three-argument export API. An export-test timeout during concurrent checks passed on an isolated rerun. TypeScript reports the two existing errors (COLOR_PALETTE.blackand that obsolete export call). Changed TypeScript files pass lint; formatting and browser checks pass.The performance check measured the actual host animator, React rendering and the complete static renderer. With 2,015 hidden entries and three animated IDs, p95 submission-to-draw completion was 2.8 ms with direct rendering and 9.1 ms with throttling. The dense slide was dominated by canvas drawing. These development-build measurements used a roughly 120 Hz cadence and exclude GPU completion; they support keeping complete snapshots and host rAF for the POC. The host README contains the workload, full results and limitations.