/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {CompileAnimationAnimateMetadata, CompileAnimationEntryMetadata, CompileAnimationGroupMetadata, CompileAnimationKeyframesSequenceMetadata, CompileAnimationMetadata, CompileAnimationSequenceMetadata, CompileAnimationStateDeclarationMetadata, CompileAnimationStateTransitionMetadata, CompileAnimationStyleMetadata, CompileAnimationWithStepsMetadata} from '../compile_metadata'; import {ListWrapper, StringMapWrapper} from '../facade/collection'; import {NumberWrapper, isArray, isBlank, isPresent, isString, isStringMap} from '../facade/lang'; import {Math} from '../facade/math'; import {ParseError} from '../parse_util'; import {ANY_STATE, AnimationOutput, FILL_STYLE_FLAG} from '../private_import_core'; import {AnimationAst, AnimationEntryAst, AnimationGroupAst, AnimationKeyframeAst, AnimationSequenceAst, AnimationStateDeclarationAst, AnimationStateTransitionAst, AnimationStateTransitionExpression, AnimationStepAst, AnimationStylesAst, AnimationWithStepsAst} from './animation_ast'; import {StylesCollection} from './styles_collection'; const _INITIAL_KEYFRAME = 0; const _TERMINAL_KEYFRAME = 1; const _ONE_SECOND = 1000; export class AnimationParseError extends ParseError { constructor(message: any /** TODO #9100 */) { super(null, message); } toString(): string { return `${this.msg}`; } } export class ParsedAnimationResult { constructor(public ast: AnimationEntryAst, public errors: AnimationParseError[]) {} } export function parseAnimationEntry(entry: CompileAnimationEntryMetadata): ParsedAnimationResult { var errors: AnimationParseError[] = []; var stateStyles: {[key: string]: AnimationStylesAst} = {}; var transitions: CompileAnimationStateTransitionMetadata[] = []; var stateDeclarationAsts: any[] /** TODO #9100 */ = []; entry.definitions.forEach(def => { if (def instanceof CompileAnimationStateDeclarationMetadata) { _parseAnimationDeclarationStates(def, errors).forEach(ast => { stateDeclarationAsts.push(ast); stateStyles[ast.stateName] = ast.styles; }); } else { transitions.push(def); } }); var stateTransitionAsts = transitions.map(transDef => _parseAnimationStateTransition(transDef, stateStyles, errors)); var ast = new AnimationEntryAst(entry.name, stateDeclarationAsts, stateTransitionAsts); return new ParsedAnimationResult(ast, errors); } export function parseAnimationOutputName( outputName: string, errors: AnimationParseError[]): AnimationOutput { var values = outputName.split('.'); var name: string; var phase: string = ''; if (values.length > 1) { name = values[0]; let parsedPhase = values[1]; switch (parsedPhase) { case 'start': case 'done': phase = parsedPhase; break; default: errors.push(new AnimationParseError( `The provided animation output phase value "${parsedPhase}" for "@${name}" is not supported (use start or done)`)); } } else { name = outputName; errors.push(new AnimationParseError( `The animation trigger output event (@${name}) is missing its phase value name (start or done are currently supported)`)); } return new AnimationOutput(name, phase, outputName); } function _parseAnimationDeclarationStates( stateMetadata: CompileAnimationStateDeclarationMetadata, errors: AnimationParseError[]): AnimationStateDeclarationAst[] { var styleValues: {[key: string]: string | number}[] = []; stateMetadata.styles.styles.forEach(stylesEntry => { // TODO (matsko): change this when we get CSS class integration support if (isStringMap(stylesEntry)) { styleValues.push(<{[key: string]: string | number}>stylesEntry); } else { errors.push(new AnimationParseError( `State based animations cannot contain references to other states`)); } }); var defStyles = new AnimationStylesAst(styleValues); var states = stateMetadata.stateNameExpr.split(/\s*,\s*/); return states.map(state => new AnimationStateDeclarationAst(state, defStyles)); } function _parseAnimationStateTransition( transitionStateMetadata: CompileAnimationStateTransitionMetadata, stateStyles: {[key: string]: AnimationStylesAst}, errors: AnimationParseError[]): AnimationStateTransitionAst { var styles = new StylesCollection(); var transitionExprs: any[] /** TODO #9100 */ = []; var transitionStates = transitionStateMetadata.stateChangeExpr.split(/\s*,\s*/); transitionStates.forEach(expr => { _parseAnimationTransitionExpr(expr, errors).forEach(transExpr => { transitionExprs.push(transExpr); }); }); var entry = _normalizeAnimationEntry(transitionStateMetadata.steps); var animation = _normalizeStyleSteps(entry, stateStyles, errors); var animationAst = _parseTransitionAnimation(animation, 0, styles, stateStyles, errors); if (errors.length == 0) { _fillAnimationAstStartingKeyframes(animationAst, styles, errors); } var stepsAst: AnimationWithStepsAst = (animationAst instanceof AnimationWithStepsAst) ? animationAst : new AnimationSequenceAst([animationAst]); return new AnimationStateTransitionAst(transitionExprs, stepsAst); } function _parseAnimationTransitionExpr( eventStr: string, errors: AnimationParseError[]): AnimationStateTransitionExpression[] { var expressions: any[] /** TODO #9100 */ = []; var match = eventStr.match(/^(\*|[-\w]+)\s*()\s*(\*|[-\w]+)$/); if (!isPresent(match) || match.length < 4) { errors.push(new AnimationParseError(`the provided ${eventStr} is not of a supported format`)); return expressions; } var fromState = match[1]; var separator = match[2]; var toState = match[3]; expressions.push(new AnimationStateTransitionExpression(fromState, toState)); var isFullAnyStateExpr = fromState == ANY_STATE && toState == ANY_STATE; if (separator[0] == '<' && !isFullAnyStateExpr) { expressions.push(new AnimationStateTransitionExpression(toState, fromState)); } return expressions; } function _fetchSylesFromState(stateName: string, stateStyles: {[key: string]: AnimationStylesAst}): CompileAnimationStyleMetadata { var entry = stateStyles[stateName]; if (isPresent(entry)) { var styles = <{[key: string]: string | number}[]>entry.styles; return new CompileAnimationStyleMetadata(0, styles); } return null; } function _normalizeAnimationEntry(entry: CompileAnimationMetadata | CompileAnimationMetadata[]): CompileAnimationMetadata { return isArray(entry) ? new CompileAnimationSequenceMetadata(entry) : entry; } function _normalizeStyleMetadata( entry: CompileAnimationStyleMetadata, stateStyles: {[key: string]: AnimationStylesAst}, errors: AnimationParseError[]): Array<{[key: string]: string | number}> { var normalizedStyles: any[] /** TODO #9100 */ = []; entry.styles.forEach(styleEntry => { if (isString(styleEntry)) { ListWrapper.addAll( normalizedStyles, _resolveStylesFromState(styleEntry, stateStyles, errors)); } else { normalizedStyles.push(<{[key: string]: string | number}>styleEntry); } }); return normalizedStyles; } function _normalizeStyleSteps( entry: CompileAnimationMetadata, stateStyles: {[key: string]: AnimationStylesAst}, errors: AnimationParseError[]): CompileAnimationMetadata { var steps = _normalizeStyleStepEntry(entry, stateStyles, errors); return (entry instanceof CompileAnimationGroupMetadata) ? new CompileAnimationGroupMetadata(steps) : new CompileAnimationSequenceMetadata(steps); } function _mergeAnimationStyles( stylesList: any[], newItem: {[key: string]: string | number} | string) { if (isStringMap(newItem) && stylesList.length > 0) { var lastIndex = stylesList.length - 1; var lastItem = stylesList[lastIndex]; if (isStringMap(lastItem)) { stylesList[lastIndex] = StringMapWrapper.merge( <{[key: string]: string | number}>lastItem, <{[key: string]: string | number}>newItem); return; } } stylesList.push(newItem); } function _normalizeStyleStepEntry( entry: CompileAnimationMetadata, stateStyles: {[key: string]: AnimationStylesAst}, errors: AnimationParseError[]): CompileAnimationMetadata[] { var steps: CompileAnimationMetadata[]; if (entry instanceof CompileAnimationWithStepsMetadata) { steps = entry.steps; } else { return [entry]; } var newSteps: CompileAnimationMetadata[] = []; var combinedStyles: {[key: string]: string | number}[]; steps.forEach(step => { if (step instanceof CompileAnimationStyleMetadata) { // this occurs when a style step is followed by a previous style step // or when the first style step is run. We want to concatenate all subsequent // style steps together into a single style step such that we have the correct // starting keyframe data to pass into the animation player. if (!isPresent(combinedStyles)) { combinedStyles = []; } _normalizeStyleMetadata(step, stateStyles, errors) .forEach(entry => { _mergeAnimationStyles(combinedStyles, entry); }); } else { // it is important that we create a metadata entry of the combined styles // before we go on an process the animate, sequence or group metadata steps. // This will ensure that the AST will have the previous styles painted on // screen before any further animations that use the styles take place. if (isPresent(combinedStyles)) { newSteps.push(new CompileAnimationStyleMetadata(0, combinedStyles)); combinedStyles = null; } if (step instanceof CompileAnimationAnimateMetadata) { // we do not recurse into CompileAnimationAnimateMetadata since // those style steps are not going to be squashed var animateStyleValue = (step).styles; if (animateStyleValue instanceof CompileAnimationStyleMetadata) { animateStyleValue.styles = _normalizeStyleMetadata(animateStyleValue, stateStyles, errors); } else if (animateStyleValue instanceof CompileAnimationKeyframesSequenceMetadata) { animateStyleValue.steps.forEach( step => { step.styles = _normalizeStyleMetadata(step, stateStyles, errors); }); } } else if (step instanceof CompileAnimationWithStepsMetadata) { let innerSteps = _normalizeStyleStepEntry(step, stateStyles, errors); step = step instanceof CompileAnimationGroupMetadata ? new CompileAnimationGroupMetadata(innerSteps) : new CompileAnimationSequenceMetadata(innerSteps); } newSteps.push(step); } }); // this happens when only styles were animated within the sequence if (isPresent(combinedStyles)) { newSteps.push(new CompileAnimationStyleMetadata(0, combinedStyles)); } return newSteps; } function _resolveStylesFromState( stateName: string, stateStyles: {[key: string]: AnimationStylesAst}, errors: AnimationParseError[]) { var styles: {[key: string]: string | number}[] = []; if (stateName[0] != ':') { errors.push(new AnimationParseError(`Animation states via styles must be prefixed with a ":"`)); } else { var normalizedStateName = stateName.substring(1); var value = stateStyles[normalizedStateName]; if (!isPresent(value)) { errors.push(new AnimationParseError( `Unable to apply styles due to missing a state: "${normalizedStateName}"`)); } else { value.styles.forEach(stylesEntry => { if (isStringMap(stylesEntry)) { styles.push(<{[key: string]: string | number}>stylesEntry); } }); } } return styles; } class _AnimationTimings { constructor(public duration: number, public delay: number, public easing: string) {} } function _parseAnimationKeyframes( keyframeSequence: CompileAnimationKeyframesSequenceMetadata, currentTime: number, collectedStyles: StylesCollection, stateStyles: {[key: string]: AnimationStylesAst}, errors: AnimationParseError[]): AnimationKeyframeAst[] { var totalEntries = keyframeSequence.steps.length; var totalOffsets = 0; keyframeSequence.steps.forEach(step => totalOffsets += (isPresent(step.offset) ? 1 : 0)); if (totalOffsets > 0 && totalOffsets < totalEntries) { errors.push(new AnimationParseError( `Not all style() entries contain an offset for the provided keyframe()`)); totalOffsets = totalEntries; } var limit = totalEntries - 1; var margin = totalOffsets == 0 ? (1 / limit) : 0; var rawKeyframes: any[] /** TODO #9100 */ = []; var index = 0; var doSortKeyframes = false; var lastOffset = 0; keyframeSequence.steps.forEach(styleMetadata => { var offset = styleMetadata.offset; var keyframeStyles: {[key: string]: string | number} = {}; styleMetadata.styles.forEach(entry => { StringMapWrapper.forEach( <{[key: string]: string | number}>entry, (value: any /** TODO #9100 */, prop: any /** TODO #9100 */) => { if (prop != 'offset') { keyframeStyles[prop] = value; } }); }); if (isPresent(offset)) { doSortKeyframes = doSortKeyframes || (offset < lastOffset); } else { offset = index == limit ? _TERMINAL_KEYFRAME : (margin * index); } rawKeyframes.push([offset, keyframeStyles]); lastOffset = offset; index++; }); if (doSortKeyframes) { ListWrapper.sort(rawKeyframes, (a, b) => a[0] <= b[0] ? -1 : 1); } var i: any /** TODO #9100 */; var firstKeyframe = rawKeyframes[0]; if (firstKeyframe[0] != _INITIAL_KEYFRAME) { ListWrapper.insert(rawKeyframes, 0, firstKeyframe = [_INITIAL_KEYFRAME, {}]); } var firstKeyframeStyles = firstKeyframe[1]; limit = rawKeyframes.length - 1; var lastKeyframe = rawKeyframes[limit]; if (lastKeyframe[0] != _TERMINAL_KEYFRAME) { rawKeyframes.push(lastKeyframe = [_TERMINAL_KEYFRAME, {}]); limit++; } var lastKeyframeStyles = lastKeyframe[1]; for (i = 1; i <= limit; i++) { let entry = rawKeyframes[i]; let styles = entry[1]; StringMapWrapper.forEach( styles, (value: any /** TODO #9100 */, prop: any /** TODO #9100 */) => { if (!isPresent(firstKeyframeStyles[prop])) { firstKeyframeStyles[prop] = FILL_STYLE_FLAG; } }); } for (i = limit - 1; i >= 0; i--) { let entry = rawKeyframes[i]; let styles = entry[1]; StringMapWrapper.forEach( styles, (value: any /** TODO #9100 */, prop: any /** TODO #9100 */) => { if (!isPresent(lastKeyframeStyles[prop])) { lastKeyframeStyles[prop] = value; } }); } return rawKeyframes.map( entry => new AnimationKeyframeAst(entry[0], new AnimationStylesAst([entry[1]]))); } function _parseTransitionAnimation( entry: CompileAnimationMetadata, currentTime: number, collectedStyles: StylesCollection, stateStyles: {[key: string]: AnimationStylesAst}, errors: AnimationParseError[]): AnimationAst { var ast: any /** TODO #9100 */; var playTime = 0; var startingTime = currentTime; if (entry instanceof CompileAnimationWithStepsMetadata) { var maxDuration = 0; var steps: any[] /** TODO #9100 */ = []; var isGroup = entry instanceof CompileAnimationGroupMetadata; var previousStyles: any /** TODO #9100 */; entry.steps.forEach(entry => { // these will get picked up by the next step... var time = isGroup ? startingTime : currentTime; if (entry instanceof CompileAnimationStyleMetadata) { entry.styles.forEach(stylesEntry => { // by this point we know that we only have stringmap values var map = <{[key: string]: string | number}>stylesEntry; StringMapWrapper.forEach( map, (value: any /** TODO #9100 */, prop: any /** TODO #9100 */) => { collectedStyles.insertAtTime(prop, time, value); }); }); previousStyles = entry.styles; return; } var innerAst = _parseTransitionAnimation(entry, time, collectedStyles, stateStyles, errors); if (isPresent(previousStyles)) { if (entry instanceof CompileAnimationWithStepsMetadata) { let startingStyles = new AnimationStylesAst(previousStyles); steps.push(new AnimationStepAst(startingStyles, [], 0, 0, '')); } else { var innerStep = innerAst; ListWrapper.addAll(innerStep.startingStyles.styles, previousStyles); } previousStyles = null; } var astDuration = innerAst.playTime; currentTime += astDuration; playTime += astDuration; maxDuration = Math.max(astDuration, maxDuration); steps.push(innerAst); }); if (isPresent(previousStyles)) { let startingStyles = new AnimationStylesAst(previousStyles); steps.push(new AnimationStepAst(startingStyles, [], 0, 0, '')); } if (isGroup) { ast = new AnimationGroupAst(steps); playTime = maxDuration; currentTime = startingTime + playTime; } else { ast = new AnimationSequenceAst(steps); } } else if (entry instanceof CompileAnimationAnimateMetadata) { var timings = _parseTimeExpression(entry.timings, errors); var styles = entry.styles; var keyframes: any /** TODO #9100 */; if (styles instanceof CompileAnimationKeyframesSequenceMetadata) { keyframes = _parseAnimationKeyframes(styles, currentTime, collectedStyles, stateStyles, errors); } else { let styleData = styles; let offset = _TERMINAL_KEYFRAME; let styleAst = new AnimationStylesAst(<{[key: string]: string | number}[]>styleData.styles); var keyframe = new AnimationKeyframeAst(offset, styleAst); keyframes = [keyframe]; } ast = new AnimationStepAst( new AnimationStylesAst([]), keyframes, timings.duration, timings.delay, timings.easing); playTime = timings.duration + timings.delay; currentTime += playTime; keyframes.forEach( (keyframe: any /** TODO #9100 */) => keyframe.styles.styles.forEach( (entry: any /** TODO #9100 */) => StringMapWrapper.forEach( entry, (value: any /** TODO #9100 */, prop: any /** TODO #9100 */) => collectedStyles.insertAtTime(prop, currentTime, value)))); } else { // if the code reaches this stage then an error // has already been populated within the _normalizeStyleSteps() // operation... ast = new AnimationStepAst(null, [], 0, 0, ''); } ast.playTime = playTime; ast.startTime = startingTime; return ast; } function _fillAnimationAstStartingKeyframes( ast: AnimationAst, collectedStyles: StylesCollection, errors: AnimationParseError[]): void { // steps that only contain style will not be filled if ((ast instanceof AnimationStepAst) && ast.keyframes.length > 0) { var keyframes = ast.keyframes; if (keyframes.length == 1) { var endKeyframe = keyframes[0]; var startKeyframe = _createStartKeyframeFromEndKeyframe( endKeyframe, ast.startTime, ast.playTime, collectedStyles, errors); ast.keyframes = [startKeyframe, endKeyframe]; } } else if (ast instanceof AnimationWithStepsAst) { ast.steps.forEach(entry => _fillAnimationAstStartingKeyframes(entry, collectedStyles, errors)); } } function _parseTimeExpression( exp: string | number, errors: AnimationParseError[]): _AnimationTimings { var regex = /^([\.\d]+)(m?s)(?:\s+([\.\d]+)(m?s))?(?:\s+([-a-z]+(?:\(.+?\))?))?/i; var duration: number; var delay: number = 0; var easing: string = null; if (isString(exp)) { const matches = exp.match(regex); if (matches === null) { errors.push(new AnimationParseError(`The provided timing value "${exp}" is invalid.`)); return new _AnimationTimings(0, 0, null); } var durationMatch = NumberWrapper.parseFloat(matches[1]); var durationUnit = matches[2]; if (durationUnit == 's') { durationMatch *= _ONE_SECOND; } duration = Math.floor(durationMatch); var delayMatch = matches[3]; var delayUnit = matches[4]; if (isPresent(delayMatch)) { var delayVal: number = NumberWrapper.parseFloat(delayMatch); if (isPresent(delayUnit) && delayUnit == 's') { delayVal *= _ONE_SECOND; } delay = Math.floor(delayVal); } var easingVal = matches[5]; if (!isBlank(easingVal)) { easing = easingVal; } } else { duration = exp; } return new _AnimationTimings(duration, delay, easing); } function _createStartKeyframeFromEndKeyframe( endKeyframe: AnimationKeyframeAst, startTime: number, duration: number, collectedStyles: StylesCollection, errors: AnimationParseError[]): AnimationKeyframeAst { var values: {[key: string]: string | number} = {}; var endTime = startTime + duration; endKeyframe.styles.styles.forEach((styleData: {[key: string]: string | number}) => { StringMapWrapper.forEach(styleData, (val: any /** TODO #9100 */, prop: any /** TODO #9100 */) => { if (prop == 'offset') return; var resultIndex = collectedStyles.indexOfAtOrBeforeTime(prop, startTime); var resultEntry: any /** TODO #9100 */, nextEntry: any /** TODO #9100 */, value: any /** TODO #9100 */; if (isPresent(resultIndex)) { resultEntry = collectedStyles.getByIndex(prop, resultIndex); value = resultEntry.value; nextEntry = collectedStyles.getByIndex(prop, resultIndex + 1); } else { // this is a flag that the runtime code uses to pass // in a value either from the state declaration styles // or using the AUTO_STYLE value (e.g. getComputedStyle) value = FILL_STYLE_FLAG; } if (isPresent(nextEntry) && !nextEntry.matches(endTime, val)) { errors.push(new AnimationParseError( `The animated CSS property "${prop}" unexpectedly changes between steps "${resultEntry.time}ms" and "${endTime}ms" at "${nextEntry.time}ms"`)); } values[prop] = value; }); }); return new AnimationKeyframeAst(_INITIAL_KEYFRAME, new AnimationStylesAst([values])); }