100 lines
2.7 KiB
TypeScript
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;
|
|
}
|