spicetools/external/stepmaniax-sdk/sdk/Windows/SMXPanelAnimation.cpp

521 lines
17 KiB
C++
Raw Normal View History

2024-08-28 15:10:34 +00:00
// Handle playing GIF animations from inside SMXConfig.
//
// This can load two GIF animations, one for when panels are released
// and one for when they're pressed, and play them automatically on the
// pad in the background. Applications that control lights can do more
// sophisticated things with the lights, but this gives an easy way for
// people to create simple animations.
//
// If you're implementing the SDK in a game, you don't need this and should
// use SMX.h instead.
//
// An animation is a single GIF with animations for all panels, in the
// following layout:
//
// 0000|1111|2222
// 0000|1111|2222
// 0000|1111|2222
// 0000|1111|2222
// --------------
// 3333|4444|5555
// 3333|4444|5555
// 3333|4444|5555
// 3333|4444|5555
// --------------
// 6666|7777|8888
// 6666|7777|8888
// 6666|7777|8888
// 6666|7777|8888
// x-------------
//
// The - | regions are ignored and are only there to space out the animation
// to make it easier to view.
//
// The extra bottom row is a flag row and should normally be black. The first
// pixel (bottom-left) optionally marks a loop frame. By default, the animation
// plays all the way through and then loops back to the beginning. If the loop
// frame pixel is white, it marks a frame to loop to instead of the beginning.
// This allows pressed animations to have a separate lead-in and loop.
//
// Each animation is for a single pad. You can load the same animation for both
// pads or use different ones.
#include "SMXPanelAnimation.h"
#include "SMXManager.h"
#include "SMXDevice.h"
#include "SMXThread.h"
#include <cmath>
using namespace std;
using namespace SMX;
namespace {
Mutex g_Lock;
}
#define LIGHTS_PER_PANEL 25
// 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:
size_t 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(size_t 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];
// Skip this panel if it's not in autoLightPanelMask.
if(!(config.autoLightPanelMask & (1 << panel)))
continue;
// Add the released animation, then overlay the pressed animation if we're pressed.
OverlayLights(out, animations[SMX_LightsType_Released][panel].GetAnimationFrame());
bool bPressed = bool(iPadState & (1 << panel));
if(bPressed && bUsePressedAnimations)
OverlayLights(out, animations[SMX_LightsType_Pressed][panel].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, static_cast<float>(c / LightsScaleFactor)));
out[light*3+i] = c;
}
}
}
}
return result;
}
// State for both animations on each panel:
AnimationState animations[NUM_SMX_LightsType][9];
};
namespace
{
// Animations and animation states for both pads.
AnimationStateForPad pad_states[2];
}
namespace {
// The X,Y positions of each possible 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 },
};
// Given a 14x15 graphic frame and a panel number, return an array of 16 colors, containing
// each light in the order it's sent to the master controller.
void ConvertToPanelGraphic16(const SMXGif::GIFImage &src, vector<SMXGif::Color> &dst, int panel)
{
dst.clear();
// The top-left corner for this panel:
int x = graphic_positions[panel].first * 5;
int y = graphic_positions[panel].second * 5;
// Add the 4x4 grid.
for(int dy = 0; dy < 4; ++dy)
for(int dx = 0; dx < 4; ++dx)
dst.push_back(src.get(x+dx, y+dy));
// These animations have no data for the 3x3 grid, so just set them to transparent.
for(int dy = 0; dy < 3; ++dy)
for(int dx = 0; dx < 3; ++dx)
dst.push_back(SMXGif::Color(0,0,0,0));
}
// 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 ConvertToPanelGraphic25(const SMXGif::GIFImage &src, vector<SMXGif::Color> &dst, int panel)
{
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));
}
}
// Load an array of animation frames as a panel animation. Each frame must
// be 14x15 or 23x24.
void SMXPanelAnimation::Load(const vector<SMXGif::SMXGifFrame> &frames, int panel)
{
m_aPanelGraphics.clear();
m_iFrameDurations.clear();
m_iLoopFrame = -1;
for(size_t frame_no = 0; frame_no < frames.size(); ++frame_no)
{
const SMXGif::SMXGifFrame &gif_frame = frames[frame_no];
// If the bottom-left pixel is white, 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.
SMXGif::Color marker = gif_frame.frame.get(0, gif_frame.frame.height-1);
if(marker.color[3] == 0xFF && marker.color[0] >= 0x80)
{
// 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. If the graphic is 14x15 it's a 4x4 animation,
// and if it's 23x24 it's 25-light.
vector<SMXGif::Color> panel_graphic;
if(frames[0].width == 14)
ConvertToPanelGraphic16(gif_frame.frame, panel_graphic, panel);
else
ConvertToPanelGraphic25(gif_frame.frame, panel_graphic, panel);
// 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_aPanelGraphics.push_back(panel_graphic);
m_iFrameDurations.push_back(seconds);
}
// By default, loop back to the first frame.
if(m_iLoopFrame == -1)
m_iLoopFrame = 0;
}
#include "SMXPanelAnimationUpload.h"
// 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) && (frames[0].width != 23 || frames[0].height != 24))
{
*error = "The GIF must be 14x15 or 23x24.";
return false;
}
// Load the graphics into SMXPanelAnimations.
SMXPanelAnimation animations[9];
for(int panel = 0; panel < 9; ++panel)
animations[panel].Load(frames, panel);
// Set up the upload for this graphic.
if(!SMX_LightsUpload_PrepareUpload(pad, type, animations, error))
return false;
// Lock while we access pad_states.
g_Lock.AssertNotLockedByCurrentThread();
LockMutex L(g_Lock);
// Commit the animation to pad_states now that we know there are no errors.
for(int panel = 0; panel < 9; ++panel)
{
SMXPanelAnimation &animation = pad_states[pad].animations[type][panel].animation;
animation = animations[panel];
}
return true;
}
namespace
{
double g_fStopAnimatingUntil = -1;
}
void SMXAutoPanelAnimations::TemporaryStopAnimating()
{
// Stop animating for 100ms.
double fStopForSeconds = 1/10.0f;
g_fStopAnimatingUntil = SMX::GetMonotonicTime() + fStopForSeconds;
}
// 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)
{
// Check if we've temporarily stopped updating lights.
bool bSkipUpdate = g_fStopAnimatingUntil > SMX::GetMonotonicTime();
// Run a single panel lights update.
if(!bSkipUpdate)
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.
bool 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 false;
// If this controller handles animation itself, don't handle it here too. It can
// lead to confusing situations if SMXConfig's animations don't match the ones stored
// on the pad.
if(config.masterVersion >= 4)
return false;
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[SMX_LightsType_Released][panel].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[SMX_LightsType_Pressed][panel].Play();
else
pad_state.animations[SMX_LightsType_Pressed][panel].Stop();
}
// Set the current state.
asLightsDataOut = pad_state.GetLightsCommand(iPadState, config);
// Advance animations.
for(int type = 0; type < NUM_SMX_LightsType; ++type)
{
for(int panel = 0; panel < 9; ++panel)
pad_state.animations[type][panel].Update();
}
return true;
}
// Run a single light animation update.
void UpdateLights()
{
string asLightsData[2];
bool bHaveLights = false;
for(int pad = 0; pad < 2; pad++)
{
int iPadState = SMXManager::g_pSMX->GetDevice(pad)->GetInputState();
if(GetCurrentLights(asLightsData[pad], pad, iPadState))
bHaveLights = true;
}
// Update lights.
if(bHaveLights)
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;