minecraft-renderer 0.1.0
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/README.md +297 -0
- package/dist/index.html +83 -0
- package/dist/static/image/arrow.6f27b59f.png +0 -0
- package/dist/static/image/blocksAtlasLatest.7850afa3.png +0 -0
- package/dist/static/image/blocksAtlasLegacy.5c76823d.png +0 -0
- package/dist/static/image/itemsAtlasLatest.36036f95.png +0 -0
- package/dist/static/image/itemsAtlasLegacy.dcb1b58d.png +0 -0
- package/dist/static/image/tipped_arrow.6f27b59f.png +0 -0
- package/dist/static/js/365.f05233ab.js +8462 -0
- package/dist/static/js/365.f05233ab.js.LICENSE.txt +52 -0
- package/dist/static/js/async/738.efa27644.js +1 -0
- package/dist/static/js/index.092ec5be.js +56 -0
- package/dist/static/js/lib-polyfill.98986ac5.js +1 -0
- package/dist/static/js/lib-react.5c9129e0.js +2 -0
- package/dist/static/js/lib-react.5c9129e0.js.LICENSE.txt +39 -0
- package/package.json +104 -0
- package/src/assets/destroy_stage_0.png +0 -0
- package/src/assets/destroy_stage_1.png +0 -0
- package/src/assets/destroy_stage_2.png +0 -0
- package/src/assets/destroy_stage_3.png +0 -0
- package/src/assets/destroy_stage_4.png +0 -0
- package/src/assets/destroy_stage_5.png +0 -0
- package/src/assets/destroy_stage_6.png +0 -0
- package/src/assets/destroy_stage_7.png +0 -0
- package/src/assets/destroy_stage_8.png +0 -0
- package/src/assets/destroy_stage_9.png +0 -0
- package/src/examples/README.md +146 -0
- package/src/examples/appViewerExample.ts +205 -0
- package/src/examples/initialMenuStart.ts +161 -0
- package/src/graphicsBackend/appViewer.ts +297 -0
- package/src/graphicsBackend/config.ts +119 -0
- package/src/graphicsBackend/index.ts +10 -0
- package/src/graphicsBackend/playerState.ts +61 -0
- package/src/graphicsBackend/types.ts +143 -0
- package/src/index.ts +97 -0
- package/src/lib/DebugGui.ts +190 -0
- package/src/lib/animationController.ts +85 -0
- package/src/lib/buildSharedConfig.mjs +1 -0
- package/src/lib/cameraBobbing.ts +94 -0
- package/src/lib/canvas2DOverlay.example.ts +361 -0
- package/src/lib/canvas2DOverlay.quickstart.ts +242 -0
- package/src/lib/canvas2DOverlay.ts +381 -0
- package/src/lib/cleanupDecorator.ts +29 -0
- package/src/lib/createPlayerObject.ts +55 -0
- package/src/lib/frameTimingCollector.ts +164 -0
- package/src/lib/guiRenderer.ts +283 -0
- package/src/lib/items.ts +140 -0
- package/src/lib/mesherlogReader.ts +131 -0
- package/src/lib/moreBlockDataGenerated.json +714 -0
- package/src/lib/preflatMap.json +1741 -0
- package/src/lib/simpleUtils.ts +40 -0
- package/src/lib/smoothSwitcher.ts +168 -0
- package/src/lib/spiral.ts +29 -0
- package/src/lib/ui/newStats.ts +120 -0
- package/src/lib/utils/proxy.ts +23 -0
- package/src/lib/utils/skins.ts +63 -0
- package/src/lib/utils.ts +76 -0
- package/src/lib/workerProxy.ts +342 -0
- package/src/lib/worldrendererCommon.ts +1088 -0
- package/src/mesher/mesher.ts +253 -0
- package/src/mesher/models.ts +769 -0
- package/src/mesher/modelsGeometryCommon.ts +142 -0
- package/src/mesher/shared.ts +80 -0
- package/src/mesher/standaloneRenderer.ts +270 -0
- package/src/mesher/test/a.ts +3 -0
- package/src/mesher/test/mesherTester.ts +76 -0
- package/src/mesher/test/playground.ts +19 -0
- package/src/mesher/test/test-perf.ts +74 -0
- package/src/mesher/test/tests.test.ts +56 -0
- package/src/mesher/world.ts +294 -0
- package/src/mesher/worldConstants.ts +1 -0
- package/src/modules/index.ts +11 -0
- package/src/modules/starfield.ts +313 -0
- package/src/modules/types.ts +110 -0
- package/src/playerState/playerState.ts +78 -0
- package/src/playerState/types.ts +36 -0
- package/src/playground/allEntitiesDebug.ts +170 -0
- package/src/playground/baseScene.ts +587 -0
- package/src/playground/mobileControls.tsx +268 -0
- package/src/playground/playground.html +83 -0
- package/src/playground/playground.ts +11 -0
- package/src/playground/playgroundUi.tsx +140 -0
- package/src/playground/reactUtils.ts +71 -0
- package/src/playground/scenes/allEntities.ts +13 -0
- package/src/playground/scenes/entities.ts +37 -0
- package/src/playground/scenes/floorRandom.ts +33 -0
- package/src/playground/scenes/frequentUpdates.ts +148 -0
- package/src/playground/scenes/geometryExport.ts +142 -0
- package/src/playground/scenes/index.ts +12 -0
- package/src/playground/scenes/lightingStarfield.ts +40 -0
- package/src/playground/scenes/main.ts +313 -0
- package/src/playground/scenes/railsCobweb.ts +14 -0
- package/src/playground/scenes/rotationIssue.ts +7 -0
- package/src/playground/scenes/slabsOptimization.ts +16 -0
- package/src/playground/scenes/transparencyIssue.ts +11 -0
- package/src/playground/shared.ts +79 -0
- package/src/resourcesManager/index.ts +5 -0
- package/src/resourcesManager/resourcesManager.ts +314 -0
- package/src/shims/minecraftData.ts +41 -0
- package/src/sign-renderer/index.html +21 -0
- package/src/sign-renderer/index.ts +216 -0
- package/src/sign-renderer/noop.js +1 -0
- package/src/sign-renderer/playground.ts +38 -0
- package/src/sign-renderer/tests.test.ts +69 -0
- package/src/sign-renderer/vite.config.ts +10 -0
- package/src/three/appShared.ts +75 -0
- package/src/three/bannerRenderer.ts +275 -0
- package/src/three/cameraShake.ts +120 -0
- package/src/three/cinimaticScript.ts +350 -0
- package/src/three/documentRenderer.ts +491 -0
- package/src/three/entities.ts +1580 -0
- package/src/three/entity/EntityMesh.ts +707 -0
- package/src/three/entity/animations.js +171 -0
- package/src/three/entity/armorModels.json +204 -0
- package/src/three/entity/armorModels.ts +36 -0
- package/src/three/entity/entities.json +6230 -0
- package/src/three/entity/exportedModels.js +38 -0
- package/src/three/entity/externalTextures.json +1 -0
- package/src/three/entity/models/allay.obj +325 -0
- package/src/three/entity/models/arrow.obj +60 -0
- package/src/three/entity/models/axolotl.obj +509 -0
- package/src/three/entity/models/blaze.obj +601 -0
- package/src/three/entity/models/boat.obj +417 -0
- package/src/three/entity/models/camel.obj +1061 -0
- package/src/three/entity/models/cat.obj +509 -0
- package/src/three/entity/models/chicken.obj +371 -0
- package/src/three/entity/models/cod.obj +371 -0
- package/src/three/entity/models/creeper.obj +279 -0
- package/src/three/entity/models/dolphin.obj +371 -0
- package/src/three/entity/models/ender_dragon.obj +2993 -0
- package/src/three/entity/models/enderman.obj +325 -0
- package/src/three/entity/models/endermite.obj +187 -0
- package/src/three/entity/models/fox.obj +463 -0
- package/src/three/entity/models/frog.obj +739 -0
- package/src/three/entity/models/ghast.obj +463 -0
- package/src/three/entity/models/goat.obj +601 -0
- package/src/three/entity/models/guardian.obj +1015 -0
- package/src/three/entity/models/horse.obj +1061 -0
- package/src/three/entity/models/llama.obj +509 -0
- package/src/three/entity/models/minecart.obj +233 -0
- package/src/three/entity/models/parrot.obj +509 -0
- package/src/three/entity/models/piglin.obj +739 -0
- package/src/three/entity/models/pillager.obj +371 -0
- package/src/three/entity/models/rabbit.obj +555 -0
- package/src/three/entity/models/sheep.obj +555 -0
- package/src/three/entity/models/shulker.obj +141 -0
- package/src/three/entity/models/sniffer.obj +693 -0
- package/src/three/entity/models/spider.obj +509 -0
- package/src/three/entity/models/tadpole.obj +95 -0
- package/src/three/entity/models/turtle.obj +371 -0
- package/src/three/entity/models/vex.obj +325 -0
- package/src/three/entity/models/villager.obj +509 -0
- package/src/three/entity/models/warden.obj +463 -0
- package/src/three/entity/models/witch.obj +647 -0
- package/src/three/entity/models/wolf.obj +509 -0
- package/src/three/entity/models/zombie_villager.obj +463 -0
- package/src/three/entity/objModels.js +1 -0
- package/src/three/fireworks.ts +661 -0
- package/src/three/fireworksRenderer.ts +434 -0
- package/src/three/globals.d.ts +7 -0
- package/src/three/graphicsBackend.ts +274 -0
- package/src/three/graphicsBackendOffThread.ts +107 -0
- package/src/three/hand.ts +89 -0
- package/src/three/holdingBlock.ts +926 -0
- package/src/three/index.ts +20 -0
- package/src/three/itemMesh.ts +427 -0
- package/src/three/modules.d.ts +14 -0
- package/src/three/panorama.ts +308 -0
- package/src/three/panoramaShared.ts +1 -0
- package/src/three/renderSlot.ts +82 -0
- package/src/three/skyboxRenderer.ts +406 -0
- package/src/three/starField.ts +13 -0
- package/src/three/threeJsMedia.ts +731 -0
- package/src/three/threeJsMethods.ts +15 -0
- package/src/three/threeJsParticles.ts +160 -0
- package/src/three/threeJsSound.ts +95 -0
- package/src/three/threeJsUtils.ts +90 -0
- package/src/three/waypointSprite.ts +435 -0
- package/src/three/waypoints.ts +163 -0
- package/src/three/world/cursorBlock.ts +172 -0
- package/src/three/world/vr.ts +257 -0
- package/src/three/worldGeometryExport.ts +259 -0
- package/src/three/worldGeometryHandler.ts +279 -0
- package/src/three/worldRendererThree.ts +1381 -0
- package/src/worldView/index.ts +6 -0
- package/src/worldView/types.ts +66 -0
- package/src/worldView/worldView.ts +424 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BasePlaygroundScene } from '../baseScene'
|
|
2
|
+
|
|
3
|
+
export default class extends BasePlaygroundScene {
|
|
4
|
+
setupWorld () {
|
|
5
|
+
this.addWorldBlock(0, 0, 0, 'water')
|
|
6
|
+
this.addWorldBlock(0, 1, 0, 'lime_stained_glass')
|
|
7
|
+
this.addWorldBlock(0, 0, -1, 'lime_stained_glass')
|
|
8
|
+
this.addWorldBlock(0, -1, 0, 'lime_stained_glass')
|
|
9
|
+
this.addWorldBlock(0, -1, -1, 'stone')
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import WorldLoader, { world } from 'prismarine-world'
|
|
2
|
+
import ChunkLoader from 'prismarine-chunk'
|
|
3
|
+
|
|
4
|
+
export type BlockFaceType = {
|
|
5
|
+
side: number
|
|
6
|
+
textureIndex: number
|
|
7
|
+
tint?: [number, number, number]
|
|
8
|
+
isTransparent?: boolean
|
|
9
|
+
|
|
10
|
+
// for testing
|
|
11
|
+
face?: string
|
|
12
|
+
neighbor?: string
|
|
13
|
+
light?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type BlockType = {
|
|
17
|
+
faces: BlockFaceType[]
|
|
18
|
+
|
|
19
|
+
// for testing
|
|
20
|
+
block: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const makeError = (str: string) => {
|
|
24
|
+
reportError?.(str)
|
|
25
|
+
}
|
|
26
|
+
export const makeErrorCritical = (str: string) => {
|
|
27
|
+
throw new Error(str)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const getSyncWorld = (version: string): world.WorldSync => {
|
|
31
|
+
const World = (WorldLoader as any)(version)
|
|
32
|
+
const Chunk = (ChunkLoader as any)(version)
|
|
33
|
+
|
|
34
|
+
const world = new World(version).sync
|
|
35
|
+
|
|
36
|
+
const methods = getAllMethods(world)
|
|
37
|
+
for (const method of methods) {
|
|
38
|
+
if (method.startsWith('set') && method !== 'setColumn') {
|
|
39
|
+
const oldMethod = world[method].bind(world)
|
|
40
|
+
world[method] = (...args) => {
|
|
41
|
+
const arg = args[0]
|
|
42
|
+
if (arg.x !== undefined && !world.getColumnAt(arg)) {
|
|
43
|
+
world.setColumn(Math.floor(arg.x / 16), Math.floor(arg.z / 16), new Chunk(undefined as any))
|
|
44
|
+
}
|
|
45
|
+
oldMethod(...args)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return world
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getAllMethods (obj) {
|
|
54
|
+
const methods = new Set()
|
|
55
|
+
let currentObj = obj
|
|
56
|
+
|
|
57
|
+
do {
|
|
58
|
+
for (const name of Object.getOwnPropertyNames(currentObj)) {
|
|
59
|
+
if (typeof obj[name] === 'function' && name !== 'constructor') {
|
|
60
|
+
methods.add(name)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} while ((currentObj = Object.getPrototypeOf(currentObj)))
|
|
64
|
+
|
|
65
|
+
return [...methods] as string[]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const delayedIterator = async <T> (arr: T[], delay: number, exec: (item: T, index: number) => Promise<void>, chunkSize = 1) => {
|
|
69
|
+
// if delay is 0 then don't use setTimeout
|
|
70
|
+
for (let i = 0; i < arr.length; i += chunkSize) {
|
|
71
|
+
if (delay) {
|
|
72
|
+
// eslint-disable-next-line no-await-in-loop
|
|
73
|
+
await new Promise(resolve => {
|
|
74
|
+
setTimeout(resolve, delay)
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
await exec(arr[i], i)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { EventEmitter } from 'events'
|
|
2
|
+
import TypedEmitter from 'typed-emitter'
|
|
3
|
+
import blocksAtlases from 'mc-assets/dist/blocksAtlases.json'
|
|
4
|
+
import itemsAtlases from 'mc-assets/dist/itemsAtlases.json'
|
|
5
|
+
import itemDefinitionsJson from 'mc-assets/dist/itemDefinitions.json'
|
|
6
|
+
import blocksAtlasLatest from 'mc-assets/dist/blocksAtlasLatest.png'
|
|
7
|
+
import blocksAtlasLegacy from 'mc-assets/dist/blocksAtlasLegacy.png'
|
|
8
|
+
import itemsAtlasLatest from 'mc-assets/dist/itemsAtlasLatest.png'
|
|
9
|
+
import itemsAtlasLegacy from 'mc-assets/dist/itemsAtlasLegacy.png'
|
|
10
|
+
import christmasPack from 'mc-assets/dist/textureReplacements/christmas'
|
|
11
|
+
import { AtlasParser, ItemsAtlasesOutputJson } from 'mc-assets/dist/atlasParser'
|
|
12
|
+
import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
|
13
|
+
import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
|
|
14
|
+
import { getLoadedItemDefinitionsStore } from 'mc-assets'
|
|
15
|
+
|
|
16
|
+
type ResourceManagerEvents = {
|
|
17
|
+
assetsTexturesUpdated: () => void
|
|
18
|
+
assetsInventoryStarted: () => void
|
|
19
|
+
assetsInventoryReady: () => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class LoadedResourcesTransferrable {
|
|
23
|
+
// todo transfer instead!
|
|
24
|
+
readonly sourceItemDefinitionsJson: any = itemDefinitionsJson
|
|
25
|
+
readonly itemsDefinitionsStore = getLoadedItemDefinitionsStore(this.sourceItemDefinitionsJson)
|
|
26
|
+
|
|
27
|
+
allReady = false
|
|
28
|
+
// Atlas parsers
|
|
29
|
+
itemsAtlasImage!: ImageBitmap
|
|
30
|
+
blocksAtlasImage!: ImageBitmap
|
|
31
|
+
blocksAtlasJson!: ItemsAtlasesOutputJson
|
|
32
|
+
// User data (specific to current resourcepack/version)
|
|
33
|
+
customBlockStates?: Record<string, any>
|
|
34
|
+
customModels?: Record<string, any>
|
|
35
|
+
/** array where the index represents the custom model data value, and the element at that index is the model path to use */
|
|
36
|
+
customItemModelNames: Record<string, string[]> = {}
|
|
37
|
+
customTextures: {
|
|
38
|
+
items?: { tileSize: number | undefined, textures: Record<string, HTMLImageElement> }
|
|
39
|
+
blocks?: { tileSize: number | undefined, textures: Record<string, HTMLImageElement> }
|
|
40
|
+
armor?: { tileSize: number | undefined, textures: Record<string, HTMLImageElement> }
|
|
41
|
+
} = {}
|
|
42
|
+
guiAtlas: { json: any, image: ImageBitmap } | null = null
|
|
43
|
+
guiAtlasVersion = 0
|
|
44
|
+
|
|
45
|
+
itemsRenderer: ItemsRenderer | undefined
|
|
46
|
+
worldBlockProvider?: WorldBlockProvider
|
|
47
|
+
blockstatesModels: any = null
|
|
48
|
+
|
|
49
|
+
version!: string
|
|
50
|
+
texturesVersion!: string
|
|
51
|
+
|
|
52
|
+
constructor(data?: any) {
|
|
53
|
+
if (data) {
|
|
54
|
+
Object.assign(this, data)
|
|
55
|
+
// this.itemsRenderer = new ItemsRenderer(
|
|
56
|
+
// this.version,
|
|
57
|
+
// this.blockstatesModels,
|
|
58
|
+
// this.itemsAtlasParser,
|
|
59
|
+
// this.blocksAtlasParser
|
|
60
|
+
// )
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
prepareForTransfer() {
|
|
65
|
+
delete this.itemsRenderer
|
|
66
|
+
delete this.worldBlockProvider
|
|
67
|
+
this.customTextures = {}
|
|
68
|
+
return this
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ResourcesCurrentConfig {
|
|
74
|
+
version: string
|
|
75
|
+
texturesVersion?: string
|
|
76
|
+
// noBlockstatesModels?: boolean
|
|
77
|
+
noInventoryGui?: boolean
|
|
78
|
+
includeOnlyBlocks?: string[]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface UpdateAssetsRequest {
|
|
82
|
+
_?: false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ResourcesManagerTransferred extends TypedEmitter<ResourceManagerEvents> {
|
|
86
|
+
currentResources: LoadedResourcesTransferrable
|
|
87
|
+
}
|
|
88
|
+
export interface ResourcesManagerCommon extends TypedEmitter<ResourceManagerEvents> {
|
|
89
|
+
currentResources: LoadedResourcesTransferrable | undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const STABLE_MODELS_VERSION = '1.21.4'
|
|
93
|
+
export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<ResourceManagerEvents>) {
|
|
94
|
+
static restorerName = 'ResourcesManager'
|
|
95
|
+
|
|
96
|
+
static restoreTransferred(data: any, worker?: Worker) {
|
|
97
|
+
const resourcesManager = new ResourcesManager()
|
|
98
|
+
const upResources = (data) => {
|
|
99
|
+
resourcesManager.currentResources = new LoadedResourcesTransferrable(data)
|
|
100
|
+
}
|
|
101
|
+
upResources(data.currentResources)
|
|
102
|
+
if (worker) {
|
|
103
|
+
worker.addEventListener('message', ({ data }) => {
|
|
104
|
+
if (data.class === ResourcesManager.restorerName) {
|
|
105
|
+
if (data.type === 'newResources') {
|
|
106
|
+
console.log('[worker] got new resources')
|
|
107
|
+
upResources(data.currentResources)
|
|
108
|
+
}
|
|
109
|
+
if (data.type === 'event') {
|
|
110
|
+
resourcesManager.emit(data.eventName, ...data.args)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
return resourcesManager
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
prepareForTransfer(worker?: Worker) {
|
|
119
|
+
if (worker) {
|
|
120
|
+
// todo do it automatically
|
|
121
|
+
const oldEmit = this.emit.bind(this) as any
|
|
122
|
+
this.emit = ((eventName: keyof ResourceManagerEvents, ...args: any[]) => {
|
|
123
|
+
oldEmit(eventName, ...args)
|
|
124
|
+
worker.postMessage({
|
|
125
|
+
class: ResourcesManager.restorerName,
|
|
126
|
+
type: 'event',
|
|
127
|
+
eventName,
|
|
128
|
+
args,
|
|
129
|
+
})
|
|
130
|
+
// todo handle assetsInventoryReady
|
|
131
|
+
if (eventName === 'assetsTexturesUpdated' || eventName === 'assetsInventoryReady') {
|
|
132
|
+
worker.postMessage({
|
|
133
|
+
class: ResourcesManager.restorerName,
|
|
134
|
+
type: 'newResources',
|
|
135
|
+
currentResources: this.currentResources?.prepareForTransfer(),
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
}) as any
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
__restorer: ResourcesManager.restorerName,
|
|
142
|
+
currentResources: this.currentResources?.prepareForTransfer(),
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Source data (imported, not changing)
|
|
147
|
+
sourceBlockStatesModels: any = null
|
|
148
|
+
readonly sourceBlocksAtlases: any = blocksAtlases
|
|
149
|
+
readonly sourceItemsAtlases: any = itemsAtlases
|
|
150
|
+
|
|
151
|
+
currentResources: LoadedResourcesTransferrable | undefined
|
|
152
|
+
itemsAtlasParser!: AtlasParser
|
|
153
|
+
blocksAtlasParser!: AtlasParser
|
|
154
|
+
currentConfig: ResourcesCurrentConfig | undefined
|
|
155
|
+
abortController = new AbortController()
|
|
156
|
+
_promiseAssetsReadyResolvers = Promise.withResolvers<void>()
|
|
157
|
+
get promiseAssetsReady() {
|
|
158
|
+
return this._promiseAssetsReadyResolvers.promise
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async loadSourceData(version: string) {
|
|
162
|
+
// TODO
|
|
163
|
+
this.sourceBlockStatesModels ??= (await import('mc-assets/dist/blockStatesModels.json')).default
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
resetResources() {
|
|
167
|
+
this.currentResources = new LoadedResourcesTransferrable()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async updateAssetsData(request: UpdateAssetsRequest, unstableSkipEvent = false) {
|
|
171
|
+
if (!this.currentConfig) throw new Error('No config loaded')
|
|
172
|
+
this._promiseAssetsReadyResolvers = Promise.withResolvers()
|
|
173
|
+
const abortController = new AbortController()
|
|
174
|
+
await this.loadSourceData(this.currentConfig.version)
|
|
175
|
+
if (abortController.signal.aborted) return
|
|
176
|
+
|
|
177
|
+
const resources = this.currentResources ?? new LoadedResourcesTransferrable()
|
|
178
|
+
resources.version = this.currentConfig.version
|
|
179
|
+
resources.texturesVersion = this.currentConfig.texturesVersion ?? resources.version
|
|
180
|
+
|
|
181
|
+
resources.blockstatesModels = {
|
|
182
|
+
blockstates: {},
|
|
183
|
+
models: {}
|
|
184
|
+
}
|
|
185
|
+
// todo-low resolve version
|
|
186
|
+
resources.blockstatesModels.blockstates.latest = {
|
|
187
|
+
...this.sourceBlockStatesModels.blockstates.latest,
|
|
188
|
+
...resources.customBlockStates
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
resources.blockstatesModels.models.latest = {
|
|
192
|
+
...this.sourceBlockStatesModels.models.latest,
|
|
193
|
+
...resources.customModels
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.time('recreateAtlases')
|
|
197
|
+
await Promise.all([
|
|
198
|
+
this.recreateBlockAtlas(resources),
|
|
199
|
+
this.recreateItemsAtlas(resources)
|
|
200
|
+
])
|
|
201
|
+
console.timeEnd('recreateAtlases')
|
|
202
|
+
|
|
203
|
+
if (abortController.signal.aborted) return
|
|
204
|
+
|
|
205
|
+
if (resources.version && resources.blockstatesModels && this.itemsAtlasParser && this.blocksAtlasParser) {
|
|
206
|
+
resources.itemsRenderer = new ItemsRenderer(
|
|
207
|
+
resources.version,
|
|
208
|
+
resources.blockstatesModels,
|
|
209
|
+
this.itemsAtlasParser,
|
|
210
|
+
this.blocksAtlasParser
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (abortController.signal.aborted) return
|
|
215
|
+
|
|
216
|
+
this.currentResources = resources
|
|
217
|
+
resources.allReady = true
|
|
218
|
+
if (!unstableSkipEvent) { // todo rework resourcepack optimization
|
|
219
|
+
this.emit('assetsTexturesUpdated')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (this.currentConfig.noInventoryGui) {
|
|
223
|
+
this._promiseAssetsReadyResolvers.resolve()
|
|
224
|
+
} else {
|
|
225
|
+
this.emit('assetsInventoryStarted')
|
|
226
|
+
void this.generateGuiTextures().then(() => {
|
|
227
|
+
if (abortController.signal.aborted) return
|
|
228
|
+
if (!unstableSkipEvent) {
|
|
229
|
+
this.emit('assetsInventoryReady')
|
|
230
|
+
}
|
|
231
|
+
this._promiseAssetsReadyResolvers.resolve()
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async recreateBlockAtlas(resources: LoadedResourcesTransferrable = this.currentResources!) {
|
|
237
|
+
const blockTexturesChanges = {} as Record<string, string>
|
|
238
|
+
const date = new Date()
|
|
239
|
+
if ((date.getMonth() === 11 && date.getDate() >= 24) || (date.getMonth() === 0 && date.getDate() <= 6)) {
|
|
240
|
+
Object.assign(blockTexturesChanges, christmasPack)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const blocksAssetsParser = new AtlasParser(this.sourceBlocksAtlases, blocksAtlasLatest, blocksAtlasLegacy)
|
|
244
|
+
|
|
245
|
+
const customBlockTextures = Object.keys(resources.customTextures.blocks?.textures ?? {})
|
|
246
|
+
console.time('createBlocksAtlas')
|
|
247
|
+
const { atlas: blocksAtlas, canvas: blocksCanvas } = await blocksAssetsParser.makeNewAtlas(
|
|
248
|
+
resources.texturesVersion,
|
|
249
|
+
(textureName) => {
|
|
250
|
+
if (this.currentConfig!.includeOnlyBlocks && !this.currentConfig!.includeOnlyBlocks.includes(textureName)) return false
|
|
251
|
+
const texture = resources.customTextures.blocks?.textures[textureName]
|
|
252
|
+
return blockTexturesChanges[textureName] ?? texture
|
|
253
|
+
},
|
|
254
|
+
undefined,
|
|
255
|
+
undefined,
|
|
256
|
+
customBlockTextures,
|
|
257
|
+
{
|
|
258
|
+
needHorizontalIndexes: !!this.currentConfig!.includeOnlyBlocks,
|
|
259
|
+
}
|
|
260
|
+
)
|
|
261
|
+
console.timeEnd('createBlocksAtlas')
|
|
262
|
+
|
|
263
|
+
this.blocksAtlasParser = new AtlasParser({ latest: blocksAtlas }, blocksCanvas.toDataURL())
|
|
264
|
+
resources.blocksAtlasImage = await createImageBitmap(blocksCanvas)
|
|
265
|
+
resources.blocksAtlasJson = this.blocksAtlasParser.atlas.latest
|
|
266
|
+
|
|
267
|
+
resources.worldBlockProvider = worldBlockProvider(
|
|
268
|
+
resources.blockstatesModels,
|
|
269
|
+
this.blocksAtlasParser.atlas,
|
|
270
|
+
STABLE_MODELS_VERSION
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async recreateItemsAtlas(resources: LoadedResourcesTransferrable = this.currentResources!) {
|
|
275
|
+
const itemsAssetsParser = new AtlasParser(this.sourceItemsAtlases, itemsAtlasLatest, itemsAtlasLegacy)
|
|
276
|
+
const customItemTextures = Object.keys(resources.customTextures.items?.textures ?? {})
|
|
277
|
+
const { atlas: itemsAtlas, canvas: itemsCanvas } = await itemsAssetsParser.makeNewAtlas(
|
|
278
|
+
resources.texturesVersion,
|
|
279
|
+
(textureName) => {
|
|
280
|
+
const texture = resources.customTextures.items?.textures[textureName]
|
|
281
|
+
if (!texture) return
|
|
282
|
+
return texture
|
|
283
|
+
},
|
|
284
|
+
resources.customTextures.items?.tileSize,
|
|
285
|
+
undefined,
|
|
286
|
+
customItemTextures
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
this.itemsAtlasParser = new AtlasParser({ latest: itemsAtlas }, itemsCanvas.toDataURL())
|
|
290
|
+
resources.itemsAtlasImage = await createImageBitmap(itemsCanvas)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async generateGuiTextures() {
|
|
294
|
+
// TODO!
|
|
295
|
+
// await generateGuiAtlas()
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async downloadDebugAtlas(isItems = false) {
|
|
299
|
+
const resources = this.currentResources
|
|
300
|
+
if (!resources) throw new Error('No resources loaded')
|
|
301
|
+
const atlasParser = (isItems ? this.itemsAtlasParser : this.blocksAtlasParser)!
|
|
302
|
+
const dataUrl = await atlasParser.createDebugImage(true)
|
|
303
|
+
const a = document.createElement('a')
|
|
304
|
+
a.href = dataUrl
|
|
305
|
+
a.download = `atlas-debug-${isItems ? 'items' : 'blocks'}.png`
|
|
306
|
+
a.click()
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
destroy() {
|
|
310
|
+
this.abortController.abort()
|
|
311
|
+
this.currentResources = undefined
|
|
312
|
+
this.abortController = new AbortController()
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minecraft Data Shim
|
|
3
|
+
*
|
|
4
|
+
* Provides a minimal minecraft-data implementation that only loads
|
|
5
|
+
* the versions specified in globalThis.includedVersions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const VERSION = '1.16.5'
|
|
9
|
+
|
|
10
|
+
// Lazy-loaded data for each data type
|
|
11
|
+
const createLazyData = (version: string) => ({
|
|
12
|
+
get attributes() { return require("minecraft-data/minecraft-data/data/pc/1.16/attributes.json") },
|
|
13
|
+
get blocks() { return require("minecraft-data/minecraft-data/data/pc/1.16.2/blocks.json") },
|
|
14
|
+
get blockCollisionShapes() { return require("minecraft-data/minecraft-data/data/pc/1.16.1/blockCollisionShapes.json") },
|
|
15
|
+
get biomes() { return require("minecraft-data/minecraft-data/data/pc/1.16.2/biomes.json") },
|
|
16
|
+
get effects() { return require("minecraft-data/minecraft-data/data/pc/1.16.1/effects.json") },
|
|
17
|
+
get items() { return require("minecraft-data/minecraft-data/data/pc/1.16.2/items.json") },
|
|
18
|
+
get enchantments() { return require("minecraft-data/minecraft-data/data/pc/1.16.4/enchantments.json") },
|
|
19
|
+
get recipes() { return require("minecraft-data/minecraft-data/data/pc/1.16.2/recipes.json") },
|
|
20
|
+
get instruments() { return require("minecraft-data/minecraft-data/data/pc/1.16.1/instruments.json") },
|
|
21
|
+
get materials() { return require("minecraft-data/minecraft-data/data/pc/1.16.2/materials.json") },
|
|
22
|
+
get language() { return require("minecraft-data/minecraft-data/data/pc/1.16.1/language.json") },
|
|
23
|
+
get entities() { return require("minecraft-data/minecraft-data/data/pc/1.16.2/entities.json") },
|
|
24
|
+
get protocol() { return require("minecraft-data/minecraft-data/data/pc/1.16.2/protocol.json") },
|
|
25
|
+
get windows() { return require("minecraft-data/minecraft-data/data/pc/1.16.1/windows.json") },
|
|
26
|
+
get version() { return require("minecraft-data/minecraft-data/data/pc/1.16.5/version.json") },
|
|
27
|
+
get foods() { return require("minecraft-data/minecraft-data/data/pc/1.16.1/foods.json") },
|
|
28
|
+
get particles() { return require("minecraft-data/minecraft-data/data/pc/1.16/particles.json") },
|
|
29
|
+
get blockLoot() { return require("minecraft-data/minecraft-data/data/pc/1.16.2/blockLoot.json") },
|
|
30
|
+
get entityLoot() { return require("minecraft-data/minecraft-data/data/pc/1.16.2/entityLoot.json") },
|
|
31
|
+
get loginPacket() { return require("minecraft-data/minecraft-data/data/pc/1.16.2/loginPacket.json") },
|
|
32
|
+
get tints() { return require("minecraft-data/minecraft-data/data/pc/1.16.2/tints.json") },
|
|
33
|
+
get mapIcons() { return require("minecraft-data/minecraft-data/data/pc/1.16/mapIcons.json") },
|
|
34
|
+
get sounds() { return require("minecraft-data/minecraft-data/data/pc/1.16/sounds.json") }
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
pc: {
|
|
39
|
+
[VERSION]: createLazyData(VERSION),
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, viewport-fit=cover" />
|
|
7
|
+
<title>%VITE_NAME%</title>
|
|
8
|
+
<style>
|
|
9
|
+
@font-face {
|
|
10
|
+
font-family: mojangles;
|
|
11
|
+
src: url(../../../assets/mojangles.ttf);
|
|
12
|
+
}
|
|
13
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
|
|
16
|
+
<body>
|
|
17
|
+
<div id="root" style="font-family: mojangles;">test</div>
|
|
18
|
+
<script src="./playground.ts" type="module"></script>
|
|
19
|
+
</body>
|
|
20
|
+
|
|
21
|
+
</html>
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import type { ChatMessage } from 'prismarine-chat'
|
|
2
|
+
import { createCanvas } from '../lib/utils'
|
|
3
|
+
|
|
4
|
+
type SignBlockEntity = {
|
|
5
|
+
Color?: string
|
|
6
|
+
GlowingText?: 0 | 1
|
|
7
|
+
Text1?: string
|
|
8
|
+
Text2?: string
|
|
9
|
+
Text3?: string
|
|
10
|
+
Text4?: string
|
|
11
|
+
} | {
|
|
12
|
+
// todo
|
|
13
|
+
is_waxed?: 0 | 1
|
|
14
|
+
front_text: {
|
|
15
|
+
color: string
|
|
16
|
+
messages: string[]
|
|
17
|
+
// todo
|
|
18
|
+
has_glowing_text?: 0 | 1
|
|
19
|
+
}
|
|
20
|
+
// todo
|
|
21
|
+
// back_text: {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type JsonEncodedType = string | null | Record<string, any>
|
|
25
|
+
|
|
26
|
+
const parseSafe = (text: string, task: string) => {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(text)
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.warn(`Failed to parse ${task}`, e)
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const LEGACY_COLORS = {
|
|
36
|
+
black: '#000000',
|
|
37
|
+
dark_blue: '#0000AA',
|
|
38
|
+
dark_green: '#00AA00',
|
|
39
|
+
dark_aqua: '#00AAAA',
|
|
40
|
+
dark_red: '#AA0000',
|
|
41
|
+
dark_purple: '#AA00AA',
|
|
42
|
+
gold: '#FFAA00',
|
|
43
|
+
gray: '#AAAAAA',
|
|
44
|
+
dark_gray: '#555555',
|
|
45
|
+
blue: '#5555FF',
|
|
46
|
+
green: '#55FF55',
|
|
47
|
+
aqua: '#55FFFF',
|
|
48
|
+
red: '#FF5555',
|
|
49
|
+
light_purple: '#FF55FF',
|
|
50
|
+
yellow: '#FFFF55',
|
|
51
|
+
white: '#FFFFFF',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const renderSign = (
|
|
55
|
+
blockEntity: SignBlockEntity,
|
|
56
|
+
isHanging: boolean,
|
|
57
|
+
PrismarineChat: typeof ChatMessage,
|
|
58
|
+
ctxHook = (ctx) => { },
|
|
59
|
+
canvasCreator = (width, height): OffscreenCanvas => { return createCanvas(width, height) }
|
|
60
|
+
) => {
|
|
61
|
+
// todo don't use texture rendering, investigate the font rendering when possible
|
|
62
|
+
// or increase factor when needed
|
|
63
|
+
const factor = 40
|
|
64
|
+
const fontSize = 1.6 * factor
|
|
65
|
+
const signboardY = [16, 9]
|
|
66
|
+
const heightOffset = signboardY[0] - signboardY[1]
|
|
67
|
+
const heightScalar = heightOffset / 16
|
|
68
|
+
// todo the text should be clipped based on it's render width (needs investigate)
|
|
69
|
+
|
|
70
|
+
const texts = 'front_text' in blockEntity ? /* > 1.20 */ blockEntity.front_text.messages : [
|
|
71
|
+
blockEntity.Text1,
|
|
72
|
+
blockEntity.Text2,
|
|
73
|
+
blockEntity.Text3,
|
|
74
|
+
blockEntity.Text4
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
if (!texts.some((text) => text !== 'null')) {
|
|
78
|
+
return undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const canvas = canvasCreator(16 * factor, heightOffset * factor)
|
|
82
|
+
|
|
83
|
+
const _ctx = canvas.getContext('2d')!
|
|
84
|
+
|
|
85
|
+
ctxHook(_ctx)
|
|
86
|
+
const defaultColor = ('front_text' in blockEntity ? blockEntity.front_text.color : blockEntity.Color) || 'black'
|
|
87
|
+
for (const [lineNum, text] of texts.slice(0, 4).entries()) {
|
|
88
|
+
if (text === 'null') continue
|
|
89
|
+
renderComponent(text, PrismarineChat, canvas, fontSize, defaultColor, fontSize * (lineNum + 1) + (isHanging ? 0 : -8))
|
|
90
|
+
}
|
|
91
|
+
return canvas
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const renderComponent = (
|
|
95
|
+
text: JsonEncodedType | string | undefined,
|
|
96
|
+
PrismarineChat: typeof ChatMessage,
|
|
97
|
+
canvas: OffscreenCanvas,
|
|
98
|
+
fontSize: number,
|
|
99
|
+
defaultColor: string,
|
|
100
|
+
offset = 0
|
|
101
|
+
) => {
|
|
102
|
+
// todo: in pre flatenning it seems the format was not json
|
|
103
|
+
const parsed = typeof text === 'string' && (text?.startsWith('{') || text?.startsWith('"')) ? parseSafe(text ?? '""', 'sign text') : text
|
|
104
|
+
if (!parsed || (typeof parsed !== 'object' && typeof parsed !== 'string')) return
|
|
105
|
+
// todo fix type
|
|
106
|
+
|
|
107
|
+
const ctx = canvas.getContext('2d')!
|
|
108
|
+
if (!ctx) throw new Error('Could not get 2d context')
|
|
109
|
+
ctx.imageSmoothingEnabled = false
|
|
110
|
+
ctx.font = `${fontSize}px mojangles`
|
|
111
|
+
|
|
112
|
+
type Formatting = {
|
|
113
|
+
color: string | undefined
|
|
114
|
+
underlined: boolean | undefined
|
|
115
|
+
strikethrough: boolean | undefined
|
|
116
|
+
bold: boolean | undefined
|
|
117
|
+
italic: boolean | undefined
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
type Message = ChatMessage & Formatting & { text: string }
|
|
121
|
+
|
|
122
|
+
const message = new PrismarineChat(parsed) as Message
|
|
123
|
+
|
|
124
|
+
const toRenderCanvas: Array<{
|
|
125
|
+
fontStyle: string
|
|
126
|
+
fillStyle: string
|
|
127
|
+
underlineStyle: boolean
|
|
128
|
+
strikeStyle: boolean
|
|
129
|
+
offset: number
|
|
130
|
+
text: string
|
|
131
|
+
}> = []
|
|
132
|
+
let visibleFormatting = false
|
|
133
|
+
let plainText = ''
|
|
134
|
+
let textOffset = offset
|
|
135
|
+
const textWidths: number[] = []
|
|
136
|
+
|
|
137
|
+
const renderText = (component: Message, parentFormatting?: Formatting | undefined) => {
|
|
138
|
+
const { text } = component
|
|
139
|
+
const formatting = {
|
|
140
|
+
color: component.color ?? parentFormatting?.color,
|
|
141
|
+
underlined: component.underlined ?? parentFormatting?.underlined,
|
|
142
|
+
strikethrough: component.strikethrough ?? parentFormatting?.strikethrough,
|
|
143
|
+
bold: component.bold ?? parentFormatting?.bold,
|
|
144
|
+
italic: component.italic ?? parentFormatting?.italic
|
|
145
|
+
}
|
|
146
|
+
visibleFormatting = visibleFormatting || formatting.underlined || formatting.strikethrough || false
|
|
147
|
+
if (text?.includes('\n')) {
|
|
148
|
+
for (const line of text.split('\n')) {
|
|
149
|
+
addTextPart(line, formatting)
|
|
150
|
+
textOffset += fontSize
|
|
151
|
+
plainText = ''
|
|
152
|
+
}
|
|
153
|
+
} else if (text) {
|
|
154
|
+
addTextPart(text, formatting)
|
|
155
|
+
}
|
|
156
|
+
if (component.extra) {
|
|
157
|
+
for (const child of component.extra) {
|
|
158
|
+
renderText(child as Message, formatting)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const addTextPart = (text: string, formatting: Formatting) => {
|
|
164
|
+
plainText += text
|
|
165
|
+
textWidths[textOffset] = ctx.measureText(plainText).width
|
|
166
|
+
let color = formatting.color ?? defaultColor
|
|
167
|
+
if (!color.startsWith('#')) {
|
|
168
|
+
color = LEGACY_COLORS[color.toLowerCase()] || color
|
|
169
|
+
}
|
|
170
|
+
toRenderCanvas.push({
|
|
171
|
+
fontStyle: `${formatting.bold ? 'bold' : ''} ${formatting.italic ? 'italic' : ''}`,
|
|
172
|
+
fillStyle: color,
|
|
173
|
+
underlineStyle: formatting.underlined ?? false,
|
|
174
|
+
strikeStyle: formatting.strikethrough ?? false,
|
|
175
|
+
offset: textOffset,
|
|
176
|
+
text
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
renderText(message)
|
|
181
|
+
|
|
182
|
+
// skip rendering empty lines
|
|
183
|
+
if (!visibleFormatting && !message.toString().trim()) return
|
|
184
|
+
|
|
185
|
+
let renderedWidth = 0
|
|
186
|
+
let previousOffsetY = 0
|
|
187
|
+
for (const { fillStyle, fontStyle, underlineStyle, strikeStyle, offset: offsetY, text } of toRenderCanvas) {
|
|
188
|
+
if (previousOffsetY !== offsetY) {
|
|
189
|
+
renderedWidth = 0
|
|
190
|
+
}
|
|
191
|
+
previousOffsetY = offsetY
|
|
192
|
+
ctx.fillStyle = fillStyle
|
|
193
|
+
ctx.textRendering = 'optimizeLegibility'
|
|
194
|
+
ctx.font = `${fontStyle} ${fontSize}px mojangles`
|
|
195
|
+
const textWidth = textWidths[offsetY] ?? ctx.measureText(text).width
|
|
196
|
+
const offsetX = (canvas.width - textWidth) / 2 + renderedWidth
|
|
197
|
+
ctx.fillText(text, offsetX, offsetY)
|
|
198
|
+
if (strikeStyle) {
|
|
199
|
+
ctx.lineWidth = fontSize / 8
|
|
200
|
+
ctx.strokeStyle = fillStyle
|
|
201
|
+
ctx.beginPath()
|
|
202
|
+
ctx.moveTo(offsetX, offsetY - ctx.lineWidth * 2.5)
|
|
203
|
+
ctx.lineTo(offsetX + ctx.measureText(text).width, offsetY - ctx.lineWidth * 2.5)
|
|
204
|
+
ctx.stroke()
|
|
205
|
+
}
|
|
206
|
+
if (underlineStyle) {
|
|
207
|
+
ctx.lineWidth = fontSize / 8
|
|
208
|
+
ctx.strokeStyle = fillStyle
|
|
209
|
+
ctx.beginPath()
|
|
210
|
+
ctx.moveTo(offsetX, offsetY + ctx.lineWidth)
|
|
211
|
+
ctx.lineTo(offsetX + ctx.measureText(text).width, offsetY + ctx.lineWidth)
|
|
212
|
+
ctx.stroke()
|
|
213
|
+
}
|
|
214
|
+
renderedWidth += ctx.measureText(text).width
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = {}
|