Sequencing of stroke/fill to create a blended overlap - javascript

I have a large array of objects to draw on a canvas.
The center of the objects needs to blend with standard alpha.
The border (stroke) of later objects needs to appear to remove any underlying borders while leaving the fill intact for blending.
An example as a code snippet with a couple of failed attempts - please note the 'desired' outcome was produced manually.
The solution needs to scale too as this is for a requestAnimationFrame and there will be thousands of objects to iterated over so performing individual beginPath()/stroke() combinations isn't likely to be viable.
var canvas = document.getElementById('canvas');
canvas.width = 600;
canvas.height = 600;
var ctx = canvas.getContext('2d');
//set up control objects
let objects = [{
x: 20,
y: 20,
w: 60,
h: 30,
rgba: "rgba(255, 0,0,.5)"
}, {
x: 40,
y: 30,
w: 60,
h: 30,
rgba: "rgba(0,255,0,.5)"
}]
//manually produce desired outcome
ctx.beginPath();
for (let i = 0, l = objects.length; i < l; i++) {
let myObject = objects[i];
ctx.fillStyle = myObject.rgba;
ctx.fillRect(myObject.x, myObject.y, myObject.w, myObject.h);
}
ctx.beginPath();
ctx.moveTo(40, 50);
ctx.lineTo(20, 50);
ctx.lineTo(20, 20);
ctx.lineTo(80, 20);
ctx.lineTo(80, 30);
ctx.rect(40, 30, 60, 30);
ctx.stroke();
ctx.font = "15px Arial"
ctx.fillStyle = "black";
ctx.fillText("Desired outcome - (done manually for example)", 120, 50);
//Attempt one: fill on iterate, stroke on end
ctx.beginPath();
for (let i = 0, l = objects.length; i < l; i++) {
let myObject = objects[i];
ctx.rect(myObject.x, myObject.y + 70, myObject.w, myObject.h);
ctx.fillStyle = myObject.rgba;
ctx.fillRect(myObject.x, myObject.y + 70, myObject.w, myObject.h);
}
ctx.stroke();
ctx.fillStyle = "black";
ctx.fillText("Attempt #1: inner corner of red box fully visible", 120, 120);
//Attempt two: fill and stroke on iterate
for (let i = 0, l = objects.length; i < l; i++) {
let myObject = objects[i];
ctx.beginPath();
ctx.rect(myObject.x, myObject.y + 140, myObject.w, myObject.h);
ctx.fillStyle = myObject.rgba;
ctx.fillRect(myObject.x, myObject.y + 140, myObject.w, myObject.h);
ctx.stroke();
}
ctx.fillStyle = "black";
ctx.fillText("Attempt #2: inner corner of red box partly visible", 120, 170);
ctx.fillText("(This also scales very badly into thousands of strokes)", 120, 190);
<canvas name="canvas" id="canvas" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>

You can achieve this by drawing in two passes:
First you will composite your strokes, by iteratively
clearing each previous strokes where the filling would fall, using globalCompositeOperation = "destination-out"
draw this rect's stroke
When this is done, your canvas will only have the final strokes remaining.
Now you have to draw the fills, but since we want the strokes to be in front of the fills, we have to use an other compositing mode: "destination-over" and to iterate our rects in reversed order:
(async () => {
var canvas = document.getElementById('canvas');
canvas.width = 600;
canvas.height = 600;
var ctx = canvas.getContext('2d');
ctx.scale(2,2)
//set up control objects
let objects = [{
x: 20,
y: 20,
w: 60,
h: 30,
rgba: "rgba(255, 0,0,.5)"
}, {
x: 40,
y: 30,
w: 60,
h: 30,
rgba: "rgba(0,255,0,.5)"
},
{
x: 10,
y: 5,
w: 60,
h: 30,
rgba: "rgba(0,0,255,.5)"
}]
// first pass, composite the strokes
for (let i = 0, l = objects.length; i < l; i++) {
let myObject = objects[i];
ctx.beginPath();
ctx.rect(myObject.x, myObject.y, myObject.w, myObject.h);
// erase the previous strokes where our fill will be
ctx.globalCompositeOperation = "destination-out";
ctx.fillStyle = "#000"; // must be opaque
ctx.fill();
// draw our stroke
ctx.globalCompositeOperation = "source-over";
ctx.stroke();
}
await wait(1000);
// second pass, draw the colored fills
// we will draw from behind to keep the stroke at frontmost
// so we need to iterate our objects in reverse order
for (let i = objects.length- 1; i >= 0; i--) {
let myObject = objects[i];
// draw behind
ctx.globalCompositeOperation = "destination-over";
ctx.fillStyle = myObject.rgba;
ctx.fillRect(myObject.x, myObject.y, myObject.w, myObject.h);
}
})();
function wait(ms){
return new Promise(res => setTimeout(res, ms));
}
<canvas name="canvas" id="canvas" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
And of course, this can be used in an animation too:
var canvas = document.getElementById('canvas');
canvas.width = 100;
canvas.height = 100;
var ctx = canvas.getContext('2d');
//set up control objects
let objects = [{
x: 20,
y: 20,
w: 60,
h: 30,
rgba: "rgba(255, 0,0,.5)"
}, {
x: 40,
y: 30,
w: 60,
h: 30,
rgba: "rgba(0,255,0,.5)"
},
{
x: 10,
y: 5,
w: 60,
h: 30,
rgba: "rgba(0,0,255,.5)"
}]
objects.forEach( rect => {
rect.speedX = Math.random() * 2 - 1;
rect.speedY = Math.random() * 2 - 1;
});
requestAnimationFrame(anim);
onclick = anim
function anim() {
update();
draw();
requestAnimationFrame( anim );
}
function update() {
objects.forEach( rect => {
rect.x = rect.x + rect.speedX;
rect.y = rect.y + rect.speedY;
if(
rect.x + rect.w > canvas.width ||
rect.x < 0
) {
rect.speedX *= -1;
}
if(
rect.y + rect.h > canvas.height ||
rect.y < 0
) {
rect.speedY *= -1;
}
});
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// first pass, composite the strokes
for (let i = 0, l = objects.length; i < l; i++) {
let myObject = objects[i];
ctx.beginPath();
ctx.rect(myObject.x, myObject.y, myObject.w, myObject.h);
// erase the previous strokes where our fill will be
ctx.globalCompositeOperation = "destination-out";
ctx.fillStyle = "#000"; // must be opaque
ctx.fill();
// draw our stroke
ctx.globalCompositeOperation = "source-over";
ctx.stroke();
}
// second pass, draw the colored fills
// we will draw from behind to keep the stroke at frontmost
// so we need to iterate our objects in reverse order
for (let i = objects.length- 1; i >= 0; i--) {
let myObject = objects[i];
// draw behind
ctx.globalCompositeOperation = "destination-over";
ctx.fillStyle = myObject.rgba;
ctx.fillRect(myObject.x, myObject.y, myObject.w, myObject.h);
}
ctx.globalCompositeOperation = "source-over";
}
<canvas name="canvas" id="canvas" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>

Related

How to add element with circular motion on HTML5 canvas using JavaScript that provides function when something goes near it?

Here is my current HTML file
<!DOCTYPE html>
<html>
<head>
<title>Space Ship</title>
<style type="text/css">
canvas{
border: 1px solid black;
}
body{
margin: 0;
}
</style>
</head>
<body>
<canvas id="game"></canvas>
<script src="game.js"></script>
</body>
</html>
And here is my current JS file which is where I make all the changes to the canvas
(function () {
const canvas = document.getElementById('game');
const context = canvas.getContext('2d');
const SPACESHIP_SIZE = { width: 15, height: 25 };
const SPACESHIP_POSITION = { x: window.innerWidth/2, y: window.innerHeight/2};
const GRAVITY = 0;
//Update thrust constant
const THRUST = 15;
class SpaceShip {
constructor(size, position) {
this.color = 'yellow';
this.size = size;
this.position = position;
this.angle = 0;
this.engineOn = false;
this.rotatingLeft = false;
this.rotatingRight = false;
this.velocity = {
x: 0,
y: 0,
};
}
draw() {
const triangleCenterX = this.position.x + 0.5 * this.size.width;
const triangleCenterY = this.position.y + 0.5 * this.size.height;
context.save();
context.translate(triangleCenterX, triangleCenterY);
context.rotate(this.angle);
context.lineWidth = 5;
context.beginPath();
// Triangle
context.moveTo(0, -this.size.height / 2);
context.lineTo(-this.size.width / 2, this.size.height / 2);
context.lineTo(this.size.width / 2, this.size.height / 2);
context.closePath();
context.strokeStyle = this.color;
context.stroke();
context.fillStyle = "red";
context.fill();
// Flame for engine
if (this.engineOn) {
const fireYPos = this.size.height / 2 + 4;
const fireXPos = this.size.width * 0.25;
context.beginPath();
context.moveTo(-fireXPos, fireYPos);
context.lineTo(fireXPos, fireYPos);
context.lineTo(0, fireYPos + Math.random() * 100);
context.lineTo(-fireXPos, fireYPos);
context.closePath();
context.fillStyle = 'orange';
context.fill();
}
context.restore();
}
moveSpaceShip() {
// Angle has to be in radians
const degToRad = Math.PI / 180;
// Change the position based on velocity
this.position.x += this.velocity.x;
this.position.y += this.velocity.y;
// Move spaceship to other side when leaving screen
this.position.x = (canvas.width + this.position.x) % canvas.width;
this.position.y = (canvas.height + this.position.y) % canvas.height;
/*
Adding floating point numbers to the end of the
rotaion handling to make roation faster
*/
if (this.rotatingLeft) this.angle -= (degToRad+.1);
if (this.rotatingRight) this.angle += (degToRad+.1);
// Acceleration
if (this.engineOn) {
this.velocity.x += (THRUST / 100) * Math.sin(this.angle);
this.velocity.y -= (THRUST / 100) * Math.cos(this.angle);
}
// Update the velocity depending on gravity
this.velocity.y += GRAVITY / 2500;
}
}
const spaceShip = new SpaceShip(SPACESHIP_SIZE, SPACESHIP_POSITION);
function handleKeyInput(event) {
const { keyCode, type } = event;
const isKeyDown = type === 'keydown' ? true : false;
if (keyCode === 37) spaceShip.rotatingLeft = isKeyDown;
if (keyCode === 39) spaceShip.rotatingRight = isKeyDown;
if (keyCode === 38) spaceShip.engineOn = isKeyDown;
}
function draw() {
console.log('drawing');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// Clear screen
context.fillStyle = 'rgb(0, 10, 60)';
context.fillRect(0, 0, window.innerWidth, canvas.height);
context.font = "30px Arial";
context.fillText("Hello World", window.innerWidth, window.innerHeight);
//Loading small stars
for (var i = 1; i<window.innerWidth; i+=100){
for(var j = 1; j<window.innerHeight; j+=100){
context.beginPath();
context.arc(i, j, 1, 0, Math.PI*2, false);
context.fillStyle = 'white';
context.fill();
context.stroke();
}
}
//loading medium stars
for (var i = 1; i<window.innerWidth; i+=150){
for(var j = 1; j<window.innerHeight; j+=150){
context.beginPath();
context.arc(i, j, 2, 0, Math.PI*2, false);
context.fillStyle = 'white';
context.fill();
context.stroke();
}
}
//loading larger stars
for (var i = 1; i<window.innerWidth; i+=225){
for(var j = 1; j<window.innerHeight; j+=225){
context.beginPath();
context.arc(i, j, 3, 0, Math.PI*2, false);
context.fillStyle = 'white';
context.fill();
context.stroke();
}
}
spaceShip.moveSpaceShip();
// Begin drawing
spaceShip.draw();
// Repeats
requestAnimationFrame(draw);
}
// Event Listeners
document.addEventListener('keydown', handleKeyInput);
document.addEventListener('keyup', handleKeyInput);
// Start the game
draw();
})();
The output works and looks like this...
Little rocket that flies around in space and it works perfectly fine
Now the issue I have is that I want to add elements that represent black holes which spin in a circular motion. When the spaceship which is controlled by the arrow keys hovers over one of these spinning "black hole" like objects, I want to prompt the user to click "enter" in order to go to a new area and space which means they will be redirected to a new canvas (which will just be a different version of the one you see here.
I have no ideas on how to go about adding one of these objects onto the canvas and then give it this function. Does anyone know how to?
Im sure its not as difficult as i'm making it seem, I just don't have that much experience using JavaScript and an HTML5 canvas. Thanks.
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth - 4;
canvas.height = window.innerHeight - 4;
ctx.font = "30px Arial";
ctx.shadowColor = "rgba(255,255,255,.6)";
// Constants in objects doesnt work cause objects passing as reference and will by modified!
// If you want constant constant, use primitives
const SPACESHIP_SIZE = { width: 15, height: 25 };
const SPACESHIP_POSITION = { x: window.innerWidth/2, y: window.innerHeight/2};
const GRAVITY = 0;
const HOVER_TICKS = 80;
//Update thrust constant
const THRUST = 15;
const Systems = {
'main': {
holes:[
{x: 100, y: 350, size: 20, dest: 'J204853'},
{x: 550, y: 50, size: 20, dest: 'J111000'},
{x: 400, y: 150, size: 30, dest: 'J235456'},
{x: 300, y: 100, size: 20, dest: 'Jita'},
{x: 200, y: 400, size: 10, dest: 'Amarr'},
{x: 450, y: 300, size: 20, dest: 'Thera'},
]
},
'J204853': {holes:[{x: 550, y: 50, size: 30, dest: 'main'}]},
'J235456': {holes:[{x: 350, y: 70, size: 20, dest: 'main'}]},
'J111000': {holes:[{x: 150, y: 150, size: 10, dest: 'main'}]},
'Amarr': {holes:[{x: 350, y: 270, size: 30, dest: 'main'}]},
'Thera': {holes:[{x: 250, y: 170, size: 40, dest: 'main'}]},
'Jita': {holes:[{x: 450, y: 250, size: 20, dest: 'main'}]},
};
let spaceShip;
let currentSystem = 'main';
const spaceObjects = [];
class SpaceObject {
constructor(size, position, color = 'black', angle = 0) {
this.color = color;
this.size = size;
this.position = position;
this.angle = angle;
spaceObjects.push(this);
}
tick() {
this.update();
this.draw();
}
update() {}
draw() {}
isAbove({x, y}) {
return Math.abs(this.position.x - x) < this.size && Math.abs(this.position.y - y) < this.size;
}
destroy() {
spaceObjects.splice(spaceObjects.indexOf(this), 1);
}
}
class SpaceShip extends SpaceObject {
constructor(size, position) {
super(size, position, 'yellow');
this.aboveHole = 0;
this.engineOn = false;
this.rotatingLeft = false;
this.rotatingRight = false;
this.velocity = {x: 0, y: 0};
}
draw() {
const triangleCenterX = this.position.x + 0.5 * this.size.width;
const triangleCenterY = this.position.y + 0.5 * this.size.height;
ctx.shadowBlur = 0;
ctx.save();
ctx.translate(triangleCenterX, triangleCenterY);
ctx.rotate(this.angle);
ctx.lineWidth = 5;
ctx.beginPath();
// Triangle
ctx.moveTo(0, -this.size.height / 2);
ctx.lineTo(-this.size.width / 2, this.size.height / 2);
ctx.lineTo(this.size.width / 2, this.size.height / 2);
ctx.closePath();
ctx.strokeStyle = this.color;
ctx.stroke();
ctx.fillStyle = "red";
ctx.fill();
// Flame for engine
if (this.engineOn) {
const fireYPos = this.size.height / 2 + 4;
const fireXPos = this.size.width * 0.25;
ctx.beginPath();
ctx.moveTo(-fireXPos, fireYPos);
ctx.lineTo(fireXPos, fireYPos);
ctx.lineTo(0, fireYPos + Math.random() * 100);
ctx.lineTo(-fireXPos, fireYPos);
ctx.closePath();
ctx.fillStyle = 'orange';
ctx.fill();
}
ctx.restore();
}
update() {
this.moveSpaceShip();
this.checkAboveHole();
}
moveSpaceShip() {
// Angle has to be in radians
const degToRad = Math.PI / 180;
// Change the position based on velocity
this.position.x += this.velocity.x;
this.position.y += this.velocity.y;
// Move spaceship to other side when leaving screen
this.position.x = (canvas.width + this.position.x) % canvas.width;
this.position.y = (canvas.height + this.position.y) % canvas.height;
/*
Adding floating point numbers to the end of the
rotaion handling to make roation faster
*/
if (this.rotatingLeft) this.angle -= (degToRad+.1);
if (this.rotatingRight) this.angle += (degToRad+.1);
// Acceleration
if (this.engineOn) {
this.velocity.x += (THRUST / 100) * Math.sin(this.angle);
this.velocity.y -= (THRUST / 100) * Math.cos(this.angle);
}
// Update the velocity depending on gravity
this.velocity.y += GRAVITY / 2500;
}
checkAboveHole() {
const hole = spaceObjects.find(spaceObject => spaceObject !== this && spaceObject.isAbove(this.position));
if(hole) {
this.aboveHole++;
if(this.aboveHole > HOVER_TICKS) {
confirm(`Jump to system ${hole.dest}?`) && jump(hole);
this.aboveHole = 0;
}
} else {
this.aboveHole = 0;
}
}
}
const circle = (ctx, x, y, radius, color = 'white') => {
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI*2, false);
ctx.fillStyle = color;
ctx.fill();
ctx.stroke();
ctx.closePath();
};
class BlackHole extends SpaceObject {
constructor(size, position, dest) {
super(size, position);
this.dest = dest;
}
update() {
// Spin?
this.angle+=.01;
}
draw() {
// Shadow
ctx.shadowBlur = this.size >>> 2;
circle(ctx, this.position.x, this.position.y, this.size + 1, `rgba(255, 255, 255, .6)`);
// Hole
circle(ctx, this.position.x, this.position.y, this.size, this.color);
// Spinning view
circle(ctx, this.position.x + (this.size * Math.sin(this.angle) - 1), this.position.y + (this.size * Math.cos(this.angle) - 1), 2, 'gray');
circle(ctx, this.position.x - (this.size * Math.sin(this.angle) - 1), this.position.y - (this.size * Math.cos(this.angle) - 1), 2, 'gray');
}
}
function handleKeyInput(event) {
const { keyCode, type } = event;
const isKeyDown = type === 'keydown' ? true : false;
if (keyCode === 37) spaceShip.rotatingLeft = isKeyDown;
if (keyCode === 39) spaceShip.rotatingRight = isKeyDown;
if (keyCode === 38) spaceShip.engineOn = isKeyDown;
}
function jump({dest}) {
currentSystem = dest || 'main';
while(spaceObjects.length) spaceObjects[0].destroy();
Systems[currentSystem].holes.forEach(hole => new BlackHole(hole.size, {x: hole.x, y: hole.y}, hole.dest));
spaceShip = new SpaceShip(SPACESHIP_SIZE, SPACESHIP_POSITION);
}
function draw() {
// Clear screen
ctx.fillStyle = 'rgb(0, 10, 60)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'rgb(150, 150, 150)';
ctx.fillText(`You are in system ${currentSystem}`, 40, 40);
//Loading small stars
ctx.shadowBlur = 1;
for (var i = 1, j = 1; j<canvas.height; i+=100, i > canvas.width && (i=1, j+=100), circle(ctx, i, j, 1));
//loading medium stars
ctx.shadowBlur = 2;
for (var i = 1, j = 1; j<canvas.height; i+=150, i > canvas.width && (i=1, j+=150), circle(ctx, i, j, 2));
//loading larger stars
ctx.shadowBlur = 3;
for (var i = 1, j = 1; j<canvas.height; i+=225, i > canvas.width && (i=1, j+=225), circle(ctx, i, j, 3));
// tick all objects
spaceObjects.forEach(spaceObject => spaceObject.tick());
// Repeats
requestAnimationFrame(draw);
}
// Event Listeners
document.addEventListener('keydown', handleKeyInput);
document.addEventListener('keyup', handleKeyInput);
// Start the game
jump({dest: 'main'});
draw();
<!DOCTYPE html>
<html>
<head>
<title>Space Ship</title>
<style type="text/css">
canvas{
border: 1px solid black;
}
body{
margin: 0;
}
</style>
</head>
<body>
<canvas id="game"></canvas>
</body>
</html>

How do I emulate Gravity in a canvas with a Rectangle or Arc?

How do I emulate Gravity with canvas Objects having only a few variables to work with.
I created the base canvas, game, time, score, everything, even gameStates, but I'm stuck on the part where you "add the velocity and the gravity factor into the Player Y variables".
I tried multiplying the gravity factor by a specified value, and then adding it to the yVel and then adding that to the actual Y value, but I cant translate the positioning correctly.
I think if I figured out how to "create gravity" creating jumping, moving right, and moving left wouldnt be too difficult.
Heres the main code im using to look for the platforms:
map.plates represents an array full of arrays which each contain 4 values for a plate (platform plate)
e is the map.plates.Arrays. playY is basically the player's exactly Y Height,, all rendered into fillRect();
function detectGravity() {
map.plates.forEach(e => {
if (playY > e[1] && playX <= e[0] && playX >= e[0] + e[2]) {
} else {
playY += 0; // Gravity Calculations here
}
});
}
I dont really know if I should include anything else here, but if you want the whole project, see the snippet below.
If theres anything wrong with the question, please tell me, I havent been on here in nearly half a year.
Full code incase codepen dies (suggested in comments):
"esversion: 6";
const can = document.querySelector(".block"),
ctx = can.getContext("2d"),
mScore = 100,
map = {
plates: [
[25, 25, 25, 2],
[75, 25, 25, 2],
[125, 25, 25, 2],
[175, 25, 25, 2],
[225, 25, 25, 2],
[25, 75, 25, 2],
[75, 62, 25, 2],
[125, 50, 25, 2],
[175, 38, 25, 2],
[25, 87, 25, 2],
[75, 100, 25, 2]
],
moneys: [
[25, 25],
[125, 25],
[225, 25],
[75, 62],
[75, 100]
],
player: [25, 25, 2, 2],
badSpt: []
};
let score = 0,
time = 60,
gameOn = 0;
let playX,
playY,
velX,
velY,
grav = 1.05;
can.addEventListener("click", startGame);
function startGame() {
if (gameOn != 1) {
gameOn = 1;
init();
gameTime = setInterval(() => {
if (time != 0) {
time -= 1;
}
}, 1000);
}
}
function init() {
can.width = 300;
can.height = 300;
drawEnviron();
drawLevel();
drawPlayer();
drawGame();
drawPixels();
if (time == 0) {
clearInterval(gameTime);
time = 60;
gameOn = 2;
}
window.requestAnimationFrame(init);
}
function drawEnviron() {
with (ctx) {
fillStyle = "#000000";
fillRect(0, 0, can.width, can.height);
fillStyle = "rgba(255, 255, 255, 0.5)";
fillRect(0, 0, can.width, can.height);
fillStyle = "#000000";
fillRect(0, 0, can.width, can.height / 15);
fillStyle = "#ffffff";
font = can.height / 15 + "px Verdana";
fillText("Score: " + score + "/" + mScore, 1, can.height / 19);
fillText("Time: " + time, can.width / 1.5 + 6, can.height / 19);
}
}
function drawLevel() {
map.plates.forEach(e => {
ctx.fillStyle = "#ffffff";
ctx.fillRect(e[0], can.height / 15 + e[1], e[2], e[3]);
});
map.moneys.forEach(e => {
ctx.beginPath();
ctx.fillStyle = "#fcba03";
ctx.arc(e[0] + 12.5, e[1] + 12.5, 4, 0, 2 * Math.PI);
ctx.fill();
});
}
function detectGravity() {
map.plates.forEach(e => {
if (playY > e[1] && playX <= e[0] && playX >= e[0] + e[2]) {
} else {
playY += 0;
}
});
}
function drawPlayer() {
const a = map.player;
if (gameOn == 0 || gameOn == 2) {
playX = a[0];
playY = a[1];
velX = 0;
velY = 0;
}
ctx.beginPath();
ctx.fillStyle = "#ff0000";
ctx.arc(playX + 12.5, playY + 12.5, 4, 0, 2 * Math.PI);
ctx.fill();
}
function drawGame() {
if (gameOn == 0) {
can.style.animation = "none";
with (ctx) {
fillStyle = "rgba(0, 0, 0, 0.5)";
fillRect(0, 0, can.width, can.height);
strokeStyle = "#000000";
lineWidth = 5;
fillStyle = "#ffffff";
textAlign = "center";
strokeText("Click to Start", 150, can.height / 4);
fillText("Click to Start", 150, can.height / 4);
}
} else if (gameOn == 2) {
can.style.animation = "0.2s flash infinite";
with (ctx) {
fillStyle = "rgba(0, 0, 0, 0.5)";
fillRect(0, 0, can.width, can.height);
strokeStyle = "#000000";
lineWidth = 5;
fillStyle = "#ff0000";
textAlign = "center";
strokeText("-- Game Over --", 150, can.height / 4);
fillText("-- Game Over --", 150, can.height / 4);
}
} else {
can.style.animation = "none";
}
}
function drawPixels() {
var fw = (can.width / 2) | 0,
fh = (can.height / 2) | 0;
ctx.imageSmoothingEnabled = ctx.mozImageSmoothingEnabled = ctx.msImageSmoothingEnabled = ctx.webkitImageSmoothingEnabled = false;
ctx.drawImage(can, 0, 0, fw, fh);
ctx.drawImage(can, 0, 0, fw, fh, 0, 0, can.width, can.height);
}
init();
* {
box-sizing: border-box;
overflow: hidden;
}
.block {
border: 2px solid black;
}
#keyframes flash {
0%, 100% {
border: 2px solid black;
}
50% {
border: 2px solid red;
}
}
<canvas class="block"></canvas>
Simple step based gravity.
Gravity
Gravity manifests as a change in speed over time (acceleration). It has a direction and magnitude (a vector)
We define the gravity vector going down the canvas
const gravity = {x: 0, y: 1};
Normally we apply gravity over time units of seconds. This is not a handy unit for animation. In this case we can define it as pixels per frame. A frame is 1/60th of a second. Thus the gravity defined above having a magnitude of 1 pixel per tick squared. So in one second an object would be traveling at 60 pixels per tick or 3600 pixels per second.
This is a little too fast for most animations so we can slow it down somewhat
const gravity = {x: 0, y: 0.1};
The object
An object has a position (a coordinate) and a velocity (a vector) having a direction and magnitude.
const object = {
pos: {x: 0, y: 0}, // position
vel: {x, 0, y: 0}, // velocity
}
To simulate gravity on this object we can add a behavior in the form of a function. In this case we can call it update. In the update function we accelerate the object, by adding the gravity vector to the velocity vector (object.vel). Then we update the position by adding the velocity vector object.vel to the position coordinate object.pos
const gravity = {x: 0, y: 0.1};
const object = {
pos: {x: 0, y: 0}, // position
vel: {x, 0, y: 0}, // velocity
update() {
this.vel.x += gravity.x;
this.vel.y += gravity.y;
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
}
}
The world
By its self this object will fall forever so we need to have it interact with the world. We can define a ground line. In its most basic a line at a y position on the canvas.
const ground = ctx.canvas.height; // ground at bottom of canvas.
To interact we need to add to the objects update function. In this we check the object position against the ground. If the position is below the ground, we move the position up away from the ground the same distance it has moved into the ground, and reverse the velocity (bounced).
We can define the springyness of the ground as a fraction of the velocity.
We also need to give the object a size.
Thus we get.
const gravity = {x: 0, y: 0.1};
const ground = ctx.canvas.height; // ground at bottom of canvas.
const bounce = 0.5;
const object = {
pos: {x: 0, y: 0}, // position
vel: {x, 0, y: 0}, // velocity
size: {w: 10, h: 10},
update() {
this.vel.x += gravity.x;
this.vel.y += gravity.y;
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
const g = ground - this.size.h; // adjust for size
if(this.pos.y >= g) {
this.pos.y = g - (this.pos.y - g); //
this.vel.y = -Math.abs(this.vel.y) * bounce; // change velocity to moving away.
}
}
}
Then all that is needed is to call update every frame and draw the object at the correct position.
Demo
Putting it into practice.
A simple box called object falls from the top of the canvas and hits the ground (bottom of the canvas) bounces a bit and stop. (Click to reset)
Update: I forgot to check if the object is at rest.
The math will have the box vibrate and never really stop moving if we don't add a little extra code to update.
The box will now appear to come to a complete stop when its bounce is less than gravity. See comment // check for rest.
const ctx = canvas.getContext("2d");
canvas.width = innerWidth-4;
canvas.height = innerHeight-4;
requestAnimationFrame(mainLoop); // starts the animation
const gravity = {x: 0, y: 0.1};
const ground = ctx.canvas.height; // ground at bottom of canvas.
const bounce = 0.9; // very bouncy
const object = {
pos: {x: ctx.canvas.width / 2, y: 0}, // position halfway on canvas
vel: {x: 0, y: 0}, // velocity
size: {w: 10, h: 10},
update() {
this.vel.x += gravity.x;
this.vel.y += gravity.y;
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
const g = ground - this.size.h; // adjust for size
if(this.pos.y >= g) {
this.pos.y = g - (this.pos.y - g); //
this.vel.y = -Math.abs(this.vel.y) * bounce;
if (this.vel.y >= -gravity.y) { // check for rest.
this.vel.y = 0;
this.pos.y = g - gravity.y;
}
}
},
draw() { ctx.fillRect(this.pos.x, this.pos.y, this.size.w, this.size.h) },
reset() { this.pos.y = this.vel.y = this.vel.x = 0 },
}
function mainLoop() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
object.update(); // move object
object.draw();
requestAnimationFrame(mainLoop);
}
canvas.addEventListener("click", object.reset.bind(object));
body {
margin: 0px;
padding: 0px;
}
canvas {
position: absolute;
top: 0px;
left: 0px;
border: 1px solid black;
}
<canvas id="canvas"></canvas>

Use image to fill in arc in canvas using javascript

So I am totally new to canvas and trying a project in which I need to make small balls move around with their background as images. Following code is what I am trying right now.
ctx.beginPath();
ctx.arc(
this.pos[0], this.pos[1], this.radius, 0, 2 * Math.PI, true
);
let tempCanvas = document.createElement("canvas"),
tCtx = tempCanvas.getContext("2d");
let ballbackground = new Image();
if (this.color === "green") {
ballbackground.src = "https://s26.postimg.cc/fl2vwj1mh/greenball.png";
}
else if (this.color === "yellow") {
ballbackground.src = "https://s26.postimg.cc/if61a18yh/yellowball.png";
}
else if (this.color === "blue") {
ballbackground.src = "https://s26.postimg.cc/xb4khn7ih/blueball.jpg";
}
tempCanvas.width = 50;
tempCanvas.height = 50;
tCtx.drawImage(ballbackground,0,0,ballbackground.width, ballbackground.height,0,0,50,50);
ctx.fillStyle = ctx.createPattern(tempCanvas, "repeat");
And for moving those balls I do as follows:
const velocityScale = timeDelta / NORMAL_FRAME_TIME_DELTA,
offsetX = this.vel[0] * velocityScale * this.speed,
offsetY = this.vel[1] * velocityScale * this.speed;
this.pos = [this.pos[0] + offsetX, this.pos[1] + offsetY];
However, the problem is when objects move they seem like sliding over background image like so:
If I try "no-repeat" with createPattern, the balls won't display at all.
What I want is those balls with background images moving on the canvas?
move the balls by using the canvas transform?
const ctx = document.querySelector("canvas").getContext("2d");
const pattern = createPattern(ctx);
function drawCircleByPosition(ctx, x, y) {
ctx.beginPath();
ctx.arc(x, y, 50, 0, Math.PI * 2, true);
ctx.fill();
}
function drawCircleByTransform(ctx, x, y) {
ctx.save();
ctx.translate(x, y);
ctx.beginPath();
ctx.arc(0, 0, 50, 0, Math.PI * 2, true);
ctx.fill();
ctx.restore();
}
function render(time) {
time *= 0.001;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.fillStyle = pattern;
drawCircleByPosition(ctx, 90, 75 + Math.sin(time) * 50);
drawCircleByTransform(ctx, 210, 75 + Math.sin(time) * 50);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
function createPattern(ctx) {
const tCtx = document.createElement("canvas").getContext("2d");
tCtx.canvas.width = 50;
tCtx.canvas.height = 50;
tCtx.fillStyle = "yellow";
tCtx.fillRect(0, 0, 50, 50);
for (let x = 0; x < 50; x += 20) {
tCtx.fillStyle = "red";
tCtx.fillRect(x, 0, 10, 50);
tCtx.fillStyle = "blue";
tCtx.fillRect(0, x, 50, 10);
}
return ctx.createPattern(tCtx.canvas, "repeat");
}
canvas { border: 1px solid black; }
<canvas></canvas>
note that rather than call save and restore you can also just set the transform with setTransform which is probably faster since save and restore saves all state (fillStyle, strokeStyle, font, globalCompositeOperation, lineWidth, etc...).
You can either pass in your own matrix. Example
ctx.setTransform(1, 0, 0, 1, x, y); // for translation
and/or you can reset it to the default whenever and then use the standard transform manipulation functions
ctx.setTransform(1, 0, 0, 1, 0, 0); // the default
ctx.translate(x, y);
or whatever combination of things you want to do.

Image filling in polygon using Raphael js or any another js

I am facing the issue on filling the background image within the polygon selected at roof top.
I have successfully created the polygon, now I want that as soon as the image is selected from the number of slides present, that image should get filled within the selected polygon.
I am using Raphael js for doing the same, if possible with any other js then please advise.
Below is the code for testing purpose:
// Creates canvas 320 × 200 at 10, 50
var paper = Raphael(10, 50, 320, 200);
//draw triangle
var t = paper.path("M0 0L250 0L100 100L 0");
// Sets the fill attribute of the circle to red (#f00)
t.attr("fill", "url('http://interlock.renoworks.com/en/data/exterior/Slate/~Interlock-01-SlateRoofing/~swatch1-400.jpg')");
// Sets the stroke attribute of the circle to white
t.attr("stroke", "#f00");
Here is the demo url: http://jsfiddle.net/hxez863d/5/
You can clip a path in HTML5 without a library. Draw on the canvas after to put things in the clip region.
var can = document.getElementById('can');
var ctx = can.getContext('2d');
var noise = makeNoise(300,200);
var squares = makeSquares(300, 200, 10, "#CCCCCC", "#999999");
// Draw background image.
ctx.drawImage(noise, 0, 0);
ctx.save();
//var paper = Raphael(10, 50, 320, 200);
ctx.translate(10, 50);
ctx.save();
//draw triangle
//var t = paper.path("M0 0L250 0L100 100L 0");
clipPath(ctx, [
[0,0],
[250, 0],
[100, 100],
[0, 0]
]);
// Draw with clip.
ctx.drawImage(squares, 0, 0);
ctx.fillRect(90, 70, 30, 30);
ctx.restore(); // <-- removes clip
// Draw without clip.
ctx.fillStyle = "red";
ctx.fillRect(100, 80, 30, 30);
ctx.restore(); // <-- removes translate
function clipPath(ctx, coords) {
ctx.beginPath();
ctx.moveTo(coords[0][0], coords[0][1]);
for (var i = 1; i < coords.length; i++) {
ctx.lineTo(coords[i][0], coords[i][1]);
}
ctx.clip();
}
function makeNoise(w, h) {
var can = document.createElement('canvas');
can.width = w;
can.height = h;
var ctx = can.getContext('2d');
var imageData = ctx.getImageData(0, 0, w, h);
var d = imageData.data;
var len = d.length;
for (var i = 0; i < len; i+=4) {
d[i] = d[i+1] = d[i+2] = Math.floor(Math.random() * 256);
d[i+3] = 255;
}
ctx.putImageData(imageData, 0, 0);
return can;
}
function makeSquares(w, h, size, color1, color2) {
var can = document.createElement('canvas');
var ctx = can.getContext('2d');
can.width = w;
can.height = h;
for (var y = 0; y < h; y+= size) {
for (var x = 0; x < w; x += size*2) {
ctx.fillStyle = color1;
ctx.fillRect(x, y, size, size);
ctx.fillStyle = color2;
ctx.fillRect(x + size, y, size, size);
}
var temp = color1;
color1 = color2;
color2 = temp;
}
return can;
}
<canvas id="can" width="300" height="200"></canvas>

Update HTML5 canvas rectangle on hover?

I've got some code which draws a rectangle on a canvas, but I want that rectangle to change color when I hover the mouse over it.
The problem is after I've drawn the rectangle I'm not sure how I select it again to make the adjustment.
What I want to do:
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.rect(20,20,150,100);
ctx.stroke();
$('c.[rectangle]').hover(function(this){
this.fillStyle = 'red';
this.fill();
});
You can't do this out-of-the-box with canvas. Canvas is just a bitmap, so the hover logic has to be implemented manually.
Here is how:
Store all the rectangles you want as simple object
For each mouse move on the canvas element:
Get mouse position
Iterate through the list of objects
use isPointInPath() to detect a "hover"
Redraw both states
Example
var canvas = document.querySelector("canvas"),
ctx = canvas.getContext("2d"),
rects = [
{x: 10, y: 10, w: 200, h: 50},
{x: 50, y: 70, w: 150, h: 30} // etc.
], i = 0, r;
// render initial rects.
while(r = rects[i++]) ctx.rect(r.x, r.y, r.w, r.h);
ctx.fillStyle = "blue"; ctx.fill();
canvas.onmousemove = function(e) {
// important: correct mouse position:
var rect = this.getBoundingClientRect(),
x = e.clientX - rect.left,
y = e.clientY - rect.top,
i = 0, r;
ctx.clearRect(0, 0, canvas.width, canvas.height); // for demo
while(r = rects[i++]) {
// add a single rect to path:
ctx.beginPath();
ctx.rect(r.x, r.y, r.w, r.h);
// check if we hover it, fill red, if not fill it blue
ctx.fillStyle = ctx.isPointInPath(x, y) ? "red" : "blue";
ctx.fill();
}
};
<canvas/>
This is a stable code in base of #K3N answer. The basic problem of his code is because when one box is over the another the two may get mouse hover at same time. My answer perfectly solves that adding a 'DESC' to 'ASC' loop.
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d");
var map = [
{x: 20, y: 20, w: 60, h: 60},
{x: 30, y: 50, w: 76, h: 60}
];
var hover = false, id;
var _i, _b;
function renderMap() {
for(_i = 0; _b = map[_i]; _i ++) {
ctx.fillStyle = (hover && id === _i) ? "red" : "blue";
ctx.fillRect(_b.x, _b.y, _b.w, _b.h);
}
}
// Render everything
renderMap();
canvas.onmousemove = function(e) {
// Get the current mouse position
var r = canvas.getBoundingClientRect(),
x = e.clientX - r.left, y = e.clientY - r.top;
hover = false;
ctx.clearRect(0, 0, canvas.width, canvas.height);
for(var i = map.length - 1, b; b = map[i]; i--) {
if(x >= b.x && x <= b.x + b.w &&
y >= b.y && y <= b.y + b.h) {
// The mouse honestly hits the rect
hover = true;
id = i;
break;
}
}
// Draw the rectangles by Z (ASC)
renderMap();
}
<canvas id="canvas"></canvas>
You may have to track the mouse on the canvas using JavaScript and see when it is over your rectangle and change the color then. See code below from my blog post
<!DOCTYPE html>
<html>
<body>
<canvas id="myCanvas" width="700" height="500" style="border:1px solid #c3c3c3;">
Your browser does not support the HTML5 canvas tag.
</canvas>
<script>
var myRect={x:150, y:75, w:50, h:50, color:"red"};
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.fillStyle = myRect.color;
ctx.fillRect(myRect.x, myRect.y, myRect.w, myRect.h);
c.addEventListener("mousemove", function(e){
if ((e.clientX>=myRect.x)&(e.clientX<=myRect.x+myRect.w)&(e.clientY>=myRect.y)&(e.clientY<=myRect.y+myRect.h)){
myRect.color = "green";}
else{
myRect.color = "red";}
updateCanvas();
}, false);
function updateCanvas(){
ctx.fillStyle = myRect.color;
ctx.fillRect(myRect.x, myRect.y, myRect.w, myRect.h);
}
</script>
</body>
</html>
I believe this is a slightly more in-depth answer that would work better for you, especially if you are interested in game design with the canvas element.
The main reason this would work better for you is because it focuses more on an OOP (object orientated programming) approach. This allows for objects to be defined, tracked and altered at a later time via some event or circumstance. It also allows for easy scaling of your code and in my opinion is just more readable and organized.
Essentially what you have here is two shapes colliding. The cursor and the individual point / object it hovers over. With basic squares, rectangles or circles this isn't too bad. But, if you are comparing two more unique shapes, you'll need to read up more on Separating Axis Theorem (SAT) and other collision techniques. At that point optimizing and performance will become a concern, but for now I think this is the optimal approach.
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const width = canvas.width = window.innerWidth;
const height = canvas.height = window.innerHeight;
const cx = width / 2;
const cy = height / 2;
const twoPie = Math.PI * 2;
const points = []; // This will be the array we store our hover points in later
class Point {
constructor(x, y, r) {
this.x = x;
this.y = y;
this.r = r || 0;
}
}
class HoverPoint extends Point {
constructor(x, y, r, color, hoverColor) {
super(x, y, r);
this.color = color;
this.hoverColor = hoverColor;
this.hovered = false;
this.path = new Path2D();
}
draw() {
this.hovered ? ctx.fillStyle = this.hoverColor : ctx.fillStyle = this.color;
this.path.arc(this.x, this.y, this.r, 0, twoPie);
ctx.fill(this.path);
}
}
class Cursor extends Point {
constructor(x, y, r) {
super(x, y, r);
}
collisionCheck(points) {
// This is the method that will be called during the animate function that
// will check the cursors position against each of our objects in the points array.
document.body.style.cursor = "default";
points.forEach(point => {
point.hovered = false;
if (ctx.isPointInPath(point.path, this.x, this.y)) {
document.body.style.cursor = "pointer";
point.hovered = true;
}
});
}
}
function createPoints() {
// Create your points and add them to the points array.
points.push(new HoverPoint(cx, cy, 100, 'red', 'coral'));
points.push(new HoverPoint(cx + 250, cy - 100, 50, 'teal', 'skyBlue'));
// ....
}
function update() {
ctx.clearRect(0, 0, width, height);
points.forEach(point => point.draw());
}
function animate(e) {
const cursor = new Cursor(e.offsetX, e.offsetY);
update();
cursor.collisionCheck(points);
}
createPoints();
update();
canvas.onmousemove = animate;
There is one more thing that I would like to suggest. I haven't done tests on this yet but I suspect that using some simple trigonometry to detect if our circular objects collide would preform better over the ctx.IsPointInPath() method.
However if you are using more complex paths and shapes, then the ctx.IsPointInPath() method would most likely be the way to go. if not some other more extensive form of collision detection as I mentioned earlier.
The resulting change would look like this...
class Cursor extends Point {
constructor(x, y, r) {
super(x, y, r);
}
collisionCheck(points) {
document.body.style.cursor = "default";
points.forEach(point => {
let dx = point.x - this.x;
let dy = point.y - this.y;
let distance = Math.hypot(dx, dy);
let dr = point.r + this.r;
point.hovered = false;
// If the distance between the two objects is less then their combined radius
// then they must be touching.
if (distance < dr) {
document.body.style.cursor = "pointer";
point.hovered = true;
}
});
}
}
here is a link containing examples an other links related to collision detection
I hope you can see how easily something like this can be modified and used in games and whatever else. Hope this helps.
Below code adds shadow to canvas circle on hovering it.
<html>
<body>
<canvas id="myCanvas" width="1000" height="500" style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag.</canvas>
</body>
<script>
var canvas = document.getElementById("myCanvas"),
ctx = canvas.getContext("2d"),
circle = [{
x: 60,
y: 50,
r: 40,
},
{
x: 100,
y: 150,
r: 50,
} // etc.
];
// render initial rects.
for (var i = 0; i < circle.length; i++) {
ctx.beginPath();
ctx.arc(circle[i].x, circle[i].y, circle[i].r, 0, 2 * Math.PI);
ctx.fillStyle = "blue";
ctx.fill();
}
canvas.onmousemove = function(e) {
var x = e.pageX,
y = e.pageY,
i = 0,
r;
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < circle.length; i++) {
if ((x > circle[i].x - circle[i].r) && (y > circle[i].y - circle[i].r) && (x < circle[i].x + circle[i].r) && (y < circle[i].y + circle[i].r)) {
ctx.beginPath();
ctx.arc(circle[i].x, circle[i].y, circle[i].r, 0, 2 * Math.PI);
ctx.fillStyle = "blue";
ctx.fill();
ctx.shadowBlur = 10;
ctx.lineWidth = 3;
ctx.strokeStyle = 'rgb(255,255,255)';
ctx.shadowColor = 'grey';
ctx.stroke();
ctx.shadowColor = 'white';
ctx.shadowBlur = 0;
} else {
ctx.beginPath();
ctx.arc(circle[i].x, circle[i].y, circle[i].r, 0, 2 * Math.PI);
ctx.fillStyle = "blue";
ctx.fill();
ctx.shadowColor = 'white';
ctx.shadowBlur = 0;
}
}
};
</script>
</html>
I know this is old, but I am surprised no one has mentioned JCanvas. It adds to the simplicity of animating canvas on events. More documentation here https://projects.calebevans.me/jcanvas/docs/mouseEvents/
<html lang="en">
<head>
<!-- css and other -->
</head>
<body onload="draw();">
<canvas id = "canvas" width="500" height="500" style= border:1px solid #000000;"> </canvas>
<script>
function draw() {
$('canvas').drawRect({
layer: true,
fillStyle:'#333',
x:100, y: 200,
width: 600,
height: 400,
mouseover: function(layer) {
$(this).animateLayer(layer, {
fillStyle: 'green'
}, 1000, 'swing');
}
});
}
<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jcanvas/21.0.1/jcanvas.js" crossorigin="anonymous"></script>
</body>
</html>
Consider this following code:
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.rect(20,20,150,100);
ctx.stroke();
c.addEventListener("mouseover", doMouseOver, false);//added event to canvas
function doMouseOver(e){
ctx.fillStyle = 'red';
ctx.fill();
}
DEMO
You could use canvas.addEventListener
var canvas = document.getElementById('canvas0');
canvas.addEventListener('mouseover', function() { /*your code*/ }, false);
It worked on google chrome
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.rect(20,20,150,100);
ctx.stroke();
$(c).hover(function(e){
ctx.fillStyle = 'red';
ctx.fill();
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<canvas id="myCanvas"/>

Categories

Resources