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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fb4e829854a53280979963178429b03b60d9deaf1350b26f06d5f30a69cc44fb
4
- data.tar.gz: c1b96fbeb407a50e073a7e2e59df91d9287d3e02f6df7bdf6b2aef7053628e45
3
+ metadata.gz: 0675f1e382659072fd53a4b9453d2a44b1bf8a208075bc43974522195540df1f
4
+ data.tar.gz: c3804ff10fa75939fd23716e555054b64ea0ba45f1eaa9beb36c8b23e5b7c025
5
5
  SHA512:
6
- metadata.gz: a4cfbbca87fe87459fed715f909d3c80743dc2ff1da0df7fc9acf82daf1e9d9319993a8733d2e417e5b46cd477becd2c7f716947f1fdf33c1707e173626c95b8
7
- data.tar.gz: 034c051d6bb5a16f98a4682caee7c0290fb80ca6a0289735b71447246592ad003eb96f869b7da3d920e30c758cceac88d87d8f3831bbe1c2739366a61ec71d10
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/growth-constant/animate_it/compare/v0.2.0...HEAD
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
- send_file file_path, type: Mime::Type.lookup_by_extension(File.extname(file_path).delete(".")),
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
@@ -1,3 +1,3 @@
1
1
  module AnimateIt
2
- VERSION = "0.2.0".freeze
2
+ VERSION = "0.3.2".freeze
3
3
  end
@@ -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
- mix = "#{audios.length.times.map { |i| "[a#{i}]" }.join}amix=inputs=#{audios.length}:duration=longest[aout]"
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.0
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