HTML5 Canvas - Wireframe Sphere in 2d - html

I'm looking to draw a 3D wire frame sphere in 2D Canvas. I'm not a math ninja by any means, so I'm wondering if anyone knows a simple way to draw one in Canvas using lineto arc connections and drawing it with :math:
I would appreciate any assistance.
Something like this: http://en.wikipedia.org/wiki/File:Sphere_wireframe_10deg_6r.svg
I'm hoping this is a simple equation, but if you know that it isn't (i.e. drawing that would be a lot of code), I would appreciate knowing that as well as I may need to reconsider what I wanna do.

The easiest for you would probably to view the source of the SVG file (here) and recreate those paths using canvas commands.
If you want an actual 3d sphere, projected onto 2d space, I'd suggest using a library like Three.js
You can also look at some of the math I've done here: swarms
The _3d and Matrix modules should be all that you need.

This time SO didn't help me, so I've helped myself and here it is: a pure HTML5 + JavaScript configurable rendering or a wireframe sphere.
I started from this excellent post and then went on. Basically I collected some vertex generation code from Qt3D and adapted to JS.
I'm not 100% sure the rotation functions are correct, but you are welcome to contribute back in case you find errors.
To be clearer, I've distinguished Z positions and draw white on the front and gray on the back.
Here's the result (16 rings x 32 slices) and related jsFiddle link
Enjoy

This is an old thread, but I had the same question and could not find any existing satisfying answer. That is, an answer other than "use WebGL" or "use Three.js". Lo and behold, I am the bearer of great news: it is actually possible to render such a sphere using exclusively Canvas2D's ellipse function, giving us:
a straightforward implementation (~130 lines everything included)
no need for computing vertices and edges
sexy smooth edges
You can find a demo on JSBin, for posterity, with a bunch of options.
The key is to notice that the "wireframe" we're trying to draw is solely composed of circles, and every circle rotated in 3d space will get projected to the camera as an ellipse. The question, then, is: how to find the ellipse corresponding to the projection of the rotated circle?
As we are only interested in circles that lay on the surface of the sphere, we can characterize each of them by the (inter)section of a plane and the (unit) sphere. Therefore, each circle can be described by a normal vector and an offset -1 < o < 1.
Then it's not too difficult to compute and draw the ellipse resulting from the projection of the circle:
function draw_section(n, o = 0) {
let {x, y, z} = project(_p, n) // project normal on camera
let a = atan2(y, x) // angle of projected normal -> angle of ellipse
let ry = sqrt(1 - o * o) // radius of section -> y-radius of ellipse
let rx = ry * abs(z) // x-radius of ellipse
let W = sqrt(x * x + y * y)
let sa = acos(clamp(-1, 1, o * (1 / W - W) / rx || 0)) // ellipse start angle
let sb = z > 0 ? 2 * PI - sa : - sa // ellipse end angle
ctx.beginPath()
ctx.ellipse(x * o * RADIUS, y * o * RADIUS, rx * RADIUS, ry * RADIUS, a, sa, sb, z <= 0)
ctx.stroke()
}
The disks from your example image can be obtained by:
rotating a plane around the z axis
shifting a plane along the z axis
function draw_arcs() {
for (let i = 10; i--;) {
let a = i / 10 * Math.PI
draw_section(vec.set(_n, cos(a), sin(a), 0))
}
for (let i = 9; i--;) {
let a = (i + 1) / 10 * Math.PI
draw_section(Z, cos(a))
}
}
A nice benefit of this method is that you can do this "shifting a plane along the Z axis" for all axes, resulting in a lovely wireframe that would be hard to reproduce if computing vertices and edges by hand:
The only change was the following:
function draw_arcs() {
for (let i = 9; i--;) {
let a = (i + 1) / 10 * Math.PI
draw_section(Z, cos(a))
draw_section(X, cos(a))
draw_section(Y, cos(a))
}
}
The function draw_section above was carefully crafted so that it only draws the camera-facing arc of a given section, which means we get occlusion-culling for free.
(and my dirty trick to render the back of the sphere with a different color is to run draw_arcs again after flipping the canvas)
It's also possible to use 2 radial gradients to have some fake depth shading like in your image:
Sadly browsers seem to struggle a lot when drawing paths with gradients.
There are but two drawbacks I see right now:
performance may vary between browsers. It's likely some optimization could be done, like merging successive calls to .stroke() into one. Frankly quite surprised by how slow using ellipse seems to be at times.
it's a parallel projection rather than a perspective one. If we were to add perspective, projected circles would still appear as ellipses, but the calculation of the ellipse would be a tad more involved. I haven't done it yet, I expect it to be possible, might update my answer if I succeed.

Look at this one: http://jsfiddle.net/aJMBp/
you should just draw a lot of these lines to create a complete sphere. This is a good starting point, give me 5 minutes and I'll see if I can improve it to draw a sphere.
Getting better:
http://jsfiddle.net/aJMBp/1/
Ok, thats def out of my capacity. However, another little improvement here: http://jsfiddle.net/aJMBp/2/

Related

Line quality is very low with jagged or blurry edges

I'm working on a drawing app and the line quality seems to be very low and jagged compared to other drawing apps.
Or it might be that other apps are doing something different than I'm doing.
What I have done so far is use the graphics property to draw the lines. I also collect the mouse positions on the mouse move events for assigning to a path later on. Summarized it:
MouseDownHandler:
mouseDownPoint.x = event.stageX;
mouseDownPoint.y = event.stageY;
drawCommands.push(GraphicsPathCommand.MOVE_TO);
simplePath = "M " + mouseDownPoint.x + " " + mouseDownPoint.y;
MouseMoveHandler:
line.graphics.lineStyle(lineWeight, lineColor, lineAlpha, pixelHinting);
line.graphics.moveTo(previousPoint.x, previousPoint.y);
scaledPoint = new Point(localPoint.x/scaleX, localPoint.y/scaleY);
line.graphics.lineTo(scaledPoint.x, scaledPoint.y);
previousPoint.x = scaledPoint.x;
previousPoint.y = scaledPoint.y;
simplePath += " L " + scaledPoint.x + " " + scaledPoint.y;
MouseUpHandler:
myPath.data = simplePath;
As I draw I update the line (which is a UIComponent but could just as well be a Shape or Sprite - anything with a graphics property). At the same time I keep track of the mouse locations in the simplePath string.
When the mouse is up I clear the line graphics and show a path graphic element. The Path isn't important to this but I noticed it looks slightly cleaner than the line that had been drawn. That might be because it has pixel hinting (it's not much cleaner). Sometimes there are artifacts. And I'm including it in case I need to use a path for some reason.
Here is the screen shot:
The pixel hinted version looks crisper but it still is far below the quality of the line drawing in other apps and in some cases it makes it look more jagged. Is there something I'm missing?
Note: I included graphics2d and canvas2d because I believe this may not be related to the specific language or platform but might be related to drawing graphics in general.
The green line is produced by Graphics.cubicCurveTo(...) method. Initially you have a list of user-provided points A1,A2,A3...An. In order to use cubic curves you also need to figure 2 control points CFk (forward) and CBk (backward), for each Ak respectively, so you draw that big curve starting from A1 and every curve piece from Ak-1 to Ak will take arguments .cubicCurveTo(CFk-1, CBk, Ak);
For each Ak (except for A1 and An) you can calculate CFk and CBk as following:
(vector)AForward = (vector)(Ak+1 - Ak-1)
(vector)AForward.length = (vector)(Ak+1 - Ak).length / 3
CFk = Ak + (point)AForward
(vector)ABackward = (vector)(Ak-1 - Ak+1)
(vector)ABackward.length = (vector)(Ak-1 - Ak).length / 3
CBk = Ak + (point)ABackward
Then, there are A1 and An that are left out, but I am sure you can figure them on your own.
For vector math you can use ru.delimiter.math.Vector2D class (works with both Cartesian and Polar coordinates) from my small collection of useful things: https://bitbucket.org/thydmitry/ru.delimiter/src/9083fb46ce1c/classes/ru/delimiter/math/
P.S. Maybe you don't need to go that extreme and will be fine with the red line, that is a simple .curveTo(Ak, (Ak + Ak+1)/2);
UPD: a simple algorithm to inscribe a curve into zigzag provided by an array of points.
function middle(A:Point, B:Point):Point
{
return new Point((A.x + B.x) / 2, (A.y + B.y) / 2);
}
function drawTo(target:Point):void
{
graphics.lineTo(target.x, target.y);
}
function bendTo(control:Point, target:Point):void
{
graphics.curveTo(control.x, control.y, target.x, target.y);
}
// This should contain at least 2 points before you start drawing.
var PP:Vector.<Point>;
// Go to the start position.
graphics.lineStyle(0, 0xFF0000);
graphics.moveTo(PP[0].x, PP[0].y);
// Draw a straight line to the center of the first zigzag segment.
drawTo(middle(PP[0], PP[1]));
// For each 3 consequent points A,B and C, connect
// the middle of AB and the middle of BC with a curve.
for (var i:int = 2; i < PP.length; i++)
{
bendTo(PP[i - 1], middle(PP[i - 1], PP[i]));
}
// Connect the center of the last zigzag segment with the end point.
drawTo(PP[PP.length - 1]);
There are multiple reasons:
Stage quality. In Flash Player you can set the stage quality to LOW, MEDIUM, HIGH, BEST, 8x8, 8x8Linear, 16x16 and 16x16Linear. This affects if there is antialiasing applied on lines / paths and how many times it's applied. Increasing the quality helps but in 8x8 and higher quality there are bugs in the Flash Player (font size reduced by 25% on non-embedded fonts, graphics artifacts, gradient fills color count reduced).
Pixel snapping. If you have a 1px line that ends up positioned on a half pixel it is anti aliased over two lines. Normally antialiasing increases quality but in the case of an offset single pixel line it reduces quality. Setting pixel snapping helps.
Using curveTo and cubicCurveTo instead of line points as #Organis suggested. Not sure how to do this yet.
Will try to post images of the difference in each case when I get a chance.

AS3 - geometry - perspective projection of a point on a 2D plane

I'm currently struggling on a problem that seems far beyond my maths capacities (been a long time since I've made some proper maths...) and I would appreciate some help on that.
Here's my setting :
I got some simple shapes (rectangles), and I "project" their bottom points on a line, coming from an Origin point.
Up to this point everything is fine.
But now I'd like to draw the original shape distorted as if it was projected with some perspective on a plane.
Please consider that I have nothing related to any rotation, isometric or any 3D or fake 2D perspective in my code, I'm only trying to draw some shapes using the graphics library to only have a feeling of something real.
Here's a quick drawing of what I'm trying to do :
What I know :
Origin point coordinates
the rect position & sizes
the red line position
the A & B points coordinates
What I want to determine is the coordinates of the C & D points, thing that could be easy if I wasn't struggling to find the "Origin bis" coordinates.
What I'm trying to do is to fake the projection of my rectangle on something that can be considered as a "floor" (related to the plane where my original rectangle is that can be seen as a wall).
Maybe I'm over-complicating the problem or maybe I fail to see any other easier way to do it, but I'm really not good anymore in any geometry or maths thing... :-(
Thanks a lot for your answers !
hmm i don't know if I undestood it correctly but I think you have too few input parameters:
you said the following information is given:
Origin point coordinates
the rect position & sizes
the red line position
the A & B points coordinates
I don't think it is possible to get your projected rectangle with this information alone.
Additionally, I think your green lines and the 'origin Bis' aren't helpful as well.
Perhaps, try this:
Supose, a blue line going through the points C & D is given as well.
Then you could find your projected rectangle by projecting the top of the rectangle onto that blue line.
So in summary:
You define an origin + two parallel lines, a red and a blue one.
Then you can project the top of the rect onto the blue line and the bottom of the rect onto the red line, yielding the points A,B,C,D
I hope this helps.
If I'm right, this code will show what you wanted to see.
First of all, I've ignored your initial setup of objects and information, and focused on the example situation itself; fake-projecting shadow for a "monolith" (any object is possible with the example below, even textured)
My reason was that it's really quite easy with the Matrix class of ActionScript, a handy tool worth learning.
Solution:
You can use the built-in Matrix class to do skew transform on DisplayObjects.
Try this example:
(The "useful" part lies in the _EF EnterFrame handler ;) )
import flash.display.MovieClip;
import flash.geom.Matrix;
import flash.events.Event;
import flash.display.BitmapData;
const PIP180:Number = Math.PI / 180;
const MAX_SHADOW_HEIGHT_MULTIPLIER:Number = 0.25; // you can also calculate this from an angle, like ... = Math.sin(angle * PIP180);
const ANIM_DEG_PER_FRAME:Number = 1.0 * PIP180; // the shadow creeps at a +1 degree per frame rate
var tx:BitmapData = new MonolithTexture(); // define this BitmapData in the library
var skew:Number = -10 * PIP180; // initial
var mono:MovieClip = new MovieClip();
mono.graphics.beginBitmapFill(tx);
// drawn that way the registration point is 0,0, so it's standing on the ground
mono.graphics.drawRect(0, -tx.height, tx.width, tx.height);
mono.graphics.endFill();
// align monolith to the "ground"
mono.x = stage.stageWidth / 2;
mono.y = stage.stageHeight - 100;
// make it be 100x300 pixel
mono.width = 100;
mono.height = 300;
var shad:MovieClip = new MovieClip();
// colored:
shad.graphics.beginFill(0x000000);
// or textured:
//shad.graphics.beginBitmapFill(tx);
shad.graphics.drawRect(0, -tx.height, tx.width, tx.height);
shad.graphics.endFill();
addChild(shad); // shadow first
addChild(mono); // then the caster object
addEventListener(Event.ENTER_FRAME, _EF);
function _EF(e:Event):void {
// animate skew on the positive half circle
skew = (skew + ANIM_DEG_PER_FRAME) % Math.PI;
// Matrix takes 6 parameters: a, b, c, d, x, y
// for this shadow trick, use them as follows:
// a = width scaling (as mono and shad are drawn in the same way, copy mono.scaleX for a perfect fit
// b = 0, because we don't want to project the vertical axis of transformation to the horizontal
// c = horizontal skew
// d = height scaling * skew * making it a bit flat using the constant
// x = mono.x, ...
// y = mono.y since originally mono and shad look alike, only the Matrix makes shad render differently
var mtx:Matrix = new Matrix(mono.scaleX, 0, Math.cos(skew), mono.scaleY * Math.sin(skew) * MAX_SHADOW_HEIGHT_MULTIPLIER, mono.x, mono.y);
shad.transform.matrix = mtx;
}
Now all you got to know to utilize this in your case, is the following N factors:
Q1: from what angle you want to project the shadow?
A1: horizontal factor is the skew variable itself, while vertical angle is stored as constant here, called MAX_SHADOW_HEIGHT_MULTIPLIER
Q2: do you want to project shadow only "upwards", or freely?
A2: if "upwards" is fine, keep skew in the positive range, otherwise let it take negative values as well for a "downward" shadow
P.S.: if you render the internals of the objects that they don't snap to 0 y as a base point, you can make them seem float/sink, or offset both objects vertically with a predefined value, with the opposite sign.
You face 1 very simple problem, as you said:
'What I want to determine is the coordinates of the C & D points, thing that could be easy if I wasn't struggling to find the "Origin bis" coordinates.'
But these co-ordinates relate to each other, so without one (or another value such as an angle) you cannot have the other. If you are to try this in 3D you are simply allowing the 3D engine to define 'Origin bis' and do your calculating for C and D itself.
So regardless you will need an 'Original bis', another value relating to the redline or your Rect for which to calculate the placement of C and D.
I remember making stuff like this and sometimes it's better to just stick with simple, you either make an 'Original bis' defines by yourself (it can be either stationary or move with the player/background) and get C and D the way you got A and B only that you use a lower line than the red line, or as I would of done, once you have A and B, simple skew/rotate your projection from those points down a bit further, and you get something the same as an 'Original bis' that follows the player. This works fine at simulating 'feeling of something real' but sadly as has been said, it looking real depends on what you are portraying. We do not know what the areas above or below the red line are (sky/ground, ground/water) and whether 'Origin' and 'Origin bis' is your light source, vanishing point, etc.

Bezier curve in f(x) form

I'm trying to get a cubic bezier curve (four points) implementation in f(x) form. Obviously bezier curves aren't perfect functions, but if the last two points are within a square made between the first and second point then they are. I'm really not that great with maths - I barely understand the implementation of a normal bezier curve, and I have no idea how or if you can equate things together to get such a function. i.e. y = f(x).
That being said, I don't necessarily need a bezier curve, I just need a curve that goes from one point to another where I can define the gradients at both points. I've tried to mess around with mathematics to get such a function, and I managed to get a function which drives at the appropriate gradients, but not the appropriate height.
y = m1*x^2 / 2w + w(m1 - m2*x/2)
This function has (0,0) with gradient = m1
and (w, y) gradient = m2
The problem is that I can't figure out how to get the height between the two points into the equation. I had a method for another equation, where the new function was f(x) * h / f(w), but in this case that changes the gradients of the points in question.
Bezier spline is a parametric function of t and control points (four in case of cubic Bezier spline)
P(t) = f(t, P1, P2, P3, P4)
More precisely for 2D case:
x(t) = (1 - t)^3*x1 + 3*(1 - t)^2*t*x2 + 3*(1 - t)*t^2*x3 + t^3*x4
y(t) = (1 - t)^3*y1 + 3*(1 - t)^2*t*y2 + 3*(1 - t)*t^2*y3 + t^3*y4
where t in [0, 1].
It will be hard to express y(t) through x(t) as it's multiple-valued function in general case.

Connecting arcs on HTML5 Canvas

I am trying to make a donut chart using the arc function in the HTML5 canvas. I am wanting to know how to use the lineTo function to connect two arcs together.
At the moment I have a pie chart which has fixed central x/y coords, so making the slices is easy as once the arc of each slice is done, the lineTo method simply uses the the fixed coords.
However with a ring/donut chart, I have two arcs, one with a smaller radius, but no idea how to connect the ends together without horrifically complicated trigonometry. Is there any way to get the 'start' and 'end' x/y coords of the arc?
I have a current hackyish 'solution' of simply drawing a smaller white circle over the pie chart to give the ring graph, but I want to know the answer to the question above.
You just have to remember a little trigonometry. If your center point is x, y and radius is r; then the coordinates on the circle at an angle alpha are:
pointX = x + Math.cos(alpha) * r;
pointY = y + Math.sin(alpha) * r;
And you have two of those angles, corresponding to the starting and the ending point.
Why are you drawing arcs? Would'nt it be easier if you just draw the circle (or circles for the ring) and then draw radius?

How can I better pack rectangles tangent to a sphere for a 3d gallery?

I am creating a 3D sphere gallery with ActionScript 3 and the Flash 10 3D (2.5D) APIs. I have found a method that works but is not ideal. I would like to see if there is a better method.
My algorithm goes like this:
Let n = the number of images
h = the height of each image
w = the width of each image
Approximate the radius of the circle by assuming (incorrectly) that the surface area of the images is equal to the surface area of the sphere we want to create.To calculate the radius solve for r in nwh = 4πr2. This is the part that needs to be improved.
Calculate the angle between rows. rowAngle = 2atan(h / 2 / r).
Calculate the number of rows.rows = floor(π / rowAngle).
Because step one is an approximation, the number of rows will not fit perfectly, so for presentation add padding rowAngle.rowAngle += (π - rowAngle * rows) / rows.
For each i in rows:
Calculate the radius of the circle of latitude for the row.latitudeRadius = radius * cos(π / 2 - rowAngle * i.
Calculate the angle between columns.columnAngle = atan(w / 2 / latitudeRadius) * 2.
Calculate the number of colums.columns = floor(2 * π / columnAngle)
Because step one is an approximation, the number of columns will not fit perfectly, so for presentation add padding to columnAngle.columnAngle += (2 * π - columnAngle * column) / column.
For each j in columns, translate -radius along the Z axis, rotate π / 2 + rowAngle * i around the X axis, and rotate columnAngle * j around the Y axis.
To see this in action, click here. alternate link. Notice that with the default settings, the number of items actually in the sphere are less by 13. I believe is the error introduced by my approximation in the first step.
I am not able to figure out a method for determining what the exact radius of such a sphere should be. I'm hoping to learn either a better method, the correct method, or that what I am trying to do is hard or very hard (in which case I will be happy with what I have).
I would divide this problem into two connected problems.
Given a radius, how do you pack things on to the sphere?
Given a number of things, how do you find the right radius?
If you have a solution to the first problem, the second is easy to solve. Here it is in pseudo-code.
lowerRadius = somethingTooSmall
fittedItems = itemsForRadius(lowerRadius)
while fittedItems < wantedItems:
lowerRadius *= 2
fittedItems = itemsForRadius(lowerRadius)
upperRadius = 2 * lowerRadius
while threshold < upperRadius - lowerRadius:
middleRadius = (upperRadius + lowerRadius)/2
if itemsForRadius(middleRadius) < wantedItems:
lowerRadius = middleRadius
else:
upperRadius = middleRadius
This will find the smallest radius that will pack the desired number of things with your packing algorithm. If you wish you could start with a better starting point - your current estimate is pretty close. But I don't think that an analytic formula will do it.
Now let's turn to the first problem. You have a very reasonable approach. It does have one serious bug though. The bug is that your columnAngle should not be calculated for the middle of your row. What you need to do is figure out the latitude which your items are in that is closest to the pole, and use that for the calculation. This is why when you try to fit 10 items you find a packing that causes the corners to overlap.
If you want a denser packing, you can try squishing rows towards the equator. This will result in sometimes having room for more items in a row so you'll get more things in a smaller sphere. But visually it may not look as nice. Play with it, and decide whether you like the result.
BTW I like the idea. It looks nice.
In the case of squares, it seems to be an approximate formula for knowing the relationship between the radius, the square's side and the number of squares embedded.
Following this, the number of squares is:
Floor[4 Pi/Integrate[(x^2 + y^2 + r^2)^(-3/2), {x, -a/2, a/2}, {y, -a/2, a/2}]]
or
Floor[(Pi r)/ArcCot[(2 Sqrt[2] r Sqrt[a^2+2 r^2])/a^2]]
where
r = Radius
a = Square side
If you plot for r=1, as a function of a:
Where you can see the case a=2 is the boundary for n=6, meaning a cube:
Still working to see if it can be extended to the case of a generic rectangle.
Edit
For rectangles, the corresponding formula is:
Floor[4 Pi/Integrate[(x^2 + y^2 + r^2)^(-3/2), {x, -a/2, a/2}, {y, -b/2, b/2}]]
which gives:
Floor[(2 Pi r)/(Pi-2 ArcTan[(2 r Sqrt[a^2+b^2+4 r^2])/(a b)])]
where
r = Radius
a,b = Rectangle sides
Let's suppose we want rectangles with one side half of the other (b = a/2) and a sphere of radius 1.
So, the number of rectangles as a function of a gives:
Where you may see that a rectangle with a "large" side of size 2 allows 10 rectangles in the sphere, while a rectangle of "large" side 4 allows only 4 rectangles.