Files
Toju/toju-app/src/app/domains/profile-avatar/domain/profile-avatar.models.ts
2026-04-17 03:06:44 +02:00

100 lines
2.7 KiB
TypeScript

export const PROFILE_AVATAR_ALLOWED_MIME_TYPES = [
'image/webp',
'image/gif',
'image/jpeg'
] as const;
export const PROFILE_AVATAR_ACCEPT_ATTRIBUTE = '.webp,.gif,.jpg,.jpeg,image/webp,image/gif,image/jpeg';
export const PROFILE_AVATAR_OUTPUT_SIZE = 256;
export const PROFILE_AVATAR_EDITOR_FRAME_SIZE = 224;
export const PROFILE_AVATAR_MIN_ZOOM = 1;
export const PROFILE_AVATAR_MAX_ZOOM = 4;
export interface ProfileAvatarDimensions {
width: number;
height: number;
}
export interface EditableProfileAvatarSource extends ProfileAvatarDimensions {
file: File;
objectUrl: string;
mime: string;
name: string;
preservesAnimation: boolean;
}
export interface ProfileAvatarTransform {
zoom: number;
offsetX: number;
offsetY: number;
}
export interface ProfileAvatarUpdates {
avatarUrl: string;
avatarHash: string;
avatarMime: string;
avatarUpdatedAt: number;
}
export interface ProcessedProfileAvatar extends ProfileAvatarUpdates, ProfileAvatarDimensions {
base64: string;
blob: Blob;
}
export function resolveProfileAvatarStorageFileName(mime: string | null | undefined): string {
switch (mime?.toLowerCase()) {
case 'image/gif':
return 'profile.gif';
case 'image/jpeg':
case 'image/jpg':
return 'profile.jpg';
default:
return 'profile.webp';
}
}
export function clampProfileAvatarZoom(zoom: number): number {
if (!Number.isFinite(zoom)) {
return PROFILE_AVATAR_MIN_ZOOM;
}
return Math.min(Math.max(zoom, PROFILE_AVATAR_MIN_ZOOM), PROFILE_AVATAR_MAX_ZOOM);
}
export function resolveProfileAvatarBaseScale(
source: ProfileAvatarDimensions,
frameSize = PROFILE_AVATAR_EDITOR_FRAME_SIZE
): number {
return Math.max(frameSize / source.width, frameSize / source.height);
}
export function clampProfileAvatarTransform(
source: ProfileAvatarDimensions,
transform: ProfileAvatarTransform,
frameSize = PROFILE_AVATAR_EDITOR_FRAME_SIZE
): ProfileAvatarTransform {
const zoom = clampProfileAvatarZoom(transform.zoom);
const renderedWidth = source.width * resolveProfileAvatarBaseScale(source, frameSize) * zoom;
const renderedHeight = source.height * resolveProfileAvatarBaseScale(source, frameSize) * zoom;
const maxOffsetX = Math.max(0, (renderedWidth - frameSize) / 2);
const maxOffsetY = Math.max(0, (renderedHeight - frameSize) / 2);
return {
zoom,
offsetX: clampOffset(transform.offsetX, maxOffsetX),
offsetY: clampOffset(transform.offsetY, maxOffsetY)
};
}
function clampOffset(value: number, maxMagnitude: number): number {
if (!Number.isFinite(value)) {
return 0;
}
const nextValue = Math.min(Math.max(value, -maxMagnitude), maxMagnitude);
return Object.is(nextValue, -0) ? 0 : nextValue;
}