Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,26 @@ jobs:
- name: Rubocop
run: bundle exec rubocop

security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
bundler-cache: true

- name: Install security scanners
run: gem install bundler-audit brakeman --no-document

- name: Audit dependencies
run: bundle-audit check --update

- name: Scan Rails engine
run: brakeman -q -p . --no-pager --confidence-level 2 --exit-on-warn --exit-on-error

test:
runs-on: ubuntu-latest
strategy:
Expand All @@ -43,6 +63,12 @@ jobs:
- name: RSpec (unit + request)
run: bundle exec rspec --exclude-pattern "spec/rendering_spec.rb"

- name: Audit appraisal dependencies
run: |
gem install bundler-audit --no-document
bundle-audit check --update \
--gemfile-lock "gemfiles/${{ matrix.gemfile }}.gemfile.lock"

# The end-to-end render smoke drives real Chromium + FFmpeg, so run it once
# on the newest supported cell rather than on all four.
- name: Set up Node
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ jobs:
echo "published=$published" >> "$GITHUB_OUTPUT"
echo "animate_it ${{ steps.version.outputs.value }} already published: $published"

- name: Build release description
if: steps.check.outputs.published == 'false'
run: ruby bin/release_notes "${{ steps.version.outputs.value }}" > release-notes.md

- name: Configure RubyGems trusted publishing
if: steps.check.outputs.published == 'false'
uses: rubygems/configure-rubygems-credentials@main
Expand All @@ -57,4 +61,6 @@ jobs:
tag="v${{ steps.version.outputs.value }}"
git tag "$tag"
git push origin "$tag"
gh release create "$tag" --title "$tag" --generate-notes
gh release create "$tag" \
--title "Animate It ${{ steps.version.outputs.value }}" \
--notes-file release-notes.md
4 changes: 2 additions & 2 deletions Appraisals
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Dependency sets the CI matrix + `bundle exec appraisal <name> rspec` run
# against. The gem supports Rails >= 7.2; these pin the two we actively test.
appraise "rails-7.2" do
gem "rails", "~> 7.2.0"
gem "rails", "~> 7.2.0", ">= 7.2.3.2"
end

appraise "rails-8.1" do
gem "rails", "~> 8.1.0"
gem "rails", "~> 8.1.0", ">= 8.1.3.1"
end
30 changes: 29 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,33 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.5.0] - 2026-08-13

### Added
- User-navigable `chapter` declarations backed by existing beats, with a
versioned player manifest and normalized progress/state variables.
- `animate_it_embed`, a poster-first custom-element embed with responsive
composition variants, proportional scaling, readiness crossfade, visibility
playback, offscreen pausing, reduced-motion fallback, and accessible controls.
- Headless Rails chapter builders for arbitrary controls plus optional desktop
and mobile-carousel pill presets and canvas-side chapter navigation.
- Stable transport commands and `ready`, `framechange`, `chapterchange`, `play`,
`pause`, `ended`, and `error` events over a source-checked same-origin message
protocol.
- Versioned, immutable JavaScript and CSS endpoints that work independently of
Stimulus and the host application's asset pipeline.

### Changed
- Public player readiness now waits for fonts, eager images, and two paint
frames. `AnimateItTransport` remains available as a compatibility alias.
- Repository and package metadata now point at `joinbuildit/animate_it`.
- Every automated GitHub release now publishes the matching curated changelog
section as its release description and fails safely when that entry is missing.

### Compatibility
- `animate_it_player`, track schema v2, existing compositions, Studio playback,
and video rendering remain backward compatible.

## [0.4.0] - 2026-08-01

### Added
Expand Down Expand Up @@ -96,7 +123,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- `render_animate_it_video` executable and `animate_it:render` rake task.
- `animate_it:install` generator.

[Unreleased]: https://github.com/joinbuildit/animate_it/compare/v0.4.0...HEAD
[Unreleased]: https://github.com/joinbuildit/animate_it/compare/v0.5.0...HEAD
[0.5.0]: https://github.com/joinbuildit/animate_it/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/joinbuildit/animate_it/compare/v0.3.2...v0.4.0
[0.3.2]: https://github.com/joinbuildit/animate_it/compare/v0.3.1...v0.3.2
[0.3.1]: https://github.com/joinbuildit/animate_it/compare/v0.3.0...v0.3.1
Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ group :development, :test do
gem "factory_bot", "~> 6.5"
gem "faker", "~> 3.5"
gem "playwright-ruby-client", "~> 1.61.0"
gem "puma", "~> 6.0"
gem "puma", ">= 7.2.1", "< 9"
gem "rspec-rails", "~> 8.0"
gem "rubocop", "~> 1.68", require: false
gem "rubocop-rails", "~> 2.28", require: false
Expand Down
77 changes: 73 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</p>

<p align="center">
<a href="https://github.com/growth-constant/animate_it/actions/workflows/ci.yml"><img src="https://github.com/growth-constant/animate_it/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<a href="https://github.com/joinbuildit/animate_it/actions/workflows/ci.yml"><img src="https://github.com/joinbuildit/animate_it/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<a href="https://rubygems.org/gems/animate_it"><img src="https://img.shields.io/gem/v/animate_it.svg" alt="Gem Version"></a>
<img src="https://img.shields.io/badge/ruby-%3E%3D%203.3-CC342D.svg" alt="Ruby >= 3.3">
</p>
Expand Down Expand Up @@ -174,6 +174,10 @@ endpoints, and public playback always uses the composition's default props.
class HelloVideo < AnimateIt::Composition
id "hello"
public_player! autoplay: false, loop: true
beat :intro, at: 0, length: 45
beat :details, at: 45, length: 45
chapter :intro, beat: :intro, label: "Intro"
chapter :details, beat: :details, label: "Details"
# ...
end
```
Expand All @@ -190,9 +194,74 @@ mount AnimateIt::Engine, at: AnimateIt.config.mount_path
<%= animate_it_player "hello", title: "Hello product demo" %>
```

The iframe renders each structural layer once and advances entirely in the
browser. Audio-capable players fall back to the visible Play button when the
browser blocks autoplay.
`animate_it_player` remains the low-level responsive iframe. For a complete
production embed with accessible chapter navigation, a poster-first handoff,
visibility playback, reduced-motion behavior, and offscreen pausing, use:

```erb
<%= animate_it_embed(
"hello",
poster: image_path("hello.webp"),
variants: [
{
media: "(max-width: 767px)",
composition: "hello-mobile",
poster: image_path("hello-mobile.webp")
}
],
navigation: { preset: :pills, mobile: :carousel },
load_when_visible: 0.25,
play_when_visible: 2.0 / 3
) %>
```

Responsive compositions may use different sizes and chapter frames, but must
declare the same ordered chapter names and labels. The embed swaps by media
query and restores the current chapter by name.

The pill rail is optional. Build cards, tabs, thumbnails, dots, or custom SVG
with the headless Rails builder:

```erb
<%= animate_it_embed("hello", poster: image_path("hello.webp")) do |embed| %>
<%= embed.chapter_navigation(class: "product-demo-cards") do |chapter| %>
<%= chapter.button(class: "product-demo-card") do %>
<strong><%= chapter.label %></strong>
<% end %>
<% end %>
<% end %>
```

Every control receives `data-chapter-state`, `data-chapter-position`, and the
normalized CSS variables `--animate-it-chapter-progress`,
`--animate-it-chapter-active`, and `--animate-it-chapter-complete`. The player
emits `animateit:ready`, `animateit:framechange`, `animateit:chapterchange`,
`animateit:play`, `animateit:pause`, `animateit:ended`, and `animateit:error`
events. Host commands use a source-checked same-origin message
boundary instead of reaching into iframe globals.

For direct player integrations, `window.AnimateItPlayer` exposes `play`,
`pause`, `toggle`, `seek`, `seekChapter`, `playing`, and `currentFrame`.
`window.AnimateItTransport` remains an alias for compatibility with 0.4.

To include the same chapter visualization in Studio and rendered media:

```erb
<%= animate_it_chapter_navigation preset: :pills, hide_when_embedded: true %>
```

The iframe still renders each structural layer once and advances entirely in
the browser. Audio-capable players fall back to the visible Play button when
the browser blocks autoplay.

### Migrating a custom iframe controller

Replace application-owned iframe scaling, `IntersectionObserver`, poster
crossfade, breakpoint swapping, frame polling, and `contentWindow` transport
calls with `animate_it_embed`. Keep application CSS by overriding the documented
`--animate-it-*` tokens or render completely custom chapter controls through the
headless builder. Continue using `animate_it_player` when the application truly
needs to own the entire lifecycle.

### HAML or ERB

Expand Down
2 changes: 1 addition & 1 deletion animate_it.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
with polished product demos, launch clips, and social ads — without leaving
Ruby, hiring an editor, or learning After Effects.
DESC
spec.homepage = "https://github.com/growth-constant/animate_it"
spec.homepage = "https://github.com/joinbuildit/animate_it"
spec.license = "MIT"
spec.required_ruby_version = ">= 3.3"

Expand Down
25 changes: 25 additions & 0 deletions app/controllers/animate_it/embed_assets_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module AnimateIt
class EmbedAssetsController < ApplicationController
layout false
skip_before_action :ensure_local_environment
skip_forgery_protection

def javascript
serve_asset(EmbedRuntime.javascript, "application/javascript")
end

def stylesheet
serve_asset(EmbedRuntime.stylesheet, "text/css")
end

private

def serve_asset(source, content_type)
return head :not_found unless params[:version] == AnimateIt::VERSION

expires_in 1.year, public: true, immutable: true
response.set_header("X-Content-Type-Options", "nosniff")
render plain: source, content_type:
end
end
end
3 changes: 3 additions & 0 deletions app/controllers/animate_it/frames_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ def player
@props = preview_props
@track_document = @composition.track_document(props: @props)
TrackDocumentSchema.validate!(@track_document)
@player_manifest = @composition.player_manifest
@embedded_player = false
@host_navigation = false
end
end
end
6 changes: 5 additions & 1 deletion app/controllers/animate_it/public_players_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ def show
@props = {}
@track_document = composition.track_document
TrackDocumentSchema.validate!(@track_document)
@player_manifest = composition.player_manifest
@audio_segments = audio_segments
@public_player = true
@public_player_options = composition.public_player_options
@embedded_player = params[:embedded] == "1"
@host_navigation = params[:host_navigation] == "1"
@public_player_options = composition.public_player_options.merge(autoplay: false) if @embedded_player
@public_player_options ||= composition.public_player_options
render "animate_it/frames/player"
end

Expand Down
13 changes: 12 additions & 1 deletion app/views/animate_it/frames/player.html.haml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
!!!
%html{ lang: "en" }
%html{ lang: "en", data: {
animate_it_embedded: @embedded_player ? "true" : "false",
animate_it_host_navigation: @host_navigation ? "true" : "false"
} }
%head
%title= "#{@composition.id} player (#{@composition.duration_in_frames} frames)"
%meta{ name: "viewport", content: "width=#{@composition.width}, initial-scale=1" }
Expand Down Expand Up @@ -52,6 +55,12 @@
font: 600 14px/1 system-ui, sans-serif;
cursor: pointer;
}
html[data-animate-it-host-navigation="true"] [data-animate-it-hide-when-embedded="true"] {
display: none !important;
}
html[data-animate-it-embedded="true"] .animate-it-public-play {
display: none !important;
}
%body
.animate-it-stage
= @composition.render_structure(self, props: @props)
Expand All @@ -78,4 +87,6 @@
animate_it_loop: @public_player_options&.fetch(:loop, true) ? "true" : "false"
} }
!= ERB::Util.json_escape(@track_document.to_json)
%script{ type: "application/json", data: { animate_it_manifest: true } }
!= ERB::Util.json_escape(@player_manifest.as_json.to_json)
= javascript_tag AnimateIt::Runtime.source.html_safe
39 changes: 39 additions & 0 deletions bin/release_notes
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

version = ARGV.fetch(0) do
warn "Usage: ruby bin/release_notes VERSION [CHANGELOG]"
exit 64
end

changelog_path = ARGV.fetch(1, File.expand_path("../CHANGELOG.md", __dir__))
changelog = File.read(changelog_path)
heading = /^## \[#{Regexp.escape(version)}\](?:\s+-\s+.+)?\s*$/
match = changelog.match(heading)

unless match
warn "CHANGELOG.md has no release description for #{version}"
exit 1
end

body_start = match.end(0)
body_end = changelog.index(/^## \[/, body_start) || changelog.length
description = changelog[body_start...body_end].strip

if description.empty?
warn "CHANGELOG.md has an empty release description for #{version}"
exit 1
end

puts <<~MARKDOWN
## Animate It #{version}

#{description}

### Install

```ruby
gem "animate_it", "~> #{version}"
```

See the [full changelog](https://github.com/joinbuildit/animate_it/blob/main/CHANGELOG.md) for earlier releases and compatibility notes.
MARKDOWN
5 changes: 5 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
AnimateIt::Engine.routes.draw do
get "assets/:version/embed.js", to: "embed_assets#javascript", as: :embed_javascript,
constraints: { version: /[0-9A-Za-z._-]+/ }
get "assets/:version/embed.css", to: "embed_assets#stylesheet", as: :embed_stylesheet,
constraints: { version: /[0-9A-Za-z._-]+/ }

root "studio#index"

get "public/compositions/:id/player", to: "public_players#show", as: :public_composition_player
Expand Down
Binary file added docs/images/interactive-embed-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/interactive-embed-mobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions gemfiles/rails_7.2.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

source "https://rubygems.org"

gem "rails", "~> 7.2.0"
gem "rails", "~> 7.2.0", ">= 7.2.3.2"

group :development, :test do
gem "appraisal", "~> 2.5"
gem "capybara", "~> 3.40"
gem "factory_bot", "~> 6.5"
gem "faker", "~> 3.5"
gem "playwright-ruby-client", "~> 1.61.0"
gem "puma", "~> 6.0"
gem "puma", ">= 7.2.1", "< 9"
gem "rspec-rails", "~> 8.0"
gem "rubocop", "~> 1.68", require: false
gem "rubocop-rails", "~> 2.28", require: false
Expand Down
4 changes: 2 additions & 2 deletions gemfiles/rails_8.1.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

source "https://rubygems.org"

gem "rails", "~> 8.1.0"
gem "rails", "~> 8.1.0", ">= 8.1.3.1"

group :development, :test do
gem "appraisal", "~> 2.5"
gem "capybara", "~> 3.40"
gem "factory_bot", "~> 6.5"
gem "faker", "~> 3.5"
gem "playwright-ruby-client", "~> 1.61.0"
gem "puma", "~> 6.0"
gem "puma", ">= 7.2.1", "< 9"
gem "rspec-rails", "~> 8.0"
gem "rubocop", "~> 1.68", require: false
gem "rubocop-rails", "~> 2.28", require: false
Expand Down
Loading
Loading