Related
I have the vanilla JS code below to fade in each item of an array of HTML elements on scroll, but I would like to refactor my code to have the option to fade an element in only (specifically the '.navmenu' element), but not fade the element out again when the user scrolls back up:
const elementsArr = [
{
elementSelec: '.navmenu',
fadeInInit: 100,
fadeOutInit: document.documentElement.scrollHeight,
initOpacity: 0,
},
{
elementSelec: '#init-element',
fadeInInit: 100,
fadeOutInit: 400,
initOpacity: 0.2,
},
{
elementSelec: '#next-element',
fadeInInit: 500,
fadeOutInit: 900,
initOpacity: 0.2,
},
{
elementSelec: '#last-element',
fadeInInit: 1100,
fadeOutInit: 1700,
initOpacity: 0.2,
},
];
window.addEventListener('scroll', () =>
fadeOnScroll(elementsArr, { fadeLen: 200 })
);
function fadeOnScroll(elementsArr, { fadeLen }) {
const posY = window.scrollY;
for (const element of elementsArr) {
let opacity;
const { elementSelec, fadeInInit, fadeOutInit, initOpacity } = element;
const fadeInEnd = fadeInInit + fadeLen;
const fadeOutEnd = fadeOutInit + fadeLen;
if (fadeInInit < posY && posY < fadeInEnd) {
opacity = (1 - initOpacity) * ((posY - fadeInInit) / fadeLen) + initOpacity;
} else if (fadeInEnd <= posY && posY <= fadeOutInit) {
opacity = 1;
} else if (fadeOutInit < posY && posY < fadeOutEnd) {
opacity = 1 - (1 - initOpacity) * ((posY - fadeOutInit) / fadeLen);
} else {
opacity = initOpacity;
}
document.querySelector(elementSelec).style.opacity = opacity;
}
}
How do you recommend approaching this problem? Thanks.
I'm making a game in Phaser which looks like that:
player has to catch the eggs, so the eggs (which are made from gameState.eggs = this.physics.add.group();) have a certain velocity while on the ramp, but then once they're off the ramp, i want to authomatically setVelocity() to one with 0 for x coordinate, instead of just shooting across the screen.
Here's my egg generating function:
function eggGen() {
let num = Math.random();
let xCoord, yCoord, eggDirection, eggAnimation, velocityX
if (num < .5) {
xCoord = 100;
eggDirection = 'eggLeft';
eggAnimation = 'rollingLeft'
velocityX = this.velocityX;
if (num < .25) {
yCoord = 232;
} else {
yCoord = 382;
}
} else {
xCoord = 700;
eggDirection = 'eggRight';
eggAnimation = 'rollingRight';
velocityX = -(this.velocityX)
if (num < .75) {
yCoord = 232;
} else {
yCoord = 382;
}
}
let egg = gameState.eggs.create(xCoord, yCoord, eggDirection).setVelocity(velocityX, this.velocityY).setScale(.6);
if (egg.x > 220 && egg.x < 580) {
egg.setVelocity(0, this.velocityY);
}
egg.anims.play(eggAnimation);
}
the last conditional is what i hoped would do the magic, but it doesn't do anything. To clarify, eggGen function is called inside this.time.addEvent();
Without knowing your code (and assuming arcade physics is used), I would:
Just check in the update function, of the scene, if a egg is "on the ramp" and has a x-velocity of 0
function update(){
// ...
gameState.eggs.getChildren().forEach(egg => {
if(egg.velocity.x > 0 && (egg.x > 220 || egg.x < 580)) {
// ... stop velocity.x or set the whole velocity new
egg.velocity.x = 0;
}
});
// ...
}
Here a mini Demo:
It just covers the basics
document.body.style = 'margin:0;';
var config = {
type: Phaser.AUTO,
width: 300,
height: 183,
physics: {
default: 'arcade',
arcade: {
debug: true,
}
},
scene: {
create,
update
},
banner: false
};
let objectGroup;
function create () {
objectGroup = this.physics.add.group();
this.time.addEvent({ delay: 500, callback: createObject, callbackScope: this, loop: true });
}
function createObject(){
let spawnLeft = Phaser.Math.Between(0, 1);
let obj = this.add.rectangle(spawnLeft ? 0 : config.width, 0, 10, 10, 0xff0000);
this.physics.add.existing(obj);
objectGroup.add(obj);
obj.body.setVelocity((spawnLeft ? 1 : -1) * 75, 30);
}
function update(){
if(!objectGroup)
return;
objectGroup.getChildren().forEach(obj =>{
if(obj.body.velocity.x > 0 && (obj.x > 100 && obj.x < 150) || (obj.x > config.width - 150 && obj.x < config.width - 100) ){
// Just to keep the same speed, even after changing direction
let speed = obj.body.velocity.length();
obj.body.velocity.x = 0;
obj.body.velocity.y = speed;
}
});
}
new Phaser.Game(config);
<script src="https://cdn.jsdelivr.net/npm/phaser#3.55.2/dist/phaser.js"></script>
I have an object rendered to a canvas. I'm trying to get the object to move along a set path on a loop. Here is what I have:
// Canvas Element
var canvas = null;
// Canvas Draw
var ctx = null;
// Static Globals
var tileSize = 16,
mapW = 10,
mapH = 10;
// Instances of entities
var entities = [
// A single entity that starts at tile 28, and uses the setPath() function
{
id: 0,
tile: 28,
xy: tileToCoords(28),
width: 16,
height: 24,
speedX: 0,
speedY: 0,
logic: {
func: 'setPath',
// These are the parameters that go into the setPath() function
data: [0, ['down', 'up', 'left', 'right'], tileToCoords(28), 0]
},
dir: {up:false, down:false, left:false, right:false}
}
];
// Array for tile data
var map = [];
window.onload = function(){
// Populate the map array with a blank map and 4 walls
testMap();
canvas = document.getElementById('save');
ctx = canvas.getContext("2d");
// Add all the entities to the map array and start their behavior
for(var i = 0; i < entities.length; ++i){
map[entities[i].tile].render.object = entities[i].id;
if(entities[i].logic){
window[entities[i].logic.func].apply(null, entities[i].logic.data);
}
}
drawGame(map);
window.requestAnimationFrame(function(){
mainLoop();
});
};
function drawGame(map){
ctx.clearRect(0, 0, canvas.width, canvas.height);
// We save all the entity data for later so the background colors don't get rendered on top
var tileObjData = [];
for(var y = 0; y < mapH; ++y){
for(var x = 0; x < mapW; ++x){
var currentPos = ((y*mapW)+x);
ctx.fillStyle = map[currentPos].render.base;
ctx.fillRect(x*tileSize, y*tileSize, tileSize, tileSize);
var thisObj = map[currentPos].render.object;
if(thisObj !== false){
thisObj = entities[thisObj];
var originX = thisObj.xy.x;
var originY = thisObj.xy.y;
tileObjData.push(
{
id: thisObj.id,
originX: originX,
originY: originY,
width: thisObj.width,
height: thisObj.height,
}
);
}
}
}
// Draw all the entities after the background tiles are drawn
for(var i = 0; i < tileObjData.length; ++i){
drawEntity(tileObjData[i].id, tileObjData[i].originX, tileObjData[i].originY, tileObjData[i].width, tileObjData[i].height);
}
}
// Draws the entity data
function drawEntity(id, posX, posY, sizeX, sizeY){
var offX = posX + entities[id].speedX;
var offY = posY + entities[id].speedY;
ctx.fillStyle = '#00F';
ctx.fillRect(offX, offY + sizeX - sizeY, sizeX, sizeY);
entities[id].xy.x = offX;
entities[id].xy.y = offY;
}
// Redraws the canvas with the browser framerate
function mainLoop(){
drawGame(map);
for(var i = 0; i < entities.length; ++i){
animateMove(i, entities[i].dir.up, entities[i].dir.down, entities[i].dir.left, entities[i].dir.right);
}
window.requestAnimationFrame(function(){
mainLoop();
});
}
// Sets the speed, direction, and collision detection of an entity
function animateMove(id, up, down, left, right){
var prevTile = entities[id].tile;
if(up){
var topLeft = {x: entities[id].xy.x, y: entities[id].xy.y};
var topRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y};
if(!map[coordsToTile(topLeft.x, topLeft.y - 1)].state.passable || !map[coordsToTile(topRight.x, topRight.y - 1)].state.passable){
entities[id].speedY = 0;
}
else{
entities[id].speedY = -1;
}
}
else if(down){
var bottomLeft = {x: entities[id].xy.x, y: entities[id].xy.y + entities[id].width - 1};
var bottomRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y + entities[id].width - 1};
if(!map[coordsToTile(bottomLeft.x, bottomLeft.y + 1)].state.passable || !map[coordsToTile(bottomRight.x, bottomRight.y + 1)].state.passable){
entities[id].speedY = 0;
}
else{
entities[id].speedY = 1;
}
}
else{
entities[id].speedY = 0;
}
if(left){
var bottomLeft = {x: entities[id].xy.x, y: entities[id].xy.y + entities[id].width - 1};
var topLeft = {x: entities[id].xy.x, y: entities[id].xy.y};
if(!map[coordsToTile(bottomLeft.x - 1, bottomLeft.y)].state.passable || !map[coordsToTile(topLeft.x - 1, topLeft.y)].state.passable){
entities[id].speedX = 0;
}
else{
entities[id].speedX = -1;
}
}
else if(right){
var bottomRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y + entities[id].width - 1};
var topRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y};
if(!map[coordsToTile(bottomRight.x + 1, bottomRight.y)].state.passable || !map[coordsToTile(topRight.x + 1, topRight.y)].state.passable){
entities[id].speedX = 0;
}
else{
entities[id].speedX = 1;
}
}
else{
entities[id].speedX = 0;
}
entities[id].tile = coordsToTile(entities[id].xy.x + (entities[id].width / 2), entities[id].xy.y + (tileSize / 2));
map[entities[id].tile].render.object = id;
if(prevTile !== entities[id].tile){
map[prevTile].render.object = false;
}
}
//////////////////////////////////////
// THIS IS WHERE I'M HAVING TROUBLE //
//////////////////////////////////////
// A function that can be used by an entity to move along a set path
// id = The id of the entity using this function
// path = An array of strings that determine the direction of movement for a single tile
// originPoint = Coordinates of the previous tile this entity was at. This variable seems to be where problems happen with this logic. It should get reset for every tile length moved, but it only gets reset once currently.
// step = The current index of the path array
function setPath(id, path, originPoint, step){
// Determine if the entity has travelled one tile from the origin
var destX = Math.abs(entities[id].xy.x - originPoint.x);
var destY = Math.abs(entities[id].xy.y - originPoint.y);
if(destX >= tileSize || destY >= tileSize){
// Go to the next step in the path array
step = step + 1;
if(step >= path.length){
step = 0;
}
// Reset the origin to the current tile coordinates
originPoint = entities[id].xy;
}
// Set the direction based on the current index of the path array
switch(path[step]) {
case 'up':
entities[id].dir.up = true;
entities[id].dir.down = false;
entities[id].dir.left = false;
entities[id].dir.right = false;
break;
case 'down':
entities[id].dir.up = false;
entities[id].dir.down = true;
entities[id].dir.left = false;
entities[id].dir.right = false;
break;
case 'left':
entities[id].dir.up = false;
entities[id].dir.down = false;
entities[id].dir.left = true;
entities[id].dir.right = false;
break;
case 'right':
entities[id].dir.up = false;
entities[id].dir.down = false;
entities[id].dir.left = false;
entities[id].dir.right = true;
break;
};
window.requestAnimationFrame(function(){
setPath(id, path, originPoint, step);
});
}
// Take a tile index and return x,y coordinates
function tileToCoords(tile){
var yIndex = Math.floor(tile / mapW);
var xIndex = tile - (yIndex * mapW);
var y = yIndex * tileSize;
var x = xIndex * tileSize;
return {x:x, y:y};
}
// Take x,y coordinates and return a tile index
function coordsToTile(x, y){
var tile = ((Math.floor(y / tileSize)) * mapW) + (Math.floor(x / tileSize));
return tile;
}
// Generate a map array with a blank map and 4 walls
function testMap(){
for(var i = 0; i < (mapH * mapW); ++i){
// Edges
if (
// top
i < mapW ||
// left
(i % mapW) == 0 ||
// right
((i + 1) % mapW) == 0 ||
// bottom
i > ((mapW * mapH) - mapW)
) {
map.push(
{
id: i,
render: {
base: '#D35',
object: false,
sprite: false
},
state: {
passable: false
}
},
);
}
else{
// Grass
map.push(
{
id: i,
render: {
base: '#0C3',
object: false,
sprite: false
},
state: {
passable: true
}
},
);
}
}
}
<!DOCTYPE html>
<html>
<head>
<style>
body{
background-color: #000;
display: flex;
align-items: center;
justify-content: center;
color: #FFF;
font-size: 18px;
padding: 0;
margin: 0;
}
main{
width: 100%;
max-width: 800px;
margin: 10px auto;
display: flex;
align-items: flex-start;
justify-content: center;
flex-wrap: wrap;
}
.game{
width: 1000px;
height: 1000px;
position: relative;
}
canvas{
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.game canvas{
position: absolute;
top: 0;
left: 0;
width: 800px;
height: 800px;
}
</style>
</head>
<body>
<main>
<div class="game">
<canvas id="save" width="200" height="200" style="z-index: 1;"></canvas>
</div>
</main>
</body>
</html>
The problem is with the setPath() function, and more specifically I think it's something with the originPoint variable. The idea is that setPath() moves the object one tile per path string, and originPoint should be the coordinates of the last tile visited (so it should only get updated once the object coordinates are one tile length away from the originPoint). Right now it only gets updated the first time and then stops. Hopefully someone can point out what I got wrong here.
Your condition to change the path direction I change it to have conditions for each direction, something like:
if ((entities[id].dir.left && entities[id].xy.x <= tileSize) ||
(entities[id].dir.right && entities[id].xy.x >= tileSize*8) ||
(entities[id].dir.up && entities[id].xy.y <= tileSize) ||
(entities[id].dir.down && entities[id].xy.y >= tileSize*8)) {
and the originPoint was just a reference you should do:
originPoint = JSON.parse(JSON.stringify(entities[id].xy));
See the working code below
// Canvas Element
var canvas = null;
// Canvas Draw
var ctx = null;
// Static Globals
var tileSize = 16,
mapW = 10,
mapH = 10;
// Instances of entities
var entities = [
// A single entity that starts at tile 28, and uses the setPath() function
{
id: 0,
tile: 28,
xy: tileToCoords(28),
width: 16,
height: 24,
speedX: 0,
speedY: 0,
logic: {
func: 'setPath',
// These are the parameters that go into the setPath() function
data: [0, ['down', 'left', 'down', 'left', 'up', 'left', 'left', 'right', 'up', 'right', 'down','right', "up"], tileToCoords(28), 0]
},
dir: {up:false, down:false, left:false, right:false}
}
];
// Array for tile data
var map = [];
window.onload = function(){
// Populate the map array with a blank map and 4 walls
testMap();
canvas = document.getElementById('save');
ctx = canvas.getContext("2d");
// Add all the entities to the map array and start their behavior
for(var i = 0; i < entities.length; ++i){
map[entities[i].tile].render.object = entities[i].id;
if(entities[i].logic){
window[entities[i].logic.func].apply(null, entities[i].logic.data);
}
}
drawGame(map);
window.requestAnimationFrame(function(){
mainLoop();
});
};
function drawGame(map){
ctx.clearRect(0, 0, canvas.width, canvas.height);
// We save all the entity data for later so the background colors don't get rendered on top
var tileObjData = [];
for(var y = 0; y < mapH; ++y){
for(var x = 0; x < mapW; ++x){
var currentPos = ((y*mapW)+x);
ctx.fillStyle = map[currentPos].render.base;
ctx.fillRect(x*tileSize, y*tileSize, tileSize, tileSize);
var thisObj = map[currentPos].render.object;
if(thisObj !== false){
thisObj = entities[thisObj];
var originX = thisObj.xy.x;
var originY = thisObj.xy.y;
tileObjData.push(
{
id: thisObj.id,
originX: originX,
originY: originY,
width: thisObj.width,
height: thisObj.height,
}
);
}
}
}
// Draw all the entities after the background tiles are drawn
for(var i = 0; i < tileObjData.length; ++i){
drawEntity(tileObjData[i].id, tileObjData[i].originX, tileObjData[i].originY, tileObjData[i].width, tileObjData[i].height);
}
}
// Draws the entity data
function drawEntity(id, posX, posY, sizeX, sizeY){
var offX = posX + entities[id].speedX;
var offY = posY + entities[id].speedY;
ctx.fillStyle = '#00F';
ctx.fillRect(offX, offY + sizeX - sizeY, sizeX, sizeY);
entities[id].xy.x = offX;
entities[id].xy.y = offY;
}
// Redraws the canvas with the browser framerate
function mainLoop(){
drawGame(map);
for(var i = 0; i < entities.length; ++i){
animateMove(i, entities[i].dir.up, entities[i].dir.down, entities[i].dir.left, entities[i].dir.right);
}
window.requestAnimationFrame(function(){
mainLoop();
});
}
// Sets the speed, direction, and collision detection of an entity
function animateMove(id, up, down, left, right){
var prevTile = entities[id].tile;
if(up){
var topLeft = {x: entities[id].xy.x, y: entities[id].xy.y};
var topRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y};
if(!map[coordsToTile(topLeft.x, topLeft.y - 1)].state.passable || !map[coordsToTile(topRight.x, topRight.y - 1)].state.passable){
entities[id].speedY = 0;
}
else{
entities[id].speedY = -1;
}
}
else if(down){
var bottomLeft = {x: entities[id].xy.x, y: entities[id].xy.y + entities[id].width - 1};
var bottomRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y + entities[id].width - 1};
if(!map[coordsToTile(bottomLeft.x, bottomLeft.y + 1)].state.passable || !map[coordsToTile(bottomRight.x, bottomRight.y + 1)].state.passable){
entities[id].speedY = 0;
}
else{
entities[id].speedY = 1;
}
}
else{
entities[id].speedY = 0;
}
if(left){
var bottomLeft = {x: entities[id].xy.x, y: entities[id].xy.y + entities[id].width - 1};
var topLeft = {x: entities[id].xy.x, y: entities[id].xy.y};
if(!map[coordsToTile(bottomLeft.x - 1, bottomLeft.y)].state.passable || !map[coordsToTile(topLeft.x - 1, topLeft.y)].state.passable){
entities[id].speedX = 0;
}
else{
entities[id].speedX = -1;
}
}
else if(right){
var bottomRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y + entities[id].width - 1};
var topRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y};
if(!map[coordsToTile(bottomRight.x + 1, bottomRight.y)].state.passable || !map[coordsToTile(topRight.x + 1, topRight.y)].state.passable){
entities[id].speedX = 0;
}
else{
entities[id].speedX = 1;
}
}
else{
entities[id].speedX = 0;
}
entities[id].tile = coordsToTile(entities[id].xy.x + (entities[id].width / 2), entities[id].xy.y + (tileSize / 2));
map[entities[id].tile].render.object = id;
if(prevTile !== entities[id].tile){
map[prevTile].render.object = false;
}
}
//////////////////////////////////////
// THIS IS WHERE I'M HAVING TROUBLE //
//////////////////////////////////////
// A function that can be used by an entity to move along a set path
// id = The id of the entity using this function
// path = An array of strings that determine the direction of movement for a single tile
// originPoint = Coordinates of the previous tile this entity was at. This variable seems to be where problems happen with this logic. It should get reset for every tile length moved, but it only gets reset once currently.
// step = The current index of the path array
function setPath(id, path, originPoint, step){
if ((entities[id].dir.left && entities[id].xy.x <= originPoint.x - tileSize) ||
(entities[id].dir.right && entities[id].xy.x >= originPoint.x + tileSize) ||
(entities[id].dir.up && entities[id].xy.y <= originPoint.y - tileSize) ||
(entities[id].dir.down && entities[id].xy.y >= originPoint.y + tileSize)) {
// Go to the next step in the path array
step = step + 1;
if(step >= path.length){
step = 0;
}
// Reset the origin to the current tile coordinates
originPoint = JSON.parse(JSON.stringify(entities[id].xy));
}
// Set the direction based on the current index of the path array
switch(path[step]) {
case 'up':
entities[id].dir.up = true;
entities[id].dir.down = false;
entities[id].dir.left = false
entities[id].dir.right = false;
break;
case 'down':
entities[id].dir.up = false;
entities[id].dir.down = true;
entities[id].dir.left = false;
entities[id].dir.right = false;
break;
case 'left':
entities[id].dir.up = false;
entities[id].dir.down = false;
entities[id].dir.left = true;
entities[id].dir.right = false;
break;
case 'right':
entities[id].dir.up = false;
entities[id].dir.down = false;
entities[id].dir.left = false;
entities[id].dir.right = true;
break;
};
window.requestAnimationFrame(function(){
setPath(id, path, originPoint, step);
});
}
// Take a tile index and return x,y coordinates
function tileToCoords(tile){
var yIndex = Math.floor(tile / mapW);
var xIndex = tile - (yIndex * mapW);
var y = yIndex * tileSize;
var x = xIndex * tileSize;
return {x:x, y:y};
}
// Take x,y coordinates and return a tile index
function coordsToTile(x, y){
var tile = ((Math.floor(y / tileSize)) * mapW) + (Math.floor(x / tileSize));
return tile;
}
// Generate a map array with a blank map and 4 walls
function testMap(){
for(var i = 0; i < (mapH * mapW); ++i){
// Edges
if (
// top
i < mapW ||
// left
(i % mapW) == 0 ||
// right
((i + 1) % mapW) == 0 ||
// bottom
i > ((mapW * mapH) - mapW)
) {
map.push(
{
id: i,
render: {
base: '#D35',
object: false,
sprite: false
},
state: {
passable: false
}
},
);
}
else{
// Grass
map.push(
{
id: i,
render: {
base: '#0C3',
object: false,
sprite: false
},
state: {
passable: true
}
},
);
}
}
}
<!DOCTYPE html>
<html>
<head>
<style>
body{
background-color: #000;
display: flex;
align-items: center;
justify-content: center;
color: #FFF;
font-size: 18px;
padding: 0;
margin: 0;
}
main{
width: 100%;
max-width: 800px;
margin: 10px auto;
display: flex;
align-items: flex-start;
justify-content: center;
flex-wrap: wrap;
}
.game{
width: 1000px;
height: 1000px;
position: relative;
}
canvas{
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.game canvas{
position: absolute;
top: 0;
left: 0;
width: 800px;
height: 800px;
}
</style>
</head>
<body>
<main>
<div class="game">
<canvas id="save" width="200" height="200" style="z-index: 1;"></canvas>
</div>
</main>
</body>
</html>
As someone has already solved your bug...
This is more than a solution to your problem as the real problem you are facing is complexity, long complex if statements using data structures representing the same information in different ways making it difficult to see simple errors in logic.
On top of that you have some poor style habits that compound the problem.
A quick fix will just mean you will be facing the next problem sooner. You need to write in a ways that reduces the chances of logic errors due to increasing complexity
Style
First style. Good style is very important
Don't assign null to declared variables. JavaScript should not need to use null, the exception to the rule is that some C++ coders infected the DOM API with null returns because they did not understand JavaScipt (or was a cruel joke), and now we are stuck with null
window is the default this (global this) and is seldom needed. Eg window.requestAnimationFrame is identical to just requestAnimationFrame and window.onload is identical to onload
Don't pollute your code with inaccurate, redundant and/or obvious comments, use good naming to provide the needed information. eg:
var map[]; has the comment // array of tile data Well really its an array that has data, who would have guessed, so the comment can be // tiles but then map is a rather ambiguous name. Remove the comment and give the variable a better name.
The comment // Static Globals above some vars. Javascript does not have static as a type so the comment is wrong and the "global's" part is "duh..."
Use const to declare constants, move all the magic numbers to the top and define them as named const. A name has meaning, an number in some code has next to no meaning.
Don't assign listener to the event name, it is unreliable and can be hijacked or overwritten. Always use addEventListener to assign an event listener
Be very careful with your naming. eg the function named coordsToTile is confusing as it does not return a tile, it returns a tile index, either change the function name to match the functions behavior, or change the behavior to match the name.
Don't use redundant intermediate functions, examples:
Your frame request requestAnimationFrame(function(){mainLoop()}); should skip the middle man and be requestAnimationFrame(mainLoop);
You use Function.apply to call the function window[entities[i].logic.func].apply(null, entities[i].logic.data);. apply is used to bind context this to the call, you don't use this in the function so you don't need use the apply. eg window[entities[i].logic.func](...entities[i].logic.data);
BTW being forced to use bracket notation to access a global is a sign of poor data structure. You should never do that.
JavaScript has an unofficial idiomatic styles, you should try to write JS in this style. Some examples from your code
else on the same line as closing }
Space after if, else, for, function() and befor else, opening block {
An id and an index are not the same, use idx or index for an index and id for an identifier
Keep it simple
The more complex you make your data structures the harder it is for you to maintain them.
Structured
Define objects to encapsulate and organize your data.
A global config object, that is transprotable ie can converted be to and from JSON. it contains all the magic numbers, defaults, type descriptions, and what not needed in the game.
Create a set of global utilities that do common repeated tasks, ie create coordinates, list of directions.
Define object that encapsulate the settings and behaviors specific only to that object.
Use polymorphic object design, meaning that different objects use named common behaviors and properties. In the example all drawable object have a function called draw that takes an argument ctx, all objects that can be updated have a function called update
Example
This example is a complete rewrite of your code and fixing your problem. It may be a little advanced, but it is only an example to look though an pick up some tips.
A quick description of the objects used.
Objects
config is transportable config data
testMap is an example map description
tileMap does map related stuff
Path Object encapsulating path logic
Entity Object a single moving entity
Tile Object representing a single tile
game The game state manager
Games have states, eg loading, intro, inPlay, gameOver etc. If you do not plan ahead and create a robust state manager you will find it very difficult to move from one state to the next
I have included the core of a finite state manager. The state manager is responsible for updating and rendering. it is also responsible for all state changes.
setTimeout(() => game.state = "setup", 0); // Will start the game
const canvas = document.getElementById('save');
const ctx = canvas.getContext("2d");
const point = (x = 0, y = 0) => ({x,y});
const dirs = Object.assign(
[point(0, -1), point(1), point(0,1), point(-1)], { // up, right, down, left
"u": 0, // defines index for direction string characters
"r": 1,
"d": 2,
"l": 3,
strToDirIdxs(str) { return str.toLowerCase().split("").map(char => dirs[char]) },
}
);
const config = {
pathIdx: 28,
pathTypes: {
standard: "dulr",
complex: "dulrldudlruldrdlrurdlurd",
},
tile: {size: 16},
defaultTileName: "grass",
entityTypes: {
e: {
speed: 1 / 32, // in fractions of a tile per frame
color: "#00F",
size: {x:16, y:24},
pathName: "standard",
},
f: {
speed: 1 / 16, // in fractions of a tile per frame
color: "#08F",
size: {x:18, y:18},
pathName: "complex",
},
},
tileTypes: {
grass: {
style: {baseColor: "#0C3", object: false, sprite: false},
state: {passable: true}
},
wall: {
style: {baseColor: "#D35", object: false, sprite: false},
state: {passable: false}
},
},
}
const testMap = {
displayChars: {
" " : "grass", // what characters mean
"#" : "wall",
"E" : "grass", // also entity spawn
"F" : "grass", // also entity spawn
},
special: { // spawn enties and what not
"E"(idx) { entities.push(new Entity(config.entityTypes.e, idx)) },
"F"(idx) { entities.push(new Entity(config.entityTypes.f, idx)) }
},
map: // I double the width and ignor every second characters as text editors tend to make chars thinner than high
// 0_1_2_3_4_5_6_7_8_9_ x coord
"####################\n" +
"##FF ## ##\n" +
"## ## ##\n" +
"## #### ##\n" +
"## ##\n" +
"## #### ##\n" +
"## ##\n" +
"## ##\n" +
"## EE##\n" +
"####################",
// 0_1_2_3_4_5_6_7_8_9_ x coord
}
const entities = Object.assign([],{
update() {
for (const entity of entities) { entity.update() }
},
draw(ctx) {
for (const entity of entities) { entity.draw(ctx) }
},
});
const tileMap = {
map: [],
mapToIndex(x, y) { return x + y * tileMap.width },
pxToIndex(x, y) { return x / config.tile.size | 0 + (y / config.tile.size | 0) * tileMap.width },
tileByIdx(idx) { return tileMap.map[idx] },
tileByIdxDir(idx, dir) { return tileMap.map[idx + dir.x + dir.y * tileMap.width] },
idxByDir(dir) { return dir.x + dir.y * tileMap.width },
create(mapConfig) {
tileMap.length = 0;
const rows = mapConfig.map.split("\n");
tileMap.width = rows[0].length / 2 | 0;
tileMap.height = rows.length;
canvas.width = tileMap.width * config.tile.size;
canvas.height = tileMap.height * config.tile.size;
var x, y = 0;
while (y < tileMap.height) {
const row = rows[y];
for (x = 0; x < tileMap.width; x += 1) {
const char = row[x * 2];
tileMap.map.push(new Tile(mapConfig.displayChars[char], x, y));
if (mapConfig.special[char]) {
mapConfig.special[char](tileMap.mapToIndex(x, y));
}
}
y++;
}
},
update () {}, // stub
draw(ctx) {
for (const tile of tileMap.map) { tile.draw(ctx) }
},
};
function Tile(typeName, x, y) {
typeName = config.tileTypes[typeName] ? typeName : config.defaultTileName;
const t = config.tileTypes[typeName];
this.idx = x + y * tileMap.width;
this.coord = point(x * config.tile.size, y * config.tile.size);
this.style = {...t.style};
this.state = {...t.state};
}
Tile.prototype = {
draw(ctx) {
ctx.fillStyle = this.style.baseColor;
ctx.fillRect(this.coord.x, this.coord.y, config.tile.size, config.tile.size);
}
};
function Path(pathName) {
if (typeof config.pathTypes[pathName] === "string") {
config.pathTypes[pathName] = dirs.strToDirIdxs(config.pathTypes[pathName]);
}
this.indexes = config.pathTypes[pathName];
this.current = -1;
}
Path.prototype = {
nextDir(tileIdx) {
var len = this.indexes.length;
while (len--) { // make sure we dont loop forever
const dirIdx = this.indexes[this.current];
if (dirIdx > - 1) {
const canMove = tileMap.tileByIdxDir(tileIdx, dirs[dirIdx]).state.passable;
if (canMove) { return dirs[dirIdx] }
}
this.current = (this.current + 1) % this.indexes.length;
}
}
};
function Entity(type, tileIdx) {
this.coord = point();
this.move = point();
this.color = type.color;
this.speed = type.speed;
this.size = {...type.size};
this.path = new Path(type.pathName);
this.pos = this.nextTileIdx = tileIdx;
this.traveled = 1; // unit dist between tiles 1 forces update to find next direction
}
Entity.prototype = {
set dir(dir) {
if (dir === undefined) { // dont move
this.move.y = this.move.x = 0;
this.nextTileIdx = this.tileIdx;
} else {
this.move.x = dir.x * config.tile.size;
this.move.y = dir.y * config.tile.size;
this.nextTileIdx = this.tileIdx + tileMap.idxByDir(dir);
}
},
set pos(tileIdx) {
this.tileIdx = tileIdx;
const tile = tileMap.map[tileIdx];
this.coord.x = tile.coord.x + config.tile.size / 2;
this.coord.y = tile.coord.y + config.tile.size / 2;
this.traveled = 0;
},
draw(ctx) {
const ox = this.move.x * this.traveled;
const oy = this.move.y * this.traveled;
ctx.fillStyle = this.color;
ctx.fillRect(ox + this.coord.x - this.size.x / 2, oy + this.coord.y - this.size.y / 2, this.size.x, this.size.y)
},
update(){
this.traveled += this.speed;
if (this.traveled >= 1) {
this.pos = this.nextTileIdx;
this.dir = this.path.nextDir(this.tileIdx);
}
}
};
const game = {
currentStateName: undefined,
currentState: undefined,
set state(str) {
if (game.states[str]) {
if (game.currentState && game.currentState.end) { game.currentState.end() }
game.currentStateName = str;
game.currentState = game.states[str];
if (game.currentState.start) { game.currentState.start() }
}
},
states: {
setup: {
start() {
tileMap.create(testMap);
game.state = "play";
},
end() {
requestAnimationFrame(game.render); // start the render loop
delete game.states.setup; // MAKE SURE THIS STATE never happens again
},
},
play: {
render(ctx) {
tileMap.update();
entities.update();
tileMap.draw(ctx);
entities.draw(ctx);
}
}
},
renderTo: ctx,
startTime: undefined,
time: 0,
render(time) {
if (game.startTime === undefined) { game.startTime = time }
game.time = time - game.startTime;
if (game.currentState && game.currentState.render) { game.currentState.render(game.renderTo) }
requestAnimationFrame(game.render);
}
};
body{
background-color: #000;
}
canvas{
image-rendering: pixelated;
position: absolute;
top: 0;
left: 0;
width: 400px;
height: 400px;
}
<canvas id="save" width="200" height="200" style="z-index: 1;"></canvas>
Please Note that there are some running states that have not been tested and as such may have a typo.
Also the tile map must be walled to contain entities or they will throw when they try to leave the playfield.
The code is designed to run in the snippet. To make it work in a standard page add above the very first line setTimeout(() => game.state = "setup", 0); the line addEventListener(load", () = { and after the very last line add the line });
In function setupGameData() I have parametars for 2 cars. In first car speed is 3.00 and second car speed is 3.50. If you click on button "Watch race" you can see first car 3.00 is faster than second car 3.50. How to repair code to see 3.50 is faster than 3.00.
/*jslint plusplus: true, sloppy: true, indent: 4 */
(function () {
"use strict";
// this function is strict...
}());
// RequestAnimFrame: a browser API for getting smooth animations
window.requestAnimFrame = (function() {
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback) {
window.setTimeout(callback, 1000 / 60);
};
})();
// Globals
var canvas = null,
ctx = null,
background = null,
car_sprite = null,
game_data = null,
CAR_WIDTH = 170,
CAR_HEIGHT = 37,
STEP_COUNT_MILLISECONDS = 1000 / 30,
RACE_LENGTH = 20,
RACE_FINISH_LINE_X = 770,
iTime = 0,
iFinishPlace = 1,
random_graph;
function clearCanvas() {
// clear canvas
ctx.clearRect(0, 0, canvas.height, canvas.width);
}
function drawBackground() {
clearCanvas();
ctx.drawImage(background, 0, -400);
loadCarSprite();
}
function loadBackground() {
// Load the timer
background = new Image();
background.src = 'http://www.upslike.net/imgdb/race-scence-f7bf19.png';
background.onload = drawBackground;
}
function setupGameData() {
var json =
{
cars:
[
{
"colour": 'blue',
"x": 0,
"y": 50,
"spritex": 0,
"spritey": 0,
"graph": null,
"step": 77,
"position": null,
"speed": 3.00,
"speed_late": 0.28 },
{
"colour": 'red',
"x": 0,
"y": 110,
"spritex": 0,
"spritey": 37,
"graph": null,
"step": 39,
"position": null,
"speed": 3.50,
"speed_late": 0.48 }
],
graphs:
[
[0,5,10,20,40,60,70],
[0,10,20,30,40,50,60],
[0,20,39,40,50,55,58],
[0,10,20,30,40,50,55],
[0,25,45,47,49,50,52],
[0,10,20,29,38,45,50],
[0,15,20,25,30,40,45],
[0,2,4,8,20,30,40],
[0,5,10,15,20,25,30],
[0,1,3,14,15,22,30],
[0,5,11,14,17,22,25],
[0,20,30,44,67,72,90],
[0,2,7,24,47,52,65],
[0,2,9,20,40,52,70]
]
};
random_graph = Math.floor( Math.random() * json.graphs.length );
return json;
}
function drawCar(car) {
// Draw the car onto the canvas
ctx.drawImage(car_sprite,
car.spritex, car.spritey,
CAR_WIDTH, CAR_HEIGHT,
car.x-70 + car.step, car.y,
CAR_WIDTH, CAR_HEIGHT);
drawText(car);
}
function drawCars() {
var iCarCounter;
for(iCarCounter = 0; iCarCounter < game_data.cars.length; iCarCounter++) {
drawCar(game_data.cars[iCarCounter]);
}
}
function initCar(current_car) {
current_car.graph = random_graph;
}
function initGameState() {
var iCarCounter;
for(iCarCounter = 0; iCarCounter < game_data.cars.length; iCarCounter++) {
initCar(game_data.cars[iCarCounter]);
}
}
function getPositionAtTime(graph_index, percentageElapsed, current_car) {
var graph = game_data.graphs[graph_index],
iNumberOfGraphPoints = graph.length,
iGraphPosition = null,
iFloor = null,
iCeil = null,
p = null;
position = null;
graph = graph.map( function( val, i ) {
if ( i === 0 ) {
return val;
}
var car_speed = undefined === current_car.speed ? 1 : current_car.speed,
car_speed_late = undefined === current_car.speed_late ? car_speed : current_car.speed_late;
return ( i < Math.floor( graph.length / 2 ) ) ? car_speed : car_speed_late;
});
iGraphPosition = (iNumberOfGraphPoints / 100) * percentageElapsed;
iFloor = Math.floor(iGraphPosition);
iCeil = Math.ceil(iGraphPosition);
if(iGraphPosition === iFloor) {
position = graph[iFloor];
} else if(iGraphPosition === iCeil) {
position = graph[iCeil];
} else {
p = (graph[iCeil] - graph[iFloor]) / 100;
position = ((iGraphPosition - iFloor) * 100) * p + graph[iFloor];
}
return position;
}
function redrawRoadSection() {
ctx.drawImage(background, 0, 400, 1000, 200, 0, 0, 1000, 200);
}
function graphPosToScreenPos() {
return (900 / 100) * (position / 60 * 100);
}
function updateDebugWindow() {
// Debug window
var time = document.getElementById('time');
if(time !== null) {
time.value = iTime / 1000;
}
}
function drawText(current_car) {
if(current_car.position !== null) {
ctx.strokeStyle = "black";
ctx.font = "normal 12px Facebook Letter Faces";
ctx.strokeText(current_car.position, RACE_FINISH_LINE_X + current_car.step + 110, current_car.y + 25);
}
}
function moveCar(iCarCounter) {
var current_car = game_data.cars[iCarCounter],
seconds = iTime / 1000,
percentageElapsed = (seconds / RACE_LENGTH) * 100,
a = 20,
velocity = 2,
position = getPositionAtTime(current_car.graph, percentageElapsed,current_car);
if(current_car.x < RACE_FINISH_LINE_X) {
current_car.x = graphPosToScreenPos(position) + (velocity * seconds) + (1/2 * a * Math.pow(seconds, 2));
}
else {
current_car.x = RACE_FINISH_LINE_X;
if(current_car.position === null) {
current_car.position = iFinishPlace++;
}
}
drawCar(current_car);
}
function initCars() {
game_data = setupGameData();
initGameState();
drawCars();
}
function stopLoop() {
iTime = 0;
iFinishPlace = 1;
}
function startRace() {
var iCarCounter;
redrawRoadSection();
for(iCarCounter = 0; iCarCounter < game_data.cars.length; iCarCounter++) {
moveCar(iCarCounter);
}
updateDebugWindow();
if(iFinishPlace > 4) {
stopLoop();
} else {
iTime += STEP_COUNT_MILLISECONDS;
requestAnimFrame(startRace);
}
}
function startLoop() {
stopLoop();
requestAnimFrame(startRace);
}
function loadCarSprite() {
// Load the timer
car_sprite = new Image();
car_sprite.src = 'http://www.upslike.net/imgdb/car-scene-53401b.png';
car_sprite.onload = initCars;
}
function draw() {
// Main entry point got the motion canvas example
canvas = document.getElementById('motion');
// Canvas supported?
if (canvas.getContext) {
ctx = canvas.getContext('2d');
loadBackground();
} else {
alert("Canvas not supported!");
}
}
<script>
window.onload = function() {
draw();
}
</script>
<center><canvas id="motion" width="1000px" height="200px"></canvas></center>
<div style="position: absolute; top: 0px; left:65px;">
<div id="alerter" class="hid">
<input id="loop" onclick="javascript:initCars(); startLoop();" type="button" class="prikaz" value="Watch race">
</div>
</div>
</br>
CodePen
First thing I would do is use console.log in your car movement function to see what speed is, to me it looks like your car speed is being converted to an int instead of a double so your speed 3.50 is 3.00.
Also in your moveCar function you are setting the velocity to 2 and using that in your function, shouldn't you be using your speed variable?
It is also depend on speed late(Json). If you increase blue car 'speed late' then blue car speed is fast. And increase red car 'speed late' then red car speed is fast.
/*jslint plusplus: true, sloppy: true, indent: 4 */
(function () {
"use strict";
// this function is strict...
}());
// RequestAnimFrame: a browser API for getting smooth animations
window.requestAnimFrame = (function() {
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback) {
window.setTimeout(callback, 1000 / 60);
};
})();
// Globals
var canvas = null,
ctx = null,
background = null,
car_sprite = null,
game_data = null,
CAR_WIDTH = 170,
CAR_HEIGHT = 37,
STEP_COUNT_MILLISECONDS = 1000 / 30,
RACE_LENGTH = 20,
RACE_FINISH_LINE_X = 770,
iTime = 0,
iFinishPlace = 1,
random_graph;
function clearCanvas() {
// clear canvas
ctx.clearRect(0, 0, canvas.height, canvas.width);
}
function drawBackground() {
clearCanvas();
ctx.drawImage(background, 0, -400);
loadCarSprite();
}
function loadBackground() {
// Load the timer
background = new Image();
background.src = 'http://www.upslike.net/imgdb/race-scence-f7bf19.png';
background.onload = drawBackground;
}
function setupGameData() {
var json =
{
cars:
[
{
"colour": 'blue',
"x": 0,
"y": 50,
"spritex": 0,
"spritey": 0,
"graph": null,
"step": 77,
"position": null,
"speed": 3.55,
"speed_late": 1 },
{
"colour": 'red',
"x": 0,
"y": 110,
"spritex": 0,
"spritey": 37,
"graph": null,
"step": 39,
"position": null,
"speed": 3.55,
"speed_late": 19 }
],
graphs:
[
[0,5,10,20,40,60,70],
[0,10,20,30,40,50,60],
[0,20,39,40,50,55,58],
[0,10,20,30,40,50,55],
[0,25,45,47,49,50,52],
[0,10,20,29,38,45,50],
[0,15,20,25,30,40,45],
[0,2,4,8,20,30,40],
[0,5,10,15,20,25,30],
[0,1,3,14,15,22,30],
[0,5,11,14,17,22,25],
[0,20,30,44,67,72,90],
[0,2,7,24,47,52,65],
[0,2,9,20,40,52,70]
]
};
random_graph = Math.floor( Math.random() * json.graphs.length );
return json;
}
function drawCar(car) {
// Draw the car onto the canvas
ctx.drawImage(car_sprite,
car.spritex, car.spritey,
CAR_WIDTH, CAR_HEIGHT,
car.x-70 + car.step, car.y,
CAR_WIDTH, CAR_HEIGHT);
drawText(car);
}
function drawCars() {
var iCarCounter;
for(iCarCounter = 0; iCarCounter < game_data.cars.length; iCarCounter++) {
drawCar(game_data.cars[iCarCounter]);
}
}
function initCar(current_car) {
current_car.graph = random_graph;
}
function initGameState() {
var iCarCounter;
for(iCarCounter = 0; iCarCounter < game_data.cars.length; iCarCounter++) {
initCar(game_data.cars[iCarCounter]);
}
}
function getPositionAtTime(graph_index, percentageElapsed, current_car) {
var graph = game_data.graphs[graph_index],
iNumberOfGraphPoints = graph.length,
iGraphPosition = null,
iFloor = null,
iCeil = null,
p = null;
position = null;
graph = graph.map( function( val, i ) {
if ( i === 0 ) {
return val;
}
var car_speed = undefined === current_car.speed ? 1 : current_car.speed,
car_speed_late = undefined === current_car.speed_late ? car_speed : current_car.speed_late;
return ( i < Math.floor( graph.length / 2 ) ) ? car_speed : car_speed_late;
});
iGraphPosition = (iNumberOfGraphPoints / 100) * percentageElapsed;
iFloor = Math.floor(iGraphPosition);
iCeil = Math.ceil(iGraphPosition);
if(iGraphPosition === iFloor) {
position = graph[iFloor];
} else if(iGraphPosition === iCeil) {
position = graph[iCeil];
} else {
p = (graph[iCeil] - graph[iFloor]) / 100;
position = ((iGraphPosition - iFloor) * 100) * p + graph[iFloor];
}
return position;
}
function redrawRoadSection() {
ctx.drawImage(background, 0, 400, 1000, 200, 0, 0, 1000, 200);
}
function graphPosToScreenPos() {
return (900 / 100) * (position / 60 * 100);
}
function updateDebugWindow() {
// Debug window
var time = document.getElementById('time');
if(time !== null) {
time.value = iTime / 1000;
}
}
function drawText(current_car) {
if(current_car.position !== null) {
ctx.strokeStyle = "black";
ctx.font = "normal 12px Facebook Letter Faces";
ctx.strokeText(current_car.position, RACE_FINISH_LINE_X + current_car.step + 110, current_car.y + 25);
}
}
function moveCar(iCarCounter) {
var current_car = game_data.cars[iCarCounter],
seconds = iTime / 1000,
percentageElapsed = (seconds / RACE_LENGTH) * 100,
a = 20,
velocity = 2,
position = getPositionAtTime(current_car.graph, percentageElapsed,current_car);
if(current_car.x < RACE_FINISH_LINE_X) {
current_car.x = graphPosToScreenPos(position) + (velocity * seconds) + (1/2 * a * Math.pow(seconds, 2));
}
else {
current_car.x = RACE_FINISH_LINE_X;
if(current_car.position === null) {
current_car.position = iFinishPlace++;
}
}
drawCar(current_car);
}
function initCars() {
game_data = setupGameData();
initGameState();
drawCars();
}
function stopLoop() {
iTime = 0;
iFinishPlace = 1;
}
function startRace() {
var iCarCounter;
redrawRoadSection();
for(iCarCounter = 0; iCarCounter < game_data.cars.length; iCarCounter++) {
moveCar(iCarCounter);
}
updateDebugWindow();
if(iFinishPlace > 4) {
stopLoop();
} else {
iTime += STEP_COUNT_MILLISECONDS;
requestAnimFrame(startRace);
}
}
function startLoop() {
stopLoop();
requestAnimFrame(startRace);
}
function loadCarSprite() {
// Load the timer
car_sprite = new Image();
car_sprite.src = 'http://www.upslike.net/imgdb/car-scene-53401b.png';
car_sprite.onload = initCars;
}
function draw() {
// Main entry point got the motion canvas example
canvas = document.getElementById('motion');
// Canvas supported?
if (canvas.getContext) {
ctx = canvas.getContext('2d');
loadBackground();
} else {
alert("Canvas not supported!");
}
}
<script>
window.onload = function() {
draw();
}
</script>
<center><canvas id="motion" width="1000px" height="200px"></canvas></center>
<div style="position: absolute; top: 0px; left:65px;">
<div id="alerter" class="hid">
<input id="loop" onclick="javascript:initCars(); startLoop();" type="button" class="prikaz" value="Watch race">
</div>
</div>
</br>
'speed late':1
'speed_late':19
good luck:
I am trying to make a sticky note type utility with the fabric canvas. It will help to be used as annotators.
I want the text to wrap by itself at the given rectangle's width.
Can someone update my fiddle work??
Suggestions are appreciated. Regards...
The following is the link to a part of my fiddle:
http://jsfiddle.net/U7E9q/5/
var canvas = new fabric.Canvas('fabric-canvas');
canvas.hoverCursor = 'pointer';
var text = new fabric.IText("Enter Text Here ",{
fontSize: 20,
top: 100,
left: 100,
backgroundColor: '#faa',
lockScalingX: true,
lockScalingY: true,
selectable: true
});
//alert(text.text);
var rect = new fabric.Rect({
text_field: text,
width: 200,
height: 50,
fill: '#faa',
rx: 10,
ry: 10,
top: 100,
left: 100
});
canvas.add(rect);
canvas.add(text);
canvas.on('object:moving', function (event){
canvas.renderAll();
});
createListenersKeyboard();
function createListenersKeyboard() {
document.onkeydown = onKeyDownHandler;
//document.onkeyup = onKeyUpHandler;
}
function onKeyDownHandler(event) {
//event.preventDefault();
var key;
if(window.event){
key = window.event.keyCode;
}
else{
key = event.keyCode;
}
switch(key){
//////////////
// Shortcuts
//////////////
// Copy (Ctrl+C)
case 67: // Ctrl+C
if(ableToShortcut()){
if(event.ctrlKey){
event.preventDefault();
copy();
}
}
break;
// Delete (Ctrl+D)
case 127: // Ctrl+D
if(ableToShortcut()){
if(event.deleteKey){
delet();
}
}
break;
// Paste (Ctrl+V)
case 86: // Ctrl+V
if(ableToShortcut()){
if(event.ctrlKey){
event.preventDefault();
paste();
}
}
break;
default:
// TODO
break;
}
}
function ableToShortcut(){
/*
TODO check all cases for this
if($("textarea").is(":focus")){
return false;
}
if($(":text").is(":focus")){
return false;
}
*/
return true;
}
function copy(){
if(canvas.getActiveGroup()){
for(var i in canvas.getActiveGroup().objects){
var object = fabric.util.object.clone(canvas.getActiveGroup().objects[i]);
object.set("top", object.top+5);
object.set("left", object.left+5);
copiedObjects[i] = object;
}
}
else if(canvas.getActiveObject()){
var object = fabric.util.object.clone(canvas.getActiveObject());
object.set("top", object.top+5);
object.set("left", object.left+5);
copiedObject = object;
copiedObjects = new Array();
}
}
function paste(){
if(copiedObjects.length > 0){
for(var i in copiedObjects){
canvas.add(copiedObjects[i]);
}
}
else if(copiedObject){
canvas.add(copiedObject);
}
canvas.renderAll();
}
function delet(){
var activeObject = canvas.getActiveObject();
canvas.remove(activeObject);
console.log('after remove getActiveObject(): ', canvas.getActiveObject(), activeObject === canvas.getActiveObject());
canvas.renderAll();
}
If you manage the sticky note as a grouped rect and text you can improve the same behavior. When you need to edit the text inside the group, you just ungroup and clone the elements, append the cloned elements to the canvas and set text as editable.
You need to handle an event like double click to handle this behavior and then handle the mousedown or other interactivity with canvas to regroup them.
http://jsfiddle.net/4HE3U/1/
Above is one fiddle that can satisfy you
Basically i have made one group of Text and Rectangle and i have added it to canvas. There is only one change you need to make is that you can take one textbox to get current sticky note text content as we can not edit text of i-text online once we are adding it any group. Currently there is no way for IText to handle the events as they are not handed down to it if it's contained in a group. I think this is also the prefered way to handle that as it would confuse the user - what if he starts to edit multiple texts. This might end up in a mess. Maybe you can rework your script a little to workaround this problems.
I have added Text and Rectangle
var canvas = new fabric.Canvas('fabric-canvas');
canvas.hoverCursor = 'pointer';
var text = new fabric.IText("Enter Text Here ",{
fontSize: 20,
top: 100,
left: 100,
backgroundColor: '#faa',
lockScalingX: true,
lockScalingY: true,
selectable: true
});
//alert(text.text);
var rect = new fabric.Rect({
text_field: text,
width: 200,
height: 50,
fill: '#faa',
rx: 10,
ry: 10,
top: 100,
left: 100
});
var group = new fabric.Group([ rect, text ], {
left: 100,
top: 100,
lockScalingX: true,
lockScalingY: true,
hasRotatingPoint: false,
transparentCorners: false,
cornerSize: 7
});
canvas.add(group);
//canvas.add(text);
canvas.on('object:moving', function (event){
canvas.renderAll();
});
createListenersKeyboard();
function createListenersKeyboard() {
document.onkeydown = onKeyDownHandler;
//document.onkeyup = onKeyUpHandler;
}
function onKeyDownHandler(event) {
//event.preventDefault();
var key;
if(window.event){
key = window.event.keyCode;
}
else{
key = event.keyCode;
}
switch(key){
//////////////
// Shortcuts
//////////////
// Copy (Ctrl+C)
case 67: // Ctrl+C
if(ableToShortcut()){
if(event.ctrlKey){
event.preventDefault();
copy();
}
}
break;
// Delete (Ctrl+D)
case 127: // Ctrl+D
if(ableToShortcut()){
if(event.deleteKey){
delet();
}
}
break;
// Paste (Ctrl+V)
case 86: // Ctrl+V
if(ableToShortcut()){
if(event.ctrlKey){
event.preventDefault();
paste();
}
}
break;
default:
// TODO
break;
}
}
function ableToShortcut(){
/*
TODO check all cases for this
if($("textarea").is(":focus")){
return false;
}
if($(":text").is(":focus")){
return false;
}
*/
return true;
}
function copy(){
if(canvas.getActiveGroup()){
for(var i in canvas.getActiveGroup().objects){
var object = fabric.util.object.clone(canvas.getActiveGroup().objects[i]);
object.set("top", object.top+5);
object.set("left", object.left+5);
copiedObjects[i] = object;
}
}
else if(canvas.getActiveObject()){
var object = fabric.util.object.clone(canvas.getActiveObject());
object.set("top", object.top+5);
object.set("left", object.left+5);
copiedObject = object;
copiedObjects = new Array();
}
}
function paste(){
if(copiedObjects.length > 0){
for(var i in copiedObjects){
canvas.add(copiedObjects[i]);
}
}
else if(copiedObject){
canvas.add(copiedObject);
}
canvas.renderAll();
}
function delet(){
var activeObject = canvas.getActiveObject();
canvas.remove(activeObject);
console.log('after remove getActiveObject(): ', canvas.getActiveObject(), activeObject === canvas.getActiveObject());
canvas.renderAll();
}
<canvas id="fabric-canvas" width="400" height="400"></canvas>
Here is Sticky note functionality. Text wrap working and font size changes w.r.t sticky note width and height. Editing mode activates on double click.
export const createStickyNotes = (canvas, options) => {
fabric.StickyNote = fabric.util.createClass(fabric.Group, {
type: "StickyNote",
initialize: function (options) {
this.set(options);
var height = this.height;
var width = this.width;
this.rectObj = new fabric.Rect({
width: width,
height: height,
fill: this.rectObj?.fill ?? "rgba(251,201,112,1)",
originX: "center",
originY: "center",
objectCaching: false,
stateProperties: ["fill"],
});
this.textObj = new fabric.Textbox(this.textObj?.text ?? "Notes", {
originX: "center",
originY: "center",
textAlign: "center",
width: 100,
hasControls: false,
fontSize: this.textObj?.fontSize ?? 30,
lineHeight: 1,
stateProperties: ["text", "fontSize"],
scaleX: this.textObj?.scaleX ?? 1,
scaleY: this.textObj?.scaleY ?? 1,
objectCaching: false,
breakWords: true,
fontFamily: "Open Sans",
});
this._objects = [this.rectObj, this.textObj];
// this custom _set function will set custom properties value to object when it will load from json.
// at that time loadFromJson function will call this initialize function.
// this._setCustomProperties(this.options);
canvas.renderAll();
//evenet will fire if the object is double clicked by mouse
this.on("mousedblclick", (e) => {
var pasteFlag = false;
var scaling = e.target.getScaledWidth() / 100;
var textForEditing;
canvas.bringToFront(e.target);
e.target.selectable = false;
const [rectObj, textObj] = this.getObjects();
textObj.clone(function (clonedObj) {
clonedObj.set({
left: e.target.left,
top: e.target.top,
lockMovementY: true,
lockMovementX: true,
hasBorders: false,
scaleX: scaling,
scaleY: scaling,
breakWords: true,
width: textObj.width,
stateProperties: [],
});
textForEditing = clonedObj;
});
this.remove(textObj);
canvas.add(textForEditing);
canvas.setActiveObject(textForEditing);
textForEditing.enterEditing();
textForEditing.selectAll();
textForEditing.paste = (function (paste) {
return function (e) {
disableScrolling();
pasteFlag = true;
};
})(textForEditing.paste);
textForEditing.on("changed", function (e) {
var fontSize = textForEditing.fontSize;
var charCount = Math.max(textForEditing._text.length, 1);
var charWR =
(textForEditing.textLines.length * width) / (charCount * fontSize);
if (textForEditing.height < height - 15) {
fontSize = Math.min(
Math.sqrt(
((height - 10 - fontSize) / 1.16) *
(width / (charCount * charWR))
),
30
);
}
if (textForEditing.height > height - 15) {
fontSize = Math.sqrt(
((height - 10) / 1.16) * (width / (charCount * charWR))
);
}
if (pasteFlag) {
pasteFlag = false;
while (
textForEditing.height > height - 15 &&
textForEditing.fontSize > 0
) {
fontSize = textForEditing.fontSize -= 0.2;
canvas.renderAll();
}
}
textForEditing.fontSize = fontSize;
});
textForEditing.on("editing:exited", () => {
enableScrolling();
canvas.setActiveObject(textObj);
textObj.set({
text: textForEditing.text,
fontSize: textForEditing.fontSize,
visible: true,
});
this.add(textObj);
this.selectable = true;
canvas.remove(textForEditing);
canvas.discardActiveObject();
});
});
function disableScrolling() {
var x = window.scrollX;
var y = window.scrollY;
window.onscroll = function () {
window.scrollTo(x, y);
};
}
var _wrapLine = function (_line, lineIndex, desiredWidth, reservedSpace) {
var lineWidth = 0,
splitByGrapheme = this.splitByGrapheme,
graphemeLines = [],
line = [],
// spaces in different languges?
words = splitByGrapheme
? fabric.util.string.graphemeSplit(_line)
: _line.split(this._wordJoiners),
word = "",
offset = 0,
infix = splitByGrapheme ? "" : " ",
wordWidth = 0,
infixWidth = 0,
largestWordWidth = 0,
lineJustStarted = true,
additionalSpace = splitByGrapheme ? 0 : this._getWidthOfCharSpacing();
reservedSpace = reservedSpace || 0;
desiredWidth -= reservedSpace;
for (var i = 0; i < words.length; i++) {
// i would avoid resplitting the graphemes
word = fabric.util.string.graphemeSplit(words[i]);
wordWidth = this._measureWord(word, lineIndex, offset);
offset += word.length;
// Break the line if a word is wider than the set width
if (this.breakWords && wordWidth >= desiredWidth) {
if (!lineJustStarted) {
graphemeLines.push(line);
line = [];
lineWidth = 0;
lineJustStarted = true;
}
this.fontSize *= desiredWidth / (wordWidth + 1);
// Loop through each character in word
for (var w = 0; w < word.length; w++) {
var letter = word[w];
var letterWidth =
(this.getMeasuringContext().measureText(letter).width *
this.fontSize) /
this.CACHE_FONT_SIZE;
line.push(letter);
lineWidth += letterWidth;
}
word = [];
} else {
lineWidth += infixWidth + wordWidth - additionalSpace;
}
if (lineWidth >= desiredWidth && !lineJustStarted) {
graphemeLines.push(line);
line = [];
lineWidth = wordWidth;
lineJustStarted = true;
} else {
lineWidth += additionalSpace;
}
if (!lineJustStarted) {
line.push(infix);
}
line = line.concat(word);
infixWidth = this._measureWord([infix], lineIndex, offset);
offset++;
lineJustStarted = false;
// keep track of largest word
if (wordWidth > largestWordWidth && !this.breakWords) {
largestWordWidth = wordWidth;
}
}
i && graphemeLines.push(line);
if (largestWordWidth + reservedSpace > this.dynamicMinWidth) {
this.dynamicMinWidth =
largestWordWidth - additionalSpace + reservedSpace;
}
return graphemeLines;
};
fabric.util.object.extend(fabric.Textbox.prototype, {
_wrapLine: _wrapLine,
});
function enableScrolling() {
window.onscroll = function () {};
}
},
toObject: function (propertiesToInclude) {
// This function is used for serialize this object. (used for create json)
// not inlclude this.textObj and this.rectObj into json because when object will load from json, init fucntion of this class is called and it will assign this two object textObj and rectObj again.
var obj = this.callSuper(
"toObject",
[
"objectCaching",
"textObj",
"rectObj",
// ... property list that you want to add into json when this object is convert into json using toJSON() function. (serialize)
].concat(propertiesToInclude)
);
// delete objects array from json because then object load from json, Init function will call. which will automatically re-assign object and assign _object array.
delete obj.objects;
return obj;
},
});
fabric.StickyNote.async = true;
fabric.StickyNote.fromObject = function (object, callback) {
// This function is used for deserialize json and convert object json into button object again. (called when we call loadFromJson() fucntion on canvas)
return fabric.Object._fromObject("StickyNote", object, callback);
};
return new fabric.StickyNote(options);
};
//How to use
var options = {
width: 100,
height: 100,
originX: "center",
originY: "center",
};
var notes = StickyNotes(canvas, options);
canvas.add(notes);