satellite options with dragndrop
This commit is contained in:
parent
f44b65eb8c
commit
0e3653f298
12 changed files with 279 additions and 61 deletions
83
src/app.css
83
src/app.css
|
@ -57,7 +57,7 @@ input[type="text"]:focus {
|
|||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
button.settings, button.delete, button.add, button.less, button.close {
|
||||
button.settings, button.delete, button.add, button.less, button.close, button.move {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid var(--bg-app);
|
||||
|
@ -72,6 +72,7 @@ button.delete { background-image: url(./assets/delete.png); }
|
|||
button.add { background-image: url(./assets/add.png); }
|
||||
button.less { background-image: url(./assets/less.png); }
|
||||
button.close { background-image: url(./assets/close.png); }
|
||||
button.move { background-image: url(./assets/move.png); }
|
||||
|
||||
.tip {
|
||||
width: calc(100% - 4em);
|
||||
|
@ -179,7 +180,8 @@ h1 {
|
|||
}
|
||||
#system-settings button { height: 2em; }
|
||||
#system-settings input { width: 200px; }
|
||||
#system-settings input[type="text"] {
|
||||
#system-settings input[type="text"],
|
||||
.satellite-name input[type="text"] {
|
||||
margin-left: 1em;
|
||||
padding: .5em 1em .4em;
|
||||
}
|
||||
|
@ -224,6 +226,83 @@ h1 {
|
|||
#object-settings section.satellite-list {
|
||||
margin: 2em 0;
|
||||
}
|
||||
|
||||
.satellite-list-entry {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: .5em 1em 0;
|
||||
}
|
||||
|
||||
.satellite-list-entry.dragging {
|
||||
opacity: .5;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
.satellite-list-entry.dragover {
|
||||
padding-top: 3em;
|
||||
}
|
||||
.satellite-list-entry.dragunder {
|
||||
padding-bottom: 3em;
|
||||
}
|
||||
.satellite-list-entry.dragover::before,
|
||||
.satellite-list-entry.dragunder::after {
|
||||
content: " insert here ";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 2em;
|
||||
margin-top: 6em;
|
||||
border: 1px solid white;
|
||||
text-align: center;
|
||||
line-height: 2em;
|
||||
}
|
||||
.satellite-list-entry.dragover::before {
|
||||
margin-top: -6em;
|
||||
}
|
||||
|
||||
.satellite-list-entry > .options {
|
||||
flex: 0 0 auto;
|
||||
width: 75%;
|
||||
text-align: right;
|
||||
}
|
||||
.satellite-list-entry > .options, .satellite-list-entry > .options > span {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
.satellite-list-entry .satellite-radius { opacity: .25; }
|
||||
.satellite-list-entry .satellite-radius.moon { opacity: 1; }
|
||||
|
||||
.satellite-list-entry > .options > .actions {
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
.satellite-name > input[type="text"] {
|
||||
width: 9em;
|
||||
}
|
||||
.satellite-radius > input[type="range"] {
|
||||
width: 50% !important;
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1.6em;
|
||||
height: 100%;
|
||||
font-size: 1.5em;
|
||||
line-height: 1.7em;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
background: var(--fg-app);
|
||||
color: var(--bg-app);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cta {
|
||||
vertical-align: middle;
|
||||
margin: .2em;
|
||||
|
|
BIN
src/assets/move.png
Normal file
BIN
src/assets/move.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/move_inverted.png
Normal file
BIN
src/assets/move_inverted.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 974 B |
|
@ -1,5 +0,0 @@
|
|||
<template>
|
||||
<button title="delete object" @click="$emit('click', $event)">
|
||||
<img src="../assets/delete.png" alt="delete icon" />
|
||||
</button>
|
||||
</template>
|
|
@ -30,14 +30,9 @@
|
|||
</div>
|
||||
<button class="close" title="close" @click="$emit('close')"> </button>
|
||||
</section>
|
||||
<section class="satellite-list">
|
||||
Satellites:
|
||||
<button class="cta" v-for="satellite in satellites">
|
||||
{{ satellite.name }}
|
||||
<template v-if="satellite.type">({{ satellite.type }})</template>
|
||||
</button>
|
||||
<button class="add"> </button>
|
||||
</section>
|
||||
|
||||
<SatelliteSettings :satellites="satellites" @update:satellites="update('satellites', $event)" />
|
||||
|
||||
<section class="additional-options">
|
||||
Other options:
|
||||
<button class="cta danger" @click="$emit('delete')">REMOVE OBJECT</button>
|
||||
|
@ -47,6 +42,8 @@
|
|||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import SatelliteSettings from './SatelliteSettings.vue'
|
||||
|
||||
import {
|
||||
MIN_SIZE_STAR,
|
||||
MAX_SIZE_STAR,
|
||||
|
@ -78,16 +75,6 @@ const emit = defineEmits([
|
|||
'close',
|
||||
])
|
||||
|
||||
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(', ')
|
||||
})
|
||||
|
||||
const numberTargets = ['distance', 'radius', 'rings']
|
||||
|
||||
function update (target, value) {
|
||||
|
|
138
src/components/SatelliteSettings.vue
Normal file
138
src/components/SatelliteSettings.vue
Normal file
|
@ -0,0 +1,138 @@
|
|||
<template>
|
||||
<section class="satellite-list">
|
||||
Satellites:
|
||||
<transition-group name="draggable-items-list">
|
||||
<div v-for="satellite,index in satellites"
|
||||
class="satellite-list-entry"
|
||||
:key="satellite.name"
|
||||
:class="{
|
||||
dragging: dragIndex === index,
|
||||
dragunder: dropIndex === index && dragIndex < index,
|
||||
dragover: dropIndex === index && dragIndex > index
|
||||
}"
|
||||
:style="dragIndex === index ? `transform: translateY(${dragDelta}px)` : ''"
|
||||
@pointerenter="onDragEnter(index)"
|
||||
@pointerleave="onDragLeave(index)"
|
||||
>
|
||||
<header>
|
||||
<span class="satellite-name">
|
||||
<input type="text"
|
||||
:value="satellite.name"
|
||||
@keydown.enter="update(index, 'name', $event.target)"
|
||||
@blur="update(index, 'name', $event.target)"
|
||||
/>
|
||||
</span>
|
||||
</header>
|
||||
<div class="options">
|
||||
<span class="satellite-type">
|
||||
<select
|
||||
:value="satellite.type"
|
||||
@change="update(index, 'type', $event.target)"
|
||||
>
|
||||
<option v-for="type in satelliteTypes">{{ type }}</option>
|
||||
</select>
|
||||
</span>
|
||||
<span class="satellite-radius" :class="satellite.type">
|
||||
Radius r:
|
||||
<input type="range" :min="MIN_SIZE_MOON" :max="MAX_SIZE_MOON"
|
||||
:disabled="satellite.type !== 'moon'"
|
||||
:value="satellite.radius"
|
||||
@input="update(index, 'radius', $event.target, true)"
|
||||
/>
|
||||
</span>
|
||||
<span class="actions">
|
||||
<button class="move" title="drag to reorder"
|
||||
@pointerdown="startDragging(index, $event)"
|
||||
@pointerup="stopDragging"
|
||||
> </button>
|
||||
<button class="delete" title="delete satellite"
|
||||
@click="deleteSatellite(index)"
|
||||
> </button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
<button class="add"> </button>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, ref } from 'vue'
|
||||
import {
|
||||
MIN_SIZE_MOON,
|
||||
MAX_SIZE_MOON,
|
||||
} from '../constants'
|
||||
|
||||
const props = defineProps({
|
||||
satellites: Array,
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:satellites'
|
||||
])
|
||||
|
||||
const satelliteTypes = ['moon', 'station']
|
||||
|
||||
function isResizable (satellite) {
|
||||
return satellite.type === 'moon'
|
||||
}
|
||||
|
||||
async function update (index, attr, target, isNumber) {
|
||||
const satellites = [...props.satellites]
|
||||
let value = target.value
|
||||
if (isNumber) value = parseInt(value)
|
||||
|
||||
satellites[index][attr] = value
|
||||
emit('update:satellites', satellites)
|
||||
}
|
||||
|
||||
function deleteSatellite (index) {
|
||||
const confirmed = confirm(`Attention! This cannot be undone! Proceed anyway?`)
|
||||
if (!confirmed) return
|
||||
|
||||
const satellites = [...props.satellites]
|
||||
satellites.splice(index, 1)
|
||||
|
||||
emit('update:satellites', satellites)
|
||||
}
|
||||
|
||||
const dragIndex = ref(null)
|
||||
const dropIndex = ref(null)
|
||||
|
||||
let dragStart = 0
|
||||
const dragDelta = ref(0)
|
||||
|
||||
function updateDelta (event) {
|
||||
dragDelta.value = event.clientY - dragStart
|
||||
}
|
||||
function onDragEnter (index) {
|
||||
if (dragIndex.value !== null && dragIndex.value !== index) dropIndex.value = index
|
||||
}
|
||||
function onDragLeave (index) {
|
||||
if (index === dropIndex.value) dropIndex.value = null
|
||||
}
|
||||
function startDragging (index, event) {
|
||||
dragIndex.value = index
|
||||
dragStart = event.clientY
|
||||
window.addEventListener('pointermove', updateDelta)
|
||||
window.addEventListener('pointerup', stopDragging)
|
||||
}
|
||||
function stopDragging () {
|
||||
window.removeEventListener('pointermove', updateDelta)
|
||||
window.removeEventListener('pointerup', stopDragging)
|
||||
|
||||
if (dropIndex !== null) {
|
||||
const oldIdx = dragIndex.value
|
||||
const newIdx = dropIndex.value
|
||||
// move element at oldIdx to newIdx
|
||||
const satellites = [...props.satellites]
|
||||
satellites.splice(newIdx, 0, satellites.splice(oldIdx, 1)[0])
|
||||
|
||||
emit('update:satellites', satellites)
|
||||
}
|
||||
|
||||
dragIndex.value = null
|
||||
dropIndex.value = null
|
||||
}
|
||||
|
||||
</script>
|
|
@ -1,5 +0,0 @@
|
|||
<template>
|
||||
<button title="settings" @click="$emit('click', $event)">
|
||||
<img src="../assets/change.png" alt="settings icon" />
|
||||
</button>
|
||||
</template>
|
|
@ -10,6 +10,7 @@
|
|||
:style="{ transform: `translateX(${o === draggedObject ? draggingDelta : 0}px)` }"
|
||||
@pointerdown.left="startDragging($event, o)"
|
||||
@wheel="resizeObject"
|
||||
@dragenter.prevent.stop="onDragEnter"
|
||||
>
|
||||
<g class="rings" v-for="i in o.rings">
|
||||
<circle :r="o.radius - 5 + 2*i" :cx="o.distance" cy="150" />
|
||||
|
@ -33,7 +34,7 @@
|
|||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import steepCurve from '../steep-curve'
|
||||
import { steepCurve } from '../utils'
|
||||
import {
|
||||
MIN_SIZE_PLANET,
|
||||
MAX_SIZE_PLANET,
|
||||
|
@ -118,4 +119,8 @@ function resizeObject (event) {
|
|||
|
||||
emit('update', { radius })
|
||||
}
|
||||
|
||||
function onDragEnter (event) {
|
||||
console.log('SystemDiagram onDragEnter', event)
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -4,6 +4,9 @@ export const MAX_SIZE_STAR = 1500
|
|||
export const MIN_SIZE_PLANET = 1
|
||||
export const MAX_SIZE_PLANET = 125
|
||||
|
||||
export const MIN_SIZE_MOON = 1
|
||||
export const MAX_SIZE_MOON = 5
|
||||
|
||||
export const MIN_AMOUNT_RINGS = 0
|
||||
export const MAX_AMOUNT_RINGS = 15
|
||||
|
||||
|
|
|
@ -2,37 +2,37 @@ 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 },
|
||||
{ name: 'ISS', radius: 1, type: 'station' },
|
||||
{ name: 'Luna', radius: 2, type: 'moon' },
|
||||
], rings: 0 },
|
||||
{ type: 'planet', name: 'Mars', radius: 2, distance: 160, satellites: [
|
||||
{ name: 'MTO', type: 'station' },
|
||||
{ name: 'Phobos', radius: 1 },
|
||||
{ name: 'Daimos', radius: 1 },
|
||||
{ name: 'MTO', radius: 1, type: 'station' },
|
||||
{ name: 'Phobos', radius: 1, type: 'moon' },
|
||||
{ name: 'Daimos', radius: 1, type: 'moon' },
|
||||
], 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 },
|
||||
{ name: 'Io', radius: 2, type: 'moon' },
|
||||
{ name: 'Europa', radius: 2, type: 'moon' },
|
||||
{ name: 'Ganymede', radius: 4, type: 'moon' },
|
||||
{ name: 'Callisto', radius: 3, type: 'moon' },
|
||||
], 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 },
|
||||
{ name: 'Mimas', radius: 1, type: 'moon' },
|
||||
{ name: 'Enceladus', radius: 1, type: 'moon' },
|
||||
{ name: 'Tethys', radius: 1, type: 'moon' },
|
||||
{ name: 'Dione', radius: 1, type: 'moon' },
|
||||
{ name: 'Rhea', radius: 1, type: 'moon' },
|
||||
{ name: 'Titan', radius: 3, type: 'moon' },
|
||||
{ name: 'Iapetus', radius: 1, type: 'moon' },
|
||||
], 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 },
|
||||
{ name: 'Miranda', radius: 1, type: 'moon' },
|
||||
{ name: 'Ariel', radius: 1, type: 'moon' },
|
||||
{ name: 'Umbriel', radius: 1, type: 'moon' },
|
||||
{ name: 'Titania', radius: 1, type: 'moon' },
|
||||
{ name: 'Oberon', radius: 1, type: 'moon' },
|
||||
], rings: 2 },
|
||||
{ type: 'planet', name: 'Neptune', radius: 15, distance: 950, satellites: [
|
||||
{ name: 'Triton', radius: 1 },
|
||||
{ name: 'Triton', radius: 1, type: 'moon' },
|
||||
], rings: 0 },
|
||||
]
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
// Thank you Ingo for your tremendous help with this one.
|
||||
|
||||
// This function returns a steep curve from [minX,0] to [infinity,maxY]
|
||||
// inc is tuned for x-values between minX and minX+100 describing a gentle curve
|
||||
export default function steepCurve (x, minX, maxY, inc=0.01) {
|
||||
// f(x) = maxY * (1 - e^(-(inc*x)+minX*inc))
|
||||
return maxY * (1 - Math.E ** (-(inc*x) + minX*inc))
|
||||
}
|
24
src/utils.js
Normal file
24
src/utils.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
/* This function returns a steep curve from [minX,0] to [infinity,maxY]
|
||||
*
|
||||
* inc is tuned for x-values between minX and minX+100 describing a gentle curve
|
||||
* towards maxY that flattens very quickly afterwards.
|
||||
* Thank you Ingo for your tremendous help with this one.
|
||||
*/
|
||||
export function steepCurve (x, minX, maxY, inc=0.01) {
|
||||
// f(x) = maxY * (1 - e^(-(inc*x)+minX*inc))
|
||||
return maxY * (1 - Math.E ** (-(inc*x) + minX*inc))
|
||||
}
|
||||
|
||||
/* throttle function calls */
|
||||
function throttle (func, duration) {
|
||||
let waiting = false
|
||||
return (...args) => {
|
||||
if (!waiting) {
|
||||
func.apply(this, args)
|
||||
waiting = true
|
||||
setTimeout(() => {
|
||||
waiting = false
|
||||
}, duration)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue