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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +63 -1
- data/README.md +212 -20
- data/app/controllers/animate_it/embed_assets_controller.rb +25 -0
- data/app/controllers/animate_it/frames_controller.rb +10 -0
- data/app/controllers/animate_it/public_players_controller.rb +73 -0
- data/app/controllers/animate_it/renders_controller.rb +3 -17
- data/app/controllers/animate_it/studio_controller.rb +1 -0
- data/app/jobs/animate_it/render_job.rb +2 -0
- data/app/views/animate_it/frames/filmstrip.html.haml +37 -6
- data/app/views/animate_it/frames/player.html.haml +92 -0
- data/app/views/animate_it/studio/_preview_pane.html.haml +1 -1
- data/app/views/animate_it/studio/_props_pane.html.haml +3 -2
- data/app/views/animate_it/studio/_studio_script.html.haml +71 -48
- data/app/views/animate_it/studio/show.html.haml +10 -3
- data/config/routes.rb +10 -0
- data/lib/animate_it/asset_manifest.rb +92 -0
- data/lib/animate_it/asset_renderer.rb +39 -7
- data/lib/animate_it/chapter_navigation.rb +160 -0
- data/lib/animate_it/chapters.rb +95 -0
- data/lib/animate_it/composition.rb +115 -9
- data/lib/animate_it/embed_helper.rb +214 -0
- data/lib/animate_it/embed_runtime/embed.js +338 -0
- data/lib/animate_it/embed_runtime.rb +13 -0
- data/lib/animate_it/embed_styles.rb +126 -0
- data/lib/animate_it/engine.rb +7 -0
- data/lib/animate_it/player_manifest.rb +29 -0
- data/lib/animate_it/runtime/runtime.js +518 -0
- data/lib/animate_it/runtime.rb +11 -0
- data/lib/animate_it/scene.rb +57 -3
- data/lib/animate_it/text_effects.rb +104 -0
- data/lib/animate_it/track_document_schema.rb +71 -0
- data/lib/animate_it/tracks/document.rb +100 -0
- data/lib/animate_it/tracks/layer.rb +13 -0
- data/lib/animate_it/tracks/recorder.rb +149 -0
- data/lib/animate_it/verification.rb +187 -0
- data/lib/animate_it/version.rb +1 -1
- data/lib/animate_it/video_renderer.rb +89 -26
- data/lib/animate_it/view_helpers.rb +11 -1
- data/lib/animate_it.rb +17 -0
- data/lib/tasks/animate_it_tasks.rake +133 -0
- 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
|
-
|
|
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(
|
|
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
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
audios
|
|
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 += [
|
|
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
|
-
#
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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/
|
|
131
|
+
homepage: https://github.com/joinbuildit/animate_it
|
|
113
132
|
licenses:
|
|
114
133
|
- MIT
|
|
115
134
|
metadata:
|
|
116
|
-
homepage_uri: https://github.com/
|
|
117
|
-
source_code_uri: https://github.com/
|
|
118
|
-
changelog_uri: https://github.com/
|
|
119
|
-
bug_tracker_uri: https://github.com/
|
|
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:
|