Vibecode SVGator
track this build4 phases, 12 steps, beginner friendly0%There is no network here and no data moat: it is a single-player editor that turns your own SVG into an animated SVG. An agent can absolutely build the spine in a session: parse the file, list the elements, keyframe transform and opacity on a timeline, emit CSS keyframes inside the SVG. What eats the weekend and then some is everything after transform: path morphing, stroke draw-on, motion along a path, a real bezier easing editor, group transform origins, and an export pipeline that does not break in Safari. The core loop survives DIY; the last 20 percent of the feature list is where the actual product lives. Build it if you animate two or three properties and hand-tune the rest, buy it if you ship illustrated micro-animations for a living.
You are building a lean indie version of SVGator. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # SVGator · indie build A browser SVG animation editor whose hard problems are a preview that cannot disagree with the export, transform origins that every engine interprets the same way, and a project file that outlives the editor, rather than the timeline widgets people notice first. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Vite, React and TypeScript, entirely in the browser with no backend | there are no secrets and no reason for a server | | Document | The original SVG kept as a live DOM, never re-serialised from a model | a round trip through a model drops the things it did not understand | | Animation | CSS keyframes generated from the timeline and injected for preview | the preview runs the export itself, so it cannot lie | | Origins | transform-box and transform-origin set explicitly on every animated element | the SVG defaults are the viewport and its corner | | Storage | Versioned project files in IndexedDB and exported to disk | browser storage is not a filing system | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the dev server, the build and the test suite. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **The three main browser engines for checking exports** · free - Why: Transform origins and animation timing differ between engines, and an export checked in one engine is not a checked export. - Get it: Install the browsers your test runner supports and run the engine suite in all three. - Verify: The engine suite runs in three engines and passes - [ ] **A folder of real SVGs including awkward ones** · free - Why: Files from real vector editors contain defs, nested groups, clip paths and elements with no ids, and the round trip is only meaningful against those. - Get it: Export a dozen files from the editors you actually use, including ones with gradients, masks and text. - Verify: Every file loads and round trips unchanged - [ ] **The CSS Transforms specification, read for transform-box and transform-origin** · free - Why: The SVG defaults are not what anyone expects, and the export depends on writing both properties on every animated element. - Get it: Read the two property definitions and note the initial values and the SVG rule. - Verify: A note records the initial values and why the export always sets both - [ ] **Your transform composition order** · free - Why: It is baked into every export and cannot change later without changing every existing animation. - Get it: Decide translate, rotate, scale or another order, and write it into the interface. - Verify: The order is documented and asserted in the suite ## Quick start ```sh npx playwright test tests/engines.spec.ts --project=chromium --project=firefox --project=webkit ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Path morphing, motion along a path and stroke draw-on. - Shape and text editing. - Script, SMIL, Lottie and video export. - Collaboration and cloud projects. - Path morphing, stroke draw-on, and motion-along-path, which are the effects people buy this for - A polished easing curve editor with presets that look intentional instead of linear - Export targets beyond CSS: SMIL, JS runtime, Lottie, video - Cross-browser export sanity checks and the fixes for Safari's transform-origin quirks - Built-in shape and text editing, so every asset change means a round trip to your vector editor If one of those is essential to you, that is the reason to keep paying for SVGator, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · SVGator The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a timeline editor that animates your own SVG and exports one file that plays the same in every browser to replace SVGator. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Vite, React and TypeScript, entirely in the browser with no backend · there are no secrets and no reason for a server - Document: The original SVG kept as a live DOM, never re-serialised from a model · a round trip through a model drops the things it did not understand - Animation: CSS keyframes generated from the timeline and injected for preview · the preview runs the export itself, so it cannot lie - Origins: transform-box and transform-origin set explicitly on every animated element · the SVG defaults are the viewport and its corner - Storage: Versioned project files in IndexedDB and exported to disk · browser storage is not a filing system ### Data model (create this before Phase 1) - projects(id, name, format_version, svg_source, duration_ms, created_at, updated_at) · the source is kept verbatim - elements(project_id, element_id, tag, label, assigned_id) · assigned_id is set only where the file had none - tracks(id, project_id, element_id, property) · property is translateX, translateY, rotate, scale or opacity - keyframes(id, track_id, at_ms, value, easing) · easing belongs to the segment leaving this keyframe - origins(project_id, element_id, box, origin_x, origin_y) · written into the export on every animated element - exports(id, project_id, kind, at, bytes, engines_checked_json) · kind is animated_svg or css_only - The SVG source is stored as text and the DOM is rebuilt from it on load, so nothing the editor did not understand is ever lost between sessions. ### Phase 1 · Loading a file without changing it Build: Parse the SVG into a live DOM rather than rasterising it, list its elements as a tree, and assign ids only to elements that lack one. Render on a checkerboard with zoom. Export an untouched project and compare it to the input. Trap: Building an internal model of shapes and re-serialising it for export drops every attribute, namespace and element the model did not have a slot for, and the user finds out when a gradient or a clip path disappears. Keep the original DOM as the document and only add attributes to it. Done when: A file loaded and exported with no animation is semantically identical to the input, tested by comparing the two DOMs. Every element appears in the tree with a stable id and selecting one outlines it on the canvas. Ids assigned by the editor are unique against the file's existing ids. A file containing defs, nested svg elements and foreignObject loads without error and exports unchanged. Do not build yet: No timeline, keyframes, easing or export of animation. ### Phase 2 · A preview that runs the export Build: A timeline with a fixed duration, keyframes per property per element and easing on each segment. Generate the CSS keyframes from the timeline, inject them into the live document, and scrub by pausing the animations and setting a negative delay equal to the playhead. Never interpolate in JavaScript for display. Trap: Interpolating in JavaScript for the preview while emitting CSS for the export produces two implementations that agree on the easy cases and diverge on easing, rotation past a half turn and overlapping keyframes, and the person only sees the difference after shipping. Make the preview run the generated stylesheet, so there is one implementation. Done when: The preview is driven by the same generated CSS as the export, verified by inspecting the injected stylesheet. Scrubbing to any time shows the state the export would show at that time, tested by comparing computed transforms at three times against the exported file in a headless browser. Easing is stored per segment and cubic-bezier values are accepted. Play, pause, loop and scrub work from the keyboard. Do not build yet: No transform-origin handling, timeline ergonomics or project files. ### Phase 3 · Origins every engine agrees on Build: Set transform-box and transform-origin explicitly on every animated element in the export, defaulting to the element's own bounding box and its centre. Compose transforms in a fixed documented order. Check the export in the three main engines and record what was checked. Trap: The specification's initial transform-box is the nearest SVG viewport, and for SVG elements without a CSS layout box the used transform-origin is the top-left corner, so a rotation exported without both properties spins the element around the corner of the whole drawing rather than its own centre, and engines have historically differed on top of that. Emit both properties on every animated element and never rely on the default. Done when: Every animated element in the export carries an explicit transform-box and transform-origin, asserted by parsing the output. A rotation about an element's centre renders in the same place in three engines, checked by rendering and comparing. The composition order is fixed, stated in the interface and identical between preview and export. An element inside a transformed group animates correctly relative to its parent. Do not build yet: No timeline ergonomics or project files yet. ### Phase 4 · A timeline you can work in for an hour Build: Drag keyframes with snapping to the time grid and to other keyframes, multi-select, copy and paste between elements, and undo covering structural changes. Scale a selection in time. Keep canvas and timeline in sync on a file with a hundred animated elements. Trap: Snapping keyframes to pixels rather than to time means the same drag lands on a different millisecond at a different zoom, and the animation subtly changes when the user zooms the timeline. Snap in time units and derive pixels from them. Done when: Keyframes snap to the time grid and to neighbours in time units regardless of zoom, tested by dragging at two zoom levels. Multi-select, copy, paste and time scaling work and are undoable. Undo covers at least fifty actions including deleting a track. Scrubbing a file with a hundred animated elements stays smooth, measured rather than assumed. Do not build yet: No project files or export packaging yet. ### Phase 5 · Projects that outlive the editor Build: Save projects to IndexedDB with a versioned format and atomic writes, and export the project as a file that reopens identically. Ask for persistent storage and still treat the file as the real save. Export the animated SVG and a CSS-only variant, and verify the animated file plays when placed in an image tag, where scripts do not run. Trap: Treating browser storage as the filing system means a cleared site, a private window or storage pressure erases weeks of work with no warning, because the browser may evict it unless persistence was granted and sometimes even then. Version the format, export to a file, and make reopening the file identical to reopening the stored project, tested. Done when: A project exported to a file and reopened reproduces every keyframe, easing and origin, asserted in the suite. Storage writes are atomic and a project written under an older format version opens after the format changes. The animated export plays inside an image tag with no script, verified in a headless browser. The CSS-only export contains the same keyframes as the animated file. Do not build yet: Nothing after this. ### Out of scope (and why) - Path morphing, motion along a path and stroke draw-on, which are the effects people pay for and each is a project of its own. - Shape and text editing, which turns an animation tool into a vector editor with all of its problems. - Runtime script, SMIL, Lottie and video export, since the CSS export is the one that plays where the file is embedded. - Collaboration and cloud projects, which add a server to a tool that has no need for one. ### README must contain - That the original SVG is kept as the document and the editor only adds attributes, so nothing it did not understand is lost - That the preview runs the generated stylesheet, so it cannot differ from the export - That transform-box and transform-origin are written on every animated element because the SVG defaults are the viewport and its corner - That the composition order is translate, rotate, scale, and is stated in the interface - That browser storage is not the save; the exported project file is, and it round trips in the suite ===== AGENTS.md ===== # Agent instructions · SVGator indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Vite, React and TypeScript, entirely in the browser with no backend, The original SVG kept as a live DOM, never re-serialised from a model, CSS keyframes generated from the timeline and injected for preview, transform-box and transform-origin set explicitly on every animated element, Versioned project files in IndexedDB and exported to disk. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Building an internal model of shapes and re-serialising it for export drops every attribute, namespace and element the model did not have a slot for, and the user finds out when a gradient or a clip path disappears. Keep the original DOM as the document and only add attributes to it. - Interpolating in JavaScript for the preview while emitting CSS for the export produces two implementations that agree on the easy cases and diverge on easing, rotation past a half turn and overlapping keyframes, and the person only sees the difference after shipping. Make the preview run the generated stylesheet, so there is one implementation. - The specification's initial transform-box is the nearest SVG viewport, and for SVG elements without a CSS layout box the used transform-origin is the top-left corner, so a rotation exported without both properties spins the element around the corner of the whole drawing rather than its own centre, and engines have historically differed on top of that. Emit both properties on every animated element and never rely on the default. - Snapping keyframes to pixels rather than to time means the same drag lands on a different millisecond at a different zoom, and the animation subtly changes when the user zooms the timeline. Snap in time units and derive pixels from them. ===== BUILD_PLAN.md ===== # Build plan · SVGator A browser SVG animation editor whose hard problems are a preview that cannot disagree with the export, transform origins that every engine interprets the same way, and a project file that outlives the editor, rather than the timeline widgets people notice first. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Loading a file without changing it The editor understands the file's structure and does not alter what it does not touch. ### Steps 1. Keep the DOM as the document A model of your own drops whatever it did not have a slot for. Files: `src/document.ts` 2. Assign ids without colliding Only where missing, and unique against what the file already has. Files: `src/ids.ts`, `src/ids.test.ts` 3. Round trip an untouched file in the suite Semantic equality of the two DOMs, not string equality. Files: `tests/roundtrip.test.ts` ### Done when - [ ] An untouched project exports semantically identical to the input - [ ] Every element has a stable unique id - [ ] Selecting a tree node outlines it on the canvas - [ ] Defs, nested svg and foreignObject survive a round trip ### Watch out - Building an internal model of shapes and re-serialising it for export drops every attribute, namespace and element the model did not have a slot for, and the user finds out when a gradient or a clip path disappears. Keep the original DOM as the document and only add attributes to it. ## Phase 2 · A preview that runs the export Scrubbing the timeline shows exactly what the exported file will show. ### Steps 1. Generate keyframes from the timeline One generator, used for preview and export alike. Files: `src/css/keyframes.ts`, `src/css/keyframes.test.ts` 2. Scrub with a paused animation and a negative delay The browser does the interpolation, which is the point. Files: `src/preview.ts` 3. Compare preview and export in a headless browser Computed transforms at three times, not a visual glance. Files: `tests/preview-equals-export.test.ts` ### Done when - [ ] The preview uses the generated stylesheet - [ ] Computed state at a scrubbed time matches the export - [ ] Easing is per segment including cubic-bezier - [ ] Transport controls work from the keyboard ### Watch out - Interpolating in JavaScript for the preview while emitting CSS for the export produces two implementations that agree on the easy cases and diverge on easing, rotation past a half turn and overlapping keyframes, and the person only sees the difference after shipping. Make the preview run the generated stylesheet, so there is one implementation. ## Phase 3 · Origins every engine agrees on A rotation spins around the point the user chose, in every browser. ### Steps 1. Emit both origin properties on every animated element The defaults for SVG are the viewport and its corner. Files: `src/css/origins.ts`, `tests/origins-present.test.ts` 2. Fix and state the composition order Translate, rotate, scale, and say so in the interface. Files: `src/css/compose.ts` 3. Render in three engines and compare A rotation about the centre must land in the same place. Files: `tests/engines.spec.ts` ```sh npx playwright test tests/engines.spec.ts --project=chromium --project=firefox --project=webkit ``` ### Done when - [ ] Every animated element carries both origin properties - [ ] A centre rotation matches across three engines - [ ] The composition order is stated and consistent - [ ] Nested transformed groups animate correctly ### Watch out - The specification's initial transform-box is the nearest SVG viewport, and for SVG elements without a CSS layout box the used transform-origin is the top-left corner, so a rotation exported without both properties spins the element around the corner of the whole drawing rather than its own centre, and engines have historically differed on top of that. Emit both properties on every animated element and never rely on the default. ## Phase 4 · A timeline you can work in for an hour Keyframes can be placed, moved, copied and undone without fighting the interface. ### Steps 1. Snap in time, derive pixels Zooming the timeline must not move a keyframe. Files: `src/timeline/snap.ts`, `src/timeline/snap.test.ts` 2. Make undo structural Deleting a track and pasting keyframes both undo cleanly. Files: `src/undo.ts` 3. Measure scrubbing at a hundred elements Smoothness is a number, not an impression. Files: `tests/perf.spec.ts` ### Done when - [ ] Snapping is zoom independent - [ ] Copy, paste and scaling are undoable - [ ] Undo covers fifty actions including track deletion - [ ] A hundred elements scrub smoothly ### Watch out - Snapping keyframes to pixels rather than to time means the same drag lands on a different millisecond at a different zoom, and the animation subtly changes when the user zooms the timeline. Snap in time units and derive pixels from them. ## Not in this build - Path morphing, motion along a path and stroke draw-on. - Shape and text editing. - Script, SMIL, Lottie and video export. - Collaboration and cloud projects. ## After v1, if you want it - Stroke draw-on through dash offset, which is the one effect that is a small addition to this design - An easing preset library with visible curves, since linear is the tell of a home-made animation - Motion along a path, which needs its own interpolation and is the first thing to charge for ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Written into every project file. Migrations read it. PROJECT_FORMAT_VERSION=1 # Required. New project duration. Editable per project. DEFAULT_DURATION_MS=3000 # Required. Fixed and stated in the interface. Changing it changes every animation. TRANSFORM_ORDER=translate,rotate,scale # Required. Emitted on every animated element with an explicit origin. DEFAULT_TRANSFORM_BOX=fill-box # Required. Fast keyframe work produces mistakes and they should be cheap. UNDO_DEPTH=50 # Required. Asked for at first save. Even when granted, the exported file is the real save. REQUEST_PERSISTENT_STORAGE=true
You are building a lean indie version of SVGator. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # SVGator · indie build A browser SVG animation editor whose hard problems are a preview that cannot disagree with the export, transform origins that every engine interprets the same way, and a project file that outlives the editor, rather than the timeline widgets people notice first. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Vite, React and TypeScript, entirely in the browser with no backend | there are no secrets and no reason for a server | | Document | The original SVG kept as a live DOM, never re-serialised from a model | a round trip through a model drops the things it did not understand | | Animation | CSS keyframes generated from the timeline and injected for preview | the preview runs the export itself, so it cannot lie | | Origins | transform-box and transform-origin set explicitly on every animated element | the SVG defaults are the viewport and its corner | | Storage | Versioned project files in IndexedDB and exported to disk | browser storage is not a filing system | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the dev server, the build and the test suite. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **The three main browser engines for checking exports** · free - Why: Transform origins and animation timing differ between engines, and an export checked in one engine is not a checked export. - Get it: Install the browsers your test runner supports and run the engine suite in all three. - Verify: The engine suite runs in three engines and passes - [ ] **A folder of real SVGs including awkward ones** · free - Why: Files from real vector editors contain defs, nested groups, clip paths and elements with no ids, and the round trip is only meaningful against those. - Get it: Export a dozen files from the editors you actually use, including ones with gradients, masks and text. - Verify: Every file loads and round trips unchanged - [ ] **The CSS Transforms specification, read for transform-box and transform-origin** · free - Why: The SVG defaults are not what anyone expects, and the export depends on writing both properties on every animated element. - Get it: Read the two property definitions and note the initial values and the SVG rule. - Verify: A note records the initial values and why the export always sets both - [ ] **Your transform composition order** · free - Why: It is baked into every export and cannot change later without changing every existing animation. - Get it: Decide translate, rotate, scale or another order, and write it into the interface. - Verify: The order is documented and asserted in the suite ## Quick start ```sh npx playwright test tests/engines.spec.ts --project=chromium --project=firefox --project=webkit ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Path morphing, motion along a path and stroke draw-on. - Shape and text editing. - Script, SMIL, Lottie and video export. - Collaboration and cloud projects. - Path morphing, stroke draw-on, and motion-along-path, which are the effects people buy this for - A polished easing curve editor with presets that look intentional instead of linear - Export targets beyond CSS: SMIL, JS runtime, Lottie, video - Cross-browser export sanity checks and the fixes for Safari's transform-origin quirks - Built-in shape and text editing, so every asset change means a round trip to your vector editor If one of those is essential to you, that is the reason to keep paying for SVGator, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · SVGator The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a timeline editor that animates your own SVG and exports one file that plays the same in every browser to replace SVGator. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Vite, React and TypeScript, entirely in the browser with no backend · there are no secrets and no reason for a server - Document: The original SVG kept as a live DOM, never re-serialised from a model · a round trip through a model drops the things it did not understand - Animation: CSS keyframes generated from the timeline and injected for preview · the preview runs the export itself, so it cannot lie - Origins: transform-box and transform-origin set explicitly on every animated element · the SVG defaults are the viewport and its corner - Storage: Versioned project files in IndexedDB and exported to disk · browser storage is not a filing system ### Data model (create this before Phase 1) - projects(id, name, format_version, svg_source, duration_ms, created_at, updated_at) · the source is kept verbatim - elements(project_id, element_id, tag, label, assigned_id) · assigned_id is set only where the file had none - tracks(id, project_id, element_id, property) · property is translateX, translateY, rotate, scale or opacity - keyframes(id, track_id, at_ms, value, easing) · easing belongs to the segment leaving this keyframe - origins(project_id, element_id, box, origin_x, origin_y) · written into the export on every animated element - exports(id, project_id, kind, at, bytes, engines_checked_json) · kind is animated_svg or css_only - The SVG source is stored as text and the DOM is rebuilt from it on load, so nothing the editor did not understand is ever lost between sessions. ### Phase 1 · Loading a file without changing it Build: Parse the SVG into a live DOM rather than rasterising it, list its elements as a tree, and assign ids only to elements that lack one. Render on a checkerboard with zoom. Export an untouched project and compare it to the input. Trap: Building an internal model of shapes and re-serialising it for export drops every attribute, namespace and element the model did not have a slot for, and the user finds out when a gradient or a clip path disappears. Keep the original DOM as the document and only add attributes to it. Done when: A file loaded and exported with no animation is semantically identical to the input, tested by comparing the two DOMs. Every element appears in the tree with a stable id and selecting one outlines it on the canvas. Ids assigned by the editor are unique against the file's existing ids. A file containing defs, nested svg elements and foreignObject loads without error and exports unchanged. Do not build yet: No timeline, keyframes, easing or export of animation. ### Phase 2 · A preview that runs the export Build: A timeline with a fixed duration, keyframes per property per element and easing on each segment. Generate the CSS keyframes from the timeline, inject them into the live document, and scrub by pausing the animations and setting a negative delay equal to the playhead. Never interpolate in JavaScript for display. Trap: Interpolating in JavaScript for the preview while emitting CSS for the export produces two implementations that agree on the easy cases and diverge on easing, rotation past a half turn and overlapping keyframes, and the person only sees the difference after shipping. Make the preview run the generated stylesheet, so there is one implementation. Done when: The preview is driven by the same generated CSS as the export, verified by inspecting the injected stylesheet. Scrubbing to any time shows the state the export would show at that time, tested by comparing computed transforms at three times against the exported file in a headless browser. Easing is stored per segment and cubic-bezier values are accepted. Play, pause, loop and scrub work from the keyboard. Do not build yet: No transform-origin handling, timeline ergonomics or project files. ### Phase 3 · Origins every engine agrees on Build: Set transform-box and transform-origin explicitly on every animated element in the export, defaulting to the element's own bounding box and its centre. Compose transforms in a fixed documented order. Check the export in the three main engines and record what was checked. Trap: The specification's initial transform-box is the nearest SVG viewport, and for SVG elements without a CSS layout box the used transform-origin is the top-left corner, so a rotation exported without both properties spins the element around the corner of the whole drawing rather than its own centre, and engines have historically differed on top of that. Emit both properties on every animated element and never rely on the default. Done when: Every animated element in the export carries an explicit transform-box and transform-origin, asserted by parsing the output. A rotation about an element's centre renders in the same place in three engines, checked by rendering and comparing. The composition order is fixed, stated in the interface and identical between preview and export. An element inside a transformed group animates correctly relative to its parent. Do not build yet: No timeline ergonomics or project files yet. ### Phase 4 · A timeline you can work in for an hour Build: Drag keyframes with snapping to the time grid and to other keyframes, multi-select, copy and paste between elements, and undo covering structural changes. Scale a selection in time. Keep canvas and timeline in sync on a file with a hundred animated elements. Trap: Snapping keyframes to pixels rather than to time means the same drag lands on a different millisecond at a different zoom, and the animation subtly changes when the user zooms the timeline. Snap in time units and derive pixels from them. Done when: Keyframes snap to the time grid and to neighbours in time units regardless of zoom, tested by dragging at two zoom levels. Multi-select, copy, paste and time scaling work and are undoable. Undo covers at least fifty actions including deleting a track. Scrubbing a file with a hundred animated elements stays smooth, measured rather than assumed. Do not build yet: No project files or export packaging yet. ### Phase 5 · Projects that outlive the editor Build: Save projects to IndexedDB with a versioned format and atomic writes, and export the project as a file that reopens identically. Ask for persistent storage and still treat the file as the real save. Export the animated SVG and a CSS-only variant, and verify the animated file plays when placed in an image tag, where scripts do not run. Trap: Treating browser storage as the filing system means a cleared site, a private window or storage pressure erases weeks of work with no warning, because the browser may evict it unless persistence was granted and sometimes even then. Version the format, export to a file, and make reopening the file identical to reopening the stored project, tested. Done when: A project exported to a file and reopened reproduces every keyframe, easing and origin, asserted in the suite. Storage writes are atomic and a project written under an older format version opens after the format changes. The animated export plays inside an image tag with no script, verified in a headless browser. The CSS-only export contains the same keyframes as the animated file. Do not build yet: Nothing after this. ### Out of scope (and why) - Path morphing, motion along a path and stroke draw-on, which are the effects people pay for and each is a project of its own. - Shape and text editing, which turns an animation tool into a vector editor with all of its problems. - Runtime script, SMIL, Lottie and video export, since the CSS export is the one that plays where the file is embedded. - Collaboration and cloud projects, which add a server to a tool that has no need for one. ### README must contain - That the original SVG is kept as the document and the editor only adds attributes, so nothing it did not understand is lost - That the preview runs the generated stylesheet, so it cannot differ from the export - That transform-box and transform-origin are written on every animated element because the SVG defaults are the viewport and its corner - That the composition order is translate, rotate, scale, and is stated in the interface - That browser storage is not the save; the exported project file is, and it round trips in the suite ===== AGENTS.md ===== # Agent instructions · SVGator indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Vite, React and TypeScript, entirely in the browser with no backend, The original SVG kept as a live DOM, never re-serialised from a model, CSS keyframes generated from the timeline and injected for preview, transform-box and transform-origin set explicitly on every animated element, Versioned project files in IndexedDB and exported to disk. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Building an internal model of shapes and re-serialising it for export drops every attribute, namespace and element the model did not have a slot for, and the user finds out when a gradient or a clip path disappears. Keep the original DOM as the document and only add attributes to it. - Interpolating in JavaScript for the preview while emitting CSS for the export produces two implementations that agree on the easy cases and diverge on easing, rotation past a half turn and overlapping keyframes, and the person only sees the difference after shipping. Make the preview run the generated stylesheet, so there is one implementation. - The specification's initial transform-box is the nearest SVG viewport, and for SVG elements without a CSS layout box the used transform-origin is the top-left corner, so a rotation exported without both properties spins the element around the corner of the whole drawing rather than its own centre, and engines have historically differed on top of that. Emit both properties on every animated element and never rely on the default. - Snapping keyframes to pixels rather than to time means the same drag lands on a different millisecond at a different zoom, and the animation subtly changes when the user zooms the timeline. Snap in time units and derive pixels from them. ===== BUILD_PLAN.md ===== # Build plan · SVGator A browser SVG animation editor whose hard problems are a preview that cannot disagree with the export, transform origins that every engine interprets the same way, and a project file that outlives the editor, rather than the timeline widgets people notice first. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Loading a file without changing it The editor understands the file's structure and does not alter what it does not touch. ### Steps 1. Keep the DOM as the document A model of your own drops whatever it did not have a slot for. Files: `src/document.ts` 2. Assign ids without colliding Only where missing, and unique against what the file already has. Files: `src/ids.ts`, `src/ids.test.ts` 3. Round trip an untouched file in the suite Semantic equality of the two DOMs, not string equality. Files: `tests/roundtrip.test.ts` ### Done when - [ ] An untouched project exports semantically identical to the input - [ ] Every element has a stable unique id - [ ] Selecting a tree node outlines it on the canvas - [ ] Defs, nested svg and foreignObject survive a round trip ### Watch out - Building an internal model of shapes and re-serialising it for export drops every attribute, namespace and element the model did not have a slot for, and the user finds out when a gradient or a clip path disappears. Keep the original DOM as the document and only add attributes to it. ## Phase 2 · A preview that runs the export Scrubbing the timeline shows exactly what the exported file will show. ### Steps 1. Generate keyframes from the timeline One generator, used for preview and export alike. Files: `src/css/keyframes.ts`, `src/css/keyframes.test.ts` 2. Scrub with a paused animation and a negative delay The browser does the interpolation, which is the point. Files: `src/preview.ts` 3. Compare preview and export in a headless browser Computed transforms at three times, not a visual glance. Files: `tests/preview-equals-export.test.ts` ### Done when - [ ] The preview uses the generated stylesheet - [ ] Computed state at a scrubbed time matches the export - [ ] Easing is per segment including cubic-bezier - [ ] Transport controls work from the keyboard ### Watch out - Interpolating in JavaScript for the preview while emitting CSS for the export produces two implementations that agree on the easy cases and diverge on easing, rotation past a half turn and overlapping keyframes, and the person only sees the difference after shipping. Make the preview run the generated stylesheet, so there is one implementation. ## Phase 3 · Origins every engine agrees on A rotation spins around the point the user chose, in every browser. ### Steps 1. Emit both origin properties on every animated element The defaults for SVG are the viewport and its corner. Files: `src/css/origins.ts`, `tests/origins-present.test.ts` 2. Fix and state the composition order Translate, rotate, scale, and say so in the interface. Files: `src/css/compose.ts` 3. Render in three engines and compare A rotation about the centre must land in the same place. Files: `tests/engines.spec.ts` ```sh npx playwright test tests/engines.spec.ts --project=chromium --project=firefox --project=webkit ``` ### Done when - [ ] Every animated element carries both origin properties - [ ] A centre rotation matches across three engines - [ ] The composition order is stated and consistent - [ ] Nested transformed groups animate correctly ### Watch out - The specification's initial transform-box is the nearest SVG viewport, and for SVG elements without a CSS layout box the used transform-origin is the top-left corner, so a rotation exported without both properties spins the element around the corner of the whole drawing rather than its own centre, and engines have historically differed on top of that. Emit both properties on every animated element and never rely on the default. ## Phase 4 · A timeline you can work in for an hour Keyframes can be placed, moved, copied and undone without fighting the interface. ### Steps 1. Snap in time, derive pixels Zooming the timeline must not move a keyframe. Files: `src/timeline/snap.ts`, `src/timeline/snap.test.ts` 2. Make undo structural Deleting a track and pasting keyframes both undo cleanly. Files: `src/undo.ts` 3. Measure scrubbing at a hundred elements Smoothness is a number, not an impression. Files: `tests/perf.spec.ts` ### Done when - [ ] Snapping is zoom independent - [ ] Copy, paste and scaling are undoable - [ ] Undo covers fifty actions including track deletion - [ ] A hundred elements scrub smoothly ### Watch out - Snapping keyframes to pixels rather than to time means the same drag lands on a different millisecond at a different zoom, and the animation subtly changes when the user zooms the timeline. Snap in time units and derive pixels from them. ## Not in this build - Path morphing, motion along a path and stroke draw-on. - Shape and text editing. - Script, SMIL, Lottie and video export. - Collaboration and cloud projects. ## After v1, if you want it - Stroke draw-on through dash offset, which is the one effect that is a small addition to this design - An easing preset library with visible curves, since linear is the tell of a home-made animation - Motion along a path, which needs its own interpolation and is the first thing to charge for ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Written into every project file. Migrations read it. PROJECT_FORMAT_VERSION=1 # Required. New project duration. Editable per project. DEFAULT_DURATION_MS=3000 # Required. Fixed and stated in the interface. Changing it changes every animation. TRANSFORM_ORDER=translate,rotate,scale # Required. Emitted on every animated element with an explicit origin. DEFAULT_TRANSFORM_BOX=fill-box # Required. Fast keyframe work produces mistakes and they should be cheap. UNDO_DEPTH=50 # Required. Asked for at first save. Even when granted, the exported file is the real save. REQUEST_PERSISTENT_STORAGE=true
You are building a production product version of SVGator. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # SVGator · product brief ## Problem There is no network here and no data moat: it is a single-player editor that turns your own SVG into an animated SVG. An agent can absolutely build the spine in a session: parse the file, list the elements, keyframe transform and opacity on a timeline, emit CSS keyframes inside the SVG. What eats the weekend and then some is everything after transform: path morphing, stroke draw-on, motion along a path, a real bezier easing editor, group transform origins, and an export pipeline that does not break in Safari. The core loop survives DIY; the last 20 percent of the feature list is where the actual product lives. Build it if you animate two or three properties and hand-tune the rest, buy it if you ship illustrated micro-animations for a living. ## Product outcome An animation editor whose preview is the export, whose rotations land in the same place in every engine, and whose projects live in files you can keep. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - A modern browser - Node for the local dev server - SVG files you actually own, ideally with sensible element ids ## Explicit non-goals for v1 - Path morphing, motion along a path and stroke draw-on. - Shape and text editing. - Script, SMIL, Lottie and video export. - Collaboration and cloud projects. - Path morphing, stroke draw-on, and motion-along-path, which are the effects people buy this for - A polished easing curve editor with presets that look intentional instead of linear - Export targets beyond CSS: SMIL, JS runtime, Lottie, video - Cross-browser export sanity checks and the fixes for Safari's transform-origin quirks - Built-in shape and text editing, so every asset change means a round trip to your vector editor ## Success criteria - An untouched project is confirmed to export identical to the input - The preview is confirmed to run the generated stylesheet - Both origin properties are confirmed present on every animated element - A centre rotation is confirmed identical across three engines - A project file is confirmed to reopen identically ===== BRIEF.md ===== # Build brief · SVGator The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a timeline editor that animates your own SVG and exports one file that plays the same in every browser to replace SVGator. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Vite, React and TypeScript, entirely in the browser with no backend · there are no secrets and no reason for a server - Document: The original SVG kept as a live DOM, never re-serialised from a model · a round trip through a model drops the things it did not understand - Animation: CSS keyframes generated from the timeline and injected for preview · the preview runs the export itself, so it cannot lie - Origins: transform-box and transform-origin set explicitly on every animated element · the SVG defaults are the viewport and its corner - Storage: Versioned project files in IndexedDB and exported to disk · browser storage is not a filing system ### Data model (create this before Phase 1) - projects(id, name, format_version, svg_source, duration_ms, created_at, updated_at) · the source is kept verbatim - elements(project_id, element_id, tag, label, assigned_id) · assigned_id is set only where the file had none - tracks(id, project_id, element_id, property) · property is translateX, translateY, rotate, scale or opacity - keyframes(id, track_id, at_ms, value, easing) · easing belongs to the segment leaving this keyframe - origins(project_id, element_id, box, origin_x, origin_y) · written into the export on every animated element - exports(id, project_id, kind, at, bytes, engines_checked_json) · kind is animated_svg or css_only - The SVG source is stored as text and the DOM is rebuilt from it on load, so nothing the editor did not understand is ever lost between sessions. ### Phase 1 · Loading a file without changing it Build: Parse the SVG into a live DOM rather than rasterising it, list its elements as a tree, and assign ids only to elements that lack one. Render on a checkerboard with zoom. Export an untouched project and compare it to the input. Trap: Building an internal model of shapes and re-serialising it for export drops every attribute, namespace and element the model did not have a slot for, and the user finds out when a gradient or a clip path disappears. Keep the original DOM as the document and only add attributes to it. Done when: A file loaded and exported with no animation is semantically identical to the input, tested by comparing the two DOMs. Every element appears in the tree with a stable id and selecting one outlines it on the canvas. Ids assigned by the editor are unique against the file's existing ids. A file containing defs, nested svg elements and foreignObject loads without error and exports unchanged. Do not build yet: No timeline, keyframes, easing or export of animation. ### Phase 2 · A preview that runs the export Build: A timeline with a fixed duration, keyframes per property per element and easing on each segment. Generate the CSS keyframes from the timeline, inject them into the live document, and scrub by pausing the animations and setting a negative delay equal to the playhead. Never interpolate in JavaScript for display. Trap: Interpolating in JavaScript for the preview while emitting CSS for the export produces two implementations that agree on the easy cases and diverge on easing, rotation past a half turn and overlapping keyframes, and the person only sees the difference after shipping. Make the preview run the generated stylesheet, so there is one implementation. Done when: The preview is driven by the same generated CSS as the export, verified by inspecting the injected stylesheet. Scrubbing to any time shows the state the export would show at that time, tested by comparing computed transforms at three times against the exported file in a headless browser. Easing is stored per segment and cubic-bezier values are accepted. Play, pause, loop and scrub work from the keyboard. Do not build yet: No transform-origin handling, timeline ergonomics or project files. ### Phase 3 · Origins every engine agrees on Build: Set transform-box and transform-origin explicitly on every animated element in the export, defaulting to the element's own bounding box and its centre. Compose transforms in a fixed documented order. Check the export in the three main engines and record what was checked. Trap: The specification's initial transform-box is the nearest SVG viewport, and for SVG elements without a CSS layout box the used transform-origin is the top-left corner, so a rotation exported without both properties spins the element around the corner of the whole drawing rather than its own centre, and engines have historically differed on top of that. Emit both properties on every animated element and never rely on the default. Done when: Every animated element in the export carries an explicit transform-box and transform-origin, asserted by parsing the output. A rotation about an element's centre renders in the same place in three engines, checked by rendering and comparing. The composition order is fixed, stated in the interface and identical between preview and export. An element inside a transformed group animates correctly relative to its parent. Do not build yet: No timeline ergonomics or project files yet. ### Phase 4 · A timeline you can work in for an hour Build: Drag keyframes with snapping to the time grid and to other keyframes, multi-select, copy and paste between elements, and undo covering structural changes. Scale a selection in time. Keep canvas and timeline in sync on a file with a hundred animated elements. Trap: Snapping keyframes to pixels rather than to time means the same drag lands on a different millisecond at a different zoom, and the animation subtly changes when the user zooms the timeline. Snap in time units and derive pixels from them. Done when: Keyframes snap to the time grid and to neighbours in time units regardless of zoom, tested by dragging at two zoom levels. Multi-select, copy, paste and time scaling work and are undoable. Undo covers at least fifty actions including deleting a track. Scrubbing a file with a hundred animated elements stays smooth, measured rather than assumed. Do not build yet: No project files or export packaging yet. ### Phase 5 · Projects that outlive the editor Build: Save projects to IndexedDB with a versioned format and atomic writes, and export the project as a file that reopens identically. Ask for persistent storage and still treat the file as the real save. Export the animated SVG and a CSS-only variant, and verify the animated file plays when placed in an image tag, where scripts do not run. Trap: Treating browser storage as the filing system means a cleared site, a private window or storage pressure erases weeks of work with no warning, because the browser may evict it unless persistence was granted and sometimes even then. Version the format, export to a file, and make reopening the file identical to reopening the stored project, tested. Done when: A project exported to a file and reopened reproduces every keyframe, easing and origin, asserted in the suite. Storage writes are atomic and a project written under an older format version opens after the format changes. The animated export plays inside an image tag with no script, verified in a headless browser. The CSS-only export contains the same keyframes as the animated file. Do not build yet: Nothing after this. ### Out of scope (and why) - Path morphing, motion along a path and stroke draw-on, which are the effects people pay for and each is a project of its own. - Shape and text editing, which turns an animation tool into a vector editor with all of its problems. - Runtime script, SMIL, Lottie and video export, since the CSS export is the one that plays where the file is embedded. - Collaboration and cloud projects, which add a server to a tool that has no need for one. ### README must contain - That the original SVG is kept as the document and the editor only adds attributes, so nothing it did not understand is lost - That the preview runs the generated stylesheet, so it cannot differ from the export - That transform-box and transform-origin are written on every animated element because the SVG defaults are the viewport and its corner - That the composition order is translate, rotate, scale, and is stated in the interface - That browser storage is not the save; the exported project file is, and it round trips in the suite ===== ARCHITECTURE.md ===== # Architecture · SVGator ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Vite, React and TypeScript, entirely in the browser with no backend | there are no secrets and no reason for a server | | Document | The original SVG kept as a live DOM, never re-serialised from a model | a round trip through a model drops the things it did not understand | | Animation | CSS keyframes generated from the timeline and injected for preview | the preview runs the export itself, so it cannot lie | | Origins | transform-box and transform-origin set explicitly on every animated element | the SVG defaults are the viewport and its corner | | Storage | Versioned project files in IndexedDB and exported to disk | browser storage is not a filing system | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Document | The original DOM plus added attributes. | Not swappable for a model of your own, which drops what it did not understand. | | Animation | Keyframe generation used by preview and export. | Any generator, provided the preview runs the same stylesheet. | | Origins | Explicit transform-box and transform-origin. | Not swappable for defaults, which are the viewport and its corner. | | Timeline | Snapping in time units and structural undo. | Any interface, provided snapping is zoom independent. | | Storage | Versioned project files. | Any store, provided the exported file reopens identically. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PROJECT_FORMAT_VERSION` · required · Written into every project file. Migrations read it. - `DEFAULT_DURATION_MS` · required · New project duration. Editable per project. - `TRANSFORM_ORDER` · required · Fixed and stated in the interface. Changing it changes every animation. - `DEFAULT_TRANSFORM_BOX` · required · Emitted on every animated element with an explicit origin. - `UNDO_DEPTH` · required · Fast keyframe work produces mistakes and they should be cheap. - `REQUEST_PERSISTENT_STORAGE` · required · Asked for at first save. Even when granted, the exported file is the real save. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · SVGator product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Vite, React and TypeScript, entirely in the browser with no backend, The original SVG kept as a live DOM, never re-serialised from a model, CSS keyframes generated from the timeline and injected for preview, transform-box and transform-origin set explicitly on every animated element, Versioned project files in IndexedDB and exported to disk. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Building an internal model of shapes and re-serialising it for export drops every attribute, namespace and element the model did not have a slot for, and the user finds out when a gradient or a clip path disappears. Keep the original DOM as the document and only add attributes to it. - Interpolating in JavaScript for the preview while emitting CSS for the export produces two implementations that agree on the easy cases and diverge on easing, rotation past a half turn and overlapping keyframes, and the person only sees the difference after shipping. Make the preview run the generated stylesheet, so there is one implementation. - The specification's initial transform-box is the nearest SVG viewport, and for SVG elements without a CSS layout box the used transform-origin is the top-left corner, so a rotation exported without both properties spins the element around the corner of the whole drawing rather than its own centre, and engines have historically differed on top of that. Emit both properties on every animated element and never rely on the default. - Snapping keyframes to pixels rather than to time means the same drag lands on a different millisecond at a different zoom, and the animation subtly changes when the user zooms the timeline. Snap in time units and derive pixels from them. - Treating browser storage as the filing system means a cleared site, a private window or storage pressure erases weeks of work with no warning, because the browser may evict it unless persistence was granted and sometimes even then. Version the format, export to a file, and make reopening the file identical to reopening the stored project, tested. ===== MILESTONES.md ===== # Delivery milestones · SVGator Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Loading a file without changing it The editor understands the file's structure and does not alter what it does not touch. ### Steps 1. Keep the DOM as the document A model of your own drops whatever it did not have a slot for. Files: `src/document.ts` 2. Assign ids without colliding Only where missing, and unique against what the file already has. Files: `src/ids.ts`, `src/ids.test.ts` 3. Round trip an untouched file in the suite Semantic equality of the two DOMs, not string equality. Files: `tests/roundtrip.test.ts` ### Done when - [ ] An untouched project exports semantically identical to the input - [ ] Every element has a stable unique id - [ ] Selecting a tree node outlines it on the canvas - [ ] Defs, nested svg and foreignObject survive a round trip ### Watch out - Building an internal model of shapes and re-serialising it for export drops every attribute, namespace and element the model did not have a slot for, and the user finds out when a gradient or a clip path disappears. Keep the original DOM as the document and only add attributes to it. ## M2 · A preview that runs the export Scrubbing the timeline shows exactly what the exported file will show. ### Steps 1. Generate keyframes from the timeline One generator, used for preview and export alike. Files: `src/css/keyframes.ts`, `src/css/keyframes.test.ts` 2. Scrub with a paused animation and a negative delay The browser does the interpolation, which is the point. Files: `src/preview.ts` 3. Compare preview and export in a headless browser Computed transforms at three times, not a visual glance. Files: `tests/preview-equals-export.test.ts` ### Done when - [ ] The preview uses the generated stylesheet - [ ] Computed state at a scrubbed time matches the export - [ ] Easing is per segment including cubic-bezier - [ ] Transport controls work from the keyboard ### Watch out - Interpolating in JavaScript for the preview while emitting CSS for the export produces two implementations that agree on the easy cases and diverge on easing, rotation past a half turn and overlapping keyframes, and the person only sees the difference after shipping. Make the preview run the generated stylesheet, so there is one implementation. ## M3 · Origins every engine agrees on A rotation spins around the point the user chose, in every browser. ### Steps 1. Emit both origin properties on every animated element The defaults for SVG are the viewport and its corner. Files: `src/css/origins.ts`, `tests/origins-present.test.ts` 2. Fix and state the composition order Translate, rotate, scale, and say so in the interface. Files: `src/css/compose.ts` 3. Render in three engines and compare A rotation about the centre must land in the same place. Files: `tests/engines.spec.ts` ```sh npx playwright test tests/engines.spec.ts --project=chromium --project=firefox --project=webkit ``` ### Done when - [ ] Every animated element carries both origin properties - [ ] A centre rotation matches across three engines - [ ] The composition order is stated and consistent - [ ] Nested transformed groups animate correctly ### Watch out - The specification's initial transform-box is the nearest SVG viewport, and for SVG elements without a CSS layout box the used transform-origin is the top-left corner, so a rotation exported without both properties spins the element around the corner of the whole drawing rather than its own centre, and engines have historically differed on top of that. Emit both properties on every animated element and never rely on the default. ## M4 · A timeline you can work in for an hour Keyframes can be placed, moved, copied and undone without fighting the interface. ### Steps 1. Snap in time, derive pixels Zooming the timeline must not move a keyframe. Files: `src/timeline/snap.ts`, `src/timeline/snap.test.ts` 2. Make undo structural Deleting a track and pasting keyframes both undo cleanly. Files: `src/undo.ts` 3. Measure scrubbing at a hundred elements Smoothness is a number, not an impression. Files: `tests/perf.spec.ts` ### Done when - [ ] Snapping is zoom independent - [ ] Copy, paste and scaling are undoable - [ ] Undo covers fifty actions including track deletion - [ ] A hundred elements scrub smoothly ### Watch out - Snapping keyframes to pixels rather than to time means the same drag lands on a different millisecond at a different zoom, and the animation subtly changes when the user zooms the timeline. Snap in time units and derive pixels from them. ## M5 · Projects that outlive the editor (production only) Nothing is trapped in browser storage and the export works where it will be used. ### Steps 1. Version the project format An old project must open after the format changes. Files: `src/project/format.ts`, `tests/format-migration.test.ts` 2. Round trip the project file Export, reopen, compare every keyframe. Files: `tests/project-roundtrip.test.ts` 3. Verify the export inside an image tag That is where most animated SVGs end up, and scripts do not run there. Files: `tests/img-embed.spec.ts` ### Done when - [ ] A project file reopens identically - [ ] Old format versions open after changes - [ ] The export plays in an image tag - [ ] CSS-only export matches the animated file ### Watch out - Treating browser storage as the filing system means a cleared site, a private window or storage pressure erases weeks of work with no warning, because the browser may evict it unless persistence was granted and sometimes even then. Version the format, export to a file, and make reopening the file identical to reopening the stored project, tested. ===== OPERATIONS.md ===== # Operations · SVGator ## Backup Exported project files on disk, in a folder ordinary backups already reach. Browser storage is a cache of them and should be treated as one. ## Restore Reopen the project file and confirm keyframes, easing and origins match the last export, which the round trip test already checks. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the engine suite is the monitor. A change in any browser engine shows up as a rotation landing somewhere else, and the suite catches it before a client does. ## Incident checklist An export that looks right in one browser and wrong in another is nearly always a missing origin property or a composition order mismatch, both of which the suite asserts. Reproduce on the saved project rather than a description. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] An untouched project is confirmed to export identical to the input - [ ] The preview is confirmed to run the generated stylesheet - [ ] Both origin properties are confirmed present on every animated element - [ ] A centre rotation is confirmed identical across three engines - [ ] A project file is confirmed to reopen identically ## Launch constraint Do not market omitted SVGator capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Written into every project file. Migrations read it. PROJECT_FORMAT_VERSION=1 # Required. New project duration. Editable per project. DEFAULT_DURATION_MS=3000 # Required. Fixed and stated in the interface. Changing it changes every animation. TRANSFORM_ORDER=translate,rotate,scale # Required. Emitted on every animated element with an explicit origin. DEFAULT_TRANSFORM_BOX=fill-box # Required. Fast keyframe work produces mistakes and they should be cheap. UNDO_DEPTH=50 # Required. Asked for at first save. Even when granted, the exported file is the real save. REQUEST_PERSISTENT_STORAGE=true
# SVGator · indie build A browser SVG animation editor whose hard problems are a preview that cannot disagree with the export, transform origins that every engine interprets the same way, and a project file that outlives the editor, rather than the timeline widgets people notice first. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Vite, React and TypeScript, entirely in the browser with no backend | there are no secrets and no reason for a server | | Document | The original SVG kept as a live DOM, never re-serialised from a model | a round trip through a model drops the things it did not understand | | Animation | CSS keyframes generated from the timeline and injected for preview | the preview runs the export itself, so it cannot lie | | Origins | transform-box and transform-origin set explicitly on every animated element | the SVG defaults are the viewport and its corner | | Storage | Versioned project files in IndexedDB and exported to disk | browser storage is not a filing system | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the dev server, the build and the test suite. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **The three main browser engines for checking exports** · free - Why: Transform origins and animation timing differ between engines, and an export checked in one engine is not a checked export. - Get it: Install the browsers your test runner supports and run the engine suite in all three. - Verify: The engine suite runs in three engines and passes - [ ] **A folder of real SVGs including awkward ones** · free - Why: Files from real vector editors contain defs, nested groups, clip paths and elements with no ids, and the round trip is only meaningful against those. - Get it: Export a dozen files from the editors you actually use, including ones with gradients, masks and text. - Verify: Every file loads and round trips unchanged - [ ] **The CSS Transforms specification, read for transform-box and transform-origin** · free - Why: The SVG defaults are not what anyone expects, and the export depends on writing both properties on every animated element. - Get it: Read the two property definitions and note the initial values and the SVG rule. - Verify: A note records the initial values and why the export always sets both - [ ] **Your transform composition order** · free - Why: It is baked into every export and cannot change later without changing every existing animation. - Get it: Decide translate, rotate, scale or another order, and write it into the interface. - Verify: The order is documented and asserted in the suite ## Quick start ```sh npx playwright test tests/engines.spec.ts --project=chromium --project=firefox --project=webkit ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Path morphing, motion along a path and stroke draw-on. - Shape and text editing. - Script, SMIL, Lottie and video export. - Collaboration and cloud projects. - Path morphing, stroke draw-on, and motion-along-path, which are the effects people buy this for - A polished easing curve editor with presets that look intentional instead of linear - Export targets beyond CSS: SMIL, JS runtime, Lottie, video - Cross-browser export sanity checks and the fixes for Safari's transform-origin quirks - Built-in shape and text editing, so every asset change means a round trip to your vector editor If one of those is essential to you, that is the reason to keep paying for SVGator, and the README should say so rather than pretend.
# Build brief · SVGator The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a timeline editor that animates your own SVG and exports one file that plays the same in every browser to replace SVGator. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Vite, React and TypeScript, entirely in the browser with no backend · there are no secrets and no reason for a server - Document: The original SVG kept as a live DOM, never re-serialised from a model · a round trip through a model drops the things it did not understand - Animation: CSS keyframes generated from the timeline and injected for preview · the preview runs the export itself, so it cannot lie - Origins: transform-box and transform-origin set explicitly on every animated element · the SVG defaults are the viewport and its corner - Storage: Versioned project files in IndexedDB and exported to disk · browser storage is not a filing system ### Data model (create this before Phase 1) - projects(id, name, format_version, svg_source, duration_ms, created_at, updated_at) · the source is kept verbatim - elements(project_id, element_id, tag, label, assigned_id) · assigned_id is set only where the file had none - tracks(id, project_id, element_id, property) · property is translateX, translateY, rotate, scale or opacity - keyframes(id, track_id, at_ms, value, easing) · easing belongs to the segment leaving this keyframe - origins(project_id, element_id, box, origin_x, origin_y) · written into the export on every animated element - exports(id, project_id, kind, at, bytes, engines_checked_json) · kind is animated_svg or css_only - The SVG source is stored as text and the DOM is rebuilt from it on load, so nothing the editor did not understand is ever lost between sessions. ### Phase 1 · Loading a file without changing it Build: Parse the SVG into a live DOM rather than rasterising it, list its elements as a tree, and assign ids only to elements that lack one. Render on a checkerboard with zoom. Export an untouched project and compare it to the input. Trap: Building an internal model of shapes and re-serialising it for export drops every attribute, namespace and element the model did not have a slot for, and the user finds out when a gradient or a clip path disappears. Keep the original DOM as the document and only add attributes to it. Done when: A file loaded and exported with no animation is semantically identical to the input, tested by comparing the two DOMs. Every element appears in the tree with a stable id and selecting one outlines it on the canvas. Ids assigned by the editor are unique against the file's existing ids. A file containing defs, nested svg elements and foreignObject loads without error and exports unchanged. Do not build yet: No timeline, keyframes, easing or export of animation. ### Phase 2 · A preview that runs the export Build: A timeline with a fixed duration, keyframes per property per element and easing on each segment. Generate the CSS keyframes from the timeline, inject them into the live document, and scrub by pausing the animations and setting a negative delay equal to the playhead. Never interpolate in JavaScript for display. Trap: Interpolating in JavaScript for the preview while emitting CSS for the export produces two implementations that agree on the easy cases and diverge on easing, rotation past a half turn and overlapping keyframes, and the person only sees the difference after shipping. Make the preview run the generated stylesheet, so there is one implementation. Done when: The preview is driven by the same generated CSS as the export, verified by inspecting the injected stylesheet. Scrubbing to any time shows the state the export would show at that time, tested by comparing computed transforms at three times against the exported file in a headless browser. Easing is stored per segment and cubic-bezier values are accepted. Play, pause, loop and scrub work from the keyboard. Do not build yet: No transform-origin handling, timeline ergonomics or project files. ### Phase 3 · Origins every engine agrees on Build: Set transform-box and transform-origin explicitly on every animated element in the export, defaulting to the element's own bounding box and its centre. Compose transforms in a fixed documented order. Check the export in the three main engines and record what was checked. Trap: The specification's initial transform-box is the nearest SVG viewport, and for SVG elements without a CSS layout box the used transform-origin is the top-left corner, so a rotation exported without both properties spins the element around the corner of the whole drawing rather than its own centre, and engines have historically differed on top of that. Emit both properties on every animated element and never rely on the default. Done when: Every animated element in the export carries an explicit transform-box and transform-origin, asserted by parsing the output. A rotation about an element's centre renders in the same place in three engines, checked by rendering and comparing. The composition order is fixed, stated in the interface and identical between preview and export. An element inside a transformed group animates correctly relative to its parent. Do not build yet: No timeline ergonomics or project files yet. ### Phase 4 · A timeline you can work in for an hour Build: Drag keyframes with snapping to the time grid and to other keyframes, multi-select, copy and paste between elements, and undo covering structural changes. Scale a selection in time. Keep canvas and timeline in sync on a file with a hundred animated elements. Trap: Snapping keyframes to pixels rather than to time means the same drag lands on a different millisecond at a different zoom, and the animation subtly changes when the user zooms the timeline. Snap in time units and derive pixels from them. Done when: Keyframes snap to the time grid and to neighbours in time units regardless of zoom, tested by dragging at two zoom levels. Multi-select, copy, paste and time scaling work and are undoable. Undo covers at least fifty actions including deleting a track. Scrubbing a file with a hundred animated elements stays smooth, measured rather than assumed. Do not build yet: No project files or export packaging yet. ### Phase 5 · Projects that outlive the editor Build: Save projects to IndexedDB with a versioned format and atomic writes, and export the project as a file that reopens identically. Ask for persistent storage and still treat the file as the real save. Export the animated SVG and a CSS-only variant, and verify the animated file plays when placed in an image tag, where scripts do not run. Trap: Treating browser storage as the filing system means a cleared site, a private window or storage pressure erases weeks of work with no warning, because the browser may evict it unless persistence was granted and sometimes even then. Version the format, export to a file, and make reopening the file identical to reopening the stored project, tested. Done when: A project exported to a file and reopened reproduces every keyframe, easing and origin, asserted in the suite. Storage writes are atomic and a project written under an older format version opens after the format changes. The animated export plays inside an image tag with no script, verified in a headless browser. The CSS-only export contains the same keyframes as the animated file. Do not build yet: Nothing after this. ### Out of scope (and why) - Path morphing, motion along a path and stroke draw-on, which are the effects people pay for and each is a project of its own. - Shape and text editing, which turns an animation tool into a vector editor with all of its problems. - Runtime script, SMIL, Lottie and video export, since the CSS export is the one that plays where the file is embedded. - Collaboration and cloud projects, which add a server to a tool that has no need for one. ### README must contain - That the original SVG is kept as the document and the editor only adds attributes, so nothing it did not understand is lost - That the preview runs the generated stylesheet, so it cannot differ from the export - That transform-box and transform-origin are written on every animated element because the SVG defaults are the viewport and its corner - That the composition order is translate, rotate, scale, and is stated in the interface - That browser storage is not the save; the exported project file is, and it round trips in the suite
# Agent instructions · SVGator indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Vite, React and TypeScript, entirely in the browser with no backend, The original SVG kept as a live DOM, never re-serialised from a model, CSS keyframes generated from the timeline and injected for preview, transform-box and transform-origin set explicitly on every animated element, Versioned project files in IndexedDB and exported to disk. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Building an internal model of shapes and re-serialising it for export drops every attribute, namespace and element the model did not have a slot for, and the user finds out when a gradient or a clip path disappears. Keep the original DOM as the document and only add attributes to it. - Interpolating in JavaScript for the preview while emitting CSS for the export produces two implementations that agree on the easy cases and diverge on easing, rotation past a half turn and overlapping keyframes, and the person only sees the difference after shipping. Make the preview run the generated stylesheet, so there is one implementation. - The specification's initial transform-box is the nearest SVG viewport, and for SVG elements without a CSS layout box the used transform-origin is the top-left corner, so a rotation exported without both properties spins the element around the corner of the whole drawing rather than its own centre, and engines have historically differed on top of that. Emit both properties on every animated element and never rely on the default. - Snapping keyframes to pixels rather than to time means the same drag lands on a different millisecond at a different zoom, and the animation subtly changes when the user zooms the timeline. Snap in time units and derive pixels from them.
# Build plan · SVGator A browser SVG animation editor whose hard problems are a preview that cannot disagree with the export, transform origins that every engine interprets the same way, and a project file that outlives the editor, rather than the timeline widgets people notice first. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Loading a file without changing it The editor understands the file's structure and does not alter what it does not touch. ### Steps 1. Keep the DOM as the document A model of your own drops whatever it did not have a slot for. Files: `src/document.ts` 2. Assign ids without colliding Only where missing, and unique against what the file already has. Files: `src/ids.ts`, `src/ids.test.ts` 3. Round trip an untouched file in the suite Semantic equality of the two DOMs, not string equality. Files: `tests/roundtrip.test.ts` ### Done when - [ ] An untouched project exports semantically identical to the input - [ ] Every element has a stable unique id - [ ] Selecting a tree node outlines it on the canvas - [ ] Defs, nested svg and foreignObject survive a round trip ### Watch out - Building an internal model of shapes and re-serialising it for export drops every attribute, namespace and element the model did not have a slot for, and the user finds out when a gradient or a clip path disappears. Keep the original DOM as the document and only add attributes to it. ## Phase 2 · A preview that runs the export Scrubbing the timeline shows exactly what the exported file will show. ### Steps 1. Generate keyframes from the timeline One generator, used for preview and export alike. Files: `src/css/keyframes.ts`, `src/css/keyframes.test.ts` 2. Scrub with a paused animation and a negative delay The browser does the interpolation, which is the point. Files: `src/preview.ts` 3. Compare preview and export in a headless browser Computed transforms at three times, not a visual glance. Files: `tests/preview-equals-export.test.ts` ### Done when - [ ] The preview uses the generated stylesheet - [ ] Computed state at a scrubbed time matches the export - [ ] Easing is per segment including cubic-bezier - [ ] Transport controls work from the keyboard ### Watch out - Interpolating in JavaScript for the preview while emitting CSS for the export produces two implementations that agree on the easy cases and diverge on easing, rotation past a half turn and overlapping keyframes, and the person only sees the difference after shipping. Make the preview run the generated stylesheet, so there is one implementation. ## Phase 3 · Origins every engine agrees on A rotation spins around the point the user chose, in every browser. ### Steps 1. Emit both origin properties on every animated element The defaults for SVG are the viewport and its corner. Files: `src/css/origins.ts`, `tests/origins-present.test.ts` 2. Fix and state the composition order Translate, rotate, scale, and say so in the interface. Files: `src/css/compose.ts` 3. Render in three engines and compare A rotation about the centre must land in the same place. Files: `tests/engines.spec.ts` ```sh npx playwright test tests/engines.spec.ts --project=chromium --project=firefox --project=webkit ``` ### Done when - [ ] Every animated element carries both origin properties - [ ] A centre rotation matches across three engines - [ ] The composition order is stated and consistent - [ ] Nested transformed groups animate correctly ### Watch out - The specification's initial transform-box is the nearest SVG viewport, and for SVG elements without a CSS layout box the used transform-origin is the top-left corner, so a rotation exported without both properties spins the element around the corner of the whole drawing rather than its own centre, and engines have historically differed on top of that. Emit both properties on every animated element and never rely on the default. ## Phase 4 · A timeline you can work in for an hour Keyframes can be placed, moved, copied and undone without fighting the interface. ### Steps 1. Snap in time, derive pixels Zooming the timeline must not move a keyframe. Files: `src/timeline/snap.ts`, `src/timeline/snap.test.ts` 2. Make undo structural Deleting a track and pasting keyframes both undo cleanly. Files: `src/undo.ts` 3. Measure scrubbing at a hundred elements Smoothness is a number, not an impression. Files: `tests/perf.spec.ts` ### Done when - [ ] Snapping is zoom independent - [ ] Copy, paste and scaling are undoable - [ ] Undo covers fifty actions including track deletion - [ ] A hundred elements scrub smoothly ### Watch out - Snapping keyframes to pixels rather than to time means the same drag lands on a different millisecond at a different zoom, and the animation subtly changes when the user zooms the timeline. Snap in time units and derive pixels from them. ## Not in this build - Path morphing, motion along a path and stroke draw-on. - Shape and text editing. - Script, SMIL, Lottie and video export. - Collaboration and cloud projects. ## After v1, if you want it - Stroke draw-on through dash offset, which is the one effect that is a small addition to this design - An easing preset library with visible curves, since linear is the tell of a home-made animation - Motion along a path, which needs its own interpolation and is the first thing to charge for
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Written into every project file. Migrations read it. PROJECT_FORMAT_VERSION=1 # Required. New project duration. Editable per project. DEFAULT_DURATION_MS=3000 # Required. Fixed and stated in the interface. Changing it changes every animation. TRANSFORM_ORDER=translate,rotate,scale # Required. Emitted on every animated element with an explicit origin. DEFAULT_TRANSFORM_BOX=fill-box # Required. Fast keyframe work produces mistakes and they should be cheap. UNDO_DEPTH=50 # Required. Asked for at first save. Even when granted, the exported file is the real save. REQUEST_PERSISTENT_STORAGE=true
# SVGator · product brief ## Problem There is no network here and no data moat: it is a single-player editor that turns your own SVG into an animated SVG. An agent can absolutely build the spine in a session: parse the file, list the elements, keyframe transform and opacity on a timeline, emit CSS keyframes inside the SVG. What eats the weekend and then some is everything after transform: path morphing, stroke draw-on, motion along a path, a real bezier easing editor, group transform origins, and an export pipeline that does not break in Safari. The core loop survives DIY; the last 20 percent of the feature list is where the actual product lives. Build it if you animate two or three properties and hand-tune the rest, buy it if you ship illustrated micro-animations for a living. ## Product outcome An animation editor whose preview is the export, whose rotations land in the same place in every engine, and whose projects live in files you can keep. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - A modern browser - Node for the local dev server - SVG files you actually own, ideally with sensible element ids ## Explicit non-goals for v1 - Path morphing, motion along a path and stroke draw-on. - Shape and text editing. - Script, SMIL, Lottie and video export. - Collaboration and cloud projects. - Path morphing, stroke draw-on, and motion-along-path, which are the effects people buy this for - A polished easing curve editor with presets that look intentional instead of linear - Export targets beyond CSS: SMIL, JS runtime, Lottie, video - Cross-browser export sanity checks and the fixes for Safari's transform-origin quirks - Built-in shape and text editing, so every asset change means a round trip to your vector editor ## Success criteria - An untouched project is confirmed to export identical to the input - The preview is confirmed to run the generated stylesheet - Both origin properties are confirmed present on every animated element - A centre rotation is confirmed identical across three engines - A project file is confirmed to reopen identically
# Build brief · SVGator The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a timeline editor that animates your own SVG and exports one file that plays the same in every browser to replace SVGator. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Vite, React and TypeScript, entirely in the browser with no backend · there are no secrets and no reason for a server - Document: The original SVG kept as a live DOM, never re-serialised from a model · a round trip through a model drops the things it did not understand - Animation: CSS keyframes generated from the timeline and injected for preview · the preview runs the export itself, so it cannot lie - Origins: transform-box and transform-origin set explicitly on every animated element · the SVG defaults are the viewport and its corner - Storage: Versioned project files in IndexedDB and exported to disk · browser storage is not a filing system ### Data model (create this before Phase 1) - projects(id, name, format_version, svg_source, duration_ms, created_at, updated_at) · the source is kept verbatim - elements(project_id, element_id, tag, label, assigned_id) · assigned_id is set only where the file had none - tracks(id, project_id, element_id, property) · property is translateX, translateY, rotate, scale or opacity - keyframes(id, track_id, at_ms, value, easing) · easing belongs to the segment leaving this keyframe - origins(project_id, element_id, box, origin_x, origin_y) · written into the export on every animated element - exports(id, project_id, kind, at, bytes, engines_checked_json) · kind is animated_svg or css_only - The SVG source is stored as text and the DOM is rebuilt from it on load, so nothing the editor did not understand is ever lost between sessions. ### Phase 1 · Loading a file without changing it Build: Parse the SVG into a live DOM rather than rasterising it, list its elements as a tree, and assign ids only to elements that lack one. Render on a checkerboard with zoom. Export an untouched project and compare it to the input. Trap: Building an internal model of shapes and re-serialising it for export drops every attribute, namespace and element the model did not have a slot for, and the user finds out when a gradient or a clip path disappears. Keep the original DOM as the document and only add attributes to it. Done when: A file loaded and exported with no animation is semantically identical to the input, tested by comparing the two DOMs. Every element appears in the tree with a stable id and selecting one outlines it on the canvas. Ids assigned by the editor are unique against the file's existing ids. A file containing defs, nested svg elements and foreignObject loads without error and exports unchanged. Do not build yet: No timeline, keyframes, easing or export of animation. ### Phase 2 · A preview that runs the export Build: A timeline with a fixed duration, keyframes per property per element and easing on each segment. Generate the CSS keyframes from the timeline, inject them into the live document, and scrub by pausing the animations and setting a negative delay equal to the playhead. Never interpolate in JavaScript for display. Trap: Interpolating in JavaScript for the preview while emitting CSS for the export produces two implementations that agree on the easy cases and diverge on easing, rotation past a half turn and overlapping keyframes, and the person only sees the difference after shipping. Make the preview run the generated stylesheet, so there is one implementation. Done when: The preview is driven by the same generated CSS as the export, verified by inspecting the injected stylesheet. Scrubbing to any time shows the state the export would show at that time, tested by comparing computed transforms at three times against the exported file in a headless browser. Easing is stored per segment and cubic-bezier values are accepted. Play, pause, loop and scrub work from the keyboard. Do not build yet: No transform-origin handling, timeline ergonomics or project files. ### Phase 3 · Origins every engine agrees on Build: Set transform-box and transform-origin explicitly on every animated element in the export, defaulting to the element's own bounding box and its centre. Compose transforms in a fixed documented order. Check the export in the three main engines and record what was checked. Trap: The specification's initial transform-box is the nearest SVG viewport, and for SVG elements without a CSS layout box the used transform-origin is the top-left corner, so a rotation exported without both properties spins the element around the corner of the whole drawing rather than its own centre, and engines have historically differed on top of that. Emit both properties on every animated element and never rely on the default. Done when: Every animated element in the export carries an explicit transform-box and transform-origin, asserted by parsing the output. A rotation about an element's centre renders in the same place in three engines, checked by rendering and comparing. The composition order is fixed, stated in the interface and identical between preview and export. An element inside a transformed group animates correctly relative to its parent. Do not build yet: No timeline ergonomics or project files yet. ### Phase 4 · A timeline you can work in for an hour Build: Drag keyframes with snapping to the time grid and to other keyframes, multi-select, copy and paste between elements, and undo covering structural changes. Scale a selection in time. Keep canvas and timeline in sync on a file with a hundred animated elements. Trap: Snapping keyframes to pixels rather than to time means the same drag lands on a different millisecond at a different zoom, and the animation subtly changes when the user zooms the timeline. Snap in time units and derive pixels from them. Done when: Keyframes snap to the time grid and to neighbours in time units regardless of zoom, tested by dragging at two zoom levels. Multi-select, copy, paste and time scaling work and are undoable. Undo covers at least fifty actions including deleting a track. Scrubbing a file with a hundred animated elements stays smooth, measured rather than assumed. Do not build yet: No project files or export packaging yet. ### Phase 5 · Projects that outlive the editor Build: Save projects to IndexedDB with a versioned format and atomic writes, and export the project as a file that reopens identically. Ask for persistent storage and still treat the file as the real save. Export the animated SVG and a CSS-only variant, and verify the animated file plays when placed in an image tag, where scripts do not run. Trap: Treating browser storage as the filing system means a cleared site, a private window or storage pressure erases weeks of work with no warning, because the browser may evict it unless persistence was granted and sometimes even then. Version the format, export to a file, and make reopening the file identical to reopening the stored project, tested. Done when: A project exported to a file and reopened reproduces every keyframe, easing and origin, asserted in the suite. Storage writes are atomic and a project written under an older format version opens after the format changes. The animated export plays inside an image tag with no script, verified in a headless browser. The CSS-only export contains the same keyframes as the animated file. Do not build yet: Nothing after this. ### Out of scope (and why) - Path morphing, motion along a path and stroke draw-on, which are the effects people pay for and each is a project of its own. - Shape and text editing, which turns an animation tool into a vector editor with all of its problems. - Runtime script, SMIL, Lottie and video export, since the CSS export is the one that plays where the file is embedded. - Collaboration and cloud projects, which add a server to a tool that has no need for one. ### README must contain - That the original SVG is kept as the document and the editor only adds attributes, so nothing it did not understand is lost - That the preview runs the generated stylesheet, so it cannot differ from the export - That transform-box and transform-origin are written on every animated element because the SVG defaults are the viewport and its corner - That the composition order is translate, rotate, scale, and is stated in the interface - That browser storage is not the save; the exported project file is, and it round trips in the suite
# Architecture · SVGator ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Vite, React and TypeScript, entirely in the browser with no backend | there are no secrets and no reason for a server | | Document | The original SVG kept as a live DOM, never re-serialised from a model | a round trip through a model drops the things it did not understand | | Animation | CSS keyframes generated from the timeline and injected for preview | the preview runs the export itself, so it cannot lie | | Origins | transform-box and transform-origin set explicitly on every animated element | the SVG defaults are the viewport and its corner | | Storage | Versioned project files in IndexedDB and exported to disk | browser storage is not a filing system | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Document | The original DOM plus added attributes. | Not swappable for a model of your own, which drops what it did not understand. | | Animation | Keyframe generation used by preview and export. | Any generator, provided the preview runs the same stylesheet. | | Origins | Explicit transform-box and transform-origin. | Not swappable for defaults, which are the viewport and its corner. | | Timeline | Snapping in time units and structural undo. | Any interface, provided snapping is zoom independent. | | Storage | Versioned project files. | Any store, provided the exported file reopens identically. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PROJECT_FORMAT_VERSION` · required · Written into every project file. Migrations read it. - `DEFAULT_DURATION_MS` · required · New project duration. Editable per project. - `TRANSFORM_ORDER` · required · Fixed and stated in the interface. Changing it changes every animation. - `DEFAULT_TRANSFORM_BOX` · required · Emitted on every animated element with an explicit origin. - `UNDO_DEPTH` · required · Fast keyframe work produces mistakes and they should be cheap. - `REQUEST_PERSISTENT_STORAGE` · required · Asked for at first save. Even when granted, the exported file is the real save. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · SVGator product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Vite, React and TypeScript, entirely in the browser with no backend, The original SVG kept as a live DOM, never re-serialised from a model, CSS keyframes generated from the timeline and injected for preview, transform-box and transform-origin set explicitly on every animated element, Versioned project files in IndexedDB and exported to disk. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Building an internal model of shapes and re-serialising it for export drops every attribute, namespace and element the model did not have a slot for, and the user finds out when a gradient or a clip path disappears. Keep the original DOM as the document and only add attributes to it. - Interpolating in JavaScript for the preview while emitting CSS for the export produces two implementations that agree on the easy cases and diverge on easing, rotation past a half turn and overlapping keyframes, and the person only sees the difference after shipping. Make the preview run the generated stylesheet, so there is one implementation. - The specification's initial transform-box is the nearest SVG viewport, and for SVG elements without a CSS layout box the used transform-origin is the top-left corner, so a rotation exported without both properties spins the element around the corner of the whole drawing rather than its own centre, and engines have historically differed on top of that. Emit both properties on every animated element and never rely on the default. - Snapping keyframes to pixels rather than to time means the same drag lands on a different millisecond at a different zoom, and the animation subtly changes when the user zooms the timeline. Snap in time units and derive pixels from them. - Treating browser storage as the filing system means a cleared site, a private window or storage pressure erases weeks of work with no warning, because the browser may evict it unless persistence was granted and sometimes even then. Version the format, export to a file, and make reopening the file identical to reopening the stored project, tested.
# Delivery milestones · SVGator Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Loading a file without changing it The editor understands the file's structure and does not alter what it does not touch. ### Steps 1. Keep the DOM as the document A model of your own drops whatever it did not have a slot for. Files: `src/document.ts` 2. Assign ids without colliding Only where missing, and unique against what the file already has. Files: `src/ids.ts`, `src/ids.test.ts` 3. Round trip an untouched file in the suite Semantic equality of the two DOMs, not string equality. Files: `tests/roundtrip.test.ts` ### Done when - [ ] An untouched project exports semantically identical to the input - [ ] Every element has a stable unique id - [ ] Selecting a tree node outlines it on the canvas - [ ] Defs, nested svg and foreignObject survive a round trip ### Watch out - Building an internal model of shapes and re-serialising it for export drops every attribute, namespace and element the model did not have a slot for, and the user finds out when a gradient or a clip path disappears. Keep the original DOM as the document and only add attributes to it. ## M2 · A preview that runs the export Scrubbing the timeline shows exactly what the exported file will show. ### Steps 1. Generate keyframes from the timeline One generator, used for preview and export alike. Files: `src/css/keyframes.ts`, `src/css/keyframes.test.ts` 2. Scrub with a paused animation and a negative delay The browser does the interpolation, which is the point. Files: `src/preview.ts` 3. Compare preview and export in a headless browser Computed transforms at three times, not a visual glance. Files: `tests/preview-equals-export.test.ts` ### Done when - [ ] The preview uses the generated stylesheet - [ ] Computed state at a scrubbed time matches the export - [ ] Easing is per segment including cubic-bezier - [ ] Transport controls work from the keyboard ### Watch out - Interpolating in JavaScript for the preview while emitting CSS for the export produces two implementations that agree on the easy cases and diverge on easing, rotation past a half turn and overlapping keyframes, and the person only sees the difference after shipping. Make the preview run the generated stylesheet, so there is one implementation. ## M3 · Origins every engine agrees on A rotation spins around the point the user chose, in every browser. ### Steps 1. Emit both origin properties on every animated element The defaults for SVG are the viewport and its corner. Files: `src/css/origins.ts`, `tests/origins-present.test.ts` 2. Fix and state the composition order Translate, rotate, scale, and say so in the interface. Files: `src/css/compose.ts` 3. Render in three engines and compare A rotation about the centre must land in the same place. Files: `tests/engines.spec.ts` ```sh npx playwright test tests/engines.spec.ts --project=chromium --project=firefox --project=webkit ``` ### Done when - [ ] Every animated element carries both origin properties - [ ] A centre rotation matches across three engines - [ ] The composition order is stated and consistent - [ ] Nested transformed groups animate correctly ### Watch out - The specification's initial transform-box is the nearest SVG viewport, and for SVG elements without a CSS layout box the used transform-origin is the top-left corner, so a rotation exported without both properties spins the element around the corner of the whole drawing rather than its own centre, and engines have historically differed on top of that. Emit both properties on every animated element and never rely on the default. ## M4 · A timeline you can work in for an hour Keyframes can be placed, moved, copied and undone without fighting the interface. ### Steps 1. Snap in time, derive pixels Zooming the timeline must not move a keyframe. Files: `src/timeline/snap.ts`, `src/timeline/snap.test.ts` 2. Make undo structural Deleting a track and pasting keyframes both undo cleanly. Files: `src/undo.ts` 3. Measure scrubbing at a hundred elements Smoothness is a number, not an impression. Files: `tests/perf.spec.ts` ### Done when - [ ] Snapping is zoom independent - [ ] Copy, paste and scaling are undoable - [ ] Undo covers fifty actions including track deletion - [ ] A hundred elements scrub smoothly ### Watch out - Snapping keyframes to pixels rather than to time means the same drag lands on a different millisecond at a different zoom, and the animation subtly changes when the user zooms the timeline. Snap in time units and derive pixels from them. ## M5 · Projects that outlive the editor (production only) Nothing is trapped in browser storage and the export works where it will be used. ### Steps 1. Version the project format An old project must open after the format changes. Files: `src/project/format.ts`, `tests/format-migration.test.ts` 2. Round trip the project file Export, reopen, compare every keyframe. Files: `tests/project-roundtrip.test.ts` 3. Verify the export inside an image tag That is where most animated SVGs end up, and scripts do not run there. Files: `tests/img-embed.spec.ts` ### Done when - [ ] A project file reopens identically - [ ] Old format versions open after changes - [ ] The export plays in an image tag - [ ] CSS-only export matches the animated file ### Watch out - Treating browser storage as the filing system means a cleared site, a private window or storage pressure erases weeks of work with no warning, because the browser may evict it unless persistence was granted and sometimes even then. Version the format, export to a file, and make reopening the file identical to reopening the stored project, tested.
# Operations · SVGator ## Backup Exported project files on disk, in a folder ordinary backups already reach. Browser storage is a cache of them and should be treated as one. ## Restore Reopen the project file and confirm keyframes, easing and origins match the last export, which the round trip test already checks. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the engine suite is the monitor. A change in any browser engine shows up as a rotation landing somewhere else, and the suite catches it before a client does. ## Incident checklist An export that looks right in one browser and wrong in another is nearly always a missing origin property or a composition order mismatch, both of which the suite asserts. Reproduce on the saved project rather than a description. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] An untouched project is confirmed to export identical to the input - [ ] The preview is confirmed to run the generated stylesheet - [ ] Both origin properties are confirmed present on every animated element - [ ] A centre rotation is confirmed identical across three engines - [ ] A project file is confirmed to reopen identically ## Launch constraint Do not market omitted SVGator capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Written into every project file. Migrations read it. PROJECT_FORMAT_VERSION=1 # Required. New project duration. Editable per project. DEFAULT_DURATION_MS=3000 # Required. Fixed and stated in the interface. Changing it changes every animation. TRANSFORM_ORDER=translate,rotate,scale # Required. Emitted on every animated element with an explicit origin. DEFAULT_TRANSFORM_BOX=fill-box # Required. Fast keyframe work produces mistakes and they should be cheap. UNDO_DEPTH=50 # Required. Asked for at first save. Even when granted, the exported file is the real save. REQUEST_PERSISTENT_STORAGE=true
$ choose a build depth, inspect the files, then open the complete pack in your agent
Animation tooling is unforgiving about details: a timeline that snaps wrong, an easing curve you cannot tweak, or an export that jitters in Safari makes the whole thing useless, and none of those are things you notice until you are three hours into a client deadline. SVGator has absorbed years of those bugs, and the effects catalog beyond simple transforms is genuine work. Paying is a way of buying someone else's cross-browser scar tissue instead of growing your own.
xPath morphing, stroke draw-on, and motion-along-path, which are the effects people buy this for
xA polished easing curve editor with presets that look intentional instead of linear
xExport targets beyond CSS: SMIL, JS runtime, Lottie, video
xCross-browser export sanity checks and the fixes for Safari's transform-origin quirks
xBuilt-in shape and text editing, so every asset change means a round trip to your vector editor
Nothing worth pointing at. That's why the prompt exists.
Vibecode SVGator
Kinda. The core of SVGator is buildable in a weekend with the prompt on this page, but there are real gaps: Path morphing, stroke draw-on, and motion-along-path, which are the effects people buy this for, A polished easing curve editor with presets that look intentional instead of linear. Read the honest list above before committing.
How much does SVGator cost?
SVGator costs about $24/month (Pro, checked 2026-08-18), which is $288 per year.
What do I lose by replacing SVGator?
Honestly: Path morphing, stroke draw-on, and motion-along-path, which are the effects people buy this for; A polished easing curve editor with presets that look intentional instead of linear; Export targets beyond CSS: SMIL, JS runtime, Lottie, video; Cross-browser export sanity checks and the fixes for Safari's transform-origin quirks; Built-in shape and text editing, so every asset change means a round trip to your vector editor. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to SVGator?
No mature open-source alternative worth pointing at, which is exactly why the one-shot prompt on this page exists.