annie.pages.timeline ==================== .. py:module:: annie.pages.timeline .. autoapi-nested-parse:: The Event-annotation timeline — an inline-SVG track/event surface (UI). A video-editor-style timeline: a time ruler, a vertical playhead across every lane, and one lane per track holding its event boxes. The surface is always the **full width of its container**; zooming does not widen it — it narrows the *time window* shown, so the ruler grows denser and boundaries are easier to place. The visible window is ``[view_start_frame, view_end_frame]`` (the task keeps it centred on the playhead); frames map to x within that window, and the gesture x-fraction maps back through the same window. Within one track, events that overlap in time are **stacked into sub-rows** rather than drawn on top of each other, so every event stays clickable; a lane's height grows with how many events are concurrent at once (see :func:`pack_lanes`). **Horizontal geometry is in percent.** The SVG has no ``viewBox``, so its user units are real pixels, while every x position and width is written as a percentage of the element's width (``x="12.5%"``). The server therefore never needs to know the browser's pixel width, nothing is scaled, and text is never stretched — the failure mode of a fixed ``viewBox`` squeezed to the container with ``preserveAspectRatio="none"``. Vertical measurements (lane heights, font size) are plain pixels. **Where the logic lives.** Python owns the model and renders the SVG *markup* — this keeps the geometry testable (:func:`build_svg`, :func:`pack_lanes`, both pure) and mirrors how the segment-review layout keeps its logic server-side. JavaScript does two things only: it reports pointer gestures back (carrying the track and an x fraction of the *window*) so Python can convert them to frames, and it moves the single playhead line during playback without a full re-render (see :func:`annie.pages.videoclock.set_playhead_x`). Attributes ---------- .. autoapisummary:: annie.pages.timeline.ROW_HEIGHT annie.pages.timeline.LANE_PAD annie.pages.timeline.RULER_HEIGHT annie.pages.timeline.LANE_GAP annie.pages.timeline.LANE_LABEL_HEIGHT annie.pages.timeline.REFERENCE_WIDTH annie.pages.timeline.MIN_BOX_PERCENT annie.pages.timeline.FONT_SIZE annie.pages.timeline.EVENT_RADIUS Classes ------- .. autoapisummary:: annie.pages.timeline.TimelineEvent annie.pages.timeline.TimelineTrack Functions --------- .. autoapisummary:: annie.pages.timeline.pack_lanes annie.pages.timeline.build_svg annie.pages.timeline.gesture_script annie.pages.timeline.scrollbar_html annie.pages.timeline.scrollbar_script Module Contents --------------- .. py:data:: ROW_HEIGHT :value: 22 .. py:data:: LANE_PAD :value: 6 .. py:data:: RULER_HEIGHT :value: 22 .. py:data:: LANE_GAP :value: 8 .. py:data:: LANE_LABEL_HEIGHT :value: 16 .. py:data:: REFERENCE_WIDTH :value: 1300 .. py:data:: MIN_BOX_PERCENT :value: 0.25 .. py:data:: FONT_SIZE :value: 12 .. py:data:: EVENT_RADIUS :value: 4 .. py:class:: TimelineEvent One event to draw on a lane. .. attribute:: event_id The event's stable id (echoed back on click, to select it). .. attribute:: start_frame Inclusive start frame. .. attribute:: end_frame Inclusive end frame. .. attribute:: label Text drawn inside the box (may be empty). .. attribute:: selected Whether to draw this box in the selected style. .. attribute:: color Per-event fill colour override (hex), or ``None`` to use the lane's colour. .. py:class:: TimelineTrack One lane: a track name and the events on it. .. attribute:: name The track (category) name, shown as the lane label. .. attribute:: events The events to draw, in any order. .. attribute:: color Optional lane colour override (hex); ``None`` uses the palette by lane order. .. py:function:: pack_lanes(events: collections.abc.Sequence[TimelineEvent]) -> list[int] Assign each event a sub-row so overlapping events never share one. A greedy interval-partitioning pass: events are considered in start-frame order and each is placed on the lowest sub-row whose last event has already ended. Two events overlap when one starts at or before the other ends (inclusive frames), so touching intervals are treated as overlapping and separated — clearer for the reviewer than a 1px seam. :param events: The events on one track, in any order. :returns: A list of sub-row indices, parallel to ``events`` in their *original* order. .. py:function:: build_svg(tracks: collections.abc.Sequence[TimelineTrack], *, view_start: int, view_end: int, fps: float, svg_id: str, pending_start: int | None = None, pending_end: int | None = None) -> str Render the timeline as an inline-SVG string for the visible frame window. Pure and deterministic — no NiceGUI, no DOM — so the geometry (tick placement, event box extents, lane stacking) is unit-testable. :param tracks: The lanes to draw, top to bottom. :param view_start: First frame shown at the left edge. :param view_end: Last frame shown at the right edge (``> view_start``). :param fps: Frames per second, for converting the window to ruler seconds. :param svg_id: DOM id for the ```` (the playhead updater and gesture bridge use it). :param pending_start: While the reviewer is mid-capture (start marked, end not yet), the start frame of the growing amber band; ``None`` when not capturing. :param pending_end: The band's right edge — the live playhead frame; defaults to ``pending_start`` (a minimal band) when omitted. Ignored if ``pending_start`` is ``None``. :returns: A complete ``…`` markup string that scales to its container's width. .. py:function:: gesture_script(svg_id: str, event_name: str, *, pannable: bool = False) -> str Return JS that reports a timeline click to the server via ``emitEvent``. Attached once after the SVG is embedded. It listens for pointer down/up on the SVG and emits ``event_name`` with a ``detail`` of ``{kind, x, ...}`` where ``x`` is a ``[0, 1]`` fraction of the *visible window*: * ``kind = "select"`` when an existing event box is clicked (carries ``event_id``); * ``kind = "seek"`` for a click anywhere else, or any drag. The timeline is navigation-only — it never creates events — so there is no click/drag authoring gesture; boundaries come from the START/END EVENT buttons and the i/o keys. :param svg_id: DOM id of the timeline ````. :param event_name: The custom event name the NiceGUI handler listens for. :param pannable: Whether the timeline is zoomed in. When true, a horizontal wheel/trackpad scroll (or Shift+wheel) pans the window and emits ``kind = "wheel"`` with ``dx`` (a fraction of the window width); the page's own scroll is left alone otherwise. :returns: The JavaScript to run once via ``ui.run_javascript``. .. py:function:: scrollbar_html(scroll_id: str, left: float, width: float) -> str Return the pan scrollbar markup: a track holding a draggable thumb. :param scroll_id: DOM id for the track element. :param left: The thumb's left edge as a ``[0, 1]`` fraction of the track. :param width: The thumb's width as a ``[0, 1]`` fraction of the track. :returns: An HTML string; positions are percentages so it fills any container width. .. py:function:: scrollbar_script(scroll_id: str, event_name: str) -> str Return JS that makes the scrollbar draggable and click-to-centre. Emits ``event_name`` with ``{kind: "pan", x}`` where ``x`` is the thumb's position as a fraction of the pannable range (``0`` = window at the start, ``1`` = at the end), throttled to ~20 Hz while dragging. :param scroll_id: DOM id of the scrollbar track. :param event_name: The custom event name the NiceGUI handler listens for. :returns: The JavaScript to run once via ``ui.run_javascript``.