I'm working on a webapp and it includes one part where I draw the graph of a function, the coordinate system is made by Canvas. The problem is, I can not zoom into my coordinate system. I want to make it able to zoom in and out + moving the coordinate system using the mouse. The x and y values should also increase/decrease while zooming in/out.
Could somebody help me with this ?
I searched for some solutions, but I couldn't find anything useful. That's why I decided to ask it here.
Here are my codes:
<canvas id="myCanvas" width="300" height="300" style="border:1px solid #d3d3d3;"></canvas>
<!--Canva startup-->
<script>
// Setup values
var height = 300;
var width = 300;
var zoomFactor = 15;
// --------
var c = document.getElementById("myCanvas");
var xZero = width / 2;
var yZero = height / 2;
var ctx = c.getContext("2d");
// Draw Cord-System-Grid
ctx.beginPath();
ctx.moveTo(xZero, 0);
ctx.lineTo(xZero, height);
ctx.strokeStyle = "#000000";
ctx.stroke();
ctx.moveTo(0, yZero);
ctx.lineTo(width, yZero);
ctx.strokeStyle = "#000000";
ctx.stroke();
ctx.beginPath();
// Draw Numbers
ctx.font = "10px Georgia";
var heightTextX = yZero + 10;
for(var i = 0; i < width; i = i + width / 10) {
var numberX = (-1 * xZero / zoomFactor) + i / zoomFactor;
ctx.fillText(numberX, i, heightTextX);
}
var heightTextY = yZero + 10;
for(var n = 0; n < height; n = n + height / 10) {
var numberY = (-1 * yZero / zoomFactor) + n / zoomFactor;
if(numberY !== 0)
ctx.fillText(numberY * -1, heightTextY, n);
}
</script>
I asked this question before a week, but couldn't get an answer.
I hope somebody can help me
Attach the onwheel event to a function and use the ctx.scale() function to zoom in and out.
You will probably want to do something like
let canvas = document.getElementById('myCanvas');
ctx.translate(canvas.width/2, canvas.height/2);
ctx.scale(zoomFactor, zoomFactor);
ctx.translate(-canvas.width/2, -canvas.height/2);
To make sure it zooms from the center.
Sorry for any minor errors I'm writing this from my phone.
Related
I have this canvas where I place a letter randomly within the canvas.
var w = Math.random() * canvas.width;
var h = Math.random() * canvas.height;
drawRandomCircle(canvas,w,h);
function drawRandomCircle(canvas,w,h)
{
var fontSize = '35';
var ctx = canvas.getContext("2d");
var color = 'rgba(245, 66, 66,1.0)';
ctx.fillStyle = color;
ctx.font = fontSize + 'pt Arial';
ctx.fillText('O', w, h);
}
The results:
I would like to improve further on the function to include an offset in % from the canvas boundary to limit where the letter will appear.
The results would be similar to something similar to this.
Any ideas?
You need to take into account the 10% on the borders.
Try the following which uses this principle... but also remember that the co-ordinates for the canvas are top-left based... but when you do the font it will go UP (not down) so you have to take that into account as well.
var canvas = document.getElementsByTagName("canvas")[0];
var fontSizePx = 35;
// Get 10% of the width/height
var cw = (canvas.width / 10);
var ch = (canvas.height / 10);
// Get 80% of the width/height but minus the size of the font
var cw80 = (cw * 8) - fontSizePx;
var ch80 = (ch * 8) - fontSizePx;
for(var i = 0; i < 10; i++) {
// Get random value within center 80%
var w = (Math.random() * cw80) + cw;
// Add on the size of the font to move it down
var h = (Math.random() * ch80) + ch + fontSizePx;
drawRandomCircle(canvas,w,h);
}
function drawRandomCircle(canvas,w,h) {
var ctx = canvas.getContext("2d");
var color = 'rgba(245, 66, 66,1.0)';
ctx.fillStyle = color;
ctx.font = fontSizePx.toString() + 'px Arial';
ctx.fillText('O', w, h);
}
canvas {
border:1px solid black;
}
<canvas></canvas>
I'm trying to fill a rectangle with diagonal lines at 30 degrees that don't get clipped by the canvas. Each line should start and end on the edges of the canvas, but do not go outside the canvas.
I've gotten somewhat of a result but is struggling to understand how I can fix the ends so the lines become evenly distributed:
Here is the code I got so far:
const myCanvas = document.getElementById("myCanvas");
const _ctx = myCanvas.getContext("2d");
const canvasWidth = 600;
const canvasHeight = 300;
// Helper function
const degToRad = (deg) => deg * (Math.PI / 180);
const angleInDeg = 30;
const spaceBetweenLines = 16;
const lineThickness = 16;
_ctx.fillStyle = `black`;
_ctx.fillRect(0, 0, canvasWidth, canvasHeight);
const step = (spaceBetweenLines + lineThickness) / Math.cos(angleInDeg * (Math.PI / 180));
for (let distance = -canvasHeight + step; distance < canvasWidth; distance += step) {
let x = 0;
let y = 0;
if(distance < 0) {
// Handle height
y = canvasHeight - (distance + canvasHeight);
} else {
// Handle height
x = distance;
}
const lineLength = canvasHeight - y;
const slant = lineLength / Math.tan(degToRad((180 - 90 - angleInDeg)));
const x2 = Math.min(x + slant, canvasWidth);
const y2 = y + lineLength;
_ctx.beginPath();
_ctx.moveTo(x, y);
_ctx.lineTo(x2, y2);
_ctx.lineWidth = lineThickness;
_ctx.strokeStyle = 'green';
_ctx.stroke();
}
and a JSFiddle for the code.
What I'm trying to achieve is drawing a pattern where I can control the angle of the lines, and that the lines are not clipped by the canvas. Reference photo (the line-ends don't have flat):
Any help?
My example is in Javascript, but it's more the logic I'm trying to wrap my head around. So I'm open to suggestions/examples in other languages.
Update 1
If the angle of the lines is 45 degree, you will see the gutter becomes correct on the left side. So I'm suspecting there is something I need to do differently on my step calculations.
My current code is based on this answer.
Maybe try something like this for drawing the lines
for(var i = 0; i < 20; i++){
_ctx.fillrect(i * spaceBetweenLines + lineThickness, -100, canvas.height + 100, lineThickness)
}
I'm really struggling with a couple problems in the HTML5 canvas.
I've posted the project to GitHub pages (https://swedy13.github.io/) and added an image (the circles are in motion) so you can see the issue. Basically, if you scroll down you'll find several green circles bouncing around on the page. I'd like to replace those with my client logos.
I'm calling requestAnimation from three files based on different actions, all of which can be found in https://github.com/swedy13/swedy13.github.io/tree/master/assets/js
Filenames:
- filters.js (calls requestAnimation when you use the filters)
- main.js (on load and resize)
- portfolio.js (this is where the canvas code is)
Update: I've added the "portfolio.js" code below so the answer can be self-contained.
function runAnimation(width, height, type){
var canvas = document.getElementsByTagName('canvas')[0];
var c = canvas.getContext('2d');
// ---- DIMENSIONS ---- //
// Container
var x = width;
var y = height - 65;
canvas.width = x;
canvas.height = y;
var container = {x: 0 ,y: 0 ,width: x, height: y};
// Portrait Variables
var cPos = 200;
var cMargin = 70;
var cSpeed = 3;
var r = x*.075;
if (y > x && x >= 500) {
cPos = x * (x / y) - 150;
cMargin = 150;
}
// Landscape Variables
if (x > y) {
cPos = y * (y / x) - 50;
cMargin = 150;
cSpeed = 3;
r = x*.05;
}
// ---- CIRCLES ---- //
// Circles
var circles = [];
var img = new Image();
// Gets active post ids and count
var activeName = [];
var activeLogo = [];
var activePosts = $('.active').map(function() {
activeName.push($(this).text().replace(/\s+/g, '-').toLowerCase());
// Returns the image source
/*activeLogo.push($(this).find('img').prop('src'));*/
// Returns an image node
var elem = document.getElementsByClassName($(this).text().replace(/\s+/g, '-').toLowerCase())
activeLogo.push(elem[0].childNodes[0]);
});
// Populates circle data
for (var i = 0; i < $('.active').length; i++) {
circles.push({
id:activeName[i],
r:r,
color: 100,
/*image: activeLogo[i],*/
x:Math.random() * cPos + cMargin,
y:Math.random() * cPos + cMargin,
vx:Math.random() * cSpeed + .25,
vy:Math.random() * cSpeed + .25
});
}
// ---- DRAW ---- //
requestAnimationFrame(draw);
function draw(){
c.fillStyle = 'white';
c.fillRect(container.x, container.y, container.width, container.height);
for (var i = 0; i < circles.length; i++){
/*var img = new Image();
var path = circles[i].image;*/
/*var size = circles[i].r * 2;*/
/*img.src = circles[4].image;*/
var img = activeLogo[i];
img.onload = function (circles) {
/*c.drawImage(img, 0, 0, size, size);*/
var pattern = c.createPattern(this, "repeat");
c.fillStyle = pattern;
c.fill();
};
c.fillStyle = 'hsl(' + circles[i].color + ', 100%, 50%)';
c.beginPath();
c.arc(circles[i].x, circles[i].y, circles[i].r, 0, 2*Math.PI, false);
c.fill();
// If the circle size/position is greater than the canvas width, bounce x
if ((circles[i].x + circles[i].vx + circles[i].r > container.width) || (circles[i].x - circles[i].r + circles[i].vx < container.x)) {
circles[i].vx = -circles[i].vx;
}
// If the circle size/position is greater than the canvas width, bounce y
if ((circles[i].y + circles[i].vy + circles[i].r > container.height) || (circles[i].y - circles[i].r + circles[i].vy < container.y)){
circles[i].vy = -circles[i].vy;
}
// Generates circle motion by adding position and velocity each frame
circles[i].x += circles[i].vx;
circles[i].y += circles[i].vy;
}
requestAnimationFrame(draw);
}
}
The way it works right now is:
1. I have my portfolio content set to "display: none" (eventually it will be a pop-up when they click on one of the circles).
2. The canvas is getting the portfolio objects from the DOM, including the image that I can't get to work.
3. If I use the "onload()" function, I can get the images to show up and repeat in the background. But it's just a static background - the circles are moving above it and revealing the background. That isn't what I want.
So basically, I'm trying to figure out how to attach the background image to the circle (based on the circle ID).
----------------- UPDATE -----------------
I can now clip the image to a circle and get the circle to move in the background. But it isn't visible on the page (I can tell it's moving by console logging it's position). The only time I see anything is when the circle lines up with the images position, then it shows.
function runAnimation(width, height, type){
var canvas = document.getElementsByTagName('canvas')[0];
var c = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
// Collects portfolio information from the DOM
var activeName = [];
var activeLogo = [];
$('.active').map(function() {
var text = $(this).text().replace(/\s+/g, '-').toLowerCase();
var elem = document.getElementsByClassName(text);
activeName.push(text);
activeLogo.push(elem[0].childNodes[0]);
});
var img = new Image();
img.onload = start;
var circles = [];
var cPos = 200;
var cMargin = 70;
var cSpeed = 3;
for (var i = 0; i < 1; i++) {
circles.push({
id: activeName[i],
img: activeLogo[i],
size: 50,
xPos: Math.random() * cPos + cMargin,
yPos: Math.random() * cPos + cMargin,
xVel: Math.random() * cSpeed + .25,
yVel: Math.random() * cSpeed + .25,
});
img.src = circles[i].img;
}
requestAnimationFrame(start);
function start(){
for (var i = 0; i < circles.length; i++) {
var circle = createImageInCircle(circles[i].img, circles[i].size, circles[i].xPos, circles[i].yPos);
c.drawImage(circle, circles[i].size, circles[i].size);
animateCircle(circles[i]);
}
requestAnimationFrame(start);
}
function createImageInCircle(img, radius, x, y){
var canvas2 = document.createElement('canvas');
var c2 = canvas2.getContext('2d');
canvas2.width = canvas2.height = radius*2;
c2.fillStyle = 'white';
c2.beginPath();
c2.arc(x, y, radius, 0, Math.PI*2);
c2.fill();
c2.globalCompositeOperation = 'source-atop';
c2.drawImage(img, 0, 0, 100, 100);
return(canvas2);
}
function animateCircle(circle) {
// If the circle size/position is greater than the canvas width, bounce x
if ((circle.xPos + circle.xVel + circle.size > canvas.width) || (circle.xPos - circle.size + circle.xVel < 0)) {
console.log('Bounce X');
circle.xVel = -circle.xVel;
}
// If the circle size/position is greater than the canvas width, bounce y
if ((circle.yPos + circle.yVel + circle.size > canvas.height) || (circle.yPos + circle.yVel - circle.size < 0)) {
console.log('Bounce Y');
circle.yVel = -circle.yVel;
}
// Generates circle motion by adding position and velocity each frame
circle.xPos += circle.xVel;
circle.yPos += circle.yVel;
}
}
I'm not sure if I'm animating the correct thing. I've tried animating canvas2, but that didn't make sense to me.
PS - Sorry for the GitHub formatting, not sure why it looks like that.
PPS - Apologies for any junk code I didn't clean up. I've tried a lot of stuff and probably lost track of some of the changes.
PPPS - And forgive me for not making the answer self-contained. I thought linking to GitHub would be more useful, but I've updated the question to contain all the necessary info. Thanks for the feedback.
To get you started...
Here's how to clip an image into a circle using compositing.
The example code creates a single canvas logo-ball that you can reuse for each of your bouncing balls.
var logoball1=dreateImageInCircle(logoImg1,50);
var logoball2=dreateImageInCircle(logoImg2,50);
Then you can draw each logo-ball onto your main canvas like this:
ctx.drawImage(logoball1,35,40);
ctx.drawImage(logoball2,100,75);
There are many examples here on Stackoverflow of how to animate the balls around the canvas so I leave that part to you.
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var img=new Image();
img.onload=start;
img.src="https://dl.dropboxusercontent.com/u/139992952/m%26m600x455.jpg";
function start(){
var copy=createImageInCircle(img,50);
ctx.drawImage(copy,20,75);
ctx.drawImage(copy,150,120);
ctx.drawImage(copy,280,75);
}
function createImageInCircle(img,radius){
var c=document.createElement('canvas');
var cctx=c.getContext('2d');
c.width=c.height=radius*2;
cctx.beginPath();
cctx.arc(radius,radius,radius,0,Math.PI*2);
cctx.fill();
cctx.globalCompositeOperation='source-atop';
cctx.drawImage(img,radius-img.width/2,radius-img.height/2);
return(c);
}
body{ background-color:white; }
#canvas{border:1px solid red; }
<canvas id="canvas" width=512 height=512></canvas>
I've been working on an isometric game engine for my own game. Currently, it's a big, open world with the map data being retrieved dynamically from the Node.js server.
To understand what I'm doing... for the most part it's a tile based world. So each map has a max number of cols,rows (19) and each world has a max number of maps by col,row (6). So it's a 6x6 world map consisting of 19x19 tiles per map. Whenever the players move onto a new map/region, the client requests a 3x3 matrix of the surrounding maps with the center map being the map the player is currently on. This part is pretty well optimized.
My problem, however, is finding a great way to optimize the drawing onto the canvas. Currently, I don't have a lot of lag doing so, but I also have a fast computer, but I worry that at times it could cause others to lag / mess with the rendering of other graphics.
Basically, how I have it working right now is when the data is sent back from the server, it adds each map and all the tile images for each col/row it has to render into a buffer. Each loop of the game loop, it will basically render a small section of the 25 tiles onto the specific map's hidden canvas. When all of the requested maps are done rendering (after a few game loops), the camera will go ahead and merge these hidden maps into 1 big map canvas of the 3x3 matrix (by slicing parts from the hidden canvases and merging them onto the new canvas).
Ideally I would love this whole process to be async. but I've been looking into web workers and apparently they do not support canvas well. Has anyone come up with a process to do something similar and keep it well optimized?
Thanks!
Here's an example of rendering a 19x19 grid in each frame. A new random tile is added from right to left top to bottom in each frame. The grid is rendered in the same order and you can see that this works for overlapping tiles.
I think it's best to save each tile and make a function that renders the entire grid. So if the player gets updates in the 3x3 surrounding area then download and keep those tiles and re-render the entire grid.
update
I provided a function to eliminate overdraw and a toggle. This may increase performance for some people. It draws from bottom to top left to right. This draws the overlaying items first and with globalCompositeOperation "distination-over" tells the canvas to leave existing pixels alone when adding new content. This should mean less work to do in putting pixels on the canvas as it's not drawing over unused pixels.
var cols = 19;
var tile_width = 32;
var rows = 19;
var tile_height = 16;
var y_offset = 64;
var h_tw = tile_width / 2;
var h_th = tile_height / 2;
var frames = 0;
var fps = "- fps";
setInterval(function(){
fps = frames + " fps";
frames = 0;
}, 1000);
var can = document.getElementById('tile');
var ctx = can.getContext('2d');
var wcan = document.getElementById('world');
var wctx = wcan.getContext('2d');
wcan.width = cols * tile_width;
wcan.height = rows * tile_height + y_offset;
var tiles = initTiles();
document.getElementById('toggle').addEventListener('click', function() {
if (this.innerHTML == 'renderWorld') {
renderFn = renderWorldNoOverdraw;
this.innerHTML = "renderWorldNoOverdraw";
} else {
renderFn = renderWorld;
this.innerHTML = "renderWorld";
}
});
//renderWorld();
var ani_x = cols;
var ani_y = 0;
var renderFn = renderWorld;
ani();
function initTiles () {
var tiles = [];
for (var y = 0; y < rows; y++) {
var row = [];
for (var x = 0; x < cols; x++) {
var can = document.createElement('canvas');
can.width=tile_width;
can.height=tile_height+y_offset;
row[x]=can;
}
tiles[y] = row;
}
return tiles;
}
function ani() {
var can = tiles[ani_y][--ani_x]
if (ani_x == 0) ani_x = cols, ani_y++;
ani_y %= rows;
var ctx = can.getContext('2d');
randTile(can, ctx);
renderFn();
requestAnimationFrame(ani);
}
// renders from bottom left to right and skips
// drawing over pixels already present.
function renderWorldNoOverdraw() {
frames++;
wctx.clearRect(0,0,wcan.width,wcan.height);
wctx.save();
wctx.globalCompositeOperation = "destination-over";
wctx.translate(0, y_offset);
var x_off = 0;
var y_off = 0;
var y_off2 = 0;
for (var y = rows; y--;) {
x_off = (cols * h_tw)- ((rows-y) * h_tw);
y_off = y * h_th + tile_height;
y_off2 = y_off;
for (var x = 0; x < cols; x++) {
var can = tiles[y][x];
wctx.drawImage(can, x_off, y_off2 + y_offset);
y_off2 -= h_th;
x_off += h_tw;
}
}
wctx.translate(0,-y_offset);
wctx.fillStyle = "#ddaadd";
wctx.fillRect(0,0,wcan.width, wcan.height);
wctx.restore();
wctx.fillStyle= "black";
wctx.fillText(fps, 10, 10);
}
function renderWorld() {
frames++;
wctx.fillStyle = "#CCEEFF";
wctx.fillRect(0, 0, wcan.width, wcan.height);
wctx.save();
wctx.translate(0, y_offset);
var x_off = 0;
var y_off = 0;
var y_off2 = 0;
for (var y = 0; y < rows; y++) {
x_off = (cols * h_tw) + (y * h_tw) - h_tw;
y_off = y * h_th;
y_off2 = y_off;
for (var x = cols; x--;) {
var can = tiles[y][x];
wctx.drawImage(can, x_off, y_off2 - 64);
y_off2 += h_th;
x_off -= h_tw;
}
y_off += h_th;
x_off -= h_tw;
}
wctx.restore();
wctx.fillStyle= "black";
wctx.fillText(fps, 10, 10);
}
function randTile(can, ctx) {
var maxH = can.height - 24;
var ranH = Math.floor(Math.random() * maxH);
var h = Math.max(ranH, 1);
ctx.clearRect(0, 0, can.width, can.height);
ctx.beginPath();
ctx.save();
ctx.translate(0, can.height - 16);
ctx.moveTo(0, 8);
ctx.lineTo(16, 0);
ctx.lineTo(32, 8);
ctx.lineTo(16, 16);
ctx.lineTo(0, 8);
ctx.strokeStyle = "#333333";
ctx.stroke();
// random floor color
var colors = ["#dd9933", "#22aa00", "#66cccc", "#996600"];
ctx.fillStyle = colors[Math.floor(Math.random() * 4)];
ctx.fill();
// random building
if (Math.floor(Math.random() * 8) == 0) {
ctx.beginPath();
ctx.moveTo(8, 8);
ctx.lineTo(8, -h - 4);
ctx.lineTo(16, -h);
ctx.lineTo(16, 12);
ctx.lineTo(8, 8);
ctx.stroke();
ctx.fillStyle = "#333333";
ctx.fill();
ctx.beginPath();
ctx.moveTo(16, 12);
ctx.lineTo(16, -h);
ctx.lineTo(24, -h - 4);
ctx.lineTo(24, 8);
ctx.lineTo(16, 12);
ctx.stroke();
ctx.fillStyle = "#999999";
ctx.fill()
ctx.beginPath();
ctx.moveTo(16, -h);
ctx.lineTo(24, -h - 4);
ctx.lineTo(16, -h - 8);
ctx.lineTo(8, -h - 4);
ctx.moveTo(16, -h);
ctx.stroke();
ctx.fillStyle = "#CCCCCC";
ctx.fill()
}
ctx.restore();
}
body {
background-color: #444444;
}
<button id="toggle">renderWorld</button><br/>
<canvas id='tile' width="32" height="32" style="display:none"></canvas>
<canvas id="world" width="608" height="368">
</canvas>
I'm trying to build a pyramid using squares in HTML5 Canvas, I have an algoritm that is half working, the only problem is that after three days and some lack of math abilities I haven't been able to find the proper formula.
Here is what I have, check the code comments so you can see what part of the algorithm we have to change.
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var W = 1000; var H = 600;
var side = 16;
canvas.width = W;
canvas.height = H;
function square(x, y) {
ctx.fillStyle = '#66FF00';
ctx.fillRect(x, y, side, side);
ctx.strokeStyle = '#000';
ctx.strokeRect(x, y, side, side);
}
function draw() {
ctx.fillRect(0, 0, W, H);
ctx.save();
for(var i = 0; i < 30; i++) {
for(var j = 0; j < i + 1; j++) {
square(
//Pos X
//This is what we have to change to
//make it look like a pyramid instead of stairs
W / 2 - ((side / 2) + (j * side)),
//Pos Y
side * (i + 1)
);
}
}
ctx.restore();
}
//STARTS DRAWING
draw();
This is the code working in jsfiddle so we can try it:
https://jsfiddle.net/g5spscpu/
The desired result is:
Well, I would love if someone could give me a hand, my brain is burning.
You need to use the i index in the formula for X position with:
W/2 - ((side / 2) + ((j - i/2) * side))
see https://jsfiddle.net/9esscdkc/