animate_it 0.1.0 → 0.3.1

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: 178ea946f7dfb1a83da1a68ba2ea514984d2ad1ab9792bbcd7d7fadff5380fc5
4
- data.tar.gz: ca63aee15a52e08db308a17a2094989131bc1c6c6625f52cfb8dd375ef2b71ec
3
+ metadata.gz: 95bcfafc0a059c64d5f3a4097a308ba2b7b9fff58cafdb18de720926e02cb07e
4
+ data.tar.gz: 268da7e42d644633ee56bafb7528e7051280474647873d31bad585b07bd472cb
5
5
  SHA512:
6
- metadata.gz: 7d4f409c4d620768e021529c8d3b7b2de5d5e8b2be1eaa05165c986c25f10e384434842f71b5c5f0062f482dc228d43efc40d65dc18feae776914a91d19985b5
7
- data.tar.gz: cdc55f7e5159a4ec9854a61b447be969a4710d95b831ff9024611afe5b88650eb29b8de2ba097cc3cfb0c91ac459fdadf60bb213e5d46ffffc279aadb393d251
6
+ metadata.gz: d4cd9739b39aecc9d9cedf7d159718a768b7ba26ad3c4382e6f1bd96226ba70c4d59d708c57c263124eb07d39f0ff21bf3daab91733e774ffa15457dd8f3cf9e
7
+ data.tar.gz: c6f480ba02780a0c2cff836d061247961985138916607a318e6a4311456e8627684179dd52a81d16ab8fe30b413192f9126e9c3073306799299c8a8ff5ec06ec
data/CHANGELOG.md CHANGED
@@ -6,6 +6,44 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ### Fixed
10
+ - Studio audio is now served with HTTP byte-range support (Accept-Ranges,
11
+ 206 Partial Content). Browsers refuse to seek media served without it, so
12
+ play-pause-play and scrub-then-play restarted clips from 0:00.
13
+
14
+ ## [0.3.1] - 2026-07-22
15
+
16
+ ### Changed
17
+ - Relaxed the HAML dependency from `~> 6.3` to `>= 6.3` so hosts on HAML 7 are
18
+ not forced to downgrade.
19
+
20
+ ## [0.3.0] - 2026-07-22
21
+
22
+ ### Fixed
23
+ - Studio playback started from a scrubbed position could seek the audio before
24
+ its metadata had loaded; the seek silently failed and the clip played from
25
+ 0:00 instead of the playhead offset. The seek is now deferred until
26
+ `loadedmetadata`.
27
+ - Studio playback now stops on Turbo navigation (`turbo:before-visit`,
28
+ `turbo:before-cache`) and `pagehide` — Turbo swaps the body without unloading
29
+ the window, so a detached, still-playing audio element kept sounding under
30
+ the next page.
31
+
32
+ ## [0.2.0] - 2026-07-09
33
+
34
+ ### Added
35
+ - Scene sidecar templates can now be authored in ERB (`canvas.html.erb`) as well
36
+ as HAML — Rails resolves whichever exists, so HAML and ERB scenes can be mixed
37
+ in one app.
38
+ - Test matrix across Rails 7.2 and 8.1 on Ruby 3.3 and 3.4, via Appraisal.
39
+
40
+ ### Changed
41
+ - Lowered the minimum supported versions to Rails >= 7.2 and Ruby >= 3.3
42
+ (previously Rails >= 8.1.3, Ruby >= 3.4).
43
+ - HAML is now a runtime dependency, so the Studio UI and HAML scenes render on
44
+ any host without the host having to add HAML itself (fixes an ERB-only host
45
+ crashing when mounting the Studio).
46
+
9
47
  ## [0.1.0] - 2026-07-08
10
48
 
11
49
  ### Added
@@ -20,5 +58,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
20
58
  - `render_animate_it_video` executable and `animate_it:render` rake task.
21
59
  - `animate_it:install` generator.
22
60
 
23
- [Unreleased]: https://github.com/growth-constant/animate_it/compare/v0.1.0...HEAD
61
+ [Unreleased]: https://github.com/joinbuildit/animate_it/compare/v0.3.1...HEAD
62
+ [0.3.1]: https://github.com/joinbuildit/animate_it/compare/v0.3.0...v0.3.1
63
+ [0.3.0]: https://github.com/growth-constant/animate_it/compare/v0.2.0...v0.3.0
64
+ [0.2.0]: https://github.com/growth-constant/animate_it/compare/v0.1.0...v0.2.0
24
65
  [0.1.0]: https://github.com/growth-constant/animate_it/releases/tag/v0.1.0
data/README.md CHANGED
@@ -1,10 +1,13 @@
1
1
  <p align="center">
2
- <img src="assets/logo.svg" alt="AnimateIt" width="360">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="assets/logo-dark.svg">
4
+ <img src="assets/logo.svg" alt="AnimateIt" width="360">
5
+ </picture>
3
6
  </p>
4
7
 
5
8
  <p align="center">
6
- <strong>Videos as code.</strong> Declarative, frame-driven video compositions for Rails,
7
- rendered with headless Chromium (Playwright) + FFmpeg.
9
+ <strong>Remotion for Rails.</strong> Make videos from your app's own components and data,
10
+ written in Ruby no React, no video editor, no After Effects.
8
11
  </p>
9
12
 
10
13
  <p align="center">
@@ -15,14 +18,23 @@
15
18
 
16
19
  ---
17
20
 
18
- AnimateIt lets you describe a video as a Ruby class and a HAML template, preview it
19
- frame-by-frame in a bundled **Studio** UI, and render it to MP4/WebM/MOV/GIF. Each
20
- frame is a real web page — so you get the full power of CSS, your app's own
21
- components, and real data — captured by a headless browser and stitched with FFmpeg.
21
+ [Remotion](https://www.remotion.dev/) made it possible to build videos in React.
22
+ **AnimateIt brings the same idea to Ruby on Rails** without React, a JavaScript
23
+ project, or a video editor.
24
+
25
+ You describe a video as a Ruby class and a HAML template, preview it frame-by-frame
26
+ in a bundled **Studio** UI, and render it to MP4/WebM/MOV/GIF. Every frame is a real
27
+ web page, so you get the full power of CSS and — crucially — **your app's own
28
+ components, styles, fonts, and real data**. A headless browser captures each frame
29
+ and FFmpeg stitches them into a video.
30
+
31
+ It's built for indie hackers and Rails developers who want to promote their projects
32
+ with polished product demos, launch clips, and social ads — reusing the UI they've
33
+ already built, staying in Ruby, and skipping the whole "learn a video editor" detour.
22
34
 
23
35
  ## Requirements
24
36
 
25
- - Ruby >= 3.4, Rails >= 8.1.3
37
+ - Ruby >= 3.3, Rails >= 7.2 (tested against Rails 7.2 and 8.1)
26
38
  - **FFmpeg** on the PATH (rendering)
27
39
  - **Node + Playwright** with a Chromium build (rendering): `npx playwright install chromium`
28
40
  - **HAML** (composition sidecar templates + Studio views)
@@ -99,6 +111,23 @@ CSS-variable bag (`animation_vars`), named `beat`s, and `audio` / `audio_loop`
99
111
  tracks are all available. Compositions can render your app's real partials and
100
112
  ViewComponents — see `AnimateIt.config.render_stylesheets` below.
101
113
 
114
+ ### HAML or ERB
115
+
116
+ Scene sidecar templates can be authored in **HAML** (`canvas.html.haml`) or
117
+ **ERB** (`canvas.html.erb`) — Rails resolves whichever file exists, so you can
118
+ mix engines across scenes. The HAML canvas above is equivalent to:
119
+
120
+ ```erb
121
+ -# app/videos/hello_video/canvas.html.erb
122
+ <style>.title { opacity: var(--title-opacity, 1); font-size: 6rem; font-weight: 800; }</style>
123
+ <div class="stage" style="<%= @vars %>">
124
+ <div class="title">Hello 👋</div>
125
+ </div>
126
+ ```
127
+
128
+ The gem bundles HAML for its own Studio UI, so you never need to add HAML to your
129
+ app to use it — ERB-only apps work out of the box.
130
+
102
131
  ## Rendering
103
132
 
104
133
  The renderer needs your Rails **server running** (it drives a real browser against
@@ -147,6 +176,19 @@ end
147
176
  Render concurrency for the Studio's background renderer is tunable via
148
177
  `ANIMATE_IT_RENDER_CONCURRENCY` and `ANIMATE_IT_RENDER_STARTS_PER_SECOND`.
149
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
+
150
192
  ## Development
151
193
 
152
194
  ```bash
@@ -156,11 +198,26 @@ bundle exec rubocop
156
198
 
157
199
  # full render smoke test (needs ffmpeg + Playwright chromium)
158
200
  RUN_RENDER_SMOKE=1 bundle exec rspec spec/rendering_spec.rb
201
+
202
+ # run the suite against a specific Rails version (see Appraisals)
203
+ bundle exec appraisal install
204
+ bundle exec appraisal rails-7.2 rspec
205
+ bundle exec appraisal rails-8.1 rspec
159
206
  ```
160
207
 
161
208
  Specs run against a minimal host app in `spec/dummy` — no external services, no
162
209
  database.
163
210
 
211
+ ## Credits
212
+
213
+ AnimateIt is inspired by [**Remotion**](https://www.remotion.dev) — the framework
214
+ that pioneered making videos programmatically in React. AnimateIt brings that
215
+ "videos as code" idea to the Ruby on Rails ecosystem, natively and without React.
216
+ Huge thanks to the Remotion team for the inspiration.
217
+
164
218
  ## License
165
219
 
166
- [MIT](MIT-LICENSE).
220
+ AnimateIt is released under the [**MIT License**](MIT-LICENSE) — free and open source,
221
+ with **no restrictions on commercial or business use**. Individuals, startups,
222
+ agencies, and companies of any size can use it in open-source and closed-source
223
+ projects at no cost, forever. No paid tiers, no seat limits.
@@ -1,7 +1,8 @@
1
1
  module AnimateIt
2
2
  class ApplicationController < ActionController::Base
3
- # Composition sidecar templates live at app/videos/<composition>/*.html.haml
3
+ # Composition sidecar templates live at app/videos/<composition>/*.html.*
4
4
  # in the host app — outside the engine's view path, so add it explicitly.
5
+ # Sidecars may be HAML or ERB; Rails' lookup resolves whichever exists.
5
6
  prepend_view_path Rails.root.join("app/videos")
6
7
 
7
8
  # Sidecar templates routinely render host-app partials (e.g. the real
@@ -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
@@ -15,8 +15,10 @@ module AnimateIt
15
15
  # ----- class-level DSL -----------------------------------------------
16
16
  class << self
17
17
  # Sidecar template name. Defaults to "canvas" — i.e. a scene whose
18
- # composition is `MyHero` looks for `app/videos/my_hero/canvas.html.haml`.
19
- # Override to point to a sibling template instead.
18
+ # composition is `MyHero` looks for `app/videos/my_hero/canvas.html.*`.
19
+ # The sidecar may be authored in HAML (`canvas.html.haml`) or ERB
20
+ # (`canvas.html.erb`); Rails resolves whichever exists. Override to point
21
+ # to a sibling template instead.
20
22
  def template(name = nil)
21
23
  @template = name.to_s if name
22
24
  @template ||= "canvas"
@@ -1,3 +1,3 @@
1
1
  module AnimateIt
2
- VERSION = "0.1.0".freeze
2
+ VERSION = "0.3.1".freeze
3
3
  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.1.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - growth-constant
@@ -9,26 +9,45 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: haml
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '6.3'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '6.3'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: rails
14
28
  requirement: !ruby/object:Gem::Requirement
15
29
  requirements:
16
30
  - - ">="
17
31
  - !ruby/object:Gem::Version
18
- version: 8.1.3
32
+ version: '7.2'
19
33
  type: :runtime
20
34
  prerelease: false
21
35
  version_requirements: !ruby/object:Gem::Requirement
22
36
  requirements:
23
37
  - - ">="
24
38
  - !ruby/object:Gem::Version
25
- version: 8.1.3
39
+ version: '7.2'
26
40
  description: |
27
- AnimateIt is a mountable Rails engine for building videos as code. Declare a
28
- composition (size, fps, duration, beats, audio), describe each frame with a
29
- Ruby + HAML scene, preview it in the bundled Studio UI, and render it to
30
- MP4/WebM/MOV/GIF with a headless Chromium (Playwright) frame capture piped
31
- through FFmpeg.
41
+ AnimateIt brings Remotion-style programmatic video to Ruby on Rails without
42
+ React or a JavaScript project. Build videos as code: a Ruby class plus a HAML
43
+ template, using your app's own components, styles, fonts, and real data. Every
44
+ frame is a real web page, so anything you can render in your app you can put in
45
+ a video. Preview frame-by-frame in the bundled Studio, then export to
46
+ MP4/WebM/MOV/GIF with headless Chromium (Playwright) + FFmpeg.
47
+
48
+ Built for indie hackers and Rails developers who want to promote their projects
49
+ with polished product demos, launch clips, and social ads — without leaving
50
+ Ruby, hiring an editor, or learning After Effects.
32
51
  executables:
33
52
  - render_animate_it_video
34
53
  extensions: []
@@ -106,7 +125,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
106
125
  requirements:
107
126
  - - ">="
108
127
  - !ruby/object:Gem::Version
109
- version: '3.4'
128
+ version: '3.3'
110
129
  required_rubygems_version: !ruby/object:Gem::Requirement
111
130
  requirements:
112
131
  - - ">="
@@ -115,6 +134,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
115
134
  requirements: []
116
135
  rubygems_version: 3.6.9
117
136
  specification_version: 4
118
- summary: Declarative, frame-driven video compositions for Rails, rendered with Playwright
119
- + FFmpeg
137
+ summary: Remotion for Rails make videos with your app's own assets, in Ruby. No
138
+ React required.
120
139
  test_files: []