I am trying to write a scroll-on-hover function in an HTML canvas by defining a hover variable, detecting mouse events over the designated hover area and (on doing so) adding or subtracting to this hover variable depending on which area is hovered over. This hover variable is connected to the position of a series of selection buttons which, for the purposes of this example, contain the numbers 0 to 30. When either end of this series of selection buttons is hovered over they all move up or down as if scrolled, but to make it act like a scroll you must keep the mouse moving as the canvas is only rendered on each new mousemove event.
My question is how can I trigger the event on mouseover such that if (lowerHoverBoxHitTest(x, y)) or (upperHoverBoxHitTest(x, y)) (i.e if the mouse is hovered over either of the hit boxes defined in the script below) the hover variable keeps being added to by the set increment (0.1) until the mouse leaves that area. I have tried replacing the if/else statement in the function mouseMove with a while loop (as it would seem this is logically akin to what I am asking) as so
while (lowerHoverBoxHitTest(x, y)) {
if (hover < 750) {
hover-=0.1;
}
}
while (upperHoverBoxHitTest(x, y)) {
if (hover > 0) {
hover+=0.1;
}
}
but this just causes the page to crash (presumably it triggers an infinite loop?). There isn't much on Stack Overflow about this besides this but this solution is not useful if you have a lot of other things in your canvas that you don't want to scroll (unless you were to define their position absolutely which I don't want to) which I do in my full project. Any help will be appreciated.
var c=document.getElementById('game'),
canvasX=c.offsetLeft,
canvasY=c.offsetTop,
ctx=c.getContext('2d');
var hover=0;
function upperHoverBoxHitTest(x, y) {
return (x >= 0) && (x <= 350) && (y >= 0) && (y <= 50);
}
function lowerHoverBoxHitTest(x, y) {
return (x >= 0) && (x <= 350) && (y >= 450) && (y <= 500);
}
var selectionForMenu = function(id, text, y) {
this.id = id;
this.text = text;
this.y = y;
}
selectionForMenu.prototype.makeSelection = function() {
ctx.beginPath();
ctx.fillStyle='#A84FA5';
ctx.fillRect(0, this.y+hover, 350, 30)
ctx.stroke();
ctx.font='10px Noto Sans';
ctx.fillStyle='white';
ctx.textAlign='left';
ctx.fillText(this.text, 10, this.y+hover+19);
}
var Paint = function(element) {
this.element = element;
this.shapes = [];
}
Paint.prototype.addShape = function(shape) {
this.shapes.push(shape);
}
Paint.prototype.render = function() {
ctx.clearRect(0, 0, this.element.width, this.element.height);
for (var i=0; i<this.shapes.length; i++) {
this.shapes[i].makeSelection();
}
}
var paint = new Paint(c);
for (i=0; i<30; i++) {
paint.addShape(new selectionForMenu(i+1, i, i*30));
}
paint.render();
function mouseMove(event) {
var x = event.x - canvasX;
var y = event.y - canvasY;
paint.render();
if (lowerHoverBoxHitTest(x, y)) {
hover+=1;
} else if (upperHoverBoxHitTest(x, y)) {
hover-=1;
}
}
c.addEventListener('mousemove', mouseMove);
canvas {
z-index: -1;
margin: 1em auto;
border: 1px solid black;
display: block;
background: #9F3A9B;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>uTalk Demo</title>
<link rel='stylesheet' type='text/css' href='wordpractice.css' media='screen'>
</head>
<body>
<canvas id="game" width = "350" height = "500"></canvas>
</body>
</html>
Animation via animation loops.
You need to have an animation loop that will increment/decrement the value if the conditions are met. This loop can be part of another if you have one (which is better than adding an animation loop for each animated object) or as its own function.
The animation loop does all the rendering, and only if needed (no point rendering something that is already rendered).
Demo
Demo is a copy of the OP's code with modifications to animate the scrolling and give a little user feed back. Though not complete as a scrolling selection box, it will need some tweaking to be useful.
var c = document.getElementById('game'),
canvasX = c.offsetLeft,
canvasY = c.offsetTop,
ctx = c.getContext('2d');
var hover = 0;
const overTypes = {
lower : 1,
raise : 2,
none : 0,
}
var overBox = 0;
var overDist = 0;
const maxSpeed = 4;
const shapeSize = 30;
const hoverScrollSize = 50;
const gradUp = ctx.createLinearGradient(0, 0, 0, hoverScrollSize);
const gradDown = ctx.createLinearGradient(0, ctx.canvas.height - hoverScrollSize, 0, ctx.canvas.height);
gradUp.addColorStop(0, `rgba(${0xA8},${0x4F},${0xB5},1)`);
gradUp.addColorStop(1, `rgba(${0xA8},${0x4F},${0xB5},0)`);
gradDown.addColorStop(1, `rgba(${0xB8},${0x5F},${0xB5},1)`);
gradDown.addColorStop(0, `rgba(${0xB8},${0x5F},${0xB5},0)`);
c.addEventListener('mousemove', mouseMove)
c.addEventListener('mouseout', () => {
overBox = overTypes.none
}); // stop scroll when mouse out of canvas
// start the first frame
requestAnimationFrame(() => {
paint.render(); // paint first frame
requestAnimationFrame(mainLoop); // start main loop
});
function mainLoop() {
if (overBox !== overTypes.none) {
hover += overDist / hoverScrollSize * (overBox === overTypes.lower ? maxSpeed : -maxSpeed);
var bottom = - (paint.shapes.length - ctx.canvas.height / shapeSize) * shapeSize;
hover = hover > 0 ? 0 : hover < bottom ? bottom : hover;
paint.render();
}
requestAnimationFrame(mainLoop); // wait for next animation frame
}
function mouseMove(event) {
var x = event.clientX - canvasX;
var y = event.clientY - canvasY;
if (lowerHoverBoxHitTest(x, y)) {
overBox = overTypes.lower;
} else if (upperHoverBoxHitTest(x, y)) {
overBox = overTypes.raise;
} else {
overBox = overTypes.none;
}
}
function upperHoverBoxHitTest(x, y) {
overDist = hoverScrollSize - y;
return (x >= 0) && (x <= 350) && (y >= 0) && (y <= hoverScrollSize);
}
function lowerHoverBoxHitTest(x, y) {
overDist = y - (ctx.canvas.height - hoverScrollSize);
return (x >= 0) && (x <= 350) && (y >= ctx.canvas.height - hoverScrollSize) && (y <= ctx.canvas.height);
}
var selectionForMenu = function (id, text, y) {
this.id = id;
this.text = text;
this.y = y;
}
selectionForMenu.prototype.makeSelection = function () {
ctx.beginPath();
ctx.fillStyle = '#A84FA5';
ctx.fillRect(0, this.y + hover, 350, shapeSize)
ctx.stroke();
ctx.font = '10px Noto Sans';
ctx.fillStyle = 'white';
ctx.textAlign = 'left';
ctx.fillText(this.text, 10, this.y + hover + 19);
}
var Paint = function (element) {
this.element = element;
this.shapes = [];
}
Paint.prototype.addShape = function (shape) {
this.shapes.push(shape);
}
Paint.prototype.render = function () {
ctx.clearRect(0, 0, this.element.width, this.element.height);
for (var i = 0; i < this.shapes.length; i++) {
this.shapes[i].makeSelection();
}
if (overBox !== overTypes.none) {
ctx.globalAlpha = 0.4 * (overDist / 50);
ctx.globalCompositeOperation = "lighter";
if (overBox === overTypes.raise) {
ctx.fillStyle = gradUp;
ctx.fillRect(0, 0, ctx.canvas.width, hoverScrollSize);
} else if (overBox === overTypes.lower) {
ctx.fillStyle = gradDown;
ctx.fillRect(0, ctx.canvas.height - hoverScrollSize, ctx.canvas.width, hoverScrollSize);
}
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1;
}
}
var paint = new Paint(c);
for (i = 0; i < 30; i++) {
paint.addShape(new selectionForMenu(i + 1, i, i * 30));
}
paint.render();
canvas {
z-index: -1;
margin: 1em auto;
border: 1px solid black;
display: block;
background: #9F3A9B;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>uTalk Demo</title>
<link rel='stylesheet' type='text/css' href='wordpractice.css' media='screen'>
</head>
<body>
<canvas id="game" width = "350" height = "150"></canvas>
</body>
</html>
Related
I'm trying to make a little game with JavaScript (no engine) and I want to get rid of frame-based animation.
I successfully added delta time for horizontal movements (work fine with 60 or 144fps).
But I can't make it work with the jump, height (or the strength) isn't always the same, and I don't know why.
I already tried (And still had the exact same problem):
Passing Delta Time at the end of update(): x += Math.round(dx * dt)
Changing Date.now() to performance.now()
Not rounding DeltaY
Locking Jump Height
I made a simplified example with 2 jump type, height locked jump and a normal jump (IDK what to call it). Both have the same problem.
const canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d'),
canvas2 = document.getElementById('canvas2'),
ctx2 = canvas2.getContext('2d');
// CLASS PLAYER ------------------------
class Actor {
constructor(color, ctx, j) {
this.c = ctx
this.w = 20
this.h = 40
this.x = canvas.width /2 - this.w/2
this.y = canvas.height/2 - this.h/2
this.color = color
// Delta
this.dy = 0
// Movement
this.gravity = 25/1000
this.maxSpeed = 600/1000
// Jump Height lock
this.jumpType = (j) ? 'capedJump' : 'normalJump'
this.jumpHeight = -50
// Booleans
this.isOnFloor = false
}
// Normal jump
normalJump(max) {
if(!this.isOnFloor) return
this.dy = -max
this.isOnFloor = false
}
// Jump lock (locked max height)
capedJump(max) {
const jh = this.jumpHeight;
if(jh >= 0) return
this.dy += -max/15
if(jh - this.dy >= 0) {
this.dy = (jh - this.dy) + jh
this.jumpHeight = 0
} else {
this.jumpHeight += -this.dy
}
}
update(dt) {
const max = this.maxSpeed*dt,
gravity = this.gravity*dt;
// JUMP
this[this.jumpType](max)
// GRAVITY
this.dy += gravity
// TOP AND DOWN COLLISION (CANVAS BORDERS)
const y = this.y + this.dy,
h = y + this.h;
if (y <= 0) this.y = this.dy = 0
else if (h >= canvas.height) {
this.y = canvas.height - this.h
this.dy = 0
this.isOnFloor = true
this.jumpHeight = -50
}
// Update Y
this.y += Math.round(this.dy)
}
draw() {
const ctx = this.c
ctx.fillStyle = this.color
ctx.fillRect(this.x, this.y, this.w, this.h)
}
}
const Player = new Actor('brown', ctx, false)
const Player2 = new Actor('blue', ctx2, true)
// ANIMATE -----------------------------
let lastRender = 0
let currRender = Date.now()
function animate() {
// Set Delta Time
lastRender = currRender
currRender = Date.now()
let dt = currRender - lastRender
// CANVAS #1 (LEFT)
ctx.clearRect(0, 0, canvas.width, canvas.height)
background(ctx)
Player.update(dt)
Player.draw()
// CANVAS #2 (RIGHT)
ctx2.clearRect(0, 0, canvas2.width, canvas2.height)
background(ctx2)
Player2.update(dt)
Player2.draw()
window.requestAnimationFrame(animate)
}
animate()
// EVENT LISTENERS -----------------------
window.addEventListener('keydown', (e) => {
e.preventDefault()
if (Player.keys.hasOwnProperty(e.code)) Player.keys[e.code] = true
})
window.addEventListener('keyup', (e) => {
e.preventDefault()
if (Player.keys.hasOwnProperty(e.code)) Player.keys[e.code] = false
})
// Just a function to draw Background nothing to see here
function background(c) {
const lineNumber = Math.floor(canvas.height/10)
c.fillStyle = 'gray'
for(let i = 0; i < lineNumber; i++) {
c.fillRect(0, lineNumber*i, canvas.width, 1)
}
}
div {
display: inline-block;
font-family: Arial;
}
canvas {
border: 1px solid black;
}
span {
display: block;
color: gray;
text-align: center;
}
<div>
<canvas width="100" height="160" id="canvas"></canvas>
<span>Normal</span>
</div>
<div>
<canvas width="100" height="160" id="canvas2"></canvas>
<span>Locked</span>
</div>
Here's how I would refactor the code:
Don't use dy for both speed and position (which you seem to be doing). Rename it vy and use it purely as the vertical velocity.
Move isOnFloor to a function so that we can always check for collisions with the floor.
Decouple the jump functions from actual movement updates. Just make them set the vertical velocity if the player is on the floor.
Perform top / bottom collision checking separately depending on the direction of movement.
Don't round DeltaY - it'll mess up small movements.
With these changes in place, the movement behavior is correct and stable:
const canvas1 = document.getElementById('canvas1'),
ctx1 = canvas1.getContext('2d'),
canvas2 = document.getElementById('canvas2'),
ctx2 = canvas2.getContext('2d');
// Global physics variables
const GRAVITY = 0.0015;
const MAXSPEED = 0.6;
const MAXHEIGHT = 95;
// CLASS PLAYER ------------------------
class Actor {
constructor(C, W, H, J) {
// World size
this.worldW = W;
this.worldH = H;
// Size & color
this.w = 20;
this.h = 40;
this.color = C;
// Speed
this.vy = 0;
// Position
this.x = W/2 - this.w/2;
this.y = H/2 - this.h/2;
// Jump Height lock
this.jumpCapped = J;
this.jumpHeight = 0;
}
// move isOnFloor() to a function
isOnFloor() {
return this.y >= this.worldH - this.h;
}
// Normal jump
normalJump() {
if(!this.isOnFloor()) return;
this.vy = -MAXSPEED;
}
// Jump lock (locked max height)
cappedJump(max) {
if(!this.isOnFloor()) return;
this.vy = -MAXSPEED;
this.jumpHeight = max;
}
update(dt) {
// JUMP
if (this.jumpCapped)
this.cappedJump(MAXHEIGHT);
else
this.normalJump();
// GRAVITY
this.vy += GRAVITY * dt;
this.y += this.vy * dt;
// Bottom collision
if (this.vy > 0) {
if (this.isOnFloor()) {
this.y = this.worldH - this.h;
this.vy = 0;
}
}
else
// Top collision
if (this.vy < 0) {
const maxh = (this.jumpCapped) ? (this.worldH - this.jumpHeight) : 0;
if (this.y < maxh) {
this.y = maxh;
this.vy = 0;
}
}
}
draw(ctx) {
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.w, this.h);
}
}
const Player1 = new Actor('brown', canvas1.width, canvas1.height, false);
const Player2 = new Actor('blue', canvas2.width, canvas2.height, true);
// ANIMATE -----------------------------
let lastUpdate = 0;
let randomDT = 0;
function animate() {
// Compute delta time
let currUpdate = Date.now();
let dt = currUpdate - lastUpdate;
// Randomize the update time interval
// to test the physics' stability
if (dt > randomDT) {
randomDT = 35 * Math.random() + 5;
Player1.update(dt);
Player2.update(dt);
lastUpdate = currUpdate;
}
// CANVAS #1 (LEFT)
ctx1.clearRect(0, 0, canvas1.width, canvas1.height);
background(ctx1);
Player1.draw(ctx1);
// CANVAS #2 (RIGHT)
ctx2.clearRect(0, 0, canvas2.width, canvas2.height);
background(ctx2);
Player2.draw(ctx2);
window.requestAnimationFrame(animate);
}
animate();
// EVENT LISTENERS -----------------------
window.addEventListener('keydown',
(e) => {
e.preventDefault();
if (Player.keys.hasOwnProperty(e.code))
Player.keys[e.code] = true;
}
)
window.addEventListener('keyup',
(e) => {
e.preventDefault()
if (Player.keys.hasOwnProperty(e.code))
Player.keys[e.code] = false;
}
)
// Just a function to draw Background nothing to see here
function background(c) {
const lineNumber = Math.floor(canvas1.height/10)
c.fillStyle = 'gray'
for(let i = 0; i < lineNumber; i++) {
c.fillRect(0, lineNumber*i, canvas1.width, 1)
}
}
div {
display: inline-block;
font-family: Arial;
}
canvas {
border: 1px solid black;
}
span {
display: block;
color: gray;
text-align: center;
}
<div>
<canvas width="100" height="160" id="canvas1"></canvas>
<span>Normal</span>
</div>
<div>
<canvas width="100" height="160" id="canvas2"></canvas>
<span>Locked</span>
</div>
The attached code shows how to select (on click) a drawn object on canvas and then the object is moved with a double click to click position or deselected with a double click prior to/ post-movement.
I have tried for days but could not work out how to apply this function to all objects in a class or an array via looping (via class constructor + prototyping). I would like to be able to select or deselect any object on screen.
Help will be very much appreciated. Thank you.
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
canvas {
display: block;
margin: 0px;
}
body {
margin: 0px;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<input id="click1" type ="button" value="x" style="position: fixed; top: 0px; left: 650px; position: absolute;"></input>
<input id="click2" type ="button" value="y" style="position: fixed; top: 0px; left: 750px; position: absolute;"></input>
<script>
window.onload = function(){
var canvas = document.getElementById("canvas");
context = canvas.getContext("2d");
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
let strokeColor;
let color;
let mouse_x;
let mouse_y;
let x;
let y;
let w;
let h;
let selected = false;
x = 50;
y = 50;
w = 50;
h = 50;
color="green";
strokeColor = "green";
document.getElementById('canvas').addEventListener("mousemove",go);
document.getElementById('canvas').addEventListener("mouseup",mouseUp);
document.getElementById('canvas').addEventListener("dblclick",dblClick);
document.getElementById('canvas').addEventListener("dblclick",move);
function move(){
if(selected == true){
x = mouse_x;
y = mouse_y;
}
}
function mouseUp(){
if(mouse_x > x && mouse_x < x+w && mouse_y > y && mouse_y < y+w){
strokeColor = "black";
selected = true;
console.log(selected);
}
}
function dblClick(){
if(mouse_x > x && mouse_x < x+w && mouse_y > y && mouse_y < y+w){
color = "green";
strokeColor = color;
selected = false;
console.log(selected);
}
}
function go(e){
mouse_x = e.clientX;
mouse_y = e.clientY;
document.getElementById('click1').value = mouse_x;
document.getElementById('click2').value = mouse_y;
}
function draw(){
context.strokeStyle = strokeColor;;
context.fillStyle = color;
context.beginPath();
context.lineWidth = 3;
context.rect(x,y,w,h);
context.fill();
context.stroke();
}
function animate(){
context.clearRect(0,0,width,height);
context.save();
draw();
context.restore();
requestAnimationFrame(animate);
}
animate();
};
</script>
</body>
</html>
This should be easy to do, let's first talk about the logic that we need.
Have a list of items that we can draw.
Make those items have their own state.
Check if when we click we hit an item or not.
Move or toggle the selected item.
Handle Clear & Draw of the canvas.
Made this simple fiddle just to show you the idea behind it,
let context = $("canvas")[0].getContext("2d");
let elements = [
new element(20, 20, 20, 20, "green"),
new element(45, 30, 30, 30, "red"),
new element(80, 40, 50, 50, "blue"),
];
let mousePosition = {
x: 0,
y: 0,
};
let selected;
function element(x, y, height, width, color) {
this.x = x;
this.y = y;
this.height = height;
this.width = width;
this.color = color;
this.selected = false;
this.draw = function(context) {
context.strokeStyle = (this.selected ? "black" : "white");
context.fillStyle = this.color;
context.beginPath();
context.lineWidth = 2;
context.rect(this.x, this.y, this.height, this.width);
context.fill();
context.stroke();
}
this.move = function(x, y) {
this.x = x;
this.y = y;
}
}
//Select Function
function get_select(x, y) {
let found;
$.each(elements, (i, element) => {
if(x > element.x
&& x < element.x + element.width
&& y > element.y
&& y < element.y + element.height) {
found = element;
}
});
return (found);
}
// Handle selection & Movement.
$("canvas").click(function() {
let found = get_select(mousePosition.x, mousePosition.y);
Clear();
// Toggle Selection
if (found && !selected) {
found.selected = true;
selected = found;
} else if (found === selected) {
found.selected = false;
selected = null;
}
// Move
if (!found && selected) {
selected.move(mousePosition.x, mousePosition.y);
}
Draw();
});
// Record mouse position.
$("canvas").mousemove((event) => {
mousePosition.x = event.pageX;
mousePosition.y = event.pageY;
});
//Draw ALL elements.
function Draw() {
$.each(elements, (i, element) => {
element.draw(context);
});
}
function Clear() {
context.clearRect(0, 0, $("canvas")[0].width, $("canvas")[0].height);
}
// Start.
$(document).ready(() => {
Draw();
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<html>
<body>
<canvas></canvas>
</body>
</html>
Hope it helps you out!
I have a game, and in it there is a main, controllable character and then enemies that shoot back at the character. For the enemies, when they shoot back I want them to shoot at intervals so that it isn't just one massive block of bullets, and it worked with a setInterval for one, but when a second enemy comes in they don't shoot. Only one of the two will. If anybody has a solution that would be great!
function enemies() {
if (enemy_soldiers.length == 0) {
level += 0.2;
for (var i = 0; i<(1 + Math.floor(Math.round(level))); i++) {
var gx = 1450
var gy = getRandom(430, 630);
enemy_soldiers.push({
x: gx,
y: gy,
l: gl,
d: getRandom(350, 600),
shooting: false,
interval: setInterval (function() {enemy.shooting = true;},fire_rate),
shoot: function() {
enemy_bullets.push({
x: enemy.x+40,
y: enemy.y+87,
vel: 10,
});
}
});
}
}
var enemy;
gctx.clearRect(0, 0, 1400, 800);
for (var i in enemy_soldiers) {
enemy = enemy_soldiers[i];
drawenemy(enemy.x, enemy.y, enemy.l);
//ai
if (distance(enemy.x, enemy.y, cx, cy) >= enemy.d && enemy.x>cx) {
enemy.x-=vel;
}
else if (distance(enemy.x, enemy.y, cx, cy) >= enemy.d && enemy.x<cx) {
enemy.x+=vel;
}
if (distance(enemy.x, enemy.y, cx, cy) <= 600) {
if (enemy.shooting == true) {
enemy.shoot(enemy.x,enemy.y);
enemy.shooting = false;
}
gbctx.clearRect(0, 0, 1400, 800);
for (var j in enemy_bullets) {
enemy_bullet = enemy_bullets[j];
enemy_bullet.x -= enemy_bullet.vel;
if (enemy_bullet.x > 1400 || enemy_bullet.x < -5 || enemy_bullet.y > 800 || enemy_bullet.y < -5) {
enemy_bullets.splice(j,1);
}
drawEnemyBullet(enemy_bullet.x, enemy_bullet.y);
}
}
}}
This solution relies on the function Date.now() which returns the current time in milliseconds. It's possible for each enemy to keep track of when they have to fire next and each one can have a different delay between shots.
I've only added a graphic to show when they are firing their weapons, but this can be easily altered to something like a raycast or spawn a projectile particle.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
background-color: black;
}
canvas {
position: absolute;
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
border: solid 1px white;
border-radius: 1px;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script type="application/javascript">
// Enemy constructor
function Enemy(x,y) {
this.x = x;
this.y = y;
this.isFiring = false;
this.fireDelay = (500 + Math.random() * 500) | 0; // Time between shots
this.lastFired = Date.now(); // Last time at which the enemy fired
}
/*
Enemy prototype
(All objects created using the constructor share these properties)
E.G.
var e1 = new Enemy(10,0);
var e2 = new Enemy(20,0);
if (e1.__proto__ === e2.__proto__) {
console.log("Match");
}
prints "Match"
*/
Enemy.prototype = {
WIDTH: 10,
HEIGHT: 20,
FIRE_DURATION: 100, // Amount of time 'fire' graphic is shown
FIRE_WIDTH: 10,
FIRE_HEIGHT: 10,
update: function() {
// If current time - time when I last fired > the amount of time between shots
if (Date.now() - this.lastFired > this.fireDelay) {
this.lastFired = Date.now();
this.isFiring = true;
// If you were using projectile particles, this is where you would spawn one
}
if (this.isFiring && Date.now() - this.lastFired > this.FIRE_DURATION) {
this.isFiring = false;
}
},
render: function(ctx) {
ctx.fillStyle = "darkred";
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.rect(
this.x,
this.y,
this.WIDTH,
this.HEIGHT
);
ctx.fill();
ctx.stroke();
if (this.isFiring) {
ctx.fillStyle = "yellow";
ctx.strokeStyle = "darkyellow";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.rect(
this.x + this.WIDTH,
this.y + this.HEIGHT * 0.5 - this.FIRE_HEIGHT * 0.5,
this.FIRE_WIDTH,
this.FIRE_HEIGHT
);
ctx.fill();
ctx.stroke();
}
}
};
var canvasWidth = 180;
var canvasHeight = 160;
var canvas = null;
var ctx = null;
var enemies = [];
window.onload = function() {
canvas = document.getElementById("canvas");
canvas.width = canvasWidth;
canvas.height = canvasHeight;
ctx = canvas.getContext("2d");
for (var i = 0; i < 5; ++i) {
enemies[i] = new Enemy(20 + i * 3,10 + i * 30);
}
loop();
}
function loop() {
// Update
for (var i = 0; i < enemies.length; ++i) {
enemies[i].update();
}
// Render
ctx.fillStyle = "gray";
ctx.fillRect(0,0,canvasWidth,canvasHeight);
for (var i = 0; i < enemies.length; ++i) {
enemies[i].render(ctx);
}
//
requestAnimationFrame(loop);
}
</script>
</body>
</html>
The script below draws an image on the left side of the screen and a selection box in the right. It then attempts to redefine the image drawn on the left on each new selection in the selection box on the right by making the imageID dependent on the selection. However, as you can see below, whatever number you select on the right the image remains the same (1) because whilst it might be redrawn, it is not redefined on selection. What I would like to happen is that on selection in the box on the right the number in the image changes with the selection box such that it always correlates with the selection. In other words, when you click on 2 the image changes to the 2nd image in images. I have found two ways of doing this but they are both flawed:
1: Define img in paint's render function. This works but it makes everything run very slowly and the hover animations on the image stop working as expected.
2: Define img in the makeSelectionInfo function. This also works but the hover animations completely stop working if this is done.
I apologise for the long code but I couldn't condense it any more. For the sake of brevity I have only included images for numbers between 1 & 5. Any help will be appreciated.
var c=document.getElementById('game'),
canvasX=c.offsetLeft,
canvasY=c.offsetTop,
ctx=c.getContext('2d');
images=['https://i.stack.imgur.com/KfN4z.jpg',
'https://i.stack.imgur.com/MyQS1.png',
'https://i.stack.imgur.com/3Vlfj.jpg',
'https://i.stack.imgur.com/u3NLH.jpg',
'https://i.stack.imgur.com/XnLwl.png'];
var curvedRect = function(text, x, y, w, h) {
this.text = text;
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.hovered = false;
this.clicked = false;
}
curvedRect.prototype.makeCurvedRect = function() {
var delta=0, theta=0, yRotation=this.y;
if (this.hovered) {
delta = 3;
shadowColor = '#000000';
shadowBlur = 20;
shadowOffsetX = 5;
shadowOffsetY = 5;
theta = -0.01;
} else {
delta = 0;
theta = 0;
shadowColor = '#9F3A9B';
shadowBlur = 0;
shadowOffsetX = 0;
shadowOffsetY = 0;
}
var x = this.x-delta;
var y = yRotation-delta;
var w = this.w+(2*delta);
var h = this.h+(2*delta);
var img=new Image();
img.src=images[this.text];
ctx.rotate(theta);
ctx.beginPath();
ctx.lineWidth='8';
ctx.strokeStyle='white';
ctx.moveTo(x+10, y);
ctx.lineTo(x+w-10, y);
ctx.quadraticCurveTo(x+w, y, x+w, y+10);
ctx.lineTo(x+w, y+h-10);
ctx.quadraticCurveTo(x+w, y+h, x+w-10, y+h);
ctx.lineTo(x+10, y+h);
ctx.quadraticCurveTo(x, y+h, x, y+h-10);
ctx.lineTo(x, y+10);
ctx.quadraticCurveTo(x, y, x+10, y);
ctx.shadowColor = shadowColor;
ctx.shadowBlur = shadowBlur;
ctx.shadowOffsetX = shadowOffsetX;
ctx.shadowOffsetY = shadowOffsetY;
ctx.stroke();
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.drawImage(img, x+2.5, y+2.5, w-5, h-5);
ctx.rotate(-theta);
}
curvedRect.prototype.hitTest = function(x, y) {
return (x >= this.x) && (x <= (this.w+this.x)) && (y >= this.y) && (y <= (this.h+this.y));
}
var selectionForMenu = function(id, text, y) {
this.id = id;
this.text = text;
this.y = y;
this.hovered = false;
this.clicked = false;
this.lastClicked = false;
}
function makeTextForSelected(text, y) {
ctx.font='bold 12px Noto Sans';
ctx.fillStyle='white';
ctx.textAlign='center';
ctx.fillText(text, 200, y);
}
function makeSelectionInfo(text) {
makeTextForSelected(text, 375);
}
selectionForMenu.prototype.makeSelection = function() {
var fillColor='#A84FA5';
if (this.hovered) {
if (this.clicked) {
if (this.lastClicked) {
fillColor='#E4C7E2';
} else {
fillColor='#D5A9D3';
}
} else if (this.lastClicked) {
fillColor='#D3A4D0';
makeSelectionInfo(this.text);
} else {
fillColor='#BA74B7';
}
} else if (this.lastClicked) {
fillColor='#C78DC5';
makeSelectionInfo(this.text);
} else {
fillColor='#A84FA5';
}
ctx.beginPath();
ctx.fillStyle=fillColor;
ctx.fillRect(400, this.y, 350, 30)
ctx.stroke();
ctx.font='10px Noto Sans';
ctx.fillStyle='white';
ctx.textAlign='left';
ctx.fillText(this.text, 410, this.y+19);
}
selectionForMenu.prototype.hitTest = function(x, y) {
return (x >= 400) && (x <= (750)) && (y >= this.y) && (y <= (this.y+30)) && !((x >= 400) && (y > 450));
}
var Paint = function(element) {
this.element = element;
this.shapes = [];
}
Paint.prototype.addShape = function(shape) {
this.shapes.push(shape);
}
Paint.prototype.render = function() {
ctx.clearRect(0, 0, this.element.width, this.element.height);
for (var i=0; i<this.shapes.length; i++) {
try {
this.shapes[i].makeSelection();
}
catch(err) {}
try {
this.shapes[i].makeCurvedRect();
}
catch(err) {}
}
ctx.beginPath();
ctx.fillStyle='white';
ctx.fillRect(0, 0, 750, 25);
ctx.stroke();
for (var i=0; i<this.shapes.length; i++) {
try {
this.shapes[i].makeBox();
}
catch(err) {}
}
ctx.beginPath();
ctx.fillStyle='#BC77BA';
ctx.fillRect(0, 450, 750, 50);
ctx.stroke();
ctx.font='bold 10px Noto Sans';
ctx.fillStyle='#9F3A9B';
ctx.textAlign='center';
ctx.fillText('Phrase Practice', 365, 17);
for (var i=0; i<this.shapes.length; i++) {
try {
this.shapes[i].makeInteractiveButton();
}
catch(err) {}
}
}
Paint.prototype.setHovered = function(shape) {
for (var i=0; i<this.shapes.length; i++) {
this.shapes[i].hovered = this.shapes[i] == shape;
}
this.render();
}
Paint.prototype.setClicked = function(shape) {
for (var i=0; i<this.shapes.length; i++) {
this.shapes[i].clicked = this.shapes[i] == shape;
}
this.render();
}
Paint.prototype.setUnclicked = function(shape) {
for (var i=0; i<this.shapes.length; i++) {
if (shape.constructor.name==this.shapes[i].constructor.name) {
this.shapes[i].clicked = false;
if (shape instanceof selectionForMenu) {
this.shapes[i].lastClicked = this.shapes[i] == shape;
}
}
}
this.render();
}
Paint.prototype.select = function(x, y) {
for (var i=this.shapes.length-1; i >= 0; i--) {
if (this.shapes[i].hitTest(x, y)) {
return this.shapes[i];
}
}
return null
}
imageID = 0;
var paint = new Paint(c);
var img = new curvedRect(imageID, 112.5, 100, 175, 175);
var selection = [];
for (i=0; i<=30; i++) {
selection.push(new selectionForMenu(i, i, 25+(i*30)));
}
paint.addShape(img);
for (i=0; i<30; i++) {
paint.addShape(selection[i])
}
paint.render();
var clickedShape=0;
var i=0;
function mouseDown(event) {
var x = event.x - canvasX;
var y = event.y - canvasY;
var shape = paint.select(x, y);
if (shape instanceof selectionForMenu) {
imageTextID = shape.id;
if (i==0) {
clickedShape=shape;
i=1;
} else if (i==1) {
i=0;
}
}
paint.setClicked(shape);
}
function mouseUp(event) {
var x = event.x - canvasX;
var y = event.y - canvasY;
var shape = paint.select(x, y);
if (clickedShape instanceof selectionForMenu) {
if (x>400 && y>25 && y<450) {
paint.setUnclicked(shape);
} else if (shape && !(shape instanceof selectionForMenu)) {
paint.setUnclicked(shape);
}
}
}
function mouseMove(event) {
var x = event.x - canvasX;
var y = event.y - canvasY;
var shape = paint.select(x, y);
paint.setHovered(shape);
}
c.addEventListener('mousedown', mouseDown);
c.addEventListener('mouseup', mouseUp);
c.addEventListener('mousemove', mouseMove);
canvas {
z-index: -1;
margin: 1em auto;
border: 1px solid black;
display: block;
background: #9F3A9B;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>uTalk Demo</title>
</head>
<body>
<canvas id="game" width = "750" height = "500"></canvas>
</body>
</html>
Fixed your code here https://jsfiddle.net/0wq0hked/2/
You can diff to see what I changed, but basically you weren't initializing and adding the multiple curvedRect to the Paint.shapes array. I also added the images as an attribute of curvedRect.
I also had to add a visible parameter to your shapes, as your mouse hover Paint.select function was not functioning properly. The way yours works, shapes that share the same (x,y) do not allow other shapes from being hovered even when they are not visible. Thus multiple shapes occupying the image area to the left stopped the hover from working properly. I suppose you could keep your Paint.select and instance/remove shapes when they are to be drawn, but you do not have shape removal functionality as far as I can tell.
Also, you call render on every event, this is a bad idea. Take a look at requestAnimationFrame and try drawing at the screen refresh rate rather than on user input.
I need to figure out how to make a canvas element become clickable as well duplicate the element on the click, with the same properties. I have tried a few tricks with finding the ball via logic and pointing to the radius and coordinates of the ball and creating a clone constructor/prototype but to no avail has it worked! Thank you for the help!
Code below:
<style type="text/css">
body {
margin: 0;
padding: 0;
overflow: hidden;
}
#balling {
border:1px solid rgb(0,0,0);
}
</style>
</head>
<body>
<canvas id="balling" width="500" height="400"></canvas>
<!-- Javascript on the bottom to make page run faster -->
<script type="text/javascript" src="js/lib/jquery.js"></script>
<script type="text/javascript">
var canvas = document.getElementById('balling');
var context = canvas.getContext('2d');
// The Properties of the Circle and Position within the Viewport
var CircleOptions = {
posBall: {
x: 160,
y: 180
},
radius: 40,
startAngle: 0,
endAngle: Math.PI * 2,
anticlockwise: false,
radians: 0,
xMove: Math.random(),
yMove: Math.random(),
speed:2,
angle:80,
velocityX:1,
velocityY:1
};
//Math to make the ball move
function moveBall() {
CircleOptions.radians = CircleOptions.angle * Math.PI/180;
CircleOptions.xMove = Math.cos(CircleOptions.radians) * CircleOptions.speed * CircleOptions.velocityX;
CircleOptions.yMove = Math.sin(CircleOptions.radians) * CircleOptions.speed * CircleOptions.velocityY;
}
//Function to draw the ball
function DrawOptions() {
//Reset Canvas
context.fillStyle = "white";
context.fillRect(0, 0, canvas.width, canvas.height);
//Drawing of the ball
context.fillStyle = "rgb(142, 68, 173)";
context.beginPath();
context.arc(CircleOptions.posBall.x, CircleOptions.posBall.y, CircleOptions.radius, CircleOptions.startAngle, CircleOptions.endAngle, CircleOptions.anticlockwise);
context.closePath();
context.fill();
}
//finding the coordinates of the circle
function CircleCoordinates(CircleOptions) {
var left = CircleOptions.posBall.x - CircleOptions.radius,
top = CircleOptions.posBall.y + CircleOptions.radius,
right = CircleOptions.posBall.x + CircleOptions.radius,
bottom = CircleOptions.posBall.y - CircleOptions.radius;
}
// Animate and call the function to move the ball
setInterval(Move, 20);
//Function call for the ball
moveBall();
//The function to make it move, reset canvas for movement and color/create shape of ball
function Move() {
//Function call for drawing and pinpointing the coordinates
DrawOptions();
CircleCoordinates(CircleOptions);
//Power to make it move
CircleOptions.posBall.x += CircleOptions.xMove;
CircleOptions.posBall.y += CircleOptions.yMove;
//checks for ball hitting the Wall
if(CircleOptions.posBall.x > canvas.width || CircleOptions.posBall.x < 0) {
CircleOptions.angle -= 770;
moveBall();
} else if(CircleOptions.posBall.y > canvas.height || CircleOptions.posBall.y < 0) {
CircleOptions.angle -= 2760;
moveBall();
} else if(CircleOptions.posBall.y == canvas.height || CircleOptions.posBall.y > canvas.width) {
CircleOptions.angle += 90;
moveBall();
}
}
$('#balling').on('click', function(e) {
var clickedX = e.pageX - this.offsetLeft;
var clickedY = e.pageY - this.offsetTop;
if (clickedX > CircleOptions.right && clickedX > CircleOptions.left && clickedY > CircleOptions.top && clickedY < CircleOptions.bottom) {
alert ('clicked number ');
}
});
//Clone prototype and constructor
function Clone() {
Clone.prototype = CircleOptions;
var c = new Clone();
if(CircleOptions.posBall.x < canvas.width || CircleOptions.posBall.y == canvas.height) {
return c;
}
}
//function call for clone
Clone();
</script>
</body>
A link to the fiddle to see the code live: http://jsfiddle.net/coder101/CMW24/
For cloning DOM nodes use cloneNode. For debugging the code use jsfiddle, so the community could help you in spotting the problem with clicks
You had few bugs in your code. The corrected version:
function CircleCoordinates() {
CircleOptions.left = CircleOptions.posBall.x - CircleOptions.radius,
CircleOptions.right = CircleOptions.posBall.x + CircleOptions.radius,
CircleOptions.top = CircleOptions.posBall.y - CircleOptions.radius,
CircleOptions.bottom = CircleOptions.posBall.y + CircleOptions.radius;
}
...
canvas.addEventListener('click', function (e) {
var clickedX = e.pageX - this.offsetLeft;
var clickedY = e.pageY - this.offsetTop;
if (clickedX < CircleOptions.right && clickedX > CircleOptions.left && clickedY > CircleOptions.top && clickedY < CircleOptions.bottom) {
alert('clicked number ');
}
});
(Why do you need jQuery?)