327 lines
10 KiB
TypeScript
327 lines
10 KiB
TypeScript
|
|
/**
|
|||
|
|
* UI Render Utilities
|
|||
|
|
* UI 渲染工具
|
|||
|
|
*
|
|||
|
|
* Shared utility functions for UI render systems to reduce code duplication.
|
|||
|
|
* 渲染系统共享的工具函数,减少代码重复。
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import type { Entity } from '@esengine/ecs-framework';
|
|||
|
|
import { UITransformComponent } from '../../components/UITransformComponent';
|
|||
|
|
import { UIWidgetMarker } from '../../components/UIWidgetMarker';
|
|||
|
|
import type { UIRenderCollector } from './UIRenderCollector';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Ensure entity has UIWidgetMarker component
|
|||
|
|
* 确保实体具有 UIWidgetMarker 组件
|
|||
|
|
*
|
|||
|
|
* Widget components add this marker to prevent UIRectRenderSystem from
|
|||
|
|
* rendering them, as they have their own specialized render systems.
|
|||
|
|
*
|
|||
|
|
* Widget 组件添加此标记以防止 UIRectRenderSystem 渲染它们,
|
|||
|
|
* 因为它们有自己专门的渲染系统。
|
|||
|
|
*
|
|||
|
|
* @param entity - Entity to check/mark
|
|||
|
|
*/
|
|||
|
|
export function ensureUIWidgetMarker(entity: Entity): void {
|
|||
|
|
if (!entity.hasComponent(UIWidgetMarker)) {
|
|||
|
|
entity.addComponent(new UIWidgetMarker());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Computed transform data for rendering
|
|||
|
|
* 用于渲染的计算后变换数据
|
|||
|
|
*/
|
|||
|
|
export interface UIRenderTransform {
|
|||
|
|
/** World X position (bottom-left corner) / 世界 X 坐标(左下角) */
|
|||
|
|
x: number;
|
|||
|
|
/** World Y position (bottom-left corner) / 世界 Y 坐标(左下角) */
|
|||
|
|
y: number;
|
|||
|
|
/** Computed width with scale / 计算后的宽度(含缩放) */
|
|||
|
|
width: number;
|
|||
|
|
/** Computed height with scale / 计算后的高度(含缩放) */
|
|||
|
|
height: number;
|
|||
|
|
/** World alpha / 世界透明度 */
|
|||
|
|
alpha: number;
|
|||
|
|
/** World rotation in radians / 世界旋转(弧度) */
|
|||
|
|
rotation: number;
|
|||
|
|
/** Pivot X (0-1) / X 轴锚点 (0-1) */
|
|||
|
|
pivotX: number;
|
|||
|
|
/** Pivot Y (0-1) / Y 轴锚点 (0-1) */
|
|||
|
|
pivotY: number;
|
|||
|
|
/** Sorting layer name / 排序层名称 */
|
|||
|
|
sortingLayer: string;
|
|||
|
|
/** Order within layer / 层内顺序 */
|
|||
|
|
orderInLayer: number;
|
|||
|
|
/** Render X position (pivot-adjusted) / 渲染 X 坐标(锚点调整后) */
|
|||
|
|
renderX: number;
|
|||
|
|
/** Render Y position (pivot-adjusted) / 渲染 Y 坐标(锚点调整后) */
|
|||
|
|
renderY: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Extract render transform data from UITransformComponent
|
|||
|
|
* 从 UITransformComponent 提取渲染变换数据
|
|||
|
|
*
|
|||
|
|
* 使用 UILayoutSystem 计算的世界坐标。如果 layoutComputed = false,回退到本地坐标。
|
|||
|
|
* Uses world coordinates computed by UILayoutSystem. If layoutComputed = false, falls back to local coordinates.
|
|||
|
|
*
|
|||
|
|
* @param transform - UITransformComponent instance
|
|||
|
|
* @param _entity - Optional entity (unused, for API compatibility)
|
|||
|
|
* @returns Computed render transform, or null if not visible
|
|||
|
|
*/
|
|||
|
|
export function getUIRenderTransform(transform: UITransformComponent, _entity?: Entity): UIRenderTransform | null {
|
|||
|
|
// 如果布局还没计算,跳过渲染(等待 UILayoutSystem 计算 worldOrderInLayer)
|
|||
|
|
// Skip if layout not computed yet (wait for UILayoutSystem to calculate worldOrderInLayer)
|
|||
|
|
if (!transform.layoutComputed) return null;
|
|||
|
|
|
|||
|
|
if (!transform.worldVisible) return null;
|
|||
|
|
|
|||
|
|
// 使用 layoutComputed 判断是否使用世界坐标
|
|||
|
|
// Use layoutComputed to determine whether to use world coordinates
|
|||
|
|
const x = transform.layoutComputed ? transform.worldX : transform.x;
|
|||
|
|
const y = transform.layoutComputed ? transform.worldY : transform.y;
|
|||
|
|
const scaleX = transform.worldScaleX ?? transform.scaleX;
|
|||
|
|
const scaleY = transform.worldScaleY ?? transform.scaleY;
|
|||
|
|
const width = (transform.layoutComputed && transform.computedWidth > 0
|
|||
|
|
? transform.computedWidth
|
|||
|
|
: transform.width) * scaleX;
|
|||
|
|
const height = (transform.layoutComputed && transform.computedHeight > 0
|
|||
|
|
? transform.computedHeight
|
|||
|
|
: transform.height) * scaleY;
|
|||
|
|
const alpha = transform.worldAlpha ?? transform.alpha;
|
|||
|
|
// 角度转弧度 | Convert degrees to radians
|
|||
|
|
const rotationDegrees = transform.worldRotation ?? transform.rotation;
|
|||
|
|
const rotation = (rotationDegrees * Math.PI) / 180;
|
|||
|
|
const pivotX = transform.pivotX;
|
|||
|
|
const pivotY = transform.pivotY;
|
|||
|
|
// 使用继承自 Canvas 的排序层,如果没有则回退到组件本身的排序层
|
|||
|
|
// Use Canvas-inherited sorting layer, fallback to component's own sortingLayer
|
|||
|
|
const sortingLayer = transform.worldSortingLayer ?? transform.sortingLayer;
|
|||
|
|
const orderInLayer = transform.worldOrderInLayer;
|
|||
|
|
|
|||
|
|
// Render position = bottom-left corner + pivot offset
|
|||
|
|
// 渲染位置 = 左下角 + 锚点偏移
|
|||
|
|
const renderX = x + width * pivotX;
|
|||
|
|
const renderY = y + height * pivotY;
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
x,
|
|||
|
|
y,
|
|||
|
|
width,
|
|||
|
|
height,
|
|||
|
|
alpha,
|
|||
|
|
rotation,
|
|||
|
|
pivotX,
|
|||
|
|
pivotY,
|
|||
|
|
sortingLayer,
|
|||
|
|
orderInLayer,
|
|||
|
|
renderX,
|
|||
|
|
renderY
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Border rendering options
|
|||
|
|
* 边框渲染选项
|
|||
|
|
*/
|
|||
|
|
export interface BorderRenderOptions {
|
|||
|
|
/** Border width in pixels / 边框宽度(像素) */
|
|||
|
|
borderWidth: number;
|
|||
|
|
/** Border color (0xRRGGBB) / 边框颜色 */
|
|||
|
|
borderColor: number;
|
|||
|
|
/** Border alpha (0-1) / 边框透明度 */
|
|||
|
|
borderAlpha: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Render a rectangular border
|
|||
|
|
* 渲染矩形边框
|
|||
|
|
*
|
|||
|
|
* @param collector - UIRenderCollector instance
|
|||
|
|
* @param rt - Render transform data
|
|||
|
|
* @param options - Border options
|
|||
|
|
* @param entityId - Entity ID for debugging
|
|||
|
|
* @param orderOffset - Order in layer offset (default: 0)
|
|||
|
|
*/
|
|||
|
|
export function renderBorder(
|
|||
|
|
collector: UIRenderCollector,
|
|||
|
|
rt: UIRenderTransform,
|
|||
|
|
options: BorderRenderOptions,
|
|||
|
|
entityId: number,
|
|||
|
|
orderOffset: number = 0
|
|||
|
|
): void {
|
|||
|
|
const { borderWidth, borderColor, borderAlpha } = options;
|
|||
|
|
if (borderWidth <= 0 || borderAlpha <= 0) return;
|
|||
|
|
|
|||
|
|
const alpha = borderAlpha * rt.alpha;
|
|||
|
|
const orderInLayer = rt.orderInLayer + orderOffset;
|
|||
|
|
|
|||
|
|
// Calculate rect boundaries relative to pivot center
|
|||
|
|
// 计算矩形边界(相对于 pivot 中心)
|
|||
|
|
const left = rt.renderX - rt.width * rt.pivotX;
|
|||
|
|
const bottom = rt.renderY - rt.height * rt.pivotY;
|
|||
|
|
const right = left + rt.width;
|
|||
|
|
const top = bottom + rt.height;
|
|||
|
|
const centerX = (left + right) / 2;
|
|||
|
|
const centerY = (top + bottom) / 2;
|
|||
|
|
|
|||
|
|
// Top border
|
|||
|
|
collector.addRect(
|
|||
|
|
centerX, top - borderWidth / 2,
|
|||
|
|
rt.width, borderWidth,
|
|||
|
|
borderColor, alpha, rt.sortingLayer, orderInLayer,
|
|||
|
|
{ rotation: rt.rotation, pivotX: 0.5, pivotY: 0.5, entityId }
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// Bottom border
|
|||
|
|
collector.addRect(
|
|||
|
|
centerX, bottom + borderWidth / 2,
|
|||
|
|
rt.width, borderWidth,
|
|||
|
|
borderColor, alpha, rt.sortingLayer, orderInLayer,
|
|||
|
|
{ rotation: rt.rotation, pivotX: 0.5, pivotY: 0.5, entityId }
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// Side borders (excluding corners)
|
|||
|
|
const sideBorderHeight = rt.height - borderWidth * 2;
|
|||
|
|
|
|||
|
|
// Left border
|
|||
|
|
collector.addRect(
|
|||
|
|
left + borderWidth / 2, centerY,
|
|||
|
|
borderWidth, sideBorderHeight,
|
|||
|
|
borderColor, alpha, rt.sortingLayer, orderInLayer,
|
|||
|
|
{ rotation: rt.rotation, pivotX: 0.5, pivotY: 0.5, entityId }
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// Right border
|
|||
|
|
collector.addRect(
|
|||
|
|
right - borderWidth / 2, centerY,
|
|||
|
|
borderWidth, sideBorderHeight,
|
|||
|
|
borderColor, alpha, rt.sortingLayer, orderInLayer,
|
|||
|
|
{ rotation: rt.rotation, pivotX: 0.5, pivotY: 0.5, entityId }
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Shadow rendering options
|
|||
|
|
* 阴影渲染选项
|
|||
|
|
*/
|
|||
|
|
export interface ShadowRenderOptions {
|
|||
|
|
/** Shadow offset X / 阴影 X 偏移 */
|
|||
|
|
offsetX: number;
|
|||
|
|
/** Shadow offset Y / 阴影 Y 偏移 */
|
|||
|
|
offsetY: number;
|
|||
|
|
/** Shadow blur radius / 阴影模糊半径 */
|
|||
|
|
blur: number;
|
|||
|
|
/** Shadow color (0xRRGGBB) / 阴影颜色 */
|
|||
|
|
color: number;
|
|||
|
|
/** Shadow alpha (0-1) / 阴影透明度 */
|
|||
|
|
alpha: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Render a shadow behind an element
|
|||
|
|
* 渲染元素后的阴影
|
|||
|
|
*
|
|||
|
|
* @param collector - UIRenderCollector instance
|
|||
|
|
* @param rt - Render transform data
|
|||
|
|
* @param options - Shadow options
|
|||
|
|
* @param entityId - Entity ID for debugging
|
|||
|
|
* @param orderOffset - Order in layer offset (default: -1 to render below)
|
|||
|
|
*/
|
|||
|
|
export function renderShadow(
|
|||
|
|
collector: UIRenderCollector,
|
|||
|
|
rt: UIRenderTransform,
|
|||
|
|
options: ShadowRenderOptions,
|
|||
|
|
entityId: number,
|
|||
|
|
orderOffset: number = -1
|
|||
|
|
): void {
|
|||
|
|
if (options.alpha <= 0) return;
|
|||
|
|
|
|||
|
|
collector.addRect(
|
|||
|
|
rt.renderX + options.offsetX,
|
|||
|
|
rt.renderY + options.offsetY,
|
|||
|
|
rt.width + options.blur * 2,
|
|||
|
|
rt.height + options.blur * 2,
|
|||
|
|
options.color,
|
|||
|
|
options.alpha * rt.alpha,
|
|||
|
|
rt.sortingLayer,
|
|||
|
|
rt.orderInLayer + orderOffset,
|
|||
|
|
{
|
|||
|
|
rotation: rt.rotation,
|
|||
|
|
pivotX: rt.pivotX,
|
|||
|
|
pivotY: rt.pivotY,
|
|||
|
|
entityId
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Color interpolation (linear)
|
|||
|
|
* 颜色线性插值
|
|||
|
|
*
|
|||
|
|
* @param from - Start color (0xRRGGBB)
|
|||
|
|
* @param to - End color (0xRRGGBB)
|
|||
|
|
* @param t - Interpolation factor (0-1)
|
|||
|
|
* @returns Interpolated color
|
|||
|
|
*/
|
|||
|
|
export function lerpColor(from: number, to: number, t: number): number {
|
|||
|
|
const fromR = (from >> 16) & 0xFF;
|
|||
|
|
const fromG = (from >> 8) & 0xFF;
|
|||
|
|
const fromB = from & 0xFF;
|
|||
|
|
|
|||
|
|
const toR = (to >> 16) & 0xFF;
|
|||
|
|
const toG = (to >> 8) & 0xFF;
|
|||
|
|
const toB = to & 0xFF;
|
|||
|
|
|
|||
|
|
const r = Math.round(fromR + (toR - fromR) * t);
|
|||
|
|
const g = Math.round(fromG + (toG - fromG) * t);
|
|||
|
|
const b = Math.round(fromB + (toB - fromB) * t);
|
|||
|
|
|
|||
|
|
return (r << 16) | (g << 8) | b;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Pack color with alpha into ARGB format
|
|||
|
|
* 将颜色和透明度打包为 ARGB 格式
|
|||
|
|
*
|
|||
|
|
* @param color - Color (0xRRGGBB)
|
|||
|
|
* @param alpha - Alpha (0-1)
|
|||
|
|
* @returns Packed color (0xAARRGGBB)
|
|||
|
|
*/
|
|||
|
|
export function packColorWithAlpha(color: number, alpha: number): number {
|
|||
|
|
const a = Math.round(alpha * 255) & 0xFF;
|
|||
|
|
return (a << 24) | (color & 0xFFFFFF);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Get nine-patch position and pivot for consistent rendering
|
|||
|
|
* 获取九宫格位置和 pivot 以实现一致的渲染
|
|||
|
|
*
|
|||
|
|
* NinePatch now uses the same coordinate system as regular rects:
|
|||
|
|
* - Position is the pivot point (same as renderX/renderY)
|
|||
|
|
* - Pivot values determine rotation center
|
|||
|
|
*
|
|||
|
|
* 九宫格现在使用与普通矩形相同的坐标系:
|
|||
|
|
* - 位置是 pivot 点(与 renderX/renderY 相同)
|
|||
|
|
* - pivot 值决定旋转中心
|
|||
|
|
*
|
|||
|
|
* @param rt - Render transform data
|
|||
|
|
* @returns Position and pivot for nine-patch rendering
|
|||
|
|
*/
|
|||
|
|
export function getNinePatchPosition(rt: UIRenderTransform): {
|
|||
|
|
x: number;
|
|||
|
|
y: number;
|
|||
|
|
pivotX: number;
|
|||
|
|
pivotY: number;
|
|||
|
|
} {
|
|||
|
|
return {
|
|||
|
|
x: rt.renderX,
|
|||
|
|
y: rt.renderY,
|
|||
|
|
pivotX: rt.pivotX,
|
|||
|
|
pivotY: rt.pivotY
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|