@@ -48,6 +48,11 @@ export interface OptimizeOptions {
4848 * (the transparent pixels are the sparse frames' unchanged ones) than without the transparent pixels
4949 */
5050 alpha : 'frame' | ( ( frames : OptimizedFrame [ ] ) => number ) ;
51+ /**
52+ * The palette size if each frame has one (GIF: 256): a frame stays dense if its colors fit, but its sparse pixels
53+ * (the transparent ones are a color more) don't, so they'd lose colors
54+ */
55+ palette ?: number ;
5156}
5257
5358// what transparent pixels make an opaque frame cost: an alpha channel, 4 channels instead of 3 (or a palette entry
@@ -59,7 +64,7 @@ const ALPHA_COST = 4 / 3;
5964 * @param width The canvas width
6065 * @param height The canvas height
6166 */
62- export function optimize ( frames : InputFrame [ ] , width : number , height : number , { replace, even, alpha } : OptimizeOptions ) : OptimizedFrame [ ] {
67+ export function optimize ( frames : InputFrame [ ] , width : number , height : number , { replace, even, alpha, palette } : OptimizeOptions ) : OptimizedFrame [ ] {
6368 const size = width * height * 4 ;
6469 // the input animation's canvas, and what the optimized animation shows after its last frame (and its disposal)
6570 const canvas = new Uint8Array ( size ) , canvas32 = new Int32Array ( canvas . buffer ) ;
@@ -111,7 +116,9 @@ export function optimize(frames: InputFrame[], width: number, height: number, {
111116 if ( ! output . length && ( w !== width || h !== height ) ) opaque = false ;
112117 if ( ! area . denseOpaque ) opaque = false ;
113118 const sparseCost = alpha === 'frame' && area . denseOpaque ? area . sparseChanges * ALPHA_COST : area . sparseChanges ;
114- const dense = ! blend || area . denseChanges < sparseCost ;
119+ const dense = ! blend || area . denseChanges < sparseCost
120+ || ( palette !== undefined && colors ( new Int32Array ( pixels . buffer ) , w , { x : 0 , y : 0 , width : w , height : h } , palette + 1 ) > palette
121+ && colors ( canvas32 , width , changed , palette + 1 ) <= palette ) ;
115122 denseTotal += area . denseChanges ;
116123 chosenTotal += dense ? area . denseChanges : area . sparseChanges ;
117124 if ( dense ) {
@@ -147,18 +154,38 @@ export function optimize(frames: InputFrame[], width: number, height: number, {
147154 shown . fill ( 0 ) ;
148155 for ( const f of output ) {
149156 const pixels32 = new Int32Array ( f . pixels . buffer , f . pixels . byteOffset , f . width * f . height ) ;
150- for ( let row = 0 , k = 0 ; row < f . height ; row ++ )
151- for ( let p = ( f . y + row ) * width + f . x , end = p + f . width ; p < end ; p ++ , k ++ ) {
152- if ( f . blend && ( pixels32 [ k ] & ALPHA ) === 0 ) pixels32 [ k ] = shown32 [ p ] ;
153- else shown32 [ p ] = pixels32 [ k ] ;
154- }
157+ for ( let row = 0 , k = 0 ; row < f . height ; row ++ , k += f . width ) {
158+ const start = ( f . y + row ) * width + f . x ;
159+ if ( ! f . blend ) shown32 . set ( pixels32 . subarray ( k , k + f . width ) , start ) ;
160+ else
161+ for ( let i = k , p = start , end = k + f . width ; i < end ; i ++ , p ++ ) {
162+ if ( ( pixels32 [ i ] & ALPHA ) === 0 ) pixels32 [ i ] = shown32 [ p ] ;
163+ else shown32 [ p ] = pixels32 [ i ] ;
164+ }
165+ }
155166 f . blend = false ;
156167 }
157168 }
158169
159170 return output ;
160171}
161172
173+ // how many colors an area of an image has, counted up to a limit
174+ function colors ( image32 : Int32Array , width : number , { x, y, width : w , height : h } : Rect , limit : number ) : number {
175+ const seen = new Set < number > ( ) ;
176+ for ( let row = y ; row < y + h ; row ++ ) {
177+ let previous : number | undefined ;
178+ for ( let p = row * width + x , end = p + w ; p < end ; p ++ ) {
179+ const value = image32 [ p ] ;
180+ if ( value === previous ) continue ;
181+ previous = value ;
182+ seen . add ( value ) ;
183+ if ( seen . size >= limit ) return seen . size ;
184+ }
185+ }
186+ return seen . size ;
187+ }
188+
162189// a frame that doesn't change anything
163190const empty = ( duration : number ) : OptimizedFrame => ( { x : 0 , y : 0 , width : 1 , height : 1 , duration, dispose : false , blend : true , pixels : new Uint8Array ( 4 ) } ) ;
164191
0 commit comments