From 00e8cbc1c8bc94c6e76fce9e99db8126958d3008 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Wed, 23 Jan 2019 16:49:58 +0200 Subject: [PATCH] Added dialog animations, still working on box shadows --- app/imports/ui/StoryBook.vue | 2 + .../{TestDialog.vue => DialogBase.Story.vue} | 5 +- .../ui/dialogStack/DialogStack.Story.vue | 22 +++++ app/imports/ui/dialogStack/DialogStack.vue | 92 ++++++++++++++++--- .../ui/dialogStack/dialogStackStore.js | 5 +- app/imports/ui/dialogStack/mockElement.js | 58 ++++++++++++ app/imports/ui/router.js | 4 - app/package-lock.json | 5 + app/package.json | 1 + 9 files changed, 174 insertions(+), 20 deletions(-) rename app/imports/ui/dialogStack/{TestDialog.vue => DialogBase.Story.vue} (78%) create mode 100644 app/imports/ui/dialogStack/DialogStack.Story.vue create mode 100644 app/imports/ui/dialogStack/mockElement.js diff --git a/app/imports/ui/StoryBook.vue b/app/imports/ui/StoryBook.vue index 0d31dc4f..a253731e 100644 --- a/app/imports/ui/StoryBook.vue +++ b/app/imports/ui/StoryBook.vue @@ -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, diff --git a/app/imports/ui/dialogStack/TestDialog.vue b/app/imports/ui/dialogStack/DialogBase.Story.vue similarity index 78% rename from app/imports/ui/dialogStack/TestDialog.vue rename to app/imports/ui/dialogStack/DialogBase.Story.vue index 798deb55..9f519dc7 100644 --- a/app/imports/ui/dialogStack/TestDialog.vue +++ b/app/imports/ui/dialogStack/DialogBase.Story.vue @@ -4,7 +4,7 @@ Test Dialog
- Open Dialog + Open Dialog
@@ -14,9 +14,10 @@ import DialogBase from "/imports/ui/dialogStack/DialogBase.vue"; const component = { methods: { - openDialog(event){ + openDialog(elementId){ store.commit("pushDialogStack", { component, + elementId, }); } }, diff --git a/app/imports/ui/dialogStack/DialogStack.Story.vue b/app/imports/ui/dialogStack/DialogStack.Story.vue new file mode 100644 index 00000000..e5068bd6 --- /dev/null +++ b/app/imports/ui/dialogStack/DialogStack.Story.vue @@ -0,0 +1,22 @@ + + + diff --git a/app/imports/ui/dialogStack/DialogStack.vue b/app/imports/ui/dialogStack/DialogStack.vue index 5a883b6c..1e78c570 100644 --- a/app/imports/ui/dialogStack/DialogStack.vue +++ b/app/imports/ui/dialogStack/DialogStack.vue @@ -5,14 +5,23 @@ @click="backdropClicked" :class="dialogs.length ? '' : 'hidden' " > - + +
- +
@@ -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); + } }, }; @@ -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%; diff --git a/app/imports/ui/dialogStack/dialogStackStore.js b/app/imports/ui/dialogStack/dialogStackStore.js index 2677ee72..8c6fe155 100644 --- a/app/imports/ui/dialogStack/dialogStackStore.js +++ b/app/imports/ui/dialogStack/dialogStackStore.js @@ -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, }); diff --git a/app/imports/ui/dialogStack/mockElement.js b/app/imports/ui/dialogStack/mockElement.js new file mode 100644 index 00000000..07f37e45 --- /dev/null +++ b/app/imports/ui/dialogStack/mockElement.js @@ -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 + //); +}; diff --git a/app/imports/ui/router.js b/app/imports/ui/router.js index 4b0f6c55..82dd19a7 100644 --- a/app/imports/ui/router.js +++ b/app/imports/ui/router.js @@ -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 diff --git a/app/package-lock.json b/app/package-lock.json index 62c448ee..6c9fe581 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -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", diff --git a/app/package.json b/app/package.json index 2bbd6a11..a98ee859 100644 --- a/app/package.json +++ b/app/package.json @@ -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",