Files

186 lines
5.7 KiB
JavaScript

const MAX_LINES = 8;
const PADDING = 16;
const LINE_HEIGHT = 22;
const EMOTE_SIZE = 18;
const FONT = "16px 'Helvetica Neue', Arial, sans-serif";
function ensureEmoteCache(state) {
if (!state.emoteCache) {
state.emoteCache = new Map();
}
return state.emoteCache;
}
function getEmoteBitmap(url, state) {
if (!url) {
return null;
}
const cache = ensureEmoteCache(state);
const existing = cache.get(url);
if (existing?.bitmap) {
return existing.bitmap;
}
if (existing?.loading) {
return null;
}
const entry = { loading: true, bitmap: null };
cache.set(url, entry);
fetch(url)
.then((response) => {
if (!response.ok) {
throw new Error("Failed to load emote");
}
return response.blob();
})
.then((blob) => createImageBitmap(blob))
.then((bitmap) => {
entry.bitmap = bitmap;
entry.loading = false;
})
.catch(() => {
cache.delete(url);
});
return null;
}
function normalizeFragments(message) {
if (Array.isArray(message?.fragments) && message.fragments.length) {
return message.fragments;
}
const text = message?.message || "";
return [{ type: "text", text }];
}
function tokenizeFragments(fragments) {
const tokens = [];
fragments.forEach((fragment) => {
if (fragment?.type === "emote" && fragment?.url) {
tokens.push({
type: "emote",
url: fragment.url,
text: fragment.text || fragment.name || "",
width: EMOTE_SIZE,
});
return;
}
const text = fragment?.text || "";
const words = text.split(" ");
words.forEach((word, index) => {
const value = index < words.length - 1 ? `${word} ` : word;
if (value) {
tokens.push({ type: "text", text: value });
}
});
});
return tokens;
}
function formatLines(messages, ctx, width) {
const maxWidth = Math.max(width - PADDING * 2, 0);
const lines = [];
messages.forEach((message) => {
const prefixText = message.displayName ? `${message.displayName}: ` : "";
const nameColor = message.tags?.color || "#ffffff";
const prefixWidth = ctx.measureText(prefixText).width;
const tokens = tokenizeFragments(normalizeFragments(message));
if (!tokens.length) {
lines.push({
prefixText,
prefixWidth,
nameColor,
fragments: [],
contentWidth: 0,
});
return;
}
let isFirstLine = true;
let availableWidth = Math.max(maxWidth - (prefixText ? prefixWidth : 0), 0);
let currentFragments = [];
let currentWidth = 0;
const flushLine = () => {
lines.push({
prefixText: isFirstLine ? prefixText : "",
prefixWidth: isFirstLine ? prefixWidth : 0,
nameColor,
fragments: currentFragments,
contentWidth: currentWidth,
});
currentFragments = [];
currentWidth = 0;
isFirstLine = false;
availableWidth = maxWidth;
};
tokens.forEach((token) => {
const tokenWidth =
token.type === "emote" ? token.width : ctx.measureText(token.text || "").width;
if (tokenWidth > availableWidth && currentFragments.length) {
flushLine();
}
currentFragments.push({ ...token, width: tokenWidth });
currentWidth += tokenWidth;
availableWidth = Math.max(availableWidth - tokenWidth, 0);
});
if (currentFragments.length) {
flushLine();
}
});
return lines.slice(-MAX_LINES);
}
function init() { }
function tick(context, state) {
const { ctx, width, height, chatMessages } = context;
if (!ctx) {
return;
}
ctx.clearRect(0, 0, width, height);
ctx.font = FONT;
ctx.textBaseline = "top";
const messages = Array.isArray(chatMessages) ? chatMessages : [];
if (messages.length === 0) {
return;
}
const lines = formatLines(messages, ctx, width);
const boxHeight = lines.length * LINE_HEIGHT + PADDING * 2;
const boxWidth = Math.max(...lines.map((line) => line.prefixWidth + line.contentWidth), 120);
ctx.fillStyle = "rgba(0, 0, 0, 0.55)";
ctx.fillRect(PADDING, height - boxHeight - PADDING, boxWidth + PADDING * 2, boxHeight);
lines.forEach((line, index) => {
const x = PADDING * 2;
const y = height - boxHeight - PADDING + PADDING + index * LINE_HEIGHT;
if (line.prefixText) {
ctx.fillStyle = line.nameColor || "#ffffff";
ctx.fillText(line.prefixText, x, y);
}
let cursorX = x + line.prefixWidth;
line.fragments.forEach((fragment) => {
if (fragment.type === "emote" && fragment.url) {
const bitmap = getEmoteBitmap(fragment.url, state);
if (bitmap) {
const yOffset = y + (LINE_HEIGHT - EMOTE_SIZE) / 2;
ctx.drawImage(bitmap, cursorX, yOffset, EMOTE_SIZE, EMOTE_SIZE);
} else if (fragment.text) {
ctx.fillStyle = "#ffffff";
ctx.fillText(fragment.text, cursorX, y);
}
cursorX += fragment.width;
return;
}
if (fragment.text) {
ctx.fillStyle = "#ffffff";
ctx.fillText(fragment.text, cursorX, y);
}
cursorX += fragment.width;
});
});
}