Initial commit

This commit is contained in:
Blue (Lukas Rieger) 2021-03-09 12:58:17 +01:00
commit 039215d7fe
No known key found for this signature in database
GPG Key ID: 904C4995F9E1F800
40 changed files with 15481 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "BlueMapWeb"]
path = BlueMapWeb
url = https://github.com/BlueMap-Minecraft/BlueMapWeb

1
BlueMapWeb Submodule

@ -0,0 +1 @@
Subproject commit 760368eaa2adae49809674ca153a215296f63f28

24
README.md Normal file
View File

@ -0,0 +1,24 @@
# bluemapvue
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

5
babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

12813
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

67
package.json Normal file
View File

@ -0,0 +1,67 @@
{
"name": "bluemapvue",
"version": "0.1.0",
"description": "A vue based frontend to load and display Minecraft maps generated by BlueMap.",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/BlueMap-Minecraft/BlueMapWeb.git"
},
"keywords": [
"minecraft",
"minecraft-mod",
"minecraft-plugin",
"threejs",
"webgl",
"bluemap"
],
"author": "Lukas Rieger <contact@bluecolored.de> (https://bluecolored.de/)",
"license": "MIT",
"bugs": {
"url": "https://github.com/BlueMap-Minecraft/BlueMap/issues"
},
"homepage": "https://bluecolo.red/bluemap",
"dependencies": {
"core-js": "^3.6.5",
"vue": "^2.6.11",
"bluemap": "file:./BlueMapWeb"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"node-sass": "^5.0.0",
"sass-loader": "^10.1.1",
"vue-template-compiler": "^2.6.11"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {
"no-unused-vars": "off",
"no-prototype-builtins": "off"
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
],
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
}
}

BIN
public/assets/steve.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

17
public/index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
<link rel="icon" href="favicon.png">
<title>BlueMap</title>
</head>
<body>
<noscript>
<strong>Sorry but BlueMap doesn't work without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="map-container"></div>
<div id="app"></div>
</body>
</html>

49
src/App.vue Normal file
View File

@ -0,0 +1,49 @@
<template>
<div id="app" :class="{'theme-light': appState.theme === 'light', 'theme-dark': appState.theme === 'dark'}">
<ControlBar />
<MainMenu :menu="appState.menu" />
</div>
</template>
<script>
import ControlBar from "@/components/ControlBar/ControlBar";
import MainMenu from "@/components/Menu/MainMenu";
export default {
name: 'App',
components: {
MainMenu,
ControlBar
},
data() {
return {
appState: this.$bluemap.appState,
}
}
}
</script>
<style lang="scss">
@import "~@/scss/global.scss";
#map-container {
position: absolute;
width: 100%;
height: 100%;
}
#app {
position: absolute;
width: 100%;
height: 100%;
z-index: 100; // put over bluemap markers
pointer-events: none;
font-size: 1rem;
@media (max-width: $mobile-break) {
font-size: 1.5rem;
}
}
</style>

View File

@ -0,0 +1,72 @@
<template>
<SvgButton class="compass" @action="action">
<svg viewBox="0 0 30 30" :style="style">
<path class="north" d="M14.792,1.04c0.114-0.354,0.299-0.354,0.412,0l4.089,12.729c0.114,0.353-0.097,0.642-0.468,0.642
l-7.651,0.001c-0.371,0-0.581-0.288-0.468-0.642L14.792,1.04z"/>
<path class="south" d="M10.707,16.23c-0.114-0.353,0.097-0.642,0.468-0.642l7.651-0.001c0.371,0,0.581,0.289,0.468,0.642
l-4.086,12.73c-0.113,0.353-0.299,0.353-0.412,0L10.707,16.23z"/>
</svg>
</SvgButton>
</template>
<script>
import {animate, EasingFunctions} from "bluemap/src/util/Utils";
import SvgButton from "@/components/ControlBar/SvgButton";
let animation;
export default {
name: "Compass",
components: {SvgButton},
data() {
return {
controls: this.$bluemap.mapViewer.controlsManager.data
}
},
computed: {
style() {
return {transform: "translate(-50%, -50%) rotate(" + (-this.controls.rotation) + "rad)"}
}
},
methods: {
action(evt) {
evt.preventDefault();
if (animation) animation.cancel();
let startRotation = this.controls.rotation;
animation = animate(t => {
this.controls.rotation = startRotation * (1-EasingFunctions.easeOutQuad(t));
}, 300);
}
}
}
</script>
<style lang="scss">
.compass {
svg {
height: 1.8em;
.north {
fill: var(--theme-fg);
}
.south {
fill: var(--theme-fg-light);
}
}
&:active {
svg {
.north {
fill: var(--theme-bg);
}
.south {
fill: var(--theme-bg-light);
}
}
}
}
</style>

View File

@ -0,0 +1,167 @@
<template>
<div class="control-bar">
<MenuButton :close="appState.menu.isOpen" :back="false" @action="appState.menu.reOpenPage()" title="Menu" />
<div class="space thin-hide"></div>
<SvgButton v-if="appState.maps.length > 0" class="thin-hide" title="Map-List"
@action="appState.menu.openPage('maps', 'Maps')">
<svg viewBox="0 0 30 30">
<polygon points="26.708,22.841 19.049,25.186 11.311,20.718 3.292,22.841 7.725,5.96 13.475,4.814 19.314,7.409 25.018,6.037 "/>
</svg>
</SvgButton>
<SvgButton v-if="markers.markerSets.length > 0 || markers.markers.length > 0" class="thin-hide" title="Marker-List"
@action="appState.menu.openPage('markers', 'Markers', {markerSet: markers})">
<svg viewBox="0 0 30 30">
<path d="M15,3.563c-4.459,0-8.073,3.615-8.073,8.073c0,6.483,8.196,14.802,8.196,14.802s7.951-8.013,7.951-14.802
C23.073,7.177,19.459,3.563,15,3.563z M15,15.734c-2.263,0-4.098-1.835-4.098-4.099c0-2.263,1.835-4.098,4.098-4.098
c2.263,0,4.098,1.835,4.098,4.098C19.098,13.899,17.263,15.734,15,15.734z"/>
</svg>
</SvgButton>
<SvgButton v-if="!playerMarkerSet.fake" class="thin-hide" title="Player-List" @action="openPlayerList">
<svg viewBox="0 0 30 30">
<g>
<path d="M8.95,14.477c0.409-0.77,1.298-1.307,2.164-1.309h0.026c-0.053-0.234-0.087-0.488-0.087-0.755
c0-1.381,0.715-2.595,1.791-3.301c-0.01,0-0.021-0.006-0.03-0.006h-1.427c-0.39,0-0.514-0.251-0.276-0.563
c0,0,0.497-0.645,0.497-1.452c0-1.48-1.2-2.681-2.679-2.681c-1.481,0-2.679,1.2-2.679,2.681c0,0.807,0.496,1.452,0.496,1.452
c0.24,0.311,0.114,0.565-0.275,0.565L5.042,9.118C4.649,9.119,4.182,9.405,3.998,9.75l-2.601,4.927
c-0.184,0.347-0.062,0.802,0.265,1.015l1.297,0.83c0.332,0.213,0.794,0.135,1.034-0.18l0.598-0.775
c0.238-0.31,0.471-0.245,0.516,0.141l0.454,3.854c0.035,0.311,0.272,0.566,0.564,0.66c0.018-0.279,0.087-0.561,0.225-0.82
L8.95,14.477z"/>
<path d="M28.604,14.677l-2.597-4.94c-0.185-0.346-0.65-0.631-1.042-0.631h-1.428c-0.39,0-0.514-0.251-0.274-0.563
c0,0,0.496-0.645,0.496-1.452c0-1.48-1.2-2.681-2.68-2.681c-1.481,0-2.679,1.2-2.679,2.681c0,0.807,0.496,1.452,0.496,1.452
c0.239,0.311,0.114,0.565-0.275,0.565l-1.428,0.009c-0.005,0-0.009,0.002-0.015,0.002c1.067,0.708,1.774,1.917,1.774,3.292
c0,0.263-0.031,0.513-0.084,0.744h0.02c0.868,0,1.758,0.537,2.166,1.305l2.598,4.944c0.137,0.262,0.205,0.539,0.222,0.818
c0.296-0.092,0.538-0.35,0.574-0.664l0.451-3.842c0.044-0.389,0.28-0.452,0.519-0.143l0.588,0.768
c0.239,0.313,0.702,0.391,1.033,0.182l1.297-0.833C28.667,15.479,28.787,15.026,28.604,14.677z"/>
</g>
<path d="M19.932,15.058c-0.184-0.346-0.651-0.63-1.043-0.63h-1.427c-0.39,0-0.515-0.252-0.275-0.564c0,0,0.496-0.645,0.496-1.451
c0-1.479-1.199-2.68-2.679-2.68c-1.482,0-2.679,1.201-2.679,2.68c0,0.806,0.496,1.451,0.496,1.451
c0.24,0.312,0.114,0.566-0.275,0.566l-1.427,0.009c-0.393,0.001-0.861,0.287-1.045,0.632l-2.602,4.925
c-0.185,0.348-0.062,0.803,0.266,1.016l1.297,0.832c0.332,0.213,0.794,0.133,1.034-0.18l0.598-0.775
c0.239-0.311,0.472-0.246,0.517,0.141l0.454,3.854c0.043,0.389,0.403,0.705,0.794,0.705h5.148c0.392,0,0.749-0.316,0.794-0.705
l0.45-3.844c0.045-0.389,0.282-0.451,0.52-0.143l0.587,0.768c0.239,0.313,0.703,0.393,1.033,0.182l1.297-0.832
c0.331-0.213,0.451-0.666,0.269-1.016L19.932,15.058z"/>
</svg>
</SvgButton>
<div class="space thin-hide greedy"></div>
<DayNightSwitch class="thin-hide" title="Day/Night" />
<div class="space thin-hide"></div>
<ControlsSwitch class="thin-hide"></ControlsSwitch>
<div class="space thin-hide"></div>
<SvgButton class="thin-hide" title="Reset Camera & Position" @action="$bluemap.resetCamera()">
<svg viewBox="0 0 30 30">
<rect x="7.085" y="4.341" transform="matrix(0.9774 0.2116 -0.2116 0.9774 3.2046 -1.394)" width="2.063" height="19.875"/>
<path d="M12.528,5.088c0,0,3.416-0.382,4.479-0.031c1.005,0.332,2.375,2.219,3.382,2.545c1.096,0.354,4.607-0.089,4.607-0.089
l-2.738,8.488c0,0-3.285,0.641-4.344,0.381c-1.049-0.257-2.607-2.015-3.642-2.324c-0.881-0.264-3.678-0.052-3.678-0.052
L12.528,5.088z"/>
</svg>
</SvgButton>
<PositionInput class="pos-input" />
<Compass title="Compass / Face North" />
</div>
</template>
<script>
import PositionInput from "@/components/ControlBar/PositionInput";
import Compass from "@/components/ControlBar/Compass";
import DayNightSwitch from "@/components/ControlBar/DayNightSwitch";
import ControlsSwitch from "@/components/ControlBar/ControlsSwitch";
import MenuButton from "@/components/ControlBar/MenuButton";
import SvgButton from "@/components/ControlBar/SvgButton";
export default {
name: "ControlBar",
components: {
SvgButton,
MenuButton,
ControlsSwitch,
DayNightSwitch,
PositionInput,
Compass
},
data() {
return {
appState: this.$bluemap.appState,
markers: this.$bluemap.mapViewer.markers.data,
}
},
computed: {
playerMarkerSet() {
for (let set of this.markers.markerSets) {
if (set.id === "bm-players") return set;
}
return {
id: "bm-players",
label: "Players",
markerSets: [],
markers: [],
fake: true,
}
}
},
methods: {
openPlayerList() {
let playerList = this.playerMarkerSet;
this.appState.menu.openPage('markers', 'Players', {markerSet: playerList});
}
}
}
</script>
<style lang="scss">
@import "~@/scss/variables.scss";
.control-bar {
position: fixed;
display: flex;
filter: drop-shadow(1px 1px 3px rgba(0, 0, 0, 0.53));
height: 2em;
margin: 0.5em;
width: calc(100% - 1em);
.pos-input {
max-width: 20em;
width: 100%;
}
> :not(:first-child) {
border-left: solid 1px var(--theme-bg-light);
}
.space {
width: 0.5em;
flex-shrink: 0;
&.greedy {
flex-grow: 1;
}
}
.space, .space + * {
border-left: none;
}
@media (max-width: $mobile-break) {
margin: 0;
width: 100%;
background-color: var(--theme-bg-light);
.pos-input {
max-width: unset;
}
.thin-hide {
display: none;
}
.space {
width: 1px;
}
}
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<div class="controls-switch">
<SvgButton :active="isPerspectiveView" @action="setPerspectiveView" title="Perspective-View">
<svg viewBox="0 0 30 30">
<path d="M19.475,10.574c-0.166-0.021-0.337-0.036-0.51-0.045c-0.174-0.009-0.35-0.013-0.525-0.011
c-0.176,0.002-0.353,0.01-0.526,0.024c-0.175,0.015-0.347,0.036-0.515,0.063l-13.39,2.189
c-0.372,0.061-0.7,0.146-0.975,0.247c-0.276,0.102-0.5,0.221-0.66,0.349c-0.161,0.129-0.259,0.268-0.282,0.408
c-0.024,0.141,0.028,0.285,0.165,0.421l5.431,5.511c0.086,0.087,0.191,0.167,0.314,0.241s0.263,0.142,0.417,0.202
c0.155,0.062,0.323,0.115,0.502,0.162c0.18,0.046,0.371,0.085,0.569,0.116s0.405,0.054,0.616,0.068
c0.211,0.015,0.427,0.021,0.645,0.017c0.217-0.003,0.436-0.016,0.652-0.037c0.217-0.022,0.431-0.054,0.641-0.095L27.12,17.43
c0.371-0.073,0.679-0.175,0.917-0.296c0.236-0.12,0.404-0.259,0.497-0.407c0.093-0.147,0.111-0.305,0.052-0.461
c-0.059-0.156-0.195-0.313-0.415-0.46l-7.089-4.742c-0.089-0.06-0.192-0.115-0.308-0.166
c-0.116-0.051-0.243-0.097-0.381-0.138c-0.137-0.041-0.283-0.078-0.438-0.108C19.803,10.621,19.641,10.595,19.475,10.574"/>
</svg>
</SvgButton>
<SvgButton :active="isFlatView" @action="setFlatView" title="Orthographic/Flat-View">
<svg viewBox="0 0 30 30">
<path d="M22.371,4.158c1.65,0,3,1.35,3,3v15.684c0,1.65-1.35,3-3,3H7.629c-1.65,0-3-1.35-3-3V7.158c0-1.65,1.35-3,3-3H22.371z"/>
</svg>
</SvgButton>
<SvgButton :active="isFreeFlight" @action="setFreeFlight" title="Free-Flight/Spectator Mode">
<svg viewBox="0 0 30 30">
<path d="M21.927,11.253c-0.256-0.487-0.915-0.885-1.465-0.885h-2.004c-0.55,0-0.726-0.356-0.39-0.792c0,0,0.698-0.905,0.698-2.041
c0-2.08-1.687-3.767-3.767-3.767s-3.767,1.687-3.767,3.767c0,1.136,0.698,2.041,0.698,2.041c0.336,0.436,0.161,0.794-0.389,0.797
l-2.005,0.01c-0.55,0.002-1.21,0.403-1.467,0.889l-3.656,6.924c-0.257,0.487-0.088,1.128,0.375,1.425l1.824,1.171
c0.462,0.297,1.116,0.184,1.451-0.253l0.839-1.092c0.335-0.437,0.662-0.346,0.726,0.2l0.637,5.415
c0.064,0.546,0.567,0.993,1.117,0.993h7.234c0.55,0,1.053-0.447,1.117-0.993l0.635-5.401c0.064-0.546,0.392-0.637,0.727-0.2
l0.828,1.078c0.335,0.437,0.988,0.55,1.451,0.253l1.823-1.171c0.463-0.297,0.633-0.938,0.377-1.425L21.927,11.253z"/>
</svg>
</SvgButton>
</div>
</template>
<script>
import SvgButton from "@/components/ControlBar/SvgButton";
export default {
name: "ControlsSwitch",
components: {SvgButton},
data() {
return {
controls: this.$bluemap.appState.controls,
}
},
computed: {
isPerspectiveView() {
return this.controls.state === "perspective";
},
isFlatView() {
return this.controls.state === "flat";
},
isFreeFlight() {
return this.controls.state === "free";
}
},
methods: {
setPerspectiveView() {
this.$bluemap.setPerspectiveView();
},
setFlatView() {
this.$bluemap.setFlatView();
},
setFreeFlight() {
this.$bluemap.setFreeFlight();
}
}
}
</script>
<style lang="scss">
.controls-switch {
display: flex;
}
</style>

View File

@ -0,0 +1,70 @@
<template>
<SvgButton class="day-night-switch" :active="!isDay" @action="action">
<svg viewBox="0 0 30 30">
<path d="M17.011,19.722c-3.778-1.613-5.533-5.982-3.921-9.76c0.576-1.348,1.505-2.432,2.631-3.204
c-3.418-0.243-6.765,1.664-8.186,4.992c-1.792,4.197,0.159,9.053,4.356,10.844c3.504,1.496,7.462,0.377,9.717-2.476
C20.123,20.465,18.521,20.365,17.011,19.722z"/>
<circle cx="5.123" cy="7.64" r="1.196"/>
<circle cx="23.178" cy="5.249" r="1.195"/>
<circle cx="20.412" cy="13.805" r="1.195"/>
<circle cx="25.878" cy="23.654" r="1.195"/>
</svg>
</SvgButton>
</template>
<script>
import {animate, EasingFunctions} from "bluemap/src/util/Utils";
import SvgButton from "@/components/ControlBar/SvgButton";
let animation;
export default {
name: "DayNightSwitch",
components: {SvgButton},
data() {
return {
mapViewer: this.$bluemap.mapViewer.data
}
},
computed: {
isDay() {
return this.mapViewer.uniforms.sunlightStrength.value > 0.6;
}
},
methods: {
action(evt) {
evt.preventDefault();
if (animation) animation.cancel();
let startValue = this.mapViewer.uniforms.sunlightStrength.value;
let targetValue = this.isDay ? 0.25 : 1;
animation = animate(t => {
let u = EasingFunctions.easeOutQuad(t);
this.mapViewer.uniforms.sunlightStrength.value = startValue * (1-u) + targetValue * u;
}, 300);
}
}
}
</script>
<style lang="scss">
.day-night-switch {
svg {
fill: var(--theme-moon-day);
circle {
fill: var(--theme-stars-day);
}
}
&:active {
svg {
fill: var(--theme-moon-night);
circle {
fill: var(--theme-stars-night);
}
}
}
}
</style>

View File

@ -0,0 +1,85 @@
<template>
<SvgButton class="menu-button" :class="{close: close, back: back}" @action="$emit('action', $event)">
<svg viewBox="0 0 30 30">
<g>
<path d="M25.004,9.294c0,0.806-0.75,1.46-1.676,1.46H6.671c-0.925,0-1.674-0.654-1.674-1.46l0,0
c0-0.807,0.749-1.461,1.674-1.461h16.657C24.254,7.833,25.004,8.487,25.004,9.294L25.004,9.294z"/>
<path d="M25.004,15c0,0.807-0.75,1.461-1.676,1.461H6.671c-0.925,0-1.674-0.654-1.674-1.461l0,0
c0-0.807,0.749-1.461,1.674-1.461h16.657C24.254,13.539,25.004,14.193,25.004,15L25.004,15z"/>
<path d="M25.004,20.706c0,0.807-0.75,1.461-1.676,1.461H6.671c-0.925,0-1.674-0.654-1.674-1.461l0,0
c0-0.807,0.749-1.461,1.674-1.461h16.657C24.254,19.245,25.004,19.899,25.004,20.706L25.004,20.706z"/>
</g>
</svg>
</SvgButton>
</template>
<script>
import SvgButton from "@/components/ControlBar/SvgButton";
export default {
name: "MenuButton",
components: {SvgButton},
props: {
close: Boolean,
back: Boolean,
}
}
</script>
<style lang="scss">
.menu-button {
svg {
g {
transform-origin: center;
transition: transform 0.3s;
}
path {
transition: transform 0.3s, fill 0.3s;
transform: translate(0 0) rotate(0);
&:nth-child(1) {
transform-origin: 15px 9px;
}
&:nth-child(2) {
transform-origin: 15px 15px;
}
&:nth-child(3) {
transform-origin: 15px 21px;
}
}
}
&.close {
svg {
path:nth-child(1) {
transform: translate(0, 5.75px) rotate(45deg);
}
path:nth-child(2) {
transform: translate(-100%, 0) rotate(0);
}
path:nth-child(3) {
transform: translate(0, -5.75px) rotate(-45deg);
}
}
&.back {
svg {
g {
transform: scale(0.75);
}
path:nth-child(1) {
transform: translate(0, 10px) rotate(30deg);
}
path:nth-child(2) {
transform: translate(-150%, 0) rotate(0);
}
path:nth-child(3) {
transform: translate(0, -10px) rotate(-30deg);
}
}
}
}
}
</style>

View File

@ -0,0 +1,65 @@
<template>
<div class="number-input">
<label>
<span class="label">{{label}}:</span>
<input type="number"
v-bind:value="value | format"
v-on:input="$emit('input', $event)"
v-on:keydown="$event.stopPropagation()"
>
</label>
</div>
</template>
<script>
export default {
name: "NumberInput",
props: {
label: String,
value: Number
},
filters: {
format(value) {
return Math.floor(value);
}
}
}
</script>
<style lang="scss">
.number-input {
pointer-events: auto;
background-color: var(--theme-bg);
color: var(--theme-fg);
min-height: 2em;
.label {
display: inline-block;
width: 1em;
padding: 0 0.5em 0 0.5em;
color: var(--theme-fg-light);
}
input {
height: 100%;
line-height: 100%;
width: calc(100% - 2em);
background-color: inherit;
color: inherit;
// remove number spinner firefox
-moz-appearance: textfield;
// remove number spinner webkit
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
}
</style>

View File

@ -0,0 +1,39 @@
<template>
<div class="position-input">
<NumberInput label="x" :value="controls.position.x" v-on:input="controls.position.x = parseFloat($event.target.value);" />
<NumberInput label="y" :value="controls.position.y" v-on:input="controls.position.y = parseFloat($event.target.value);" v-if="appState.controls.state === 'free'" />
<NumberInput label="z" :value="controls.position.z" v-on:input="controls.position.z = parseFloat($event.target.value);" />
</div>
</template>
<script>
import NumberInput from "@/components/ControlBar/NumberInput";
export default {
name: "PositionInput",
components: {
NumberInput
},
data() {
return {
controls: this.$bluemap.mapViewer.controlsManager.data,
appState: this.$bluemap.appState
}
}
}
</script>
<style lang="scss">
.position-input {
display: flex;
user-select: none;
> * {
width: 100%;
&:not(:first-child) {
border-left: solid 1px var(--theme-bg-light);
}
}
}
</style>

View File

@ -0,0 +1,55 @@
<template>
<div class="svg-button" :class="{active: active}" @click="$emit('action', $event)">
<slot />
</div>
</template>
<script>
export default {
name: "SvgButton",
props: {
active: Boolean,
}
}
</script>
<style lang="scss">
.svg-button {
position: relative;
pointer-events: auto;
overflow: hidden;
cursor: pointer;
min-width: 2em;
min-height: 2em;
background-color: var(--theme-bg);
color: var(--theme-fg);
&:hover, &.active {
background-color: var(--theme-bg-light);
}
&:active {
background-color: var(--theme-fg-light);
color: var(--theme-bg);
}
svg {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: 1.8em;
fill: var(--theme-fg-light);
}
&:active {
svg {
fill: var(--theme-bg-light);
}
}
}
</style>

View File

@ -0,0 +1,38 @@
<template>
<div class="group">
<span class="title">{{ title }}</span>
<div class="content">
<slot/>
</div>
</div>
</template>
<script>
export default {
name: "Group",
props: {
title: String
}
}
</script>
<style lang="scss">
.side-menu .group {
position: relative;
margin: 2em 0 1em;
padding-top: 1em;
border: solid 1px var(--theme-bg-light);
> .title {
position: absolute;
top: calc(-0.5em - 1px);
right: 0.5em;
padding: 0 0.5em;
background-color: var(--theme-bg);
}
&:first-child {
margin-top: 1em;
}
}
</style>

View File

@ -0,0 +1,53 @@
<template>
<SideMenu :open="menu.isOpen"
:title="menu.currentPage().title"
:back="menu.pageStack.length > 1"
@back="menu.closePage()"
@close="menu.closeAll()">
<div v-if="menu.currentPage().id === 'root'">
<SimpleButton @action="menu.openPage('maps', 'Maps')" :submenu="true">Maps</SimpleButton>
<SimpleButton @action="menu.openPage('markers', 'Markers', {markerSet: markers})" :submenu="true">Markers</SimpleButton>
<SimpleButton @action="menu.openPage('settings', 'Settings')" :submenu="true">Settings</SimpleButton>
<hr>
<SimpleButton @action="$bluemap.resetCamera()">Reset Camera</SimpleButton>
<SimpleButton>Update Map</SimpleButton>
</div>
<div v-if="menu.currentPage().id === 'maps'">
<MapButton v-for="map of appState.maps" :key="map.id" :map="map" />
</div>
<MarkerSetMenu v-if="menu.currentPage().id === 'markers'" :menu="menu" />
<SettingsMenu v-if="menu.currentPage().id === 'settings'" />
</SideMenu>
</template>
<script>
import SideMenu from "@/components/Menu/SideMenu";
import SimpleButton from "@/components/Menu/SimpleButton";
import SettingsMenu from "@/components/Menu/SettingsMenu";
import {MainMenu} from "@/js/MainMenu";
import MarkerSetMenu from "@/components/Menu/MarkerSetMenu";
import MapButton from "@/components/Menu/MapButton";
export default {
name: "MainMenu",
components: {MapButton, MarkerSetMenu, SettingsMenu, SimpleButton, SideMenu},
props: {
menu: MainMenu
},
data() {
return {
appState: this.$bluemap.appState,
markers: this.$bluemap.mapViewer.markers.data,
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,72 @@
<template>
<div class="map-button" :class="{selected: map.id === selectedMapId}" @click="switchMap(map.id)" :title="map.id">
<span class="sky" :style="{color: 'rgb(' + map.skyColor.r * 255 + ',' + map.skyColor.g * 255 + ',' + map.skyColor.b * 255 + ')'}">&bull;</span>
<span class="name">{{map.name}}</span>
</div>
</template>
<script>
export default {
name: "MapButton",
props: {
map: Object,
},
data() {
return {
mapViewer: this.$bluemap.mapViewer.data,
appState: this.$bluemap.appState,
}
},
computed: {
selectedMapId() {
return this.mapViewer.map ? this.mapViewer.map.id : null;
}
},
methods: {
switchMap(mapId) {
this.$bluemap.switchMap(mapId);
}
}
}
</script>
<style lang="scss">
.side-menu .map-button {
position: relative;
cursor: pointer;
user-select: none;
height: 2em;
line-height: 2em;
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
&.selected, &:hover {
background-color: var(--theme-bg-light);
}
&:active {
background-color: var(--theme-fg-light);
color: var(--theme-bg);
}
.sky {
float: left;
border-radius: 100%;
width: 0.5em;
height: 0.5em;
margin: 0 0.25em 0 0.5em;
}
.id {
font-style: italic;
color: var(--theme-fg-light);
margin: 0 0.5em;
}
}
</style>

View File

@ -0,0 +1,131 @@
<template>
<div class="marker-item" :class="{'marker-hidden': !marker.visible}" :title="marker.id" @click="click">
<div class="icon" v-if="marker.type === 'player'">
<img :src="'assets/playerheads/' + marker.playerUuid + '.png'" alt="playerhead" @error="steve">
</div>
<div class="info">
<div class="label">{{markerLabel}}</div>
<div class="stats">
<div>
{{marker.type}}-marker
</div>
<div>
({{marker.position.x | position}} | {{marker.position.y | position}} | {{marker.position.z | position}})
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "MarkerItem",
props: {
marker: Object
},
computed: {
markerLabel() {
switch (this.marker.type) {
case "player" : return this.marker.name;
default : break;
}
if (this.marker.label){
let strippedLabel = /^(?:<[^>]*>\s*)*([^<>]*\S[^<>]*)(?:<|$)/gi.exec(this.marker.label);
if (strippedLabel && strippedLabel.length > 1) {
return strippedLabel[1];
}
}
return this.marker.id;
}
},
filters: {
position(v) {
return Math.floor(v);
}
},
methods: {
click() {
if (!this.marker.visible) return;
this.$bluemap.mapViewer.controlsManager.position.copy(this.marker.position);
},
steve(event) {
event.target.src = "assets/steve.png";
}
}
}
</script>
<style lang="scss">
@import "~@/scss/variables.scss";
.side-menu .marker-item {
display: flex;
margin: 0.5em 0;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
white-space: nowrap;
user-select: none;
&:hover {
background-color: var(--theme-bg-light);
}
&.marker-hidden {
opacity: 0.5;
filter: grayscale(1);
}
.info {
position: relative;
flex-grow: 1;
overflow-x: hidden;
text-overflow: ellipsis;
.label {
line-height: 2em;
overflow-x: hidden;
text-overflow: ellipsis;
margin: 0 0.5em 1.5em 0.5em;
}
.stats {
display: flex;
margin: 0 0.5em;
position: absolute;
bottom: 0;
font-size: 0.8em;
line-height: 2em;
color: var(--theme-fg-light);
> div {
&:not(:first-child) {
margin-left: 0.5em;
padding-left: 0.5em;
border-left: solid 1px var(--theme-bg-light);
}
}
}
}
.icon {
height: 2.5em;
margin: 0.5em;
flex-shrink: 0;
img {
image-rendering: pixelated;
height: 100%;
}
}
}
</style>

View File

@ -0,0 +1,150 @@
<template>
<div class="marker-set" :title="markerSet.id">
<div class="info" @click="toggle">
<div class="marker-set-switch">
<div class="label">{{ markerSet.label }}</div>
<SwitchHandle :on="markerSet.visible" v-if="markerSet.toggleable"/>
</div>
<div class="stats">
<div>
{{ markerSet.markers.length }}
{{ markerSet.markers.length !== 1 ? "markers" : "marker" }}
</div>
<div v-if="filteredMarkerSets.length > 0">
{{ filteredMarkerSets.length }}
{{ filteredMarkerSets.length !== 1 ? "marker-sets" : "marker-set" }}
</div>
</div>
</div>
<div class="open-menu-button" @click="$emit('more', $event)">
<svg viewBox="0 0 30 30">
<path d="M25.004,9.294c0,0.806-0.75,1.46-1.676,1.46H6.671c-0.925,0-1.674-0.654-1.674-1.46l0,0
c0-0.807,0.749-1.461,1.674-1.461h16.657C24.254,7.833,25.004,8.487,25.004,9.294L25.004,9.294z"/>
<path d="M25.004,20.706c0,0.807-0.75,1.461-1.676,1.461H6.671c-0.925,0-1.674-0.654-1.674-1.461l0,0
c0-0.807,0.749-1.461,1.674-1.461h16.657C24.254,19.245,25.004,19.899,25.004,20.706L25.004,20.706z"/>
</svg>
</div>
</div>
</template>
<script>
import SwitchHandle from "@/components/Menu/SwitchHandle";
export default {
name: "MarkerSet",
components: {SwitchHandle},
props: {
markerSet: Object,
},
computed: {
filteredMarkerSets() {
return this.markerSet.markerSets.filter(markerSet => {
return (markerSet.id !== "bm-popup-set");
});
}
},
methods: {
toggle() {
if (this.markerSet.toggleable) {
this.markerSet.visible = !this.markerSet.visible
}
}
}
}
</script>
<style lang="scss">
.side-menu .marker-set {
display: flex;
user-select: none;
line-height: 2em;
margin: 0.5em 0;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
> .info {
flex-grow: 1;
cursor: pointer;
&:hover {
background-color: var(--theme-bg-light);
}
> .marker-set-switch {
position: relative;
.label {
margin: 0 3em 0 0.5em;
}
> .switch {
position: absolute;
top: 0.5em;
right: 0.5em;
}
}
> .stats {
display: flex;
margin: 0 0.5em;
font-size: 0.8em;
line-height: 2em;
color: var(--theme-fg-light);
> div {
&:not(:first-child) {
margin-left: 0.5em;
padding-left: 0.5em;
border-left: solid 1px var(--theme-bg-light);
}
}
}
}
> .open-menu-button {
width: 2em;
cursor: pointer;
&:hover {
background-color: var(--theme-bg-light);
}
> svg {
position: relative;
fill: var(--theme-fg-light);
top: 50%;
transform: translate(0, -50%) scale(0.75);
path:nth-child(1) {
transform-origin: 15px 9px;
transform: translate(0, 10px) rotate(-30deg);
}
path:nth-child(2) {
transform-origin: 15px 21px;
transform: translate(0, -10px) rotate(30deg);
}
}
&:active {
background-color: var(--theme-fg-light);
color: var(--theme-bg);
> svg {
fill: var(--theme-bg-light);
}
}
}
}
</style>

View File

@ -0,0 +1,69 @@
<template>
<div>
<div class="marker-sets">
<MarkerSet v-for="markerSet of filteredMarkerSets" :key="markerSet.id" :marker-set="markerSet" @more="openMore(markerSet)" />
</div>
<hr v-if="filteredMarkerSets.length > 0 & thisMarkerSet.markers.length > 0">
<div class="markers" v-if="thisMarkerSet.markers.length > 0">
<TextInput :value="filter.search" @input="filter.search = $event.target.value" placeholder="Search..." />
<MarkerItem v-for="marker of filteredMarkers" :key="marker.id" :marker="marker" />
</div>
</div>
</template>
<script>
import MarkerItem from "@/components/Menu/MarkerItem";
import TextInput from "@/components/Menu/TextInput";
import MarkerSet from "@/components/Menu/MarkerSet";
import {MainMenu} from "@/js/MainMenu";
export default {
name: "MarkerSetMenu",
components: {MarkerSet, TextInput, MarkerItem},
props: {
menu: MainMenu
},
data() {
return {
filter: {
search: "",
}
}
},
computed: {
thisMarkerSet() {
return this.menu.currentPage().markerSet;
},
filteredMarkers() {
return [...this.thisMarkerSet.markers].sort((a, b) => {
if (a.id < b.id) return -1;
if (a.id > b.id) return 1;
return 0;
}).filter(marker => {
if (!this.filter.search) return true;
if (marker.id.includesCI(this.filter.search)) return true;
if (marker.label && marker.label.includesCI(this.filter.search)) return true;
if (marker.type === "player" && (marker.name.includesCI(this.filter.search) || marker.playerUuid.includesCI(this.filter.search))) return true;
return false;
});
},
filteredMarkerSets() {
return this.thisMarkerSet.markerSets.filter(markerSet => {
return (markerSet.id !== "bm-popup-set");
});
}
},
methods: {
openMore(markerSet) {
this.menu.openPage(
this.menu.currentPage().id,
this.menu.currentPage().title + " > " + markerSet.label,
{markerSet: markerSet}
)
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,87 @@
<template>
<div>
<Group title="View / Controls">
<SimpleButton :active="appState.controls.state === 'perspective'" @action="$bluemap.setPerspectiveView()">Perspective</SimpleButton>
<SimpleButton :active="appState.controls.state === 'flat'" @action="$bluemap.setFlatView()">Flat</SimpleButton>
<SimpleButton :active="appState.controls.state === 'free'" @action="$bluemap.setFreeFlight()">Free-Flight</SimpleButton>
</Group>
<Group title="Lighting">
<Slider :value="mapViewer.uniforms.sunlightStrength.value" :min="0" :max="1" :step="0.01"
@update="mapViewer.uniforms.sunlightStrength.value = $event">Sunlight</Slider>
<Slider :value="mapViewer.uniforms.ambientLight.value" :min="0" :max="1" :step="0.01"
@update="mapViewer.uniforms.ambientLight.value = $event">Ambient-Light</Slider>
</Group>
<Group title="Resolution">
<SimpleButton v-for="stage of qualityStages" :key="stage.name"
:active="mapViewer.superSampling === stage.value"
@action="$bluemap.mapViewer.superSampling = stage.value"
>{{stage.name}}</SimpleButton>
</Group>
<Group title="Render-Distance">
<Slider :value="mapViewer.loadedHiresViewDistance" :min="50" :max="500" :step="10"
@update="mapViewer.loadedHiresViewDistance = $event; $bluemap.mapViewer.updateLoadedMapArea();">Hires layer</Slider>
<Slider :value="mapViewer.loadedLowresViewDistance" :min="500" :max="7000" :step="100"
@update="mapViewer.loadedLowresViewDistance = $event; $bluemap.mapViewer.updateLoadedMapArea();">Lowres layer</Slider>
</Group>
<Group title="Free-Flight Controls">
<Slider :value="appState.controls.mouseSensitivity" :min="0.1" :max="5" :step="0.05"
@update="appState.controls.mouseSensitivity = $event; $bluemap.updateControlsSettings();">Mouse-Sensitivity</Slider>
<SwitchButton :on="appState.controls.invertMouse" @action="appState.controls.invertMouse = !appState.controls.invertMouse; $bluemap.updateControlsSettings()">Invert Mouse Y</SwitchButton>
</Group>
<Group title="Theme">
<SimpleButton v-for="theme of themes" :key="theme.name"
:active="appState.theme === theme.value"
@action="$bluemap.setTheme(theme.value)"
>{{theme.name}}</SimpleButton>
</Group>
<SwitchButton :on="appState.debug" @action="switchDebug">Debug</SwitchButton>
</div>
</template>
<script>
import Group from "@/components/Menu/Group";
import SimpleButton from "@/components/Menu/SimpleButton";
import Slider from "@/components/Menu/Slider";
import SwitchButton from "@/components/Menu/SwitchButton";
const themes = [
{name: "Default (System/Browser)", value: null},
{name: "Dark", value: 'dark'},
{name: "Light", value: 'light'},
];
const qualityStages = [
{name: "High (SSAA, x2)", value: 2},
{name: "Normal (Native, x1)", value: 1},
{name: "Low (Upscaling, x0.5)", value: 0.5},
];
export default {
name: "SettingsMenu",
components: {SwitchButton, Slider, SimpleButton, Group},
data() {
return {
appState: this.$bluemap.appState,
mapViewer: this.$bluemap.mapViewer.data,
qualityStages: qualityStages,
themes: themes,
}
},
methods: {
switchDebug() {
this.$bluemap.setDebug(!this.appState.debug);
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,131 @@
<template>
<Transition name="side-menu" @enter="buttonEnterAnimation(); $emit('enter', $event)">
<div class="side-menu" v-if="open">
<MenuButton :close="open && rendered" :back="back" @action="$emit('back', $event)" />
<MenuButton class="full-close" v-if="open && back" :close="true" @action="$emit('close', $event)" />
<div class="title">{{ title }}</div>
<div class="content">
<slot />
</div>
</div>
</Transition>
</template>
<script>
import MenuButton from "@/components/ControlBar/MenuButton";
export default {
name: "SideMenu",
components: {MenuButton},
props: {
title: {
type: String,
default: "Menu"
},
open: {
type: Boolean,
default: true
},
back: Boolean
},
data() {
return {
rendered: false
}
},
methods: {
async buttonEnterAnimation() {
this.rendered = false;
await this.$nextTick();
await this.$nextTick();
this.rendered = true;
}
}
}
</script>
<style lang="scss">
@import "~@/scss/variables.scss";
.side-menu {
position: fixed;
top: 0;
left: 0;
overflow: hidden;
pointer-events: auto;
width: 100%;
max-width: 20em;
height: 100%;
filter: drop-shadow(1px 1px 3px #0008);
background-color: var(--theme-bg);
color: var(--theme-fg);
&-enter-active, &-leave-active {
transition: opacity 0.3s;
}
&-enter, &-leave-to {
opacity: 0;
pointer-events: none;
* {
pointer-events: none !important;
}
}
> .menu-button {
position: absolute;
top: 0;
left: 0;
margin: 0.5em;
@media (max-width: $mobile-break) {
margin: 0;
}
&.full-close {
right: 0;
left: unset;
}
}
> .title {
line-height: 2em;
text-align: center;
background-color: inherit;
border-bottom: solid 1px var(--theme-bg-light);
padding: 0.5em;
@media (max-width: $mobile-break) {
padding: 0;
}
}
> .content {
position: relative;
overflow-y: auto;
overflow-x: hidden;
padding: 0.5em;
height: calc(100% - 4em - 1px);
@media (max-width: $mobile-break) {
height: calc(100% - 3em - 1px);
}
hr {
border: none;
border-bottom: solid 1px var(--theme-bg-light);
margin: 0.5em 0;
}
}
}
</style>

View File

@ -0,0 +1,85 @@
<template>
<div class="simple-button" :class="{active: active}" @click="$emit('action')">
<div class="label"><slot /></div>
<div class="submenu-icon" v-if="submenu">
<svg viewBox="0 0 30 30">
<path d="M25.004,9.294c0,0.806-0.75,1.46-1.676,1.46H6.671c-0.925,0-1.674-0.654-1.674-1.46l0,0
c0-0.807,0.749-1.461,1.674-1.461h16.657C24.254,7.833,25.004,8.487,25.004,9.294L25.004,9.294z"/>
<path d="M25.004,20.706c0,0.807-0.75,1.461-1.676,1.461H6.671c-0.925,0-1.674-0.654-1.674-1.461l0,0
c0-0.807,0.749-1.461,1.674-1.461h16.657C24.254,19.245,25.004,19.899,25.004,20.706L25.004,20.706z"/>
</svg>
</div>
</div>
</template>
<script>
export default {
name: "SimpleButton",
props: {
submenu: Boolean,
active: {
type: Boolean,
default: false,
}
}
}
</script>
<style lang="scss">
.side-menu .simple-button {
cursor: pointer;
user-select: none;
display: flex;
line-height: 2em;
padding: 0 0.5em;
> .label {
flex-grow: 1;
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
}
&:hover, &.active {
background-color: var(--theme-bg-light);
}
> .submenu-icon {
width: 2em;
height: 2em;
flex-shrink: 0;
margin-right: -0.5em;
> svg {
fill: var(--theme-fg-light);
path:nth-child(1) {
transform-origin: 15px 9px;
transform: translate(0, 10px) rotate(-30deg);
}
path:nth-child(2) {
transform-origin: 15px 21px;
transform: translate(0, -10px) rotate(30deg);
}
transform: scale(0.75);
}
}
&:active {
background-color: var(--theme-fg-light);
color: var(--theme-bg);
> .submenu-icon > svg {
fill: var(--theme-bg-light);
}
}
}
</style>

View File

@ -0,0 +1,93 @@
<template>
<div class="slider">
<div class="label"><slot />: <span class="value">{{formattedValue}}</span></div>
<label>
<input type="range" :min="min" :max="max" :step="step" :value="value" @input="$emit('update', parseFloat($event.target.value))">
</label>
</div>
</template>
<script>
function countDecimals(value) {
if(Math.floor(value) === value) return 0;
return value.toString().split(".")[1].length || 0;
}
export default {
name: "Slider",
props: {
value: Number,
min: Number,
max: Number,
step: Number,
},
computed: {
formattedValue() {
return this.value.toFixed(countDecimals(this.step));
}
}
}
</script>
<style lang="scss">
.side-menu .slider {
line-height: 2em;
padding: 0 0.5em;
&:hover {
background-color: var(--theme-bg-light);
}
> .label {
> .value {
float: right;
}
}
> label {
> input {
appearance: none;
-moz-appearance: none;
-webkit-appearance: none;
outline: none;
width: 100%;
height: 1em;
border-radius: 1em;
//border: solid 0.125em var(--theme-fg-light);
overflow: hidden;
background-color: var(--theme-fg-light);
&::-webkit-slider-thumb {
appearance: none;
-moz-appearance: none;
-webkit-appearance: none;
outline: none;
width: 1em;
height: 1em;
border-radius: 100%;
border: solid 0.125em var(--theme-fg-light);
background-color: var(--theme-bg);
//box-shadow: calc(-100vw - 0.375em) 0 0 100vw var(--theme-switch-button-on);
}
&::-moz-range-thumb {
width: 0.75em;
height: 0.75em;
border-radius: 100%;
border: solid 0.125em var(--theme-fg-light);
background-color: var(--theme-bg);
}
}
}
}
</style>

View File

@ -0,0 +1,46 @@
<template>
<div class="switch-button" @click="$emit('action')">
<div class="label"><slot /></div>
<SwitchHandle :on="on" />
</div>
</template>
<script>
import SwitchHandle from "@/components/Menu/SwitchHandle";
export default {
name: "SwitchButton",
components: {SwitchHandle},
props: {
on: Boolean
}
}
</script>
<style lang="scss">
.side-menu .switch-button {
cursor: pointer;
user-select: none;
display: flex;
line-height: 2em;
padding: 0 0.5em;
> .label {
flex-grow: 1;
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
}
> .switch {
margin: 0.5em 0;
}
&:hover {
background-color: var(--theme-bg-light);
}
}
</style>

View File

@ -0,0 +1,48 @@
<template>
<div class="switch" :class="{on: on}"></div>
</template>
<script>
export default {
name: "SwitchHandle",
props: {
on: Boolean
}
}
</script>
<style lang="scss">
.side-menu .switch {
height: 1em;
width: 2em;
border-radius: 1em;
background-color: var(--theme-fg-light);
transition: background-color 0.3s;
&::after {
content: "";
display: block;
width: 0.75em;
height: 0.75em;
border-radius: 100%;
background-color: var(--theme-bg);
position: relative;
top: 0.125em;
left: 0.125em;
transition: left 0.3s;
}
&.on {
background-color: var(--theme-switch-button-on);
&::after {
left: 1.125em;
}
}
}
</style>

View File

@ -0,0 +1,20 @@
<template>
<input class="text-input" type="text" :value="value" @input="$emit('input', $event)" @keydown="$event.stopPropagation()">
</template>
<script>
export default {
name: "TextInput",
props: {
value: String
}
}
</script>
<style lang="scss">
.side-menu .text-input {
background-color: var(--theme-bg-light);
width: calc(100% - 1em);
padding: 0.5em;
}
</style>

283
src/js/BlueMapApp.js Normal file
View File

@ -0,0 +1,283 @@
import "bluemap/src/BlueMap";
import {MapViewer} from "bluemap/src/MapViewer";
import {MapControls} from "bluemap/src/controls/map/MapControls";
import {FreeFlightControls} from "bluemap/src/controls/freeflight/FreeFlightControls";
import {FileLoader} from "three";
import {Map as BlueMapMap} from "bluemap/src/map/Map";
import {alert} from "bluemap/src/util/Utils";
import {PlayerMarkerManager} from "bluemap/src/markers/PlayerMarkerManager";
import {MarkerFileManager} from "bluemap/src/markers/MarkerFileManager";
import {MainMenu} from "@/js/MainMenu";
import {PopupMarker} from "@/js/PopupMarker";
import {MarkerSet} from "bluemap/src/markers/MarkerSet";
export class BlueMapApp {
/**
* @param rootElement {Element}
*/
constructor(rootElement) {
this.events = rootElement;
this.mapViewer = new MapViewer(rootElement, this.events);
this.mapControls = new MapControls(this.mapViewer.renderer.domElement);
this.freeFlightControls = new FreeFlightControls(this.mapViewer.renderer.domElement);
/** @type {PlayerMarkerManager} */
this.playerMarkerManager = null;
/** @type {MarkerFileManager} */
this.markerFileManager = null;
this.maps = [];
this.mapsMap = new Map();
this.dataUrl = "data/";
this.mainMenu = new MainMenu();
this.appState = {
controls: {
state: "perspective",
mouseSensitivity: 1,
invertMouse: false,
},
menu: this.mainMenu,
maps: [],
theme: null,
debug: false
};
// init
this.updateControlsSettings();
// popup on click
this.popupMarkerSet = new MarkerSet("bm-popup-set");
this.popupMarker = new PopupMarker("bm-popup", this.appState, this.events);
this.popupMarkerSet.add(this.popupMarker);
this.mapViewer.markers.add(this.popupMarkerSet);
}
/**
* @returns {Promise<void|never>}
*/
async load() {
let oldMaps = this.maps;
this.maps = [];
this.appState.maps.splice(0, this.appState.maps.length);
this.mapsMap.clear();
let unloadPromise = this.mapViewer.switchMap(null)
.then(() => {
oldMaps.forEach(map => map.dispose());
});
let loadPromise = this.loadMaps()
.then(maps => {
this.maps = maps;
for (let map of maps) {
this.mapsMap.set(map.data.id, map);
this.appState.maps.push(map.data);
}
})
try {
await unloadPromise;
await loadPromise;
} catch (err) {
alert(this.events, "Failed to load map: " + err.toString(), "error");
return;
}
if (this.maps.length > 0) {
this.switchMap(this.maps[0].data.id)
.catch(err => {
alert(this.events, "Failed to switch to map: " + err.toString(), "error");
});
}
}
/**
* @param mapId {String}
* @returns {Promise<void>}
*/
switchMap(mapId) {
let map = this.mapsMap.get(mapId);
if (!map) return Promise.reject(`There is no map with the id "${mapId}" loaded!`);
let oldWorld = this.mapViewer.map ? this.mapViewer.map.data.world : null;
return this.mapViewer.switchMap(map).then(() => {
if (map && map.data.world !== oldWorld) {
this.initPlayerMarkerManager();
this.initMarkerFileManager();
}
this.resetCamera();
});
}
resetCamera() {
let map = this.mapViewer.map;
let controls = this.mapViewer.controlsManager;
if (map) {
controls.position.set(map.data.startPos.x, 0, map.data.startPos.z);
controls.distance = 500;
controls.angle = 0;
controls.rotation = 0;
controls.tilt = 0;
controls.ortho = 0;
}
controls.controls = this.mapControls;
this.appState.controls.state = "perspective";
}
/**
* @returns {Promise<BlueMapMap[]>}
*/
loadMaps() {
return this.loadSettings().then(settings => {
let maps = [];
// create maps
if (settings.maps !== undefined){
for (let mapId in settings.maps) {
if (!Object.prototype.hasOwnProperty.call(settings.maps, mapId)) continue;
let mapSettings = settings.maps[mapId];
if (mapSettings.enabled) {
let map = new BlueMapMap(mapId, this.dataUrl + mapId + "/", this.dataUrl + "settings.json", this.dataUrl + "textures.json", this.mapViewer.events);
maps.push(map);
map.loadSettings()
.catch(error => {
alert(this.events, `Failed to load settings for map '${map.data.id}':` + error, "warning");
});
}
}
}
// sort maps
maps.sort((map1, map2) => {
let sort = settings.maps[map1.data.id].ordinal - settings.maps[map2.data.id].ordinal;
if (isNaN(sort)) return 0;
return sort;
});
return maps;
});
}
/**
* @returns {Promise<Object>}
*/
loadSettings() {
return new Promise((resolve, reject) => {
let loader = new FileLoader();
loader.setResponseType("json");
loader.load(this.dataUrl + "settings.json",
resolve,
() => {},
() => reject("Failed to load the settings.json!")
);
});
}
initPlayerMarkerManager() {
if (this.playerMarkerManager) {
this.playerMarkerManager.clear();
this.playerMarkerManager.dispose();
}
if (!this.mapViewer.map) return;
this.playerMarkerManager = new PlayerMarkerManager(this.mapViewer.markers, "live/players", this.mapViewer.map.data.world, this.events);
this.playerMarkerManager.update()
.then(() => {
this.playerMarkerManager.setAutoUpdateInterval(1000);
})
.catch(e => {
alert(this.events, e, "warning");
});
}
initMarkerFileManager() {
if (this.markerFileManager) {
this.markerFileManager.clear();
this.markerFileManager.dispose();
}
if (!this.mapViewer.map) return;
this.markerFileManager = new MarkerFileManager(this.mapViewer.markers, "data/markers.json", this.mapViewer.map.data.id, this.events);
this.markerFileManager.update()
.then(() => {
this.markerFileManager.setAutoUpdateInterval(1000 * 10);
})
.catch(e => {
alert(this.events, e, "warning");
});
}
updateControlsSettings() {
let mouseInvert = this.appState.controls.invertMouse ? -1 : 1;
this.freeFlightControls.mouseRotate.speedCapture = -0.002 * this.appState.controls.mouseSensitivity;
this.freeFlightControls.mouseAngle.speedCapture = -0.002 * this.appState.controls.mouseSensitivity * mouseInvert;
this.freeFlightControls.mouseRotate.speedRight = -0.002 * this.appState.controls.mouseSensitivity;
this.freeFlightControls.mouseAngle.speedRight = -0.002 * this.appState.controls.mouseSensitivity * mouseInvert;
}
setPerspectiveView() {
if (this.mapViewer.controlsManager.controls !== this.mapControls) {
this.mapViewer.controlsManager.controls = this.mapControls;
} else {
this.mapControls.setPerspectiveView();
}
this.appState.controls.state = "perspective";
}
setFlatView() {
if (this.mapViewer.controlsManager.controls !== this.mapControls) {
this.mapViewer.controlsManager.controls = this.mapControls;
}
this.mapControls.setOrthographicView();
this.appState.controls.state = "flat";
}
setFreeFlight() {
this.mapViewer.controlsManager.controls = this.freeFlightControls;
this.appState.controls.state = "free";
}
setDebug(debug) {
this.appState.debug = debug;
if (debug){
this.mapViewer.stats.showPanel(0);
} else {
this.mapViewer.stats.showPanel(-1);
}
}
setTheme(theme) {
this.appState.theme = theme;
if (theme === "light") {
this.mapViewer.rootElement.classList.remove("theme-dark");
this.mapViewer.rootElement.classList.add("theme-light");
}
else if (theme === "dark") {
this.mapViewer.rootElement.classList.remove("theme-light");
this.mapViewer.rootElement.classList.add("theme-dark");
}
else {
this.mapViewer.rootElement.classList.remove("theme-light");
this.mapViewer.rootElement.classList.remove("theme-dark");
}
}
}

54
src/js/MainMenu.js Normal file
View File

@ -0,0 +1,54 @@
export class MainMenu {
static NULL_PAGE = {
id: "-",
title: "-"
}
constructor() {
this.isOpen = false;
this.pageStack = [];
}
currentPage() {
if (this.pageStack.length === 0) return MainMenu.NULL_PAGE;
return this.pageStack[this.pageStack.length - 1];
}
openPage(id = "root", title = "Menu", data = {}) {
if (!this.isOpen){
this.pageStack.splice(0, this.pageStack.length);
this.isOpen = true;
}
this.pageStack.push({
id: id,
title: title,
...data
});
}
closePage() {
this.pageStack.splice(this.pageStack.length - 1, 1);
if (this.pageStack.length < 1) {
this.isOpen = false
}
}
reOpenPage() {
if (this.pageStack.length === 0){
this.openPage();
} else if (this.pageStack[0].id !== 'root') {
this.pageStack.splice(0, this.pageStack.length);
this.openPage();
} else {
this.isOpen = true;
}
}
closeAll() {
this.isOpen = false;
}
}

207
src/js/PopupMarker.js Normal file
View File

@ -0,0 +1,207 @@
import {Marker} from "bluemap/src/markers/Marker";
import {CSS2DObject} from "bluemap/src/util/CSS2DRenderer";
import {animate, htmlToElement} from "bluemap/src/util/Utils";
import {BoxGeometry, MeshBasicMaterial, Mesh, Vector2} from "three";
export class PopupMarker extends Marker {
constructor(id, appState, events) {
super(id);
this.data.type = "popup";
this.data.label = "Last Map Interaction";
this.appState = appState;
this.events = events;
this.visible = false;
this.elementObject = new CSS2DObject(htmlToElement(`<div id="bm-marker-${this.data.id}" class="bm-marker-${this.data.type}">Test</div>`));
this.elementObject.position.set(0.5, 1, 0.5);
this.addEventListener( 'removed', () => {
if (this.element.parentNode) this.element.parentNode.removeChild(this.element);
});
let cubeGeo = new BoxGeometry(1.01, 1.01, 1.01).translate(0.5, 0.5, 0.5);
let cubeMaterial = new MeshBasicMaterial( {color: 0xffffff, opacity: 0.5, transparent: true} );
this.cube = new Mesh(cubeGeo, cubeMaterial);
this.cube.onClick = evt => this.onClick(evt);
this.add(this.elementObject);
this.add(this.cube);
this.animation = null;
this.events.addEventListener('bluemapMapInteraction', this.onMapInteraction);
window.addEventListener("mousedown", this.removeHandler);
window.addEventListener("touchstart", this.removeHandler);
window.addEventListener("keydown", this.removeHandler);
window.addEventListener("mousewheel", this.removeHandler);
}
onClick(event) {
return true;
}
onMapInteraction = evt => {
let isHires = true;
let int = evt.detail.hiresHit;
if (!int) {
isHires = false;
int = evt.detail.lowresHit;
}
if (!int) return;
this.position
.copy(int.pointOnLine || int.point)
.add(evt.detail.ray.direction.clone().multiplyScalar(0.05))
.floor();
//this.elementObject.position
//.copy(evt.detail.intersection.pointOnLine || evt.detail.intersection.point)
//.sub(this.position);
console.log(int);
if (isHires) {
this.element.innerHTML = `
<div class="group">
<div class="label">Block:</div>
<div class="content">
<div class="entry"><span class="label">x: </span><span class="value">${this.position.x}</span></div>
<div class="entry"><span class="label">y: </span><span class="value">${this.position.y}</span></div>
<div class="entry"><span class="label">z: </span><span class="value">${this.position.z}</span></div>
</div>
</div>
`;
} else {
this.element.innerHTML = `
<div class="group">
<div class="label">Position:</div>
<div class="content">
<div class="entry"><span class="label">x: </span><span class="value">${this.position.x}</span></div>
<div class="entry"><span class="label">z: </span><span class="value">${this.position.z}</span></div>
</div>
</div>
`;
}
if (this.appState.debug) {
let chunkCoords = this.position.clone().divideScalar(16).floor();
let regionCoords = new Vector2(this.position.x, this.position.z).divideScalar(512).floor();
let regionFile = `r.${regionCoords.x}.${regionCoords.y}.mca`;
this.element.innerHTML += `
<hr>
<div class="group">
<div class="label">Chunk:</div>
<div class="content">
<div class="entry"><span class="label">x: </span><span class="value">${chunkCoords.x}</span></div>
<div class="entry"><span class="label">y: </span><span class="value">${chunkCoords.y}</span></div>
<div class="entry"><span class="label">z: </span><span class="value">${chunkCoords.z}</span></div>
</div>
</div>
<hr>
<div class="group">
<div class="label">Region:</div>
<div class="content">
<div class="entry"><span class="label">x: </span><span class="value">${regionCoords.x}</span></div>
<div class="entry"><span class="label">z: </span><span class="value">${regionCoords.y}</span></div>
</div>
<div class="content">
<div class="entry"><span class="label">File: </span><span class="value">${regionFile}</span></div>
</div>
</div>
`;
}
if (this.appState.debug) {
let faceIndex = int.faceIndex;
let attributes = int.object.geometry.attributes;
if (attributes.sunlight && attributes.blocklight) {
let sunlight = attributes.sunlight.array[faceIndex * 3];
let blocklight = attributes.blocklight.array[faceIndex * 3];
this.element.innerHTML += `
<hr>
<div class="group">
<div class="label">Light:</div>
<div class="content">
<div class="entry"><span class="label">Sun: </span><span class="value">${sunlight}</span></div>
<div class="entry"><span class="label">Block: </span><span class="value">${blocklight}</span></div>
</div>
</div>
`;
}
}
if (this.appState.debug) {
let info = "";
if (isHires) {
let hrPath = evt.detail.hiresHit.object.userData.tileUrl;
info += `<div>${hrPath}</div>`;
}
let lrPath = evt.detail.lowresHit.object.userData.tileUrl;
info += `<div>${lrPath}</div>`;
this.element.innerHTML += `
<hr>
<div class="files">
${info}
</div>
`;
}
this.open();
};
open() {
if (this.animation) this.animation.cancel();
this.visible = true;
this.cube.visible = true;
let targetOpacity = 1;
this.element.style.opacity = "0";
this.animation = animate(progress => {
this.element.style.opacity = (progress * targetOpacity).toString();
}, 300);
}
removeHandler = evt => {
if (evt.composedPath().includes(this.element)) return;
this.close();
}
close() {
if (this.animation) this.animation.cancel();
this.cube.visible = false;
let startOpacity = parseFloat(this.element.style.opacity);
this.animation = animate(progress => {
this.element.style.opacity = (startOpacity - progress * startOpacity).toString();
}, 300, finished => {
if (finished) this.visible = false;
});
}
/**
* @returns {Element}
*/
get element() {
return this.elementObject.element.getElementsByTagName("div")[0];
}
dispose() {
super.dispose();
if (this.element.parentNode) this.element.parentNode.removeChild(this.element);
}
}

29
src/main.js Normal file
View File

@ -0,0 +1,29 @@
import Vue from 'vue'
import App from './App.vue'
import {BlueMapApp} from "@/js/BlueMapApp";
// utils
String.prototype.includesCI = function (val) {
return this.toLowerCase().includes(val.toLowerCase());
}
// bluemap app
const bluemap = new BlueMapApp(document.getElementById("map-container"));
// init vue
Vue.config.productionTip = false;
Object.defineProperty(Vue.prototype, '$bluemap', {
get () { return bluemap }
});
let vue = new Vue({
render: h => h(App)
}).$mount('#app');
// make bluemap accessible in console
window.bluemap = bluemap;
// load bluemap next tick (to let the assets load first)
vue.$nextTick(() => {
bluemap.load().catch(error => console.error(error));
});

58
src/scss/global.scss Normal file
View File

@ -0,0 +1,58 @@
@import "variables.scss";
// ### global rules ###
:root {
line-height: 1rem;
@include dark-theme;
.theme-light {
@include light-theme;
}
}
@media (prefers-color-scheme: light) {
:root {
@include light-theme;
.theme-dark {
@include dark-theme;
}
}
}
body {
margin: 0;
padding: 0;
}
// normalize input fields
input {
display: inline-block;
box-sizing: content-box;
border: none;
outline: none;
margin: 0;
padding: 0;
font: inherit;
color: inherit;
}
// scrollbar
::-webkit-scrollbar {
width: 0.5em;
}
::-webkit-scrollbar-track {
background: var(--theme-bg-light);
}
::-webkit-scrollbar-thumb {
background: var(--theme-fg-light);
}
::-webkit-scrollbar-thumb:hover {
background: var(--theme-fg);
}
@import "markers.scss";

131
src/scss/markers.scss Normal file
View File

@ -0,0 +1,131 @@
#map-container {
.bm-marker-html {
position: relative;
.bm-marker-poi-label {
position: absolute;
top: 0;
left: 0;
opacity: 0;
transition: opacity 0.3s;
}
.bm-marker-poi-icon {
opacity: 1;
transition: opacity 0.3s;
filter: drop-shadow(1px 1px 3px #0008);
}
&.bm-marker-highlight {
.bm-marker-poi-label {
opacity: 1;
}
.bm-marker-poi-icon {
opacity: 0;
}
}
}
.bm-marker-html .bm-marker-poi-label,
.bm-marker-labelpopup,
.bm-marker-popup {
transform: translate(-50%, -100%) translate(0, -0.5em);
max-width: 15em;
color: var(--theme-fg);
background-color: var(--theme-bg);
filter: drop-shadow(1px 1px 3px #0008);
padding: 0.5em;
> hr {
border: none;
border-bottom: solid 1px var(--theme-bg-light);
margin: 0.5em -0.5em;
}
&:after {
position: absolute;
bottom: calc(-1em + 1px);
left: 50%;
transform: translate(-50%, 0);
content: '';
border: solid 0.5em transparent;
border-top-color: var(--theme-bg);
}
}
.bm-marker-popup {
line-height: 1.2em;
.group {
> .label {
position: relative;
top: 0;
left: 0.5em;
margin: 0 0.5em;
font-size: 0.8em;
color: var(--theme-fg-light);
}
> .content {
display: flex;
justify-content: center;
> .entry {
margin: 0 0.5em;
> .label {
color: var(--theme-fg-light);
}
}
}
}
.files {
font-size: 0.8em;
color: var(--theme-fg-light);
}
}
.bm-marker-player {
position: relative;
transform: translate(-50%, -50%);
img {
width: 32px;
image-rendering: pixelated;
transition: width 0.3s;
}
.bm-player-name {
position: absolute;
top: -0.5em;
left: 50%;
transform: translate(-50%, -100%);
padding: 0.25em;
background-color: #0008;
color: #fff;
transition: opacity 0.3s;
}
&[distance-data="med"],
&[distance-data="far"] {
img {
width: 16px;
}
.bm-player-name {
opacity: 0;
}
}
}
}

45
src/scss/variables.scss Normal file
View File

@ -0,0 +1,45 @@
// responsive breaks
$mobile-break: 575.98px;
// themes
@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500&display=swap');
@mixin dark-theme {
font-family: 'Quicksand', sans-serif;
font-size: 16px;
font-weight: 400;
--theme-bg: #181818;
--theme-bg-light: #333;
--theme-fg: #fff;
--theme-fg-light: #aaa;
//SwitchButton
--theme-switch-button-on: #006EDE;
//DayNightSwitch
--theme-stars-day: #fff;
--theme-moon-day: #ff0;
--theme-stars-night: #444;
--theme-moon-night: #000;
}
@mixin light-theme {
font-family: 'Quicksand', sans-serif;
font-size: 16px;
font-weight: 500;
--theme-bg: #eee;
--theme-bg-light: #aaa;
--theme-fg: #000;
--theme-fg-light: #444;
//SwitchButton
--theme-switch-button-on: #006EDE;
//DayNightSwitch
--theme-stars-day: #444;
--theme-moon-day: #000;
--theme-stars-night: #fff;
--theme-moon-night: #ff0;
}

22
vue.config.js Normal file
View File

@ -0,0 +1,22 @@
/**
* @type {import('@vue/cli-service').ProjectOptions}
*/
module.exports = {
publicPath: './',
devServer: {
proxy: {
'/data': {
target: 'https://bluecolored.de/bluemap',
changeOrigin: true,
},
'/assets/playerheads': {
target: 'https://bluecolored.de/bluemap',
changeOrigin: true,
},
'/live': {
target: 'https://bluecolored.de/bluemap',
changeOrigin: true,
},
}
}
}