renderer.js
8.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
// renderer.js — createPixiRenderer:实现 @drama/core Renderer 全部方法,持有 stage / actor 注册表。
//
// 这是 core 接口的实现外壳:把每个接口方法转成对 stage / actor 的操作。
// core 那侧全是数字与字符串(SlotTransform / CameraRenderState / 字符串 / 数字),
// 本包这侧才出现 PIXI.Sprite——本文件是「翻译层」的总装配。
//
// addActor / removeActor / loadScene 作为 Renderer 的**扩展方法**挂在返回对象上
// (scheduler 的 cast 出场 / transition 换班底调用),委托 stage(render-pixi §4)。
import { createAssetLoader } from './assets.js'
import { PixiStage } from './stage.js'
import { PixiActor } from './actor.js'
/**
* 创建满足 @drama/core Renderer 接口的 PixiJS 渲染后端。
* @param {import('pixi.js').Application} app 已 init 的 Pixi Application
* @param {import('./index.js').RendererOpts} opts
* @returns {import('./index.js').PixiRenderer}
*/
export function createPixiRenderer(app, opts) {
const o = opts || /** @type {*} */ ({})
const { PIXI, rig } = o
// 工厂入口自检(架构 §10.3):少注入即在装配时崩,不拖到运行某行。
if (!app) throw new Error('createPixiRenderer: app is required')
if (!PIXI) throw new Error('createPixiRenderer: opts.PIXI is required')
if (!rig) throw new Error('createPixiRenderer: opts.rig is required')
const assets = createAssetLoader({ PIXI, loadTexture: o.loadTexture })
const stage = new PixiStage({ app, PIXI, assets, resolveEffect: o.resolveEffect })
// 道具库解析:name -> propDef({url,scale,anchor,holds}),供手持道具挂载(DramaPlayer 注入 bundle.props)。
const resolveProp = typeof o.resolveProp === 'function' ? o.resolveProp : () => null
/** @type {Map<string, PixiActor>} */
const actors = new Map()
/** @param {string} id */
const getActor = (id) => actors.get(id) || null
// 场景表(id -> sceneDef)+ 转场镜头回调,由装配层(usePlayback)注入。
// transition.to 是场景 id(移植自 OLD play-demo.html:100 `getScene(t.to)`),按表解析成 def → loadScene。
/** @type {Record<string, any>} */
let sceneTable = {}
/** @type {((sceneDef:any)=>void)|null} 换场后施新场景标准镜头(OLD play-demo.html:104 `cut(def.camera)`) */
let onSceneCut = null
// ───────────────────── Renderer 扩展方法(出场 / 换场) ─────────────────────
/**
* 注册一个角色到舞台(建容器、按 rig.slots 贴皮)。
* @param {string} actorId
* @param {import('./index.js').CharacterSkin} skin
* @param {object} placement {x,y,facing,scale}
*/
function addActor(actorId, skin, placement) {
if (actors.has(actorId)) return
const p = /** @type {{x:number,y:number,facing:'left'|'right',scale:number}} */ (
placement || { x: 0, y: 0, facing: 'right', scale: 1 }
)
const actor = new PixiActor({
PIXI,
rig,
skin,
assets,
placement: p,
depthScale: stage.depthScale,
})
stage.addActorRoot(actor.root)
actors.set(actorId, actor)
}
/** @param {string} actorId */
function removeActor(actorId) {
const a = getActor(actorId)
if (!a) return
a.destroy()
actors.delete(actorId)
}
/**
* 一条龙加载场景:背景 + 世界尺寸 + 纵深 + 遮挡片。
* @param {{background?:string,world?:{w:number,h:number},depth?:{nearY:number,farY:number,nearScale:number,farScale:number}|null,occluders?:Array<{polygon:number[][],baseY:number}>}} [def]
*/
async function loadScene(def = {}) {
if (def.world) stage.setWorldSize(def.world.w, def.world.h)
stage.setDepth(def.depth || null)
// 同步已有角色的透视。
for (const a of actors.values()) {
a.depthScale = stage.depthScale
a.setWorldPos(a.worldX, a.worldY)
}
let tex = null
if (def.background) tex = await assets.load(def.background) // 先就绪(遮挡片要裁它)
if (def.background) stage.setBackgroundImage(def.background, tex)
stage.clearOccluders()
for (const o of def.occluders || []) stage.addOccluder(o)
}
// ───────────────────────── Renderer 接口(core §3.3) ─────────────────────────
/** @type {import('@drama/core').Renderer} */
const renderer = {
applyActorPose(actorId, slotTransforms) {
const a = getActor(actorId)
if (a) a.applyActorPose(slotTransforms || {})
},
applyFace(actorId, emotion, talking) {
const a = getActor(actorId)
if (a) a.applyFace(emotion, talking, o.faceFrameDt || 16)
},
applyFacing(actorId, facing, headFacing) {
const a = getActor(actorId)
if (a) a.applyFacing(facing, headFacing)
},
applyCamera(state) {
stage.applyCamera(state)
},
setActorWorldPos(actorId, worldX, worldY) {
const a = getActor(actorId)
if (a) a.setWorldPos(worldX, worldY)
},
moveActor(actorId, worldX, worldY, durMs, easing) {
const a = getActor(actorId)
if (a) a.moveTo(worldX, worldY, durMs, easing)
},
playEffect(name, fxOpts) {
stage.effects.play(name, fxOpts || {})
},
stopEffect(name) {
stage.effects.stop(name)
},
/**
* 角色手持道具:把 name 解析成 propDef(resolveProp)后挂到该角色(actor.holdProp)。
* @param {string} actorId @param {string} name 道具名 @param {string=} hold 握法(carry/raise)
*/
holdProp(actorId, name, hold) {
const a = getActor(actorId)
const def = resolveProp(name)
if (a && def) a.holdProp(def, hold)
},
/** @param {string} actorId 松开该角色手持道具。 */
releaseProp(actorId) {
const a = getActor(actorId)
if (a) a.releaseProp()
},
resetActor(actorId) {
const a = getActor(actorId)
if (a) a.reset()
},
async runTransition(transition) {
// 换场数据落地(移植自 OLD play-demo.html:100-104 transitionTo 的换场段):
// 黑场动画 / 停旧场景音频 / 换班底 由 player-vue(DramaPlayer overlay)与 ctx.applyCast 负责;
// 本包只负责「把 transition.to 解析成场景 def → loadScene(换背景/地面/遮挡/点位)→ 施新场景标准镜头」。
/** @type {{to?:string,scene?:*,clearActors?:boolean,cast?:Array<{actorId:string,skin:*,placement:*}>}} */
const t = transition || {}
if (t.clearActors) {
for (const id of [...actors.keys()]) removeActor(id)
}
// to=场景 id(OLD 行为):查场景表拿 def;也兼容直接传 scene 对象。
const def = t.scene || (t.to != null ? sceneTable[t.to] : null)
if (def) {
await loadScene(def)
if (onSceneCut) onSceneCut(def) // 施新场景标准镜头 cut(def.camera)(OLD play-demo.html:104)
}
if (Array.isArray(t.cast)) {
for (const c of t.cast) addActor(c.actorId, c.skin, c.placement)
}
},
}
// 扩展方法挂上返回对象(scheduler 的 cast / transition 调用)。
return Object.assign(renderer, {
addActor,
removeActor,
loadScene,
// 相机跟随焦点 / 当前世界坐标查询(usePlayback 的 ctx.focusLookup / getActorWorldPos 用,
// 从 renderer 取 actor 当前真实坐标,moveTo 补间后自动是移动后的最新值——见任务清单 6)。
getActorFocus: (/** @type {string} */ id) => {
const a = getActor(id)
return a ? a.getFocusPoint() : null
},
getActorWorldPos: (/** @type {string} */ id) => {
const a = getActor(id)
return a ? { x: a.worldX, y: a.worldY } : null
},
// 转场场景表 + 标准镜头回调注入(transition.to 解析用)。
setSceneTable: (/** @type {Record<string,any>} */ t) => {
sceneTable = t || {}
},
setSceneCut: (/** @type {((def:any)=>void)|null} */ fn) => {
onSceneCut = fn || null
},
// 内部访问(player-vue 装配 ticker 时驱动特效轮播 + 走位补间;非 core 接口)。
_stage: stage,
_actors: actors,
updateEffects: (/** @type {number} */ dtMs) => stage.effects.update(dtMs),
// 每帧推进所有角色走位补间(移植自 OLD:每帧 actor.update 里 lerp 位置)。
updateMoves: (/** @type {number} */ dtMs) => {
for (const a of actors.values()) a.updateMove(dtMs)
},
// 每帧推进所有角色口型(说话态 talkFps 轮播;移植自 OLD actor.update 的表情段,任务清单 11)。
updateFaces: (/** @type {number} */ dtMs) => {
for (const a of actors.values()) a.tickFace(dtMs)
},
// 停所有屏幕特效(转场清场用)。
stopAllEffects: () => stage.effects.stopAll(),
})
}