@@ -49,9 +49,12 @@ export class ProfilePictureSettingsComponent {
4949 readonly faMagnifyingGlassPlus = faMagnifyingGlassPlus ;
5050 readonly faRotateRight = faRotateRight ;
5151
52+ // Dialog + image source state used by the cropper overlay.
5253 cropDialogVisible = signal ( false ) ;
5354 rawImageSrc = signal < string | null > ( null ) ;
5455
56+ // User-controlled crop transform. `zoomFactor` is applied on top of `coverScale()`,
57+ // which already guarantees that the circular crop area is fully covered.
5558 zoomFactor = signal ( 1 ) ;
5659 minZoomFactor = 0.5 ;
5760 maxZoomFactor = 3 ;
@@ -66,6 +69,8 @@ export class ProfilePictureSettingsComponent {
6669
6770 currentProfilePictureUrl = computed < string | null > ( ( ) => this . normalizeAvatarUrl ( this . accountService . loadedUser ( ) ?. avatar ) ) ;
6871
72+ // Foreground preview shown inside the crop dialog. The image is centered,
73+ // then translated by the user's pan and finally scaled for the active zoom level.
6974 imageStyle = computed < Record < string , string > > ( ( ) => {
7075 const scale = this . effectiveScale ( ) ;
7176 const pan = this . clampedPanForScale ( scale ) ;
@@ -83,6 +88,8 @@ export class ProfilePictureSettingsComponent {
8388 } ;
8489 } ) ;
8590
91+ // Soft blurred background behind the sharp foreground image so empty corners
92+ // never appear while the user pans a portrait-oriented or landscape-oriented image.
8693 blurImageStyle = computed < Record < string , string > > ( ( ) => {
8794 const scale = this . blurScale ( ) ;
8895 const pan = this . clampedPanForScale ( scale , CROP_RADIUS + BLUR_PADDING_PX ) ;
@@ -196,6 +203,10 @@ export class ProfilePictureSettingsComponent {
196203 }
197204 }
198205
206+ /**
207+ * Rotates the currently loaded image by 90 degrees clockwise and rebuilds the
208+ * crop state from the rotated result so panning limits and zoom are recalculated.
209+ */
199210 onRotate ( ) : void {
200211 if ( ! this . img ) return ;
201212
@@ -221,6 +232,13 @@ export class ProfilePictureSettingsComponent {
221232 rotated . src = rotatedSrc ;
222233 }
223234
235+ /**
236+ * Renders the visible crop result into a square canvas, masks it into a circle,
237+ * uploads the generated JPEG and updates the in-memory avatar shown in the app shell.
238+ *
239+ * The preview uses a 360px interaction area with a 150px crop radius, while the
240+ * exported avatar is normalized to a fixed 300x300px output for storage/display.
241+ */
224242 async onSave ( ) : Promise < void > {
225243 if ( ! this . img ) return ;
226244
@@ -249,6 +267,7 @@ export class ProfilePictureSettingsComponent {
249267 const cropTop = cc - CROP_RADIUS ;
250268 const scaleToOut = outSize / ( CROP_RADIUS * 2 ) ;
251269
270+ // Convert from dialog-space coordinates into export-canvas coordinates.
252271 const fgLeft = cc + fgPan . x - fgDisplayW / 2 ;
253272 const fgTop = cc + fgPan . y - fgDisplayH / 2 ;
254273 const bgLeft = cc + bgPan . x - bgDisplayW / 2 ;
@@ -299,6 +318,8 @@ export class ProfilePictureSettingsComponent {
299318 }
300319 }
301320
321+ // Reset to a safe initial crop: centered image with the minimum zoom required
322+ // to fully cover the circular crop area.
302323 private resetCropState ( ) : void {
303324 if ( this . imgNatW ( ) <= 0 || this . imgNatH ( ) <= 0 ) return ;
304325 const initialZoom = Math . max ( this . minZoomFactor , Math . min ( 1 , this . maxZoomFactor ) ) ;
@@ -326,13 +347,15 @@ export class ProfilePictureSettingsComponent {
326347 return this . coverScale ( ) * 1.2 ;
327348 }
328349
350+ // Enforces the current pan limits after any zoom or drag update.
329351 private clampPan ( ) : void {
330352 const scale = this . effectiveScale ( ) ;
331353 const pan = this . clampedPanForScale ( scale ) ;
332354 this . panX . set ( pan . x ) ;
333355 this . panY . set ( pan . y ) ;
334356 }
335357
358+ // Prevents the visible crop circle from ever exposing empty space around the image.
336359 private clampedPanForScale ( scale : number , coverRadius : number = CROP_RADIUS ) : { x : number ; y : number } {
337360 const natW = this . imgNatW ( ) ;
338361 const natH = this . imgNatH ( ) ;
@@ -347,6 +370,10 @@ export class ProfilePictureSettingsComponent {
347370 } ;
348371 }
349372
373+ /**
374+ * Validates the selected file, loads it as a data URL and initializes the crop dialog
375+ * once the browser has decoded the image dimensions.
376+ */
350377 private loadFileForCrop ( file : File ) : void {
351378 if ( file . size > MAX_UPLOAD_SIZE_BYTES ) {
352379 this . toastService . showErrorKey ( 'settings.profilePicture.fileTooLarge' ) ;
0 commit comments