Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transform animation parser fails with eg. transform: 'scale(2,2)' #738

Closed
skycaptainskycaptain opened this issue Apr 4, 2017 · 4 comments · Fixed by #844
Closed

Transform animation parser fails with eg. transform: 'scale(2,2)' #738

skycaptainskycaptain opened this issue Apr 4, 2017 · 4 comments · Fixed by #844
Assignees
Milestone

Comments

@skycaptainskycaptain
Copy link

I posted about this accidentally to nativescript main issue board; since I have zeroed in on the root cause of why certain animations cause the element to disappear.

Here's the original issue I posted with isolated component example for verifying the issue and all other details:
NativeScript/NativeScript#3937

In the comments you can see what I found out to be the cause.

@skycaptainskycaptain
Copy link
Author

Ok the STYLE_TRANSFORMATION_MAP explains why something like transform: "translateX(100px) translateY(100px)" only translates the y, as only using "translateX" or "translateY" defaults the other to 0.

So here as well I would need "translate(100px, 100px)", but that same parsing issue butchers the expression at the comma. So fixing that pretty much makes it all work, then one just needs to use the double value syntax.

@skycaptainskycaptain
Copy link
Author

There's something else seriously wrong. I prototyped a fix which handles each subsequent transform value in a way that it combines already applied values with the new one, which essentially fixes the original issues. So I can write like: transform: "scaleX(2) scaleY(3) translateX(100px) translateY(200px)" and it works.

BUT any initially applied transform causes the element to disappear. I can't attach the debugger in time, so I don't know what happens. If I put there another element I can use to trigger a state change, it will reveal the element, it will animate, and I can make it go back and forth between the states. The code example can be found in this post.

Here's the code that makes my syntax work

I hacked the "nativescript-angular/nativescript-angular/animations/utils.ts", typed directly in the built code - it's not typescript, but JavaScript.

Simple to spot the changes though - what I did was that if the "parseTransformation" reduce argument "transformation.property" already is a "base" version of the property being applied (eg. "transition" and "transitionX"), it will detect it and get the "mirror" value already applied, if applied. So when it applies "transitionX", it passes in "y" value if it was found set already.

var enums_1 = require("ui/enums");
var style_property_1 = require("ui/styling/style-property");
var TRANSFORM_MATCHER = new RegExp(/(.+)\((.+)\)/);
var TRANSFORM_SPLITTER = new RegExp(/[\s,]+/);
var TRANSFORM_PROPERTY_BASE_MATCHER = new RegExp(/(.+)([YX])/);
var TRANSFORM_PARTIAL_APPLICATION_MIRROR = Object.freeze({
    x: 'y',
    y: 'x'
});
var STYLE_TRANSFORMATION_MAP = Object.freeze({
    "scale": function (value) { return ({ property: "scale", value: value }); },
    "scale3d": function (value) { return ({ property: "scale", value: value }); },
    "scaleX": function (value, preset) { return ({ property: "scale", value: { x: value, y: typeof(preset) != "undefined" ? preset : 1 } }); },
    "scaleY": function (value, preset) { return ({ property: "scale", value: { x: typeof(preset) != "undefined" ? preset : 1, y: value } }); },
    "translate": function (value) { return ({ property: "translate", value: value }); },
    "translate3d": function (value) { return ({ property: "translate", value: value }); },
    "translateX": function (value, preset) { return ({ property: "translate", value: { x: value, y: typeof(preset) != "undefined" ? preset : 0 } }); },
    "translateY": function (value, preset) { return ({ property: "translate", value: { x: typeof(preset) != "undefined" ? preset : 0, y: value } }); },
    "rotate": function (value) { return ({ property: "rotate", value: value }); },
    "none": function (_value) { return [
        { property: "scale", value: { x: 1, y: 1 } },
        { property: "translate", value: { x: 0, y: 0 } },
        { property: "rotate", value: 0 },
    ]; },
});
var STYLE_CURVE_MAP = Object.freeze({
    "ease": enums_1.AnimationCurve.ease,
    "linear": enums_1.AnimationCurve.linear,
    "ease-in": enums_1.AnimationCurve.easeIn,
    "ease-out": enums_1.AnimationCurve.easeOut,
    "ease-in-out": enums_1.AnimationCurve.easeInOut,
    "spring": enums_1.AnimationCurve.spring,
});
function getAnimationCurve(value) {
    if (!value) {
        return enums_1.AnimationCurve.ease;
    }
    var curve = STYLE_CURVE_MAP[value];
    if (curve) {
        return curve;
    }
    var _a = TRANSFORM_MATCHER.exec(value) || [], _b = _a[1], property = _b === void 0 ? "" : _b, _c = _a[2], pointsString = _c === void 0 ? "" : _c;
    var coords = pointsString.split(TRANSFORM_SPLITTER).map(stringToBezieCoords);
    if (property !== "cubic-bezier" || coords.length !== 4) {
        throw new Error("Invalid value for animation: " + value);
    }
    else {
        return (_d = enums_1.AnimationCurve).cubicBezier.apply(_d, coords);
    }
    var _d;
}
exports.getAnimationCurve = getAnimationCurve;
function parseAnimationKeyframe(styles) {
    var keyframeInfo = {};
    keyframeInfo.duration = styles["offset"];
    keyframeInfo.declarations = Object.keys(styles).reduce(function (declarations, prop) {
        var value = styles[prop];
        var kebapCaseProp = prop.split(/(?=[A-Z])/).join("-").toLowerCase();
        var property = style_property_1.getPropertyByCssName(kebapCaseProp);
        if (property) {
            if (typeof value === "string" && property.valueConverter) {
                value = property.valueConverter(value);
            }
            declarations.push({ property: property.name, value: value });
        }
        else if (typeof value === "string" && prop === "transform") {
            declarations.push.apply(declarations, parseTransformation(value));
        }
        return declarations;
    }, new Array());
    return keyframeInfo;
}
exports.parseAnimationKeyframe = parseAnimationKeyframe;
function stringToBezieCoords(value) {
    var result = parseFloat(value);
    if (result < 0) {
        return 0;
    }
    else if (result > 1) {
        return 1;
    }
    return result;
}
function parseTransformation(styleString) {
    return parseStyle(styleString)
        .reduce(function (transformations, style) {
            var partiallyAppliedTransformation = getPartiallyAppliedTransformationValue(transformations, style.property);
            var transform = STYLE_TRANSFORMATION_MAP[style.property](style.value,partiallyAppliedTransformation);
            if (Array.isArray(transform)) {
                transformations.push.apply(transformations, transform);
            }
            else if (typeof transform !== "undefined") {
                transformations.push(transform);
            }
            return transformations;
        }, new Array());
}

function getPartiallyAppliedTransformationValue(transformations, property) {
    var transformPropertyInfo = parseTransformPropertyInfo(property);
    if(!transformPropertyInfo) return null;

    var appliedTransformation = transformations.find(function(transformation) {
        return transformation.property == transformPropertyInfo.basePropertyName;
    });

    return appliedTransformation ? appliedTransformation.value[transformPropertyInfo.subPropertyName] : null;
}

function parseTransformPropertyInfo(property) {
    var match = property.match(TRANSFORM_PROPERTY_BASE_MATCHER);
    return match ? {
        basePropertyName: match[1],
        subPropertyName: TRANSFORM_PARTIAL_APPLICATION_MIRROR[match[2].toLowerCase()]
    } : null;
}

function parseStyle(text) {
    return text.split(TRANSFORM_SPLITTER).map(stringToTransformation).filter(function (t) { return !!t; });
}
function stringToTransformation(text) {
    var _a = TRANSFORM_MATCHER.exec(text) || [], _b = _a[1], property = _b === void 0 ? "" : _b, _c = _a[2], stringValue = _c === void 0 ? "" : _c;
    if (!property) {
        return;
    }
    var _d = stringValue.split(",").map(parseFloat), x = _d[0], y = _d[1];
    if (x && y) {
        return { property: property, value: { x: x, y: y } };
    }
    else {
        var value = x;
        if (stringValue.slice(-3) === "rad") {
            value *= 180.0 / Math.PI;
        }
        return { property: property, value: value };
    }
}

You gotta try this example to see my point

If you remove the ":enter" transition, the element is invisible. Click the debug button to trigger the first transition, and you get to toggle it normally. Note: the ":enter" is empty, to demonstrate the bare minimum required to keep the element visible.

In any case, the "inactive" state is not applied initially. Here's the example case:

import {trigger, style, animate, state, transition } from "@angular/animations";
import {Component} from "@angular/core";

@Component({
    selector: "animation-states",
    template: `
        <stack-layout>
            <Button class="btn" text="Touch me!" [@state]=" isOn ? 'active' : 'inactive' " (tap)="onTap()"></Button>
            <button text="Debug button to interact" (tap)="onTap()"></button>
        </stack-layout>`,
    animations: [
        trigger("state", [
            state("inactive", style({ backgroundColor: "blue", transform: "scaleX(0.5) scaleY(0.5) translateX(-10px) translateY(-10px)"})),
            state("active", style({ backgroundColor: "red", transform: "scaleX(2) scaleY(2) translateX(100px) translateY(100px)"})),
            transition(':enter', []),
            transition('inactive => active', animate('600ms ease-in-out')),
            transition('active => inactive', animate('600ms ease-in-out'))
        ])
    ],
    styles: [
        `.btn {
            width: 100px;
            height: 100px;
        }
        `
    ]
})
export class AnimationStatesTest {

    isOn = false;
    onTap() {
        this.isOn = !this.isOn;
    }
}

@NickIliev
Copy link

regarding:

Now I wonder if I should look into the actual source and make a pull request.

@skycaptainskycaptain we greatly appreciate all PRs made from our community members :)
Here is some contribution tips, but I would also recommend getting an opinion from either @vakrilov or @sis0k0 regarding the angular animations.

Ping @sis0k0 , @vakrilov , @PanayotCankov

@sis0k0 sis0k0 added this to the 3.1 TBD milestone May 5, 2017
@sis0k0 sis0k0 added the bug label May 30, 2017
@sis0k0 sis0k0 self-assigned this May 30, 2017
sis0k0 added a commit that referenced this issue Jun 13, 2017
@srnaik
Copy link

srnaik commented Jun 20, 2017

Is there any example related to flip-flop animation in nativescript like below link
FLip-Flop Animation:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants