Related
I've been trying to make a ball pit sim using matter.js + p5.js to use as an interactive website background.
With some help from Mr Shiffman's video I've managed to get it working well with circle shapes, but I want to take it to the next level and use custom blob shapes (taken from the client's logo) and apply the same physics to them.
I've been able to get the custom shapes to render using a combination of p5's Beginshape() and matter's bodies.fromVertices. It sort of works as you can see but the physics is very weird, and the collisions don't seem to match even though I used the same vertices for both.
I think it may be to do with this quote from the p5 docs
Transformations such as translate(), rotate(), and scale() do not work within beginShape().
but I don't know what I can do to get around this as I need them to be able to translate / rotate for the physics to work...
Any ideas? help much appreciated!
Codepen
var fric = .6;
var rest = .7
//blob creation function
function Blob(x, y) {
var options = {
friction: fric,
restitution: rest
}
this.body = Bodies.fromVertices(x,y, blob, options);;
World.add(world, this.body);
this.show = function() {
var pos = this.body.position;
var angle = this.body.angle;
push();
translate(pos.x, pos.y);
rotate(angle);
rectMode(CENTER);
strokeWeight(0);
fill('#546B2E')
beginShape();
curveVertex(10.4235750010825,77.51573373392407);
curveVertex(3.142478233002126,70.89274677890447);
curveVertex(0.09197006398718799,61.45980047762196);
curveVertex(1.1915720013184474,51.59196924554452);
curveVertex(4.497757286928595,42.162760563619436);
curveVertex(5.252622102311041,32.216346235505895);
curveVertex(4.731619980811491,22.230638463608106);
curveVertex(4.748780859149178,12.256964518539956);
curveVertex(8.728313738681376,3.3252404103204602);
curveVertex(17.998080279150148,0.07532797415084502);
curveVertex(27.955564903146588,0.6294681264134124);
curveVertex(37.68448491855515,2.8865688476481735);
curveVertex(46.899804284802386,6.733477319787068);
curveVertex(55.386932458422265,12.031766230704845);
curveVertex(62.886098235421045,18.623827217916812);
curveVertex(69.13243582467831,26.40824364010799);
curveVertex(73.70136375533966,35.2754654128657);
curveVertex(75.90839243871912,44.99927633563314);
curveVertex(74.84120838749334,54.8784706257129);
curveVertex(70.09272040861401,63.61579878615303);
curveVertex(62.590342401896606,70.15080526550207);
curveVertex(53.62552650480876,74.54988781923045);
curveVertex(44.08788115809841,77.55817639102708);
curveVertex(34.30859814694884,79.58860716640554);
curveVertex(24.334764892578125,80.23994384765624);
curveVertex(14.444775242328642,78.88621691226959);
endShape(CLOSE);
pop();
}
}
var clientHeight = document.getElementById('physBox').clientHeight;
var clientWidth = document.getElementById('physBox').clientWidth;
var Engine = Matter.Engine,
World = Matter.World,
Bodies = Matter.Bodies,
Common = Matter.Common,
Composite = Matter.Composite,
Mouse = Matter.Mouse,
MouseConstraint = Matter.MouseConstraint,
Vertices = Matter.Vertices;
var blob = Vertices.fromPath('10.4235750010825 77.51573373392407 3.142478233002126 70.89274677890447 0.09197006398718799 61.45980047762196 1.1915720013184474 51.59196924554452 4.497757286928595 42.162760563619436 5.252622102311041 32.216346235505895 4.731619980811491 22.230638463608106 4.748780859149178 12.256964518539956 8.728313738681376 3.3252404103204602 17.998080279150148 0.07532797415084502 27.955564903146588 0.6294681264134124 37.68448491855515 2.8865688476481735 46.899804284802386 6.733477319787068 55.386932458422265 12.031766230704845 62.886098235421045 18.623827217916812 69.13243582467831 26.40824364010799 73.70136375533966 35.2754654128657 75.90839243871912 44.99927633563314 74.84120838749334 54.8784706257129 70.09272040861401 63.61579878615303 62.590342401896606 70.15080526550207 53.62552650480876 74.54988781923045 44.08788115809841 77.55817639102708 34.30859814694884 79.58860716640554 24.334764892578125 80.23994384765624 14.444775242328642 78.88621691226959');
var engine;
var world;
var blobs =[];
var ground;
var ceiling;
var wallLeft;
var wallRight;
var mConstraint;
//start sim after x time
setTimeout(function setup() {
var cnv = createCanvas(clientWidth, clientHeight);
cnv.parent("physBox");
engine = Engine.create();
world = engine.world;
Engine.run(engine);
//add ground
ground = Bodies.rectangle(clientWidth/2, clientHeight+500, clientWidth, 1000, { isStatic: true });
World.add(world, ground);
//add ceiling
ceiling = Bodies.rectangle(clientWidth/2, -clientHeight-500, clientWidth, 1000, { isStatic: true });
World.add(world, ceiling);
//add left wall
wallLeft = Bodies.rectangle(-500, clientHeight/2, 1000, clientHeight*2, { isStatic: true });
World.add(world, wallLeft);
//add right wall
wallRight = Bodies.rectangle(clientWidth+500, clientHeight/2, 1000, clientHeight*2, { isStatic: true });
World.add(world, wallRight);
//create x bodies
for (var i = 0; i < 4; i++) {
blobs.push(new Blob(clientWidth/2, 100));
}
//mouse controls
var options = {
mouse: canvasmouse
}
var canvasmouse = Mouse.create(cnv.elt);
mConstraint = MouseConstraint.create(engine);
World.add(world,mConstraint);
}, 2000);
function draw() {
background('#EEF2FD');
//show all bodies
for (var i = 0; i < blobs.length; i++) {
blobs[i].show();
}
}
body, html {
overflow: hidden;
padding:0;
margin:0;
}
h1 {
font-family: sans-serif;
font-size: 4vw;
background: none;
position: absolute;
margin-top:10%;
margin-left: 10%;
user-select: none;
}
#physBox {
width: 100%;
height: 100vh;
padding: 0;
left: 0;
z-index: -1;
box-sizing: border-box;
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Old versions of Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
supported by Chrome, Edge, Opera and Firefox */
}
<script src="https://cdn.jsdelivr.net/npm/poly-decomp#0.2.1/build/decomp.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.17.1/matter.min.js"></script>
<section id="physBox">
<h1> Welcome to my website</h1>
</section>
Note: I originally tried doing this all with SVGs instead of custom shapes but struggled to understand how to make it work so I gave up, if someone could help me solve this using SVGs instead i'd be happy with that! Also, apologies for my terrible code formatting - I'm learning ;)
The root issue here is that matter.js positions objects based on their center of mass rather than the origin of the coordinate system that your vertices are in, and it is clear that the origin of the coordinate system for you blob vertices is not its center of mass since all of your vertices are positive. You can calculate the center of mass for your blob and then use that offset before drawing:
const fric = 0.6;
const rest = 0.7;
const {
Engine,
Runner,
World,
Bodies,
Body,
Common,
Composite,
Mouse,
MouseConstraint,
Vertices
} = Matter;
const blob = Vertices.fromPath('10.4235750010825 77.51573373392407 3.142478233002126 70.89274677890447 0.09197006398718799 61.45980047762196 1.1915720013184474 51.59196924554452 4.497757286928595 42.162760563619436 5.252622102311041 32.216346235505895 4.731619980811491 22.230638463608106 4.748780859149178 12.256964518539956 8.728313738681376 3.3252404103204602 17.998080279150148 0.07532797415084502 27.955564903146588 0.6294681264134124 37.68448491855515 2.8865688476481735 46.899804284802386 6.733477319787068 55.386932458422265 12.031766230704845 62.886098235421045 18.623827217916812 69.13243582467831 26.40824364010799 73.70136375533966 35.2754654128657 75.90839243871912 44.99927633563314 74.84120838749334 54.8784706257129 70.09272040861401 63.61579878615303 62.590342401896606 70.15080526550207 53.62552650480876 74.54988781923045 44.08788115809841 77.55817639102708 34.30859814694884 79.58860716640554 24.334764892578125 80.23994384765624 14.444775242328642 78.88621691226959');
// from http://paulbourke.net/geometry/polygonmesh/
function computeArea(vertices) {
let area = 0;
for (let i = 0; i < vertices.length - 1; i++) {
let v = vertices[i];
let vn = vertices[i + 1];
area += (v.x * vn.y - vn.x * v.y) / 2;
}
return area;
}
function computeCenter(vertices) {
let area = computeArea(vertices);
let cx = 0,
cy = 0;
for (let i = 0; i < vertices.length - 1; i++) {
let v = vertices[i];
let vn = vertices[i + 1];
cx += (v.x + vn.x) * (v.x * vn.y - vn.x * v.y) / (6 * area);
cy += (v.y + vn.y) * (v.x * vn.y - vn.x * v.y) / (6 * area);
}
return {
x: cx,
y: cy
};
}
const center = computeCenter(blob);
let engine;
let world;
let blobs = [];
let ground;
let ceiling;
let wallLeft;
let wallRight;
let mConstraint;
//blob creation function
function Blob(x, y) {
let options = {
friction: fric,
restitution: rest
}
this.body = Bodies.fromVertices(x, y, blob, options);
World.add(world, this.body);
// Scales the body around the center
Body.scale(this.body, 0.5, 0.5);
this.show = function() {
var pos = this.body.position;
var angle = this.body.angle;
push();
translate(pos.x, pos.y);
rotate(angle);
scale(0.5, 0.5);
translate(-center.x, -center.y);
strokeWeight(0);
fill('#546B2E')
beginShape();
for (const {
x,
y
} of blob) {
curveVertex(x, y);
}
endShape(CLOSE);
pop();
// Alternately, when drawing your blobs you could use
// the bodies vertices, but it looks like these are
// converted into a convex polygon.
push();
stroke('red');
strokeWeight(1);
noFill();
beginShape();
for (const {
x,
y
} of this.body.vertices) {
curveVertex(x, y);
}
endShape(CLOSE);
pop();
}
}
//start sim after x time
function setup() {
const cnv = createCanvas(windowWidth, Math.max(windowHeight, 300));
engine = Engine.create();
world = engine.world;
const runner = Runner.create();
Runner.run(runner, engine);
//add ground
ground = Bodies.rectangle(width / 2, height, width, 50, {
isStatic: true
});
World.add(world, ground);
//add ceiling
ceiling = Bodies.rectangle(width / 2, 0, width, 50, {
isStatic: true
});
World.add(world, ceiling);
//add left wall
wallLeft = Bodies.rectangle(0, height / 2, 50, height, {
isStatic: true
});
World.add(world, wallLeft);
//add right wall
wallRight = Bodies.rectangle(width, height / 2, 50, height, {
isStatic: true
});
World.add(world, wallRight);
//create x bodies
for (let i = 0; i < 4; i++) {
blobs.push(new Blob(random(50, width - 100), random(50, height - 100)));
}
}
function draw() {
background('#EEF2FD');
//show all bodies
for (var i = 0; i < blobs.length; i++) {
blobs[i].show();
}
}
function mouseClicked() {
blobs.push(new Blob(mouseX, mouseY));
}
html,
body {
margin: 0;
overflow-x: hidden;
}
<script src="https://cdn.jsdelivr.net/npm/poly-decomp#0.2.1/build/decomp.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.17.1/matter.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
So I wish to add an effect to my website of floating particles. Came across this codepen which does the exact thing - https://codepen.io/OliverKrieger/pen/EjLEVX.
I've been trying to change it a bit so that the particles only trigger when a checkbox is checked. I understand it might've been already answered on SO but in the codepen code, there is an window.onload function which automatically fires it when window is loaded. I want there to be a checkbox instead.
I tried putting the following in html -
<input type="checkbox" id="switch" onchange="function()"">
<label for="switch" >Toggle</label>
But it seems like the script still triggers automatically. Can someone help me with this please? I'm new to programming so I apologize for any vagueness. Please lemme know if I can provide more details. Any input would be really appreciated.
In your code, function is a keyword and not a function name. The function is still run as soon as the page loads because of the way it is defined in the javascript, and your HTML addition has no effect on this.
You need to change your function into a named function, declared using the function keyword:
function functionName() { ... }
And then you can call it by its name in your HTML:
<input type="checkbox" id="switch" onchange="functionName()">
This should work
(function() {
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
window.requestAnimationFrame = requestAnimationFrame;
})();
$(window).resize(function() {
ParticleCanvas.width = ($(window).width() - 20);
ParticleCanvas.height = ($(window).height() - 10);
});
$(function() {
let request = null;
$("#switch").change(function() {
if (this.checked) {
animateDust();
} else {
window.cancelAnimationFrame(request);
}
})
var ParticleCanvas = document.getElementById("ParticleCanvas");
var context = ParticleCanvas.getContext("2d");
ParticleCanvas.width = ($(window).width() - 20);
ParticleCanvas.height = ($(window).height() - 10);
$("switch").click(function() {
animateDust();
})
// All the info stored into an array for the particles
var particles = {},
particleIndex = 0,
settings = {
density: 20,
particleSize: 2,
startingX: ParticleCanvas.width / 2,
startingY: ParticleCanvas.height,
gravity: -0.01
};
// Set up a function to create multiple particles
function Particle() {
// Establish starting positions and velocities from the settings array, the math random introduces the particles being pointed out from a random x coordinate
this.x = settings.startingX * (Math.random() * 10);
this.y = settings.startingY;
// Determine original X-axis speed based on setting limitation
this.vx = (Math.random() * 2 / 3) - (Math.random() * 3 / 3);
this.vy = -(Math.random() * 5 / 3);
// Add new particle to the index
// Object used as it's simpler to manage that an array
particleIndex++;
particles[particleIndex] = this;
this.id = particleIndex;
this.life = 0;
this.maxLife = 200;
this.alpha = 1;
this.red = 0;
this.green = 255;
this.blue = 255;
}
// Some prototype methods for the particle's "draw" function
Particle.prototype.draw = function() {
this.x += this.vx;
this.y += this.vy;
// Adjust for gravity
this.vy += settings.gravity;
// Age the particle
this.life++;
this.red += 2;
this.alpha -= 0.005;
// If Particle is old, it goes in the chamber for renewal
if (this.life >= this.maxLife) {
delete particles[this.id];
}
// Create the shapes
context.clearRect(settings.leftWall, settings.groundLevel, ParticleCanvas.width, ParticleCanvas.height);
context.beginPath();
context.fillStyle = "rgba(" + this.red + ", " + this.green + ", " + this.blue + ", " + this.alpha + ")";
// Draws a circle of radius 20 at the coordinates 100,100 on the ParticleCanvas
context.arc(this.x, this.y, settings.particleSize, 0, Math.PI * 2, true);
context.closePath();
context.fill();
}
function animateDust() {
context.clearRect(0, 0, ParticleCanvas.width, ParticleCanvas.height);
// Draw the particles
for (var i = 0; i < settings.density; i++) {
if (Math.random() > 0.97) {
// Introducing a random chance of creating a particle
// corresponding to an chance of 1 per second,
// per "density" value
new Particle();
}
}
for (var i in particles) {
particles[i].draw();
}
request = window.requestAnimationFrame(animateDust);
}
})
body {
background-color: #000000;
color: #555555;
margin: 0;
padding: 0;
}
#ParticleCanvas {
border: 1px solid white;
position: absolute;
z-index: -1;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
}
#container {
background: white;
padding: 1rem;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<canvas id="ParticleCanvas"></canvas>
<div id="container">
<form>
<input type="checkbox" id="switch" name="switch">
</form>
<label for="switch">Toggle</label>
</div>
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 });
I have a canvas script, with a dynamic of data. I want to add a link to share the website to facebook:
https://gyazo.com/c1fd1fe956fddba27b48907dc0e9de0a
The icons are part of the image I have not generated them via canvas, now if I listen for a click for co-ords it won't work because it'll look for clicks on the first canvas part aswell.... How can I go about making those icons part of the image clickable....
Part that makes the menu:
ig.module("game.entities.gameover").requires("impact.entity", "game.entities.button-gameover").defines(function() {
var b = new ig.Timer;
EntityGameover = ig.Entity.extend({
size: {
x: 302,
y: 355
},
type: ig.Entity.TYPE.B,
animSheet: new ig.AnimationSheet("media/graphics/game/gameover.png", 301, 352),
zIndex: 900,
globalAlpha: 0.1,
closeDialogue: !0,
init: function(c, d, g) {
this.parent(c, d, g);
this.addAnim("idle", 1, [0]);
this.currentAnim = this.anims.idle;
this.tween({
pos: {
x: 89,
y: 120
}
}, 0.5, {
easing: ig.Tween.Easing.Back.EaseInOut
}).start();
this.storage = new ig.Storage;
this.storage.initUnset("highscore-CTF", 0);
this.storage.initUnset("highscore-CTF2", 0);
this.storage.initUnset("highscore-CTF3", 0);
ig.global.score > this.storage.get("highscore-CTF") ? (this.storage.set("highscore-CTF3", this.storage.get("highscore-CTF2")), this.storage.set("highscore-CTF2", this.storage.get("highscore-CTF")), this.storage.set("highscore-CTF", ig.global.score), this.storage.initUnset("highscore-CTF2", 0), this.storage.initUnset("highscore-CTF3", 0)) : ig.global.score > this.storage.get("highscore-CTF2") ?
(this.storage.set("highscore-CTF3", this.storage.get("highscore-CTF2")), this.storage.set("highscore-CTF2", ig.global.score), this.storage.initUnset("highscore-CTF2", 0), this.storage.initUnset("highscore-CTF3", 0)) : ig.global.score > this.storage.get("highscore-CTF3") && this.storage.set("highscore-CTF3", ig.global.score);
this.storage.initUnset("total-CTF", 0);
this.storage.set("total-CTF", this.storage.get("total-CTF") + ig.global.score);
ig.game.spawnEntity(EntityButtonGameover, 23, 700, {
buttonID: 1
});
ig.game.spawnEntity(EntityButtonGameover,
220, 700, {
buttonID: 2
});
ig.game.spawnEntity(EntityButtonGameover, 390, 700, {
buttonID: 3
});
b.set(0.3)
},
update: function() {
this.parent()
},
draw: function() {
this.ctx = ig.system.context;
this.closeDialogue ? (this.ctx.save(), this.ctx.fillStyle = "#000000", this.ctx.globalAlpha = this.globalAlpha, this.ctx.fillRect(0, 0, 480, 640), this.ctx.restore(), this.globalAlpha = 0.7 <= this.globalAlpha ? 0.7 : this.globalAlpha + 0.01) : this.closeDialogue || (this.ctx.save(), this.ctx.fillStyle = "#000000", this.ctx.globalAlpha = this.globalAlpha, this.ctx.fillRect(0,
0, 480, 640), this.ctx.restore(), this.globalAlpha = 0.1 >= this.globalAlpha ? 0 : this.globalAlpha - 0.05);
this.parent();
this.ctx.font = "30px happy-hell";
this.ctx.fillStyle = "#5b2a0b";
this.ctx.textAlign = "center";
this.ctx.fillText(_STRINGS.UI.Best, this.pos.x + 70, this.pos.y + 180);
this.ctx.fillText(_STRINGS.UI.Score, this.pos.x + 70, this.pos.y + 260);
//share
this.ctx.font = "30px happy-hell";
this.ctx.fillStyle = "#ffffff";
this.ctx.textAlign = "left";
this.ctx.fillText(this.storage.getInt("highscore-CTF"), this.pos.x + 140, this.pos.y + 180);
this.ctx.fillText(ig.global.score, this.pos.x + 140, this.pos.y + 260)
},
closeDialogueFunc: function() {
this.closeDialogue && (this.tween({
pos: {
x: 89,
y: -600
}
}, 0.5, {
easing: ig.Tween.Easing.Back.EaseInOut
}).start(), this.closeDialogue = !1)
}
})
});
A simple, versatile way to add menus to canvas graphics is to simply overlay an absolutely positioned DOM structure. Your browser has already all event handling build-in, there is no need to reinvent the wheel.
var canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d');
ctx.fillStyle = 'rgb(0, 155, 255)';
ctx.fillRect(0, 0, canvas.width, canvas.height)
#container {
position: relative;
display: inline-block;
}
#menu {
position: absolute;
text-align: center;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
#menu a {
padding: 15px;
font-size: 50px;
line-height: 100px;
color: black;
text-shadow: 2px 2px 5px white;
}
#menu a:hover {
color: white;
}
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet" />
<div id='container'>
<canvas id='canvas' width='400' height='100'></canvas>
<div id='menu'>
<a href='http://facebook.com'><i class='fa fa-facebook-official'></i></a>
<a href='http://twitter.com'><i class='fa fa-twitter'></i></a>
<a href='http://whatsapp.com'><i class='fa fa-whatsapp'></i></a>
</div>
</div>
Your browser is capable of rendering such overlay menus very quickly. You should use CSS to style your overlay menu links or buttons.
I have managed to click on a certain element in a canvas.
I have tried to explain with comments what it does.
I have made 3 limits as shown in the image below.
And I'm comparing only with x value, if it's in between these limits. It can be more complex so getCursorPosition() method returns an object with x and y components, just in case if you need to make more comparisons.
https://jsfiddle.net/_jserodio/asa10pye/
var canvas;
var ctx;
// first get your canvas
canvas = document.getElementById('canvas');
canvas.width = 253;
canvas.heigth = 68;
// assign the context
ctx = canvas.getContext("2d");
// asign click event to the canvas
addEventListener("click", listener, false);
function listener(e) {
// if you have 3 buttons
var position = getCursorPosition(e);
var limit1 = canvas.width / 3;
//console.log("limit1: " + limit1);
var limit2 = canvas.width * 2 / 3;
//console.log("limit2: " + limit2);
var limit3 = canvas.width;
//console.log("limit3: " + limit3);
if (position.x < limit1) {
console.log("go to facebook");
//window.open("http://www.facebook.com");
} else if (position.x < limit2) {
console.log("go to twitter");
//window.open("http://www.twitter.com");
} else if (position.x < limit3) {
console.log("go to whatsapp");
//window.open("http://www.whatsapp.com");
}
//console.log("\nx" + position.x);
//console.log("y" + position.y);
}
function getCursorPosition(event) {
var rect = canvas.getBoundingClientRect();
var x = event.clientX - rect.left;
var y = event.clientY - rect.top;
var data = {
x: x,
y: y
};
return data;
}
// load image from data url
var imageObj = new Image();
imageObj.onload = function() {
ctx.drawImage(this, 0, 0);
};
imageObj.src = 'https://justpaste.it/files/justpaste/d307/a11791570/file1.png';
<canvas id='canvas' width="253" height="68">
</canvas>
Bonus!
Here you have a demo I made using this. demo (Draughts/checkers).
You can check the entire code here if you want.
You have several operations in this case.
Option 1:
You'll need to remember the "bounding area" of the three buttons. Anytime the canvas receives a "click", get the click position (How do I get the coordinates of a mouse click on a canvas element?). Once you get the click position. Detect if said click occurs within the bounding area of the button. If it does, use window.open to go there.
Option 2: Similar to Option 1, but instead of remembering a "bounding area", create a separate hidden canvas of the same size as your image with the backgrounds black ('#000000') and the button given distinctive colors (for example, red for Facebook, green for Twitter, and blue for Hangout?).
Then, similar to Option 1, get the click position relative to the canvas. Then use ctx.getImageData(sx, sy, sw, sh) on the context of the hidden canvas layer. If the value you get back is red, then user clicked on Facebook, if green, Twitter, and if blue, Hangout.
I used interact.js library to write this piece of code which works absolutely fine standalone on chrome, firefox and w3schools "Try it Yourself" (doesn't work on Edge and IE for some reason). The problem is that when I call a template.phtml with this code inside from the layout.xml, the magento renders it only once, thus the user is not allowed to resize the cubes.
<!-- CSS -->
<style type="text/css">
svg {
width: 100%;
height: 300px;
background-color: #CDC9C9;
-ms-touch-action: none;
touch-action: none;
}
.edit-rectangle {
fill: black;
stroke: #fff;
}
body { margin: 0; }
</style>
<!-- Content -->
<br>
<svg>
</svg>
<br>
<button onclick="location.href = 'square';" id="previousbutton">Go back</button>
<button onclick="location.href = 'squaresection';" style="float:right" id="nextButton">Proceed to next step</button>
<br>
<br>
<script type="text/javascript" src="interact.js">
</script>
<!-- JavaScript -->
<script type="text/javascript">
var svgCanvas = document.querySelector('svg'),
svgNS = 'http://www.w3.org/2000/svg',
rectangles = [];
labels = [];
rectNumb = 5;
function Rectangle (x, y, w, h, svgCanvas) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.stroke = 0;
this.el = document.createElementNS(svgNS, 'rect');
this.el.setAttribute('data-index', rectangles.length);
this.el.setAttribute('class', 'edit-rectangle');
rectangles.push(this);
this.draw();
svgCanvas.appendChild(this.el);
}
function Label (x, y, text, svgCanvas){
this.x = x;
this.y = y;
this.text = text;
this.el = document.createElementNS(svgNS, 'text');
labels.push(this);
this.draw();
svgCanvas.appendChild(this.el);
}
Label.prototype.draw = function () {
this.el.setAttribute('x', this.x);
this.el.setAttribute('y', this.y);
this.el.setAttribute('font-family', "Verdana");
this.el.setAttribute('font-size', 14);
this.el.setAttribute('fill', "black");
this.el.innerHTML = this.text;
}
Rectangle.prototype.draw = function () {
this.el.setAttribute('x', this.x + this.stroke / 2);
this.el.setAttribute('y', this.y + this.stroke / 2);
this.el.setAttribute('width' , this.w - this.stroke);
this.el.setAttribute('height', this.h - this.stroke);
this.el.setAttribute('stroke-width', this.stroke);
}
interact('.edit-rectangle')
// change how interact gets the
// dimensions of '.edit-rectangle' elements
.rectChecker(function (element) {
// find the Rectangle object that the element belongs to
var rectangle = rectangles[element.getAttribute('data-index')];
// return a suitable object for interact.js
return {
left : rectangle.x,
top : rectangle.y,
right : rectangle.x + rectangle.w,
bottom: rectangle.y + rectangle.h
};
})
/*
.draggable({
max: Infinity,
onmove: function (event) {
var rectangle = rectangles[event.target.getAttribute('data-index')];
rectangle.x += event.dx;
rectangle.y += event.dy;
rectangle.draw();
}
})
*/
.resizable({
onstart: function (event) {},
onmove : function (event) {
if (event.target.getAttribute('data-index') > 0)
{
// Main Rect
var rectangle = rectangles[event.target.getAttribute('data-index')];
var rectangle2 = rectangles[event.target.getAttribute('data-index') - 1];
if (rectangle.w - event.dx > 10 && rectangle2.w + event.dx > 10){
rectangle.x += event.dx;
rectangle.w = rectangle.w - event.dx;
rectangle2.w = rectangle2.w + event.dx;
}
rectangle.draw();
rectangle2.draw();
var label = labels[event.target.getAttribute('data-index')];
var label2 = labels[event.target.getAttribute('data-index') - 1];
label.text = rectangle.w + " mm";
label2.text = rectangle2.w + " mm";
label.x = rectangle.x + rectangle.w / 4;
label2.x = rectangle2.x + rectangle2.w / 4;
label.draw();
label2.draw();
}
},
onend : function (event) {},
edges: {
top : false, // Disable resizing from top edge.
left : true,
bottom: false,
right : false // Enable resizing on right edge
},
inertia: false,
// Width and height can be adjusted independently. When `true`, width and
// height are adjusted at a 1:1 ratio.
square: false,
// Width and height can be adjusted independently. When `true`, width and
// height maintain the aspect ratio they had when resizing started.
preserveAspectRatio: false,
// a value of 'none' will limit the resize rect to a minimum of 0x0
// 'negate' will allow the rect to have negative width/height
// 'reposition' will keep the width/height positive by swapping
// the top and bottom edges and/or swapping the left and right edges
invert: 'reposition',
// limit multiple resizes.
// See the explanation in the #Interactable.draggable example
max: Infinity,
maxPerElement: 3,
});
interact.maxInteractions(Infinity);
var positionX = 50,
positionY = 80,
width = 80,
height = 80;
for (var i = 0; i < rectNumb; i++) {
positionX = 50 + 82 * i;
new Rectangle(positionX, positionY, width, height, svgCanvas);
}
for (var i = 0; i < rectNumb; i++) {
positionX = 50 + 82 * i;
new Label(positionX + width/4, positionY + height + 20, width +" mm", svgCanvas);
}
</script>
Any suggestions of what I could do to implement this code into magento would be much appreciated.
Magento did not render the code only once. The problem was that canvas event listener always assumed that pointer coordinates were wrong. Since canvas is the first element of the page(because it is the first element in that .phtml file), event listener assumed it will be displayed at the top, but that was not the case because of the way magento page rendering works.
This issue was resolved simply by measuring the height of content above canvas and just mathematically subtracting that from pointers position before passing it to event listener.
The problem with this solution is that it works only for single page or with multiple pages that have the same height of content above canvas(=>same design). If anyone knows a way in which person would not need to "recalculate" the height for every single page that has different design, sharing knowledge would be much appreciated.