Class: AnimateIt::Composition

Inherits:
Object
  • Object
show all
Defined in:
lib/animate_it/composition.rb

Defined Under Namespace

Classes: SeriesBuilder, TransitionSeriesBuilder

Constant Summary collapse

SUPPORTED_OUTPUT_FORMATS =
%i[webm mp4 mov gif png_sequence png].freeze

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.duration_in_framesObject (readonly)

Returns the value of attribute duration_in_frames.



6
7
8
# File 'lib/animate_it/composition.rb', line 6

def duration_in_frames
  @duration_in_frames
end

.heightObject (readonly)

Returns the value of attribute height.



6
7
8
# File 'lib/animate_it/composition.rb', line 6

def height
  @height
end

.props_schemaObject (readonly)

Returns the value of attribute props_schema.



6
7
8
# File 'lib/animate_it/composition.rb', line 6

def props_schema
  @props_schema
end

.timelineObject (readonly)

Returns the value of attribute timeline.



6
7
8
# File 'lib/animate_it/composition.rb', line 6

def timeline
  @timeline
end

.widthObject (readonly)

Returns the value of attribute width.



6
7
8
# File 'lib/animate_it/composition.rb', line 6

def width
  @width
end

Class Method Details

.assets_dir(value = nil) ⇒ Object

----- Outputs path helpers -------------------------------------- Declare a directory + basename so outputs do ... end entries can be specified by format alone:

assets_dir "app/assets/images/pages/product"
output_basename "screener-hero"

outputs do
mp4
webm
gif
png frame: 0       # → screener-hero-first.png
end


130
131
132
133
# File 'lib/animate_it/composition.rb', line 130

def assets_dir(value = nil)
  @assets_dir = value.to_s if value
  @assets_dir
end

.audio(path, duration: nil, from: 0, start_at: nil, name: nil, track: :audio, gain: 1.0) ⇒ Object

Place an audio segment on its own track. The renderer doesn't draw audio; the studio plays it via



215
216
217
218
219
220
221
222
223
224
225
# File 'lib/animate_it/composition.rb', line 215

def audio(path, duration: nil, from: 0, start_at: nil, name: nil, track: :audio, gain: 1.0)
  timeline.add_segment(
    name: name || File.basename(path.to_s),
    from_frame: Units.frames(start_at || from, fps:),
    duration_frames: Units.frames(duration, fps:),
    kind: :audio,
    track:,
    source: { path: path.to_s, gain: gain.to_f },
    show_in_timeline: true
  )
end

.audio_loop(path, duration: nil, from: 0, name: nil, gain: 1.0, track: :background) ⇒ Object

----- Audio DSL -------------------------------------------------- Loop a track for the full duration (or a slice). Background music.



142
143
144
145
146
147
148
149
# File 'lib/animate_it/composition.rb', line 142

def audio_loop(path, duration: nil, from: 0, name: nil, gain: 1.0, track: :background)
  # Simplest implementation: place the same source once at `from`
  # for the requested length. The renderer's audio mixer handles
  # truncation if the underlying file is longer than the segment.
  length = duration || (@duration_in_frames - Units.frames(from, fps: fps))
  audio(path, duration: length, from: from, name: name || "loop-#{File.basename(path.to_s)}", track: track,
              gain: gain)
end

.auto_mount_single_scene!Object

Sweep nested AnimateIt::Scene subclasses and auto-mount the single scene if no explicit scene / series was declared. Called by the registrar after composition file load.



201
202
203
204
205
206
207
208
209
210
# File 'lib/animate_it/composition.rb', line 201

def auto_mount_single_scene!
  return if timeline.segments.any?

  nested = constants.map { |c| const_get(c) }.select do |const|
    const.is_a?(Class) && const < AnimateIt::Scene
  end
  return unless nested.size == 1

  scene(nested.first)
end

.beat(name, at:, length:) ⇒ Object

----- Beats: named time markers on the composition timeline ------



109
110
111
# File 'lib/animate_it/composition.rb', line 109

def beat(name, at:, length:)
  beats.add(name, at: at, length: length)
end

.beatsObject



113
114
115
# File 'lib/animate_it/composition.rb', line 113

def beats
  @beats ||= Beats.new(fps: fps)
end

.duration(value = nil) ⇒ Object



97
98
99
100
101
# File 'lib/animate_it/composition.rb', line 97

def duration(value = nil)
  return @duration_in_frames if value.nil?

  @duration_in_frames = Units.frames(value, fps:)
end

.fps(value = nil) ⇒ Object



68
69
70
71
72
# File 'lib/animate_it/composition.rb', line 68

def fps(value = nil)
  return @fps if value.nil?

  @fps = value.to_i
end

.frame_context(frame:, props:, segment: nil) ⇒ Object



235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/animate_it/composition.rb', line 235

def frame_context(frame:, props:, segment: nil)
  FrameContext.new(
    self,
    props_schema.resolve(props),
    frame.to_i,
    segment ? segment.local_frame(frame.to_i) : frame.to_i,
    fps,
    duration_in_frames,
    width,
    height,
    segment
  )
end

.id(value = nil) ⇒ Object



61
62
63
64
65
66
# File 'lib/animate_it/composition.rb', line 61

def id(value = nil)
  return @id if value.nil?

  @id = value.to_s
  AnimateIt.register(self)
end

.inherited(subclass) ⇒ Object



8
9
10
11
12
13
14
15
16
17
18
# File 'lib/animate_it/composition.rb', line 8

def inherited(subclass)
  subclass.instance_variable_set(:@timeline, Timeline.new)
  subclass.instance_variable_set(:@props_schema, PropsSchema.new)
  subclass.instance_variable_set(:@fps, 30)
  subclass.instance_variable_set(:@width, 1920)
  subclass.instance_variable_set(:@height, 1080)
  subclass.instance_variable_set(:@zoom, 1.0)
  subclass.instance_variable_set(:@duration_in_frames, 30)
  subclass.instance_variable_set(:@output_format, :webm)
  super
end

.output_basename(value = nil) ⇒ Object



135
136
137
138
# File 'lib/animate_it/composition.rb', line 135

def output_basename(value = nil)
  @output_basename = value.to_s if value
  @output_basename || @id
end

.output_format(value = nil) ⇒ Object



20
21
22
23
24
25
26
27
28
29
# File 'lib/animate_it/composition.rb', line 20

def output_format(value = nil)
  return @output_format if value.nil?

  unless SUPPORTED_OUTPUT_FORMATS.include?(value)
    raise ArgumentError,
          "Unsupported output_format #{value.inspect} — pick :webm, :mp4, :mov, :gif, :png_sequence, or :png"
  end

  @output_format = value
end

.outputs(&block) ⇒ Object

Declarative outputs: list every (format, path) target this composition should produce. Without a block, returns the registered outputs (empty array if none have been declared).

outputs do
mp4 to: "app/assets/images/pages/product/screener-hero.mp4"
gif to: "app/assets/images/pages/product/screener-hero.gif"
png to: "app/assets/images/pages/product/screener-hero-first.png", frame: 0
end

Paths are repo-relative; the renderer joins them onto Rails.root.



46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/animate_it/composition.rb', line 46

def outputs(&block)
  @outputs ||= []
  return @outputs unless block

  builder = OutputsBuilder.new(assets_dir: assets_dir, basename: output_basename)
  builder.instance_eval(&block)
  @outputs = builder.outputs.freeze
  # Keep `output_format` in sync with the first animated output so single-
  # output legacy callers (bin/render_animate_it_video, AnimateIt Studio)
  # see a sensible format without re-declaring it.
  primary = @outputs.find { |o| o.frame.nil? }
  @output_format = primary.format if primary
  @outputs
end

.props(&block) ⇒ Object



103
104
105
106
# File 'lib/animate_it/composition.rb', line 103

def props(&block)
  props_schema.instance_eval(&block) if block
  props_schema
end

.render_frame(view_context, frame:, props: {}) ⇒ Object



249
250
251
252
253
254
255
256
257
258
259
# File 'lib/animate_it/composition.rb', line 249

def render_frame(view_context, frame:, props: {})
  resolved_props = props_schema.resolve(props)
  segments = timeline.active_segments(frame.to_i, kind: :scene)
  return "" if segments.empty?

  rendered_segments = segments.map do |segment|
    context = frame_context(frame:, props: resolved_props, segment:)
    render_segment(view_context, segment, context)
  end
  view_context.safe_join(rendered_segments)
end

.scene(scene_class, duration: nil, from: 0, start_at: nil, name: nil) ⇒ Object



187
188
189
190
191
192
193
194
195
196
# File 'lib/animate_it/composition.rb', line 187

def scene(scene_class, duration: nil, from: 0, start_at: nil, name: nil, **)
  # Tell the scene class which composition it belongs to so its
  # animation property procs can resolve symbolic beat names against
  # the composition's beat registry.
  scene_class.composition_class = self if scene_class.respond_to?(:composition_class=)
  # If duration isn't given, default to the full composition duration
  # (single-scene shorthand support).
  duration ||= @duration_in_frames
  sequence(from:, start_at:, duration:, name:, scene: scene_class, **)
end

.sequence(from: 0, start_at: nil, duration: nil, name: nil, scene: nil, layout: :none, class_name: nil, style: nil, show_in_timeline: true, track: :main, &block) ⇒ Object



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/animate_it/composition.rb', line 160

def sequence(
  from: 0,
  start_at: nil,
  duration: nil,
  name: nil,
  scene: nil,
  layout: :none,
  class_name: nil,
  style: nil,
  show_in_timeline: true,
  track: :main,
  &block
)
  timeline.add_segment(
    name: name || scene&.name || "sequence-#{timeline.segments.size + 1}",
    from_frame: Units.frames(start_at || from, fps:),
    duration_frames: Units.frames(duration, fps:),
    scene_class: scene,
    renderer: block,
    layout:,
    class_name:,
    style:,
    show_in_timeline:,
    track:
  )
end

.seriesObject



227
228
229
# File 'lib/animate_it/composition.rb', line 227

def series(&)
  SeriesBuilder.new(self).instance_eval(&)
end

.size(width, height) ⇒ Object



74
75
76
77
# File 'lib/animate_it/composition.rb', line 74

def size(width, height)
  @width = width.to_i
  @height = height.to_i
end

.transition_seriesObject



231
232
233
# File 'lib/animate_it/composition.rb', line 231

def transition_series(&)
  TransitionSeriesBuilder.new(self).instance_eval(&)
end

.transparent?Boolean

Returns:

  • (Boolean)


31
32
33
# File 'lib/animate_it/composition.rb', line 31

def transparent?
  %i[webm mov gif png_sequence png].include?(output_format)
end

.voice_over(path, at:, duration: nil, name: nil, gain: 1.0, track: :voice) ⇒ Object

Play a voice-over clip at a named beat (or time/frame). The clip's duration defaults to the beat's length.



153
154
155
156
157
158
# File 'lib/animate_it/composition.rb', line 153

def voice_over(path, at:, duration: nil, name: nil, gain: 1.0, track: :voice)
  beat = at.is_a?(Symbol) ? beats.fetch(at) : nil
  from = beat ? beat.start_frame : at
  length = duration || (beat ? beat.duration_frames : 1.second)
  audio(path, duration: length, from: from, name: name || File.basename(path.to_s), track: track, gain: gain)
end

.zoom(value = nil) ⇒ Object

Browser-side magnification applied via CSS zoom on <body> in the frame layout. Multiplies every visual length (font sizes, padding, widths) without changing the output viewport, so heros built against px-sized host partials can lift up the apparent scale without rewriting CSS. Default 1.0 (no zoom).

size 1080, 1170
zoom 1.1

The viewport stays at size(...); only the rendered content inside <body> is magnified. Combine zoom with a smaller size to "zoom in" the apparent scale of the composition at the same aspect ratio.



91
92
93
94
95
# File 'lib/animate_it/composition.rb', line 91

def zoom(value = nil)
  return @zoom if value.nil?

  @zoom = value.to_f
end