Skip to content

Support for basic shape circle % #73

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added examples/Nyancat.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions examples/basicShapeCirclePath.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!-- <!DOCTYPE html>
<script src="../third_party/web-animations-js/web-animations.dev.js"></script>
<script src="../motion-path-polyfill.min.js"></script>

<div id="container">
<div id="target">
<img src="Nyancat.png" height="10%" width="10%">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kill the cat

</div>
</div>

<style>
body {
margin: 0px;
}

#container {
height: 100px;
width: 100px;
}
#target {
position: absolute;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think we need this.

}

</style>

<script>
var keyframes = [ {offsetPath: 'circle(100px at 500px 500px)', offsetDistance: '0%'},
{offsetPath: 'circle(100px at 500px 500px)', offsetDistance: '100%'}];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The demo should test percentages. I suspect it will fail given the dependency you have on the element during initial keyframe parsing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although that issue is currently hidden by the soon to be removed startKeyframe == endKeyframe optimisation.

var timing = {duration: 20000, iterations: 3};
target.animate(keyframes, timing);
</script> -->
138 changes: 118 additions & 20 deletions src/offsetPath.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
'use strict';

(function () {
var isNumeric = internalScope.isNumeric;

function roundToHundredth (number) {
return Math.round(number * 100) / 100;
}

function basicShapePolygonParse (input, element) {
// TODO: Support the fill-rule option and %
var argumentList = input.split(',');
Expand Down Expand Up @@ -50,37 +56,127 @@
return {type: 'path', path: path};
}

function basicShapeCircleParse (input, element) {
// TODO: Need element as an argument to this function
var radius;
var position = /at (.*?)$/.exec(input);
function getCirclePathPosition (parentProperties, position) {
var analysedPosition = [];

if (position !== null) {
var positionList = position[1].split(/\s+/);

// If only one value is specified, the second value is assumed to be 'center'
// https://drafts.csswg.org/css-backgrounds-3/#position
for (var index in positionList) {
var aPosition = positionList[index];

var aPositionUnit = /(%|px)$/.exec(aPosition)[1];
if (aPositionUnit === null) {
return null;
}

var aPositionValueString = aPosition.substring(0, aPosition.length - aPositionUnit.length);
if (!isNumeric(aPositionValueString) || aPositionValueString === '') {
return null;
}

var aPositionValue = Number(aPositionValueString);
if (aPositionUnit === '%') {
if (Number(index) === 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To get rid of magic numbers you could possibly set a boolean flag outside of the for loop. E.g var isWidth = true

if (!parentProperties || !parentProperties.width) {
return null;
}
aPositionValue *= (parentProperties.width / 100);
}
if (Number(index) === 1) {
if (!parentProperties || !parentProperties.height) {
return null;
}
aPositionValue *= (parentProperties.height / 100);
}
}
analysedPosition[index] = aPositionValue;
}
}

// TODO: Need to support other positions as currently this only supports positions in which both x and y are specified and are in px
if (analysedPosition.length < 2) {
for (var i = analysedPosition.length; i < 2; i++) {
if (Number(i) === 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could maybe use the bool flag idea here again

if (!parentProperties || !parentProperties.width) {
return null;
}
analysedPosition[i] = parentProperties.width / 2;
} else if (Number(i) === 1) {
if (!parentProperties || !parentProperties.height) {
return null;
}
analysedPosition[i] = parentProperties.height / 2;
}
}
}

return analysedPosition;
}

function getCirclePathRadius (parentProperties, position, input) {
var radiusString;
if (position === null) {
// TODO: Set default position to the center of the reference box
position = '0px 0px';
if (input !== '') {
radius = input;
radiusString = input;
}
} else {
position = position[1].split(/\s+/);
radius = (/^(.*?) at/.exec(input));
if (radius === null) {
radius = 'closest-side';
radiusString = (/^(.*?) at/.exec(input));
// TODO: Add support for when a radius had not been specified
if (radiusString === null) {
radiusString = 'closest-side';
} else {
radius = radius[1];
radiusString = radiusString[1];
}
}

radius = Number(radius.substring(0, radius.length - 2));
var radiusUnit = /(%|px)$/.exec(radiusString);
if (radiusUnit === null) {
return null;
}

var radiusValueString = radiusString.substring(0, radiusString.length - radiusUnit[1].length);
if (!isNumeric(radiusValueString)) {
return null;
}
var radiusValue = Number(radiusValueString);

if (radiusUnit[1] === '%') {
var height = parentProperties.height;
var width = parentProperties.width;
if (!height || !width) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should just be checking !parentProperties.

return null;
}

var positionX = Number(position[0].substring(0, position[0].length - 2));
var positionY = Number(position[1].substring(0, position[1].length - 2));
return roundToHundredth((Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)) / Math.sqrt(2)) * radiusValue / 100);
}

return Number(radiusValueString);
}

function basicShapeCircleParse (input, element) {
var parentProperties = null;
if (element) {
parentProperties = element.offsetParent ? element.offsetParent.getBoundingClientRect() : null;
}

var position = /at (.*?)$/.exec(input);

var radiusValue = getCirclePathRadius(parentProperties, position, input);
if (!radiusValue) {
return undefined;
}

var analysedPosition = getCirclePathPosition(parentProperties, position);
if (!analysedPosition) {
return undefined;
}

var pathString = 'M ' + positionX + ' ' + positionY +
' m 0,' + (-radius) +
' a ' + radius + ',' + radius + ' 0 0,1 ' + radius + ',' + radius +
' a ' + radius + ',' + radius + ' 0 1,1 ' + (-radius) + ',' + (-radius) + ' z';
var pathString = 'M ' + analysedPosition[0] + ' ' + analysedPosition[1] +
' m 0,' + (-radiusValue) +
' a ' + radiusValue + ',' + radiusValue + ' 0 0,1 ' + radiusValue + ',' + radiusValue +
' a ' + radiusValue + ',' + radiusValue + ' 0 1,1 ' + (-radiusValue) + ',' + (-radiusValue) + ' z';

return {type: 'path', path: pathString};
}
Expand Down Expand Up @@ -166,6 +262,7 @@
return undefined;
}
var toParse = [basicShapePolygonParse, basicShapeCircleParse, basicShapeInsetParse, basicShapeEllipseParse];
// var toParse = [basicShapeCircleParse];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed anymore 😄

for (var i = 0; i < toParse.length; i++) {
var result = toParse[i](shapeArguments[1], element);
if (result) {
Expand Down Expand Up @@ -228,4 +325,5 @@

internalScope.offsetPathParse = offsetPathParse;
internalScope.offsetPathMerge = offsetPathMerge;
internalScope.roundToHundredth = roundToHundredth;
})();
6 changes: 2 additions & 4 deletions src/toTransform.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
'use strict';

(function () {
var roundToHundredth = internalScope.roundToHundredth;

function convertTranslate (input) {
var valuesArray = internalScope.translateParse(input);

Expand Down Expand Up @@ -88,10 +90,6 @@
return offsetDistanceLength;
}

function roundToHundredth (number) {
return Math.round(number * 100) / 100;
}

function convertPathString (properties) {
var offsetPath = internalScope.offsetPathParse(properties['offsetPath']);
var closedLoop = isClosedLoop(offsetPath);
Expand Down
78 changes: 55 additions & 23 deletions test/pathBasicShapeCircleTest.js
Original file line number Diff line number Diff line change
@@ -1,65 +1,97 @@
/* global suite test internalScope */
/* global assert suite test internalScope */

(function () {
suite('offsetPath', function () {
test('basicShapeCircle', function () {
var assertTransformInterpolation = internalScope.assertTransformInterpolation;
(function () {
suite('offsetPath', function () {
test('basicShapeCircle', function () {
var assertTransformInterpolation = internalScope.assertTransformInterpolation;
var offsetPathParse = internalScope.offsetPathParse;

assertTransformInterpolation([
var containerStyle = {
position: 'absolute',
height: '100px',
width: '200px'
};

var container = document.createElement('div');

for (var property in containerStyle) {
container.style[property] = containerStyle[property];
}

var target = document.createElement('div');
container.appendChild(target);
document.body.appendChild(container);

var circlePathString = offsetPathParse('circle(50%)', target).path;
assert.equal(circlePathString, 'M 100 50 m 0,-79.06 a 79.06,79.06 0 0,1 79.06,79.06 a 79.06,79.06 0 1,1 -79.06,-79.06 z');

circlePathString = offsetPathParse('circle(10px)', target).path;
assert.equal(circlePathString, 'M 100 50 m 0,-10 a 10,10 0 0,1 10,10 a 10,10 0 1,1 -10,-10 z');

circlePathString = offsetPathParse('circle(10px at 50%)', target).path;
assert.equal(circlePathString, 'M 100 50 m 0,-10 a 10,10 0 0,1 10,10 a 10,10 0 1,1 -10,-10 z');

circlePathString = offsetPathParse('circle(50% at 100px 200px)', target).path;
assert.equal(circlePathString, 'M 100 200 m 0,-79.06 a 79.06,79.06 0 0,1 79.06,79.06 a 79.06,79.06 0 1,1 -79.06,-79.06 z');

circlePathString = offsetPathParse('circle(10px at 50% 50%)', target).path;
assert.equal(circlePathString, 'M 100 50 m 0,-10 a 10,10 0 0,1 10,10 a 10,10 0 1,1 -10,-10 z');

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add a test for garbage input to circle

assertTransformInterpolation([
{'offsetPath': 'circle(10px at 0px 0px)', 'offsetDistance': '0%'},
{'offsetPath': 'circle(10px at 0px 0px)', 'offsetDistance': '100%'}],
[
[
{at: 0, is: 'translate3d(0px, -10px, 0px)'},
{at: 0.25, is: 'translate3d(10px, 0px, 0px) rotate(90deg)'},
{at: 0.5, is: 'translate3d(0px, 10px, 0px) rotate(180deg)'},
{at: 0.75, is: 'translate3d(-10px, 0px, 0px) rotate(-90deg)'},
{at: 1, is: 'translate3d(0px, -10px, 0px)'}
]
]
);

assertTransformInterpolation([
assertTransformInterpolation([
{'offsetPath': 'circle(10px at 0px 0px)', 'offsetDistance': '0px'},
{'offsetPath': 'circle(10px at 0px 0px)', 'offsetDistance': '62.83px'}],
[
[
{at: 0, is: 'translate3d(0px, -10px, 0px)'},
{at: 0.25, is: 'translate3d(10px, 0px, 0px) rotate(89.45deg)'},
{at: 0.5, is: 'translate3d(0.01px, 10px, 0px) rotate(180deg)'},
{at: 0.75, is: 'translate3d(-10px, 0.01px, 0px) rotate(-90deg)'},
{at: 1, is: 'translate3d(-0.01px, -10px, 0px) rotate(-0.55deg)'}
]
]
);

assertTransformInterpolation([
assertTransformInterpolation([
{'offsetPath': 'circle(10px at 0px 0px)', 'offsetDistance': '0%'},
{'offsetPath': 'circle(10px at 0px 0px)', 'offsetDistance': '50%'}],
[
[
{at: 0, is: 'translate3d(0px, -10px, 0px)'},
{at: 0.5, is: 'translate3d(10px, 0px, 0px) rotate(90deg)'},
{at: 1, is: 'translate3d(0px, 10px, 0px) rotate(180deg)'}
]
]
);

assertTransformInterpolation([
assertTransformInterpolation([
{'offsetPath': 'circle(10px at 0px 0px)', 'offsetDistance': '0px'},
{'offsetPath': 'circle(10px at 0px 0px)', 'offsetDistance': '31.42px'}],
[
[
{at: 0, is: 'translate3d(0px, -10px, 0px)'},
{at: 0.5, is: 'translate3d(10px, 0px, 0px) rotate(90deg)'},
{at: 1, is: 'translate3d(0px, 10px, 0px) rotate(179.45deg)'}
]
]
);

assertTransformInterpolation([
assertTransformInterpolation([
{'offsetPath': 'circle(10px at 100px 100px)', 'offsetDistance': '0%'},
{'offsetPath': 'circle(10px at 100px 100px)', 'offsetDistance': '100%'}],
[
[
{at: 0, is: 'translate3d(100px, 90px, 0px)'},
{at: 0.25, is: 'translate3d(110px, 100px, 0px) rotate(90deg)'},
{at: 0.5, is: 'translate3d(100px, 110px, 0px) rotate(180deg)'},
{at: 0.75, is: 'translate3d(90px, 100px, 0px) rotate(-90deg)'},
{at: 1, is: 'translate3d(100px, 90px, 0px)'}
]
]
);
});
});
})();
});
});
})();