1+ class BnaPlayer {
2+ constructor ( canvas ) {
3+ this . canvas = canvas ;
4+ this . ctx = canvas . getContext ( '2d' ) ;
5+ this . meta = null ;
6+ this . frameIdx = 0 ;
7+ this . timer = null ;
8+ this . fps = 30 ;
9+ }
10+
11+ async load ( fileOrBuffer ) {
12+ const buffer = fileOrBuffer instanceof ArrayBuffer
13+ ? fileOrBuffer
14+ : await fileOrBuffer . arrayBuffer ( ) ;
15+ const view = new DataView ( buffer ) ;
16+ const signature = String . fromCharCode ( view . getUint8 ( 0 ) , view . getUint8 ( 1 ) ) ;
17+ const type = String . fromCharCode ( view . getUint8 ( 2 ) , view . getUint8 ( 3 ) ) ;
18+
19+ if ( signature !== 'bA' ) throw new Error ( "Invalid file signature" ) ;
20+
21+ const bpp = type === 'b3' ? 3 : type === 'b2' ? 2 : type === 'b1' ? 1 : - 1 ;
22+ const w = view . getUint16 ( 4 , true ) ;
23+ const h = view . getUint16 ( 6 , true ) ;
24+ const frames = Math . floor ( ( buffer . byteLength - 8 ) / ( w * h * bpp ) ) ;
25+
26+ this . meta = { buffer, view, w, h, bpp, frames, headerSize : 8 } ;
27+ this . canvas . width = w ;
28+ this . canvas . height = h ;
29+ this . frameIdx = 0 ;
30+ this . renderFrame ( 0 ) ;
31+ return this . meta ;
32+ }
33+
34+ renderFrame ( idx ) {
35+ if ( ! this . meta ) return ;
36+ const { headerSize, w, h, bpp, view, frames } = this . meta ;
37+ const imageData = this . ctx . createImageData ( w , h ) ;
38+ const pixelsPerFrame = w * h ;
39+ const frameOffset = headerSize + ( ( idx % frames ) * pixelsPerFrame * bpp ) ;
40+
41+ for ( let i = 0 ; i < pixelsPerFrame ; i ++ ) {
42+ let r , g , b ;
43+ const pxIdx = i * 4 ;
44+
45+ if ( bpp === 1 ) { // RGB332
46+ const byte = view . getUint8 ( frameOffset + i ) ;
47+ r = byte & 0xe0 ; // mask out the 3 bits of red at the start of the byte
48+ r |= ( r >> 3 ) ; // extend limited 0-224 range to 0-252
49+ r |= ( r >> 3 ) ; // extend limited 0-252 range to 0-255
50+ g = byte & 0x1c ; // mask out the 3 bits of green in the middle of the byte
51+ g |= ( g << 3 ) | ( r >> 3 ) ; // extend limited 0-34 range to 0-255
52+ b = byte & 0x03 ; // mask out the 2 bits of blue at the end of the byte
53+ b |= b << 2 ; // extend 0-3 range to 0-15
54+ b |= b << 4 ;
55+ } else if ( bpp === 2 ) { // RGB565
56+ const word = view . getUint8 ( frameOffset + ( i * 2 ) ) << 8
57+ | view . getUint8 ( frameOffset + ( i * 2 ) + 1 ) ;
58+ r = ( word & 0xF800 ) >> 8 ;
59+ g = ( word & 0x07E0 ) >> 3 ;
60+ b = ( word & 0x001F ) << 3 ;
61+ } else { // RGB888
62+ r = view . getUint8 ( frameOffset + ( i * 3 ) ) ;
63+ g = view . getUint8 ( frameOffset + ( i * 3 ) + 1 ) ;
64+ b = view . getUint8 ( frameOffset + ( i * 3 ) + 2 ) ;
65+ }
66+
67+ imageData . data [ pxIdx ] = r ;
68+ imageData . data [ pxIdx + 1 ] = g ;
69+ imageData . data [ pxIdx + 2 ] = b ;
70+ imageData . data [ pxIdx + 3 ] = 255 ;
71+ }
72+ this . ctx . putImageData ( imageData , 0 , 0 ) ;
73+ }
74+
75+ play ( fps = this . fps ) {
76+ this . stop ( ) ;
77+ this . fps = fps ;
78+ this . timer = setInterval ( ( ) => {
79+ this . renderFrame ( this . frameIdx ) ;
80+ this . frameIdx = ( this . frameIdx + 1 ) % this . meta . frames ;
81+ } , 1000 / this . fps ) ;
82+ }
83+
84+ stop ( ) {
85+ if ( this . timer ) clearInterval ( this . timer ) ;
86+ }
87+ }
88+
89+ class BnaEncoder {
90+ /**
91+ * Converts a GIF file to a BNA Blob
92+ * @param {File } file - The GIF file
93+ * @param {number } width - Target width
94+ * @param {number } height - Target height
95+ * @param {string } type - type: 'b1' (RGB332), 'b2' (RGB565), 'b3' (RGB888)
96+ */
97+ static async fromGif ( file , width , height , type ) {
98+ const decoder = new ImageDecoder ( { data : file . stream ( ) , type : "image/gif" } ) ;
99+ await decoder . completed ;
100+ const totalFrames = decoder . tracks . selectedTrack . frameCount ;
101+
102+ const bpp = ( type === 'b1' ) ? 1 : ( type === 'b2' ) ? 2 : ( type === 'b3' ) ? 3 : - 1 ;
103+ const bytesPerFrame = width * height * bpp ;
104+ const totalSize = 8 + ( bytesPerFrame * totalFrames ) ;
105+ const buffer = new ArrayBuffer ( totalSize ) ;
106+ const view = new DataView ( buffer ) ;
107+ const uint8 = new Uint8Array ( buffer ) ;
108+
109+ // Header: "bA" + type + width + height
110+ uint8 [ 0 ] = 0x62 ; // b
111+ uint8 [ 1 ] = 0x41 ; // A
112+ uint8 [ 2 ] = type . charCodeAt ( 0 ) ;
113+ uint8 [ 3 ] = type . charCodeAt ( 1 ) ;
114+
115+ view . setUint16 ( 4 , width , true ) ;
116+ view . setUint16 ( 6 , height , true ) ;
117+
118+ const canvas = document . createElement ( 'canvas' ) ;
119+ const ctx = canvas . getContext ( '2d' , { willReadFrequently : true } ) ;
120+ canvas . width = width ;
121+ canvas . height = height ;
122+
123+ for ( let i = 0 ; i < totalFrames ; i ++ ) {
124+ const { image } = await decoder . decode ( { frameIndex : i } ) ;
125+ ctx . clearRect ( 0 , 0 , width , height ) ;
126+ ctx . drawImage ( image , 0 , 0 , width , height ) ;
127+ const rgba = ctx . getImageData ( 0 , 0 , width , height ) . data ;
128+ const frameOffset = 8 + ( i * bytesPerFrame ) ;
129+
130+ for ( let j = 0 ; j < width * height ; j ++ ) {
131+ const r = rgba [ j * 4 ] ;
132+ const g = rgba [ j * 4 + 1 ] ;
133+ const b = rgba [ j * 4 + 2 ] ;
134+ const pixelPos = frameOffset + ( j * bpp ) ;
135+
136+ if ( bpp === 1 ) {
137+ // RGB332
138+ uint8 [ pixelPos ] = ( r & 0xE0 ) | ( ( g & 0xE0 ) >> 3 ) | ( ( b & 0xC0 ) >> 6 ) ;
139+ } else if ( bpp === 2 ) {
140+ // RGB565
141+ uint8 [ pixelPos ] = ( r & 0xf8 ) | ( ( g & 0xE0 ) >> 5 ) ;
142+ uint8 [ pixelPos + 1 ] = ( ( g & 0x1C ) << 5 ) | ( ( b & 0xF8 ) >> 3 ) ;
143+ } else {
144+ // RGB888
145+ uint8 [ pixelPos ] = r ;
146+ uint8 [ pixelPos + 1 ] = g ;
147+ uint8 [ pixelPos + 2 ] = b ;
148+ }
149+ }
150+ image . close ( ) ;
151+ }
152+
153+ return new Blob ( [ buffer ] , { type : 'application/octet-stream' } ) ;
154+ }
155+ }
0 commit comments