/* -*- mode: c++ -*- * Kaleidoscope-Heatmap -- Heatmap LED effect for Kaleidoscope. * Copyright (C) 2016, 2017 Keyboard.io, Inc. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ #include #include namespace kaleidoscope { namespace plugin { // store the number of times each key has been strock uint16_t Heatmap::heatmap_[ROWS][COLS]; // max of heatmap_ (we divide by it so we start at 1) uint16_t Heatmap::highest_ = 1; // next heatmap computation time uint32_t Heatmap::next_heatmap_comp_time_ = 0; // in the cRGB struct the order is blue, green, red (It should be called cBGR…) // default heat_colors black green yellow red static const cRGB heat_colors_default_[] PROGMEM = {{0, 0, 0}, {25, 255, 25}, {25, 255, 255}, {25, 25, 255}}; // colors from cold to hot const cRGB *Heatmap::heat_colors = heat_colors_default_; uint8_t Heatmap::heat_colors_length = 4; // number of millisecond to wait between each heatmap computation uint16_t Heatmap::update_delay = 1000; cRGB Heatmap::computeColor(float v) { // compute the color corresponding to a value between 0 and 1 /* * for exemple, if: * v=0.8 * heat_colors_lenth=4 (hcl) * the red components of heat_colors are: 0, 25, 25, 255 (rhc) * the red component returned by computeColor will be: 117 * * 255 | / * | / * | / * 117 | - - - - - - -/ * | / * 25 | ______/ | * | __/ * | _/ | * |/_________________ * 0 1 2 ↑ 3 * ↑ 2.4 ↑ * idx1 | idx2 * <—–> * fb * * in this exemple, I call red heat_colors: rhc * idx1 = floor(v×(hcl-1)) = floor(0.8×3) = floor(2.4) = 2 * idx2 = idx1 + 1 = 3 * fb = v×(hcl-1)-idx1 = 0.8×3 - 2 = 0.4 * red = (rhc[idx2]-rhc[idx1])×fb + rhc[idx1] = (255-25)×(2.4-2) + 25 = 117 */ float fb = 0; uint8_t idx1, idx2; if (v <= 0) { // if v = 0, don't bother computing fb and use heat_colors[0] idx1 = idx2 = 0; } else if (v >= 1) { // if v = 1, // don't bother computing fb and use heat_colors[heat_colors_length-1] idx1 = idx2 = heat_colors_length - 1; } else { float val = v * (heat_colors_length - 1); // static_cast from float to int just drop the decimal part of the number // static_cast(5.9) → 5 idx1 = static_cast(val); idx2 = idx1 + 1; fb = val - static_cast(idx1); } uint8_t r = static_cast((pgm_read_byte(&(heat_colors[idx2].r)) - pgm_read_byte(&(heat_colors[idx1].r))) * fb + pgm_read_byte(&(heat_colors[idx1].r))); uint8_t g = static_cast((pgm_read_byte(&(heat_colors[idx2].g)) - pgm_read_byte(&(heat_colors[idx1].g))) * fb + pgm_read_byte(&(heat_colors[idx1].g))); uint8_t b = static_cast((pgm_read_byte(&(heat_colors[idx2].b)) - pgm_read_byte(&(heat_colors[idx1].b))) * fb + pgm_read_byte(&(heat_colors[idx1].b))); return {b, g, r}; } void Heatmap::shiftStats(void) { // this method is called when: // 1. a value in heatmap_ reach INT8_MAX // 2. highest_ reach heat_colors_length*512 (see Heatmap::loopHook) // we divide every heatmap element by 2 for (uint8_t r = 0; r < ROWS; r++) { for (uint8_t c = 0; c < COLS; c++) { heatmap_[r][c] = heatmap_[r][c] >> 1; } } // and also divide highest_ accordingly highest_ = highest_ >> 1; } void Heatmap::resetMap(void) { // this method can be used as a way to work around an existing bug with a single key // getting special attention or if the user just wants a button to reset the map for (uint8_t r = 0; r < ROWS; r++) { for (uint8_t c = 0; c < COLS; c++) { heatmap_[r][c] = 0; } } highest_ = 1; } EventHandlerResult Heatmap::onKeyswitchEvent(Key &mapped_key, byte row, byte col, uint8_t key_state) { if (!Kaleidoscope.has_leds) return EventHandlerResult::OK; // this methode is called frequently by Kaleidoscope // even if the module isn't activated // if it is a synthetic key, skip it if (key_state & INJECTED) return EventHandlerResult::OK; // if the key is not toggled on, skip it if (!keyToggledOn(key_state)) return EventHandlerResult::OK; // increment the heatmap_ value related to the key heatmap_[row][col]++; // check highest_ if (highest_ < heatmap_[row][col]) { highest_ = heatmap_[row][col]; // if highest_ (and so heatmap_ value related to the key) // is close to overflow: call shiftStats // NOTE: this is barely impossible since shiftStats should be // called much sooner by Heatmap::loopHook if (highest_ == INT16_MAX) shiftStats(); } return EventHandlerResult::OK; } EventHandlerResult Heatmap::beforeEachCycle() { if (!Kaleidoscope.has_leds) return EventHandlerResult::OK; // this methode is called frequently by Kaleidoscope // even if the module isn't activated // call shiftStats (divide every heatmap value by 2) // if highest_ reach heat_colors_length*512. // So after the shift, highest_ will be heat_colors_length*256. We // didn't lose any precision in our heatmap since between each color we have a // maximum precision of 256 ; said differently, there is 256 state (at max) // between heat_colors[x] and heat_colors[x+1]. if (highest_ > (static_cast(heat_colors_length) << 9)) shiftStats(); return EventHandlerResult::OK; } void Heatmap::update(void) { if (!Kaleidoscope.has_leds) return; // this methode is called frequently by the LEDControl::loopHook // do nothing if we didn't reach next_heatmap_comp_time_ yet if (next_heatmap_comp_time_ && (millis() < next_heatmap_comp_time_)) return; // do the heatmap computing // (we reach next_heatmap_comp_time_ or next_heatmap_comp_time_ was never scheduled) // schedule the next heatmap computing next_heatmap_comp_time_ = millis() + update_delay; // for each key for (uint8_t r = 0; r < ROWS; r++) { for (uint8_t c = 0; c < COLS; c++) { // how much the key was pressed compared to the others (between 0 and 1) // (total_keys_ can't be equal to 0) float v = static_cast(heatmap_[r][c]) / highest_; // we could have used an interger instead of a float, but then we would // have had to change some multiplication in division. // / on uint is slower than * on float, so I stay with the float // https://forum.arduino.cc/index.php?topic=92684.msg2733723#msg2733723 // set the LED color accordingly ::LEDControl.setCrgbAt(r, c, computeColor(v)); } } } } } kaleidoscope::plugin::Heatmap HeatmapEffect;