Keyframe/timeline animation editor (replaces hardcoded slides)#9
Merged
Conversation
GAP-B foundation (engine). Replaces the two hardcoded CSS-transition slides with
a timeline-driven model that compiles to the Web Animations API in the generated
component, so animation is spec-correct and portable.
- Timeline model stored under the vendor key v_ografEditorTimeline (manifest is
additionalProperties:false, so a v_ key is the spec-safe home). Per element:
in (play) and out (stop) lanes of keyframes {t, props:{opacity,tx,ty,scale},
easing}, with per-lane delay. getTimeline() repairs older/partial shapes.
- The Simple slide/fade presets now GENERATE keyframes into the timeline, so the
existing quick-preset path is preserved and feeds one source of truth.
- Generated component animates via element.animate(); playAction/stopAction await
animation.finished (not a setTimeout that can lie), and honor skipAnimation by
snapping to the end state.
- Publishes honest actionDurations (real computed ms per play/stop) so on-air
automation can schedule correctly.
- Element removal drops its timeline lanes. Tests updated for WAAPI output.
The visual timeline panel UI (Advanced keyframe editing) lands next as GAP-B.2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review follow-ups on the animation engine, both latent until the Advanced keyframe editor lets users type arbitrary times: - computeActionDuration now clamps to a finite, non-negative integer. The OGraf schema requires actionDurations[].duration to be an integer, and a non-finite value serializes to null (an invalid manifest); guard at the model layer. - buildLaneEffect dropped its unused totalDuration param and the misleading "shared clock" comment: each element animates over its own span offset by its delay, which is what the code actually does. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GAP-B.2 (the visible editor on top of the GAP-B.1 engine). - A collapsible Timeline panel docked at the bottom of the editor area (drag to resize, arrow-key operable, collapsed/height persisted), operable on the Visual tab and collapsed to its header otherwise. - Simple path: the sidebar Animation section becomes "Animation (quick presets)" with None/Fade/Slide/Pop chips + duration + easing that generate keyframes. - Advanced path: per-element in/out lanes with keyframe add/move/edit (numeric opacity/offset/scale + easing + per-lane delay), a playhead/scrubber, and local Play/Stop/Snap preview on the canvas. - Simple/Advanced guardrail: a lane edited in Advanced is marked custom; a preset cannot silently overwrite it (explicit confirm + Custom badge). - Keyboard operable and labelled per WCAG (focusable keyframes, slider playhead, grouped tracks/lanes). Tests for the model edits and the panel DOM. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P1: colorToHex now parses rgb()/rgba() and 3/4/6/8-digit hex, expanding
short hex and dropping alpha for the opaque swatch. rgba() with partial
alpha returns null so the authored value (and its alpha) is preserved
rather than overwritten by the swatch fallback. The swatch only writes
its hex into the text field on an explicit user pick, never on the
programmatic render-time value assignment.
P1: 'transparent' no longer maps to #000000. It returns null, so touching
the swatch on a transparent element no longer paints it opaque black; the
authored 'transparent' value stands unless the user picks a real color.
P2: commitDataInputKey no-ops when the input is not connected, so the
trailing blur after a rename-driven re-render does not recommit against
stale state or focus a detached node.
P2: a blank Slide In/Out Duration field is treated as no change instead of
Number('') === 0, so clearing the field mid-edit never stores a 0ms
animation.
Adds test/bugfix-propertypanel.test.js covering colorToHex across rgba,
3/4/8-digit hex, transparent, and named colors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PreviewEngine had several verified bugs around the generated-component lifecycle. The generated component bakes elements/styles/timeline at generateWebComponent() time and never reads animationSettings, so the only correct refresh path is to rebuild the component. - reloadComponent: remove the always-true animationSettings short-circuit that wrote a field the component never reads and returned early, so live preview never refreshed after an edit. Always take the recreate path. - reloadComponent recreate-while-playing: after a successful re-create set isPlaying + updateControlButtons and await playAction with a catch that routes failures to showWebComponentError, keeping isPlaying correct. - render: tear down the previous component synchronously before scheduling setup, store the setup timeout id and clear it on re-render/teardown/ destroy, and bail in the scheduled setup if the template changed. Fixes a stale-template race on fast template switch / select-then-play. - showWebComponentError: guard against a null scaledContainer (log and return instead of throwing). stop() with no component is now a silent no-op that just resets play state instead of showing a scary error. - renderDataInputs: seed and read previewData with presence checks so falsy numeric/boolean defaults (0/false) survive instead of becoming ''. Adds test/bugfix-preview.test.js covering the recreate path, the no-op stop, the null-container guard, and falsy-default preservation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P1: TemplateManager.saveToStorage no longer swallows save failures (e.g. QuotaExceededError). It now records a flag, logs a clear error, and dispatches an ograf-storage-error CustomEvent so the app can surface it. loadFromStorage logs corrupt top-level JSON and per-template parse failures and continues with an empty/partial set instead of silently dropping everything. P2: OGrafTemplate.addElement now combines the timestamp with a random suffix and guarantees uniqueness against existing ids, so elements added within the same millisecond no longer collide. P2: createElementsFromSchema de-duplicates generated element ids, so schema keys differing only by case or punctuation (e.g. Title vs title) no longer collapse into one id and break style/animation lookup. P2: A spec manifest/bundle round-trip is no longer lossy. buildManifest now persists the authored editor elements under v_ografEditorElements (and timeline under v_ografEditorTimeline when present); both are spec-valid vendor keys allowed by the OGraf v1 graphics schema patternProperties ^v_.* even under additionalProperties:false. importTemplate restores elements from that vendor key (ids sanitized via slugifyId) instead of regenerating defaults from the schema, preserving authored geometry, style, content, and non-text elements. Adds test/bugfix-model-io.test.js covering id uniqueness, schema-id dedup, visible save-failure handling, corrupt-load handling, and the bundle round-trip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
VisualEditor: - Remove the double-counted panOffset in hit-testing, drag, and resize (the transform already folds pan into getBoundingClientRect), so clicks hit the right element and resize anchors hold when panned. - Recompute the moving edge from the fixed edge after the min-size clamp so NW/NE/SW resize no longer slides the anchored edge. - Ignore the Delete key when an editable field is focused, so typing in a property input no longer deletes the selected element. - Bind event handlers once so destroy() actually removes them (no leaked document keydown deleting elements in a new editor). - Replace the defunct via.placeholder.com default image with an inline data URI. TimelinePanel: - Align the ruler/playhead with the keyframe lanes (shared x-origin), so keyframes no longer sit ~256ms off the playhead. - Reset stale element selection on template switch (no phantom lanes). - Seed an added keyframe by interpolating the lane at the playhead instead of a hardcoded rest state. - Restore inspector focus across re-render on a value commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…meline-animation # Conflicts: # src/components/PropertyPanel.js
The element ID field was readonly, so users could not change an element's id,
confusing and limiting. It is now editable and commits on change/blur (never
per keystroke).
- OGrafTemplate.renameElementId(oldId, newId) validates the new id (slug-safe,
unique among elements), updates element.id, and migrates the element's
timeline lane key under v_ografEditorTimeline. Data tokens ({{...}}) are a
separate namespace and are untouched.
- The panel validates before renaming (inline aria error on empty/invalid/
duplicate, focus kept), then saves, regenerates, keeps the element selected
under its new id, and refreshes canvas/timeline/preview.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…robust offsets BUG 1: elements that should start hidden flashed visible on the first painted frame, and stayed visible during a lane delay. playAction() rendered elements at their resting (visible) state and let the browser paint that frame before the WAAPI in-animation hid them. Add applyInitialState(action) on the generated component: it reads each animated element's offset:0 keyframe from buildLaneEffect and writes opacity/transform to the inline style synchronously, BEFORE the rAF/paint. Call it in playAction() after render() (before the rAF) and in load() after render(), so a loaded-but-not-played graphic also shows its in-start state. Elements with no in-lane stay at their resting state. fill:'both' still holds the visible end state when the animation finishes, and skipAnimation still snaps correctly. BUG 2a: editing keyframes in the Timeline panel did not refresh the live preview, so edits appeared to do nothing. Wire TimelinePanel.persist() to call previewEngine.reloadComponent() (mirroring the PropertyPanel path), add setPreviewEngine() and connect it in main.js, with a fallback to window.ografEditor.previewEngine. BUG 2b: buildLaneEffect dropped the authored first value when the earliest keyframe was at t>0 (WAAPI synthesized offset:0 from underlying style). Always prepend an explicit offset:0 frame holding the earliest authored value when the first frame is not at offset 0, in both the generated component and the TimelinePanel local-preview mirror, so offsets stay monotonic in 0..1 and intermediate waypoints render. Verified in a real browser (headless Chrome, CDP): a fade-in shows opacity 0 at load and at play-start, ~0 through an 800ms delay, and ends at opacity 1; a slide-in keeps opacity at rest and applies transform. Tests: add test/bugfix-animation-faithfulness.test.js (8 cases). Full suite 93 passed, lint 0 errors, build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Keyframe focus calls (select, keyboard move, edit, focus restore) now pass
{ preventScroll: true }, so grabbing a keyframe far to the right no longer
makes the browser scroll the page to reveal the focused element. scrollIntoView
for a selected track uses nearest on both axes so it never scrolls the page.
- The Templates list had two nested scroll areas (.template-list-content and an
inner .template-list-container with its own max-height + overflow). Removed the
inner scroll so the panel is the single scroll area.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… scrollbars - The playhead now sweeps across the ruler in real time during a local Play (jumps to the end on Snap, resets to 0 on Stop), with the sweep loop cancelled on Stop/re-render/destroy so it never leaks or double-runs. - Added an In/Out toggle to the timeline transport so Play/Snap can preview the out animation, not just the in animation. - Fixed the real "grabbing a keyframe scrolls the view to the top": render() rebuilt the panel innerHTML, resetting the scrolled track list and lanes. We now preserve and restore the timeline-body scroll and each lane's horizontal scroll across the rebuild (preventScroll on focus alone could not fix this). - Added slim dark-theme custom scrollbars (Firefox + WebKit) so the sidebar, timeline, and other scroll areas match the rest of the UI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The add-text/image/rectangle/circle toolbar was below the canvas. Move it above the canvas so the tools are at the top of the visual editor, and flip its border to the bottom edge accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keep the dark theme, but lift the all-grey look with the OSC brand cyan (#4dc9ff) on header text: a cyan-to-blue gradient app wordmark, cyan panel headers (Templates/Properties) with a blue accent bar, cyan collapsible section headers, cyan active editor/code tabs, and a cyan uppercase Timeline label. Interactive borders and focus rings stay #007acc for consistency. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Export button used the editor-bundle 'json' format, producing a single <id>-template.json. Per the EBU OGraf v1 spec a Graphic is a manifest whose file name MUST end with .ograf.json plus the Javascript module the manifest's "main" references. TemplateManager.exportTemplate already builds exactly those two files (<id>.ograf.json + template.mjs); switch the Export action to emit them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the fake JSON "zip" (which just wrote a .json) with a real, dependency- free stored-ZIP writer, and point Export at it. One click now downloads a single <id>.ograf.zip containing the spec files: the <id>.ograf.json manifest and the template.mjs component. Adds a zip util (CRC32 + ZIP local/central/EOCD records) with tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the three levels of verifying an OGraf export: JSON Schema validation against the published schema, the ograf-devtool reference checker, and an end-to-end playout test in a real renderer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The element toolbar mixed a plain letter, a color emoji, and two thin unicode glyphs, so the buttons rendered at different sizes, colors, and baselines. Replace all four with a uniform inline-SVG icon set using currentColor so they align and inherit the hover/active state, and tidy the button styling. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Import now accepts the exported .ograf.zip: it unzips, reads the .ograf.json manifest (which carries authored elements/timeline under v_ vendor keys), and keeps the exact .mjs component when present. Single .ograf.json import still works; importing a bare .mjs gives a clear message. Adds a ZIP reader (stored + deflate via DecompressionStream) and a createZip/readZip round-trip test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add TimelinePanel.setVisible(), which main.js calls on tab switch so the bottom timeline (and its Play/Stop transport) is hidden in the Preview tab, leaving only the preview's own transport. Hiding also stops any in-flight local preview. Also remove the per-lane Custom badge and its CSS, which read as noise in the timeline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The import dialog now allows multiple selection, so a user with the loose files (not a zip) can pick the .ograf.json manifest and the .mjs component together. importFromFiles finds the manifest (and a .zip or .mjs among the selection), imports the manifest, and keeps the .mjs as the component. The zip and single-manifest paths are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per scope decision (A), the visual editor only opens templates it created: - Import now refuses a code-first OGraf graphic (no v_ografEditorElements) with a clear message, instead of fabricating misleading default elements from the schema. Our own .ograf.json/.zip/editor-bundle still import (they carry the v_ authoring data). - Zip and multi-file import select the component by the manifest's "main" field (exact name or basename), falling back to the first .mjs/.js, so a graphic with a lib/*.js no longer risks picking the wrong file. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review caught a regression: the editor JSON bundle keeps elements in a top-level field, not in the manifest, so the new "no v_ografEditorElements -> refuse" guard threw before importEditorTemplate could restore them. Fold the bundle's elements/timeline into the manifest vendor keys so the importer accepts it, and keep the local element id-sanitize so it works regardless of the manager. Adds a regression test for editor-bundle re-import. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dead code
- sanitizeCssValue now rejects backtick and ${, not just {}<>". A style
value lands inside a JS template literal in the generated .mjs source, so
a backtick could close it and inject executable code (reachable via an
imported .ograf.zip/.json). (HIGH)
- Validate CSS property names (sanitizeCssKey / inlined safeCssKey) at both
build time (generateElementStyles) and runtime (the generated component's
renderElement); a crafted style key could otherwise break out of the rule
or the style="..." attribute. Invalid keys are dropped. (MEDIUM)
- VisualEditor design canvas now only loads http(s)/data:image img.src,
matching the generated component's safeSrc guard; other schemes and
unresolved {{tokens}} fall back to the placeholder. (LOW)
- Remove computeActionTotal, dead code shipped into every exported .mjs.
The dead exportTemplateBundle/importTemplateBundle methods were already
removed upstream.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The headline "close the gap" feature: a real per-element keyframe animation editor, replacing the two hardcoded slide animations. It has since grown to also make the editor's output portable and spec-clean (zip + manifest export/import) and to harden the generated graphics against injection. Built against the UX design and the OGraf v1 spec brief.
Animation engine
v_ografEditorTimeline, so the manifest staysadditionalProperties:false-clean. Per element: in (play) and out (stop) lanes of keyframes{t, props:{opacity,tx,ty,scale}, easing}plus a per-lane delay.playAction/stopActionawaitanimation.finished(no lying setTimeout) and honorskipAnimation(snap to end).actionDurations(real computed ms), clamped to a finite integer so the manifest stays schema-valid.Timeline UI
Portable, spec-valid export/import
.ograf.zipbundle (the<id>.ograf.jsonmanifest plus the.mjscomponent), or as the spec package files directly..ograf.zip, a raw.ograf.jsonmanifest, the editor JSON bundle, or the manifest and.mjschosen as separate files. The component is picked by the manifestmainfield.v_vendor keys, so an export/import round-trip preserves the full authoring state. Import is scoped to editor-made templates (a foreign code-first graphic is declined for visual editing rather than reconstructed misleadingly).Secure rendering (XSS hardening)
load()/updateAction()) is escaped before it reaches shadow-DOM innerHTML; imagesrcis restricted tohttp(s)/data:image(safeSrc); element ids are slug-restricted to[a-z0-9-].sanitizeCssValuerejects backtick and${(a style value is spliced into a JS template literal in the generated.mjs, so a backtick could close it and inject code), and CSS property names are validated as plain dashed identifiers at both build time and runtime. Reachable now that arbitrary.ograf.zip/.jsonfiles can be imported.UI polish and editing
Docs
Deferred (noted): rotation, cubic-bezier curve editor, multi-step (GAP-C), drag-on-canvas to set keyframe values.
Verified: lint clean (0 errors), 110 tests pass across 14 files, build clean. Browser-verified the toolbar icons and the Preview-tab timeline hiding; broader visual review of the panel is ongoing.
🤖 Generated with Claude Code