All Downloads are FREE. Search and download functionalities are using the official Maven repository.

package.src.svg.cssAnimation.ts Maven / Gradle / Ivy

The newest version!
import Transformable, { copyTransform } from '../core/Transformable';
import Displayable from '../graphic/Displayable';
import { SVGVNodeAttrs, BrushScope, createBrushScope} from './core';
import Path from '../graphic/Path';
import SVGPathRebuilder from './SVGPathRebuilder';
import PathProxy from '../core/PathProxy';
import { getPathPrecision, getSRTTransformString } from './helper';
import { each, extend, filter, isNumber, isString, keys } from '../core/util';
import Animator from '../animation/Animator';
import CompoundPath from '../graphic/CompoundPath';
import { AnimationEasing } from '../animation/easing';
import { createCubicEasingFunc } from '../animation/cubicEasing';
import { getClassId } from './cssClassId';

export const EASING_MAP: Record = {
    // From https://easings.net/
    cubicIn: '0.32,0,0.67,0',
    cubicOut: '0.33,1,0.68,1',
    cubicInOut: '0.65,0,0.35,1',
    quadraticIn: '0.11,0,0.5,0',
    quadraticOut: '0.5,1,0.89,1',
    quadraticInOut: '0.45,0,0.55,1',
    quarticIn: '0.5,0,0.75,0',
    quarticOut: '0.25,1,0.5,1',
    quarticInOut: '0.76,0,0.24,1',
    quinticIn: '0.64,0,0.78,0',
    quinticOut: '0.22,1,0.36,1',
    quinticInOut: '0.83,0,0.17,1',
    sinusoidalIn: '0.12,0,0.39,0',
    sinusoidalOut: '0.61,1,0.88,1',
    sinusoidalInOut: '0.37,0,0.63,1',
    exponentialIn: '0.7,0,0.84,0',
    exponentialOut: '0.16,1,0.3,1',
    exponentialInOut: '0.87,0,0.13,1',
    circularIn: '0.55,0,1,0.45',
    circularOut: '0,0.55,0.45,1',
    circularInOut: '0.85,0,0.15,1'
    // TODO elastic, bounce
};

const transformOriginKey = 'transform-origin';

function buildPathString(el: Path, kfShape: Path['shape'], path: PathProxy) {
    const shape = extend({}, el.shape);
    extend(shape, kfShape);

    el.buildPath(path, shape);
    const svgPathBuilder = new SVGPathRebuilder();
    svgPathBuilder.reset(getPathPrecision(el));
    path.rebuildPath(svgPathBuilder, 1);
    svgPathBuilder.generateStr();
    // will add path("") when generated to css string in the final step.
    return svgPathBuilder.getStr();
}

function setTransformOrigin(target: Record, transform: Transformable) {
    const {originX, originY} = transform;
    if (originX || originY) {
        target[transformOriginKey] = `${originX}px ${originY}px`;
    }
}

export const ANIMATE_STYLE_MAP: Record = {
    fill: 'fill',
    opacity: 'opacity',
    lineWidth: 'stroke-width',
    lineDashOffset: 'stroke-dashoffset'
    // TODO shadow is not supported.
};

type CssKF = Record;

function addAnimation(cssAnim: Record, scope: BrushScope) {
    const animationName = scope.zrId + '-ani-' + scope.cssAnimIdx++;
    scope.cssAnims[animationName] = cssAnim;
    return animationName;
}

function createCompoundPathCSSAnimation(
    el: CompoundPath,
    attrs: SVGVNodeAttrs,
    scope: BrushScope
) {
    const paths = el.shape.paths;
    const composedAnim: Record = {};
    let cssAnimationCfg: string;
    let cssAnimationName: string;
    each(paths, path => {
        const subScope = createBrushScope(scope.zrId);
        subScope.animation = true;
        createCSSAnimation(path, {}, subScope, true);
        const cssAnims = subScope.cssAnims;
        const cssNodes = subScope.cssNodes;
        const animNames = keys(cssAnims);
        const len = animNames.length;
        if (!len) {
            return;
        }
        cssAnimationName = animNames[len - 1];
        // Only use last animation because they are conflicted.
        const lastAnim = cssAnims[cssAnimationName];
        // eslint-disable-next-line
        for (let percent in lastAnim) {
            const kf = lastAnim[percent];
            composedAnim[percent] = composedAnim[percent] || { d: '' };
            composedAnim[percent].d += kf.d || '';
        }
        // eslint-disable-next-line
        for (let className in cssNodes) {
            const val = cssNodes[className].animation;
            if (val.indexOf(cssAnimationName) >= 0) {
                // Only pick the animation configuration of last subpath.
                cssAnimationCfg = val;
            }
        }
    });

    if (!cssAnimationCfg) {
        return;
    }

    // Remove the attrs in the element because it will be set by animation.
    // Reduce the size.
    attrs.d = false;
    const animationName = addAnimation(composedAnim, scope);
    return cssAnimationCfg.replace(cssAnimationName, animationName);
}

function getEasingFunc(easing: AnimationEasing) {
    return isString(easing)
        ? EASING_MAP[easing]
            ? `cubic-bezier(${EASING_MAP[easing]})`
            : createCubicEasingFunc(easing) ? easing : ''
        : '';
}

export function createCSSAnimation(
    el: Displayable,
    attrs: SVGVNodeAttrs,
    scope: BrushScope,
    onlyShape?: boolean
) {
    const animators = el.animators;
    const len = animators.length;

    const cssAnimations: string[] = [];

    if (el instanceof CompoundPath) {
        const animationCfg = createCompoundPathCSSAnimation(el, attrs, scope);
        if (animationCfg) {
            cssAnimations.push(animationCfg);
        }
        else if (!len) {
            return;
        }
    }
    else if (!len) {
        return;
    }
    // Group animators by it's configuration
    const groupAnimators: Record[]]> = {};
    for (let i = 0; i < len; i++) {
        const animator = animators[i];
        const cfgArr: (string | number)[] = [animator.getMaxTime() / 1000 + 's'];
        const easing = getEasingFunc(animator.getClip().easing);
        const delay = animator.getDelay();

        if (easing) {
            cfgArr.push(easing);
        }
        else {
            cfgArr.push('linear');
        }
        if (delay) {
            cfgArr.push(delay / 1000 + 's');
        }
        if (animator.getLoop()) {
            cfgArr.push('infinite');
        }
        const cfg = cfgArr.join(' ');

        // TODO fill mode
        groupAnimators[cfg] = groupAnimators[cfg] || [cfg, [] as Animator[]];
        groupAnimators[cfg][1].push(animator);
    }

    function createSingleCSSAnimation(groupAnimator: [string, Animator[]]) {
        const animators = groupAnimator[1];
        const len = animators.length;
        const transformKfs: Record = {};
        const shapeKfs: Record = {};

        const finalKfs: Record = {};

        const animationTimingFunctionAttrName = 'animation-timing-function';

        function saveAnimatorTrackToCssKfs(
            animator: Animator,
            cssKfs: Record,
            toCssAttrName?: (propName: string) => string
        ) {
            const tracks = animator.getTracks();
            const maxTime = animator.getMaxTime();
            for (let k = 0; k < tracks.length; k++) {
                const track = tracks[k];
                if (track.needsAnimate()) {
                    const kfs = track.keyframes;
                    let attrName = track.propName;
                    toCssAttrName && (attrName = toCssAttrName(attrName));
                    if (attrName) {
                        for (let i = 0; i < kfs.length; i++) {
                            const kf = kfs[i];
                            const percent = Math.round(kf.time / maxTime * 100) + '%';
                            const kfEasing = getEasingFunc(kf.easing);
                            const rawValue = kf.rawValue;

                            // TODO gradient
                            if (isString(rawValue) || isNumber(rawValue)) {
                                cssKfs[percent] = cssKfs[percent] || {};
                                cssKfs[percent][attrName] = kf.rawValue;

                                if (kfEasing) {
                                    // TODO. If different property have different easings.
                                    cssKfs[percent][animationTimingFunctionAttrName] = kfEasing;
                                }
                            }
                        }
                    }
                }
            }
        }

        // Find all transform animations.
        // TODO origin, parent
        for (let i = 0; i < len; i++) {
            const animator = animators[i];
            const targetProp = animator.targetName;
            if (!targetProp) {
                !onlyShape && saveAnimatorTrackToCssKfs(animator, transformKfs);
            }
            else if (targetProp === 'shape') {
                saveAnimatorTrackToCssKfs(animator, shapeKfs);
            }
        }

        // eslint-disable-next-line
        for (let percent in transformKfs) {
            const transform = {} as Transformable;
            copyTransform(transform, el);
            extend(transform, transformKfs[percent]);
            const str = getSRTTransformString(transform);
            const timingFunction = transformKfs[percent][animationTimingFunctionAttrName];
            finalKfs[percent] = str ? {
                transform: str
            } : {};
            // TODO set transform origin in element?
            setTransformOrigin(finalKfs[percent], transform);

            // Save timing function
            if (timingFunction) {
                finalKfs[percent][animationTimingFunctionAttrName] = timingFunction;
            }
        };


        let path: PathProxy;
        let canAnimateShape = true;
        // eslint-disable-next-line
        for (let percent in shapeKfs) {
            finalKfs[percent] = finalKfs[percent] || {};

            const isFirst = !path;
            const timingFunction = shapeKfs[percent][animationTimingFunctionAttrName];

            if (isFirst) {
                path = new PathProxy();
            }
            let len = path.len();
            path.reset();
            finalKfs[percent].d = buildPathString(el as Path, shapeKfs[percent], path);
            let newLen = path.len();
            // Path data don't match.
            if (!isFirst && len !== newLen) {
                canAnimateShape = false;
                break;
            }

            // Save timing function
            if (timingFunction) {
                finalKfs[percent][animationTimingFunctionAttrName] = timingFunction;
            }
        };
        if (!canAnimateShape) {
            // eslint-disable-next-line
            for (let percent in finalKfs) {
                delete finalKfs[percent].d;
            }
        }

        if (!onlyShape) {
            for (let i = 0; i < len; i++) {
                const animator = animators[i];
                const targetProp = animator.targetName;
                if (targetProp === 'style') {
                    saveAnimatorTrackToCssKfs(
                        animator, finalKfs, (propName) => ANIMATE_STYLE_MAP[propName]
                    );
                }
            }
        }

        const percents = keys(finalKfs);

        // Set transform origin in attribute to reduce the size.
        let allTransformOriginSame = true;
        let transformOrigin;
        for (let i = 1; i < percents.length; i++) {
            const p0 = percents[i - 1];
            const p1 = percents[i];
            if (finalKfs[p0][transformOriginKey] !== finalKfs[p1][transformOriginKey]) {
                allTransformOriginSame = false;
                break;
            }
            transformOrigin = finalKfs[p0][transformOriginKey];
        }
        if (allTransformOriginSame && transformOrigin) {
            for (const percent in finalKfs) {
                if (finalKfs[percent][transformOriginKey]) {
                    delete finalKfs[percent][transformOriginKey];
                }
            }
            attrs[transformOriginKey] = transformOrigin;
        }

        if (filter(
            percents, (percent) => keys(finalKfs[percent]).length > 0
        ).length) {
            const animationName = addAnimation(finalKfs, scope);
            // eslint-disable-next-line
            // for (const attrName in finalKfs[percents[0]]) {
            //     // Remove the attrs in the element because it will be set by animation.
            //     // Reduce the size.
            //     attrs[attrName] = false;
            // }
            // animationName {duration easing delay loop} fillMode
            return `${animationName} ${groupAnimator[0]} both`;
        }
    }

    // eslint-disable-next-line
    for (let key in groupAnimators) {
        const animationCfg = createSingleCSSAnimation(groupAnimators[key]);
        if (animationCfg) {
            cssAnimations.push(animationCfg);
        }
    }

    if (cssAnimations.length) {
        const className = scope.zrId + '-cls-' + getClassId();
        scope.cssNodes['.' + className] = {
            animation: cssAnimations.join(',')
        };
        // TODO exists class?
        attrs.class = className;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy