import {Helpers, IelementTransform, ICSSElementTransform} from './Helpers'; import { IscrollData } from './Scrolling'; import { Viewport } from 'csstype'; import * as jQuery from 'jquery'; import * as $ from 'jquery'; import styler from 'stylefire'; import {action,Action, ColdSubscription } from 'popmotion'; import { Predicate } from 'popmotion/lib/chainable/types'; export class Loc{ constructor(public Left:number=0,public Top:number=0){ } add(point:Loc):Loc{ return new Loc( point.Left+this.Left, point.Top+this.Top); } subtract(point:Loc):Loc{ return new Loc( this.Left-point.Left, this.Top-point.Top); } scale(scale:number):Loc{ return new Loc(this.Left*scale, this.Top*scale); } combineWithTransforms(inputElement:HTMLElement):Loc{ //TODO:Combine with scales //totalScale = totalScale.scale(elemTransform.sx, elemTransform.sy); //transformation = transformation.translate(elemTransform.tx, elemTransform.ty); // @ts-ignore //let eletxTrans = ((input.outerWidth() * (1 - elemTransform.sx)) / 2); // @ts-ignore //let eletyTrans = ((input.outerHeight() * (1 - elemTransform.sy)) / 2); // @ts-ignore //totalScaleTrans = constructTransformation().translate(eletxTrans, eletyTrans); let elemTransform = Helpers.getElemTransform(inputElement); let translate = new Loc(elemTransform.tx, elemTransform.ty); let scale = new Loc((inputElement.getOuterWidth()*(1-elemTransform.sx))/2, (inputElement.getOuterHeight()*(1-elemTransform.sy))/2); return this.add(translate).add(scale); } } declare global{ interface HTMLElement { offsetLoc:()=>Loc; scrollLoc:()=>Loc; marginLoc:()=>Loc; getOuterWidth:()=>number; getOuterHeight:()=>number; } interface CSSStyleDeclaration{ borderLoc:()=>Loc; } } CSSStyleDeclaration.prototype.borderLoc = function():Loc{ return new Loc( this.borderLeftWidth && parseFloat(this.borderLeftWidth) || 0, this.borderTopWidth && parseFloat(this.borderTopWidth) || 0); }; HTMLElement.prototype.offsetLoc = function():Loc{ return new Loc(this.offsetLeft, this.offsetTop); }; HTMLElement.prototype.scrollLoc = function():Loc{ return new Loc(this.scrollLeft, this.scrollTop); }; HTMLElement.prototype.marginLoc = function():Loc{ return new Loc(parseFloat($(this).css("margin-left")) || 0, parseFloat($(this).css("margin-top")) || 0); }; HTMLElement.prototype.getOuterHeight = function():number{ let thisElem = $(this); let outerHeight = thisElem.outerHeight(); if (outerHeight == 0) { outerHeight = $(this).prop('scrollHeight'); } return outerHeight ? outerHeight : 0; }; HTMLElement.prototype.getOuterWidth = function():number{ let thisElem = $(this); let outerWidth = thisElem.outerWidth(); if (outerWidth == 0) { outerWidth = $(this).prop('scrollWidth'); } return outerWidth ? outerWidth : 0; }; type relatedElements = {bodyElement:HTMLElement, documentElement:HTMLElement, defaultView:Window, offSetParent:Element|null, prevComputedStyle:CSSStyleDeclaration}; export interface IPopMotionZoomSettings{ popMotionAction?:(from:ICSSElementTransform,to:ICSSElementTransform)=>Action; popMotionWhile?:Predicate; popMotionPipe?:Function[]; popMotionFilter?:Predicate; } export interface IBaseSettings{ duration?:number; easing?:string|Array; nativeAnimation?:boolean; root?:HTMLElement; animationStarted?:Array<()=>void>; animationCompleted?:Array<()=>void>; popMotionSettings?: IPopMotionZoomSettings; } export interface ISettings extends IBaseSettings{ targetSize?:number; scaleMode?:'both'|'height'|'width'|'none'|'scale'; navOffset?:Loc; //>0-1. The amount that this zoom should be completed. By default this is 1. completion?:number; } export interface IResetSettings extends IBaseSettings{ } //TODO:Add reset function and take out reset flag from settings export class Zoomooz{ transitionAnimator:TransitionAnimator; popMotiontransitionAnimator:PopMotionTransitionAnimator; constructor(){ this.transitionAnimator = new TransitionAnimator(); this.popMotiontransitionAnimator = new PopMotionTransitionAnimator(); } /** @internal */ isElementBodyElement(elem:HTMLElement):boolean{ return elem === elem.ownerDocument!.body; } /** @internal */ getBodyElementPosition(elem:HTMLElement){ let bOffset = (jQuery).offset.bodyOffset(elem); // @ts-ignore let loc = new Loc(bOffset.left, bOffset.top); return loc; } /** @internal */ getRelatedElements(inputElement:HTMLElement):relatedElements{ let offsetParent = inputElement.offsetParent; let doc = inputElement.ownerDocument; //@ts-ignore let docElem = doc.documentElement; //@ts-ignore let body = doc.body; //@ts-ignore let defaultView = doc.defaultView; let prevComputedStyle:CSSStyleDeclaration; if (defaultView) { prevComputedStyle = defaultView.getComputedStyle(inputElement, null); } else { throw new Error('There is no default view!'); //This is not supported on modern browsers //prevComputedStyle = inputElement.currentStyle; } return {bodyElement:body, documentElement:docElem, defaultView:defaultView, offSetParent:offsetParent, prevComputedStyle:prevComputedStyle}; } /** @internal */ get support() { let support; if ((jQuery).offset.initialize) { (jQuery).offset.initialize(); support = { fixedPosition: (jQuery).offset.supportsFixedPosition, doesNotAddBorder: (jQuery).offset.doesNotAddBorder, doesAddBorderForTableAndCells: jQuery.support.doesAddBorderForTableAndCells, subtractsBorderForOverflowNotVisible: (jQuery).offset.subtractsBorderForOverflowNotVisible }; } else { support = jQuery.support; } return support; } private verifyInputElements(elem:HTMLElement, root:HTMLElement){ if (!elem || elem==null) { throw new Error('Input element does not exist!') } if (!root || root==null) { throw new Error('Input element does not exist!') } } /** @internal */ //Returns all the offsets of the parent container elements combined public getRecursiveOffsets(input:HTMLElement, root:HTMLElement,relatedElements:relatedElements):Loc{ let support = this.support; let offset:Loc = new Loc(); let cursorElement:HTMLElement = input; let offsetParent = input.offsetParent; while ((cursorElement = cursorElement.parentNode) && cursorElement !== root && cursorElement.ownerDocument!.body !== cursorElement) { if (support.fixedPosition && relatedElements.prevComputedStyle.position === "fixed") { return offset; } if(cursorElement!=offsetParent) continue; offset = offset.combineWithTransforms(cursorElement); let computedStyle = relatedElements.defaultView.getComputedStyle(cursorElement, null); offset = offset.subtract(cursorElement.scrollLoc()); offset = offset.add(computedStyle.borderLoc()); offset = offset.add(this.getElementOffset(cursorElement, relatedElements.defaultView)); offsetParent = cursorElement.offsetParent; if(false){ //Not sure I understand why this is done //TODO:May have to include padding and margins //This is from https://robflaherty.github.io/jquery-annotated-source/docs/14-offset.html if (cursorElement === relatedElements.offSetParent) { offset = offset.add(cursorElement.offsetLoc()); if (support.doesNotAddBorder && !(support.doesAddBorderForTableAndCells && /^t(?:able|d|h)$/i.test(cursorElement.nodeName))) { offset = offset.add(computedStyle.borderLoc()); } relatedElements.offSetParent = cursorElement.offsetParent; } if (support.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible") { offset = offset.add(computedStyle.borderLoc()); } } } return offset; } /** @internal */ public getBodyOffsets(relatedElements:relatedElements, root:HTMLElement):Loc{ let offsetLoc:Loc = new Loc(); //If the root is the body then this should be ignored if(relatedElements.bodyElement==root) { return offsetLoc; } if (relatedElements.prevComputedStyle.position === "relative" || relatedElements.prevComputedStyle.position === "static") { offsetLoc = offsetLoc.add(relatedElements.bodyElement.offsetLoc()); } else if (this.support().fixedPosition && relatedElements.prevComputedStyle.position === "fixed") { let maxScroll = new Loc(Math.max(relatedElements.documentElement.scrollLeft, relatedElements.bodyElement.scrollLeft), Math.max(relatedElements.documentElement.scrollTop, relatedElements.bodyElement.scrollTop)); offsetLoc = offsetLoc.add(maxScroll); } return offsetLoc; } public getRootOffsets(root:HTMLElement){ return root.marginLoc(); } public computeActualPosition(input:HTMLElement, rootElement?:HTMLElement):Loc { //TODO:Add outermost element margin if outermost element pushes down the root let inputElement = input; let root = rootElement ? rootElement : $('#root')[0]; this.verifyInputElements(inputElement, root); let totalTranslationPoint:Loc = new Loc(); //*** */Legacy may end up removing this***** if (this.isElementBodyElement(inputElement)) { return this.getBodyElementPosition(inputElement) } let relatedElements = this.getRelatedElements(inputElement); let offsetLoc = this.getElementOffset(inputElement, relatedElements.defaultView); offsetLoc = offsetLoc.combineWithTransforms(inputElement); let recursiveOffsets = this.getRecursiveOffsets(inputElement, root, relatedElements); offsetLoc = offsetLoc.add(recursiveOffsets); let bodyOffsets = this.getBodyOffsets(relatedElements,root); offsetLoc = offsetLoc.add(bodyOffsets); let rootOffsets = this.getElementOffset(root, relatedElements.defaultView);//this.getRootOffsets(root); offsetLoc = offsetLoc.add(rootOffsets); return offsetLoc; } private getElementOffset(inputElement: HTMLElement, defaultView:Window, includeBorderAsOffset:boolean=false) { //TODO:Add em conversion let computedStyle = defaultView.getComputedStyle(inputElement); let offsetLoc:Loc = new Loc(); offsetLoc = offsetLoc.add(new Loc(inputElement.offsetLeft, inputElement.offsetTop)); if(false){ offsetLoc = offsetLoc.add(this.parsePixelLoc(computedStyle.left, computedStyle.top)); offsetLoc = offsetLoc.add(this.parsePixelLoc(computedStyle.paddingLeft, computedStyle.paddingTop)); offsetLoc = offsetLoc.add(this.parsePixelLoc(computedStyle.marginLeft, computedStyle.marginTop)); } if(includeBorderAsOffset) offsetLoc = offsetLoc.add(this.parsePixelLoc(computedStyle.borderLeft, computedStyle.borderTop)); return offsetLoc; } private parsePixelLoc(left:string|null, top:string|null):Loc{ return new Loc(parseFloat(this.parsePixel(left)) || 0, parseFloat(this.parsePixel(top))|| 0); } private parsePixel(pixel:string|null){ if(!pixel) return '0'; return pixel.split(' ')[0].replace('px', ''); } public computeZoomedPosition(scale:number, elementOffset:Loc, elemWidth:number, elemHeight:number, navOffset?:Loc, root?:HTMLElement):Loc{ root = root ? root : $('#root')[0]; navOffset = navOffset ? navOffset : new Loc(); let width = root.getOuterWidth(); let height = root.getOuterHeight(); let xrotorigin = width / 2.0; let yrotorigin = height / 2.0; let zoomedOffset = elementOffset.scale(scale); let originOffset = new Loc((scale -1) * (xrotorigin), (scale-1) * yrotorigin); let centerOffset = new Loc((width - (scale*elemWidth))/2, (height - (scale*elemHeight))/2); navOffset = navOffset.scale(scale); zoomedOffset = originOffset.subtract(zoomedOffset); // zoomedOffset = originOffset.subtract(centerOffset); zoomedOffset = zoomedOffset.add(centerOffset); zoomedOffset = zoomedOffset.add(navOffset); return zoomedOffset; } public ZoomTo(zoomToElement:HTMLElement, settings:ISettings){ this.zoomTo($(zoomToElement),settings); } public Reset(settings:IResetSettings){ this.zoomTo($(), settings, true); } /**@internal */ public zoomTo(zoomToElement:JQuery, settings:ISettings, reset:boolean=false){ let root = settings.root ? $(settings.root) : $('#root'); let rootElem = root[0]; let width = rootElem.getOuterWidth(); let height = rootElem.getOuterHeight(); let scale:number; let zoomedPosition:Loc; if(!reset){ let elementPosition = this.computeActualPosition(zoomToElement[0], root[0]); scale = this.getScale(height!, width!, zoomToElement[0], settings); let zoomElem = zoomToElement[0]; zoomedPosition = this.computeZoomedPosition(scale, elementPosition, zoomElem.getOuterWidth(), zoomElem.getOuterHeight(), settings.navOffset ? settings.navOffset : new Loc(), rootElem); } else{ zoomedPosition = new Loc(); scale = 1; } let currentPosition = Helpers.getElemTransform(rootElem); if(!settings.completion){ settings.completion = 1; } let completionScale = scale; let transformationMatrix = {tx:(zoomedPosition.Left), ty:(zoomedPosition.Top), sx:completionScale, sy:completionScale}; let zoomRootTransform = {}; if(!settings.popMotionSettings) { if(settings.nativeAnimation){ let cssTransform = Helpers.matrixCompose(transformationMatrix); zoomRootTransform = Helpers.constructZoomRootCssTransform(cssTransform, settings.duration, Helpers.constructBezierArray(settings.easing)); root.css(zoomRootTransform); if (settings.animationStarted) { settings.animationStarted.forEach(i=>i()); } } else{ zoomRootTransform = Helpers.constructResetZoomRootCssTransform(); root.css(zoomRootTransform); this.transitionAnimator.AnimateTransition(rootElem, currentPosition, transformationMatrix, settings); } } else{ if(settings.nativeAnimation){ } else{ this.popMotiontransitionAnimator.AnimateTransition(rootElem, currentPosition, transformationMatrix, settings); } // animateTransition($target, current_affine, final_affine, settings, animateEndCallback, animateStartedCallback); } } public getScale(height:number, width:number, elem:HTMLElement, settings:ISettings):number { let outerHeight = elem.getOuterHeight(), outerWidth = elem.getOuterWidth(); let relw = width / outerWidth; let relh = height / outerHeight; let zoomMode = settings.scaleMode ? settings.scaleMode:"both"; let zoomAmount = settings.targetSize ? settings.targetSize : 1; let scale; if (zoomMode == "none") { scale = 1; } else if (zoomMode == "width") { scale = zoomAmount * relw; } else if (zoomMode == "height") { scale = zoomAmount * relh; } else if (zoomMode == "both") { scale = zoomAmount * Math.min(relw, relh); } else if (zoomMode == "scale") { scale = zoomAmount; } else { console.log("wrong zoommode:" + zoomMode); throw "wrong zoommode:" + zoomMode; } return scale; } getScaledOffset(scale:number,width:number, height:number):Loc{ let halfWidth = width/2; let halfHeight = height/2; let scaleWidth = halfWidth*scale; let scaleHeight = halfHeight*scale; return new Loc(halfWidth - scaleWidth, halfHeight - scaleHeight); } public stop(){ this.transitionAnimator.Stop(); this.popMotiontransitionAnimator.Stop(); } } export class PopMotionTransitionAnimator implements ITransitionAnimator{ private coldSubscription?:ColdSubscription; AnimateTransition(targetElement: HTMLElement, startTransform: IelementTransform, endTransform: IelementTransform, settings: ISettings) { this.Stop(); let thestyler = styler(targetElement); let startTime:number; let completionSpecified = settings.completion && settings.completion!=1; let popMotionSettings = settings.popMotionSettings!; let action = popMotionSettings.popMotionAction!(Helpers.constructCssObject(startTransform), Helpers.constructCssObject(endTransform)); if(popMotionSettings.popMotionPipe){ popMotionSettings.popMotionPipe.forEach(pipe=>action = action.pipe(pipe)); } if(popMotionSettings.popMotionFilter){ action = action.filter(popMotionSettings.popMotionFilter); } if(popMotionSettings.popMotionWhile){ action = action.while(popMotionSettings.popMotionWhile); } if(completionSpecified){ let actualDuration = settings.completion! * settings.duration!; action = action.while((i)=> !startTime || (new Date()).getTime() - startTime>=actualDuration); } this.coldSubscription = action.start( { complete:()=>{ this.coldSubscription = undefined; if(settings.animationCompleted) settings.animationCompleted.forEach(i=>i()); }, update: v=>{ if(!startTime){ startTime = new Date().getTime(); if(settings.animationStarted) settings.animationStarted.forEach(i=>i()); } thestyler.set({transform:Helpers.matrixComposeCSS(v)}); }}); } Stop(){ if(this.coldSubscription) this.coldSubscription!.stop(); this.coldSubscription = undefined; } } export interface ITransitionAnimator{ AnimateTransition(targetElement:HTMLElement, startTransform:IelementTransform, endTransform:IelementTransform, settings:ISettings); Stop(); } export class TransitionAnimator implements ITransitionAnimator{ private animation_interval_timer?:NodeJS.Timeout|null; private easingFunction?:(t:number)=>number; private animation_start_time?:number; private stop?:boolean; private started:boolean=false; public AnimateTransition(targetElement:HTMLElement, startTransform:IelementTransform, endTransform:IelementTransform, settings:ISettings){ if(!settings.duration){ throw 'Must set duration on settings!'; } this.animation_start_time = (new Date()).getTime(); if (this.animation_interval_timer) { clearInterval(this.animation_interval_timer); this.animation_interval_timer = null; } if (settings.easing) { this.easingFunction = Helpers.constructEasingFunction(settings.easing, settings.duration); } this.stop = false; let actualDuration = settings.duration! * settings.completion!; // // first step this.animationStep(targetElement, startTransform, endTransform, actualDuration, settings); this.animation_interval_timer = setInterval(()=> { this.animationStep(targetElement, startTransform, endTransform, actualDuration, settings); }, 1); } private animationStep(targetElement:HTMLElement, affine_start:IelementTransform, affine_end:IelementTransform, actualDuration:number,settings:ISettings) { if(!this.started && settings.animationStarted) settings.animationStarted.forEach(i=>i()); this.started = true; var current_time = (new Date()).getTime() - this.animation_start_time!; var time_value; if (this.easingFunction) { time_value = this.easingFunction(current_time / settings.duration!); } else { time_value = current_time / settings.duration!; } var interArrays = Helpers.interpolateArrays(affine_start, affine_end, time_value) as IelementTransform; $(targetElement).css(Helpers.constructZoomRootCssTransform(Helpers.matrixCompose(interArrays))); if (current_time > actualDuration || this.stop) { clearInterval(this.animation_interval_timer!); this.animation_interval_timer = null; time_value = 1.0; this.stop = false; if (settings.animationCompleted) { settings.animationCompleted.forEach(i=>i()); } } } Stop(){ this.stop = true; } // private animateTransition($target, st, et, settings, animateEndCallback, animateStartedCallback) { // if (!st) { // st = affineTransformDecompose(new PureCSSMatrix()); // } // animation_start_time = (new Date()).getTime(); // if (animation_interval_timer) { // clearInterval(animation_interval_timer); // animation_interval_timer = null; // } // if (settings.easing) { // settings.easingfunction = constructEasingFunction(settings.easing, settings.duration); // } // // first step // animationStep($target, st, et, settings, animateEndCallback); // if (animateStartedCallback) { // animateStartedCallback(); // } // animation_interval_timer = setInterval(function () { animationStep($target, st, et, settings, animateEndCallback); }, 1); // } // private animationStep($target, affine_start, affine_end, settings, animateEndCallback) { // var current_time = (new Date()).getTime() - animation_start_time; // var time_value; // if (settings.easingfunction) { // time_value = settings.easingfunction(current_time / settings.duration); // } else { // time_value = current_time / settings.duration; // } // var interArrays = Helpers.interpolateArrays(affine_start, affine_end, time_value); // $target.css(Helpers.constructZoomRootCssTransform(matrixCompose(interArrays))); // if (current_time > settings.duration) { // clearInterval(animation_interval_timer); // animation_interval_timer = null; // time_value = 1.0; // if (animateEndCallback) { // animateEndCallback(interArrays); // } // } // } }