sdl3/src/Framework/TextRenderer.php
2025-11-29 22:10:58 +01:00

411 lines
11 KiB
PHP

<?php
/**
* TextRenderer - Wrapper for RFont text rendering
*/
namespace PHPNative\Framework;
class TextRenderer
{
private const MAX_TEXTURE_SIZE = 16000;
private $renderer;
private bool $initialized = false;
private string $fontPath = '';
private int $defaultFontSize = 16;
/** @var array<int, resource> */
private array $fonts = [];
private float $colorR = 1.0;
private float $colorG = 1.0;
private float $colorB = 1.0;
private float $colorA = 1.0;
private float $pixelRatio = 1.0;
/**
* Constructor
*
* @param resource $renderer SDL Renderer resource
*/
public function __construct($renderer)
{
$this->renderer = $renderer;
}
/**
* Initialize TTF with a font file
*
* @param string|null $fontPath Path to TTF font file
* @param int $fontSize Default font size
* @param int $atlasWidth Font atlas texture width (ignored, for compatibility)
* @param int $atlasHeight Font atlas texture height (ignored, for compatibility)
* @return bool Success
*/
public function init(
null|string $fontPath = null,
int $fontSize = 16,
int $atlasWidth = 512,
int $atlasHeight = 512,
): bool {
// Try to find a suitable font if none provided
if ($fontPath === null) {
$fontPath = $this->findSystemFont();
}
if (!file_exists($fontPath)) {
error_log("Font file not found: {$fontPath}");
return false;
}
$this->fontPath = $fontPath;
$this->defaultFontSize = $fontSize;
$font = $this->loadFont($fontSize);
if ($font === null) {
error_log("Failed to open font: {$fontPath}");
$this->initialized = false;
return false;
}
$this->initialized = true;
// Set default color to white
$this->setColor(1.0, 1.0, 1.0, 1.0);
return true;
}
private function loadFont(int $size): mixed
{
$size = max(1, (int) round($size)) * $this->pixelRatio;
if (isset($this->fonts[$size])) {
return $this->fonts[$size];
}
if ($this->fontPath === '') {
return null;
}
$font = ttf_open_font($this->fontPath, $size);
if (!$font) {
return null;
}
$this->fonts[$size] = $font;
return $font;
}
private function getFont(int $size): mixed
{
if (!$this->initialized) {
return null;
}
return $this->loadFont($size);
}
/**
* Ensure that rendered text does not exceed the maximum supported
* texture size by truncating very long strings.
*/
private function truncateToMaxTextureSize(string $text, $font): string
{
if ($text === '') {
return $text;
}
$dimensions = ttf_size_text($font, $text);
$width = (int) ($dimensions['w'] ?? 0);
$height = (int) ($dimensions['h'] ?? 0);
if ($width <= self::MAX_TEXTURE_SIZE && $height <= self::MAX_TEXTURE_SIZE) {
return $text;
}
$length = \function_exists('mb_strlen') ? mb_strlen($text) : strlen($text);
if ($length <= 1) {
return $text;
}
// Estimate how much we can keep based on width ratio
$scale = self::MAX_TEXTURE_SIZE / max($width, 1);
$targetLength = max(1, (int) floor($length * $scale));
$substr = \function_exists('mb_substr') ? 'mb_substr' : 'substr';
$text = $substr($text, 0, $targetLength);
// Safety: if still too large, iteratively shrink
for ($i = 0; $i < 3; $i++) {
$dimensions = ttf_size_text($font, $text);
$width = (int) ($dimensions['w'] ?? 0);
$height = (int) ($dimensions['h'] ?? 0);
if ($width <= self::MAX_TEXTURE_SIZE && $height <= self::MAX_TEXTURE_SIZE) {
break;
}
$length = \function_exists('mb_strlen') ? mb_strlen($text) : strlen($text);
if ($length <= 1) {
break;
}
$targetLength = max(1, (int) floor($length * 0.7));
$text = $substr($text, 0, $targetLength);
}
return $text;
}
private function getScaledFontSize(int $size): int
{
if ($this->pixelRatio <= 1.0) {
return $size;
}
return max(1, (int) round($size * $this->pixelRatio));
}
public function setPixelRatio(float $ratio): void
{
$this->pixelRatio = max(1.0, $ratio);
}
/**
* Find a system font
*
* @return string|null Font path or null if not found
*/
private function findSystemFont(): null|string
{
$fontPaths = [
// Linux
'/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',
'/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf',
'/usr/share/fonts/TTF/DejaVuSans.ttf',
// macOS
'/System/Library/Fonts/Helvetica.ttc',
'/Library/Fonts/Arial.ttf',
// Windows
'C:/Windows/Fonts/arial.ttf',
'C:/Windows/Fonts/calibri.ttf',
];
foreach ($fontPaths as $path) {
if (file_exists($path)) {
return $path;
}
}
return null;
}
/**
* Draw text at position
*
* @param string $text Text to draw
* @param int $x X position
* @param int $y Y position
* @param int|null $size Font size
*/
public function drawText(string $text, int $x, int $y, null|int $size = null): void
{
if (!$this->initialized) {
return;
}
$fontSize = $size ?? $this->defaultFontSize;
$baseFont = $this->getFont($fontSize);
if ($baseFont === null) {
return;
}
$renderFontSize = $this->getScaledFontSize($fontSize);
$renderFont = $this->getFont($renderFontSize) ?? $baseFont;
// Convert float color (0.0-1.0) to int (0-255)
$r = (int) ($this->colorR * 255);
$g = (int) ($this->colorG * 255);
$b = (int) ($this->colorB * 255);
// Render text to surface with anti-aliasing (blended mode for smooth text)
if (strlen($text) < 1) {
return;
}
// Truncate extremely long text so that the resulting texture
// stays within the GPU's max texture size.
$safeText = $this->truncateToMaxTextureSize($text, $renderFont);
if ($safeText === '') {
return;
}
$surface = ttf_render_text_blended($renderFont, $safeText, $r, $g, $b);
if (!$surface) {
return;
}
// Create texture from surface
$texture = sdl_create_texture_from_surface($this->renderer, $surface);
if (!$texture) {
return;
}
// Get text size
$textSize = ttf_size_text($baseFont, $safeText);
// Render texture
sdl_render_texture($this->renderer, $texture, [
'x' => $x,
'y' => $y,
'w' => $textSize['w'],
'h' => $textSize['h'],
]);
// Note: Texture and surface are automatically cleaned up by PHP resource destructors
}
/**
* Create an SDL texture for the given text using the current color settings.
*
* @param string $text Text to render into a texture
* @return array|null Returns ['texture' => resource, 'width' => int, 'height' => int] or null on failure
*/
public function createTextTexture(string $text, null|int $size = null): null|array
{
if (!$this->initialized) {
return null;
}
$fontSize = $size ?? $this->defaultFontSize;
$baseFont = $this->getFont($fontSize);
if ($baseFont === null) {
return null;
}
$renderFontSize = $this->getScaledFontSize($fontSize);
$renderFont = $this->getFont($renderFontSize) ?? $baseFont;
$r = (int) ($this->colorR * 255);
$g = (int) ($this->colorG * 255);
$b = (int) ($this->colorB * 255);
if (strlen($text) < 1) {
return null;
}
// Truncate extremely long text so that the resulting texture
// stays within the GPU's max texture size.
$safeText = $this->truncateToMaxTextureSize($text, $renderFont);
if ($safeText === '') {
return null;
}
$surface = ttf_render_text_blended($renderFont, $safeText, $r, $g, $b);
if (!$surface) {
return null;
}
$texture = sdl_create_texture_from_surface($this->renderer, $surface);
if (!$texture) {
return null;
}
$dimensions = ttf_size_text($baseFont, $safeText);
sdl_set_texture_blend_mode($texture, SDL_BLENDMODE_BLEND);
if (\function_exists('sdl_set_texture_alpha_mod')) {
sdl_set_texture_alpha_mod($texture, (int) ($this->colorA * 255));
}
return [
'texture' => $texture,
'width' => (int) $dimensions['w'],
'height' => (int) $dimensions['h'],
];
}
/**
* Set text color
*
* @param float $r Red (0.0 - 1.0)
* @param float $g Green (0.0 - 1.0)
* @param float $b Blue (0.0 - 1.0)
* @param float $a Alpha (0.0 - 1.0)
*/
public function setColor(float $r, float $g, float $b, float $a = 1.0): void
{
$this->colorR = $r;
$this->colorG = $g;
$this->colorB = $b;
$this->colorA = $a;
}
/**
* Measure text dimensions
*
* @param string $text Text to measure
* @param int|null $size Font size
* @return array [width, height]
*/
public function measureText(string $text, null|int $size = null): array
{
if (!$this->initialized) {
return [0, 0];
}
$fontSize = $size ?? $this->defaultFontSize;
$font = $this->getFont($fontSize);
if ($font === null) {
return [0, 0];
}
$dimensions = ttf_size_text($font, $text);
return [(int) $dimensions['w'], (int) $dimensions['h']];
}
/**
* Update framebuffer size (call on window resize)
*
* @param int $width Window width
* @param int $height Window height
*/
public function updateFramebuffer(int $width, int $height): void
{
// SDL3 handles this automatically through the renderer
// This method is kept for compatibility but does nothing
}
/**
* Free TTF resources
*/
public function free(): void
{
if (!$this->initialized) {
return;
}
foreach ($this->fonts as $font) {
ttf_close_font($font);
}
$this->fonts = [];
$this->initialized = false;
}
/**
* Check if initialized
*
* @return bool
*/
public function isInitialized(): bool
{
return $this->initialized;
}
/**
* Destructor
*/
public function __destruct()
{
$this->free();
}
}