mirror of
https://github.com/BlueMap-Minecraft/BlueMapVue.git
synced 2024-11-22 01:25:11 +01:00
Initial commit
This commit is contained in:
commit
039215d7fe
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal 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
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "BlueMapWeb"]
|
||||
path = BlueMapWeb
|
||||
url = https://github.com/BlueMap-Minecraft/BlueMapWeb
|
1
BlueMapWeb
Submodule
1
BlueMapWeb
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 760368eaa2adae49809674ca153a215296f63f28
|
24
README.md
Normal file
24
README.md
Normal 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
5
babel.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
12813
package-lock.json
generated
Normal file
12813
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
67
package.json
Normal file
67
package.json
Normal 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
BIN
public/assets/steve.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.9 KiB |
17
public/index.html
Normal file
17
public/index.html
Normal 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
49
src/App.vue
Normal 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>
|
72
src/components/ControlBar/Compass.vue
Normal file
72
src/components/ControlBar/Compass.vue
Normal 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>
|
167
src/components/ControlBar/ControlBar.vue
Normal file
167
src/components/ControlBar/ControlBar.vue
Normal 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>
|
74
src/components/ControlBar/ControlsSwitch.vue
Normal file
74
src/components/ControlBar/ControlsSwitch.vue
Normal 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>
|
70
src/components/ControlBar/DayNightSwitch.vue
Normal file
70
src/components/ControlBar/DayNightSwitch.vue
Normal 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>
|
85
src/components/ControlBar/MenuButton.vue
Normal file
85
src/components/ControlBar/MenuButton.vue
Normal 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>
|
65
src/components/ControlBar/NumberInput.vue
Normal file
65
src/components/ControlBar/NumberInput.vue
Normal 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>
|
39
src/components/ControlBar/PositionInput.vue
Normal file
39
src/components/ControlBar/PositionInput.vue
Normal 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>
|
55
src/components/ControlBar/SvgButton.vue
Normal file
55
src/components/ControlBar/SvgButton.vue
Normal 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>
|
38
src/components/Menu/Group.vue
Normal file
38
src/components/Menu/Group.vue
Normal 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>
|
53
src/components/Menu/MainMenu.vue
Normal file
53
src/components/Menu/MainMenu.vue
Normal 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>
|
72
src/components/Menu/MapButton.vue
Normal file
72
src/components/Menu/MapButton.vue
Normal 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 + ')'}">•</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>
|
131
src/components/Menu/MarkerItem.vue
Normal file
131
src/components/Menu/MarkerItem.vue
Normal 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>
|
150
src/components/Menu/MarkerSet.vue
Normal file
150
src/components/Menu/MarkerSet.vue
Normal 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>
|
69
src/components/Menu/MarkerSetMenu.vue
Normal file
69
src/components/Menu/MarkerSetMenu.vue
Normal 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>
|
87
src/components/Menu/SettingsMenu.vue
Normal file
87
src/components/Menu/SettingsMenu.vue
Normal 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>
|
131
src/components/Menu/SideMenu.vue
Normal file
131
src/components/Menu/SideMenu.vue
Normal 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>
|
85
src/components/Menu/SimpleButton.vue
Normal file
85
src/components/Menu/SimpleButton.vue
Normal 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>
|
93
src/components/Menu/Slider.vue
Normal file
93
src/components/Menu/Slider.vue
Normal 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>
|
46
src/components/Menu/SwitchButton.vue
Normal file
46
src/components/Menu/SwitchButton.vue
Normal 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>
|
48
src/components/Menu/SwitchHandle.vue
Normal file
48
src/components/Menu/SwitchHandle.vue
Normal 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>
|
20
src/components/Menu/TextInput.vue
Normal file
20
src/components/Menu/TextInput.vue
Normal 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
283
src/js/BlueMapApp.js
Normal 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
54
src/js/MainMenu.js
Normal 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
207
src/js/PopupMarker.js
Normal 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
29
src/main.js
Normal 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
58
src/scss/global.scss
Normal 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
131
src/scss/markers.scss
Normal 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
45
src/scss/variables.scss
Normal 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
22
vue.config.js
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user