parent
303283624a
commit
2628c078fa
@ -0,0 +1,416 @@ |
||||
#include "SMXPanelAnimation.h" |
||||
#include "SMXManager.h" |
||||
#include "SMXDevice.h" |
||||
#include "SMXThread.h" |
||||
using namespace std; |
||||
using namespace SMX; |
||||
|
||||
namespace { |
||||
Mutex g_Lock; |
||||
} |
||||
|
||||
#define LIGHTS_PER_PANEL 16 |
||||
|
||||
// XXX: go to sleep if there are no pads connected
|
||||
|
||||
struct AnimationState |
||||
{ |
||||
SMXPanelAnimation animation; |
||||
|
||||
// Seconds into the animation:
|
||||
float fTime = 0; |
||||
|
||||
// The currently displayed frame:
|
||||
int iCurrentFrame = 0; |
||||
|
||||
bool bPlaying = false; |
||||
|
||||
double m_fLastUpdateTime = -1; |
||||
|
||||
// Return the current animation frame.
|
||||
const vector<SMXGif::Color> &GetAnimationFrame() const |
||||
{ |
||||
// If we're not playing, return an empty array. As a sanity check, do this
|
||||
// if the frame is out of bounds too.
|
||||
if(!bPlaying || iCurrentFrame >= animation.m_aPanelGraphics.size()) |
||||
{ |
||||
static vector<SMXGif::Color> dummy; |
||||
return dummy; |
||||
} |
||||
|
||||
return animation.m_aPanelGraphics[iCurrentFrame]; |
||||
} |
||||
|
||||
// Start the animation if it's not playing.
|
||||
void Play() |
||||
{ |
||||
bPlaying = true; |
||||
} |
||||
|
||||
// Stop and disable the animation.
|
||||
void Stop() |
||||
{ |
||||
bPlaying = false; |
||||
Rewind(); |
||||
} |
||||
|
||||
// Reset to the first frame.
|
||||
void Rewind() |
||||
{ |
||||
fTime = 0; |
||||
iCurrentFrame = 0; |
||||
} |
||||
|
||||
// Advance the animation by fSeconds.
|
||||
void Update() |
||||
{ |
||||
// fSeconds is the time since the last update:
|
||||
double fNow = SMX::GetMonotonicTime(); |
||||
double fSeconds = m_fLastUpdateTime == -1? 0: (fNow - m_fLastUpdateTime); |
||||
m_fLastUpdateTime = fNow; |
||||
|
||||
if(!bPlaying || animation.m_aPanelGraphics.empty()) |
||||
return; |
||||
|
||||
// If the current frame is past the end, a new animation was probably
|
||||
// loaded.
|
||||
if(iCurrentFrame >= animation.m_aPanelGraphics.size()) |
||||
Rewind(); |
||||
|
||||
// Advance time.
|
||||
fTime += fSeconds; |
||||
|
||||
// If we're still on this frame, we're done.
|
||||
float fFrameDuration = animation.m_iFrameDurations[iCurrentFrame]; |
||||
if(fTime - 0.00001f < fFrameDuration) |
||||
return; |
||||
|
||||
// If we've passed the end of the frame, move to the next frame. Don't
|
||||
// skip frames if we're updating too quickly.
|
||||
fTime -= fFrameDuration; |
||||
if(fTime > 0) |
||||
fTime = 0; |
||||
|
||||
// Advance the frame.
|
||||
iCurrentFrame++; |
||||
|
||||
// If we're at the end of the frame, rewind to the loop frame.
|
||||
if(iCurrentFrame == animation.m_aPanelGraphics.size()) |
||||
iCurrentFrame = animation.m_iLoopFrame; |
||||
} |
||||
}; |
||||
|
||||
struct AnimationStateForPad |
||||
{ |
||||
// asLightsData is an array of lights data to send to the pad and graphic
|
||||
// is an animation graphic. Overlay graphic on top of the lights.
|
||||
void OverlayLights(char *asLightsData, const vector<SMXGif::Color> &graphic) const |
||||
{ |
||||
// Stop if this graphic isn't loaded or is paused.
|
||||
if(graphic.empty()) |
||||
return; |
||||
|
||||
for(int i = 0; i < graphic.size(); ++i) |
||||
{ |
||||
if(i >= LIGHTS_PER_PANEL) |
||||
return; |
||||
|
||||
// If this color is transparent, leave the released animation alone.
|
||||
if(graphic[i].color[3] == 0) |
||||
continue; |
||||
|
||||
asLightsData[i*3+0] = graphic[i].color[0]; |
||||
asLightsData[i*3+1] = graphic[i].color[1]; |
||||
asLightsData[i*3+2] = graphic[i].color[2]; |
||||
} |
||||
} |
||||
|
||||
// Return the command to set the current animation state as pad lights.
|
||||
string GetLightsCommand(int iPadState, const SMXConfig &config) const |
||||
{ |
||||
g_Lock.AssertLockedByCurrentThread(); |
||||
|
||||
// If AutoLightingUsePressedAnimations is set, use lights animations.
|
||||
// If it's not (the config tool is set to step color), mimic the built-in
|
||||
// step color behavior instead of using pressed animations. Any released
|
||||
// animation will always be used.
|
||||
bool bUsePressedAnimations = config.flags & PlatformFlags_AutoLightingUsePressedAnimations; |
||||
|
||||
const int iBytesPerPanel = LIGHTS_PER_PANEL*3; |
||||
const int iTotalLights = 9*iBytesPerPanel; |
||||
string result(iTotalLights, 0); |
||||
|
||||
for(int panel = 0; panel < 9; ++panel) |
||||
{ |
||||
// The portion of lights data for this panel:
|
||||
char *out = &result[panel*iBytesPerPanel]; |
||||
|
||||
// Add the released animation, then overlay the pressed animation if we're pressed.
|
||||
OverlayLights(out, animations[panel][SMX_LightsType_Released].GetAnimationFrame()); |
||||
bool bPressed = bool(iPadState & (1 << panel)); |
||||
if(bPressed && bUsePressedAnimations) |
||||
OverlayLights(out, animations[panel][SMX_LightsType_Pressed].GetAnimationFrame()); |
||||
else if(bPressed && !bUsePressedAnimations) |
||||
{ |
||||
// Light all LEDs on this panel using stepColor.
|
||||
double LightsScaleFactor = 0.666666f; |
||||
const uint8_t *color = &config.stepColor[panel*3]; |
||||
|
||||
for(int light = 0; light < LIGHTS_PER_PANEL; ++light) |
||||
{ |
||||
for(int i = 0; i < 3; ++i) |
||||
{ |
||||
// stepColor is scaled to the 0-170 range. Scale it back to the 0-255 range.
|
||||
// User applications don't need to worry about this since they normally don't
|
||||
// need to care about stepColor.
|
||||
uint8_t c = color[i]; |
||||
c = (uint8_t) lrintf(min(255.0f, c / LightsScaleFactor)); |
||||
out[light*3+i] = c; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
// State for both animations on each panel:
|
||||
AnimationState animations[9][NUM_SMX_LightsType]; |
||||
}; |
||||
|
||||
namespace |
||||
{ |
||||
// Animations and animation states for both pads.
|
||||
AnimationStateForPad pad_states[2]; |
||||
} |
||||
|
||||
namespace { |
||||
// Given a 23x24 graphic frame and a panel number, return an array of 25 colors, containing
|
||||
// each light in the order it's sent to the master controller.
|
||||
void ConvertToPanelGraphic(const SMXGif::GIFImage &src, vector<SMXGif::Color> &dst, int panel) |
||||
{ |
||||
vector<pair<int,int>> graphic_positions = { |
||||
{ 0,0 }, |
||||
{ 1,0 }, |
||||
{ 2,0 }, |
||||
{ 0,1 }, |
||||
{ 1,1 }, |
||||
{ 2,1 }, |
||||
{ 0,2 }, |
||||
{ 1,2 }, |
||||
{ 2,2 }, |
||||
}; |
||||
|
||||
dst.clear(); |
||||
|
||||
// The top-left corner for this panel:
|
||||
int x = graphic_positions[panel].first * 8; |
||||
int y = graphic_positions[panel].second * 8; |
||||
|
||||
// Add the 4x4 grid first.
|
||||
for(int dy = 0; dy < 4; ++dy) |
||||
for(int dx = 0; dx < 4; ++dx) |
||||
dst.push_back(src.get(x+dx*2, y+dy*2)); |
||||
|
||||
// Add the 3x3 grid.
|
||||
for(int dy = 0; dy < 3; ++dy) |
||||
for(int dx = 0; dx < 3; ++dx) |
||||
dst.push_back(src.get(x+dx*2+1, y+dy*2+1)); |
||||
} |
||||
} |
||||
|
||||
// Return the SMXPanelAnimation. The rest of the animation state is internal.
|
||||
SMXPanelAnimation SMXPanelAnimation::GetLoadedAnimation(int pad, int panel, SMX_LightsType type) |
||||
{ |
||||
g_Lock.AssertNotLockedByCurrentThread(); |
||||
LockMutex L(g_Lock); |
||||
return pad_states[pad].animations[panel][type].animation; |
||||
} |
||||
|
||||
// Load an array of animation frames as a panel animation. Each frame must
|
||||
// be 14x15.
|
||||
void SMXPanelAnimation::Load(const vector<SMXGif::SMXGifFrame> &frames, int panel) |
||||
{ |
||||
m_aPanelGraphics.clear(); |
||||
m_iFrameDurations.clear(); |
||||
m_iLoopFrame = -1; |
||||
|
||||
for(int frame_no = 0; frame_no < frames.size(); ++frame_no) |
||||
{ |
||||
const SMXGif::SMXGifFrame &gif_frame = frames[frame_no]; |
||||
|
||||
// If the bottom-left pixel is opaque, this is the loop frame, which marks the
|
||||
// frame the animation should start at after a loop. This is global to the
|
||||
// animation, not specific to each panel.
|
||||
if(gif_frame.frame.get(0, gif_frame.frame.height-1).color[3] != 0) |
||||
{ |
||||
// We shouldn't see more than one of these. If we do, use the first.
|
||||
if(m_iLoopFrame != -1) |
||||
m_iLoopFrame = frame_no; |
||||
} |
||||
|
||||
// Extract this frame.
|
||||
vector<SMXGif::Color> panel_graphic; |
||||
ConvertToPanelGraphic(gif_frame.frame, panel_graphic, panel); |
||||
m_aPanelGraphics.push_back(panel_graphic); |
||||
|
||||
// GIFs have a very low-resolution duration field, with 10ms units.
|
||||
// The panels run at 30 FPS internally, or 33 1/3 ms, but GIF can only
|
||||
// represent 30ms or 40ms. Most applications will probably output 30,
|
||||
// but snap both 30ms and 40ms to exactly 30 FPS to make sure animations
|
||||
// that are meant to run at native framerate do.
|
||||
float seconds; |
||||
if(gif_frame.milliseconds == 30 || gif_frame.milliseconds == 40) |
||||
seconds = 1 / 30.0f; |
||||
else |
||||
seconds = gif_frame.milliseconds / 1000.0; |
||||
|
||||
m_iFrameDurations.push_back(seconds); |
||||
} |
||||
|
||||
// By default, loop back to the first frame.
|
||||
if(m_iLoopFrame == -1) |
||||
m_iLoopFrame = 0; |
||||
} |
||||
|
||||
// Load a GIF into SMXLoadedPanelAnimations::animations.
|
||||
bool SMX_LightsAnimation_Load(const char *gif, int size, int pad, SMX_LightsType type, const char **error) |
||||
{ |
||||
// Parse the GIF.
|
||||
string buf(gif, size); |
||||
vector<SMXGif::SMXGifFrame> frames; |
||||
if(!SMXGif::DecodeGIF(buf, frames) || frames.empty()) |
||||
{ |
||||
*error = "The GIF couldn't be read."; |
||||
return false; |
||||
} |
||||
|
||||
// Check the dimensions of the image. We only need to check the first, the
|
||||
// others will always have the same size.
|
||||
if(frames[0].width != 14 || frames[0].height != 15) |
||||
{ |
||||
*error = "The GIF must be 14x15."; |
||||
return false; |
||||
} |
||||
|
||||
// Lock while we access pad_states.
|
||||
g_Lock.AssertNotLockedByCurrentThread(); |
||||
LockMutex L(g_Lock); |
||||
|
||||
// Load the animation for each panel into SMXPanelAnimations.
|
||||
for(int panel = 0; panel < 9; ++panel) |
||||
{ |
||||
SMXPanelAnimation &animation = pad_states[pad].animations[panel][type].animation; |
||||
animation.Load(frames, panel); |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
// A thread to handle setting light animations. We do this in a separate
|
||||
// thread rather than in the SMXManager thread so this can be treated as
|
||||
// if it's external application thread, and it's making normal threaded
|
||||
// calls to SetLights.
|
||||
class PanelAnimationThread: public SMXThread |
||||
{ |
||||
public: |
||||
static shared_ptr<PanelAnimationThread> g_pSingleton; |
||||
PanelAnimationThread(): |
||||
SMXThread(g_Lock) |
||||
{ |
||||
Start("SMX light animations"); |
||||
} |
||||
|
||||
private: |
||||
void ThreadMain() |
||||
{ |
||||
m_Lock.Lock(); |
||||
|
||||
// Update lights at 30 FPS.
|
||||
const int iDelayMS = 33; |
||||
|
||||
while(!m_bShutdown) |
||||
{ |
||||
// Run a single panel lights update.
|
||||
UpdateLights(); |
||||
|
||||
// Wait up to 30 FPS, or until we're signalled. We can only be signalled
|
||||
// if we're shutting down, so we don't need to worry about partial frame
|
||||
// delays.
|
||||
m_Event.Wait(iDelayMS); |
||||
} |
||||
|
||||
m_Lock.Unlock(); |
||||
} |
||||
|
||||
// Return lights for the given pad and pad state, using the loaded panel animations.
|
||||
void GetCurrentLights(string &asLightsDataOut, int pad, int iPadState) |
||||
{ |
||||
m_Lock.AssertLockedByCurrentThread(); |
||||
|
||||
// Get this pad's configuration.
|
||||
SMXConfig config; |
||||
if(!SMXManager::g_pSMX->GetDevice(pad)->GetConfig(config)) |
||||
return; |
||||
|
||||
AnimationStateForPad &pad_state = pad_states[pad]; |
||||
|
||||
// Make sure the correct animations are playing.
|
||||
for(int panel = 0; panel < 9; ++panel) |
||||
{ |
||||
// The released animation is always playing.
|
||||
pad_state.animations[panel][SMX_LightsType_Released].Play(); |
||||
|
||||
// The pressed animation only plays while the button is pressed,
|
||||
// and rewind when it's released.
|
||||
bool bPressed = iPadState & (1 << panel); |
||||
if(bPressed) |
||||
pad_state.animations[panel][SMX_LightsType_Pressed].Play(); |
||||
else |
||||
pad_state.animations[panel][SMX_LightsType_Pressed].Stop(); |
||||
} |
||||
|
||||
// Set the current state.
|
||||
asLightsDataOut = pad_state.GetLightsCommand(iPadState, config); |
||||
|
||||
// Advance animations.
|
||||
for(int panel = 0; panel < 9; ++panel) |
||||
{ |
||||
for(auto &animation_state: pad_state.animations[panel]) |
||||
animation_state.Update(); |
||||
} |
||||
} |
||||
|
||||
// Run a single light animation update.
|
||||
void UpdateLights() |
||||
{ |
||||
string asLightsData[2]; |
||||
for(int pad = 0; pad < 2; pad++) |
||||
{ |
||||
int iPadState = SMXManager::g_pSMX->GetDevice(pad)->GetInputState(); |
||||
GetCurrentLights(asLightsData[pad], pad, iPadState); |
||||
} |
||||
|
||||
// Update lights.
|
||||
SMXManager::g_pSMX->SetLights(asLightsData); |
||||
} |
||||
}; |
||||
|
||||
void SMX_LightsAnimation_SetAuto(bool enable) |
||||
{ |
||||
if(!enable) |
||||
{ |
||||
// If we're turning off, shut down the thread if it's running.
|
||||
if(PanelAnimationThread::g_pSingleton) |
||||
PanelAnimationThread::g_pSingleton->Shutdown(); |
||||
PanelAnimationThread::g_pSingleton.reset(); |
||||
return; |
||||
} |
||||
|
||||
// Create the animation thread if it's not already running.
|
||||
if(PanelAnimationThread::g_pSingleton) |
||||
return; |
||||
PanelAnimationThread::g_pSingleton.reset(new PanelAnimationThread()); |
||||
} |
||||
|
||||
shared_ptr<PanelAnimationThread> PanelAnimationThread::g_pSingleton; |
@ -0,0 +1,50 @@ |
||||
#ifndef SMXPanelAnimation_h |
||||
#define SMXPanelAnimation_h |
||||
|
||||
#include <vector> |
||||
#include "SMXGif.h" |
||||
|
||||
enum SMX_LightsType |
||||
{ |
||||
SMX_LightsType_Released, // animation while panels are released
|
||||
SMX_LightsType_Pressed, // animation while panel is pressed
|
||||
NUM_SMX_LightsType, |
||||
}; |
||||
|
||||
// SMXPanelAnimation holds an animation, with graphics for a single panel.
|
||||
class SMXPanelAnimation |
||||
{ |
||||
public: |
||||
// Return the animation loaded by SMX_LightsAnimation_Load.
|
||||
static SMXPanelAnimation GetLoadedAnimation(int pad, int panel, SMX_LightsType type); |
||||
|
||||
void Load(const std::vector<SMXGif::SMXGifFrame> &frames, int panel); |
||||
|
||||
// The high-level animated GIF frames:
|
||||
std::vector<std::vector<SMXGif::Color>> m_aPanelGraphics; |
||||
|
||||
// The animation starts on frame 0. When it reaches the end, it loops
|
||||
// back to this frame.
|
||||
int m_iLoopFrame = 0; |
||||
|
||||
// The duration of each frame in seconds.
|
||||
std::vector<float> m_iFrameDurations; |
||||
}; |
||||
|
||||
// For SMX_API:
|
||||
#include "../SMX.h" |
||||
|
||||
// High-level interface for C# bindings:
|
||||
//
|
||||
// Load an animated GIF as a panel animation. pad is the pad this animation is for (0 or 1),
|
||||
// and type is which animation this is for. Any previously loaded animation will be replaced.
|
||||
// On error, false is returned and error is set to a plain-text error message which is valid
|
||||
// until the next call.
|
||||
SMX_API bool SMX_LightsAnimation_Load(const char *gif, int size, int pad, SMX_LightsType type, const char **error); |
||||
|
||||
// Enable or disable automatically handling lights animations. If enabled, any animations
|
||||
// loaded with SMX_LightsAnimation_Load will run automatically as long as the SDK is loaded.
|
||||
// XXX: should we automatically disable SMX_SetLights when this is enabled?
|
||||
SMX_API void SMX_LightsAnimation_SetAuto(bool enable); |
||||
|
||||
#endif |
Loading…
Reference in new issue