iteration on tabletop UI

This commit is contained in:
Stefan Zermatten
2023-09-04 14:02:45 +02:00
parent 4ea28acdee
commit 1847525e62
6 changed files with 213 additions and 49 deletions

View File

@@ -19,34 +19,18 @@
<v-flex
style="height: 24px; flex-basis: 300px; flex-grow: 100;"
>
<div
column
align-center
<health-bar-progress
:model="model"
style="cursor: pointer;"
class="bar"
@click="edit"
>
<div
style="height: 24px; width: 100%; position: relative; transition: background-color 0.5s ease;"
:style="{
backgroundColor: barBackgroundColor
class="value"
:class="{
'white--text': isTextLight,
'black--text': !isTextLight,
}"
>
<div
class="filler"
style="height: 100%; transform-origin: left; transition: all 0.5s ease;"
:style="{
backgroundColor: barColor,
transform: `scaleX(${fillFraction})`,
}"
/>
<div
class="value"
:class="{
'white--text': isTextLight,
'black--text': !isTextLight,
}"
style="font-size: 15px;
style="font-size: 15px;
line-height: 24px;
font-weight: 600;
position: absolute;
@@ -55,11 +39,10 @@
right: 0;
bottom: 0;
text-align: center;"
>
{{ model.value }} / {{ model.total }}
</div>
>
{{ model.value }} / {{ model.total }}
</div>
</div>
</health-bar-progress>
<v-menu
v-model="editing"
absolute
@@ -85,11 +68,13 @@
<script lang="js">
import IncrementMenu from '/imports/client/ui/components/IncrementMenu.vue';
import isDarkColor from '/imports/client/ui/utility/isDarkColor.js';
import HealthBarProgress from '/imports/client/ui/properties/components/attributes/HealthBarProgress.vue';
import chroma from 'chroma-js';
export default {
components: {
IncrementMenu
IncrementMenu,
HealthBarProgress,
},
inject: {
theme: {

View File

@@ -0,0 +1,69 @@
<template lang="html">
<div
class="bar"
@click="e => $emit('click', e)"
>
<div
style="width: 100%; position: relative; transition: background-color 0.5s ease;"
:style="{
backgroundColor: barBackgroundColor,
height: `${height}px`,
}"
>
<div
class="filler"
style="height: 100%; transform-origin: left; transition: all 0.5s ease;"
:style="{
backgroundColor: barColor,
transform: `scaleX(${fillFraction})`,
}"
/>
<slot />
</div>
</div>
</template>
<script lang="js">
import chroma from 'chroma-js';
export default {
props: {
model: {
type: Object,
required: true,
},
height: {
type: Number,
default: 24,
},
},
computed: {
fillFraction() {
let fraction = this.model.value / this.model.total;
if (fraction < 0) fraction = 0;
if (fraction > 1) fraction = 1;
return fraction;
},
color() {
return this.model.color || this.$vuetify.theme.currentTheme.primary
},
barColor() {
const fraction = this.model.value / this.model.total;
if (!Number.isFinite(fraction)) return this.color;
if (fraction > 0.5) {
return this.color;
} else if (this.model.healthBarColorMid && this.model.healthBarColorLow) {
return chroma.mix(this.model.healthBarColorLow, this.model.healthBarColorMid, fraction * 2).hex();
} else if (this.model.healthBarColorMid) {
return this.model.healthBarColorMid;
}
return this.color;
},
barBackgroundColor() {
return chroma(this.barColor)
.darken(1.5)
.desaturate(1.5)
.hex();
},
},
}
</script>

View File

@@ -80,7 +80,6 @@
bottom:0;
right: 0;
overflow-x: auto;
overflow-y: hidden;
"
>
<v-slide-y-reverse-transition mode="out-in">

View File

@@ -1,6 +1,6 @@
<template lang="html">
<v-card
style="height: 100px; width: 70px;"
:style="`height: ${height}px; width: ${width}px; overflow: hidden;`"
class="tabletop-creature-card"
:class="{ active }"
:hover="hasClickListener"
@@ -9,20 +9,28 @@
@mouseleave="hover = false"
v-on="hasClickListener ? {click: () => $emit('click')} : {}"
>
<v-progress-linear
v-if="variables.hitPoints"
:value="variables.hitPoints.value * 100 / variables.hitPoints.total"
/>
<v-img
:src="model.picture"
aspect-ratio="1"
:src="model.picture || '/images/ui/missing-portrait.png'"
:lazy-src="loadingImg"
:height="height"
:width="width"
class="align-end"
:class="{placeholder: !model.picture}"
gradient="to bottom, rgba(0,0,0,.1), rgba(0,0,0,.5)"
position="top center"
/>
<div
class="small-title"
>
{{ model.name }}
</div>
<v-card-title
class="small-title"
v-text="model.name"
/>
<health-bar-progress
v-for="bar in healthBars"
:key="bar._id"
:model="bar"
:height="4"
style="opacity: 0.7; margin-top: 2px"
/>
</v-img>
<card-highlight :active="hover" />
<div class="d-flex justify-center">
<v-scale-transition>
@@ -42,24 +50,35 @@
</template>
<script lang="js">
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CardHighlight from '/imports/client/ui/components/CardHighlight.vue';
import HealthBarProgress from '/imports/client/ui/properties/components/attributes/HealthBarProgress.vue';
export default {
components: {
CardHighlight,
HealthBarProgress,
},
props: {
model: {
type: Object,
required: true,
},
height: {
type: Number,
default: 100,
},
width: {
type: Number,
default: 75,
},
active: Boolean,
targeted: Boolean,
showTargetBtn: Boolean,
},
data(){return {
hover: false,
loadingImg: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAXdJREFUWEftlq1vAkEQxedSB7pYDklBNsgmyCa4Cv5GBI6k8tJKUglULrWcBlvySA4B+/HeAcV07O3t/ObtvNnNHsyc3TEyAGRm+T0Yfs3WFwE0mk3bbbe12WsDtDsda+e5PbZaR4C1c/a9XEowtQC6vZ499ftniaCECiEDoPLnwSBa5WdRWLnZUErIAK+jkeHcY4HkgGBCAmCqr5KyKkgAobP3VXoTAJw9VGBitVhQjpAUeBkOD7Zj4sc5+5rPk0slAOUIkBwQqZAAUD1UYOImALAfbMjE+2xGjWhJASRmGpFtQOwnAzAqTCcTRqTDGhkAP8UGklK9BIDKq9svZUVcShjHZVkmnUApoNjvVHvAfBRFsCGTAJckr2AAAVf4Igqg+D7VdaHJGAVQRm8KIKRCFOBtPE7tK3333ZBBAOXuZyl8Fv1TAF8fBAGYkctWXq3zPdWCANdswJgdgwDM41NVwOeEf4BoE6be/3WO4PSdeARQN7vm+j0BlQ9wWvLB6AAAAABJRU5ErkJggg==',
}
},
computed: {
@@ -67,10 +86,33 @@ export default {
return this.$listeners && !!this.$listeners.click;
},
},
// @ts-ignore
meteor: {
variables() {
return CreatureVariables.findOne({ _creatureId: this.model._id }) || {};
healthBars() {
const folderIds = CreatureProperties.find({
'ancestors.id': this.model._id,
type: 'folder',
groupStats: true,
hideStatsGroup: true,
removed: { $ne: true },
inactive: { $ne: true },
}, { fields: { _id: 1 } }).map(folder => folder._id);
// Get the properties that need to be shown as a health bar
return CreatureProperties.find({
'ancestors.id': this.model._id,
'parent.id': {
$nin: folderIds,
},
type: 'attribute',
attributeType: 'healthBar',
healthBarNoDamage: { $ne: true },
inactive: { $ne: true } ,
removed: { $ne: true },
}, {
sort: {
order: 1,
},
});
}
}
}
@@ -79,10 +121,21 @@ export default {
<style lang="css" scoped>
.small-title {
font-size: 12px;
padding: 4px;
padding: 4px 4px 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: block;
line-height: normal;
}
.active {
transform: scale(1.2);
margin-left: 12px !important;
margin-right: 12px !important;
transform-origin: top center;
}
.tabletop-creature-card {
transition: all .15s ease;
}
</style>

View File

@@ -1,7 +1,7 @@
<template lang="html">
<div
v-if="creatureId"
class="selected-creature-bar d-flex pa-3"
class="selected-creature-bar d-flex pa-3 align-end"
style="gap: 8px;"
>
<!--
@@ -35,7 +35,7 @@
style="width: 300px;"
:style="{
width: '300px',
opacity: selectedIcon ? 1 : 0.5,
opacity: selectedIcon ? 1 : 0.7,
transition: 'opacity 0.2s ease',
}"
:model="selectedProp"
@@ -52,6 +52,54 @@
</v-card-title>
</v-card>
</v-menu>
<v-card
v-if="iconGroups.buffs"
class="buffs-card"
>
<div
v-for="(row, rowIndex) in iconGroups.buffs.rows"
:key="rowIndex"
class="d-flex"
>
<template
v-for="(icon, iconIndex) in row"
>
<creature-bar-icon
:key="icon.propId || iconIndex"
:prop-id="icon.propId"
:icon="icon.icon"
:selected="selectedIcon === icon"
:data-id="icon.propId || icon.standardId"
@click="e => selectIcon(e, icon)"
@mouseenter="e => hoverIcon(e, icon)"
@mouseleave="unHoverIcon(icon)"
/>
</template>
</div>
</v-card>
<v-card
class="creature-portrait"
:width="90"
:height="120"
>
<v-img
v-if="creature.picture"
:height="120"
:src="creature.picture"
position="top center"
/>
<div
v-else
class="fill-height d-flex align-center justify-center"
style="opacity: 0.2;"
>
<v-icon
size="90"
>
mdi-account
</v-icon>
</div>
</v-card>
<v-card
v-for="group in iconGroups"
:key="group.name"
@@ -259,6 +307,7 @@ export default {
{ type: 'folder', groupStats: true },
{ type: 'attribute' },
{ type: 'toggle' },
{ type: 'buff' }
],
},
{
@@ -314,6 +363,7 @@ export default {
if (prop._placedInGroup) return;
let groupName;
switch (prop.type) {
case 'buff': groupName = 'Buffs'; break;
case 'action': groupName = 'Actions'; break;
case 'resource': groupName = 'Resources'; break;
case 'folder': groupName = 'Folders'; break;
@@ -321,7 +371,9 @@ export default {
if (!groupName) return;
if (!groupsByName[groupName]) {
groupsByName[groupName] = { name: groupName, iconList: [] };
defaultGroups.push(groupsByName[groupName]);
if (groupName !== 'Buffs') { // don't add buffs to the default groups, it is handled differently
defaultGroups.push(groupsByName[groupName]);
}
}
groupsByName[groupName].iconList.push({ propId: prop._id });
});
@@ -343,10 +395,16 @@ export default {
iconGroups.push(...defaultGroups);
// Store a specific reference to buffs outside of the list order
iconGroups.buffs = groupsByName['Buffs'];
// Divide the icons into rows
iconGroups.forEach(group => {
group.rows = splitToNChunks(group.iconList, this.rows);
});
if (iconGroups.buffs) {
iconGroups.buffs.rows = splitToNChunks(iconGroups.buffs.iconList, this.rows);
}
return iconGroups;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB