animate_it 0.2.0 → 0.3.2
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 +32 -1
- data/README.md +13 -0
- data/app/controllers/animate_it/audio_controller.rb +15 -2
- data/app/views/animate_it/studio/_studio_script.html.haml +23 -4
- data/lib/animate_it/version.rb +1 -1
- data/lib/animate_it/video_renderer.rb +4 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0675f1e382659072fd53a4b9453d2a44b1bf8a208075bc43974522195540df1f
|
|
4
|
+
data.tar.gz: c3804ff10fa75939fd23716e555054b64ea0ba45f1eaa9beb36c8b23e5b7c025
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 06fe263c48072dc834609a78c5f3f3f09a2897872740e11d54fd8edcf2dcd986113ae1338ffd941f491077de0c4a3feb100cda5b679ef161052c8cbe123a55f3
|
|
7
|
+
data.tar.gz: d2f7f86b39e2aa3308dc98e9e6cd5bc4956716e03e0e81cc2a5857892ac9f1dea584634b13f3632450561285f361dbb3491964ac58a10296118e2a1375499b26
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,34 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.2] - 2026-07-23
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Studio audio is now served with HTTP byte-range support (Accept-Ranges,
|
|
13
|
+
206 Partial Content). Browsers refuse to seek media served without it, so
|
|
14
|
+
play-pause-play and scrub-then-play restarted clips from 0:00.
|
|
15
|
+
- Multi-track renders no longer bury quiet tracks: the ffmpeg `amix` mux now
|
|
16
|
+
passes `normalize=0`, so declared per-segment gains are the only scaling
|
|
17
|
+
(amix's default divides every input by the track count).
|
|
18
|
+
|
|
19
|
+
## [0.3.1] - 2026-07-22
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Relaxed the HAML dependency from `~> 6.3` to `>= 6.3` so hosts on HAML 7 are
|
|
23
|
+
not forced to downgrade.
|
|
24
|
+
|
|
25
|
+
## [0.3.0] - 2026-07-22
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Studio playback started from a scrubbed position could seek the audio before
|
|
29
|
+
its metadata had loaded; the seek silently failed and the clip played from
|
|
30
|
+
0:00 instead of the playhead offset. The seek is now deferred until
|
|
31
|
+
`loadedmetadata`.
|
|
32
|
+
- Studio playback now stops on Turbo navigation (`turbo:before-visit`,
|
|
33
|
+
`turbo:before-cache`) and `pagehide` — Turbo swaps the body without unloading
|
|
34
|
+
the window, so a detached, still-playing audio element kept sounding under
|
|
35
|
+
the next page.
|
|
36
|
+
|
|
9
37
|
## [0.2.0] - 2026-07-09
|
|
10
38
|
|
|
11
39
|
### Added
|
|
@@ -35,6 +63,9 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
35
63
|
- `render_animate_it_video` executable and `animate_it:render` rake task.
|
|
36
64
|
- `animate_it:install` generator.
|
|
37
65
|
|
|
38
|
-
[Unreleased]: https://github.com/
|
|
66
|
+
[Unreleased]: https://github.com/joinbuildit/animate_it/compare/v0.3.2...HEAD
|
|
67
|
+
[0.3.2]: https://github.com/joinbuildit/animate_it/compare/v0.3.1...v0.3.2
|
|
68
|
+
[0.3.1]: https://github.com/joinbuildit/animate_it/compare/v0.3.0...v0.3.1
|
|
69
|
+
[0.3.0]: https://github.com/growth-constant/animate_it/compare/v0.2.0...v0.3.0
|
|
39
70
|
[0.2.0]: https://github.com/growth-constant/animate_it/compare/v0.1.0...v0.2.0
|
|
40
71
|
[0.1.0]: https://github.com/growth-constant/animate_it/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -176,6 +176,19 @@ end
|
|
|
176
176
|
Render concurrency for the Studio's background renderer is tunable via
|
|
177
177
|
`ANIMATE_IT_RENDER_CONCURRENCY` and `ANIMATE_IT_RENDER_STARTS_PER_SECOND`.
|
|
178
178
|
|
|
179
|
+
## Claude skill
|
|
180
|
+
|
|
181
|
+
If you use [Claude Code](https://claude.com/claude-code), this repo ships an
|
|
182
|
+
Agent Skill that teaches Claude how to author AnimateIt compositions — the DSL,
|
|
183
|
+
the render pipeline, embedding, rendering your app's real partials, motion
|
|
184
|
+
craft, and the gotchas. Install it into your project's `.claude/skills/`:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
npx animate-it-skills # copies the skill into ./.claude/skills
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The skill source lives in [`skills/`](skills/).
|
|
191
|
+
|
|
179
192
|
## Development
|
|
180
193
|
|
|
181
194
|
```bash
|
|
@@ -10,12 +10,25 @@ module AnimateIt
|
|
|
10
10
|
file_path = resolve_audio_path(segment.source[:path])
|
|
11
11
|
return head :not_found unless file_path && File.file?(file_path)
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
disposition: "inline"
|
|
13
|
+
serve_with_byte_ranges(file_path)
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
private
|
|
18
17
|
|
|
18
|
+
# send_file ignores Range headers, and browsers refuse to seek media
|
|
19
|
+
# served without byte-range support — every currentTime seek snaps back
|
|
20
|
+
# to 0:00, so the Studio player restarts clips from the beginning.
|
|
21
|
+
# Rack::Files implements range semantics (Accept-Ranges, 206/416).
|
|
22
|
+
def serve_with_byte_ranges(file_path)
|
|
23
|
+
status, headers, body = Rack::Files.new(File.dirname(file_path)).serving(request, file_path)
|
|
24
|
+
headers["content-type"] = Mime::Type.lookup_by_extension(File.extname(file_path).delete(".")).to_s
|
|
25
|
+
headers["content-disposition"] = "inline"
|
|
26
|
+
headers["accept-ranges"] = "bytes"
|
|
27
|
+
self.status = status
|
|
28
|
+
self.response_body = body
|
|
29
|
+
headers.each { |key, value| response.headers[key] = value }
|
|
30
|
+
end
|
|
31
|
+
|
|
19
32
|
def resolve_audio_path(path)
|
|
20
33
|
return nil if path.blank?
|
|
21
34
|
|
|
@@ -141,6 +141,21 @@
|
|
|
141
141
|
playheads.forEach((el) => { el.style.left = `${pct}%`; });
|
|
142
142
|
};
|
|
143
143
|
|
|
144
|
+
// Seeking before the clip's metadata has loaded doesn't stick — it would
|
|
145
|
+
// start at 0:00 regardless of the scrubbed position. Defer the seek
|
|
146
|
+
// (recomputed from the then-current frame) until metadata is available.
|
|
147
|
+
const startAudio = (el) => {
|
|
148
|
+
const begin = () => {
|
|
149
|
+
if (timer === null) return; // stopped before metadata arrived
|
|
150
|
+
const { start, len } = audioWindow(el);
|
|
151
|
+
if (frame < start || frame >= start + len) return; // playhead left the window
|
|
152
|
+
el.currentTime = (frame - start) / fps;
|
|
153
|
+
el.play().catch(() => {});
|
|
154
|
+
};
|
|
155
|
+
if (el.readyState >= 1) begin();
|
|
156
|
+
else el.addEventListener("loadedmetadata", begin, { once: true });
|
|
157
|
+
};
|
|
158
|
+
|
|
144
159
|
// Audio is audible only while actually playing — seeking/scrubbing is
|
|
145
160
|
// silent. Each element plays only inside its own window, seeked to the
|
|
146
161
|
// matching offset, so you hear exactly what belongs at the current frame.
|
|
@@ -153,10 +168,7 @@
|
|
|
153
168
|
// playing, leave it on its own real-time clock: re-seeking every
|
|
154
169
|
// frame to chase the setInterval-driven frame counter (which lags
|
|
155
170
|
// real time) is what chops the audio.
|
|
156
|
-
if (el.paused)
|
|
157
|
-
el.currentTime = (currentFrame - start) / fps;
|
|
158
|
-
el.play().catch(() => {});
|
|
159
|
-
}
|
|
171
|
+
if (el.paused) startAudio(el);
|
|
160
172
|
} else if (!el.paused) {
|
|
161
173
|
el.pause();
|
|
162
174
|
}
|
|
@@ -264,6 +276,13 @@
|
|
|
264
276
|
}
|
|
265
277
|
});
|
|
266
278
|
|
|
279
|
+
// Turbo Drive swaps the body without unloading the window: detached
|
|
280
|
+
// <audio> elements keep playing and the playback loop keeps ticking.
|
|
281
|
+
// Stop this page's playback before navigating away or caching.
|
|
282
|
+
document.addEventListener("turbo:before-visit", stop, { once: true });
|
|
283
|
+
document.addEventListener("turbo:before-cache", stop, { once: true });
|
|
284
|
+
window.addEventListener("pagehide", stop, { once: true });
|
|
285
|
+
|
|
267
286
|
window.AnimateItStudio = {
|
|
268
287
|
setFrame,
|
|
269
288
|
currentFrame: () => frame
|
data/lib/animate_it/version.rb
CHANGED
|
@@ -182,7 +182,10 @@ module AnimateIt
|
|
|
182
182
|
gain = seg.source[:gain] || 1.0
|
|
183
183
|
"[#{i + 1}:a]adelay=#{delay_ms}|#{delay_ms},volume=#{gain}[a#{i}]"
|
|
184
184
|
end
|
|
185
|
-
|
|
185
|
+
# normalize=0: amix's default rescales every input by 1/N, which
|
|
186
|
+
# silently buries a voice-over under a music bed the moment a second
|
|
187
|
+
# 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]"
|
|
186
189
|
[legs, mix].flatten.join(";")
|
|
187
190
|
end
|
|
188
191
|
|
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.2
|
|
4
|
+
version: 0.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- growth-constant
|
|
@@ -13,14 +13,14 @@ dependencies:
|
|
|
13
13
|
name: haml
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
18
|
version: '6.3'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
|
-
- - "
|
|
23
|
+
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '6.3'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|