How to get CSS transform rotation value in degrees with JavaScript - javascript

I'm using the code found at CSS-Tricks to get the current rotation transform (in CSS) with JavaScript.
JavaScript function:
function getCurrentRotation( elid ) {
var el = document.getElementById(elid);
var st = window.getComputedStyle(el, null);
var tr = st.getPropertyValue("-webkit-transform") ||
st.getPropertyValue("-moz-transform") ||
st.getPropertyValue("-ms-transform") ||
st.getPropertyValue("-o-transform") ||
st.getPropertyValue("transform") ||
"fail...";
if( tr !== "none") {
console.log('Matrix: ' + tr);
var values = tr.split('(')[1];
values = values.split(')')[0];
values = values.split(',');
var a = values[0];
var b = values[1];
var c = values[2];
var d = values[3];
var scale = Math.sqrt(a*a + b*b);
// arc sin, convert from radians to degrees, round
/** /
var sin = b/scale;
var angle = Math.round(Math.asin(sin) * (180/Math.PI));
/*/
var angle = Math.round(Math.atan2(b, a) * (180/Math.PI));
/**/
} else {
var angle = 0;
}
// works!
console.log('Rotate: ' + angle + 'deg');
$('#results').append('<p>Rotate: ' + angle + 'deg</p>');
}
According to the post, this works, however, for values over 180 degrees, I get negative numbers, and 360deg returns zero. I need to be able to correctly return the degree value from 180-360 degrees.
What am I doing wrong with this code that won't let it return the correct degree turn over 180 degrees?
It will make a lot more sense if you view the demo: See the pen for a demo of this in action.

I came in need of something like this too and decided to start from the initial code, doing a little clean up and some little improvement; then I modified as for the OP needing, so I wanted to share it here now:
function getCurrentRotation(el){
var st = window.getComputedStyle(el, null);
var tm = st.getPropertyValue("-webkit-transform") ||
st.getPropertyValue("-moz-transform") ||
st.getPropertyValue("-ms-transform") ||
st.getPropertyValue("-o-transform") ||
st.getPropertyValue("transform") ||
"none";
if (tm != "none") {
var values = tm.split('(')[1].split(')')[0].split(',');
/*
a = values[0];
b = values[1];
angle = Math.round(Math.atan2(b,a) * (180/Math.PI));
*/
//return Math.round(Math.atan2(values[1],values[0]) * (180/Math.PI)); //this would return negative values the OP doesn't wants so it got commented and the next lines of code added
var angle = Math.round(Math.atan2(values[1],values[0]) * (180/Math.PI));
return (angle < 0 ? angle + 360 : angle); //adding 360 degrees here when angle < 0 is equivalent to adding (2 * Math.PI) radians before
}
return 0;
}
Use it like this:
getCurrentRotation(document.getElementById("el_id"));

Found the answer in another SO question, you have to add (2 * PI) if the result in radians is less than zero.
This line:
var angle = Math.round(Math.atan2(b, a) * (180/Math.PI));
Needs to be replaced with this:
var radians = Math.atan2(b, a);
if ( radians < 0 ) {
radians += (2 * Math.PI);
}
var angle = Math.round( radians * (180/Math.PI));

Related

Css3 transform touch input

I have a transform 3D on a map object, however, when I transform the whole map the touch input doesn't follow and it results in panning not functioning when rotating the map full 180 the controls are inverted.
Is there a way to tell CSS to invert or rotate the way touch inputs are read by the browser to allow for a normal panning even when the map is rotated.
I do know this isn't the preferred way of rotating a map, the library should have a function for it but in this case, it doesn't and the only solution is to rotate the whole div containing the map.
What I am wondering about is a way to do this either with CSS or to override angular in some way to modify the touch on a 360-degree variable.
The map changes rotation frequently, so it can't be a static solution.
Css used to rotate the map:
transform-origin: 50% 50%; transform: rotate({{deg}}deg); transition: 300ms ease-out;
Code behind that is:
$scope.degraw = Math.round(heading.magneticHeading);
var aR;
$scope.rot = $scope.rot || 0; // if rot undefined or 0, make 0, else rot
aR = $scope.rot % 360;
if ( aR < 0 ) { aR += 360; }
if ( aR < 180 && ($scope.degraw > (aR + 180)) ) { $scope.rot -= 360; }
if ( aR >= 180 && ($scope.degraw <= (aR - 180)) ) { $scope.rot += 360; }
$scope.rot += ($scope.degraw - aR);
if($scope.isInCompass == 1) {
$scope.deg = $scope.rot * -1;
$scope.northdeg = $scope.rot * -1;
}
The panning and all movement is controlled by the map library, however the code I've been able to get out of the library (Might not be the code at all but atleast this is what I'm looking at right now)
_touchMove: function(a) {
a.preventDefault();
this._updateTouch(a);
var b = this._touches,
c, d = a.changedTouches.length,
f, e, g, h;
if (!(l("android") && l("safari") && 1 === a.targetTouches.length && a.touches.length === a.targetTouches.length && a.targetTouches.length === a.changedTouches.length && 0 === a.changedTouches[0].identifier && b[a.changedTouches[0].identifier] && 1 < this._touchIds.length)) {
for (c = 0; c < d; c++)
if (f = a.changedTouches[c], e = b[f.identifier]) g = Math.abs(f.pageX -
e.startX), f = Math.abs(f.pageY - e.startY), !e.moved && (g >= this.tapRadius || f >= this.tapRadius) && (e.moved = e.absMoved = !0), h = h ? h : e.moved;
1 === this._numTouches ? (b = a.changedTouches[0], this._swipeActive ? this._fire("onSwipeMove", this._processTouchEvent(a, b)) : h && (this._swipeActive = !0, this._fire("onSwipeStart", this._processTouchEvent(a, b)))) : 2 === this._numTouches && (c = this._nodeTouches[0], d = this._nodeTouches[1], this._pinchActive ? this._fire("onPinchMove", this._processTouchEvent(a, [c, d])) : h && (h = b[c.identifier], e = b[d.identifier],
b = Math.abs(h.startX - e.startX), h = Math.abs(h.startY - e.startY), e = Math.abs(c.pageX - d.pageX), g = Math.abs(c.pageY - d.pageY), Math.abs(Math.sqrt(e * e + g * g) - Math.sqrt(b * b + h * h)) >= 2 * this.tapRadius && (this._pinchActive = !0, this._fire("onPinchStart", this._processTouchEvent(a, [c, d])))))
}
},
I cannot really understand that touch code, but the point is that you should transform the input to follow your rotation:
get a rotation matrix:
var radians = deg / 180 * Math.Pi;
var cos = Math.cos(radians), sin = Math.sin(radians);
var matrix = [cos, sin, -sin, cos];
get a rotated touch point:
var rotatedX = matrix[0] * p.x + matrix[2] * p.y;
var rotatedY = matrix[1] * p.x + matrix[3] * p.y;
At this point if you do not have a knowledge of how your input are handled you can do this:
var originalTouchMove = _touchMove;
_touchMove = function(a) {
// inspect a and find where clientX and clientY are.
// obtain rotatedX and rotatedY and overwrite them
originalTouchMove(a) // a has been mutated with rotated coords
}

Function to find point of intersection between a ray and a sphere? (Javascript)

I was going to stop at intervals on the ray and check if they were within the radius of the sphere. I found much more efficient mathematical ways to do this but they are all written in C++.
Something like this should work (inefficient version but without any dependency and easy to follow):
function dotProduct(v1, v2) {
return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
}
function squaredLength(v) {
return dotProduct(v, v);
}
// Returns whether the ray intersects the sphere
// #param[in] center center point of the sphere (C)
// #param[in] radius radius of the sphere (R)
// #param[in] origin origin point of the ray (O)
// #param[in] direction direction vector of the ray (D)
// #param[out] intersection closest intersection point of the ray with the sphere, if any
function intersectRayWithSphere(center, radius,
origin, direction,
intersection) {
// Solve |O + t D - C|^2 = R^2
// t^2 |D|^2 + 2 t < D, O - C > + |O - C|^2 - R^2 = 0
var OC = intersection; // Use the output parameter as temporary workspace
OC.x = origin.x - center.x;
OC.y = origin.y - center.y;
OC.z = origin.z - center.z;
// Solve the quadratic equation a t^2 + 2 t b + c = 0
var a = squaredLength(direction);
var b = dotProduct(direction, OC);
var c = squaredLength(OC) - radius * radius;
var delta = b * b - a * c;
if (delta < 0) // No solution
return false;
// One or two solutions, take the closest (positive) intersection
var sqrtDelta = Math.sqrt(delta);
// a >= 0
var tMin = (-b - sqrtDelta) / a;
var tMax = (-b + sqrtDelta) / a;
if (tMax < 0) // All intersection points are behind the origin of the ray
return false;
// tMax >= 0
var t = tMin >= 0 ? tMin : tMax;
intersection.x = origin.x + t * direction.x;
intersection.y = origin.y + t * direction.y;
intersection.z = origin.z + t * direction.z;
return true;
}

Get the length of a SVG line,rect,polygon and circle tags

I managed to find the length of the paths in svg, but now i want to find the length for the line, rect, polygon and circle tags from SVG, I am really lost right now, and clues ? or are there already some functions like there is for path?
In case anyone else wants to find the length of these tags I made some functions for each of them, tested them and I say they work pretty ok, this was what i needed.
var tools = {
/**
*
* Used to get the length of a rect
*
* #param el is the rect element ex $('.rect')
* #return the length of the rect in px
*/
getRectLength:function(el){
var w = el.attr('width');
var h = el.attr('height');
return (w*2)+(h*2);
},
/**
*
* Used to get the length of a Polygon
*
* #param el is the Polygon element ex $('.polygon')
* #return the length of the Polygon in px
*/
getPolygonLength:function(el){
var points = el.attr('points');
points = points.split(" ");
var x1 = null, x2, y1 = null, y2 , lineLength = 0, x3, y3;
for(var i = 0; i < points.length; i++){
var coords = points[i].split(",");
if(x1 == null && y1 == null){
if(/(\r\n|\n|\r)/gm.test(coords[0])){
coords[0] = coords[0].replace(/(\r\n|\n|\r)/gm,"");
coords[0] = coords[0].replace(/\s+/g,"");
}
if(/(\r\n|\n|\r)/gm.test(coords[1])){
coords[0] = coords[1].replace(/(\r\n|\n|\r)/gm,"");
coords[0] = coords[1].replace(/\s+/g,"");
}
x1 = coords[0];
y1 = coords[1];
x3 = coords[0];
y3 = coords[1];
}else{
if(coords[0] != "" && coords[1] != ""){
if(/(\r\n|\n|\r)/gm.test(coords[0])){
coords[0] = coords[0].replace(/(\r\n|\n|\r)/gm,"");
coords[0] = coords[0].replace(/\s+/g,"");
}
if(/(\r\n|\n|\r)/gm.test(coords[1])){
coords[0] = coords[1].replace(/(\r\n|\n|\r)/gm,"");
coords[0] = coords[1].replace(/\s+/g,"");
}
x2 = coords[0];
y2 = coords[1];
lineLength += Math.sqrt(Math.pow((x2-x1), 2)+Math.pow((y2-y1),2));
x1 = x2;
y1 = y2;
if(i == points.length-2){
lineLength += Math.sqrt(Math.pow((x3-x1), 2)+Math.pow((y3-y1),2));
}
}
}
}
return lineLength;
},
/**
*
* Used to get the length of a line
*
* #param el is the line element ex $('.line')
* #return the length of the line in px
*/
getLineLength:function(el){
var x1 = el.attr('x1');
var x2 = el.attr('x2');
var y1 = el.attr('y1');
var y2 = el.attr('y2');
var lineLength = Math.sqrt(Math.pow((x2-x1), 2)+Math.pow((y2-y1),2));
return lineLength;
},
/**
*
* Used to get the length of a circle
*
* #param el is the circle element
* #return the length of the circle in px
*/
getCircleLength:function(el){
var r = el.attr('r');
var circleLength = 2 * Math.PI * r;
return circleLength;
},
/**
*
* Used to get the length of the path
*
* #param el is the path element
* #return the length of the path in px
*/
getPathLength:function(el){
var pathCoords = el.get(0);
var pathLength = pathCoords.getTotalLength();
return pathLength;
}
}
I think you are looking at the problem incorrectly :
length of rectangle = 2 * (width + height)
length of line ( use pythagorean theorem for any non vertical line c^2 = a^2 + b^2 ) or use ( x1 to x2 ) for horizontal , ( y1 to y2 ) for vertical
length of circle = 2 × π × radius ... etc
I tried to use the answer specified by ZetCoby for polygons, but in testing I found that the path length it returns is wrong.
Example:
<polygon points="10.524,10.524 10.524,24.525 24.525,24.525 24.525,10.524" style="fill:none;stroke-width:0.2;stroke:black"></polygon>
The the above polygon should have a length of 56, but the getPolygonLength(el) function returns a value of 61.79898987322332.
I wrote an algorithm to correctly calculate the path length of an SVG polygon, so I thought I should contribute it back since this is the first hit on google when searching for this problem.
Here is my function. Enjoy...
function polygon_length(el) {
var points = el.attr('points');
points = points.split(' ');
if (points.length > 1) {
function coord(c_str) {
var c = c_str.split(',');
if (c.length != 2) {
return; // return undefined
}
if (isNaN(c[0]) || isNaN(c[1])) {
return;
}
return [parseFloat(c[0]), parseFloat(c[1])];
}
function dist(c1, c2) {
if (c1 != undefined && c2 != undefined) {
return Math.sqrt(Math.pow((c2[0]-c1[0]), 2) + Math.pow((c2[1]-c1[1]), 2));
} else {
return 0;
}
}
var len = 0;
// measure polygon
if (points.length > 2) {
for (var i=0; i<points.length-1; i++) {
len += dist(coord(points[i]), coord(points[i+1]));
}
}
// measure line or measure polygon close line
len += dist(coord(points[0]), coord(points[points.length-1]));
return len;
} else {
return 0;
}
}
In SVG 2 all geometry elements will have a pathLength property but as of May 2017 this is still to be implemented in most browsers.
See https://developer.mozilla.org/en-US/docs/Web/API/SVGGeometryElement for more info.
We can future proof #zetcoby 's answer with:
if( el.pathLength ) {
return el.pathLength;
}
else {
// rest of code...
}

How do I draw x number of circles around a central circle, starting at the top of the center circle?

I'm trying to create a UI that has a lot of items in circles. Sometimes these circles will have related circles that should be displayed around them.
I was able to cobble together something that works, here.
The problem is that the outer circles start near 0 degrees, and I'd like them to start at an angle supplied by the consumer of the function/library. I was never a star at trigonometry, or geometry, so I could use a little help.
As you can see in the consuming code, there is a setting: startingDegree: 270 that the function getPosition should honor, but I haven't been able to figure out how.
Update 04/02/2014:
as I mentioned in my comment to Salix alba, I wasn't clear above, but what I needed was to be able to specify the radius of the satellite circles, and to have them go only partly all the way around. Salix gave a solution that calculates the size the satellites need to be to fit around the center circle uniformly.
Using some of the hints in Salix's answer, I was able to achieve the desired result... and have an extra "mode," thanks to Salix, in the future.
The working, though still rough, solution is here: http://jsfiddle.net/RD4RZ/11/. Here is the entire code (just so it's all on SO):
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<script type="text/javascript" src="//code.jquery.com/jquery-1.10.1.js"></script>
<style type="text/css">
.circle
{
position: absolute;
width: 100px;
height: 100px;
background-repeat: no-repeat;background-position: center center;
border: 80px solid #a19084;
border-radius: 50%;
-moz-border-radius: 50%;
}
.sm
{
border: 2px solid #a19084;
}
</style>
<script type="text/javascript">//<![CDATA[
$(function () {
function sind(x) {
return Math.sin(x * Math.PI / 180);
}
/*the law of cosines:
cc = aa + bb - 2ab cos(C), where c is the satellite diameter a and b are the legs
solving for cos C, cos C = ( aa + bb - cc ) / 2ab
Math.acos((a * a + b * b - c * c) / (2 * a * b)) = C
*/
function solveAngle(a, b, c) { // Returns angle C using law of cosines
var temp = (a * a + b * b - c * c) / (2 * a * b);
if (temp >= -1 && temp <= 1)
return radToDeg(Math.acos(temp));
else
throw "No solution";
}
function radToDeg(x) {
return x / Math.PI * 180;
}
function degToRad(x) {
return x * (Math.PI / 180);
}
var satellite = {
//settings must have: collection (array), itemDiameter (number), minCenterDiameter (number), center (json with x, y numbers)
//optional: itemPadding (number), evenDistribution (boolean), centerPadding (boolean), noOverLap (boolean)
getPosition: function (settings) {
//backwards compat
settings.centerPadding = settings.centerPadding || settings.itemPadding;
settings.noOverLap = typeof settings.noOverLap == 'undefined' ? true : settings.noOverLap;
settings.startingDegree = settings.startingDegree || 270;
settings.startSatellitesOnEdge = typeof settings.startSatellitesOnEdge == 'undefined' ? true : settings.startSatellitesOnEdge;
var itemIndex = $.inArray(settings.item, settings.collection);
var itemCnt = settings.collection.length;
var satelliteSide = settings.itemDiameter + (settings.itemSeparation || 0) + (settings.itemPadding || 0);
var evenDistribution = typeof settings.evenDistribution == 'undefined' ? true : settings.evenDistribution;
var degreeOfSeparation = (360 / itemCnt);
/*
we know all three sides:
one side is the diameter of the satellite itself (plus any padding). the other two
are the parent radius + the radius of the satellite itself (plus any padding).
given that, we need to find the angle of separation using the law of cosines (solveAngle)
*/
//if (!evenDistribution) {
var side1 = ((satelliteSide / 2)) + ((settings.minCenterDiameter + (2 * settings.centerPadding)) / 2);
var side2 = satelliteSide;;
var degreeOfSeparationBasedOnSatellite = solveAngle(side1, side1, side2); //Math.acos(((((side1 * side1) + (side2 * side2)) - (side2 * side2)) / (side2 * side2 * 2)) / 180 * Math.PI) * Math.PI;
degreeOfSeparation = evenDistribution? degreeOfSeparation: settings.noOverLap ? Math.min(degreeOfSeparation, degreeOfSeparationBasedOnSatellite) : degreeOfSeparationBasedOnSatellite;
//}
//angle-angle-side
//a-A-B
var a = satelliteSide;
var A = degreeOfSeparation;
/*
the three angles of any triangle add up to 180. We know one angle (degreeOfSeparation)
and we know the other two are equivalent to each other, so...
*/
var B = (180 - A) / 2;
//b is length necessary to fit all satellites, might be too short to be outside of base circle
var b = a * sind(B) / sind(A);
var offset = (settings.itemDiameter / 2) + (settings.itemPadding || 0); // 1; //
var onBaseCircleLegLength = ((settings.minCenterDiameter / 2) + settings.centerPadding) + offset;
var offBase = false;
if (b > onBaseCircleLegLength) {
offBase = true;
}
b = settings.noOverLap ? Math.max(b, onBaseCircleLegLength) : onBaseCircleLegLength;
var radianDegree = degToRad(degreeOfSeparation);
//log('b=' + b);
//log('settings.center.x=' + settings.center.x);
//log('settings.center.y=' + settings.center.y);
var degreeOffset = settings.startingDegree;
if (settings.startSatellitesOnEdge) {
degreeOffset += ((offBase ? degreeOfSeparation : degreeOfSeparationBasedOnSatellite) / 2);
}
var i = ((Math.PI * degreeOffset) / 180) + (radianDegree * itemIndex);// + (degToRad(degreeOfSeparationBasedOnSatellite) / 2); //(radianDegree) * (itemIndex);
var x = (Math.cos(i) * b) + (settings.center.x - offset);
var y = (Math.sin(i) * b) + (settings.center.y - offset);
return { 'x': Math.round(x), 'y': Math.round(y) };
}
,
/* if we ever want to size satellite by how many need to fit tight around the base circle:
x: function calcCircles(n) {
circles.splice(0); // clear out old circles
var angle = Math.PI / n;
var s = Math.sin(angle);
var r = baseRadius * s / (1 - s);
console.log(angle);
console.log(s);
console.log(r);
console.log(startAngle);
console.log(startAngle / (Math.PI * 2));
for (var i = 0; i < n; ++i) {
var phi = ((Math.PI * startAngle) / 180) + (angle * i * 2);
var cx = 150 + (baseRadius + r) * Math.cos(phi);
var cy = 150 + (baseRadius + r) * Math.sin(phi);
circles.push(new Circle(cx, cy, r));
}
},
*/
//settings must have: collection (array), itemDiameter (number), minCenterDiameter (number), center (json with x, y numbers)
//optional: itemPadding (number), evenDistribution (boolean), centerPadding (boolean), noOverLap (boolean)
getAllPositions: function (settings) {
var point;
var points = [];
var collection = settings.collection;
for (var i = 0; i < collection.length; i++) {
settings.item = collection[i]
points.push(satellite.getPosition(settings));
}
return points;
}
};
var el = $("#center"), cnt = 10, arr = [], itemDiameter= 100;
for (var c = 0; c < cnt; c++) {
arr.push(c);
}
var settings = {
collection: arr,
itemDiameter: itemDiameter,
minCenterDiameter: el.width(),
center: { x: el.width() / 2, y: el.width() / 2 },
itemPadding: 2,
evenDistribution: false,
centerPadding: parseInt(el.css("border-width")),
noOverLap: false,
startingDegree: 270
};
var points = satellite.getAllPositions(settings);
for (var i = 0; i < points.length; i++) {
var $newdiv1 = $("<div></div>");
var div = el.append($newdiv1);
$newdiv1.addClass("circle").addClass("sm");
$newdiv1.text(i);
$newdiv1.css({ left: points[i].x, top: points[i].y, width: itemDiameter +'px', height: itemDiameter +'px' });
}
});//]]>
</script>
</head>
<body>
<div id="center" class="circle" style="left:250px;top:250px" >
</div>
</body>
</html>
The central bit you need to work out is radius of the small circles. If you have R for radius of the central circle and you want to fit n smaller circles around it. Let the as yet unknown radius of the small circle be r. We can construct a right angle triangle with one corner in the center of the big circle one in the center of the small circle and one which is where a line from the center is tangent to the small circle. This will be a right angle. The angle at the center is a the hypotenuse has length R+r the opposite is r and we don't need the adjacent. Using trig
sin(a) = op / hyp = r / (R + r)
rearrange
(R+r) sin(a) = r
R sin(a) + r sin(a) = r
R sin(a) = r - r sin(a)
R sin(a) = (1 - sin(a)) r
r = R sin(a) / ( 1 - sin(a))
once we have r we are pretty much done.
You can see this as a fiddle http://jsfiddle.net/SalixAlba/7mAAS/
// canvas and mousedown related variables
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var $canvas = $("#canvas");
var canvasOffset = $canvas.offset();
var offsetX = canvasOffset.left;
var offsetY = canvasOffset.top;
var scrollX = $canvas.scrollLeft();
var scrollY = $canvas.scrollTop();
// save canvas size to vars b/ they're used often
var canvasWidth = canvas.width;
var canvasHeight = canvas.height;
var baseRadius = 50;
var baseCircle = new Circle(150,150,50);
var nCircles = 7;
var startAngle = 15.0;
function Circle(x,y,r) {
this.x = x;
this.y = y;
this.r = r;
}
Circle.prototype.draw = function() {
ctx.beginPath();
ctx.arc(this.x,this.y,this.r, 0, 2 * Math.PI, false);
ctx.stroke();
}
var circles = new Array();
function calcCircles(n) {
circles.splice(0); // clear out old circles
var angle = Math.PI / n;
var s = Math.sin(angle);
var r = baseRadius * s / (1-s);
console.log(angle);
console.log(s);
console.log(r);
for(var i=0;i<n;++i) {
var phi = startAngle + angle * i * 2;
var cx = 150+(baseRadius + r) * Math.cos(phi);
var cy = 150+(baseRadius + r) * Math.sin(phi);
circles.push(new Circle(cx,cy,r));
}
}
function draw() {
baseCircle.draw();
circles.forEach(function(ele){ele.draw()});
}
calcCircles(7);
draw();

dotted stroke in <canvas>

I guess it is not possible to set stroke property such as CSS which is quite easy. With CSS we have dashed, dotted, solid but on canvas when drawing lines/or strokes this doesn't seem to be an option. How have you implemented this?
I've seen some examples but they are really long for such a silly function.
For example:
http://groups.google.com/group/javascript-information-visualization-toolkit/browse_thread/thread/22000c0d0a1c54f9?pli=1
Fun question! I've written a custom implementation of dashed lines; you can try it out here. I took the route of Adobe Illustrator and allow you to specify an array of dash/gap lengths.
For stackoverflow posterity, here's my implementation (slightly altered for s/o line widths):
var CP = window.CanvasRenderingContext2D && CanvasRenderingContext2D.prototype;
if (CP && CP.lineTo){
CP.dashedLine = function(x,y,x2,y2,dashArray){
if (!dashArray) dashArray=[10,5];
if (dashLength==0) dashLength = 0.001; // Hack for Safari
var dashCount = dashArray.length;
this.moveTo(x, y);
var dx = (x2-x), dy = (y2-y);
var slope = dx ? dy/dx : 1e15;
var distRemaining = Math.sqrt( dx*dx + dy*dy );
var dashIndex=0, draw=true;
while (distRemaining>=0.1){
var dashLength = dashArray[dashIndex++%dashCount];
if (dashLength > distRemaining) dashLength = distRemaining;
var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
if (dx<0) xStep = -xStep;
x += xStep
y += slope*xStep;
this[draw ? 'lineTo' : 'moveTo'](x,y);
distRemaining -= dashLength;
draw = !draw;
}
}
}
To draw a line from 20,150 to 170,10 with dashes that are 30px long followed by a gap of 10px, you would use:
myContext.dashedLine(20,150,170,10,[30,10]);
To draw alternating dashes and dots, use (for example):
myContext.lineCap = 'round';
myContext.lineWidth = 4; // Lines 4px wide, dots of diameter 4
myContext.dashedLine(20,150,170,10,[30,10,0,10]);
The "very short" dash length of 0 combined with the rounded lineCap results in dots along your line.
If anyone knows of a way to access the current point of a canvas context path, I'd love to know about it, as it would allow me to write this as ctx.dashTo(x,y,dashes) instead of requiring you to re-specify the start point in the method call.
This simplified version of Phrogz's code utilises the built-in transformation functionality of Canvas and also handles special cases e.g. when dx = 0
var CP = window.CanvasRenderingContext2D && CanvasRenderingContext2D.prototype;
if (CP.lineTo) {
CP.dashedLine = function(x, y, x2, y2, da) {
if (!da) da = [10,5];
this.save();
var dx = (x2-x), dy = (y2-y);
var len = Math.sqrt(dx*dx + dy*dy);
var rot = Math.atan2(dy, dx);
this.translate(x, y);
this.moveTo(0, 0);
this.rotate(rot);
var dc = da.length;
var di = 0, draw = true;
x = 0;
while (len > x) {
x += da[di++ % dc];
if (x > len) x = len;
draw ? this.lineTo(x, 0): this.moveTo(x, 0);
draw = !draw;
}
this.restore();
}
}
I think my calculations are correct and it seems to render OK.
At the moment at least setLineDash([5,10]) works with Chrome and ctx.mozDash = [5,10] works with FF:
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
if ( ctx.setLineDash !== undefined ) ctx.setLineDash([5,10]);
if ( ctx.mozDash !== undefined ) ctx.mozDash = [5,10];
ctx.beginPath();
ctx.lineWidth="2";
ctx.strokeStyle="green";
ctx.moveTo(0,75);
ctx.lineTo(250,75);
ctx.stroke();
Setting to null makes the line solid.
Phroz's solution is great. But when I used it in my application, I found two bugs.
Following code is debugged (and refactored for readability) version of Phroz's one.
// Fixed: Minus xStep bug (when x2 < x, original code bugs)
// Fixed: Vertical line bug (when abs(x - x2) is zero, original code bugs because of NaN)
var CP = window.CanvasRenderingContext2D && CanvasRenderingContext2D.prototype;
if(CP && CP.lineTo) CP.dashedLine = function(x, y, x2, y2, dashArray){
if(! dashArray) dashArray=[10,5];
var dashCount = dashArray.length;
var dx = (x2 - x);
var dy = (y2 - y);
var xSlope = (Math.abs(dx) > Math.abs(dy));
var slope = (xSlope) ? dy / dx : dx / dy;
this.moveTo(x, y);
var distRemaining = Math.sqrt(dx * dx + dy * dy);
var dashIndex = 0;
while(distRemaining >= 0.1){
var dashLength = Math.min(distRemaining, dashArray[dashIndex % dashCount]);
var step = Math.sqrt(dashLength * dashLength / (1 + slope * slope));
if(xSlope){
if(dx < 0) step = -step;
x += step
y += slope * step;
}else{
if(dy < 0) step = -step;
x += slope * step;
y += step;
}
this[(dashIndex % 2 == 0) ? 'lineTo' : 'moveTo'](x, y);
distRemaining -= dashLength;
dashIndex++;
}
}
Mozilla has been working on an implementation of dashed stroking for canvas, so we may see it added to the spec in the near future.
There's a much simpler way to do this. According to http://www.w3.org/TR/2dcontext/#dom-context-2d-strokestyle strokeStyle accepts strings, CanvasGradients, or CanvasPatterns. So we just take an image like this:
<img src="images/dashedLineProto.jpg" id="cvpattern1" width="32" height="32" />
load it into a canvas, and draw our little rectangle with it.
var img=document.getElementById("cvpattern1");
var pat=ctx.createPattern(img,"repeat");
ctx.strokeStyle = pat;
ctx.strokeRect(20,20,150,100);
that doesnt result in a perfect dashed line, but it's really straightforward and modifiable. Results may of course become imperfect when you're drawing lines which arent horizontal or vertical, a dotted pattern might help there.
PS. keep in mind SOP applies when you're trying to use imgs from external sources in your code.
Looks like context.setLineDash is pretty much implemented.
See this.
" context.setLineDash([5])
will result in a dashed line where both the dashes and spaces are 5 pixels in size. "
There is currently no support in HTML5 Canvas specification for dashed lines.
check this out:
http://davidowens.wordpress.com/2010/09/07/html-5-canvas-and-dashed-lines/
or
Check out the Raphael JS Library:
http://raphaeljs.com/
There are support for it in Firefox at least
ctx.mozDash = [5,10];
seems like ctx.webkitLineDash worked before, but they removed it because it had some compabillity issues.
The W3C specs says ctx.setLineDash([5,10]); but it doesn't seem to be implemented yet anywhere.
I made modified the dashedLine function to add support for offsetting. It utilizes native dashed lines if the browser supports ctx.setLineDash and ctx.lineDashOffset.
Example: http://jsfiddle.net/mLY8Q/6/
var CP = window.CanvasRenderingContext2D && CanvasRenderingContext2D.prototype;
if (CP.lineTo) {
CP.dashedLine = CP.dashedLine || function (x, y, x2, y2, da, offset) {
if (!da) da = [10, 5];
if (!offset) offset = 0;
if (CP.setLineDash && typeof (CP.lineDashOffset) == "number") {
this.save();
this.setLineDash(da);
this.lineDashOffset = offset;
this.moveTo(x, y);
this.lineTo(x2, y2);
this.restore();
return;
}
this.save();
var dx = (x2 - x),
dy = (y2 - y);
var len = Math.sqrt(dx * dx + dy * dy);
var rot = Math.atan2(dy, dx);
this.translate(x, y);
this.moveTo(0, 0);
this.rotate(rot);
var dc = da.length;
var di = 0;
var patternLength = 0;
for (var i = 0; i < dc; i++) {
patternLength += da[i];
}
if (dc % 2 == 1) {
patternLength *= 2;
}
offset = offset % patternLength;
if (offset < 0) {
offset += patternLength;
}
var startPos = 0;
var startSegment = 0;
while (offset >= startPos) {
if (offset >= startPos + da[startSegment % dc]) {
startPos += da[startSegment % dc];
startSegment++;
} else {
offset = Math.abs(offset - startPos);
break;
}
if (startSegment > 100) break;
}
draw = startSegment % 2 === 0;
x = 0;
di = startSegment;
while (len > x) {
var interval = da[di++ % dc];
if (x < offset) {
interval = Math.max(interval - offset, 1);
offset = 0;
}
x += interval;
if (x > len) x = len;
draw ? this.lineTo(x, 0) : this.moveTo(x, 0);
draw = !draw;
}
this.restore();
};
}
I found properties mozDash and mozDashOffset in Mozilla specification:
http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/canvas/nsIDOMCanvasRenderingContext2D.idl
They're probaly used to control dashes, but i haven't used them.

Categories

Resources