/* Crafter®.Graphics Copyright (C) 2025 Catcrafts® catcrafts.net This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 3.0 as published by the Free Software Foundation; This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; #define STB_IMAGE_IMPLEMENTATION #include "../lib/stb_image.h" export module Crafter.Graphics:RenderingElement; import std; import :Transform; import :Types; import :Image; import :Window; export namespace Crafter { struct RenderElementScalingOwning { std::vector scalingBuffer; std::uint_fast32_t bufferWidth; std::uint_fast32_t bufferHeight; bool bufferUpdated = true; RenderElementScalingOwning() = default; RenderElementScalingOwning(std::uint_fast32_t bufferWidth, std::uint_fast32_t bufferHeight) : scalingBuffer(bufferWidth*bufferHeight), bufferWidth(bufferWidth), bufferHeight(bufferHeight) { } }; struct RenderElementScalingNonOwning { Pixel_BU8_GU8_RU8_AU8* scalingBuffer; std::uint_fast32_t bufferWidth; std::uint_fast32_t bufferHeight; bool bufferUpdated = true; RenderElementScalingNonOwning() = default; RenderElementScalingNonOwning(Pixel_BU8_GU8_RU8_AU8* scalingBuffer, std::uint_fast32_t bufferWidth, std::uint_fast32_t bufferHeight) : scalingBuffer(scalingBuffer), bufferWidth(bufferWidth), bufferHeight(bufferHeight) { } }; struct RenderElementRotating { std::uint_fast32_t rotation; bool rotationUpdated = true; RenderElementRotating() = default; RenderElementRotating(std::uint_fast32_t rotation) : rotation(rotation) { } }; struct EmptyScalingBase {}; struct EmptyRotatingBase {}; template using ScalingBase = std::conditional_t< Scaling, std::conditional_t, EmptyScalingBase >; template using RotatingBase = std::conditional_t< Rotating, RenderElementRotating, EmptyRotatingBase >; class RenderingElementBase : public Transform { public: std::vector buffer; OpaqueType opaque; RenderingElementBase(Anchor anchor) : Transform(anchor) { scaled.width = 0; } RenderingElementBase(Anchor anchor, OpaqueType opaque) : Transform(anchor), opaque(opaque) { scaled.width = 0; } }; template requires ((!Rotating || Scaling) && (!Owning || Scaling)) class RenderingElement : public RenderingElementBase, public ScalingBase, public RotatingBase { public: RenderingElement() = default; RenderingElement(Anchor anchor, OpaqueType opaque) : RenderingElementBase(anchor, opaque) { } RenderingElement(Anchor anchor, OpaqueType opaque, std::uint_fast32_t rotation) requires(Rotating) : RenderingElementBase(anchor, opaque), RotatingBase(rotation) { } RenderingElement(Anchor anchor, const std::string_view imagePath) : RenderingElementBase(anchor) { LoadImage(imagePath); } RenderingElement(Anchor anchor, const std::string_view imagePath, std::uint_fast32_t rotation) requires(Rotating) : RenderingElementBase(anchor), RotatingBase(rotation) { LoadImage(imagePath); } RenderingElement(Anchor anchor, const std::string_view imagePath, OpaqueType opaque) : RenderingElementBase(anchor, opaque) { LoadImageNoOpaqueCheck(imagePath); } RenderingElement(Anchor anchor, const std::string_view imagePath, OpaqueType opaque, std::uint_fast32_t rotation) requires(Rotating) : RenderingElementBase(anchor, opaque), RotatingBase(rotation) { LoadImageNoOpaqueCheck(imagePath); } RenderingElement(Anchor anchor, OpaqueType opaque, std::uint_fast32_t bufferWidth, std::uint_fast32_t bufferHeight, Pixel_BU8_GU8_RU8_AU8* scalingBuffer) requires(Scaling && !Owning) : RenderingElementBase(anchor, opaque), ScalingBase(bufferWidth, bufferHeight, scalingBuffer) { } RenderingElement(Anchor anchor, OpaqueType opaque, std::uint_fast32_t bufferWidth, std::uint_fast32_t bufferHeight, Pixel_BU8_GU8_RU8_AU8* scalingBuffer, std::uint_fast32_t rotation) requires(Scaling && !Owning && Rotating) : RenderingElementBase(anchor, opaque), ScalingBase(bufferWidth, bufferHeight, scalingBuffer), RotatingBase(rotation) { } RenderingElement(Anchor anchor, OpaqueType opaque, Image& image) requires(Scaling && !Owning) : RenderingElementBase(anchor, opaque), ScalingBase(image.buffer.data(), image.width, image.height) { } RenderingElement(Anchor anchor, OpaqueType opaque, Image& image, std::uint_fast32_t rotation) requires(Scaling && !Owning && Rotating) : RenderingElementBase(anchor, opaque), ScalingBase(image.buffer.data(), image.width, image.height), RotatingBase(rotation) { } RenderingElement(Anchor anchor, Image& image) requires(Scaling && !Owning) : RenderingElementBase(anchor, image.opaque), ScalingBase(image.buffer.data(), image.width, image.height) { } RenderingElement(Anchor anchor, Image& image, std::uint_fast32_t rotation) requires(Scaling && !Owning && Rotating) : RenderingElementBase(anchor, image.opaque), ScalingBase(image.buffer.data(), image.width, image.height), RotatingBase(rotation) { } RenderingElement(Anchor anchor, OpaqueType opaque, std::uint_fast32_t bufferWidth, std::uint_fast32_t bufferHeight) requires(Owning) : RenderingElementBase(anchor, opaque), ScalingBase(bufferWidth, bufferHeight) { } RenderingElement(Anchor anchor, OpaqueType opaque, std::uint_fast32_t bufferWidth, std::uint_fast32_t bufferHeight, std::uint_fast32_t rotation) requires(Owning && Rotating) : RenderingElementBase(anchor, opaque), ScalingBase(bufferWidth, bufferHeight) , RotatingBase(rotation) { } RenderingElement(RenderingElement&) = delete; RenderingElement& operator=(RenderingElement&) = delete; void ScaleNearestNeighbor() requires(Scaling) { for (std::uint_fast32_t y = 0; y < scaled.height; y++) { std::uint_fast32_t srcY = y * ScalingBase::bufferHeight / scaled.height; for (std::uint_fast32_t x = 0; x < scaled.width; x++) { std::uint_fast32_t srcX = x * ScalingBase::bufferWidth / scaled.width; buffer[y * scaled.width + x] = ScalingBase::scalingBuffer[srcY * ScalingBase::bufferWidth + srcX]; } } } void ScaleRotating() requires(Scaling) { const double rad = (static_cast(RotatingBase::rotation) / static_cast(std::numeric_limits::max())) * 2.0 * std::numbers::pi; const std::uint_fast32_t dstWidth = scaled.width; const std::uint_fast32_t dstHeight = scaled.height; const double c2 = std::abs(std::cos(rad)); const double s2 = std::abs(std::sin(rad)); const double rotatedWidth = dstWidth * c2 + dstHeight * s2; const double rotatedHeight = dstWidth * s2 + dstHeight * c2; const std::uint_fast32_t diffX = static_cast(std::ceil((rotatedWidth - dstWidth) * 0.5)); const std::uint_fast32_t diffY = static_cast(std::ceil((rotatedHeight - dstHeight) * 0.5)); scaled.width += diffX + diffX; scaled.height += diffY + diffY; scaled.x -= diffX; scaled.y -= diffY; buffer.clear(); buffer.resize(scaled.width * scaled.height); // Destination center const double dstCx = (static_cast(scaled.width) - 1.0) * 0.5; const double dstCy = (static_cast(scaled.height) - 1.0) * 0.5; // Source center const double srcCx = (static_cast(ScalingBase::bufferWidth) - 1.0) * 0.5; const double srcCy = (static_cast(ScalingBase::bufferHeight) - 1.0) * 0.5; const double c = std::cos(rad); const double s = std::sin(rad); // Scale factors (destination → source) const double scaleX = static_cast(ScalingBase::bufferWidth) / dstWidth; const double scaleY = static_cast(ScalingBase::bufferHeight) / dstHeight; for (std::uint_fast32_t yB = 0; yB < scaled.height; ++yB) { for (std::uint_fast32_t xB = 0; xB < scaled.width; ++xB) { // ---- Destination pixel relative to center ---- const double dx = (static_cast(xB) - dstCx) * scaleX; const double dy = (static_cast(yB) - dstCy) * scaleY; // ---- Inverse rotation ---- const double sx = (c * dx - s * dy) + srcCx; const double sy = (s * dx + c * dy) + srcCy; // ---- Nearest neighbour sampling ---- const std::int_fast32_t srcX = static_cast(std::round(sx)); const std::int_fast32_t srcY = static_cast(std::round(sy)); if (srcX >= 0 && srcX < ScalingBase::bufferWidth && srcY >= 0 && srcY < ScalingBase::bufferHeight) { buffer[yB * scaled.width + xB] = ScalingBase::scalingBuffer[srcY * ScalingBase::bufferWidth + srcX]; } } } } void UpdatePosition(Window& window, ScaleData oldScale) { if constexpr(Scaling && !Rotating) { if(oldScale.width != scaled.width || oldScale.height != scaled.height) { buffer.resize(scaled.width * scaled.height); ScaleNearestNeighbor(); window.AddDirtyRect(oldScale); window.AddDirtyRect(scaled); } else if(oldScale.x != scaled.x || oldScale.y != scaled.y) { window.AddDirtyRect(oldScale); window.AddDirtyRect(scaled); if(ScalingBase::bufferUpdated) { ScaleNearestNeighbor(); ScalingBase::bufferUpdated = false; } } else if(ScalingBase::bufferUpdated) { ScaleNearestNeighbor(); ScalingBase::bufferUpdated = false; } } else if constexpr(Rotating) { if(oldScale.width != scaled.width || oldScale.height != scaled.height) { buffer.resize(scaled.width * scaled.height); ScaleNearestNeighbor(); window.AddDirtyRect(oldScale); window.AddDirtyRect(scaled); } else if(oldScale.x != scaled.x || oldScale.y != scaled.y) { window.AddDirtyRect(oldScale); window.AddDirtyRect(scaled); if(ScalingBase::bufferUpdated) { ScaleRotating(); ScalingBase::bufferUpdated = false; RotatingBase::rotationUpdated = false; } } else if(ScalingBase::bufferUpdated || RotatingBase::rotationUpdated) { ScaleRotating(); ScalingBase::bufferUpdated = false; RotatingBase::rotationUpdated = false; } } else { if(oldScale.width != scaled.width || oldScale.height != scaled.height) { buffer.resize(scaled.width * scaled.height); window.AddDirtyRect(oldScale); window.AddDirtyRect(scaled); } if(oldScale.x != scaled.x || oldScale.y != scaled.y) { window.AddDirtyRect(oldScale); window.AddDirtyRect(scaled); } } } void UpdatePosition(Window& window) override { ScaleData oldScale = scaled; window.ScaleElement(*this); UpdatePosition(window, oldScale); for(Transform* child : children) { child->UpdatePosition(window, *this); } } void UpdatePosition(Window& window, Transform& parent) override { ScaleData oldScale = scaled; window.ScaleElement(*this, parent); UpdatePosition(window, oldScale); for(Transform* child : children) { child->UpdatePosition(window, *this); } } void LoadImage(const std::string_view imagePath) { std::filesystem::path abs = std::filesystem::absolute(imagePath); int xSize; int ySize; unsigned char* bgData = stbi_load(abs.string().c_str(), &xSize, &ySize, nullptr, 4); if constexpr(Scaling && !Owning) { ScalingBase::bufferUpdated = true; } else if constexpr(Scaling && Owning) { ScalingBase::bufferWidth = xSize; ScalingBase::bufferHeight = ySize; ScalingBase::bufferUpdated = true; ScalingBase::scalingBuffer.resize(xSize*ySize); } else { buffer.resize(xSize*ySize); } opaque = OpaqueType::FullyOpaque; if constexpr(Scaling) { for(std::uint_fast32_t x = 0; x < xSize; x++) { for(std::uint_fast32_t y = 0; y < ySize; y++) { std::uint_fast32_t idx = (x*ySize+y)*4; ScalingBase::scalingBuffer[x*ySize+y].r = bgData[idx]; ScalingBase::scalingBuffer[x*ySize+y].g = bgData[idx+1]; ScalingBase::scalingBuffer[x*ySize+y].b = bgData[idx+2]; ScalingBase::scalingBuffer[x*ySize+y].a = bgData[idx+3]; } } for(std::uint_fast32_t i = 0; i < xSize*ySize; i++) { if(ScalingBase::scalingBuffer[i].a != 255) { opaque = OpaqueType::SemiOpaque; for(std::uint_fast32_t i2 = 0; i2 < xSize*ySize; i2++) { if(ScalingBase::scalingBuffer[i2].a != 0 && ScalingBase::scalingBuffer[i2].a != 255) { opaque = OpaqueType::Transparent; return; } } return; } } } else { for(std::uint_fast32_t x = 0; x < xSize; x++) { for(std::uint_fast32_t y = 0; y < ySize; y++) { std::uint_fast32_t idx = (x*ySize+y)*4; buffer[x*ySize+y].r = bgData[idx]; buffer[x*ySize+y].g = bgData[idx+1]; buffer[x*ySize+y].b = bgData[idx+2]; buffer[x*ySize+y].a = bgData[idx+3]; } } for(std::uint_fast32_t i = 0; i < xSize*ySize; i++) { if(buffer[i].a != 255) { opaque = OpaqueType::SemiOpaque; for(std::uint_fast32_t i2 = 0; i2 < xSize*ySize; i2++) { if(buffer[i2].a != 0 && buffer[i2].a != 255) { opaque = OpaqueType::Transparent; return; } } return; } } } } void LoadImageNoOpaqueCheck(const std::string_view imagePath) { std::filesystem::path abs = std::filesystem::absolute(imagePath); int xSize; int ySize; unsigned char* bgData = stbi_load(abs.string().c_str(), &xSize, &ySize, nullptr, 4); if constexpr(Scaling && !Owning) { ScalingBase::bufferUpdated = true; } else if constexpr(Scaling && Owning) { ScalingBase::bufferWidth = xSize; ScalingBase::bufferHeight = ySize; ScalingBase::bufferUpdated = true; ScalingBase::scalingBuffer.resize(xSize*ySize); } else { buffer.resize(xSize*ySize); } if constexpr(Scaling) { for(std::uint_fast32_t x = 0; x < xSize; x++) { for(std::uint_fast32_t y = 0; y < ySize; y++) { std::uint_fast32_t idx = (x*ySize+y)*4; ScalingBase::scalingBuffer[x*ySize+y].r = bgData[idx]; ScalingBase::scalingBuffer[x*ySize+y].g = bgData[idx+1]; ScalingBase::scalingBuffer[x*ySize+y].b = bgData[idx+2]; ScalingBase::scalingBuffer[x*ySize+y].a = bgData[idx+3]; } } } else { for(std::uint_fast32_t x = 0; x < xSize; x++) { for(std::uint_fast32_t y = 0; y < ySize; y++) { std::uint_fast32_t idx = (x*ySize+y)*4; buffer[x*ySize+y].r = bgData[idx]; buffer[x*ySize+y].g = bgData[idx+1]; buffer[x*ySize+y].b = bgData[idx+2]; buffer[x*ySize+y].a = bgData[idx+3]; } } } } }; }