Javascript - interactive particle logo not working - javascript

I'm working through instructions to construct an interactive particle logo design and can't seem to get to the finished product. This is the logo image file -
I'm using a canvas structure / background. Here's the code -
var canvasInteractive = document.getElementById('canvas-interactive');
var canvasReference = document.getElementById('canvas-reference');
var contextInteractive = canvasInteractive.getContext('2d');
var contextReference = canvasReference.getContext('2d');
var image = document.getElementById('img');
var width = canvasInteractive.width = canvasReference.width = window.innerWidth;
var height = canvasInteractive.height = canvasReference.height = window.innerHeight;
var logoDimensions = {
x: 500,
y: 500
};
var center = {
x: width / 2,
y: height / 2
};
var logoLocation = {
x: center.x - logoDimensions.x / 2,
y: center.y - logoDimensions.y / 2
};
var mouse = {
radius: Math.pow(100, 2),
x: 0,
y: 0
};
var particleArr = [];
var particleAttributes = {
friction: 0.95,
ease: 0.19,
spacing: 6,
size: 4,
color: "#ffffff"
};
function Particle(x, y) {
this.x = this.originX = x;
this.y = this.originY = y;
this.rx = 0;
this.ry = 0;
this.vx = 0;
this.vy = 0;
this.force = 0;
this.angle = 0;
this.distance = 0;
}
Particle.prototype.update = function() {
this.rx = mouse.x - this.x;
this.ry = mouse.y - this.y;
this.distance = this.rx * this.rx + this.ry * this.ry;
this.force = -mouse.radius / this.distance;
if (this.distance < mouse.radius) {
this.angle = Math.atan2(this.ry, this.rx);
this.vx += this.force * Math.cos(this.angle);
this.vy += this.force * Math.sin(this.angle);
}
this.x += (this.vx *= particleAttributes.friction) + (this.originX - this.x) * particleAttributes.ease;
this.y += (this.vy *= particleAttributes.friction) + (this.originY - this.y) * particleAttributes.ease;
};
function init() {
contextReference.drawImage(image, logoLocation.x, logoLocation.y);
var pixels = contextReference.getImageData(0, 0, width, height).data;
var index;
for (var y = 0; y < height; y += particleAttributes.spacing) {
for (var x = 0; x < width; x += particleAttributes.spacing) {
index = (y * width + x) * 4;
if (pixels[++index] > 0) {
particleArr.push(new Particle(x, y));
}
}
}
};
init();
function update() {
for (var i = 0; i < particleArr.length; i++) {
var p = particleArr[i];
p.update();
}
};
function render() {
contextInteractive.clearRect(0, 0, width, height);
for (var i = 0; i < particleArr.length; i++) {
var p = particleArr[i];
contextInteractive.fillStyle = particleAttributes.color;
contextInteractive.fillRect(p.x, p.y, particleAttributes.size, particleAttributes.size);
}
};
function animate() {
update();
render();
requestAnimationFrame(animate);
}
animate();
document.body.addEventListener("mousemove", function(event) {
mouse.x = event.clientX;
mouse.y = event.clientY;
});
document.body.addEventListener("touchstart", function(event) {
mouse.x = event.changedTouches[0].clientX;
mouse.y = event.changedTouches[0].clientY;
}, false);
document.body.addEventListener("touchmove", function(event) {
event.preventDefault();
mouse.x = event.targetTouches[0].clientX;
mouse.y = event.targetTouches[0].clientY;
}, false);
document.body.addEventListener("touchend", function(event) {
event.preventDefault();
mouse.x = 0;
mouse.y = 0;
}, false);
html,
body {
margin: 0px;
position: relative;
background-color: #000;
}
canvas {
display: block;
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
img {
display: none;
width: 70%;
height: 400px;
position: absolute;
left: 50%;
transform: translate(-50%, 30%);
}
<html>
<body>
<canvas id="canvas-interactive"></canvas>
<canvas id="canvas-reference"></canvas>
<img src="https://i.stack.imgur.com/duv9h.png" alt="..." id="img">
</body>
</html>
My understanding is the image file has to be set to display: none; and then the image needs to be re-drawn using the javascript commands but I'm not sure if this image is compatible or not. When finished I want the image on a white background.
By way of an example the end design needs to resemble this - Logo particle design

Particle positions from bitmap.
To get the FX you want you need to create a particle system. This is just an array of objects, each with a position, the position where they want to be (Home), a vector defining their current movement, and the colour.
You get each particle's home position and colour by reading pixels from the image. You can access pixel data by rendering an image on a canvas and the using ctx.getImageData to get the pixel data (Note image must be on same domain or have CORS headers to access pixel data). As you read each pixel in turn, if not transparent, create a particle for that pixel and set it colour and home position from the pixels colour and position.
Use requestAnimationFrame to call a render function that every frame iterates all the particles moving them by some set of rules that give you the motion you are after. Once you have move each particle, render them to the canvas using simple shapes eg fillRect
Mouse interaction
To have interaction with the mouse you will need to use mouse move events to keep track of the mouse position relative to the canvas you are rendering to. As you update each particle you also check how far it is from the mouse. You can then push or pull the particle from or to the mouse (depending on the effect you want.
Rendering speed will limit the particle count.
The only issue with these types of FX is that you will be pushing the rendering speed limits as the particle count goes up. What may work well on one machine, will run very slow on another.
To avoid being too slow, and not looking good on some machines you should consider keeping an eye on the frame rate and reducing the particle count if it runs slow. To compensate you can increase the particle size or even reduce the canvas resolution.
The bottleneck is the actual rendering of each particle. When you get to large numbers the path methods really grinds down. If you want really high numbers you will have to render pixels directly to the bitmap, using the same method as reading but in reverse of course.
Example simple particles read from bitmap.
The example below uses text rendered to a canvas to create the particles, and to use an image you would just draw the image rather than the text. The example is a bit overkill as I ripped it from an old answer of mine. It is just as an example of the various ways to get stuff done.
const ctx = canvas.getContext("2d");
const Vec = (x, y) => ({x, y});
const setStyle = (ctx,style) => { Object.keys(style).forEach(key => ctx[key] = style[key]) }
const createImage = (w,h) => {var i=document.createElement("canvas");i.width=w;i.height=h;i.ctx=i.getContext("2d");return i}
const textList = ["Particles"];
var textPos = 0;
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var globalTime;
var started = false;
requestAnimationFrame(update);
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
function onResize(){
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
if (!started) { startIt() }
}
function update(timer){
globalTime = timer;
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
if (w !== innerWidth || h !== innerHeight){ onResize() }
else { ctx.clearRect(0,0,w,h) }
particles.update();
particles.draw();
requestAnimationFrame(update);
}
function createParticles(text){
createTextMap(
text, 60, "Arial",
{ fillStyle : "#FF0", strokeStyle : "#F00", lineWidth : 2, lineJoin : "round", },
{ top : 0, left : 0, width : canvas.width, height : canvas.height }
)
}
// This function starts the animations
function startIt(){
started = true;
const next = ()=>{
var text = textList[(textPos++ ) % textList.length];
createParticles(text);
setTimeout(moveOut,text.length * 100 + 12000);
}
const moveOut = ()=>{
particles.moveOut();
setTimeout(next,2000);
}
setTimeout(next,0);
}
// the following function create the particles from text using a canvas
// the canvas used is displayed on the main canvas top left fro reference.
var tCan = createImage(100, 100); // canvas used to draw text
function createTextMap(text,size,font,style,fit){
const hex = (v)=> (v < 16 ? "0" : "") + v.toString(16);
tCan.ctx.font = size + "px " + font;
var width = Math.ceil(tCan.ctx.measureText(text).width + size);
tCan.width = width;
tCan.height = Math.ceil(size *1.2);
var c = tCan.ctx;
c.font = size + "px " + font;
c.textAlign = "center";
c.textBaseline = "middle";
setStyle(c,style);
if (style.strokeStyle) { c.strokeText(text, width / 2, tCan.height / 2) }
if (style.fillStyle) { c.fillText(text, width / 2, tCan.height/ 2) }
particles.empty();
var data = c.getImageData(0,0,width,tCan.height).data;
var x,y,ind,rgb,a;
for(y = 0; y < tCan.height; y += 1){
for(x = 0; x < width; x += 1){
ind = (y * width + x) << 2; // << 2 is equiv to * 4
if(data[ind + 3] > 128){ // is alpha above half
rgb = `#${hex(data[ind ++])}${hex(data[ind ++])}${hex(data[ind ++])}`;
particles.add(Vec(x, y), Vec(x, y), rgb);
}
}
}
particles.sortByCol
var scale = Math.min(fit.width / width, fit.height / tCan.height);
particles.each(p=>{
p.home.x = ((fit.left + fit.width) / 2) + (p.home.x - (width / 2)) * scale;
p.home.y = ((fit.top + fit.height) / 2) + (p.home.y - (tCan.height / 2)) * scale;
})
.findCenter() // get center used to move particles on and off of screen
.moveOffscreen() // moves particles off the screen
.moveIn(); // set the particles to move into view.
}
// basic particle
const particle = { pos : null, delta : null, home : null, col : "black", }
// array of particles
const particles = {
items : [], // actual array of particles
mouseFX : { power : 12,dist :110, curve : 2, on : true },
fx : { speed : 0.3, drag : 0.6, size : 4, jiggle : 1 },
// direction 1 move in -1 move out
direction : 1,
moveOut () {this.direction = -1; return this},
moveIn () {this.direction = 1; return this},
length : 0,
each(callback){ // custom iteration
for(var i = 0; i < this.length; i++){ callback(this.items[i],i) }
return this;
},
empty() { this.length = 0; return this },
deRef(){ this.items.length = 0; this.length = 0 },
sortByCol() { this.items.sort((a,b) => a.col === b.col ? 0 : a.col < b.col ? 1 : -1 ) },
add(pos, home, col){ // adds a particle
var p;
if(this.length < this.items.length){
p = this.items[this.length++];
p.home.x = home.x;
p.home.y = home.y;
p.delta.x = 0;
p.delta.y = 0;
p.col = col;
}else{
this.items.push( Object.assign({}, particle,{ pos, home, col, delta : Vec(0,0) } ) );
this.length = this.items.length
}
return this;
},
draw(){ // draws all
var p, size, sizeh;
sizeh = (size = this.fx.size) / 2;
for(var i = 0; i < this.length; i++){
p = this.items[i];
ctx.fillStyle = p.col;
ctx.fillRect(p.pos.x - sizeh, p.pos.y - sizeh, size, size);
}
},
update(){ // update all particles
var p,x,y,d;
const mP = this.mouseFX.power;
const mD = this.mouseFX.dist;
const mC = this.mouseFX.curve;
const fxJ = this.fx.jiggle;
const fxD = this.fx.drag;
const fxS = this.fx.speed;
for(var i = 0; i < this.length; i++){
p = this.items[i];
p.delta.x += (p.home.x - p.pos.x ) * fxS + (Math.random() - 0.5) * fxJ;
p.delta.y += (p.home.y - p.pos.y ) * fxS + (Math.random() - 0.5) * fxJ;
p.delta.x *= fxD;
p.delta.y *= fxD;
p.pos.x += p.delta.x * this.direction;
p.pos.y += p.delta.y * this.direction;
if(this.mouseFX.on){
x = p.pos.x - mouse.x;
y = p.pos.y - mouse.y;
d = Math.sqrt(x * x + y * y);
if(d < mD){
x /= d;
y /= d;
d /= mD;
d = (1-Math.pow(d, mC)) * mP;
p.pos.x += x * d;
p.pos.y += y * d;
}
}
}
return this;
},
findCenter(){ // find the center of particles maybe could do without
var x,y;
y = x = 0;
this.each(p => { x += p.home.x; y += p.home.y });
this.center = Vec(x / this.length, y / this.length);
return this;
},
moveOffscreen(){ // move start pos offscreen
var dist,x,y;
dist = Math.sqrt(this.center.x * this.center.x + this.center.y * this.center.y);
this.each(p => {
var d;
x = p.home.x - this.center.x;
y = p.home.y - this.center.y;
d = Math.max(0.0001,Math.sqrt(x * x + y * y)); // max to make sure no zeros
p.pos.x = p.home.x + (x / d) * dist;
p.pos.y = p.home.y + (y / d) * dist;
});
return this;
},
}
canvas { position : absolute; top : 0px; left : 0px; background : black;}
<canvas id="canvas"></canvas>

Use png saved as PNG-8 and and allow cross-origin
I saw the cool article from Bricks and mortar and thought I'd try it out.
I battled with it for an eternity, thinking that my js was wrong... Turns out that the image has to be saved as a PNG-8 without dither instead of a PNG-24.
Then make sure that you add the crossOrigin="Anonymous" attribute to the image tag:
<img crossOrigin="Anonymous" id="img" src="[link to wherever you host the image]" alt="logo">
I also hid the reference canvas by adding the following styles:
canvas#canvas-reference {
display: none;
}
I also added a debounce and resize function, so it's responsive.
The result:
See Demo with inverted logo

Related

How to animate several dots moving here and there in html canvas

I have a requirement to move many dots here and there inside a canvas.
Hence I created several arcs with different radius and placed them at random places.
var context = document.getElementById('stage').getContext('2d');
var radian = Math.PI / 180;
var x = 40;
var y = 40;
var r = 20;
var colorPoints = [];
var frames = 50;
var currentFrame = 0;
var toggle = false;
var iconsLoaded = false;
context.beginPath();
context.arc(x,y, r, 0 * radian, 360 * radian, false)
context.fill();
var drawMultipleCurves = function(ctx){
if(!iconsLoaded){
for (let i = 0; i < 600; i++) {
ctx.beginPath();
ctx.filter = 'blur(5px)';
ctx.fillStyle = '#B835FF';
colorPoints.push({x: Math.floor((Math.random() * 700) + 0), xMove: Math.floor((Math.random() * 2) + 0) , yMove: Math.floor((Math.random() * 2) + 0) , y: Math.floor((Math.random() * 700) + 0), radius: Math.floor((Math.random() * 20) + 5)});
ctx.arc(colorPoints[colorPoints.length - 1].x, colorPoints[colorPoints.length - 1].y, colorPoints[colorPoints.length - 1].radius, 0 * radian, 360 * radian, false);
ctx.fill();
ctx.closePath();
iconsLoaded = true;
}
}
else{
for(let i =0;i< colorPoints.length; i++){
if(frames === currentFrame ){
toggle = !toggle;
currentFrame = 0;
}
if(!toggle){
colorPoints[i].xMove === 1 ? colorPoints[i].x = colorPoints[i].x + 5 : colorPoints[i].x = colorPoints[i].x - 5;
colorPoints[i].yMove === 1 ? colorPoints[i].y = colorPoints[i].y + 5 : colorPoints[i].y = colorPoints[i].y - 5;
}
else{
colorPoints[i].xMove === 1 ? colorPoints[i].x = colorPoints[i].x - 5 : colorPoints[i].x = colorPoints[i].x + 5;
colorPoints[i].yMove === 1 ? colorPoints[i].y = colorPoints[i].y - 5 : colorPoints[i].y = colorPoints[i].y + 5;
}
ctx.beginPath();
ctx.arc(colorPoints[i].x, colorPoints[i].y, colorPoints[i].radius, 0 * radian, 360 * radian, false);
context.closePath( );
ctx.fill();
currentFrame = currentFrame + 1;
}
}
}
var animate = function(){
setTimeout(()=>{
context.clearRect(0,0,400,400);
context.beginPath();
drawMultipleCurves(context);
context.fill();
requestAnimationFrame(animate)
}, 1000/30)
}
requestAnimationFrame(animate)
<canvas id="stage" width="400" height="400">
<p>Your browser doesn't support canvas.</p>
</canvas>
Above is the code that I have tried.
I have first created and placed several dots at random places with random radius.
When I created them I saved all these random places in an array 'colorPoints'
Now I'm looping into this array and moving all the dots everytime 'requestAnimation' is called.
I'm able to achieve my animation of moving the dots randomly but as I have used 800 dots and then saving them into an array and then again looping them to move their position, the animation is not looking smooth.
It looks like it is moving and strucking. How can I achieve this animation smoothly?
Thanks in advance :)
Render "fill" once per style
Your code is slowing down due to where you placed fill (same if you use stroke)
When you have many objects with the same style call fill only once per frame for each object.
You had something like
for (const c of circles) {
ctx.beginPath();
ctx.arc(c.x, c.y, c.r, 0, TAU)
ctx.fill();
}
With a filter active the fill command forces the filter to be reset, which for blur is complex.
Rather add all the arcs then fill.
ctx.beginPath();
for (const c of circles) {
ctx.moveTo(c.x + c.r, c.y);
ctx.arc(c.x, c.y, c.r, 0, TAU)
}
ctx.fill();
The move ctx.moveTo(c.x + c.r, c.y); is used to close the previous arc.
You can also close the arc with ctx.closePath but this can be a lot slower when you have many arcs in the path buffer.
// slower than using moveTo
ctx.beginPath();
for (const c of circles) {
ctx.arc(c.x, c.y, c.r, 0, TAU)
ctx.closePath();
}
ctx.fill();
Example
Example draws 600 arcs using the blur filter as it only calls fill once per frame. This should run smooth on all but the most low end devices.
See function drawCircles
requestAnimationFrame(animate);
const ctx = canvas.getContext('2d');
const W = canvas.width;
const BLUR = 5;
const CIRCLE_COUNT = 600;
const MIN_RADIUS = BLUR;
const MAX_RADIUS = 30;
const MAX_DELTA = 1;
const MAX_CIR_R = MAX_RADIUS + BLUR;
const MOVE_SIZE = MAX_CIR_R * 2 + W;
const TAU = 2 * Math.PI;
const setOf = (c, cb, i = 0, a = []) => { while(i < c) { a.push(cb(i++)) } return a };
const rnd = (m, M) => Math.random() * (M - m) + m;
const style = {
filter: "blur(" + BLUR + "px)",
fillStyle: '#B835FF',
};
var currentStyle;
function setStyle(ctx, style) {
if (currentStyle !== style) {
Object.assign(ctx, style);
currentStyle = style;
}
}
const circle = {
get x() { return rnd(-MAX_CIR_R, W + MAX_CIR_R) },
get y() { return rnd(-MAX_CIR_R, W + MAX_CIR_R) },
get dx() { return rnd(-MAX_DELTA, MAX_DELTA) },
get dy() { return rnd(-MAX_DELTA, MAX_DELTA) },
get r() { return rnd(MIN_RADIUS, MAX_RADIUS) },
move() {
var x = this.x + this.dx + MOVE_SIZE + MAX_CIR_R;
var y = this.y + this.dy + MOVE_SIZE + MAX_CIR_R;
this.x = x % MOVE_SIZE - MAX_CIR_R;
this.y = y % MOVE_SIZE - MAX_CIR_R;
}
};
const circles = setOf(CIRCLE_COUNT, () => Object.assign({}, circle));
function drawCircles(circles, ctx, style) {
setStyle(ctx, style);
ctx.beginPath();
for (const c of circles) {
ctx.moveTo(c.x + c.r, c.y);
ctx.arc(c.x, c.y, c.r, 0, TAU);
}
ctx.fill();
}
function updateCircles(circles) {
for (const c of circles) { c.move(); }
}
function animate() {
ctx.clearRect(0,0,W, W);
updateCircles(circles);
drawCircles(circles, ctx, style);
requestAnimationFrame(animate);
}
<canvas id="canvas" width="600" height="600"> </canvas>
If you have several colors, group all the same colors so you can keep the number of fill calls as low as possible.
There are many ways to get the same effect with many colors (each circle a different color) but will need more setup code.
The CanvasRenderingContext2D blur filter is quite heavy - especially if you use it on a canvas consisting of 600 circles. That means on every screen update it has to re-draw 600 circles and apply a blur filter afterwards.
The usual approach is a little different. Initially you create a master texture with a blurred circle. This texture can then be re-used and drawn onto the canvas using the drawImage() method. To vary the size of the circles there is no radius anymore though. We can get the same effect by using a scale instead.
Here's an example:
var context = document.getElementById('stage').getContext('2d');
var radian = Math.PI / 180;
var x = 40;
var y = 40;
var r = 20;
var colorPoints = [];
var frames = 50;
var currentFrame = 0;
var toggle = false;
var iconsLoaded = false;
var texture = document.createElement("canvas");
var textureContext = texture.getContext("2d");
texture.width = 80;
texture.height = 80;
textureContext.filter = 'blur(5px)';
textureContext.fillStyle = '#B835FF';
textureContext.arc(texture.width / 2, texture.height / 2, 25, 0 * radian, 360 * radian, false);
textureContext.fill();
textureContext.closePath();
var drawMultipleCurves = function(ctx) {
if (!iconsLoaded) {
for (let i = 0; i < 600; i++) {
colorPoints.push({
x: Math.floor((Math.random() * 700) + 0),
xMove: Math.floor((Math.random() * 2) + 0),
yMove: Math.floor((Math.random() * 2) + 0),
y: Math.floor((Math.random() * 700) + 0),
scale: 0.2 + Math.random() * 0.8
});
iconsLoaded = true;
}
} else {
for (let i = 0; i < colorPoints.length; i++) {
if (frames === currentFrame) {
toggle = !toggle;
currentFrame = 0;
}
if (!toggle) {
colorPoints[i].xMove === 1 ? colorPoints[i].x = colorPoints[i].x + 5 : colorPoints[i].x = colorPoints[i].x - 5;
colorPoints[i].yMove === 1 ? colorPoints[i].y = colorPoints[i].y + 5 : colorPoints[i].y = colorPoints[i].y - 5;
} else {
colorPoints[i].xMove === 1 ? colorPoints[i].x = colorPoints[i].x - 5 : colorPoints[i].x = colorPoints[i].x + 5;
colorPoints[i].yMove === 1 ? colorPoints[i].y = colorPoints[i].y - 5 : colorPoints[i].y = colorPoints[i].y + 5;
}
ctx.drawImage(texture, colorPoints[i].x, colorPoints[i].y, texture.width * colorPoints[i].scale, texture.height * colorPoints[i].scale);
currentFrame = currentFrame + 1;
}
}
}
var animate = function() {
setTimeout(() => {
context.clearRect(0, 0, 400, 400);
context.beginPath();
drawMultipleCurves(context);
context.fill();
requestAnimationFrame(animate)
})
}
requestAnimationFrame(animate)
<canvas id="stage" width="400" height="400">
<p>Your browser doesn't support canvas.</p>
</canvas>

detecting a click inside a hexagon drawn using canvas?

I'm working on a certain layout where I need to draw a hexagon which needs to be clickable. I'm using the Path2D construct and isPointInPath function. I'm constructing an animation where a number of hexagons is created and then each moved to a certain position. After the movement is done, I am attaching onclick event handlers to certain hexagons. However there is weird behaviour.
Some initialized variables
const COLOR_DARK = "#73b6c6";
const COLOR_LIGHT = "#c3dadd";
const COLOR_PRIMARY = "#39a4c9";
const TYPE_PRIMARY = 'primary';
let hexagons = [];
Below is the function which draws the hexagons.
function drawHex(ctx, x, y, hexProps, stroke, color) {
let myPath = new Path2D();
myPath.moveTo(x + hexProps.width*0.5, y);
myPath.lineTo(x, y + hexProps.height*hexProps.facShort);
myPath.lineTo(x, y + hexProps.height*hexProps.facLong);
myPath.lineTo(x + hexProps.width*0.5, y + hexProps.height);
myPath.lineTo(x + hexProps.width, y + hexProps.height*hexProps.facLong);
myPath.lineTo(x + hexProps.width, y + hexProps.height*hexProps.facShort);
myPath.lineTo(x + hexProps.width*0.5, y);
myPath.closePath();
if (stroke){
ctx.strokeStyle = color;
ctx.stroke(myPath);
} else {
ctx.fillStyle = color;
ctx.fill(myPath);
}
return myPath;
}
This function populates the hexagon array
function populateLeftHex(canvasWidth, canvasHeight, hexProps) {
const startX = canvasWidth / 2;
const startY = canvasHeight / 2;
const baseLeft = canvasWidth * 0.05;
for(let i = 0; i < 5; i++){
let hexNumber = (i % 4 == 0)? 2: 1;
for(let j = 0; j < hexNumber; j++){
hexagons.push({
startX: startX,
startY: startY,
endX: baseLeft + (2 * j) + ((i % 2 == 0)? (hexProps.width * j) : (hexProps.width/2)),
endY: ((i + 1) * hexProps.height) - ((i) * hexProps.height * hexProps.facShort) + (i* 2),
stroke: true,
color: ( i % 2 == 0 && j % 2 == 0)? COLOR_DARK : COLOR_LIGHT,
type: TYPE_PRIMARY
});
}
}
}
And here is where Im calling the isPointInPath function.
window.onload = function (){
const c = document.getElementById('canvas');
const canvasWidth = c.width = window.innerWidth,
canvasHeight = c.height = window.innerHeight,
ctx = c.getContext('2d');
window.requestAnimFrame = (function (callback) {
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {
window.setTimeout(callback, 1000 / 60);
};
})();
console.log(canvasWidth);
let hexProps = {
width: canvasWidth * 0.075,
get height () {
return this.width/Math.sqrt(3) + (1.5)*(this.width/Math.sqrt(2)/2);
} ,
facShort: 0.225,
get facLong () {
return 1 - this.facShort;
}
};
populateLeftHex(canvasWidth, canvasHeight, hexProps);
let pct = 0;
const fps = 200;
animate();
function animate () {
setTimeout(function () {
// increment pct towards 100%
pct += .03;
// if we're not done, request another animation frame
if (pct < 1.00) {
requestAnimFrame(animate);
} else { //if pct is no longer less than 1.00, then the movement animation is over.
hexagons.forEach(function (hex) {
if(hex.type === TYPE_PRIMARY) {
console.info(hex.path);
c.onclick = function(e) {
let x = e.clientX - c.offsetLeft,
y = e.clientY - c.offsetTop;
console.info(ctx.isPointInPath(hex.path, (e.clientX - c.offsetLeft), (e.clientY - c.offsetTop) ));
};
}
})
}
ctx.clearRect(0, 0, c.width, c.height);
// draw all hexagons
for ( let i = 0; i < hexagons.length; i++) {
// get reference to next shape
let hex = hexagons[i];
// note: dx/dy are fixed values
// they could be put in the shape object for efficiency
let dx = hex.endX - hex.startX;
let dy = hex.endY - hex.startY;
let nextX = hex.startX + dx * pct;
let nextY = hex.startY + dy * pct;
hex = hexagons[i];
ctx.fillStyle = hex.color;
hex.path = drawHex(ctx, nextX, nextY, hexProps, hex.stroke, hex.color);
}
}, 1000 / fps);
}
Can you help me figure out what I'm doing wrong? Maybe I misunderstood how Path2D works? Thanks in advance.
Had to do a bit of work to build a test page as your example is incomplete, but this is working for me - though my hexagon is concave...
var myCanvas = document.getElementById("myCanvas");
var ctx = myCanvas.getContext("2d");
var hexProps = {width:100, height:100, facShort:-2, facLong:10};
var hexagons = [];
function drawHex(ctx, x, y, hexProps, stroke, color) {
let myPath = new Path2D();
myPath.moveTo(x + hexProps.width*0.5, y);
myPath.lineTo(x, y + hexProps.height*hexProps.facShort);
myPath.lineTo(x, y + hexProps.height*hexProps.facLong);
myPath.lineTo(x + hexProps.width*0.5, y + hexProps.height);
myPath.lineTo(x + hexProps.width, y + hexProps.height*hexProps.facLong);
myPath.lineTo(x + hexProps.width, y + hexProps.height*hexProps.facShort);
myPath.lineTo(x + hexProps.width*0.5, y);
myPath.closePath();
if (stroke){
ctx.strokeStyle = color;
ctx.stroke(myPath);
} else {
ctx.fillStyle = color;
ctx.fill(myPath);
}
return myPath;
}
hexagons.push({type:0, path:drawHex(ctx,100,100,hexProps,false,"#0f0")});
hexagons.forEach(function (hex) {
if(hex.type === 0) {
console.info(hex.path);
myCanvas.onclick = function(e) {
let x = e.clientX - myCanvas.offsetLeft,
y = e.clientY - myCanvas.offsetTop;
console.info(x,y);
console.info(ctx.isPointInPath(hex.path, (e.clientX -
myCanvas.offsetLeft), (e.clientY - myCanvas.offsetTop) ));
};
}
})
<canvas width=500 height=500 id=myCanvas style='border:1px solid red'></canvas>
Test clicks give true and false where expected:
test.htm:48 165 168
test.htm:49 true
test.htm:48 151 336
test.htm:49 false
test.htm:48 124 314
test.htm:49 true
test.htm:48 87 311
test.htm:49 false

How to draw only visible part of the tilemap on js canvas?

I created simple tilemap using Tiled (3200 x 3200 pixels). I loaded it on my canvas using this library
I draw entire tilemap 3200 x 3200 60 times per seocnd.
I tried to move around and it works fine. Btw, I move around canvas using ctx.translate. I included this in my own function
But when I created bigger map in Tiled ( 32000 x 32000 pixels ) - I got a very freezing page. I couldn't move around fast, I think there was about 10 fps
So how to fix it? I have to call drawTiles() function 60 times per second. But is there any way to draw only visible part of the tile? Like draw only what I see on my screen (0, 0, monitorWidth, monitorHeight I guess)
Thank you
##Drawing a large tileset
If you have a large tile set and only see part of it in the canvas you just need to calculate the tile at the top left of the canvas and the number of tiles across and down that will fit the canvas.
Then draw the square array of tiles that fit the canvas.
In the example the tile set is 1024 by 1024 tiles (worldTileCount = 1024), each tile is 64 by 64 pixels tileSize = 64, making the total playfield 65536 pixels square
The position of the top left tile is set by the variables worldX, worldY
###Function to draw tiles
// val | 0 is the same as Math.floor(val)
var worldX = 512 * tileSize; // pixel position of playfield
var worldY = 512 * tileSize;
function drawWorld(){
const c = worldTileCount; // get the width of the tile array
const s = tileSize; // get the tile size in pixels
// get the tile position
const tx = worldX / s | 0; // get the top left tile
const ty = worldY / s | 0;
// get the number of tiles that will fit the canvas
const tW = (canvas.width / s | 0) + 2;
const tH = (canvas.height / s | 0) + 2;
// set the location. Must floor to pixel boundary or you get holes
ctx.setTransform(1,0,0,1,-worldX | 0,-worldY | 0);
// Draw the tiles across and down
for(var y = 0; y < tH; y += 1){
for(var x = 0; x < tW; x += 1){
// get the index into the tile array for the tile at x,y plus the topleft tile
const i = tx + x + (ty + y) * c;
// get the tile id from the tileMap. If outside map default to tile 6
const tindx = tileMap[i] === undefined ? 6 : tileMap[i];
// draw the tile at its location. last 2 args are x,y pixel location
imageTools.drawSpriteQuick(tileSet, tindx, (tx + x) * s, (ty + y) * s);
}
}
}
###setTransform and absolute coordinates.
Use absolute coordinates makes everything simple.
Use the canvas context setTransform to set the world position and then each tile can be drawn at its own coordinate.
// set the world location. The | 0 floors the values and ensures no holes
ctx.setTransform(1,0,0,1,-worldX | 0,-worldY | 0);
That way if you have a character at position 51023, 34256 you can just draw it at that location.
playerX = 51023;
playerY = 34256;
ctx.drawImage(myPlayerImage,playerX,playerY);
If you want the tile map relative to the player then just set the world position to be half the canvas size up and to the left plus one tile to ensure overlap
playerX = 51023;
playerY = 34256;
worldX = playerX - canvas.width / 2 - tileWidth;
worldY = playerY - canvas.height / 2 - tileHeight;
###Demo of large 65536 by 65536 pixel tile map.
At 60fps if you have the horses and can handle much much bigger without any frame rate loss. (map size limit using this method is approx 4,000,000,000 by 4,000,000,000pixels (32 bit integers coordinates))
#UPDATE 15/5/2019 re Jitter
The comments have pointed out that there is some jitter as the map scrolls.
I have made changes to smooth out the random path with a strong ease in out turn every 240 frame (4 seconds at 60fps) Also added a frame rate reducer, if you click and hold the mouse button on the canvas the frame rate will be slowed to 1/8th normal so that the jitter is easier to see.
There are two reasons for the jitter.
###Time error
The first and least is the time passed to the update function by requestAnimationFrame, the interval is not perfect and rounding errors due to the time is compounding the alignment problems.
To reduce the time error I have set the move speed to a constant interval to minimize the rounding error drift between frames.
###Aligning tiles to pixels
The main reason for the jitter is that the tiles must be rendered on pixel boundaries. If not then aliasing errors will create visible seams between tiles.
To see the difference click the button top left to toggle pixel alignment on and off.
To get smooth scrolling (sub pixel positioning) draw the map to an offscreen canvas aligning to the pixels, then render that canvas to the display canvas adding the sub pixel offset. That will give the best possible result using the canvas. For better you will need to use webGL
###End of update
var refereshSkip = false; // when true drops frame rate by 4
var dontAlignToPixel = false;
var ctx = canvas.getContext("2d");
function mouseEvent(e) {
if(e.type === "click") {
dontAlignToPixel = !dontAlignToPixel;
pixAlignInfo.textContent = dontAlignToPixel ? "Pixel Align is OFF" : "Pixel Align is ON";
} else {
refereshSkip = e.type === "mousedown";
}
}
pixAlignInfo.addEventListener("click",mouseEvent);
canvas.addEventListener("mousedown",mouseEvent);
canvas.addEventListener("mouseup",mouseEvent);
// wait for code under this to setup
setTimeout(() => {
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
// create tile map
const worldTileCount = 1024;
const tileMap = new Uint8Array(worldTileCount * worldTileCount);
// add random tiles
doFor(worldTileCount * worldTileCount, i => {
tileMap[i] = randI(1, tileCount);
});
// this is the movement direction of the map
var worldDir = Math.PI / 4;
/* =======================================================================
Drawing the tileMap
========================================================================*/
var worldX = 512 * tileSize;
var worldY = 512 * tileSize;
function drawWorld() {
const c = worldTileCount; // get the width of the tile array
const s = tileSize; // get the tile size in pixels
const tx = worldX / s | 0; // get the top left tile
const ty = worldY / s | 0;
const tW = (canvas.width / s | 0) + 2; // get the number of tiles to fit canvas
const tH = (canvas.height / s | 0) + 2;
// set the location
if(dontAlignToPixel) {
ctx.setTransform(1, 0, 0, 1, -worldX,-worldY);
} else {
ctx.setTransform(1, 0, 0, 1, Math.floor(-worldX),Math.floor(-worldY));
}
// Draw the tiles
for (var y = 0; y < tH; y += 1) {
for (var x = 0; x < tW; x += 1) {
const i = tx + x + (ty + y) * c;
const tindx = tileMap[i] === undefined ? 6 : tileMap[i];
imageTools.drawSpriteQuick(tileSet, tindx, (tx + x) * s, (ty + y) * s);
}
}
}
var timer = 0;
var refreshFrames = 0;
const dirChangeMax = 3.5;
const framesBetweenDirChange = 240;
var dirChangeDelay = 1;
var dirChange = 0;
var prevDir = worldDir;
const eCurve = (v, p = 2) => v < 0 ? 0 : v > 1 ? 1 : v ** p / (v ** p + (1 - v) ** p);
//==============================================================
// main render function
function update() {
refreshFrames ++;
if(!refereshSkip || (refereshSkip && refreshFrames % 8 === 0)){
timer += 1000 / 60;
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
if (w !== innerWidth || h !== innerHeight) {
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
} else {
ctx.clearRect(0, 0, w, h);
}
// Move the map
var speed = Math.sin(timer / 10000) * 8;
worldX += Math.cos(worldDir) * speed;
worldY += Math.sin(worldDir) * speed;
if(dirChangeDelay-- <= 0) {
dirChangeDelay = framesBetweenDirChange;
prevDir = worldDir = prevDir + dirChange;
dirChange = rand(-dirChangeMax , dirChangeMax);
}
worldDir = prevDir + (1-eCurve(dirChangeDelay / framesBetweenDirChange,3)) * dirChange;
// Draw the map
drawWorld();
}
requestAnimationFrame(update);
}
requestAnimationFrame(update);
}, 0);
/*===========================================================================
CODE FROM HERE DOWN UNRELATED TO THE ANSWER
===========================================================================*/
const imageTools = (function() {
// This interface is as is. No warenties no garenties, and NOT to be used comercialy
var workImg, workImg1, keep; // for internal use
keep = false;
var tools = {
canvas(width, height) { // create a blank image (canvas)
var c = document.createElement("canvas");
c.width = width;
c.height = height;
return c;
},
createImage: function(width, height) {
var i = this.canvas(width, height);
i.ctx = i.getContext("2d");
return i;
},
drawSpriteQuick: function(image, spriteIndex, x, y) {
var w, h, spr;
spr = image.sprites[spriteIndex];
w = spr.w;
h = spr.h;
ctx.drawImage(image, spr.x, spr.y, w, h, x, y, w, h);
},
line(x1, y1, x2, y2) {
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
},
circle(x, y, r) {
ctx.moveTo(x + r, y);
ctx.arc(x, y, r, 0, Math.PI * 2);
},
};
return tools;
})();
const doFor = (count, cb) => {
var i = 0;
while (i < count && cb(i++) !== true);
}; // the ; after while loop is important don't remove
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const seededRandom = (() => {
var seed = 1;
return {
max: 2576436549074795,
reseed(s) {
seed = s
},
random() {
return seed = ((8765432352450986 * seed) + 8507698654323524) % this.max
}
}
})();
const randSeed = (seed) => seededRandom.reseed(seed | 0);
const randSI = (min, max = min + (min = 0)) => (seededRandom.random() % (max - min)) + min;
const randS = (min = 1, max = min + (min = 0)) => (seededRandom.random() / seededRandom.max) * (max - min) + min;
const tileSize = 64;
const tileCount = 7;
function drawGrass(ctx, c1, c2, c3) {
const s = tileSize;
const gs = s / (8 * c3);
ctx.fillStyle = c1;
ctx.fillRect(0, 0, s, s);
ctx.strokeStyle = c2;
ctx.lineWidth = 2;
ctx.lineCap = "round";
ctx.beginPath();
doFor(s, i => {
const x = rand(-gs, s + gs);
const y = rand(-gs, s + gs);
const x1 = rand(x - gs, x + gs);
const y1 = rand(y - gs, y + gs);
imageTools.line(x, y, x1, y1);
imageTools.line(x + s, y, x1 + s, y1);
imageTools.line(x - s, y, x1 - s, y1);
imageTools.line(x, y + s, x1, y1 + s);
imageTools.line(x, y - s, x1, y1 - s);
})
ctx.stroke();
}
function drawTree(ctx, c1, c2, c3) {
const seed = Date.now();
const s = tileSize;
const gs = s / 2;
const gh = gs / 2;
ctx.fillStyle = c1;
ctx.strokeStyle = "#000";
ctx.lineWidth = 2;
ctx.save();
ctx.shadowColor = "rgba(0,0,0,0.5)";
ctx.shadowBlur = 4;
ctx.shadowOffsetX = 8;
ctx.shadowOffsetY = 8;
randSeed(seed);
ctx.beginPath();
doFor(18, i => {
const ss = 1 - i / 18;
imageTools.circle(randS(gs - gh * ss, gs + gh * ss), randS(gs - gh * ss, gs + gh * ss), randS(gh / 4, gh / 2));
})
ctx.stroke();
ctx.fill();
ctx.restore();
ctx.fillStyle = c2;
ctx.strokeStyle = c3;
ctx.lineWidth = 2;
ctx.save();
randSeed(seed);
ctx.beginPath();
doFor(18, i => {
const ss = 1 - i / 18;
imageTools.circle(randS(gs - gh * ss, gs + gh * ss) - 2, randS(gs - gh * ss, gs + gh * ss) - 2, randS(gh / 4, gh / 2) / 1.6);
})
ctx.stroke();
ctx.fill();
ctx.restore();
}
const tileRenders = [
(ctx) => {
drawGrass(ctx, "#4C4", "#4F4", 1)
},
(ctx) => {
drawGrass(ctx, "#644", "#844", 2)
},
(ctx) => {
tileRenders[0](ctx);
drawTree(ctx, "#480", "#8E0", "#7C0")
},
(ctx) => {
tileRenders[1](ctx);
drawTree(ctx, "#680", "#AE0", "#8C0")
},
(ctx) => {
drawGrass(ctx, "#008", "#00A", 4)
},
(ctx) => {
drawGrass(ctx, "#009", "#00C", 4)
},
(ctx) => {
drawGrass(ctx, "#00B", "#00D", 4)
},
]
const tileSet = imageTools.createImage(tileSize * tileCount, tileSize);
const ctxMain = ctx;
ctx = tileSet.ctx;
tileSet.sprites = [];
doFor(tileCount, i => {
x = i * tileSize;
ctx.save();
ctx.setTransform(1, 0, 0, 1, x, 0);
ctx.beginPath();
ctx.rect(0, 0, tileSize, tileSize);
ctx.clip()
if (tileRenders[i]) {
tileRenders[i](ctx)
}
tileSet.sprites.push({
x,
y: 0,
w: tileSize,
h: tileSize
});
ctx.restore();
});
ctx = ctxMain;
canvas {
position: absolute;
top: 0px;
left: 0px;
}
div {
position: absolute;
top: 8px;
left: 8px;
color: white;
}
#pixAlignInfo {
color: yellow;
cursor: pointer;
border: 2px solid green;
margin: 4px;
}
#pixAlignInfo:hover {
color: white;
background: #0008;
cursor: pointer;
}
body {
background: #49c;
}
<canvas id="canvas"></canvas>
<div>Hold left button to slow to 1/8th<br>
<span id="pixAlignInfo">Click this button to toggle pixel alignment. Alignment is ON</span></div>

hyperdrive effect in canvas across randomly placed circles

I'm trying to create a hyperdrive effect, like from Star Wars, where the stars have a motion trail. I've gotten as far as creating the motion trail on a single circle, it still looks like the trail is going down in the y direction and not forwards or positive in the z direction.
Also, how could I do this with (many) randomly placed circles as if they were stars?
My code is on jsfiddle (https://jsfiddle.net/5m7x5zxu/) and below:
var canvas = document.querySelector("canvas");
var context = canvas.getContext("2d");
var xPos = 180;
var yPos = 100;
var motionTrailLength = 16;
var positions = [];
function storeLastPosition(xPos, yPos) {
// push an item
positions.push({
x: xPos,
y: yPos
});
//get rid of first item
if (positions.length > motionTrailLength) {
positions.pop();
}
}
function update() {
context.clearRect(0, 0, canvas.width, canvas.height);
for (var i = positions.length-1; i > 0; i--) {
var ratio = (i - 1) / positions.length;
drawCircle(positions[i].x, positions[i].y, ratio);
}
drawCircle(xPos, yPos, "source");
var k=2;
storeLastPosition(xPos, yPos);
// update position
if (yPos > 125) {
positions.pop();
}
else{
yPos += k*1.1;
}
requestAnimationFrame(update);
}
update();
function drawCircle(x, y, r) {
if (r == "source") {
r = 1;
} else {
r*=1.1;
}
context.beginPath();
context.arc(x, y, 3, 0, 2 * Math.PI, true);
context.fillStyle = "rgba(255, 255, 255, " + parseFloat(1-r) + ")";
context.fill();
}
Canvas feedback and particles.
This type of FX can be done many ways.
You could just use a particle systems and draw stars (as lines) moving away from a central point, as the speed increase you increase the line length. When at low speed the line becomes a circle if you set ctx.lineWidth > 1 and ctx.lineCap = "round"
To add to the FX you can use render feedback as I think you have done by rendering the canvas over its self. If you render it slightly larger you get a zoom FX. If you use ctx.globalCompositeOperation = "lighter" you can increase the stars intensity as you speed up to make up for the overall loss of brightness as stars move faster.
Example
I got carried away so you will have to sift through the code to find what you need.
The particle system uses the Point object and a special array called bubbleArray to stop GC hits from janking the animation.
You can use just an ordinary array if you want. The particles are independent of the bubble array. When they have moved outside the screen they are move to a pool and used again when a new particle is needed. The update function moves them and the draw Function draws them I guess LOL
The function loop is the main loop and adds and draws particles (I have set the particle count to 400 but should handle many more)
The hyper drive is operated via the mouse button. Press for on, let go for off. (It will distort the text if it's being displayed)
The canvas feedback is set via that hyperSpeed variable, the math is a little complex. The sCurce function just limits the value to 0,1 in this case to stop alpha from going over or under 1,0. The hyperZero is just the sCurve return for 1 which is the hyper drives slowest speed.
I have pushed the feedback very close to the limit. In the first few lines of the loop function you can set the top speed if(mouse.button){ if(hyperSpeed < 1.75){ Over this value 1.75 and you will start to get bad FX, at about 2 the whole screen will just go white (I think that was where)
Just play with it and if you have questions ask in the comments.
const ctx = canvas.getContext("2d");
// very simple mouse
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
// High performance array pool using buubleArray to separate pool objects and active object.
// This is designed to eliminate GC hits involved with particle systems and
// objects that have short lifetimes but used often.
// Warning this code is not well tested.
const bubbleArray = () => {
const items = [];
var count = 0;
return {
clear(){ // warning this dereferences all locally held references and can incur Big GC hit. Use it wisely.
this.items.length = 0;
count = 0;
},
update() {
var head, tail;
head = tail = 0;
while(head < count){
if(items[head].update() === false) {head += 1 }
else{
if(tail < head){
const temp = items[head];
items[head] = items[tail];
items[tail] = temp;
}
head += 1;
tail += 1;
}
}
return count = tail;
},
createCallFunction(name, earlyExit = false){
name = name.split(" ")[0];
const keys = Object.keys(this);
if(Object.keys(this).indexOf(name) > -1){ throw new Error(`Can not create function name '${name}' as it already exists.`) }
if(!/\W/g.test(name)){
let func;
if(earlyExit){
func = `var items = this.items; var count = this.getCount(); var i = 0;\nwhile(i < count){ if (items[i++].${name}() === true) { break } }`;
}else{
func = `var items = this.items; var count = this.getCount(); var i = 0;\nwhile(i < count){ items[i++].${name}() }`;
}
!this.items && (this.items = items);
this[name] = new Function(func);
}else{ throw new Error(`Function name '${name}' contains illegal characters. Use alpha numeric characters.`) }
},
callEach(name){var i = 0; while(i < count){ if (items[i++][name]() === true) { break } } },
each(cb) { var i = 0; while(i < count){ if (cb(items[i], i++) === true) { break } } },
next() { if (count < items.length) { return items[count ++] } },
add(item) {
if(count === items.length){
items.push(item);
count ++;
}else{
items.push(items[count]);
items[count++] = item;
}
return item;
},
getCount() { return count },
}
}
// Helpers rand float, randI random Int
// doFor iterator
// sCurve curve input -Infinity to Infinity out -1 to 1
// randHSLA creates random colour
// CImage, CImageCtx create image and image with context attached
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); }; // the ; after while loop is important don't remove
const sCurve = (v,p) => (2 / (1 + Math.pow(p,-v))) -1;
const randHSLA = (h, h1, s = 100, s1 = 100, l = 50, l1 = 50, a = 1, a1 = 1) => { return `hsla(${randI(h,h1) % 360},${randI(s,s1)}%,${randI(l,l1)}%,${rand(a,a1)})` }
const CImage = (w = 128, h = w) => (c = document.createElement("canvas"),c.width = w,c.height = h, c);
const CImageCtx = (w = 128, h = w) => (c = CImage(w,h), c.ctx = c.getContext("2d"), c);
// create image to hold text
var textImage = CImageCtx(1024, 1024);
var c = textImage.ctx;
c.fillStyle = "#FF0";
c.font = "64px arial black";
c.textAlign = "center";
c.textBaseline = "middle";
const text = "HYPER,SPEED FX,VII,,Battle of Jank,,Hold the mouse,button to increase,speed.".split(",");
text.forEach((line,i) => { c.fillText(line,512,i * 68 + 68) });
const maxLines = text.length * 68 + 68;
function starWarIntro(image,x1,y1,x2,y2,pos){
var iw = image.width;
var ih = image.height;
var hh = (x2 - x1) / (y2 - y1); // Slope of left edge
var w2 = iw / 2; // half width
var z1 = w2 - x1; // Distance (z) to first line
var z2 = (z1 / (w2 - x2)) * z1 - z1; // distance (z) between first and last line
var sk,t3,t3a,z3a,lines, z3, dd = 0, a = 0, as = 2 / (y2 - y1);
for (var y = y1; y < y2 && dd < maxLines; y++) { // for each line
t3 = ((y - y1) * hh) + x1; // get scan line top left edge
t3a = (((y+1) - y1) * hh) + x1; // get scan line bottom left edge
z3 = (z1 / (w2 - t3)) * z1; // get Z distance to top of this line
z3a = (z1 / (w2 - t3a)) * z1; // get Z distance to bottom of this line
dd = ((z3 - z1) / z2) * ih; // get y bitmap coord
a += as;
ctx.globalAlpha = a < 1 ? a : 1;
dd += pos; // kludge for this answer to make text move
// does not move text correctly
lines = ((z3a - z1) / z2) * ih-dd; // get number of lines to copy
ctx.drawImage(image, 0, dd , iw, lines, t3, y, w - t3 * 2, 1.5);
}
}
// canvas settings
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
// diagonal distance used to set point alpha (see point update)
var diag = Math.sqrt(w * w + h * h);
// If window size is changed this is called to resize the canvas
// It is not called via the resize event as that can fire to often and
// debounce makes it feel sluggish so is called from main loop.
function resizeCanvas(){
points.clear();
canvas.width = innerWidth;
canvas.height = innerHeight;
w = canvas.width;
h = canvas.height;
cw = w / 2; // center
ch = h / 2;
diag = Math.sqrt(w * w + h * h);
}
// create array of points
const points = bubbleArray();
// create optimised draw function itterator
points.createCallFunction("draw",false);
// spawns a new star
function spawnPoint(pos){
var p = points.next();
p = points.add(new Point())
if (p === undefined) { p = points.add(new Point()) }
p.reset(pos);
}
// point object represents a single star
function Point(pos){ // this function is duplicated as reset
if(pos){
this.x = pos.x;
this.y = pos.y;
this.dead = false;
}else{
this.x = 0;
this.y = 0;
this.dead = true;
}
this.alpha = 0;
var x = this.x - cw;
var y = this.y - ch;
this.dir = Math.atan2(y,x);
this.distStart = Math.sqrt(x * x + y * y);
this.speed = rand(0.01,1);
this.col = randHSLA(220,280,100,100,50,100);
this.dx = Math.cos(this.dir) * this.speed;
this.dy = Math.sin(this.dir) * this.speed;
}
Point.prototype = {
reset : Point, // resets the point
update(){ // moves point and returns false when outside
this.speed *= hyperSpeed; // increase speed the more it has moved
this.x += Math.cos(this.dir) * this.speed;
this.y += Math.sin(this.dir) * this.speed;
var x = this.x - cw;
var y = this.y - ch;
this.alpha = (Math.sqrt(x * x + y * y) - this.distStart) / (diag * 0.5 - this.distStart);
if(this.alpha > 1 || this.x < 0 || this.y < 0 || this.x > w || this.h > h){
this.dead = true;
}
return !this.dead;
},
draw(){ // draws the point
ctx.strokeStyle = this.col;
ctx.globalAlpha = 0.25 + this.alpha *0.75;
ctx.beginPath();
ctx.lineTo(this.x - this.dx * this.speed, this.y - this.dy * this.speed);
ctx.lineTo(this.x, this.y);
ctx.stroke();
}
}
const maxStarCount = 400;
const p = {x : 0, y : 0};
var hyperSpeed = 1.001;
const alphaZero = sCurve(1,2);
var startTime;
function loop(time){
if(startTime === undefined){
startTime = time;
}
if(w !== innerWidth || h !== innerHeight){
resizeCanvas();
}
// if mouse down then go to hyper speed
if(mouse.button){
if(hyperSpeed < 1.75){
hyperSpeed += 0.01;
}
}else{
if(hyperSpeed > 1.01){
hyperSpeed -= 0.01;
}else if(hyperSpeed > 1.001){
hyperSpeed -= 0.001;
}
}
var hs = sCurve(hyperSpeed,2);
ctx.globalAlpha = 1;
ctx.setTransform(1,0,0,1,0,0); // reset transform
//==============================================================
// UPDATE the line below could be the problem. Remove it and try
// what is under that
//==============================================================
//ctx.fillStyle = `rgba(0,0,0,${1-(hs-alphaZero)*2})`;
// next two lines are the replacement
ctx.fillStyle = "Black";
ctx.globalAlpha = 1-(hs-alphaZero) * 2;
//==============================================================
ctx.fillRect(0,0,w,h);
// the amount to expand canvas feedback
var sx = (hyperSpeed-1) * cw * 0.1;
var sy = (hyperSpeed-1) * ch * 0.1;
// increase alpha as speed increases
ctx.globalAlpha = (hs-alphaZero)*2;
ctx.globalCompositeOperation = "lighter";
// draws feedback twice
ctx.drawImage(canvas,-sx, -sy, w + sx*2 , h + sy*2)
ctx.drawImage(canvas,-sx/2, -sy/2, w + sx , h + sy)
ctx.globalCompositeOperation = "source-over";
// add stars if count < maxStarCount
if(points.getCount() < maxStarCount){
var cent = (hyperSpeed - 1) *0.5; // pulls stars to center as speed increases
doFor(10,()=>{
p.x = rand(cw * cent ,w - cw * cent); // random screen position
p.y = rand(ch * cent,h - ch * cent);
spawnPoint(p)
})
}
// as speed increases make lines thicker
ctx.lineWidth = 2 + hs*2;
ctx.lineCap = "round";
points.update(); // update points
points.draw(); // draw points
ctx.globalAlpha = 1;
// scroll the perspective star wars text FX
var scrollTime = (time - startTime) / 5 - 2312;
if(scrollTime < 1024){
starWarIntro(textImage,cw - h * 0.5, h * 0.2, cw - h * 3, h , scrollTime );
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
canvas { position : absolute; top : 0px; left : 0px; }
<canvas id="canvas"></canvas>
Here's another simple example, based mainly on the same idea as Blindman67, concetric lines moving away from center at different velocities (the farther from center, the faster it moves..) also no recycling pool here.
"use strict"
var c = document.createElement("canvas");
document.body.append(c);
var ctx = c.getContext("2d");
var w = window.innerWidth;
var h = window.innerHeight;
var ox = w / 2;
var oy = h / 2;
c.width = w; c.height = h;
const stars = 120;
const speed = 0.5;
const trailLength = 90;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, w, h);
ctx.fillStyle = "#fff"
ctx.fillRect(ox, oy, 1, 1);
init();
function init() {
var X = [];
var Y = [];
for(var i = 0; i < stars; i++) {
var x = Math.random() * w;
var y = Math.random() * h;
X.push( translateX(x) );
Y.push( translateY(y) );
}
drawTrails(X, Y)
}
function translateX(x) {
return x - ox;
}
function translateY(y) {
return oy - y;
}
function getDistance(x, y) {
return Math.sqrt(x * x + y * y);
}
function getLineEquation(x, y) {
return function(n) {
return y / x * n;
}
}
function drawTrails(X, Y) {
var count = 1;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, w, h);
function anim() {
for(var i = 0; i < X.length; i++) {
var x = X[i];
var y = Y[i];
drawNextPoint(x, y, count);
}
count+= speed;
if(count < trailLength) {
window.requestAnimationFrame(anim);
}
else {
init();
}
}
anim();
}
function drawNextPoint(x, y, step) {
ctx.fillStyle = "#fff";
var f = getLineEquation(x, y);
var coef = Math.abs(x) / 100;
var dist = getDistance( x, y);
var sp = speed * dist / 100;
for(var i = 0; i < sp; i++) {
var newX = x + Math.sign(x) * (step + i) * coef;
var newY = translateY( f(newX) );
ctx.fillRect(newX + ox, newY, 1, 1);
}
}
body {
overflow: hidden;
}
canvas {
position: absolute;
left: 0;
top: 0;
}

How to make a game on javascript with canvas that makes a sprite appear to be jumping

I am new to javascript, and am trying to make a game that would hopefully end up isometric (I don't care so much about that, as long as I get an idea of how to). My code is:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<style>
canvas {
border:1px solid #d3d3d3;
background-color: #f1f1f1;
}
</style>
</head>
<body onload="startGame()">
<script>
var myGamePiece;
function startGame() {
myGamePiece = new component(30, 30, "blue", 225, 225);
myGameArea.start();
}
var myGameArea = {
canvas : document.createElement("canvas"),
start : function() {
this.canvas.width = 480;
this.canvas.height = 270;
this.context = this.canvas.getContext("2d");
document.body.insertBefore(this.canvas, document.body.childNodes[0]);
this.frameNo = 0;
this.interval = setInterval(updateGameArea, 20);
window.addEventListener('keydown', function (e) {
e.preventDefault();
myGameArea.keys = (myGameArea.keys || []);
myGameArea.keys[e.keyCode] = (e.type == "keydown");
})
window.addEventListener('keyup', function (e) {
myGameArea.keys[e.keyCode] = (e.type == "keydown");
})
},
stop : function() {
clearInterval(this.interval);
},
clear : function() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
function component(width, height, color, x, y, type) {
this.type = type;
this.width = width;
this.height = height;
this.speed = 0;
this.angle = 0;
this.moveAngle = 0;
this.x = x;
this.y = y;
this.update = function() {
ctx = myGameArea.context;
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.angle);
ctx.fillStyle = color;
ctx.fillRect(this.width / -2, this.height / -2, this.width, this.height);
ctx.restore();
}
this.newPos = function() {
this.angle += this.moveAngle * Math.PI / 180;
this.x += this.speed * Math.sin(this.angle);
this.y -= this.speed * Math.cos(this.angle);
}
}
function updateGameArea() {
myGameArea.clear();
myGamePiece.moveAngle = 0;
myGamePiece.speed = 0;
if (myGameArea.keys && myGameArea.keys[37]) {myGamePiece.x -=2; }
if (myGameArea.keys && myGameArea.keys[39]) {myGamePiece.x += 2; }
if (myGameArea.keys && myGameArea.keys[38]) {myGamePiece.y -= 1; }
if (myGameArea.keys && myGameArea.keys[40]) {myGamePiece.y += 1; }
if (myGameArea.keys && myGameArea.keys[32]) {myGamePiece.y -= 3;}
myGamePiece.newPos();
myGamePiece.update();
}
</script>
<p></p>
</body>
</html>
which I mostly copied and pasted from another website (http://www.w3schools.com/games/tryit.asp?filename=trygame_movement_keyboard). What I want to know is how to make it so that when the player presses space, myGamePiece goes up and down to appear to be jumping; making it move up a certain number of spaces, but then return back to the coordinates it was before.
Game Physics. JUMPING the basics
Real world V game world.
Game jumping is usually done non deterministically, that means you are not sure when or where the play may land. Very much not like real life. In real life once you jump, where and when you land is up to gravity and air friction, unless you can fly the result of jumping is up to the universe.
In the game world this is far from true. The jumper can usually change direction, double jump, do some hang time, or combo accelerated power punch down. All these things can happen at any time depending on the input of the user. Also gravity in the game world does not act like real gravity, sometimes some thing fall faster because they are heavy, some things need a second or to to feel the effect of gravity. The list goes on.
FALLING
BUT with all that said the game must still do the important thing that makes falling unlike an elevator ride. When in free fall you accelerate, every instance of time your speed changes, when you jump up you decelerate, when you fall you accelerate. We have our position y and our speed dy (delta y) to add gravity (g) we add a constant to the speed, when traveling up the screen (dy is < 0) or down the gravity changes the speed in the same direction at the same rate.
So every frame, add gravity dy += g then add our speed to our position y += dy. And that is it a very simple simulation of gravity, which if you measure time in game frames is also a perfect simulation of real gravity (near a big thing like the earth)
Thus the best way to do things like jumping, and the gravity that comes into play is to do it frame by frame.
Lets define what we need to do a jump.
A simple character
var c = {
x : ?, // this character's position
y : ?,
dx : ?, // the amount to move per frame The players velocity in x and y
dy : ?,
w : ?, // the character's width and height
h : ?,
onGround : false, // a flag to indicate on the ground or not
}
And some environment info
const GROUND_Y = canvas.height - 10; // where the ground is
const GRAVITY = 1; // in pixels per frame
Then every frame we update the character checking if on the ground and if not applying gravity and checking for the ground.
c.update = function(){
if(this.onGround){ // nothing to do but wait
}else{ // must be in the air
// Every frame the player accelerates down by the pull of gravity
// so increase the player y speed
this.dy += GRAVITY; // apply the gravity to the speed.
// now add the y speed to the y position
this.y += this.dy;
// Now we must check for the ground which if the player position x,y is for
// its center the ground will be half it's height away
if(this.y + (this.h / 2) > GROUND_Y){ // have we hit the ground
// yes stop downward motion
this.dy = 0;
// the speed may have put the character slightly below the ground
// so fix the postion so that it is correct
this.y = GROUND_Y - this.h /2; // set position to the ground - half its height
// And set the flag to indicate that the character is on the ground
this.onGround = true;
}
}
}
So that is gravity taken care of.
JUMPING
To jump we apply a force that accelerates us away from the ground. This force is only for an instant, once of the ground we have nothing to push against so we can apply no more force, it is up to gravity to bring us down. As gravity has been sorted in the above function all we need to do is the apply the jumping force.
const JUMP_ACCELERATION = GRAVITY * 20; // the bigger this number the higher the jump
Now add the function to make the jump
c.jump = function(){
// check if we can jump. That is are we on the ground
if(this.onGround){
// flag that we are no longer on the ground and left to the will of gravity
this.onGround = false;
// then apply the change in speed.
this.dy -= JUMP_ACCELERATION; // subtract jump accel from the speed
// to give a negative speed (up)
}
}
And that is it, the gravity function will take care of everything for you so you must call the c.update Function once every frame, the jump function you call only once per jump.
JUMPING IT DEMO
Click mouse to jump, a non challenging flappy It.
Taken from an old project this demo shows a very simple jumping character. The object name is it and the functions you want to look at are it.update(), it.jump(), and it.preJump() The code you want is between the comments //Answer code
All the character can do is jump, it can multy jump, and can jump higher if you click and hold the mouse, then release to jump.
/** ImageTools.js begin **/
var imageTools = (function () {
var tools = {
canvas : function (width, height) { // create a blank image (canvas)
var c = document.createElement("canvas");
c.width = width;
c.height = height;
return c;
},
createImage : function (width, height) {
var i = this.canvas(width, height);
i.ctx = i.getContext("2d");
return i;
},
loadImage : function (url, cb) {
var i = new Image();
i.src = url;
i.addEventListener('load', cb);
i.addEventListener('error', cb);
return i;
},
image2Canvas : function (img) {
var i = this.canvas(img.width, img.height);
i.ctx = i.getContext("2d");
i.drawImage(i, 0, 0);
return i;
},
drawSpriteLinked : function(image,spriteIndex, x, y, scale, ang, alpha) {
var w,h,spr;
spr = image.sprites[spriteIndex];
w = spr.w; h = spr.h;
ctx.globalAlpha = alpha;
var xdx = Math.cos(ang) * scale;
var xdy = Math.sin(ang) * scale;
ctx.save();
ctx.transform(xdx, xdy, -xdy, xdx, x, y);
ctx.drawImage(image, spr.x, spr.y, w, h, -w/2, -h/2, w, h);
ctx.restore();
},
drawSprite : function(image,spriteIndex, x, y, scale, ang, alpha) {
var w,h,spr;
spr = image.sprites[spriteIndex];
w = spr.w; h = spr.h;
ctx.globalAlpha = alpha;
ctx.setTransform(scale, 0, 0, scale, x, y);
ctx.rotate(ang);
ctx.drawImage(image, spr.x, spr.y, w, h, -w/2, -h/2, w, h);
},
drawSpriteSLinked : function(image,spriteIndex, x, y, scale, scaleX, ang, alpha) {
var w,h,spr;
spr = image.sprites[spriteIndex];
w = spr.w; h = spr.h;
ctx.globalAlpha = alpha;
var xdx = Math.cos(ang) * scale;
var xdy = Math.sin(ang) * scale;
ctx.save()
ctx.transform(xdx * scaleX, xdy * scaleX, -xdy, xdx, x, y);
ctx.drawImage(image, spr.x, spr.y, w, h, -w/2, -h/2, w, h);
ctx.restore();
},
drawSpriteS : function(image,spriteIndex, x, y, scale, scaleX, ang, alpha) {
var w,h,spr;
spr = image.sprites[spriteIndex];
w = spr.w; h = spr.h;
ctx.globalAlpha = alpha;
ctx.setTransform(scale * scaleX, 0, 0, scale, x, y);
ctx.rotate(ang);
ctx.drawImage(image, spr.x, spr.y, w, h, -w/2, -h/2, w, h);
},
hex2RGBA : function(hex){
if(typeof hex === "string"){
var str = "rgba(";
if(hex.length === 4 || hex.length === 5){
str += (parseInt(hex.substr(1,1),16) * 16) + ",";
str += (parseInt(hex.substr(2,1),16) * 16) + ",";
str += (parseInt(hex.substr(3,1),16) * 16) + ",";
if(hex.length === 5){
str += (parseInt(hex.substr(3,1),16) / 16);
}else{
str += "1";
}
return str + ")";
}
if(hex.length === 7 || hex.length === 8){
str += parseInt(hex.substr(1,2),16) + ",";
str += parseInt(hex.substr(3,2),16) + ",";
str += parseInt(hex.substr(5,2),16) + ",";
if(hex.length === 5){
str += (parseInt(hex.substr(7,2),16) / 255).toFixed(3);
}else{
str += "1";
}
return str + ")";
}
return "rgba(0,0,0,0)";
}
},
createGradient : function(ctx, type, x, y, xx, yy, colours){
var i,g,c;
var len = colours.length;
if(type.toLowerCase() === "linear"){
g = ctx.createLinearGradient(x,y,xx,yy);
}else{
g = ctx.createRadialGradient(x,y,xx,x,y,yy);
}
for(i = 0; i < len; i++){
c = colours[i];
if(typeof c === "string"){
if(c[0] === " #"){
c = this.hex2RGBA(c);
}
g.addColorStop(Math.min(1,i / (len -1)),c); // need to clamp top to 1 due to floating point errors causes addColorStop to throw rangeError when number over 1
}
}
return g;
},
};
return tools;
})();
/** ImageTools.js end **/
/** SimpleFullCanvasMouse.js begin **/
const CANVAS_ELEMENT_ID = "canv";
const U = undefined;
var w, h, cw, ch; // short cut vars
var canvas, ctx, mouse;
var globalTime = 0;
var globalTimeInt = 0;
var createCanvas, resizeCanvas, setGlobals;
var L = typeof log === "function" ? log : function(d){ console.log(d); }
createCanvas = function () {
var c,cs;
cs = (c = document.createElement("canvas")).style;
c.id = CANVAS_ELEMENT_ID;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
var resized = false;
resizeCanvas = function () {
if (canvas === U) { canvas = createCanvas(); }
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
resized = true;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") { setGlobals(); }
}
setGlobals = function(){
cw = (w = canvas.width) / 2; ch = (h = canvas.height) / 2;
if(it !== undefined){
it = createIt(cw,ch,sprites);
}
}
mouse = (function(){
function preventDefault(e) { e.preventDefault(); }
var mouse = {
x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false, buttonRaw : 0,
over : false, // mouse is over the element
bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.x = e.offsetX; m.y = e.offsetY;
if (m.x === U) { m.x = e.clientX; m.y = e.clientY; }
m.alt = e.altKey; m.shift = e.shiftKey; m.ctrl = e.ctrlKey;
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1]; }
else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2]; }
else if (t === "mouseout") { m.buttonRaw = 0; m.over = false; }
else if (t === "mouseover") { m.over = true; }
else if (t === "mousewheel") { m.w = e.wheelDelta; }
else if (t === "DOMMouseScroll") { m.w = -e.detail; }
if (m.callbacks) { m.callbacks.forEach(c => c(e)); }
e.preventDefault();
}
m.addCallback = function (callback) {
if (typeof callback === "function") {
if (m.callbacks === U) { m.callbacks = [callback]; }
else { m.callbacks.push(callback); }
} else { throw new TypeError("mouse.addCallback argument must be a function"); }
}
m.start = function (element, blockContextMenu) {
if (m.element !== U) { m.removeMouse(); }
m.element = element === U ? document : element;
m.blockContextMenu = blockContextMenu === U ? false : blockContextMenu;
m.mouseEvents.forEach( n => { m.element.addEventListener(n, mouseMove); } );
if (m.blockContextMenu === true) { m.element.addEventListener("contextmenu", preventDefault, false); }
}
m.remove = function () {
if (m.element !== U) {
m.mouseEvents.forEach(n => { m.element.removeEventListener(n, mouseMove); } );
if (m.contextMenuBlocked === true) { m.element.removeEventListener("contextmenu", preventDefault);}
m.element = m.callbacks = m.contextMenuBlocked = U;
}
}
return mouse;
})();
var done = function(){
window.removeEventListener("resize",resizeCanvas)
mouse.remove();
document.body.removeChild(canvas);
canvas = ctx = mouse = U;
L("All done!")
}
resizeCanvas(); // create and size canvas
resized = false;
mouse.start(canvas,true); // start mouse on canvas and block context menu
window.addEventListener("resize",resizeCanvas); // add resize event
function drawText(text,x,y,size,col){
var f = size + "px Arial";
if(f !== ctx.font){
ctx.font = f;
}
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = col;
ctx.fillText(text,x,y);
}
function drawLoad(){
if(!resourcesReady || !canPlay){
drawText(message,cw,ch * 0.5, FONT_SIZE, MESSAGE_COL);
if (!canPlay && resourcesReady){
drawText("Try reloading the page.",cw,ch * 0.5 + FONT_SIZE + 8,Math.floor(FONT_SIZE /2) ,MESSAGE_COL);
}else{
drawText("Loading resources." ,cw,ch * 0.5 + FONT_SIZE + 8,Math.floor(FONT_SIZE /2) ,MESSAGE_COL);
}
}else{
if(message !== ""){
drawText(message,cw,ch * 0.5, FONT_SIZE, MESSAGE_COL);
}
}
}
const FONT = "px Arial"
const FONT_SIZE = Math.max(Math.floor(window.innerHeight/20),24)
ctx.textAlign = "center";
ctx.textBaseline = "middle";
function loaded(e){
if(e.type !== "error"){
this.sprites = [
{ x : 0, y : 0, w : 74, h : 116, },
{ x : 0, y : 126, w : 100, h : 113, },
{ x : 75, y : 0, w : 29, h : 42, },
{ x : 75, y : 43, w : 17, h : 22, },
{ x : 0, y : 249, w : 42, h : 18, },
{ x : 75, y : 66, w : 17, h : 15, },
{ x : 75, y : 82, w : 17, h : 12, },
{ x : 75, y : 95, w : 16, h : 9, },
{ x : 75, y : 105, w : 7, h : 7, },
{ x : 0, y : 268, w : 11, h : 5, },
]
resourcesReady = true;
canPlay = true;
it = createIt(cw,ch,this );
message = "";
return;
}
resourcesReady = true;
message = "LOAD FAILED!"
}
var it = null; // it is the character
var resourcesReady = false;
var canPlay = false;
var message = "Please Wait..."
const MESSAGE_COL = "white";
//var sprites = imageTools.loadImage("GreenIt.png",loaded )
var sprites = imageTools.loadImage("http://i.stack.imgur.com/ED6oC.png",loaded )
var background = imageTools.createImage(8,8);
background.ctx.fillStyle = imageTools.createGradient(ctx,"linear",0,0,8,8,["#0AF","#05A"]);
background.ctx.fillRect(0,0,8,8);
var ground = imageTools.createImage(8,32);
ground.ctx.fillStyle = imageTools.createGradient(ctx,"linear",0,0,8,32,["#0A0","#450","#754"]);
ground.ctx.fillRect(0,0,8,32);
ground.ctx.fillStyle = "black";
ground.ctx.fillRect(0,0,8,4);
const GROUND_OFFSET = 32;
const GRAV = 1;
var landed = false;
const MESSAGES = [
"Click mouse button to Jump",
"Click hold ... release to to add power to jump",
"Double click to double jump",
""
];
var messageCount = 0;
var fly = { // something to see
x : 0,
y : 0,
dx : 0,
dy : 0,
wait : 0,
onTheWall : false,
update : function(){
if(this.wait <= 0){
this.wait = Math.random() * 200+ 60;
this.onTheWall = Math.random() < 0.1 ? true : false;
if(this.onTheWall){
this.dx = 0;
this.dy = 0;
}else{
this.wait = Math.random() < 0.2 ? 10 : this.wait;
var x = (Math.random()-0.5) * 200;
var y = (Math.random()-0.5) * 200;
this.dx = (x - this.x) / this.wait;
this.dx = (y - this.y) / this.wait;
}
}else{
this.wait -= 1;
this.x += this.dx;
this.y += this.dy;
}
}
};
/*==============================================================================================
// Answer code
==============================================================================================*/
// info to define the character
const IT = {
body : 0, // sprite indexes
bodyFly : 1,
footDown : 2,
eyeOpen : 3,
foot : 4,
mouthOpen : 5,
eyeShut : 6,
mouthSmirk : 7,
eyeBall : 8,
mouth : 9, // sprite index end
grav : GRAV, // grav accel
maxJumpPower : 40,
minJump : 10,
jumpPower : 30, // mutiplys squat amount to give jump power
squatRate : 1, // how quick the squat is
squatResist : 0.8, // limits the amount of squat
landingBlinkTime : 30, // how long blink is on landing
blinkTime : 15, // how many frames to close eyes
blinkRate : 60 * 3, // 60 is one second . Time between blinks average
eyePos : {x : 0.13, y : -0.1}, // position as fraction of size
footPos : {x : 0.3, y : 0.5}, // position as fraction of size
lookAtGround : 1, // look ats
lookAtMouse : 2,
lookAtUser : 3,
lookAtFly : 4,
angle: 0,
jumpDy: 0, // the jump up speed used to rotate It when in air
}
// Function updates the character
const updateIt = function(){
if(this.blink > 0){
this.blink -= 1;
}
if(this.blinkTimer > 0){
this.blinkTimer -= 1;
if(this.blinkTimer === 0){
this.blink = IT.blinkTime;
}
}else{
// the two randoms create a random number that has a gausian distrabution centered on 0.5
// this creates a more realistic set of numbers.
this.blinkTimer = Math.floor(IT.blinkRate * (Math.random() + Math.random())/2 + IT.blinkRate / 2);
this.lookAt = Math.random() < 0.33 ? IT.lookAtUser : (Math.random() < 0.5 ? IT.lookAtMouse : IT.lookAtFly);
}
if(!this.onGround){
this.squat = 0;
//-------------------------------------
// do gravity
this.dy += IT.grav;
this.y += this.dy;
this.x += this.dx;
this.x = (this.x + ctx.canvas.width) % ctx.canvas.width;
var rotFraction = (this.jumpDy - this.dy) / this.jumpDy;
this.angle = this.jumpAngle * -rotFraction ;
if(this.dy > 13){
this.lookAt = IT.lookAtGround;
}
// check for the ground
if(this.y + this.tall / 2 > h - GROUND_OFFSET){
this.y = h - GROUND_OFFSET - this.tall / 2;
this.blink = Math.floor(IT.landingBlinkTime * (this.dy / 20));
this.blinkTimer = this.blink + 30;
this.squat = this.dy;
this.dy = 0;
this.onGround = true;
this.angle = -this.jumpAngle
}
}else{
this.squat *= IT.squatResist;
}
}
// draw the character
const drawIt = function(){
var bod = IT.body;
var spr = this.img.sprites;
var eye = this.blink > 0 ? IT.eyeShut : IT.eyeOpen;
var foot = IT.foot;
var footBehind = false; // draw feet behind or infront of body
if(!this.onGround){
if(this.dy >= 0){
if(this.dy > 2){
bod = IT.bodyFly;
}
}else{
footBehind = true;
foot = IT.footDown;
}
}
var xdx = Math.cos(this.angle);
var xdy = Math.sin(this.angle);
var px = this.x; // pivot
var py = this.y + 50;
var x = this.x ;
var y = this.y + this.squat;
var t = this.tall;
var f = this.fat;
if(footBehind){
if(!this.onGround){
var r = 1 - Math.min(1,-this.dy / 10);
imageTools.drawSpriteS(this.img,foot,x + f * IT.footPos.x,y - this.squat+ t * IT.footPos.y,1,-1,r,1);
imageTools.drawSprite(this.img,foot,x - f * IT.footPos.x,y - this.squat + t * IT.footPos.y,1,r,1);
}
}
ctx.setTransform(xdx,xdy,-xdy,xdx,px,py);
imageTools.drawSpriteLinked(this.img,bod,x - px,y - py,1,0,1);
if(!footBehind){
if(this.onGround){
imageTools.drawSpriteS(this.img,foot,x + f * IT.footPos.x,y - this.squat+ t * IT.footPos.y,1,-1,0,1);
imageTools.drawSprite(this.img,foot,x - f * IT.footPos.x,y - this.squat + t * IT.footPos.y,1,0,1);
}else{
var r = this.dy / 10;
imageTools.drawSpriteS(this.img,foot,x + f * IT.footPos.x,y - this.squat+ t * IT.footPos.y,1,-1,r,1);
imageTools.drawSprite(this.img,foot,x - f * IT.footPos.x,y - this.squat + t * IT.footPos.y,1,r,1);
}
}
if(this.blink){
ctx.setTransform(xdx,xdy,-xdy,xdx,px,py);
imageTools.drawSpriteLinked(this.img,eye,x + f * IT.eyePos.x - px, y + t * IT.eyePos.y - py,1,0,1);
imageTools.drawSpriteSLinked(this.img,eye,x - f * IT.eyePos.x - px, y + t * IT.eyePos.y - py,1,-1,0,1);
}else{
ctx.setTransform(xdx,xdy,-xdy,xdx,px,py);
imageTools.drawSpriteLinked(this.img,eye,x + f * IT.eyePos.x - px, y + t * IT.eyePos.y - py,1,0,1);
imageTools.drawSpriteSLinked(this.img,eye,x - f * IT.eyePos.x - px, y + t * IT.eyePos.y - py,1,-1,0,1);
var eyeDir = 0;
var eyeDist = 0;
if(this.blink === 0){
if(this.lookAt === IT.lookAtGround){
eyeDir = Math.PI/2;
eyeDist = 0.3;
}else if(this.lookAt === IT.lookAtUser){
eyeDir = 0;
eyeDist = 0;
}else if(this.lookAt === IT.lookAtFly){
eyeDir = Math.atan2(fly.y, fly.x);
eyeDist = (Math.hypot(fly.y ,fly.x) / 200) * 0.3;
}else{
eyeDir = Math.atan2(mouse.y - this.y, mouse.x - this.x);
eyeDist = (Math.hypot(this.y - mouse.y,this.x - mouse.x) / (Math.min(w,h)/2)) * 0.3;
}
eyeDist = Math.max(-0.3, Math.min(0.3, eyeDist));
var ex,ey;
ex = Math.cos(eyeDir) * spr[IT.eyeOpen].w * eyeDist;
ey = Math.sin(eyeDir) * spr[IT.eyeOpen].h * eyeDist;
imageTools.drawSpriteLinked(this.img, IT.eyeBall, x + f * IT.eyePos.x + ex - px, y + t * IT.eyePos.y + ey-py,1,0,1);
imageTools.drawSpriteLinked(this.img, IT.eyeBall, x - f * IT.eyePos.x + ex - px, y + t * IT.eyePos.y + ey-py,1,0,1);
}
}
}
// While mouse is down squat and prep to jump
const preJump = function(){
this.squat += IT.squatRate;
this.jumpPower += 0.5;
if(this.jumpPower > 30 && this.wiggle === 0) {
this.wiggle = 1;
}
this.jumpReady = true;
}
// when mouse released apply jump force
const jumpIt = function(){
var power = -IT.jumpPower * Math.min(IT.maxJumpPower,Math.max(IT.minJump,this.jumpPower))/IT.maxJumpPower;
this.dy = Math.sin(this.angle + Math.PI /2) * power;
this.dx = Math.cos(this.angle + Math.PI /2) * power;
if(this.onGround){
this.jumpDy = this.dy;
this.jumpAngle = this.angle;
}
this.wiggle = 0;
this.jumpPower = 0;
this.jumpReady = false;
this.squat = 0;
this.onGround = false;
}
// creates a character
var createIt = function(x,y,img){
return {
img : img,
x : x, // position
y : y,
dx : 0, // deltat speed
dy : 0,
sqaut : 0, // for landing and pre jump slight squat
onGround : false,
jumpPower : 0,
blink : 0, // blink controls
blinkTimer : 0,
lookAt : "ground", /// where to look
jumpReady : false, // flags if ready to jump
tall : img.sprites[IT.body].h, // how tall
fat : img.sprites[IT.body].w, // how wide
draw : drawIt, // functions
update : updateIt,
jump : jumpIt,
squatF : preJump,
}
}
function display(){ // put code in here
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.drawImage(background,0,0,w,h)
ctx.drawImage(ground,0,h-GROUND_OFFSET,w,GROUND_OFFSET);
fly.update()
drawLoad();
if(canPlay){
if(messageCount < MESSAGES.length){
if(it.onGround && !landed){
landed = true;
message = MESSAGES[messageCount];
messageCount += 1;
}
}
if(resized) { // to prevent resize display issue
resized = false;
it.y = h - GROUND_OFFSET - it.tall / 2;
}
if(it.onGround) {
it.angle = Math.atan2((it.y + 130)-10, it.x- mouse.x) / 3;
it.angle = it.angle < -1 ? -1 : it.angle > 1 ? 1 : it.angle;
it.angle = Math.pow(Math.abs(it.angle),0.5) * Math.sign(it.angle);
it.angle -= Math.PI / 4;
if(it.wiggle > 0.1) {
it.angle += Math.sin((it.wiggle * Math.PI) ** 2) * 0.01 * it.wiggle;
it.wiggle *= 0.95;
}
}
if(mouse.buttonRaw & 1){
it.squatF();
}else{
if(it.jumpReady){
it.jump();
landed = false;
}
}
it.update();
it.draw();
}
//ctx.clearRect(0,0,w,h);
}
/*==============================================================================================
// Answer End
==============================================================================================*/
function update(timer){ // Main update loop
globalTimeInt = Math.floor(globalTime = timer);
display(); // call demo code
requestAnimationFrame(update);
}
requestAnimationFrame(update);
/** SimpleFullCanvasMouse.js end **/
I recommend you check this (part 1) and this(part 2) tutorial that I have followed.
Your "jump" animation just boils down to creating a jump function that sets a max jump height to your object and sets a boolean to var jumping = true. As long as your character is "jumping" you increment the y position of your character.
Once you get to your desired height, create a land function that does the opposite.
Make a Javascript setinterval to update the height of object after every 20ms.
after every 20s set height = Initial_Height + u*t - (1/2)gt^2
use g = 9.8, u = some constant according to your screen.
t = Time passed till now. Which mean, Initially t=0, on first update t=20ms, on second update t=40ms.
Basically, you are simulating real life jumping in gravity.

Categories