// SPDX-License-Identifier: MIT // // ES-DE Frontend // ImageComponent.cpp // // Handles images: loading, resizing, cropping, color shifting etc. // #include "components/ImageComponent.h" #include "Log.h" #include "Settings.h" #include "ThemeData.h" #include "Window.h" #include "resources/TextureResource.h" #include "utils/CImgUtil.h" #include "utils/LocalizationUtil.h" #include "utils/StringUtil.h" ImageComponent::ImageComponent(bool forceLoad, bool dynamic) : mRenderer {Renderer::getInstance()} , mTargetSize {0.0f, 0.0f} , mFlipX {false} , mFlipY {false} , mTargetIsMax {false} , mTargetIsCrop {false} , mCropPos {0.5f, 0.5f} , mCropOffset {0.0f, 0.0f} , mTileWidth {0.0f} , mTileHeight {0.0f} , mColorShift {0xFFFFFFFF} , mColorShiftEnd {0xFFFFFFFF} , mColorGradientHorizontal {true} , mFadeOpacity {0.0f} , mCornerRadius {0.0f} , mReflectionsFalloff {0.0f} , mCornerAntiAliasing {true} , mFading {false} , mForceLoad {forceLoad} , mDynamic {dynamic} , mRotateByTargetSize {false} , mLinearInterpolation {false} , mMipmapping {false} , mTileHorizontalAlignment {ALIGN_LEFT} , mTileVerticalAlignment {ALIGN_BOTTOM} , mTopLeftCrop {0.0f, 0.0f} , mBottomRightCrop {1.0f, 1.0f} , mClipRegion {0.0f, 0.0f, 0.0f, 0.0f} { updateColors(); } void ImageComponent::setImage(const std::string& path, bool tile) { // Always load bundled graphic resources statically, unless mForceLoad has been set. // This eliminates annoying texture pop-in problems that would otherwise occur. if (!mForceLoad && (path[0] == ':') && (path[1] == '/')) { mDynamic = false; } else if (Utils::FileSystem::isDirectory(path)) { LOG(LogError) << "ImageComponent: Path is a directory and not a file: \"" << path << "\""; return; } const bool isScalable { (path != "" && path.length() > 4) ? Utils::String::toLower(path.substr(path.size() - 4, std::string::npos)) == ".svg" : false}; // Create an initial blank texture if needed. if (path.empty() || !ResourceManager::getInstance().fileExists(path)) { if (mDefaultPath.empty() || !ResourceManager::getInstance().fileExists(mDefaultPath)) { mTexture.reset(); } else { mTexture = TextureResource::get(mDefaultPath, tile, mForceLoad, mDynamic, mLinearInterpolation, mMipmapping); resize(true); } } else { // For raster images we just load and resize but for SVG images we first need to resize // without rasterizing in order to calculate the correct image size. Then we delete and // reload the texture at the requested size in order to add a valid cache entry. Finally // we perform the actual rasterization to have the cache entry updated with the proper // texture. For SVG images this requires that every call to setImage is made only after // a call to setResize or setMaxSize (so the requested size is known upfront). if (isScalable) { mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic, mLinearInterpolation, mMipmapping, 0, 0, 0.0f, 0.0f); if (mTexture->getIsInvalidSVGFile()) { mTexture.reset(); } else { if (tile && (mTileWidth == 0.0f || mTileHeight == 0.0f)) setTileAxes(); resize(false); mTexture.reset(); mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic, mLinearInterpolation, mMipmapping, static_cast(mSize.x), static_cast(mSize.y), mTileWidth, mTileHeight); mTexture->rasterizeAt(mSize.x, mSize.y); if (mTargetIsCrop) coverFitCrop(); onSizeChanged(); } } else { mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic, mLinearInterpolation, mMipmapping, 0, 0, mTileWidth, mTileHeight); if (tile && (mTileWidth == 0.0f || mTileHeight == 0.0f)) setTileAxes(); resize(true); } } } void ImageComponent::setImage(const char* data, size_t length, bool tile) { mTexture.reset(); mTexture = TextureResource::get("", tile); mTexture->initFromMemory(data, length); resize(); } void ImageComponent::setImage(const std::shared_ptr& texture, bool resizeTexture) { mTexture = texture; if (resizeTexture) resize(); } void ImageComponent::setRawImage(const unsigned char* data, size_t width, size_t height) { mTexture.reset(); mTexture = TextureResource::get("", false, false, true, mLinearInterpolation); mTexture->initFromPixels(data, width, height); resize(); } void ImageComponent::setGameOverrideImage(const std::string& basename, const std::string& system) { if (mGameOverridePath == "") return; const std::string imageFilePath {mGameOverridePath + system + "/" + basename}; for (auto& extension : sSupportedOverrideExtensions) { if (Utils::FileSystem::exists(imageFilePath + extension)) { setImage(imageFilePath + extension); return; } } setImage(mGameOverrideOriginalPath); } void ImageComponent::setResize(const float width, const float height) { mTargetSize = glm::vec2 {width, height}; mTargetIsMax = false; mTargetIsCrop = false; resize(); } void ImageComponent::setResize(const glm::vec2& size, bool rasterize) { mTargetSize = size; mTargetIsMax = false; mTargetIsCrop = false; resize(rasterize); } void ImageComponent::setMaxSize(const float width, const float height) { mTargetSize = glm::vec2 {width, height}; mTargetIsMax = true; mTargetIsCrop = false; resize(); } void ImageComponent::setCroppedSize(const glm::vec2& size) { mTargetSize = size; mTargetIsMax = false; mTargetIsCrop = true; resize(); } void ImageComponent::cropLeft(const float value) { assert(value >= 0.0f && value <= 1.0f); mTopLeftCrop.x = value; } void ImageComponent::cropTop(const float value) { assert(value >= 0.0f && value <= 1.0f); mTopLeftCrop.y = value; } void ImageComponent::cropRight(const float value) { assert(value >= 0.0f && value <= 1.0f); mBottomRightCrop.x = 1.0f - value; } void ImageComponent::cropBottom(const float value) { assert(value >= 0.0f && value <= 1.0f); mBottomRightCrop.y = 1.0f - value; } void ImageComponent::crop(const float left, const float top, const float right, const float bottom) { cropLeft(left); cropTop(top); cropRight(right); cropBottom(bottom); } void ImageComponent::uncrop() { // Remove any applied crop. crop(0.0f, 0.0f, 0.0f, 0.0f); } void ImageComponent::coverFitCrop() { assert(mTargetIsCrop); mTopLeftCrop = {0.0f, 0.0f}; mBottomRightCrop = {1.0f, 1.0f}; mCropOffset = {0.0f, 0.0f}; if (std::round(mSize.y) > std::round(mTargetSize.y)) { const float cropSize {1.0f - (std::round(mTargetSize.y) / std::round(mSize.y))}; cropTop(cropSize / 2.0f); cropBottom(cropSize / 2.0f); mSize.y = mSize.y - (mSize.y * cropSize); if (mCropPos.y != 0.5f) { const float cropPosY {mCropPos.y + 0.5f}; mCropOffset.y = (cropSize * cropPosY) - cropSize; } } else { const float cropSize {1.0f - (std::round(mTargetSize.x) / std::round(mSize.x))}; cropLeft(cropSize / 2.0f); cropRight(cropSize / 2.0f); mSize.x = mSize.x - (mSize.x * cropSize); if (mCropPos.x != 0.5f) { const float cropPosX {mCropPos.x + 0.5f}; mCropOffset.x = (cropSize * cropPosX) - cropSize; } } } void ImageComponent::cropTransparentPadding(const float maxSizeX, const float maxSizeY) { if (mSize == glm::vec2 {0.0f, 0.0f}) return; std::vector imageBGRA {mTexture.get()->getRawRGBAData()}; if (imageBGRA.size() == 0) return; glm::ivec2 imageSize {mTexture.get()->getSize()}; cimg_library::CImg imageCImg(imageSize.x, imageSize.y, 1, 4, 0); int paddingCoords[4] {0, 0, 0, 0}; // We need to convert our RGBA data to the CImg internal format as CImg does not interleave // the pixels (as in BGRABGRABGRA). Utils::CImg::convertBGRAToCImg(imageBGRA, imageCImg); // This will give us the coordinates for the fully transparent areas. Utils::CImg::getTransparentPaddingCoords(imageCImg, paddingCoords); glm::vec2 originalSize {mSize}; float cropLeft {static_cast(paddingCoords[0]) / static_cast(imageSize.x)}; float cropTop {static_cast(paddingCoords[1]) / static_cast(imageSize.y)}; float cropRight {static_cast(paddingCoords[2]) / static_cast(imageSize.x)}; float cropBottom {static_cast(paddingCoords[3]) / static_cast(imageSize.y)}; crop(cropLeft, cropTop, cropRight, cropBottom); // Cropping the image obviously leads to a reduction in size, so we need to determine // how much to scale up after cropping to keep within the max size restrictions that // were passed as arguments. mSize.x -= mSize.x * (cropLeft + cropRight); mSize.y -= mSize.y * (cropTop + cropBottom); float scaleFactor {originalSize.y / mSize.y}; if (scaleFactor * mSize.x < maxSizeX) scaleFactor = maxSizeX / mSize.x; if (scaleFactor * mSize.y < maxSizeY) scaleFactor = maxSizeY / mSize.y; if (scaleFactor * mSize.x > maxSizeX) scaleFactor = maxSizeX / mSize.x; if (scaleFactor * mSize.y > maxSizeY) scaleFactor = maxSizeY / mSize.y; setResize(mSize.x * scaleFactor, mSize.y * scaleFactor); updateVertices(); } void ImageComponent::setColorShift(unsigned int color) { if (mColorShift == color && mColorShiftEnd == color) return; mColorShift = color; mColorShiftEnd = color; updateColors(); } void ImageComponent::setColorShiftEnd(unsigned int color) { if (mColorShiftEnd == color) return; mColorShiftEnd = color; updateColors(); } void ImageComponent::setColorGradientHorizontal(bool horizontal) { if (mColorGradientHorizontal == horizontal) return; mColorGradientHorizontal = horizontal; updateColors(); } void ImageComponent::setOpacity(float opacity) { if (mOpacity == opacity) return; mOpacity = opacity; updateColors(); } void ImageComponent::setSaturation(float saturation) { if (mSaturation == saturation) return; mSaturation = saturation; updateColors(); } void ImageComponent::setDimming(float dimming) { if (mDimming == dimming) return; mDimming = dimming; } void ImageComponent::setClipRegion(const glm::vec4& clipRegionArg) { if (mVertices[0].clipRegion == clipRegionArg) return; mClipRegion = clipRegionArg; if (mClipRegion == glm::vec4 {0.0f, 0.0f, 0.0f, 0.0f}) { if (mVertices[0].shaderFlags & Renderer::ShaderFlags::CLIPPING) { mVertices[0].shaderFlags ^= Renderer::ShaderFlags::CLIPPING; mVertices[1].shaderFlags ^= Renderer::ShaderFlags::CLIPPING; mVertices[2].shaderFlags ^= Renderer::ShaderFlags::CLIPPING; mVertices[3].shaderFlags ^= Renderer::ShaderFlags::CLIPPING; } } else { mVertices[0].shaderFlags |= Renderer::ShaderFlags::CLIPPING; mVertices[1].shaderFlags |= Renderer::ShaderFlags::CLIPPING; mVertices[2].shaderFlags |= Renderer::ShaderFlags::CLIPPING; mVertices[3].shaderFlags |= Renderer::ShaderFlags::CLIPPING; } mVertices[0].clipRegion = mClipRegion; mVertices[1].clipRegion = mClipRegion; mVertices[2].clipRegion = mClipRegion; mVertices[3].clipRegion = mClipRegion; } void ImageComponent::setFlipX(bool state) { mFlipX = state; updateVertices(); } void ImageComponent::setFlipY(bool state) { mFlipY = state; updateVertices(); } glm::ivec2 ImageComponent::getTextureSize() const { if (mTexture) return mTexture->getSize(); else return glm::ivec2 {0, 0}; } glm::vec2 ImageComponent::getSize() const { return GuiComponent::getSize() * (mBottomRightCrop - mTopLeftCrop); } void ImageComponent::render(const glm::mat4& parentTrans) { if (!isVisible() || mThemeOpacity == 0.0f || mTexture == nullptr || mTargetSize == glm::vec2 {0.0f, 0.0f} || mSize == glm::vec2 {0.0f, 0.0f}) return; glm::mat4 trans {parentTrans * getTransform()}; mRenderer->setMatrix(trans); if (mTexture && mOpacity > 0.0f) { if (Settings::getInstance()->getBool("DebugImage")) { if (mTargetIsMax) { const glm::vec2 targetSizePos {(mTargetSize - mSize) * mOrigin * glm::vec2 {-1.0f}}; mRenderer->drawRect(targetSizePos.x, targetSizePos.y, mTargetSize.x, mTargetSize.y, 0xFF000033, 0xFF000033); } if (mClipRegion == glm::vec4 {0.0f, 0.0f, 0.0f, 0.0f}) mRenderer->drawRect(0.0f, 0.0f, mSize.x, mSize.y, 0xFF000033, 0xFF000033); else mRenderer->drawRect(mClipRegion.x, mClipRegion.y, mClipRegion.z - mClipRegion.x, mClipRegion.w - mClipRegion.y, 0xFF000033, 0xFF000033); } // An image with zero size would normally indicate a corrupt image file. if (mTexture->getSize() != glm::ivec2 {0, 0}) { // Actually draw the image. // The bind() function returns false if the texture is not currently loaded. A blank // texture is bound in this case but we want to handle a fade so it doesn't just // 'jump' in when it finally loads. The exception is if the cached background is // getting invalidated, in which case we want to make sure to not get a partially // faded texture rendered onto the new background. if (mWindow->isInvalidatingCachedBackground()) mTexture->bind(0); else fadeIn(mTexture->bind(0)); mVertices->brightness = mBrightness; mVertices->opacity = mThemeOpacity; mVertices->saturation = mSaturation * mThemeSaturation; mVertices->dimming = mDimming; mVertices->reflectionsFalloff = mReflectionsFalloff; if (mCornerRadius > 0.0f) { mVertices->cornerRadius = mCornerRadius; if (mCornerAntiAliasing) { mVertices->shaderFlags = mVertices->shaderFlags | Renderer::ShaderFlags::ROUNDED_CORNERS; } else { mVertices->shaderFlags = mVertices->shaderFlags | Renderer::ShaderFlags::ROUNDED_CORNERS_NO_AA; } } mVertices->shaderFlags = mVertices->shaderFlags | Renderer::ShaderFlags::PREMULTIPLIED; #if defined(USE_OPENGLES) // This is required as not all mobile GPUs support mipmapping when using the BGRA // pixel format. if (mMipmapping) mVertices->shaderFlags = mVertices->shaderFlags | Renderer::ShaderFlags::CONVERT_PIXEL_FORMAT; #endif mRenderer->drawTriangleStrips(&mVertices[0], 4); } else { if (!mTexture) { LOG(LogError) << "Image texture is not initialized"; } else { std::string textureFilePath = mTexture->getTextureFilePath(); if (textureFilePath != "") { LOG(LogError) << "Image texture for file \"" << textureFilePath << "\" has zero size"; } else { LOG(LogError) << "Image texture has zero size"; } } mTexture.reset(); } } GuiComponent::renderChildren(trans); } void ImageComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) { using namespace ThemeFlags; GuiComponent::applyTheme(theme, view, element, (properties ^ ThemeFlags::SIZE) | ((properties & (ThemeFlags::SIZE | POSITION)) ? ORIGIN : 0)); const ThemeData::ThemeElement* elem {theme->getElement(view, element, "image")}; if (!elem) return; const glm::vec2 scale {getParent() ? getParent()->getSize() : glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight())}; if (properties & ThemeFlags::SIZE) { if (elem->has("size")) { glm::vec2 imageSize {elem->get("size")}; if (imageSize == glm::vec2 {0.0f, 0.0f}) { LOG(LogWarning) << "ImageComponent: Invalid theme configuration, property \"size\" " "for element \"" << element.substr(6) << "\" is set to zero"; imageSize = {0.001f, 0.001f}; } if (imageSize.x > 0.0f) imageSize.x = glm::clamp(imageSize.x, 0.001f, 3.0f); if (imageSize.y > 0.0f) imageSize.y = glm::clamp(imageSize.y, 0.001f, 3.0f); setResize(imageSize * scale); } else if (elem->has("maxSize")) { glm::vec2 imageMaxSize {elem->get("maxSize")}; imageMaxSize.x = glm::clamp(imageMaxSize.x, 0.001f, 3.0f); imageMaxSize.y = glm::clamp(imageMaxSize.y, 0.001f, 3.0f); setMaxSize(imageMaxSize * scale); } else if (elem->has("cropSize")) { glm::vec2 imageCropSize {elem->get("cropSize")}; imageCropSize.x = glm::clamp(imageCropSize.x, 0.001f, 3.0f); imageCropSize.y = glm::clamp(imageCropSize.y, 0.001f, 3.0f); if (elem->has("cropPos")) mCropPos = glm::clamp(elem->get("cropPos"), 0.0f, 1.0f); setCroppedSize(imageCropSize * scale); } } if (properties & ThemeFlags::POSITION && elem->has("stationary")) { const std::string& stationary {elem->get("stationary")}; if (stationary == "never") mStationary = Stationary::NEVER; else if (stationary == "always") mStationary = Stationary::ALWAYS; else if (stationary == "withinView") mStationary = Stationary::WITHIN_VIEW; else if (stationary == "betweenViews") mStationary = Stationary::BETWEEN_VIEWS; else LOG(LogWarning) << "ImageComponent: Invalid theme configuration, property " "\"stationary\" for element \"" << element.substr(6) << "\" defined as \"" << stationary << "\""; } if (elem->has("renderDuringTransitions")) mRenderDuringTransitions = elem->get("renderDuringTransitions"); // Enable linear interpolation by default if element is arbitrarily rotated. if (properties & ThemeFlags::ROTATION && elem->has("rotation")) { const float rotation {std::abs(elem->get("rotation"))}; if (rotation != 0.0f && (std::round(rotation) != rotation || static_cast(rotation) % 90 != 0)) mLinearInterpolation = true; } if (elem->has("interpolation")) { const std::string& interpolation {elem->get("interpolation")}; if (interpolation == "linear") { mLinearInterpolation = true; } else if (interpolation == "nearest") { mLinearInterpolation = false; } else { LOG(LogWarning) << "ImageComponent: Invalid theme configuration, property " "\"interpolation\" for element \"" << element.substr(6) << "\" defined as \"" << interpolation << "\""; } } if (elem->has("cornerRadius")) mCornerRadius = glm::clamp(elem->get("cornerRadius"), 0.0f, 0.5f) * mRenderer->getScreenWidth(); if (properties && elem->has("imageType")) { std::string imageTypes {elem->get("imageType")}; for (auto& character : imageTypes) { if (std::isspace(character)) character = ','; } imageTypes = Utils::String::replace(imageTypes, ",,", ","); mThemeImageTypes = Utils::String::delimitedStringToVector(imageTypes, ","); if (mThemeImageTypes.empty()) { LOG(LogError) << "ImageComponent: Invalid theme configuration, property \"imageType\" " "for element \"" << element.substr(6) << "\" contains no values"; } for (std::string& type : mThemeImageTypes) { if (std::find(sSupportedImageTypes.cbegin(), sSupportedImageTypes.cend(), type) == sSupportedImageTypes.cend()) { LOG(LogError) << "ImageComponent: Invalid theme configuration, property \"imageType\" " "for element \"" << element.substr(6) << "\" defined as \"" << type << "\""; mThemeImageTypes.clear(); break; } } std::vector sortedTypes {mThemeImageTypes}; std::stable_sort(sortedTypes.begin(), sortedTypes.end()); if (std::adjacent_find(sortedTypes.begin(), sortedTypes.end()) != sortedTypes.end()) { LOG(LogError) << "ImageComponent: Invalid theme configuration, property \"imageType\" " "for element \"" << element.substr(6) << "\" contains duplicate values"; mThemeImageTypes.clear(); } } if (elem->has("default")) setDefaultImage(elem->get("default")); bool tile {elem->has("tile") && elem->get("tile")}; if (properties & PATH && elem->has("path")) { const std::string& path {elem->get("path")}; if (tile && elem->has("tileSize")) { glm::vec2 tileSize {elem->get("tileSize")}; if (tileSize.x == 0.0f && tileSize.y == 0.0f) { LOG(LogWarning) << "ImageComponent: Invalid theme configuration, property \"tileSize\" " "for element \"" << element.substr(6) << "\" is set to zero"; tile = false; } else { tileSize.x = glm::clamp(tileSize.x, 0.0f, 1.0f); tileSize.y = glm::clamp(tileSize.y, 0.0f, 1.0f); mTileWidth = tileSize.x * scale.x; mTileHeight = tileSize.y * scale.y; } } setImage(path, tile); } if (elem->has("flipHorizontal")) setFlipX(elem->get("flipHorizontal")); if (elem->has("flipVertical")) setFlipY(elem->get("flipVertical")); bool updateAlignment {false}; if (elem->has("tileHorizontalAlignment")) { const std::string& alignment {elem->get("tileHorizontalAlignment")}; updateAlignment = true; if (alignment == "left") { mTileHorizontalAlignment = ALIGN_LEFT; } else if (alignment == "right") { mTileHorizontalAlignment = ALIGN_RIGHT; } else { LOG(LogWarning) << "ImageComponent: Invalid theme configuration, property " "\"tileHorizontalAlignment\" for element \"" << element.substr(6) << "\" defined as \"" << alignment << "\""; mTileHorizontalAlignment = ALIGN_LEFT; } } if (elem->has("tileVerticalAlignment")) { const std::string& alignment {elem->get("tileVerticalAlignment")}; updateAlignment = true; if (alignment == "top") { mTileVerticalAlignment = ALIGN_TOP; } else if (alignment == "bottom") { mTileVerticalAlignment = ALIGN_BOTTOM; } else { LOG(LogWarning) << "ImageComponent: Invalid theme configuration, property " "\"tileVerticalAlignment\" for element \"" << element.substr(6) << "\" defined as \"" << alignment << "\""; mTileVerticalAlignment = ALIGN_TOP; } } if (tile && updateAlignment) updateVertices(); // Per-game overrides of static images using the game file's basename. It's by design not // possible to override scraped media. if (mThemeImageTypes.empty() && elem->has("gameOverridePath")) { mGameOverridePath = elem->get("gameOverridePath"); #if defined(_WIN64) mGameOverridePath = Utils::String::replace(mGameOverridePath, "\\", "/"); #endif if (mGameOverridePath.back() != '/') mGameOverridePath.push_back('/'); if (elem->has("path")) mGameOverrideOriginalPath = elem->get("path"); else mGameOverrideOriginalPath = ""; } if (elem->has("metadataElement") && elem->get("metadataElement")) mComponentThemeFlags |= ComponentThemeFlags::METADATA_ELEMENT; if (properties & COLOR) { if (elem->has("color")) setColorShift(elem->get("color")); if (elem->has("colorEnd")) setColorShiftEnd(elem->get("colorEnd")); if (elem->has("gradientType")) { const std::string& gradientType {elem->get("gradientType")}; if (gradientType == "horizontal") { setColorGradientHorizontal(true); } else if (gradientType == "vertical") { setColorGradientHorizontal(false); } else { setColorGradientHorizontal(true); LOG(LogWarning) << "ImageComponent: Invalid theme configuration, property " "\"gradientType\" for element \"" << element.substr(6) << "\" defined as \"" << gradientType << "\""; } } } if (elem->has("scrollFadeIn") && elem->get("scrollFadeIn")) mComponentThemeFlags |= ComponentThemeFlags::SCROLL_FADE_IN; } std::vector ImageComponent::getHelpPrompts() { std::vector ret; ret.push_back(HelpPrompt("a", _("select"))); return ret; } void ImageComponent::resize(bool rasterize) { if (!mTexture) return; const glm::vec2 textureSize {mTexture->getSourceImageSize()}; if (textureSize == glm::vec2 {0.0f, 0.0f}) return; if (mTexture->isTiled()) { mSize = mTargetSize; } else { if (mTargetIsMax) { // Maintain image aspect ratio. mSize = textureSize; glm::vec2 resizeScale {mTargetSize.x / mSize.x, mTargetSize.y / mSize.y}; if (resizeScale.x < resizeScale.y) { mSize.x *= resizeScale.x; mSize.y = std::min(mSize.y * resizeScale.x, mTargetSize.y); } else { // This will be mTargetSize.y(). We can't exceed it. mSize.y *= resizeScale.y; mSize.x = std::min((mSize.y / textureSize.y) * textureSize.x, mTargetSize.x); } } else if (mTargetIsCrop) { // Size texture to allow for cropped image to fill the entire area. const float cropFactor { std::max(mTargetSize.x / textureSize.x, mTargetSize.y / textureSize.y)}; mSize = textureSize * cropFactor; } else { // If both axes are set we just stretch or squash, if no axes are set we do nothing. mSize = mTargetSize == glm::vec2 {0.0f, 0.0f} ? textureSize : mTargetSize; // If only one axis is set, we resize in a way that maintains aspect ratio. if (!mTargetSize.x && mTargetSize.y) { mSize.y = mTargetSize.y; mSize.x = (mSize.y / textureSize.y) * textureSize.x; } else if (mTargetSize.x && !mTargetSize.y) { mSize.y = (mTargetSize.x / textureSize.x) * textureSize.y; mSize.x = (mSize.y / textureSize.y) * textureSize.x; } } } // Make sure sub-pixel values are not rounded to zero and that the size is not unreasonably // large (which may be caused by a mistake in the theme configuration). mSize.x = glm::clamp(mSize.x, 1.0f, mRenderer->getScreenWidth() * 3.0f); mSize.y = glm::clamp(mSize.y, 1.0f, mRenderer->getScreenHeight() * 3.0f); if (rasterize) { mTexture->rasterizeAt(mSize.x, mSize.y); if (mTargetIsCrop) coverFitCrop(); onSizeChanged(); } } void ImageComponent::setTileAxes() { if (mTileWidth == 0.0f && mTileHeight == 0.0f) { mTileWidth = static_cast(mTexture->getSize().x); mTileHeight = static_cast(mTexture->getSize().y); return; } const float ratio {mTexture->getSourceImageSize().x / mTexture->getSourceImageSize().y}; if (mTileWidth == 0.0f) mTileWidth = std::round(mTileHeight * ratio); else if (mTileHeight == 0.0f) mTileHeight = std::round(mTileWidth / ratio); } void ImageComponent::updateVertices() { if (!mTexture) return; const glm::vec2 topLeft {0.0f, 0.0f}; const glm::vec2 bottomRight {mSize}; const float px {mTexture->isTiled() ? mSize.x / getTextureSize().x : 1.0f}; const float py {mTexture->isTiled() ? mSize.y / getTextureSize().y : 1.0f}; if (mTileHeight == 0.0f) { // clang-format off mVertices[0] = {{topLeft.x, topLeft.y }, {mTopLeftCrop.x + mCropOffset.x, py - mTopLeftCrop.y - mCropOffset.y }, 0}; mVertices[1] = {{topLeft.x, bottomRight.y}, {mTopLeftCrop.x + mCropOffset.x, 1.0f - mBottomRightCrop.y - mCropOffset.y}, 0}; mVertices[2] = {{bottomRight.x, topLeft.y }, {(mBottomRightCrop.x * px) + mCropOffset.x, py - mTopLeftCrop.y - mCropOffset.y }, 0}; mVertices[3] = {{bottomRight.x, bottomRight.y}, {(mBottomRightCrop.x * px) + mCropOffset.x, 1.0f - mBottomRightCrop.y - mCropOffset.y}, 0}; // clang-format on } else { // Resize and align tiled textures. glm::vec2 topLeftAlign {mTopLeftCrop}; glm::vec2 bottomRightAlign {mBottomRightCrop}; const float pxA {mSize.x / mTileWidth}; const float pyA {mSize.y / mTileHeight}; if (mTileHorizontalAlignment == (mFlipX ? Alignment::ALIGN_LEFT : Alignment::ALIGN_RIGHT)) { float offsetX {pxA - std::floor(pxA)}; if (offsetX != 0.0f) { const float moveX {(mTileWidth * offsetX) / mSize.x}; if (mFlipX) { topLeftAlign.x += moveX * pxA; bottomRightAlign.x += moveX; } else { topLeftAlign.x -= moveX * pxA; bottomRightAlign.x -= moveX; } } } if (mTileVerticalAlignment == (mFlipY ? Alignment::ALIGN_BOTTOM : Alignment::ALIGN_TOP)) { float offsetY {pyA - std::floor(pyA)}; if (offsetY != 0.0f) { const float moveY {(mTileHeight * offsetY) / mSize.y}; if (mFlipY) { topLeftAlign.y -= moveY * pyA; bottomRightAlign.y -= moveY * pyA; } else { topLeftAlign.y += moveY * pyA; bottomRightAlign.y += moveY * pyA; } } } // clang-format off mVertices[0] = {{topLeft.x, topLeft.y }, {topLeftAlign.x, pyA - topLeftAlign.y }, 0}; mVertices[1] = {{topLeft.x, bottomRight.y}, {topLeftAlign.x, 1.0f - bottomRightAlign.y}, 0}; mVertices[2] = {{bottomRight.x, topLeft.y }, {bottomRightAlign.x * pxA, pyA - topLeftAlign.y }, 0}; mVertices[3] = {{bottomRight.x, bottomRight.y}, {bottomRightAlign.x * pxA, 1.0f - bottomRightAlign.y}, 0}; // clang-format on } updateColors(); // Round vertices. for (int i {0}; i < 4; ++i) mVertices[i].position = glm::round(mVertices[i].position); if (mFlipX) { for (int i {0}; i < 4; ++i) mVertices[i].texcoord[0] = px - mVertices[i].texcoord[0]; } if (mFlipY) { for (int i {0}; i < 4; ++i) mVertices[i].texcoord[1] = py - mVertices[i].texcoord[1]; } setClipRegion(mClipRegion); } void ImageComponent::updateColors() { const float opacity {mOpacity * (mFading ? mFadeOpacity : 1.0f)}; const unsigned int color {(mColorShift & 0xFFFFFF00) | static_cast((mColorShift & 0xFF) * opacity)}; const unsigned int colorEnd {(mColorShiftEnd & 0xFFFFFF00) | static_cast((mColorShiftEnd & 0xFF) * opacity)}; mVertices[0].color = color; mVertices[1].color = mColorGradientHorizontal ? color : colorEnd; mVertices[2].color = mColorGradientHorizontal ? colorEnd : color; mVertices[3].color = colorEnd; } void ImageComponent::fadeIn(bool textureLoaded) { if (!mForceLoad) { if (!textureLoaded) { // Start the fade if this is the first time we've encountered the unloaded texture. if (!mFading) { // Start with a zero opacity and flag it as fading. mFadeOpacity = 0.0f; mFading = true; updateColors(); } } else if (mFading) { // The texture is loaded and we need to fade it in. The fade is based on the frame // rate and is 1/4 second if running at 60 frames per second although the actual // value is not that important. float opacity {mFadeOpacity + 1.0f / 15.0f}; // See if we've finished fading. if (opacity >= 1.0f) { mFadeOpacity = 1.0f; mFading = false; } else { mFadeOpacity = opacity; } updateColors(); } } }