Class: AnimateIt::Composition
- Inherits:
-
Object
- Object
- AnimateIt::Composition
- 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
-
.duration_in_frames ⇒ Object
readonly
Returns the value of attribute duration_in_frames.
-
.height ⇒ Object
readonly
Returns the value of attribute height.
-
.props_schema ⇒ Object
readonly
Returns the value of attribute props_schema.
-
.timeline ⇒ Object
readonly
Returns the value of attribute timeline.
-
.width ⇒ Object
readonly
Returns the value of attribute width.
Class Method Summary collapse
-
.assets_dir(value = nil) ⇒ Object
----- Outputs path helpers -------------------------------------- Declare a directory + basename so `outputs do ...
-
.audio(path, duration: nil, from: 0, start_at: nil, name: nil, track: :audio, gain: 1.0, loop: false) ⇒ Object
Place an audio segment on its own track.
-
.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).
-
.auto_mount_single_scene! ⇒ Object
Sweep nested AnimateIt::Scene subclasses and auto-mount the single scene if no explicit
scene/serieswas declared. -
.beat(name, at:, length:) ⇒ Object
----- Beats: named time markers on the composition timeline ------.
- .beats ⇒ Object
-
.client_driven! ⇒ Object
Opt in to the seekable browser runtime.
- .client_driven? ⇒ Boolean
- .duration(value = nil) ⇒ Object
- .fps(value = nil) ⇒ Object
- .frame_context(frame:, props:, segment: nil) ⇒ Object
- .id(value = nil) ⇒ Object
- .inherited(subclass) ⇒ Object
- .output_basename(value = nil) ⇒ Object
- .output_format(value = nil) ⇒ Object
-
.outputs(&block) ⇒ Object
Declarative outputs: list every (format, path) target this composition should produce.
- .props(&block) ⇒ Object
-
.public_player!(autoplay: false, loop: true) ⇒ Object
Explicitly expose this composition through the production-safe public player endpoint.
- .public_player? ⇒ Boolean
- .public_player_options ⇒ Object
- .render_frame(view_context, frame:, props: {}, segment_origins: false) ⇒ Object
- .render_structure(view_context, props: {}) ⇒ Object
- .scene(scene_class, duration: nil, from: 0, start_at: nil, name: nil) ⇒ Object
- .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
- .series ⇒ Object
- .size(width, height) ⇒ Object
- .structure_epochs(*frames) ⇒ Object
- .structure_layers ⇒ Object
- .track_document(props: {}) ⇒ Object
- .transition_series ⇒ Object
- .transparent? ⇒ Boolean
- .verification_props(*variants) ⇒ Object
-
.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).
-
.zoom(value = nil) ⇒ Object
Browser-side magnification applied via CSS
zoomon<body>in the frame layout.
Class Attribute Details
.duration_in_frames ⇒ Object (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 |
.height ⇒ Object (readonly)
Returns the value of attribute height.
6 7 8 |
# File 'lib/animate_it/composition.rb', line 6 def height @height end |
.props_schema ⇒ Object (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 |
.timeline ⇒ Object (readonly)
Returns the value of attribute timeline.
6 7 8 |
# File 'lib/animate_it/composition.rb', line 6 def timeline @timeline end |
.width ⇒ Object (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
207 208 209 210 |
# File 'lib/animate_it/composition.rb', line 207 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, loop: false) ⇒ Object
Place an audio segment on its own track. The renderer doesn't draw audio; the studio plays it via
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 |
# File 'lib/animate_it/composition.rb', line 296 def audio(path, duration: nil, from: 0, start_at: nil, name: nil, track: :audio, gain: 1.0, loop: false) begin gain = Float(gain) rescue TypeError, ArgumentError raise ArgumentError, "Audio gain must be a number between 0.0 and 1.0" end raise ArgumentError, "Audio gain must be between 0.0 and 1.0" unless gain.between?(0.0, 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:, loop: }, 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.
219 220 221 222 223 224 225 226 227 228 229 230 |
# File 'lib/animate_it/composition.rb', line 219 def audio_loop(path, duration: nil, from: 0, name: nil, gain: 1.0, track: :background) length = duration || (@duration_in_frames - Units.frames(from, fps: fps)) audio( path, duration: length, from:, name: name || "loop-#{File.basename(path.to_s)}", track:, gain:, loop: true ) 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.
282 283 284 285 286 287 288 289 290 291 |
# File 'lib/animate_it/composition.rb', line 282 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 ------
186 187 188 |
# File 'lib/animate_it/composition.rb', line 186 def beat(name, at:, length:) beats.add(name, at: at, length: length) end |
.beats ⇒ Object
190 191 192 |
# File 'lib/animate_it/composition.rb', line 190 def beats @beats ||= Beats.new(fps: fps) end |
.client_driven! ⇒ Object
Opt in to the seekable browser runtime. Legacy compositions continue rendering individual frames on the server.
72 73 74 |
# File 'lib/animate_it/composition.rb', line 72 def client_driven! @client_driven = true end |
.client_driven? ⇒ Boolean
76 77 78 |
# File 'lib/animate_it/composition.rb', line 76 def client_driven? @client_driven == true end |
.duration(value = nil) ⇒ Object
174 175 176 177 178 |
# File 'lib/animate_it/composition.rb', line 174 def duration(value = nil) return @duration_in_frames if value.nil? @duration_in_frames = Units.frames(value, fps:) end |
.fps(value = nil) ⇒ Object
145 146 147 148 149 |
# File 'lib/animate_it/composition.rb', line 145 def fps(value = nil) return @fps if value.nil? @fps = value.to_i end |
.frame_context(frame:, props:, segment: nil) ⇒ Object
323 324 325 326 327 328 329 330 331 332 333 334 335 |
# File 'lib/animate_it/composition.rb', line 323 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
63 64 65 66 67 68 |
# File 'lib/animate_it/composition.rb', line 63 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 19 20 |
# 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) subclass.instance_variable_set(:@verification_props, [{}].freeze) subclass.instance_variable_set(:@public_player_options, nil) super end |
.output_basename(value = nil) ⇒ Object
212 213 214 215 |
# File 'lib/animate_it/composition.rb', line 212 def output_basename(value = nil) @output_basename = value.to_s if value @output_basename || @id end |
.output_format(value = nil) ⇒ Object
22 23 24 25 26 27 28 29 30 31 |
# File 'lib/animate_it/composition.rb', line 22 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.
48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/animate_it/composition.rb', line 48 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
180 181 182 183 |
# File 'lib/animate_it/composition.rb', line 180 def props(&block) props_schema.instance_eval(&block) if block props_schema end |
.public_player!(autoplay: false, loop: true) ⇒ Object
Explicitly expose this composition through the production-safe public player endpoint. Studio, frame, filmstrip, props, and render endpoints remain local-only. Public playback always uses schema-default props.
83 84 85 86 |
# File 'lib/animate_it/composition.rb', line 83 def public_player!(autoplay: false, loop: true) client_driven! @public_player_options = { autoplay: autoplay == true, loop: loop == true }.freeze end |
.public_player? ⇒ Boolean
88 89 90 |
# File 'lib/animate_it/composition.rb', line 88 def public_player? @public_player_options.present? end |
.public_player_options ⇒ Object
92 93 94 |
# File 'lib/animate_it/composition.rb', line 92 def @public_player_options || { autoplay: false, loop: true }.freeze end |
.render_frame(view_context, frame:, props: {}, segment_origins: false) ⇒ Object
337 338 339 340 341 342 343 344 345 346 347 348 349 350 |
# File 'lib/animate_it/composition.rb', line 337 def render_frame(view_context, frame:, props: {}, segment_origins: false) 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:) content = render_segment(view_context, segment, context) next content unless segment_origins view_context.tag.div(content, data: { animate_it_segment_origin: segment.from_frame }) end view_context.safe_join(rendered_segments) end |
.render_structure(view_context, props: {}) ⇒ Object
128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/animate_it/composition.rb', line 128 def render_structure(view_context, props: {}) resolved_props = props_schema.resolve(props) rendered_layers = structure_layers.map do |layer| context = frame_context(frame: layer.from_frame, props: resolved_props, segment: layer.segment) view_context.tag.div( render_segment(view_context, layer.segment, context), class: "animate-it-layer", data: { animate_layer: layer.key } ) end view_context.safe_join(rendered_layers) end |
.scene(scene_class, duration: nil, from: 0, start_at: nil, name: nil) ⇒ Object
268 269 270 271 272 273 274 275 276 277 |
# File 'lib/animate_it/composition.rb', line 268 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
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 |
# File 'lib/animate_it/composition.rb', line 241 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 |
.series ⇒ Object
315 316 317 |
# File 'lib/animate_it/composition.rb', line 315 def series(&) SeriesBuilder.new(self).instance_eval(&) end |
.size(width, height) ⇒ Object
151 152 153 154 |
# File 'lib/animate_it/composition.rb', line 151 def size(width, height) @width = width.to_i @height = height.to_i end |
.structure_epochs(*frames) ⇒ Object
104 105 106 107 108 |
# File 'lib/animate_it/composition.rb', line 104 def structure_epochs(*frames) return @structure_epochs || [] if frames.empty? @structure_epochs = frames.map(&:to_i).sort.uniq end |
.structure_layers ⇒ Object
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/animate_it/composition.rb', line 110 def structure_layers timeline.segments.each_with_index.flat_map do |segment, segment_index| next [] unless segment.kind == :scene segment_end = segment.duration_frames ? segment.from_frame + segment.duration_frames : duration_in_frames epochs = structure_epochs.select { |frame| frame > segment.from_frame && frame < segment_end } bounds = ([segment.from_frame] + epochs).sort.uniq bounds.each_with_index.map do |from_frame, index| Tracks::Layer.new( segment:, segment_index:, from_frame:, to_frame: bounds[index + 1] || segment_end ) end end end |
.track_document(props: {}) ⇒ Object
141 142 143 |
# File 'lib/animate_it/composition.rb', line 141 def track_document(props: {}) Tracks::Recorder.new(self, props:).call end |
.transition_series ⇒ Object
319 320 321 |
# File 'lib/animate_it/composition.rb', line 319 def transition_series(&) TransitionSeriesBuilder.new(self).instance_eval(&) end |
.transparent? ⇒ Boolean
33 34 35 |
# File 'lib/animate_it/composition.rb', line 33 def transparent? %i[webm mov gif png_sequence png].include?(output_format) end |
.verification_props(*variants) ⇒ Object
96 97 98 99 100 101 102 |
# File 'lib/animate_it/composition.rb', line 96 def verification_props(*variants) return @verification_props if variants.empty? raise ArgumentError, "verification_props entries must be hashes" unless variants.all?(Hash) @verification_props = variants.map(&:deep_symbolize_keys).freeze 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.
234 235 236 237 238 239 |
# File 'lib/animate_it/composition.rb', line 234 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.
168 169 170 171 172 |
# File 'lib/animate_it/composition.rb', line 168 def zoom(value = nil) return @zoom if value.nil? @zoom = value.to_f end |