411 lines
11 KiB
PHP
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();
|
|
}
|
|
}
|