Skip to content
Open
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
2 changes: 0 additions & 2 deletions apps/playground/server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import 'zone.js/node';

import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { join } from 'path';
Expand Down
8 changes: 3 additions & 5 deletions apps/playground/src/environments/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ export const environment = {
};

/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* For easier debugging in development mode, you can import the following file.
* This import should remain commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
// import 'zone.js/plugins/zone-error';
4 changes: 2 additions & 2 deletions apps/playground/src/polyfills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@
*/

/***************************************************************************************************
* Zone JS is required by default for Angular itself.
* Zone JS is no longer required with Angular 20.
*/
import 'zone.js'; // Included with Angular CLI.
// import 'zone.js'; // Included with Angular CLI.

/***************************************************************************************************
* APPLICATION IMPORTS
Expand Down
8 changes: 3 additions & 5 deletions apps/recorder/src/environments/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ export const environment = {
};

/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* For easier debugging in development mode, you can import the following file.
* This import should remain commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
// import 'zone.js/plugins/zone-error';
4 changes: 2 additions & 2 deletions apps/recorder/src/polyfills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@
*/

/***************************************************************************************************
* Zone JS is required by default for Angular itself.
* Zone JS is no longer required with Angular 20.
*/
import 'zone.js'; // Included with Angular CLI.
// import 'zone.js'; // Included with Angular CLI.

/***************************************************************************************************
* APPLICATION IMPORTS
Expand Down
6 changes: 4 additions & 2 deletions libs/ng-rive/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,12 @@ import { RiveModule, RIVE_FOLDER } from 'ng-rive';
export class MyModule { }
```

4. Use in template :
4. Use in template :
```html
<canvas riv="knight" width="500" height="500">
<riv-animation name="idle" play></riv-animation>
@if (playing()) {
<riv-animation name="idle" play></riv-animation>
}
</canvas>
```

Expand Down
6 changes: 3 additions & 3 deletions libs/ng-rive/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
"name": "ng-rive",
"version": "0.3.0",
"peerDependencies": {
"@angular/common": ">13.0.0",
"@angular/core": ">13.0.0",
"@angular/common": ">=20.0.0",
"@angular/core": ">=20.0.0",
"rxjs": ">7.0.0"
},
"dependencies": {
"@rive-app/canvas-advanced": "2.1.0",
"@rive-app/canvas-advanced": "^2.31.1",
"tslib": "^2.0.0"
},
"keywords": [
Expand Down
41 changes: 17 additions & 24 deletions libs/ng-rive/src/lib/animation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Directive, EventEmitter, Input, NgZone, OnDestroy, Output } from "@angular/core";
import { BehaviorSubject, of, Subscription } from "rxjs";
import { Directive, EventEmitter, Input, OnDestroy, Output, inject, signal } from "@angular/core";
import { of, Subscription } from "rxjs";
import { filter, map, switchMap } from "rxjs/operators";
import { toObservable } from '@angular/core/rxjs-interop';
import { RiveCanvas } from './canvas';
import { RiveService } from "./service";
import type { Artboard, LinearAnimationInstance, LinearAnimation } from "@rive-app/canvas-advanced";
Expand Down Expand Up @@ -47,10 +48,11 @@ function assertAnimation(animation: LinearAnimation, artboard: Artboard, name: s
standalone: true
})
export class RiveLinearAnimation implements OnDestroy {
private canvas = inject(RiveCanvas);
private service = inject(RiveService);
private sub?: Subscription;
private instance?: LinearAnimationInstance;
distance = new BehaviorSubject<number | null>(null);
state = new BehaviorSubject<RiveAnimationState>(getRiveAnimationState());
state = signal<RiveAnimationState>(getRiveAnimationState());

/**
* Name of the rive animation in the current Artboard
Expand All @@ -59,9 +61,7 @@ export class RiveLinearAnimation implements OnDestroy {
@Input()
set name(name: string | undefined | null) {
if (typeof name !== 'string') return;
this.zone.runOutsideAngular(() => {
this.register(name);
});
this.register(name);
}

/**
Expand All @@ -72,19 +72,17 @@ export class RiveLinearAnimation implements OnDestroy {
set index(value: number | string | undefined | null) {
const index = typeof value === 'string' ? parseInt(value) : value;
if (typeof index !== 'number') return;
this.zone.runOutsideAngular(() => {
this.register(index);
});
this.register(index);
}

/** The mix of this animation in the current arboard */
@Input()
set mix(value: number | string | undefined | null) {
const mix = typeof value === 'string' ? parseFloat(value) : value;
const mix = typeof value === 'string' ? parseFloat(value) : value;
if (mix && mix >= 0 && mix <= 1) this.update({ mix });
}
get mix() {
return this.state.getValue().mix;
return this.state().mix;
}

/** Multiplicator for the speed of the animation */
Expand All @@ -94,7 +92,7 @@ export class RiveLinearAnimation implements OnDestroy {
if (typeof speed === 'number') this.update({ speed });
}
get speed() {
return this.state.getValue().speed;
return this.state().speed;
}

/** If true, this animation is playing */
Expand All @@ -106,17 +104,13 @@ export class RiveLinearAnimation implements OnDestroy {
}
}
get play() {
return this.state.getValue().playing;
return this.state().playing;
}

/** Emit when the LinearAnimation has been instantiated */
@Output() load = new EventEmitter<LinearAnimationInstance>();

constructor(
private zone: NgZone,
private canvas: RiveCanvas,
private service: RiveService,
) {}
constructor() {}

ngOnDestroy() {
this.sub?.unsubscribe();
Expand All @@ -125,13 +119,12 @@ export class RiveLinearAnimation implements OnDestroy {
}

private update(state: Partial<RiveAnimationState>) {
const next = getRiveAnimationState({...this.state.getValue(), ...state })
this.state.next(next);
this.state.update(s => ({...s, ...state }));
}

private getFrame(state: RiveAnimationState) {
if (state.playing && this.service.frame) {
return this.service.frame.pipe(map((time) => [state, time] as const));
return toObservable(this.service.frame).pipe(map((time) => [state, time] as const));
} else {
return of(null)
}
Expand All @@ -155,7 +148,7 @@ export class RiveLinearAnimation implements OnDestroy {
this.sub?.unsubscribe();

// Update on frame change if playing
const onFrameChange = this.state.pipe(
const onFrameChange = toObservable(this.state).pipe(
switchMap((state) => this.getFrame(state)),
filter(exist),
map(([state, time]) => (time / 1000) * state.speed),
Expand All @@ -170,7 +163,7 @@ export class RiveLinearAnimation implements OnDestroy {

private applyChange(delta: number) {
if (!this.instance) throw new Error('Could not load animation instance before running it');
this.canvas.draw(this.instance, delta, this.state.getValue().mix);
this.canvas.draw(this.instance, delta, this.state().mix);
}

}
37 changes: 22 additions & 15 deletions libs/ng-rive/src/lib/canvas.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Directive, ElementRef, EventEmitter, HostListener, Input, NgZone, OnDestroy, OnInit, Output } from '@angular/core';
import { Directive, ElementRef, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output, inject } from '@angular/core';
import { Observable, BehaviorSubject, from } from 'rxjs';
import { distinctUntilChanged, filter, map, shareReplay, switchMap, tap } from 'rxjs/operators';
import { RiveService } from './service';
Expand Down Expand Up @@ -36,38 +36,28 @@ const onVisible = (element: HTMLElement) => new Promise<boolean>((res, rej) => {
});


// Force event to run inside zones
export function enterZone(zone: NgZone) {
return <T>(source: Observable<T>) =>
new Observable<T>(observer =>
source.subscribe({
next: (x) => zone.run(() => observer.next(x)),
error: (err) => observer.error(err),
complete: () => observer.complete()
})
);
}

@Directive({
selector: 'canvas[riv]',
exportAs: 'rivCanvas',
standalone: true
})
export class RiveCanvas implements OnInit, OnDestroy {
private service = inject(RiveService);
private element = inject<ElementRef<HTMLCanvasElement>>(ElementRef);
private url = new BehaviorSubject<RiveOrigin>(null);
private arboardName = new BehaviorSubject<string | null>(null);
private _ctx?: CanvasRenderingContext2D | null;
private loaded: Observable<boolean>;
private boxes: Record<string, AABB> = {};
public canvas: HTMLCanvasElement;
public canvas: HTMLCanvasElement = this.element.nativeElement;
public rive?: Rive;
public file?: RiveFile;
public artboard?: Artboard;
public renderer?: CanvasRenderer;
// Keep track of current state machine for event listeners
public stateMachines: Record<string, StateMachineInstance> = {};

public whenVisible: Promise<boolean>;
public whenVisible: Promise<boolean> = onVisible(this.canvas);

@Input() set riv(url: RiveOrigin) {
this.url.next(url);
Expand Down Expand Up @@ -102,6 +92,23 @@ export class RiveCanvas implements OnInit, OnDestroy {

@Output() artboardChange = new EventEmitter<Artboard>();

constructor() {
this.loaded = this.url.pipe(
filter(exist),
distinctUntilChanged(),
filter(() => typeof window !== 'undefined' && !!this.ctx),
switchMap(async (url) => {
this.file = await this.service.load(url);
this.rive = this.service.rive;
if (!this.rive) throw new Error('Service could not load rive');
// TODO: set offscreen renderer to true for webgl
this.renderer = this.rive.makeRenderer(this.canvas) as CanvasRenderer;
}),
switchMap(_ => this.setArtboard()),
shareReplay({ bufferSize: 1, refCount: true })
);
}


@HostListener('touchmove', ['$event'])
@HostListener('mouseover', ['$event'])
Expand Down
7 changes: 1 addition & 6 deletions libs/ng-rive/src/lib/component/bone.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Directive, Input, NgZone } from '@angular/core';
import { RiveCanvas } from '../canvas';
import { Directive, Input } from '@angular/core';
import { RiveTransformComponent } from './transform-component';
import { Bone } from '@rive-app/canvas-advanced';

Expand All @@ -14,10 +13,6 @@ export class RiveBone extends RiveTransformComponent<Bone> {
this.set('length', value);
}

constructor(zone: NgZone, canvas: RiveCanvas) {
super(zone, canvas);
}

getComponent(name: string) {
return this.canvas.artboard?.bone(name);
}
Expand Down
7 changes: 1 addition & 6 deletions libs/ng-rive/src/lib/component/node.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Directive, Input, NgZone } from '@angular/core';
import { RiveCanvas } from '../canvas';
import { Directive, Input } from '@angular/core';
import { RiveTransformComponent } from './transform-component';
import { Node } from '@rive-app/canvas-advanced';

Expand All @@ -25,10 +24,6 @@ export class RiveNode extends RiveTransformComponent<Node> {
return this.component?.y;
}

constructor(zone: NgZone, canvas: RiveCanvas) {
super(zone, canvas);
}

getComponent(name: string) {
return this.canvas.artboard?.node(name);
}
Expand Down
7 changes: 1 addition & 6 deletions libs/ng-rive/src/lib/component/root-bone.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Directive, Input, NgZone } from '@angular/core';
import { RiveCanvas } from '../canvas';
import { Directive, Input } from '@angular/core';
import { RiveTransformComponent } from './transform-component';
import { RootBone } from '@rive-app/canvas-advanced';

Expand All @@ -22,10 +21,6 @@ export class RiveRootBone extends RiveTransformComponent<RootBone> {
this.set('length', value);
}

constructor(zone: NgZone, canvas: RiveCanvas) {
super(zone, canvas);
}

getComponent(name: string) {
return this.canvas.artboard?.rootBone(name);
}
Expand Down
20 changes: 8 additions & 12 deletions libs/ng-rive/src/lib/component/transform-component.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Directive, Input, NgZone } from '@angular/core';
import { Directive, Input, inject } from '@angular/core';
import { RiveCanvas } from '../canvas';
import { TransformComponent } from '@rive-app/canvas-advanced';

@Directive()
export abstract class RiveTransformComponent<T extends TransformComponent> {
protected canvas = inject(RiveCanvas);
protected component?: T;
protected state: Partial<T> = {};

Expand Down Expand Up @@ -39,20 +40,15 @@ export abstract class RiveTransformComponent<T extends TransformComponent> {
}
}

constructor(
private zone: NgZone,
protected canvas: RiveCanvas
) {}
constructor() {}

abstract getComponent(name: string): T | undefined;

protected set(key: keyof T, value: number | string | null | undefined) {
this.zone.runOutsideAngular(() => {
const v = typeof value === 'string' ? parseFloat(value) : value;
if (typeof v === 'number') {
if (this.component) this.component[key] = v as any;
else this.state[key] = v as any;
}
});
const v = typeof value === 'string' ? parseFloat(value) : value;
if (typeof v === 'number') {
if (this.component) this.component[key] = v as any;
else this.state[key] = v as any;
}
}
}
Loading