animate_it 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +63 -1
  3. data/README.md +212 -20
  4. data/app/controllers/animate_it/embed_assets_controller.rb +25 -0
  5. data/app/controllers/animate_it/frames_controller.rb +10 -0
  6. data/app/controllers/animate_it/public_players_controller.rb +73 -0
  7. data/app/controllers/animate_it/renders_controller.rb +3 -17
  8. data/app/controllers/animate_it/studio_controller.rb +1 -0
  9. data/app/jobs/animate_it/render_job.rb +2 -0
  10. data/app/views/animate_it/frames/filmstrip.html.haml +37 -6
  11. data/app/views/animate_it/frames/player.html.haml +92 -0
  12. data/app/views/animate_it/studio/_preview_pane.html.haml +1 -1
  13. data/app/views/animate_it/studio/_props_pane.html.haml +3 -2
  14. data/app/views/animate_it/studio/_studio_script.html.haml +71 -48
  15. data/app/views/animate_it/studio/show.html.haml +10 -3
  16. data/config/routes.rb +10 -0
  17. data/lib/animate_it/asset_manifest.rb +92 -0
  18. data/lib/animate_it/asset_renderer.rb +39 -7
  19. data/lib/animate_it/chapter_navigation.rb +160 -0
  20. data/lib/animate_it/chapters.rb +95 -0
  21. data/lib/animate_it/composition.rb +115 -9
  22. data/lib/animate_it/embed_helper.rb +214 -0
  23. data/lib/animate_it/embed_runtime/embed.js +338 -0
  24. data/lib/animate_it/embed_runtime.rb +13 -0
  25. data/lib/animate_it/embed_styles.rb +126 -0
  26. data/lib/animate_it/engine.rb +7 -0
  27. data/lib/animate_it/player_manifest.rb +29 -0
  28. data/lib/animate_it/runtime/runtime.js +518 -0
  29. data/lib/animate_it/runtime.rb +11 -0
  30. data/lib/animate_it/scene.rb +57 -3
  31. data/lib/animate_it/text_effects.rb +104 -0
  32. data/lib/animate_it/track_document_schema.rb +71 -0
  33. data/lib/animate_it/tracks/document.rb +100 -0
  34. data/lib/animate_it/tracks/layer.rb +13 -0
  35. data/lib/animate_it/tracks/recorder.rb +149 -0
  36. data/lib/animate_it/verification.rb +187 -0
  37. data/lib/animate_it/version.rb +1 -1
  38. data/lib/animate_it/video_renderer.rb +89 -26
  39. data/lib/animate_it/view_helpers.rb +11 -1
  40. data/lib/animate_it.rb +17 -0
  41. data/lib/tasks/animate_it_tasks.rake +133 -0
  42. metadata +25 -6
@@ -39,34 +39,49 @@ module AnimateIt
39
39
  @output_path = Pathname(output_path)
40
40
  @frames_dir = Pathname(frames_dir || Rails.root.join("tmp/animate_it/#{composition.id}"))
41
41
  @playwright_cli = playwright_cli || ENV.fetch("PLAYWRIGHT_CLI_EXECUTABLE_PATH", DEFAULT_PLAYWRIGHT_CLI)
42
-
43
- return unless audio_segments.any? && AUDIO_INCAPABLE_FORMATS.include?(@output_format)
44
-
45
- raise Error,
46
- "Composition #{composition.id} declares audio but format=#{@output_format} doesn't support audio. Use :webm/:mp4/:mov."
47
42
  end
48
43
 
49
44
  class CancelledError < AnimateIt::Error; end
50
45
 
51
- def render(frame_range: nil, every_nth_frame: 1, props: {}, on_progress: nil, cancel_check: nil)
46
+ def render(frame_range: nil, every_nth_frame: 1, props: {}, on_progress: nil, cancel_check: nil,
47
+ reuse_captured_frames: false)
52
48
  FileUtils.mkdir_p(frames_dir)
53
49
  FileUtils.mkdir_p(output_path.dirname)
54
50
 
55
51
  frame_list = frames(frame_range:, every_nth_frame:)
56
- capture_status = capture_frames(frame_list, props:, on_progress:, cancel_check:)
52
+ if reuse_captured_frames
53
+ validate_captured_frames!(frame_list.size)
54
+ capture_status = :complete
55
+ else
56
+ clear_captured_frames!
57
+ capture_status = capture_frames(frame_list, props:, on_progress:, cancel_check:)
58
+ end
57
59
 
58
60
  if capture_status == :cancelled || cancel_check&.call
59
61
  frame_count = contiguous_frame_count
60
- encode_video(frame_count:) if frame_count.positive?
62
+ encode_video(frame_count:, start_frame: frame_list.first) if frame_count.positive?
61
63
  raise CancelledError, "Render cancelled"
62
64
  end
63
65
 
64
- encode_video
66
+ encode_video(frame_count: frame_list.size, start_frame: frame_list.first)
65
67
  output_path
66
68
  end
67
69
 
68
70
  private
69
71
 
72
+ def clear_captured_frames!
73
+ Dir.glob(frames_dir.join("frame-*.png")).each { |path| FileUtils.rm_f(path) }
74
+ end
75
+
76
+ def validate_captured_frames!(frame_count)
77
+ missing = frame_count.times.find do |index|
78
+ !frames_dir.join(format("frame-%05d.png", index)).file?
79
+ end
80
+ return unless missing
81
+
82
+ raise Error, "Captured frame not found: #{frames_dir.join(format("frame-%05d.png", missing))}"
83
+ end
84
+
70
85
  def frames(frame_range:, every_nth_frame:)
71
86
  range = frame_range || (0...composition.duration_in_frames)
72
87
  range.step(every_nth_frame).to_a
@@ -87,7 +102,7 @@ module AnimateIt
87
102
  )
88
103
  page = context.new_page
89
104
 
90
- page.goto(filmstrip_url(props:), waitUntil: "networkidle")
105
+ page.goto(page_url(props:), waitUntil: "networkidle")
91
106
  page.wait_for_function('document.documentElement.dataset.animateItReady === "1"')
92
107
 
93
108
  frame_list.each_with_index do |frame, index|
@@ -110,15 +125,19 @@ module AnimateIt
110
125
  cancelled || cancel_check&.call ? :cancelled : :complete
111
126
  end
112
127
 
113
- def filmstrip_url(props:)
128
+ def page_url(props:)
114
129
  query = { pp: "disable" }
115
130
  query[:props_json] = JSON.generate(props) if props.present?
131
+ endpoint = composition.client_driven? ? "player" : "filmstrip"
116
132
 
117
- "#{host}#{AnimateIt.config.mount_path}/compositions/#{composition.id}/filmstrip?#{URI.encode_www_form(query)}"
133
+ "#{host}#{AnimateIt.config.mount_path}/compositions/#{composition.id}/#{endpoint}?#{URI.encode_www_form(query)}"
118
134
  end
119
135
 
120
- def encode_video(frame_count: nil)
121
- return if output_format == :png_sequence # frames already on disk; nothing to encode
136
+ def encode_video(frame_count: nil, start_frame: 0)
137
+ if output_format == :png_sequence
138
+ publish_png_sequence(frame_count || contiguous_frame_count)
139
+ return
140
+ end
122
141
 
123
142
  if output_format == :png
124
143
  # Single-frame still: copy the captured PNG straight to output_path.
@@ -131,14 +150,24 @@ module AnimateIt
131
150
  command = ["ffmpeg", "-y", "-framerate", composition.fps.to_s,
132
151
  "-i", frames_dir.join("frame-%05d.png").to_s]
133
152
 
134
- audios = audio_segments
135
- audios.each { |seg| command += ["-i", resolve_audio_path!(seg.source[:path])] }
153
+ clip_frame_count = frame_count || composition.duration_in_frames
154
+ audios = audio_capable? ? audio_segments(start_frame:, frame_count: clip_frame_count) : []
155
+ audios.each do |segment|
156
+ command += ["-stream_loop", "-1"] if segment.source[:loop]
157
+ command += ["-i", resolve_audio_path!(segment.source[:path])]
158
+ end
136
159
 
137
160
  command += video_codec_args
138
161
  command += ["-frames:v", frame_count.to_s] if frame_count
139
162
 
140
163
  if audios.any?
141
- command += ["-filter_complex", audio_filter_graph(audios), "-map", "0:v", "-map", "[aout]", "-shortest"]
164
+ command += [
165
+ "-filter_complex",
166
+ audio_filter_graph(audios, start_frame:, frame_count: clip_frame_count),
167
+ "-map", "0:v",
168
+ "-map", "[aout]",
169
+ "-shortest"
170
+ ]
142
171
  command += audio_codec_args
143
172
  else
144
173
  command += ["-an"] # explicit no-audio so output containers like .mov stay clean
@@ -149,6 +178,19 @@ module AnimateIt
149
178
  run!(command)
150
179
  end
151
180
 
181
+ def publish_png_sequence(frame_count)
182
+ FileUtils.mkdir_p(output_path)
183
+ Dir.glob(output_path.join("frame-*.png")).each { |path| FileUtils.rm_f(path) }
184
+
185
+ frame_count.times do |index|
186
+ filename = format("frame-%05d.png", index)
187
+ source = frames_dir.join(filename)
188
+ raise Error, "Captured frame not found: #{source}" unless source.file?
189
+
190
+ FileUtils.cp(source, output_path.join(filename))
191
+ end
192
+ end
193
+
152
194
  def video_codec_args
153
195
  case output_format
154
196
  when :mp4
@@ -174,23 +216,44 @@ module AnimateIt
174
216
  end
175
217
  end
176
218
 
177
- # Build an `adelay=...|...,volume=g[aN]` chain per audio input, then mix.
178
- def audio_filter_graph(audios)
219
+ # Trim each source to its timeline window before delaying and mixing it.
220
+ def audio_filter_graph(audios, start_frame: 0, frame_count: composition.duration_in_frames)
179
221
  ms_per_frame = 1000.0 / composition.fps
180
- legs = audios.each_with_index.map do |seg, i|
181
- delay_ms = (seg.from_frame * ms_per_frame).round
182
- gain = seg.source[:gain] || 1.0
183
- "[#{i + 1}:a]adelay=#{delay_ms}|#{delay_ms},volume=#{gain}[a#{i}]"
222
+ clip_end_frame = start_frame + frame_count
223
+ legs = audios.each_with_index.map do |segment, index|
224
+ segment_end_frame = segment.duration_frames ? segment.from_frame + segment.duration_frames : composition.duration_in_frames
225
+ overlap_start_frame = [segment.from_frame, start_frame].max
226
+ overlap_end_frame = [segment_end_frame, clip_end_frame].min
227
+ source_start_seconds = (overlap_start_frame - segment.from_frame).fdiv(composition.fps)
228
+ overlap_seconds = (overlap_end_frame - overlap_start_frame).fdiv(composition.fps)
229
+ delay_ms = ((overlap_start_frame - start_frame) * ms_per_frame).round
230
+ gain = segment.source[:gain] || 1.0
231
+ "[#{index + 1}:a]atrim=start=#{source_start_seconds}:duration=#{overlap_seconds}," \
232
+ "asetpts=PTS-STARTPTS," \
233
+ "adelay=#{delay_ms}|#{delay_ms},volume=#{gain}[a#{index}]"
184
234
  end
185
235
  # normalize=0: amix's default rescales every input by 1/N, which
186
236
  # silently buries a voice-over under a music bed the moment a second
187
237
  # track is declared. Declared gains are the only intended scaling.
188
- mix = "#{audios.length.times.map { |i| "[a#{i}]" }.join}amix=inputs=#{audios.length}:duration=longest:normalize=0[aout]"
238
+ clip_seconds = frame_count.fdiv(composition.fps)
239
+ mix = "#{audios.length.times.map { |i| "[a#{i}]" }.join}" \
240
+ "amix=inputs=#{audios.length}:duration=longest:normalize=0," \
241
+ "apad=whole_dur=#{clip_seconds},atrim=duration=#{clip_seconds}[aout]"
189
242
  [legs, mix].flatten.join(";")
190
243
  end
191
244
 
192
- def audio_segments
193
- composition.timeline.segments.select { |seg| seg.kind == :audio }
245
+ def audio_segments(start_frame: 0, frame_count: composition.duration_in_frames)
246
+ clip_end_frame = start_frame + frame_count
247
+ composition.timeline.segments.select do |segment|
248
+ next false unless segment.kind == :audio
249
+
250
+ segment_end_frame = segment.duration_frames ? segment.from_frame + segment.duration_frames : composition.duration_in_frames
251
+ segment.from_frame < clip_end_frame && segment_end_frame > start_frame
252
+ end
253
+ end
254
+
255
+ def audio_capable?
256
+ AUDIO_INCAPABLE_FORMATS.exclude?(output_format)
194
257
  end
195
258
 
196
259
  def resolve_audio_path!(path)
@@ -4,9 +4,14 @@ module AnimateIt
4
4
  Style.build(*rules, **properties)
5
5
  end
6
6
 
7
- def absolute_fill(class_name: nil, style: nil, **attributes, &block)
7
+ def absolute_fill(class_name: nil, style: nil, vars: nil, **attributes, &block)
8
8
  content = block.call
9
9
 
10
+ if vars
11
+ style = [style.presence, animation_vars(vars)].compact.join("; ")
12
+ attributes = { data: { animate_vars: vars } }.deep_merge(attributes)
13
+ end
14
+
10
15
  tag.div(
11
16
  **attributes,
12
17
  class: ["animate-it-absolute-fill", class_name].compact,
@@ -16,6 +21,11 @@ module AnimateIt
16
21
  end
17
22
  end
18
23
 
24
+ def animate_text(key, tag_name: :span, **attributes)
25
+ attributes = { data: { animate_text: key } }.deep_merge(attributes)
26
+ tag.public_send(tag_name, evaluate_text_track(key).to_s, **attributes)
27
+ end
28
+
19
29
  def render_template(template, assigns: {}, **)
20
30
  view_context.render(template:, assigns:, layout: false, **)
21
31
  end
data/lib/animate_it.rb CHANGED
@@ -1,3 +1,6 @@
1
+ require "active_support"
2
+ require "active_support/core_ext"
3
+
1
4
  require_relative "animate_it/version"
2
5
  require_relative "animate_it/errors"
3
6
  require_relative "animate_it/frame_duration"
@@ -13,13 +16,27 @@ require_relative "animate_it/timeline"
13
16
  require_relative "animate_it/registry"
14
17
  require_relative "animate_it/render_store"
15
18
  require_relative "animate_it/beats"
19
+ require_relative "animate_it/chapters"
16
20
  require_relative "animate_it/animation"
21
+ require_relative "animate_it/text_effects"
17
22
  require_relative "animate_it/scene"
18
23
  require_relative "animate_it/configuration"
19
24
  require_relative "animate_it/composition"
25
+ require_relative "animate_it/tracks/layer"
26
+ require_relative "animate_it/tracks/document"
27
+ require_relative "animate_it/tracks/recorder"
28
+ require_relative "animate_it/track_document_schema"
29
+ require_relative "animate_it/player_manifest"
30
+ require_relative "animate_it/embed_styles"
31
+ require_relative "animate_it/chapter_navigation"
32
+ require_relative "animate_it/embed_runtime"
33
+ require_relative "animate_it/runtime"
34
+ require_relative "animate_it/embed_helper"
20
35
  require_relative "animate_it/output"
21
36
  require_relative "animate_it/video_renderer"
37
+ require_relative "animate_it/verification"
22
38
  require_relative "animate_it/asset_renderer"
39
+ require_relative "animate_it/asset_manifest"
23
40
  # Controllers (app/controllers/animate_it/*) and AnimateIt::RenderJob
24
41
  # (app/jobs/animate_it/render_job.rb) autoload via Rails — the engine's
25
42
  # `isolate_namespace AnimateIt` wires `app/` paths into the autoloader.
@@ -1,3 +1,5 @@
1
+ require "json"
2
+
1
3
  def animate_it_render_host
2
4
  # Prefer ANIMATE_IT_HOST; fall back to the legacy RAILS_MOTION_HOST, then
3
5
  # RAILS_HOST, then localhost.
@@ -8,6 +10,12 @@ def animate_it_render_host
8
10
  end
9
11
 
10
12
  namespace :animate_it do
13
+ desc "Validate source assets and provenance required by AnimateIt compositions"
14
+ task preflight: :environment do
15
+ result = AnimateIt::AssetManifest.new.validate!
16
+ puts "AnimateIt asset preflight passed (#{result.checked} assets)."
17
+ end
18
+
11
19
  desc <<~DESC
12
20
  Render a single composition's declared outputs to their asset paths.
13
21
 
@@ -52,6 +60,61 @@ namespace :animate_it do
52
60
  end
53
61
  end
54
62
 
63
+ desc <<~DESC
64
+ Pixel-diff a composition's client-driven /player page against the legacy
65
+ /filmstrip. Samples every <step>-th frame (default 10; use 1 for the full
66
+ gate) plus structural layer boundaries and adjacent frames.
67
+
68
+ Requires a running Rails server.
69
+
70
+ Usage:
71
+ bin/rails 'animate_it:verify[<composition-id>]'
72
+ bin/rails 'animate_it:verify[<composition-id>,1]'
73
+ ANIMATE_IT_PROPS_JSON='{"title":"Variant"}' bin/rails 'animate_it:verify[<composition-id>,1]'
74
+ ANIMATE_IT_PROPS_MATRIX_JSON='[{}, {"title":"Variant"}]' bin/rails 'animate_it:verify[<composition-id>,1]'
75
+
76
+ Set ANIMATE_IT_READY_TIMEOUT_MS to override the 30000ms readiness timeout.
77
+ DESC
78
+ task :verify, %i[id step] => :environment do |_task, args|
79
+ raise "Usage: bin/rails 'animate_it:verify[<composition-id>,(<step>)?]'" if args[:id].blank?
80
+
81
+ AnimateIt.load_compositions!
82
+ composition = AnimateIt.registry.fetch(args[:id])
83
+ verify_composition!(
84
+ composition,
85
+ host: animate_it_render_host,
86
+ step: (args[:step] || 10).to_i,
87
+ props_variants: verification_props_variants(composition)
88
+ )
89
+ end
90
+
91
+ desc <<~DESC
92
+ Every-frame verification gate for every client-driven composition and each
93
+ composition's declared `verification_props` variant.
94
+
95
+ Requires a running Rails server.
96
+
97
+ Usage:
98
+ bin/rails animate_it:verify_all
99
+ ANIMATE_IT_PROPS_MATRIX_JSON='[{}, {"title":"Variant"}]' bin/rails animate_it:verify_all
100
+ DESC
101
+ task verify_all: :environment do
102
+ AnimateIt.load_compositions!
103
+ compositions = AnimateIt.compositions.select(&:client_driven?)
104
+ raise "No client-driven AnimateIt compositions are registered" if compositions.empty?
105
+
106
+ compositions.each do |composition|
107
+ verify_composition!(
108
+ composition,
109
+ host: animate_it_render_host,
110
+ step: 1,
111
+ props_variants: verification_props_variants(composition)
112
+ )
113
+ end
114
+
115
+ puts "Verified #{compositions.size} client-driven compositions at every frame."
116
+ end
117
+
55
118
  # Parse `30..120` / `30-120` / `30` (single frame) into a Range. Returns
56
119
  # nil for blank input, meaning "use the whole composition".
57
120
  def parse_frame_range(raw, composition)
@@ -80,4 +143,74 @@ namespace :animate_it do
80
143
  $stdout.flush
81
144
  end
82
145
  end
146
+
147
+ def verification_props_variants(composition)
148
+ matrix_json = ENV.fetch("ANIMATE_IT_PROPS_MATRIX_JSON", nil)
149
+ single_json = ENV.fetch("ANIMATE_IT_PROPS_JSON", nil)
150
+ return parse_props_matrix(matrix_json) if matrix_json.present?
151
+ return [parse_props(single_json, "ANIMATE_IT_PROPS_JSON")] if single_json.present?
152
+
153
+ composition.verification_props
154
+ end
155
+
156
+ def parse_props_matrix(raw)
157
+ variants = JSON.parse(raw)
158
+ valid_matrix = variants.is_a?(Array) && variants.present? && variants.all?(Hash)
159
+ raise "ANIMATE_IT_PROPS_MATRIX_JSON must contain a non-empty JSON array of objects" unless valid_matrix
160
+
161
+ variants.map(&:deep_symbolize_keys)
162
+ rescue JSON::ParserError => e
163
+ raise "ANIMATE_IT_PROPS_MATRIX_JSON must contain a JSON array of objects: #{e.message}"
164
+ end
165
+
166
+ def parse_props(raw, name)
167
+ props = JSON.parse(raw)
168
+ raise "#{name} must contain a JSON object" unless props.is_a?(Hash)
169
+
170
+ props.deep_symbolize_keys
171
+ rescue JSON::ParserError => e
172
+ raise "#{name} must contain a JSON object: #{e.message}"
173
+ end
174
+
175
+ def verify_composition!(composition, host:, step:, props_variants:)
176
+ props_variants.each_with_index do |props, variant_index|
177
+ output_dir = Rails.root.join("tmp/animate_it/verify/#{composition.id}/variant-#{variant_index + 1}")
178
+ verification = AnimateIt::Verification.new(
179
+ composition:,
180
+ host:,
181
+ step:,
182
+ props:,
183
+ output_dir:,
184
+ ready_timeout: ENV.fetch("ANIMATE_IT_READY_TIMEOUT_MS", 30_000).to_i
185
+ )
186
+ puts "Verifying #{composition.id} variant #{variant_index + 1}/#{props_variants.size}: " \
187
+ "#{verification.sample_frames.size} frames (step #{verification.step}, " \
188
+ "RGB threshold #{verification.threshold}dB, alpha threshold #{verification.alpha_threshold}dB)"
189
+
190
+ results = verification.call
191
+ failures = results.reject(&:passed)
192
+ print_verification_results(results)
193
+ if failures.any?
194
+ raise "#{failures.size}/#{results.size} frames below threshold. " \
195
+ "Screenshots in #{verification.output_dir}"
196
+ end
197
+
198
+ puts "All #{results.size} frames match " \
199
+ "(RGB >= #{verification.threshold}dB, alpha >= #{verification.alpha_threshold}dB)."
200
+ end
201
+ end
202
+
203
+ def print_verification_results(results)
204
+ results.each do |result|
205
+ rgb = result.rgb_psnr.infinite? ? "identical" : format("%.2fdB", result.rgb_psnr)
206
+ alpha = result.alpha_psnr.infinite? ? "identical" : format("%.2fdB", result.alpha_psnr)
207
+ puts format(
208
+ " frame %<frame>5d RGB %<rgb>s alpha %<alpha>s%<flag>s",
209
+ frame: result.frame,
210
+ rgb:,
211
+ alpha:,
212
+ flag: result.passed ? "" : " ← FAIL"
213
+ )
214
+ end
215
+ end
83
216
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: animate_it
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - growth-constant
@@ -58,14 +58,17 @@ files:
58
58
  - README.md
59
59
  - app/controllers/animate_it/application_controller.rb
60
60
  - app/controllers/animate_it/audio_controller.rb
61
+ - app/controllers/animate_it/embed_assets_controller.rb
61
62
  - app/controllers/animate_it/frames_controller.rb
62
63
  - app/controllers/animate_it/props_controller.rb
64
+ - app/controllers/animate_it/public_players_controller.rb
63
65
  - app/controllers/animate_it/renders_controller.rb
64
66
  - app/controllers/animate_it/studio_controller.rb
65
67
  - app/jobs/animate_it/application_job.rb
66
68
  - app/jobs/animate_it/render_job.rb
67
69
  - app/views/animate_it/frames/filmstrip.html.haml
68
70
  - app/views/animate_it/frames/fragment.html.haml
71
+ - app/views/animate_it/frames/player.html.haml
69
72
  - app/views/animate_it/frames/show.html.haml
70
73
  - app/views/animate_it/props/_form.html.haml
71
74
  - app/views/animate_it/props/update.html.haml
@@ -85,38 +88,54 @@ files:
85
88
  - lib/animate_it.rb
86
89
  - lib/animate_it/animation.rb
87
90
  - lib/animate_it/animation_helpers.rb
91
+ - lib/animate_it/asset_manifest.rb
88
92
  - lib/animate_it/asset_renderer.rb
89
93
  - lib/animate_it/beats.rb
94
+ - lib/animate_it/chapter_navigation.rb
95
+ - lib/animate_it/chapters.rb
90
96
  - lib/animate_it/composition.rb
91
97
  - lib/animate_it/configuration.rb
92
98
  - lib/animate_it/easing.rb
99
+ - lib/animate_it/embed_helper.rb
100
+ - lib/animate_it/embed_runtime.rb
101
+ - lib/animate_it/embed_runtime/embed.js
102
+ - lib/animate_it/embed_styles.rb
93
103
  - lib/animate_it/engine.rb
94
104
  - lib/animate_it/errors.rb
95
105
  - lib/animate_it/frame_context.rb
96
106
  - lib/animate_it/frame_duration.rb
97
107
  - lib/animate_it/output.rb
108
+ - lib/animate_it/player_manifest.rb
98
109
  - lib/animate_it/props_schema.rb
99
110
  - lib/animate_it/registry.rb
100
111
  - lib/animate_it/render_store.rb
112
+ - lib/animate_it/runtime.rb
113
+ - lib/animate_it/runtime/runtime.js
101
114
  - lib/animate_it/scene.rb
102
115
  - lib/animate_it/style.rb
116
+ - lib/animate_it/text_effects.rb
103
117
  - lib/animate_it/timeline.rb
104
118
  - lib/animate_it/timing.rb
119
+ - lib/animate_it/track_document_schema.rb
120
+ - lib/animate_it/tracks/document.rb
121
+ - lib/animate_it/tracks/layer.rb
122
+ - lib/animate_it/tracks/recorder.rb
105
123
  - lib/animate_it/units.rb
124
+ - lib/animate_it/verification.rb
106
125
  - lib/animate_it/version.rb
107
126
  - lib/animate_it/video_renderer.rb
108
127
  - lib/animate_it/view_helpers.rb
109
128
  - lib/generators/animate_it/install/install_generator.rb
110
129
  - lib/generators/animate_it/install/templates/animate_it.rb
111
130
  - lib/tasks/animate_it_tasks.rake
112
- homepage: https://github.com/growth-constant/animate_it
131
+ homepage: https://github.com/joinbuildit/animate_it
113
132
  licenses:
114
133
  - MIT
115
134
  metadata:
116
- homepage_uri: https://github.com/growth-constant/animate_it
117
- source_code_uri: https://github.com/growth-constant/animate_it
118
- changelog_uri: https://github.com/growth-constant/animate_it/blob/main/CHANGELOG.md
119
- bug_tracker_uri: https://github.com/growth-constant/animate_it/issues
135
+ homepage_uri: https://github.com/joinbuildit/animate_it
136
+ source_code_uri: https://github.com/joinbuildit/animate_it
137
+ changelog_uri: https://github.com/joinbuildit/animate_it/blob/main/CHANGELOG.md
138
+ bug_tracker_uri: https://github.com/joinbuildit/animate_it/issues
120
139
  rubygems_mfa_required: 'true'
121
140
  rdoc_options: []
122
141
  require_paths: