Added dialog animations, still working on box shadows
This commit is contained in:
@@ -41,6 +41,7 @@
|
||||
import AbilityListTile from '/imports/ui/components/AbilityListTile.Story.vue';
|
||||
import AttributeCard from '/imports/ui/components/AttributeCard.Story.vue';
|
||||
import ColumnLayout from "/imports/ui/components/ColumnLayout.Story.vue";
|
||||
import DialogStack from '/imports/ui/dialogStack/DialogStack.Story.vue';
|
||||
import HealthBar from '/imports/ui/components/HealthBar.Story.vue';
|
||||
import HitDiceListTile from '/imports/ui/components/HitDiceListTile.Story.vue';
|
||||
import SkillListTile from '/imports/ui/components/SkillListTile.Story.vue';
|
||||
@@ -50,6 +51,7 @@
|
||||
AbilityListTile,
|
||||
AttributeCard,
|
||||
ColumnLayout,
|
||||
DialogStack,
|
||||
HealthBar,
|
||||
HitDiceListTile,
|
||||
SkillListTile,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
Test Dialog
|
||||
</div>
|
||||
<div>
|
||||
<v-btn @click="openDialog">Open Dialog</v-btn>
|
||||
<v-btn @click="openDialog(_uid + 'btn')" :id="_uid + 'btn'">Open Dialog</v-btn>
|
||||
</div>
|
||||
</dialog-base>
|
||||
</template>
|
||||
@@ -14,9 +14,10 @@
|
||||
import DialogBase from "/imports/ui/dialogStack/DialogBase.vue";
|
||||
const component = {
|
||||
methods: {
|
||||
openDialog(event){
|
||||
openDialog(elementId){
|
||||
store.commit("pushDialogStack", {
|
||||
component,
|
||||
elementId,
|
||||
});
|
||||
}
|
||||
},
|
||||
22
app/imports/ui/dialogStack/DialogStack.Story.vue
Normal file
22
app/imports/ui/dialogStack/DialogStack.Story.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template lang="html">
|
||||
<v-card-text>
|
||||
<v-layout align-center justify-center>
|
||||
<v-btn @click="openDialog(_uid + 'btn')" :id="_uid + 'btn'"/>
|
||||
</v-layout>
|
||||
</v-card-text>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DialogBaseStory from '/imports/ui/dialogStack/DialogBase.Story.vue';
|
||||
import store from "/imports/ui/vuexStore.js";
|
||||
export default {
|
||||
methods: {
|
||||
openDialog(elementId){
|
||||
store.commit("pushDialogStack", {
|
||||
component: DialogBaseStory,
|
||||
elementId,
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -5,14 +5,23 @@
|
||||
@click="backdropClicked"
|
||||
:class="dialogs.length ? '' : 'hidden' "
|
||||
></div>
|
||||
<transition-group name="dialog-list" class="dialog-sizer" tag="div">
|
||||
<transition-group
|
||||
name="dialog-list"
|
||||
class="dialog-sizer"
|
||||
tag="div"
|
||||
@enter="enter"
|
||||
@leave="leave"
|
||||
>
|
||||
<div class="sibling" key="sibling"/>
|
||||
<div
|
||||
v-for="(dialog, index) in dialogs"
|
||||
:key="dialog._id"
|
||||
class="dialog"
|
||||
:style="getDialogStyle(index)"
|
||||
:data-element-id="dialog.elementId"
|
||||
:data-index="index"
|
||||
:style="getDialogStyle(index)"
|
||||
>
|
||||
<component :is="dialog.component" :data="dialog.data" @pop="popDialogStack($event)"></component>
|
||||
<component :is="dialog.component" :data="dialog.data" @pop="popDialogStack($event)" class="dialog-component"></component>
|
||||
</div>
|
||||
</transition-group>
|
||||
</v-layout>
|
||||
@@ -22,8 +31,11 @@
|
||||
import "/imports/ui/dialogStack/dialogStackWindowEvents.js";
|
||||
import store from "/imports/ui/vuexStore.js";
|
||||
import anime from "animejs";
|
||||
import mockElement from '/imports/ui/dialogStack/mockElement.js';
|
||||
import Vue from "vue";
|
||||
|
||||
const offset = 16;
|
||||
const OFFSET = 16;
|
||||
const MOCK_DURATION = 8000; // Keep in sync with css transition of .dialog
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
@@ -42,10 +54,57 @@
|
||||
const length = store.state.dialogStack.dialogs.length;
|
||||
if (index >= length) return;
|
||||
const num = length - 1;
|
||||
const left = (num - index) * -offset;
|
||||
const top = (num - index) * -offset;
|
||||
const left = (num - index) * -OFFSET;
|
||||
const top = (num - index) * -OFFSET;
|
||||
return `left:${left}px; top:${top}px;`;
|
||||
},
|
||||
enter(target, done){
|
||||
let elementId = target.attributes['data-element-id'].value;
|
||||
let source = document.getElementById(elementId);
|
||||
// Get the original styles so we can repair them later
|
||||
let originalStyle = {
|
||||
transform: target.style.transform,
|
||||
background: target.style.background,
|
||||
borderRadius: target.style.borderRadius,
|
||||
transition: target.style.transition,
|
||||
boxShadow: target.style.boxShadow,
|
||||
sourceTransition: source.style.transition,
|
||||
}
|
||||
|
||||
// hide the source
|
||||
source.style.transition = "none";
|
||||
source.style.visibility = "hidden";
|
||||
|
||||
// Instantly mock the source
|
||||
target.style.transition = 'none';
|
||||
mockElement({source, target});
|
||||
|
||||
// After a full tick, repair the original styles
|
||||
Vue.nextTick(() => {
|
||||
target.style.transform = originalStyle.transform;
|
||||
target.style.background = originalStyle.background;
|
||||
target.style.borderRadius = originalStyle.borderRadius;
|
||||
target.style.transition = originalStyle.transition;
|
||||
target.style.boxShadow = originalStyle.boxShadow;
|
||||
source.style.transition = originalStyle.sourceTransition;
|
||||
setTimeout(done, MOCK_DURATION);
|
||||
});
|
||||
},
|
||||
leave(target, done){
|
||||
let elementId = target.attributes['data-element-id'].value;
|
||||
let source = document.getElementById(elementId);
|
||||
let index = target.attributes['data-index'].value;
|
||||
if (index != 0){
|
||||
// If we aren't the only dialog, we'll need compensate for offset
|
||||
mockElement({source, target, offset: {x: OFFSET, y: OFFSET}})
|
||||
} else {
|
||||
mockElement({source, target});
|
||||
}
|
||||
setTimeout(() => {
|
||||
source.style.visibility = null;
|
||||
done();
|
||||
}, MOCK_DURATION);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -58,6 +117,7 @@
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
}
|
||||
.dialog-sizer {
|
||||
position: relative;
|
||||
@@ -79,17 +139,25 @@
|
||||
.backdrop.hidden {
|
||||
display: none
|
||||
}
|
||||
.dialog-list-move {
|
||||
transition: transform 400ms;
|
||||
}
|
||||
.dialog-list-leave-active {
|
||||
|
||||
}
|
||||
.dialog-list-enter .dialog-component, .dialog-list-leave-to .dialog-component {
|
||||
opacity: 0;
|
||||
}
|
||||
.dialog-list-enter-active .dialog-component {
|
||||
transition: opacity 4s;
|
||||
}
|
||||
.dialog-list-leave-active .dialog-component {
|
||||
transition: opacity 4s 4s;
|
||||
}
|
||||
.dialog {
|
||||
transition: all 8s;
|
||||
transform-origin: top left;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
pointer-events: initial;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
}
|
||||
.dialog > * {
|
||||
height: 100%;
|
||||
|
||||
@@ -7,14 +7,15 @@ const dialogStackStore = {
|
||||
currentResult: null,
|
||||
},
|
||||
mutations: {
|
||||
pushDialogStack(state, {component, data, element, returnElement, callback}){
|
||||
pushDialogStack(state, {component, data, elementId, returnElement, callback}){
|
||||
// Generate a new _id so that Vue knows how to shuffle the array
|
||||
console.log({elementId});
|
||||
const _id = Random.id();
|
||||
state.dialogs.push({
|
||||
_id,
|
||||
component,
|
||||
data,
|
||||
element,
|
||||
elementId,
|
||||
returnElement,
|
||||
callback,
|
||||
});
|
||||
|
||||
58
app/imports/ui/dialogStack/mockElement.js
Normal file
58
app/imports/ui/dialogStack/mockElement.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { parse, stringify } from 'css-box-shadow';
|
||||
|
||||
// Only supports border radius defined like "20px" or "100%"
|
||||
const transformedRadius = (radiusString, deltaWidth, deltaHeight) => {
|
||||
if (/^\d+\.?\d*px$/.test(radiusString)){
|
||||
//The radius is defined in pixel units, so get the radius as a number
|
||||
const rad = +radiusString.match(/\d+\.?\d*/)[0];
|
||||
// Set the x and y radius of the "to" element, compensating for scale
|
||||
return `${rad / deltaWidth}px / ${rad / deltaHeight}px`;
|
||||
} else if (/^\d+\.?\d*%$/.test(radiusString)) {
|
||||
//The radius is defined as a percentage, so just use it as is
|
||||
return radiusString;
|
||||
}
|
||||
};
|
||||
|
||||
const transformedBoxShadow = (shadowString, deltaWidth, deltaHeight) => {
|
||||
if (shadowString[0] === 'r'){
|
||||
let strings = shadowString.match(/rgba\([^)]+\)[^,]+/g);
|
||||
strings = strings.map(string => {
|
||||
// TODO move color to end
|
||||
strings.match(/(rgba\([^)]+\))([^,]+)/)
|
||||
});
|
||||
}
|
||||
let scaleAverage = (deltaWidth + deltaHeight) / 2;
|
||||
let shadows = parse(shadowString);
|
||||
console.log({shadowString, shadows});
|
||||
shadows.forEach(shadow => {
|
||||
shadow.offsetX /= deltaWidth;
|
||||
shadow.offsetY /= deltaHeight;
|
||||
shadow.blurRadius /= scaleAverage;
|
||||
shadow.spreadRadius /= scaleAverage;
|
||||
})
|
||||
console.log({newShadows: shadows});
|
||||
return stringify(shadows);
|
||||
}
|
||||
|
||||
export default function mockElement({source, target, offset = {x: 0, y: 0}}){
|
||||
if (!source || !target) throw `Can't mock without ${source ? 'target' : 'source'}` ;
|
||||
sourceRect = source.getBoundingClientRect();
|
||||
targetRect = target.getBoundingClientRect();
|
||||
|
||||
// Get how must the target change to become the source
|
||||
const deltaWidth = sourceRect.width / targetRect.width;
|
||||
const deltaHeight = sourceRect.height / targetRect.height;
|
||||
const deltaLeft = sourceRect.left - targetRect.left + offset.x;
|
||||
const deltaTop = sourceRect.top - targetRect.top + offset.y;
|
||||
// Mock the source
|
||||
target.style.transform = `translate(${deltaLeft}px, ${deltaTop}px) ` +
|
||||
`scale(${deltaWidth}, ${deltaHeight})`;
|
||||
|
||||
target.style.background = getComputedStyle(source).background;
|
||||
target.style.borderRadius = transformedRadius(
|
||||
getComputedStyle(source).borderRadius, deltaWidth, deltaHeight
|
||||
);
|
||||
//target.style.boxShadow = transformedBoxShadow(
|
||||
// getComputedStyle(source).boxShadow, deltaWidth, deltaHeight
|
||||
//);
|
||||
};
|
||||
@@ -8,7 +8,6 @@ import CharacterSheetPage from "/imports/ui/pages/CharacterSheetPage.vue";
|
||||
import SignIn from "/imports/ui/pages/SignIn.vue" ;
|
||||
import Register from "/imports/ui/pages/Register.vue" ;
|
||||
import Account from "/imports/ui/pages/Account.vue" ;
|
||||
import TestDialog from "/imports/ui/dialogStack/TestDialog.vue";
|
||||
|
||||
// Not found
|
||||
import NotFound from '/imports/ui/pages/NotFound.vue';
|
||||
@@ -45,9 +44,6 @@ RouterFactory.configure(factory => {
|
||||
},{
|
||||
path: "/account",
|
||||
component: Account,
|
||||
},{
|
||||
path: "/test-dialog",
|
||||
component: TestDialog,
|
||||
},
|
||||
]);
|
||||
//Development routes
|
||||
|
||||
5
app/package-lock.json
generated
5
app/package-lock.json
generated
@@ -243,6 +243,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"css-box-shadow": {
|
||||
"version": "1.0.0-3",
|
||||
"resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz",
|
||||
"integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg=="
|
||||
},
|
||||
"dashdash": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"bcrypt": "^1.0.3",
|
||||
"bower": "^1.7.9",
|
||||
"core-js": "^2.6.2",
|
||||
"css-box-shadow": "^1.0.0-3",
|
||||
"fibers": "^2.0.2",
|
||||
"meteor-node-stubs": "^0.3.3",
|
||||
"qrcode": "^1.3.3",
|
||||
|
||||
Reference in New Issue
Block a user