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).
|
||||
|
||||
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>
|
||||
<header>
|
||||
<div class="headline">
|
||||
<h1>Starsy</h1>
|
||||
<p>Starsystem<br/>Generator</p>
|
||||
</div>
|
||||
<div class="options">
|
||||
<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>
|
||||
<Headline
|
||||
v-bind="{ labelFonts, themes }"
|
||||
@select:font="setFont($event)"
|
||||
@select:theme="setTheme($event)"
|
||||
/>
|
||||
<SystemDiagram v-bind="{ star, objects }" />
|
||||
|
||||
<section id="settings">
|
||||
<header>
|
||||
<h1>Star System Parameters</h1>
|
||||
<menu id="system-settings">
|
||||
<label>
|
||||
Name
|
||||
<input type="text" v-model="star.designation" />
|
||||
</label>
|
||||
<label>
|
||||
Star Size
|
||||
<input type="range" min="50" max="1500" v-model="star.radius" />
|
||||
({{ 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>
|
||||
<ObjectSettings v-model:object="selectedObject" v-if="selectedObject" />
|
||||
<Tips>
|
||||
<li>Edit planets by clicking directly inside the graphic or in the table below.</li>
|
||||
<li>Drag planets around to change their distance.</li>
|
||||
<li>Use the scrollwheel to change their size.</li>
|
||||
<li>To change satellites, click their respective buttons in the planet dialog.</li>
|
||||
<li>You can also drag satellite buttons around to reorder them.</li>
|
||||
</Tips>
|
||||
<SystemSettings v-model:designation="star.designation" v-model:radius="star.radius" />
|
||||
<ObjectList v-bind="{ objects, editObject, deleteObject }" />
|
||||
</section>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import steepCurve from './steep-curve'
|
||||
import { ref, reactive } from 'vue'
|
||||
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({
|
||||
designation: 'Sol',
|
||||
radius: 400,
|
||||
})
|
||||
|
||||
const starCX = computed(() => -1 * star.radius * steepCurve(star.radius, 50, 0.955))
|
||||
|
||||
const objects = ref([
|
||||
{ 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 objects = ref(exampleData)
|
||||
const labelFonts = ['xolonium', 'douar', 'lack']
|
||||
const themes = ['default', 'retro', 'inverse', 'paper']
|
||||
|
||||
const selectedObject = ref(null)
|
||||
|
||||
function toggleObject (obj) {
|
||||
selectedObject.value = selectedObject.value === obj ? null : obj
|
||||
function editObject (obj) {
|
||||
selectedObject.value = obj
|
||||
}
|
||||
function checkName (obj) {
|
||||
if (!obj.name.trim().length) {
|
||||
const index = objects.value.indexOf(obj)
|
||||
obj.name = `${star.designation}-${index}`
|
||||
|
||||
function deleteObject (obj) {
|
||||
console.log('delete object not yet implemented')
|
||||
}
|
||||
|
||||
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']
|
||||
const selectedFont = ref('xolonium')
|
||||
|
||||
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()
|
||||
setTheme(themes[0])
|
||||
setFont(labelFonts[0])
|
||||
</script>
|
||||
|
|
102
src/app.css
|
@ -43,7 +43,7 @@ html,body {
|
|||
color: var(--fg-app);
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
input[type="text"], input[type="number"] {
|
||||
background: transparent;
|
||||
border: 1px solid var(--fg-app);
|
||||
color: var(--fg-app);
|
||||
|
@ -53,6 +53,34 @@ input[type="text"]:focus {
|
|||
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 {
|
||||
--bg-app: #4B4839;
|
||||
--fg-app: #E4DCB5;
|
||||
|
@ -84,9 +112,9 @@ body.theme-paper {
|
|||
--fg-settings: #000;
|
||||
}
|
||||
|
||||
.title-douar { font-family: 'douar'; }
|
||||
.title-lack { font-family: 'lack'; }
|
||||
.title-xolonium { font-family: 'xolonium'; }
|
||||
body.title-douar { --title-font: 'douar'; }
|
||||
body.title-lack { --title-font: 'lack'; }
|
||||
body.title-xolonium { --title-font: 'xolonium'; }
|
||||
|
||||
.menu-item {
|
||||
padding: 1em;
|
||||
|
@ -121,7 +149,7 @@ svg { background: var(--bg-graphic); }
|
|||
line { stroke: var(--fg-graphic); }
|
||||
text.tilted { transform: rotate(-45deg) translate(0, 100%); transform-origin: left top; transform-box: fill-box; }
|
||||
#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); }
|
||||
.object { fill: var(--fg-graphic); text-anchor: middle; font-size: .6em; cursor: pointer; }
|
||||
.object > line { stroke-width: .5; }
|
||||
|
@ -149,20 +177,22 @@ h1 {
|
|||
}
|
||||
|
||||
#settings {
|
||||
max-width: 700px;
|
||||
margin: auto;
|
||||
padding: 1em 2em;
|
||||
}
|
||||
#settings > header, .object-settings > header {
|
||||
#settings > header {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
#settings > header > h1 {
|
||||
min-width: 330px;
|
||||
}
|
||||
|
||||
#system-settings, #object-list {
|
||||
#system-settings {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: calc(100vw - 4em);
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
@ -174,20 +204,38 @@ h1 {
|
|||
margin: 0 1em;
|
||||
}
|
||||
#system-settings button { height: 2em; }
|
||||
#system-settings input { width: 220px; }
|
||||
#system-settings input { width: 200px; }
|
||||
#system-settings input[type="text"] {
|
||||
margin-left: 1em;
|
||||
padding: .5em 1em .4em;
|
||||
}
|
||||
|
||||
.object-settings > header > p {
|
||||
margin-left: 2em;
|
||||
padding-top: .37em;
|
||||
#object-list {
|
||||
display: block;
|
||||
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;
|
||||
align-items: center;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.object-settings > header > p {
|
||||
margin-left: 2em;
|
||||
}
|
||||
.object-settings > header input[type="text"] {
|
||||
width: 7em;
|
||||
|
@ -195,9 +243,27 @@ h1 {
|
|||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.object-settings > label > span {
|
||||
width: 2em;
|
||||
.object-settings input[type="range"] { width: 100%; }
|
||||
.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 },
|
||||
]
|