YUK_KND
Published © GPL3+

FlowTime

FlowTIME: Tilt to make light flow, stop to see the time. Fluid animations, hourglass, and maze—time you can play with.

BeginnerFull instructions provided3 days11
FlowTime

Things used in this project

Hardware components

ATOM TailBat
M5Stack ATOM TailBat
×1
M5Stack AtomS3
×1
NeoPixel WS2812B Full-Color LED Display 16×16
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

top_cover

topcover_case

Sketchfab still processing.

buttom_case

buttom_case

Sketchfab still processing.

top_screen

topp_screen

Sketchfab still processing.

Schematics

connection M5AtomS3 and WS2812B

Example connection between M5AtomS3 (with ATOM TailBAT) and WS2812B 16×16 LED matrix

Code

atomled_kansei

Arduino
This Arduino code controls a 16×16 WS2812B LED matrix with M5Atom S3 and an accelerometer.
Modes include:
1.Clock (mirrored for reflection)
2.Flow (liquid-like motion when tilted)
3.Hourglass (90 seconds, fully packed sand)
4.Maze (goal changes color upon completion)
Button and motion control allow switching between modes.
// -----------------------------------------------------------------------------
//  M5Atom S3 + WS2812 1616  v12.8-hourglass90 + shake-gate
//  Modes: CLOCK / FLOW / HOURGLASS(90s, fully packed & symmetric) / MAZE
//  Btn: Single=next / Double=IMU invert save / Triple=Clock mirror save / Long(2s)=OFF
//  Wi-Fi + NTP(JST)  SSID: khome / PASS: Kiwamot0
// -----------------------------------------------------------------------------
#include <M5Unified.h>
#include <Adafruit_NeoPixel.h>
#include <Preferences.h>
#include <WiFi.h>
#include <esp_random.h>
#include <time.h>
#include <cmath>
#include <cstring>

// ===== Board / LED ===========================================================
constexpr uint8_t DIN_PIN = 2;    // AtomS3: G2  WS2812 DIN
constexpr uint8_t PWR_PIN = 16;   // FET
constexpr int W = 16, H = 16;
constexpr int NLED = W * H;
Adafruit_NeoPixel strip(NLED, DIN_PIN, NEO_GRB + NEO_KHZ800);

// 
inline uint16_t idx(int x,int y){ return (y & 1) ? (y*W + (W-1-x)) : (y*W + x); }

// ===== Wi-Fi / NTP ===========================================================
const char* WIFI_SSID = "khome";
const char* WIFI_PASS = "Kiwamot0";
constexpr uint32_t WIFI_TIMEOUT_MS = 10000;

// ===== Modes / Buttons =======================================================
enum Mode : uint8_t { MODE_CLOCK, MODE_FLOW, MODE_HOURGLASS, MODE_MAZE };
Mode mode = MODE_CLOCK;
bool  powerOn = true;
constexpr uint16_t LONG_PRESS_MS = 2000;
uint32_t btnHoldStart = 0;
uint32_t lastActive_ms = 0;                   // FLOW
constexpr uint32_t FLOW_IDLE_TO_CLOCK_MS = 30000; //30

static uint32_t lastClick=0;
static uint8_t  clickCnt=0;
constexpr uint16_t MULTICLICK_WINDOW_MS = 350;

// ===== Preferences ===========================================================
Preferences prefs;
bool invertX=false, invertY=false;   // IMU
bool clockMirrorX = true;            // 
constexpr const char* PREF_NS   = "lab_v128";
constexpr const char* KEY_INV   = "invMask";
constexpr const char* KEY_CMIR  = "clockMX";
void saveIMU(){ prefs.putUChar(KEY_INV, (invertX?1:0)|(invertY?2:0)); }
void saveClockMirror(){ prefs.putBool(KEY_CMIR, clockMirrorX); }
void loadPrefs(){
  prefs.begin(PREF_NS, false);
  uint8_t m = prefs.getUChar(KEY_INV, 0);
  invertX = (m & 1);
  invertY = (m & 2);
  clockMirrorX = prefs.getBool(KEY_CMIR, true);
}

// ===== Common Tuning =========================================================
constexpr uint8_t MAIN_BRIGHT = 48;

// ===== FLOWCLOCK  ========================
// 
constexpr float LP_ALPHA            = 0.12f;    // 0..1
constexpr float SHAKE_JERK_THR      = 0.35f;    // 
constexpr uint16_t SHAKE_DEBOUNCE_MS= 250;      // 
constexpr float STILL_JERK_THR      = 0.05f;    // 
constexpr uint32_t STILL_HOLD_MS    = 10000;    // CLOCK10s

static float axLP=0, ayLP=0, azLP=0;            // LP
static uint32_t lastShakeMs = 0;
static uint32_t stillSinceMs = 0;

// ===== FLOW ====================================================
constexpr float  G_GAIN       = 36000.0f;
constexpr float  DEADZONE_G   = 0.03f;
constexpr float  CURVE_GAMMA  = 1.4f;
constexpr float  FRICTION     = 0.995f;
constexpr float  BOUNCE       = 0.75f;
constexpr int32_t VEL_CLAMP   = 16*256*6;
constexpr bool   GRAVITY_INVERT_Y = true;

constexpr uint8_t TAIL_KEEP   = 216;
constexpr float   KERNEL_SHARP= 1.6f;
constexpr uint8_t SPARK_GAIN  = 60;

constexpr int     SEP_DIST    = 180;
constexpr float   SEP_PUSH    = 0.5f;

constexpr int BALLS = 35;
struct Ball{ int32_t px,py,vx,vy; uint32_t col; };
Ball balls[BALLS];
static uint32_t accR[NLED], accG[NLED], accB[NLED];

inline uint8_t fade8(uint8_t v, uint8_t keep){ return (uint16_t(v)*keep)>>8; }

uint32_t hsv(uint8_t h, uint8_t s, uint8_t v){
  uint8_t r,g,b; uint8_t region=h/43, rem=(h-region*43)*6;
  uint8_t p=(v*(255-s))>>8, q=(v*(255-((s*rem)>>8)))>>8, t=(v*(255-((s*(255-rem))>>8)))>>8;
  switch(region){
    case 0: r=v; g=t; b=p; break; case 1: r=q; g=v; b=p; break;
    case 2: r=p; g=v; b=t; break; case 3: r=p; g=q; b=v; break;
    case 4: r=t; g=p; b=v; break; default:r=v; g=p; b=q; break;
  }
  return (uint32_t)r<<16 | (uint32_t)g<<8 | b;
}
void initBalls(){
  for(int i=0;i<BALLS;++i){
    balls[i].px = (W/2)*256;  balls[i].py = (H/2)*256;
    balls[i].vx = (int32_t)((int32_t)(esp_random()%401) - 200);
    balls[i].vy = (int32_t)((int32_t)(esp_random()%401) - 200);
    uint8_t hue = (uint8_t)((i * 255) / BALLS);
    balls[i].col = hsv(hue, 255, 255);
  }
}
static uint8_t  lutPow[256], lutPowInv[256];
void buildLUT(){
  for(int i=0;i<256;++i){
    float x = i/255.0f;
    float a = powf(x, KERNEL_SHARP);
    float b = powf(1.0f - x, KERNEL_SHARP);
    lutPow[i]    = (uint8_t)(a*255.0f + 0.5f);
    lutPowInv[i] = (uint8_t)(b*255.0f + 0.5f);
  }
}
inline void accAdd(uint16_t p, uint32_t col, uint16_t w){
  uint8_t cr = col >> 16, cg = (col >> 8) & 0xFF, cb = col & 0xFF;
  accR[p] += (uint32_t)cr * w;
  accG[p] += (uint32_t)cg * w;
  accB[p] += (uint32_t)cb * w;
}
inline void drawSubpixelSharpLUT(int32_t px, int32_t py, uint32_t col){
  int x0 = px >> 8, y0 = py >> 8;
  uint8_t fx = px & 0xFF, fy = py & 0xFF;
  uint16_t u0 = lutPowInv[fx], u1 = lutPow[fx];
  uint16_t v0 = lutPowInv[fy], v1 = lutPow[fy];
  uint16_t w00 = (u0*v0)>>8, w10 = (u1*v0)>>8, w01 = (u0*v1)>>8, w11 = (u1*v1)>>8;

  if((unsigned)x0 < W && (unsigned)y0 < H)         accAdd(idx(x0,   y0  ), col, w00);
  if((unsigned)(x0+1) < W && (unsigned)y0 < H)     accAdd(idx(x0+1, y0  ), col, w10);
  if((unsigned)x0 < W && (unsigned)(y0+1) < H)     accAdd(idx(x0,   y0+1), col, w01);
  if((unsigned)(x0+1) < W && (unsigned)(y0+1) < H) accAdd(idx(x0+1, y0+1), col, w11);

  if((unsigned)x0 < W && (unsigned)y0 < H)         accAdd(idx(x0,   y0  ), col, SPARK_GAIN);
}
uint32_t tPrev_us = 0;

// ===== Clock (3x5) ===========================================================
const uint8_t FONT[10][5] = {
  {0b111,0b101,0b101,0b101,0b111}, {0b010,0b110,0b010,0b010,0b111},
  {0b111,0b001,0b111,0b100,0b111}, {0b111,0b001,0b111,0b001,0b111},
  {0b101,0b101,0b111,0b001,0b001}, {0b111,0b100,0b111,0b001,0b111},
  {0b111,0b100,0b111,0b101,0b111}, {0b111,0b001,0b001,0b001,0b001},
  {0b111,0b101,0b111,0b101,0b111}, {0b111,0b101,0b111,0b001,0b111}
};
inline uint16_t clockIndex(int x, int y){
  int xx = clockMirrorX ? (W-1-x) : x; // 
  return idx(xx, y);
}
inline void clockPix(int x, int y, uint32_t c){
  if ((unsigned)x < W && (unsigned)y < H) strip.setPixelColor(clockIndex(x,y), c);
}
void drawDigit3x5_clock(int d,int ox,int oy,uint32_t col){
  for(int y=0;y<5;++y) for(int x=0;x<3;++x)
    if(FONT[d][y] & (1<<(2-x))) clockPix(ox+x, oy+y, col);
}
void renderClock(bool colonOn){
  strip.clear();
  strip.setBrightness(MAIN_BRIGHT);
  time_t now=time(nullptr); struct tm lt; localtime_r(&now,&lt);
  int hh=lt.tm_hour, mm=lt.tm_min;

  int x=0, y=5; // 354 + 1 = 15
  uint32_t cH = strip.Color(0,128,255);
  uint32_t cM = strip.Color(0,255,96);

  drawDigit3x5_clock(hh/10, x,   y, cH); x+=4;
  drawDigit3x5_clock(hh%10, x,   y, cH); x+=4;
  if(colonOn){ clockPix(x, y+1, cH); clockPix(x, y+3, cH); }
  x+=1;
  drawDigit3x5_clock(mm/10, x,   y, cM); x+=4;
  drawDigit3x5_clock(mm%10, x,   y, cM);

  strip.show();
}

// ===== Gravity helper ========================================================
void xyGravityCurve(float ax, float ay, float &gx, float &gy){
  if(invertX) ax = -ax;
  if(invertY) ay = -ay;
  float mag = sqrtf(ax*ax + ay*ay);
  if(mag < 1e-6f){ gx=gy=0; return; }
  float norm = (mag - DEADZONE_G)/(1.0f - DEADZONE_G);
  if(norm < 0){ gx=gy=0; return; }
  norm = powf(norm, CURVE_GAMMA);
  gx = (ax/mag) * norm;
  gy = (ay/mag) * norm;
  if(GRAVITY_INVERT_Y) gy = -gy; // 
}

// ===== FLOW step =============================================================
void physicsStepFLOW(float gx, float gy, float dt){
  memset(accR, 0, sizeof(accR));
  memset(accG, 0, sizeof(accG));
  memset(accB, 0, sizeof(accB));

  int32_t minU=0, maxU=(W-1)*256;
  for(int i=0;i<BALLS;++i){
    balls[i].vx += (int32_t)(gx*G_GAIN*dt);
    balls[i].vy += (int32_t)(gy*G_GAIN*dt);
    balls[i].vx = (int32_t)(balls[i].vx * FRICTION);
    balls[i].vy = (int32_t)(balls[i].vy * FRICTION);
    if(balls[i].vx >  VEL_CLAMP) balls[i].vx =  VEL_CLAMP;
    if(balls[i].vx < -VEL_CLAMP) balls[i].vx = -VEL_CLAMP;
    if(balls[i].vy >  VEL_CLAMP) balls[i].vy =  VEL_CLAMP;
    if(balls[i].vy < -VEL_CLAMP) balls[i].vy = -VEL_CLAMP;
    balls[i].px += (int32_t)(balls[i].vx * dt);
    balls[i].py += (int32_t)(balls[i].vy * dt);

    bool hit=false;
    if(balls[i].px < minU){ balls[i].px=minU; balls[i].vx = -(int32_t)(balls[i].vx*BOUNCE); hit=true; }
    if(balls[i].px > maxU){ balls[i].px=maxU; balls[i].vx = -(int32_t)(balls[i].vx*BOUNCE); hit=true; }
    if(balls[i].py < minU){ balls[i].py=minU; balls[i].vy = -(int32_t)(balls[i].vy*BOUNCE); hit=true; }
    if(balls[i].py > maxU){ balls[i].py=maxU; balls[i].vy = -(int32_t)(balls[i].vy*BOUNCE); hit=true; }
    if(hit){ balls[i].vx = (int32_t)(balls[i].vx * 0.90f); balls[i].vy = (int32_t)(balls[i].vy * 0.90f); }

    drawSubpixelSharpLUT(balls[i].px, balls[i].py, balls[i].col);
  }

  // 
  for(int i=0;i<BALLS;++i){
    for(int j=i+1;j<BALLS;++j){
      int32_t dx = balls[j].px - balls[i].px;
      int32_t dy = balls[j].py - balls[i].py;
      if(abs(dx) < SEP_DIST && abs(dy) < SEP_DIST){
        int64_t d2 = (int64_t)dx*dx + (int64_t)dy*dy;
        int32_t thresh2 = (int32_t)SEP_DIST * (int32_t)SEP_DIST;
        if(d2 == 0){
          int32_t jx = (int32_t)((int32_t)(esp_random()%101) - 50);
          int32_t jy = (int32_t)((int32_t)(esp_random()%101) - 50);
          balls[i].px -= jx; balls[i].py -= jy;
          balls[j].px += jx; balls[j].py += jy;
        } else if(d2 < thresh2){
          float d = sqrtf((float)d2);
          float nx = dx / d, ny = dy / d;
          float corr = (SEP_DIST - d) * SEP_PUSH;
          int32_t cx = (int32_t)(nx * corr);
          int32_t cy = (int32_t)(ny * corr);
          balls[i].px -= cx; balls[i].py -= cy;
          balls[j].px += cx; balls[j].py += cy;
        }
      }
    }
  }

  // HDR max
  for(int p=0; p<NLED; ++p){
    uint32_t prev = strip.getPixelColor(p);
    uint8_t rTail = fade8((prev>>16)&0xFF, TAIL_KEEP);
    uint8_t gTail = fade8((prev>> 8)&0xFF, TAIL_KEEP);
    uint8_t bTail = fade8((prev    )&0xFF, TAIL_KEEP);

    uint16_t rAcc = accR[p] >> 8; if(rAcc>255) rAcc=255;
    uint16_t gAcc = accG[p] >> 8; if(gAcc>255) gAcc=255;
    uint16_t bAcc = accB[p] >> 8; if(bAcc>255) bAcc=255;

    uint8_t r = (rTail > (uint8_t)rAcc) ? rTail : (uint8_t)rAcc;
    uint8_t g = (gTail > (uint8_t)gAcc) ? gTail : (uint8_t)gAcc;
    uint8_t b = (bTail > (uint8_t)bAcc) ? bTail : (uint8_t)bAcc;

    strip.setPixelColor(p, ((uint32_t)r<<16)|((uint32_t)g<<8)|b);
  }
  strip.show();
}

// ===== HOURGLASS (90s, fully packed & symmetric) ============================
static bool hgInside[H][W];   // 
static bool hgSand[H][W];     // 

constexpr float HG_DURATION_SEC = 90.0f;   // 90
constexpr float HG_MIN_TILT     = 0.30f;   // |gy|
static float   hgRateTPS = 0.0f;           // /
static float   hgTokens  = 0.0f;           // 
static uint32_t hgLastMs = 0;

uint32_t hgSandColor = 0xFFD070;
uint32_t hgWallColor = 0x203050;
uint8_t  hgBGFade    = 190;

// (0..7)(15..8)  
void buildHourglassMask(){
  memset(hgInside, 0, sizeof(hgInside));
  const float midX = (W-1)*0.5f;   // 7.5
  const float neckHalf = 1.0f;     // =1  x=7,8
  const float maxHalf  = 7.0f;     // 
  const float curve    = 1.35f;    // 

  auto buildRow=[&](int y)->void{
    float t = (7 - y) / 7.0f; // y=0t=1, y=7t=0
    float half = neckHalf + (maxHalf - neckHalf) * powf(t, curve);

    // +0.5
    int xL = (int)ceilf (midX - half);
    int xR = (int)floorf(midX + half);

    if(xL<0) xL=0; if(xR>=W) xR=W-1;
    for(int x=xL;x<=xR;++x) hgInside[y][x]=true;
  };

  for(int y=0;y<=7;++y) buildRow(y);
  for(int y=0;y<=7;++y){
    int ym = H-1-y; // 15..8
    for(int x=0;x<W;++x) hgInside[ym][x] = hgInside[y][x];
  }

  // 22y=7/8, x=7/8
  for(int y=0;y<H;y++){
    if(y==7 || y==8){
      for(int x=0;x<W;x++) hgInside[y][x]=false;
      hgInside[y][7]=hgInside[y][8]=true;
    }
  }
}

inline bool isUpperRow(int y, int gySign){ return gySign>=0 ? (y<=7) : (y>=8); }

void fillUpperBulb(int gySign){
  memset(hgSand, 0, sizeof(hgSand));
  for(int y=0;y<H;y++){
    if(!isUpperRow(y,gySign)) continue;
    for(int x=0;x<W;x++){
      if(hgInside[y][x]) hgSand[y][x]=true;
    }
  }
}

void hgRecalcRate(int gySign){
  int upper=0;
  for(int y=0;y<H;y++){
    if(!isUpperRow(y,gySign)) continue;
    for(int x=0;x<W;x++) if(hgInside[y][x] && hgSand[y][x]) upper++;
  }
  hgRateTPS = (upper>0) ? ((float)upper / HG_DURATION_SEC) : 0.0f;
  hgTokens  = 0.0f;
  hgLastMs  = millis();
}

inline bool isThroatDest(int x,int y){ return ( (x==7||x==8) && (y==7||y==8) ); }
inline bool isCrossingThroat(int sy,int sx,int ny,int nx){
  if(!isThroatDest(nx,ny)) return false;
  return ( (sy==7 && ny==8) || (sy==8 && ny==7) );
}
inline bool insideHG(int x,int y){ return (unsigned)x<W && (unsigned)y<H && hgInside[y][x]; }

static inline void hgDir(float gx, float gy,
                         int &dx0,int &dy0,int &dxa,int &dya,int &dxb,int &dyb,
                         int &sx,int &sy,int &ex,int &ey){
  if(fabsf(gy) >= fabsf(gx)){
    dy0 = (gy>=0)? 1:-1; dx0 = 0;
    dxa = (gx>=0)? +1:-1; dya = dy0;
    dxb = -dxa;           dyb = dy0;
    sy = (dy0>0)? H-1:0;  ey = (dy0>0)? -1:H; sx=0; ex=W;
  }else{
    dx0 = (gx>=0)? 1:-1; dy0 = 0;
    dya = (gy>=0)? +1:-1; dxa = dx0;
    dyb = -dya;           dxb = dx0;
    sx = (dx0>0)? W-1:0; ex = (dx0>0)? -1:W; sy=0; ey=H;
  }
}

void stepHourglass(float gx, float gy){
  // 
  uint32_t now = millis();
  if(hgLastMs==0) hgLastMs = now;
  float dt = (now - hgLastMs)*0.001f;
  hgLastMs = now;
  if(fabsf(gy) > HG_MIN_TILT) hgTokens += hgRateTPS * dt;

  // 
  static int lastSign = +1;
  int gySign = (gy>=0)? +1 : -1;
  if(gySign != lastSign && fabsf(gy) > 0.6f){
    fillUpperBulb(gySign);
    hgRecalcRate(gySign);
    lastSign = gySign;
  }

  int dx0,dy0,dxa,dya,dxb,dyb,sx,sy,ex,ey;
  hgDir(gx,gy,dx0,dy0,dxa,dya,dxb,dyb,sx,sy,ex,ey);

  auto tryMove = [&](int y,int x,int ny,int nx)->bool{
    if(!insideHG(nx,ny) || hgSand[ny][nx]) return false;
    if(isCrossingThroat(y,x,ny,nx)){
      if(hgTokens >= 1.0f){
        hgSand[y][x]=false; hgSand[ny][nx]=true; hgTokens -= 1.0f;
        return true;
      } else return false;
    }else{
      hgSand[y][x]=false; hgSand[ny][nx]=true; return true;
    }
  };

  if(dy0!=0){ // 
    for(int y=(dy0>0? H-1:0); (dy0>0? y>=0:y<H); y+=(dy0>0? -1:+1)){
      for(int x=0;x<W;++x){
        if(!hgSand[y][x]) continue;
        int ny=y+dy0, nx=x+dx0;
        if(tryMove(y,x,ny,nx)) continue;
        int ay=y+dya, ax=x+dxa;
        int by=y+dyb, bx=x+dxb;
        if(tryMove(y,x,ay,ax)) continue;
        (void)tryMove(y,x,by,bx);
      }
    }
  }else{      // 
    for(int x=(dx0>0? W-1:0); (dx0>0? x>=0:x<W); x+=(dx0>0? -1:+1)){
      for(int y=0;y<H;++y){
        if(!hgSand[y][x]) continue;
        int ny=y+dy0, nx=x+dx0;
        if(tryMove(y,x,ny,nx)) continue;
        int ay=y+dya, ax=x+dxa;
        int by=y+dyb, bx=x+dxb;
        if(tryMove(y,x,ay,ax)) continue;
        (void)tryMove(y,x,by,bx);
      }
    }
  }
}

void renderHourglass(){
  // 
  for(int p=0;p<NLED;++p){
    uint32_t c = strip.getPixelColor(p);
    uint8_t r = fade8((c>>16)&0xFF, hgBGFade);
    uint8_t g = fade8((c>> 8)&0xFF, hgBGFade);
    uint8_t b = fade8((c    )&0xFF, hgBGFade);
    strip.setPixelColor(p, ((uint32_t)r<<16)|((uint32_t)g<<8)|b);
  }
  // 
  for(int y=0;y<H;++y){
    for(int x=0;x<W;++x){
      if(!hgInside[y][x]){
        strip.setPixelColor(idx(x,y), hgWallColor);
      }
    }
  }
  // 
  for(int y=0;y<H;++y){
    for(int x=0;x<W;++x){
      if(hgSand[y][x]){
        strip.setPixelColor(idx(x,y), hgSandColor);
      }
    }
  }
  strip.show();
}

// /90s
bool hgFirstEnter = true;
void enterHourglass(float gy){
  int gySign = (gy>=0)? +1:-1;
  fillUpperBulb(gySign);
  hgRecalcRate(gySign);
  hgFirstEnter = false;
}

// ===== MAZE =========================================
constexpr int MZ_CW=7, MZ_CH=7;
static bool   mzVisited[MZ_CH][MZ_CW];
static uint8_t mzHWall[MZ_CH+1][MZ_CW];
static uint8_t mzVWall[MZ_CH][MZ_CW+1];
int mzPx=1, mzPy=1;
int mzGx=13, mzGy=13;
uint32_t mzWallColor = 0x203030;
uint32_t mzPathColor = 0x001010;
uint32_t mzBallColor = 0x80FF40;
uint32_t mzGoalBase  = 0xFF20FF; // 

void mzClear(){
  memset(mzVisited,0,sizeof(mzVisited));
  for(int y=0;y<=MZ_CH;y++) for(int x=0;x<MZ_CW;x++) mzHWall[y][x]=1;
  for(int y=0;y<MZ_CH;y++)  for(int x=0;x<=MZ_CW;x++) mzVWall[y][x]=1;
}
void mzDFS(int cx,int cy){
  mzVisited[cy][cx]=true;
  int dirs[4]={0,1,2,3};
  for(int i=0;i<4;i++){ int j=esp_random()%4; int t=dirs[i]; dirs[i]=dirs[j]; dirs[j]=t; }
  for(int k=0;k<4;k++){
    int d=dirs[k]; int nx=cx, ny=cy;
    if(d==0) ny=cy-1; else if(d==1) nx=cx+1; else if(d==2) ny=cy+1; else nx=cx-1;
    if(nx<0||ny<0||nx>=MZ_CW||ny>=MZ_CH) continue;
    if(!mzVisited[ny][nx]){
      if(d==0) mzHWall[cy][cx]=0;
      if(d==2) mzHWall[cy+1][cx]=0;
      if(d==1) mzVWall[cy][cx+1]=0;
      if(d==3) mzVWall[cy][cx]=0;
      mzDFS(nx,ny);
    }
  }
}
inline bool mzIsWallPixel(int X,int Y){
  if(X==0||Y==0||X==14||Y==14) return true;
  if((X%2==1)&&(Y%2==1)) return false;
  if(Y%2==0 && X%2==1){ int cx=(X-1)/2, ry=Y/2; return mzHWall[ry][cx]; }
  if(X%2==0 && Y%2==1){ int cy=(Y-1)/2, cx=X/2; return mzVWall[cy][cx]; }
  return true;
}
void initMaze(){
  mzClear(); mzDFS(0,0);
  mzPx=1; mzPy=1; mzGx=13; mzGy=13;
}
static inline uint32_t scaleColor(uint32_t c, float s){
  uint8_t r=(c>>16)&0xFF, g=(c>>8)&0xFF, b=c&0xFF;
  int R=(int)(r*s); if(R>255)R=255;
  int G=(int)(g*s); if(G>255)G=255;
  int B=(int)(b*s); if(B>255)B=255;
  return ((uint32_t)R<<16)|((uint32_t)G<<8)|B;
}
void stepMaze(float gx, float gy){
  static uint32_t lastStep = 0;
  if (millis() - lastStep < 60) return;
  lastStep = millis();

  int dx = (fabsf(gx) >= fabsf(gy)) ? ((gx > 0) ? +2 : (gx < 0 ? -2 : 0)) : 0;
  int dy = (fabsf(gy) >  fabsf(gx)) ? ((gy > 0) ? +2 : (gy < 0 ? -2 : 0)) : 0;

  auto canMove = [&](int x,int y,int dx,int dy)->bool{
    int mx = x + dx/2, my = y + dy/2;
    int tx = x + dx,   ty = y + dy;
    if (mx<0||my<0||mx>14||my>14||tx<0||ty<0||tx>14||ty>14) return false;
    if (mzIsWallPixel(mx,my)) return false;
    if (mzIsWallPixel(tx,ty)) return false;
    return true;
  };

  int nx=mzPx, ny=mzPy;
  if (dx!=0 && canMove(nx,ny,dx,0)) nx += dx;
  if (dy!=0 && canMove(nx,ny,0,dy)) ny += dy;
  mzPx = nx; mzPy = ny;

  if (mzPx==mzGx && mzPy==mzGy){
    for (int r=0; r<7; r++){
      for (int y=0; y<15; y++) for (int x=0; x<15; x++){
        uint32_t c = 0;
        if (!mzIsWallPixel(x,y))
          c = ((abs(x-mzGx)+abs(y-mzGy))<=r) ? 0xFFFFFF : 0x000000;
        strip.setPixelColor(idx(x,y), c);
      }
      strip.show(); delay(25);
    }
    initMaze();
  }
}
void renderMaze(){
  for(int y=0;y<15;y++){
    for(int x=0;x<15;x++){
      uint32_t c = mzIsWallPixel(x,y) ? mzWallColor : mzPathColor;
      strip.setPixelColor(idx(x,y), c);
    }
  }
  // 
  float pulse = 0.5f + 0.5f * sinf(millis()*0.010f);
  strip.setPixelColor(idx(mzGx,mzGy), scaleColor(mzGoalBase, 0.4f + 0.6f*pulse));
  // 
  strip.setPixelColor(idx(mzPx,mzPy), 0xFFFFFF);
  strip.show();
}

// ===== Power / Wi-Fi / Setup / Loop =========================================
void enterOff(){ strip.clear(); strip.show(); digitalWrite(PWR_PIN, LOW); powerOn=false; }
void leaveOff(){
  digitalWrite(PWR_PIN, HIGH);
  strip.setBrightness(MAIN_BRIGHT);
  initBalls();
  tPrev_us = micros();
  lastActive_ms = millis();
  mode = MODE_CLOCK;
  powerOn = true;
}

void setupWiFiAndTime(){
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  uint32_t start = millis();
  while (WiFi.status() != WL_CONNECTED && (millis() - start) < WIFI_TIMEOUT_MS){ delay(200); }
  configTime(9*3600, 0, "ntp.jst.mfeed.ad.jp", "ntp.nict.jp", "pool.ntp.org");
  struct tm tinfo; getLocalTime(&tinfo, 5000);
}

void setup(){
  pinMode(PWR_PIN, OUTPUT); digitalWrite(PWR_PIN, HIGH);
  M5.begin();
  strip.begin(); strip.setBrightness(MAIN_BRIGHT); strip.show();
  loadPrefs();
  setupWiFiAndTime();
  buildLUT();
  initBalls();
  buildHourglassMask();
  initMaze();
  tPrev_us = micros();
  lastActive_ms = millis();
  renderClock(true);  // 
}

void loop(){
  M5.update();

  if(!powerOn){
    if(M5.BtnA.wasPressed()){ leaveOff(); }
    delay(50);
    return;
  }

  // 
  if(M5.BtnA.wasPressed()){
    uint32_t now = millis();
    if(now - lastClick <= MULTICLICK_WINDOW_MS){ clickCnt++; }
    else { clickCnt=1; }
    lastClick = now;
  }
  // 
  if(clickCnt && (millis()-lastClick) > MULTICLICK_WINDOW_MS){
    if(clickCnt>=3){ clockMirrorX = !clockMirrorX; saveClockMirror(); if(mode==MODE_CLOCK) renderClock(true); }
    else if(clickCnt==2){ invertX=!invertX; invertY=!invertY; saveIMU(); }
    else {
      mode = (Mode)((mode + 1) % 4);
      if(mode==MODE_CLOCK) renderClock(true);
      if(mode==MODE_FLOW){ initBalls(); lastActive_ms=millis(); stillSinceMs=0; }
      if(mode==MODE_HOURGLASS){ hgFirstEnter=true; } // 
    }
    clickCnt=0;
  }

  //  2s  OFF
  if(M5.BtnA.isPressed()){
    if(btnHoldStart==0) btnHoldStart = millis();
    else if(millis()-btnHoldStart >= LONG_PRESS_MS){ enterOff(); btnHoldStart=0; return; }
  } else { btnHoldStart = 0; }

  // ==== IMU &  ==========================================
  float ax,ay,az; M5.Imu.getAccel(&ax,&ay,&az);

  // LP
  axLP = axLP + LP_ALPHA*(ax - axLP);
  ayLP = ayLP + LP_ALPHA*(ay - ayLP);
  azLP = azLP + LP_ALPHA*(az - azLP);

  // LPXY
  float jx = ax - axLP;
  float jy = ay - ayLP;
  float jerk = sqrtf(jx*jx + jy*jy);

  // 
  uint32_t nowMs = millis();
  if(jerk < STILL_JERK_THR){
    if(stillSinceMs==0) stillSinceMs = nowMs;
  }else{
    stillSinceMs = 0;
  }

  // FLOW
  float gx, gy; xyGravityCurve(ax, ay, gx, gy);

  // ==== CLOCK ================================================================
  if(mode==MODE_CLOCK){
    static uint32_t prevHalf = 0xFFFFFFFF;
    uint32_t half = millis()/500;
    if(half != prevHalf){ renderClock((half % 2)==0); prevHalf = half; }

    // FLOW
    if(jerk > SHAKE_JERK_THR && (nowMs - lastShakeMs) > SHAKE_DEBOUNCE_MS){
      lastShakeMs = nowMs;
      mode=MODE_FLOW;
      initBalls();
      lastActive_ms=nowMs;
      stillSinceMs=0;
    }
    delay(5);
    return;
  }

  // ==== FLOW ================================================================
  if(mode==MODE_FLOW){
    // 
    if(jerk > SHAKE_JERK_THR){ lastActive_ms = nowMs; lastShakeMs = nowMs; }

    // 
    if(stillSinceMs && (nowMs - stillSinceMs) > STILL_HOLD_MS){
      mode = MODE_CLOCK; renderClock(true); return;
    }

    // 
    if(nowMs - lastActive_ms > FLOW_IDLE_TO_CLOCK_MS){
      mode = MODE_CLOCK; renderClock(true); return;
    }

    uint32_t tNow = micros();
    float dt = (tNow - tPrev_us) * 1e-6f; tPrev_us = tNow;
    physicsStepFLOW(gx, gy, dt);
    delayMicroseconds(200);
    return;
  }

  // ==== HOURGLASS ===========================================================
  if(mode==MODE_HOURGLASS){
    if(hgFirstEnter){ enterHourglass(gy); }
    stepHourglass(gx, gy);
    renderHourglass();
    delay(18);
    return;
  }

  // ==== MAZE ================================================================
  if(mode==MODE_MAZE){
    stepMaze(gx, gy);
    renderMaze();
    delay(10);
    return;
  }
}

Credits

YUK_KND
1 project • 0 followers
NothingJPFontProject・Manus・LLM・Figma・AI・Xiaomi・SIGMA・OpenAI・Gemini・Python・TypeScript・Photography・3DPrinter・🎾・🐶🐶・🏕️・📸メガネ=⚪︎◻︎

Comments