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.

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

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.

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.