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
-
.chapter(name, beat:, label:, metadata: {}) ⇒ Object
Public, user-navigable moments.
- .chapters ⇒ 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.
- .player_manifest ⇒ Object
- .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
222 223 224 225 |
# File 'lib/animate_it/composition.rb', line 222 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
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 |
# File 'lib/animate_it/composition.rb', line 311 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.
234 235 236 237 238 239 240 241 242 243 244 245 |
# File 'lib/animate_it/composition.rb', line 234 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.
297 298 299 300 301 302 303 304 305 306 |
# File 'lib/animate_it/composition.rb', line 297 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 ------
187 188 189 |
# File 'lib/animate_it/composition.rb', line 187 def beat(name, at:, length:) beats.add(name, at: at, length: length) end |
.beats ⇒ Object
191 192 193 |
# File 'lib/animate_it/composition.rb', line 191 def beats @beats ||= Beats.new(fps: fps) end |
.chapter(name, beat:, label:, metadata: {}) ⇒ Object
Public, user-navigable moments. Chapters reference existing beats so animation timing stays the single source of truth.
197 198 199 |
# File 'lib/animate_it/composition.rb', line 197 def chapter(name, beat:, label:, metadata: {}) chapters.add(name, beat:, label:, metadata:) end |
.chapters ⇒ Object
201 202 203 |
# File 'lib/animate_it/composition.rb', line 201 def chapters @chapters ||= Chapters.new(self) end |
.client_driven! ⇒ Object
Opt in to the seekable browser runtime. Legacy compositions continue rendering individual frames on the server.
73 74 75 |
# File 'lib/animate_it/composition.rb', line 73 def client_driven! @client_driven = true end |
.client_driven? ⇒ Boolean
77 78 79 |
# File 'lib/animate_it/composition.rb', line 77 def client_driven? @client_driven == true end |
.duration(value = nil) ⇒ Object
175 176 177 178 179 |
# File 'lib/animate_it/composition.rb', line 175 def duration(value = nil) return @duration_in_frames if value.nil? @duration_in_frames = Units.frames(value, fps:) end |
.fps(value = nil) ⇒ Object
146 147 148 149 150 |
# File 'lib/animate_it/composition.rb', line 146 def fps(value = nil) return @fps if value.nil? @fps = value.to_i end |
.frame_context(frame:, props:, segment: nil) ⇒ Object
338 339 340 341 342 343 344 345 346 347 348 349 350 |
# File 'lib/animate_it/composition.rb', line 338 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
64 65 66 67 68 69 |
# File 'lib/animate_it/composition.rb', line 64 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 21 |
# 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) subclass.instance_variable_set(:@chapters, Chapters.new(subclass)) super end |
.output_basename(value = nil) ⇒ Object
227 228 229 230 |
# File 'lib/animate_it/composition.rb', line 227 def output_basename(value = nil) @output_basename = value.to_s if value @output_basename || @id end |
.output_format(value = nil) ⇒ Object
23 24 25 26 27 28 29 30 31 32 |
# File 'lib/animate_it/composition.rb', line 23 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.
49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/animate_it/composition.rb', line 49 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 |
.player_manifest ⇒ Object
205 206 207 |
# File 'lib/animate_it/composition.rb', line 205 def player_manifest PlayerManifest.new(self) end |
.props(&block) ⇒ Object
181 182 183 184 |
# File 'lib/animate_it/composition.rb', line 181 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.
84 85 86 87 |
# File 'lib/animate_it/composition.rb', line 84 def public_player!(autoplay: false, loop: true) client_driven! @public_player_options = { autoplay: autoplay == true, loop: loop == true }.freeze end |
.public_player? ⇒ Boolean
89 90 91 |
# File 'lib/animate_it/composition.rb', line 89 def public_player? @public_player_options.present? end |
.public_player_options ⇒ Object
93 94 95 |
# File 'lib/animate_it/composition.rb', line 93 def @public_player_options || { autoplay: false, loop: true }.freeze end |
.render_frame(view_context, frame:, props: {}, segment_origins: false) ⇒ Object
352 353 354 355 356 357 358 359 360 361 362 363 364 365 |
# File 'lib/animate_it/composition.rb', line 352 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
129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/animate_it/composition.rb', line 129 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
283 284 285 286 287 288 289 290 291 292 |
# File 'lib/animate_it/composition.rb', line 283 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
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'lib/animate_it/composition.rb', line 256 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
330 331 332 |
# File 'lib/animate_it/composition.rb', line 330 def series(&) SeriesBuilder.new(self).instance_eval(&) end |
.size(width, height) ⇒ Object
152 153 154 155 |
# File 'lib/animate_it/composition.rb', line 152 def size(width, height) @width = width.to_i @height = height.to_i end |
.structure_epochs(*frames) ⇒ Object
105 106 107 108 109 |
# File 'lib/animate_it/composition.rb', line 105 def structure_epochs(*frames) return @structure_epochs || [] if frames.empty? @structure_epochs = frames.map(&:to_i).sort.uniq end |
.structure_layers ⇒ Object
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/animate_it/composition.rb', line 111 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
142 143 144 |
# File 'lib/animate_it/composition.rb', line 142 def track_document(props: {}) Tracks::Recorder.new(self, props:).call end |
.transition_series ⇒ Object
334 335 336 |
# File 'lib/animate_it/composition.rb', line 334 def transition_series(&) TransitionSeriesBuilder.new(self).instance_eval(&) end |
.transparent? ⇒ Boolean
34 35 36 |
# File 'lib/animate_it/composition.rb', line 34 def transparent? %i[webm mov gif png_sequence png].include?(output_format) end |
.verification_props(*variants) ⇒ Object
97 98 99 100 101 102 103 |
# File 'lib/animate_it/composition.rb', line 97 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.
249 250 251 252 253 254 |
# File 'lib/animate_it/composition.rb', line 249 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.
169 170 171 172 173 |
# File 'lib/animate_it/composition.rb', line 169 def zoom(value = nil) return @zoom if value.nil? @zoom = value.to_f end |