Files
fluxer/fluxer_app/src/features/guild/utils/GuildAcronymUtils.ts
T

87 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-License-Identifier: AGPL-3.0-or-later
const POSSESSIVE_SUFFIX = /['’]s(?=\s)/gu;
const WORD_RUN = /([\p{L}\p{N}_]\p{M}*)[\p{L}\p{N}\p{M}_‌‍]*/gu;
const WHITESPACE = /\s+/gu;
const GUILD_ACRONYM_REFERENCE_ICON_SIZE_PX = 48;
const GUILD_ACRONYM_FONT_SIZES_PX = [18, 18, 16, 14, 12, 10];
const GUILD_ACRONYM_FIT_PADDING_PX = 4;
const GUILD_ACRONYM_FIT_WIDTH_RATIO =
(GUILD_ACRONYM_REFERENCE_ICON_SIZE_PX - GUILD_ACRONYM_FIT_PADDING_PX * 2) / GUILD_ACRONYM_REFERENCE_ICON_SIZE_PX;
const GUILD_ACRONYM_MEASURE_FONT_SIZE_PX = 100;
const GUILD_ACRONYM_WIDTH_CACHE_CAPACITY = 512;
const acronymWidthCache = new Map<string, number>();
let measureContext: CanvasRenderingContext2D | null | undefined;
export const GUILD_ACRONYM_FALLBACK = '?';
const graphemeSegmenter =
typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function'
? new Intl.Segmenter(undefined, {granularity: 'grapheme'})
: null;
function firstGrapheme(wordRun: string, leadingCharacterWithMarks: string): string {
if (graphemeSegmenter == null) return leadingCharacterWithMarks;
for (const {segment} of graphemeSegmenter.segment(wordRun)) {
return segment;
}
return leadingCharacterWithMarks;
}
export function getGuildAcronym(name: string): string {
return name.replace(POSSESSIVE_SUFFIX, '').replace(WORD_RUN, firstGrapheme).replace(WHITESPACE, '');
}
export function getGuildAcronymLength(acronym: string): number {
if (graphemeSegmenter == null) return Array.from(acronym).length;
let length = 0;
for (const _segment of graphemeSegmenter.segment(acronym)) {
length += 1;
}
return length;
}
export function getGuildAcronymFontScale(acronymLength: number): number {
const index = Math.min(Math.max(acronymLength, 1), GUILD_ACRONYM_FONT_SIZES_PX.length) - 1;
return GUILD_ACRONYM_FONT_SIZES_PX[index] / GUILD_ACRONYM_REFERENCE_ICON_SIZE_PX;
}
function getMeasureContext(): CanvasRenderingContext2D | null {
if (measureContext !== undefined) return measureContext;
if (typeof document === 'undefined' || document.body == null) return null;
const context = document.createElement('canvas').getContext('2d');
if (context != null) {
context.font = `500 ${GUILD_ACRONYM_MEASURE_FONT_SIZE_PX}px ${getComputedStyle(document.body).fontFamily}`;
}
measureContext = context;
return context;
}
function getGuildAcronymWidthPerEm(acronym: string): number | null {
const cached = acronymWidthCache.get(acronym);
if (cached !== undefined) return cached;
const context = getMeasureContext();
if (context == null) return null;
const widthPerEm = context.measureText(acronym).width / GUILD_ACRONYM_MEASURE_FONT_SIZE_PX;
if (acronymWidthCache.size >= GUILD_ACRONYM_WIDTH_CACHE_CAPACITY) acronymWidthCache.clear();
acronymWidthCache.set(acronym, widthPerEm);
return widthPerEm;
}
export function fitGuildAcronymFontScale(acronymLength: number, widthPerEm: number | null): number {
const scale = getGuildAcronymFontScale(acronymLength);
if (widthPerEm == null || widthPerEm <= 0) return scale;
const maxScale = GUILD_ACRONYM_FIT_WIDTH_RATIO / widthPerEm;
if (scale <= maxScale) return scale;
for (const fontSize of GUILD_ACRONYM_FONT_SIZES_PX) {
const candidate = fontSize / GUILD_ACRONYM_REFERENCE_ICON_SIZE_PX;
if (candidate < scale && candidate <= maxScale) return candidate;
}
return maxScale;
}
export function getFittedGuildAcronymFontScale(acronym: string): number {
return fitGuildAcronymFontScale(getGuildAcronymLength(acronym), getGuildAcronymWidthPerEm(acronym));
}