Skip to content

Commit

Permalink
Changes to Emote Popups and Animation Component (#36)
Browse files Browse the repository at this point in the history
* Emote Popup Enhancements
Made Emotes jump higher on average
Added emote zone that adjusts amounts of emotes displayed based on usage

* Overhauled animation component
Duration is now in ms
Display waits for content loaded
Supports image and video types
  • Loading branch information
Tiranthine authored Apr 4, 2024
1 parent e1cca0b commit 81c97e9
Show file tree
Hide file tree
Showing 11 changed files with 231 additions and 53 deletions.
5 changes: 5 additions & 0 deletions src/app/animation/animation-host.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div class="animation-display">
<div class="vertical">
<ng-template animationSpace></ng-template>
</div>
</div>
55 changes: 55 additions & 0 deletions src/app/animation/animation-host.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Component, Directive, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { BotConnectorService } from '../services/bot-connector.service';
import { AnimationComponent } from './animation.component';

@Directive({
selector: '[animationSpace]',
})
export class AnimationHostDirective {
constructor(public viewContainerRef: ViewContainerRef) { }
}

@Component({
selector: 'app-animation-host',
templateUrl: './animation-host.component.html',
styleUrls: ['./animation.component.scss']
})
export class AnimationHostComponent implements OnInit {

@ViewChild(AnimationHostDirective, { static: true }) animationSpace!: AnimationHostDirective;

constructor(private botService: BotConnectorService) { }

ngOnInit(): void {
this.botService.getStream('streamdeck').subscribe(data => {
if (data.type === "animation") {

const viewContainerRef = this.animationSpace.viewContainerRef;
const componentRef = viewContainerRef.createComponent<AnimationComponent>(AnimationComponent);

let source = data.source;

//typescript compiler is drunk, just leave it
if ("canParse" in URL && typeof URL.canParse == "function") {
if (URL.canParse(source)) {
let url = new URL(source);
//adding a random bit of get query so the browser doesn't try to cache our stuff
url.searchParams.append("random", Date.now().toString());
source = url.href;
}

}

componentRef.instance.source = source;
componentRef.instance.contentLoadedCallack = function() {
setTimeout(() => {
componentRef.destroy();
}, data.duration);
}
}
});
}

}


13 changes: 6 additions & 7 deletions src/app/animation/animation.component.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<ng-container *ngIf="display">
<div class="animation-display">
<div class="vertical">
<img class="animation" src="{{source}}"/>
</div>
</div>
</ng-container>
<div class="">
<img [hidden]="!image" class="animation" src="{{source}}" (load)="_imageLoaded()">
<video #videoPlayer [hidden]="!video" class="animation" autoplay muted>
<source src="{{source}}">
</video>
</div>
51 changes: 36 additions & 15 deletions src/app/animation/animation.component.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,48 @@
import { Component, OnInit } from '@angular/core';
import { BotConnectorService } from '../services/bot-connector.service';
import { AfterViewInit, Component, ElementRef, OnDestroy, ViewChild, ViewContainerRef } from '@angular/core';

@Component({
selector: 'app-animation',
templateUrl: './animation.component.html',
styleUrls: ['./animation.component.scss']
})
export class AnimationComponent implements OnInit {
display: boolean = false;
export class AnimationComponent implements OnDestroy, AfterViewInit {
source: string = "";
image = false;
video = false;
playing = false;

constructor(private botService: BotConnectorService) { }
contentLoadedCallack!: Function;

ngOnInit(): void {
this.botService.getStream('streamdeck').subscribe(data => {
if (data.type === "animation") {
this.display = true;
this.source = data.source;
@ViewChild('videoPlayer') videoPlayer!: ElementRef;

setTimeout(() => {
this.display = false;
}, data.duration * 1000);
}
});
constructor(public viewContainerRef: ViewContainerRef) { }

ngAfterViewInit(): void {
this.videoPlayer.nativeElement.onloadeddata = this._videoLoaded.bind(this);
}

ngOnDestroy(): void {
this.playing = false;
this.image = false;
this.video = false;
this.source = "";
}

_imageLoaded() {
if (this.playing) return;
this.playing = true;
this.image = true;
this.contentLoaded();
}

_videoLoaded() {
if (this.playing) return;
this.playing = true;
this.video = true;
this.contentLoaded();
}

contentLoaded(): void {
this.contentLoadedCallack();
}
}
5 changes: 5 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ import { SpotifyComponent } from './spotify-nowplaying/spotify-nowplaying.compon
import { EmotePopupsComponent } from './emote-popups/emote-popups.component';
import { EmotePopupsDirective } from './emote-popups/emote-popups-directive';
import { EmotePopupIconComponent } from './emote-popup-icon/emote-popup-icon.component';
import { EmotePopupsComponentMultiple } from './emote-popups/subclasses/emote-popups-multiple.component';
import { AnimationHostComponent, AnimationHostDirective } from './animation/animation-host.component';

@NgModule({
declarations: [
Expand Down Expand Up @@ -83,11 +85,14 @@ import { EmotePopupIconComponent } from './emote-popup-icon/emote-popup-icon.com
ConnectFourComponent,
TeamLogoComponent,
AnimationComponent,
AnimationHostComponent,
AnimationHostDirective,
AiChatInteractorComponent,
ChatMessageComponent,
ViewerComponent,
SpotifyComponent,
EmotePopupsComponent,
EmotePopupsComponentMultiple,
EmotePopupsDirective,
EmotePopupIconComponent
],
Expand Down
2 changes: 1 addition & 1 deletion src/app/emote-popup-icon/emote-popup-icon.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class EmotePopupIconComponent implements OnInit, AfterViewInit {
*/
getRandomJumpHeight(): number {
var x = Math.random() * 100;
x = Math.min(x, 70); //70 is minimum jump height, otherwise it looks bad
x = Math.min(x, 50); //70 is minimum jump height, otherwise it looks bad
return Math.round(x);
}
}
80 changes: 60 additions & 20 deletions src/app/emote-popups/emote-popups.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ export class EmotePopupsComponent implements OnInit {
public static isTesting: boolean = false;

static emojiRegex = new RegExp(/\p{Emoji_Presentation}/gu);
constructor(private botService: BotConnectorService, private chatProcessorService: ChatProcessorService) { }
constructor(private botService: BotConnectorService, private chatProcessorService: ChatProcessorService) {
this.defaultEmoteProperties = new EmoteProperties();
}

@ViewChild(EmotePopupsDirective, { static: true }) emotePopupSpace!: EmotePopupsDirective;

Expand All @@ -27,11 +29,8 @@ export class EmotePopupsComponent implements OnInit {
// Whether chat messages should currently trigger emote jumps
protected enabled: boolean = false;

// Jump duration in ms
protected duration: number = 1500;
protected defaultEmoteProperties: EmoteProperties;

// the size of an emote blip in px (square) //a size of 40 or smaller is recommended
protected size: number = 40;
ngOnInit(): void {

this.botService.getStream("streamdeck").subscribe(data => {
Expand All @@ -43,16 +42,16 @@ export class EmotePopupsComponent implements OnInit {
}

if (data.duration != null) {
this.duration = data.duration;
this.defaultEmoteProperties.duration = data.duration;
}

if (data.emoteSize != null) {
this.size = data.emoteSize;
this.defaultEmoteProperties.size = data.emoteSize;
}

if (data.amount != null) {
for (let index = 0; index < data.amount; index++) {
this.createPopupEmote({imageUrl: "assets/cool.webp"});
this.createPopupEmote(structuredClone(this.defaultEmoteProperties));
}
}
}
Expand All @@ -62,48 +61,89 @@ export class EmotePopupsComponent implements OnInit {
if (!this.enabled) return;
if (data.content.length > 200) return;

var emoteProperties = structuredClone(this.defaultEmoteProperties);

// creates a default emote from each message if we are in test mode
// done so fake messages trigger emotes to display
if (EmotePopupsComponent.isTesting) this.createPopupEmote({imageUrl: "assets/cool.webp"});
// if (EmotePopupsComponent.isTesting) this.createPopupEmote(structuredClone(emoteProperties));

const message = this.chatProcessorService.processChat(data, undefined, 0, 0);
const emotes = message.chatMessage.chunks.filter((e: ChatChunk) => {return e.type === ChatChunkType.IMG});
emoteProperties.chatData = message;

// block for identifying unicode emojis and rendering up to 4
let unicodeEmotes = [...message.content.matchAll(EmotePopupsComponent.emojiRegex)];
unicodeEmotes = unicodeEmotes.slice(0, 4);
unicodeEmotes.forEach(e => {
this.createPopupEmote({textContent: e});
emoteProperties.asset = e.toString();
emoteProperties.type = "text";
this.createPopupEmote(emoteProperties);
});

// block for rendering discord emotes
if (emotes.length > 0) {
emotes.forEach((emote: ChatChunk) => {
this.createPopupEmote({imageUrl: emote.content});
emoteProperties.asset = emote.content;
this.createPopupEmote(emoteProperties);
});
}

//block for rendering stickers
if (message.stickerURL !== "") {
this.createPopupEmote({imageUrl: message.stickerURL});
emoteProperties.asset = message.stickerURL;
this.createPopupEmote(emoteProperties);
}
});
}

createPopupEmote(assets: {imageUrl?: string, textContent?: string}): void {
createPopupEmote(properties: EmoteProperties): void {
if (this.filterEmotes(properties)) {
this.adjustEmoteProperties(properties);
this._createPopupEmoteContainer(properties);
}
}

_createPopupEmoteContainer(properties: EmoteProperties): void {
if (properties.amount <= 0) return; //recursion exit

const viewContainerRef = this.emotePopupSpace.viewContainerRef;
const componentRef = viewContainerRef.createComponent<EmotePopupIconComponent>(EmotePopupIconComponent);

if (assets.imageUrl)
componentRef.instance.imageAsset = assets.imageUrl;
else if (assets.textContent)
componentRef.instance.textAsset = assets.textContent;
if (properties.type == "img")
componentRef.instance.imageAsset = properties.asset;
else if (properties.type == "text")
componentRef.instance.textAsset = properties.asset;

componentRef.instance.direction = this.direction;
componentRef.instance.size = this.size;
componentRef.instance.duration = this.duration;
timer(this.duration).subscribe(() => {
componentRef.instance.size = properties.size;
componentRef.instance.duration = properties.duration;
timer(properties.duration).subscribe(() => {
componentRef.destroy();
});

//recursion entry
properties.amount--;
timer(properties.consecutiveDelay).subscribe(() => {
this._createPopupEmoteContainer(properties);
});
}

//return true when emote should be displayed
filterEmotes(properties: EmoteProperties): boolean {
return true;
}

adjustEmoteProperties(properties: EmoteProperties): void {}

}

export class EmoteProperties {
//default values can be set here
asset: string = "assets/cool.webp";
type: "img" | "text" = "img";
size = 80;
duration = 1500;
amount = 1;
consecutiveDelay = 200;
chatData?: any;
}
50 changes: 50 additions & 0 deletions src/app/emote-popups/subclasses/emote-popups-multiple.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Component } from '@angular/core';
import { EmotePopupsComponent, EmoteProperties } from '../emote-popups.component';

@Component({
selector: 'app-emote-popups-multiple',
templateUrl: '../emote-popups.component.html',
styleUrls: ['../emote-popups.component.scss']
})
export class EmotePopupsComponentMultiple extends EmotePopupsComponent {

protected amount = 0;
protected timeframe = 10 * 1000;
protected emoteUsages: { [id: string] : number[] } = {};

override adjustEmoteProperties(properties: EmoteProperties): void {
let amountInLastTimeframe = this.findAmountUsedLastTimeframe(properties.asset);
properties.amount = this.amountFunction(amountInLastTimeframe);
}

findAmountUsedLastTimeframe(emote: string): number {
let now = Date.now();
let latestTimestampAcceptable = now - this.timeframe;

let usages = this.emoteUsages[emote];
if (!usages) {
usages = [];
}
usages = usages.filter((e) => {
return e >= latestTimestampAcceptable;
});
let result = usages.length;

usages.push(now);
this.emoteUsages[emote] = usages;

return result;
}

amountFunction(amountInLastTimeframe: number): number {
let hist = amountInLastTimeframe;

let a = 2, b = 2, c = -0.4, d = 1, e = 1;
let result = Math.pow(a, c*hist+d) * b + e;

result = Math.round(result);

return result;
}

}
3 changes: 2 additions & 1 deletion src/app/stream-layout/stream-layout.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<app-chat [mainChatOverlay]="true" width="274px" height="778px" left="0px" bottom="228px" position="absolute"></app-chat>
<app-team-logo></app-team-logo>
<app-ai-chat-interactor></app-ai-chat-interactor>
<app-animation></app-animation>
<app-animation-host></app-animation-host>
<app-cool></app-cool>
<app-prediction-render></app-prediction-render>
<app-poll-render></app-poll-render>
Expand All @@ -23,6 +23,7 @@
</ng-container>
<img id="left" src="{{themeService.getLeftImage()}}" />
<app-emote-popups id="bottomEdgeEmotePopupContainer" name="bottomEdge" direction="up"></app-emote-popups>
<app-emote-popups-multiple id="bottomEdgeEmotePopupContainer" name="bottomEdgeMultiple" direction="up"></app-emote-popups-multiple>
<app-emote-popups id="leftEdgeEmotePopupContainer" name="leftEdge" direction="right"></app-emote-popups>
<app-emote-popups id="topEdgeEmotePopupContainer" name="topEdge" direction="down"></app-emote-popups>
<app-emote-popups id="rightEdgeEmotePopupContainer" name="rightEdge" direction="left"></app-emote-popups>
Expand Down
Loading

0 comments on commit 81c97e9

Please sign in to comment.