Skip to content

Commit c4eb977

Browse files
committed
Gif to Bitmap array convertor
1 parent a2362b4 commit c4eb977

8 files changed

Lines changed: 443 additions & 2 deletions

File tree

BitmapArrayTools/bitmap_array.js

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
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+
}

BitmapArrayTools/converter.html

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<!DOCTYPE html>
2+
<html lang="uk">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>GIF to .BNA (v1.1)</title>
6+
<link rel="stylesheet" href="styles.css">
7+
<script src="bitmap_array.js"></script>
8+
</head>
9+
<body>
10+
<div id="container">
11+
<h2>.GIF to .BNA Converter</h2>
12+
13+
<div class="control-group">
14+
<label>Choose GIF:</label>
15+
<input type="file" id="inputGif" accept="image/gif">
16+
</div>
17+
<button id="btnConvert">Generate & download .BNA</button>
18+
<div class="control-group" style="align-items: flex-end; display: flex;">
19+
<div style="flex: 1;">
20+
<label>Out width:</label>
21+
<input type="number" id="w" value="16" min="1" title="Width">
22+
</div>
23+
<div style="flex: 1;">
24+
<label>Out height:</label>
25+
<input type="number" id="h" value="16" min="1" title="Height">
26+
</div>
27+
<div style="flex: 1;">
28+
<label>Bits per pixel:</label>
29+
<input type="number" id="bpp" value="1" min="1" max="3" title="Bits Per Pixel">
30+
</div>
31+
</div>
32+
33+
<div class="info" id="status">Waiting for file...</div>
34+
</div>
35+
36+
<script>
37+
document.getElementById('btnConvert').onclick = async () => {
38+
const fileInput = document.getElementById('inputGif');
39+
const file = fileInput.files[0];
40+
if (!file) return alert("Choose a file!");
41+
42+
const bpp = parseInt(document.getElementById('bpp').value);
43+
const w = parseInt(document.getElementById('w').value);
44+
const h = parseInt(document.getElementById('h').value);
45+
46+
const type = bpp === 1 ? 'b1' : bpp === 2 ? 'b2' : bpp === 3 ? 'b3' : null;
47+
if (!type) return alert("Bits per pixel must be 1, 2, or 3!");
48+
const blob = await BnaEncoder.fromGif(file, w, h, type);
49+
status.innerText = `Encoding...`;
50+
51+
const a = document.createElement('a');
52+
a.href = URL.createObjectURL(blob);
53+
a.download = file.name.replace(/\.[^/.]+$/, "") + ".bna";
54+
a.click();
55+
status.innerText = "Done!";
56+
};
57+
</script>
58+
</body>
59+
</html>

BitmapArrayTools/index.html

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Smooth Tool Suite</title>
7+
<style>
8+
body, html { margin: 0; padding: 0; height: 100%; overflow: hidden; font-family: sans-serif; }
9+
10+
nav {
11+
height: 60px;
12+
background: #31455a;
13+
display: flex;
14+
align-items: center;
15+
justify-content: center;
16+
z-index: 100;
17+
position: relative;
18+
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
19+
}
20+
21+
button {
22+
padding: 10px 20px;
23+
margin: 0 10px;
24+
cursor: pointer;
25+
border: none;
26+
border-radius: 5px;
27+
background-color: #3498db;
28+
color: white;
29+
}
30+
31+
button:hover {
32+
background-color: #2980b9;
33+
}
34+
35+
button:active {
36+
transform: scale(0.98);
37+
background-color: #1f6391;
38+
}
39+
40+
/* The Container for our frames */
41+
.frame-container {
42+
position: relative;
43+
height: calc(100vh - 60px);
44+
width: 100%;
45+
}
46+
47+
iframe {
48+
position: absolute;
49+
top: 0;
50+
left: 0;
51+
width: 100%;
52+
height: 100%;
53+
border: none;
54+
visibility: hidden; /* Hidden by default */
55+
}
56+
57+
iframe.show {
58+
visibility: visible; /* Show the active one */
59+
}
60+
</style>
61+
</head>
62+
<body>
63+
64+
<nav>
65+
<button id="btn-play" onclick="switchTab('player')">Player</button>
66+
<button id="btn-conv" class="active" onclick="switchTab('converter')">Converter</button>
67+
</nav>
68+
69+
<div class="frame-container">
70+
<iframe id="frame-converter" src="converter.html"></iframe>
71+
<iframe id="frame-player" src="player.html" class="show"></iframe>
72+
</div>
73+
74+
<script>
75+
function switchTab(type) {
76+
// 1. Handle Frame Visibility
77+
document.getElementById('frame-converter').classList.toggle('show', type === 'converter');
78+
document.getElementById('frame-player').classList.toggle('show', type === 'player');
79+
80+
// 2. Handle Button Styling
81+
document.getElementById('btn-conv').classList.toggle('active', type === 'converter');
82+
document.getElementById('btn-play').classList.toggle('active', type === 'player');
83+
}
84+
</script>
85+
86+
</body>
87+
</html>

BitmapArrayTools/player.html

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Bitmap array player</title>
7+
<link rel="stylesheet" href="styles.css">
8+
<script src="bitmap_array.js"></script>
9+
</head>
10+
<body>
11+
12+
<div id="container">
13+
<h2>Bitmap Array player</h2>
14+
15+
<div class="control-group">
16+
<label>Select .bna File:</label>
17+
<input type="file" id="fileInput" accept=".bna">
18+
</div>
19+
20+
<div class="control-group">
21+
<label>Playback Speed (FPS): <span id="fpsVal">10</span></label>
22+
<input type="range" id="speedSlider" min="1" max="120" value="10">
23+
</div>
24+
25+
<div class="info" id="status">Waiting for file...</div>
26+
27+
<canvas id="canvas"></canvas>
28+
</div>
29+
30+
<script>
31+
const player = new BnaPlayer(document.getElementById('canvas'));
32+
const status = document.getElementById('status');
33+
34+
speedSlider.oninput = () => {
35+
fpsVal.innerText = speedSlider.value;
36+
player.play(speedSlider.value);
37+
};
38+
39+
document.getElementById('fileInput').onchange = async (e) => {
40+
const meta = await player.load(e.target.files[0]);
41+
status.innerText = `Res: ${meta.w}x${meta.h} | Frames: ${meta.frames}
42+
Type: ${meta.bpp === 1 ? '8-bit RGB332' : meta.bpp === 2 ? '16-bit RGB565' : '24-bit RGB888'}`;
43+
player.play(speedSlider.value);
44+
};
45+
</script>
46+
</body>
47+
</html>

0 commit comments

Comments
 (0)