Related
I want to make an animation using the HTML5 canvas and JavaScript. The idea is to write classes for different objects, like this:
class Line {
constructor(x1, y1, x2, y2) {
this.x1 = x1;
this.y1 = y2;
...
}
draw() {
}
}
class Circle {
constructor(x, y, radius) {
this.x = x;
...
}
draw() {}
}
...
Then all you would have to do in the main code is to draw the shapes one after another with pauses in between:
let line1 = new Line(x1, y1, x2, y2);
let circle = new Circle(x, y, r);
let line2 = new Line(x1, y1, x2, y2);
line1.draw()
pause()
circle.draw()
pause()
line2.draw()
...
Is there an easy way to this (without having to deal with Promises and nested Callback Functions), for example by using some library?
Key frames
You can use key frames to great effect to animate almost anything.
The example below (was going to do more of a write up but I was too late, you have accepted an answer) shows how a very basic key frame utility can create animations.
A key frame is just a time and a value
Key frames are added to tracks that give a name to the value.
Thus the name x (position) and the keys {time:0, value:100}, {time:1000, value:900} will change the x property from 100 to 900 during the time 0 to 1 second
For example a circle
const circle = {
x: 0,
y: 0,
r: 10,
col : "",
draw() {
ctx.fillStyle = this.col;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.fill()
}
};
can have any of its properties changed over time.
First create a tracks object and define the keys
const circleTracks = createTracks();
// properties to animate
circleTracks.addTrack("x");
circleTracks.addTrack("y");
circleTracks.addTrack("r");
circleTracks.addTrack("col");
Then add key frames at specific time stamps.
circleTracks.addKeysAtTime(0, {x: 220, y :85, r: 20, col: "#F00"});
circleTracks.addKeysAtTime(1000, {x: 220, y :50, r: 50, col: "#0F0"});
circleTracks.addKeysAtTime(2000, {x: 420, y :100, r: 20, col: "#00F"});
circleTracks.addKeysAtTime(3000, {x: 180, y :160, r: 10, col: "#444"});
circleTracks.addKeysAtTime(4000, {x: 20, y :100, r: 20});
circleTracks.addKeysAtTime(5000, {x: 220, y :85, r: 10, col: "#888"});
circleTracks.addKeysAtTime(5500, {r: 10, col: "#08F"});
circleTracks.addKeysAtTime(6000, {r: 340, col: "#00F"});
When ready clean up the the keys (You can add them out of time order)
circleTracks.clean();
Seek to the start
circleTracks.seek(0);
And update the object
circleTracks.update(circle);
To animate just call the tick and update functions, and draw the circle
circleTracks.tick();
circleTracks.update(circle);
circle.draw();
Example
Click to start the animation.
When it ends you can scrub the animation using tracks.seek(time)
This is the most basic keyframe animations.
And the best thing about key frames is that they separate the animation from the code, letting you import and export animations as simple data structures.
const ctx = canvas.getContext("2d");
requestAnimationFrame(mainLoop);
const allTracks = [];
function addKeyframedObject(tracks, object) {
tracks.clean();
tracks.seek(0);
tracks.update(object);
allTracks.push({tracks, object});
}
const FRAMES_PER_SEC = 60, TICK = 1000 / FRAMES_PER_SEC; //
const key = (time, value) => ({time, value});
var playing = false;
var showScrubber = false;
var currentTime = 0;
function mainLoop() {
ctx.clearRect(0 ,0 ,ctx.canvas.width, ctx.canvas.height);
if(playing) {
for (const animated of allTracks) {
animated.tracks.tick();
animated.tracks.update(animated.object);
}
}
for (const animated of allTracks) {
animated.object.draw();
}
if(showScrubber) {
slide.update();
slide.draw();
if(slide.value !== currentTime) {
currentTime = slide.value;
for (const animated of allTracks) {
animated.tracks.seek(currentTime);
animated.tracks.update(animated.object);
}
}
} else {
if(mouse.button) { playing = true }
}
if(allTracks[0].tracks.time > 6300) {
showScrubber = true
playing = false;
}
requestAnimationFrame(mainLoop);
}
const text = {
x: canvas.width / 2,
y: canvas.height / 2,
alpha: 1,
text: "",
draw() {
ctx.font = "24px arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "#000";
ctx.globalAlpha = this.alpha;
ctx.fillText(this.text, this.x, this.y);
ctx.globalAlpha = 1;
}
}
const circle = {
x: 0,
y: 0,
r: 10,
col : "",
draw() {
ctx.fillStyle = this.col;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.fill()
}
}
const circleTracks = createTracks();
circleTracks.addTrack("x");
circleTracks.addTrack("y");
circleTracks.addTrack("r");
circleTracks.addTrack("col");
circleTracks.addKeysAtTime(0, {x: 220, y :85, r: 20, col: "#F00"});
circleTracks.addKeysAtTime(1000, {x: 220, y :50, r: 50, col: "#0F0"});
circleTracks.addKeysAtTime(2000, {x: 420, y :100, r: 20, col: "#00F"});
circleTracks.addKeysAtTime(3000, {x: 180, y :160, r: 10, col: "#444"});
circleTracks.addKeysAtTime(4000, {x: 20, y :100, r: 20});
circleTracks.addKeysAtTime(5000, {x: 220, y :85, r: 10, col: "#888"});
circleTracks.addKeysAtTime(5500, {r: 10, col: "#08F"});
circleTracks.addKeysAtTime(6000, {r: 340, col: "#00F"});
addKeyframedObject(circleTracks, circle);
const textTracks = createTracks();
textTracks.addTrack("alpha");
textTracks.addTrack("text");
textTracks.addKeysAtTime(0, {alpha: 1, text: "Click to start"});
textTracks.addKeysAtTime(1, {alpha: 0});
textTracks.addKeysAtTime(20, {alpha: 0, text: "Simple keyframed animation"});
textTracks.addKeysAtTime(1000, {alpha: 1});
textTracks.addKeysAtTime(2000, {alpha: 0});
textTracks.addKeysAtTime(3500, {alpha: 0, text: "The END!" });
textTracks.addKeysAtTime(3500, {alpha: 1});
textTracks.addKeysAtTime(5500, {alpha: 1});
textTracks.addKeysAtTime(6000, {alpha: 0, text: "Use slider to scrub"});
textTracks.addKeysAtTime(6300, {alpha: 1});
addKeyframedObject(textTracks, text);
function createTracks() {
return {
tracks: {},
addTrack(name, keys = [], value) {
this.tracks[name] = {name, keys, idx: -1, value}
},
addKeysAtTime(time, keys) {
for(const name of Object.keys(keys)) {
this.tracks[name].keys.push(key(time, keys[name]));
}
},
clean() {
for(const track of Object.values(this.tracks)) {
track.keys.sort((a,b) => a.time - b.time);
}
},
seek(time) { // seek to random time
this.time = time;
for(const track of Object.values(this.tracks)) {
if (track.keys[0].time > time) {
track.idx = -1; // befor first key
}else {
let idx = 1;
while(idx < track.keys.length) {
if(track.keys[idx].time > time && track.keys[idx-1].time <= time) {
track.idx = idx - 1;
break;
}
idx += 1;
}
}
}
this.tick(0);
},
tick(timeStep = TICK) {
const time = this.time += timeStep;
for(const track of Object.values(this.tracks)) {
if(track.keys[track.idx + 1] && track.keys[track.idx + 1].time <= time) {
track.idx += 1;
}
if(track.idx === -1) {
track.value = track.keys[0].value;
} else {
const k1 = track.keys[track.idx];
const k2 = track.keys[track.idx + 1];
if (typeof k1.value !== "number" || !k2) {
track.value = k1.value;
} else if (k2) {
const unitTime = (time - k1.time) / (k2.time - k1.time);
track.value = (k2.value - k1.value) * unitTime + k1.value;
}
}
}
},
update(obj) {
for(const track of Object.values(this.tracks)) {
obj[track.name] = track.value;
}
}
};
};
const slide = {
min: 0,
max: 6300,
value: 6300,
top: 160,
left: 1,
height: 9,
width: 438,
slide: 10,
slideX: 0,
draw() {
ctx.fillStyle = "#000";
ctx.fillRect(this.left-1, this.top-1, this.width+ 2, this.height+ 2);
ctx.fillStyle = "#888";
ctx.fillRect(this.left, this.top, this.width, this.height);
ctx.fillStyle = "#DDD";
this.slideX = (this.value - this.min) / (this.max - this.min) * (this.width - this.slide) + this.left;
ctx.fillRect(this.slideX, this.top + 1, this.slide, this.height - 2);
},
update() {
if(mouse.x > this.left && mouse.x < this.left + this.width &&
mouse.y > this.top && mouse.y < this.top + this.height) {
if (mouse.button && !this.captured) {
this.captured = true;
} else {
canvas.style.cursor = "ew-resize";
}
}
if (this.captured) {
if (!mouse.button) {
this.captured = false;
canvas.style.cursor = "default";
} else {
this.value = ((mouse.x - this.left) / this.width) * (this.max - this.min) + this.min;
canvas.style.cursor = "none";
this.value = this.value < this.min ? this.min : this.value > this.max ? this.max : this.value;
}
}
}
};
const mouse = {x : 0, y : 0, button : false};
function mouseEvents(e){
const bounds = canvas.getBoundingClientRect();
mouse.x = e.pageX - bounds.left - scrollX;
mouse.y = e.pageY - bounds.top - scrollY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
canvas { border: 1px solid black; }
<canvas id="canvas" width="440" height="170"><canvas>
A good question given that what you don't want to do (use promises and/or callbacks) would effectively mean hard coding the animation in script with limited potential for re-use, and possibly creating difficulties in making modifications in the future.
A solution that I've used is to create a story book of functions that draw frames, so you would put
()=>line1.draw()
into the book rather than
line1.draw()
which would draw it immediately and try adding its return value to the book!
The next part (in no particular order) is a player that uses requestAnimationFrame to time stepping through the story book and calling functions to draw the frame. Minimally it would need methods for script to
add a frame drawing function,
add a delay before advancing to the next frame, and
play the animation.
Making the delay function take a number of frames to wait before calling the next entry in the story book keeps it simple, but creates timings based on frame rate which may not be constant.
Here's a simplified example in pure JavaScript that changes background color (not canvas manipulation) for demonstration - have a look for reference if you can't get it working.
"use strict";
class AnimePlayer {
constructor() {
this.storyBook = [];
this.pause = 0;
this.drawFrame = this.drawFrame.bind( this);
this.frameNum = 0;
}
addFrame( frameDrawer) {
this.storyBook.push( frameDrawer);
}
pauseFrames(n) {
this.storyBook.push ( ()=>this.pause = n);
}
play() {
this.frameNum = 0;
this.drawFrame();
}
drawFrame() {
if( this.pause > 0) {
--this.pause;
requestAnimationFrame( this.drawFrame);
}
else if( this.frameNum < this.storyBook.length) {
this.storyBook[this.frameNum]();
++this.frameNum;
requestAnimationFrame( this.drawFrame);
}
}
}
let player = new AnimePlayer();
let style = document.body.style;
player.addFrame( ()=> style.backgroundColor = "green");
player.pauseFrames(60);
player.addFrame( ()=> style.backgroundColor = "yellow");
player.pauseFrames(5);
player.addFrame( ()=>style.backgroundColor = "orange");
player.pauseFrames(60);
player.addFrame( ()=> style.backgroundColor = "red");
player.pauseFrames(60);
player.addFrame( ()=> style.backgroundColor = "");
function tryMe() {
console.clear();
player.play();
}
<button type="button" onclick="tryMe()">try me</button>
So I'm making this pretty basic game I would say for a school project. I have all the basic controllers working, jumping, navigating left and right, but I'm struggling to find a way to make the character be able to shoot. I would also like to make it so it's like a break between each shoot, like a bolt action. The collision part shouldn't be a problem, so no need for help there.
(function() {
var requestAnimationFrame =
window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
window.requestAnimationFrame = requestAnimationFrame;
})();
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d"),
width = 640,
height = 480,
player = {
x: width / 2,
y: height - 140,
width: 35,
height: 90,
speed: 3,
velX: 0,
velY: 0,
jumping: false,
grounded: false
},
keys = [],
friction = 0.8,
gravity = 0.3;
var hovedkarakterBilde = new Image();
hovedkarakterBilde.src = "mc1.png";
var boxes = [];
// dimensions
boxes.push({
// venstre vegg
x: 0,
y: 0,
width: 10,
height: height
});
boxes.push({
// gulv
x: 0,
y: height - 68,
width: width,
height: 1
});
boxes.push({
x: 120,
y: 250,
width: 80,
height: 80
});
boxes.push({
x: 170,
y: 275,
width: 80,
height: 80
});
boxes.push({
x: 220,
y: 325,
width: 80,
height: 80
});
boxes.push({
x: 270,
y: 225,
width: 40,
height: 40
});
canvas.width = width;
canvas.height = height;
function update() {
// check keys
if (keys[38]) {
// up arrow or space
if (!player.jumping && player.grounded) {
player.jumping = true;
player.grounded = false;
player.velY = -player.speed * 2;
}
}
if (keys[39]) {
// right arrow
if (player.velX < player.speed) {
player.velX++;
}
}
if (keys[37]) {
// left arrow
if (player.velX > -player.speed) {
player.velX--;
}
}
player.velX *= friction;
player.velY += gravity;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = "black";
ctx.beginPath();
player.grounded = false;
for (var i = 0; i < boxes.length; i++) {
ctx.rect(boxes[i].x, boxes[i].y, boxes[i].width, boxes[i].height);
var dir = colCheck(player, boxes[i]);
if (dir === "l" || dir === "r") {
player.velX = 0;
player.jumping = false;
} else if (dir === "b") {
player.grounded = true;
player.jumping = false;
} else if (dir === "t") {
player.velY *= -1;
}
}
if (player.grounded) {
player.velY = 0;
}
player.x += player.velX;
player.y += player.velY;
ctx.fill();
ctx.drawImage(
hovedkarakterBilde,
player.x,
player.y,
player.width,
player.height
);
requestAnimationFrame(update);
}
function colCheck(shapeA, shapeB) {
// get the vectors to check against
var vX = shapeA.x + shapeA.width / 2 - (shapeB.x + shapeB.width / 2),
vY = shapeA.y + shapeA.height / 2 - (shapeB.y + shapeB.height / 2),
// add the half widths and half heights of the objects
hWidths = shapeA.width / 2 + shapeB.width / 2,
hHeights = shapeA.height / 2 + shapeB.height / 2,
colDir = null;
// if the x and y vector are less than the half width or half height, they
we must be inside the object, causing a collision
if (Math.abs(vX) < hWidths && Math.abs(vY) < hHeights) {
// figures out on which side we are colliding (top, bottom, left, or right)
var oX = hWidths - Math.abs(vX),
oY = hHeights - Math.abs(vY);
if (oX >= oY) {
if (vY > 0) {
colDir = "t";
shapeA.y += oY;
} else {
colDir = "b";
shapeA.y -= oY;
}
} else {
if (vX > 0) {
colDir = "l";
shapeA.x += oX;
} else {
colDir = "r";
shapeA.x -= oX;
}
}
}
return colDir;
}
document.body.addEventListener("keydown", function(e) {
keys[e.keyCode] = true;
});
document.body.addEventListener("keyup", function(e) {
keys[e.keyCode] = false;
});
window.addEventListener("load", function() {
update();
});
HTML:
</head>
<body>
<canvas id="canvas" style="background: url('bakgrunn1.png')"></canvas>
</body>
<script src="spillv2.js"></script>
First thing you have to consider is if the bullet is hit-scan or projectile. Hit-scan means when the bullet is shot, the bullet instantly hits the target. This can be done by using a ray-cast to check if it hits an object. Projectile based bullets are when the user points in a direction, the bullet actually "moves". This can be implemented by adding an array of "bullets" to the player. So when the player clicks, a bullet object is added the array. This bullet object will be drawn on it's own in the draw loop and will move from the user to the direction it's pointed at.
Adding a delay is simple, you can have a "cooldown" variable that is a counter that lasts for n milliseconds. When the user fires, the counter is set to n and starts to count down to 0. When it reaches 0, you are able to fire again.
Well, you could make a shoot(e) function, which will be called when user presses (keydown) space for example and then make a new array, lets say ammo[]. Then, inside shoot() you will do something like:
const ammo = [];
let shooting = false;
function shoot(e) {
e = e || event;
const key = e.keyCode;
if(shooting) return; // This will prevent from shooting more bullets while holding space
// So you'd need to press space for each time you want to shoot.
if(key == 32) { // Key code 32 is SPACE
shooting = true;
// You get the coordinates from your player, from which the bullet will shoot
ammo.push({
x: player.x,
y: player.y + (player.height / 2)
// Player.height / 2 is basically setting the ammo at the middle of the character
});
}
}
And then, I presume you do all the updates in the update() function (yea, logically :D), you'll do something like:
function update() {
// Insert it somewhere
for(let i = 0; i < ammo.length; i++) {
// The "theBullet" part refers to either your image for the bullet
ctx.drawImage(theBullet, ammo[i].x, ammo[i].y;
ammo[i].x++ // Or however fast you'd like it to go.
}
}
Hope its somewhat clear, I am working myself on a spaceshooter type of game, almost finished, so shared some of my code here :) Not implying that its the best, but it does the work :)
I'm currently working on a canvas timeline-like animation.
This is what I made so far...
$(function() {
'use strict';
var canvas = document.querySelector('#canvas');
var ctx = canvas.getContext('2d');
var s = 20;
var arr = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100];
var colorP = ['#ff5454', '#ffa144', '#ffe256', '#aaff75', '#8cd8ff', '#b5b6ff', '#b882ff'];
var dots = [];
var rDots = [];
function init() {
var reverse = true;
for (var i = 0; i < 100; i++) {
var dot = new Object();
var height = null;
if (arr.indexOf(i) != -1) {
dot.x = s;
dot.y = 50;
dot.r = 3;
dot.c = 'red';
dot.f = 'rgba(0,0,0,0)';
dot.t = '1';
dot.s = 0;
rDots.push(dot);
} else {
dot.x = s;
dot.y = 50;
dot.r = 1;
dot.c = 'red';
dot.f = '';
dot.t = '';
dot.s = 0;
}
s += 10;
dots.push(dot);
};
function tween() {
height = Math.floor(Math.random() * (75 - 25) + 25);
TweenMax.staggerTo(dots, 5, {
y: height,
yoyo: true,
repeat: 'repeat',
repeatDelay: 1,
ease: Sine.easeInOut
}, 0.5);
};
tween();
setInterval(function() {
tween()
}, 4800);
}
init();
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < dots.length - 1; i++) {
ctx.beginPath();
ctx.moveTo(dots[i].x, dots[i].y);
ctx.lineTo(dots[i + 1].x, dots[i + 1].y);
ctx.lineWidth = 3;
ctx.strokeStyle = 'red';
ctx.stroke();
};
for (var i = 0; i < dots.length; i++) {
ctx.beginPath();
ctx.arc(dots[i].x, dots[i].y, dots[i].r, 0, 2 * Math.PI);
ctx.strokeStyle = dots[i].c;
ctx.lineWidth = 1;
ctx.fillStyle = dots[i].f;
ctx.fill();
ctx.stroke();
ctx.font = dots[i].s + 'px Arial';
ctx.textAlign = 'center';
ctx.fillStyle = '#FFF';
ctx.fillText(dots[i].t, dots[i].x, dots[i].y + 4);
};
setTimeout(function() {
draw();
}, 5);
}
draw();
function hover(e, bool) {
var dot = canvas.getBoundingClientRect();
var x = e.clientX - dot.left;
var y = e.clientY - dot.top;
for (var i = 0; i < rDots.length; i++) {
if (x == rDots[i].x) {
TweenMax.to(rDots[i], 0.1, {
r: 10,
f: 'red',
s: 8
});
$('body').css('cursor', 'pointer');
} else {
TweenMax.to(rDots[i], 0.1, {
r: 3,
f: 'rgba(0,0,0,0)',
s: 0
});
}
};
};
$(canvas).on('mousemove', function(e) {
hover(e, true);
});
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="canvas" height="100" width="1050" style="background: #EEE"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.19.1/TweenMax.min.js"></script>
The idea is,
I want it to swing randomly (checked)
and when the cursor close in the knot, it will enlarge and show the text in it...
I tried to use x and y axis to do the trick,
but it doesn't work well...
Then I tried to make another function to draw a bigger circle to cover the original knot,
but since my draw() keeping clear the canvas, so I failed again...
Wondering is there any better ways to make it work?
any suggestions or hints are welcome!
You may find jCanvas helpful.
It's a JavaScript library that wraps the HTML5 canvas API, letting you add certain object-like functionality using a jQuery-style syntax. You could refactor your code a bit and use a mouseOver effect rather than binding a mousemove event to the canvas, which will let you create a smother animation.
Also, if you increase the area of the rDots.x that's triggering your animation and set your Tween time interval to something a bit longer than 0.1 that makes the animation slightly less jerky as well.
Not sure if this solves your issue, but I hope it helps!
Ok, I've work my way out.
$(function() {
'use strict';
var dots = [],
eDots = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100],
rDots = [],
stagger = 0;
var canvas = document.querySelector('#canvas'),
ctx = canvas.getContext('2d');
//initialize all the dots obj
function init() {
for (var i = 0; i < 100; i++) {
if (eDots.indexOf(i) != -1) {
var dot = {
xAxis: stagger,
yAxis: 50,
radius: 3,
color: 'rgba(0,0,0,0)',
num: 1,
};
rDots.push(dot);
} else {
var dot = {
xAxis: stagger,
yAxis: 50,
radius: .5,
color: 'rgba(0,0,0,0)',
num: ''
};
}
dots.push(dot);
stagger += 10;
}
};
init();
//Save position property for click event
function getSize() {
for (var i = 0; i < rDots.length; i++) {
rDots[i].top = rDots[i].yAxis - rDots[i].radius;
rDots[i].right = rDots[i].xAxis + rDots[i].radius;
rDots[i].bottom = rDots[i].yAxis + rDots[i].radius;
rDots[i].left = rDots[i].xAxis - rDots[i].radius;
}
}
getSize();
//Hover event dots to change style
function hover() {
$(canvas).bind('mousemove', function(e) {
var dot = canvas.getBoundingClientRect(),
x = e.clientX - dot.left,
y = e.clientY - dot.top;
for (var i = 0; i < rDots.length; i++) {
ctx.beginPath();
ctx.arc(rDots[i].xAxis, rDots[i].yAxis, rDots[i].radius, 0, 2 * Math.PI);
//rDots[i].radius = ctx.isPointInPath(x, y) ? 10 : 3;
//rDots[i].color = ctx.isPointInPath(x, y) ? 'red' : 'rgba(0, 0, 0, 0)';
if (ctx.isPointInPath(x, y)) {
TweenMax.to(rDots[i], 0.1, {
radius: 10,
color: 'red',
});
$(canvas).css({
cursor: 'pointer'
});
return;
} else {
TweenMax.to(rDots[i], 0.1, {
radius: 3,
color: 'rgba(0,0,0,0)'
});
}
ctx.stroke();
ctx.fill();
$(canvas).css({
cursor: 'default'
});
}
});
};
hover();
//Setup click event for functioning purpose
function click(e) {
var dot = canvas.getBoundingClientRect(),
x = e.clientX - dot.left,
y = e.clientY - dot.top;
for (var i = 0; i < rDots.length; i++) {
if (x < rDots[i].right && x > rDots[i].left && y > rDots[i].top && y < rDots[i].bottom) {
console.log('This is dot ' + i);
}
}
};
$(canvas).on('click', function(e) {
click(e);
})
//Let the line start to twist
function tween() {
var height = Math.floor(Math.random() * (75 - 25) + 25);
TweenMax.staggerTo(dots, 4, {
yAxis: height,
yoyo: true,
repeat: 'repeat',
repeatDelay: 1,
ease: Sine.easeInOut
}, 0.5);
setTimeout(function() {
tween();
}, 3800);
}
tween();
//Let's get some paint
function draw() {
//clear canvas for animate
ctx.clearRect(0, 0, canvas.width, canvas.height);
//draw the lines
for (var i = 0; i < dots.length - 1; i++) {
ctx.moveTo(dots[i].xAxis, dots[i].yAxis);
ctx.lineTo(dots[i + 1].xAxis, dots[i + 1].yAxis);
ctx.lineWidth = 3;
ctx.stroke();
}
//draw the dots
for (var i = 0; i < dots.length; i++) {
ctx.beginPath();
ctx.arc(dots[i].xAxis, dots[i].yAxis, dots[i].radius, 0, 2 * Math.PI);
ctx.strokeStyle = 'red';
ctx.strokeWidth = '1px';
ctx.fillStyle = dots[i].color;
ctx.stroke();
ctx.fill()
};
setTimeout(function() {
draw();
}, 10);
}
draw();
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.19.1/TweenMax.min.js"></script>
<canvas id="canvas" height="100" width="1000" style="background:#EEE"></canvas>
Turn's out all I need is to do is use isPointOnPath to get the path's axis,
then manipulate the certain dot's property with if statement, in my case is it's radius and color.
simple enough...
Can't believe I couldn't figured it out.
I guess I need some sleep now XD
OK, if I have the following shapes that are rotated and then selected you will see their bounding boxes:
I am trying to write some code to align objects with respect to each other. So I would like to get each object's "containing box".
I am aware of getBoundingRect but, for the above shapes this gives me the following:
As such, these boxes are not that useful to me. Is there a standard method of getting what I would call the "containing boxes" for all shapes? For example, I would like to be able to have the following boxes returned to me:
So, for any given shape I would like to be able get the red bounding rectangle (with no rotation).
Obviously, I could write a routine for each possible shape within fabricJS but I would prefer not to reinvent the wheel! Any ideas?
Edit Here's an interactive snippet that shows the current bounding boxes (in red):
$(function ()
{
canvas = new fabric.Canvas('c');
canvas.add(new fabric.Triangle({
left: 50,
top: 50,
fill: '#FF0000',
width: 50,
height: 50,
angle : 30
}));
canvas.add(new fabric.Circle({
left: 250,
top: 50,
fill: '#00ff00',
radius: 50,
angle : 30
}));
canvas.add(new fabric.Polygon([
{x: 185, y: 0},
{x: 250, y: 100},
{x: 385, y: 170},
{x: 0, y: 245} ], {
left: 450,
top: 50,
fill: '#0000ff',
angle : 30
}));
canvas.on("after:render", function(opt)
{
canvas.contextContainer.strokeStyle = '#FF0000';
canvas.forEachObject(function(obj)
{
var bound = obj.getBoundingRect();
canvas.contextContainer.strokeRect(
bound.left + 0.5,
bound.top + 0.5,
bound.width,
bound.height
);
});
});
canvas.renderAll();
});
<script src="//cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.6/fabric.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="c" width="800" height="600"></canvas><br/>
So the getBoundingBox is a method of the Object class of fabricjs.
Nothing stops you from rewriting this method for each shape of which you can think of.
I ll start with circle and triangle, i'll let you imagine polygon. It gets harder and harder when shapes are paths or when circle is scaled as an ellipse.
Circle is the hardest.
I sampled the circle at 30, 60, 90 degrees for all the quadrants. still is not perfect. You may need to increase sampling or find a better formula (maybe sample every 15 degrees will make the trick ).
Triangle is the easier since it has 3 points of interest.
Polygon is derived from triangle, nothing difficult here.
fabric.Circle.prototype.getBoundingRect = function() {
var matrix = this.calcTransformMatrix();
var points = [{x:-this.width/2, y:0}, {x:this.width/2, y:0}, {x:0, y: -this.height/2}, {x: 0, y: this.height/2}, {x: 0, y: -this.height/2}, {x: 0.433 * this.width, y: this.height/4}, {x: -0.433 * this.width, y: this.height/4}, {y: 0.433 * this.height, x: this.width/4}, {y: -0.433 * this.height, x: this.width/4}, {y: -0.433 * this.height, x: -this.width/4}, {y: 0.433 * this.height, x: -this.width/4}, {x: 0.433 * this.width, y: -this.height/4}, {x: -0.433 * this.width, y: -this.height/4}];
points = points.map(function(p) {
return fabric.util.transformPoint(p, matrix);
});
return fabric.util.makeBoundingBoxFromPoints(points);
}
fabric.Triangle.prototype.getBoundingRect = function() {
var matrix = this.calcTransformMatrix();
var points = [{x:-this.width/2, y:this.height/2}, {x:this.width/2, y:this.height/2}, {x:0, y: -this.height/2}, {x: 0, y: 0}];
points = points.map(function(p) {
return fabric.util.transformPoint(p, matrix);
});
return fabric.util.makeBoundingBoxFromPoints(points);
}
fabric.Polygon.prototype.getBoundingRect = function() {
var matrix = this.calcTransformMatrix();
var points = this.points;
var offsetX = this.pathOffset.x;
var offsetY = this.pathOffset.y;
points = points.map(function(p) {
return fabric.util.transformPoint({x: p.x - offsetX , y: p.y -
offsetY}, matrix);
});
return fabric.util.makeBoundingBoxFromPoints(points);
}
$(function ()
{
fabric.util.makeBoundingBoxFromPoints = function(points) {
var minX = fabric.util.array.min(points, 'x'),
maxX = fabric.util.array.max(points, 'x'),
width = Math.abs(minX - maxX),
minY = fabric.util.array.min(points, 'y'),
maxY = fabric.util.array.max(points, 'y'),
height = Math.abs(minY - maxY);
return {
left: minX,
top: minY,
width: width,
height: height
};
};
fabric.Circle.prototype.getBoundingRect = function() {
var matrix = this.calcTransformMatrix();
var points = [{x:-this.width/2, y:0}, {x:this.width/2, y:0}, {x:0, y: -this.height/2}, {x: 0, y: this.height/2}, {x: 0, y: -this.height/2}, {x: 0.433 * this.width, y: this.height/4}, {x: -0.433 * this.width, y: this.height/4}, {y: 0.433 * this.height, x: this.width/4}, {y: -0.433 * this.height, x: this.width/4}, {y: -0.433 * this.height, x: -this.width/4}, {y: 0.433 * this.height, x: -this.width/4}, {x: 0.433 * this.width, y: -this.height/4}, {x: -0.433 * this.width, y: -this.height/4}];
points = points.map(function(p) {
return fabric.util.transformPoint(p, matrix);
});
return fabric.util.makeBoundingBoxFromPoints(points);
}
fabric.Triangle.prototype.getBoundingRect = function() {
var matrix = this.calcTransformMatrix();
var points = [{x:-this.width/2, y:this.height/2}, {x:this.width/2, y:this.height/2}, {x:0, y: -this.height/2}, {x: 0, y: 0}];
points = points.map(function(p) {
return fabric.util.transformPoint(p, matrix);
});
return fabric.util.makeBoundingBoxFromPoints(points);
}
fabric.Polygon.prototype.getBoundingRect = function() {
var matrix = this.calcTransformMatrix();
var points = this.points;
var offsetX = this.pathOffset.x;
var offsetY = this.pathOffset.y;
points = points.map(function(p) {
return fabric.util.transformPoint({x: p.x - offsetX , y: p.y -
offsetY}, matrix);
});
return fabric.util.makeBoundingBoxFromPoints(points);
}
canvas = new fabric.Canvas('c');
canvas.add(new fabric.Triangle({
left: 50,
top: 50,
fill: '#FF0000',
width: 50,
height: 50,
angle : 30
}));
canvas.add(new fabric.Circle({
left: 250,
top: 50,
fill: '#00ff00',
radius: 50,
angle : 30
}));
canvas.add(new fabric.Polygon([
{x: 185, y: 0},
{x: 250, y: 100},
{x: 385, y: 170},
{x: 0, y: 245} ], {
left: 450,
top: 50,
fill: '#0000ff',
angle : 30
}));
canvas.on("after:render", function(opt)
{
canvas.contextContainer.strokeStyle = '#FF0000';
canvas.forEachObject(function(obj)
{
var bound = obj.getBoundingRect();
if(bound)
{
canvas.contextContainer.strokeRect(
bound.left + 0.5,
bound.top + 0.5,
bound.width,
bound.height
);
}
});
});
canvas.renderAll();
});
<script src="//cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.6/fabric.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="c" width="800" height="600"></canvas><br/>
I had the same issue and I found a workaround. I created a temporary SVG from the active object and placed it outside the viewport. Then I measured the real bounding box using the native getBBox() function.
UPDATE
Apparently, the solution above works only in Firefox (76), so I came up with a different solution. Since I couldn't find a properly working, native function to get the real bounding box of a shape, I decided to scan the pixels and retrieve the boundaries from there.
Fiddle: https://jsfiddle.net/divpusher/2m7c61gw/118/
How it works:
export the selected fabric object toDataURL()
place it in a hidden canvas and get the pixels with getImageData()
scan the pixels to retrieve x1, x2, y1, y2 edge coords of the shape
Demo below
// ---------------------------
// the magic
var tempCanv, ctx, w, h;
function getImageData(dataUrl) {
// we need to use a temp canvas to get imagedata
if (tempCanv == null) {
tempCanv = document.createElement('canvas');
tempCanv.style.border = '1px solid blue';
tempCanv.style.visibility = 'hidden';
ctx = tempCanv.getContext('2d');
document.body.appendChild(tempCanv);
}
return new Promise(function(resolve, reject) {
if (dataUrl == null) return reject();
var image = new Image();
image.addEventListener('load', function() {
w = image.width;
h = image.height;
tempCanv.width = w;
tempCanv.height = h;
ctx.drawImage(image, 0, 0, w, h);
var imageData = ctx.getImageData(0, 0, w, h).data.buffer;
resolve(imageData, false);
});
image.src = dataUrl;
});
}
function scanPixels(imageData) {
var data = new Uint32Array(imageData),
len = data.length,
x, y, y1, y2, x1 = w, x2 = 0;
// y1
for(y = 0; y < h; y++) {
for(x = 0; x < w; x++) {
if (data[y * w + x] & 0xff000000) {
y1 = y;
y = h;
break;
}
}
}
// y2
for(y = h - 1; y > y1; y--) {
for(x = 0; x < w; x++) {
if (data[y * w + x] & 0xff000000) {
y2 = y;
y = 0;
break;
}
}
}
// x1
for(y = y1; y < y2; y++) {
for(x = 0; x < w; x++) {
if (x < x1 && data[y * w + x] & 0xff000000) {
x1 = x;
break;
}
}
}
// x2
for(y = y1; y < y2; y++) {
for(x = w - 1; x > x1; x--) {
if (x > x2 && data[y * w + x] & 0xff000000) {
x2 = x;
break;
}
}
}
return {
x1: x1,
x2: x2,
y1: y1,
y2: y2
}
}
// ---------------------------
// align buttons
function alignLeft(){
var obj = canvas.getActiveObject();
obj.set('left', 0);
obj.setCoords();
canvas.renderAll();
}
function alignLeftbyBoundRect(){
var obj = canvas.getActiveObject();
var bound = obj.getBoundingRect();
obj.set('left', (obj.left - bound.left));
obj.setCoords();
canvas.renderAll();
}
function alignRealLeft(){
var obj = canvas.getActiveObject();
getImageData(obj.toDataURL())
.then(function(data) {
var bound = obj.getBoundingRect();
var realBound = scanPixels(data);
obj.set('left', (obj.left - bound.left - realBound.x1));
obj.setCoords();
canvas.renderAll();
});
}
// ---------------------------
// set up canvas
var canvas = new fabric.Canvas('c');
var path = new fabric.Path('M 0 0 L 150 50 L 120 150 z');
path.set({
left: 170,
top: 30,
fill: 'rgba(0, 128, 0, 0.5)',
stroke: '#000',
strokeWidth: 4,
strokeLineCap: 'square',
angle: 65
});
canvas.add(path);
canvas.setActiveObject(path);
var circle = new fabric.Circle({
left: 370,
top: 30,
radius: 45,
fill: 'blue',
scaleX: 1.5,
angle: 30
});
canvas.add(circle);
canvas.forEachObject(function(obj) {
var setCoords = obj.setCoords.bind(obj);
obj.on({
moving: setCoords,
scaling: setCoords,
rotating: setCoords
});
});
canvas.on('after:render', function() {
canvas.contextContainer.strokeStyle = 'red';
canvas.forEachObject(function(obj) {
getImageData(obj.toDataURL())
.then(function(data) {
var boundRect = obj.getBoundingRect();
var realBound = scanPixels(data);
canvas.contextContainer.strokeRect(
boundRect.left + realBound.x1,
boundRect.top + realBound.y1,
realBound.x2 - realBound.x1,
realBound.y2 - realBound.y1
);
});
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.3/fabric.min.js"></script>
<p> </p>
<button onclick="alignLeft()">align left (default)</button>
<button onclick="alignLeftbyBoundRect()">align left (by bounding rect)</button>
<button onclick="alignRealLeft()">align REAL left (by pixel)</button>
<p></p>
<canvas id="c" width="600" height="250" style="border: 1px solid rgb(204, 204, 204); touch-action: none; user-select: none;" class="lower-canvas"></canvas>
<p></p>
I'm trying to to make it possible for an image on a canvas to have a box over it with low opacity, to make it be shown that it has been selected. So I need an onclick function, but i'm stuck on what to do. Any advice? I would aslo appreciate if someone could point me in the right direction on how to make the box also pulsate.
var image1 = new Kinetic.Image({
image: imageObj,
x: xposition,
y: yposition,
width: width,
height: height,
stroke: 'blue',
srokeFill: 'red',
strokeWidth: 5,
draggable: true,
dragBoundFunc: function (pos) {
if (pos.x < this.minX)
this.minX = pos.x;
return {
x: pos.x,
y: this.getAbsolutePosition().y
}
}
});
layer.on('mouseover', function (evt) {
var shape = evt.target;
document.body.style.cursor = 'pointer';
shape.strokeEnabled(true);
layer.draw();
});
layer.on('mouseout', function (evt) {
var shape = evt.target;
document.body.style.cursor = 'default';
shape.strokeEnabled(false);
layer.draw();
});
When making a clickable region on a canvas you need to.
- Create a redraw function.
- Find the mouse point inside of canvas.
- Do collision detection on the mouse point and region.
update: added multiple clickable regions.
// x1, y1, x2, y2, clicked
var boxes = [{
x1: 10,
y1: 10,
x2: 110,
y2: 110,
clicked: false
}, {
x1: 120,
y1: 10,
x2: 220,
y2: 110,
clicked: false
}];
function go() {
var can = document.getElementById('can');
var ctx = can.getContext('2d');
var w = can.width;
var h = can.height;
makeNoise(ctx);
var imageData = ctx.getImageData(0, 0, w, h);
var mx = 0;
var my = 0;
$('#can').mousemove(function(event) {
var offset = $(this).offset();
mx = event.pageX - offset.left;
my = event.pageY - offset.top;
redraw();
});
$('#can').click(function(event) {
var offset = $(this).offset();
mx = event.pageX - offset.left;
my = event.pageY - offset.top;
for (var i = 0, len = boxes.length; i < len; i++) {
var b = boxes[i];
if (hit(b.x1, b.y1, b.x2, b.y2)) {
b.clicked = !b.clicked;
}
}
redraw();
});
function redraw() {
// Draw the original image
ctx.putImageData(imageData, 0, 0);
// Draw the clickable region
for (var i = 0, len = boxes.length; i < len; i++) {
drawBox(boxes[i]);
}
// Draw the mouse. Probably only needed when testing
// mouse location.
drawMouse();
}
redraw();
// Collision dection of a square against mouse point.
// square points x1,y1 must be less than x2,y2
function hit(x1, y1, x2, y2) {
if (mx < x1 || my < y1 || mx > x2 || my > y2) return false;
return true;
}
function drawMouse() {
ctx.moveTo(mx, my);
ctx.fillStyle = "yellow";
ctx.beginPath();
ctx.arc(mx, my, 5, 0, Math.PI * 360);
ctx.fill();
}
function drawBox(b) {
if (b.clicked) {
// Mouse clicked.
ctx.fillStyle = "rgba(255,0,255,.5)"
} else if (hit(b.x1, b.y1, b.x2, b.y2)) {
// Mouse Over
ctx.fillStyle = "rgba(255,255,255,.5)"
} else {
// Mouse Out
ctx.fillStyle = "red";
}
ctx.fillRect(b.x1, b.y1, b.x2 - b.x1, b.y2 - b.y1);
}
}
// Fill a canvas context with noise.
function makeNoise(ctx) {
var can = ctx.canvas;
var imageData = ctx.getImageData(0, 0, can.width, can.height);
var d = imageData.data;
for (var i = 0, len = d.length; i < len; i += 4) {
d[i] = d[i + 1] = d[i + 2] = (Math.random() * 8) << 4;
d[i + 3] = 255;
}
ctx.putImageData(imageData, 0, 0);
}
go();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="can" width="400" height="300"></canvas>