@@ -90,6 +90,76 @@ export function resizeForModeRAuto(
9090 return outCtx . getImageData ( 0 , 0 , targetWidth , targetHeight )
9191}
9292
93+ /**
94+ * Resize image to Mode R target dimensions for COVER mode
95+ *
96+ * Cover mode: Scale the image to fill the target dimensions completely,
97+ * cropping any excess. The image is centered, so cropping is symmetric.
98+ * Mode R has SQUARE perceived pixels, so no aspect ratio correction needed.
99+ *
100+ * @param src - Source ImageData (from cropped image)
101+ * @param targetWidth - Target width (e.g., 320 for standard Mode 0 R)
102+ * @param targetHeight - Target height (e.g., 200 for standard Mode 0 R)
103+ * @returns ImageData filled with the scaled and cropped image
104+ */
105+ export function resizeForModeRCover (
106+ src : ImageData ,
107+ targetWidth : number ,
108+ targetHeight : number
109+ ) : ImageData {
110+ // Mode R has SQUARE perceived pixels
111+ const sourceAspect = src . width / src . height
112+ const targetAspect = targetWidth / targetHeight
113+
114+ // Create temporary canvas for the source
115+ const srcCanvas = document . createElement ( 'canvas' )
116+ srcCanvas . width = src . width
117+ srcCanvas . height = src . height
118+ const srcCtx = srcCanvas . getContext ( '2d' ) !
119+ srcCtx . putImageData ( src , 0 , 0 )
120+
121+ // Calculate source region to crop (cover mode)
122+ let srcX = 0
123+ let srcY = 0
124+ let srcW = src . width
125+ let srcH = src . height
126+
127+ if ( sourceAspect > targetAspect ) {
128+ // Source is wider: crop horizontally
129+ const newWidth = src . height * targetAspect
130+ srcX = ( src . width - newWidth ) / 2
131+ srcW = newWidth
132+ } else if ( sourceAspect < targetAspect ) {
133+ // Source is taller: crop vertically
134+ const newHeight = src . width / targetAspect
135+ srcY = ( src . height - newHeight ) / 2
136+ srcH = newHeight
137+ }
138+
139+ // Create output canvas at full target dimensions
140+ const outCanvas = document . createElement ( 'canvas' )
141+ outCanvas . width = targetWidth
142+ outCanvas . height = targetHeight
143+ const outCtx = outCanvas . getContext ( '2d' ) !
144+ outCtx . imageSmoothingEnabled = true
145+ outCtx . imageSmoothingQuality = 'high'
146+
147+ // Draw cropped and scaled image
148+ outCtx . drawImage (
149+ srcCanvas ,
150+ srcX ,
151+ srcY ,
152+ srcW ,
153+ srcH ,
154+ 0 ,
155+ 0 ,
156+ targetWidth ,
157+ targetHeight
158+ )
159+
160+ return outCtx . getImageData ( 0 , 0 , targetWidth , targetHeight )
161+ }
162+
93163/**
94164 * Resize image to Mode R target dimensions for ORIGIN mode
95165 *
@@ -166,22 +236,20 @@ export const modeRSourceImageAtom = atom(async (get) => {
166236 const resizeMode = get ( resizeModeAtom )
167237 const centerImage = get ( centerImageAtom )
168238
169- // In 'origin' mode, use the cropped image BEFORE the standard resize pipeline
170- // because the standard pipeline compresses to Mode 0 dimensions (160×200)
171- // but Mode R needs the full 320×200 resolution
172- // In 'auto' and 'cover' modes, use resizedImageAtom (NOT smoothedImageAtom) to skip horizontal smoothing
173- // Mode R has its own sub-pixel resolution, horizontal smoothing would blur it
239+ // Target dimensions for Mode R: doubled horizontal resolution
240+ const targetWidth = modeConfig . width * 2 // 320 for standard Mode 0
241+ const targetHeight = modeConfig . height // 200 for standard
242+
243+ // Select source image based on resize mode:
244+ // - 'origin' and 'cover': use cropped image (before resize) to apply Mode R-specific resize
245+ // - 'auto': use resizedImageAtom (already fit to Mode 0 dimensions, then scaled to Mode R)
174246 const sourceImage =
175- resizeMode === 'origin'
247+ resizeMode === 'origin' || resizeMode === 'cover'
176248 ? await get ( croppedImageAtom )
177249 : await get ( resizedImageAtom )
178250
179251 if ( ! sourceImage ) return null
180252
181- // Target dimensions for Mode R: doubled horizontal resolution
182- const targetWidth = modeConfig . width * 2 // 320 for standard Mode 0
183- const targetHeight = modeConfig . height // 200 for standard
184-
185253 // Skip resize if in 'origin' mode and image already matches target
186254 if (
187255 resizeMode === 'origin' &&
@@ -200,21 +268,32 @@ export const modeRSourceImageAtom = atom(async (get) => {
200268 // Resize to Mode R target dimensions
201269 // Use different resize strategy based on resize mode:
202270 // - origin: pixel-perfect 1:1 mapping (like Mode 1)
203- // - auto/cover: fit with aspect ratio preservation (cover already cropped by resizedImageAtom)
204- const modeRImage =
205- resizeMode === 'origin'
206- ? resizeForModeROrigin (
207- sourceImage ,
208- targetWidth ,
209- targetHeight ,
210- centerImage
211- )
212- : resizeForModeRAuto ( sourceImage , targetWidth , targetHeight , centerImage )
271+ // - cover: fill target dimensions, cropping excess (preserves aspect ratio)
272+ // - auto: fit with aspect ratio preservation (may have margins)
273+ let modeRImage : ImageData
274+ if ( resizeMode === 'origin' ) {
275+ modeRImage = resizeForModeROrigin (
276+ sourceImage ,
277+ targetWidth ,
278+ targetHeight ,
279+ centerImage
280+ )
281+ } else if ( resizeMode === 'cover' ) {
282+ modeRImage = resizeForModeRCover ( sourceImage , targetWidth , targetHeight )
283+ } else {
284+ modeRImage = resizeForModeRAuto (
285+ sourceImage ,
286+ targetWidth ,
287+ targetHeight ,
288+ centerImage
289+ )
290+ }
213291
214292 logger . info ( '[Mode R] Source image resized to true high resolution' , {
215293 sourceSize : `${ sourceImage . width } ×${ sourceImage . height } ` ,
216294 modeRSize : `${ modeRImage . width } ×${ modeRImage . height } ` ,
217- targetDimensions : `${ targetWidth } ×${ targetHeight } `
295+ targetDimensions : `${ targetWidth } ×${ targetHeight } ` ,
296+ resizeMode
218297 } )
219298
220299 return modeRImage
0 commit comments