InfoLens / client /src /ts /vis /RenderAnimator.ts
dqy08's picture
initial beta release
494c9e4
/**
* 渲染动画器 - 独立的动态渲染模块
* 提供分批渲染和顺序动画效果(颜色瞬间变化,但按顺序从上到下显示),不影响主干代码
*/
export interface TokenRenderTask {
tokenObj: any;
index: number;
offset: [number, number];
}
export interface RenderCallback {
(task: TokenRenderTask, containerText: string, rd: any): void;
}
export interface RenderAnimatorOptions {
/** 是否启用动画渲染 */
enabled?: boolean;
/** 每批处理的token数量(根据总数动态调整) */
batchSize?: number | ((totalTokens: number) => number);
/** 每批之间的延迟(毫秒)- 控制渲染速度,值越大越慢 */
delayBetweenBatches?: number;
}
/**
* 渲染动画器类
* 提供分批渲染和顺序动画功能(颜色瞬间变化,按顺序从上到下显示)
*/
export class RenderAnimator {
private options: Required<RenderAnimatorOptions>;
constructor(options: RenderAnimatorOptions = {}) {
this.options = {
enabled: options.enabled ?? true,
batchSize: options.batchSize ?? 32, // 初始批次大小,后续会每次加倍
delayBetweenBatches: options.delayBetweenBatches ?? 8,
};
}
/**
* 执行动画渲染
* @param tasks token渲染任务数组(应该按从后往前的顺序)
* @param containerText 容器文本
* @param rd 分析结果数据
* @param renderCallback 渲染回调函数
* @param baseNode 容器DOM节点(用于查找token元素)
* @returns Promise,渲染完成后resolve
*/
public async renderWithAnimation(
tasks: TokenRenderTask[],
containerText: string,
rd: any,
renderCallback: RenderCallback,
baseNode?: Node
): Promise<void> {
if (!this.options.enabled || tasks.length === 0) {
// 如果禁用动画或没有任务,直接同步渲染
tasks.forEach(task => {
renderCallback(task, containerText, rd);
});
return;
}
const totalTokens = tasks.length;
// 获取初始批次大小
const initialBatchSize = typeof this.options.batchSize === 'function'
? this.options.batchSize(totalTokens)
: this.options.batchSize;
// 第一步:先按从后往前的顺序同步渲染所有token(避免Range跨越问题)
// 但保持它们为透明状态(不添加动画类)
for (let i = tasks.length - 1; i >= 0; i--) {
renderCallback(tasks[i], containerText, rd);
}
// 第二步:按从前往后的顺序(从上到下)分批添加动画效果
// 批次大小每次加倍,形成越来越快的视觉效果
let currentIndex = 0;
let currentBatchSize = initialBatchSize;
// 第一批处理之前也添加延迟
await new Promise(resolve => setTimeout(resolve, this.options.delayBetweenBatches));
while (currentIndex < tasks.length) {
const batch: TokenRenderTask[] = [];
// 收集当前批次的token(从前往后)
const actualBatchSize = Math.min(currentBatchSize, tasks.length - currentIndex);
for (let j = 0; j < actualBatchSize; j++) {
batch.push(tasks[currentIndex + j]);
}
// 为当前批次的token瞬间设置颜色(按顺序从上到下)
this.animateBatch(batch.map(t => t.index), baseNode);
// 更新索引
currentIndex += actualBatchSize;
// 如果不是最后一批,等待一段时间再处理下一批,并将批次大小乘以1.5(取整)
if (currentIndex < tasks.length) {
await new Promise(resolve => setTimeout(resolve, this.options.delayBetweenBatches));
currentBatchSize = Math.floor(currentBatchSize * 1.5); // 批次大小乘以1.5并取整,形成加速效果
}
}
}
/**
* 为一批token瞬间设置颜色(无过渡动画,只有顺序效果)
* @param tokenIndices token索引数组
* @param baseNode 容器DOM节点
*/
private animateBatch(tokenIndices: number[], baseNode?: Node): void {
if (!baseNode) return;
// 使用requestAnimationFrame确保DOM已更新
requestAnimationFrame(() => {
tokenIndices.forEach(tokenIndex => {
const tokenElement = (baseNode as Element).querySelector(`[data-token-index="${tokenIndex}"]`) as HTMLElement;
if (tokenElement) {
// 从data属性读取目标颜色
const targetColor = tokenElement.getAttribute('data-target-color');
if (targetColor) {
// 确保没有过渡动画,颜色瞬间变化
tokenElement.style.transition = 'none';
// 瞬间设置目标背景色
tokenElement.style.backgroundColor = targetColor;
}
}
});
});
}
/**
* 设置是否启用动画
*/
public setEnabled(enabled: boolean): void {
this.options.enabled = enabled;
}
/**
* 获取是否启用动画
*/
public isEnabled(): boolean {
return this.options.enabled;
}
}