I have an Isometric engine that I am building:
http://jsfiddle.net/neuroflux/09h43kz7/1/
(Arrow keys to move).
I am updating the Engine.player.x and Engine.player.y to move the character, but (obviously) the player just "pops" from one tile to another.
I wondered if there was a way to make him "slide" from tile to tile?
Or better still, free movement...
I've been pulling my hair out trying.
Here's the relevant code:
var Engine = {
// canvas variables
canvas: null,
ctx: null,
// map
map: [
[2,2,2,2,2,2,2,2,2,2,2,2,2,2,2],
[2,1,1,1,1,1,1,1,1,1,1,1,1,1,2],
[2,1,1,0,0,1,1,1,1,0,0,0,1,1,2],
[2,2,1,0,0,0,0,1,0,0,0,0,1,1,2],
[2,2,1,1,1,1,0,0,0,0,1,0,0,1,2],
[2,2,2,2,2,1,0,0,0,1,0,0,0,1,2],
[2,2,1,1,1,1,0,0,0,0,0,0,0,1,2],
[2,1,1,0,1,0,0,0,0,1,1,0,0,1,2],
[2,1,0,0,0,0,0,1,0,0,0,0,0,1,2],
[2,1,0,0,0,0,0,0,0,0,1,0,0,1,2],
[2,1,0,0,0,0,1,0,0,0,0,0,0,1,2],
[2,1,0,1,1,0,0,0,0,1,0,0,0,1,2],
[2,1,0,0,0,0,0,0,0,0,1,0,1,1,2],
[2,1,1,1,1,1,1,1,1,1,1,1,1,1,2],
[2,2,2,2,2,2,2,2,2,2,2,2,2,2,2]
],
// player info
player: {
x:1,
y:1
},
// tile size
tileH: 31,
tileW: 63,
// map position
mapX: window.innerWidth/2,
mapY: window.innerHeight/3,
// tile images
tileSources: [
"images/stone.png",
"images/grass.png",
"images/water.png",
"images/ralph.png"
],
// for pre-loading
tileGraphics: [],
tilesLoaded: 0,
// image preloader
loadImages: function() {
for (var i = 0; i < Engine.tileSources.length; i++) {
Engine.tileGraphics[i] = new Image();
Engine.tileGraphics[i].src = Engine.tileSources[i];
Engine.tileGraphics[i].onload = function() {
Engine.tilesLoaded++;
if (Engine.tilesLoaded === Engine.tileSources.length) {
Engine.draw();
}
}
}
},
// update logic
update: function() {
Engine.draw();
},
// draw the scene
draw: function() {
Engine.ctx.clearRect(0, 0, Engine.canvas.width, Engine.canvas.height);
var drawTile;
for (var i = 0; i < Engine.map.length; i++) {
for (var j = 0; j < Engine.map[i].length; j++) {
drawTile = Engine.map[i][j];
Engine.ctx.drawImage(Engine.tileGraphics[drawTile], (i - j) * Engine.tileH + Engine.mapX, (i + j) * Engine.tileH / 2 + Engine.mapY);
if (Engine.player.x === i && Engine.player.y === j) {
Engine.ctx.drawImage(Engine.tileGraphics[3], (i - j) * Engine.tileH + Engine.mapX, (i + j) * Engine.tileH / 2 + Engine.mapY - Engine.tileH + 10);
}
}
}
Engine.gameLoop();
},
// game loop
gameLoop: function() {
Engine.gameTimer = setTimeout(function() {
requestAnimFrame(Engine.update, Engine.canvas);
}, 1);
},
// start
init: function() {
Engine.canvas = document.getElementById("main");
Engine.canvas.width = window.innerWidth;
Engine.canvas.height = window.innerHeight;
Engine.ctx = Engine.canvas.getContext("2d");
document.addEventListener("keyup", function(e) {
//console.log(e.keyCode);
switch(e.keyCode) {
case 38:
if (Engine.map[Engine.player.x-1][Engine.player.y] !== 2) {
Engine.player.x--;
}
break;
case 40:
if (Engine.map[Engine.player.x+1][Engine.player.y] !== 2) {
Engine.player.x++;
}
break;
case 39:
if (Engine.map[Engine.player.x][Engine.player.y-1] !== 2) {
Engine.player.y--;
}
break;
case 37:
if (Engine.map[Engine.player.x][Engine.player.y+1] !== 2) {
Engine.player.y++;
}
break;
}
});
Engine.loadImages();
}
}
// loaded
window.onload = function() {
Engine.init();
};
// request animation frame
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback, element){
fpsLoop = window.setTimeout(callback, 1000 / 60);
};
}());
Thanks in advance!
You are drawing the character at tile positions. What you want is to just add a second set of coordinates for the character representing its destination. To move smoothly you can set the characters position in fractions of a tile. Eg player.x = 2.5 the character is halfway between tiles 2 and 3.
Also you want to get rid of the messing about in isometric space. Off load the transformation from 2d to isometric to the draw function, rather than doing it by hand each time you draw to the playfield.
Create the draw function
// add to the Engine object.
// img is the image to draw. x and y are the tile locations.
// offsetX and offsetY [optional] are pixel offsets for fine tuning;
drawImageIso:function (img,x,y,offsetX,offsetY){
offsetX = offsetX === undefined ? 0: offsetX; // so you dont have to
offsetY = offsetY === undefined ? 0: offsetY; // add the offset if you
// are not using it;
Engine.ctx.drawImage( // draw the image
img,
(x - y) * Engine.tileH + Engine.mapX + offsetX,
(x + y) * Engine.tileH / 2 + Engine.mapY - Engine.tileH+offsetY
);
},
Change the player object to
player: {
x:1,
y:1,
destX:1, // the destination tile
destY:1,
playerAtDest:true, // true if the player has arrived
},
Add this befor the tile render loops
var p = Engine.player; // because I am lazy and dont like typing.
var dx = p.destX;
var dy = p.destY;
var maxPlayerSpeed = 0.1; // max speed in tiles per frame
var mps = maxPlayerSpeed; // because I am lazy
// check if the player needs to move
if( Math.abs(p.x - dx) > mps || Math.abs(p.y - dy) > mps ){
p.x += Math.max( -mps , Math.min( mps , dx - p.x )); // move to destination clamping speed;
p.y += Math.max( -mps , Math.min( mps , dy - p.y ));
p.playerAtDest = false; // flag the player is on the way
}else{
// player directly over a till and not moving;
p.x = dx; // ensure the player positioned correctly;
p.y = dy;
p.playerAtDest = true; // flag the player has arrived
}
Add the following where you used to draw the player. Use the destination x,y to determine when to draw or use Math.round(Engine.player.x) and y to determine when.
// now draw the player at its current position
Engine.drawImageIso( Engine.tileGraphics[3] , p.x , p.y , 0 , 10);
You will have to change the interface to move player destination rather than x and y. You may also want to delay the move until the player has arrived at the current destination.
That covers the basics.
Related
This is my first post so I'm trying to make my problem as clear as possible. I'm making a game and I want to improve my collision detection. This is because I want to check what side is being hit and stop the player from moving past it without using something general like if(collision(player, enemy)) player.x = enemy.x - player.w(width) because if the player were to collide with the top it wouldn't keep the player on top.
In the code it checks if any one of the statements is true and then returns it but it doesn't tell me which statement was the one that was equal to true so I can stop the player from moving accordingly, if that makes sense. If you have a more efficient collision detection for me to use it would be greatly appreciated.
I've already tried to make a position variable to be equal to whatever side gets collided into and then stop the player from moving past it but it only works for the left side and won't let my player jump over the enemy or block.
function collision(object1, object2) {
return !(
object1.x > object2.x + object2.w ||
object1.x + object1.w < object2.x ||
object1.y > object2.y + object2.h ||
object1.y + object1.h < object2.y
)
}
//Only works for the left side
if(collision(player, enemy)) player.x = enemy.x - player.w
I expect it to be able to tell me what side is being collided into and then either stop the player from moving past/into it and for the player to be able to be on top of the block/enemy without just being pushed to the left.
You'll want to calculate the distance between the x's and y's and also use the minimum distance that they could be colliding along each axis to find the depth along both axes. Then you can pick the smaller depth and move along that one. Here's an example:
if(collision(player, enemy)){
// Most of this stuff would probably be good to keep stored inside the player
// along side their x and y position. That way it doesn't have to be recalculated
// every collision check
var playerHalfW = player.w/2
var playerHalfH = player.h/2
var enemyHalfW = enemy.w/2
var enemyHalfH = enemy.h/2
var playerCenterX = player.x + player.w/2
var playerCenterY = player.y + player.h/2
var enemyCenterX = enemy.x + enemy.w/2
var enemyCenterY = enemy.y + enemy.h/2
// Calculate the distance between centers
var diffX = playerCenterX - enemyCenterX
var diffY = playerCenterY - enemyCenterY
// Calculate the minimum distance to separate along X and Y
var minXDist = playerHalfW + enemyHalfW
var minYDist = playerHalfH + enemyHalfH
// Calculate the depth of collision for both the X and Y axis
var depthX = diffX > 0 ? minXDist - diffX : -minXDist - diffX
var depthY = diffY > 0 ? minYDist - diffY : -minYDist - diffY
// Now that you have the depth, you can pick the smaller depth and move
// along that axis.
if(depthX != 0 && depthY != 0){
if(Math.abs(depthX) < Math.abs(depthY)){
// Collision along the X axis. React accordingly
if(depthX > 0){
// Left side collision
}
else{
// Right side collision
}
}
else{
// Collision along the Y axis.
if(depthY > 0){
// Top side collision
}
else{
// Bottom side collision
}
}
}
}
Working example
Here's a working example that you can play around with. Use the arrow keys to move the player around.
player = {
x: 9,
y: 50,
w: 100,
h: 100
}
enemy = {
x: 100,
y: 100,
w: 100,
h: 100
}
output = document.getElementById("collisionType");
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d")
function collision(object1, object2) {
return !(
object1.x > object2.x + object2.w ||
object1.x + object1.w < object2.x ||
object1.y > object2.y + object2.h ||
object1.y + object1.h < object2.y
)
}
function draw() {
ctx.clearRect(0, 0, 400, 400)
ctx.lineWidth = "5"
ctx.beginPath();
ctx.strokeStyle = "red";
ctx.rect(player.x, player.y, player.w, player.h);
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = "blue";
ctx.rect(enemy.x, enemy.y, enemy.w, enemy.h);
ctx.stroke();
}
function handleCollision() {
if (collision(player, enemy)) {
var playerHalfW = player.w / 2
var playerHalfH = player.h / 2
var enemyHalfW = enemy.w / 2
var enemyHalfH = enemy.h / 2
var playerCenterX = player.x + player.w / 2
var playerCenterY = player.y + player.h / 2
var enemyCenterX = enemy.x + enemy.w / 2
var enemyCenterY = enemy.y + enemy.h / 2
// Calculate the distance between centers
var diffX = playerCenterX - enemyCenterX
var diffY = playerCenterY - enemyCenterY
// Calculate the minimum distance to separate along X and Y
var minXDist = playerHalfW + enemyHalfW
var minYDist = playerHalfH + enemyHalfH
// Calculate the depth of collision for both the X and Y axis
var depthX = diffX > 0 ? minXDist - diffX : -minXDist - diffX
var depthY = diffY > 0 ? minYDist - diffY : -minYDist - diffY
// Now that you have the depth, you can pick the smaller depth and move
// along that axis.
if (depthX != 0 && depthY != 0) {
if (Math.abs(depthX) < Math.abs(depthY)) {
// Collision along the X axis. React accordingly
if (depthX > 0) {
output.innerHTML = "left side collision"
} else {
output.innerHTML = "right side collision"
}
} else {
// Collision along the Y axis.
if (depthY > 0) {
output.innerHTML = "top side collision"
} else {
output.innerHTML = "bottom side collision"
}
}
}
} else {
output.innerHTML = "No collision"
}
}
keyStates = []
function handleKeys() {
if (keyStates[39]) {
player.x += 2 //Move right
} else if (keyStates[37]) {
player.x -= 2 //Move left
}
if (keyStates[38]) {
player.y -= 2 //Move up
}
if (keyStates[40]) {
player.y += 2 //Move down
}
}
function main() {
handleKeys();
draw();
handleCollision();
window.requestAnimationFrame(main);
}
window.onkeydown = function(e) {
keyStates[e.keyCode] = true
}
window.onkeyup = function(e) {
keyStates[e.keyCode] = false
}
main();
<h2 id="collisionType"></h2>
<canvas id="canvas" width='300' height='300'></canvas>
Reacting to the collision
Now that you know the side the collision happened on, it should be fairly trivial to decide how to react. It would be very similar to what you are currently doing for the left side just flip some signs around and change the axis.
Other Considerations
You may want to take into account your player's velocity (if it has one) otherwise the detection may fail.
If the player's velocity is too high, it might 'tunnel' through the enemy and no collision will be detected.
The player's movement can also look jittery if the velocity is not stopped upon collision
Can your objects rotate or have more than 4 sides? If so, you'll probably want to use another method as described below.
Here's a good answer to another post that talks in depth about collision engines
Other Methods
As for other collision detection methods, there's quite a few but one that comes to mind is Separating Axis Theorem which is a little more complex than what you have but will work with more complex convex shapes and rotation. It also tells you the direction and distance needed to move to resolve the collision. Here's a site that has interactive examples and goes in-depth on the subject. It doesn't appear to give a full implementation but those can be found other places.
I'm currently coding a Pac-man clone with p5.js, and have ran into an issue. I have created a function which draws the map by using a multi-dimensional array, drawing a wall block where a 1 is, and nothing where a 0 is.
This works fine, however i'm struggling to detect collision between the player and the walls. I have tried to use a for loop to go through the array, checking the x and y co-ordinates to see if there is a collision, however it doesn't register at all.This is the code i have used to detect collision:
for(i=0;i<walls.length;i++){
walls[i].draw();
if(player.x > walls[i].x && player.x < walls[i].x + gridsize && player.y > walls[i].y && player.y < walls[i].y + gridsize){
console.log('collision')
}
}
I can't see where the issue is here, as it seems to have worked in other programs.
This runs in the Draw() function, meaning it loops 30 times a second.
This is the full code, incase the issue lies elsewhere:
var gridsize = 20;
var walls = [];
var dots = [];
var player;
var score =0;
var maps = [[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,1,1,0,1,1,1,1,1,1,0,1,1,0,1],
[1,0,1,1,0,0,0,0,0,0,0,0,1,1,0,1],
[1,0,0,0,0,1,1,1,1,1,1,0,0,0,0,1],
[1,1,1,1,0,0,0,0,0,0,0,0,1,1,1,1],
[1,0,0,0,0,1,1,0,0,1,1,0,0,0,0,1],
[1,0,1,0,0,1,0,0,0,0,1,0,0,1,0,1],
[1,0,1,0,0,1,0,0,0,0,1,0,0,1,0,1],
[1,0,1,0,0,1,1,1,1,1,1,0,0,1,0,1],
[1,0,1,0,0,0,0,2,0,0,0,0,0,1,0,1],
[1,0,1,1,1,0,1,1,1,1,0,1,1,1,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,1,1,1,1,1,1,1,1,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]];
function setup(){
createCanvas(320,320);
frameRate(30);
createMap();
}
function draw(){
background(51);
for(i=0;i<walls.length;i++){
walls[i].draw();
if(player.x > walls[i].x && player.x < walls[i].x + gridsize && player.y
> walls[i].y && player.y < walls[i].y + gridsize){
console.log('collision')
}
}
fill('white');
text('Score: ' + score, 5,10);
for(i=0;i<dots.length;i++){
if(player.x == dots[i].x && player.y == dots[i].y && dots[i].collect ==
false){
dots[i].collect = true;
score++;
}
dots[i].draw();
}
player.update();
player.draw();
}
function Block(x,y){
this.x = x;
this.y = y;
this.draw = function(){
fill('black');
rect(this.x,this.y,gridsize,gridsize);
}
}
function Dot(x,y){
this.x = x;
this.y = y;
this.collect = false;
this.draw = function(){
if(!this.collect){
fill('yellow');
ellipse(this.x+gridsize/2,this.y+gridsize/2,gridsize/3,gridsize/3);
}else if(this.collect){
noFill();
noStroke();
ellipse(this.x+gridsize/2,this.y+gridsize/2,gridsize/3,gridsize/3);
}
}
}
function Player(x,y){
this.x = x;
this.y = y;
this.update = function(){
if(keyIsDown(UP_ARROW) && frameCount%5 == 0){
player.y -= gridsize;
}
if(keyIsDown(DOWN_ARROW) && frameCount%5 == 0){
player.y += gridsize;
}
if(keyIsDown(LEFT_ARROW) && frameCount%5 == 0){
player.x -= gridsize;
}
if(keyIsDown(RIGHT_ARROW) && frameCount%5 == 0){
player.x += gridsize;
}
}
this.draw = function(){
fill('blue');
ellipse(this.x+gridsize/2,this.y+gridsize/2,gridsize/1.2,gridsize/1.2);
}
}
function createMap(){
for(i=0;i<maps.length;i++){
for(j=0;j<maps[i].length;j++){
if (maps[i][j] == 1){
walls.push(new Block(j*gridsize,i*gridsize));
}else if(maps[i][j] == 0){
dots.push(new Dot(j*gridsize,i*gridsize))
}else if(maps[i][j] = 2){
player = new Player(j*gridsize,i*gridsize)
}
}
}
}
I presume the issue lies with the fact that the walls are stored in an array, however i have done very similar programs in which the same code works.
PacMan controls
The best way to check for this type of map is to use the player's input.
The player must line up with the walls so assuming the player position is relative to the top left and the player is one map unit wide and deep.
Key input requests a direction to move dx, dy hold the directions which could be more than one at a time. If dx or dy are not 0 then first check if the player is lined up with a passage, if so then check if a block is in the direction of travel. If the player is not lined up or blocked set the movement var to 0
After checking both x and y directions, then if dx or dy have a value then that must be a valid move.
Code changes
Remove the player collision checking code from the main loop and call the player update function with the current map as the 2D original.
player.update(maps); // move the player
Change the Player and update function
function Player(x,y){
this.x = x;
this.y = y;
var dx = 0; // hold current movement
var dy = 0;
const speed = 1; // per Frame pixel speed best as an integer (whole number) and evenly divisible into gridSize
// need the map so that must be passed to the update function
this.update = function(map){
// assuming keys are held to move up to stop
dx = 0; // stop by default
dy = 0;
if (keyIsDown(UP_ARROW)) { dy = -speed }
if (keyIsDown(DOWN_ARROW)) { dy = speed }
if (keyIsDown(LEFT_ARROW)) { dx = -speed }
if (keyIsDown(RIGHT_ARROW)){ dx = speed }
// get player map coords
var x = Math.floor(this.x / gridSize); // get map coord
var y = Math.floor(this.y / gridSize); // get map coord
// the two if statement are best aas function
// so you can select which one to call first. Depending on the latest
// new keys down and if the result allows movement in that
// direction then set the other direction to zero.
if (dy !== 0) { // is moving up or down?
if (this.y % gridsize === 0) { // only if lined up
if (dy > 0){ // is moving down
if (map[y + 1][x] === 1) { // down blocked
dy = 0;
}
}else if (map[y - 1][x] === 1) { // up blocked
dy = 0;
}
} else { // block move if not lined up with passage
dy = 0;
}
}
if(dx !== 0){ // is moving left or right?
if (this.x % gridsize === 0) { // only if lined up
if (dx > 0) { // is moving right
if (map[y][x + 1] === 1) { // right blocked
dx = 0;
}
} else if (map[y][x - 1] === 1) { // left blocked
dx = 0;
}
} else { // block move if not lined up with passage
dx = 0;
}
}
// could have two moves, but can only move left right or up down
// you need to add some input smarts to pick which one
// this just favours up down
if(dy !== 0) { dx = 0 };
// only valid moves will come through the filter above.
// so move the player.
this.x += dx;
this.y += dy;
}
Adding more smarts
Note I have changed the way the player moves, I set a speed per frame (1 pixel) that must be an even divisor of gridSize.
The code above is the simplest implementation. This type of games needs some extra smarts in controls. You should check in the direction of the newest key down. Ie if the player traveling down and right is pressed then moving right should have priority. If player moving right and left is pressed then you should move left, not keep moving right.
Extras
While looking at this question I wanted to visualize the map. Maps as arrays are painful to create and modify, and very hard to find mistakes in. Much easier as a a set of strings that gets converted to an array at run time.
As i have done the conversion no point wasting it. maps is identical to the original array but now easier to read and change.
const maps = [
"################",
"# #",
"# ## ###### ## #",
"# ## ## #",
"# ###### #",
"#### ####",
"# ## ## #",
"# # # # # #",
"# # # # # #",
"# # ###### # #",
"# # 2 # #",
"# ### #### ### #",
"# #",
"# ######## #",
"# #",
"################"
].map(row => row.split("").map(c => c === "#" ? 1 : c === " " ? 0 : 2));
I'm not quite sure why you're using rectangle-rectangle collision detection when you could just use grid-based collision detection. You could just use the array directly.
But since you are using rectangle-rectangle collision, this line looks a little bit off:
if(player.x > walls[i].x && player.x < walls[i].x + gridsize && player.y > walls[i].y && player.y < walls[i].y + gridsize){
You're checking whether the left edge of the player is inside the wall and whether the top edge of the player is inside the wall. But you aren't detecting the other edges. Usually you'd want to do something like this:
if(rectOneRight > rectTwoLeft && rectOneLeft < rectTwoRight && rectOneBottom > rectTwoTop && rectOneTop < rectTwoBottom){
Notice how this if statement checks all of the edges, not just the top and left. But like I said, you might be better off just using grid collision detection, since you already have a grid of walls.
Shameless self-promotion: here is a tutorial on collision detection. It's written for Processing, but everything should translate pretty directly to P5.js.
if the player is not sprite here then point-in-rect collision detection will be appropriate here.
// point in rect collision detection
function pointInRect (x, y, rect) {
return inRange(x, rect.x, rect.x + gridsize) &&
inRange(y, rect.y, rect.y + gridsize);
}
// check a value is in range or not
function inRange (value, min, max) {
return value >= Math.min(min, max) && value <= Math.max(min, max);
}
// checking player is hitting the wall or not
if(pointInRect(player.x,player.y,walls[i].x,walls[i].y))
{
console.log('collision')
}
Hitbox Overlay IIFE Code
//CSS Hitbox Solution 08-26-2015
//StackOverflow - https://stackoverflow.com/questions/32233084/show-an-element-without-hitbox-does-not-take-mouse-touch-input
//Detect MouseOver https://stackoverflow.com/questions/1273566/how-do-i-check-if-the-mouse-is-over-an-element-in-jquery
//Source: https://stackoverflow.com/questions/3942776/using-jquery-to-find-an-element-at-a-particular-position
//https://css-tricks.com/snippets/jquery/get-x-y-mouse-coordinates/
(function($) {
$.mlp = {
x: 0,
y: 0
}; // Mouse Last Position
function documentHandler() {
var $current = this === document ? $(this) : $(this).contents();
$current.mousemove(function(e) {
jQuery.mlp = {
x: e.pageX,
y: e.pageY
};
});
$current.find("iframe").load(documentHandler);
}
$(documentHandler);
$.fn.ismouseover = function(overThis) {
var result = false;
this.eq(0).each(function() {
var $current = $(this).is("iframe") ? $(this).contents().find("body") : $(this);
var offset = $current.offset();
result = offset.left <= $.mlp.x && offset.left + $current.outerWidth() > $.mlp.x && offset.top <= $.mlp.y && offset.top + $current.outerHeight() > $.mlp.y;
});
return result;
};
})(jQuery);
$('.notification-box').on("click", function() {
$("button").each(function(i) {
var iteratedButton = $('button:eq(' + i + ')');
var buttonID = iteratedButton.attr("id");
if (iteratedButton.ismouseover()) {
iteratedButton.toggleClass(buttonID);
}
});
});
Example 01: Overlay Example for context
Example 02: Concept for auto generating content - Derived from this stackoverflow question.
There is a way by which one can have multiple objects underneath an overlay that masks them. Then, there is a way to have the pointer interact with the elements underneath said overlay if the user clicks at the predetermined point. My question is, may someone please write the code that would, marry the concept of the <map> tag with the IIFE that detects if the point of reference the user clicked is that image and then, act as though it was clicked.
If that did not make sense, simply, I am looking for a process that deviates away from manually setting up coordinates for <area> or having to use tool (which are profound) such as http://www.image-maps.com/. Rather, we would let the pointer do all the work.
We have the following high utility + highly compatible methods: .offset(), .position(), elementFromPoint() and the ability to put elements behind a mask utilizing basic CSS.
So we could combine the IIFE Overlay hitbox method above + ???? = Profit (good bye mapping coordinates via <map>).
I just do not know what the ???? is. I do know that whatever the solution is, I would prefer that it works in all browsers (including IE 5).
Lastly, the process should be fairly automatic in design, setup and implementation.
Whoever creates it, please dub it autoMapperJs (as it would not be limited to images).
Update:
A core feature component of the ???? has been realized as noted by #Alex in the comments. CreateJs notices when the pointer is hovered over a non-transparent area of a image. That is powerful and should be standard in the tool created. It also seems to utilize .mousemove() and z-index. Please keep commenting, as collectively, I feel a solution can be found.
Here's a start. Put images into an array of layers and placements on canvas then run through them on mouse over for hit. Also put over images in layers array to draw that image when hit.
var can = document.getElementById('image-map');
var W = can.width;
var H = can.height;
var ctx = can.getContext('2d');
var layers = [];
var mouse = {x:0,y:0};
can.addEventListener('mousemove', function(evt) {
mouse = getMousePos(can, evt);
drawCanvas();
}, false);
function getMousePos(canvas, evt) {
var rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
};
}
main();
function main() {
initLayers();
drawCanvas();
}
function drawCanvas() {
ctx.clearRect(0, 0, W, H);
var hit = -1;
for (var i =layers.length; i--;) {
var c = layers[i];
if(maskHit(c.img, c.x, c.y)) {
hit = i;
break;
}
}
for (var i =0; i < layers.length; i++) {
var c = layers[i];
var img = hit === i ? c.hov : c.img;
ctx.drawImage(img, c.x, c.y);
}
ctx.drawImage(circ(10,"rgba(255,200,0,.75)"), mouse.x-10/2,mouse.y-10/2);
}
// UTILITY TO DRAW SAMPLE IMAGES
function circ(size, color) {
var can = document.createElement('canvas');
can.width = can.height = size;
var to_rad = Math.PI / 180;
var ctx = can.getContext('2d');
ctx.beginPath();
ctx.moveTo(size, size / 2);
ctx.arc(size / 2, size / 2, size / 2, 0, 360 * to_rad);
ctx.fillStyle = color;
ctx.fill();
return can;
}
function initLayers() {
var s = 75; // size
// PUT YOUR IMAGES IN A LAYERS ARRAY WITH X,Y COORDS FOR CANVAS
// PLACEMENT. X AND Y ARE TOP LEFT CORNDER OF IMAGE. STORE HOVER
// IMAGE FOR MOUSE HIT.
layers = [{
img: circ(s, "#090"),
hov: circ(s, "#C0C"),
x: 123,
y: 12
}, {
img: circ(s, "#F00"),
hov: circ(s, "#C0C"),
x: 63,
y: 12
}, {
img: circ(s, "#00F"),
hov: circ(s, "#C0C"),
x: 3,
y: 12
}];
}
var maskCan = document.createElement("canvas");
maskCan.width=maskCan.height=1;
var maskCtx = maskCan.getContext('2d');
function maskHit(img, x, y) {
// get relative coords to image upper left corner
x = mouse.x - x;
y = mouse.y - y;
if (x < 0 || y < 0 || x > img.width || y > img.height) return false;
//return 1; // square hit, no alpha check
// ALPHA CHECK - draw one pixel, get and check alpha.
// sx sy sw sh dx dy dw dh
maskCtx.clearRect(0,0,1,1);
maskCtx.drawImage(img, x, y, 1, 1, 0, 0, 1, 1);
var imageData = maskCtx.getImageData(0,0,1,1);
//console.log(imageData.data[3])
return imageData.data[3] === 255;
}
#image-map {
border: 1px solid #ACE;
}
<canvas id="image-map" width="200" height="100"></canvas>
Into this simple code I use an eventListener which doesn't look to work at all. The canvas display an image and the given hitpaint() function is supposed determines whether a click occurs. I cant understand why the eventListener behaves like that. Any insight would be helpful.
mycanv.addEventListener("click", function(e) {
var output = document.getElementByID("output");
ctx.fillStyle = 'blue';
//ctx.clearRect(0,0,100,20);
if (hitpaint) {
//ctx.fillText("hit",100,20);
output.innerHTML = "hit";
} else {
//ctx.fillText("miss",100,20);
output.innerHTML = "miss";
}
}, false);
The hitpaint() function is defined as:
function hitpaint(mouse_event) {
var bounding_box = mycanv.getBoundingClientRect();
var mousex = (mouse_event.clientX - bounding_box.left) *
(mycanv.width / bounding_box.width);
var mousey = (mouse_event.clientY - bounding_box.top) *
(mycanv.height / bounding_box.height);
var pixels = ctx.getImageData(mousex, mousey, 1, 1);
for (var i = 3; i < pixels.data.length; i += 4) {
// If we find a non-zero alpha we can just stop and return
// "true" - the click was on a part of the canvas that's
// got colour on it.
if (pixels.data[i] !== 0) return true;
}
// The function will only get here if none of the pixels matched in
return false;
}
Finally, the main loop which display the picture in random location into the canvas:
function start() {
// main game function, called on page load
setInterval(function() {
ctx.clearRect(cat_x, cat_y, 100, 100);
cat_x = Math.random() * mycanv.width - 20;
cat_y = Math.random() * mycanv.height - 20;
draw_katy(cat_x, cat_y);
}, 1000);
}
There are a some issues here:
As Grundy points out in the comment, the hitpaint is never called; right now it checks for it's existence and will always return true
The mouse coordinates risk ending up as fractional values which is no-go with getImageData
Scaling the mouse coordinates is usually not necessary. Canvas should preferably have a fixed size without an additional CSS size
Add boundary check for x/y to make sure they are inside canvas bitmap
I would suggest this rewrite:
mycanv.addEventListener("click", function(e) {
var output = document.getElementByID("output");
ctx.fillStyle = 'blue';
//ctx.clearRect(0,0,100,20);
if (hitpaint(e)) { // here, call hitpaint()
//ctx.fillText("hit",100,20);
output.innerHTML = "hit";
} else {
//ctx.fillText("miss",100,20);
output.innerHTML = "miss";
}
}, false);
Then in hitpaint:
function hitpaint(mouse_event) {
var bounding_box = mycanv.getBoundingClientRect();
var x = ((mouse_event.clientX - bounding_box.left) *
(mycanv.width / bounding_box.width))|0; // |0 cuts off any fraction
var y = ((mouse_event.clientY - bounding_box.top) *
(mycanv.height / bounding_box.height))|0;
if (x >= 0 && x < mycanv.width && y >= 0 && y < mycanv.height) {
// as we only have one pixel, we can address alpha channel directly
return ctx.getImageData(x, y, 1, 1).data[3] !== 0;
}
else return false; // x/y out of range
}
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/