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) ⇒ 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
- .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
- .render_frame(view_context, frame:, 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
- .transition_series ⇒ Object
- .transparent? ⇒ Boolean
-
.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
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 |
.beats ⇒ Object
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 |
.series ⇒ Object
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_series ⇒ Object
231 232 233 |
# File 'lib/animate_it/composition.rb', line 231 def transition_series(&) TransitionSeriesBuilder.new(self).instance_eval(&) end |
.transparent? ⇒ 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 |