/* Crafter®.Graphics Copyright (C) 2026 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; #include "../lib/stb_truetype.h" export module Crafter.Graphics:RenderingElement2D; import Crafter.Asset; import std; import :Transform2D; import :RenderingElement2DBase; import :Font; import :Types; import :Window; export namespace Crafter { template requires ((!Rotating || Scaling) && (!Owning || Scaling)) struct RenderingElement2D : RenderingElement2DBase, ScalingBase, RotatingBase { RenderingElement2D() = default; RenderingElement2D(Anchor2D anchor, OpaqueType opaque) : RenderingElement2DBase(anchor, opaque) { } RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t rotation) requires(Rotating) : RenderingElement2DBase(anchor, opaque), RotatingBase(rotation) { } RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight, Vector* scalingBuffer) requires(Scaling && !Owning) : RenderingElement2DBase(anchor, opaque), ScalingBase(bufferWidth, bufferHeight, scalingBuffer) { } RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight, Vector* scalingBuffer, std::uint32_t rotation) requires(Scaling && !Owning && Rotating) : RenderingElement2DBase(anchor, opaque), ScalingBase(bufferWidth, bufferHeight, scalingBuffer), RotatingBase(rotation) { } RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight) requires(Owning) : RenderingElement2DBase(anchor, opaque), ScalingBase(bufferWidth, bufferHeight) { } RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight, std::uint32_t rotation) requires(Owning && Rotating) : RenderingElement2DBase(anchor, opaque), ScalingBase(bufferWidth, bufferHeight) , RotatingBase(rotation) { } RenderingElement2D(Anchor2D anchor, TextureAsset>& texture) requires(!Owning && Scaling) : RenderingElement2DBase(anchor, texture.opaque), ScalingBase(texture.pixels.data(), texture.sizeX, texture.sizeY) { } RenderingElement2D(Anchor2D anchor, TextureAsset>& texture, std::uint32_t rotation) requires(!Owning && Scaling && Rotating) : RenderingElement2DBase(anchor, texture.opaque), ScalingBase(texture.pixels.data(), texture.sizeX, texture.sizeY), RotatingBase(rotation) { } RenderingElement2D(RenderingElement2D&) = delete; RenderingElement2D& operator=(RenderingElement2D&) = delete; void ScaleNearestNeighbor() requires(Scaling) { for (std::uint32_t y = 0; y < this->scaled.size.y; y++) { std::uint32_t srcY = y * ScalingBase::bufferHeight / this->scaled.size.y; for (std::uint32_t x = 0; x < this->scaled.size.x; x++) { std::uint32_t srcX = x * ScalingBase::bufferWidth / this->scaled.size.x; this->buffer[y * this->scaled.size.x + x] = ScalingBase::scalingBuffer[srcY * ScalingBase::bufferWidth + srcX]; } } } void ScaleRotating() requires(Scaling) { const float dstWidth = this->scaled.size.x; const float dstHeight = this->scaled.size.y; const float c2 = std::abs(std::cos(RotatingBase::rotation)); const float s2 = std::abs(std::sin(RotatingBase::rotation)); const float rotatedWidth = dstWidth * c2 + dstHeight * s2; const float rotatedHeight = dstWidth * s2 + dstHeight * c2; const float diffX = std::ceil((rotatedWidth - dstWidth) * 0.5); const float diffY = std::ceil((rotatedHeight - dstHeight) * 0.5); this->scaled.size.x += diffX + diffX; this->scaled.size.y += diffY + diffY; this->scaled.position.x -= diffX; this->scaled.position.y -= diffY; this->buffer.clear(); this->buffer.resize(this->scaled.size.x * this->scaled.size.y); // Destination center const float dstCx = (this->scaled.size.x - 1.0) * 0.5; const float dstCy = (this->scaled.size.y - 1.0) * 0.5; // Source center const float srcCx = (ScalingBase::bufferWidth - 1.0) * 0.5; const float srcCy = (ScalingBase::bufferHeight - 1.0) * 0.5; const float c = std::cos(RotatingBase::rotation); const float s = std::sin(RotatingBase::rotation); // Scale factors (destination → source) const float scaleX = static_cast(ScalingBase::bufferWidth) / dstWidth; const float scaleY = static_cast(ScalingBase::bufferHeight) / dstHeight; for (std::uint32_t yB = 0; yB < this->scaled.size.y; ++yB) { for (std::uint32_t xB = 0; xB < this->scaled.size.x; ++xB) { // ---- Destination pixel relative to center ---- const float dx = (static_cast(xB) - dstCx) * scaleX; const float dy = (static_cast(yB) - dstCy) * scaleY; // ---- Inverse rotation ---- const float sx = (c * dx - s * dy) + srcCx; const float sy = (s * dx + c * dy) + srcCy; // ---- Nearest neighbour sampling ---- const std::int32_t srcX = static_cast(std::round(sx)); const std::int32_t srcY = static_cast(std::round(sy)); if (srcX >= 0 && srcX < ScalingBase::bufferWidth && srcY >= 0 && srcY < ScalingBase::bufferHeight) { this->buffer[yB * this->scaled.size.x + xB] = ScalingBase::scalingBuffer[srcY * ScalingBase::bufferWidth + srcX]; } } } } void UpdatePosition(RendertargetBase& window, Transform2D& parent) override { ScaleData2D oldScale = this->scaled; this->ScaleElement(parent); if constexpr(Scaling && !Rotating) { if(oldScale.size.x != this->scaled.size.x || oldScale.size.y != this->scaled.size.y) { this->buffer.resize(this->scaled.size.x * this->scaled.size.y); ScaleNearestNeighbor(); } } else if constexpr(Rotating) { if(oldScale.size.x != this->scaled.size.x || oldScale.size.y != this->scaled.size.y) { this->buffer.resize(this->scaled.size.x * this->scaled.size.y); ScaleRotating(); } } else { if(oldScale.size.x != this->scaled.size.x || oldScale.size.y != this->scaled.size.y) { this->buffer.resize(this->scaled.size.x * this->scaled.size.y); } } for(Transform2D* child : this->children) { child->UpdatePosition(window, *this); } } std::vector ResizeText(RendertargetBase& window, Transform2D& parent, const std::string_view text, float& size, Font& font, std::uint8_t frame, TextOverflowMode overflowMode = TextOverflowMode::Clip, TextScaleMode scaleMode = TextScaleMode::None) { float scale = stbtt_ScaleForPixelHeight(&font.font, size); int baseline = (int)(font.ascent * scale); std::vector lines; std::string_view remaining = text; std::uint32_t lineHeight = (font.ascent - font.descent) * scale; if(overflowMode == TextOverflowMode::Clip) { while (!remaining.empty()) { // Find next newline or end of string auto newlinePos = remaining.find('\n'); if (newlinePos != std::string_view::npos) { lines.emplace_back(remaining.substr(0, newlinePos)); remaining = remaining.substr(newlinePos + 1); } else { lines.emplace_back(remaining); break; } } std::uint32_t maxWidth = 0; for(const std::string_view line: lines) { std::uint32_t lineWidth = 0; for (const char c : line) { int advance, lsb; stbtt_GetCodepointHMetrics(&font.font, c, &advance, &lsb); lineWidth += (int)(advance * scale); } if(lineWidth > maxWidth) { maxWidth = lineWidth; } } if(scaleMode == TextScaleMode::Element) { std::int32_t logicalPerPixelY = this->anchor.height / this->scaled.size.y; std::int32_t oldHeight = this->anchor.height; std::int32_t logicalPerPixelX = this->anchor.width / this->scaled.size.x; std::int32_t oldwidth = this->anchor.width; this->anchor.height = lineHeight * logicalPerPixelY; this->anchor.width = maxWidth * logicalPerPixelX; if(oldHeight != this->anchor.height || oldwidth != this->anchor.width) { UpdatePosition(window, parent); } } else if(scaleMode == TextScaleMode::Font) { float lineHeightPerFont = lineHeight / size; float lineWidthPerFont = maxWidth / size; float maxFontHeight = this->scaled.size.y / lineHeightPerFont; float maxFontWidth = this->scaled.size.x / lineWidthPerFont; size = std::min(maxFontHeight, maxFontWidth); } else { if constexpr(Scaling) { lines.resize(ScalingBase::bufferHeight / lines.size()); } else { lines.resize(this->scaled.size.y / lines.size()); } } } else { while (!remaining.empty()) { std::string_view line; auto newlinePos = remaining.find('\n'); if (newlinePos != std::string_view::npos) { line = remaining.substr(0, newlinePos); remaining = remaining.substr(newlinePos + 1); } else { line = remaining; remaining = ""; } std::uint32_t lineWidth = 0; std::size_t lastWrapPos = 0; // position of last space that can be used to wrap std::size_t startPos = 0; for (std::size_t i = 0; i < line.size(); ++i) { char c = line[i]; // get width of this character int advance, lsb; stbtt_GetCodepointHMetrics(&font.font, c, &advance, &lsb); lineWidth += (std::uint32_t)(advance * scale); // remember last space for wrapping if (c == ' ') { lastWrapPos = i; } // if line exceeds width, wrap if (lineWidth > this->scaled.size.x) { std::size_t wrapPos; if (lastWrapPos > startPos) { wrapPos = lastWrapPos; // wrap at last space } else { wrapPos = i; // no space, hard wrap } // push the line up to wrapPos lines.push_back(line.substr(startPos, wrapPos - startPos)); // skip any spaces at the beginning of next line startPos = wrapPos; while (startPos < line.size() && line[startPos] == ' ') { ++startPos; } // reset width and i lineWidth = 0; i = startPos - 1; // -1 because loop will increment i } } // add the remaining part of the line if (startPos < line.size()) { lines.push_back(line.substr(startPos)); } } if(scaleMode == TextScaleMode::Element) { float logicalPerPixelY = this->anchor.height / this->scaled.size.y; float oldHeight = this->anchor.height; this->anchor.height = lineHeight * logicalPerPixelY; if(oldHeight != this->anchor.height) { UpdatePosition(window, parent); } } else if(scaleMode == TextScaleMode::Font) { float lineHeightPerFont = lineHeight / size; size = this->scaled.size.y / lineHeightPerFont; } else { if constexpr(Scaling) { lines.resize(ScalingBase::bufferHeight / lines.size()); } else { lines.resize(this->scaled.size.y / lines.size()); } } } return lines; } void RenderText(std::span lines, float size, Vector color, Font& font, std::uint8_t frame, TextAlignment alignment = TextAlignment::Left, std::uint32_t offsetX = 0, std::uint32_t offsetY = 0, OpaqueType opaque = OpaqueType::FullyOpaque) { float scale = stbtt_ScaleForPixelHeight(&font.font, size); int baseline = (int)(font.ascent * scale); std::uint32_t lineHeight = (font.ascent - font.descent) * scale; std::uint32_t currentY = baseline; for(std::string_view line : lines) { std::uint32_t lineWidth = 0; for (const char c : line) { int advance, lsb; stbtt_GetCodepointHMetrics(&font.font, c, &advance, &lsb); lineWidth += (int)(advance * scale); } std::uint32_t x = 0; switch (alignment) { case TextAlignment::Left: x = 0; break; case TextAlignment::Center: x = (this->scaled.size.x - lineWidth) / 2; break; case TextAlignment::Right: x = this->scaled.size.x - lineWidth; break; } for (std::size_t i = 0; i < line.size(); ++i) { int codepoint = line[i]; int ax; int lsb; stbtt_GetCodepointHMetrics(&font.font, codepoint, &ax, &lsb); int c_x1, c_y1, c_x2, c_y2; stbtt_GetCodepointBitmapBox(&font.font, codepoint, scale, scale, &c_x1, &c_y1, &c_x2, &c_y2); int w = c_x2 - c_x1; int h = c_y2 - c_y1; std::vector bitmap(w * h); stbtt_MakeCodepointBitmap(&font.font, bitmap.data(), w, h, w, scale, scale, codepoint); // Only render characters that fit within the scaled bounds switch(opaque) { case OpaqueType::FullyOpaque: { for (int j = 0; j < h; j++) { for (int i = 0; i < w; i++) { int bufferX = x + i + c_x1 + offsetX; int bufferY = currentY + j + c_y1 + offsetY; // Only draw pixels that are within our scaled buffer bounds if constexpr(Scaling) { if (bufferX >= 0 && bufferX < ScalingBase::bufferWidth && bufferY >= 0 && bufferY < ScalingBase::bufferHeight) { ScalingBase::scalingBuffer[bufferY * ScalingBase::bufferWidth + bufferX] = {color.r, color.g, color.b, bitmap[j * w + i]}; } } else { if (bufferX >= 0 && bufferX < (int)this->scaled.size.x && bufferY >= 0 && bufferY < (int)this->scaled.size.y) { this->buffer[bufferY * this->scaled.size.x + bufferX] = {color.r, color.g, color.b, bitmap[j * w + i]}; } } } } break; } case OpaqueType::SemiOpaque: { for (int j = 0; j < h; j++) { for (int i = 0; i < w; i++) { int bufferX = x + i + c_x1 + offsetX; int bufferY = currentY + j + c_y1 + offsetY; // Only draw pixels that are within our scaled buffer bounds if constexpr(Scaling) { if (bufferX >= 0 && bufferX < ScalingBase::bufferWidth && bufferY >= 0 && bufferY < ScalingBase::bufferHeight) { ScalingBase::scalingBuffer[bufferY * ScalingBase::bufferWidth + bufferX] = {color.r, color.g, color.b, bitmap[j * w + i]}; } } else { if (bufferX >= 0 && bufferX < (int)this->scaled.size.x && bufferY >= 0 && bufferY < (int)this->scaled.size.y) { std::uint8_t alpha = bitmap[j * w + i]; if(alpha != 0) { this->buffer[bufferY * this->scaled.size.x + bufferX] = {color.r, color.g, color.b, bitmap[j * w + i]}; } } } } } break; } case OpaqueType::Transparent: { for (int j = 0; j < h; j++) { for (int i = 0; i < w; i++) { int bufferX = x + i + c_x1 + offsetX; int bufferY = currentY + j + c_y1 + offsetY; // Only draw pixels that are within our scaled buffer bounds if constexpr(Scaling) { if (bufferX >= 0 && bufferX < ScalingBase::bufferWidth && bufferY >= 0 && bufferY < ScalingBase::bufferHeight) { ScalingBase::scalingBuffer[bufferY * ScalingBase::bufferWidth + bufferX] = {color.r, color.g, color.b, bitmap[j * w + i]}; } } else { if (bufferX >= 0 && bufferX < (int)this->scaled.size.x && bufferY >= 0 && bufferY < (int)this->scaled.size.y) { std::uint8_t alpha = bitmap[j * w + i]; if(alpha == 0) { continue; } Vector dst = this->buffer[bufferY * this->scaled.size.x + bufferX]; float srcA = (alpha / 255.0f) * (color.a / 255.0f); float dstA = dst.a / 255.0f; float outA = srcA + dstA * (1.0f - srcA); this->buffer[bufferY * this->scaled.size.x + bufferX] = Vector( static_cast((color.r * srcA + dst.r * dstA * (1.0f - srcA)) / outA), static_cast((color.g * srcA + dst.g * dstA * (1.0f - srcA)) / outA), static_cast((color.b * srcA + dst.b * dstA * (1.0f - srcA)) / outA), static_cast(outA * 255) ); } } } } break; } } x += (int)(ax * scale); if (i + 1 < line.size()) { x += (int)stbtt_GetGlyphKernAdvance(&font.font, codepoint, line[i+1]); } } currentY += lineHeight; } } }; }