I currently have a d3 hierarchical circle pack graph in canvas (with help from this awesome tutorial!)
I wanted to make the graph also pan with the mouse and scale with the wheel in addition to zooming in to a node. I used this.
Now, when I first pan and then zoom to a node using the initial scale (When the page first loads), it works as expected. It zooms to the node from wherever the graph's last panned position was.
But when I try to pan and zoom from the zoomed state, the view starts at a completely different place before it ends at the expected node. If from the zoomed state I don't pan, but jump/zoom from one node to another, the transition works as expected. I'm not sure why this happens. Can anyone help me figure out what I'm doing wrong? Thanks in advance!
(Sorry for any faux-pas -- Its my first stack overflow question...)
#ViewChild('myCanvas') canvas: ElementRef;
#ViewChild('myCanvas') hiddenCanvas: ElementRef;
public context: CanvasRenderingContext2D;
public hiddenContext: CanvasRenderingContext2D;
public focus;
public xleftView = 0;
public ytopView = 0;
public widthViewOriginal = window.innerWidth;
public heightViewOriginal = window.innerHeight;
public widthView = this.widthViewOriginal;
public heightView = this.heightViewOriginal;
public widthCanvas = window.innerWidth;
public heightCanvas = window.innerHeight;
public mouseDown = false;
public movedCoordinatesX;
public movedCoordinatesY;
public lastX = 0;
public lastY = 0;
public zoomInfo = {
centerX: window.innerWidth / 2,
centerY: window.innerHeight / 2,
scale: 1
};
public handleMouseDown(event) {
// get where the mouse first clicked to pan the graph
this.movedCoordinatesX = event.clientX
this.movedCoordinatesY = event.clientY
if (event.which === 1) {
this.mouseDown = true;
}
}
public handleMouseUp(event) {
this.mouseDown = false;
// Get the distance moved in the x & y direction from the initial click to the end mouse position
this.movedCoordinatesX = this.movedCoordinatesX - event.clientX
this.movedCoordinatesY = this.movedCoordinatesY - event.clientY
// add distance in the X & Y to the old view's X & Y (to make the transition happen from the current position
this.vOld[0] = (this.vOld[0] + this.movedCoordinatesX)
this.vOld[1] = (this.vOld[1] + this.movedCoordinatesY)
}
public handleMouseMove(event) {
var X = event.clientX - this.context.canvas.offsetLeft - this.context.canvas.clientLeft + this.context.canvas.scrollLeft;
var Y = event.clientY - this.context.canvas.offsetTop - this.context.canvas.clientTop + this.context.canvas.scrollTop;
if (this.mouseDown) {
var dx = (X - this.lastX) / this.widthCanvas * this.widthView;
var dy = (Y - this.lastY)/ this.heightCanvas * this.heightView;
this.xleftView -= dx;
this.ytopView -= dy;
}
this.lastX = X;
this.lastY = Y;
}
// Listen for clicks on the main canvas
public zoomInToNode(e) {
// We actually only need to draw the hidden canvas when there is an interaction.
// This sketch can draw it on each loop, but that is only for demonstration.
this.drawCanvas(this.hiddenContext, true);
//Figure out where the mouse click occurred.
var mouseX = e.layerX;
var mouseY = e.layerY;
this.lastX = mouseX
this.lastY = mouseY
// Get the corresponding pixel color on the hidden canvas and look up the node in our map.
// This will return that pixel's color
var col = this.hiddenContext.getImageData(mouseX, mouseY, 1, 1).data;
//Our map uses these rgb strings as keys to nodes.
var colString = "rgb(" + col[0] + "," + col[1] + ","+ col[2] + ")";
var node = this.colToCircle[colString];
if(node) {
if (this.focus !== node) {
this.xleftView = 0
this.ytopView = 0
this.hiddenContext.setTransform(1, 0, 0, 1, 0, 0);
this.zoomToCanvas(node);
}
else {
this.xleftView = 0
this.ytopView = 0
this.hiddenContext.setTransform(1, 0, 0, 1, 0, 0);
this. widthView = this.widthViewOriginal;
this. heightView = this.heightViewOriginal;
this. widthCanvas = window.innerWidth;
this. heightCanvas = window.innerHeight;
this.zoomToCanvas(this.rooting);
}
}//if
};
public zoomToCanvas(focusNode) {
// the node that we want to zoom to
this.focus = focusNode;
var v = [this.focus.x, this.focus.y, this.focus.r * 2.05]; //The center and width of the new "viewport"
this.interpolator = d3.interpolateZoom(this.vOld, v); //Create interpolation between current and new "viewport"
this.duration = this.interpolator.duration; //Interpolation gives back a suggested duration
this.timeElapsed = 0; //Set the time elapsed for the interpolateZoom function to 0
this.vOld = v; //Save the "viewport" of the next state as the next "old" state
}//function zoomToCanvas
// Perform the interpolation and continuously change the zoomInfo while the "transition" occurs
public interpolateZoom(dt) {
if (this.interpolator) {
this.timeElapsed += dt;
var t = this.ease(this.timeElapsed / this.duration);
if(isFinite(t)) {
this.zoomInfo.centerX = this.interpolator(t)[0];
this.zoomInfo.centerY = this.interpolator(t)[1];
this.zoomInfo.scale = this.diameter / this.interpolator(t)[2];
if (this.timeElapsed >= this.duration) this.interpolator = null;
}
}//if
}//function zoomToCanvas
public drawCanvas(chosenContext, hidden) {
chosenContext.save();
chosenContext.setTransform(1,0,0,1,0,0);
chosenContext.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
chosenContext.scale(this.widthCanvas/this.widthView, this.heightCanvas/this.heightView);
chosenContext.translate(-this.xleftView,-this.ytopView);
chosenContext.restore();
}
So I figured it out... I was supposed to divide by the overall scale.
I really want to thank this answer for giving me the inspiration!
public handleMouseUp(event) {
this.mouseDown = false;
this.movedCoordinatesX = this.movedCoordinatesX - event.clientX
this.movedCoordinatesY = this.movedCoordinatesY - event.clientY
if(this.zoomInfo.scale == 1) {
this.vOld[0] = (this.vOld[0] + this.movedCoordinatesX)
this.vOld[1] = (this.vOld[1] + this.movedCoordinatesY)
} else {
this.vOld[0] = (this.vOld[0] + (this.movedCoordinatesX/this.zoomInfo.scale))
this.vOld[1] = (this.vOld[1] + (this.movedCoordinatesY/this.zoomInfo.scale))
}
}
I'm pretty close to finish this program using Canvas. This program is simply a ball that falls down from top to bottom and there's a basket that catches it, that is it. However, I have the following issues.
1) When I press the left or right arrows from keyboard for more than couple of times somehow the basket will go all the way to either left or right and disappear.
2) When the ball hits the basket nothing happens (my Collision detection function doesn't work properly). However, I should say that my collision detection works just fine when the balls hits the ground (alert message shows up saying "Ball hit the ground").
Is there a way to show a message on the top of the canvas like "1 point" every time the basket catches a ball ( if there are 5 balls then I should get a message to say "5 points")
Can someone tell me what I am doing wrong please? Thank you so much in advance!!
LIVE CODE HERE
http://codepen.io/HenryGranados/pen/QNOZRa
Here's my code :
//create the constructor for the class pad
function Pad() {
//initialisation code will go here
//create private variables for the x and y coordinates
var x = 200,
y = 200,
vx = 0,
vy = 0,
padX = (canvas.width - 20) / 2;
rightPressed = false,
leftPressed = false;
//create the draw function to give us the draw method
//it accepts one parameter which is the context from the canvas it is drawn on
Pad.prototype.draw = function (context) {
//save the state of the drawing context before we change it
context.save();
//set the coordinates of the drawing area of the new shape to x and y
context.translate(x, y);
//start the line (path)
context.beginPath();
context.fillStyle = "#800000"; // This is the basket
context.moveTo(15, 20);
context.bezierCurveTo(20, 100, 150, 100, 150, 20);
//close the path
context.closePath();
context.fill();
//go ahead and draw the line
context.stroke();
//restore the state of the context to what it was before our drawing
context.restore();
}
//create a public property called X (note caps!)
Object.defineProperty(this, 'X',
{
//getter
get: function () {
//return the value of x (lower case)
return x;
},
//setter
set: function (value) {
//ste the value of x (lower case)
x = value;
}
}
)
//create a public property called Y (note caps!)
Object.defineProperty(this, 'Y',
{
//getter
get: function () {
//return the value of y (lower case)
return y;
},
//setter
set: function (value) {
//ste the value of y (lower case)
y = value;
}
}
)
padX = function () {
if (rightPressed && padX < canvas.width - 20) {
padX += 5;
}
else if (leftPressed && padX > 0) {
padX -= 5;
}
}
Pad.prototype.move = function () {
//change the x axis by the x velocity
x += vx;
//change the y axis by the y velocity
y += vy;
}
Pad.prototype.setVector = function (vector) {
//set the vx value based on this vector
vx = vector.VX;
//set the vy value based on this vector
vy = vector.VY;
}
//public method to set the vector of the saucer
Pad.prototype.accelerate = function (Acceleration) {
//set vx
vx += Acceleration.AX;
////set vy
//vy += Acceleration.AY;
}
//create a public property called Top
Object.defineProperty(this, 'Top',
{
//getter
get: function () {
//return the y posn less the height
return y - 10;
}
}
)
//create a public property called Bottom
Object.defineProperty(this, 'Bottom',
{
//getter
get: function () {
//return the y posn plus the height
return y + 10;
}
}
)
//create a public property called Left
Object.defineProperty(this, 'Left',
{
//getter
get: function () {
//return the x posn less the width
return x - 80;
}
}
)
//create a public property called Right
Object.defineProperty(this, 'Right',
{
//getter
get: function () {
//return the x posn plus the width
return x + 80;
}
}
)
}
(1) There are at least two options to solve this problem
in your Pad.move function you could limit the change of x. You change it only when its within canvas width:
Pad.prototype.move = function() {
//change the x axis by the x velocity
var canvasWidth = 400,
padWidth = 150;
if (x + vx < canvasWidth - padWidth && x + vx >= 0)
x += vx;
//change the y axis by the y velocity
y += vy;
}
or similarly as you create ground you could create walls on both sides and collide pad with them.
(2) There is no collision handling between ball and pad:
place it in function drawFrame():
if (collision.Overlapping(ball, pad)) {
context.strokeText('ball hit pad!',20,100)
//..do some other stuff here
}
(3)Which brings us to showing message on canvas, you can just draw text on canvas
var ctx = canvas.getContext("2d");
ctx.font = "30px Arial";
ctx.fillText("Hello World",10,50);
Demo: http://codepen.io/anon/pen/RaxwLp?editors=1011
Pad was blocked because when key is pressed acceleration is always increased, so in order to move in opposite direction first it must go to 0 which takes quite some time. I have added keyup event and when key is released acceleration is zeroed:
if(leftPressed){
acceleraton.HThrust(.01);
}else if(rightPressed){
acceleraton.HThrust(-.01);
}else{
acceleraton.Halt();
}
My implementation is this:
Detect if mousedown and mousemove and if true then draw and saved the points in an array.
In my mousemove I will convert the points that will be drawn in
I converted the curPath to (Date,value) then to (X and Y-axis) so that they will be saved in the implementation in my canvas.
My problem is that how will I detect points[] ? so that I can highlight it and drag as well.
UPDATE.
This is bigger than I expected. I will continue to improve the quality of the answer as I go. See the bottom of the answer for status.
Picking.
The simplest way is to check how far the mouse is from each point in the line, and highlight the line that has the closest point. The problem is that when you have many lines and lots of points it slows down and become unusable.
Another ways is to store some extra info on each line to help you vet lines that are not going to be picked. In the example I create a bounding box for each line and check if the mouse is inside or near that box. If so then I search the line some more checking each line segment and keeping the line that is closest to the mouse.
Some of the function to look at.
Helpers.prototype.resetExtent();
Helpers.prototype.extent();
Helpers.prototype.copyOfExtent();
Used to find the bounding box. (extent)
function refreshLine(line);
Called after a line is drawn, it takes a set of points drawn and adds the bounding box (extent), plus other stuff for the demo.
function findLineAt(x,y){
This function takes the x,y position of the mouse (or what ever) and returns the closest line within 20 pixels. It first checks the bounding box, if that passes it calls
Helpers.prototype.getDistToPath = function (line,x,y) {
This gets the line as just a set of points and checks the distance to the center of each line. It also checks if the check needs more details and calls the function.
Helpers.prototype.distPointToLine = function (x, y, x1, y1, x2, y2) {
This function will return the shortest distance from a point to a line. x,y is the point x1,y1,x2,y2 is the line. It does not check the line segment but the line which is infinitely long. Helpers.lineSegPos property will hold the normalised position of the closest point on the line. If you need it.
So back to findLineAt(x,y), after all those calls it will return the line if found or undefined if not.
Highlight and dragging.
Highlighting is up to you. I just cycle the hue of the line closest very quickly. You may wish to put a bounding box around it. Easy to do as all you do is redraw the closest line each update.
How its works
The main loop.
update()
Handles the main loop, is called 60 times a second and has to parts, the Draw section is for drawing and pick for picking. See if(drawMode==="Pick"). The mouse is only read in the update, the mouse is set independently by the mouse listener. At the end of every loop I save the mouse button state mouse.lastButton so that I can check when the mouse moves down and up.
In the pick part if the mouse is not down I call the findLineAt function. If I find a line (line !== undefined) I highlight the line by changing its colour and drawing it.
Because every update I have the current mouseButton state and what it was last update, I will know when the mouse button first moves down because mouse.button is true and mouse.lastButton is false. If there is a line near the mouse, I record the mouse position in dragOffX and dragOffY and set a flag dragging to true. I also draw the canvas onto another canvas to keep as background. At this point I also check which mouse button is down. If right button I copy the line and set it as the line to be dragged, or if the middle button I search all the lines to find its index in the lineArray and delete it, then just redraw.
Next update (1/60th second later) and dragging flag is true mouse.button is true and lastLine (the line that was closest) is not undefined I know I am dragging a line. I clear the canvas, draw the saved copy of the canvas (it's faster to draw that then redraw all the lines again especially if you have 100's of lines with 1000's of points), and then redraw the line I am dragging.
To workout where to draw the dragged line I get the distance the mouse is from dragOffX and dragOffY and set the transpose part of setTransform(1, 0 , 0, 1, mouse.x - dragOffX, mouse.y - dragOffY) to that distance. That has the effect of moving the line by the drag amount. I keep doing this until the mouse button is up.
Drop
Next update and mouse.button is up.
If the mouse button is up and the dragging flag is true then I must drop the line. At this point a get the mouse distance from dragOffX dragOffY and add it to each point in the line. Effectively moving the line. I also update the bounding box. I then clear the screen and redraw all the line, that removes the old position of the line from the canvas and put it at it's new home.
Done.
The code grew a little long. If this answer gets some support then I will clean it up some more. If not well then it does not matter..
It covers the basics of your question, detecting and moving points via mouse action. Highlighting and moving lines made of sets of points. My point are arrays of objects each with an x and y. Each line is stored in the lineArray a line has style, extent, id properties, and an array called line with all the points.
There is one mouse handler that takes the required mouse events. Move, mouse down and up, and mouse out. Mouse out stops the mouse locking up by turning the mouse buttons off. I also stop the context menu while the mouse is over the canvas.
I use requestAnimationFrame to call update to keep it all running smoothly.
I hope this helps you. I will improve it if it is what you are after. If not you will have to give a litte more info. Please do ask if you have problems.
Updates.
Added bounding box and improved the Helpers.prototype.getDistToPath(line,x,y) which I forgot to fix last night. Now its quicker and does not miss lines parallel to x and y axis. Moved screen redraw to accommodate the bounding box and add more comments.
Please do not hesitate to ask if you have question to this problem.
function log(){}; // duck up the logs
customCursors ={
encoding:"url('data:image/png;base64,",
drag_small : {
center : " 25 25,",
image : "iVBORw0KGgoAAAANSUhEUgAAADMAAAAzCAYAAAA6oTAqAAACQElEQVRoQ+2azW7DIAyAYZdJW6vlVmmnvcLe/yH2CjtN6i1Tu0m9rIMsJIYChmCvCWkuqZSA/fkPQyoF83VWl5RSqJtQd8kpjnVyB4QdiA0GghhvcHuIBcYH8h9A5DAxEG4gUhgN8rzbiY/9Hs1zjpAjg0nxiEtIDUQCMwWEI+SKYfJBzorDFkvloSvAXKZTs92K9nAoXlTJYFwV9YofunyNAEWHQALjU9qETijpA2OK9CkaHLJ8NYumBrzBoMss/sK6wkyHDLRJyp6EKsxyZUc9Y5R62mzE5/GYvB+hhNFVMVV+EMZVKGeVpoYxwYHp4IUp3VhxwehwjwFdwIQUwawC84oTJgZkwaQogRfIvzcA/DCkb1m63Eu9sE4CFqQBxgty+hLi/mHocnMOVyzFf96EuHv1AkKopmlE27YW5wiuDHD6Vvo8Ds/daOlggh7pYMbBqdaEnon9zpmve9ejDwSS0f3IRBgYGqOwF2W0dysEKWCskO4dkz1vbADMF9PaQ6OF8qBECT1ndZ6pJ2eMa6upZlGg/mFunF91ncGAFtcBxIDmApPVm4WA5gCD6bCO/Qz0EFzMFrvTnLoip3TfKUbJlb+uA41c60S7cPUQS+Ip8syYm2eg9dzjoMFK/edy19KxTqI0j4o9Y5LdVXqxXwFy+zYXfHbfZ9IPKWb85QyrXlh1oqxuxTmDdduJ22sSPUgmgUBV/A8gx0OUoWX1jVhMT3leVW8WKgpcHmFtZ3whxw2iZZIWAF9IOod/rPJ+AQ3iOFgpekFcAAAAAElFTkSuQmCC')"
},
}
function setCursor (name){
if(name === undefined){
canvas.style.cursor = "default";
}
if(customCursors[name] !== undefined){
var cur = customCursors[name];
canvas.style.cursor = customCursors.encoding + cur.image + cur.center + " pointer";
}else{
canvas.style.cursor = name;
}
}
// get canvas button and creat context2D
var canvas = document.getElementById("canV");
var ctx = canvas.getContext("2d");
var but = document.getElementById("redrawAllID");
but.addEventListener("click",function(){
if(drawMode === "Pick"){
drawMode = "Draw";
but.value = "Draw Mode";
}else{
drawMode = "Pick";
but.value = "Pick Mode";
lastLine = undefined;
backGroundImage.ctx.clearRect(0,0,backGroundImage.width,backGroundImage.height);
backGroundImage.ctx.drawImage(canvas,0,0);
}
})
// Groover Bitmaps API dependency replacement
// Extracted from Groover.Bitmaps
var createImage= function(w,h){ // create a image of requier size
var image = document.createElement("canvas");
image.width = w;
image.height =h;
image.ctx = image.getContext("2d"); // tack the context onto the image
return image;
}
var backGroundImage = createImage(canvas.width,canvas.height);
if(!mouse){
// get all the mouse events
canvas.addEventListener('mousemove',mouseMoveEvent);
canvas.addEventListener('mousedown',mouseMoveEvent);
canvas.addEventListener('mouseup' ,mouseMoveEvent);
canvas.addEventListener('mouseout' ,mouseMoveEvent);
canvas.addEventListener("contextmenu", function(e){ e.preventDefault();}, false);
// helper for random colour
var mouse = { // mouse data
x:0,
y:0,
button:false,
lastButton:false, // need this to see when the mouse goes down
which:[false,false,false],
};
}
function mouseMoveEvent(event){// handle all canvas mouse events as they come in
// get new mouse positions
mouse.x = event.offsetX;
mouse.y = event.offsetY;
if(mouse.x === undefined){ // if firefox
mouse.x = event.clientX;
mouse.y = event.clientY;
}
if(event.type === "mouseout"){
mouse.button = false;
mouse.which[0] = false;
mouse.which[1] = false;
mouse.which[2] = false;
}
if(event.type === "mousedown"){ // now see if there is extra info
mouse.button = true;
mouse.which[event.which-1] = true;
}
if(event.type === "mouseup"){ // now see if there is extra info
mouse.button = false;
mouse.which[event.which-1] = false;
}
event.preventDefault();
}
// because forEach is too slow
if (Array.prototype.each === undefined) {
Object.defineProperty(Array.prototype, 'each', {
writable : false,
enumerable : false,
configurable : false,
value : function (func) {
var i,
returned;
var len = this.length;
for (i = 0; i < len; i++) {
returned = func(this[i], i);
if (returned !== undefined) {
this[i] = returned;
}
}
}
});
}
// helper functions
function Helpers(){
}
Helpers.prototype.randomColour = function(){
return "hsl("+Math.floor(Math.random()*360)+",100%,50%)";
}
Helpers.prototype.occilatingColour = function(){
var t = (new Date()).valueOf()
return "hsl("+(Math.floor(t/2)%360)+",100%,50%)";
}
// used for building up the extent of a cloud of points
Helpers.prototype.resetExtent = function(){
if(this.extentObj === undefined){ // check if the extentObj is there
this.extentObj = {}; // if not create it
}
this.extentObj.minX = Infinity;
this.extentObj.minY = Infinity;
this.extentObj.maxX = -Infinity;
this.extentObj.maxY = -Infinity;
}
Helpers.prototype.extent = function( p) { // add a point to the extent
this.extentObj.minX = Math.min(this.extentObj.minX, p.x);
this.extentObj.minY = Math.min(this.extentObj.minY, p.y);
this.extentObj.maxX = Math.max(this.extentObj.maxX, p.x);
this.extentObj.maxY = Math.max(this.extentObj.maxY, p.y);
}
Helpers.prototype.copyOfExtent = function () { // get a copy of the extent object
return {
minX : this.extentObj.minX,
minY : this.extentObj.minY,
maxX : this.extentObj.maxX,
maxY : this.extentObj.maxY,
centerX : (this.extentObj.maxX-this.extentObj.minX)/2,
centerY : (this.extentObj.maxY-this.extentObj.minY)/2,
width:this.extentObj.maxX-this.extentObj.minX,
height:this.extentObj.maxY-this.extentObj.minY,
};
}
Helpers.prototype.getID = function(){ // return a unique ID for this session
if(this.id === undefined){
this.id = 0;
}
this.id += 1;
return this.id;
}
// function to get distance of point to a line
Helpers.prototype.distPointToLine = function (x, y, x1, y1, x2, y2) {
var px = x2 - x1;
var py = y2 - y1;
var u = this.lineSegPos = Math.max(0, Math.min(1, ((x - x1) * px + (y - y1) * py) / (this.distSqr1 = (px * px + py * py))));
return Math.sqrt(Math.pow((x1 + u * px) - x, 2) + Math.pow((y1 + u * py) - y, 2));
}
// function to get the distance of a point to a set of point describing a line
Helpers.prototype.getDistToPath = function (line,x,y) {
var i,len, lineLen,dist;
len = line.length;
x1 = line[0].x;
y1 = line[0].y;
var minDist = Infinity;
for(i = 1; i < len-1; i++){
var near = false;
x2 = line[i].x;
y2 = line[i].y;
lineLen = Math.hypot(x1-x2,y1-y2);
dist = Math.hypot((x1+x2)/2-x,(y1+y2)/2-y);
minDist = Math.min(minDist,dist);
if(dist < lineLen ){
minDist = Math.min(minDist,helpers.distPointToLine(x,y,x1,y1,x2,y2));
}
if(minDist < minDistToPass){
return minDist;
}
x1 = x2;
y1 = y2;
}
return minDist;
}
var helpers = new Helpers();
// Stuff for paths and drawing
var lineArray = []; // list of paths
var lastLine; // last line drawn
var points; // current recording path
var drawing = false; // flag is mouse down and drawing
var dragging = false;
var dragOffX;
var dragOffY;
var drawMode = "Draw";
var minDistToPass = 2; // If a line is closer than this then stop search we found the winning line
// functions to redraw all recorded lines
function redrawAll(){ // style to draw in
ctx.clearRect(0,0,canvas.width,canvas.height);
lineArray.each(function(p){ // draw each one point at atime
redraw(p,p.style);
})
}
// lineDesc is a line and its description
// style is a the style to draw the line in.
// withBox if true draw bounding box [optional]
function redraw(lineDesc,style,withBox){ // draws a single line with style
var line = lineDesc.line;
var len = line.length;
var i;
ctx.beginPath(); //
ctx.strokeStyle = style.colour; // set style and colour
ctx.lineWidth = lineDesc.style.width;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.moveTo(line[0].x,line[0].y); // move to the first pont
for(i = 1; i < line.length; i++){ // lineto all the rest
ctx.lineTo(line[i].x,line[i].y);
};
ctx.stroke(); // stroke
if(withBox){
var w = Math.ceil(lineDesc.style.width/2); // need the lines width to expand the bounding box
ctx.lineWidth = 1;
ctx.strokeStyle = "black";
ctx.strokeRect( // draw the box around the line
lineDesc.extent.minX-w,
lineDesc.extent.minY-w,
lineDesc.extent.width+w*2,
lineDesc.extent.height+w*2
)
}
// done
}
// Finds the closest line and returns it. If no line can be found it returns undefined.
function findLineAt(x,y){
var minDist = 20; // Set the cutoff limit. Lines further than this are ignored
var minLine;
var w;
lineArray.each(function(line){ // do ech line one at a time
w = line.style.width;
if(x >= line.extent.minX-w && x <= line.extent.maxX+w && // is the point inside the bounding
y >= line.extent.minY-w && y <= line.extent.maxY+w){ // boc
var dist = helpers.getDistToPath(line.line,x,y); // if so then do a detailed distance check
if(dist < minDist){ // is the distance to the line less than any other distance found
minDist = dist; // if so remember the line
minLine = line;
}
}
dist = Math.hypot(line.extent.centerX-x,line.extent.centerY-y); // check the distance to the
if(dist<minDist){ // center of the bounding boc
minDist = dist; // use this one if bound box center if close
minLine = line;
}
});
return minLine;
}
function refreshLine(line){ // updates the line to get extend and add stuff if needed
// a good place to smooth the line if need
if(!line.isLine){
var newLine = {}; // new object
newLine.isLine = true; // flag to indicate that the line has been updated
newLine.line = line; // attach the line
newLine.id = helpers.getID(); // get a unique Id for the line
newLine.style = { // give it a style
colour:helpers.randomColour(),
width:Math.random()*4+10,
};
}else{
var newLine = line;
}
var extent = helpers.extent.bind(helpers)
helpers.resetExtent();
line.each(extent);
newLine.extent = helpers.copyOfExtent();
return newLine;
}
function update(){ // one animframe
if(drawMode === "Draw"){
if(!mouse.lastButton && mouse.button ){ // if the mouse but just down;
points = []; // create an new array
drawing = true; // flag drawinf
lineArray.push(points); // save the point array onto the pointsArray
points.push({x:mouse.x,y:mouse.y}); // add the first point
setCursor("none");
}else
if(drawing && mouse.button){ // while the mouse is down keep drawing
points.push({x:mouse.x,y:mouse.y}); // save new point
var p1 = points[points.length-2]; // get last line seg and draw it
var p2 = points[points.length-1];
ctx.lineWidth = 1;
ctx.strokeStyle = "black";
ctx.beginPath();
ctx.moveTo(p1.x,p1.y);
ctx.lineTo(p2.x,p2.y);
ctx.stroke();
}else{
if(drawing){ // if drawing and mouse up
points.push({x:mouse.x,y:mouse.y}); // add the last point
lineArray.push(points = refreshLine(lineArray.pop()))
// redraw the newly draw line
redraw(points,points.style);
drawing = false; // flag that drawing is off.
}else{
setCursor("crosshair");
}
}
}else
if(drawMode = "Pick"){
if(!dragging && !mouse.button){ // is the mouse near a line and not dragging
ctx.clearRect(0,0,canvas.width,canvas.height); // clear background
ctx.drawImage(backGroundImage,0,0); // draw copy of existing lines
var line = findLineAt(mouse.x,mouse.y); // find the line
if(line !== undefined){ // is a line is near
setCursor("drag_small"); // highlight it
lastLine = line; // remember it
// draw it hightlighted with bounding box.
redraw(lastLine,{colour:helpers.occilatingColour(),width:lastLine.width},true);
}else{
setCursor(); // not near a line so turn of cursoe
}
}else // next check if the mouse has jsut been click to start a drag.
if(lastLine !== undefined && !mouse.lastButton && mouse.button){
if(mouse.which[2]){ // Check which button. Right? then copy
var newLine = [];
lastLine.line.each(function(p){newLine.push({x:p.x,y:p.y})});
newLine = refreshLine(newLine)
newLine.style = lastLine.style;
lastLine = newLine;
lineArray.push(newLine)
}else
if(mouse.which[1]){ // Check which button. Middle? then delete
var index;
lineArray.each(function(line,i){
if(line.id === lastLine.id){
index = i;
}
})
if(index !== undefined){
lineArray.splice(index,1);
}
ctx.clearRect(0,0,canvas.width,canvas.height);
redrawAll();
lastLine = undefined;
if(lineArray.length === 0){
drawMode = "Draw";
but.value = "Draw Mode";
}
}
if(lastLine !== undefined){
dragging = true;
dragOffX = mouse.x;
dragOffY = mouse.y;
// backGroundImage.ctx.clearRect(0,0,canvas.width,canvas.height);
// backGroundImage.ctx.drawImage(canvas,0,0);
}
}else{
if(dragging && !mouse.button){ // Drop is dragging true and not mouse down
dragging = false;
var ox = mouse.x-dragOffX; // Find the drag offset
var oy = mouse.y-dragOffY;
helpers.resetExtent(); // get ready for new bounding box.
lastLine.line.each(function(p){ // move each point of the line
p.x += ox;
p.y += oy;
helpers.extent(p); // and test the bounding box
return p;
})
lastLine.extent = helpers.copyOfExtent(); // get the new boundong box
ctx.clearRect(0,0,canvas.width,canvas.height);
redrawAll();
backGroundImage.ctx.clearRect(0,0,backGroundImage.width,backGroundImage.height);
backGroundImage.ctx.drawImage(canvas,0,0);
}else
if(dragging){ // if dragging
ctx.clearRect(0,0,canvas.width,canvas.height); // clear
ctx.drawImage(backGroundImage,0,0); // draw existing lines
var ox = mouse.x-dragOffX; // get the drag offset
var oy = mouse.y-dragOffY;
ctx.setTransform(1,0,0,1,ox,oy); // translate by drag offset
redraw(lastLine,lastLine.style); //draw the dragged line
ctx.setTransform(1,0,0,1,0,0); // reset transform
}
}
}
mouse.lastButton = mouse.button; // set the last button state
window.requestAnimationFrame(update); // request a new frame
}
window.requestAnimationFrame(update)
.canC {
width:256px;
height:256px;
border:black 2px solid;
}
.info{
font-size:x-small;
}
<input type="button" id="redrawAllID" value="Click to Pick"></input>
<div class="info">Mouse down to draw.In pick mode mouse hover over line.<br> Left Button drag,middle delete, right copy.</div>
<canvas class="canC" id="canV" width=256 height=256></canvas>
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/