Skip to content

Commit 7f6a651

Browse files
committed
Refactor Pixel2CPP to support multiple draw modes and output formats. Added horizontal and vertical packing options for 1-bit and alpha images. Updated UI to allow selection of draw mode and output format. Enhanced packing functions for better organization and maintainability.
1 parent 05d87ea commit 7f6a651

File tree

2 files changed

+303
-37
lines changed

2 files changed

+303
-37
lines changed

src/Pixel2CPP.jsx

Lines changed: 231 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import React, { useEffect, useMemo, useRef, useState } from "react";
22
import PixelCanvas from "./components/PixelCanvas.jsx";
33
import { clamp, transparent, black, white, rgbaEq, rgbaToHex, parseCssColor } from "./lib/colors.js";
4-
import { pack1bit, packRGB565, packRGB24, packRGB332, packGray4, rgbTo332, hex565 } from "./lib/packers.js";
4+
import { pack1bit, pack1bitAlpha, packRGB565, packRGB24, packRGB332, packGray4, rgbTo332, hex565 } from "./lib/packers.js";
55
import { download, copyToClipboard } from "./lib/io.js";
66

77
// ---------- Component ----------
88
export default function Pixel2CPP() {
99
// Core state (make sure w/h are defined before any use)
10-
const [mode, setMode] = useState("1BIT"); // "1BIT" | "RGB565" | "RGB24" | "RGB332" | "GRAY4"
10+
const [drawMode, setDrawMode] = useState("HORIZONTAL_1BIT"); // Draw mode from dropdown
11+
const [outputFormat, setOutputFormat] = useState("ARDUINO_CODE"); // Output format from dropdown
1112
const [displayType, setDisplayType] = useState("SSD1306"); // Display-specific options
1213
const [w, setW] = useState(64);
1314
const [h, setH] = useState(64);
@@ -173,7 +174,7 @@ export default function Pixel2CPP() {
173174
b: id[i * 4 + 2],
174175
a: id[i * 4 + 3],
175176
}));
176-
if (mode === "1BIT") {
177+
if (drawMode.includes("1BIT") || drawMode.includes("ALPHA")) {
177178
for (let i = 0; i < out.length; i++) {
178179
const p = out[i];
179180
const luma = 0.2126 * p.r + 0.7152 * p.g + 0.0722 * p.b;
@@ -190,9 +191,57 @@ export default function Pixel2CPP() {
190191

191192
const generateCppCode = () => {
192193
const safeName = name.replace(/[^a-zA-Z0-9_]/g, "_");
193-
if (mode === "1BIT") {
194-
const bytes = pack1bit(data, w, h);
195-
const byteStr = bytes.map((b) => "0x" + b.toString(16).toUpperCase().padStart(2, "0")).join(", ");
194+
195+
// Determine data and format based on draw mode
196+
let bytes, byteStr, dataType, dataFormat;
197+
198+
if (drawMode === "HORIZONTAL_1BIT") {
199+
bytes = pack1bit(data, w, h, 'horizontal');
200+
dataType = "uint8_t";
201+
dataFormat = "bits";
202+
} else if (drawMode === "VERTICAL_1BIT") {
203+
bytes = pack1bit(data, w, h, 'vertical');
204+
dataType = "uint8_t";
205+
dataFormat = "bits";
206+
} else if (drawMode === "HORIZONTAL_ALPHA") {
207+
bytes = pack1bitAlpha(data, w, h, 'horizontal');
208+
dataType = "uint8_t";
209+
dataFormat = "alpha";
210+
} else if (drawMode === "HORIZONTAL_RGB565") {
211+
bytes = packRGB565(data, w, h);
212+
dataType = "uint16_t";
213+
dataFormat = "pixels";
214+
} else if (drawMode === "HORIZONTAL_RGB888_24") {
215+
bytes = packRGB24(data, w, h);
216+
dataType = "uint8_t";
217+
dataFormat = "pixels";
218+
} else if (drawMode === "HORIZONTAL_RGB888_32") {
219+
// 32-bit RGBA format
220+
bytes = [];
221+
for (let y = 0; y < h; y++) {
222+
for (let x = 0; x < w; x++) {
223+
const p = data[y * w + x];
224+
bytes.push(p.r, p.g, p.b, p.a);
225+
}
226+
}
227+
dataType = "uint8_t";
228+
dataFormat = "pixels";
229+
}
230+
231+
// Generate byte string based on output format
232+
if (outputFormat === "PLAIN_BYTES") {
233+
return generatePlainBytes(bytes, safeName, w, h, dataType);
234+
} else if (outputFormat === "ARDUINO_CODE") {
235+
return generateArduinoCode(bytes, safeName, w, h, dataType, dataFormat, drawMode);
236+
} else if (outputFormat === "ARDUINO_SINGLE_BITMAP") {
237+
return generateArduinoSingleBitmap(bytes, safeName, w, h, dataType, dataFormat);
238+
} else if (outputFormat === "GFX_BITMAP_FONT") {
239+
return generateGFXBitmapFont(bytes, safeName, w, h);
240+
}
241+
242+
// Legacy fallback for old mode system
243+
if (drawMode === "HORIZONTAL_1BIT") {
244+
byteStr = bytes.map((b) => "0x" + b.toString(16).toUpperCase().padStart(2, "0")).join(", ");
196245
return `// Generated by Pixel2CPP (1-bit)
197246
#include <Adafruit_GFX.h>
198247
#include <Adafruit_SSD1306.h>
@@ -213,7 +262,7 @@ void setup(){
213262
}
214263
215264
void loop(){}`;
216-
} else if (mode === "RGB565") {
265+
} else if (drawMode === "HORIZONTAL_RGB565") {
217266
const words = packRGB565(data, w, h);
218267
const wordStr = words.map(hex565).join(", ");
219268
return `// Generated by Pixel2CPP (RGB565 for TFT displays)
@@ -443,6 +492,130 @@ void drawGrayImage(int16_t x0, int16_t y0) {
443492
}
444493
};
445494

495+
// Output format generators
496+
const generatePlainBytes = (bytes, safeName, w, h, dataType) => {
497+
const formatByte = (b) => dataType === "uint16_t"
498+
? "0x" + b.toString(16).toUpperCase().padStart(4, "0")
499+
: "0x" + b.toString(16).toUpperCase().padStart(2, "0");
500+
501+
const byteStr = bytes.map(formatByte).join(", ");
502+
503+
return `// Generated by Pixel2CPP - Plain bytes format
504+
// ${w}x${h} pixels, ${bytes.length} bytes total
505+
// Data type: ${dataType}
506+
507+
${byteStr}`;
508+
};
509+
510+
const generateArduinoCode = (bytes, safeName, w, h, dataType, dataFormat, drawMode) => {
511+
const formatByte = (b) => dataType === "uint16_t"
512+
? "0x" + b.toString(16).toUpperCase().padStart(4, "0")
513+
: "0x" + b.toString(16).toUpperCase().padStart(2, "0");
514+
515+
const byteStr = bytes.map(formatByte).join(", ");
516+
517+
if (drawMode.includes("1BIT")) {
518+
return `// Generated by Pixel2CPP (${drawMode})
519+
#include <Adafruit_GFX.h>
520+
#include <Adafruit_SSD1306.h>
521+
522+
const uint16_t ${safeName}_w = ${w};
523+
const uint16_t ${safeName}_h = ${h};
524+
const ${dataType} ${safeName}_${dataFormat}[] PROGMEM = {
525+
${byteStr}
526+
};
527+
528+
Adafruit_SSD1306 display(128, 64, &Wire, -1);
529+
530+
void setup() {
531+
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
532+
display.clearDisplay();
533+
display.drawBitmap(0, 0, ${safeName}_${dataFormat}, ${safeName}_w, ${safeName}_h, 1);
534+
display.display();
535+
}
536+
537+
void loop() {}`;
538+
} else if (drawMode.includes("RGB565")) {
539+
return `// Generated by Pixel2CPP (RGB565)
540+
#include <Adafruit_GFX.h>
541+
#include <Adafruit_ST7735.h>
542+
543+
const uint16_t ${safeName}_w = ${w};
544+
const uint16_t ${safeName}_h = ${h};
545+
const ${dataType} ${safeName}_${dataFormat}[] PROGMEM = {
546+
${byteStr}
547+
};
548+
549+
#define TFT_CS 10
550+
#define TFT_RST 9
551+
#define TFT_DC 8
552+
553+
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);
554+
555+
void setup() {
556+
tft.initR(INITR_BLACKTAB);
557+
tft.fillScreen(ST77XX_BLACK);
558+
drawImage(0, 0);
559+
}
560+
561+
void drawImage(int16_t x0, int16_t y0) {
562+
tft.startWrite();
563+
tft.setAddrWindow(x0, y0, ${safeName}_w, ${safeName}_h);
564+
for (uint16_t i = 0; i < ${safeName}_w * ${safeName}_h; i++) {
565+
uint16_t color = pgm_read_word(&${safeName}_${dataFormat}[i]);
566+
tft.writePixel(color);
567+
}
568+
tft.endWrite();
569+
}
570+
571+
void loop() {}`;
572+
} else {
573+
return `// Generated by Pixel2CPP (${drawMode})
574+
#include <Adafruit_GFX.h>
575+
576+
const uint16_t ${safeName}_w = ${w};
577+
const uint16_t ${safeName}_h = ${h};
578+
const ${dataType} ${safeName}_${dataFormat}[] PROGMEM = {
579+
${byteStr}
580+
};
581+
582+
// Add your display setup and drawing code here`;
583+
}
584+
};
585+
586+
const generateArduinoSingleBitmap = (bytes, safeName, w, h, dataType, dataFormat) => {
587+
const formatByte = (b) => dataType === "uint16_t"
588+
? "0x" + b.toString(16).toUpperCase().padStart(4, "0")
589+
: "0x" + b.toString(16).toUpperCase().padStart(2, "0");
590+
591+
const byteStr = bytes.map(formatByte).join(", ");
592+
593+
return `// Single bitmap array - ${safeName}
594+
// ${w}x${h} pixels, ${bytes.length} bytes
595+
const ${dataType} ${safeName}[] PROGMEM = { ${byteStr} };`;
596+
};
597+
598+
const generateGFXBitmapFont = (bytes, safeName, w, h) => {
599+
const byteStr = bytes.map((b) => "0x" + b.toString(16).toUpperCase().padStart(2, "0")).join(", ");
600+
601+
return `// GFX Bitmap Font format - ${safeName}
602+
#include <Adafruit_GFX.h>
603+
604+
const uint8_t ${safeName}Bitmaps[] PROGMEM = {
605+
${byteStr}
606+
};
607+
608+
const GFXglyph ${safeName}Glyphs[] PROGMEM = {
609+
{ 0, ${w}, ${h}, ${w}, 0, 0 } // Single glyph covering entire bitmap
610+
};
611+
612+
const GFXfont ${safeName} PROGMEM = {
613+
(uint8_t *)${safeName}Bitmaps,
614+
(GFXglyph *)${safeName}Glyphs,
615+
0, 0, ${h}
616+
};`;
617+
};
618+
446619
const exportCpp = () => {
447620
const code = generateCppCode();
448621
const safeName = name.replace(/[^a-zA-Z0-9_]/g, "_");
@@ -477,26 +650,46 @@ void drawGrayImage(int16_t x0, int16_t y0) {
477650
const runTests = () => {
478651
const results = [];
479652

480-
// Test 1: 1-bit exact 8 pixels 10101010 → 0xAA
653+
// Test 1: 1-bit horizontal exact 8 pixels 10101010 → 0xAA
481654
{
482655
const tw = 8, th = 1;
483656
const px = [];
484657
for (let i = 0; i < 8; i++) px.push((i % 2 === 0) ? black() : white()); // 1,0,1,0,... (black=1)
485-
const bytes = pack1bit(px, tw, th);
658+
const bytes = pack1bit(px, tw, th, 'horizontal');
486659
const expect = [0xAA];
487-
results.push({ name: "1BIT 8px 10101010", pass: JSON.stringify(bytes) === JSON.stringify(expect), got: bytes, expect });
660+
results.push({ name: "1BIT Horizontal 8px 10101010", pass: JSON.stringify(bytes) === JSON.stringify(expect), got: bytes, expect });
488661
}
489662

490-
// Test 2: 1-bit width 10, row of all ones → [0xFF, 0xC0]
663+
// Test 2: 1-bit horizontal width 10, row of all ones → [0xFF, 0xC0]
491664
{
492665
const tw = 10, th = 1;
493666
const px = Array.from({ length: tw * th }, () => black());
494-
const bytes = pack1bit(px, tw, th);
667+
const bytes = pack1bit(px, tw, th, 'horizontal');
495668
const expect = [0xFF, 0xC0];
496-
results.push({ name: "1BIT 10px row all 1s", pass: JSON.stringify(bytes) === JSON.stringify(expect), got: bytes, expect });
669+
results.push({ name: "1BIT Horizontal 10px row all 1s", pass: JSON.stringify(bytes) === JSON.stringify(expect), got: bytes, expect });
497670
}
498671

499-
// Test 3: RGB565 primary colors
672+
// Test 3: 1-bit vertical packing
673+
{
674+
const tw = 1, th = 8;
675+
const px = [];
676+
for (let i = 0; i < 8; i++) px.push((i % 2 === 0) ? black() : white()); // 1,0,1,0,... (black=1)
677+
const bytes = pack1bit(px, tw, th, 'vertical');
678+
const expect = [0xAA]; // Same pattern but vertical
679+
results.push({ name: "1BIT Vertical 1x8 10101010", pass: JSON.stringify(bytes) === JSON.stringify(expect), got: bytes, expect });
680+
}
681+
682+
// Test 4: 1-bit alpha map
683+
{
684+
const tw = 8, th = 1;
685+
const px = [];
686+
for (let i = 0; i < 8; i++) px.push({ r: 255, g: 255, b: 255, a: (i % 2 === 0) ? 255 : 0 }); // alpha pattern
687+
const bytes = pack1bitAlpha(px, tw, th, 'horizontal');
688+
const expect = [0xAA];
689+
results.push({ name: "1BIT Alpha map 8px pattern", pass: JSON.stringify(bytes) === JSON.stringify(expect), got: bytes, expect });
690+
}
691+
692+
// Test 5: RGB565 primary colors
500693
{
501694
const tw = 3, th = 1;
502695
const px = [
@@ -509,7 +702,7 @@ void drawGrayImage(int16_t x0, int16_t y0) {
509702
results.push({ name: "RGB565 R,G,B", pass: JSON.stringify(words) === JSON.stringify(expect), got: words, expect });
510703
}
511704

512-
// Test 4: RGB24 format - 2x2 image with RGB colors
705+
// Test 6: RGB24 format - 2x2 image with RGB colors
513706
{
514707
const tw = 2, th = 2;
515708
const px = [
@@ -528,7 +721,7 @@ void drawGrayImage(int16_t x0, int16_t y0) {
528721
results.push({ name: "RGB24 2x2 colors", pass: JSON.stringify(rgb24Data) === JSON.stringify(expect), got: rgb24Data, expect });
529722
}
530723

531-
// Test 5: RGB332 format - primary colors
724+
// Test 7: RGB332 format - primary colors
532725
{
533726
const tw = 3, th = 1;
534727
const px = [
@@ -545,7 +738,7 @@ void drawGrayImage(int16_t x0, int16_t y0) {
545738
results.push({ name: "RGB332 R,G,B", pass: JSON.stringify(rgb332Data) === JSON.stringify(expect), got: rgb332Data, expect });
546739
}
547740

548-
// Test 6: GRAY4 format - grayscale values
741+
// Test 8: GRAY4 format - grayscale values
549742
{
550743
const tw = 4, th = 1;
551744
const px = [
@@ -626,13 +819,25 @@ void drawGrayImage(int16_t x0, int16_t y0) {
626819
<label className="flex items-center gap-1"><input type="checkbox" checked={mirrorX} onChange={(e) => setMirrorX(e.target.checked)} />Mirror X</label>
627820
<label className="flex items-center gap-1"><input type="checkbox" checked={mirrorY} onChange={(e) => setMirrorY(e.target.checked)} />Mirror Y</label>
628821
</div>
629-
<select value={mode} onChange={(e) => setMode(e.target.value)} className="bg-neutral-800 rounded px-2 py-1 text-sm">
630-
<option value="1BIT">1-bit (SSD1306 OLED)</option>
631-
<option value="RGB565">RGB565 (ST7735/ILI9341 TFT)</option>
632-
<option value="RGB24">RGB24 (ESP32/High Memory)</option>
633-
<option value="RGB332">RGB332 (SSD1331/Low Memory)</option>
634-
<option value="GRAY4">4-bit Grayscale (E-ink/EPD)</option>
635-
</select>
822+
<div className="space-y-2 w-full">
823+
<label className="block text-sm font-medium">Draw mode:</label>
824+
<select value={drawMode} onChange={(e) => setDrawMode(e.target.value)} className="w-full bg-neutral-800 rounded px-2 py-1 text-sm">
825+
<option value="HORIZONTAL_1BIT">Horizontal - 1 bit per pixel</option>
826+
<option value="VERTICAL_1BIT">Vertical - 1 bit per pixel</option>
827+
<option value="HORIZONTAL_RGB565">Horizontal - 2 bytes per pixel (565)</option>
828+
<option value="HORIZONTAL_ALPHA">Horizontal - 1 bit per pixel alpha map</option>
829+
<option value="HORIZONTAL_RGB888_24">Horizontal - 3 bytes per pixel (rgb888)</option>
830+
<option value="HORIZONTAL_RGB888_32">Horizontal - 4 bytes per pixel (rgba888)</option>
831+
</select>
832+
833+
<label className="block text-sm font-medium mt-2">Code output format:</label>
834+
<select value={outputFormat} onChange={(e) => setOutputFormat(e.target.value)} className="w-full bg-neutral-800 rounded px-2 py-1 text-sm">
835+
<option value="ARDUINO_CODE">Arduino code</option>
836+
<option value="PLAIN_BYTES">Plain bytes</option>
837+
<option value="ARDUINO_SINGLE_BITMAP">Arduino code, single bitmap</option>
838+
<option value="GFX_BITMAP_FONT">Adafruit GFXbitmapFont</option>
839+
</select>
840+
</div>
636841
<div className="flex gap-2 w-full">
637842
<button onClick={clearCanvas} className="px-3 py-1.5 rounded-xl bg-neutral-800 hover:bg-neutral-700 text-sm">Clear</button>
638843
<button onClick={() => { if (canUndo) setRedo((r) => [data.map(p=>({...p})), ...r]); if (canUndo) { const last = history[history.length - 1]; setHistory((h) => h.slice(0, -1)); setData(last.map(p => ({ ...p }))); } }} disabled={!canUndo} className={`px-3 py-1.5 rounded-xl text-sm ${canUndo ? "bg-neutral-800 hover:bg-neutral-700" : "bg-neutral-900 opacity-50"}`}>Undo</button>
@@ -659,7 +864,8 @@ void drawGrayImage(int16_t x0, int16_t y0) {
659864
</div>
660865
<button onClick={swapColors} className="px-3 py-1.5 rounded-xl bg-neutral-800 hover:bg-neutral-700 text-sm">Swap</button>
661866
</div>
662-
<div className="text-sm opacity-80">Mode: <span className="font-mono">{mode}</span></div>
867+
<div className="text-sm opacity-80">Draw mode: <span className="font-mono text-xs">{drawMode}</span></div>
868+
<div className="text-sm opacity-80">Output: <span className="font-mono text-xs">{outputFormat}</span></div>
663869
<div className="text-xs opacity-70">Tip: Right‑click draws with Secondary. Eyedropper picks Primary from canvas.</div>
664870
</div>
665871
</aside>

0 commit comments

Comments
 (0)