Uncategorized

Make Pocket Color Mixer for Artists

Hello people in this article i will show you how to make pocket color mixer for artists that can help you with color mixing.

Imagine you want to pain something and miss some tones of colors in such case what would you do?

Obviosuly most of us will search online and get the desired colors! But what if you dont have internet?

This offline pocket color mixer is the solution! With this device you can mix and see the resulting colors.

There is no limit to colors that you can generate with this device! and the best part is this fits in your pocket.

LCD screen shows the colors live and a single switch will help you choose and select colors.

Live RGB LED strip shows the mixed colors and the resulting colors for more detailed output

Materials required to build pocket color mixer

  • These are the parts which you will need to make this pocket color mixer
  • Wemos D1 Mini
  • Rotary Encoder
  • Jumper Cables
  • 240×240 GC9A01 Round Display Module
  • ARGB Led strip (straight 8 bits)
  • Programming Cable
  • Arduino IDE
  • 3D Printer

Designing the frame

I will be 3d printing the frame for this project, i will design them using Tinkercad.

3d design for color mixer

You can simply use my STL files for 3d printing, the parts are small so you cna expect print time to be less.

These are the parts that we will be using for this project and i chose different colors.

Dont have a 3d printer? There is nothing to worry about!

You can get the 3d printed parts for this project from Justway

High-quality prints with fast shipping and many offers if you sign up now!

They not only have PLA 3d printing but also offer services in Stereolithography (SLA), Selective Laser Sintering (SLS), Digital Light Processing (DLP), Multi Jet Fusion (MJF), Fused Deposition Modeling (FDM), and Selective Laser Melting (SLM).

Justway checks every part with ISO9001:2015 standards and routine in-process inspections every 2 hours during production.

Also a final inspection report covering dimensions, tolerances, and appearance before shipping

Their summer sale is on and rapid prototyping cnc machine services(aluminum, mild steel, alloy steel and ABS) starting at just $9 only and in-demand laser cutting is as low as $9, Grab this soon.

Whether you need one part for your project or for a small production run, JUSTWAY makes it easier to get professional results. Check them out at Justway.com

Arduino Code for this project

This is arduino program which we will be using in this project, You can simply use my sketch.

I recommend you to upload the code first and then proceed with the circuit making part.

If the number of LED in your strip is more or less than 8 you can make that changes in the code.

#include <Adafruit_GFX.h>
#include <Adafruit_GC9A01A.h>
#include <Adafruit_NeoPixel.h>

// ═══════════════════════════════════════════
// PINS
// ═══════════════════════════════════════════
#define TFT_CS   15
#define TFT_DC    4
#define TFT_RST   5
#define ENC_CLK   0
#define ENC_DT    2
#define ENC_SW    1
#define LED_PIN  12
#define LED_COUNT 8

Adafruit_GC9A01A  tft(TFT_CS, TFT_DC, TFT_RST);
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN,
                         NEO_GRB + NEO_KHZ800);

// ═══════════════════════════════════════════
// DISPLAY CONSTANTS
// Round display — safe drawing radius = 115px
// Center = (120,120)
// ═══════════════════════════════════════════
#define CX       120
#define CY       120
#define SAFE_R   114    // stay inside this radius

// ═══════════════════════════════════════════
// PALETTE — 32 artist colors
// ═══════════════════════════════════════════
struct PColor { const char* name; uint8_t r,g,b; };

static const PColor palette[] = {
  {"White",        253,253,253},
  {"Ivory",        255,255,240},
  {"Cream",        255,253,208},
  {"Lemon Yellow", 255,250, 90},
  {"Cad Yellow",   255,246,  0},
  {"Yellow Ochre", 204,153,  0},
  {"Raw Sienna",   210,125, 50},
  {"Burnt Sienna", 138, 54, 15},
  {"Scarlet",      255, 36,  0},
  {"Cadmium Red",  227, 66, 52},
  {"Crimson",      220, 20, 60},
  {"Rose",         255,  0,127},
  {"Magenta",      255,  0,255},
  {"Violet",       148, 88,178},
  {"Purple",       128,  0,128},
  {"Indigo",        75,  0,130},
  {"Ultramarine",   18, 10,143},
  {"Cobalt Blue",    0, 71,171},
  {"Cerulean",      42,130,188},
  {"Sky Blue",     135,206,235},
  {"Cyan",           0,255,255},
  {"Emerald",        0,201, 87},
  {"Viridian",      64,130,109},
  {"Sap Green",     80,125, 42},
  {"Olive",        100,100,  0},
  {"Raw Umber",    115, 74, 18},
  {"Burnt Umber",  138, 51, 36},
  {"Brown",        139, 90, 43},
  {"Warm Gray",    180,170,160},
  {"Cool Gray",    150,160,175},
  {"Ivory Black",   25, 25, 25},
  {"Mars Black",    10, 10, 10},
};
#define PAL_COUNT 32

// ═══════════════════════════════════════════
// REALISTIC PAINT MIX RULES
// ═══════════════════════════════════════════
struct MixRule {
  uint8_t ai, bi, rr, rg, rb;
  const char* rname;
};

static const MixRule mixRules[] = {
  { 4, 9,  230,100,  0, "Orange"       },
  { 3, 8,  240, 80,  0, "Warm Orange"  },
  { 9,17,  128,  0,128, "Purple"       },
  {10,16,  140, 20,120, "Red-Violet"   },
  { 4,17,   50,150, 50, "Mid Green"    },
  { 3,17,   80,180, 30, "Yellow-Green" },
  { 9, 0,  255,160,170, "Pink"         },
  {10, 0,  255,140,160, "Rose Pink"    },
  {30, 0,  200,200,200, "Light Gray"   },
  {31, 0,  128,128,128, "Gray"         },
  {17,22,    0,128,128, "Teal"         },
  {18,21,    0,180,180, "Cyan-Teal"    },
  {27, 0,  220,190,160, "Beige"        },
  {31, 9,   90,  0,  0, "Dark Red"     },
  {31,17,    0, 20, 80, "Navy"         },
  {31, 4,   90, 90,  0, "Olive"        },
  { 0,17,  173,216,230, "Light Blue"   },
  {12, 4,  255, 60,  0, "Flame"        },
  {20, 4,  150,230,  0, "Chartreuse"   },
  { 9,22,  100, 50, 20, "Mud Brown"    },
  { 4,22,  140,200,  0, "Lime"         },
  {12,17,  100,  0,200, "Blue-Violet"  },
  { 8, 4,  200,110,  0, "Amber"        },
  {17, 0,  173,216,230, "Baby Blue"    },
};
#define MIX_RULE_COUNT \
  (sizeof(mixRules)/sizeof(MixRule))

// ═══════════════════════════════════════════
// ENCODER
// ═══════════════════════════════════════════
volatile int encRaw  = 0;
int          encLast = 0;

ICACHE_RAM_ATTR void encISR() {
  if(digitalRead(ENC_CLK) != digitalRead(ENC_DT))
    encRaw++;
  else
    encRaw--;
}

int encDelta() {
  int d = encRaw - encLast;
  if(d >=  2) { encLast = encRaw; return  1; }
  if(d <= -2) { encLast = encRaw; return -1; }
  return 0;
}

bool          btnLast   = HIGH;
unsigned long btnMs     = 0;
bool          gotShort  = false;
bool          gotLong   = false;
bool          longFired = false;
#define LONG_MS 700

void pollBtn() {
  gotShort = gotLong = false;
  bool b = digitalRead(ENC_SW);
  if(btnLast==HIGH && b==LOW) {
    btnMs=millis(); longFired=false;
  }
  if(b==LOW && !longFired &&
     millis()-btnMs >= LONG_MS) {
    gotLong=true; longFired=true;
  }
  if(btnLast==LOW && b==HIGH) {
    if(!longFired && millis()-btnMs>40)
      gotShort=true;
  }
  btnLast=b;
}

// ═══════════════════════════════════════════
// APP STATE
// ═══════════════════════════════════════════
enum AppState {
  BOOT, MAIN_MENU,
  MIXER_PRESET, MIXER_WHEEL
};
AppState state = BOOT;

int   menuSel   = 0;
int   activeSlot = 0;
int   pSel[2]   = {4, 17};
float wHue[2]   = {0.0f, 200.0f};

uint8_t     curR[2], curG[2], curB[2];
char        cName[2][14];
uint8_t     mixR, mixG, mixB;
const char* mixName = "Blend";

bool needRedraw = true;

// Slot-switch flash
bool          slotFlash    = false;
unsigned long slotFlashMs  = 0;
#define FLASH_DUR 120

// ═══════════════════════════════════════════
// LED smooth fade
// ═══════════════════════════════════════════
struct LEDRGB { uint8_t r,g,b; };
LEDRGB        ledCur[LED_COUNT];
LEDRGB        ledTgt[LED_COUNT];
unsigned long lastLedMs = 0;

void setLedTarget(int i,
                  uint8_t r,
                  uint8_t g,
                  uint8_t b) {
  if(i>=0 && i<LED_COUNT)
    ledTgt[i] = {r,g,b};
}

void tickLEDs() {
  if(millis()-lastLedMs < 16) return;
  lastLedMs = millis();
  bool ch = false;
  for(int i=0;i<LED_COUNT;i++) {
    auto step=[](uint8_t a,uint8_t b)
               ->uint8_t {
      if(a==b) return a;
      int d=(int)b-(int)a;
      int s=d/5; if(!s) s=d>0?1:-1;
      return (uint8_t)constrain(a+s,0,255);
    };
    uint8_t nr=step(ledCur[i].r,ledTgt[i].r);
    uint8_t ng=step(ledCur[i].g,ledTgt[i].g);
    uint8_t nb=step(ledCur[i].b,ledTgt[i].b);
    if(nr!=ledCur[i].r ||
       ng!=ledCur[i].g ||
       nb!=ledCur[i].b) {
      ledCur[i]={nr,ng,nb};
      strip.setPixelColor(i,
        strip.Color(nr,ng,nb));
      ch=true;
    }
  }
  if(ch) strip.show();
}

// ═══════════════════════════════════════════
// COLOR UTILS
// ═══════════════════════════════════════════
#define C_BG      0x0002
#define C_PANEL   0x0863
#define C_DGRAY   0x2104
#define C_GRAY    0x4A69
#define C_LGRAY   0xBDF7
#define C_WHITE   0xFFFF
#define C_ACCENT  0x0FFF   // cyan
#define C_GOLD    0xFCE0
#define C_BLACK   0x0000

uint16_t rgb565(uint8_t r,
                uint8_t g,
                uint8_t b) {
  return ((r&0xF8)<<8)|((g&0xFC)<<3)|(b>>3);
}

// FIX 1: Complete HSV — bf was missing
void hsv2rgb(float h, float s, float v,
             uint8_t &r,
             uint8_t &g,
             uint8_t &b) {
  float c  = v*s;
  float x  = c*(1-fabs(fmod(h/60.0f,2)-1));
  float m  = v-c;
  float rf=0,gf=0,bf=0;
  int   hi = (int)(h/60.0f)%6;
  if(hi==0){rf=c;gf=x;bf=0;}
  else if(hi==1){rf=x;gf=c;bf=0;}
  else if(hi==2){rf=0;gf=c;bf=x;}
  else if(hi==3){rf=0;gf=x;bf=c;}
  else if(hi==4){rf=x;gf=0;bf=c;}
  else          {rf=c;gf=0;bf=x;}
  r=(uint8_t)((rf+m)*255.0f);
  g=(uint8_t)((gf+m)*255.0f);
  b=(uint8_t)((bf+m)*255.0f);
}

// ═══════════════════════════════════════════
// COMPUTE MIX + LED TARGETS
// ═══════════════════════════════════════════
void computeColors(bool wheelMode) {
  if(wheelMode) {
    for(int i=0;i<2;i++) {
      hsv2rgb(wHue[i],1.0f,0.95f,
              curR[i],curG[i],curB[i]);
      sprintf(cName[i],"H:%.0f",wHue[i]);
    }
  } else {
    for(int i=0;i<2;i++) {
      curR[i]=palette[pSel[i]].r;
      curG[i]=palette[pSel[i]].g;
      curB[i]=palette[pSel[i]].b;
      strncpy(cName[i],
              palette[pSel[i]].name,13);
      cName[i][13]='\0';
    }
  }

  // Lookup realistic mix
  mixName = nullptr;
  if(!wheelMode) {
    for(int i=0;i<(int)MIX_RULE_COUNT;i++) {
      const MixRule& m = mixRules[i];
      if((m.ai==pSel[0]&&m.bi==pSel[1])||
         (m.ai==pSel[1]&&m.bi==pSel[0])) {
        mixR=m.rr; mixG=m.rg; mixB=m.rb;
        mixName=m.rname;
        break;
      }
    }
  }

  // Quadratic blend fallback
  if(!mixName) {
    mixR=(uint8_t)sqrt(
      ((float)curR[0]*curR[0]+
       (float)curR[1]*curR[1])/2.0f);
    mixG=(uint8_t)sqrt(
      ((float)curG[0]*curG[0]+
       (float)curG[1]*curG[1])/2.0f);
    mixB=(uint8_t)sqrt(
      ((float)curB[0]*curB[0]+
       (float)curB[1]*curB[1])/2.0f);
    mixName="Blend";
  }

  // LED targets
  setLedTarget(0,curR[0],curG[0],curB[0]);
  setLedTarget(1,curR[0],curG[0],curB[0]);
  setLedTarget(2,0,0,0);
  setLedTarget(3,curR[1],curG[1],curB[1]);
  setLedTarget(4,curR[1],curG[1],curB[1]);
  setLedTarget(5,0,0,0);
  setLedTarget(6,mixR,mixG,mixB);
  setLedTarget(7,mixR,mixG,mixB);
}

// ═══════════════════════════════════════════
// DRAW HELPERS
// ═══════════════════════════════════════════

// FIX 2: centered() — simpler, faster,
// no getTextBounds call needed
void centered(const char* s, int x, int y,
              int sz, uint16_t col) {
  int cw = 6*sz;   // character width
  int len = strlen(s);
  tft.setTextSize(sz);
  tft.setTextColor(col, C_BG);
  tft.setCursor(x - (len*cw)/2,
                y - (8*sz)/2);
  tft.print(s);
}

// centered with explicit background
void centeredBG(const char* s, int x, int y,
                int sz, uint16_t col,
                uint16_t bg) {
  int cw  = 6*sz;
  int len = strlen(s);
  tft.setTextSize(sz);
  tft.setTextColor(col, bg);
  tft.setCursor(x-(len*cw)/2,
                y-(8*sz)/2);
  tft.print(s);
}

// FIX 3: drawBall — correct specular position
// and proper rim gradient
void drawBall(int cx, int cy, int r,
              uint8_t R, uint8_t G,
              uint8_t B,
              bool active,
              uint16_t glowCol) {
  // Outer glow / inactive ring
  if(active) {
    tft.drawCircle(cx,cy,r+6,C_DGRAY);
    tft.drawCircle(cx,cy,r+5,glowCol);
    tft.drawCircle(cx,cy,r+4,glowCol);
    tft.drawCircle(cx,cy,r+3,
      rgb565(
        min(255,(int)((glowCol>>11&0x1F)*9)),
        min(255,(int)((glowCol>>5 &0x3F)*4)),
        min(255,(int)((glowCol    &0x1F)*9))
      ));
  } else {
    tft.drawCircle(cx,cy,r+4,C_DGRAY);
    tft.drawCircle(cx,cy,r+3,C_DGRAY);
  }

  // Main fill
  tft.fillCircle(cx,cy,r,rgb565(R,G,B));

  // Darker rim for 3D depth
  uint8_t rr=max(0,R-50);
  uint8_t rg=max(0,G-50);
  uint8_t rb=max(0,B-50);
  tft.drawCircle(cx,cy,r,  rgb565(rr,rg,rb));
  tft.drawCircle(cx,cy,r-1,
    rgb565(max(0,R-25),
           max(0,G-25),
           max(0,B-25)));

  // FIX: specular at top-left of sphere
  // offset = r*0.35 at 315deg (top-left)
  int hx = cx - (int)(r*0.32f);
  int hy = cy - (int)(r*0.32f);

  // Soft specular gradient
  int hr = max(3, r/4);
  tft.fillCircle(hx, hy, hr,
    rgb565(min(255,R+120),
           min(255,G+120),
           min(255,B+120)));
  tft.fillCircle(hx-1, hy-1,
    max(2,hr-2), C_WHITE);
}

// FIX 4: Safe fill for round display
// Clips rect to stay inside circle
void safeFillRect(int x, int y,
                  int w, int h,
                  uint16_t col) {
  for(int row=y; row<y+h; row++) {
    // Clip horizontal extent to circle
    int dy = row - CY;
    int maxX = (int)sqrt(
      max(0, SAFE_R*SAFE_R - dy*dy));
    int x1 = max(x,  CX-maxX);
    int x2 = min(x+w,CX+maxX);
    if(x2>x1)
      tft.drawFastHLine(x1,row,x2-x1,col);
  }
}

// ═══════════════════════════════════════════
// DRAW: BOOT SCREEN
// FIX 5: step=1 for gap-free rainbow ring
// ═══════════════════════════════════════════
void drawBoot() {
  tft.fillScreen(C_BG);

  // FIX: draw 2 pixels wide at each angle
  // so ring has no gaps at any radius
  for(int a=0;a<360;a++) {
    uint8_t r,g,b;
    hsv2rgb(a,1.0f,0.9f,r,g,b);
    uint16_t c = rgb565(r,g,b);
    float rad = a*PI/180.0f;
    float cs  = cos(rad), sn = sin(rad);
    // Draw ring from r=106 to r=117
    for(int rr=106;rr<=117;rr++) {
      int px = CX+(int)(rr*cs);
      int py = CY+(int)(rr*sn);
      tft.drawPixel(px,py,c);
    }
    if(a%90==0) yield();
  }

  // Title
  centered("POCKET",  CX,76,3,C_GOLD);
  tft.drawFastHLine(50,100,140,C_GRAY);
  centered("COLOR MIXER",CX,118,2,C_ACCENT);
  centered("FOR ARTISTS", CX,140,1,C_LGRAY);
  centered("By RoboHub",        CX,158,1,C_GRAY);

  // Three color dots
  uint8_t r,g,b;
  hsv2rgb(0,  1,0.9f,r,g,b);
  tft.fillCircle(90, 183,10,rgb565(r,g,b));
  hsv2rgb(120,1,0.9f,r,g,b);
  tft.fillCircle(120,183,10,rgb565(r,g,b));
  hsv2rgb(240,1,0.9f,r,g,b);
  tft.fillCircle(150,183,10,rgb565(r,g,b));

  // Thin ring around dots
  tft.drawCircle(90, 183,11,C_DGRAY);
  tft.drawCircle(120,183,11,C_DGRAY);
  tft.drawCircle(150,183,11,C_DGRAY);

  centered("Click knob to start",
           CX,212,1,C_DGRAY);
}

// ═══════════════════════════════════════════
// DRAW: MAIN MENU
// FIX 6: Arrow replaced with left-edge bar
// so it never clips on round display
// ═══════════════════════════════════════════
void drawMenu() {
  tft.fillScreen(C_BG);

  // Safe outer decorative rings
  tft.drawCircle(CX,CY,118,C_DGRAY);
  tft.drawCircle(CX,CY,116,C_PANEL);

  centered("ATELIER v7.0",CX,44,2,C_GOLD);
  tft.drawFastHLine(40,62,160,C_GRAY);
  centered("SELECT MODE",CX,78,1,C_LGRAY);

  // ── Option 1: Preset Palette ──
  bool s0 = (menuSel==0);
  tft.fillRoundRect(30,98,180,40,
                    10,s0?C_PANEL:C_BG);
  if(s0) {
    tft.drawRoundRect(30,98,180,40,
                      10,C_ACCENT);
    // Left selection bar
    tft.fillRect(30,102,4,32,C_ACCENT);
  }
  // Color swatches
  uint8_t dr,dg,db;
  hsv2rgb(  0,1,0.8f,dr,dg,db);
  tft.fillCircle(52,118,6,rgb565(dr,dg,db));
  hsv2rgb(120,1,0.8f,dr,dg,db);
  tft.fillCircle(65,118,6,rgb565(dr,dg,db));
  hsv2rgb(240,1,0.8f,dr,dg,db);
  tft.fillCircle(78,118,6,rgb565(dr,dg,db));
  centered("PRESET PALETTE",145,118,1,
           s0?C_ACCENT:C_GRAY);

  // ── Option 2: Colour Wheel ──
  bool s1 = (menuSel==1);
  tft.fillRoundRect(30,148,180,40,
                    10,s1?C_PANEL:C_BG);
  if(s1) {
    tft.drawRoundRect(30,148,180,40,
                      10,C_GOLD);
    tft.fillRect(30,152,4,32,C_GOLD);
  }
  // Mini rainbow arc
  for(int a=0;a<=180;a+=10) {
    hsv2rgb(a*2,1,0.8f,dr,dg,db);
    float rad=a*PI/180.0f;
    int px=52+(int)(14*cos(rad));
    int py=168-(int)(14*sin(rad));
    tft.fillCircle(px,py,3,
                   rgb565(dr,dg,db));
  }
  centered("COLOUR WHEEL",145,168,1,
           s1?C_GOLD:C_GRAY);

  centered("Rotate = Navigate   Click = OK",
           CX,206,1,C_DGRAY);
}

// ═══════════════════════════════════════════
// DRAW: PALETTE SCROLL STRIP
// FIX 7: dots stay inside display bounds
// Reduced to 5 visible (not 7) to fit safely
// ═══════════════════════════════════════════
void drawPaletteStrip(int sel) {
  // Clear strip area
  tft.fillRect(14,32,212,30,C_BG);

  // Show 5 dots: -2 to +2
  int dotR   = 9;
  int spacing= 34;
  // Center 5 dots: positions at -2,-1,0,+1,+2
  // x positions: 120 + off*34
  // Range: 120-68=52 to 120+68=188 — safe

  for(int off=-2; off<=2; off++) {
    int idx = (sel+off+PAL_COUNT)%PAL_COUNT;
    int x   = CX + off*spacing;
    int y   = 47;
    bool isSel = (off==0);
    int r = isSel ? dotR+4 : dotR-1;

    uint16_t c = rgb565(
      palette[idx].r,
      palette[idx].g,
      palette[idx].b);

    tft.fillCircle(x,y,r,c);

    if(isSel) {
      tft.drawCircle(x,y,r+2,C_WHITE);
      tft.drawCircle(x,y,r+3,C_ACCENT);
      // Small notch below selected
      tft.fillTriangle(x-3,y+r+5,
                       x+3,y+r+5,
                       x,  y+r+9,
                       C_ACCENT);
    } else {
      // Dim ring on non-selected
      tft.drawCircle(x,y,r+1,C_DGRAY);
    }
  }

  // FIX 8: Index in a proper pill — top right
  char buf[8];
  sprintf(buf,"%d/%d",sel+1,PAL_COUNT);
  tft.fillRoundRect(191,33,42,16,4,C_PANEL);
  tft.setTextSize(1);
  tft.setTextColor(C_ACCENT,C_PANEL);
  tft.setCursor(196,38);
  tft.print(buf);
}

// ═══════════════════════════════════════════
// DRAW: MIXER PRESET
// FIX 9: All labels repositioned to be
// inside the 114px safe radius
// ═══════════════════════════════════════════
void drawMixerPreset() {
  tft.fillScreen(C_BG);

  // Header pill
  tft.fillRoundRect(25,6,190,22,
                    6,C_PANEL);
  tft.drawRoundRect(25,6,190,22,
                    6,C_ACCENT);
  centered("PRESET STUDIO",CX,17,
           1,C_ACCENT);

  // Palette strip
  drawPaletteStrip(pSel[activeSlot]);

  // ── SLOT A — left ball ──
  bool aActive = (activeSlot==0);
  // Ball center at (72,112)
  drawBall(72,112,34,
           curR[0],curG[0],curB[0],
           aActive,C_ACCENT);

  // Slot A label pill above ball
  tft.fillRoundRect(36,70,72,16,
                    4,aActive?C_PANEL:C_BG);
  if(aActive)
    tft.drawRoundRect(36,70,72,16,
                      4,C_ACCENT);
  centered("SLOT  A",72,78,1,
           aActive?C_ACCENT:C_GRAY);

  // Color name A — below ball, one line
  tft.fillRect(8,150,112,11,C_BG);
  centered(cName[0],72,156,1,C_WHITE);

  // HEX A
  char hexA[10];
  sprintf(hexA,"#%02X%02X%02X",
          curR[0],curG[0],curB[0]);
  tft.fillRect(8,162,112,10,C_BG);
  centered(hexA,72,168,1,
           aActive?C_ACCENT:C_DGRAY);

  // ── Plus sign ──
  centered("+",CX,112,2,C_GRAY);

  // ── SLOT B — right ball ──
  bool bActive = (activeSlot==1);
  drawBall(168,112,34,
           curR[1],curG[1],curB[1],
           bActive,C_GOLD);

  tft.fillRoundRect(132,70,72,16,
                    4,bActive?C_PANEL:C_BG);
  if(bActive)
    tft.drawRoundRect(132,70,72,16,
                      4,C_GOLD);
  centered("SLOT  B",168,78,1,
           bActive?C_GOLD:C_GRAY);

  tft.fillRect(120,150,112,11,C_BG);
  centered(cName[1],168,156,1,C_WHITE);

  char hexB[10];
  sprintf(hexB,"#%02X%02X%02X",
          curR[1],curG[1],curB[1]);
  tft.fillRect(120,162,112,10,C_BG);
  centered(hexB,168,168,1,
           bActive?C_GOLD:C_DGRAY);

  // ── Equals line ──
  tft.drawFastHLine(44,180,152,C_DGRAY);
  centered("=",CX,180,1,C_GRAY);

  // FIX 10: Result ball moved up to y=200
  // so it stays inside round display
  drawBall(CX,200,20,
           mixR,mixG,mixB,false,C_WHITE);

  // Result name on same y-zone
  tft.fillRect(44,220,152,11,C_BG);
  centered(mixName,CX,226,1,C_GOLD);

  // Bottom hint — very bottom safe area
  tft.fillRect(30,232,180,8,C_BG);
  centered("click=swap  hold=menu",
           CX,236,1,C_DGRAY);
}

// ═══════════════════════════════════════════
// DRAW: MIXER WHEEL
// FIX 11: Wheel ring drawn without gaps
// using pixel-fill approach
// ═══════════════════════════════════════════
void drawMixerWheel() {
  tft.fillScreen(C_BG);

  // Header
  tft.fillRoundRect(25,6,190,22,6,C_PANEL);
  bool aA=(activeSlot==0);
  tft.drawRoundRect(25,6,190,22,6,
                    aA?C_ACCENT:C_GOLD);
  centered(aA?"WHEEL  SLOT A":
              "WHEEL  SLOT B",
           CX,17,1,aA?C_ACCENT:C_GOLD);

  // FIX: Draw ring pixel by pixel at step=1
  // to eliminate gaps
  int wcx=CX, wcy=118, ro=86, ri=58;
  for(int a=0;a<360;a++) {
    uint8_t r,g,b;
    hsv2rgb(a,1.0f,0.9f,r,g,b);
    uint16_t c = rgb565(r,g,b);
    float rad = (a-90)*PI/180.0f;
    float cs  = cos(rad), sn = sin(rad);
    for(int rr=ri;rr<=ro;rr++) {
      tft.drawPixel(
        wcx+(int)(rr*cs),
        wcy+(int)(rr*sn), c);
    }
    if(a%90==0) yield();
  }

  // Inner circle fill
  tft.fillCircle(wcx,wcy,ri-1,C_BG);

  // Current hue color in inner circle
  uint8_t cr,cg,cb;
  hsv2rgb(wHue[activeSlot],1,0.9f,cr,cg,cb);
  tft.fillCircle(wcx,wcy,ri-3,
                 rgb565(cr,cg,cb));
  // Inner circle rim
  tft.drawCircle(wcx,wcy,ri-3,
    rgb565(max(0,cr-60),
           max(0,cg-60),
           max(0,cb-60)));

  // Hue number in center
  char hbuf[8];
  sprintf(hbuf,"%.0f",
          wHue[activeSlot]);
  centeredBG(hbuf,wcx,wcy,2,
             C_WHITE,rgb565(cr,cg,cb));

  // FIX 12: Pointer uses filled triangle
  // for better visibility
  float prad=(wHue[activeSlot]-90)
             *PI/180.0f;
  int pmid=(ro+ri)/2+3;
  int px=wcx+(int)(pmid*cos(prad));
  int py=wcy+(int)(pmid*sin(prad));
  // White dot with dark outline
  tft.fillCircle(px,py,7,C_WHITE);
  tft.drawCircle(px,py,8,C_BLACK);
  tft.drawCircle(px,py,9,C_DGRAY);

  // ── Bottom previews ──
  // A preview (left)
  uint8_t r0,g0,b0,r1,g1,b1;
  hsv2rgb(wHue[0],1,0.9f,r0,g0,b0);
  hsv2rgb(wHue[1],1,0.9f,r1,g1,b1);

  // A circle
  tft.fillCircle(48,207,14,
                 rgb565(r0,g0,b0));
  tft.drawCircle(48,207,15,
    activeSlot==0?C_ACCENT:C_DGRAY);
  tft.drawCircle(48,207,16,
    activeSlot==0?C_ACCENT:C_BG);
  centered("A",48,207,1,C_WHITE);

  // Mix circle (center)
  tft.fillCircle(CX,207,12,
                 rgb565(mixR,mixG,mixB));
  tft.drawCircle(CX,207,13,C_WHITE);

  // B circle (right)
  tft.fillCircle(192,207,14,
                 rgb565(r1,g1,b1));
  tft.drawCircle(192,207,15,
    activeSlot==1?C_GOLD:C_DGRAY);
  tft.drawCircle(192,207,16,
    activeSlot==1?C_GOLD:C_BG);
  centered("B",192,207,1,C_WHITE);

  // Hue values under circles
  char hA[8],hB[8];
  sprintf(hA,"%.0f",wHue[0]);
  sprintf(hB,"%.0f",wHue[1]);
  tft.fillRect(30,220,40,10,C_BG);
  centered(hA,48,226,1,C_LGRAY);
  tft.fillRect(80,220,80,10,C_BG);
  centered("MIX",CX,226,1,C_GOLD);
  tft.fillRect(172,220,40,10,C_BG);
  centered(hB,192,226,1,C_LGRAY);

  centered("click=swap  hold=menu",
           CX,237,1,C_DGRAY);
}

// ═══════════════════════════════════════════
// MENU LED AMBIENT
// ═══════════════════════════════════════════
unsigned long lastMenuLed  = 0;
float         menuPulse    = 0;

void updateMenuLEDs() {
  if(millis()-lastMenuLed < 30) return;
  lastMenuLed = millis();
  menuPulse  += 0.07f;
  if(menuPulse > TWO_PI) menuPulse -= TWO_PI;

  float bright = (sin(menuPulse)+1.0f)
                 /2.0f*0.55f + 0.1f;

  for(int i=0;i<LED_COUNT;i++) {
    float hue = (float)i/LED_COUNT*360.0f
                + menuPulse*28.0f;
    if(hue>=360) hue-=360;
    uint8_t r,g,b;
    hsv2rgb(hue,1,bright,r,g,b);
    setLedTarget(i,r,g,b);
  }
}

// ═══════════════════════════════════════════
// SETUP
// ═══════════════════════════════════════════
void setup() {
  Serial.begin(115200);

  pinMode(ENC_CLK, INPUT_PULLUP);
  pinMode(ENC_DT,  INPUT_PULLUP);
  pinMode(ENC_SW,  INPUT_PULLUP);

  attachInterrupt(
    digitalPinToInterrupt(ENC_CLK),
    encISR, CHANGE);

  tft.begin();
  tft.setRotation(0);
  tft.fillScreen(C_BG);

  strip.begin();
  strip.setBrightness(110);
  strip.clear();
  strip.show();
  memset(ledCur,0,sizeof(ledCur));
  memset(ledTgt,0,sizeof(ledTgt));

  drawBoot();

  // Wait for click or 3s timeout
  unsigned long bootMs = millis();
  while(millis()-bootMs < 3000) {
    pollBtn();
    tickLEDs();
    updateMenuLEDs();
    if(gotShort||gotLong) break;
    yield();
  }

  computeColors(false);
  state      = MAIN_MENU;
  needRedraw = true;
}

// ═══════════════════════════════════════════
// LOOP
// ═══════════════════════════════════════════
void loop() {
  yield();

  pollBtn();
  int delta = encDelta();

  switch(state) {

    case MAIN_MENU:
      updateMenuLEDs();
      if(delta) {
        menuSel=(menuSel+delta+2)%2;
        needRedraw=true;
      }
      if(gotShort) {
        activeSlot=0;
        if(menuSel==0) {
          computeColors(false);
          state=MIXER_PRESET;
        } else {
          computeColors(true);
          state=MIXER_WHEEL;
        }
        needRedraw=true;
      }
      break;

    case MIXER_PRESET:
      if(gotLong) {
        state=MAIN_MENU;
        needRedraw=true;
      }
      else if(gotShort) {
        activeSlot=1-activeSlot;
        // FIX 13: Visual slot-switch flash
        slotFlash   = true;
        slotFlashMs = millis();
        needRedraw  = true;
      }
      else if(delta) {
        pSel[activeSlot]=
          (pSel[activeSlot]+delta+PAL_COUNT)
          %PAL_COUNT;
        computeColors(false);
        needRedraw=true;
      }
      break;

    case MIXER_WHEEL:
      if(gotLong) {
        state=MAIN_MENU;
        needRedraw=true;
      }
      else if(gotShort) {
        activeSlot=1-activeSlot;
        slotFlash   = true;
        slotFlashMs = millis();
        needRedraw  = true;
      }
      else if(delta) {
        wHue[activeSlot]+=delta*4.0f;
        if(wHue[activeSlot]<0)
          wHue[activeSlot]+=360;
        if(wHue[activeSlot]>=360)
          wHue[activeSlot]-=360;
        computeColors(true);
        needRedraw=true;
      }
      break;

    default: break;
  }

  // Clear slot flash after duration
  if(slotFlash &&
     millis()-slotFlashMs > FLASH_DUR) {
    slotFlash=false;
  }

  if(needRedraw) {
    needRedraw=false;
    switch(state) {
      case MAIN_MENU:
        drawMenu(); break;
      case MIXER_PRESET:
        drawMixerPreset(); break;
      case MIXER_WHEEL:
        drawMixerWheel(); break;
      default: break;
    }
  }

  tickLEDs();
}

Make sure to check if all the libraries are present in your Arduino IDE.

In the board type select wemos d1 mini and select proper port number before uploading this program.

After you upload the program you can head over to building the circuit connections.

Circuit diagram for mini color mixer project

This is the circuit that we will be using in this project, the circuit is simple i will explain step by step.

GC9A01 or LCD display connections

CS pin from the LCD goes to D8 on wemos board

DC to D2 pin and RST to D1 on board

SDA to D7 pin and SCK to D5

The power pins Vcc and Gnd to 3.3v and the ground on the wemos board

Circuit diagram for mini color mixer project

WS2812B Strip or the ARGB strip connections

Vcc and the ground pins from strip is connected to 3v and the ground pins on the controller board

DIN or the signal pin is connected to D6 pin of wemos board

Rotary Encoder connections

This has 5 pins and all are used here in this project

Clk is connected to D3, DT From Encoder goes to D4

SW pin is connected to the TX pin on the wemos board

Power pins to the respective pins on board

Working Circuit for pocket color mixer

After you build the connection you can test the circuit for its working.

small color mixer circuit testing

You can simply connect the wemos board to usb supply via the USB cable and you will see the LCD screen message.

Use the encoder to navigate and select from the menu.

You can choose the default or the preset colors or the custom color option.

Use the desired option and confirm if everything is working fine and now we can case all the electronics in a 3d printed case.

Finishing the Pocket color mixer project

Enclose all the electronics inside the 3d printed case.

Start by gluing the wemos board to base of the 3d printed part and then work with LCD display.

Then the LED strip and then add the lid by connecting the rotary encoder.

Finish the project by adding the extra LCD rings and the diffuser for LED strip

Use superglue wherever necessary in small quantities.

This was all about building this pocket color moxer project, dont miss to check the video tutorial of the build and the working.

Jeevan

ADMIN

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button