11import React , { useEffect , useMemo , useRef , useState } from "react" ;
22import PixelCanvas from "./components/PixelCanvas.jsx" ;
33import { 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" ;
55import { download , copyToClipboard } from "./lib/io.js" ;
66
77// ---------- Component ----------
88export 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 - z A - Z 0 - 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
215264void 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 - z A - Z 0 - 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