Animated footsteps in html svg - javascript
Is it possible to create small animated footsteps in HTML SVG or Canvas. I am just starting out with these technologies, and it is very much necessary for a small project i am working on.
My current idea was to create and use a "gif" of animated footsteps. But i would like to know if it can be achieved in any other way through HTML/CSS/JS
PS : The footsteps i keep mentioning should be similar to the footsteps that appear on the "Marauders Map" in harry potter Movies.
Thanks for any help
Walk about.
I have never seen the movie you talk about so I am guessing what you are after.
To do it on the canvas is easy, and I am more than happy to write an example of how it's done for you.
Draw a foot
You need an image of a foot, or some way to draw a foot. I used a set of shapes I happened to have to draw a foot.
Then create a function that draws the foot at a location and in a direction. You also need to specify if its a left or right step.
If you have a foot image you want to use just replace the code in the drawFoot function after the comment // draw the foot... with ctx.drawImage(footImage,-footImage.width / 2, -footImage.height / 2);
You may have to scale the image, and the foot image should be of the left foot toes pointing to the right of screen
Path
The path to walk along is just an array of points, [x,y,x,y...] in order that define the path to travel along, They are just a guide as each foot step is a fixed distance apart and the points can be any distance appart.
Animate
Each step is some time apart (demo is 500ms or half a second). I use setTimeout to call the function to draw the next step. When the function is called I find the next position on the path and draw the next foot step.
When the walk has gone past the end of the path I clear the canvas and restart.
Demo
It's self explanatory in the demo. I track two positions on the path, one is where the foot step is, and the other is just ahead and used to get the direction the foot should be drawn.
The function that gets the distance along the path is recursive because the path points are not uniform in distance apart, so may have to skip line segments if the distance to travel is greater than the current or next or few line segments.
// create a canvas and add it to the DOM
var createImage=function(w,h){var i=document.createElement("canvas");i.width=w;i.height=h;i.ctx=i.getContext("2d");return i;}
var canvas = createImage(1024,512);
var ctx = canvas.ctx;
document.body.appendChild(canvas);
// shapes for the foot. Left foot
const foot = [
[50.86759744022923,-21.798383338076917,54.16000854997335,-23.917474843549847,57.778065334829385,-25.310771525314685,61.579706305344985,-24.823762596975733,65.0168431653022,-22.69100336700319,65.22736925598322,-19.777045647294724,63.09649708656406,-16.826669881834157,59.66512409715465,-15.356252875428147,56.12216018899968,-14.92259970211097,52.06404407417057,-16.231839553378,50.2945579442406,-18.938589556263246],
[60.12039562389232,-12.45714817900668,63.92394094034509,-14.994440399059425,68.75013312287521,-15.737202635924493,73.10937504616481,-14.878459739068003,76.36064492186433,-12.559833524837757,77.6863729180915,-9.181208344485064,75.4625672565435,-5.673231626251427,71.79886053681197,-3.7381471592011817,66.8618942467243,-3.4903609701416993,62.29264392518654,-5.1248680438596885,58.98975517513061,-8.760952968033395],
[65.57414109270752,1.1797411270282658,69.37768640916029,-1.3575510930244814,74.20387859169041,-2.1003133298895467,78.56312051498001,-1.241570433033059,81.81439039067953,1.0770557811971881,83.14011838690669,4.455680961549877,80.9163127253587,7.963657679783514,77.25260600562717,9.898742146833756,72.3156397155395,10.146528335893239,67.74638939400174,8.512021262175251,64.44350064394581,4.875936338001546],
[65.89915812212375,15.917267748549033,69.97688522977245,12.635767809535894,76.25232402290966,11.736330263416008,81.47328014710077,12.477566678331382,86.09545897877996,15.579569438835726,86.99032987455637,21.425830739951824,83.82647235747945,24.97297628549917,79.18353918133074,27.064789354098487,73.69821507617947,27.23418460503707,68.46309469655478,24.972976285499172,64.88602415530316,20.55351481505123],
[58.48881251239855,36.09589759380796,65.7080603859302,29.82038065752831,74.19222753183148,28.331948884004674,82.75081761131048,31.085582242549528,88.10923922724437,34.28575070762116,91.45825273720305,41.65358042953028,87.067323913035,47.45853718012533,79.77391671356942,50.28659303297933,71.73628428966856,50.06332546564875,64.14518700042888,47.45853718012533,60.12637078847845,42.69549574373964],
[-73.48953442580058,20.579088801900756,-80.48690958722098,13.959950657259256,-81.93681598574045,6.269142804242765,-81.49554012532147,-1.6107832746678348,-77.90207999991593,-9.181527272091415,-71.6611785393426,-14.98115303708649,-64.60076477263831,-17.880965834138024,-57.35123278004056,-19.078714598132443,-50.09111381970131,-19.902008890566687,-42.96765884171789,-19.08249722231963,-35.655087439697766,-18.51514254492067,-28.90987071615029,-18.578181953551955,-21.74774447703016,-19.60773669210723,-14.309090001741001,-20.364210136314323,-6.933479190821032,-21.688037717705875,0.33383104043200396,-22.888118253462963,7.772483543580581,-23.77067027395373,15.274173171058239,-24.338024951681817,22.460665755024706,-24.590182586206954,29.710197747622452,-23.707630865368966,36.8557533915613,-22.565778766908277,44.16832856198768,-19.28772830000901,51.48089996424725,-14.370654426435763,56.713170880643965,-7.499358885625703,60.24016927073608,-0.41616112008138906,61.75311234056134,6.518193267525415,62.38350642684393,13.515567625813127,61.67231220934209,20.50500542283382,59.08769637136125,27.56541945045329,54.35974072401175,34.87799085169213,48.686193947196124,39.41682827314464,41.87793781501736,42.379680478815025,34.313208779263185,43.26223219965301,27.063676786665432,42.25360166155246,19.625026568173826,38.28211891778152,13.457927624759604,31.720792179781256,9.486440924356895,25.290772320002496,6.019273449215143,18.7346738223298,0.21964785513691965,13.565442314564446,-5.832135373466421,9.467880753530935,-12.632472285211591,8.882365666622222,-19.188577231399584,11.277861070017005,-26.31203040678842,14.49287091019486,-32.99420772170462,17.833959567652954,-39.2981485848331,20.670732956060768,-45.854247082486715,23.192309301312157,-52.47338498877162,24.89437333435685,-59.5968381641068,25.020452151619416,-67.33461307855538,23.697386555044208],
]
const footScale = 0.2; // how big the foot is
const stepLen = 60; // distance between steps
var stepCount = 0; // current set num so left and right can be known
var stepTime = 500; // time ms between steps
// path to walk
const path = [56.20191403126277,162.4451015086347,83.38688266344644,172.80127602121507,107.98280549274708,192.86637096042372,121.57528916149568,221.34586055208607,124.81159479691195,256.2979614145819,141.64038410107662,293.8391067854107,168.82535143857336,311.9624183437419,203.77745230106916,336.5583411729056,238.0822920364817,344.9727358249879,278.2124819156436,341.0891690624884,316.40088841355566,329.43846877498976,343.58585575105235,310.6678960895754,370.77082308854904,275.7157952270795,359.12012280105046,244.64726112708325,344.23311687813566,207.10611575625444,355.23655603855093,168.9177092583423,394.0722236635463,137.2019140312628,438.0859803052077,137.84917515834604,487.27782596353507,174.0957982750084,507.9901820301992,221.9931216791693,513.1682710468652,269.243183956247,500.87030963228347,318.43502961457443,480.1579535656192,354.68165273123674,453.62744426338134,396.86543776550707,414.1445200788371,427.9340428271046,372.7198079555102,447.3518767949864,320.2916566617712,442.173787778395,272.39433325761024,427.9340429825634,218.02439858261675,441.5265266513118,185.66134222845398,472.59506075130815,160.418158272207,514.6670340117198,168.2291881671332,557.5405924870362,200.59229872785795,598.9654951914354,232.9553551615553,627.4449850627141,273.08554504131894,651.3936467669101,320.3356073183967,663.0443470544095,368.2329307225576,663.6916081814927,417.4247763808851,649.4518633856611,460.7912718954633,626.150462810664,509.33585642670744,593.1401453294179,530.6954736204549,556.8935222127556,559.9273870166451,517.9197870310509,582.4287517306153,484.11964343037323,597.1560832290169,459.03222473087396,621.0274949086466,438.11039022321154,651.3689440081158,429.43667093843567,686.3731150817684,432.05029606733103,726.1878666750503,421.6902139845064,748.5744620042316,397.8927935245363,778.6337708564557,367.5111094723503,792.6287871481064,335.0802046803193,795.4641381478963,294.8623601925252,790.3177294792127,255.26933447013639,776.3228370821212,225.344431214269,746.3711518226298,192.73203550406103,713.7991149596966,199.06094085265394,674.3068624609349,207.5062077919911,638.4763261746227,190.31310645331482,613.6509940547375,146.74931837304698,621.5992452450397,103.454341485492,665.5124383180124,60.96567428151931,716.1845355322713,48.49595708249788,763.6383682758693,51.23726133320403,810.1045243122669,71.53440096982465,842.407749982487,97.97907893142005,879.5993779794437,131.14717279570036,903.6790094213126,175.24174017377706,915.9471803279671,219.31612086267396,902.1335310600084,270.1561321514687,880.1365756762476,315.0232456643523,884.5103070340778,370.89556334366307,909.7723644212043,407.9691345807976,947.0675376346722,439.8492118274288,990.6429384281869,439.26727537005956,1036.8675099917996,414.23364852545194,1070.3264506272462,387.0500494883014,1100.6074853525497,351.4546920217324,1119.943854180156,306.7958514306488,1128.5371035594999,259.67124425611814,1122.6651029017848,208.79760059460207,1106.8898009575623,162.16340658911932,1082.4004208812885,108.81054339506417,1050.2046949092428,81.72759371897288,1016.627194271211,46.42875173061529];
const pLen = path.length;
// fix the path so it starts at zero
for(var i = 2; i < pLen; i += 2){
path[i] -= path[0];
path[i+1] -= path[1];
}
path[0] = path[1] = 0;
// tracks the foot pos
var pos = {
x : path[0],
y : path[1],
index : 0,
};
// tracks the foot pointing to pos
var pos1 = {
x : path[0],
y : path[1],
index : 0,
};
// get a distance alone the path
function getDistOnPath(dist,pos){
var nx = path[(pos.index + 2) % pLen] - pos.x;
var ny = path[(pos.index + 3) % pLen] - pos.y;
var d = Math.hypot(nx, ny);
if(d > dist){
pos.x += (nx / d) * dist;
pos.y += (ny / d) * dist;
return pos;
}
dist -= d;
pos.index += 2;
pos.x = path[pos.index % pLen];
pos.y = path[(pos.index + 1) % pLen];
return getDistOnPath(dist, pos);
}
function drawFoot(x,y,dir,left){
var i,j,shape;
var xdx = Math.cos(dir) * footScale;
var xdy = Math.sin(dir) * footScale;
if(left){
ctx.setTransform(xdx, xdy, -xdy, xdx, x + xdy * 50, y - xdx * 50);
ctx.rotate(-0.1); // make the foot turn out a bit
}else{
ctx.setTransform(xdx, xdy, -xdy, xdx, x - xdy * 50, y + xdx * 50);
ctx.rotate(-0.1); // make the foot turn out a bit
ctx.scale(1,-1); // right foot needs to be mirrored
}
// draw the foot as a set of paths points
ctx.beginPath();
for(j = 0; j < foot.length; j ++){
shape = foot[j];
i = 0;
ctx.moveTo(shape[i++],shape[i++]);
while(i < shape.length){
ctx.lineTo(shape[i++],shape[i++]);
}
ctx.closePath();
}
ctx.fill();
}
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
pos1 = getDistOnPath(stepLen/10,pos1); // put the second pos infront so that a direction can be found
function drawStep(){
if(pos.index > pLen){ // if past end of path clear and restart
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
pos.index = 0;
pos1.index = 0;
pos1.x = pos.x = path[0];
pos1.y = pos.y = path[1];
pos1 = getDistOnPath(stepLen/10,pos1);
}
pos = getDistOnPath(stepLen,pos);
pos1 = getDistOnPath(stepLen,pos1);
drawFoot(pos.x,pos.y,Math.atan2(pos1.y - pos.y, pos1.x - pos.x),(stepCount++) % 2 === 0);
setTimeout(drawStep,stepTime);
}
drawStep();
Note some of the code is ES6 and will require babel (or equivilent) to run on legacy browsers.
Related
Finding length of arc on unit circle only given x position
Some background: I've been trying to map a texture onto a "sphere" using a look up table of texture co-ordinate changes. This is for a really slow micro controller to draw on a little LCD panel. So Three.JS is out, WebGL etc... the look up table should work! The equations for texturing a sphere all pinch the poles. I can't "pre-spread" the texture of these extremes because the texture offset changes to make the "sphere" appear to rotate. If you examine the code for making the lookup table here, you'll see the approach, and the running demo shows the issue. https://codepen.io/SarahC/pen/KKoKqKW I figured I'd try and come up with a new approach myself! After thinking a while, I realised a sphere texture in effect moves the location of the texture pixel further from the spheres origin the further away from the origin it is! In a straight line from the origin. So I figured - calculate the angle the current pixel is from the origin, calculate it's unit distance, then all I need to do is make a function that is given the distance, and calculates the new distance based on some "sphere calculation". That new distance is almost 1 to 1 near the center of the sphere, and rapidly increases right at the edges. Mapping a sphere texture! That offset function I figured (may be wrong here?) (diagrammed below) given the distance from the origin L1 (unit circle) it returns the length of the arc L2 which in effect is the flat pixel offset to use from the source texture. (I asked on Reddit, and got given Math.acos for X, but now I know that's wrong, because that's the X position of the circle! Not the straight line X position from the offset, AND it gives an angle, not the Y position... wrong on two counts. Oooph! Oddly, surprisingly, because I thought it gave the Y position, I dropped it into an atan2 function, and it ALMOST worked... for all the wrong reasons of course but it made a bump at the center of the "sphere". The current "state of the mistake" is right here: https://codepen.io/SarahC/pen/abYbgwa?editors=0010 ) Now I know that aCos isn't the function I need, I'm at a loss for the solution. But! Perhaps this approach I thought up is stupid? I'd happily use any look-up mapping function you think would work. =) Thanks for your time and reading and sharing, I like learning new stuff. //JS
An interesting but befuddling problem... Per Spektre's comment and my follow up comment, the mapping of x to the length of the circle's arc still resulted in the center bubble effect of the image as described in the question. I tried numerous mathematically "correct" attempts, including picking a distant view point from the sphere and then calculating the compression of the 2d image as the view point angle swept from top center of the sphere to the edge, but again, to no avail, as the bubble effect persisted... In the end, I introduced a double fudge factor. To eliminate the bubble effect, I took the 32nd root of the unit radius to stretch the sphere's central points. Then, when calculating the arc length (per the diagram in the question and the comments on "L2") I undid the stretching fudge factor by raising to the 128th power the unit radius to compress and accentuate the curvature towards the edge of the sphere. Although this solution appears to provide the proper results, it offends the mathematician in me, as it is a pure fudge to eliminate the confusing bubble effect. The use of the 32nd root and 128th power were simply arrived at via trial and error, rather than any true mathematical reasoning. Ugh... So, FWIW, the code snippet below exemplifies both the calculation and use of the lookup table in functions unitCircleLut2() and drawSphere2(), respectively... // https://www.reddit.com/r/GraphicsProgramming/comments/vlnqjc/oldskool_textured_sphere_using_lut_and_texture_xy/ // Perhaps useable as a terminator eye?........ // https://www.youtube.com/watch?v=nSlEQumWLHE // https://www.youtube.com/watch?v=hx_0Ge4hDpI // This is an attempt to recreate the 3D eyeball that the Terminator upgrade produces on the Adafruit M4sk system. // As the micro control unit only has 200Kb RAM stack and C and no 3D graphics support, chances are there's no textured 3D sphere, but a look up table to map an eyeball texture to a sphere shape on the display. // I've got close - but this thing pinches the two poles - which I can't see happening with the M4sk version. // Setup the display, and get its pixels so we can write to them later. let c = document.createElement("canvas"); c.width = 300; c.height = 300; document.body.appendChild(c); let ctx = c.getContext("2d"); let imageDataWrapper = ctx.getImageData(0, 0, c.width, c.height); let imageData = imageDataWrapper.data; // 8 bit ARGB let imageData32 = new Uint32Array(imageData.buffer); // 32 bit pixel // Declare the look up table - dimensions same as display. let offsetLUT = null; // Texture to map to sphere. let textureCanvas = null; let textureCtx = null; let textureDataWrapper = null; let textureData = null; let textureData32 = null; let px = 0; let py = 0; let vx = 2; let vy = 0.5; // Load the texture and get its pixels. let textureImage = new Image(); textureImage.crossOrigin = "anonymous"; textureImage.onload = _ => { textureCanvas = document.createElement("canvas"); textureCtx = textureCanvas.getContext("2d"); offsetLUT = unitCircleLut2( 300 ); textureCanvas.width = textureImage.width; textureCanvas.height = textureImage.height; textureCtx.drawImage(textureImage, 0, 0); textureDataWrapper = textureCtx.getImageData(0, 0, textureCanvas.width, textureCanvas.height); textureData = textureDataWrapper.data; textureData32 = new Uint32Array(textureData.buffer); // Draw texture to display - just to show we got it. // Won't appear if everything works, as it will be replaced with the sphere draw. for(let i = 0; i < imageData32.length; i++) { imageData32[i] = textureData32[i]; } ctx.putImageData(imageDataWrapper, 0, 0); requestAnimationFrame(animation); } textureImage.src = "https://untamed.zone/miscImages/metalEye.jpg"; function unitCircleLut2( resolution ) { function y( x ) { // x ** 128 compresses when x approaches 1. This helps accentuate the // curvature of the sphere near the edges... return ( Math.PI / 2 - Math.acos( x ** 128 ) ) / ( Math.PI / 2 ); } let r = resolution / 2 |0; // Rough calculate the length of the arc... let arc = new Array( r ); for ( let i = 0; i < r; i++ ) { // The calculation for nx stretches x when approaching 0. This removes the // center bubble effect... let nx = ( i / r ) ** ( 1 / 32 ); arc[ i ] = { x: nx, y: y( nx ), arcLen: 0 }; if ( 0 < i ) { arc[ i ].arcLen = arc[ i - 1 ].arcLen + Math.sqrt( ( arc[ i ].x - arc[ i - 1 ].x ) ** 2 + ( arc[ i ].y - arc[ i - 1 ].y ) ** 2 ); } } let arcLength = arc[ r - 1 ].arcLen; // Now, for each element in the array, calculate the factor to apply to the // metal eye to either stretch (center) or compress (edges) the image... let lut = new Array( resolution ); let centerX = r; let centerY = r; for( let y = 0; y < resolution; y++ ) { let ny = y - centerY; lut[ y ] = new Array( resolution ); for( let x = 0; x < resolution; x++ ) { let nx = x - centerX; let nd = Math.sqrt( nx * nx + ny * ny ) |0; if ( r <= nd ) { lut[ y ][ x ] = null; } else { lut[ y ][ x ] = arc[ nd ].arcLen / arcLength; } } } return lut; } function drawSphere2(dx, dy){ const mx = textureCanvas.width - c.width; const my = textureCanvas.height - c.height; const idx = Math.round(dx); const idy = Math.round(dy); const textureCenterX = textureCanvas.width / 2 |0; const textureCenterY = textureCanvas.height / 2 |0; let iD32index = 0; for(let y = 0; y < c.height; y++){ for(let x = 0; x < c.width; x++){ let stretch = offsetLUT[y][x]; if(stretch == null){ imageData32[iD32index++] = 0; }else{ // The 600/150 is the ratio of the metal eye to the offsetLut. But, since the // eye doesn't fill the entire image, the ratio is fudged to get more of the // eye into the sphere... let tx = ( x - 150 ) * 600/150 * Math.abs( stretch ) + textureCenterX + dx |0; let ty = ( y - 150 ) * 600/150 * Math.abs( stretch ) + textureCenterY + dy |0; let textureIndex = tx + ty * textureCanvas.width; imageData32[iD32index++] = textureData32[textureIndex]; } } } ctx.putImageData(imageDataWrapper, 0, 0); } // Move the texture on the sphere and keep redrawing. function animation(){ px += vx; py += vy; let xx = Math.cos(px / 180 * Math.PI) * 180 + 0; let yy = Math.cos(py / 180 * Math.PI) * 180 + 0; drawSphere2(xx, yy); requestAnimationFrame(animation); } body { background: #202020; color: #f0f0f0; font-family: arial; } canvas { border: 1px solid #303030; }
wall collision detection with html5 canvas [closed]
Closed. This question needs debugging details. It is not currently accepting answers. Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question. Closed 7 years ago. Improve this question I am making a game and there is a wall that I don't want the player to pass. I am using html5 canvas and have a player object to hold the x and y values. The wall is at x: 650 and y: 0. Since the player is 20x20 pixels when its x coordinate is 630, it touches the wall. if(player.x > 630 && player.y <= 500) { player.x = 630; } What is wrong with this code? I appreciate any help!
Answer The code you have give is OK, there is nothing wrong with it. So I suspect the problem is elsewhere in the code, most likely in the movement code. If you are moving the player after the wall test and then display it, the player may start to creep into the wall, but without the rest of the code it is hard to know what is wrong with your code. I have included more details on the correct way to do collision tests as there are two answers showing only a partial solution. It is there as a general guide to collision testing and may not be directly applicable to the question. Inter frame movement The correct way to reflect an object from a surface. You must take into account that the ball is moving between frames and that the collision may have happened at any time during the previous frame. The ball's distance from the wall after the collision is dependent on when during the previous frame it hit the wall. This is important if the ball moves slowly or quickly. var dx = 10; // delta x velocity of object in pixels var wx = 10; // width of object in pixels var px = 90; // position of object in pixels var wallX = 105; // position of wall px += dx; // move the ball. Its position is now 100. // its right side is at px + wx = 110. // test if it has it the wall if(px+wx > wallX){ dx = -dx; // reflect delta x // The object is 5 pixel into the wall. // The object has hit the wall some time during the last frame // We need to adjust the position as the ball may have been // traveling away from the wall for some time during the last frame. var dist = (px+wx)-wallX; // get the distance into the wall px -= dist*2; // the object hit the wall at position 95 and has been // traveling away since then so it is easy to just // subtract 2 times the distance the ball entered the wall // the above two lines can be done in one // px -= ((px+wx)-wallX)*2; } Why it matters Below is a simulation of a ball bouncing inside the canvas. To illustrate that the ball is moving between frames it has been motion blurred to show its motion between frames. Please note this is not the perfect solution as the bounce is assumed to occur while the ball is in linear motion while infact it is in freefall and under constant acceleration. But it still conserves energy. In the correct test the height the ball bounces back to, stays around the same over time. No energy is lost or gained. Right click to turn off the inter frame adjustment and you will notice that the ball begins to decrease its height each frame. This is because at each collision the ball loses a little energy because it motion during the previous frame is not taken into account when positioning it after the collision test. It will settle down to a constant rate when the collision occurres at precisely the frame time. When that will be is very hard to determine in advance. Left click to slow the simulation frame rate, left click again to return to normal. The code below is not really part of the answer, it is there to demonstrate the effect of not correctly adjusting the position during collision test on the overall accuracy of the simulation. // helper functions. NOT part of the answer var canvas = document.getElementById("canV"); var ctx = canvas.getContext("2d"); var mouseButton = 0; canvas.addEventListener('mousedown',function(event){mouseButton = event.which;}); canvas.addEventListener('mouseup' ,function(){mouseButton = 0;}); canvas.addEventListener("contextmenu", function(e){ e.preventDefault();}, false); var currentSurface = ctx; var createImage = function (w, h) {// create an canvas image of size w,h and attach context 2d var image = document.createElement("canvas"); image.width = w; image.height = h !== undefined?h:w; currentSurface = image.ctx = image.getContext("2d"); return image; } var setColour = function (fillC, strokeC, lineW) { currentSurface.fillStyle = fillC !== undefined ? fillC : currentSurface.fillStyle; currentSurface.strokeStyle = strokeC !== undefined ? strokeC : currentSurface.strokeStyle; currentSurface.lineWidth = lineW !== undefined ? lineW : currentSurface.lineWidth; } var circle = function(x,y,r,how){ currentSurface.beginPath(); currentSurface.arc(x,y,r,0,Math.PI*2); how = how.toLowerCase().replace(/[os]/g,"l"); // how to draw switch(how){ case "f": // fill currentSurface.fill(); break; case "l": currentSurface.stroke(); break; case "lf": currentSurface.stroke(); currentSurface.fill(); break; case "fl": currentSurface.fill(); currentSurface.stroke(); break; } } function createGradImage(size,col1,col2){ var image = createImage(size); var g = currentSurface.createLinearGradient(0,0,0,currentSurface.canvas.height); g.addColorStop(0,col1); g.addColorStop(1,col2); currentSurface.fillStyle = g; currentSurface.fillRect(0,0,currentSurface.canvas.width,currentSurface.canvas.height); return image; } function createColouredBall (ballR,col) { var ball = createImage(ballR*2); var unit = ballR/100; setColour("black"); circle(ballR,ballR,ballR,"f"); setColour("hsl("+col+",100%,30%)"); circle(ballR-unit*3,ballR-unit*3,ballR-unit*7,"f"); setColour("hsl("+col+",100%,50%)"); circle(ballR-unit*10,ballR-unit*10,ballR-unit*16,"f"); setColour("White"); circle(ballR-unit*50,ballR-unit*50,unit*16,"f"); return ball; } //=================================== // _ // /_\ _ _ ____ __ _____ _ _ // / _ \| ' \(_-< V V / -_) '_| // /_/ \_\_||_/__/\_/\_/\___|_| // // ================================== // Answer code // lazy coder variables var w = canvas.width; var h = canvas.height; // ball is simulated 5cm var pixSize = 0.24; // in millimeters for simulation // Gravity is 9.8 ms^2 so convert to pixels per frame squared // Assuming constant 60 frames per second. () var gravity = 9800*pixSize/60; gravity *= 0.101; // because Earth's gravity is stupidly large let's move to Pluto // ball 5cm var ballR = (25/pixSize)/2; // radius is 2.5cm for 5cm diamiter ball var ballX = w/2; // get center of canvas var ballY = ballR+3; // start at the top var ballDX = (Math.random()-0.5)*15; // start with random x speed ballDX += ballDX < 0 ? -5 : 5; // make sure it's not too slow var ballDY = 0; // star with no downward speed; var ballLastX = ballX; var ballLastY = ballY; //create an image of the Ball var ball = createColouredBall(ballR,Math.floor(Math.random()*360)); // create an image of ball // create a background. Image is small as it does not have much detail in it var background = createGradImage(16,"#5af","#08C"); // time to run for var runFor = 10*60; // ten secons yimes 60 frames per second // draws the ball motion blured. This introduces extra complexity var drawMotionBlur = function(image,px,py,dx,dy,steps){ var i,sx,sy; sx = dx / steps; sy = dy / steps; px -= dx; // move back to start position py -= dy; ctx.globalAlpha = 1/(steps*0.8); // set alpha to slightly higher for each step for(i = 0; i < steps; i+= 1){ ctx.drawImage(image,px+i*sx,py+i*sy); } ctx.globalAlpha = 1; // reset alpha } // style for text ctx.fillStyle = "white"; ctx.strokeStyle = "black"; ctx.textAlign = "center"; ctx.lineJoin = "round"; // stop some letters getting ears. ctx.lineWidth = 3; ctx.textBaseline = "bottom"; var textCenterX = w/2; var maxHeight = Infinity; var lastMaxHeight = ballY; var slowMotion = false; // slow motion flag var frameTravel = true; // use frame travel in collision test var update = function(){ var blurSteps = 10; // motion blur ball render steps const bSteps = 10; if(mouseButton === 1){ slowMotion = ! slowMotion; mouseButton = 0; } if(mouseButton === 3){ frameTravel = ! frameTravel; ballX = w/2; // get center of canvas ballY = ballR+3; // start at the top ballDY = 0; // start at 0 y speed mouseButton = 0; } // clear the canvas with background canvas image ctx.drawImage(background,0,0,w,h); ballDY += gravity; // accelrate due to grav // add deltas to ball position ballX += ballDX; ballY += ballDY; // test for collison on left and right walls. Need to // ajust for motion blur if (ballX < ballR) { ballDX = -ballDX; // refect delta x if (frameTravel) { // if using frame travel time // blur the outward traveling ball only for the time it has been traveling away blurSteps = Math.ceil(10 * ((ballX - ballR) / -ballDX)); // get position it should have traveled since ballX -= (ballX - ballR) * 2; }else{ ballX = ballR; // move ball to touching wall blurSteps = 1; // there is no outward motion } } else if (ballX > w - ballR) { ballDX = -ballDX; if (frameTravel) { // if using frame travel time // blur the outward traveling ball only for the time it has been traveling away blurSteps = Math.ceil(10 * ((ballX - (w - ballR)) / -ballDX)); ballX -= (ballX - (w - ballR)) * 2; }else{ ballX = w - ballR; // move ball to touching wall blurSteps = 1; // there is no outward motion } } if (ballY > h - ballR) { ballDY = -ballDY; // to show max height lastMaxHeight = maxHeight; maxHeight = Infinity; if (frameTravel) { // if using frame travel time // blur the outward traveling ball only for the time it has been traveling away blurSteps = Math.ceil(10 * ((ballY - (h - ballR)) / -ballDY)); ballY -= (ballY - (h - ballR)) * 2; }else{ ballY = h - ballR; // move ball to touching wall blurSteps = 1; // there is no outward motion } } // draw the ball motion blured drawMotionBlur( ball, // image to draw ballX - ballR, // offset radius ballY - ballR, ballDX * (blurSteps / bSteps), // speed and adjust for bounced ballDY * (blurSteps / bSteps), blurSteps // number of blurs ); // show max height. Yes it is min but everything is upside down. maxHeight = Math.min(maxHeight,ballY); lastMaxHeight = Math.min(ballY,lastMaxHeight); // show max height ctx.font = "12px arial black"; ctx.beginPath(); ctx.moveTo(0,lastMaxHeight - ballR); ctx.lineTo(w,lastMaxHeight - ballR); ctx.stroke(); ctx.fillText("Max height.",40,lastMaxHeight - ballR + 6); var str = ""; // display status string if(slowMotion){ // show left click help str += "10fps." ctx.fillText("click for 60fps.",textCenterX,43); }else{ str += "60fps." ctx.fillText("click for 10fps.",textCenterX,43); } if(frameTravel){ // show mode and right click help str += " Mid frame collision."; ctx.fillText("Right click for Simple collision",textCenterX,55); }else{ str += " Simple collision."; ctx.fillText("Right click for mid frame collision",textCenterX,55); } // display help text ctx.font = "18px arial black"; ctx.strokeText(str,textCenterX,30); ctx.fillText(str,textCenterX,28); if(slowMotion){ setTimeout(update,100); // show in slow motion }else{ requestAnimationFrame(update); // request next frame (1/60) seconds from now } // all done } update(); // to start the ball rolling .canC { width:500px; height:500px;} <canvas class="canC" id="canV" width=500 height=500></canvas>
For a wall running along the X axis at the bottom (Y = 0) of a 650 x 650 field, we would want: if (player.y <= 20) { player.y = 20; } For a wall running along the Y axis at the left side (X = 0) of a 650 x 650 field, we would want: if (player.x <= 20) { player.x = 20; } For a wall running along the Y axis at the right side (X = 650) of a 650 x 650 field, we would want: if (player.x >= 630) { player.x = 630; } For a wall running along the X axis at the top (Y = 650) of a 650 x 650 field, we would want: if (player.y >= 630) { player.y = 630; }
This code is similar to the code I use, if we attach horizontal (h) and vertical (v) velocity attributes to the player object we can multiply them by negative one to get them to bounce off of the wall if the player is going to go beyond the bounds. Or if you want it to stop, set them equal to zero at the wall. //player.x+player.h gives us the future position of the player if (player.x+player.h>630||player.x+player.h<0) { player.h*=-1;//bounce //stop player.h=0; } if (player.y+player.v>500||player.y+player.v<0) { player.v*=-1; //stop player.v=0; } //new player coordinates player.x+=player.h; player.y+=player.v; Hope this helps.
HTML 5 Canvas rotate mulitple text items around circular point
I'm trying to display numbers around the spokes of a bicycle wheel. In the process of creating the 'spokes' for the wheel I can't seem to get the text to rotate without messing up the rotation of the wheel. var arr = ['1','2','3','4','5','1','2','3','4','5','1','2','3','4','5','1','2','3','4','5']; function drawNumber() { var cID = document.getElementById('cogs'); var canW = cID.width, canH = cID.height; if (cID && cID.getContext){ var ctx = cID.getContext('2d'); if(ctx) { ctx.translate(ctx.canvas.width/2, ctx.canvas.height/2); var radian = (Math.PI / 180) * 18; var i = 0 for (var degrees = 0; degrees < 360; degrees += 18) { var fillNum = arr[i]; ctx.font = "12pt Arial" ctx.fillStyle = 'white'; ctx.rotate(radian); rotateText(fillNum); i++; } ctx.translate(-canW/2, -canH/2); } } } function rotateText(i){ var cID = document.getElementById('cogs'); ctx = cID.getContext('2d'); ctx.fillText(i, -5, 150); } drawNumber(); http://jsfiddle.net/rdo64wv1/8/ If I add a rotate to the rotate text function it doesn't rotate the text, it just moves around the spokes further. Any ideas?
If I understand you correctly, you want to numbers to continue along the spoke direction at 90°. What you mean exactly is a bit unclear as what direction is text continuing at in the first place. Considering that the fiddle shows the text continuing at the y-axis, here is how to draw text with the text result continuing at the x-axis instead (if this is not what you're after, please include a mock-up of what result you expect - just adjust the angle at the commented-out line as needed). Think of the process as an arm: shoulder is rotated first, then the elbow, both at their pivot points, but elbow is always relative to shoulder angle. So, first rotate at center of wheel to get the spoke angle. Then translate to the origin of the text along that spoke (x-axis in canvas as 0° points right) to get to the "elbow" pivot point, or origin. Rotate (if needed) and draw text, finally reset transformation and repeat for next number. Here's an updated example with some additional adjustments: var arr = ['1','2','3','4','5','1','2','3','4','5','1','2','3','4','5','1','2','3','4','5']; function drawNumber() { var cID = document.getElementById('cogs'), cx = cID.width * 0.5, // we'll only use the center value cy = cID.height * 0.5; if (cID && cID.getContext){ var ctx = cID.getContext('2d'); if(ctx) { ctx.font = "12pt Arial" // set font only once.. ctx.textAlign = "center"; // align to center so we don't ctx.textBaseline = "middle"; // need to calculate center.. var step = Math.PI * 2 / arr.length; // step based on array length (2xPI=360°) for (var angle = 0, i = 0; angle < Math.PI * 2; angle += step) { ctx.setTransform(1,0,0,1,cx, cy); // hard reset transforms + translate ctx.rotate(angle); // absolute rotate wheel center ctx.translate(cx - 10, 0); // translate along x axis //ctx.rotate(-Math.PI*0.5); // 90°, if needed... ctx.fillText(arr[i++], 0, 0); // draw at new origin } } } ctx.setTransform(1,0,0,1,0,0); // reset all transforms } drawNumber(); <canvas id='cogs' width='300' height='300'></canvas>
Returning precise vector components in js canvas
I have been wrestling with rendering an animation that fires a projectile accurately from an "enemy" node to a "player" node in a 2D 11:11 grid (0:0 = top-left) in JS/Canvas. After a lot of reading up I've managed to get the shots close, but not quite bang on. I think my velocity function is a little out but I really don't know why. This is the trigonometric function: this.getVelocityComponents = function(speed){ // loc (location of enemy actor) = array(2) [X_coord, Y_coord] // des (destination (ie. player in this instance)) = array(2) [X_coord, Y_coord] var i, sum, hyp, output = [], dis = []; var higher = false; for (i in loc) { sum = 0; if (loc[i] > des[i]) sum = loc[i] - des[i]; if (loc[i] < des[i]) sum = des[i] - loc[i]; dis.push(sum); } hyp = Math.sqrt(Math.pow(dis[X], 2) + Math.pow(dis[Y], 2)); if (dis[X] > dis[Y]) { output[X] = (speed * Math.cos(dis[X]/hyp)) output[Y] = (speed * Math.sin(dis[Y]/hyp)) } else if (dis[X] < dis[Y]) { output[X] = (speed * Math.cos(dis[Y]/hyp)) output[Y] = (speed * Math.sin(dis[X]/hyp)) } return output; } and this is the instruction that tells the X and the Y of the projectile frame to advance: var distance = []; for (i in loc) { var sum = 0; if (loc[i] > des[i]) sum = loc[i] - des[i]; if (loc[i] < des[i]) sum = des[i] - loc[i]; distance.push(sum); } if (distance[X] > distance[Y]) { frm[X] += (loc[X] < des[X]) ? v[X] : -v[X]; frm[Y] += (loc[Y] < des[Y]) ? v[Y] : -v[Y]; } else { frm[Y] += (loc[Y] < des[Y]) ? v[X] : -v[X]; frm[X] += (loc[X] < des[X]) ? v[Y] : -v[Y]; } Below is a screenshot. Blue is player, pink enemy and the yellow circles are projectiles as you can see, it's almost on the mark. Have I done something wrong? what do I need to do?
To calculate the direction from enemy to player you can simplify the calculations a little. Find direction angle var diffX = Player.x - Enemy.x, // difference in position diffY = Player.y - Enemy.y, angle = Math.atan2(diffY, diffX); // atan2 will give the angle in radians Notice also difference for Y comes first for atan2 as canvas is oriented 0° pointing right. Velocity vector Then calculate the velocity vector using angle and speed: // calculate velocity vector var speed = 8, vx = Math.cos(angle) * speed, // angle x speed vy = Math.sin(angle) * speed; You might want to consider using time as a factor if that is important. You can see my answer from a while back here for an example on this. Demo Using these calculations you will be able to always "hit" the player with the projectile (reload demo to change enemy position to random y): var ctx = document.querySelector("canvas").getContext("2d"), Player = { x: 470, y: 75 }, Enemy = { x: 100, y: Math.random() * 150 // reload demo to change y-position }; // calculate angle var diffX = Player.x - Enemy.x, diffY = Player.y - Enemy.y, angle = Math.atan2(diffY, diffX); // calculate velocity vector var speed = 8, vx = Math.cos(angle) * speed, // angle x speed vy = Math.sin(angle) * speed, x = Enemy.x, // projectil start y = Enemy.y + 50; // render (function loop() { ctx.clearRect(0, 0, 500, 300); ctx.fillRect(Player.x, Player.y, 30, 100); ctx.fillRect(Enemy.x, Enemy.y, 30, 100); ctx.fillRect(x - 3, y -3, 6, 6); x += vx; y += vy; if (x < 500) requestAnimationFrame(loop); })(); <canvas width=500 height=300></canvas>
The solution is much simpler than that. What should you do ? 1) compute the vector that leads from you enemy to the player. That will be the shooting direction. 2) normalize the vector : meaning you build a vector that has a length of 1, with the same direction. 3) multiply that vector by your speed : now you have a correct speed vector, with the right norm, aimed at the player. Below some code to help you understand : function spawnBullet(enemy, player) { var shootVector = []; shootVector[0] = player[0] - enemy[0]; shootVector[1] = player[1] - enemy[1]; var shootVectorLength = Math.sqrt(Math.pow(shootVector[0], 2) + Math.pow(shootVector[1],2)); shootVector[0]/=shootVectorLength; shootVector[1]/=shootVectorLength; shootVector[0]*=bulletSpeed; shootVector[1]*=bulletSpeed; // ... here return an object that has the enemy's coordinate // and shootVector as speed } Then, since you don't use time in your computations (!! wrooong !! ;-) ) you will make the bullet move with the straightforward : bullet[0] += bullet.speed[0]; bullet[1] += bullet.speed[1]; Now the issue with fixed-step is that your game will run, say, twice slower on a 30fps device than on a 60fps device. The solution is to compute how much time elapsed since the last refresh, let's call this time 'dt'. Using that time will lead you to an update like : bullet[0] += dt * bullet.speed[0]; bullet[1] += dt * bullet.speed[1]; and now you'll be framerate-agnostic, your game will feel the same on any device.
Grass like smoothing animation on beziercurve?
This is what I am trying to achieve--GRASS Animation(Desired animation) This is where the project is standing currently --My hair animation This is a more structurised code of the above code --My hair animation(by markE)--markE`s code of hair animation PROBLEM:-- I am able to give movements to hairs but animation should be more like wavy grass like freeflowing.Its not very smooth now.What can be done to make the hairs flow in more natural manner. Please provide me with a small sample if possible!!! <canvas id="myCanvas" width="500" height="500" style="background-color: antiquewhite" ></canvas> JAVASCRIPT //mouse position var x2=0; var y2=0; window.addEventListener("mousemove",function(){moving(event);init()},false) //these variables define the bend in our bezier curve var bend9=0; var bend8=0; var bend7=0; var bend6=0; var bend5=0; var bend4=0; var bend3=0; var bend2=0; var bend1=0; //function to get the mouse cordinates function moving(event) { bend_value();//this function is defined below try { x2 = event.touches[0].pageX; y2 = event.touches[0].pageY; } catch (error) { try { x2 = event.clientX; y2 = event.clientY; } catch (e) { } } try { event.preventDefault(); } catch (e) { } if(between(y2,204,237) && between(x2,115,272)) { console.log("Xmove="+x2,"Ymove="+y2) } } //function for declaring range of bezier curve function between(val, min, max) { return val >= min && val <= max; } (function() { hair = function() { return this; }; hair.prototype={ draw_hair:function(a,b,c,d,e,f,g,h){ var sx =136+a;//start position of curve.used in moveTo(sx,sy) var sy =235+b; var cp1x=136+c;//control point 1 var cp1y=222+d; var cp2x=136+e;//control point 2 var cp2y=222+f; var endx=136+g;//end points var endy=210+h; var canvas = document.getElementById('myCanvas'); var context = canvas.getContext('2d'); // context.clearRect(0, 0,500,500); context.strokeStyle="grey"; context.lineWidth="8"; context.beginPath(); context.moveTo(sx,sy); context.bezierCurveTo(cp1x,cp1y,cp2x,cp2y,endx,endy); context.lineCap = 'round'; context.stroke(); // context.restore(); // context.save(); } }; })(); //this function provides and calculate the bend on mousemove function bend_value(){ var ref1=135;//this is ref point for hair or curve no 1 var ref2=150;//hair no 2 and so on var ref3=165; var ref4=180; var ref5=195; var ref6=210; var ref7=225; var ref8=240; var ref9=255; if(between(x2,115,270) && between(y2,205,236)) { if(x2>=135 && x2<=145){bend1=(x2-ref1)*(2.2);} if(x2<=135 && x2>=125){bend1=(x2-ref1)*(2.2);} if(x2>=150 && x2<=160){bend2=(x2-ref2)*(2.2);} if(x2<=150 && x2>=140){bend2=(x2-ref2)*(2.2);} if(x2>=165 && x2<=175){bend3=(x2-ref3)*(2.2);} if(x2<=165 && x2>=155){bend3=(x2-ref3)*(2.2);} if(x2>=180 && x2<=190){bend4=(x2-ref4)*(2.2);} if(x2<=180 && x2>=170){bend4=(x2-ref4)*(2.2);} if(x2>=195 && x2<=205){bend5=(x2-ref5)*(2.2);} if(x2<=195 && x2>=185){bend5=(x2-ref5)*(2.2);} if(x2>=210 && x2<=220){bend6=(x2-ref6)*(2.2);} if(x2<=210 && x2>=200){bend6=(x2-ref6)*(2.2);} if(x2>=225 && x2<=235){bend7=(x2-ref7)*(2.2);} if(x2<=225 && x2>=215){bend7=(x2-ref7)*(2.2);} if(x2>=240 && x2<=250){bend8=(x2-ref8)*(2.2);} if(x2<=240 && x2>=230){bend8=(x2-ref8)*(2.2);} if(x2>=255 && x2<=265){bend9=(x2-ref9)*(2.2);} if(x2<=255 && x2>=245){bend9=(x2-ref9)*(2.2);} } } function init(){//this function draws each hair/curve var canvas = document.getElementById('myCanvas'); var context = canvas.getContext('2d'); var clear=context.clearRect(0, 0,500,500); var save=context.save(); // /* console.log("bend2="+bend2) // console.log("bend3="+bend3) // console.log("bend4="+bend4) // console.log("bend5="+bend5) // console.log("bend6="+bend6) // console.log("bend7="+bend7) // console.log("bend8="+bend8) // console.log("bend9="+bend9)*/ hd1 = new hair();//hd1 stands for hair draw 1.this is an instance created for drawing hair no 1 clear; hd1.draw_hair(0,0,0,0,0,0,0+bend1/2,0);//these parameters passed to function drawhair and bend is beint retrieved from function bend_value() save; hd2 = new hair(); clear; hd2.draw_hair(15,0,15,0,15,0,15+bend2/2,0); save; hd3 = new hair(); clear; hd3.draw_hair(30,0,30,0,30,0,30+bend3/2,0); save; hd4 = new hair(); clear; hd4.draw_hair(45,0,45,0,45,0,45+bend4/2,0); save; hd5 = new hair(); clear; hd5.draw_hair(60,0,60,0,60,0,60+bend5/2,0); save; } window.onload = function() { init(); disableSelection(document.body) } function disableSelection(target){ if (typeof target.onselectstart!="undefined") //IE target.onselectstart=function(){return false} else if (typeof target.style.MozUserSelect!="undefined") //Firefox target.style.MozUserSelect="none" else //All other ie: Opera target.onmousedown=function(){return false} target.style.cursor = "default" }
Update: I'm currently adjusting the code to produce the requested result and commenting it. (function() { // The code is encapsulated in a self invoking function to isolate the scope "use strict"; // The following lines creates shortcuts to the constructors of the Box2D types used var B2Vec2 = Box2D.Common.Math.b2Vec2, B2BodyDef = Box2D.Dynamics.b2BodyDef, B2Body = Box2D.Dynamics.b2Body, B2FixtureDef = Box2D.Dynamics.b2FixtureDef, B2Fixture = Box2D.Dynamics.b2Fixture, B2World = Box2D.Dynamics.b2World, B2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape, B2RevoluteJoint = Box2D.Dynamics.Joints.b2RevoluteJoint, B2RevoluteJointDef = Box2D.Dynamics.Joints.b2RevoluteJointDef; // This makes sure that there is a method to request a callback to update the graphics for next frame window.requestAnimationFrame = window.requestAnimationFrame || // According to the standard window.mozRequestAnimationFrame || // For mozilla window.webkitRequestAnimationFrame || // For webkit window.msRequestAnimationFrame || // For ie function (f) { window.setTimeout(function () { f(Date.now()); }, 1000/60); }; // If everthing else fails var world = new B2World(new B2Vec2(0, -10), true), // Create a world with gravity physicalObjects = [], // Maintain a list of the simulated objects windInput = 0, // The input for the wind in the current frame wind = 0, // The current wind (smoothing the input values + randomness) STRAW_COUNT = 10, // Number of straws GRASS_RESET_SPEED = 2, // How quick should the straw reset to its target angle POWER_MOUSE_WIND = 120, // How much does the mouse affect the wind POWER_RANDOM_WIND = 180; // How much does the randomness affect the wind // GrassPart is a prototype for a piece of a straw. It has the following properties // position: the position of the piece // density: the density of the piece // target: the target angle of the piece // statik: a boolean stating if the piece is static (i.e. does not move) function GrassPart(position, density, target, statik) { this.width = 0.05; this.height = 0.5; this.target = target; // To create a physical body in Box2D you have to setup a body definition // and create at least one fixture. var bdef = new B2BodyDef(), fdef = new B2FixtureDef(); // In this example we specify if the body is static or not (the grass roots // has to be static to keep the straw in its position), and its original // position. bdef.type = statik? B2Body.b2_staticBody : B2Body.b2_dynamicBody; bdef.position.SetV(position); // The fixture of the piece is a box with a given density. The negative group index // makes sure that the straws does not collide. fdef.shape = new B2PolygonShape(); fdef.shape.SetAsBox(this.width/2, this.height/2); fdef.density = density; fdef.filter.groupIndex = -1; // The body and fixture is created and added to the world this.body = world.CreateBody(bdef); this.body.CreateFixture(fdef); } // This method is called for every frame of animation. It strives to reset the original // angle of the straw (the joint). The time parameter is unused here but contains the // current time. GrassPart.prototype.update = function (time) { if (this.joint) { this.joint.SetMotorSpeed(GRASS_RESET_SPEED*(this.target - this.joint.GetJointAngle())); } }; // The link method is used to link the pieces of the straw together using a joint // other: the piece to link to // torque: the strength of the joint (stiffness) GrassPart.prototype.link = function(other, torque) { // This is all Box2D specific. Look it up in the manual. var jdef = new B2RevoluteJointDef(); var p = this.body.GetWorldPoint(new B2Vec2(0, 0.5)); // Get the world coordinates of where the joint jdef.Initialize(this.body, other.body, p); jdef.maxMotorTorque = torque; jdef.motorSpeed = 0; jdef.enableMotor = true; // Add the joint to the world this.joint = world.CreateJoint(jdef); }; // A prototype for a straw of grass // position: the position of the bottom of the root of the straw function Grass(position) { var pos = new B2Vec2(position.x, position.y); var angle = 1.2*Math.random() - 0.6; // Randomize the target angle // Create three pieces, the static root and to more, and place them in line. // The second parameter is the stiffness of the joints. It controls how the straw bends. // The third is the target angle and different angles are specified for the pieces. this.g1 = new GrassPart(pos, 1, angle/4, true); // This is the static root pos.Add(new B2Vec2(0, 1)); this.g2 = new GrassPart(pos, 0.75, angle); pos.Add(new B2Vec2(0, 1)); this.g3 = new GrassPart(pos, 0.5); // Link the pieces into a straw this.g1.link(this.g2, 20); this.g2.link(this.g3, 3); // Add the pieces to the list of simulate objects physicalObjects.push(this.g1); physicalObjects.push(this.g2); physicalObjects.push(this.g3); } Grass.prototype.draw = function (context) { var p = new B2Vec2(0, 0.5); var p1 = this.g1.body.GetWorldPoint(p); var p2 = this.g2.body.GetWorldPoint(p); var p3 = this.g3.body.GetWorldPoint(p); context.strokeStyle = 'grey'; context.lineWidth = 0.4; context.lineCap = 'round'; context.beginPath(); context.moveTo(p1.x, p1.y); context.quadraticCurveTo(p2.x, p2.y, p3.x, p3.y); context.stroke(); }; var lastX, grass = [], context = document.getElementById('canvas').getContext('2d'); function updateGraphics(time) { window.requestAnimationFrame(updateGraphics); wind = 0.95*wind + 0.05*(POWER_MOUSE_WIND*windInput + POWER_RANDOM_WIND*Math.random() - POWER_RANDOM_WIND/2); windInput = 0; world.SetGravity(new B2Vec2(wind, -10)); physicalObjects.forEach(function(obj) { if (obj.update) obj.update(time); }); world.Step(1/60, 8, 3); world.ClearForces(); context.clearRect(0, 0, context.canvas.width, context.canvas.height); context.save(); context.translate(context.canvas.width/2, context.canvas.height/2); context.scale(context.canvas.width/20, -context.canvas.width/20); grass.forEach(function (o) { o.draw(context); }); context.restore(); } document.getElementsByTagName('body')[0].addEventListener("mousemove", function (e) { windInput = Math.abs(lastX - e.x) < 200? 0.2*(e.x - lastX) : 0; lastX = e.x; }); var W = 8; for (var i = 0; i < STRAW_COUNT; i++) { grass.push(new Grass(new B2Vec2(W*(i/(STRAW_COUNT-1))-W/2, -1))); } window.requestAnimationFrame(updateGraphics); })();
Waving grass algorithm UPDATE I made a reduced update to better meet what I believe is your requirements. To use mouse you just calculate the angle between the mouse point and the strain root and use that for new angle in the update. I have incorporated a simple mouse-move sensitive approach which makes the strains "point" towards the mouse, but you can add random angles to this as deltas and so forth. Everything you need is as said in the code - adjust as needed. New fiddle (based on previous with a few modifications): http://jsfiddle.net/AbdiasSoftware/yEwGc/ Image showing 150 strains being simulated. Grass simulation demo: http://jsfiddle.net/AbdiasSoftware/5z89V/ This will generate a nice realistic looking grass field. The demo has 70 grass rendered (works best in Chrome or just lower the number for Firefox). The code is rather simple. It consists of a main object (grassObj) which contains its geometry as well as functions to calculate the angles, segments, movements and so forth. I'll show this in detail below. First some inits that are accessed globally by the functions: var numOfGrass = 70, /// number of grass strains grass, /// get canvas context ctx = canvas.getContext('2d'), w = canvas.width, h = canvas.height, /// we use an animated image for the background /// The image also clears the canvas for each loop call /// I rendered the clouds in a 3D software. img = document.createElement('img'), ix = 0, /// background image position iw = -1; /// used for with and initial for flag /// load background image, use it whenever it's ready img.onload = function() {iw = this.width} img.src = 'http://i.imgur.com/zzjtzG7.jpg'; The heart - grassObj The main object as mentioned above is the grassObj: function grassObj(x, y, seg1, seg2, maxAngle) { /// exposed properties we need for rendering this.x = x; /// bottom position of grass this.y = y; this.seg1 = seg1; /// segments of grass this.seg2 = seg2; this.gradient = getGradient(Math.random() * 50 + 50, 100 * Math.random() + 170); this.currentAngle; ///current angle that will be rendered /// internals used for calculating new angle, goal, difference and speed var counter, /// counter between 0-1 for ease-in/out delta, /// random steps in the direction goal rel. c.angle. angle, /// current angle, does not change until goal is reached diff, /// diff between goal and angle goal = getAngle(); /// internal: returns an angel between 0 and maxAngle function getAngle() { return maxAngle * Math.random(); } /// ease in-out function function easeInOut(t) { return t < 0.5 ? 4 * t * t * t : (t-1) * (2 * t - 2) * (2 * t - 2) + 1; } /// sets a new goal for grass to move to. Does the main calculations function newGoal() { angle = goal; /// set goal as new angle when reached this.currentAngle = angle; goal = getAngle(); /// get new goal diff = goal - angle; /// calc diff counter = 0; /// reset counter delta = (4 * Math.random() + 1) / 100; } /// creates a gradient for this grass to increase realism function getGradient(min, max) { var g = ctx.createLinearGradient(0, 0, 0, h); g.addColorStop(1, 'rgb(0,' + parseInt(min) + ', 0)'); g.addColorStop(0, 'rgb(0,' + parseInt(max) + ', 0)'); return g; } /// this is called from animation loop. Counts and keeps tracks of /// current position and calls new goal when current goal is reached this.update = function() { /// count from 0 to 1 with random delta value counter += delta; /// if counter passes 1 then goal is reached -> get new goal if (counter > 1) { newGoal(); return; } /// ease in/out function var t = easeInOut(counter); /// update current angle for render this.currentAngle = angle + t * diff; } /// init newGoal(); return this; } Grass generator We call makeGrass to generate grass at random positions, random heights and with random segments. The function is called with number of grass to render, width and height of canvas to fill and a variation variable in percent (0 - 1 float). The single grass consist only of four points in total. The two middle points are spread about 1/3 and 2/3 of the total height with a little variation to break pattern. The points when rendered, are smoother using a cardinal spline with full tension to make the grass look smooth. function makeGrass(numOfGrass, width, height, hVariation) { /// setup variables var x, y, seg1, seg2, angle, hf = height * hVariation, /// get variation i = 0, grass = []; /// array to hold the grass /// generate grass for(; i < numOfGrass; i++) { x = width * Math.random(); /// random x position y = height - hf * Math.random(); /// random height /// break grass into 3 segments with random variation seg1 = y / 3 + y * hVariation * Math.random() * 0.1; seg2 = (y / 3 * 2) + y * hVariation * Math.random() * 0.1; grass.push(new grassObj(x, y, seg1, seg2, 15 * Math.random() + 50)); } return grass; } Render The render function just loops through the objects and updates the current geometry: function renderGrass(ctx, grass) { /// local vars for animation var len = grass.length, i = 0, gr, pos, diff, pts, x, y; /// renders background when loaded if (iw > -1) { ctx.drawImage(img, ix--, 0); if (ix < -w) { ctx.drawImage(img, ix + iw, 0); } if (ix <= -iw) ix = 0; } else { ctx.clearRect(0, 0, w, h); } /// loops through the grass object and renders current state for(; gr = grass[i]; i++) { x = gr.x; y = gr.y; ctx.beginPath(); /// calculates the end-point based on length and angle /// Angle is limited [0, 60] which we add 225 deg. to get /// it upwards. Alter 225 to make grass lean more to a side. pos = lineToAngle(ctx, x, h, y, gr.currentAngle + 225); /// diff between end point and root point diff = (pos[0] - x) pts = []; /// starts at bottom, goes to top middle and then back /// down with a slight offset to make the grass pts.push(x); /// first couple at bottom pts.push(h); /// first segment 1/4 of the difference pts.push(x + (diff / 4)); pts.push(h - gr.seg1); /// second segment 2/3 of the difference pts.push(x + (diff / 3 * 2)); pts.push(h - gr.seg2); pts.push(pos[0]); /// top point pts.push(pos[1]); /// re-use previous data, but go backward down to root again /// with a slight offset pts.push(x + (diff / 3 * 2) + 10); pts.push(h - gr.seg2); pts.push(x + (diff / 4) + 12); pts.push(h - gr.seg1 + 10); pts.push(x + 15); /// end couple at bottom pts.push(h); /// smooth points (extended context function, see demo) ctx.curve(pts, 0.8, 5); ctx.closePath(); /// fill grass with its gradient ctx.fillStyle = gr.gradient; ctx.fill(); } } Animate The main loop where we animate everything: function animate() { /// update each grass objects for(var i = 0;i < grass.length; i++) grass[i].update(); /// render them renderGrass(ctx, grass); /// loop requestAnimationFrame(animate); } And that's all there is to it for this version.
Darn! Late to the party... But LOTS of neat answers here -- I'm upvoting all ! Anyway, here's my idea: Here's code and a Fiddle: http://jsfiddle.net/m1erickson/MJjHz/ <!doctype html> <html> <head> <link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css --> <script src="http://code.jquery.com/jquery-1.9.1.js"></script> <script src="http://code.jquery.com/ui/1.10.1/jquery-ui.js"></script> <style> body { font-family: arial; padding:15px; } canvas { border: 1px solid red;} input[type="text"]{width:35px;} </style> </head> <body> <p>Move mouse across hairs</p> <canvas height="100" width="250" id="canvas"></canvas> <script> $(function() { var canvas=document.getElementById("canvas"); var ctx = canvas.getContext("2d"); var canvasOffset=$("#canvas").offset(); var offsetX=canvasOffset.left; var offsetY=canvasOffset.top; var cHeight=canvas.height; var showControls=false; var lastMouseX=0; // preset styling CONSTANTS var SWAY=.55; // max endpoint sway from center var C1Y=.40; // fixed Y of cp#1 var C2SWAY=.20 // max cp#2 sway from center var C2Y=.75; // fixed Y of cp#2 var YY=20; // max height of ellipse at top of hair var PIPERCENT=Math.PI/100; var hairs=[]; // create hairs var newHairX=40; var hairCount=20; for(var i=0;i<hairCount;i++){ var randomLength=50+parseInt(Math.random()*5); addHair(newHairX+(i*8),randomLength); } function addHair(x,length){ hairs.push({ x:x, length:length, left:0, right:0, top:0, s:{x:0,y:0}, c1:{x:0,y:0}, c2:{x:0,y:0}, e:{x:0,y:0}, isInMotion:false, currentX:0 }); } for(var i=0;i<hairs.length;i++){ var h=hairs[i]; setHairPointsFixed(h); setHairPointsPct(h,50); draw(h); } function setHairPointsFixed(h){ h.s.x = h.x; h.s.y = cHeight; h.c1.x = h.x; h.c1.y = cHeight-h.length*C1Y; h.c2.y = cHeight-h.length*C2Y; h.top = cHeight-h.length; h.left = h.x-h.length*SWAY; h.right = h.x+h.length*SWAY; } function setHairPointsPct(h,pct){ // endpoint var a=Math.PI+PIPERCENT*pct; h.e.x = h.x - ((h.length*SWAY)*Math.cos(a)); h.e.y = h.top + (YY*Math.sin(a)); // controlpoint#2 h.c2.x = h.x + h.length*(C2SWAY*2*pct/100-C2SWAY); } ////////////////////////////// function handleMouseMove(e){ mouseX=parseInt(e.clientX-offsetX); mouseY=parseInt(e.clientY-offsetY); // draw this frame based on mouse moves ctx.clearRect(0,0,canvas.width,canvas.height); for(var i=0;i<hairs.length;i++){ hairMoves(hairs[i],mouseX,mouseY); } lastMouseX=mouseX; } $("#canvas").mousemove(function(e){handleMouseMove(e);}); function hairMoves(h,mouseX,mouseY){ // No hair movement if not touching hair if(mouseY<cHeight-h.length-YY){ if(h.isInMotion){ h.isInMotion=false; setHairPointsPct(h,50); } draw(h); return; } // No hair movement if too deep in hair if(mouseY>h.c1.y){ draw(h); return; } // var pct=50; if(mouseX>=h.left && mouseX<=h.right){ if(h.isInMotion){ var pct=-(mouseX-h.right)/(h.right-h.left)*100; setHairPointsPct(h,pct); draw(h); }else{ // if hair is at rest // but mouse has just contacted hair // set hair in motion if( (lastMouseX<=h.x && mouseX>=h.x ) ||(lastMouseX>=h.x && mouseX<=h.x ) ){ h.isInMotion=true; var pct=-(mouseX-h.right)/(h.right-h.left)*100; } setHairPointsPct(h,pct); draw(h); } }else{ if(h.isInMotion){ h.isInMotion=false; setHairPointsPct(h,50); }; draw(h); } } function dot(pt,color){ ctx.beginPath(); ctx.arc(pt.x,pt.y,5,0,Math.PI*2,false); ctx.closePath(); ctx.fillStyle=color; ctx.fill(); } function draw(h){ ctx.beginPath(); ctx.moveTo(h.s.x,h.s.y); ctx.bezierCurveTo(h.c1.x,h.c1.y,h.c2.x,h.c2.y,h.e.x,h.e.y); ctx.strokeStyle="orange"; ctx.lineWidth=3; ctx.stroke(); if(showControls){ dot(h.s,"green"); dot(h.c1,"red"); dot(h.c2,"blue"); dot(h.e,"purple"); ctx.beginPath(); ctx.rect(h.left,h.top-YY,(h.right-h.left),h.length*(1-C1Y)+YY) ctx.lineWidth=1; ctx.strokeStyle="lightgray"; ctx.stroke(); } } }); </script> </body> </html>
Here is a simple hair simulation that seems to be what you are looking for. The basic idea is to draw a bezier curve (in this case I use two curves to provide thickness for the hair). The curve will have a base, a bending point, and a tip. I set the bending point halfway up the hair. The tip of the hair will rotate about the axis of the base of the hair in response to mouse movement. Place this code in a script tag below the canvas element declaration. function Point(x, y) { this.x = x; this.y = y; } function Hair( ) { this.height = 100; // hair height this.baseWidth = 3; // hair base width. this.thickness = 1.5; // hair thickness this.points = {}; this.points.base1 = new Point(Math.random()*canvas.width, canvas.height); // The point at which the hair will bend. I set it to the middle of the hair, but you can adjust this. this.points.bendPoint1 = new Point(this.points.base1.x-this.thickness, this.points.base1.y - this.height / 2) this.points.bendPoint2 = new Point(this.points.bendPoint1.x, this.points.bendPoint1.y-this.thickness); // complement of bendPoint1 - we use this because the hair has thickness this.points.base2 = new Point(this.points.base1.x + this.baseWidth, this.points.base1.y) // complement of base1 - we use this because the hair has thickness } Hair.prototype.paint = function(mouseX, mouseY, direction) { ctx.save(); // rotate the the tip of the hair var tipRotationAngle = Math.atan(Math.abs(this.points.base1.y - mouseY)/Math.abs(this.points.base1.x - mouseX)); // if the mouse is on the other side of the hair, adjust the angle if (mouseX < this.points.base1.x) { tipRotationAngle = Math.PI - tipRotationAngle; } // if the mouse isn't close enough to the hair, it shouldn't affect the hair if (mouseX < this.points.base1.x - this.height/2 || mouseX > this.points.base1.x + this.height/2 || mouseY < this.points.base1.y - this.height || mouseY > this.points.base1.y) { tipRotationAngle = Math.PI/2; // 90 degrees, which means the hair is straight } // Use the direction of the mouse to as a lazy way to simulate the direction the hair should bend. // Note that in real life, the direction that the hair should bend has nothing to do with the direction of motion. It actually depends on which side of the hair the force is being applied. // Figuring out which side of the hair the force is being applied is a little tricky, so I took this shortcut. // If you run your finger along a comb quickly, this approximation will work. However if you are in the middle of the comb and slowly change direction, you will notice that the force is still applied in the opposite direction of motion as you slowly back off the set of tines. if ((mouseX < this.points.base1.x && direction == 'right') || (mouseX > this.points.base1.x && direction == 'left')) { tipRotationAngle = Math.PI/2; // 90 degrees, which means the hair is straight } var tipPoint = new Point(this.points.base1.x + this.baseWidth + this.height*Math.cos(tipRotationAngle), this.points.base1.y - this.height*Math.sin(tipRotationAngle)); ctx.beginPath(); ctx.moveTo(this.points.base1.x, this.points.base1.y); // start at the base ctx.bezierCurveTo(this.points.base1.x, this.points.base1.y, this.points.bendPoint1.x, this.points.bendPoint1.y, tipPoint.x, tipPoint.y); // draw a curve to the tip of the hair ctx.bezierCurveTo(tipPoint.x, tipPoint.y, this.points.bendPoint2.x, this.points.bendPoint2.y, this.points.base2.x, this.points.base2.y); // draw a curve back down to the base using the complement points since the hair has thickness. ctx.closePath(); // complete the path so we have a shape that we can fill with color ctx.fillStyle='rgb(0,0,0)'; ctx.fill(); ctx.restore(); } // I used global variables to keep the example simple, but it is generally best to avoid using global variables window.canvas = document.getElementById('myCanvas'); window.ctx = canvas.getContext('2d'); ctx.fillStyle = 'rgb(200,255,255)'; // background color window.hair = []; window.prevClientX = 0; for (var i = 0; i < 100; i++) { hair.push(new Hair()); } // initial draw ctx.fillRect(0,0,canvas.width,canvas.height); // clear canvas for (var i = 0; i < hair.length; i++) { hair[i].paint(0, 0, 'right'); } window.onmousemove = function(e) { ctx.fillRect(0,0,canvas.width,canvas.height); // clear canvas for (var i = 0; i < hair.length; i++) { hair[i].paint(e.clientX, e.clientY, e.clientX > window.prevClientX ? 'right' : 'left'); } window.prevClientX = e.clientX; }
Made this some time ago, might be useful to some people. Just adjust the variables at the beginning of the code with the values that fits your wishes: ... Mheight = 1; height = 33; width = 17; distance = 10; randomness = 14; angle = Math.PI / 2; ... Also on http://lucasm0ta.github.io/JsGrass/