From b8fdc27df908cabefa658cffb7814223f334c3b7 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Tue, 17 Jan 2017 15:00:05 +0200 Subject: [PATCH] Rebuilt dialog animations with cross-fade effect --- .../dialogStack/dialogStack.css | 10 +- .../paperTemplates/dialogStack/dialogStack.js | 176 ++++++++++++------ 2 files changed, 121 insertions(+), 65 deletions(-) diff --git a/rpg-docs/client/views/paperTemplates/dialogStack/dialogStack.css b/rpg-docs/client/views/paperTemplates/dialogStack/dialogStack.css index 9f743de7..d3df2419 100644 --- a/rpg-docs/client/views/paperTemplates/dialogStack/dialogStack.css +++ b/rpg-docs/client/views/paperTemplates/dialogStack/dialogStack.css @@ -6,23 +6,23 @@ bottom: 0; left: 0; visibility: visible; - transition: visibility 0s linear; + transition: visibility 0s; } .dialog-stack.hide { visibility: hidden; - transition: visibility 0s linear 0.4s; + transition: visibility 0s linear 400ms; } .dialog-stack .backdrop { background: rgba(0,0,0,0.4); opacity: 1; - transition: opacity 0.4s linear; + transition: opacity 400ms linear; } .dialog-stack.hide .backdrop { opacity: 0; - transition: opacity 0.2s linear; + transition: opacity 200ms linear; } .dialog-stack .dialog-sizer { @@ -39,9 +39,9 @@ box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.4); - transition: all 0.4s ease; overflow: hidden; border-radius: 2px; + transform-origin: top left; } .dialog-stack .dialog .testButton { diff --git a/rpg-docs/client/views/paperTemplates/dialogStack/dialogStack.js b/rpg-docs/client/views/paperTemplates/dialogStack/dialogStack.js index ff22965a..332274b3 100644 --- a/rpg-docs/client/views/paperTemplates/dialogStack/dialogStack.js +++ b/rpg-docs/client/views/paperTemplates/dialogStack/dialogStack.js @@ -1,5 +1,6 @@ dialogs = new ReactiveArray(); const offset = 16; +const duration = 400; pushDialogStack = function({template, data, element, callback}){ // Generate a new _id so that Blaze knows how to shuffle the array @@ -19,6 +20,11 @@ popDialogStack = function(result){ dialog.callback && dialog.callback(result); }; +let cloneHolder; +Template.dialogStack.onRendered(function(){ + cloneHolder = this.find(".clone-holder"); +}); + Template.dialogStack.helpers({ dialogStackClass(){ if (!dialogs.get().length) return "hide"; @@ -27,7 +33,9 @@ Template.dialogStack.helpers({ return dialogs.get(); }, dialogStyle(index){ - const num = dialogs.get().length - 1; + const length = dialogs.get().length; + if (!length) return; + const num = length - 1; const left = (num - index) * -offset; const top = (num - index) * -offset; return `left:${left}px; top:${top}px;`; @@ -45,69 +53,111 @@ Template.dialogStack.events({ }, }); -const heroAnimate = ({from, to, duration, useClone, callback}) => { - if (!from) throw "From element must be defined"; - if (!to) throw "To element must be defined"; - duration = duration || 400; - // Get the bounding rectangles of both elements - const toRect = to.getBoundingClientRect(); - const fromRect = from.getBoundingClientRect(); - let originalNode; - let originalVis; - if (useClone){ - originalNode = to; - to = originalNode.cloneNode(true); - originalNode.parentNode.insertBefore(to, originalNode); - to.style.position = "fixed"; - to.style.zIndex = "9999"; - originalVis = originalNode.style.visibility; - originalNode.style.visibility = "hidden"; - } - // Get how they have changed - const deltaLeft = fromRect.left - toRect.left; - const deltaTop = fromRect.top - toRect.top; - const deltaWidth = fromRect.width / toRect.width; - const deltaHeight = fromRect.height / toRect.height; - // Make the "to" element imitate the "from" element - to.style.transition = "none"; - to.style.transform = `translate(${deltaLeft}px, ${deltaTop}px) ` + - `scale(${deltaWidth}, ${deltaHeight})`; - to.style.background = $(from).css("background"); - to.style.boxShadow = $(from).css("box-shadow"); - // Imitate the border radius after transform - // Only supports border radius defined like "20px" or "100%" - let radius = $(from).css("border-radius"); - if (/^\d+\.?\d*px$/.test(radius)){ +// 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 = +radius.match(/\d+\.?\d*/)[0]; + const rad = +radiusString.match(/\d+\.?\d*/)[0]; // Set the x and y radius of the "to" element, compensating for scale - to.style.borderRadius = `${rad / deltaWidth}px / ${rad / deltaHeight}px`; - } else if (/^\d+\.?\d*%$/.test(radius)) { + 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 - to.style.borderRadius = radius; + return radiusString; } - // Don't animate to the imitation position - to.style.transition = "none"; - // We calculate everything from the top left, so use that as origin - to.style.transformOrigin = "top left"; +}; + +const imitate = ( + element, source, deltaLeft, deltaTop, deltaWidth, deltaHeight +) => { + element.style.transform = `translate(${deltaLeft}px, ${deltaTop}px) ` + + `scale(${deltaWidth}, ${deltaHeight})`; + element.style.background = $(source).css("background"); + // Imitate the border radius after transform + element.style.borderRadius = transformedRadius($(source).css("border-radius")); +} + +const dialogOpenAnimation = ({element, dialog}) => { + const dialogRect = dialog.getBoundingClientRect(); + const elementRect = element.getBoundingClientRect(); + element.style.visibility = "hidden"; + // Get how must the element change to become the dialog + const deltaLeft = elementRect.left - dialogRect.left; + const deltaTop = elementRect.top - dialogRect.top; + const deltaWidth = elementRect.width / dialogRect.width; + const deltaHeight = elementRect.height / dialogRect.height; + + // Make the dialog imitate the element, immediately + dialog.style.transition = "none"; + imitate(dialog, element, deltaLeft, deltaTop, deltaWidth, deltaHeight); - // Next frame, undo the imitation, let "to" animate into its place _.defer(() => { - to.style.transition = `all ${duration / 1000}s ease, ` + - `box-shadow ${duration / 1000}s linear 0.1s`; - to.style.transform = ""; - to.style.borderRadius = ""; - to.style.background = ""; - to.style.boxShadow = ""; + // Next frame, undo the imitation, let dialog animate back into place + dialog.style.transition = `all ${duration}ms ease`; + dialog.style.transform = ""; + dialog.style.borderRadius = ""; + dialog.style.background = ""; }); // Clean up after the animation is done and call our callback _.delay(() => { - to.style.transition = ""; - to.style.transformOrigin = ""; - if (useClone){ - originalNode.style.visibility = originalVis; - to.remove(); - } + dialog.style.transition = ""; + }, duration); +} + +const dialogCloseAnimation = ({element, dialog, callback}) => { + // Reset the dialog if it is mid-transition + dialog.style.transition = "none"; + dialog.style.transform = "none"; + dialog.style.borderRadius = ""; + dialog.style.background = ""; + dialog.style.opacity = "1"; + // Get the original bounding rectangles of both elements + const dialogRect = dialog.getBoundingClientRect(); + const elementRect = element.getBoundingClientRect(); + // Set up a clone of the original element + // This lets us have a fixed position element which isn't clipped + clone = element.cloneNode(true); + clone.style.position = "fixed"; + clone.style.top = 0; + clone.style.left = 0; + clone.style.width = elementRect.width + "px"; + clone.style.height = elementRect.height + "px"; + clone.style.visibility = ""; + clone.style.zIndex = 9999; + // Insert clone before its progenitor so it can inherit css correctly + element.parentNode.insertBefore(clone, element); + // Polymer messes up fixed positioning, measure and compensate + startingRect = clone.getBoundingClientRect(); + clone.style.top = elementRect.top - startingRect.top + "px"; + clone.style.left = elementRect.left - startingRect.left + "px"; + + // How must the original dialog change to become the element + const deltaLeft = dialogRect.left - elementRect.left; + const deltaTop = dialogRect.top - elementRect.top; + const deltaWidth = dialogRect.width / elementRect.width; + const deltaHeight = dialogRect.height / elementRect.height; + + // Make the clone imitate the dialog + clone.style.transition = "none"; + clone.style.transformOrigin = "top left" + imitate(clone, dialog, deltaLeft, deltaTop, deltaWidth, deltaHeight); + + _.defer(() => { + // Next frame, undo the imitation, let clone animate into its place + clone.style.transition = `all ${duration}ms ease`; + clone.style.transform = ""; + clone.style.borderRadius = ""; + clone.style.background = ""; + // Make the dialog follow the clone in and fade away + dialog.style.transition = `all ${duration}ms ease, ` + + `opacity ${duration * 0.75}ms ease-in`; + dialog.style.opacity = 0; + imitate(dialog, element, -deltaLeft, + -deltaTop, 1 / deltaWidth, 1 / deltaHeight); + }); + // Clean up after the animation is done and call our callback + _.delay(() => { + element.style.visibility = ""; + clone.remove(); if (callback) callback(); }, duration); }; @@ -119,19 +169,25 @@ Template.dialogStack.uihooks({ $(node).insertBefore(next); const data = Blaze.getData(node); if (data.element){ - data.element.style.visibility = "hidden"; // Store the reference to the element on the DOM node itself, // since Blaze won't keep the data around for the remove hook node["data-element"] = data.element; - heroAnimate({from: data.element, to: node}); + dialogOpenAnimation({ + element: data.element, + dialog: node, + }); } }, remove: function(node, tpl) { const element = node["data-element"]; if (element){ - element.style.visibility = ""; - heroAnimate({from: node, to: element, useClone: true}); - node.remove(); + dialogCloseAnimation({ + element, + dialog: node, + callback(){ + node.remove(); + }, + }); } else { node.remove(); }