Source code for camtasia.timeline.transitions

"""Track-level transitions between clips."""

from __future__ import annotations

from typing import Any, Iterator

from camtasia.timing import EDIT_RATE
from camtasia.types import TransitionType, _TransitionData


[docs] class Transition: """Wraps a single transition dict from the track's transitions array. Args: data: The raw transition dict from the JSON project. """ def __init__(self, data: dict[str, Any]) -> None: self._data: _TransitionData = data # type: ignore[assignment] @property def name(self) -> str: """Transition type name (e.g. 'FadeThroughBlack').""" return self._data['name'] @property def duration(self) -> int: """Duration in editRate ticks.""" return int(self._data['duration']) @property def duration_seconds(self) -> float: """Duration in seconds.""" return self.duration / EDIT_RATE @property def left_media_id(self) -> int | None: """Clip ID on the left side, or None for fade-in at start.""" return self._data.get('leftMedia') @property def right_media_id(self) -> int | None: """Clip ID on the right side, or None for fade-out at end.""" return self._data.get('rightMedia') @property def attributes(self) -> dict[str, Any]: """Raw attributes dict.""" return self._data.get('attributes', {}) @property def bypassed(self) -> bool: """Whether the transition is bypassed (disabled).""" return bool(self.attributes.get('bypass', False)) @property def color(self) -> tuple[float, float, float]: """Transition color as (red, green, blue) floats.""" attrs = self.attributes return ( attrs.get('Color-red', 0.0), attrs.get('Color-green', 0.0), attrs.get('Color-blue', 0.0), ) def __eq__(self, other: object) -> bool: if not isinstance(other, Transition): return NotImplemented return self._data is other._data def __hash__(self) -> int: return id(self._data) def __repr__(self) -> str: right = self.right_media_id return ( f'Transition(name={self.name!r}, left={self.left_media_id}, ' f'right={right}, duration_s={self.duration_seconds:.2f})' )
[docs] class TransitionList: """Wraps the track-level transitions array. Args: data: The track dict containing a 'transitions' key. """ def __init__(self, data: dict[str, Any]) -> None: self._data = data @property def _transitions(self) -> list[dict[str, Any]]: return self._data.setdefault('transitions', []) # type: ignore[no-any-return] def __len__(self) -> int: return len(self._transitions) def __repr__(self) -> str: return f'TransitionList(count={len(self)})' def __iter__(self) -> Iterator[Transition]: for t in self._transitions: yield Transition(t) def __getitem__(self, index: int) -> Transition: return Transition(self._transitions[index])
[docs] def add( self, name: str, left_clip_id: int, right_clip_id: int | None, duration_ticks: int, **attributes: Any, ) -> Transition: """Add a transition between two clips. Args: name: Transition type name (e.g. 'FadeThroughBlack'). left_clip_id: ID of the clip on the left. right_clip_id: ID of the clip on the right, or None for fade-out. duration_ticks: Duration in editRate ticks. **attributes: Additional transition attributes. Returns: The newly created Transition. """ default_attributes = { 'bypass': False, 'reverse': False, 'trivial': False, 'useAudioPreRoll': True, 'useVisualPreRoll': True, } merged = {**default_attributes, **attributes} record: dict[str, Any] = { 'name': name, 'duration': duration_ticks, 'leftMedia': left_clip_id, 'attributes': merged, } if right_clip_id is not None: record['rightMedia'] = right_clip_id self._transitions.append(record) return Transition(record)
[docs] def add_fade_through_black( self, left_clip_id: int, right_clip_id: int | None, duration_ticks: int, ) -> Transition: """Add a FadeThroughBlack transition with default attributes. Args: left_clip_id: ID of the clip on the left. right_clip_id: ID of the clip on the right, or None for fade-out. duration_ticks: Duration in editRate ticks. Returns: The newly created Transition. """ return self.add( TransitionType.FADE_THROUGH_BLACK, left_clip_id, right_clip_id, duration_ticks, **{ 'Color-blue': 0.0, 'Color-green': 0.0, 'Color-red': 0.0, }, )
@staticmethod def _clip_id(clip: Any) -> int: """Extract clip ID from a BaseClip or plain int.""" return clip.id if hasattr(clip, 'id') else int(clip)
[docs] def add_dissolve( self, left_clip: Any, right_clip: Any, duration_seconds: float = 0.5, ) -> Transition: """Add a dissolve (fade) transition between two clips.""" ticks = int(duration_seconds * EDIT_RATE) return self.add(TransitionType.FADE, self._clip_id(left_clip), self._clip_id(right_clip), ticks)
[docs] def add_fade_to_white( self, left_clip: Any, right_clip: Any, duration_seconds: float = 0.5, ) -> Transition: """Add a fade-through-white transition. .. warning:: The transition name 'FadeThroughColor' is not in the JSON schema (built from sample projects) and may not work in all Camtasia versions. """ ticks = int(duration_seconds * EDIT_RATE) t = self.add('FadeThroughColor', self._clip_id(left_clip), self._clip_id(right_clip), ticks) t._data['attributes']['Color-red'] = 1.0 t._data['attributes']['Color-green'] = 1.0 t._data['attributes']['Color-blue'] = 1.0 return t
[docs] def add_slide( self, left_clip: Any, right_clip: Any, duration_seconds: float = 0.5, *, direction: str = 'left', ) -> Transition: """Add a slide transition. Args: direction: 'left', 'right', 'up', or 'down'. .. warning:: The 'up' and 'down' directions use transition names ('SlideUp', 'SlideDown') not in the JSON schema (built from sample projects) and may not work in all Camtasia versions. """ name_map = { 'left': TransitionType.SLIDE_LEFT, 'right': TransitionType.SLIDE_RIGHT, 'up': 'SlideUp', 'down': 'SlideDown', } if direction not in name_map: raise ValueError(f'Invalid direction {direction!r}. Use: {sorted(name_map)}') ticks = int(duration_seconds * EDIT_RATE) return self.add(name_map[direction], self._clip_id(left_clip), self._clip_id(right_clip), ticks)
[docs] def add_wipe( self, left_clip: Any, right_clip: Any, duration_seconds: float = 0.5, *, direction: str = 'left', ) -> Transition: """Add a wipe transition. Args: direction: 'left', 'right', 'up', or 'down'. .. warning:: Wipe transition names ('WipeLeft', 'WipeRight', 'WipeUp', 'WipeDown') are not in the JSON schema (built from sample projects) and may not work in all Camtasia versions. """ name_map = { 'left': 'WipeLeft', 'right': 'WipeRight', 'up': 'WipeUp', 'down': 'WipeDown', } if direction not in name_map: raise ValueError(f'Invalid direction {direction!r}. Use: {sorted(name_map)}') ticks = int(duration_seconds * EDIT_RATE) return self.add(name_map[direction], self._clip_id(left_clip), self._clip_id(right_clip), ticks)
[docs] def add_card_flip( self, left_clip: Any, right_clip: Any, duration_seconds: float = 0.5, ) -> Transition: """Add a card-flip transition.""" ticks = int(duration_seconds * EDIT_RATE) return self.add(TransitionType.CARD_FLIP, self._clip_id(left_clip), self._clip_id(right_clip), ticks)
[docs] def add_glitch( self, left_clip: Any, right_clip: Any, duration_seconds: float = 0.5, ) -> Transition: """Add a glitch transition.""" ticks = int(duration_seconds * EDIT_RATE) return self.add('Glitch3', self._clip_id(left_clip), self._clip_id(right_clip), ticks)
[docs] def add_linear_blur( self, left_clip: Any, right_clip: Any, duration_seconds: float = 0.5, ) -> Transition: """Add a linear-blur transition.""" ticks = int(duration_seconds * EDIT_RATE) return self.add(TransitionType.LINEAR_BLUR, self._clip_id(left_clip), self._clip_id(right_clip), ticks)
[docs] def add_stretch( self, left_clip: Any, right_clip: Any, duration_seconds: float = 0.5, ) -> Transition: """Add a stretch transition.""" ticks = int(duration_seconds * EDIT_RATE) return self.add(TransitionType.STRETCH, self._clip_id(left_clip), self._clip_id(right_clip), ticks)
[docs] def add_paint_arcs( self, left_clip: Any, right_clip: Any, duration_seconds: float = 0.5, ) -> Transition: """Add a paint-arcs transition.""" ticks = int(duration_seconds * EDIT_RATE) return self.add(TransitionType.PAINT_ARCS, self._clip_id(left_clip), self._clip_id(right_clip), ticks)
[docs] def add_spherical_spin( self, left_clip: Any, right_clip: Any, duration_seconds: float = 0.5, ) -> Transition: """Add a spherical-spin transition.""" ticks = int(duration_seconds * EDIT_RATE) return self.add(TransitionType.SPHERICAL_SPIN, self._clip_id(left_clip), self._clip_id(right_clip), ticks)
[docs] def remove(self, index: int) -> None: """Remove a transition by index. Args: index: Index of the transition to remove. Raises: IndexError: If the index is out of range. """ del self._transitions[index]
[docs] def clear(self) -> None: """Remove all transitions.""" self._data['transitions'] = []