ANDROID: Rework the virtual gamepad controller

- Use a SVG asset to allow for better scalability
- Make the controller visible when touching the screen
- Allow for oblique moving by placing finger between two directions
- Add more buttons on the center area of the screen (GUIDE, START, LEFT
  STICK, RIGHT STICK) and simplify right area (only four buttons)
- Don't track all fingers, only the ones needed
- Cleanup now unused code
This commit is contained in:
Le Philousophe 2024-08-30 16:26:12 +02:00
parent 5065891f0c
commit a98df4e445
14 changed files with 865 additions and 403 deletions

View File

@ -42,24 +42,7 @@
#include "backends/graphics/opengl/pipelines/pipeline.h"
#include "graphics/blit.h"
static void loadBuiltinTexture(JNI::BitmapResources resource, OpenGL::Surface *surf) {
const Graphics::Surface *src = JNI::getBitmapResource(resource);
if (!src) {
error("Failed to fetch touch arrows bitmap");
}
surf->allocate(src->w, src->h);
Graphics::Surface *dst = surf->getSurface();
Graphics::crossBlit(
(byte *)dst->getPixels(), (const byte *)src->getPixels(),
dst->pitch, src->pitch,
src->w, src->h,
src->format, dst->format);
delete src;
}
#include "graphics/managed_surface.h"
//
// AndroidGraphicsManager
@ -72,10 +55,6 @@ AndroidGraphicsManager::AndroidGraphicsManager() :
// Initialize our OpenGL ES context.
initSurface();
_touchcontrols = createSurface(_defaultFormatAlpha);
loadBuiltinTexture(JNI::BitmapResources::TOUCH_ARROWS_BITMAP, _touchcontrols);
_touchcontrols->updateGLTexture();
// not in 3D, not in GUI
dynamic_cast<OSystem_Android *>(g_system)->applyTouchSettings(false, false);
dynamic_cast<OSystem_Android *>(g_system)->applyOrientationSettings();
@ -85,6 +64,8 @@ AndroidGraphicsManager::~AndroidGraphicsManager() {
ENTER();
deinitSurface();
delete _touchcontrols;
}
void AndroidGraphicsManager::initSurface() {
@ -118,8 +99,10 @@ void AndroidGraphicsManager::initSurface() {
if (_touchcontrols) {
_touchcontrols->recreate();
_touchcontrols->updateGLTexture();
} else {
_touchcontrols = createSurface(_defaultFormatAlpha);
}
dynamic_cast<OSystem_Android *>(g_system)->getTouchControls().init(
dynamic_cast<OSystem_Android *>(g_system)->getTouchControls().setDrawer(
this, JNI::egl_surface_width, JNI::egl_surface_height);
handleResize(JNI::egl_surface_width, JNI::egl_surface_height);
@ -132,7 +115,7 @@ void AndroidGraphicsManager::deinitSurface() {
LOGD("deinitializing 2D surface");
// Deregister us from touch control
dynamic_cast<OSystem_Android *>(g_system)->getTouchControls().init(
dynamic_cast<OSystem_Android *>(g_system)->getTouchControls().setDrawer(
nullptr, 0, 0);
if (_touchcontrols) {
_touchcontrols->destroy();
@ -157,7 +140,7 @@ void AndroidGraphicsManager::resizeSurface() {
error("JNI::initSurface failed");
}
dynamic_cast<OSystem_Android *>(g_system)->getTouchControls().init(
dynamic_cast<OSystem_Android *>(g_system)->getTouchControls().setDrawer(
this, JNI::egl_surface_width, JNI::egl_surface_height);
handleResize(JNI::egl_surface_width, JNI::egl_surface_height);
@ -170,6 +153,9 @@ void AndroidGraphicsManager::updateScreen() {
if (!JNI::haveSurface())
return;
// Sets _forceRedraw if needed
dynamic_cast<OSystem_Android *>(g_system)->getTouchControls().beforeDraw();
OpenGLGraphicsManager::updateScreen();
}
@ -246,12 +232,34 @@ void AndroidGraphicsManager::syncVirtkeyboardState(bool virtkeybd_on) {
_forceRedraw = true;
}
void AndroidGraphicsManager::touchControlDraw(int16 x, int16 y, int16 w, int16 h, const Common::Rect &clip) {
void AndroidGraphicsManager::touchControlInitSurface(const Graphics::ManagedSurface &surf) {
if (_touchcontrols->getWidth() == surf.w && _touchcontrols->getHeight() == surf.h) {
return;
}
_touchcontrols->allocate(surf.w, surf.h);
Graphics::Surface *dst = _touchcontrols->getSurface();
Graphics::crossBlit(
(byte *)dst->getPixels(), (const byte *)surf.getPixels(),
dst->pitch, surf.pitch,
surf.w, surf.h,
surf.format, dst->format);
_touchcontrols->updateGLTexture();
}
void AndroidGraphicsManager::touchControlDraw(uint8 alpha, int16 x, int16 y, int16 w, int16 h, const Common::Rect &clip) {
_targetBuffer->enableBlend(OpenGL::Framebuffer::kBlendModeTraditionalTransparency);
OpenGL::Pipeline *pipeline = getPipeline();
pipeline->activate();
if (alpha != 255) {
pipeline->setColor(1.0f, 1.0f, 1.0f, alpha / 255.0f);
}
pipeline->drawTexture(_touchcontrols->getGLTexture(),
x, y, w, h, clip);
if (alpha != 255) {
pipeline->setColor(1.0f, 1.0f, 1.0f, 1.0f);
}
}
void AndroidGraphicsManager::touchControlNotifyChanged() {

View File

@ -88,8 +88,9 @@ public:
float getHiDPIScreenFactor() const override;
void touchControlInitSurface(const Graphics::ManagedSurface &surf) override;
void touchControlNotifyChanged() override;
void touchControlDraw(int16 x, int16 y, int16 w, int16 h, const Common::Rect &clip) override;
void touchControlDraw(uint8 alpha, int16 x, int16 y, int16 w, int16 h, const Common::Rect &clip) override;
protected:
void setSystemMousePosition(const int x, const int y) override {}

View File

@ -41,6 +41,7 @@
#include "common/tokenizer.h"
#include "graphics/blit.h"
#include "graphics/managed_surface.h"
#include "graphics/opengl/shader.h"
#include "graphics/opengl/context.h"
@ -54,26 +55,6 @@
#define CONTEXT_RESET_ENABLE(gl_param) if (!(saved ## gl_param)) { GLCALL(glDisable(gl_param)); }
#define CONTEXT_RESET_DISABLE(gl_param) if (saved ## gl_param) { GLCALL(glEnable(gl_param)); }
static GLES8888Texture *loadBuiltinTexture(JNI::BitmapResources resource) {
const Graphics::Surface *src = JNI::getBitmapResource(JNI::BitmapResources::TOUCH_ARROWS_BITMAP);
if (!src) {
error("Failed to fetch touch arrows bitmap");
}
GLES8888Texture *ret = new GLES8888Texture();
ret->allocBuffer(src->w, src->h);
Graphics::Surface *dst = ret->surface();
Graphics::crossBlit(
(byte *)dst->getPixels(), (const byte *)src->getPixels(),
dst->pitch, src->pitch,
src->w, src->h,
src->format, dst->format);
delete src;
return ret;
}
AndroidGraphics3dManager::AndroidGraphics3dManager() :
_screenChangeID(0),
_graphicsMode(0),
@ -94,7 +75,7 @@ AndroidGraphics3dManager::AndroidGraphics3dManager() :
_mouse_hotspot(),
_mouse_dont_scale(false),
_show_mouse(false),
_touchcontrols_texture(nullptr),
_touchcontrols_texture(new GLES8888Texture()),
_old_touch_mode(OSystem_Android::TOUCH_MODE_TOUCHPAD) {
if (JNI::egl_bits_per_pixel == 16) {
@ -112,8 +93,6 @@ AndroidGraphics3dManager::AndroidGraphics3dManager() :
}
_mouse_texture = _mouse_texture_palette;
_touchcontrols_texture = loadBuiltinTexture(JNI::BitmapResources::TOUCH_ARROWS_BITMAP);
initSurface();
// in 3D, not in GUI
@ -218,7 +197,7 @@ void AndroidGraphics3dManager::initSurface() {
if (_touchcontrols_texture) {
_touchcontrols_texture->reinit();
}
dynamic_cast<OSystem_Android *>(g_system)->getTouchControls().init(
dynamic_cast<OSystem_Android *>(g_system)->getTouchControls().setDrawer(
this, JNI::egl_surface_width, JNI::egl_surface_height);
updateScreenRect();
@ -256,7 +235,7 @@ void AndroidGraphics3dManager::deinitSurface() {
_mouse_texture_palette->release();
}
dynamic_cast<OSystem_Android *>(g_system)->getTouchControls().init(
dynamic_cast<OSystem_Android *>(g_system)->getTouchControls().setDrawer(
nullptr, 0, 0);
if (_touchcontrols_texture) {
_touchcontrols_texture->release();
@ -286,7 +265,7 @@ void AndroidGraphics3dManager::resizeSurface() {
initOverlay();
}
dynamic_cast<OSystem_Android *>(g_system)->getTouchControls().init(
dynamic_cast<OSystem_Android *>(g_system)->getTouchControls().setDrawer(
this, JNI::egl_surface_width, JNI::egl_surface_height);
updateScreenRect();
@ -303,6 +282,9 @@ void AndroidGraphics3dManager::updateScreen() {
return;
}
// Sets _forceRedraw if needed
dynamic_cast<OSystem_Android *>(g_system)->getTouchControls().beforeDraw();
if (!_force_redraw &&
!_game_texture->dirty() &&
!_overlay_texture->dirty() &&
@ -1103,12 +1085,28 @@ bool AndroidGraphics3dManager::setState(const AndroidCommonGraphics::State &stat
return true;
}
void AndroidGraphics3dManager::touchControlInitSurface(const Graphics::ManagedSurface &surf) {
if (_touchcontrols_texture->width() == surf.w && _touchcontrols_texture->height() == surf.h) {
return;
}
_touchcontrols_texture->allocBuffer(surf.w, surf.h);
Graphics::Surface *dst = _touchcontrols_texture->surface();
Graphics::crossBlit(
(byte *)dst->getPixels(), (const byte *)surf.getPixels(),
dst->pitch, surf.pitch,
surf.w, surf.h,
surf.format, dst->format);
}
void AndroidGraphics3dManager::touchControlNotifyChanged() {
// Make sure we redraw the screen
_force_redraw = true;
}
void AndroidGraphics3dManager::touchControlDraw(int16 x, int16 y, int16 w, int16 h, const Common::Rect &clip) {
void AndroidGraphics3dManager::touchControlDraw(uint8 alpha, int16 x, int16 y, int16 w, int16 h, const Common::Rect &clip) {
_touchcontrols_texture->setAlpha(alpha / 255.f);
_touchcontrols_texture->drawTexture(x, y, w, h, clip);
}

View File

@ -121,8 +121,9 @@ public:
virtual Common::List<Graphics::PixelFormat> getSupportedFormats() const override;
#endif
void touchControlInitSurface(const Graphics::ManagedSurface &surf) override;
void touchControlNotifyChanged() override;
void touchControlDraw(int16 x, int16 y, int16 w, int16 h, const Common::Rect &clip) override;
void touchControlDraw(uint8 alpha, int16 x, int16 y, int16 w, int16 h, const Common::Rect &clip) override;
void syncVirtkeyboardState(bool virtkeybd_on) override;

View File

@ -557,6 +557,10 @@ void OSystem_Android::initBackend() {
_audio_thread_exit = false;
pthread_create(&_audio_thread, 0, audioThreadFunc, this);
JNI::DPIValues dpi;
JNI::getDPI(dpi);
_touchControls.init(dpi[2]);
_graphicsManager = new AndroidGraphicsManager();
// renice this thread to boost the audio thread

View File

@ -13,7 +13,7 @@ APK_MAIN = ScummVM-debug.apk
APK_MAIN_RELEASE = ScummVM-release-unsigned.apk
AAB_MAIN_RELEASE = ScummVM-release.aab
DIST_FILES_HELP = $(PATH_DIST)/android-help.zip
DIST_FILES_PLATFORM = $(PATH_DIST)/android-help.zip $(PATH_DIST)/gamepad.svg
$(PATH_BUILD):
$(MKDIR) $(PATH_BUILD)
@ -38,9 +38,9 @@ $(PATH_BUILD)/local.properties: configure.stamp | $(PATH_BUILD)
$(PATH_BUILD)/src.properties: configure.stamp | $(PATH_BUILD)
$(ECHO) "srcdir=$(realpath $(srcdir))\n" > $(PATH_BUILD)/src.properties
$(PATH_BUILD_ASSETS): $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) $(DIST_FILES_ENGINEDATA_BIG) $(DIST_FILES_SOUNDFONTS) $(DIST_FILES_NETWORKING) $(DIST_FILES_VKEYBD) $(DIST_FILES_DOCS) $(DIST_FILES_HELP) | $(PATH_BUILD)
$(PATH_BUILD_ASSETS): $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) $(DIST_FILES_ENGINEDATA_BIG) $(DIST_FILES_SOUNDFONTS) $(DIST_FILES_NETWORKING) $(DIST_FILES_VKEYBD) $(DIST_FILES_DOCS) $(DIST_FILES_PLATFORM) | $(PATH_BUILD)
$(INSTALL) -d $(PATH_BUILD_ASSETS)
$(INSTALL) -c -m 644 $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) $(DIST_FILES_ENGINEDATA_BIG) $(DIST_FILES_SOUNDFONTS) $(DIST_FILES_NETWORKING) $(DIST_FILES_VKEYBD) $(DIST_FILES_DOCS) $(DIST_FILES_HELP) $(PATH_BUILD_ASSETS)/
$(INSTALL) -c -m 644 $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) $(DIST_FILES_ENGINEDATA_BIG) $(DIST_FILES_SOUNDFONTS) $(DIST_FILES_NETWORKING) $(DIST_FILES_VKEYBD) $(DIST_FILES_DOCS) $(DIST_FILES_PLATFORM) $(PATH_BUILD_ASSETS)/
ifneq ($(DIST_FILES_SHADERS),)
$(INSTALL) -d $(PATH_BUILD_ASSETS)/shaders
$(INSTALL) -c -m 644 $(DIST_FILES_SHADERS) $(PATH_BUILD_ASSETS)/shaders

View File

@ -92,7 +92,6 @@ jmethodID JNI::_MID_isConnectionLimited = 0;
jmethodID JNI::_MID_setWindowCaption = 0;
jmethodID JNI::_MID_showVirtualKeyboard = 0;
jmethodID JNI::_MID_showOnScreenControls = 0;
jmethodID JNI::_MID_getBitmapResource = 0;
jmethodID JNI::_MID_setTouchMode = 0;
jmethodID JNI::_MID_getTouchMode = 0;
jmethodID JNI::_MID_setOrientation = 0;
@ -437,71 +436,6 @@ void JNI::showOnScreenControls(int enableMask) {
}
}
Graphics::Surface *JNI::getBitmapResource(BitmapResources resource) {
JNIEnv *env = JNI::getEnv();
jobject bitmap = env->CallObjectMethod(_jobj, _MID_getBitmapResource, (int) resource);
if (env->ExceptionCheck()) {
LOGE("Can't get bitmap resource");
env->ExceptionDescribe();
env->ExceptionClear();
return nullptr;
}
if (bitmap == nullptr) {
LOGE("Bitmap resource was not found");
return nullptr;
}
AndroidBitmapInfo bitmap_info;
if (AndroidBitmap_getInfo(env, bitmap, &bitmap_info) != ANDROID_BITMAP_RESULT_SUCCESS) {
LOGE("Error reading bitmap info");
env->DeleteLocalRef(bitmap);
return nullptr;
}
Graphics::PixelFormat fmt;
switch(bitmap_info.format) {
case ANDROID_BITMAP_FORMAT_RGBA_8888:
#ifdef SCUMM_BIG_ENDIAN
fmt = Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 16, 8, 0);
#else
fmt = Graphics::PixelFormat(4, 8, 8, 8, 8, 0, 8, 16, 24);
#endif
break;
case ANDROID_BITMAP_FORMAT_RGBA_4444:
fmt = Graphics::PixelFormat(2, 4, 4, 4, 4, 12, 8, 4, 0);
break;
case ANDROID_BITMAP_FORMAT_RGB_565:
fmt = Graphics::PixelFormat(2, 5, 6, 5, 0, 11, 5, 0, 0);
break;
default:
LOGE("Bitmap has unsupported format");
env->DeleteLocalRef(bitmap);
return nullptr;
}
void *src_pixels = nullptr;
if (AndroidBitmap_lockPixels(env, bitmap, &src_pixels) != ANDROID_BITMAP_RESULT_SUCCESS) {
LOGE("Error locking bitmap pixels");
env->DeleteLocalRef(bitmap);
return nullptr;
}
Graphics::Surface *ret = new Graphics::Surface();
ret->create(bitmap_info.width, bitmap_info.height, fmt);
ret->copyRectToSurface(src_pixels, bitmap_info.stride,
0, 0, bitmap_info.width, bitmap_info.height);
AndroidBitmap_unlockPixels(env, bitmap);
env->DeleteLocalRef(bitmap);
return ret;
}
void JNI::setTouchMode(int touchMode) {
JNIEnv *env = JNI::getEnv();
@ -822,7 +756,6 @@ void JNI::create(JNIEnv *env, jobject self, jobject asset_manager,
FIND_METHOD(, isConnectionLimited, "()Z");
FIND_METHOD(, showVirtualKeyboard, "(Z)V");
FIND_METHOD(, showOnScreenControls, "(I)V");
FIND_METHOD(, getBitmapResource, "(I)Landroid/graphics/Bitmap;");
FIND_METHOD(, setTouchMode, "(I)V");
FIND_METHOD(, getTouchMode, "()I");
FIND_METHOD(, setOrientation, "(I)V");

View File

@ -46,10 +46,6 @@ private:
virtual ~JNI();
public:
enum struct BitmapResources {
TOUCH_ARROWS_BITMAP = 0
};
static bool pause;
static sem_t pause_sem;
@ -94,7 +90,6 @@ public:
static bool isConnectionLimited();
static void showVirtualKeyboard(bool enable);
static void showOnScreenControls(int enableMask);
static Graphics::Surface *getBitmapResource(BitmapResources resource);
static void setTouchMode(int touchMode);
static int getTouchMode();
static void setOrientation(int touchMode);
@ -157,7 +152,6 @@ private:
static jmethodID _MID_setWindowCaption;
static jmethodID _MID_showVirtualKeyboard;
static jmethodID _MID_showOnScreenControls;
static jmethodID _MID_getBitmapResource;
static jmethodID _MID_setTouchMode;
static jmethodID _MID_getTouchMode;
static jmethodID _MID_setOrientation;

View File

@ -84,7 +84,6 @@ public abstract class ScummVM implements SurfaceHolder.Callback,
abstract protected void setWindowCaption(String caption);
abstract protected void showVirtualKeyboard(boolean enable);
abstract protected void showOnScreenControls(int enableMask);
abstract protected Bitmap getBitmapResource(int resource);
abstract protected void setTouchMode(int touchMode);
abstract protected int getTouchMode();
abstract protected void setOrientation(int orientation);

View File

@ -777,23 +777,6 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
});
}
@Override
protected Bitmap getBitmapResource(int resource) {
int id;
switch(resource) {
case 0: // TOUCH_ARROWS_BITMAP
id = R.drawable.touch_arrows;
break;
default:
return null;
}
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inScaled = false;
return BitmapFactory.decodeResource(getResources(), id, opts);
}
@Override
protected void setTouchMode(final int touchMode) {
if (_events.getTouchMode() == touchMode) {

View File

@ -38,150 +38,174 @@
#define FORBIDDEN_SYMBOL_EXCEPTION_printf
#include "backends/platform/android/android.h"
#include "backends/platform/android/jni-android.h"
#include "backends/platform/android/touchcontrols.h"
#include "common/file.h"
#include "graphics/svg.h"
#define SVG_WIDTH 384
#define SVG_HEIGHT 256
#define BG_WIDTH 128
#define BG_HEIGHT 128
#define ARROW_DEAD 21
#define ARROW_SUPER 42
#define ARROW_END 64
#define ARROW_WIDTH 64
#define ARROW_SEMI_HEIGHT 52
#define ARROW_HEIGHT 64
#define ARROW_HL_LEFT 0
#define ARROW_HL_TOP 128
#define BUTTON_DEAD 12
#define BUTTON_END 64
#define BUTTON_WIDTH 64
#define BUTTON_HEIGHT 64
#define BUTTON_HL_LEFT 128
#define BUTTON_HL_TOP 128
#define BUTTON2_HL_LEFT 256
#define BUTTON2_HL_TOP 128
#define ALPHA_OPAQUE 255
#define ZOMBIE_TIMEOUT 500 // ms
// The scale factor is stored as a fixed point 30.2 bits
// This avoids floating point operations
#define SCALE_FACTOR_FXP 4
// gamepad.svg was designed with a basis of 128x128 sized widget
// As it's too small on screen, we apply a factor of 1.5x
// It can be tuned here and in SVG viewBox without changing anything else
#define SVG_UNSCALED(x) ((x) * 3 / 2)
#define SVG_SQ_UNSCALED(x) ((x * x) * 9 / 4)
#define SVG_SCALED(x) (SVG_UNSCALED(x) * _scale)
#define SCALED_PIXELS(x) ((x) / SCALE_FACTOR_FXP)
#define SVG_PIXELS(x) SCALED_PIXELS(SVG_SCALED(x))
#define FUNC_SVG_SQ_SCALED(x) (SVG_SQ_UNSCALED(x) * parent->_scale2)
TouchControls::TouchControls() :
_drawer(nullptr),
_screen_width(0),
_screen_height(0) {
_screen_height(0),
_svg(nullptr),
_zombieCount(0),
_scale(0),
_scale2(0) {
_functions[kFunctionLeft] = new FunctionLeft(this);
_functions[kFunctionRight] = new FunctionRight(this);
_functions[kFunctionCenter] = new FunctionCenter(this);
}
TouchControls::Function TouchControls::getFunction(int x, int y) {
void TouchControls::init(float scale) {
_scale = scale * SCALE_FACTOR_FXP;
// As scale is small, this should fit in int
_scale2 = _scale * _scale;
Common::File stream;
if (!stream.open("gamepad.svg")) {
error("Failed to fetch gamepad image");
}
delete _svg;
_svg = new Graphics::SVGBitmap(&stream, SVG_PIXELS(SVG_WIDTH), SVG_PIXELS(SVG_HEIGHT));
}
TouchControls::~TouchControls() {
delete _svg;
for(unsigned int i = 0; i < kFunctionCount; i++) {
delete _functions[i];
}
}
void TouchControls::beforeDraw() {
if (!_zombieCount) {
return;
}
// We have zombies, force redraw to render fading out
if (_drawer) {
_drawer->touchControlNotifyChanged();
}
// Check for zombie functions, clear them out if expired
unsigned int zombieCount = 0;
uint32 now = g_system->getMillis(true);
for (uint i = 0; i < kFunctionCount; i++) {
Function *func = _functions[i];
if (func->status != kFunctionZombie) {
continue;
}
if (func->lastActivable < now) {
func->status = kFunctionInactive;
continue;
}
zombieCount++;
}
_zombieCount = zombieCount;
}
TouchControls::FunctionId TouchControls::getFunctionId(int x, int y) {
if (_screen_width == 0) {
// Avoid divide by 0 error
return kFunctionNone;
}
float xPercent = float(x) / _screen_width;
// Exclude areas reserved for system
if ((x < JNI::gestures_insets[0] * SCALE_FACTOR_FXP) ||
(y < JNI::gestures_insets[1] * SCALE_FACTOR_FXP) ||
(x >= _screen_width - JNI::gestures_insets[2] * SCALE_FACTOR_FXP) ||
(y >= _screen_height - JNI::gestures_insets[3] * SCALE_FACTOR_FXP)) {
return kFunctionNone;
}
if (xPercent < 0.3) {
return kFunctionJoystick;
} else if (xPercent < 0.8) {
float xRatio = float(x) / _screen_width;
if (xRatio < 0.3) {
return kFunctionLeft;
} else if (xRatio < 0.7) {
return kFunctionCenter;
} else {
return kFunctionRight;
}
}
void TouchControls::touchToJoystickState(int dX, int dY, FunctionState &state) {
int sqNorm = dX * dX + dY * dY;
if (sqNorm < 50 * 50) {
return;
}
if (dY > abs(dX)) {
state.main = Common::JOYSTICK_BUTTON_DPAD_DOWN;
state.clip = Common::Rect(256, 0, 384, 128);
} else if (dX > abs(dY)) {
state.main = Common::JOYSTICK_BUTTON_DPAD_RIGHT;
state.clip = Common::Rect(128, 0, 256, 128);
} else if (-dY > abs(dX)) {
state.main = Common::JOYSTICK_BUTTON_DPAD_UP;
state.clip = Common::Rect(0, 0, 128, 128);
} else if (-dX > abs(dY)) {
state.main = Common::JOYSTICK_BUTTON_DPAD_LEFT;
state.clip = Common::Rect(384, 0, 512, 128);
} else {
return;
}
if (sqNorm > 20000) {
state.modifier = Common::JOYSTICK_BUTTON_RIGHT_SHOULDER;
}
}
void TouchControls::touchToCenterState(int dX, int dY, FunctionState &state) {
int sqNorm = dX * dX + dY * dY;
if (sqNorm < 50 * 50) {
state.main = Common::JOYSTICK_BUTTON_GUIDE;
}
}
void TouchControls::touchToRightState(int dX, int dY, FunctionState &state) {
if (dX * dX + dY * dY < 100 * 100) {
return;
}
if (dX > abs(dY)) {
// right
state.main = Common::JOYSTICK_BUTTON_RIGHT_STICK;
state.clip = Common::Rect(512, 128, 640, 256);
return;
} else if (-dX > abs(dY)) {
// left
state.main = Common::JOYSTICK_BUTTON_LEFT_STICK;
state.clip = Common::Rect(512, 0, 640, 128);
return;
}
static Common::JoystickButton buttons[5] = {
Common::JOYSTICK_BUTTON_Y, Common::JOYSTICK_BUTTON_B, // top zone
Common::JOYSTICK_BUTTON_INVALID, // center
Common::JOYSTICK_BUTTON_A, Common::JOYSTICK_BUTTON_X // bottom zone
};
static int16 clips[5][4] = {
{ 0, 128, 128, 256 }, // y
{ 128, 128, 256, 256 }, // b
{ 0, 0, 0, 0 }, // center
{ 256, 128, 384, 256 }, // a
{ 384, 128, 512, 256 } // x
};
static const uint offset = (ARRAYSIZE(buttons) - 1) / 2;
int idx = (dY / 100) + offset;
if (idx < 0) {
idx = 0;
}
if (idx >= ARRAYSIZE(buttons)) {
idx = ARRAYSIZE(buttons) - 1;
}
state.main = buttons[idx];
state.clip = Common::Rect(clips[idx][0], clips[idx][1], clips[idx][2], clips[idx][3]);
}
TouchControls::FunctionBehavior TouchControls::functionBehaviors[TouchControls::kFunctionMax + 1] =
{
{ touchToJoystickState, false, .2f, .5f },
{ touchToCenterState, true, .5f, .5f },
{ touchToRightState, true, .8f, .5f }
};
void TouchControls::init(TouchControlsDrawer *drawer, int width, int height) {
void TouchControls::setDrawer(TouchControlsDrawer *drawer, int width, int height) {
_drawer = drawer;
_screen_width = width;
_screen_height = height;
_screen_width = width * SCALE_FACTOR_FXP;
_screen_height = height * SCALE_FACTOR_FXP;
if (drawer) {
drawer->touchControlInitSurface(*_svg);
}
}
TouchControls::Pointer *TouchControls::getPointerFromId(int ptrId, bool createNotFound) {
uint freeEntry = -1;
for (uint i = 0; i < kNumPointers; i++) {
Pointer &ptr = _pointers[i];
if (ptr.active && (ptr.id == ptrId)) {
return &ptr;
TouchControls::Function *TouchControls::getFunctionFromPointerId(int ptrId) {
for (uint i = 0; i < kFunctionCount; i++) {
Function *func = _functions[i];
if (func->status != kFunctionActive) {
continue;
}
if (createNotFound && (freeEntry == -1) && !ptr.active) {
freeEntry = i;
if (func->pointerId == ptrId) {
return func;
}
}
// Too much fingers or not found
if (freeEntry == -1) {
return nullptr;
}
TouchControls::Function *TouchControls::getZombieFunctionFromPos(int x, int y) {
if (!_zombieCount) {
return nullptr;
}
Pointer &ptr = _pointers[freeEntry];
ptr.reset();
ptr.id = ptrId;
return &ptr;
}
TouchControls::Pointer *TouchControls::findPointerFromFunction(Function function) {
for (uint i = 0; i < kNumPointers; i++) {
Pointer &ptr = _pointers[i];
if (ptr.active && (ptr.function == function)) {
return &ptr;
for (uint i = 0; i < kFunctionCount; i++) {
Function *func = _functions[i];
if (func->status != kFunctionZombie) {
// Already assigned to a finger or dead
continue;
}
if (func->isInside(x, y)) {
return func;
}
}
return nullptr;
@ -190,130 +214,119 @@ TouchControls::Pointer *TouchControls::findPointerFromFunction(Function function
void TouchControls::draw() {
assert(_drawer != nullptr);
for (uint i = 0; i < kFunctionMax + 1; i++) {
FunctionState &state = _functionStates[i];
FunctionBehavior behavior = functionBehaviors[i];
uint32 now = g_system->getMillis(true);
if (state.clip.isEmpty()) {
for (uint i = 0; i < kFunctionCount; i++) {
Function *func = _functions[i];
uint8 alpha;
switch (func->status) {
case kFunctionActive:
alpha = ALPHA_OPAQUE;
break;
case kFunctionZombie:
if (func->lastActivable < now) {
// This function is definitely dead
continue;
}
alpha = (func->lastActivable - now) * ALPHA_OPAQUE / ZOMBIE_TIMEOUT;
break;
case kFunctionInactive:
default:
continue;
}
_drawer->touchControlDraw(_screen_width * behavior.xRatio, _screen_height * behavior.yRatio,
64, 64, state.clip);
func->draw(alpha);
}
}
void TouchControls::update(Action action, int ptrId, int x, int y) {
x *= SCALE_FACTOR_FXP;
y *= SCALE_FACTOR_FXP;
if (action == JACTION_DOWN) {
Pointer *ptr = getPointerFromId(ptrId, true);
if (!ptr) {
return;
Function *func = getZombieFunctionFromPos(x, y);
if (!func) {
// Finger was not pressed on a zombie function
// Determine which function it could be
FunctionId funcId = getFunctionId(x, y);
if (funcId != kFunctionNone) {
func = _functions[funcId];
}
if (!func) {
// No function for this finger
return;
}
if (func->status == kFunctionActive) {
// Another finger is already on this function
return;
}
// When zombie, we reuse the old start coordinates
// but not when starting over
func->reset();
func->startX = x;
func->startY = y;
}
TouchControls::Function function = getFunction(x, y);
// ptrId is active no matter what
ptr->active = true;
if (function == kFunctionNone) {
// No function for this finger
return;
}
if (findPointerFromFunction(function)) {
// Some finger is already using this function: don't do anything
return;
}
func->status = kFunctionActive;
func->pointerId = ptrId;
func->currentX = x;
func->currentY = y;
ptr->startX = ptr->currentX = x;
ptr->startY = ptr->currentY = y;
ptr->function = function;
int dX = x - func->startX;
int dY = y - func->startY;
func->touch(dX, dY, action);
if (_drawer) {
_drawer->touchControlNotifyChanged();
}
} else if (action == JACTION_MOVE) {
Pointer *ptr = getPointerFromId(ptrId, false);
if (!ptr || ptr->function == kFunctionNone) {
Function *func = getFunctionFromPointerId(ptrId);
if (!func) {
return;
}
FunctionBehavior &behavior = functionBehaviors[ptr->function];
func->currentX = x;
func->currentY = y;
ptr->currentX = x;
ptr->currentY = y;
int dX = x - func->startX;
int dY = y - func->startY;
int dX = x - ptr->startX;
int dY = y - ptr->startY;
FunctionState newState;
functionBehaviors[ptr->function].touchToState(dX, dY, newState);
FunctionState &oldState = _functionStates[ptr->function];
if (!behavior.pressOnRelease) {
// send key presses continuously
// first old remove main key, then update modifier, then press new main key
if (oldState.main != newState.main) {
buttonUp(oldState.main);
}
if (oldState.modifier != newState.modifier) {
buttonUp(oldState.modifier);
buttonDown(newState.modifier);
}
if (oldState.main != newState.main) {
buttonDown(newState.main);
}
}
oldState = newState;
func->touch(dX, dY, action);
if (_drawer) {
_drawer->touchControlNotifyChanged();
}
} else if (action == JACTION_UP) {
Pointer *ptr = getPointerFromId(ptrId, false);
if (!ptr || ptr->function == kFunctionNone) {
Function *func = getFunctionFromPointerId(ptrId);
if (!func) {
return;
}
FunctionBehavior &behavior = functionBehaviors[ptr->function];
FunctionState &functionState = _functionStates[ptr->function];
func->currentX = x;
func->currentY = y;
if (!behavior.pressOnRelease) {
// We sent key down continuously: buttonUp everything
buttonUp(functionState.main);
buttonUp(functionState.modifier);
} else {
int dX = x - ptr->startX;
int dY = y - ptr->startY;
int dX = x - func->startX;
int dY = y - func->startY;
FunctionState newState;
functionBehaviors[ptr->function].touchToState(dX, dY, newState);
buttonDown(newState.modifier);
buttonPress(newState.main);
buttonUp(newState.modifier);
}
functionState.reset();
ptr->active = false;
func->touch(dX, dY, action);
func->status = kFunctionZombie;
func->lastActivable = g_system->getMillis(true) + ZOMBIE_TIMEOUT;
if (_drawer) {
_drawer->touchControlNotifyChanged();
}
_zombieCount++;
} else if (action == JACTION_CANCEL) {
for (uint i = 0; i < kNumPointers; i++) {
Pointer &ptr = _pointers[i];
ptr.reset();
}
for (uint i = 0; i < kFunctionCount; i++) {
Function *func = _functions[i];
for (uint i = 0; i < kFunctionMax + 1; i++) {
FunctionBehavior &behavior = functionBehaviors[i];
FunctionState &functionState = _functionStates[i];
if (!behavior.pressOnRelease) {
// We sent key down continuously: buttonUp everything
buttonUp(functionState.main);
buttonUp(functionState.modifier);
if (func->status == kFunctionActive) {
func->touch(0, 0, action);
}
functionState.reset();
func->reset();
}
if (_drawer) {
_drawer->touchControlNotifyChanged();
}
_zombieCount = 0;
}
}
@ -322,6 +335,7 @@ void TouchControls::buttonDown(Common::JoystickButton jb) {
return;
}
//LOGD("TouchControls::buttonDown: %d", jb);
Common::Event ev;
ev.type = Common::EVENT_JOYBUTTON_DOWN;
ev.joystick.button = jb;
@ -333,6 +347,7 @@ void TouchControls::buttonUp(Common::JoystickButton jb) {
return;
}
//LOGD("TouchControls::buttonUp: %d", jb);
Common::Event ev;
ev.type = Common::EVENT_JOYBUTTON_UP;
ev.joystick.button = jb;
@ -344,6 +359,7 @@ void TouchControls::buttonPress(Common::JoystickButton jb) {
return;
}
//LOGD("TouchControls::buttonPress: %d", jb);
Common::Event ev1, ev2;
ev1.type = Common::EVENT_JOYBUTTON_DOWN;
ev1.joystick.button = jb;
@ -351,3 +367,388 @@ void TouchControls::buttonPress(Common::JoystickButton jb) {
ev2.joystick.button = jb;
dynamic_cast<OSystem_Android *>(g_system)->pushEvent(ev1, ev2);
}
void TouchControls::drawSurface(uint8 alpha, int x, int y, int offX, int offY, const Common::Rect &clip) const {
Common::Rect clip_(SVG_PIXELS(clip.left), SVG_PIXELS(clip.top),
SVG_PIXELS(clip.right), SVG_PIXELS(clip.bottom));
_drawer->touchControlDraw(alpha,
SCALED_PIXELS(x + SVG_SCALED(offX)), SCALED_PIXELS(y + SVG_SCALED(offY)),
clip_.width(), clip_.height(), clip_);
}
bool TouchControls::FunctionLeft::isInside(int x, int y) {
int dX = x - startX;
int dY = y - startY;
// norm 2 squared (to avoid square root)
unsigned int sqNorm = (unsigned int)(dX * dX) + (unsigned int)(dY * dY);
if (sqNorm <= FUNC_SVG_SQ_SCALED(ARROW_END)) {
return true;
}
// Also accept touches near the old last touch
dX = x - currentX;
dY = y - currentY;
sqNorm = (unsigned int)(dX * dX) + (unsigned int)(dY * dY);
if (sqNorm <= FUNC_SVG_SQ_SCALED(ARROW_DEAD)) {
return true;
}
return false;
}
void TouchControls::FunctionLeft::maskToLeftButtons(uint32 oldMask, uint32 newMask) {
static const Common::JoystickButton buttons[] = {
Common::JOYSTICK_BUTTON_DPAD_UP, Common::JOYSTICK_BUTTON_DPAD_RIGHT,
Common::JOYSTICK_BUTTON_DPAD_DOWN, Common::JOYSTICK_BUTTON_DPAD_LEFT
};
uint32 diff = newMask ^ oldMask;
for(int i = 0, m = 1; i < ARRAYSIZE(buttons); i++, m <<= 1) {
if (!(diff & m)) {
continue;
}
if (oldMask & m) {
TouchControls::buttonUp(buttons[i]);
}
}
if (diff & 16) {
if (oldMask & 16) {
TouchControls::buttonUp(Common::JOYSTICK_BUTTON_RIGHT_SHOULDER);
} else {
TouchControls::buttonDown(Common::JOYSTICK_BUTTON_RIGHT_SHOULDER);
}
}
for(int i = 0, m = 1; i < ARRAYSIZE(buttons); i++, m <<= 1) {
if (!(diff & m)) {
continue;
}
if (newMask & m) {
TouchControls::buttonDown(buttons[i]);
}
}
}
void TouchControls::FunctionLeft::touch(int dX, int dY, Action action) {
if (action == JACTION_CANCEL ||
action == JACTION_UP) {
maskToLeftButtons(mask, 0);
resetState();
return;
}
uint32 newMask = 0;
// norm 2 squared (to avoid square root)
unsigned int sqNorm = (unsigned int)(dX * dX) + (unsigned int)(dY * dY);
if (sqNorm >= FUNC_SVG_SQ_SCALED(ARROW_DEAD)) {
// We are far enough from the center
// For right we use angles -60,60 as a sensitive zone
// For left it's the same but mirrored in negative
// We must be between the two lines which are of tan(60),tan(-60) and this corrsponds to sqrt(3)
// For up down we use angles -30,30 as a sensitive zone
// We must be outside the two lines which are of tan(30),tan(-30) and this corrsponds to 1/sqrt(3)
/*
static const double SQRT3 = 1.73205080756887719318;
unsigned int sq3 = SQRT3 * abs(dX);
*/
// Optimize by using an approximation of sqrt(3)
unsigned int sq3 = abs(dX) * 51409 / 29681;
unsigned int isq3 = abs(dX) * 29681 / 51409;
unsigned int adY = abs(dY);
if (adY <= sq3) {
// Left or right
if (dX < 0) {
newMask |= 8;
} else {
newMask |= 2;
}
}
if (adY >= isq3) {
// Up or down
if (dY < 0) {
newMask |= 1;
} else {
newMask |= 4;
}
}
}
if (sqNorm > FUNC_SVG_SQ_SCALED(ARROW_SUPER)) {
newMask |= 16;
}
if (mask != newMask) {
maskToLeftButtons(mask, newMask);
}
mask = newMask;
}
void TouchControls::FunctionLeft::draw(uint8 alpha) {
// Draw background
{
Common::Rect clip(0, 0, BG_WIDTH, BG_HEIGHT);
parent->drawSurface(alpha, startX, startY, -clip.width() / 2, -clip.height() / 2, clip);
}
if (mask == 0) {
return;
}
// Width and height here are rotated for left/right
uint16 width = ARROW_WIDTH;
uint16 height;
if (mask & 16) {
height = ARROW_HEIGHT;
} else {
height = ARROW_SEMI_HEIGHT;
}
// We can draw multiple arrows
if (mask & 1) {
// Draw UP
Common::Rect clip(width, height);
clip.translate(0, ARROW_HL_TOP + ARROW_HEIGHT - height);
int16 offX = -1, offY = -2;
parent->drawSurface(ALPHA_OPAQUE, startX, startY, offX * clip.width() / 2, offY * clip.height() / 2, clip);
}
if (mask & 2) {
// Draw RIGHT
Common::Rect clip(height, width);
clip.translate(ARROW_WIDTH, ARROW_HL_TOP);
int16 offX = 0, offY = -1;
parent->drawSurface(ALPHA_OPAQUE, startX, startY, offX * clip.width() / 2, offY * clip.height() / 2, clip);
}
if (mask & 4) {
// Draw DOWN
Common::Rect clip(width, height);
clip.translate(0, ARROW_HL_TOP + ARROW_HEIGHT);
int16 offX = -1, offY = 0;
parent->drawSurface(ALPHA_OPAQUE, startX, startY, offX * clip.width() / 2, offY * clip.height() / 2, clip);
}
if (mask & 8) {
// Draw LEFT
Common::Rect clip(height, width);
clip.translate(ARROW_WIDTH + ARROW_WIDTH - height, ARROW_HL_TOP + ARROW_HEIGHT);
int16 offX = -2, offY = -1;
parent->drawSurface(ALPHA_OPAQUE, startX, startY, offX * clip.width() / 2, offY * clip.height() / 2, clip);
}
}
bool TouchControls::FunctionRight::isInside(int x, int y) {
int dX = x - startX;
int dY = y - startY;
// norm 2 squared (to avoid square root)
unsigned int sqNorm = (unsigned int)(dX * dX) + (unsigned int)(dY * dY);
return sqNorm <= FUNC_SVG_SQ_SCALED(BUTTON_END);
}
void TouchControls::FunctionRight::touch(int dX, int dY, Action action) {
if (action == JACTION_CANCEL) {
return;
}
// norm 2 squared (to avoid square root)
unsigned int sqNorm = (unsigned int)(dX * dX) + (unsigned int)(dY * dY);
if (sqNorm >= FUNC_SVG_SQ_SCALED(BUTTON_DEAD) && sqNorm <= FUNC_SVG_SQ_SCALED(BUTTON_END)) {
// We are far enough from the center
// For right we use angles -45,45 as a sensitive zone
// For left it's the same but mirrored in negative
// We must be between the two lines which are of tan(45),tan(-45) and this corrsponds to 1
// For up down we use angles -45,45 as a sensitive zone
// We must be outside the two lines which are of tan(45),tan(-45) and this corrsponds to 1
unsigned int adX = abs(dX);
unsigned int adY = abs(dY);
if (adY <= adX) {
// X or B
if (dX < 0) {
button = 4;
} else {
button = 2;
}
} else {
// Y or A
if (dY < 0) {
button = 1;
} else {
button = 3;
}
}
} else {
button = 0;
}
static const Common::JoystickButton buttons[] = {
Common::JOYSTICK_BUTTON_Y, Common::JOYSTICK_BUTTON_B,
Common::JOYSTICK_BUTTON_A, Common::JOYSTICK_BUTTON_X
};
static const Common::JoystickButton modifiers[] = {
Common::JOYSTICK_BUTTON_INVALID, Common::JOYSTICK_BUTTON_INVALID,
Common::JOYSTICK_BUTTON_INVALID, Common::JOYSTICK_BUTTON_INVALID
};
if (action == JACTION_UP && button) {
buttonDown(modifiers[button - 1]);
buttonPress(buttons[button - 1]);
buttonUp(modifiers[button - 1]);
button = 0;
}
}
void TouchControls::FunctionRight::draw(uint8 alpha) {
// Draw background
{
Common::Rect clip(BG_WIDTH, 0, 2 * BG_WIDTH, BG_HEIGHT);
parent->drawSurface(alpha, startX, startY, -clip.width() / 2, -clip.height() / 2, clip);
}
if (button == 0) {
return;
}
Common::Rect clip(BUTTON_WIDTH, BUTTON_HEIGHT);
int16 offX, offY;
if (button == 1) {
// Draw Y
clip.translate(BUTTON_HL_LEFT, BUTTON_HL_TOP);
offX = -1;
offY = -2;
} else if (button == 2) {
// Draw B
clip.translate(BUTTON_HL_LEFT + BUTTON_WIDTH, BUTTON_HL_TOP);
offX = 0;
offY = -1;
} else if (button == 3) {
// Draw A
clip.translate(BUTTON_HL_LEFT, BUTTON_HL_TOP + BUTTON_HEIGHT);
offX = -1;
offY = 0;
} else if (button == 4) {
// Draw X
clip.translate(BUTTON_HL_LEFT + BUTTON_WIDTH, BUTTON_HL_TOP + BUTTON_HEIGHT);
offX = -2;
offY = -1;
} else {
return;
}
parent->drawSurface(ALPHA_OPAQUE, startX, startY, offX * clip.width() / 2, offY * clip.height() / 2, clip);
}
bool TouchControls::FunctionCenter::isInside(int x, int y) {
int dX = x - startX;
int dY = y - startY;
// norm 2 squared (to avoid square root)
unsigned int sqNorm = (unsigned int)(dX * dX) + (unsigned int)(dY * dY);
return sqNorm <= FUNC_SVG_SQ_SCALED(BUTTON_END);
}
void TouchControls::FunctionCenter::touch(int dX, int dY, Action action) {
if (action == JACTION_CANCEL) {
return;
}
// norm 2 squared (to avoid square root)
unsigned int sqNorm = (unsigned int)(dX * dX) + (unsigned int)(dY * dY);
if (sqNorm >= FUNC_SVG_SQ_SCALED(BUTTON_DEAD) && sqNorm <= FUNC_SVG_SQ_SCALED(BUTTON_END)) {
// We are far enough from the center
// For right we use angles -45,45 as a sensitive zone
// For left it's the same but mirrored in negative
// We must be between the two lines which are of tan(45),tan(-45) and this corrsponds to 1
// For up down we use angles -45,45 as a sensitive zone
// We must be outside the two lines which are of tan(45),tan(-45) and this corrsponds to 1
unsigned int adX = abs(dX);
unsigned int adY = abs(dY);
if (adY <= adX) {
// X or B
if (dX < 0) {
button = 4;
} else {
button = 2;
}
} else {
// Y or A
if (dY < 0) {
button = 1;
} else {
button = 3;
}
}
} else {
button = 0;
}
static const Common::JoystickButton buttons[] = {
Common::JOYSTICK_BUTTON_GUIDE, Common::JOYSTICK_BUTTON_RIGHT_STICK,
Common::JOYSTICK_BUTTON_START, Common::JOYSTICK_BUTTON_LEFT_STICK
};
static const Common::JoystickButton modifiers[] = {
Common::JOYSTICK_BUTTON_INVALID, Common::JOYSTICK_BUTTON_INVALID,
Common::JOYSTICK_BUTTON_INVALID, Common::JOYSTICK_BUTTON_INVALID
};
if (action == JACTION_UP && button) {
buttonDown(modifiers[button - 1]);
buttonPress(buttons[button - 1]);
buttonUp(modifiers[button - 1]);
button = 0;
}
}
void TouchControls::FunctionCenter::draw(uint8 alpha) {
// Draw background
{
Common::Rect clip(BG_WIDTH * 2, 0, 3 * BG_WIDTH, BG_HEIGHT);
parent->drawSurface(alpha, startX, startY, -clip.width() / 2, -clip.height() / 2, clip);
}
if (button == 0) {
return;
}
Common::Rect clip(BUTTON_WIDTH, BUTTON_HEIGHT);
int16 offX, offY;
if (button == 1) {
// Draw Y
clip.translate(BUTTON2_HL_LEFT, BUTTON2_HL_TOP);
offX = -1;
offY = -2;
} else if (button == 2) {
// Draw B
clip.translate(BUTTON2_HL_LEFT + BUTTON_WIDTH, BUTTON2_HL_TOP);
offX = 0;
offY = -1;
} else if (button == 3) {
// Draw A
clip.translate(BUTTON2_HL_LEFT, BUTTON2_HL_TOP + BUTTON_HEIGHT);
offX = -1;
offY = 0;
} else if (button == 4) {
// Draw X
clip.translate(BUTTON2_HL_LEFT + BUTTON_WIDTH, BUTTON2_HL_TOP + BUTTON_HEIGHT);
offX = -2;
offY = -1;
} else {
return;
}
// Always draw overlay as opaque
parent->drawSurface(ALPHA_OPAQUE, startX, startY, offX * clip.width() / 2, offY * clip.height() / 2, clip);
}

View File

@ -24,10 +24,15 @@
#include "common/events.h"
namespace Graphics {
class ManagedSurface;
}
class TouchControlsDrawer {
public:
virtual void touchControlInitSurface(const Graphics::ManagedSurface &surf) = 0;
virtual void touchControlNotifyChanged() = 0;
virtual void touchControlDraw(int16 x, int16 y, int16 w, int16 h, const Common::Rect &clip) = 0;
virtual void touchControlDraw(uint8 alpha, int16 x, int16 y, int16 w, int16 h, const Common::Rect &clip) = 0;
protected:
~TouchControlsDrawer() {}
@ -44,81 +49,126 @@ public:
};
TouchControls();
~TouchControls();
void init(TouchControlsDrawer *drawer, int width, int height);
void init(float scale);
void setDrawer(TouchControlsDrawer *drawer, int width, int height);
void beforeDraw();
void draw();
void update(Action action, int ptr, int x, int y);
private:
TouchControlsDrawer *_drawer;
int _screen_width, _screen_height;
unsigned int _screen_width, _screen_height;
unsigned int _scale, _scale2;
enum Function {
kFunctionNone = -1,
kFunctionJoystick = 0,
kFunctionCenter = 1,
kFunctionRight = 2,
kFunctionMax = 2
Graphics::ManagedSurface *_svg;
unsigned int _zombieCount;
enum State {
kFunctionInactive = 0,
kFunctionActive = 1,
kFunctionZombie = 2
};
Function getFunction(int x, int y);
struct Pointer {
Pointer() : id(-1), startX(-1), startY(-1),
struct Function {
virtual bool isInside(int, int) = 0;
virtual void touch(int, int, Action) = 0;
virtual void draw(uint8 alpha) = 0;
virtual void resetState() {}
Function(const TouchControls *parent_) :
parent(parent_), pointerId(-1),
startX(-1), startY(-1),
currentX(-1), currentY(-1),
function(kFunctionNone), active(false) {}
lastActivable(0), status(kFunctionInactive) {}
virtual ~Function() {}
void reset() {
id = -1;
pointerId = -1;
startX = startY = currentX = currentY = -1;
function = kFunctionNone;
active = false;
lastActivable = 0;
status = kFunctionInactive;
resetState();
}
int id;
const TouchControls *parent;
int pointerId;
uint16 startX, startY;
uint16 currentX, currentY;
Function function;
bool active;
uint32 lastActivable;
State status;
};
Function *getFunctionFromPointerId(int ptr);
Function *getZombieFunctionFromPos(int x, int y);
enum FunctionId {
kFunctionNone = -1,
kFunctionLeft = 0,
kFunctionRight = 1,
kFunctionCenter = 2,
kFunctionCount = 3
};
FunctionId getFunctionId(int x, int y);
Function *_functions[kFunctionCount];
static void buttonDown(Common::JoystickButton jb);
static void buttonUp(Common::JoystickButton jb);
static void buttonPress(Common::JoystickButton jb);
/**
* Draws a part of the joystick surface on the screen
*
* @param x The left coordinate in fixed-point screen pixels
* @param y The top coordinate in fixed-point screen pixels
* @param offX The left offset in SVG pixels
* @param offY The top offset in SVG pixels
* @param clip The clipping rectangle in source surface in SVG pixels
*/
void drawSurface(uint8 alpha, int x, int y, int offX, int offY, const Common::Rect &clip) const;
// Functions implementations
struct FunctionLeft : Function {
FunctionLeft(const TouchControls *parent) :
Function(parent), mask(0) {}
void resetState() override { mask = 0; }
bool isInside(int, int) override;
void touch(int, int, Action) override;
void draw(uint8 alpha) override;
uint32 mask;
void maskToLeftButtons(uint32 oldMask, uint32 newMask);
};
enum { kNumPointers = 5 };
Pointer _pointers[kNumPointers];
struct FunctionRight : Function {
FunctionRight(const TouchControls *parent) :
Function(parent), button(0) {}
void resetState() override { button = 0; }
Pointer *getPointerFromId(int ptr, bool createNotFound);
Pointer *findPointerFromFunction(Function function);
bool isInside(int, int) override;
void touch(int, int, Action) override;
void draw(uint8 alpha) override;
struct FunctionState {
FunctionState() : main(Common::JOYSTICK_BUTTON_INVALID),
modifier(Common::JOYSTICK_BUTTON_INVALID) {}
void reset() {
main = Common::JOYSTICK_BUTTON_INVALID;
modifier = Common::JOYSTICK_BUTTON_INVALID;
clip = Common::Rect();
}
Common::JoystickButton main;
Common::JoystickButton modifier;
Common::Rect clip;
uint32 button;
};
FunctionState _functionStates[kFunctionMax + 1];
struct FunctionCenter : Function {
FunctionCenter(const TouchControls *parent) :
Function(parent), button(0) {}
void resetState() override { button = 0; }
void buttonDown(Common::JoystickButton jb);
void buttonUp(Common::JoystickButton jb);
void buttonPress(Common::JoystickButton jb);
bool isInside(int, int) override;
void touch(int, int, Action) override;
void draw(uint8 alpha) override;
/* Functions implementations */
struct FunctionBehavior {
void (*touchToState)(int, int, TouchControls::FunctionState &);
bool pressOnRelease;
float xRatio;
float yRatio;
uint32 button;
};
static FunctionBehavior functionBehaviors[TouchControls::kFunctionMax + 1];
static void touchToJoystickState(int dX, int dY, FunctionState &state);
static void touchToCenterState(int dX, int dY, FunctionState &state);
static void touchToRightState(int dX, int dY, FunctionState &state);
};
#endif

90
dists/android/gamepad.svg Normal file
View File

@ -0,0 +1,90 @@
<svg width="384" height="256" version="1.1" viewBox="0 0 384 256" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(64,64)">
<path d="m62 0a62 62 0 0 1-62 62 62 62 0 0 1-62-62 62 62 0 0 1 62-62 62 62 0 0 1 62 62z" fill="#808080" fill-opacity=".4"/>
<g fill-opacity=".4" fill-rule="evenodd" stroke="#808080" stroke-opacity=".8">
<path d="m-18-48h36v27l-18 18-18-18z"/>
<path d="m48-18v36h-27l-18-18 18-18z"/>
<path d="m18 48h-36v-27l18-18 18 18z"/>
<path d="m-48 18v-36h27l18 18-18 18z"/>
</g>
<g fill="none" fill-rule="evenodd" stroke="#808080" stroke-opacity=".2">
<path d="m-16-56 16-4 16 4"/>
<path d="m56-16 4 16-4 16"/>
<path d="m16 56-16 4-16-4"/>
<path d="m-56 16-4-16 4-16"/>
</g>
</g>
<g transform="translate(32,160)" fill="#fff" fill-rule="evenodd" stroke="#808080">
<path d="m-18-16h36v27l-18 18-18-18z"/>
<path d="m80-18v36h-27l-18-18 18-18z"/>
<path d="m18 80h-36v-27l18-18 18 18z"/>
<path d="m48 82v-36h27l18 18-18 18z"/>
</g>
<g transform="translate(32,160)" fill="none" fill-rule="evenodd" stroke="#fff" stroke-width="2">
<path d="m-16-24 16-4 16 4"/>
<path d="m88-16 4 16-4 16"/>
<path d="m16 88-16 4-16-4"/>
<path d="m40 80-4-16 4-16"/>
</g>
<g transform="translate(192,64)">
<path d="m62 0a62 62 0 0 1-62 62 62 62 0 0 1-62-62 62 62 0 0 1 62-62 62 62 0 0 1 62 62z" fill="#808080" fill-opacity=".4"/>
<g transform="translate(0,-36)">
<path d="m24 0a24 24 0 0 1-24 24 24 24 0 0 1-24-24 24 24 0 0 1 24-24 24 24 0 0 1 24 24z" fill-opacity=".4" stroke="#808080" stroke-opacity=".8"/>
<path d="m3.3496 3.0547v11.945h-6.3164v-11.945l-9.9688-17.037h6.6387l6.4453 12.16 6.5312-12.16h6.6387z" fill="#ffd733" aria-label="Y"/>
</g>
<g transform="translate(36)">
<path d="m24 0a24 24 0 0 1-24 24 24 24 0 0 1-24-24 24 24 0 0 1 24-24 24 24 0 0 1 24 24z" fill-opacity=".4" stroke="#808080" stroke-opacity=".8"/>
<path d="m12.48 6.7285q0 3.8887-2.6641 6.0801-2.6641 2.1914-7.3691 2.1914h-12.504v-28.982h11.279q4.6836 0 7.0898 1.8906 2.4277 1.8691 2.4277 5.3926 0 2.4707-1.418 4.168-1.418 1.6758-4.3613 2.2988 3.6738 0.38672 5.5859 2.1914 1.9336 1.8047 1.9336 4.7695zm-8.1211-12.568q0-3.4375-3.5879-3.4375h-4.4902v6.8535h4.5332q3.5449 0 3.5449-3.416zm1.7402 12.031q0-1.8906-1.1387-2.9004-1.1387-1.0312-3.3086-1.0312h-5.3711v8.0352h5.5215q4.2969 0 4.2969-4.1035z" fill="#f33" aria-label="B"/>
</g>
<g transform="translate(0,36)">
<path d="m24 0a24 24 0 0 1-24 24 24 24 0 0 1-24-24 24 24 0 0 1 24-24 24 24 0 0 1 24 24z" fill-opacity=".4" stroke="#808080" stroke-opacity=".8"/>
<path d="m13.404 15h-6.3164l-1.9766-7.0898h-9.7969l-1.9766 7.0898h-6.3379l9.5176-28.982h7.3906zm-13.191-25.072q-0.17188 0.90234-0.64453 2.707-0.45117 1.7832-2.9219 10.506h7.1328q-2.5137-8.8516-2.9648-10.592-0.42969-1.7402-0.60156-2.6211z" fill="#3f3" aria-label="A"/>
</g>
<g transform="translate(-36)">
<path d="m24 0a24 24 0 0 1-24 24 24 24 0 0 1-24-24 24 24 0 0 1 24-24 24 24 0 0 1 24 24z" fill-opacity=".4" stroke="#808080" stroke-opacity=".8"/>
<path d="m6.7441 15-6.5098-10.592-6.5312 10.592h-6.7031l9.8613-15.275-8.9805-13.707h6.7031l5.6504 9.2383 5.6289-9.2383h6.6602l-9.1953 13.707 10.076 15.275z" fill="#33f" aria-label="X"/>
</g>
</g>
<g transform="translate(160,160)" stroke="#808080">
<path d="m24-4a24 24 0 0 1-24 24 24 24 0 0 1-24-24 24 24 0 0 1 24-24 24 24 0 0 1 24 24z" fill="#ffd733"/>
<path d="m92 0a24 24 0 0 1-24 24 24 24 0 0 1-24-24 24 24 0 0 1 24-24 24 24 0 0 1 24 24z" fill="#f33"/>
<path d="m24 68a24 24 0 0 1-24 24 24 24 0 0 1-24-24 24 24 0 0 1 24-24 24 24 0 0 1 24 24z" fill="#3f3"/>
<path d="m84 64a24 24 0 0 1-24 24 24 24 0 0 1-24-24 24 24 0 0 1 24-24 24 24 0 0 1 24 24z" fill="#33f"/>
</g>
<g transform="translate(320,64)">
<path d="m62 0a62 62 0 0 1-62 62 62 62 0 0 1-62-62 62 62 0 0 1 62-62 62 62 0 0 1 62 62z" fill="#808080" fill-opacity=".4"/>
<g transform="translate(0,-36)">
<path d="m24 0a24 24 0 0 1-24 24 24 24 0 0 1-24-24 24 24 0 0 1 24-24 24 24 0 0 1 24 24z" fill-opacity=".4" stroke="#808080" stroke-opacity=".8"/>
<g transform="translate(-16,-20)" stroke-width=".080658">
<path d="m29.088 19.268c1.9849 2.3662 2.7235 6.6713 2.4405 8.9046-0.42208 3.3316-1.9359 6.5163-4.4324 8.3948-2.5977 1.9549-5.7752 3.1121-9.741 3.3791-5.9662 0.40054-10.606-1.4999-13.691-4.3769-1.5813-1.4765-4.0645-3.5962-3.0067-6.6043 0.54452-1.5481 2.5441-3.4614 3.8371-4.7854 0.46435-0.47556 1.3787-1.3564 1.279-1.9332-0.22891-0.97692-1.7271-2.4455-2.3604-3.4873-1.8986-2.9765-2.3227-7.4998-0.65171-11.259 1.9924-4.4807 7.0057-7.3285 13.861-7.0827 2.838 0.10179 6.2954 1.0368 8.6715 2.4034 3.3942 1.9507 6.1567 4.2012 5.8079 6.6797-0.21439 1.5229-1.346 3.2141-2.6848 4.8449-0.83037 1.0115-1.216 1.6294-1.1682 2.2229 0.04065 0.5037 1.2677 2.0168 1.8397 2.6986" opacity=".3"/>
<path d="m28.685 18.865c1.9849 2.3662 2.7235 6.6713 2.4405 8.9046-0.42208 3.3316-1.9359 6.5163-4.4325 8.3948-2.5977 1.9549-5.7752 3.1121-9.741 3.3791-5.9662 0.40054-10.606-1.4999-13.691-4.3769-1.5813-1.4765-4.0645-3.5962-3.0067-6.6043 0.54452-1.5481 2.5441-3.4614 3.8371-4.7854 0.46434-0.47556 1.3787-1.3564 1.279-1.9332-0.22891-0.97692-1.7271-2.4455-2.3604-3.4873-1.8986-2.9765-2.3227-7.4998-0.65171-11.259 1.9924-4.4807 7.0057-7.3285 13.861-7.0827 2.838 0.10179 6.2954 1.0368 8.6715 2.4034 3.3942 1.9507 6.1567 4.2012 5.8079 6.6797-0.21439 1.5229-1.346 3.2141-2.6848 4.8449-0.83037 1.0115-1.216 1.6294-1.1682 2.2229 0.04065 0.50371 1.2677 2.0168 1.8397 2.6986" fill="#333"/>
<g fill="#00721d">
<path d="m27.778 8.29-2.7389 4.149v0.0062c-0.04291 0.06107-0.10136 0.12276-0.15207 0.16752-0.14412 0.12258-0.32261 0.1914-0.50195 0.1914-0.11486 0-0.2382-0.03322-0.34813-0.08605v0.0036c-0.06298-0.03098-0.12371-0.07051-0.18103-0.11943-0.03436-0.02928-0.68677-0.58475-1.4468-1.0681-0.71729-0.45475-1.4261-0.86126-2.1151-1.1924l-1.61e-4 0.01155c-0.03923-0.01889-0.07858-0.03746-0.11774-0.056-0.7289-0.34166-1.4096-0.59904-2.0434-0.76987-0.63373-0.17091-1.1877-0.25736-1.6625-0.25736-0.48643 0-0.86336 0.10201-1.1305 0.30576v-0.011706l-0.01598 0.00902c-0.02299 0.020245-0.09185 0.095192-0.11298 0.11721-0.26932 0.2802-0.40351 0.63779-0.40351 1.0739v1.4005c0-0.43603 0.13421-0.79354 0.40351-1.0737 0.02113-0.02202 0.1063-0.09001 0.12929-0.11025v-0.0046c0.26711-0.20359 0.64394-0.30561 1.1302-0.30561 0.47483 0 1.0288 0.08653 1.6625 0.25736 0.63373 0.17083 1.3145 0.42829 2.0434 0.77004 0.03554 0.01668 0.07121 0.03382 0.10682 0.05093v0.0043c0.68898 0.3311 1.4088 0.72664 2.1261 1.1815 0.76004 0.48322 1.4125 1.0389 1.4468 1.0681 0.05712 0.04875 0.11764 0.08817 0.1804 0.11912v0.0016c0.10994 0.05291 0.23391 0.08115 0.34876 0.08115 0.17752 0 0.35166-0.06213 0.49483-0.18191v0.0046c0.05493-0.04847 0.13898-0.13768 0.18246-0.20358l-1.61e-4 -0.01234 2.7157-4.1208z"/>
<path d="m17.754 26.593v1.4005c0-0.59122-0.32594-1.1509-0.97418-1.6807-0.6497-0.52903-1.4576-1.0809-2.4238-1.6578-0.9662-0.57549-2.0116-1.1974-3.136-1.868-1.1281-0.67204-2.1799-1.4619-3.1352-2.357-0.96692-0.90215-1.7741-1.938-2.4238-3.1047-0.64897-1.1674-0.97346-2.5135-0.97346-4.0391v-1.4006c0 1.5256 0.32448 2.8716 0.97346 4.0391 0.64978 1.1667 1.4569 2.2026 2.4238 3.1048 0.95531 0.89498 2.0072 1.6849 3.1352 2.3569 1.1244 0.67067 2.1698 1.2926 3.136 1.868 0.9662 0.57694 1.7741 1.1289 2.4238 1.6578 0.64824 0.52968 0.97418 1.0895 0.97418 1.6807"/>
<path d="m27.969 25.612c0 1.6807-0.35639 3.1362-1.0693 4.365-0.5254 0.90683-1.1667 1.6945-1.9242 2.3641v1.4004c0.75754-0.66946 1.3988-1.4569 1.9242-2.3637 0.71285-1.2289 1.0693-2.6843 1.0693-4.365zm-24.755 3.3622v1.4005c1.584 2.1476 3.3654 3.7511 5.3449 4.8092 0.11731 0.06258 0.23654 0.12295 0.35715 0.18191l-6.453e-4 0.03069c1.9013 0.91345 4.2758 1.3743 6.9847 1.3743 1.4562 0 2.9058-0.19509 4.3475-0.58338 1.4401-0.38958 2.7323-0.9808 3.8719-1.7743 0.29988-0.20882 0.58502-0.43274 0.85529-0.67164v-1.4005c-0.27028 0.23891-0.55542 0.46274-0.85529 0.67164-1.1396 0.79351-2.4317 1.3846-3.8719 1.7743-1.4417 0.3882-2.8913 0.58338-4.3475 0.58338-2.7089 0-5.057-0.50562-6.9582-1.4191l-4.84e-4 0.02642c-0.12932-0.062824-0.25693-0.12736-0.38247-0.19441-1.9796-1.058-3.761-2.6616-5.3449-4.809z"/>
</g>
<path d="m15.235 2.827c2.5335 0 4.8063 0.45967 6.8178 1.3776 2.0115 0.91861 3.92 2.2805 5.7253 4.0849l-2.7331 4.1463c-0.15898 0.24133-0.40611 0.36772-0.65994 0.36772-0.18309 0-0.36973-0.06582-0.52911-0.2018-0.03436-0.02928-0.68672-0.58477-1.4468-1.0681-0.76012-0.48185-1.5048-0.8936-2.2329-1.2368-0.72882-0.34167-1.4097-0.59904-2.0435-0.76988-0.63372-0.17091-1.1876-0.25738-1.6624-0.25738-0.57049 0-0.9908 0.1401-1.2594 0.42031-0.26932 0.28028-0.40361 0.63776-0.40361 1.0738 0 0.55976 0.32448 1.0817 0.9741 1.5642 0.64824 0.48257 1.4643 0.98007 2.4463 1.4941 1.0628 0.55831 2.1169 1.1339 3.16 1.7272 1.1237 0.6384 2.1769 1.409 3.1592 2.3112 0.98217 0.90361 1.7981 1.9852 2.4478 3.2456 0.64897 1.2604 0.97418 2.7624 0.97418 4.5052 0 1.6807-0.35643 3.1363-1.0693 4.3651-0.71285 1.2303-1.6391 2.2412-2.7795 3.0355-1.1397 0.79351-2.4318 1.3847-3.872 1.7744-1.4417 0.38812-2.8913 0.58332-4.3475 0.58332-2.9146 0-5.3616-0.52976-7.3412-1.587-1.9796-1.0581-3.761-2.6615-5.3449-4.809l3.3755-4.1964c0.16164-0.2006 0.38127-0.30174 0.60211-0.30174 0.20648 0 0.41401 0.08848 0.57606 0.26673 0.019358 0.02129 0.38764 0.42603 1.0533 0.94006 0.67365 0.51968 1.3981 0.97289 2.1618 1.3533 0.76294 0.38466 1.5571 0.70487 2.3752 0.95724 0.80795 0.2502 1.56 0.37385 2.2569 0.37385 0.69616 0 1.2275-0.12437 1.5919-0.37385 0.36368-0.24883 0.54654-0.71559 0.54654-1.4005 0-0.59122-0.32594-1.151-0.97418-1.6807-0.64962-0.52895-1.4576-1.0809-2.4238-1.6578-0.9662-0.57541-2.0115-1.1974-3.136-1.868-1.1281-0.67196-2.1799-1.4619-3.1352-2.3569-0.96692-0.90223-1.7741-1.9381-2.4238-3.1048-0.64897-1.1674-0.97346-2.5135-0.97346-4.0391 0-1.4942 0.28456-2.801 0.85513-3.9212 0.56985-1.121 1.3379-2.0618 2.3041-2.8253 0.9662-0.76205 2.0827-1.3375 3.3494-1.7271 1.2667-0.38893 2.6133-0.58412 4.039-0.58412" fill="#00c832"/>
</g>
</g>
<g transform="translate(36)">
<path d="m24 0a24 24 0 0 1-24 24 24 24 0 0 1-24-24 24 24 0 0 1 24-24 24 24 0 0 1 24 24z" fill-opacity=".4" stroke="#808080" stroke-opacity=".8"/>
<path d="m-6.0156 10.5-4.5312-8.0469h-2.7031v8.0469h-4.6094v-21.078h7.7188q4.125 0 6.1719 1.6094t2.0469 4.6406q0 2.1406-1.1562 3.6406t-3.2031 2.1094l5.4531 9.0781zm-0.54688-14.641q0-1.4688-0.96875-2.1562-0.96875-0.70312-3.0938-0.70312h-2.625v5.875h2.75q3.9375 0 3.9375-3.0156zm23.969 8.5312q0 3-2.2969 4.7188-2.2812 1.7031-6.3594 1.7031-3.6719 0-5.9844-1.5469t-2.9688-4.5l4.4531-0.57812q0.35938 1.4688 1.5 2.3281 1.1406 0.84375 3.125 0.84375 2.0625 0 3.0469-0.65625 1-0.65625 1-2.0469 0-1.0312-0.875-1.7344-0.875-0.71875-2.5469-1.125-3.3594-0.8125-4.75-1.4062-1.375-0.60938-2.2031-1.3594t-1.2812-1.75q-0.4375-1.0156-0.4375-2.2656 0-2.7344 2.1875-4.3281 2.2031-1.5938 5.7969-1.5938 3.5 0 5.4688 1.3906 1.9688 1.375 2.5469 4.2812l-4.4688 0.45312q-0.6875-2.8594-3.6406-2.8594-1.6562 0-2.5312 0.625-0.875 0.60938-0.875 1.7656 0 0.76562 0.42188 1.2812 0.42188 0.5 1.1406 0.85938 0.73438 0.34375 2.9688 0.92188 2.9375 0.71875 4.5156 1.6094 1.5781 0.875 2.3125 2.125t0.73438 2.8438z" fill="#c0c0c0" aria-label="RS"/>
</g>
<g transform="translate(0,36)">
<path d="m24 0a24 24 0 0 1-24 24 24 24 0 0 1-24-24 24 24 0 0 1 24-24 24 24 0 0 1 24 24z" fill-opacity=".4" stroke="#808080" stroke-opacity=".8"/>
<path d="m-11.812-0.1875c0.32476-20.46 0.32476-20.46 17.881-9.9487 17.556 10.511 17.556 10.511-0.32476 20.46-17.881 9.9487-17.881 9.9487-17.556-10.511z" fill="#c0c0c0" fill-rule="evenodd"/>
</g>
<g transform="translate(-36)">
<path d="m24 0a24 24 0 0 1-24 24 24 24 0 0 1-24-24 24 24 0 0 1 24-24 24 24 0 0 1 24 24z" fill-opacity=".4" stroke="#808080" stroke-opacity=".8"/>
<path d="m-16.703 10.5v-21.078h4.6094v17.516h9.625v3.5625zm34.109-6.1094q0 3-2.2969 4.7188-2.2812 1.7031-6.3594 1.7031-3.6719 0-5.9844-1.5469t-2.9688-4.5l4.4531-0.57812q0.35938 1.4688 1.5 2.3281 1.1406 0.84375 3.125 0.84375 2.0625 0 3.0469-0.65625 1-0.65625 1-2.0469 0-1.0312-0.875-1.7344-0.875-0.71875-2.5469-1.125-3.3594-0.8125-4.75-1.4062-1.375-0.60938-2.2031-1.3594t-1.2812-1.75q-0.4375-1.0156-0.4375-2.2656 0-2.7344 2.1875-4.3281 2.2031-1.5938 5.7969-1.5938 3.5 0 5.4688 1.3906 1.9688 1.375 2.5469 4.2812l-4.4688 0.45312q-0.6875-2.8594-3.6406-2.8594-1.6562 0-2.5312 0.625-0.875 0.60938-0.875 1.7656 0 0.76562 0.42188 1.2812 0.42188 0.5 1.1406 0.85938 0.73438 0.34375 2.9688 0.92188 2.9375 0.71875 4.5156 1.6094 1.5781 0.875 2.3125 2.125t0.73438 2.8438z" fill="#c0c0c0" aria-label="LS"/>
</g>
</g>
<g transform="translate(288,160)" stroke="#808080">
<path d="m24-4a24 24 0 0 1-24 24 24 24 0 0 1-24-24 24 24 0 0 1 24-24 24 24 0 0 1 24 24z" fill="#00c832"/>
<g fill="#c0c0c0">
<path d="m92 0a24 24 0 0 1-24 24 24 24 0 0 1-24-24 24 24 0 0 1 24-24 24 24 0 0 1 24 24z"/>
<path d="m24 68a24 24 0 0 1-24 24 24 24 0 0 1-24-24 24 24 0 0 1 24-24 24 24 0 0 1 24 24z"/>
<path d="m84 64a24 24 0 0 1-24 24 24 24 0 0 1-24-24 24 24 0 0 1 24-24 24 24 0 0 1 24 24z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB