1- import { Image , ResizeOptions , ResizeToOptions , TrimOptions , resizeArguments } from './image.ts' ;
1+ import { Image , ResizeOptions , ResizeToOptions , TrimOptions , ContainOptions , RotateOptions , TransformOptions , ShadowOptions , OutlineOptions } from './image.ts' ;
22import { checkOptions , finite } from './util/options.ts' ;
33
44/**
@@ -29,8 +29,10 @@ export interface FrameOptions {
2929
3030/**
3131 * A frame of an {@link Animation}: an {@link Image} ({@link Frame.image}) with a duration, offsets and a disposal mode.
32- * The frame's own methods (resize, scale, crop, trim) keep its pixels where they are in the animation by moving the
33- * offsets along; every other change goes to the image (`frame.image.blur(2)`), which leaves the offsets as they are
32+ * The frame has the methods that change the image's size (resize, scale, contain, cover, crop, trim, rotate, transform,
33+ * dropShadow, outline), which move the offsets along so the frame stays in place in the animation; every other change
34+ * goes to the image (`frame.image.blur(2)`). The image's own size-changing methods leave the offsets as they are.
35+ * Offsets can become negative: the parts of a frame outside the animation's canvas aren't encoded
3436 */
3537export class Frame {
3638 /** @internal @ignore */
@@ -177,6 +179,25 @@ export class Frame {
177179 return new Frame ( this . __image__ . clone ( ) , { duration : this . duration , xOffset : this . xOffset , yOffset : this . yOffset , disposalMode : this . disposalMode } ) ;
178180 }
179181
182+ /**
183+ * Runs a change of the image, moving the offsets the way the image reports its pixels moved
184+ * @internal
185+ * @ignore
186+ */
187+ private __tracked__ ( change : ( image : Image ) => unknown ) : this {
188+ const image = this . __image__ ;
189+ image . __moved__ = ( scaleX , scaleY , x , y ) => {
190+ this . xOffset = Math . round ( this . xOffset * scaleX + x ) ;
191+ this . yOffset = Math . round ( this . yOffset * scaleY + y ) ;
192+ } ;
193+ try {
194+ change ( image ) ;
195+ } finally {
196+ delete image . __moved__ ;
197+ }
198+ return this ;
199+ }
200+
180201 /**
181202 * Resizes the frame, scaling its offsets along with it (as if the whole animation was resized by the same factor)
182203 * @param width The new width
@@ -191,15 +212,9 @@ export class Frame {
191212 */
192213 resize ( options : ResizeToOptions ) : this;
193214 resize ( widthOrOptions : number | ResizeToOptions , heightArgument ?: number , optionsArgument ?: ResizeOptions ) : this {
194- const originalWidth = this . width , originalHeight = this . height ;
195- if ( typeof widthOrOptions === 'number' ) this . __image__ . resize ( widthOrOptions , heightArgument as number , optionsArgument ) ;
196- else {
197- resizeArguments ( widthOrOptions , heightArgument , optionsArgument ) ;
198- this . __image__ . resize ( widthOrOptions ) ;
199- }
200- this . xOffset = Math . round ( this . xOffset * this . width / originalWidth ) ;
201- this . yOffset = Math . round ( this . yOffset * this . height / originalHeight ) ;
202- return this ;
215+ return this . __tracked__ ( image => typeof widthOrOptions === 'number'
216+ ? image . resize ( widthOrOptions , heightArgument as number , optionsArgument )
217+ : image . resize ( widthOrOptions ) ) ;
203218 }
204219
205220 /**
@@ -208,11 +223,30 @@ export class Frame {
208223 * @param options See {@link ResizeOptions}
209224 */
210225 scale ( factor : number , options ?: ResizeOptions ) : this {
211- const originalWidth = this . width , originalHeight = this . height ;
212- this . __image__ . scale ( factor , options ) ;
213- this . xOffset = Math . round ( this . xOffset * this . width / originalWidth ) ;
214- this . yOffset = Math . round ( this . yOffset * this . height / originalHeight ) ;
215- return this ;
226+ return this . __tracked__ ( image => image . scale ( factor , options ) ) ;
227+ }
228+
229+ /**
230+ * Resizes the frame to fit inside the given box and pads it to exactly that size, like {@link Image.contain}. The
231+ * offsets scale with the image and then move by the padding, so the image stays where the scaled frame would be
232+ * @param width The width of the box
233+ * @param height The height of the box
234+ * @param options The background and how to resize, see {@link ContainOptions}
235+ */
236+ contain ( width : number , height : number , options ?: ContainOptions ) : this {
237+ return this . __tracked__ ( image => image . contain ( width , height , options ) ) ;
238+ }
239+
240+ /**
241+ * Resizes the frame to cover the given box and crops the overflowing edges, like {@link Image.cover}. The offsets
242+ * scale with the image and then move by the cropped amount, so the remaining pixels stay where the scaled frame
243+ * had them
244+ * @param width The width of the box
245+ * @param height The height of the box
246+ * @param options See {@link ResizeOptions}
247+ */
248+ cover ( width : number , height : number , options ?: ResizeOptions ) : this {
249+ return this . __tracked__ ( image => image . cover ( width , height , options ) ) ;
216250 }
217251
218252 /**
@@ -224,26 +258,52 @@ export class Frame {
224258 * @param height The new height
225259 */
226260 crop ( x : number , y : number , width : number , height : number ) : this {
227- const left = Math . max ( 0 , Math . floor ( finite ( x , 'x' ) ) ) , top = Math . max ( 0 , Math . floor ( finite ( y , 'y' ) ) ) ;
228- this . __image__ . crop ( x , y , width , height ) ;
229- this . xOffset += left ;
230- this . yOffset += top ;
231- return this ;
261+ return this . __tracked__ ( image => image . crop ( x , y , width , height ) ) ;
232262 }
233263
234264 /**
235265 * Removes borders of one color, moving the offsets so the remaining pixels stay where they are in the animation
236266 * @param options See {@link TrimOptions}
237267 */
238- trim ( options ?: TrimOptions ) : this;
239- trim ( options : TrimOptions = { } ) : this {
240- checkOptions ( options , 'trim({ tolerance: 0.1 })' ) ;
241- const { color, ...rest } = options ;
242- const bounds = this . __image__ . __trim_bounds__ ( color , rest ) ;
243- if ( ! bounds ) return this ;
244- this . __image__ . crop ( ...bounds ) ;
245- this . xOffset += bounds [ 0 ] ;
246- this . yOffset += bounds [ 1 ] ;
247- return this ;
268+ trim ( options ?: TrimOptions ) : this {
269+ return this . __tracked__ ( image => image . trim ( options ) ) ;
270+ }
271+
272+ /**
273+ * Rotates the frame clockwise around its middle, like {@link Image.rotate}: when it grows (or shrinks), the
274+ * offsets move so its middle stays where it is in the animation
275+ * @param angle The angle in degrees, clockwise
276+ * @param options See {@link RotateOptions}
277+ */
278+ rotate ( angle : number , options ?: RotateOptions ) : this {
279+ return this . __tracked__ ( image => image . rotate ( angle , options ) ) ;
280+ }
281+
282+ /**
283+ * Transforms the frame with an affine transform, like {@link Image.transform}: when it grows, the offsets move
284+ * so the transform's origin stays where it is in the animation
285+ * @param matrix The six values [a, b, c, d, e, f]
286+ * @param options See {@link TransformOptions}
287+ */
288+ transform ( matrix : ArrayLike < number > , options ?: TransformOptions ) : this {
289+ return this . __tracked__ ( image => image . transform ( matrix , options ) ) ;
290+ }
291+
292+ /**
293+ * Adds a shadow behind the frame's image, like {@link Image.dropShadow}: when it grows up or left, the offsets
294+ * move so the image stays where it is in the animation
295+ * @param options See {@link ShadowOptions}
296+ */
297+ dropShadow ( options ?: ShadowOptions ) : this {
298+ return this . __tracked__ ( image => image . dropShadow ( options ) ) ;
299+ }
300+
301+ /**
302+ * Draws an outline around the frame's shape, like {@link Image.outline}: when it grows up or left, the offsets
303+ * move so the image stays where it is in the animation
304+ * @param options See {@link OutlineOptions}
305+ */
306+ outline ( options ?: OutlineOptions ) : this {
307+ return this . __tracked__ ( image => image . outline ( options ) ) ;
248308 }
249309}
0 commit comments