componentize all the things (also: icons)
|
@ -15,3 +15,4 @@ Title fonts are from [fontlibrary.org](https://fontlibrary.org/):
|
||||||
|
|
||||||
Fonts are optimized with the [FontSquirrel webfont generator](https://www.fontsquirrel.com/tools/webfont-generator).
|
Fonts are optimized with the [FontSquirrel webfont generator](https://www.fontsquirrel.com/tools/webfont-generator).
|
||||||
|
|
||||||
|
Icons made by [Reussy](https://www.flaticon.com/authors/reussy) from [Flaticon](https://www.flaticon.com/).
|
||||||
|
|
214
src/App.vue
|
@ -1,175 +1,83 @@
|
||||||
<template>
|
<template>
|
||||||
<header>
|
<Headline
|
||||||
<div class="headline">
|
v-bind="{ labelFonts, themes }"
|
||||||
<h1>Starsy</h1>
|
@select:font="setFont($event)"
|
||||||
<p>Starsystem<br/>Generator</p>
|
@select:theme="setTheme($event)"
|
||||||
</div>
|
/>
|
||||||
<div class="options">
|
<SystemDiagram v-bind="{ star, objects }" />
|
||||||
<label>
|
|
||||||
Title Font
|
|
||||||
<select v-model="selectedFont">
|
|
||||||
<option v-for="f in labelFonts">{{ f }}</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
Color Theme
|
|
||||||
<select v-model="selectedTheme" @change="setTheme">
|
|
||||||
<option v-for="t in themes">{{ t }}</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1000 300">
|
|
||||||
<line id="axis" x1="0" y1="150" x2="1000" y2="150" />
|
|
||||||
<circle id="star" :r="star.radius" :cx="starCX" cy="150" />
|
|
||||||
|
|
||||||
<g class="object" :id="o.name" v-for="o in objects">
|
|
||||||
<g class="rings" v-for="i in o.rings">
|
|
||||||
<circle :r="o.radius - 5 + 2*i" :cx="o.distance" cy="150" />
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<text :class="{ tilted: o.radius < 10 }" :x="o.distance" :y="140 - o.radius">{{ o.name }}</text>
|
|
||||||
<circle v-if="o.type === 'planet'" :r="o.radius" :cx="o.distance" cy="150" />
|
|
||||||
<line v-if="o.satellites.length" :x1="o.distance" y1="150" :x2="o.distance" :y2="150 + o.radius + 10*o.satellites.length" />
|
|
||||||
|
|
||||||
<g class="satellite" v-for="m,i in o.satellites">
|
|
||||||
<rect v-if="m.type === 'station'" class="station" :x="o.distance - 2" :y="158 + o.radius + 10*i" width="4" height="4" />
|
|
||||||
<circle v-else :r="m.radius" :cx="o.distance" :cy="160 + o.radius + 10*i" />
|
|
||||||
<text :x="o.distance + 5" :y="162 + o.radius + 10*i">{{ m.name }}</text>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<text id="designation" :class="`title-${selectedFont}`" x="980" y="30">{{ star.designation }}</text>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<section id="settings">
|
<section id="settings">
|
||||||
<header>
|
<ObjectSettings v-model:object="selectedObject" v-if="selectedObject" />
|
||||||
<h1>Star System Parameters</h1>
|
<Tips>
|
||||||
<menu id="system-settings">
|
<li>Edit planets by clicking directly inside the graphic or in the table below.</li>
|
||||||
<label>
|
<li>Drag planets around to change their distance.</li>
|
||||||
Name
|
<li>Use the scrollwheel to change their size.</li>
|
||||||
<input type="text" v-model="star.designation" />
|
<li>To change satellites, click their respective buttons in the planet dialog.</li>
|
||||||
</label>
|
<li>You can also drag satellite buttons around to reorder them.</li>
|
||||||
<label>
|
</Tips>
|
||||||
Star Size
|
<SystemSettings v-model:designation="star.designation" v-model:radius="star.radius" />
|
||||||
<input type="range" min="50" max="1500" v-model="star.radius" />
|
<ObjectList v-bind="{ objects, editObject, deleteObject }" />
|
||||||
({{ star.radius }})
|
|
||||||
</label>
|
|
||||||
</menu>
|
|
||||||
</header>
|
|
||||||
<menu id="object-list">
|
|
||||||
<div class="menu-item" :class="{ open: selectedObject === o }" v-for="o in objects">
|
|
||||||
<label @click="toggleObject(o)">{{ o.name }}</label>
|
|
||||||
<div class="object-settings">
|
|
||||||
<header>
|
|
||||||
<h2><input type="text" v-model="o.name" @blur="checkName(o)"/> settings</h2>
|
|
||||||
<p>Distance Δ: {{ o.distance }}</p>
|
|
||||||
<p>Radius r: {{ o.radius }}</p>
|
|
||||||
<p>Rings ⬭: {{ o.rings > 0 ? o.rings : 'none' }}</p>
|
|
||||||
<p>Satellites: {{ listSatellites(o) }}</p>
|
|
||||||
</header>
|
|
||||||
<label>
|
|
||||||
<span>Δ</span>
|
|
||||||
<input class="planet-distance" type="range" min="50" max="1000" v-model="o.distance" />
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>r</span>
|
|
||||||
<input class="planet-radius" type="range" min="1" max="125" v-model="o.radius" />
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>⬭</span>
|
|
||||||
<input class="planet-rings" type="number" min="0" max="15" v-model="o.rings" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button>add object</button>
|
|
||||||
</menu>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed } from 'vue'
|
import { ref, reactive } from 'vue'
|
||||||
import steepCurve from './steep-curve'
|
import exampleData from './example-data'
|
||||||
|
import Headline from './components/Headline.vue'
|
||||||
|
import SystemDiagram from './components/SystemDiagram.vue'
|
||||||
|
import Tips from './components/Tips.vue'
|
||||||
|
import SystemSettings from './components/SystemSettings.vue'
|
||||||
|
import ObjectList from './components/ObjectList.vue'
|
||||||
|
import ObjectSettings from './components/ObjectSettings.vue'
|
||||||
|
|
||||||
const star = reactive({
|
const star = reactive({
|
||||||
designation: 'Sol',
|
designation: 'Sol',
|
||||||
radius: 400,
|
radius: 400,
|
||||||
})
|
})
|
||||||
|
|
||||||
const starCX = computed(() => -1 * star.radius * steepCurve(star.radius, 50, 0.955))
|
const objects = ref(exampleData)
|
||||||
|
const labelFonts = ['xolonium', 'douar', 'lack']
|
||||||
const objects = ref([
|
const themes = ['default', 'retro', 'inverse', 'paper']
|
||||||
{ type: 'planet', name: 'Mercury', radius: 1, distance: 100, satellites: [], rings: 0 },
|
|
||||||
{ type: 'planet', name: 'Venus', radius: 4, distance: 120, satellites: [], rings: 0 },
|
|
||||||
{ type: 'planet', name: 'Terra', radius: 4, distance: 140, satellites: [
|
|
||||||
{ name: 'ISS', type: 'station' },
|
|
||||||
{ name: 'Luna', radius: 2 },
|
|
||||||
], rings: 0 },
|
|
||||||
{ type: 'planet', name: 'Mars', radius: 2, distance: 160, satellites: [
|
|
||||||
{ name: 'MTO', type: 'station' },
|
|
||||||
{ name: 'Phobos', radius: 1 },
|
|
||||||
{ name: 'Daimos', radius: 1 },
|
|
||||||
], rings: 0 },
|
|
||||||
{ type: 'planet', name: 'Jupiter', radius: 40, distance: 260, satellites: [
|
|
||||||
{ name: 'Io', radius: 2 },
|
|
||||||
{ name: 'Europa', radius: 2 },
|
|
||||||
{ name: 'Ganymede', radius: 4 },
|
|
||||||
{ name: 'Callisto', radius: 3 },
|
|
||||||
], rings: 1 },
|
|
||||||
{ type: 'planet', name: 'Saturn', radius: 36, distance: 410, satellites: [
|
|
||||||
{ name: 'Mimas', radius: 1 },
|
|
||||||
{ name: 'Enceladus', radius: 1 },
|
|
||||||
{ name: 'Tethys', radius: 1 },
|
|
||||||
{ name: 'Dione', radius: 1 },
|
|
||||||
{ name: 'Rhea', radius: 1 },
|
|
||||||
{ name: 'Titan', radius: 3 },
|
|
||||||
{ name: 'Iapetus', radius: 1 },
|
|
||||||
], rings: 5 },
|
|
||||||
{ type: 'planet', name: 'Uranus', radius: 16, distance: 680, satellites: [
|
|
||||||
{ name: 'Miranda', radius: 1 },
|
|
||||||
{ name: 'Ariel', radius: 1 },
|
|
||||||
{ name: 'Umbriel', radius: 1 },
|
|
||||||
{ name: 'Titania', radius: 1 },
|
|
||||||
{ name: 'Oberon', radius: 1 },
|
|
||||||
], rings: 2 },
|
|
||||||
{ type: 'planet', name: 'Neptune', radius: 15, distance: 950, satellites: [
|
|
||||||
{ name: 'Triton', radius: 1 },
|
|
||||||
], rings: 0 },
|
|
||||||
])
|
|
||||||
|
|
||||||
const selectedObject = ref(null)
|
const selectedObject = ref(null)
|
||||||
|
|
||||||
function toggleObject (obj) {
|
function editObject (obj) {
|
||||||
selectedObject.value = selectedObject.value === obj ? null : obj
|
selectedObject.value = obj
|
||||||
}
|
}
|
||||||
function checkName (obj) {
|
|
||||||
if (!obj.name.trim().length) {
|
function deleteObject (obj) {
|
||||||
const index = objects.value.indexOf(obj)
|
console.log('delete object not yet implemented')
|
||||||
obj.name = `${star.designation}-${index}`
|
}
|
||||||
|
|
||||||
|
function autoName (obj) {
|
||||||
|
const index = objects.value.indexOf(obj)
|
||||||
|
return `${star.designation}-${index}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTheme (theme) {
|
||||||
|
const classes = document.body.className.split(' ')
|
||||||
|
const currentTheme = classes.find(c => c.startsWith('theme-'))
|
||||||
|
const newTheme = `theme-${theme}`
|
||||||
|
|
||||||
|
if (currentTheme) {
|
||||||
|
document.body.classList.replace(currentTheme, newTheme)
|
||||||
|
} else {
|
||||||
|
document.body.classList.add(newTheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function setFont (font) {
|
||||||
|
const classes = document.body.className.split(' ')
|
||||||
|
const currentFont = classes.find(c => c.startsWith('title-'))
|
||||||
|
const newFont = `title-${font}`
|
||||||
|
|
||||||
|
if (currentFont) {
|
||||||
|
document.body.classList.replace(currentFont, newFont)
|
||||||
|
} else {
|
||||||
|
document.body.classList.add(newFont)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const labelFonts = ['douar', 'lack', 'xolonium']
|
setTheme(themes[0])
|
||||||
const selectedFont = ref('xolonium')
|
setFont(labelFonts[0])
|
||||||
|
|
||||||
const themes = ['default', 'retro', 'inverse', 'paper']
|
|
||||||
const selectedTheme = ref('default')
|
|
||||||
|
|
||||||
function setTheme () {
|
|
||||||
document.body.className = `theme-${selectedTheme.value}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function listSatellites (obj) {
|
|
||||||
if (!obj.satellites || !obj.satellites.length) return 'none'
|
|
||||||
return obj.satellites.reduce((acc, satellite) => {
|
|
||||||
let s = satellite.name
|
|
||||||
if (satellite.type) s += ` (${satellite.type})`
|
|
||||||
acc.push(s)
|
|
||||||
return acc
|
|
||||||
}, []).join(', ')
|
|
||||||
}
|
|
||||||
setTheme()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
102
src/app.css
|
@ -43,7 +43,7 @@ html,body {
|
||||||
color: var(--fg-app);
|
color: var(--fg-app);
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"] {
|
input[type="text"], input[type="number"] {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid var(--fg-app);
|
border: 1px solid var(--fg-app);
|
||||||
color: var(--fg-app);
|
color: var(--fg-app);
|
||||||
|
@ -53,6 +53,34 @@ input[type="text"]:focus {
|
||||||
border: 1px solid #FFF5;
|
border: 1px solid #FFF5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button.settings, button.delete, button.add, button.less {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border: 4px solid var(--bg-app);
|
||||||
|
background-color: var(--bg-app);
|
||||||
|
background-size: 32px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button.settings { background-image: url(./assets/change.png); }
|
||||||
|
button.delete { background-image: url(./assets/delete.png); }
|
||||||
|
button.add { background-image: url(./assets/add.png); }
|
||||||
|
button.less { background-image: url(./assets/less.png); }
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
width: calc(100% - 4em);
|
||||||
|
margin-left: -1em;
|
||||||
|
padding: 1em 2em;
|
||||||
|
border: 2px solid #8888;
|
||||||
|
border-left-width: 1em;
|
||||||
|
}
|
||||||
|
.tip > header { font-weight: bold; }
|
||||||
|
.tip > li { margin-top: .75em; }
|
||||||
|
|
||||||
body.theme-retro {
|
body.theme-retro {
|
||||||
--bg-app: #4B4839;
|
--bg-app: #4B4839;
|
||||||
--fg-app: #E4DCB5;
|
--fg-app: #E4DCB5;
|
||||||
|
@ -84,9 +112,9 @@ body.theme-paper {
|
||||||
--fg-settings: #000;
|
--fg-settings: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-douar { font-family: 'douar'; }
|
body.title-douar { --title-font: 'douar'; }
|
||||||
.title-lack { font-family: 'lack'; }
|
body.title-lack { --title-font: 'lack'; }
|
||||||
.title-xolonium { font-family: 'xolonium'; }
|
body.title-xolonium { --title-font: 'xolonium'; }
|
||||||
|
|
||||||
.menu-item {
|
.menu-item {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
|
@ -121,7 +149,7 @@ svg { background: var(--bg-graphic); }
|
||||||
line { stroke: var(--fg-graphic); }
|
line { stroke: var(--fg-graphic); }
|
||||||
text.tilted { transform: rotate(-45deg) translate(0, 100%); transform-origin: left top; transform-box: fill-box; }
|
text.tilted { transform: rotate(-45deg) translate(0, 100%); transform-origin: left top; transform-box: fill-box; }
|
||||||
#axis { stroke-width: 1; }
|
#axis { stroke-width: 1; }
|
||||||
#designation { fill: var(--fg-graphic); text-anchor: end; }
|
#designation { fill: var(--fg-graphic); text-anchor: end; font-family: var(--title-font); }
|
||||||
#star { fill: var(--fill-star); }
|
#star { fill: var(--fill-star); }
|
||||||
.object { fill: var(--fg-graphic); text-anchor: middle; font-size: .6em; cursor: pointer; }
|
.object { fill: var(--fg-graphic); text-anchor: middle; font-size: .6em; cursor: pointer; }
|
||||||
.object > line { stroke-width: .5; }
|
.object > line { stroke-width: .5; }
|
||||||
|
@ -149,20 +177,22 @@ h1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
#settings {
|
#settings {
|
||||||
|
max-width: 700px;
|
||||||
|
margin: auto;
|
||||||
padding: 1em 2em;
|
padding: 1em 2em;
|
||||||
}
|
}
|
||||||
#settings > header, .object-settings > header {
|
#settings > header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
#settings > header > h1 {
|
#settings > header > h1 {
|
||||||
min-width: 330px;
|
min-width: 330px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#system-settings, #object-list {
|
#system-settings {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: calc(100vw - 4em);
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
@ -174,20 +204,38 @@ h1 {
|
||||||
margin: 0 1em;
|
margin: 0 1em;
|
||||||
}
|
}
|
||||||
#system-settings button { height: 2em; }
|
#system-settings button { height: 2em; }
|
||||||
#system-settings input { width: 220px; }
|
#system-settings input { width: 200px; }
|
||||||
#system-settings input[type="text"] {
|
#system-settings input[type="text"] {
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
padding: .5em 1em .4em;
|
padding: .5em 1em .4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.object-settings > header > p {
|
#object-list {
|
||||||
margin-left: 2em;
|
display: block;
|
||||||
padding-top: .37em;
|
width: 100%;
|
||||||
|
margin: 3em auto;
|
||||||
|
}
|
||||||
|
#object-list th {
|
||||||
|
padding: .25em 1em;
|
||||||
|
border-bottom: 2px solid var(--fg-app);
|
||||||
|
}
|
||||||
|
#object-list .cell {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
padding: .25em 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.object-settings > label {
|
.object-settings {
|
||||||
|
margin-bottom: 5em;
|
||||||
|
}
|
||||||
|
.object-settings > header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
.object-settings > header > p {
|
||||||
|
margin-left: 2em;
|
||||||
}
|
}
|
||||||
.object-settings > header input[type="text"] {
|
.object-settings > header input[type="text"] {
|
||||||
width: 7em;
|
width: 7em;
|
||||||
|
@ -195,9 +243,27 @@ h1 {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.object-settings > label > span {
|
.object-settings input[type="range"] { width: 100%; }
|
||||||
width: 2em;
|
.object-settings input[type="number"] {
|
||||||
|
width: 4em;
|
||||||
|
padding: .2em 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.object-settings .satellite-list {
|
||||||
|
|
||||||
|
}
|
||||||
|
.object-settings .satellite {
|
||||||
|
vertical-align: middle;
|
||||||
|
margin: 0 .2em;
|
||||||
|
padding: 2px .5em;
|
||||||
|
font-size: 1.2em;
|
||||||
|
background: var(--bg-app);
|
||||||
|
color: var(--fg-app);
|
||||||
|
border: 3px solid var(--fg-app);
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: border-color .2s ease-out;
|
||||||
|
}
|
||||||
|
.object-settings .satellite:hover {
|
||||||
|
outline: 1px solid var(--fg-app);
|
||||||
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
.object-settings .planet-distance { width: 100%; }
|
|
||||||
.object-settings .planet-radius { width: 100%; }
|
|
||||||
.object-settings .planet-rings { width: 3em; padding: .2em; }
|
|
||||||
|
|
BIN
src/assets/add.png
Normal file
After Width: | Height: | Size: 801 B |
BIN
src/assets/add_inverted.png
Normal file
After Width: | Height: | Size: 763 B |
BIN
src/assets/change.png
Normal file
After Width: | Height: | Size: 958 B |
BIN
src/assets/change_inverted.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/delete.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/assets/less.png
Normal file
After Width: | Height: | Size: 717 B |
BIN
src/assets/less_inverted.png
Normal file
After Width: | Height: | Size: 691 B |
5
src/components/DeleteButton.vue
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<template>
|
||||||
|
<button title="delete object" @click="$emit('click', $event)">
|
||||||
|
<img src="../assets/delete.png" alt="delete icon" />
|
||||||
|
</button>
|
||||||
|
</template>
|
34
src/components/Headline.vue
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<template>
|
||||||
|
<header>
|
||||||
|
<div class="headline">
|
||||||
|
<h1>Starsy</h1>
|
||||||
|
<p>Starsystem<br/>Generator</p>
|
||||||
|
</div>
|
||||||
|
<div class="options">
|
||||||
|
<label>
|
||||||
|
Title Font
|
||||||
|
<select v-model="selectedFont" @change="$emit('select:font', selectedFont)">
|
||||||
|
<option v-for="f in labelFonts">{{ f }}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Color Theme
|
||||||
|
<select v-model="selectedTheme" @change="$emit('select:theme', selectedTheme)">
|
||||||
|
<option v-for="t in themes">{{ t }}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
labelFonts: Array,
|
||||||
|
themes: Array,
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedFont = ref(props.labelFonts[0])
|
||||||
|
const selectedTheme = ref('default')
|
||||||
|
</script>
|
|
@ -1,21 +0,0 @@
|
||||||
<template>
|
|
||||||
<g class="planetary">
|
|
||||||
</g>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
name: String,
|
|
||||||
size: Number,
|
|
||||||
type: String, // TODO
|
|
||||||
moons: Object, // moons: { name, size as fraction of planet size }
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
a {
|
|
||||||
color: #42b983;
|
|
||||||
}
|
|
||||||
</style>
|
|
29
src/components/ObjectList.vue
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<table id="object-list">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" v-for="col in columns">{{ col }}</th>
|
||||||
|
<th scope="col">actions</th>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="o in objects">
|
||||||
|
<td v-for="value in values">
|
||||||
|
<div class="cell">{{ o[value] }}</div>
|
||||||
|
</td>
|
||||||
|
<td><div class="cell">{{ o.satellites.length }}</div></td>
|
||||||
|
<td><div class="cell">
|
||||||
|
<button class="settings" @click="editObject(o)"> </button>
|
||||||
|
<button class="delete" @click="deleteObject(o)"> </button>
|
||||||
|
</div></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
objects: Array,
|
||||||
|
editObject: Function,
|
||||||
|
deleteObject: Function,
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = ['Δ', 'Name', 'Type', 'Radius', 'Rings', 'Satellites']
|
||||||
|
const values = ['distance', 'name', 'type', 'radius', 'rings']
|
||||||
|
</script>
|
62
src/components/ObjectSettings.vue
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<template>
|
||||||
|
<div class="object-settings">
|
||||||
|
<header>
|
||||||
|
<h2><input type="text" v-model="name" @blur="checkName"/></h2>
|
||||||
|
<p>
|
||||||
|
Distance Δ:
|
||||||
|
<input type="number" min="50" max="1000" v-model="distance" />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Radius r:
|
||||||
|
<input type="number" min="1" max="125" v-model="radius" />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Rings:
|
||||||
|
<input type="number" min="0" max="15" v-model="rings" />
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<div class="satellite-list">
|
||||||
|
Satellites:
|
||||||
|
<button class="satellite" v-for="satellite in satellites">
|
||||||
|
{{ satellite.name }}
|
||||||
|
<template v-if="satellite.type">({{ satellite.type }})</template>
|
||||||
|
</button>
|
||||||
|
<button class="add"> </button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
object: Object,
|
||||||
|
})
|
||||||
|
const emit = defineEmits([
|
||||||
|
'update:object',
|
||||||
|
])
|
||||||
|
|
||||||
|
const tipsShown = ref(true)
|
||||||
|
|
||||||
|
const distance = ref(props.object.distance)
|
||||||
|
const name = ref(props.object.name)
|
||||||
|
const type = ref(props.object.type)
|
||||||
|
const radius = ref(props.object.radius)
|
||||||
|
const rings = ref(props.object.rings)
|
||||||
|
const satellites = ref(props.object.satellites)
|
||||||
|
|
||||||
|
const satellitesList = computed(() => {
|
||||||
|
if (!satellites.value || !satellites.value.length) return 'none'
|
||||||
|
return satellites.value.reduce((acc, satellite) => {
|
||||||
|
let s = satellite.name
|
||||||
|
if (satellite.type) s += ` (${satellite.type})`
|
||||||
|
acc.push(s)
|
||||||
|
return acc
|
||||||
|
}, []).join(', ')
|
||||||
|
})
|
||||||
|
|
||||||
|
function update (target, value) {
|
||||||
|
if (target === 'radius') value = parseInt(value)
|
||||||
|
emit(`update:${target}`, value)
|
||||||
|
}
|
||||||
|
</script>
|
5
src/components/SettingsButton.vue
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<template>
|
||||||
|
<button title="settings" @click="$emit('click', $event)">
|
||||||
|
<img src="../assets/change.png" alt="settings icon" />
|
||||||
|
</button>
|
||||||
|
</template>
|
40
src/components/SystemDiagram.vue
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1000 300">
|
||||||
|
<line id="axis" x1="0" y1="150" x2="1000" y2="150" />
|
||||||
|
<circle id="star" :r="star.radius" :cx="starCX" cy="150" />
|
||||||
|
|
||||||
|
<g class="object" :id="o.name" v-for="o in objects">
|
||||||
|
<g class="rings" v-for="i in o.rings">
|
||||||
|
<circle :r="o.radius - 5 + 2*i" :cx="o.distance" cy="150" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<text :class="{ tilted: o.radius < 10 }" :x="o.distance" :y="140 - o.radius">{{ o.name }}</text>
|
||||||
|
<circle v-if="o.type === 'planet'" :r="o.radius" :cx="o.distance" cy="150" />
|
||||||
|
<line v-if="o.satellites.length" :x1="o.distance" y1="150" :x2="o.distance" :y2="150 + o.radius + 10*o.satellites.length" />
|
||||||
|
|
||||||
|
<g class="satellite" v-for="m,i in o.satellites">
|
||||||
|
<rect v-if="m.type === 'station'" class="station" :x="o.distance - 2" :y="158 + o.radius + 10*i" width="4" height="4" />
|
||||||
|
<circle v-else :r="m.radius" :cx="o.distance" :cy="160 + o.radius + 10*i" />
|
||||||
|
<text :x="o.distance + 5" :y="162 + o.radius + 10*i">{{ m.name }}</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<text id="designation" x="980" y="30">{{ star.designation }}</text>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import steepCurve from '../steep-curve'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
star: Object,
|
||||||
|
objects: Array,
|
||||||
|
})
|
||||||
|
|
||||||
|
const starCX = computed(() => {
|
||||||
|
const r = props.star.radius
|
||||||
|
return -1 * r * steepCurve(r, 50, 0.955)
|
||||||
|
})
|
||||||
|
</script>
|
34
src/components/SystemSettings.vue
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<template>
|
||||||
|
<header>
|
||||||
|
<h1>Star System Parameters</h1>
|
||||||
|
<menu id="system-settings">
|
||||||
|
<label>
|
||||||
|
Name
|
||||||
|
<input type="text" :value="designation" @input="update('designation', $event.target.value)" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Star Size
|
||||||
|
<input type="range" min="50" max="1500" :value="radius" @input="update('radius', $event.target.value)" />
|
||||||
|
({{ radius }})
|
||||||
|
</label>
|
||||||
|
</menu>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
designation: String,
|
||||||
|
radius: Number,
|
||||||
|
})
|
||||||
|
const emit = defineEmits([
|
||||||
|
'update:designation',
|
||||||
|
'update:radius',
|
||||||
|
])
|
||||||
|
|
||||||
|
function update (target, value) {
|
||||||
|
if (target === 'radius') value = parseInt(value)
|
||||||
|
emit(`update:${target}`, value)
|
||||||
|
}
|
||||||
|
</script>
|
16
src/components/Tips.vue
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<template>
|
||||||
|
<ul class="tip">
|
||||||
|
<header>
|
||||||
|
Tips:
|
||||||
|
<button @click="tipsShown = !tipsShown">{{ tipsShown ? 'close' : 'expand' }}</button>
|
||||||
|
</header>
|
||||||
|
<template v-if="tipsShown">
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
const tipsShown = ref(true)
|
||||||
|
</script>
|
38
src/example-data.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
export default [
|
||||||
|
{ type: 'planet', name: 'Mercury', radius: 1, distance: 100, satellites: [], rings: 0 },
|
||||||
|
{ type: 'planet', name: 'Venus', radius: 4, distance: 120, satellites: [], rings: 0 },
|
||||||
|
{ type: 'planet', name: 'Terra', radius: 4, distance: 140, satellites: [
|
||||||
|
{ name: 'ISS', type: 'station' },
|
||||||
|
{ name: 'Luna', radius: 2 },
|
||||||
|
], rings: 0 },
|
||||||
|
{ type: 'planet', name: 'Mars', radius: 2, distance: 160, satellites: [
|
||||||
|
{ name: 'MTO', type: 'station' },
|
||||||
|
{ name: 'Phobos', radius: 1 },
|
||||||
|
{ name: 'Daimos', radius: 1 },
|
||||||
|
], rings: 0 },
|
||||||
|
{ type: 'planet', name: 'Jupiter', radius: 40, distance: 260, satellites: [
|
||||||
|
{ name: 'Io', radius: 2 },
|
||||||
|
{ name: 'Europa', radius: 2 },
|
||||||
|
{ name: 'Ganymede', radius: 4 },
|
||||||
|
{ name: 'Callisto', radius: 3 },
|
||||||
|
], rings: 1 },
|
||||||
|
{ type: 'planet', name: 'Saturn', radius: 36, distance: 410, satellites: [
|
||||||
|
{ name: 'Mimas', radius: 1 },
|
||||||
|
{ name: 'Enceladus', radius: 1 },
|
||||||
|
{ name: 'Tethys', radius: 1 },
|
||||||
|
{ name: 'Dione', radius: 1 },
|
||||||
|
{ name: 'Rhea', radius: 1 },
|
||||||
|
{ name: 'Titan', radius: 3 },
|
||||||
|
{ name: 'Iapetus', radius: 1 },
|
||||||
|
], rings: 5 },
|
||||||
|
{ type: 'planet', name: 'Uranus', radius: 16, distance: 680, satellites: [
|
||||||
|
{ name: 'Miranda', radius: 1 },
|
||||||
|
{ name: 'Ariel', radius: 1 },
|
||||||
|
{ name: 'Umbriel', radius: 1 },
|
||||||
|
{ name: 'Titania', radius: 1 },
|
||||||
|
{ name: 'Oberon', radius: 1 },
|
||||||
|
], rings: 2 },
|
||||||
|
{ type: 'planet', name: 'Neptune', radius: 15, distance: 950, satellites: [
|
||||||
|
{ name: 'Triton', radius: 1 },
|
||||||
|
], rings: 0 },
|
||||||
|
]
|