Skip to content

Animation on hover w/ Gleam Logo #2807

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 36 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
52b2af2
Animation on hover w/ Gleam Logo
Arpan-206 Apr 20, 2025
6859dfc
linting stuff
Arpan-206 Apr 20, 2025
175ee69
manually making the linter happy
Arpan-206 Apr 20, 2025
c864859
Removing debugging stuff
Arpan-206 Apr 20, 2025
c4ab10f
Working on hover too
Arpan-206 Apr 20, 2025
ea04fe0
linter wasn't feeling too happy ig
Arpan-206 Apr 20, 2025
a20cf9f
remove logs and stuff
Arpan-206 Apr 20, 2025
6cc3d5b
Add integration test
Arpan-206 Apr 20, 2025
0335cf1
linters will be the end of me
Arpan-206 Apr 20, 2025
72c9864
pretty much the last msg
Arpan-206 Apr 20, 2025
4ecb22e
quality fix?
Arpan-206 Apr 21, 2025
0ac1be3
linter
Arpan-206 Apr 21, 2025
cf62de1
tests ain't running
Arpan-206 Apr 21, 2025
1d67c55
Fixed most stuff except maybe the moseevents
Arpan-206 Apr 21, 2025
d007423
used SMs and stuff
Arpan-206 Apr 23, 2025
88fa7b8
committing if we ever need to debug animations
Arpan-206 Apr 30, 2025
257025c
Remove all the manual animation bits, fully automatic now
Arpan-206 Apr 30, 2025
e951505
Fix tests
Arpan-206 Apr 30, 2025
d798652
linter needs to be happy
Arpan-206 Apr 30, 2025
8052924
[percy] add sentry logging
Arpan-206 Apr 30, 2025
8603f7e
Changes incorporation w/o breaks
Arpan-206 Apr 30, 2025
5853d88
Linter was sad
Arpan-206 Apr 30, 2025
53f6401
It resizes dynamically
Arpan-206 May 7, 2025
711a877
Doing anything to make this work best atp
Arpan-206 May 7, 2025
733209d
Updated the animation file
Arpan-206 May 8, 2025
63cdade
move gleam logo thingy to tracks page + dynamic sizing
Arpan-206 May 8, 2025
f6bfef5
Renamed stuff to be more generic and not just gleam specific + demo
Arpan-206 May 9, 2025
a9d58af
linter worship go brrrr
Arpan-206 May 9, 2025
662128f
Add some more demos + fix tests
Arpan-206 May 9, 2025
1ea7dda
minor bunny fix
Arpan-206 May 9, 2025
2b86774
should fix the resize issues
Arpan-206 May 9, 2025
ad8dbb2
linter doin linting
Arpan-206 May 9, 2025
75d66f3
use shorthands
Arpan-206 May 9, 2025
9c91bd9
🤞
Arpan-206 May 11, 2025
bac2acc
[percy] this needed too
Arpan-206 May 11, 2025
8e9e0d0
tests are acting weird
Arpan-206 May 11, 2025
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
9 changes: 9 additions & 0 deletions app/components/gleam-logo.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div
class="gleam-logo-container"
{{did-insert this.setupRive}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our convention for handlers is to always name them after the event (i.e. something like this.handleDidInsert in this case). Let's us now have to worry about naming each of these + allows us to easily edit handlers to add more functionality without having to rename them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noted!

{{will-destroy this.cleanupRive}}
{{on "mouseenter" this.handleMouseEnter}}
{{on "mouseleave" this.handleMouseLeave}}
>
{{! The canvas will be inserted here by Rive }}
</div>
124 changes: 124 additions & 0 deletions app/components/gleam-logo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { Rive } from '@rive-app/canvas';

interface GleamLogoSignature {
Element: HTMLDivElement;
Args: {
class?: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Arpan-206 in Ember you don't need to accept DOM-level attributes like class/style, these just get passed along with ...attributes. You only need to use Args for things that you'll explicitly pass in

style?: string;
[key: string]: unknown;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Arpan-206 what is this used for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2025-04-21 at 18 59 57

due to this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well the first one, forgot to remove the signature

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have to do this anywhere else in the repository, try figuring out why?

};
Blocks: Record<string, never>;
}

export default class GleamLogoComponent extends Component<GleamLogoSignature> {
@tracked riveInstance: Rive | null = null;
container: HTMLElement | null = null;

Check warning on line 18 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L18

Added line #L18 was not covered by tests
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Add unit tests for this component.

The component is completely missing test coverage according to static analysis. Consider adding unit tests that verify the initialization, animation behaviors, and cleanup processes.

I can help you implement comprehensive tests for this component that verify:

  1. Component rendering
  2. Animation initialization and event handling
  3. Cleanup processes

Would you like me to provide a test implementation?


🌐 Web query:

How to write unit tests for Ember components with Rive animations?

💡 Result:

Writing Unit Tests for Ember Components with Rive Animations

Overview

Testing Ember components that embed Rive animations requires a combination of standard Ember component testing strategies and considerations specific to third-party rendering handled by Rive. Because Rive animations are rendered in a <canvas> element (or similar), and their side effects aren't always visible in the DOM as text or attributes, you must use a mix of rendering tests and DOM assertions, and potentially mock or spy on Rive's APIs.


Recommended Approach

1. Use Rendering or Integration Tests

While "unit tests" in Ember often refer to logic-only tests, components—especially those outputting dynamic UI like Rive animations—are best tested with rendering (also known as integration) tests. These let you render the component, interact with it, and assert on the DOM or called behaviors[1][5][7][8].

  • Setup using setupRenderingTest(hooks) in your test module.
  • Use the render helper to instantiate your component in the test environment.
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';

module('Integration | Component | my-rive-component', function(hooks) {
  setupRenderingTest(hooks);

  test('it renders the canvas for Rive', async function(assert) {
    await render(hbs`<MyRiveComponent />`);
    assert.dom('canvas').exists('Rive canvas is rendered');
  });
});

[1][5][7]


2. Mock Rive Animation Logic

External libraries like Rive often require DOM interaction and asynchronous file loading. To keep your tests fast and reliable:

  • Stub or Mock Rive: Mock the global Rive constructor or inject a mock via Ember's dependency injection. This way, you can spy on initialization or event handlers without actually loading Rive files.

    • For example, if your component instantiates new Rive({ ... }), mock this in your test so you can assert it was called with the expected props.
// In your test setup
let riveInstanceMock = {
  on: sinon.spy(),
  play: sinon.spy(),
  pause: sinon.spy()
};

window.Rive = sinon.stub().returns(riveInstanceMock);
  • Assert event handlers: If your component listens for Rive events (like on(EventType.RiveEvent, handler)), you can simulate these events in your test by calling the handler directly and asserting component behavior[6].

3. Assert DOM and Behavioral Effects

  • Check that the correct canvas or container is rendered.
  • If your UI reacts to Rive events (for example, on animation completion), simulate those events and verify the correct DOM changes or actions are triggered.
test('it reacts to Rive animation event', async function(assert) {
  await render(hbs`<MyRiveComponent />`);

  // Simulate event
  riveInstanceMock.on.withArgs('riveevent').callArgWith(1, { data: { name: 'someEvent' } });

  // Assert component behavior
  assert.dom('.some-result').hasText('Expected Result');
});

4. Handle Asynchronous Behavior

Many Rive interactions (loading, animation events) are async. Use async/await in your test functions and await any helpers or simulated event triggers[7].


5. Tips and Good Practices

  • Only assert what your component is responsible for (e.g., the canvas is rendered, specific event handlers are registered, or the component reacts to Rive events).
  • Avoid dependence on actual Rive files or network requests.
  • Use Ember’s pauseTest() helper if you need to manually inspect DOM output during test development[7].

Example Workflow

Purpose Test Method
Canvas is rendered Assert with assert.dom('canvas').exists()
Rive is initialized Mock Rive and assert initialization parameters
Animation event fires Simulate event, assert component reaction
UI updates after event Assert DOM updates or action calls

Summary

  • Use rendering (integration) tests for Ember components with Rive.
  • Mock Rive APIs to avoid real animation/network dependencies.
  • Simulate Rive events and assert component reactions.
  • Focus assertions on rendered elements and component behavior, not Rive’s internal rendering[1][5][6][7].

This test strategy ensures that your components work as intended, even when integrating complex, third-party animation libraries like Rive.

Citations:


Add integration tests for GleamLogoComponent

The GleamLogoComponent currently has no test coverage. To ensure its Rive‑based animation logic works as intended, add an integration (rendering) test that:

  • Sets up the rendering test environment (setupRenderingTest(hooks))
  • Renders the component via await render(hbs)
  • Asserts that a <canvas> (or designated container) is inserted into the DOM
  • Stubs or mocks the global Rive constructor:
    • e.g.
      let riveMock = { on: sinon.spy(), play: sinon.spy(), pause: sinon.spy() };
      window.Rive = sinon.stub().returns(riveMock);
  • Verifies new Rive({...}) is invoked with the expected init options (canvas element, autoplay, etc.)
  • Simulates a Rive event via riveMock.on.callArgWith(1, { data: { name: '…' } }) and asserts the component updates accordingly (DOM changes or tracked‐property updates)
  • Confirms cleanup: after destroying the component, ensure any Rive playback was stopped or the instance reference cleared

Feel free to follow Ember’s testing guide for rendering tests and let me know if you’d like a full example test implementation.

animationInterval: number | null = null;

@action
cleanupRive() {
if (this.animationInterval) {
clearInterval(this.animationInterval);
this.animationInterval = null;
}

if (this.riveInstance) {
this.riveInstance.stop();
this.riveInstance = null;
}
}

@action
handleMouseEnter() {
if (this.riveInstance) {
const stateMachines = this.riveInstance.stateMachineNames;

if (stateMachines && stateMachines.length > 0) {
const stateMachineName = 'State Machine 1';

if (stateMachines.includes(stateMachineName)) {
this.riveInstance.reset();
this.riveInstance.play(stateMachineName);
}
}
}
}

@action
handleMouseLeave() {
if (this.riveInstance) {
this.riveInstance.stop();
this.riveInstance.reset();
}

Check warning on line 55 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L54-L55

Added lines #L54 - L55 were not covered by tests
}

@action
setupRive(element: HTMLDivElement) {
this.container = element;

try {
const canvas = document.createElement('canvas');
canvas.width = 141;
canvas.height = 144;
canvas.style.width = '100%';
canvas.style.height = '100%';
element.appendChild(canvas);

this.riveInstance = new Rive({
src: '/assets/animations/gleam_logo_animation.riv',
canvas: canvas,
autoplay: true,
automaticallyHandleEvents: true,
onLoad: () => {
// Set up interval to play animation every 15 seconds
this.animationInterval = window.setInterval(() => {
if (this.riveInstance) {
const stateMachines = this.riveInstance.stateMachineNames;

Check warning on line 80 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L80

Added line #L80 was not covered by tests
if (stateMachines && stateMachines.length > 0) {
const stateMachineName = 'State Machine 2';

Check warning on line 83 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L83

Added line #L83 was not covered by tests
if (stateMachines.includes(stateMachineName)) {
this.riveInstance.reset();
this.riveInstance.play(stateMachineName);
}

Check warning on line 87 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L86-L87

Added lines #L86 - L87 were not covered by tests
}
}

// Simulate mouse leave after a short delay
setTimeout(() => {
this.handleMouseLeave();
}, 2000);

Check warning on line 94 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L93-L94

Added lines #L93 - L94 were not covered by tests
}, 15000);
// Play initial animation with State Machine 2
setTimeout(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does autoplay not cover this? And it not, what does autoplay do?

if (this.riveInstance) {
const stateMachines = this.riveInstance.stateMachineNames;

Check warning on line 100 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L100

Added line #L100 was not covered by tests
if (stateMachines && stateMachines.length > 0) {
const stateMachine2 = 'State Machine 2';

Check warning on line 103 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L103

Added line #L103 was not covered by tests
if (stateMachines.includes(stateMachine2)) {
this.riveInstance.reset();
this.riveInstance.play(stateMachine2);

Check warning on line 107 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L106-L107

Added lines #L106 - L107 were not covered by tests
// Then after 800ms, reset State Machine 2
setTimeout(() => {
if (this.riveInstance) {

Check warning on line 110 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L110

Added line #L110 was not covered by tests
this.riveInstance.reset();
}

Check warning on line 112 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L112

Added line #L112 was not covered by tests
}, 800);
}
}
}
}, 500);
},
});
} catch (error: unknown) {
console.error('Error setting up Rive:', error);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's report this to sentry, don't just swallow and ignore

}

Check warning on line 122 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L122

Added line #L122 was not covered by tests
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Implement better error handling for Rive initialization failures.

Currently, errors are only logged to the console, which isn't sufficient for production environments. Consider sending these errors to a monitoring service or providing a fallback UI.


🌐 Web query:

Does Ember have a built-in error reporting service or recommended error handling pattern for components?

💡 Result:

Ember Error Reporting and Error Handling Patterns

Ember does not provide a built-in error reporting service directly integrated into the framework, but it does offer several built-in hooks and patterns for handling errors in components and across the application.

Global Error Handling

Ember provides a global error handling mechanism through the Ember.onerror hook. This function catches uncaught errors thrown anywhere within your Ember application, allowing you to implement custom logic for logging, user notification, or reporting to external services. For promise-related errors, Ember exposes the Ember.RSVP.on('error', handler) hook, which allows you to catch unhandled promise rejections globally[2][6].

Recommended Error Handling Patterns

  • Ember.onerror: Define a global error handler to catch and process uncaught exceptions. This can be used for logging and reporting errors.
  • RSVP Promise Errors: Use Ember.RSVP.configure('onerror', handler) to capture errors from unhandled promise rejections[2][6].
  • Route-Specific Errors: Handle errors in route actions such as the error action within a route, which will be triggered when errors occur during transitions.
  • Try-Catch Blocks: Use try-catch blocks in your methods or actions for more granular error handling, especially in asynchronous code[6].
  • Display User-Friendly Messages: Normalize errors and display clear, actionable error messages to users.

Component Error Handling

While Ember does not offer component-level error “boundaries” like React, you can use try-catch logic within component actions or lifecycle hooks. For more advanced use, some community addons and patterns facilitate error propagation and management within components[6].

Error Reporting

For reporting errors to external services, Ember supports integration with many third-party error monitoring platforms, such as Sentry, Rollbar, and Airbrake:

  • Sentry: Official Sentry SDK for Ember (@sentry/ember) automatically captures uncaught errors and exceptions. After installing the addon, you initialize Sentry early in your application’s startup file, and errors will be reported automatically[3][8]. You can also manually report errors using Sentry.captureException() within your code[4].
  • Other Services: Platforms like Rollbar, Errorception, and ExceptionHub are also compatible with Ember via global hooks[2].

Community Addons

There are several Ember addons to enhance error handling and reporting. For example, ember-error-handler centralizes error capturing and lets you route errors to logging services, custom error pages, or whatever logic you need[1][9].

Summary Table: Ember Error Handling Approaches

Pattern/Tool Purpose Built-in or Addon Usage
Ember.onerror Catch uncaught errors app-wide Built-in Global handler for logging/reporting
RSVP.on('error') Handle uncaught promise rejections Built-in Promise rejection handling
Route error action Catch errors in specific route contexts Built-in Show route-level error UI
Sentry/Rollbar Report errors to external services Third-party addon Error monitoring and alerting
ember-error-handler Centralize and customize error handling Community addon Custom logic, UI, or remote logging

Conclusion

Ember does not have an all-in-one built-in error reporting service, but it provides robust primitives (Ember.onerror, RSVP hooks, and route error actions) for handling and customizing error behavior. For error reporting, the Ember ecosystem supports seamless integration with tools like Sentry via official and community-maintained addons, and there are established patterns for routing errors and providing user feedback in components and across the app[2][6][8][9].

Citations:


Enhance Rive Initialization Error Handling

Ember doesn’t include a turnkey error-reporting service, but you can hook into its global error APIs (or integrate Sentry/Rollbar) and fall back to a safe UI when initialization fails.

• Location to address:
– app/components/gleam-logo.ts (lines 96–98)

• Suggested changes:

     } catch (error: unknown) {
-      console.error('Error setting up Rive:', error);
+      // 1) Report the error globally or to an external service
+      Ember.onerror?.(error);
+      // If using Sentry add-on:
+      // import { captureException } from '@sentry/ember';
+      // captureException(error);
+
+      // 2) Trigger a fallback UI state
+      this.hasLoadError = true;
     }

• Next steps:

  1. Add @tracked hasLoadError = false; in the component.
  2. Update the template to render a static logo (or message) when hasLoadError is true.
  3. Initialise and configure your chosen monitoring addon (e.g. @sentry/ember) and/or implement an Ember.onerror hook in app/app.js for centralized reporting.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error: unknown) {
console.error('Error setting up Rive:', error);
}
} catch (error: unknown) {
// 1) Report the error globally or to an external service
Ember.onerror?.(error);
// If using Sentry add-on:
// import { captureException } from '@sentry/ember';
// captureException(error);
// 2) Trigger a fallback UI state
this.hasLoadError = true;
}

}
}
4 changes: 3 additions & 1 deletion app/components/language-logo.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{{#if (eq @variant "color")}}
{{#if (and (eq @language.slug "gleam") (eq @variant "color"))}}
<GleamLogo ...attributes />
{{else if (eq @variant "color")}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Arpan-206 let's not assume that every color language logo usage of Gleam is going to be animated for now - let's keep this explicit (use GleamLogo instead of LanguageLogo in the tracks template).

You can imagine using a color logo in things like user profile pages where we might not want this animation to occur

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Arpan-206 this hasn't been addressed

<img alt={{@language.name}} src="{{@language.colorLogoUrl}}" ...attributes />
{{else if (eq @variant "gray")}}
<img alt={{@language.name}} src="{{@language.grayLogoUrl}}" ...attributes />
Expand Down
1 change: 1 addition & 0 deletions app/components/language-logo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ export default class LanguageLogoComponent extends Component<Signature> {}
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
LanguageLogo: typeof LanguageLogoComponent;
GleamLogo: typeof import('./gleam-logo').default;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Arpan-206 why is this registered here and not in gleam-logo.ts (like every other component is)?

}
}
12 changes: 12 additions & 0 deletions app/styles/components/gleam-logo.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.gleam-logo-container {
display: block;
width: 100%;
height: 100%;
}

.gleam-logo-container canvas {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@
},
"dependencies": {
"@rails/actioncable": "^8.0.200",
"@rive-app/canvas": "^2.27.0",
"@stripe/stripe-js": "^5.5.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.16",
Expand Down
Binary file added public/assets/animations/gleam_logo_animation.riv
Binary file not shown.
Loading