hyperdrive effect in canvas across randomly placed circles - javascript

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;
}

Related

Trying to modify a path walking javascript

I found a code im trying to modify, what I can't understand is how to modify amount of steps to take and properly control the direction? Is there some trick or easier way to control the direction.. I kinda only need like 10 steps in one line. I've also tried working with the cordinates but it doesn't give me much. I can change the path but it still goes in ways I don't understand.
Here is the code I have atm.
// create a canvas and add it to the DOM
var createImage = function(w, h) {
var i = document.createElement("canvas");
i.width = w;
i.height = h;
i.ctx = i.getContext("2d");
return i;
}
var canvas = createImage(941, 600);
var ctx = canvas.ctx;
document.getElementsByClassName('bb-item')[0].appendChild(canvas);
// shapes for the foot. Left foot
const footScale = 0.05; // how big the foot is
const stepLen = 20; // distance between steps
var stepCount = 0; // current set num so left and right can be known
var stepTime = 100; // time ms between steps
// path to walk
const path = [
15.20191403126277, 62.4451015086347, 15.38688266344644,
15.80127602121507, 15.98280549274708, 192.86637096042372,
121.57528916149568, 221.34586055208607, 124.81159479691195,
256.2979614145819, 141.64038410107662, 293.8391067854107,
168.82535143857336, 311.9624183437419, 203.77745230106916,
336.5583411729056, 238.0822920364817, 344.9727358249879,
278.2124819156436, 341.0891690624884, 316.40088841355566,
329.43846877498976, 343.58585575105235, 310.6678960895754,
370.77082308854904, 275.7157952270795, 359.12012280105046,
244.64726112708325, 344.23311687813566, 207.10611575625444,
355.23655603855093, 168.9177092583423, 394.0722236635463,
37.2019140312628, 438.0859803052077, 137.84917515834604,
487.27782596353507, 174.0957982750084, 507.9901820301992,
21.9931216791693, 513.1682710468652, 269.243183956247,
500.87030963228347, 318.43502961457443, 480.1579535656192,
54.68165273123674, 453.62744426338134, 396.86543776550707,
414.1445200788371, 427.9340428271046, 372.7198079555102,
47.3518767949864, 320.2916566617712, 442.173787778395,
272.39433325761024, 427.9340429825634, 218.02439858261675,
41.5265266513118, 185.66134222845398, 472.59506075130815,
160.418158272207, 514.6670340117198, 168.2291881671332,
57.5405924870362, 200.59229872785795, 598.9654951914354,
232.9553551615553, 627.4449850627141, 273.08554504131894,
651.3936467669101, 320.3356073183967, 663.0443470544095,
368.2329307225576, 663.6916081814927, 417.4247763808851,
649.4518633856611, 460.7912718954633, 626.150462810664,
509.33585642670744, 593.1401453294179, 530.6954736204549,
556.8935222127556, 559.9273870166451, 517.9197870310509,
582.4287517306153
];
const pLen = path.length;
// fix the path so it starts at zero
for (var i = 2; i < pLen; i += 2) {
path[i] -= path[0];
path[i + 1] -= path[1];
}
path[0] = path[1] = 0;
// tracks the foot pos
var pos = {
x: path[0],
y: path[1],
index: 0,
};
// tracks the foot pointing to pos
var pos1 = {
x: path[0],
y: path[1],
index: 0,
};
// get a distance alone the path
function getDistOnPath(dist, pos) {
var nx = path[(pos.index + 2) % pLen] - pos.x;
var ny = path[(pos.index + 3) % pLen] - pos.y;
var d = Math.hypot(nx, ny);
if (d > dist) {
pos.x += (nx / d) * dist;
pos.y += (ny / d) * dist;
return pos;
}
dist -= d;
pos.index += 2;
pos.x = path[pos.index % pLen];
pos.y = path[(pos.index + 1) % pLen];
return getDistOnPath(dist, pos);
}
function drawFoot(x, y, dir, left) {
var i, j, shape;
var xdx = Math.cos(dir) * footScale;
var xdy = Math.sin(dir) * footScale;
if (left) {
ctx.setTransform(xdx, xdy, -xdy, xdx, x + xdy * 50, y - xdx * 50);
ctx.rotate(-0.1); // make the foot turn out a bit
} else {
}
// draw the foot as a set of paths points
var footImage = new Image();
footImage.src = './images/paw_print.svg';
ctx.drawImage(footImage, -footImage.width / 2, -footImage.height / 2);
}
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
pos1 = getDistOnPath(stepLen / 10, pos1);
function drawStep() {
pos = getDistOnPath(stepLen, pos);
pos1 = getDistOnPath(stepLen, pos1);
drawFoot(pos.x, pos.y, Math.atan2(pos1.y - pos.y, pos1.x - pos.x),
(stepCount++) % 2 === 0);
setTimeout(drawStep, stepTime);
}
drawStep();

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>

Javascript - interactive particle logo not working

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

scrambling images on canvas producing whitespace

I'm starting with a canvas element. I'm making the left half red, and the right side blue. Every half second, setInterval calls a function, scramble, which splits both RHS and LHS into pieces, and shuffles them.
Here is a fiddle: https://jsfiddle.net/aeq1g3yb/
The code is below. The reason I'm using window.onload is because this thing is supposed to scramble pictures and I want the pictures to load first. I'm using colors here because of the cross-origin business that I don't know enough about, so this is my accommodation.
var n = 1;
var v = 1;
function scramble() {
//get the canvas and change its width
var c = document.getElementById("myCanvas");
c.width = 600;
var ctx = c.getContext("2d");
//drawing 2 different colors side by side
ctx.fillStyle = "red";
ctx.fillRect(0, 0, c.width/2, c.height);
ctx.fillStyle = "blue";
ctx.fillRect(c.width/2, 0, c.width/2, c.height);
//how big will each shuffled chunk be
var stepsA = (c.width/2) / n;
var stepsB = (c.width/2) / n;
var step = stepsA + stepsB;
var imgDataA = [];
var imgDataB = [];
for (var i = 0; i < n; i++) {
var imgDataElementA = ctx.getImageData(stepsA*i, 0, stepsA, c.height);
var imgDataElementB = ctx.getImageData(c.width/2+stepsB*i, 0, stepsB, c.height);
imgDataA.push(imgDataElementA);
imgDataB.push(imgDataElementB);
}
//clearing out the canvas before laying on the new stuff
ctx.fillStyle = "white";
ctx.fillRect(0, 0, c.width, c.height);
//put the images back
for (var i = 0; i < n; i++) {
ctx.putImageData(imgDataA[i], step*i, 0);
ctx.putImageData(imgDataB[i], step*i+stepsA, 0);
}
//gonna count the steps
var count = document.getElementById("count");
count.innerHTML = n;
n += v;
if (n >= 100 || n <= 1) {
v *= -1;
}
}; //closing function scramble
window.onload = function() { //gotta do this bc code executes before image loads
scramble();
};
window.setInterval(scramble, 500);
More or less, this thing works the way I want it to. But there is one problem: Sometimes there are vertical white lines.
My question is:
Why are there white lines? If you view the fiddle, you will see the degree to which this impairs the effect of the shuffle.
You can`t divide a Pixel
The problem can be solve but will introduce some other artifacts as you can not divide integer pixels into fractions.
Quick solution
The following solution for your existing code rounds down for the start of a section and up for the width.
for (var i = 0; i < n; i++) {
var imgDataElementA = ctx.getImageData(
Math.floor(stepsA * i), 0,
Math.ceil(stepsA + stepsA * i) - Math.floor(stepsA * i), c.height
);
var imgDataElementB = ctx.getImageData(
Math.floor(c.width / 2 + stepsB * i), 0,
Math.ceil(c.width / 2 + stepsB * i + stepsB) - Math.floor(c.width / 2 + stepsB * i), c.height);
imgDataA.push(imgDataElementA);
imgDataB.push(imgDataElementB);
}
Quicker options
But doing this via the pixel image data is about the slowest possible way you could find to do it. You can just use the 2D context.imageDraw function to do the movement for you. Or if you want the best in terms of performance a WebGL solution would be the best with the fragment shader doing the scrambling for you as a parallel solution.
There is no perfect solution
But in the end you can not cut a pixel in half, there are a wide range of ways to attempt to solve this but each method has its own artifacts. Ideally you should only slice an image if the rule image.width % slices === 0 in all other cases you will have one or more slices that will not fit on an integer number of pixels.
Example of 4 rounding methods.
The demo shows 4 different methods and with 2 colors. Mouse over to see a closer view. Each method is separated horizontally with a white line. Hold the mouse button to increase the slice counter.
The top is your original.
The next three are 3 different ways of dealing with the fractional pixel width.
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
const m = mouse;
if(m.element){
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left - scrollX;
m.y = e.pageY - m.bounds.top - scrollY;
m.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : m.button;
}
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
const counterElement = document.getElementById("count");
// get constants for the demo
const c = document.getElementById("myCanvas");
mouse.element = c;
// The image with the blue and red
const img = document.createElement("canvas");
// the zoom image overlay
const zoom = document.createElement("canvas");
// the scrambled image
const scram = document.createElement("canvas");
// Set sizes and get context
const w = scram.width = zoom.width = img.width = c.width = 500;
const h = scram.height = zoom.height = img.height = c.height;
const dCtx = c.getContext("2d"); // display context
const iCtx = img.getContext("2d"); // source image context
const zCtx = zoom.getContext("2d"); // zoom context
const sCtx = scram.getContext("2d"); // scrambled context
// some constants
const zoomAmount = 4;
const zoomRadius = 60;
const framesToStep = 10;
function createTestPattern(ctx){
ctx.fillStyle = "red";
ctx.fillRect(0, 0, c.width/2, c.height/2);
ctx.fillStyle = "blue";
ctx.fillRect(c.width/2, 0, c.width/2, c.height/2);
ctx.fillStyle = "black";
ctx.fillRect(0, c.height/2, c.width/2, c.height/2);
ctx.fillStyle = "#CCC";
ctx.fillRect(c.width/2, c.height/2, c.width/2, c.height/2);
}
createTestPattern(iCtx);
sCtx.drawImage(iCtx.canvas, 0, 0);
// Shows a zoom area so that blind men like me can see what is going on.
function showMouseZoom(src,dest,zoom = zoomAmount,radius = zoomRadius){
dest.clearRect(0,0,w,h);
dest.imageSmoothingEnabled = false;
if(mouse.x >= 0 && mouse.y >= 0 && mouse.x < w && mouse.y < h){
dest.setTransform(zoom,0,0,zoom,mouse.x,mouse.y)
dest.drawImage(src.canvas, -mouse.x, -mouse.y);
dest.setTransform(1,0,0,1,0,0);
dest.globalCompositeOperation = "destination-in";
dest.beginPath();
dest.arc(mouse.x,mouse.y,radius,0,Math.PI * 2);
dest.fill();
dest.globalCompositeOperation = "source-over";
dest.lineWidth = 4;
dest.strokeStyle = "black";
dest.stroke();
}
}
function scramble(src,dest,y,height) {
const w = src.canvas.width;
const h = src.canvas.height;
const steps = (w/2) / slices;
dest.fillStyle = "white";
dest.fillRect(0, y, w, height);
for (var i = 0; i < slices * 2; i++) {
dest.drawImage(src.canvas,
((i / 2) | 0) * steps + (i % 2) * (w / 2)- 0.5, y,
steps + 1, height,
i * steps - 0.5, y,
steps+ 1, height
);
}
}
function scrambleFloor(src,dest,y,height) {
const w = src.canvas.width;
const h = src.canvas.height;
const steps = (w/2) / slices;
dest.fillStyle = "white";
dest.fillRect(0, y, w, height);
for (var i = 0; i < slices * 2; i++) {
dest.drawImage(src.canvas,
(((i / 2) | 0) * steps + (i % 2) * (w / 2)- 0.5) | 0, y,
steps + 1, height,
(i * steps - 0.5) | 0, y,
steps + 1, height
);
}
}
function scrambleNoOverlap(src,dest,y,height) {
const w = src.canvas.width;
const h = src.canvas.height;
const steps = (w / 2) / slices;
dest.fillStyle = "white";
dest.fillRect(0, y, w, height);
for (var i = 0; i < slices * 2; i++) {
dest.drawImage(src.canvas,
((i / 2) | 0) * steps + (i % 2) * (w / 2), y,
steps, height,
i * steps - 0.5, y,
steps, height
);
}
}
function scrambleOriginal(src,dest,y,height) {
const w = src.canvas.width;
const h = src.canvas.height;
//how big will each shuffled chunk be
var stepsA = (w/2) / slices;
var stepsB = (w/2) / slices;
var step = stepsA + stepsB;
var imgDataA = [];
var imgDataB = [];
for (var i = 0; i < slices; i++) {
var imgDataElementA = src.getImageData(stepsA*i, y, stepsA, height);
var imgDataElementB = src.getImageData(w/2+stepsB*i, y, stepsB, height);
imgDataA.push(imgDataElementA);
imgDataB.push(imgDataElementB);
}
//clearing out the canvas before laying on the new stuff
dest.fillStyle = "white";
dest.fillRect(0, y, w, height);
//put the images back
for (var i = 0; i < slices; i++) {
dest.putImageData(imgDataA[i], step*i, y);
dest.putImageData(imgDataB[i], step*i+stepsA, y);
}
}; //closing function scramble
const scrambleMethods = [scrambleOriginal,scramble,scrambleFloor,scrambleNoOverlap];
var frameCount = 0;
var sliceStep = 1;
var slices = 1;
function mainLoop(){
if(mouse.button){
if(frameCount++ % framesToStep === framesToStep-1){ // every 30 Frames
slices += sliceStep;
if(slices > 150 || slices < 2){ sliceStep = -sliceStep }
counterElement.textContent = slices; // Prevent reflow by using textContent
sCtx.clearRect(0,0,w,h);
sCtx.imageSmoothingEnabled = true;
const len = scrambleMethods.length;
for(var i = 0; i < len; i ++){
scrambleMethods[i](iCtx,sCtx,(128/len) * i, 128/len-2);
scrambleMethods[i](iCtx,sCtx,(128/len) * i + 128, 128/len-2);
}
}
}
dCtx.fillStyle = "white";
dCtx.fillRect(0,0,w,h);
dCtx.drawImage(sCtx.canvas,0,0);
showMouseZoom(dCtx,zCtx);
dCtx.drawImage(zCtx.canvas,0,0);
requestAnimationFrame(mainLoop);
}
//scramble(iCtx,sCtx);
requestAnimationFrame(mainLoop);
canvas {
border: 1px solid black;
}
#count {
position : absolute;
top : 0px;
left : 10px;
font-family: monospace;
font-size: 20px;
}
<canvas id="myCanvas" height = "256" title="Hold mouse button to chance slice count"></canvas>
<p id="count"></p>

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