feat: setup project

This commit is contained in:
2026-09-05 15:27:53 +05:00
parent ae2a7e1ce9
commit c79681dc59
43 changed files with 2194 additions and 14 deletions
+20
View File
@@ -0,0 +1,20 @@
{
"name": "@idle/render",
"private": true,
"version": "0.0.0",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"build": "tsc --noEmit",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@idle/shared": "workspace:*",
"pixi.js": "^8.6.0"
},
"devDependencies": {
"typescript": "^5.6.0"
}
}
+58
View File
@@ -0,0 +1,58 @@
import { Container, Graphics, Text } from 'pixi.js';
import type { AvatarState } from '@idle/shared';
/**
* Заглушка M0: человечек из примитивов. С M1 сюда придут спрайты из паков —
* состав (тело + ник над головой) сохранится.
*/
export class AvatarView {
readonly container = new Container();
private readonly rig = new Container();
private readonly label: Text;
private targetX: number;
private targetY: number;
constructor(av: AvatarState) {
const shadow = new Graphics().ellipse(0, 0, 9, 3.5).fill({ color: 0x000000, alpha: 0.3 });
const body = new Graphics()
.roundRect(-7, -17, 14, 17, 5)
.fill(av.color)
.circle(0, -23, 7.5)
.fill(0xffe3c0);
this.label = new Text({
text: av.name,
style: {
fontFamily: 'monospace',
fontSize: 13,
fontWeight: 'bold',
fill: av.color,
stroke: { color: 0x10131a, width: 3 },
},
});
this.label.anchor.set(0.5, 1);
this.label.y = -36;
this.rig.addChild(shadow, body);
this.container.addChild(this.rig, this.label);
this.targetX = av.x;
this.targetY = av.y;
this.container.position.set(av.x, av.y);
this.apply(av);
}
apply(av: AvatarState): void {
this.targetX = av.x;
this.targetY = av.y;
this.rig.scale.x = av.face === 'left' ? -1 : 1;
if (this.label.text !== av.name) this.label.text = av.name;
if (this.label.style.fill !== av.color) this.label.style.fill = av.color;
}
/** Плавное дотягивание до последнего снапшота — сглаживает дискретность тиков. */
tick(dtMs: number): void {
const k = 1 - Math.exp(-dtMs / 120);
this.container.x += (this.targetX - this.container.x) * k;
this.container.y += (this.targetY - this.container.y) * k;
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from './avatar';
export * from './world';
+64
View File
@@ -0,0 +1,64 @@
import { Application, Container } from 'pixi.js';
import type { AvatarState } from '@idle/shared';
import { AvatarView } from './avatar';
export type WorldStatus = 'connecting' | 'online' | 'reconnecting';
export interface MountWorldOptions {
/** 0 — прозрачный фон (overlay для OBS). */
backgroundAlpha?: number;
background?: number;
onStatus?: (status: WorldStatus) => void;
}
export interface WorldHandle {
applySnapshot(avatars: AvatarState[]): void;
destroy(): void;
}
/** Инициализация pixi-канваса в хосте; состояние мира придёт через applySnapshot. */
export async function mountWorld(host: HTMLElement, opts: MountWorldOptions = {}): Promise<WorldHandle> {
opts.onStatus?.('connecting');
const app = new Application();
await app.init({
backgroundAlpha: opts.backgroundAlpha ?? 1,
background: opts.background ?? 0x151a21,
resizeTo: host,
antialias: true,
});
host.appendChild(app.canvas);
app.canvas.style.display = 'block';
const world = new Container();
app.stage.addChild(world);
const views = new Map<string, AvatarView>();
app.ticker.add((ticker) => {
for (const view of views.values()) view.tick(ticker.deltaMS);
});
return {
applySnapshot(avatars) {
for (const av of avatars) {
let view = views.get(av.id);
if (!view) {
view = new AvatarView(av);
views.set(av.id, view);
world.addChild(view.container);
}
view.apply(av);
}
for (const [id, view] of views) {
if (!avatars.some((av) => av.id === id)) {
view.container.destroy();
views.delete(id);
}
}
},
destroy() {
void app.destroy(true, { children: true });
},
};
}
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"]
},
"include": ["src"]
}