@@ -37,7 +37,7 @@ interface Coefficients {
3737 taps : number ;
3838}
3939
40- function coefficients ( inSize : number , outSize : number , filter : Filter , bits : number ) : Coefficients {
40+ function coefficients ( inSize : number , outSize : number , filter : Filter , bits : number , pad = false ) : Coefficients {
4141 const scale = inSize / outSize ;
4242 const filterScale = Math . max ( 1 , scale ) ;
4343 const support = filter . support * filterScale ;
@@ -51,6 +51,8 @@ function coefficients(inSize: number, outSize: number, filter: Filter, bits: num
5151 maxs [ i ] = Math . min ( inSize , Math . floor ( center + support + 0.5 ) ) ;
5252 taps = Math . max ( taps , maxs [ i ] - mins [ i ] ) ;
5353 }
54+ // padded to the unrolled counts (zero weights for the extra pixels)
55+ if ( pad ) taps = Math . min ( inSize , taps <= 2 ? 2 : taps <= 4 ? 4 : taps ) ;
5456
5557 // every output index gets `taps` weights, so the passes can unroll by the count: a shorter range starts earlier
5658 // (zero weights for the pixels before it) where it would read past the end
@@ -77,14 +79,17 @@ function coefficients(inSize: number, outSize: number, filter: Filter, bits: num
7779 return { start, weights, taps } ;
7880}
7981
82+ /** most taps per pass of the fast resampling */
83+ const FAST_TAPS = 4 ;
84+
8085const littleEndian = new Uint8Array ( new Uint32Array ( [ 1 ] ) . buffer ) [ 0 ] === 1 ;
8186
8287/**
8388 * Resamples RGBA pixels with the given filter, in premultiplied alpha so transparent pixels don't bleed their color
8489 */
85- export function resample ( src : Uint8ClampedArray , width : number , height : number , outWidth : number , outHeight : number , filter : Filter , out : Uint8ClampedArray ) : void {
90+ export function resample ( src : Uint8ClampedArray , width : number , height : number , outWidth : number , outHeight : number , filter : Filter , out : Uint8ClampedArray , fast = false ) : void {
8691 // opaque pixels don't need premultiplying, and with non-negative weights two channels share a multiplication
87- if ( littleEndian && filter . support <= 1 && resampleOpaque ( src , width , height , outWidth , outHeight , filter , out ) ) return ;
92+ if ( littleEndian && filter . support <= 1 && ( fast ? resampleOpaqueFast : resampleOpaque ) ( src , width , height , outWidth , outHeight , filter , out ) ) return ;
8893 resamplePremultiplied ( src , width , height , outWidth , outHeight , filter , out ) ;
8994}
9095
@@ -234,6 +239,102 @@ function resampleOpaque(src: Uint8ClampedArray, width: number, height: number, o
234239 return true ;
235240}
236241
242+ // sums the words of `taps` pixels `step` apart with 8-bit weights, two channels per multiplication, and rounds them
243+ // back to an opaque pixel: the weights sum to 256, so a lane holds at most 255 * 256 plus the rounding
244+ function packed ( pixels : Int32Array , s : number , step : number , weights : Int32Array , w : number , taps : number ) : number {
245+ let rb = 0x800080 , g = 128 ;
246+ for ( let k = 0 ; k < taps ; k ++ , s += step ) {
247+ const p = pixels [ s ] , wk = weights [ w + k ] ;
248+ rb += Math . imul ( p & 0xff00ff , wk ) ;
249+ g += ( p >> 8 & 255 ) * wk ;
250+ }
251+ return ( rb >>> 8 & 0xff00ff ) | ( g & 0xff00 ) | 0xff000000 ;
252+ }
253+
254+ /**
255+ * Like {@link resampleOpaque}, but with 8-bit weights in both passes and 8-bit pixels between them, so two channels
256+ * share every multiplication of both passes. More values end up a step off the exact result
257+ */
258+ function resampleOpaqueFast ( src : Uint8ClampedArray , width : number , height : number , outWidth : number , outHeight : number , filter : Filter , out : Uint8ClampedArray ) : boolean {
259+ // the rounding error of 8-bit weights adds up with the taps
260+ const vertical = coefficients ( height , outHeight , filter , 8 , true ) , horizontal = coefficients ( width , outWidth , filter , 8 , true ) ;
261+ if ( vertical . taps > FAST_TAPS || horizontal . taps > FAST_TAPS ) return resampleOpaque ( src , width , height , outWidth , outHeight , filter , out ) ;
262+
263+ const pixels = new Int32Array ( src . buffer , src . byteOffset , width * height ) ;
264+ let all = - 1 ;
265+ for ( let x = 0 , last = ( height - 1 ) * width ; x < width ; x ++ ) all &= pixels [ x ] & pixels [ last + x ] ;
266+ if ( all >>> 24 !== 255 ) return false ;
267+
268+ let rows = pixels ;
269+ if ( outHeight === height ) {
270+ for ( let i = 0 ; i < pixels . length ; i ++ ) all &= pixels [ i ] ;
271+ } else {
272+ rows = new Int32Array ( width * outHeight ) ;
273+ const { start, weights, taps } = vertical ;
274+ for ( let y = 0 , o = 0 ; y < outHeight ; y ++ ) {
275+ const w = y * taps , r0 = start [ y ] * width ;
276+ if ( taps === 4 ) {
277+ const w0 = weights [ w ] , w1 = weights [ w + 1 ] , w2 = weights [ w + 2 ] , w3 = weights [ w + 3 ] ;
278+ const r1 = r0 + width , r2 = r1 + width , r3 = r2 + width ;
279+ for ( let x = 0 ; x < width ; x ++ , o ++ ) {
280+ const p0 = pixels [ r0 + x ] , p1 = pixels [ r1 + x ] , p2 = pixels [ r2 + x ] , p3 = pixels [ r3 + x ] ;
281+ all &= p0 & p1 & p2 & p3 ;
282+ const rb = Math . imul ( p0 & 0xff00ff , w0 ) + Math . imul ( p1 & 0xff00ff , w1 ) + Math . imul ( p2 & 0xff00ff , w2 ) + Math . imul ( p3 & 0xff00ff , w3 ) + 0x800080 ;
283+ const g = ( p0 >> 8 & 255 ) * w0 + ( p1 >> 8 & 255 ) * w1 + ( p2 >> 8 & 255 ) * w2 + ( p3 >> 8 & 255 ) * w3 + 128 ;
284+ rows [ o ] = ( rb >>> 8 & 0xff00ff ) | ( g & 0xff00 ) | 0xff000000 ;
285+ }
286+ } else if ( taps === 2 ) {
287+ const w0 = weights [ w ] , w1 = weights [ w + 1 ] , r1 = r0 + width ;
288+ for ( let x = 0 ; x < width ; x ++ , o ++ ) {
289+ const p0 = pixels [ r0 + x ] , p1 = pixels [ r1 + x ] ;
290+ all &= p0 & p1 ;
291+ const rb = Math . imul ( p0 & 0xff00ff , w0 ) + Math . imul ( p1 & 0xff00ff , w1 ) + 0x800080 ;
292+ const g = ( p0 >> 8 & 255 ) * w0 + ( p1 >> 8 & 255 ) * w1 + 128 ;
293+ rows [ o ] = ( rb >>> 8 & 0xff00ff ) | ( g & 0xff00 ) | 0xff000000 ;
294+ }
295+ } else {
296+ for ( let x = 0 ; x < width ; x ++ , o ++ ) {
297+ for ( let k = 0 , s = r0 + x ; k < taps ; k ++ , s += width ) all &= pixels [ s ] ;
298+ rows [ o ] = packed ( pixels , r0 + x , width , weights , w , taps ) ;
299+ }
300+ }
301+ }
302+ }
303+ if ( all >>> 24 !== 255 ) return false ;
304+
305+ const result = new Int32Array ( out . buffer , out . byteOffset , outWidth * outHeight ) ;
306+ if ( outWidth === width ) {
307+ result . set ( rows ) ;
308+ return true ;
309+ }
310+
311+ const { start, weights, taps } = horizontal ;
312+ for ( let y = 0 , o = 0 ; y < outHeight ; y ++ ) {
313+ const row = y * width ;
314+ if ( taps === 4 ) {
315+ for ( let x = 0 ; x < outWidth ; x ++ , o ++ ) {
316+ const s = row + start [ x ] , w = x * 4 ;
317+ const p0 = rows [ s ] , p1 = rows [ s + 1 ] , p2 = rows [ s + 2 ] , p3 = rows [ s + 3 ] ;
318+ const w0 = weights [ w ] , w1 = weights [ w + 1 ] , w2 = weights [ w + 2 ] , w3 = weights [ w + 3 ] ;
319+ const rb = Math . imul ( p0 & 0xff00ff , w0 ) + Math . imul ( p1 & 0xff00ff , w1 ) + Math . imul ( p2 & 0xff00ff , w2 ) + Math . imul ( p3 & 0xff00ff , w3 ) + 0x800080 ;
320+ const g = ( p0 >> 8 & 255 ) * w0 + ( p1 >> 8 & 255 ) * w1 + ( p2 >> 8 & 255 ) * w2 + ( p3 >> 8 & 255 ) * w3 + 128 ;
321+ result [ o ] = ( rb >>> 8 & 0xff00ff ) | ( g & 0xff00 ) | 0xff000000 ;
322+ }
323+ } else if ( taps === 2 ) {
324+ for ( let x = 0 ; x < outWidth ; x ++ , o ++ ) {
325+ const s = row + start [ x ] , w = x * 2 ;
326+ const p0 = rows [ s ] , p1 = rows [ s + 1 ] , w0 = weights [ w ] , w1 = weights [ w + 1 ] ;
327+ const rb = Math . imul ( p0 & 0xff00ff , w0 ) + Math . imul ( p1 & 0xff00ff , w1 ) + 0x800080 ;
328+ const g = ( p0 >> 8 & 255 ) * w0 + ( p1 >> 8 & 255 ) * w1 + 128 ;
329+ result [ o ] = ( rb >>> 8 & 0xff00ff ) | ( g & 0xff00 ) | 0xff000000 ;
330+ }
331+ } else {
332+ for ( let x = 0 ; x < outWidth ; x ++ , o ++ ) result [ o ] = packed ( rows , row + start [ x ] , 1 , weights , x * taps , taps ) ;
333+ }
334+ }
335+ return true ;
336+ }
337+
237338/**
238339 * Resamples any pixels, premultiplied: between the passes, the color channels are channel * alpha and the alpha
239340 * channel is alpha * 255 (0-65025 each, beyond with overshooting filters)
0 commit comments