componentize all the things (also: icons)

This commit is contained in:
koehr 2022-01-04 10:42:01 +01:00
parent a0ad14b972
commit 6d016f68fe
20 changed files with 409 additions and 192 deletions

View file

@ -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/).

View file

@ -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>

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 B

BIN
src/assets/add_inverted.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

BIN
src/assets/change.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src/assets/delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
src/assets/less.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 717 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 B

View file

@ -0,0 +1,5 @@
<template>
<button title="delete object" @click="$emit('click', $event)">
<img src="../assets/delete.png" alt="delete icon" />
</button>
</template>

View 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>

View file

@ -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>

View 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)">&nbsp;</button>
<button class="delete" @click="deleteObject(o)">&nbsp;</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>

View 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">&nbsp;</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>

View file

@ -0,0 +1,5 @@
<template>
<button title="settings" @click="$emit('click', $event)">
<img src="../assets/change.png" alt="settings icon" />
</button>
</template>

View 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>

View 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
View 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
View 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 },
]