satellite options with dragndrop

This commit is contained in:
koehr 2022-01-05 22:05:11 +01:00
parent f44b65eb8c
commit 0e3653f298
12 changed files with 279 additions and 61 deletions

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

View file

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

View file

@ -30,14 +30,9 @@
</div>
<button class="close" title="close" @click="$emit('close')">&nbsp;</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">&nbsp;</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) {

View 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"
>&nbsp;</button>
<button class="delete" title="delete satellite"
@click="deleteSatellite(index)"
>&nbsp;</button>
</span>
</div>
</div>
</transition-group>
<button class="add">&nbsp;</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>

View file

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

View file

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

View file

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

View file

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

View file

@ -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
View 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)
}
}
}