mirror of
https://github.com/RetroDECK/ES-DE.git
synced 2025-01-30 03:55:40 +00:00
Added support for sizing SVG images arbitrarily (overriding the image aspect ratio by stretching and squashing).
This commit is contained in:
parent
41def158bb
commit
039c27fa8e
|
@ -35,7 +35,7 @@ ImageComponent::ImageComponent(bool forceLoad, bool dynamic)
|
||||||
, mFlipX {false}
|
, mFlipX {false}
|
||||||
, mFlipY {false}
|
, mFlipY {false}
|
||||||
, mTargetIsMax {false}
|
, mTargetIsMax {false}
|
||||||
, mTargetIsMin {false}
|
, mScalableNonAspect {false}
|
||||||
, mTileWidth {0.0f}
|
, mTileWidth {0.0f}
|
||||||
, mTileHeight {0.0f}
|
, mTileHeight {0.0f}
|
||||||
, mColorShift {0xFFFFFFFF}
|
, mColorShift {0xFFFFFFFF}
|
||||||
|
@ -68,20 +68,15 @@ void ImageComponent::resize(bool rasterize)
|
||||||
mSize = mTargetSize;
|
mSize = mTargetSize;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// SVG rasterization is determined by height and rasterization is done in terms of pixels.
|
|
||||||
// If rounding is off enough in the rasterization step (for images with extreme aspect
|
|
||||||
// ratios), it can cause cutoff when the aspect ratio breaks. So we always make sure to
|
|
||||||
// round accordingly to avoid such issues.
|
|
||||||
if (mTargetIsMax) {
|
if (mTargetIsMax) {
|
||||||
|
// Maintain image aspect ratio.
|
||||||
mSize = textureSize;
|
mSize = textureSize;
|
||||||
|
glm::vec2 resizeScale {mTargetSize.x / mSize.x, mTargetSize.y / mSize.y};
|
||||||
glm::vec2 resizeScale {(mTargetSize.x / mSize.x), (mTargetSize.y / mSize.y)};
|
|
||||||
|
|
||||||
if (resizeScale.x < resizeScale.y) {
|
if (resizeScale.x < resizeScale.y) {
|
||||||
// This will be mTargetSize.x. We can't exceed it, nor be lower than it.
|
// This will be mTargetSize.x. We can't exceed it, nor be lower than it.
|
||||||
mSize.x *= resizeScale.x;
|
mSize.x *= resizeScale.x;
|
||||||
// We need to make sure we're not creating an image larger than max size.
|
mSize.y = std::min(mSize.y * resizeScale.x, mTargetSize.y);
|
||||||
mSize.y = floorf(std::min(mSize.y * resizeScale.x, mTargetSize.y));
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// This will be mTargetSize.y(). We can't exceed it.
|
// This will be mTargetSize.y(). We can't exceed it.
|
||||||
|
@ -89,34 +84,11 @@ void ImageComponent::resize(bool rasterize)
|
||||||
mSize.x = std::min((mSize.y / textureSize.y) * textureSize.x, mTargetSize.x);
|
mSize.x = std::min((mSize.y / textureSize.y) * textureSize.x, mTargetSize.x);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (mTargetIsMin) {
|
|
||||||
mSize = textureSize;
|
|
||||||
|
|
||||||
glm::vec2 resizeScale {(mTargetSize.x / mSize.x), (mTargetSize.y / mSize.y)};
|
|
||||||
|
|
||||||
if (resizeScale.x > resizeScale.y) {
|
|
||||||
mSize.x *= resizeScale.x;
|
|
||||||
mSize.y *= resizeScale.x;
|
|
||||||
|
|
||||||
float cropPercent {(mSize.y - mTargetSize.y) / (mSize.y * 2.0f)};
|
|
||||||
crop(0.0f, cropPercent, 0.0f, cropPercent);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
mSize.x *= resizeScale.y;
|
|
||||||
mSize.y *= resizeScale.y;
|
|
||||||
|
|
||||||
float cropPercent {(mSize.x - mTargetSize.x) / (mSize.x * 2.0f)};
|
|
||||||
crop(cropPercent, 0.0f, cropPercent, 0.0f);
|
|
||||||
}
|
|
||||||
mSize.y = std::max(mSize.y, mTargetSize.y);
|
|
||||||
mSize.x = std::max((mSize.y / textureSize.y) * textureSize.x, mTargetSize.x);
|
|
||||||
}
|
|
||||||
else {
|
else {
|
||||||
// If both components are set, we just stretch.
|
// If both axes are set we just stretch or squash, if no axes are set we do nothing.
|
||||||
// If no components are set, we don't resize at all.
|
|
||||||
mSize = mTargetSize == glm::vec2 {0.0f, 0.0f} ? textureSize : mTargetSize;
|
mSize = mTargetSize == glm::vec2 {0.0f, 0.0f} ? textureSize : mTargetSize;
|
||||||
|
|
||||||
// If only one component is set, we resize in a way that maintains aspect ratio.
|
// If only one axis is set, we resize in a way that maintains aspect ratio.
|
||||||
if (!mTargetSize.x && mTargetSize.y) {
|
if (!mTargetSize.x && mTargetSize.y) {
|
||||||
mSize.y = mTargetSize.y;
|
mSize.y = mTargetSize.y;
|
||||||
mSize.x = (mSize.y / textureSize.y) * textureSize.x;
|
mSize.x = (mSize.y / textureSize.y) * textureSize.x;
|
||||||
|
@ -181,6 +153,7 @@ void ImageComponent::setImage(const std::string& path, bool tile)
|
||||||
mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic, mLinearInterpolation,
|
mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic, mLinearInterpolation,
|
||||||
static_cast<size_t>(mSize.x),
|
static_cast<size_t>(mSize.x),
|
||||||
static_cast<size_t>(mSize.y), mTileWidth, mTileHeight);
|
static_cast<size_t>(mSize.y), mTileWidth, mTileHeight);
|
||||||
|
mTexture->setScalableNonAspect(mScalableNonAspect);
|
||||||
mTexture->rasterizeAt(mSize.x, mSize.y);
|
mTexture->rasterizeAt(mSize.x, mSize.y);
|
||||||
onSizeChanged();
|
onSizeChanged();
|
||||||
}
|
}
|
||||||
|
@ -211,7 +184,6 @@ void ImageComponent::setResize(float width, float height)
|
||||||
{
|
{
|
||||||
mTargetSize = glm::vec2 {width, height};
|
mTargetSize = glm::vec2 {width, height};
|
||||||
mTargetIsMax = false;
|
mTargetIsMax = false;
|
||||||
mTargetIsMin = false;
|
|
||||||
resize();
|
resize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,7 +191,6 @@ void ImageComponent::setResize(float width, float height, bool rasterize)
|
||||||
{
|
{
|
||||||
mTargetSize = glm::vec2 {width, height};
|
mTargetSize = glm::vec2 {width, height};
|
||||||
mTargetIsMax = false;
|
mTargetIsMax = false;
|
||||||
mTargetIsMin = false;
|
|
||||||
resize(rasterize);
|
resize(rasterize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,7 +198,6 @@ void ImageComponent::setMaxSize(const float width, const float height)
|
||||||
{
|
{
|
||||||
mTargetSize = glm::vec2 {width, height};
|
mTargetSize = glm::vec2 {width, height};
|
||||||
mTargetIsMax = true;
|
mTargetIsMax = true;
|
||||||
mTargetIsMin = false;
|
|
||||||
resize();
|
resize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -460,12 +430,7 @@ void ImageComponent::render(const glm::mat4& parentTrans)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
glm::mat4 trans {parentTrans * getTransform()};
|
glm::mat4 trans {parentTrans * getTransform()};
|
||||||
|
mRenderer->setMatrix(trans, false);
|
||||||
// Don't round vertices if scaled as it may lead to single-pixel alignment issues.
|
|
||||||
if (mScale == 1.0f)
|
|
||||||
mRenderer->setMatrix(trans, true);
|
|
||||||
else
|
|
||||||
mRenderer->setMatrix(trans, false);
|
|
||||||
|
|
||||||
if (mTexture && mOpacity > 0.0f) {
|
if (mTexture && mOpacity > 0.0f) {
|
||||||
if (Settings::getInstance()->getBool("DebugImage")) {
|
if (Settings::getInstance()->getBool("DebugImage")) {
|
||||||
|
@ -567,6 +532,8 @@ void ImageComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
|
||||||
getParent()->getSize() :
|
getParent()->getSize() :
|
||||||
glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight())};
|
glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight())};
|
||||||
|
|
||||||
|
bool noMax {false};
|
||||||
|
|
||||||
if (properties & ThemeFlags::SIZE) {
|
if (properties & ThemeFlags::SIZE) {
|
||||||
if (elem->has("size")) {
|
if (elem->has("size")) {
|
||||||
glm::vec2 imageSize {elem->get<glm::vec2>("size")};
|
glm::vec2 imageSize {elem->get<glm::vec2>("size")};
|
||||||
|
@ -581,6 +548,8 @@ void ImageComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
|
||||||
if (imageSize.y > 0.0f)
|
if (imageSize.y > 0.0f)
|
||||||
imageSize.y = glm::clamp(imageSize.y, 0.001f, 2.0f);
|
imageSize.y = glm::clamp(imageSize.y, 0.001f, 2.0f);
|
||||||
setResize(imageSize * scale);
|
setResize(imageSize * scale);
|
||||||
|
if (imageSize.x != 0.0f && imageSize.y != 0.0f)
|
||||||
|
noMax = true;
|
||||||
}
|
}
|
||||||
else if (elem->has("maxSize")) {
|
else if (elem->has("maxSize")) {
|
||||||
glm::vec2 imageMaxSize {elem->get<glm::vec2>("maxSize")};
|
glm::vec2 imageMaxSize {elem->get<glm::vec2>("maxSize")};
|
||||||
|
@ -610,8 +579,15 @@ void ImageComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
|
||||||
setDefaultImage(elem->get<std::string>("default"));
|
setDefaultImage(elem->get<std::string>("default"));
|
||||||
|
|
||||||
if (properties & PATH && elem->has("path")) {
|
if (properties & PATH && elem->has("path")) {
|
||||||
bool tile {elem->has("tile") && elem->get<bool>("tile")};
|
const bool tile {elem->has("tile") && elem->get<bool>("tile")};
|
||||||
setImage(elem->get<std::string>("path"), tile);
|
const std::string path {elem->get<std::string>("path")};
|
||||||
|
|
||||||
|
if (!theme->isLegacyTheme() && noMax && path.length() > 4 &&
|
||||||
|
Utils::String::toLower(path.substr(path.size() - 4, std::string::npos)) == ".svg") {
|
||||||
|
mScalableNonAspect = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
setImage(path, tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (properties && elem->has("imageType")) {
|
if (properties && elem->has("imageType")) {
|
||||||
|
|
|
@ -118,7 +118,7 @@ private:
|
||||||
bool mFlipX;
|
bool mFlipX;
|
||||||
bool mFlipY;
|
bool mFlipY;
|
||||||
bool mTargetIsMax;
|
bool mTargetIsMax;
|
||||||
bool mTargetIsMin;
|
bool mScalableNonAspect;
|
||||||
|
|
||||||
float mTileWidth;
|
float mTileWidth;
|
||||||
float mTileHeight;
|
float mTileHeight;
|
||||||
|
|
|
@ -35,6 +35,7 @@ TextureData::TextureData(bool tile)
|
||||||
, mSourceWidth {0.0f}
|
, mSourceWidth {0.0f}
|
||||||
, mSourceHeight {0.0f}
|
, mSourceHeight {0.0f}
|
||||||
, mScalable {false}
|
, mScalable {false}
|
||||||
|
, mScalableNonAspect {false}
|
||||||
, mHasRGBAData {false}
|
, mHasRGBAData {false}
|
||||||
, mPendingRasterization {false}
|
, mPendingRasterization {false}
|
||||||
, mLinearMagnify {false}
|
, mLinearMagnify {false}
|
||||||
|
@ -107,17 +108,31 @@ bool TextureData::initSVGFromMemory(const std::string& fileData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rasterize) {
|
if (rasterize) {
|
||||||
|
float height {static_cast<float>(mHeight)};
|
||||||
|
const float aspectW {svgImage->width / static_cast<float>(mWidth)};
|
||||||
|
const float aspectH {svgImage->height / static_cast<float>(mHeight)};
|
||||||
|
|
||||||
|
if (mScalableNonAspect && aspectW != aspectH) {
|
||||||
|
// If the size property has been used to override the aspect ratio of the SVG image,
|
||||||
|
// then we need to rasterize at a lower resolution and let the GPU scale the texture.
|
||||||
|
// This is necessary as the rasterization always maintains the image aspect ratio.
|
||||||
|
mWidth = static_cast<int>(std::round(svgImage->width / aspectH));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// For very wide and short images we need to check that the target width is enough
|
||||||
|
// to fit the rasterized image, if not we'll decrease the height as needed.
|
||||||
|
const float requiredWidth {height * (svgImage->width / svgImage->height)};
|
||||||
|
if (std::round(requiredWidth) > static_cast<float>(mWidth))
|
||||||
|
height = std::floor(height * (static_cast<float>(mWidth) / requiredWidth));
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<unsigned char> tempVector;
|
std::vector<unsigned char> tempVector;
|
||||||
tempVector.reserve(mWidth * mHeight * 4);
|
tempVector.reserve(mWidth * mHeight * 4);
|
||||||
|
|
||||||
NSVGrasterizer* rast {nsvgCreateRasterizer()};
|
NSVGrasterizer* rast {nsvgCreateRasterizer()};
|
||||||
|
|
||||||
// Compensate for rounding losses for a slightly more accurate rasterization.
|
nsvgRasterize(rast, svgImage, 0, 0, height / svgImage->height, tempVector.data(), mWidth,
|
||||||
const float compScale {(static_cast<float>(mHeight) / svgImage->height) *
|
mHeight, mWidth * 4);
|
||||||
(mSourceHeight / static_cast<float>(mHeight))};
|
|
||||||
|
|
||||||
nsvgRasterize(rast, svgImage, 0.0f, 0.0f, compScale, tempVector.data(), mWidth, mHeight,
|
|
||||||
mWidth * 4);
|
|
||||||
|
|
||||||
nsvgDeleteRasterizer(rast);
|
nsvgDeleteRasterizer(rast);
|
||||||
|
|
||||||
|
|
|
@ -64,8 +64,12 @@ public:
|
||||||
}
|
}
|
||||||
glm::vec2 getSize() { return glm::vec2 {static_cast<int>(mWidth), static_cast<int>(mHeight)}; }
|
glm::vec2 getSize() { return glm::vec2 {static_cast<int>(mWidth), static_cast<int>(mHeight)}; }
|
||||||
|
|
||||||
|
// Whether to stretch or squash SVG images if the size property has been used to override
|
||||||
|
// the aspect ratio (accomplished by rasterizing at a lower resolution and letting the GPU
|
||||||
|
// scale the texture).
|
||||||
|
void setScalableNonAspect(bool state) { mScalableNonAspect = state; }
|
||||||
// Whether to use linear filtering when magnifying the texture.
|
// Whether to use linear filtering when magnifying the texture.
|
||||||
void setLinearMagnify(bool setting) { mLinearMagnify = setting; }
|
void setLinearMagnify(bool state) { mLinearMagnify = state; }
|
||||||
|
|
||||||
// Has the image been loaded but not yet been rasterized as the size was not known?
|
// Has the image been loaded but not yet been rasterized as the size was not known?
|
||||||
const bool getPendingRasterization() { return mPendingRasterization; }
|
const bool getPendingRasterization() { return mPendingRasterization; }
|
||||||
|
@ -90,6 +94,7 @@ private:
|
||||||
std::atomic<float> mSourceWidth;
|
std::atomic<float> mSourceWidth;
|
||||||
std::atomic<float> mSourceHeight;
|
std::atomic<float> mSourceHeight;
|
||||||
std::atomic<bool> mScalable;
|
std::atomic<bool> mScalable;
|
||||||
|
std::atomic<bool> mScalableNonAspect;
|
||||||
std::atomic<bool> mHasRGBAData;
|
std::atomic<bool> mHasRGBAData;
|
||||||
std::atomic<bool> mPendingRasterization;
|
std::atomic<bool> mPendingRasterization;
|
||||||
bool mLinearMagnify;
|
bool mLinearMagnify;
|
||||||
|
|
|
@ -23,6 +23,7 @@ TextureResource::TextureResource(const std::string& path,
|
||||||
bool scalable)
|
bool scalable)
|
||||||
: mTextureData {nullptr}
|
: mTextureData {nullptr}
|
||||||
, mForceLoad {false}
|
, mForceLoad {false}
|
||||||
|
, mScalableNonAspect {false}
|
||||||
{
|
{
|
||||||
// Create a texture data object for this texture.
|
// Create a texture data object for this texture.
|
||||||
if (!path.empty()) {
|
if (!path.empty()) {
|
||||||
|
@ -245,6 +246,8 @@ void TextureResource::rasterizeAt(float width, float height)
|
||||||
else
|
else
|
||||||
data = sTextureDataManager.get(this);
|
data = sTextureDataManager.get(this);
|
||||||
|
|
||||||
|
data->setScalableNonAspect(mScalableNonAspect);
|
||||||
|
|
||||||
if (mTextureData && mTextureData.get()->getScalable())
|
if (mTextureData && mTextureData.get()->getScalable())
|
||||||
mSourceSize = glm::vec2 {static_cast<float>(width), static_cast<float>(height)};
|
mSourceSize = glm::vec2 {static_cast<float>(width), static_cast<float>(height)};
|
||||||
data->setSourceSize(static_cast<float>(width), static_cast<float>(height));
|
data->setSourceSize(static_cast<float>(width), static_cast<float>(height));
|
||||||
|
|
|
@ -54,7 +54,8 @@ public:
|
||||||
return (mTextureData != nullptr ? mTextureData->getScalable() : false);
|
return (mTextureData != nullptr ? mTextureData->getScalable() : false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setLinearMagnify(bool setting) { mTextureData->setLinearMagnify(setting); }
|
void setScalableNonAspect(bool state) { mScalableNonAspect = state; }
|
||||||
|
void setLinearMagnify(bool state) { mTextureData->setLinearMagnify(state); }
|
||||||
|
|
||||||
std::string getTextureFilePath();
|
std::string getTextureFilePath();
|
||||||
|
|
||||||
|
@ -100,6 +101,7 @@ private:
|
||||||
glm::ivec2 mSize;
|
glm::ivec2 mSize;
|
||||||
glm::vec2 mSourceSize;
|
glm::vec2 mSourceSize;
|
||||||
bool mForceLoad;
|
bool mForceLoad;
|
||||||
|
bool mScalableNonAspect;
|
||||||
|
|
||||||
// File path, tile, linear interpolation, scalable/SVG, width, height.
|
// File path, tile, linear interpolation, scalable/SVG, width, height.
|
||||||
using TextureKeyType = std::tuple<std::string, bool, bool, bool, size_t, size_t>;
|
using TextureKeyType = std::tuple<std::string, bool, bool, bool, size_t, size_t>;
|
||||||
|
|
Loading…
Reference in a new issue