Files
2026-06-07 23:12:46 +02:00

33 KiB

Camera Area / Camera Track Plan

Date: 2026-06-07

Core Rule

The camera is a stage-area framing tool, not the editor viewport.

The normal editor stage must stay unchanged. CanvasStage.vue should keep using editor.canvasZoom, editor.canvasViewX, and editor.canvasViewY only for the workbench view. Moving or resizing the camera must never pan, zoom, crop, or transform the editable stage.

The camera affects only final output surfaces:

  • fullscreen/final preview
  • exported media
  • exported runtime/example HTML playback

In the normal editor view, the camera is only an overlay: a selectable, manipulatable rectangle drawn in stage coordinates.

Terminology

  • Stage: the document canvas coordinate system, currently scene.canvas.width by scene.canvas.height.
  • Editor viewport: the user's zoomed/panned workbench view in CanvasStage.vue.
  • Camera frame: an animatable stage-space rectangle that defines the final visible area.
  • Final preview: fullscreen/runtime preview through FullscreenPreview.vue and src/lib/exportPlayer.ts. This does not mean the existing stagePreview button-mode toggle in CanvasStage.vue.

Current Architecture

Scene And Object Model

Primary types live in src/types/scene.ts.

  • Project is the saved top-level container.
  • Project-wide shared fields include canvas, duration, fps, inTime, outTime, assets, and the clip library.
  • Each ScenePage owns page-specific content: layers, clipInstances, clipTracks, scriptTracks, scriptFrames, markers, groups, trackOrder, backgroundVideo, audioTracks, and audioClips.
  • useSceneStore().scene returns a merged Scene view combining project shared fields with the active page. Many render/export paths consume this merged Scene.

Stage objects are mostly Layer records that point at an Asset by assetId. Layer order is controlled by trackOrder, with groups expanding into child track ids. Clip placements use ClipInstance records on ClipTracks; a ClipInstance has static per-instance x, y, scaleX, scaleY, and rotation.

Object Transforms

Layer transforms are stored in Layer.base plus keyframes:

  • Layer.base: AnimatableProps
  • Layer.keyframes: Keyframe[]
  • optional Layer.positionKeyframes
  • optional Layer.rotationKeyframes
  • Layer.separated switches position/rotation to those separated tracks

AnimatableProps currently includes transform fields (x, y, scaleX, scaleY, rotation, opacity) and visual fields such as fill, stroke, blur, shadow, polygon points, kaleido pivot, and so on.

Timeline And Keyframes

src/components/Timeline.vue builds visible timeline rows from:

  • layer tracks
  • clip tracks
  • groups
  • script tracks
  • audio tracks

Layer rows can be split into main, position, and rotation subtracks. Keyframe selection is held by src/composables/useKfSelection.ts and is currently keyed as layerId:kind:keyframeId, so camera keyframes need either an expanded selection key model or a small camera-specific selection model.

Playback And Interpolation

Playback sampling is centralized for layer content in src/lib/interpolate.ts:

  • sampleLayer(layer, time)
  • sampleKeyframeSet(...)
  • easing: linear, easeIn, easeOut, easeInOut
  • hold keyframes
  • kind: 'end' visibility markers
  • separated position/rotation support
  • wiggle applied after keyframe sampling

The camera should reuse the same easing and hold semantics, but not become a Layer. It is not content and should not be affected by masks, groups, visibility tails, asset rendering, or layer z-order.

Editor Stage Rendering

src/components/CanvasStage.vue uses Konva/vue-konva for the editable stage.

Important stage pieces:

  • viewConfig applies editor zoom/pan.
  • clipGroupConfig applies the user's stage object crop toggle.
  • stackItems samples layers/clips at store.playhead and renders editable Konva nodes.
  • selection and manipulation route through selectedLayerId, selectedLayerIds, selectedClipInstanceId, and the Konva transformer.
  • drawPreview() paints a separate mask-preview canvas using drawSceneFrame().

The camera overlay belongs in CanvasStage.vue inside the zoomed/panned viewConfig group, but outside content rendering. It should not alter viewConfig, clipGroupConfig, stackItems, or object transforms.

Preview And Export Rendering

There are multiple render paths:

Surface Current code Notes
Editable stage CanvasStage.vue Konva tree Editor-only, interactive. Must not render through camera.
Mask preview canvas CanvasStage.vue drawPreview() -> drawSceneFrame() Editor aid. Should not become camera preview unless explicitly changed later.
Fullscreen preview FullscreenPreview.vue -> mountPlayer() -> src/lib/exportPlayer.ts This is final preview and should render through camera.
Still/GIF/APNG stillImageExport.ts, gifExport.ts, apngExport.ts, frameExport.ts -> drawSceneFrame() TypeScript canvas path.
WebM videoExport.ts -> realtime canvas -> drawSceneFrame() TypeScript canvas path plus MediaRecorder.
MP4 server server/server.js -> server/render.html -> Puppeteer screenshot Separate vanilla JS renderer.
MP4 Electron electron/renderWorker.cts -> server/render.html -> capturePage() Same render page as server.
Runtime/example HTML export src/lib/exportPlayer.ts generated runtime Separate generated JS renderer.

Camera work must be mirrored across the TypeScript canvas renderer, generated runtime renderer, and server/render.html, otherwise preview/export parity will diverge.

Data Model Proposal

Use one optional camera per ScenePage in v1. This matches scene-specific framing and avoids making a global project camera that every page must share.

Add camera types in src/types/scene.ts:

export interface CameraProps {
  /** Top-left stage coordinate of the visible rectangle. */
  x: number
  y: number
  width: number
  height: number
}

export interface CameraKeyframe {
  id: string
  time: number
  props: Partial<CameraProps>
  easing: Easing
  hold?: boolean
}

export interface CameraTrack {
  id: string
  name?: string
  enabled: boolean
  base: CameraProps
  keyframes: CameraKeyframe[]
  /** Editor overlay flags only. They do not affect preview/export. */
  overlayVisible?: boolean
  locked?: boolean
}

Add optional fields:

export interface Scene {
  camera?: CameraTrack
}

export interface ScenePage {
  camera?: CameraTrack
}

The store's merged scene() getter should include camera: page.camera. createBlankScenePage() should omit camera by default. A camera is created only when the user enables/adds it.

Why Not A Normal Layer?

Do not store the camera as a Layer.

A normal layer would:

  • require a fake Asset
  • participate in z-order and grouping
  • be affected by masks/editor-hidden/export-hidden behavior
  • collide with layer selection, deletion, duplication, and asset logic
  • risk being exported as visible scene content

The camera should instead be a special page-level track with keyframe semantics similar to a layer, but rendering semantics completely separate from scene content.

Defaults And Backward Compatibility

Missing camera means disabled camera.

Effective default when no camera exists:

{
  enabled: false,
  base: {
    x: 0,
    y: 0,
    width: scene.canvas.width,
    height: scene.canvas.height,
  },
  keyframes: [],
}

When the user first enables the camera, create a CameraTrack with enabled: true and a single keyframe at the current playhead or at 0. Prefer 0 for predictable exports unless the user explicitly adds a keyframe from the timeline.

Old project files remain unchanged:

  • project.camera and page.camera can be missing.
  • parseProject() does not need a hard migration for missing camera data.
  • normalizeProject() should validate existing camera data when present and drop/repair invalid values.
  • serializeProject() can keep FILE_VERSION = 2 unless the team wants a schema-version marker for camera. The feature is optional and additive.

Invalid camera fallback:

  • missing width/height or non-finite values: treat camera as disabled for that render
  • width/height less than 1: clamp to 1
  • missing base: synthesize stage-bounds base
  • malformed keyframe props: ignore only those malformed props

Sampling And Framing Helpers

Add src/lib/camera.ts as the shared TypeScript source of truth:

export function defaultCameraProps(scene: Pick<Scene, 'canvas'>): CameraProps
export function isCameraEnabled(scene: Scene): boolean
export function sampleCamera(scene: Scene, time: number): CameraProps
export function cameraAspect(scene: Scene, time?: number): number
export function renderSceneFrameForOutput(
  ctx: CanvasRenderingContext2D,
  scene: Scene,
  time: number,
  images: Record<string, HTMLImageElement>,
  options?: {
    width: number
    height: number
    useCamera?: boolean
    cropStage?: boolean
  },
): void

sampleCamera() should mirror sampleLayer() semantics for numeric props:

  • sort camera keyframes by time
  • merge keyframe props over base
  • apply easing from the later keyframe
  • honor hold
  • clamp width/height to positive values

Do not apply wiggle, layer active ranges, end keyframes, masks, or separated tracks to the camera.

Rendering Strategy

Axis-Aligned Camera Only In V1

Do not support camera rotation in v1.

Rotation is possible later by applying an inverse camera transform, but it touches every render path and has tricky edge cases:

  • output bounds for rotated rectangles
  • shadows and blurs clipped by rotated camera edges
  • MP4 render page parity
  • pointer hit-testing in runtime preview
  • UI handles and resize math

V1 should support x, y, width, and height only. Add a future migration slot for rotation, but do not render or expose it yet.

Camera Transform

For camera-enabled output, render the stage through an inverse camera transform onto the output canvas:

ctx.clearRect(0, 0, outputWidth, outputHeight)
ctx.save()
ctx.scale(outputWidth / camera.width, outputHeight / camera.height)
ctx.translate(-camera.x, -camera.y)
drawSceneFrame(ctx, scene, time, images, { crop: true })
ctx.restore()

This keeps object coordinates unchanged. The camera changes only how stage coordinates map into final pixels.

This approach is preferred over rendering a full-stage bitmap and cropping it because it preserves vector/text/shadow fidelity when the export output is larger than the stage or when the camera zooms into a small area.

Aspect Ratio Policy

V1 should lock the camera aspect ratio to the final output aspect.

Default final output aspect is the current stage aspect:

scene.canvas.width / scene.canvas.height

When camera is enabled:

  • the camera frame starts at the stage bounds
  • resize handles preserve aspect ratio
  • width/height keyframes should preserve the same aspect when created through the UI
  • export target height is derived from the camera/output aspect

If a loaded file contains a mismatched camera aspect, rendering should still be safe. The fallback policy should be:

  • map the sampled camera rectangle to the fixed output frame
  • show a non-blocking warning in camera properties
  • do not mutate the file automatically

This fallback may stretch content. The UI should prevent creating such frames in v1.

Output Size

No-camera behavior remains exactly as today:

  • still/GIF/APNG target size defaults to scene.canvas
  • WebM/MP4 output defaults to scene.canvas
  • fullscreen/example HTML aspect uses scene.canvas

Camera-enabled v1:

  • final output still defaults to scene.canvas.width by scene.canvas.height
  • the camera rect is mapped into that output frame
  • existing projects without enabled camera export identically

Future improvement: extend MP4/WebM/still/GIF/APNG target-size controls so all media formats can explicitly export through the camera at custom dimensions.

Empty Areas And Bounds

Allow camera rectangles outside the stage.

Expected output:

  • pixels inside the stage render normally
  • pixels outside the stage are transparent for alpha-capable paths
  • MP4/JPG and other opaque paths flatten empty pixels using the current opaque export behavior, effectively white in the MP4 render windows today

Do not clamp camera x/y to the stage. Animators may intentionally pan past the canvas edge.

Shadows, Masks, Effects, Clips, Groups

The camera should be applied after normal scene compositing.

Implications:

  • masks keep their existing behavior inside the stage render
  • groups and track order remain unchanged
  • shadows and blurs may be clipped by the camera edge, just as they are currently clipped by the stage edge
  • clip instances remain in stage coordinates and are only framed by the camera
  • nested button/SVG assets render normally before camera framing

Do not make the camera a mask layer. It is a final output transform.

Render Path Changes

TypeScript canvas paths:

  • src/lib/render.ts: keep drawSceneFrame() as the full-stage renderer.
  • src/lib/camera.ts: add the output-camera wrapper.
  • src/lib/frameExport.ts: call the wrapper when camera is enabled.
  • src/lib/stillImageExport.ts: same wrapper for snapshots.
  • src/lib/videoExport.ts: create the recording canvas at output size and draw through the wrapper each animation frame.

Fullscreen/runtime paths:

  • src/components/FullscreenPreview.vue: compute aspect ratio from the camera output aspect when camera is enabled.
  • src/lib/exportPlayer.ts: mirror camera sampling and camera transform in the generated runtime.
  • generateExampleHtml() should use camera aspect for the .stage aspect-ratio when the exported payload has an enabled camera.
  • Runtime button hit-testing must use the post-camera draw matrix. The existing buttonInstancesInFrame matrix capture should work if the camera transform is applied before drawing layers, but this needs an explicit test.

MP4 paths:

  • server/render.html: mirror the camera sampling/transform.
  • server/server.js: keep no-camera capture exactly as today. With camera enabled, capture the render page's full output canvas after the render page has mapped the camera into it.
  • electron/renderWorker.cts: same as server, because it loads server/render.html.
  • shared/renderValidation.cjs: include camera keyframes in keyframe count limits and validate camera width/height if camera is enabled.

Editor Interaction Design

Modes

V1 needs three practical modes:

  1. Normal stage editing

    • full stage visible
    • all objects editable as today
    • camera frame appears as an overlay when enabled and overlay is visible
    • editor zoom/pan/crop controls unchanged
  2. Camera frame editing

    • selecting the camera frame selects the camera, not objects behind it
    • moving/resizing the frame upserts camera keyframes at the playhead
    • object positions and object keyframes are untouched
  3. Preview through camera

    • fullscreen/final preview renders only the camera result
    • normal stage stays unchanged after exiting preview

Do not make the main editor stage become a camera viewport in v1.

Store State

Add small editor selection state in src/stores/scene.ts:

selectedCamera: boolean

or a slightly more future-proof:

selectedSpecialTrack: 'camera' | null

Update useDeselectAll() to clear it. Selecting a layer or clip instance should clear camera selection, and selecting the camera should clear layer and clip selection.

Canvas Overlay

Add camera overlay rendering to CanvasStage.vue:

  • draw the frame after stage content and canvas border
  • place it inside the zoomed viewConfig group so it follows the same stage coordinates as objects
  • do not place it inside the cropped content group, so the frame/handles remain visible even when it extends outside the stage
  • use stroke-only hit testing with a generous hitStrokeWidth
  • keep the frame interior non-listening so objects behind the camera remain easy to select
  • show resize handles when selected and unlocked
  • use screen-stable stroke width/handle size via 1 / zoom

Suggested visual treatment:

  • cyan/blue frame stroke
  • dashed outside-frame guide or subtle corner ticks
  • small label only on hover/selection if needed
  • no filled overlay by default

Timeline

Add a special camera row above normal content tracks in Timeline.vue.

V1 behavior:

  • one row named Camera
  • only appears after camera is enabled, or appears as a disabled/add row in the timeline header
  • keyframes show as camera-specific diamonds
  • double-click lane adds a camera keyframe
  • drag keyframes with the existing snap system
  • context menu supports easing, hold, duplicate, delete
  • lock/hide controls apply only to editing/overlay visibility, not render output

Do not include the camera in trackOrder. It is not stack content and should not be groupable, draggable into groups, masked, hidden as content, duplicated as a layer, or converted into a clip.

Properties Panel

Add a selectedCamera branch in PropertiesPanel.vue:

  • enabled toggle
  • overlay visible toggle
  • locked toggle
  • X/Y
  • Width/Height
  • aspect lock indicator
  • keyframe status at playhead
  • remove camera keyframe at playhead

Keep camera properties separate from layer properties. Do not show fill, stroke, mask, shadow, effects, or delete-layer actions for the camera.

UX Behavior

When camera is enabled:

  • the editor still shows the full stage
  • the camera frame is visible unless overlayVisible === false
  • dragging the camera frame feels like moving a crop/framing rectangle
  • objects behind the camera do not move
  • object selection remains predictable because only the frame stroke/handles intercept pointer events

Selection priority:

  • camera handles win when directly hit
  • camera frame stroke wins when directly hit
  • clicks inside the camera frame pass through to objects
  • empty-stage clicks clear camera/layer/clip/keyframe selection

Lock behavior:

  • locked camera cannot be dragged, resized, or keyframed from the stage
  • locked camera can still affect preview/export if enabled
  • locked camera overlay may remain visible as a guide

Hide behavior:

  • hiding the overlay hides only the editor guide
  • hidden overlay does not disable the camera
  • disabled camera does not affect preview/export

Export Behavior

Exports should evaluate camera state at the same timestamp used to evaluate the scene frame.

For each output frame:

  1. Determine render time using the existing playback range and fps logic.
  2. Sample scene objects normally.
  3. Sample camera at the same time.
  4. If camera is enabled and valid, draw scene content through the camera transform.
  5. If camera is missing/disabled, run the current render path exactly as today.

Old exports remain unchanged because missing/disabled camera bypasses the new wrapper.

Format Notes

  • PNG/JPG still: snapshot current playhead through camera if enabled.
  • GIF/APNG: use camera for every frame in renderFrames().
  • WebM: realtime MediaRecorder canvas should be the final output frame, with every animation frame drawn through camera.
  • MP4 server/Electron: server/render.html must render through camera so both Puppeteer screenshot and Electron capturePage() capture the same pixels.
  • Runtime/example HTML: generated playback should use camera. Raw .ruw, project JS, and runtime JS exports preserve camera data rather than baking pixels.

Target Size And MP4

Current MP4 export accepts fps but not target width/height from the export dialog. Do not add unrelated MP4 size controls in the first camera patch unless needed.

With v1 camera enabled, MP4 output can remain scene.canvas.width by scene.canvas.height, with the sampled camera rect mapped into that frame.

Backward Compatibility And Project Safety

Required guarantees:

  • old project files load without migration prompts
  • missing camera means disabled camera
  • no-camera render output is byte/pixel-equivalent where practical
  • editor viewport state remains editor-only
  • camera movement never mutates Layer, ClipInstance, or asset coordinates
  • hidden/locked camera editor flags do not accidentally strip render output

Implementation safeguards:

  • normalize camera data in normalizeProject() and normalizeScenePage()
  • never synthesize a saved camera unless the user enables it
  • keep default camera stage-bounds and disabled
  • include camera in duplicateScene() with fresh camera/keyframe ids
  • include camera in undo/redo snapshots naturally through Project
  • update .ruw packing indirectly by storing camera in project JSON

Implementation Phases

Phase 1: Camera Helpers And Data Shape

Files likely touched:

  • src/types/scene.ts
  • src/stores/scene.ts
  • src/lib/camera.ts
  • src/devtools/tests.ts

Changes:

  • add CameraProps, CameraKeyframe, CameraTrack
  • add optional camera to Scene and ScenePage
  • include camera in the store's merged scene() getter
  • add createDefaultCameraTrack(scene, enabled?)
  • add sampleCamera(), defaultCameraProps(), validation helpers
  • normalize existing camera payloads defensively

Risks:

  • accidentally saving default disabled cameras into every old project
  • duplicating scenes without fresh camera keyframe ids

Acceptance:

  • old project with no camera loads and serializes without behavior changes
  • enabling camera creates a valid full-stage frame
  • sampleCamera() interpolates x/y/width/height with easing and hold

Phase 2: Output Render Wrapper Proof Of Concept

Files likely touched:

  • src/lib/camera.ts
  • src/lib/frameExport.ts
  • src/lib/stillImageExport.ts
  • src/devtools/tests.ts

Changes:

  • implement renderSceneFrameForOutput()
  • keep drawSceneFrame() as the full-stage primitive
  • switch still and frame exports to the wrapper
  • no-camera path should preserve existing transform behavior

Risks:

  • clear/clip behavior with camera outside stage
  • mask offscreen buffers with transformed contexts

Acceptance:

  • no-camera render tests pass unchanged
  • camera at full stage matches no-camera pixels
  • camera cropping a quadrant maps that quadrant into the output frame
  • camera outside stage leaves expected empty pixels

Phase 3: Store Actions And Selection

Files likely touched:

  • src/stores/scene.ts
  • src/composables/useDeselectAll.ts
  • src/devtools/tests.ts

Changes:

  • add selected-camera state
  • add actions:
    • enableCamera()
    • disableCamera()
    • selectCamera()
    • setCameraOverlayVisible()
    • setCameraLocked()
    • upsertCameraKeyframe(time, props)
    • removeCameraKeyframeAt(time)
    • removeCameraKeyframe(id)
    • setCameraKeyframeTime(id, time)
    • setCameraKeyframeEasing(id, easing)
    • setCameraKeyframeHold(id, hold)

Risks:

  • overlapping with layer selection and keyframe selection
  • undo snapshots missing editor-only selection clears

Acceptance:

  • selecting camera clears layer and clip selection
  • selecting layer/clip clears camera selection
  • locked camera cannot be edited by camera actions meant for UI gestures
  • camera keyframes can be added, moved, removed, and sampled

Phase 4: Editor Stage Camera Overlay

Files likely touched:

  • src/components/CanvasStage.vue
  • src/i18n/messages.ts
  • src/devtools/tests.ts

Changes:

  • render camera frame overlay
  • add frame drag to update x/y at playhead
  • add resize handles preserving aspect ratio
  • add live camera pose state during drag
  • avoid interior hit interception
  • hide overlay when overlayVisible === false

Risks:

  • camera frame stealing clicks from objects behind it
  • handles resizing the editor viewport by mistake
  • overlay clipped by the stage content crop group

Acceptance:

  • moving camera does not change editor zoom/pan
  • moving camera does not mutate object positions
  • clicks inside frame still select objects
  • frame stroke/handles select and edit camera
  • overlay follows editor zoom/pan visually as a stage-space guide

Phase 5: Camera Timeline Track

Files likely touched:

  • src/components/Timeline.vue
  • src/composables/useKfSelection.ts or a new camera kf selection helper
  • src/i18n/messages.ts
  • src/devtools/tests.ts

Changes:

  • add special Camera row above content tracks
  • add camera keyframe dots and segments
  • support add/move/delete/duplicate/easing/hold
  • include camera keyframe times in timeline snapping
  • do not include camera in trackOrder, groups, layer menus, or clip conversion

Risks:

  • overloading existing layerId keyframe selection with a fake camera id
  • accidentally making camera draggable/groupable as a content track

Acceptance:

  • camera keyframes are distinguishable from object keyframes
  • camera kf drag snaps like object kfs
  • camera row lock blocks edits
  • disabling camera leaves row/keyframes intact but render output bypasses camera

Phase 6: Camera Properties Panel

Files likely touched:

  • src/components/PropertiesPanel.vue
  • src/i18n/messages.ts
  • src/devtools/tests.ts

Changes:

  • add selected-camera branch
  • expose enabled, overlay visible, locked, x/y/width/height
  • show keyframe-at-playhead status
  • remove camera keyframe at playhead
  • warn if loaded camera aspect does not match output aspect

Risks:

  • camera selected but properties panel still showing stale layer properties
  • writing width/height without aspect lock

Acceptance:

  • selected camera shows camera properties only
  • edits create/update camera keyframes at playhead
  • locked camera shows read-only state
  • no layer delete/fill/mask controls appear for camera

Phase 7: Fullscreen Preview And Runtime Export

Files likely touched:

  • src/components/FullscreenPreview.vue
  • src/lib/exportPlayer.ts
  • src/devtools/tests.ts

Changes:

  • compute fullscreen aspect from camera output aspect when enabled
  • mirror camera sampling in generated runtime
  • apply camera transform before drawing scene content
  • make example HTML aspect-ratio use camera aspect when enabled
  • ensure button hit-testing still works through the camera transform

Risks:

  • fullscreen preview diverging from media exports
  • runtime button hit areas offset when camera is not at 0,0
  • scene switches need per-page camera state

Acceptance:

  • fullscreen preview shows cropped/framed camera output
  • normal editor stage remains unchanged before/after preview
  • generated example HTML matches fullscreen preview framing
  • runtime button clicks work when camera is panned/zoomed

Phase 8: WebM And MP4 Integration

Files likely touched:

  • src/lib/videoExport.ts
  • server/render.html
  • server/server.js
  • electron/renderWorker.cts
  • shared/renderValidation.cjs
  • server/render.e2e.test.cjs
  • scripts/verify-*.cjs

Changes:

  • WebM draws through the TypeScript camera wrapper
  • server/render.html mirrors camera sampling and transform
  • MP4 server and Electron continue to capture full output frame
  • validation counts camera keyframes and rejects invalid enabled camera sizes

Risks:

  • MP4 render page drifting from TypeScript renderer
  • Electron hidden-window dimensions and camera output dimensions diverging
  • odd output dimensions still require ffmpeg pad behavior

Acceptance:

  • camera-enabled MP4 server export matches TypeScript still-frame sample within tolerance
  • camera-enabled Electron MP4 export matches server path within tolerance
  • no-camera MP4 still passes existing render harnesses
  • camera keyframes are evaluated per frame, not once at export start

Phase 9: Polish, Documentation, And Cleanup

Files likely touched:

  • src/i18n/messages.ts
  • src/devtools/tests.ts
  • dev_docs/* as needed

Changes:

  • final copy/tooltips
  • help text for camera behavior
  • remove temporary flags/debug code
  • document v1 limitations: one camera, no rotation, aspect locked

Risks:

  • unclear UX around overlay hide vs camera disable

Acceptance:

  • user can discover how to enable, edit, preview, and export through camera
  • no visible in-app text claims the editor stage itself is camera view
  • tests cover old-project no-camera behavior and camera-enabled behavior

Concrete Testing Plan

Unit / Helper Tests

Add in src/devtools/tests.ts or a future pure test file:

  • defaultCameraProps() equals stage bounds.
  • missing camera means disabled.
  • enabled full-stage camera renders same pixels as no camera.
  • sampleCamera() clamps invalid width/height.
  • sampleCamera() interpolates x/y/width/height linearly.
  • sampleCamera() honors easeIn/easeOut/easeInOut.
  • sampleCamera() honors hold keyframes.
  • camera keyframes do not affect sampleLayer().

Project Safety Tests

  • parseProject() loads v1/v2 files without camera.
  • serializeProject() round-trips a project with camera.
  • projectFromLegacyScene() leaves camera missing/disabled.
  • duplicateScene() gives cloned camera/keyframes fresh ids.
  • loadProject() normalizes malformed camera data without crashing.
  • undo/redo snapshots restore camera track changes.

Editor UI Tests

  • enabling camera shows overlay frame.
  • hiding overlay hides only the frame, not camera render output.
  • selecting camera clears layer/clip selection.
  • selecting layer clears camera selection.
  • camera frame interior does not block object selection behind it.
  • camera frame stroke selects camera.
  • dragging camera frame upserts camera x/y at playhead.
  • resizing camera preserves aspect ratio.
  • locked camera cannot be dragged/resized.
  • normal canvasZoom/canvasViewX/canvasViewY are unchanged after camera movement.

Timeline Tests

  • camera row appears when enabled.
  • camera row is not part of trackOrder.
  • camera row cannot be grouped, duplicated as a layer, or converted to a clip.
  • double-click camera lane adds a camera keyframe.
  • camera keyframe drag snaps to playhead/object keyframes.
  • camera keyframe context menu applies easing/hold/delete/duplicate.
  • disabling camera keeps keyframes but bypasses render framing.

Render And Export Tests

  • no-camera still/GIF/APNG/WebM use old full-stage output.
  • full-stage camera output equals no-camera output.
  • panned camera crops expected pixels.
  • zoomed camera scales expected pixels into full output.
  • camera outside stage leaves transparent/empty pixels.
  • camera keyframes change crop over time in animated exports.
  • fullscreen preview matches still export at the same time.
  • generated example HTML uses camera aspect/framing.
  • WebM recording canvas dimensions stay fixed while camera moves.
  • MP4 server render samples camera per frame.
  • Electron MP4 render samples camera per frame.
  • camera keyframes are included in render validation limits.

Edge Cases

  • camera width/height near 1.
  • camera exactly stage bounds.
  • camera partially outside all four stage edges.
  • object shadow crossing camera edge.
  • masks inside camera.
  • top-level masks and grouped masks.
  • clip instances inside and outside camera.
  • button hit-testing through camera in runtime preview.
  • background video scenes, preserving current limitations/behavior.
  • odd output dimensions still encode through MP4 pad filter.

Risks And Mitigations

  • Preview/export diverge from editor behavior: keep editor stage unchanged and add explicit fullscreen/export tests. The editor is a guide; output surfaces own camera rendering.
  • Render paths diverge from each other: implement camera once in src/lib/camera.ts, then mirror the smallest possible helper in exportPlayer.ts and server/render.html. Add parity tests.
  • Camera frame interferes with object selection: make only stroke/handles listening; keep the interior pass-through.
  • Hit-testing ambiguity: camera selection has priority only on direct frame hits; timeline/properties provide non-spatial selection fallback.
  • Aspect mismatch: lock aspect in v1; tolerate malformed loaded data without crashing.
  • Shadows/effects clipped unexpectedly: document that camera is a final frame edge, like the current stage edge. Do not expand camera bounds for effects in v1.
  • MP4 render page has separate logic: update server/render.html in the same phase as TypeScript render wrapper and add server/Electron capture tests.
  • Schema breakage: optional per-page camera; missing camera is disabled; no eager migration required.
  • Over-refactoring: do not replace Layer, trackOrder, or Konva stage architecture. Add a special camera path only where final framing is needed.

Open Questions

  1. Should camera be per scene page only, or should there later be an option to share one project-wide camera across all pages?
  2. Should MP4/WebM get target-size controls now, or should v1 keep current scene-canvas output dimensions and only change framing?
  3. Should the UI expose arbitrary camera aspect ratios later, and if so should export dimensions follow camera aspect or preserve an independent output aspect?
  4. Should camera overlay visibility be saved per project, or should it behave like an editor preference only?
  5. Should rotation become v2, and should it rotate the camera frame itself or only add a rendered output transform?

Ship the smallest robust version:

  • one optional camera per scene page
  • x/y/width/height only
  • aspect locked to output/stage aspect
  • no rotation
  • normal editor stage unchanged
  • camera overlay in editor
  • camera row in timeline
  • camera properties branch
  • fullscreen preview and all media/runtime exports render through camera
  • old projects and no-camera exports unchanged

This keeps the feature local and preserves the core rule: the camera frames the final output, not the editor viewport.