minecraft-renderer 0.1.96 → 0.1.97
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/minecraft-renderer.js +26 -26
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +351 -351
- package/package.json +1 -1
- package/src/three/entities.ts +132 -19
- package/src/three/graphicsBackendBase.ts +1 -0
package/package.json
CHANGED
package/src/three/entities.ts
CHANGED
|
@@ -145,41 +145,116 @@ function shortestYawRadians(fromYawRad: number, toYawRad: number): number {
|
|
|
145
145
|
return norm > Math.PI ? norm - TAU_YAW : norm
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/** Voice chat presence for a player, drawn as a small mic glyph before their nametag. */
|
|
149
|
+
export type PlayerVoiceState = 'speaking' | 'muted' | 'idle'
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Draws a small mic glyph centered at the origin of the current transform.
|
|
153
|
+
* `size` is the glyph's bounding box (both width and height).
|
|
154
|
+
*/
|
|
155
|
+
function drawVoiceStateIcon(ctx: OffscreenCanvasRenderingContext2D, state: PlayerVoiceState, cx: number, cy: number, size: number) {
|
|
156
|
+
const color = state === 'speaking' ? '#3ddc47' : state === 'muted' ? '#ff5555' : '#cfcfcf'
|
|
157
|
+
|
|
158
|
+
ctx.save()
|
|
159
|
+
ctx.translate(cx, cy)
|
|
160
|
+
|
|
161
|
+
if (state === 'speaking') {
|
|
162
|
+
ctx.beginPath()
|
|
163
|
+
ctx.arc(0, 0, size * 0.62, 0, Math.PI * 2)
|
|
164
|
+
ctx.strokeStyle = 'rgba(61, 220, 71, 0.55)'
|
|
165
|
+
ctx.lineWidth = Math.max(1, size * 0.08)
|
|
166
|
+
ctx.stroke()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
ctx.fillStyle = color
|
|
170
|
+
ctx.strokeStyle = color
|
|
171
|
+
ctx.lineWidth = Math.max(1, size * 0.12)
|
|
172
|
+
|
|
173
|
+
// Mic body: a rounded capsule
|
|
174
|
+
const bodyWidth = size * 0.34
|
|
175
|
+
const bodyHeight = size * 0.55
|
|
176
|
+
const bodyRadius = bodyWidth / 2
|
|
177
|
+
ctx.beginPath()
|
|
178
|
+
ctx.moveTo(-bodyRadius, -bodyHeight / 2 + bodyRadius)
|
|
179
|
+
ctx.arcTo(-bodyRadius, -bodyHeight / 2, 0, -bodyHeight / 2, bodyRadius)
|
|
180
|
+
ctx.arcTo(bodyRadius, -bodyHeight / 2, bodyRadius, -bodyHeight / 2 + bodyRadius, bodyRadius)
|
|
181
|
+
ctx.lineTo(bodyRadius, bodyHeight / 2 - bodyRadius)
|
|
182
|
+
ctx.arcTo(bodyRadius, bodyHeight / 2, 0, bodyHeight / 2, bodyRadius)
|
|
183
|
+
ctx.arcTo(-bodyRadius, bodyHeight / 2, -bodyRadius, bodyHeight / 2 - bodyRadius, bodyRadius)
|
|
184
|
+
ctx.closePath()
|
|
185
|
+
ctx.fill()
|
|
186
|
+
|
|
187
|
+
// Stand: arc + base line
|
|
188
|
+
ctx.beginPath()
|
|
189
|
+
ctx.arc(0, bodyHeight / 2 - bodyRadius / 2, size * 0.32, Math.PI * 0.15, Math.PI * 0.85, false)
|
|
190
|
+
ctx.stroke()
|
|
191
|
+
ctx.beginPath()
|
|
192
|
+
ctx.moveTo(0, size * 0.5)
|
|
193
|
+
ctx.lineTo(0, size * 0.62)
|
|
194
|
+
ctx.stroke()
|
|
195
|
+
|
|
196
|
+
if (state === 'muted') {
|
|
197
|
+
ctx.beginPath()
|
|
198
|
+
ctx.moveTo(-size * 0.42, -size * 0.42)
|
|
199
|
+
ctx.lineTo(size * 0.42, size * 0.42)
|
|
200
|
+
ctx.stroke()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
ctx.restore()
|
|
204
|
+
}
|
|
205
|
+
|
|
148
206
|
function getUsernameTexture(
|
|
149
|
-
{ username, nameTagBackgroundColor = 'rgba(0, 0, 0, 0.3)', nameTagTextOpacity = 255 }: any,
|
|
207
|
+
{ username, nameTagBackgroundColor = 'rgba(0, 0, 0, 0.3)', nameTagTextOpacity = 255, voiceState }: any,
|
|
150
208
|
{ fontFamily = 'mojangles' }: any,
|
|
151
209
|
version: string
|
|
152
210
|
) {
|
|
153
|
-
const
|
|
154
|
-
|
|
211
|
+
const measureCanvas = createCanvas(64, 64)
|
|
155
212
|
const PrismarineChat = PrismarineChatLoader(version)
|
|
156
213
|
|
|
157
|
-
const
|
|
158
|
-
if (!
|
|
214
|
+
const measureCtx = measureCanvas.getContext('2d')
|
|
215
|
+
if (!measureCtx) throw new Error('Could not get 2d context')
|
|
159
216
|
|
|
160
217
|
const fontSize = 48
|
|
161
218
|
const padding = 5
|
|
162
|
-
|
|
219
|
+
measureCtx.font = `${fontSize}px ${fontFamily}`
|
|
163
220
|
|
|
164
221
|
const plainLines = String(typeof username === 'string' ? username : new PrismarineChat(username).toString()).split('\n')
|
|
165
222
|
let textWidth = 0
|
|
166
223
|
for (const line of plainLines) {
|
|
167
|
-
const width =
|
|
224
|
+
const width = measureCtx.measureText(line).width + padding * 2
|
|
168
225
|
if (width > textWidth) textWidth = width
|
|
169
226
|
}
|
|
170
227
|
|
|
171
|
-
|
|
172
|
-
canvas.height = (fontSize + padding) * plainLines.length
|
|
228
|
+
const height = (fontSize + padding) * plainLines.length
|
|
173
229
|
|
|
174
|
-
|
|
175
|
-
|
|
230
|
+
// Render the text on its own canvas first so the existing centered-text
|
|
231
|
+
// layout in renderComponent() is unaffected by the icon we add alongside it.
|
|
232
|
+
const textCanvas = createCanvas(textWidth, height)
|
|
233
|
+
const textCtx = textCanvas.getContext('2d')
|
|
234
|
+
if (!textCtx) throw new Error('Could not get 2d context')
|
|
176
235
|
|
|
177
|
-
|
|
236
|
+
textCtx.fillStyle = nameTagBackgroundColor
|
|
237
|
+
textCtx.fillRect(0, 0, textCanvas.width, textCanvas.height)
|
|
238
|
+
textCtx.globalAlpha = nameTagTextOpacity / 255
|
|
178
239
|
|
|
179
|
-
const textRendered = renderComponent(username, PrismarineChat,
|
|
240
|
+
const textRendered = renderComponent(username, PrismarineChat, textCanvas, fontSize, 'white', -padding + fontSize)
|
|
180
241
|
if (!textRendered) return undefined
|
|
181
242
|
|
|
182
|
-
|
|
243
|
+
textCtx.globalAlpha = 1
|
|
244
|
+
|
|
245
|
+
if (!voiceState) return textCanvas
|
|
246
|
+
|
|
247
|
+
const iconSize = fontSize * 0.55
|
|
248
|
+
const iconAreaWidth = iconSize + padding * 2
|
|
249
|
+
|
|
250
|
+
const canvas = createCanvas(iconAreaWidth + textWidth, height)
|
|
251
|
+
const ctx = canvas.getContext('2d')
|
|
252
|
+
if (!ctx) throw new Error('Could not get 2d context')
|
|
253
|
+
|
|
254
|
+
ctx.fillStyle = nameTagBackgroundColor
|
|
255
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|
256
|
+
drawVoiceStateIcon(ctx, voiceState, iconAreaWidth / 2, canvas.height / 2, iconSize)
|
|
257
|
+
ctx.drawImage(textCanvas, iconAreaWidth, 0)
|
|
183
258
|
|
|
184
259
|
return canvas
|
|
185
260
|
}
|
|
@@ -234,6 +309,13 @@ const addNametag = (entity, options: { fontFamily: string }, mesh, version: stri
|
|
|
234
309
|
return nameTag
|
|
235
310
|
}
|
|
236
311
|
|
|
312
|
+
/** Player nametags are placed above the skin model and scaled up; text_display/mob nametags are not. */
|
|
313
|
+
function applyPlayerNametagTransform(nametag: THREE.Object3D | undefined, playerObject: { position: THREE.Vector3; scale: THREE.Vector3 }) {
|
|
314
|
+
if (!nametag) return
|
|
315
|
+
nametag.position.y = playerObject.position.y + playerObject.scale.y * 16 + 3
|
|
316
|
+
nametag.scale.multiplyScalar(12)
|
|
317
|
+
}
|
|
318
|
+
|
|
237
319
|
// todo cleanup
|
|
238
320
|
const nametags = {}
|
|
239
321
|
|
|
@@ -395,6 +477,7 @@ export class Entities {
|
|
|
395
477
|
}
|
|
396
478
|
this.entities = {}
|
|
397
479
|
this.currentSkinUrls = {}
|
|
480
|
+
this.playerVoiceStates = {}
|
|
398
481
|
|
|
399
482
|
// Clean up player entity
|
|
400
483
|
if (this.playerEntity) {
|
|
@@ -737,6 +820,33 @@ export class Entities {
|
|
|
737
820
|
return playerObject
|
|
738
821
|
}
|
|
739
822
|
|
|
823
|
+
/** Current voice chat presence per entity id, keyed as a string. Absent = no voice indicator. */
|
|
824
|
+
playerVoiceStates: Record<string, PlayerVoiceState> = {}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Show or hide the mic glyph on a player's nametag. `state` undefined hides it
|
|
828
|
+
* (e.g. the player left voice chat, or walked out of proximity range).
|
|
829
|
+
* Regenerates the nametag texture, so callers should only invoke this on
|
|
830
|
+
* actual state transitions (speaking start/stop, mute toggle), not per-frame.
|
|
831
|
+
*/
|
|
832
|
+
setPlayerVoiceState(entityId: string | number, username: string, state: PlayerVoiceState | undefined) {
|
|
833
|
+
const key = String(entityId)
|
|
834
|
+
if (state) {
|
|
835
|
+
this.playerVoiceStates[key] = state
|
|
836
|
+
} else {
|
|
837
|
+
delete this.playerVoiceStates[key]
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const e = this.entities[entityId]
|
|
841
|
+
const playerObject = e?.playerObject
|
|
842
|
+
if (!e || !playerObject) return
|
|
843
|
+
const mesh = e.children.find(c => c.name === 'mesh')
|
|
844
|
+
if (!mesh) return
|
|
845
|
+
|
|
846
|
+
const nametag = addNametag({ username, voiceState: state }, this.entitiesOptions, mesh, this.worldRenderer.version)
|
|
847
|
+
applyPlayerNametagTransform(nametag, playerObject)
|
|
848
|
+
}
|
|
849
|
+
|
|
740
850
|
uuidPerSkinUrlsCache = {} as Record<string, { skinUrl?: string; capeUrl?: string }>
|
|
741
851
|
currentSkinUrls = {} as Record<string, string>
|
|
742
852
|
|
|
@@ -1087,6 +1197,7 @@ export class Entities {
|
|
|
1087
1197
|
disposeObject(e)
|
|
1088
1198
|
// todo dispose textures as well ?
|
|
1089
1199
|
delete this.entities[entity.id]
|
|
1200
|
+
delete this.playerVoiceStates[String(entity.id)]
|
|
1090
1201
|
return
|
|
1091
1202
|
}
|
|
1092
1203
|
if (!this.isRenderingAllowed()) return
|
|
@@ -1158,11 +1269,13 @@ export class Entities {
|
|
|
1158
1269
|
mesh = wrapper
|
|
1159
1270
|
|
|
1160
1271
|
if (entity.username) {
|
|
1161
|
-
const nametag = addNametag(
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1272
|
+
const nametag = addNametag(
|
|
1273
|
+
{ ...entity, voiceState: this.playerVoiceStates[entity.id] },
|
|
1274
|
+
{ fontFamily: 'mojangles' },
|
|
1275
|
+
wrapper,
|
|
1276
|
+
this.worldRenderer.version
|
|
1277
|
+
)
|
|
1278
|
+
applyPlayerNametagTransform(nametag, playerObject)
|
|
1166
1279
|
}
|
|
1167
1280
|
} else {
|
|
1168
1281
|
mesh = getEntityMesh(this.mcData, entity, this.worldRenderer, this.entitiesOptions, { ...overrides, customModel: entity['customModel'] })
|
|
@@ -35,6 +35,7 @@ export const getBackendMethods = (worldRenderer: WorldRendererThree): any => {
|
|
|
35
35
|
playEntityAnimation: worldRenderer.entities.playAnimation.bind(worldRenderer.entities),
|
|
36
36
|
damageEntity: worldRenderer.entities.handleDamageEvent.bind(worldRenderer.entities),
|
|
37
37
|
updatePlayerSkin: worldRenderer.entities.updatePlayerSkin.bind(worldRenderer.entities),
|
|
38
|
+
setPlayerVoiceState: worldRenderer.entities.setPlayerVoiceState.bind(worldRenderer.entities),
|
|
38
39
|
applyTemporaryPlayerSkinOverride: worldRenderer.entities.applyTemporaryPlayerSkinOverride.bind(worldRenderer.entities),
|
|
39
40
|
changeHandSwingingState: worldRenderer.changeHandSwingingState.bind(worldRenderer),
|
|
40
41
|
getHighestBlocks: worldRenderer.getHighestBlocks.bind(worldRenderer),
|