You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Kaleidoscope/src/kaleidoscope/plugin/Heatmap.cpp

214 lines
7.2 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/* -*- 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 <http://www.gnu.org/licenses/>.
*/
#include <Kaleidoscope.h>
#include <Kaleidoscope-Heatmap.h>
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<int>(5.9) → 5
idx1 = static_cast<int>(val);
idx2 = idx1 + 1;
fb = val - static_cast<float>(idx1);
}
uint8_t r = static_cast<uint8_t>((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<uint8_t>((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<uint8_t>((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<uint16_t>(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<float>(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;