minecraft-renderer 0.1.96 → 0.1.98
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 +128 -20
- package/src/three/graphicsBackendBase.ts +1 -0
package/package.json
CHANGED
package/src/three/entities.ts
CHANGED
|
@@ -145,41 +145,111 @@ 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 speaker glyph before their nametag. */
|
|
149
|
+
export type PlayerVoiceState = 'speaking' | 'muted' | 'idle'
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Draws a small speaker 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
|
+
ctx.fillStyle = color
|
|
161
|
+
ctx.strokeStyle = color
|
|
162
|
+
ctx.lineWidth = Math.max(1.5, size * 0.09)
|
|
163
|
+
ctx.lineCap = 'round'
|
|
164
|
+
|
|
165
|
+
// Speaker silhouette: a small box with a cone opening to the right
|
|
166
|
+
const boxWidth = size * 0.22
|
|
167
|
+
const boxHeight = size * 0.34
|
|
168
|
+
const coneWidth = size * 0.24
|
|
169
|
+
const left = -size * 0.44
|
|
170
|
+
|
|
171
|
+
ctx.beginPath()
|
|
172
|
+
ctx.moveTo(left, -boxHeight / 2)
|
|
173
|
+
ctx.lineTo(left + boxWidth, -boxHeight / 2)
|
|
174
|
+
ctx.lineTo(left + boxWidth + coneWidth, -size * 0.5)
|
|
175
|
+
ctx.lineTo(left + boxWidth + coneWidth, size * 0.5)
|
|
176
|
+
ctx.lineTo(left + boxWidth, boxHeight / 2)
|
|
177
|
+
ctx.lineTo(left, boxHeight / 2)
|
|
178
|
+
ctx.closePath()
|
|
179
|
+
ctx.fill()
|
|
180
|
+
|
|
181
|
+
const coneTip = left + boxWidth + coneWidth
|
|
182
|
+
|
|
183
|
+
if (state === 'speaking') {
|
|
184
|
+
ctx.beginPath()
|
|
185
|
+
ctx.arc(coneTip + size * 0.06, 0, size * 0.16, -Math.PI * 0.32, Math.PI * 0.32)
|
|
186
|
+
ctx.stroke()
|
|
187
|
+
ctx.beginPath()
|
|
188
|
+
ctx.arc(coneTip + size * 0.06, 0, size * 0.28, -Math.PI * 0.28, Math.PI * 0.28)
|
|
189
|
+
ctx.stroke()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (state === 'muted') {
|
|
193
|
+
ctx.beginPath()
|
|
194
|
+
ctx.moveTo(-size * 0.46, -size * 0.46)
|
|
195
|
+
ctx.lineTo(size * 0.46, size * 0.46)
|
|
196
|
+
ctx.stroke()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
ctx.restore()
|
|
200
|
+
}
|
|
201
|
+
|
|
148
202
|
function getUsernameTexture(
|
|
149
|
-
{ username, nameTagBackgroundColor = 'rgba(0, 0, 0, 0.3)', nameTagTextOpacity = 255 }: any,
|
|
203
|
+
{ username, nameTagBackgroundColor = 'rgba(0, 0, 0, 0.3)', nameTagTextOpacity = 255, voiceState }: any,
|
|
150
204
|
{ fontFamily = 'mojangles' }: any,
|
|
151
205
|
version: string
|
|
152
206
|
) {
|
|
153
|
-
const
|
|
154
|
-
|
|
207
|
+
const measureCanvas = createCanvas(64, 64)
|
|
155
208
|
const PrismarineChat = PrismarineChatLoader(version)
|
|
156
209
|
|
|
157
|
-
const
|
|
158
|
-
if (!
|
|
210
|
+
const measureCtx = measureCanvas.getContext('2d')
|
|
211
|
+
if (!measureCtx) throw new Error('Could not get 2d context')
|
|
159
212
|
|
|
160
213
|
const fontSize = 48
|
|
161
214
|
const padding = 5
|
|
162
|
-
|
|
215
|
+
measureCtx.font = `${fontSize}px ${fontFamily}`
|
|
163
216
|
|
|
164
217
|
const plainLines = String(typeof username === 'string' ? username : new PrismarineChat(username).toString()).split('\n')
|
|
165
218
|
let textWidth = 0
|
|
166
219
|
for (const line of plainLines) {
|
|
167
|
-
const width =
|
|
220
|
+
const width = measureCtx.measureText(line).width + padding * 2
|
|
168
221
|
if (width > textWidth) textWidth = width
|
|
169
222
|
}
|
|
170
223
|
|
|
171
|
-
|
|
172
|
-
canvas.height = (fontSize + padding) * plainLines.length
|
|
173
|
-
|
|
174
|
-
ctx.fillStyle = nameTagBackgroundColor
|
|
175
|
-
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|
224
|
+
const height = (fontSize + padding) * plainLines.length
|
|
176
225
|
|
|
177
|
-
|
|
226
|
+
// Render the text onto its own transparent canvas so the existing
|
|
227
|
+
// centered-text layout in renderComponent() is unaffected by compositing it
|
|
228
|
+
// alongside the icon. The background plate is painted exactly once, on the
|
|
229
|
+
// final canvas below — painting it here too would double-blend the alpha
|
|
230
|
+
// under the text relative to the icon area, producing a visible seam.
|
|
231
|
+
const textCanvas = createCanvas(textWidth, height)
|
|
232
|
+
const textCtx = textCanvas.getContext('2d')
|
|
233
|
+
if (!textCtx) throw new Error('Could not get 2d context')
|
|
178
234
|
|
|
179
|
-
|
|
235
|
+
textCtx.globalAlpha = nameTagTextOpacity / 255
|
|
236
|
+
const textRendered = renderComponent(username, PrismarineChat, textCanvas, fontSize, 'white', -padding + fontSize)
|
|
180
237
|
if (!textRendered) return undefined
|
|
238
|
+
textCtx.globalAlpha = 1
|
|
239
|
+
|
|
240
|
+
const iconSize = fontSize * 0.85
|
|
241
|
+
const iconAreaWidth = voiceState ? iconSize + padding * 2.5 : 0
|
|
181
242
|
|
|
182
|
-
|
|
243
|
+
const canvas = createCanvas(iconAreaWidth + textWidth, height)
|
|
244
|
+
const ctx = canvas.getContext('2d')
|
|
245
|
+
if (!ctx) throw new Error('Could not get 2d context')
|
|
246
|
+
|
|
247
|
+
ctx.fillStyle = nameTagBackgroundColor
|
|
248
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|
249
|
+
if (voiceState) {
|
|
250
|
+
drawVoiceStateIcon(ctx, voiceState, iconAreaWidth / 2, canvas.height / 2, iconSize)
|
|
251
|
+
}
|
|
252
|
+
ctx.drawImage(textCanvas, iconAreaWidth, 0)
|
|
183
253
|
|
|
184
254
|
return canvas
|
|
185
255
|
}
|
|
@@ -234,6 +304,13 @@ const addNametag = (entity, options: { fontFamily: string }, mesh, version: stri
|
|
|
234
304
|
return nameTag
|
|
235
305
|
}
|
|
236
306
|
|
|
307
|
+
/** Player nametags are placed above the skin model and scaled up; text_display/mob nametags are not. */
|
|
308
|
+
function applyPlayerNametagTransform(nametag: THREE.Object3D | undefined, playerObject: { position: THREE.Vector3; scale: THREE.Vector3 }) {
|
|
309
|
+
if (!nametag) return
|
|
310
|
+
nametag.position.y = playerObject.position.y + playerObject.scale.y * 16 + 3
|
|
311
|
+
nametag.scale.multiplyScalar(12)
|
|
312
|
+
}
|
|
313
|
+
|
|
237
314
|
// todo cleanup
|
|
238
315
|
const nametags = {}
|
|
239
316
|
|
|
@@ -395,6 +472,7 @@ export class Entities {
|
|
|
395
472
|
}
|
|
396
473
|
this.entities = {}
|
|
397
474
|
this.currentSkinUrls = {}
|
|
475
|
+
this.playerVoiceStates = {}
|
|
398
476
|
|
|
399
477
|
// Clean up player entity
|
|
400
478
|
if (this.playerEntity) {
|
|
@@ -737,6 +815,33 @@ export class Entities {
|
|
|
737
815
|
return playerObject
|
|
738
816
|
}
|
|
739
817
|
|
|
818
|
+
/** Current voice chat presence per entity id, keyed as a string. Absent = no voice indicator. */
|
|
819
|
+
playerVoiceStates: Record<string, PlayerVoiceState> = {}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Show or hide the mic glyph on a player's nametag. `state` undefined hides it
|
|
823
|
+
* (e.g. the player left voice chat, or walked out of proximity range).
|
|
824
|
+
* Regenerates the nametag texture, so callers should only invoke this on
|
|
825
|
+
* actual state transitions (speaking start/stop, mute toggle), not per-frame.
|
|
826
|
+
*/
|
|
827
|
+
setPlayerVoiceState(entityId: string | number, username: string, state: PlayerVoiceState | undefined) {
|
|
828
|
+
const key = String(entityId)
|
|
829
|
+
if (state) {
|
|
830
|
+
this.playerVoiceStates[key] = state
|
|
831
|
+
} else {
|
|
832
|
+
delete this.playerVoiceStates[key]
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const e = this.entities[entityId]
|
|
836
|
+
const playerObject = e?.playerObject
|
|
837
|
+
if (!e || !playerObject) return
|
|
838
|
+
const mesh = e.children.find(c => c.name === 'mesh')
|
|
839
|
+
if (!mesh) return
|
|
840
|
+
|
|
841
|
+
const nametag = addNametag({ username, voiceState: state }, this.entitiesOptions, mesh, this.worldRenderer.version)
|
|
842
|
+
applyPlayerNametagTransform(nametag, playerObject)
|
|
843
|
+
}
|
|
844
|
+
|
|
740
845
|
uuidPerSkinUrlsCache = {} as Record<string, { skinUrl?: string; capeUrl?: string }>
|
|
741
846
|
currentSkinUrls = {} as Record<string, string>
|
|
742
847
|
|
|
@@ -1087,6 +1192,7 @@ export class Entities {
|
|
|
1087
1192
|
disposeObject(e)
|
|
1088
1193
|
// todo dispose textures as well ?
|
|
1089
1194
|
delete this.entities[entity.id]
|
|
1195
|
+
delete this.playerVoiceStates[String(entity.id)]
|
|
1090
1196
|
return
|
|
1091
1197
|
}
|
|
1092
1198
|
if (!this.isRenderingAllowed()) return
|
|
@@ -1158,11 +1264,13 @@ export class Entities {
|
|
|
1158
1264
|
mesh = wrapper
|
|
1159
1265
|
|
|
1160
1266
|
if (entity.username) {
|
|
1161
|
-
const nametag = addNametag(
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1267
|
+
const nametag = addNametag(
|
|
1268
|
+
{ ...entity, voiceState: this.playerVoiceStates[entity.id] },
|
|
1269
|
+
{ fontFamily: 'mojangles' },
|
|
1270
|
+
wrapper,
|
|
1271
|
+
this.worldRenderer.version
|
|
1272
|
+
)
|
|
1273
|
+
applyPlayerNametagTransform(nametag, playerObject)
|
|
1166
1274
|
}
|
|
1167
1275
|
} else {
|
|
1168
1276
|
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),
|