-
Notifications
You must be signed in to change notification settings - Fork 18
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
base: main
Are you sure you want to change the base?
Changes from 10 commits
52b2af2
6859dfc
175ee69
c864859
c4ab10f
ea04fe0
a20cf9f
6cc3d5b
0335cf1
72c9864
4ecb22e
0ac1be3
cf62de1
1d67c55
d007423
88fa7b8
257025c
e951505
d798652
8052924
8603f7e
5853d88
53f6401
711a877
733209d
63cdade
f6bfef5
a9d58af
662128f
1ea7dda
2b86774
ad8dbb2
75d66f3
9c91bd9
bac2acc
8e9e0d0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<div | ||
class="gleam-logo-container" | ||
{{did-insert this.setupRive}} | ||
{{will-destroy this.cleanupRive}} | ||
{{on "mouseenter" this.handleMouseEnter}} | ||
{{on "mouseleave" this.handleMouseLeave}} | ||
> | ||
{{! The canvas will be inserted here by Rive }} | ||
</div> |
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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||||||||||||||||||||||||||||||||||||||||||
style?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
[key: string]: unknown; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Arpan-206 what is this used for? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. well the first one, forgot to remove the signature There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainAdd 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:
Would you like me to provide a test implementation? 🌐 Web query:
💡 Result: Writing Unit Tests for Ember Components with Rive AnimationsOverview 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 Recommended Approach 1. Use Rendering or Integration TestsWhile "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].
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 LogicExternal libraries like Rive often require DOM interaction and asynchronous file loading. To keep your tests fast and reliable:
// In your test setup
let riveInstanceMock = {
on: sinon.spy(),
play: sinon.spy(),
pause: sinon.spy()
};
window.Rive = sinon.stub().returns(riveInstanceMock);
3. Assert DOM and Behavioral Effects
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 BehaviorMany Rive interactions (loading, animation events) are async. Use 5. Tips and Good Practices
Example Workflow
Summary
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
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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
rohitpaulk marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
@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(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
@action | ||||||||||||||||||||||||||||||||||||||||||||||||||||
setupRive(element: HTMLDivElement) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
this.container = element; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
const canvas = document.createElement('canvas'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
canvas.width = 141; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
canvas.height = 144; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
canvas.style.width = '100%'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Arpan-206 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
canvas.style.height = '100%'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Arpan-206 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Arpan-206 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
this.animationInterval = window.setInterval(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
if (this.riveInstance) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
const stateMachines = this.riveInstance.stateMachineNames; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
if (stateMachines && stateMachines.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
const stateMachineName = 'State Machine 2'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
if (stateMachines.includes(stateMachineName)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
this.riveInstance.reset(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
this.riveInstance.play(stateMachineName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
// Simulate mouse leave after a short delay | ||||||||||||||||||||||||||||||||||||||||||||||||||||
setTimeout(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
this.handleMouseLeave(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
}, 2000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
}, 15000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
// Play initial animation with State Machine 2 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
setTimeout(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does |
||||||||||||||||||||||||||||||||||||||||||||||||||||
if (this.riveInstance) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
const stateMachines = this.riveInstance.stateMachineNames; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
if (stateMachines && stateMachines.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
const stateMachine2 = 'State Machine 2'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
if (stateMachines.includes(stateMachine2)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
this.riveInstance.reset(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
this.riveInstance.play(stateMachine2); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
// Then after 800ms, reset State Machine 2 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
setTimeout(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
if (this.riveInstance) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
this.riveInstance.reset(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
}, 800); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
}, 500); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} catch (error: unknown) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
console.error('Error setting up Rive:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's report this to sentry, don't just swallow and ignore |
||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainImplement 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:
💡 Result: Ember Error Reporting and Error Handling PatternsEmber 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 Recommended Error Handling Patterns
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:
Community Addons There are several Ember addons to enhance error handling and reporting. For example, Summary Table: Ember Error Handling Approaches
ConclusionEmber does not have an all-in-one built-in error reporting service, but it provides robust primitives ( 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: • 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:
📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} |
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")}} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 You can imagine using a color logo in things like user profile pages where we might not want this animation to occur There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 /> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Arpan-206 why is this registered here and not in |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
.gleam-logo-container { | ||
Arpan-206 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
display: block; | ||
width: 100%; | ||
height: 100%; | ||
} | ||
|
||
.gleam-logo-container canvas { | ||
display: block; | ||
width: 100%; | ||
height: 100%; | ||
object-fit: contain; | ||
Arpan-206 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
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.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
noted!