I have a question about Faking 3d in HTML5/Canvas/Javascript...
Basically, I have a 2d image drawn on a <canvas> using drawImage().
What I would like to do, is draw the image, then displace the texture on the sphere to look... sphere-like...
see image below for clarity:
Any ideas?
I have googled myself to near-death over this, also it cannot be WebGL as it has to work on Mobiles... is there a way to do this?
You can check a working prototype here: http://jsbin.com/ipaliq/edit
I bet there's ton's of room for optimization and better/faster algorithms, but I hope this proof of concepts will point you in the right direction.
The basic algorithm goes through each pixel of the original image and calculates its new position if it was subject to an spherical deformation.
Here's the result:
Code:
var w = 150, h = 150;
var canvas=document.getElementById("myCanvas");
var ctx=canvas.getContext("2d");
ctx.fillStyle="red";
ctx.fillRect(0,0,w,h);
//-- Init Canvas
var initCanvas = function()
{
var imgData=ctx.getImageData(0,0,w,h);
for (i=0; i<imgData.width*imgData.height*4;i+=4)
{
imgData.data[i]=i%150*1.5;
imgData.data[i+1]=i%150*1.5;
imgData.data[i+2]=(i/imgData.width)%150*1.5;
imgData.data[i+3]=255;
}
ctx.putImageData(imgData,0,0);
};
initCanvas();
var doSpherize = function()
{
var refractionIndex = 0.5; // [0..1]
//refraction index of the sphere
var radius = 75;
var radius2 = radius * radius;
var centerX = 75;
var centerY = 75;
//center of the sphere
var origX = 0;
var origY = 0;
for (x=0; x<w;x+=1)
for (y=0; y<h;y+=1)
{
var distX = x - centerX;
var distY = y - centerY;
var r2 = distX * distX + distY * distY;
origX = x;
origY = y;
if ( r2 > 0.0 && r2 < radius2 )
{
// distance
var z2 = radius2 - r2;
var z = Math.sqrt(z2);
// refraction
var xa = Math.asin( distX / Math.sqrt( distX * distX + z2 ) );
var xb = xa - xa * refractionIndex;
var ya = Math.asin( distY / Math.sqrt( distY * distY + z2 ) );
var yb = ya - ya * refractionIndex;
// displacement
origX = origX - z * Math.tan( xb );
origY = origY - z * Math.tan( yb );
}
// read
var imgData=ctx.getImageData(origX,origY,1,1);
// write
ctx.putImageData(imgData,x+w,y+h);
}
};
doSpherize();
Note
I would advice agains such an intense operation to be handed by the frontend without webGL pixel shaders. Those are very optimized and will be able to take advantage of GPU accelaration.
You can definitely do something like this. I'm not sure if the quality and speed will be good enough though, particularly on mobile.
You're going to want to getImageData the pixels, perform some mathematical transformation on them I can't think of at the moment, and then putImageData them back.
It's probably going to look kind of blurry or pixelated (you could try interpolating but that will only get you so far). And you may have to optimize your javascript considerably to get it done in a reasonable time on a mobile device.
The transformation is probably something like an inverse azimuthal projection.
Related
two.js library has a function as follows;
makeArcSegment two.makeArcSegment(ox, oy, ir, or, sa, ea, res);
I calculate my parameters as follows;
var prevPt = points[i - 1];
var dxNext = nextPt.x - pt.x;
var dyNext = nextPt.y - pt.y;
var angleNext = Math.atan2(dyNext, dxNext);
var dxPrev = prevPt.x - pt.x;
var dyPrev = prevPt.y - pt.y;
var anglePrev = Math.atan2(dyPrev, dxPrev);
var innerRadius = 0;
var outerRadius = 20;
var arc = two.makeArcSegment(pt.x, pt.y, innerRadius, outerRadius, anglePrev, angleNext)
And my output is kind of unexpected.
At first node (after the line with the length of 225.46), sweep direction is counter clockwise.
At second node (after 142.35), sweep direction is clockwise.How do I force it to be always counter clockwise?
Thanks.
This is the workaround I found.
if (anglePrev < 0.0)
{
anglePrev += Math.PI * 2.0;
}
if (angleNext < 0.0)
{
angleNext += Math.PI * 2.0;
}
if (angleNext > anglePrev)
{
anglePrev += Math.PI * 2.0;
}
According to this if you make the arc.endAngle less than the arc.startAngle then it should go in the opposite direction..
So you can try to draw between min(prev,next) and max(prev,next) as workaround.
Also make all angles positive (adding 2*Pi to negative ones).
Looks like serious flaw of library though.
I am trying to make a ball move slowly towards my mouse.
Im using paper.js which is a simple animation library. Using this i have a ball moving on screen. These are some of the properties of the ball:
balls[0].vector.angle is its direction. 0 = right, 90 = down, 180 = left etc and everything in between
balls[0].point.x is its x position and .y for y position.
balls[0].vector.length is its speed.
I have put in a mouse move event and i think ive got the angle between them below:
canvas.addEventListener("mousemove", function(e){
var a = balls[0].point.y - e.clientY;
var b = balls[0].point.x - e.clientX;
var angleDeg = Math.atan2(a, b) * 180 / Math.PI;
});
So i have made the ball stationary to test this and moved my mouse around it. To the left of the ball gives me 0 degrees. Above gives me 90. To the right gives me 180. And below the ball gives me -90 etc and everything in between.
I then calculated the distance in the same event and changed the speed to reflect the distance giving it a cap as max speed:
var distance = Math.sqrt( a*a + b*b );
var maxSpeed = 20;
balls[0].vector.length = (distance/30 > maxSpeed) ? maxSpeed : distance/30;
So ive tested the speed and this and it works perfect. When i give the ball the angle from earlier its going in all sorts of directions. The speed still works, its just the ball is going in the wrong direction and im not sure what ive done wrong.
Frankly, you don't need trig functions. All you need is good old Pythagoras theorem.
var MAX_SPEED = 20;
var MIN_SPEED = 0.25; // Very slow if close but not frozen.
var ATTRACTION = 0.5;
var diff_y = e.clientY - balls[0].point.y;
var diff_x = e.clientX - balls[0].point.x;
var distance = Math.sqrt(diff_x * diff_x + diff_y * diff_y)
var speed = distance * ATTRACTION;
if (speed > MAX_SPEED) speed = MAX_SPEED;
if (speed < MIN_SPEED) speed = MIN_SPEED;
// The rates along axes are proportional to speed;
// we use ratios instead of sine / cosine.
balls[0].point.x += (diff_x / distance) * speed;
balls[0].point.y += (diff_y / distance) * speed;
Much more fun can be had by introducing forces and inertia.
Specify the direction in terms of deltas
var deltaX = e.clientX - balls[0].point.x;
var deltaY = e.clientY - balls[0].point.y;
var distance = Math.sqrt(deltaX*deltaX+deltaY*deltaY);
var maxSpeed = 20;
balls[0].vector.length = (distance/30 > maxSpeed ) ? maxSpeed : distance / 30;
balls[0].point.x = balls[0].point.x + (balls[0].vector.length * deltaX / distance);
balls[0].point.y = balls[0].point.y + (balls[0].vector.length * deltaY / distance);
I think that will work
I'm working on a canvas-based animation, and I'm trying to get a 3D effect in a 2D canvas.
So far, things are going well! I've got my "orbiting line of triangles" working very well:
var c = document.createElement('canvas');
c.width = c.height = 100;
document.body.appendChild(c);
var ctx = c.getContext("2d");
function Triangles() {
this.rotation = {
x: Math.random()*Math.PI*2,
y: Math.random()*Math.PI*2,
z: Math.random()*Math.PI*2
};
/* Uncomment this for testing perspective...
this.rotation = {
x: Math.PI/2,
y: 0,
z: 0
};
*/
}
Triangles.prototype.draw = function(t) {
this.rotation.z += t/1000;
var i, points;
for( i=0; i<15; i++) {
points = [
this.computeRotation(Math.cos(0.25*i),-Math.sin(0.25*i),0),
this.computeRotation(Math.cos(0.25*(i+1)),-Math.sin(0.25*(i+1)),-0.1),
this.computeRotation(Math.cos(0.25*(i+1)),-Math.sin(0.25*(i+1)),0.1)
];
ctx.fillStyle = "black";
ctx.beginPath();
ctx.moveTo(50+40*points[0][0],50+40*points[0][1]);
ctx.lineTo(50+40*points[1][0],50+40*points[1][1]);
ctx.lineTo(50+40*points[2][0],50+40*points[2][1]);
ctx.closePath();
ctx.fill();
}
};
Triangles.prototype.computeRotation = function(x,y,z) {
var rz, ry, rx;
rz = [
Math.cos(this.rotation.z) * x - Math.sin(this.rotation.z) * y,
Math.sin(this.rotation.z) * x + Math.cos(this.rotation.z) * y,
z
];
ry = [
Math.cos(this.rotation.y) * rz[0] + Math.sin(this.rotation.y) * rz[2],
rz[1],
-Math.sin(this.rotation.y) * rz[0] + Math.cos(this.rotation.y) * rz[2]
];
rx = [
ry[0],
Math.cos(this.rotation.x) * ry[1] - Math.sin(this.rotation.x) * ry[2],
Math.sin(this.rotation.x) * ry[1] + Math.cos(this.rotation.x) * ry[2]
];
return rx;
};
var tri = new Triangles();
requestAnimationFrame(function(start) {
function step(t) {
var delta = t-start;
ctx.clearRect(0,0,100,100)
tri.draw(delta);
start = t;
requestAnimationFrame(step);
}
step(start);
});
As you can see it's using rotation matrices for calculating the position of the points after their rotation, and I'm using this to draw the triangles using the output x and y coordinates.
I want to take this a step further by using the z coordinate and adding perspective to this animation, which will make the triangles slightly bigger when in the foreground, and smaller when in the background. However, I'm not sure how to go about doing this.
I guess this is more of a maths question than a programming one, sorry about that!
Define a focal length to control the amount of perspective. The greater the value the less the amount of perspective. Then
var fl = 200; // focal length;
var px = 100; // point in 3D space
var py = 200;
var pz = 500;
Then to get the screen X,Y
var sx = (px * fl) / pz;
var sy = (py * fl) / pz;
The resulting point is relative to the center of the veiw so you need to center it to the canvas.
sx += canvas.width/2;
sy += canvas.height/2;
That is a point.
It assumes that the point being viewed is in front of the view and further than the focal length from the focal point.
I've managed to figure out a basic solution, but I'm sure there's better ones, so if you have a more complete answer feel free to add it! But for now...
Since the coordinate system is already based around the origin with the viewpoint directly on the Z axis looking at the (x,y) plane, it's actually sufficient to just multiply the (x,y) coordinates by a value proportional to z. For example, x * (z+2)/2 will do just fine in this case
There's bound to be a more proper, general solution though!
I am studying JavaScript and I discovered the following article: http://www.html5code.nl/tutorial-canvas-animation-spiral-movement/
Could you tell me how does the function spiralMotion1() works?
I would like to customize speed and distance.
edit: breaking it down to specifics: why use cos. and sin.? why use rotationRadius? how does the setAngle function influence the result? where does the degrees variable come into play?
the code:
function spiralMotion1(){
var degrees = 0;
var Angle;
var rotationRadius=2;
var rotationRadiusIncrease = 1;
var ballRadius=20
var centerX;
var centerY;
var x;
var y;
var animate=true;
var breadcrumbs = new Array();
var crumbRadius=1;
var canvas = jQuery("#spiral_motion1");
var context = canvas.get(0).getContext("2d");
//function Ball(x,y,radius,color,strokeColor,lineWidth) in ball.js
var ball_3 = new Ball(-10,-10,20,'#f00','#000',7);
var parentWidth=jQuery(canvas).parent().width();
var canvasWidth=context.canvas.width = parentWidth;
var canvasHeight=context.canvas.height= 288;
if (!checkForCanvasSupport) {
return;
}
(function drawFrame() {
window.requestAnimationFrame(drawFrame, canvas);
if(animate){
context.clearRect(0,0,canvasWidth,canvasHeight); // clear canvas
//Make the Canvas element responsive for desktop, tablet and smartphone.
centerX = canvasWidth/2;
centerY = canvasHeight/2
Angle = degrees * (Math.PI / 180);
degrees = degrees + 1;
ball_3.x=rotationRadius * Math.cos(setAngle()) + centerX;
ball_3.y=rotationRadius * Math.sin(setAngle()) + centerY;
ball_3.draw(context);
//add a breadcrumb to the breadcrumbs array
breadcrumbs.push({x:ball_3.x,y:ball_3.y});
//draw the breadcrumbs that shows the track of the movement
context.globalCompositeOperation = "destination-over";
showBreadcrumbs(breadcrumbs);
rotationRadius += rotationRadiusIncrease/5
if ((ball_3.y + ballRadius+4) > canvas.height()){
animate=false;
}
}
}());//end drawFrame
function setAngle(){
Angle = degrees * (Math.PI / 180);
degrees = degrees + 2;
return Angle;
}//end setAngl()
function showBreadcrumbs(breadcrumbs){
for (var i = 0; i< breadcrumbs.length; i++) {
context.beginPath();
context.arc(breadcrumbs[i].x,breadcrumbs[i].y,crumbRadius,0, 2*Math.PI,false);
context.closePath();
context.fillStyle="#999";
context.fill();
}
}//end showBreadcrumbs()
}//end spiralMotion1()
It boils down to basic geometry. If you think of a body orbiting a point in 2D, it's movement can be characterised by a radius (distance from the orbited point), and an angle which is a function of time. If you know the radius and the angle, then you can calculate the body position with the cos and sin function.
]1
By changing the radius over time, you obtain a spiral instead of a simple circle.
The math behind this question has been asked numerous times, so that's not specifically what I'm after. Rather, I'm trying to program the equation for determining these points into a loop in JavaScript, so that I can display points the evenly around the circle.
So with the equations for the X and Y positions of the points:
pointX = r * cos(theta) + centerX
pointY = r * sin(theta) + centerY
I should be able to calculate it with this:
var centerX = 300;
var centerY = 175;
var radius = 100;
var numberOfPoints = 8;
var theta = 360/numberOfPoints;
for ( var i = 1; i <= numberOfPoints; i++ ) {
pointX = ( radius * Math.cos(theta * i) + centerX );
pointY = ( radius * Math.sin(theta * i) + centerY );
// Draw point ( pointX , pointY )
}
And it should give me the x,y coordinates along the perimeter for 8 points, spread 45° from each other. But this doesn't work, and I'm not understanding why.
This is the output that I get (using the HTML5 Canvas element). The points should reside on the innermost red circle, as that one has a
Incorrect:
When it "should" look like this (although this is with just 1 point, placed manually):
Correct:
Could someone help me out? It's been years since I took trig, but even with looking at other examples (from various languages), I don't see why this isn't working.
Update: Figured it out!
I didn't need to add the centerX and centerY to each calculation, because in my code, those points were already relative to the center of the circle. So, while the canvas center was at point (300, 175), all points were relative to the circle that I created (the stroke line that they need to be placed on), and so the center for them was at (0, 0). I removed this from the code, and split the theta and angle calculations into two variables for better readability, and voila!
totalPoints = 8;
for (var i = 1; i <= totalPoints ; i++) {
drawPoint(100, i, totalPoints);
}
function drawPoint(r, currentPoint, totalPoints) {
var theta = ((Math.PI*2) / totalPoints);
var angle = (theta * currentPoint);
electron.pivot.x = (r * Math.cos(angle));
electron.pivot.y = (r * Math.sin(angle));
return electron;
}
Correct:
cos and sin in Javascript accept an argument in radians, not degrees. You can change your theta calculation to
var theta = (Math.PI*2)/numberOfPoints;
See the Math.cos documentation for details
#Emmett J. Butler's solution should work. The following is a complete working example
// 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 centerX = 150;
var centerY = 175;
var radius = 100;
var numberOfPoints = 8;
var theta = 2.0*Math.PI/numberOfPoints;
ctx.beginPath();
for ( var i = 1; i <= numberOfPoints; i++ ) {
pointX = ( radius * Math.cos(theta * i) + centerX );
pointY = ( radius * Math.sin(theta * i) + centerY );
ctx.fillStyle = "Red";
ctx.fillRect(pointX-5,pointY-5,10,10);
ctx.fillStyle = "Green";
}
ctx.stroke();