diff --git a/src/@types/betterttv.d.ts b/src/@types/betterttv.d.ts index 479544d..a02fab3 100644 --- a/src/@types/betterttv.d.ts +++ b/src/@types/betterttv.d.ts @@ -18,6 +18,10 @@ namespace BetterTTV { userId: string } + type GlobalEmote = ChannelEmote & { + modifier: boolean + } + type SharedEmote = Emote & { user: { id: string diff --git a/src/@types/slime2.d.ts b/src/@types/slime2.d.ts index 2426ed5..6fe8351 100644 --- a/src/@types/slime2.d.ts +++ b/src/@types/slime2.d.ts @@ -120,6 +120,7 @@ namespace Slime2 { name: string images: Emote.Images source: Emote.Source + isModifier: boolean } type EmoteMap = Map diff --git a/src/@types/twitch.d.ts b/src/@types/twitch.d.ts index 60c808c..9cce6f1 100644 --- a/src/@types/twitch.d.ts +++ b/src/@types/twitch.d.ts @@ -107,7 +107,7 @@ namespace Twitch { type Type = | { type: 'text' } | { type: 'cheer'; cheer: Cheermote } - | { type: 'emote'; emote: Slime2.Event.Message.Emote } + | { type: 'emote'; emote: Slime2.Event.Message.Emote; modifier?: string } } type Cheermote = { diff --git a/src/main.css b/src/main.css index 13f8b90..b22e45b 100644 --- a/src/main.css +++ b/src/main.css @@ -129,3 +129,12 @@ html { opacity: 0; } } + +@keyframes slime2-emote-modifier-party { + from { + filter: sepia(0.5) hue-rotate(0deg) saturate(2.5); + } + to { + filter: sepia(0.5) hue-rotate(360deg) saturate(2.5); + } +} diff --git a/src/services/emotes/BetterTTV.ts b/src/services/emotes/BetterTTV.ts index 6ee290f..a4d2ffb 100644 --- a/src/services/emotes/BetterTTV.ts +++ b/src/services/emotes/BetterTTV.ts @@ -10,6 +10,73 @@ export default async function getChannelEmotes( ): Promise { const emoteMap = new Map() + setEmotes([ + { + id: '6468f7acaee1f7f47567708e', + code: 'c!', + imageType: 'png', + animated: false, + userId: '5561169bd6b9d206222a8c19', + modifier: true, + }, + { + id: '6468f845aee1f7f47567709b', + code: 'h!', + imageType: 'png', + animated: false, + userId: '5561169bd6b9d206222a8c19', + modifier: true, + }, + { + id: '6468f869aee1f7f4756770a8', + code: 'l!', + imageType: 'png', + animated: false, + userId: '5561169bd6b9d206222a8c19', + modifier: true, + }, + { + id: '6468f883aee1f7f4756770b5', + code: 'r!', + imageType: 'png', + animated: false, + userId: '5561169bd6b9d206222a8c19', + modifier: true, + }, + { + id: '6468f89caee1f7f4756770c2', + code: 'v!', + imageType: 'png', + animated: false, + userId: '5561169bd6b9d206222a8c19', + modifier: true, + }, + { + id: '6468f8d1aee1f7f4756770cf', + code: 'z!', + imageType: 'png', + animated: false, + userId: '5561169bd6b9d206222a8c19', + modifier: true, + }, + { + id: '64e3b31920cb0d25d950a9f9', + code: 'w!', + imageType: 'png', + animated: false, + userId: '5561169bd6b9d206222a8c19', + modifier: true, + }, + { + id: '65cbe7dbaed093b2eaf87c65', + code: 'p!', + imageType: 'png', + animated: false, + userId: '5561169bd6b9d206222a8c19', + modifier: true, + }, + ]) + const user = await bttvApi .get(`/users/${platform}/${userId}`) .then(response => response.data) @@ -31,6 +98,8 @@ export default async function getChannelEmotes( static: buildEmoteUrls(emote.id, true), }, source: 'betterttv', + isModifier: + 'modifier' in emote ? (emote).modifier : false, }) }) } diff --git a/src/services/emotes/FrankerFaceZ.ts b/src/services/emotes/FrankerFaceZ.ts index 7bf1207..497eeed 100644 --- a/src/services/emotes/FrankerFaceZ.ts +++ b/src/services/emotes/FrankerFaceZ.ts @@ -33,6 +33,7 @@ export default async function getChannelEmotes( static: buildEmoteUrls(emote, true), }, source: 'frankerfacez', + isModifier: false, }) }) }) diff --git a/src/services/emotes/YouTube.ts b/src/services/emotes/YouTube.ts index a7f38dd..15ace1e 100644 --- a/src/services/emotes/YouTube.ts +++ b/src/services/emotes/YouTube.ts @@ -11,6 +11,7 @@ export function getGlobalEmotes(): Slime2.Event.Message.EmoteMap { static: buildEmoteUrls(emoji.image), }, source: 'youtube', + isModifier: false, }) }) diff --git a/src/services/platforms/twitch/chat/transforms/useEmotePart.ts b/src/services/platforms/twitch/chat/transforms/useEmotePart.ts index cdbbca1..b84a09c 100644 --- a/src/services/platforms/twitch/chat/transforms/useEmotePart.ts +++ b/src/services/platforms/twitch/chat/transforms/useEmotePart.ts @@ -41,6 +41,7 @@ export function useEmotePart() { static: buildEmoteUrls(id, true), }, source: 'twitch', + isModifier: false, }, } } diff --git a/src/services/platforms/twitch/chat/transforms/useMessage.ts b/src/services/platforms/twitch/chat/transforms/useMessage.ts index 7b56f66..56c7e68 100644 --- a/src/services/platforms/twitch/chat/transforms/useMessage.ts +++ b/src/services/platforms/twitch/chat/transforms/useMessage.ts @@ -6,9 +6,9 @@ import useUser from './useUser' /** * Hook that returns the function {@link transform} */ -export default function useMessage() { +export default function useMessage(enableEmoteModifiers: boolean = false) { const { data: channelPointRewards } = useChannelPointRewards() - const transformText = useText() + const transformText = useText(enableEmoteModifiers) const transformUser = useUser() /** diff --git a/src/services/platforms/twitch/chat/transforms/useText.ts b/src/services/platforms/twitch/chat/transforms/useText.ts index 956dc58..1e1921a 100644 --- a/src/services/platforms/twitch/chat/transforms/useText.ts +++ b/src/services/platforms/twitch/chat/transforms/useText.ts @@ -7,7 +7,7 @@ import { useTextPart } from './useTextPart' /** * Hook that returns the function {@link transform} */ -export default function useText() { +export default function useText(enableEmoteModifiers: boolean = false) { const { data: cheermotes } = useCheermotes() const transformTextPart = useTextPart() const transformCheerPart = useCheerPart() @@ -47,8 +47,102 @@ export default function useText() { } }) + let emoteModifiersQueued: Slime2.Event.Message.Emote[] = [] + for (let i = 0; i < parts.length; i++) { + const part = parts[i] + if (part.type === 'emote') { + if (part.emote.isModifier) { + emoteModifiersQueued.push(part.emote) + if ( + i + 1 < parts.length && + parts[i + 1].type === 'text' && + parts[i + 1].text.trim() === '' + ) { + // Drop whitespace seperating the modifier from the following emote + parts.splice(i + 1, 1) + } + } else if (emoteModifiersQueued.length > 0) { + i -= emoteModifiersQueued.length + parts.splice(i, emoteModifiersQueued.length) + + part.modifier = '' + emoteModifiersQueued.forEach(emote => { + switch (emote.name) { + case 'c!': // Cursed + part.modifier += + 'filter: grayscale(1) brightness(.7) contrast(2.5);' + break + case 'h!': // Horizontal Flip + part.modifier += 'transform: scaleX(-1);' + break + case 'l!': // Left Rotate + part.modifier += 'transform: rotate(-90deg);' + break + case 'p!': // Party + part.modifier += + 'animation: slime2-emote-modifier-party 1.5s linear infinite;' + break + case 'r!': // Right Rotate + part.modifier += 'transform: rotate(90deg);' + break + case 'v!': // Vertical Flip + part.modifier += 'transform: scaleY(-1);' + break + case 'w!': // Wide + part.modifier += 'transform: scaleX(3);' + // We can't hardcod emote size to make widened emote spacing work. + // Instead, add blank emotes before and after this emote, for easy spacing no matter what widget + parts.splice(i + 1, 0, BLANK_EMOTE_PART) + parts.splice(i, 0, BLANK_EMOTE_PART) + i += 1 + break + case 'z!': // Zero Space + // Simply remove the whitespace-only text part before this emote + if ( + i - 1 < parts.length && + parts[i - 1].type === 'text' && + parts[i - 1].text.trim() === '' + ) { + parts.splice(i - 1, 1) + } + break + } + }) + emoteModifiersQueued = [] + + if (part.modifier === '') part.modifier = undefined + } + } else if (emoteModifiersQueued.length > 0) { + emoteModifiersQueued = [] + } + } + console.log(parts) + return parts } return transform } + +const BLANK_EMOTE_PART: Twitch.Event.Message.Part = { + type: 'emote', + text: '', + emote: { + id: '', + name: ':blank:', + images: { + default: { + x1: 'data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', + x2: 'data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', + x4: 'data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', + }, + static: { + x1: 'data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', + x2: 'data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', + x4: 'data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', + }, + }, + source: 'betterttv', + isModifier: false, + }, +} diff --git a/src/services/platforms/twitch/chat/useEmulate.ts b/src/services/platforms/twitch/chat/useEmulate.ts index 822f68a..76246ac 100644 --- a/src/services/platforms/twitch/chat/useEmulate.ts +++ b/src/services/platforms/twitch/chat/useEmulate.ts @@ -41,7 +41,7 @@ export default function useEmulateTwitchMessage() { const emotes = [ ...channelEmotes!, ...Array.from(thirdPartyEmoteMap!.values()), - ] + ].filter(e => !e.isModifier) const date = new Date() const first = Random.chance(5) // 5% chance of being first time chat diff --git a/src/services/platforms/twitch/useChannelEmotes.ts b/src/services/platforms/twitch/useChannelEmotes.ts index cee82b3..3d3e561 100644 --- a/src/services/platforms/twitch/useChannelEmotes.ts +++ b/src/services/platforms/twitch/useChannelEmotes.ts @@ -23,6 +23,7 @@ export default function useChannelEmotes() { static: buildEmoteUrls(id, true), }, source: 'twitch', + isModifier: false, } }) },