Related
window.onload = function(){
theVideo();
playVideo();
Move();
Draw();
};
let objectInfo = {
canvas: null,
context: null,
// Number of sprites
numberOfFrames: 16,
image: null,
imageWidth: 128,
imageHeight: 192,
frameIndex: 0,
frameWidth: 0,
// Animation interval (ms)
msInterval: 1000,
x: 10,
y: 10,
};
const imageFile = "shaggy.png";
function Draw(){
objectInfo.context.drawImage(myImage, shift, 0, frameWidth, frameHeight, 120, 25, frameWidth, frameHeight);
}
//image setup
window.onload= function () {
// Canvas setup
objectInfo.canvas = document.querySelector("#myCanvas");
objectInfo.context = objectInfo.canvas.getContext("2d");
// Image setup
objectInfo.image = new Image();
objectInfo.image.onload = function() {
// The this object refers to image because within image onload event handler
objectInfo.imageWidth = this.width;
objectInfo.imageHeight = this.height;
// Calculate framewidth (size of each sprite)
objectInfo.frameWidth = objectInfo.imageWidth / objectInfo.numberOfFrames;
};
// Load image
objectInfo.image.src = imageFile;
};
var xPos = 0;
var yPos = 0;
//move image
function Move(e){
//right
if(e.keyCode==39){
xPos+=5;
}
//left
if(e.keyCode==37){
xPos-=5;
}
//up
if(e.keyCode==38){
yPos-=5;
}
//down
if(e.keyCode==40){
yPos+=5;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Sprite</title>
<meta charset="utf-8">
<meta name="author" content="Peyton">
<meta name="description" content="115">
<link rel= 'stylesheet' href="p4.css">
<script src="p4.js"> </script>
<style>canvas { border: 1px solid black; }</style>
<body>
<canvas width= "1300" height= "600" id= "myCanvas">
<video id="video" controls >
<source src="ScoobyDooV.mp4"/>
<source src="ScoobyDooV.ogv"/>
</video>
</canvas>
</body>
</html>
<nav>
</nav>
<main id="#wrapper"><br>
</main>
</body>
</html>
I'm really new to coding and am not sure what I am missing to call my sprite and draw the first image onto the canvas. I later have to call each measurement of my sprite and assign to a function keydown event to make it look like its walking each direction so if I could get any guidance on that too that would be great.
It looks like you're calling Draw before the image is loaded. Try placing the Draw() call within the image.onload method.
You are defining window.onload twice, only one out of your two callbacks will be executed
Your code is completely not suited to the task you are attempting. Animations require regular rendering. There is a huge amount of missing code in your example so i can really solve any problems directly.
So just thought I would give an example of how to load, animate, and render sprites sheets.
Sprite sheets
There are many ways to handle sprite sheets, though I find that using a standard method for all sprite sheets makes life easy.
Some sprite sheets have a regular layout and evenly spaced sprites, other sprite sheets, have been packed together to conserve pixels and memory.
Each sprite has a location on the sheet, the top left corner and the size as width and height.
You can attach an array of these locations to an image
For example the next function creates sprites for a regular layout (like image in your question)
function createSprites(width, height, columns, rows, image) {
const sprites = [];
var w = width / columns;
var h = height / rows;
var ix, iy;
for (iy = 0; iy < rows; iy++) {
for (ix = 0; ix < columns; ix++) {
const x = ix * w;
const y = iy * h;
sprites.push({ x, y, w, h });
}
}
image.sprites = sprites;
}
The array is added to the img so you don't have to add additional management
You can then draw the sprite by creating a custom draw function.
function drawSprite(img, sprIndex, x, y) {
const spr = img.sprites[sprIndex];
ctx.drawImage(img,
spr.x, spr.y, spr.w, spr.h, // location on sprite sheet
x , y , // location on canvas
spr.w, spr.h, // size on canvas;
);
}
You pass the sprite sheet image, the sprite index in the sprite array, and the location you want to draw the sprite.
Easy as
Because you likely know the size of the sprite sheet, and the location of the sprites you don't have to wait for the image to load to attach the sprite data.
const ctx = canvas.getContext("2d");
const spriteSheet = new Image;
spriteSheet.src = "https://i.stack.imgur.com/hOrC1.png";
// The image size is known so you dont have to wait for it to load
createSprites(128, 192, 4, 4, spriteSheet); // add a array of sprite locations
// It is important that the sprite sizes are integers
// width must be divisible by columns and height by rows
function createSprites(width, height, columns, rows, image) {
const sprites = [];
var w = width / columns;
var h = height / rows;
var ix, iy;
for (iy = 0; iy < rows; iy++) {
for (ix = 0; ix < columns; ix++) {
const x = ix * w;
const y = iy * h;
sprites.push({ x, y, w, h });
}
}
image.sprites = sprites;
}
function drawSprite(img, sprIndex, x, y) {
const spr = img.sprites[sprIndex];
ctx.drawImage(img,
spr.x, spr.y, spr.w, spr.h, // location on sprite sheet
x , y , // location on canvas
spr.w, spr.h, // size on canvas;
);
}
const walkerInfo = {
framesPerDir: 4,
movements: [{x: 0,y: 3 },{ x: -5, y: 0 }, { x: 5, y: 0 }, { x: 0, y: -3 } ],
}
const walker = {
dir: 0, // 0,1,2,3
time: 0, // time in Frames
rate: 0, // steps per frame
x: 0, // position
y: 0, //
update() {
this.time += 1;
// only move when sprite frame changes
if ((this.time % this.rate) === 0) {
this.x += walkerInfo.movements[this.dir].x;
this.y += walkerInfo.movements[this.dir].y;
if(this.x < -128 || this.x > canvas.width ||
this.y < -192 || this.y > canvas.height) {
this.x = randI(canvas.width);
this.y = randI(canvas.height);
this.dir = randI(4)
this.rate = randI(6, 12)
}
}
if(randI(1000) === 0){
this.dir = (this.dir + (randI(2) ? 2 : 1)) % 4;
this.rate = randI(6, 12)
}
},
draw() {
var index = this.dir * walkerInfo.framesPerDir;
index += (this.time / this.rate | 0) % walkerInfo.framesPerDir;
drawSprite(
spriteSheet, index,
this.x, this.y
);
}
}
function createWalker(x = randI(w), y = randI(h), dir = randI(4), rate = randI(6, 18)) {
return { ...walker, x, y, dir, rate, time: randI(100) };
}
const walkers = [];
// main update function
function update(timer) {
globalTime = timer;
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
if (w !== innerWidth || h !== innerHeight) {
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
} else {
ctx.clearRect(0, 0, w, h);
}
if (spriteSheet.complete) { // has the image loaded
if (randI(walkers.length) === 0) { // odd 1/100 to create a walker
walkers.push(createWalker());
}
walkers.sort((a,b)=>a.y - b.y);
eachOf(walkers, walk => walk.update());
eachOf(walkers, walk => walk.draw());
}
requestAnimationFrame(update);
}
requestAnimationFrame(update);
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var globalTime;
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const eachOf = (array, cb) => {
var i = 0;
const len = array.length;
while (i < len && cb(array[i], i++, len) !== true);
};
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
I'm working through instructions to construct an interactive particle logo design and can't seem to get to the finished product. This is the logo image file -
I'm using a canvas structure / background. Here's the code -
var canvasInteractive = document.getElementById('canvas-interactive');
var canvasReference = document.getElementById('canvas-reference');
var contextInteractive = canvasInteractive.getContext('2d');
var contextReference = canvasReference.getContext('2d');
var image = document.getElementById('img');
var width = canvasInteractive.width = canvasReference.width = window.innerWidth;
var height = canvasInteractive.height = canvasReference.height = window.innerHeight;
var logoDimensions = {
x: 500,
y: 500
};
var center = {
x: width / 2,
y: height / 2
};
var logoLocation = {
x: center.x - logoDimensions.x / 2,
y: center.y - logoDimensions.y / 2
};
var mouse = {
radius: Math.pow(100, 2),
x: 0,
y: 0
};
var particleArr = [];
var particleAttributes = {
friction: 0.95,
ease: 0.19,
spacing: 6,
size: 4,
color: "#ffffff"
};
function Particle(x, y) {
this.x = this.originX = x;
this.y = this.originY = y;
this.rx = 0;
this.ry = 0;
this.vx = 0;
this.vy = 0;
this.force = 0;
this.angle = 0;
this.distance = 0;
}
Particle.prototype.update = function() {
this.rx = mouse.x - this.x;
this.ry = mouse.y - this.y;
this.distance = this.rx * this.rx + this.ry * this.ry;
this.force = -mouse.radius / this.distance;
if (this.distance < mouse.radius) {
this.angle = Math.atan2(this.ry, this.rx);
this.vx += this.force * Math.cos(this.angle);
this.vy += this.force * Math.sin(this.angle);
}
this.x += (this.vx *= particleAttributes.friction) + (this.originX - this.x) * particleAttributes.ease;
this.y += (this.vy *= particleAttributes.friction) + (this.originY - this.y) * particleAttributes.ease;
};
function init() {
contextReference.drawImage(image, logoLocation.x, logoLocation.y);
var pixels = contextReference.getImageData(0, 0, width, height).data;
var index;
for (var y = 0; y < height; y += particleAttributes.spacing) {
for (var x = 0; x < width; x += particleAttributes.spacing) {
index = (y * width + x) * 4;
if (pixels[++index] > 0) {
particleArr.push(new Particle(x, y));
}
}
}
};
init();
function update() {
for (var i = 0; i < particleArr.length; i++) {
var p = particleArr[i];
p.update();
}
};
function render() {
contextInteractive.clearRect(0, 0, width, height);
for (var i = 0; i < particleArr.length; i++) {
var p = particleArr[i];
contextInteractive.fillStyle = particleAttributes.color;
contextInteractive.fillRect(p.x, p.y, particleAttributes.size, particleAttributes.size);
}
};
function animate() {
update();
render();
requestAnimationFrame(animate);
}
animate();
document.body.addEventListener("mousemove", function(event) {
mouse.x = event.clientX;
mouse.y = event.clientY;
});
document.body.addEventListener("touchstart", function(event) {
mouse.x = event.changedTouches[0].clientX;
mouse.y = event.changedTouches[0].clientY;
}, false);
document.body.addEventListener("touchmove", function(event) {
event.preventDefault();
mouse.x = event.targetTouches[0].clientX;
mouse.y = event.targetTouches[0].clientY;
}, false);
document.body.addEventListener("touchend", function(event) {
event.preventDefault();
mouse.x = 0;
mouse.y = 0;
}, false);
html,
body {
margin: 0px;
position: relative;
background-color: #000;
}
canvas {
display: block;
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
img {
display: none;
width: 70%;
height: 400px;
position: absolute;
left: 50%;
transform: translate(-50%, 30%);
}
<html>
<body>
<canvas id="canvas-interactive"></canvas>
<canvas id="canvas-reference"></canvas>
<img src="https://i.stack.imgur.com/duv9h.png" alt="..." id="img">
</body>
</html>
My understanding is the image file has to be set to display: none; and then the image needs to be re-drawn using the javascript commands but I'm not sure if this image is compatible or not. When finished I want the image on a white background.
By way of an example the end design needs to resemble this - Logo particle design
Particle positions from bitmap.
To get the FX you want you need to create a particle system. This is just an array of objects, each with a position, the position where they want to be (Home), a vector defining their current movement, and the colour.
You get each particle's home position and colour by reading pixels from the image. You can access pixel data by rendering an image on a canvas and the using ctx.getImageData to get the pixel data (Note image must be on same domain or have CORS headers to access pixel data). As you read each pixel in turn, if not transparent, create a particle for that pixel and set it colour and home position from the pixels colour and position.
Use requestAnimationFrame to call a render function that every frame iterates all the particles moving them by some set of rules that give you the motion you are after. Once you have move each particle, render them to the canvas using simple shapes eg fillRect
Mouse interaction
To have interaction with the mouse you will need to use mouse move events to keep track of the mouse position relative to the canvas you are rendering to. As you update each particle you also check how far it is from the mouse. You can then push or pull the particle from or to the mouse (depending on the effect you want.
Rendering speed will limit the particle count.
The only issue with these types of FX is that you will be pushing the rendering speed limits as the particle count goes up. What may work well on one machine, will run very slow on another.
To avoid being too slow, and not looking good on some machines you should consider keeping an eye on the frame rate and reducing the particle count if it runs slow. To compensate you can increase the particle size or even reduce the canvas resolution.
The bottleneck is the actual rendering of each particle. When you get to large numbers the path methods really grinds down. If you want really high numbers you will have to render pixels directly to the bitmap, using the same method as reading but in reverse of course.
Example simple particles read from bitmap.
The example below uses text rendered to a canvas to create the particles, and to use an image you would just draw the image rather than the text. The example is a bit overkill as I ripped it from an old answer of mine. It is just as an example of the various ways to get stuff done.
const ctx = canvas.getContext("2d");
const Vec = (x, y) => ({x, y});
const setStyle = (ctx,style) => { Object.keys(style).forEach(key => ctx[key] = style[key]) }
const createImage = (w,h) => {var i=document.createElement("canvas");i.width=w;i.height=h;i.ctx=i.getContext("2d");return i}
const textList = ["Particles"];
var textPos = 0;
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var globalTime;
var started = false;
requestAnimationFrame(update);
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
function onResize(){
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
if (!started) { startIt() }
}
function update(timer){
globalTime = timer;
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
if (w !== innerWidth || h !== innerHeight){ onResize() }
else { ctx.clearRect(0,0,w,h) }
particles.update();
particles.draw();
requestAnimationFrame(update);
}
function createParticles(text){
createTextMap(
text, 60, "Arial",
{ fillStyle : "#FF0", strokeStyle : "#F00", lineWidth : 2, lineJoin : "round", },
{ top : 0, left : 0, width : canvas.width, height : canvas.height }
)
}
// This function starts the animations
function startIt(){
started = true;
const next = ()=>{
var text = textList[(textPos++ ) % textList.length];
createParticles(text);
setTimeout(moveOut,text.length * 100 + 12000);
}
const moveOut = ()=>{
particles.moveOut();
setTimeout(next,2000);
}
setTimeout(next,0);
}
// the following function create the particles from text using a canvas
// the canvas used is displayed on the main canvas top left fro reference.
var tCan = createImage(100, 100); // canvas used to draw text
function createTextMap(text,size,font,style,fit){
const hex = (v)=> (v < 16 ? "0" : "") + v.toString(16);
tCan.ctx.font = size + "px " + font;
var width = Math.ceil(tCan.ctx.measureText(text).width + size);
tCan.width = width;
tCan.height = Math.ceil(size *1.2);
var c = tCan.ctx;
c.font = size + "px " + font;
c.textAlign = "center";
c.textBaseline = "middle";
setStyle(c,style);
if (style.strokeStyle) { c.strokeText(text, width / 2, tCan.height / 2) }
if (style.fillStyle) { c.fillText(text, width / 2, tCan.height/ 2) }
particles.empty();
var data = c.getImageData(0,0,width,tCan.height).data;
var x,y,ind,rgb,a;
for(y = 0; y < tCan.height; y += 1){
for(x = 0; x < width; x += 1){
ind = (y * width + x) << 2; // << 2 is equiv to * 4
if(data[ind + 3] > 128){ // is alpha above half
rgb = `#${hex(data[ind ++])}${hex(data[ind ++])}${hex(data[ind ++])}`;
particles.add(Vec(x, y), Vec(x, y), rgb);
}
}
}
particles.sortByCol
var scale = Math.min(fit.width / width, fit.height / tCan.height);
particles.each(p=>{
p.home.x = ((fit.left + fit.width) / 2) + (p.home.x - (width / 2)) * scale;
p.home.y = ((fit.top + fit.height) / 2) + (p.home.y - (tCan.height / 2)) * scale;
})
.findCenter() // get center used to move particles on and off of screen
.moveOffscreen() // moves particles off the screen
.moveIn(); // set the particles to move into view.
}
// basic particle
const particle = { pos : null, delta : null, home : null, col : "black", }
// array of particles
const particles = {
items : [], // actual array of particles
mouseFX : { power : 12,dist :110, curve : 2, on : true },
fx : { speed : 0.3, drag : 0.6, size : 4, jiggle : 1 },
// direction 1 move in -1 move out
direction : 1,
moveOut () {this.direction = -1; return this},
moveIn () {this.direction = 1; return this},
length : 0,
each(callback){ // custom iteration
for(var i = 0; i < this.length; i++){ callback(this.items[i],i) }
return this;
},
empty() { this.length = 0; return this },
deRef(){ this.items.length = 0; this.length = 0 },
sortByCol() { this.items.sort((a,b) => a.col === b.col ? 0 : a.col < b.col ? 1 : -1 ) },
add(pos, home, col){ // adds a particle
var p;
if(this.length < this.items.length){
p = this.items[this.length++];
p.home.x = home.x;
p.home.y = home.y;
p.delta.x = 0;
p.delta.y = 0;
p.col = col;
}else{
this.items.push( Object.assign({}, particle,{ pos, home, col, delta : Vec(0,0) } ) );
this.length = this.items.length
}
return this;
},
draw(){ // draws all
var p, size, sizeh;
sizeh = (size = this.fx.size) / 2;
for(var i = 0; i < this.length; i++){
p = this.items[i];
ctx.fillStyle = p.col;
ctx.fillRect(p.pos.x - sizeh, p.pos.y - sizeh, size, size);
}
},
update(){ // update all particles
var p,x,y,d;
const mP = this.mouseFX.power;
const mD = this.mouseFX.dist;
const mC = this.mouseFX.curve;
const fxJ = this.fx.jiggle;
const fxD = this.fx.drag;
const fxS = this.fx.speed;
for(var i = 0; i < this.length; i++){
p = this.items[i];
p.delta.x += (p.home.x - p.pos.x ) * fxS + (Math.random() - 0.5) * fxJ;
p.delta.y += (p.home.y - p.pos.y ) * fxS + (Math.random() - 0.5) * fxJ;
p.delta.x *= fxD;
p.delta.y *= fxD;
p.pos.x += p.delta.x * this.direction;
p.pos.y += p.delta.y * this.direction;
if(this.mouseFX.on){
x = p.pos.x - mouse.x;
y = p.pos.y - mouse.y;
d = Math.sqrt(x * x + y * y);
if(d < mD){
x /= d;
y /= d;
d /= mD;
d = (1-Math.pow(d, mC)) * mP;
p.pos.x += x * d;
p.pos.y += y * d;
}
}
}
return this;
},
findCenter(){ // find the center of particles maybe could do without
var x,y;
y = x = 0;
this.each(p => { x += p.home.x; y += p.home.y });
this.center = Vec(x / this.length, y / this.length);
return this;
},
moveOffscreen(){ // move start pos offscreen
var dist,x,y;
dist = Math.sqrt(this.center.x * this.center.x + this.center.y * this.center.y);
this.each(p => {
var d;
x = p.home.x - this.center.x;
y = p.home.y - this.center.y;
d = Math.max(0.0001,Math.sqrt(x * x + y * y)); // max to make sure no zeros
p.pos.x = p.home.x + (x / d) * dist;
p.pos.y = p.home.y + (y / d) * dist;
});
return this;
},
}
canvas { position : absolute; top : 0px; left : 0px; background : black;}
<canvas id="canvas"></canvas>
Use png saved as PNG-8 and and allow cross-origin
I saw the cool article from Bricks and mortar and thought I'd try it out.
I battled with it for an eternity, thinking that my js was wrong... Turns out that the image has to be saved as a PNG-8 without dither instead of a PNG-24.
Then make sure that you add the crossOrigin="Anonymous" attribute to the image tag:
<img crossOrigin="Anonymous" id="img" src="[link to wherever you host the image]" alt="logo">
I also hid the reference canvas by adding the following styles:
canvas#canvas-reference {
display: none;
}
I also added a debounce and resize function, so it's responsive.
The result:
See Demo with inverted logo
Im currently in the process of animation some laser effects on the canvas for the purpose of making my game a bit more enjoyable.
For the purpose of that, i require the drawing and animation of "laser" weapons fire, something along the lines of Star Wars.
So far, im basicly only drawing a short line in red or blue and then drawing a thinner, white, line on top of it, so it gives the impression of a gradient.
I also use linecap = "round";
my current code:
function drawProjectile(weapon, ox, oy, x, y){
var trailEnd = getPointInDirection(weapon.projSize*-2, getAngleFromTo({x: ox, y: oy}, {x: x, y: y}), x, y);
fxCtx.lineCap = "round";
fxCtx.beginPath(); //the background, wider beam
fxCtx.moveTo(x+cam.o.x, y+cam.o.y);
fxCtx.lineTo(trailEnd.x+cam.o.x, trailEnd.y+cam.o.y);
fxCtx.closePath();
fxCtx.strokeStyle = weapon.animColor //blue or red
fxCtx.lineWidth = weapon.projSize;
fxCtx.stroke();
fxCtx.globalAlpha = 0.5; // the inner, thin, white beam
fxCtx.strokeStyle = "white";
fxCtx.lineWidth = 2;
fxCtx.stroke();
fxCtx.globalAlpha = 1;
fxCtx.lineCap = "butt";
}
Can someone advice how to improve my laser beam effect ?
Using canvas as images, glows, and more.
The demo below creates all the graphics it needs as offscreen canvases. The background is also drawn on to leave a burn mark when a laser hits the ground.
Lasers
Laser shots are 3 layered ctx.drawImage calls. The first 2 are glow with ctx.globalCompositeOperation = "lighter" . One has fixed alpha, the second has a random alpha. The last is drawn ctx.globalCompositeOperation = "source-over" and is just an image of a line.
There are 4 images (canvas) for the laaser called laserRed, laserGRed, laserGreen, and laserGGreen. the ones with the extra G are the laser glow.
When the laser shot is at the end I draw 4 frames of expanding glow from pre- rendered image. In the first of the 4 frames I draw to the background canvas leaving a burn mark.
Pre rendered images
All the graphics used are rendered in the function onResize which is called on resize from the boilerplate code.
The function display is called once every frame and handles all the animation.
There is an object called imageTools that has some helper functions to make the coding a little simplier. var image = imageTools.createImage(width,height) creates a canvas. The image has the context attached image.ctx so you can draw to it just like any canvas. You can then draw that image onto the global canvas with imageTools.drawImage(image,x,y,scale,rotation,alpha) The image is drawn at it center point with a scale, rotation, and alpha.
The bullets use an object pool so that the GC (Garbage collection) does not interfere to much.
I did not set any limits so those with high res devices or those with low end machines may see some slowdown. If its a problem OP you can reduce the bullet count by making them go a little faster, you can also reduce the rendering to just two or one layer. But this is a lot faster than if you were rendering with canvas vector calls, shadows, etc...
I will leave the rest for you to work out. Its a bit sparse in the comments but I did not have much time.
There is also some boilerplate at the bottom that is not very readable.
Left click to start the war.
/*************************************************************************************
* Called from boilerplate code and is debounced by 100ms
* Creates all the images used in the demo.
************************************************************************************/
var onResize = function(){
// create a background as drawable image
background = imageTools.createImage(canvas.width,canvas.height);
// create tile image
tile = imageTools.createImage(64,64);
tile.ctx.fillStyle = imageTools.createGradient(ctx,"linear",0,0,64,64,["#555","#666"]);
tile.ctx.fillRect(0,0,64,64);
tile.ctx.fillStyle = "#333"; // add colour
tile.ctx.globalCompositeOperation = "lighter";
tile.ctx.fillRect(0,0,62,2);
tile.ctx.fillRect(0,0,2,62);
tile.ctx.fillStyle = "#AAA"; // multiply colour to darken
tile.ctx.globalCompositeOperation = "multiply";
tile.ctx.fillRect(62,1,2,62);
tile.ctx.fillRect(1,62,62,2);
for(var y = -32; y < canvas.height; y += 64 ){
for(var x = -32; x < canvas.width; x += 64 ){
background.ctx.drawImage(tile,x,y);
}
}
background.ctx.globalCompositeOperation = "multiply"; // setup for rendering burn marks
burn = imageTools.createImage(flashSize/2,flashSize/2);
burn.ctx.fillStyle = imageTools.createGradient(ctx,"radial",flashSize/4,flashSize/4,0,flashSize/4,["#444","#444","#333","#000","#0000"]);
burn.ctx.fillRect(0,0,flashSize/2,flashSize/2);
glowRed = imageTools.createImage(flashSize,flashSize);
glowRed.ctx.fillStyle = imageTools.createGradient(ctx,"radial",flashSize/2,flashSize/2,0,flashSize/2,["#855F","#8000"]);
// #855F is non standard colour last digit is alpha
// 8,8 is ceneter 0 first radius 8 second
glowRed.ctx.fillRect(0,0,flashSize,flashSize);
glowGreen = imageTools.createImage(flashSize,flashSize);
glowGreen.ctx.fillStyle = imageTools.createGradient(ctx,"radial",flashSize/2,flashSize/2,0,flashSize/2,["#585F","#0600"]);
// #855F is non standard colour last digit is alpha
// 8,8 is ceneter 0 first radius 8 second
glowGreen.ctx.fillRect(0,0,flashSize,flashSize);
// draw the laser
laserLen = 32;
laserWidth = 4;
laserRed = imageTools.createImage(laserLen,laserWidth);
laserGreen = imageTools.createImage(laserLen,laserWidth);
laserRed.ctx.lineCap = laserGreen.ctx.lineCap = "round";
laserRed.ctx.lineWidth = laserGreen.ctx.lineWidth = laserWidth;
laserRed.ctx.strokeStyle = "#F33";
laserGreen.ctx.strokeStyle = "#3F3";
laserRed.ctx.beginPath();
laserGreen.ctx.beginPath();
laserRed.ctx.moveTo(laserWidth/2 + 1,laserWidth/2);
laserGreen.ctx.moveTo(laserWidth/2 + 1,laserWidth/2);
laserRed.ctx.lineTo(laserLen - (laserWidth/2 + 1),laserWidth/2);
laserGreen.ctx.lineTo(laserLen - (laserWidth/2 + 1),laserWidth/2);
laserRed.ctx.stroke();
laserGreen.ctx.stroke();
// draw the laser glow FX
var glowSize = 8;
laserGRed = imageTools.createImage(laserLen + glowSize * 2,laserWidth + glowSize * 2);
laserGGreen = imageTools.createImage(laserLen + glowSize * 2,laserWidth + glowSize * 2);
laserGRed.ctx.lineCap = laserGGreen.ctx.lineCap = "round";
laserGRed.ctx.shadowBlur = laserGGreen.ctx.shadowBlur = glowSize;
laserGRed.ctx.shadowColor = "#F33"
laserGGreen.ctx.shadowColor = "#3F3";
laserGRed.ctx.lineWidth = laserGGreen.ctx.lineWidth = laserWidth;
laserGRed.ctx.strokeStyle = "#F33";
laserGGreen.ctx.strokeStyle = "#3F3";
laserGRed.ctx.beginPath();
laserGGreen.ctx.beginPath();
laserGRed.ctx.moveTo(laserWidth/2 + 1 + glowSize,laserWidth/2 + glowSize);
laserGGreen.ctx.moveTo(laserWidth/2 + 1 + glowSize,laserWidth/2 + glowSize);
laserGRed.ctx.lineTo(laserLen + glowSize * 2 - (laserWidth/2 + 1 + glowSize),laserWidth/2 + glowSize);
laserGGreen.ctx.lineTo(laserLen + glowSize * 2 - (laserWidth/2 + 1 + glowSize),laserWidth/2 + glowSize);
laserGRed.ctx.stroke();
laserGGreen.ctx.stroke();
readyToRock = true;
}
var flashSize = 16;
const flashBrightNorm = 4 * (flashSize/2) * (flashSize/2) * Math.PI; // area of the flash
var background,tile,glowRed,glowGreen,grad, laserGreen,laserRed,laserGGreen,laserGRed,readyToRock,burn;
readyToRock = false;
/*************************************************************************************
* create or reset a bullet
************************************************************************************/
function createShot(x,y,xx,yy,speed,type,bullet){ // create a bullet object
if(bullet === undefined){
bullet = {};
}
var nx = xx-x; // normalise
var ny = yy-y;
var dist = Math.sqrt(nx*nx+ny*ny);
nx /= dist;
ny /= dist;
bullet.x = x;
bullet.y = y;
bullet.speed = speed;
bullet.type = type;
bullet.xx = xx;
bullet.yy = yy;
bullet.nx = nx; // normalised vector
bullet.ny = ny;
bullet.rot = Math.atan2(ny,nx); // will draw rotated so get the rotation
bullet.life = Math.ceil(dist/speed); // how long to keep alive
return bullet;
}
// semi static array with object pool.
var bullets=[]; // array of bullets
var bulletPool=[]; // array of used bullets. Use to create new bullets this stops GC messing with frame rate
const BULLET_TYPES = {
red : 0,
green : 1,
}
/*************************************************************************************
* Add a bullet to the bullet array
************************************************************************************/
function addBullet(xx,yy,type){
var bullet,x,y;
if(bulletPool.length > 0){
bullet = bulletPool.pop(); // get bullet from pool
}
if(type === BULLET_TYPES.red){
x = canvas.width + 16 + 32 * Math.random();
y = Math.random() * canvas.height;
}else if(type === BULLET_TYPES.green){
x = - 16 - 32 * Math.random();
y = Math.random() * canvas.height;
}
// randomise shoot to position
var r = Math.random() * Math.PI * 2;
var d = Math.random() * 128 + 16;
xx += Math.cos(r)* d;
yy += Math.sin(r)* d;
bullets[bullets.length] = createShot(x,y,xx,yy,16,type,bullet);
}
/*************************************************************************************
* update and draw bullets
************************************************************************************/
function updateDrawAllBullets(){
var i,img,imgGlow;
for(i = 0; i < bullets.length; i++){
var b = bullets[i];
b.life -= 1;
if(b.life <= 0){ // bullet end remove it and put it in the pool
bulletPool[bulletPool.length] = bullets.splice(i,1)[0];
i--; // to stop from skipping a bullet
}else{
if(b.life < 5){
if(b.life===4){
b.x += b.nx * b.speed * 0.5; // set to front of laser
b.y += b.ny * b.speed * 0.5;
var scale = 0.9 + Math.random() *1;
background.ctx.setTransform(scale,0,0,scale,b.x,b.y);
background.ctx.globalAlpha = 0.1 + Math.random() *0.2;;
background.ctx.drawImage(burn,-burn.width /2 ,-burn.height/2);
}
if(b.type === BULLET_TYPES.red){
img = glowRed;
}else{
img = glowGreen;
}
ctx.globalCompositeOperation = "lighter";
imageTools.drawImage(img,b.x,b.y,(4-b.life)*(4-b.life),b.rot,1);//b.life/4);
imageTools.drawImage(img,b.x,b.y,4,b.rot,b.life/4);
ctx.globalCompositeOperation = "source-over";
}else{
b.x += b.nx * b.speed;
b.y += b.ny * b.speed;
if(b.type === BULLET_TYPES.red){
img = laserRed;
imgGlow = laserGRed;
}else{
img = laserGreen;
imgGlow = laserGGreen;
}
ctx.globalCompositeOperation = "lighter";
imageTools.drawImage(imgGlow,b.x,b.y,1,b.rot,1);
imageTools.drawImage(imgGlow,b.x,b.y,2,b.rot,Math.random()/2);
ctx.globalCompositeOperation = "source-over";
imageTools.drawImage(img,b.x,b.y,1,b.rot,1);
}
}
}
}
/*************************************************************************************
* Main display loop
************************************************************************************/
function display() {
if(readyToRock){
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.drawImage(background,0,0);
ctx.globalCompositeOperation = "source-over";
if(mouse.buttonRaw & 1){
addBullet(mouse.x,mouse.y,BULLET_TYPES.red);
addBullet(mouse.x,mouse.y,BULLET_TYPES.green);
}
updateDrawAllBullets();
}
}
/*************************************************************************************
* Tools for creating canvas images and what not
************************************************************************************/
var imageTools = (function () {
// This interface is as is. No warenties no garenties, and NOT to be used comercialy
var workImg,workImg1,keep; // for internal use
var xdx,xdy,spr; // static vars for drawImage and drawSprite
keep = false;
var tools = {
canvas : function (width, height) { // create a blank image (canvas)
var c = document.createElement("canvas");
c.width = width;
c.height = height;
return c;
},
createImage : function (width, height) {
var i = this.canvas(width, height);
i.ctx = i.getContext("2d");
return i;
},
drawImage : function(image, x, y, scale, ang, alpha) {
ctx.globalAlpha = alpha;
xdx = Math.cos(ang) * scale;
xdy = Math.sin(ang) * scale;
ctx.setTransform(xdx, xdy, -xdy, xdx, x, y);
ctx.drawImage(image, -image.width/2,-image.height/2);
},
hex2RGBA : function(hex){ // Not CSS colour as can have extra 2 or 1 chars for alpha
// #FFFF & #FFFFFFFF last F and FF are the alpha range 0-F & 00-FF
if(typeof hex === "string"){
var str = "rgba(";
if(hex.length === 4 || hex.length === 5){
str += (parseInt(hex.substr(1,1),16) * 16) + ",";
str += (parseInt(hex.substr(2,1),16) * 16) + ",";
str += (parseInt(hex.substr(3,1),16) * 16) + ",";
if(hex.length === 5){
str += (parseInt(hex.substr(4,1),16) / 16);
}else{
str += "1";
}
return str + ")";
}
if(hex.length === 7 || hex.length === 8){
str += parseInt(hex.substr(1,2),16) + ",";
str += parseInt(hex.substr(3,2),16) + ",";
str += parseInt(hex.substr(5,2),16) + ",";
if(hex.length === 5){
str += (parseInt(hex.substr(7,2),16) / 255).toFixed(3);
}else{
str += "1";
}
return str + ")";
}
return "rgba(0,0,0,0)";
}
},
createGradient : function(ctx, type, x, y, xx, yy, colours){ // Colours MUST be array of hex colours NOT CSS colours
// See this.hex2RGBA for details of format
var i,g,c;
var len = colours.length;
if(type.toLowerCase() === "linear"){
g = ctx.createLinearGradient(x,y,xx,yy);
}else{
g = ctx.createRadialGradient(x,y,xx,x,y,yy);
}
for(i = 0; i < len; i++){
c = colours[i];
if(typeof c === "string"){
if(c[0] === "#"){
c = this.hex2RGBA(c);
}
g.addColorStop(Math.min(1,i / (len -1)),c); // need to clamp top to 1 due to floating point errors causes addColorStop to throw rangeError when number over 1
}
}
return g;
},
};
return tools;
})();
// CODE FROM HERE DOWN IS SUPPORT CODE AN HAS LITTLE TO DO WITH THE ANSWER
//==================================================================================================
// The following code is support code that provides me with a standard interface to various forums.
// It provides a mouse interface, a full screen canvas, and some global often used variable
// like canvas, ctx, mouse, w, h (width and height), globalTime
// This code is not intended to be part of the answer unless specified and has been formated to reduce
// display size. It should not be used as an example of how to write a canvas interface.
// By Blindman67
if(typeof onResize === "undefined"){
window["onResize"] = undefined; // create without the JS parser knowing it exists.
// this allows for it to be declared in an outside
// modal.
}
const RESIZE_DEBOUNCE_TIME = 100;
var w, h, cw, ch, canvas, ctx, mouse, createCanvas, resizeCanvas, setGlobals, globalTime = 0, resizeCount = 0;
createCanvas = function () {
var c,
cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === undefined) {
canvas = createCanvas();
}
canvas.width = window.innerWidth-2;
canvas.height = window.innerHeight-2;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") {
setGlobals();
}
if (typeof onResize === "function") {
resizeCount += 1;
setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
}
}
function debounceResize() {
resizeCount -= 1;
if (resizeCount <= 0) {
onResize();
}
}
setGlobals = function () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
mouse.updateBounds();
}
mouse = (function () {
function preventDefault(e) {
e.preventDefault();
}
var mouse = {
x : 0,
y : 0,
w : 0,
alt : false,
shift : false,
ctrl : false,
buttonRaw : 0,
over : false,
bm : [1, 2, 4, 6, 5, 3],
active : false,
bounds : null,
crashRecover : null,
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.x = e.clientX - m.bounds.left;
m.y = e.clientY - m.bounds.top;
m.alt = e.altKey;
m.shift = e.shiftKey;
m.ctrl = e.ctrlKey;
if (t === "mousedown") {
m.buttonRaw |= m.bm[e.which - 1];
} else if (t === "mouseup") {
m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") {
m.buttonRaw = 0;
m.over = false;
} else if (t === "mouseover") {
m.over = true;
} else if (t === "mousewheel") {
m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") {
m.w = -e.detail;
}
if (m.callbacks) {
m.callbacks.forEach(c => c(e));
}
if ((m.buttonRaw & 2) && m.crashRecover !== null) {
if (typeof m.crashRecover === "function") {
setTimeout(m.crashRecover, 0);
}
}
e.preventDefault();
}
m.updateBounds = function () {
if (m.active) {
m.bounds = m.element.getBoundingClientRect();
}
}
m.addCallback = function (callback) {
if (typeof callback === "function") {
if (m.callbacks === undefined) {
m.callbacks = [callback];
} else {
m.callbacks.push(callback);
}
} else {
throw new TypeError("mouse.addCallback argument must be a function");
}
}
m.start = function (element, blockContextMenu) {
if (m.element !== undefined) {
m.removeMouse();
}
m.element = element === undefined ? document : element;
m.blockContextMenu = blockContextMenu === undefined ? false : blockContextMenu;
m.mouseEvents.forEach(n => {
m.element.addEventListener(n, mouseMove);
});
if (m.blockContextMenu === true) {
m.element.addEventListener("contextmenu", preventDefault, false);
}
m.active = true;
m.updateBounds();
}
m.remove = function () {
if (m.element !== undefined) {
m.mouseEvents.forEach(n => {
m.element.removeEventListener(n, mouseMove);
});
if (m.contextMenuBlocked === true) {
m.element.removeEventListener("contextmenu", preventDefault);
}
m.element = m.callbacks = m.contextMenuBlocked = undefined;
m.active = false;
}
}
return mouse;
})();
// Clean up. Used where the IDE is on the same page.
var done = function () {
window.removeEventListener("resize", resizeCanvas)
mouse.remove();
document.body.removeChild(canvas);
canvas = ctx = mouse = undefined;
}
resizeCanvas();
mouse.start(canvas, true);
mouse.crashRecover = done;
window.addEventListener("resize", resizeCanvas);
function update(timer) { // Main update loop
globalTime = timer;
display(); // call demo code
requestAnimationFrame(update);
}
requestAnimationFrame(update);
/** SimpleFullCanvasMouse.js end **/
You can add a shadow for a glowing effect
fxCtx.shadowBlur = 10;
fxCtx.shadowColor = '#FD0100';
but i think thats all, bacause your laser is so small that a real gradient would not make sense.
Use some nice colors like #FEF1BA insted of white for the red laser and thats it
In my on-the-fly editor tool I would really appreciate to get actual rendered height of the text / font - (I do not mean just getting CSS font-size, neither computed nor preset).
Is this achieveable in javascript?
If not directly, is possible something as rendering font in canvas the same way as it is rendered as regular text - and then finding out?
EDIT - my "dev" solution: Based on suggested links I've built a little pure-javascript code, that goes through pixels in canvas and analyses whether the pixel is white or not and acts accordingly, it is hardly a developer version of a code - just outputs few useful info and shows how to access computed data - http://jsfiddle.net/DV9Bw/1325/
HTML:
<canvas id="exampleSomePrettyRandomness" width="200" height="60"></canvas>
<div id="statusSomePrettyRandomness"></div>
JS:
function findPos(obj) {
var curleft = 0, curtop = 0;
if (obj.offsetParent) {
do {
curleft += obj.offsetLeft;
curtop += obj.offsetTop;
} while (obj = obj.offsetParent);
return { x: curleft, y: curtop };
}
return undefined;
}
var status = document.getElementById('statusSomePrettyRandomness');
var example = document.getElementById('exampleSomePrettyRandomness');
var context = example.getContext('2d');
context.fillStyle = "rgb(255,255,255)";
context.fillRect(0, 0, 200, 200);
context.fillStyle = "rgb(0,0,0)";
context.font = "30px Arial";
context.fillText("Hello World",0,30);
var pos = findPos(example);
var x = example.pageX - pos.x;
var y = example.pageY - pos.y;
var foundTop = false;
xPos = 0;
yPos = 0;
topY = -1;
bottomY = -1;
var fuse = 1000;
while( fuse-- > 0 ){
//status.innerHTML += yPos+"<br>";
if( yPos == (example.offsetHeight - 2) ){
xPos++;
yPos = 0;
continue;
}
var data = context.getImageData(xPos, yPos, 1, 1).data;
if( ! foundTop ){
if( (data[0] != 255) && (data[1] != 255) && (data[2] != 255) ){
topY = yPos;
status.innerHTML += "<br>Found top: "+topY+" X:"+xPos+" Color: rgba("+data[0]+","+data[1]+","+data[2]+")"+"<br>";
foundTop = true;
}
} else {
if( (data[0] == 255) && (data[1] == 255) && (data[2] == 255) ){
bottomY = yPos;
status.innerHTML += "<br>Found bottom: "+bottomY+" X:"+xPos+"<br>";
break;
}
}
yPos++;
if( yPos > example.offsetHeight ){
status.innerHTML += ""
+"Y overflow ("+yPos+">"+example.offsetHeight+")"
+" - moving X to "+xPos
+" - reseting Y to "+yPos
+"<br>"
;
xPos++;
yPos = 0;
}
}
status.innerHTML += "Fuse:"+fuse+", Top:"+topY+", Bottom: "+bottomY+"<br>";
status.innerHTML += "Font height should be: "+(bottomY-topY)+"<br>";
EDIT 2: Why this is not a duplicate: My question is about really just real rendered height of a font or a letter, "possible duplicate" is about how much space do you need to print a text, answers provided there don't answer my exact problem anyways.
I am not aware of any method that would return the height of a text such as measureText (which does currently return the width).
However, in theory you can simply draw your text in the canvas then trim the surrounding transparent pixels then measure the canvas height..
Here is an example (the height will be logged in the console):
// Create a blank canvas (by not filling a background color).
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
// Fill it with some coloured text.. (black is default)
ctx.font = "48px serif";
ctx.textBaseline = "hanging";
ctx.fillText("Hello world", 0, 0);
// Remove the surrounding transparent pixels
// result is an actual canvas element
var result = trim(canvas);
// you could query it's width, draw it, etc..
document.body.appendChild(result);
// get the height of the trimmed area
console.log(result.height);
// Trim Canvas Pixels Method
// https://gist.github.com/remy/784508
function trim(c) {
var ctx = c.getContext('2d'),
// create a temporary canvas in which we will draw back the trimmed text
copy = document.createElement('canvas').getContext('2d'),
// Use the Canvas Image Data API, in order to get all the
// underlying pixels data of that canvas. This will basically
// return an array (Uint8ClampedArray) containing the data in the
// RGBA order. Every 4 items represent one pixel.
pixels = ctx.getImageData(0, 0, c.width, c.height),
// total pixels
l = pixels.data.length,
// main loop counter and pixels coordinates
i, x, y,
// an object that will store the area that isn't transparent
bound = { top: null, left: null, right: null, bottom: null };
// for every pixel in there
for (i = 0; i < l; i += 4) {
// if the alpha value isn't ZERO (transparent pixel)
if (pixels.data[i+3] !== 0) {
// find it's coordinates
x = (i / 4) % c.width;
y = ~~((i / 4) / c.width);
// store/update those coordinates
// inside our bounding box Object
if (bound.top === null) {
bound.top = y;
}
if (bound.left === null) {
bound.left = x;
} else if (x < bound.left) {
bound.left = x;
}
if (bound.right === null) {
bound.right = x;
} else if (bound.right < x) {
bound.right = x;
}
if (bound.bottom === null) {
bound.bottom = y;
} else if (bound.bottom < y) {
bound.bottom = y;
}
}
}
// actual height and width of the text
// (the zone that is actually filled with pixels)
var trimHeight = bound.bottom - bound.top,
trimWidth = bound.right - bound.left,
// get the zone (trimWidth x trimHeight) as an ImageData
// (Uint8ClampedArray of pixels) from our canvas
trimmed = ctx.getImageData(bound.left, bound.top, trimWidth, trimHeight);
// Draw back the ImageData into the canvas
copy.canvas.width = trimWidth;
copy.canvas.height = trimHeight;
copy.putImageData(trimmed, 0, 0);
// return the canvas element
return copy.canvas;
}
<canvas id="canvas"></canvas>
Image Data API: https://developer.mozilla.org/en-US/docs/Web/API/ImageData
Let's say this is my canvas, with an evil-looking face drawn on it. I want to use toDataURL() to export my evil face as a PNG; however, the whole canvas is rasterised, including the 'whitespace' between the evil face and canvas edges.
+---------------+
| |
| |
| (.Y. ) |
| /_ |
| \____/ |
| |
| |
+---------------+
What is the best way to crop/trim/shrinkwrap my canvas to its contents, so my PNG is no larger than the face's 'bounding-box', like below? The best way seems to be scaling the canvas, but supposing the contents are dynamic...? I'm sure there should be a simple solution to this, but it's escaping me, with much Googling.
+------+
|(.Y. )|
| /_ |
|\____/|
+------+
Thanks!
Edited (see comments)
function cropImageFromCanvas(ctx) {
var canvas = ctx.canvas,
w = canvas.width, h = canvas.height,
pix = {x:[], y:[]},
imageData = ctx.getImageData(0,0,canvas.width,canvas.height),
x, y, index;
for (y = 0; y < h; y++) {
for (x = 0; x < w; x++) {
index = (y * w + x) * 4;
if (imageData.data[index+3] > 0) {
pix.x.push(x);
pix.y.push(y);
}
}
}
pix.x.sort(function(a,b){return a-b});
pix.y.sort(function(a,b){return a-b});
var n = pix.x.length-1;
w = 1 + pix.x[n] - pix.x[0];
h = 1 + pix.y[n] - pix.y[0];
var cut = ctx.getImageData(pix.x[0], pix.y[0], w, h);
canvas.width = w;
canvas.height = h;
ctx.putImageData(cut, 0, 0);
var image = canvas.toDataURL();
}
If I understood well you want to "trim" away all the surronding your image / drawing, and adjust the canvas to that size (like if you do a "trim" command in Photoshop).
Here is how I'll do it.
Run thru all the canvas pixels checking if their alpha component is > 0 (that means that something is drawn in that pixel). Alternativelly you could check for the r,g,b values, if your canvas background is fullfilled with a solid color, for instance.
Get te coordinates of the top most left pixel non-empty, and same for the bottom most right one. So you'll get the coordinates of an imaginay "rectangle" containing the canvas area that is not empty.
Store that region of pixeldata.
Resize your canvas to its new dimensions (the ones of the region we got at step 2.)
Paste the saved region back to the canvas.
Et, voilá :)
Accesing pixeldata is quite slow depending on the size of your canvas (if its huge it can take a while). There are some optimizations around to work with raw canvas pixeldata (I think there is an article about this topic at MDN), I suggest you to google about it.
I prepared a small sketch in jsFiddle that you can use as starting point for your code.
Working sample at jsFiddle
Hope I've helped you.
c:.
Here's my take. I felt like all the other solutions were overly complicated. Though, after creating it, I now see it's the same solution as one other's, expect they just shared a fiddle and not a function.
function trimCanvas(canvas){
const context = canvas.getContext('2d');
const topLeft = {
x: canvas.width,
y: canvas.height,
update(x,y){
this.x = Math.min(this.x,x);
this.y = Math.min(this.y,y);
}
};
const bottomRight = {
x: 0,
y: 0,
update(x,y){
this.x = Math.max(this.x,x);
this.y = Math.max(this.y,y);
}
};
const imageData = context.getImageData(0,0,canvas.width,canvas.height);
for(let x = 0; x < canvas.width; x++){
for(let y = 0; y < canvas.height; y++){
const alpha = imageData.data[((y * (canvas.width * 4)) + (x * 4)) + 3];
if(alpha !== 0){
topLeft.update(x,y);
bottomRight.update(x,y);
}
}
}
const width = bottomRight.x - topLeft.x;
const height = bottomRight.y - topLeft.y;
const croppedCanvas = context.getImageData(topLeft.x,topLeft.y,width,height);
canvas.width = width;
canvas.height = height;
context.putImageData(croppedCanvas,0,0);
return canvas;
}
Here's code in ES syntax, short, fast and concise:
/**
* Trim a canvas.
*
* #author Arjan Haverkamp (arjan at avoid dot org)
* #param {canvas} canvas A canvas element to trim. This element will be trimmed (reference)
* #param {int} threshold Alpha threshold. Allows for trimming semi-opaque pixels too. Range: 0 - 255
* #returns {Object} Width and height of trimmed canvcas and left-top coordinate of trimmed area. Example: {width:400, height:300, x:65, y:104}
*/
const trimCanvas = (canvas, threshold = 0) => {
const ctx = canvas.getContext('2d'),
w = canvas.width, h = canvas.height,
imageData = ctx.getImageData(0, 0, w, h),
tlCorner = { x:w+1, y:h+1 },
brCorner = { x:-1, y:-1 };
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
if (imageData.data[((y * w + x) * 4) + 3] > threshold) {
tlCorner.x = Math.min(x, tlCorner.x);
tlCorner.y = Math.min(y, tlCorner.y);
brCorner.x = Math.max(x, brCorner.x);
brCorner.y = Math.max(y, brCorner.y);
}
}
}
const cut = ctx.getImageData(tlCorner.x, tlCorner.y, brCorner.x - tlCorner.x, brCorner.y - tlCorner.y);
canvas.width = brCorner.x - tlCorner.x;
canvas.height = brCorner.y - tlCorner.y;
ctx.putImageData(cut, 0, 0);
return {width:canvas.width, height:canvas.height, x:tlCorner.x, y:tlCorner.y};
}
The top voted answer here, as well as the implementations i found online trim one extra pixel which was very apparent when trying to trim text out of canvas. I wrote my own that worked better for me:
var img = new Image;
img.onload = () => {
var canvas = document.getElementById('canvas');
canvas.width = img.width;
canvas.height = img.height;
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
document.getElementById('button').addEventListener('click', ()=>{
autoCropCanvas(canvas, ctx);
document.getElementById('button').remove();
});
};
img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABooAAAA2CAYAAADwOsspAAAF/0lEQVR4nO3dTagdZx3H8W+sxQgqGrWbahEqLopGUAm60iqI2IWrdKOigmC7EepLNi6ELiwUFLTNQiG1i4ogUrUKgvj+AoouasWXlrZWogYsxlZFE5umLmZKbk7n3Nxz3zI3fD4wXGbuM//n95zlf86ZpwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgPEeqp6oPXGDc5dUt1R+rv1SPVJ/c0WTnu7s63ZD1YxP/v9j5VjH3tci3NfLNc24AAAAAACbc19C0/f4Fxh2pzlQHx/Prqx/uXKxJr255g3kO+VYx97XItzXyzXNuAAAAAADWeE31aPXX6snqZeuM/U51/5rzZ1UHdi7apPUazHPIt4q5r0W+rZFvnnMDAAAAALDGrdXR6jMNjdsj64z9VXXvboRax3oN5jnkW8Xc1yLf1sg3z7kBAAAAAC5pz60+VT1YnWjY5+Mr1Tsnxu6rjldvql7X0Li9b2Lc4epUdXY8To3HDWvGHKy+W/2n+nt1V/XWseYT4/hVM66t+bfq9upQz2wwX4x8V1Wfrn47jjle3dPQAJ8y57XIJ99O5dvuuQEAAAAAuIDPVw9ULx/PX1x9u+lv6F9bPbTm/HcNzduDE2Nr+Tf9r64eqx6u3lJdWd04nk/9amAjGZfV/NmSmrud7/3VyYaGd9XzqzsamuHXbHD+uaxFPvl2Kt92zg0AAAAAwAacqI4tXDtYfW1i7LHq5jXnn2ho3t66pPayBu6XxvvevHD9c003gzeScdWau53vuuqmhTHPaXhQdHSL85fPWr5LI992zg0AAAAAwAb8uvpn9Z6GBxfL7G/4pv+r1lx7RcMrn/7csIH8oqkG7r7q8YZXUC16R9PN4Atl3EzN3cy3ngeqH2xx/vJZy7f3823n3AAAAAAAbNCh6pGGJuxjnds/ZNHh6pcT13863jt1z1QD9yXj+N9MjH9t083gC2XcTM3dzFfD3jBHxvn+0bn9VM5WP99Da5FPvp3Kt51zAwAAAACwgmdX76q+XP23oSF758KYr3du4/m1xxPj+Dsm6k41cF/a5prB62XcbM3dylf11YaHQjc27E/0tD90/oOiua9FPvl2Kt92zg0AAAAAwAZdtnB+RfXjhqbs68drB6p/N3zjf9GB6n8Nr4zav/C/9V5HdXKi1rLXS10o42Zq7ma+FzQ8JPrFRM3FB0VzX4t88u1Uvu2cGwAAAACADTrd+b9wqfpgQ1P2beP5DdU969T4xjj++oXrq25w/9mmm8Ebybhqzd3Mt786M8631uXVvzr/QdFm5i+ftXyXRr7tnBsAAAAAgA04U32hc83jK6ofVX9q2Fenhn2IDq9T43BDE/ebC9eXNXCvbtgf5eGGhvCV1YeqnzTdDN5IxmU1H1pSc7fz3T3e+9HqeeOYO8driw+K5r4W+eTbqXzbOTcAAAAAABvw7upbDY3iEw0b3R+rrmpo0p5qaNCerm6buP+28X9Pjcep6qbx79nxOFU9uHDfwep7DXukPFodrd441vjIChmX1TxZ3VVdO9Z8en+lGh5s7Xa+F1a3V8cbXuN3b/Xh6v41GQ7tkbXIJ99O5dvuuQEAAAAA2EPe3tAMft/FDrLE3POtYu5rkW9r5AMAAAAAYLauqb44cf3mhl8GvHJ34zzD3POtYu5rkW9r5AMAAAAAYM95Q/Vk9d5qX3VZdV31eMP+KRfb3POtYu5rkW9r5AMAAAAAYM95UXVLwz49Jxqaxr+vPt7QSL7Y5p5vFXNfi3xbIx8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACXrv8D9cs03XV5TWUAAAAASUVORK5CYII=';
function autoCropCanvas(canvas, ctx) {
var bounds = {
left: 0,
right: canvas.width,
top: 0,
bottom: canvas.height
};
var rows = [];
var cols = [];
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
for (var x = 0; x < canvas.width; x++) {
cols[x] = cols[x] || false;
for (var y = 0; y < canvas.height; y++) {
rows[y] = rows[y] || false;
const p = y * (canvas.width * 4) + x * 4;
const [r, g, b, a] = [imageData.data[p], imageData.data[p + 1], imageData.data[p + 2], imageData.data[p + 3]];
var isEmptyPixel = Math.max(r, g, b, a) === 0;
if (!isEmptyPixel) {
cols[x] = true;
rows[y] = true;
}
}
}
for (var i = 0; i < rows.length; i++) {
if (rows[i]) {
bounds.top = i ? i - 1 : i;
break;
}
}
for (var i = rows.length; i--; ) {
if (rows[i]) {
bounds.bottom = i < canvas.height ? i + 1 : i;
break;
}
}
for (var i = 0; i < cols.length; i++) {
if (cols[i]) {
bounds.left = i ? i - 1 : i;
break;
}
}
for (var i = cols.length; i--; ) {
if (cols[i]) {
bounds.right = i < canvas.width ? i + 1 : i;
break;
}
}
var newWidth = bounds.right - bounds.left;
var newHeight = bounds.bottom - bounds.top;
var cut = ctx.getImageData(bounds.left, bounds.top, newWidth, newHeight);
canvas.width = newWidth;
canvas.height = newHeight;
ctx.putImageData(cut, 0, 0);
}
<canvas id=canvas style='border: 1px solid pink'></canvas>
<button id=button>crop canvas</button>