trying to draw a sprite onto canvas but I am missing something - javascript

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>

Related

How to add a custom position inside a forEach loop for each Canvas DrawImage?

I am trying to display multiple images using Canvas DrawImages with each a unique position. I have created an array with multiple images and I would like to position them on different parts of the canvas on load.
Now, all the canvas images are stacked on each other.
This is my JS:
(() => {
// Canvas Setup
let canvas = document.getElementById('hedoneCanvas');
let context = canvas.getContext('2d');
// // Canvas Images
let imageSources = [
'https://images.pexels.com/photos/1313267/pexels-photo-1313267.jpeg?cs=srgb&dl=food-fruit-green-1313267.jpg&fm=jpg',
'https://images.pexels.com/photos/2965413/pexels-photo-2965413.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
'https://images.pexels.com/photos/2196602/pexels-photo-2196602.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
'https://images.pexels.com/photos/2955490/pexels-photo-2955490.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940'
];
// Canvas Size
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const loadImage = imagePath => {
return new Promise((resolve, reject) => {
let image = new Image();
image.addEventListener('load', () => {
resolve(image);
});
image.addEventListener('error', err => {
reject(err);
});
image.src = imagePath;
});
};
const canvasOnResize = () => {
canvas.width = window.innerWidth;
canvas.style.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.height = window.innerHeight;
};
window.addEventListener('resize', canvasOnResize);
Promise.all(imageSources.map(i => loadImage(i)))
.then(images => {
images.forEach(image => {
console.log(context);
context.drawImage(image, 0, 0, canvas.width, canvas.height);
});
})
.catch(err => {
console.error(err);
});
})();
I also want to make them responsive how can I archive this?
I have demo on Codepen: https://codepen.io/Merdzanovich/pen/dybLQqL
Trying to do something like this on hover:
http://vlnc.studio/
Responsive canvas
To make the content of a canvas responsive it is best to use a reference display size. This represents the ideal display size that your content is viewed in.
The reference is used to then calculate how to display the content on displays that do not match the ideal size.
In the example below the object reference defines the reference display and provides methods to resize the canvas and scale and position the content.
With the reference defined you can then position and size your content for the reference display.
For example the constant imageMaxSize = 512 sets the max size (width or height) of an image. The 512 is relative to the reference display (1920, 1080). The actual size that the image is display depends on the size of the page.
It sets a matrix that is used to transform the content to fit the display. Rather then use the top left of the display as origin (0,0) it sets the center of the canvas as the origin.
The example lets you set how the canvas responds to the display resolution, the const scaleMethod can be set to
"fit" will ensure that all the content will be displayed (as long as it fits the reference). However there may be blank areas above below or left and right of the content if the display aspect is different from the reference.
"fill" will ensure that the content will fill the display. However some of the content (top and bottom, or left and right) may be clipped if the display aspect does not match the reference.
Positioning images.
That just requires an array that holds the image position and size relative to the reference display.
In the example the array displayList which extends an array has the function
add(image,x,y) that adds an image to the list. The x and y represent the position of the image center and is relative to the reference display origin (center of canvas)
When an images is added its reference size is calculated from its natural size
draw(ctx) will draw all the items in the display list using the reference matrix to scale and position the images.
Rendering
Rather than render to the canvas ad-hock a render loop is used updateCanvas that ensures content is updated in sync with the display hardware. The ensure that if you have animated content it does not produce artifacts (shearing, flicker)
To prevent the rendering to needlessly draw content the render loop will only render the content when the semaphore update is set to true. For example when resizing the canvas the content needs to be rendered. This is achieved by simply setting update=true
Rather than use the resize event to resize the canvas, the render loop checks if the canvas size matches the page size. If there is a miss match then the canvas is resize. this is done because the resize event is not synced with the display hardware and will cause poor quality rendering while the display is being resized. it also ensures that the canvas is not resized more than once between display frames.
Example
requestAnimationFrame(updateCanvas);
const ctx = canvas.getContext('2d');
const SCALE_METHOD = "fit";
const images = [];
const ALPHA_FADE_IN_SPEED = 0.04; // for fade in out approx time use
// seconds = (0.016666 / ALPHA_FADE_IN_SPEED)
const FADE_OVERLAP = 0.4; // fraction of fade time. NOT less or equal to
// ALPHA_FADE_IN_SPEED and not greater equal to 0.5
const IMAGE_MAX_SIZE = 480; // image isze in pixel of reference display
const IMAGE_MIN_SIZE = IMAGE_MAX_SIZE * 0.8;
const IMAGE_SCALE_FLICK = IMAGE_MAX_SIZE * 0.05;
// sigmoid curve return val 0-1. P is power.
// 0 < p < 1 curve eases center
// 1 == p linear curve
// 1 < p curve eases out from 0 and into 1
Math.sCurve = (u, p = 2) => u <= 0 ? 0 : u >= 1 ? 1 : u ** p / (u ** p + (1 - u) ** p);
// Simple spring
// constructor(u,[a,[d,[t]]])
// u is spring position
// a is acceleration default 0.1
// d is dampening default 0.9
// t is spring target (equalibrium) default t = u
// properties
// u current spring length
// flick(v) // adds movement to spring
// step(u) gets next value of spring. target defaults to this.target
Math.freeSpring = (u, a = 0.3 , d = 0.65, t = u) => ({
u,
v : 0,
set target(v) { t = v },
flick(v) { this.v = v * (1/d) *(1/a)},
step(u = t) { return this.u += (this.v = (this.v += (u - this.u) * a) * d) }
})
var update = false;
const reference = {
get width() { return 1920 }, // ideal display resolution
get height() { return 1080 },
matrix: [1, 0, 0, 1, 0, 0],
resize(method, width = innerWidth, height = innerHeight) {
method = method.toLowerCase();
var scale = 1; // one to one of reference
if (method === "fit") {
scale = Math.min(width / reference.width, height / reference.height);
} else if (method === "fill") {
scale = Math.max(width / reference.width, height / reference.height);
}
const mat = reference.matrix;
mat[3] = mat[0] = scale;
mat[4] = width / 2;
mat[5] = height / 2;
canvas.width = width;
canvas.height = height;
update = true;
},
checkSize() {
if (canvas.width !== innerWidth || canvas.height !== innerHeight) {
reference.resize(SCALE_METHOD);
}
},
};
{
let count = 0;
[
'https://images.pexels.com/photos/1313267/pexels-photo-1313267.jpeg?cs=srgb&dl=food-fruit-green-1313267.jpg&fm=jpg',
'https://images.pexels.com/photos/2965413/pexels-photo-2965413.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
'https://images.pexels.com/photos/2196602/pexels-photo-2196602.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
'https://images.pexels.com/photos/2955490/pexels-photo-2955490.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940'
].forEach(src => {
count++;
const img = new Image;
img.src = src;
img.addEventListener("load", () => {
images.push(img);
if (! --count) { setup() }
})
img.addEventListener("error", () => {if (! --count) { setup() }});
});
}
const displayList = Object.assign([], {
add(image, x, y) {
var item;
var w = image.naturalWidth;
var h = image.naturalHeight;
const scale = Math.min(IMAGE_MAX_SIZE / w, IMAGE_MAX_SIZE / h);
w *= scale;
h *= scale;
displayList.push(item = {
image, x, y, w, h,
fading: false,
alpha: 0,
alphaStep: 0,
onAlphaReady: undefined,
scaleFX: Math.freeSpring(IMAGE_MIN_SIZE)
});
displayList.fadeQueue.push(item);
return item;
},
fadeQueue: [],
draw(ctx) {
var curvePower = 2
ctx.setTransform(...reference.matrix);
for (const item of displayList) {
if (item.fading) {
item.alpha += item.alphaStep;
curvePower = item.alphaStep > 0 ? 2 : 2;
if (item.onAlphaReady && (
(item.alphaStep < 0 && item.alpha <= FADE_OVERLAP) ||
(item.alphaStep > 0 && item.alpha >= 1 - FADE_OVERLAP))) {
item.onAlphaReady(item);
item.onAlphaReady = undefined;
} else if (item.alpha <= 0 || item.alpha >= 1) {
item.fading = false;
}
update = true;
}
ctx.globalAlpha = Math.sCurve(item.alpha, curvePower);
const s = item.scaleFX.step() / IMAGE_MAX_SIZE;
ctx.drawImage(item.image, item.x - item.w / 2 * s, item.y - item.h / 2 * s, item.w * s, item.h * s);
}
ctx.globalAlpha = 1;
ctx.setTransform(1, 0, 0, 1, 0, 0); // default transform
}
});
function fadeNextImage() {
const next = displayList.fadeQueue.shift();
if(next.alpha < 0.5) { // Start fade in
next.scaleFX.flick(IMAGE_SCALE_FLICK);
next.scaleFX.target = IMAGE_MAX_SIZE;
next.alphaStep = ALPHA_FADE_IN_SPEED;
} else { // Start fade out
next.scaleFX.flick(IMAGE_SCALE_FLICK);
next.scaleFX.target = IMAGE_MIN_SIZE;
next.alphaStep = -ALPHA_FADE_IN_SPEED;
}
next.onAlphaReady = fadeNextImage;
next.fading = true;
displayList.fadeQueue.push(next);
}
function setup() {
const repeat = 2;
var i, len = images.length;
const distX = (reference.width - IMAGE_MAX_SIZE) * 0.45;
const distY = (reference.height - IMAGE_MAX_SIZE) * 0.45;
for (i = 0; i < len * repeat; i++) {
const ang = i / (len * repeat) * Math.PI * 2 - Math.PI / 2;
displayList.add(images[i % len], Math.cos(ang) * distX, Math.sin(ang) * distY);
}
fadeNextImage();
}
function clearCanvas() {
ctx.globalAlpha = 1;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
function loading(time) {
clearCanvas();
ctx.font = "12px arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.strokeStyle = "#aaa";
ctx.fillStyle = "white";
ctx.setTransform(1,0,0,1,ctx.canvas.width / 2, ctx.canvas.height / 2);
ctx.fillText("loading",0,0);
ctx.beginPath();
ctx.lineWidth = 2;
ctx.lineCap = "round";
const pos = time + Math.cos(time) * 0.25 + 1;
ctx.arc(0 ,0, 24, pos, pos + Math.cos(time * 0.1) * 0.5 + 1);
ctx.stroke();
}
function updateCanvas(time) {
reference.checkSize()
if(!displayList.length) {
loading(time / 100);
} else if (update) {
update = false;
clearCanvas();
displayList.draw(ctx);
}
requestAnimationFrame(updateCanvas);
}
canvas {
position: absolute;
top: 0px;
left: 0px;
background: black;
}
<canvas id="canvas"></canvas>
Try this:
Promise.all(imageSources.map(i => loadImage(i)))
.then(images => {
images.forEach((image,key) => {
context.drawImage(image, key*100, key*100, canvas.width, canvas.height);
});
})
.catch(err => {
console.error(err);
});

Canvas code not updating and I'm not sure why

I'm trying to make a simple canvas program where the user clicks to create bouncing moving circles. It keeps freezing but still creates the circles without updating. I'm not sure whats going on, please help!
I'm adding each circle to an array of circles with the constructor
The setInterval loop seems to be freezing but the circles are still created even when this is happening
I'm having a hard time debugging this, any advice is greatly appreciated
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Background Test</title>
<style>
* { margin: 0; padding: 0; overflow: hidden; }
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
// Request animation frame -> Optimizes animation speed
const requestAnimationFrame = window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
const c = document.getElementById('canvas');
const ctx = c.getContext('2d');
// Fullscreen
c.width = window.innerWidth;
c.height = window.innerHeight;
ctx.fillStyle = 'red';
let fps = 60;
// FOR MOBILE DEVICES
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent))
fps = 29;
// Options
const background = '#333';
const circleMinSpeed = 3;
const circleMaxSpeed = 6;
const circleMinSize = 3;
const circleMaxSize = 10;
const circles = [];
let circlesCounter = 0;
const circlesTimeAlive = 20 * fps; // seconds
let i = 0;
const interval = 1000 / fps;
let now, delta;
let then = Date.now();
// Coordinate variables
let mouseX, mouseY, clickX, clickY;
// Tracks mouse movement
c.onmousemove = function(event)
{
mouseX = event.clientX;
mouseY = event.clientY;
};
// Tracks mouse click
c.onmousedown = function(event)
{
clickX = event.clientX;
clickY = event.clientY;
circle(clickX, clickY);
};
function draw()
{
// Loop
requestAnimationFrame(draw);
// Set NOW and DELTA
now = Date.now();
delta = now - then;
// New frame
if (delta > interval) {
// Update THEN
then = now - (delta % interval);
// Our animation
// Clear canvas then draw
ctx.clearRect(0, 0, c.width, c.height);
drawBackground();
drawCos();
drawCircles();
drawTest();
}
}
// Circle constructor
function circle(x, y)
{
// Pick random color
let r = Math.floor(Math.random() * 255);
let g = Math.floor(Math.random() * 255);
let b = Math.floor(Math.random() * 255);
self.color = 'rgb(' + r + ', ' + g + ', ' + b + ')';
self.xCo = x;
self.yCo = y;
// Pick random size within ranges
self.size = circleMinSize + Math.floor(Math.random() *
(circleMaxSize - circleMinSize));
// Pick random direction & speed (spdX spdY)
self.speed = circleMinSpeed + Math.floor(Math.random() *
(circleMaxSpeed - circleMinSpeed));
self.spdX = self.speed * (Math.random() * 2) - 1; // picks -1 to 1
self.spdY = self.speed * (Math.random() * 2) - 1;
self.draw = function()
{
ctx.beginPath();
ctx.arc(self.xCo, self.yCo, self.size, 0, 2*Math.PI);
ctx.fillStyle = self.color;
ctx.fill();
};
circles[circlesCounter++] = self;
}
// Draw the background
function drawBackground()
{
ctx.fillStyle = background;
ctx.fillRect(0, 0, c.width, c.height);
}
function drawCircles()
{
for (let i = 0; i < circles.length; i++)
circles[i].draw();
}
function drawTest()
{
ctx.fillStyle = 'red';
ctx.fillRect(i++, i, 5, 5);
}
function drawCos()
{
ctx.fillStyle = 'white';
ctx.fillText("X: " + mouseX + " Y:" + mouseY, 10, 10, 200);
}
// Main loop
setInterval(function()
{
// Loop through circles and move them
for (let i = 0; i < circles.length; i++)
{
if (circle[i])
{
// Check left and right bounce
if (circle[i].xCo <= 0 || circle[i].xCo >= c.width)
circle[i].spdX = -circle[i].spdX;
circle[i].xCo += circle[i].spdX;
// Check left and right bounce
if (circle[i].yCo <= 0 || circle[i].yCo >= c.height)
circle[i].spdY = -circle[i].spdY;
circle[i].yCo += circle[i].spdY;
}
}
// Draw Everything
draw();
}, interval);
</script>
</body>
</html>
This code:
self.draw = function()
{
ctx.beginPath();
ctx.arc(self.xCo, self.yCo, self.size, 0, 2*Math.PI);
ctx.fillStyle = self.color;
ctx.fill();
};
Is overriding this function:
function draw()
{
// Loop
requestAnimationFrame(draw);
// Set NOW and DELTA
now = Date.now();
delta = now - then;
// New frame
if (delta > interval) {
// Update THEN
then = now - (delta % interval);
// Our animation
// Clear canvas then draw
ctx.clearRect(0, 0, c.width, c.height);
drawBackground();
drawCos();
drawCircles();
drawTest();
}
}
You need to rethink how you want to draw your circles because you're re-drawing the black canvas every time a click event is triggered. I mean, when a click is triggered, you're applying new coordinates, color, Etc, and probably that's not what you want to do.
My suggestion is create canvas per circle and append them into a DIV.
Hope it helps!

Javascript - interactive particle logo not working

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

scrambling images on canvas producing whitespace

I'm starting with a canvas element. I'm making the left half red, and the right side blue. Every half second, setInterval calls a function, scramble, which splits both RHS and LHS into pieces, and shuffles them.
Here is a fiddle: https://jsfiddle.net/aeq1g3yb/
The code is below. The reason I'm using window.onload is because this thing is supposed to scramble pictures and I want the pictures to load first. I'm using colors here because of the cross-origin business that I don't know enough about, so this is my accommodation.
var n = 1;
var v = 1;
function scramble() {
//get the canvas and change its width
var c = document.getElementById("myCanvas");
c.width = 600;
var ctx = c.getContext("2d");
//drawing 2 different colors side by side
ctx.fillStyle = "red";
ctx.fillRect(0, 0, c.width/2, c.height);
ctx.fillStyle = "blue";
ctx.fillRect(c.width/2, 0, c.width/2, c.height);
//how big will each shuffled chunk be
var stepsA = (c.width/2) / n;
var stepsB = (c.width/2) / n;
var step = stepsA + stepsB;
var imgDataA = [];
var imgDataB = [];
for (var i = 0; i < n; i++) {
var imgDataElementA = ctx.getImageData(stepsA*i, 0, stepsA, c.height);
var imgDataElementB = ctx.getImageData(c.width/2+stepsB*i, 0, stepsB, c.height);
imgDataA.push(imgDataElementA);
imgDataB.push(imgDataElementB);
}
//clearing out the canvas before laying on the new stuff
ctx.fillStyle = "white";
ctx.fillRect(0, 0, c.width, c.height);
//put the images back
for (var i = 0; i < n; i++) {
ctx.putImageData(imgDataA[i], step*i, 0);
ctx.putImageData(imgDataB[i], step*i+stepsA, 0);
}
//gonna count the steps
var count = document.getElementById("count");
count.innerHTML = n;
n += v;
if (n >= 100 || n <= 1) {
v *= -1;
}
}; //closing function scramble
window.onload = function() { //gotta do this bc code executes before image loads
scramble();
};
window.setInterval(scramble, 500);
More or less, this thing works the way I want it to. But there is one problem: Sometimes there are vertical white lines.
My question is:
Why are there white lines? If you view the fiddle, you will see the degree to which this impairs the effect of the shuffle.
You can`t divide a Pixel
The problem can be solve but will introduce some other artifacts as you can not divide integer pixels into fractions.
Quick solution
The following solution for your existing code rounds down for the start of a section and up for the width.
for (var i = 0; i < n; i++) {
var imgDataElementA = ctx.getImageData(
Math.floor(stepsA * i), 0,
Math.ceil(stepsA + stepsA * i) - Math.floor(stepsA * i), c.height
);
var imgDataElementB = ctx.getImageData(
Math.floor(c.width / 2 + stepsB * i), 0,
Math.ceil(c.width / 2 + stepsB * i + stepsB) - Math.floor(c.width / 2 + stepsB * i), c.height);
imgDataA.push(imgDataElementA);
imgDataB.push(imgDataElementB);
}
Quicker options
But doing this via the pixel image data is about the slowest possible way you could find to do it. You can just use the 2D context.imageDraw function to do the movement for you. Or if you want the best in terms of performance a WebGL solution would be the best with the fragment shader doing the scrambling for you as a parallel solution.
There is no perfect solution
But in the end you can not cut a pixel in half, there are a wide range of ways to attempt to solve this but each method has its own artifacts. Ideally you should only slice an image if the rule image.width % slices === 0 in all other cases you will have one or more slices that will not fit on an integer number of pixels.
Example of 4 rounding methods.
The demo shows 4 different methods and with 2 colors. Mouse over to see a closer view. Each method is separated horizontally with a white line. Hold the mouse button to increase the slice counter.
The top is your original.
The next three are 3 different ways of dealing with the fractional pixel width.
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
const m = mouse;
if(m.element){
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left - scrollX;
m.y = e.pageY - m.bounds.top - scrollY;
m.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : m.button;
}
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
const counterElement = document.getElementById("count");
// get constants for the demo
const c = document.getElementById("myCanvas");
mouse.element = c;
// The image with the blue and red
const img = document.createElement("canvas");
// the zoom image overlay
const zoom = document.createElement("canvas");
// the scrambled image
const scram = document.createElement("canvas");
// Set sizes and get context
const w = scram.width = zoom.width = img.width = c.width = 500;
const h = scram.height = zoom.height = img.height = c.height;
const dCtx = c.getContext("2d"); // display context
const iCtx = img.getContext("2d"); // source image context
const zCtx = zoom.getContext("2d"); // zoom context
const sCtx = scram.getContext("2d"); // scrambled context
// some constants
const zoomAmount = 4;
const zoomRadius = 60;
const framesToStep = 10;
function createTestPattern(ctx){
ctx.fillStyle = "red";
ctx.fillRect(0, 0, c.width/2, c.height/2);
ctx.fillStyle = "blue";
ctx.fillRect(c.width/2, 0, c.width/2, c.height/2);
ctx.fillStyle = "black";
ctx.fillRect(0, c.height/2, c.width/2, c.height/2);
ctx.fillStyle = "#CCC";
ctx.fillRect(c.width/2, c.height/2, c.width/2, c.height/2);
}
createTestPattern(iCtx);
sCtx.drawImage(iCtx.canvas, 0, 0);
// Shows a zoom area so that blind men like me can see what is going on.
function showMouseZoom(src,dest,zoom = zoomAmount,radius = zoomRadius){
dest.clearRect(0,0,w,h);
dest.imageSmoothingEnabled = false;
if(mouse.x >= 0 && mouse.y >= 0 && mouse.x < w && mouse.y < h){
dest.setTransform(zoom,0,0,zoom,mouse.x,mouse.y)
dest.drawImage(src.canvas, -mouse.x, -mouse.y);
dest.setTransform(1,0,0,1,0,0);
dest.globalCompositeOperation = "destination-in";
dest.beginPath();
dest.arc(mouse.x,mouse.y,radius,0,Math.PI * 2);
dest.fill();
dest.globalCompositeOperation = "source-over";
dest.lineWidth = 4;
dest.strokeStyle = "black";
dest.stroke();
}
}
function scramble(src,dest,y,height) {
const w = src.canvas.width;
const h = src.canvas.height;
const steps = (w/2) / slices;
dest.fillStyle = "white";
dest.fillRect(0, y, w, height);
for (var i = 0; i < slices * 2; i++) {
dest.drawImage(src.canvas,
((i / 2) | 0) * steps + (i % 2) * (w / 2)- 0.5, y,
steps + 1, height,
i * steps - 0.5, y,
steps+ 1, height
);
}
}
function scrambleFloor(src,dest,y,height) {
const w = src.canvas.width;
const h = src.canvas.height;
const steps = (w/2) / slices;
dest.fillStyle = "white";
dest.fillRect(0, y, w, height);
for (var i = 0; i < slices * 2; i++) {
dest.drawImage(src.canvas,
(((i / 2) | 0) * steps + (i % 2) * (w / 2)- 0.5) | 0, y,
steps + 1, height,
(i * steps - 0.5) | 0, y,
steps + 1, height
);
}
}
function scrambleNoOverlap(src,dest,y,height) {
const w = src.canvas.width;
const h = src.canvas.height;
const steps = (w / 2) / slices;
dest.fillStyle = "white";
dest.fillRect(0, y, w, height);
for (var i = 0; i < slices * 2; i++) {
dest.drawImage(src.canvas,
((i / 2) | 0) * steps + (i % 2) * (w / 2), y,
steps, height,
i * steps - 0.5, y,
steps, height
);
}
}
function scrambleOriginal(src,dest,y,height) {
const w = src.canvas.width;
const h = src.canvas.height;
//how big will each shuffled chunk be
var stepsA = (w/2) / slices;
var stepsB = (w/2) / slices;
var step = stepsA + stepsB;
var imgDataA = [];
var imgDataB = [];
for (var i = 0; i < slices; i++) {
var imgDataElementA = src.getImageData(stepsA*i, y, stepsA, height);
var imgDataElementB = src.getImageData(w/2+stepsB*i, y, stepsB, height);
imgDataA.push(imgDataElementA);
imgDataB.push(imgDataElementB);
}
//clearing out the canvas before laying on the new stuff
dest.fillStyle = "white";
dest.fillRect(0, y, w, height);
//put the images back
for (var i = 0; i < slices; i++) {
dest.putImageData(imgDataA[i], step*i, y);
dest.putImageData(imgDataB[i], step*i+stepsA, y);
}
}; //closing function scramble
const scrambleMethods = [scrambleOriginal,scramble,scrambleFloor,scrambleNoOverlap];
var frameCount = 0;
var sliceStep = 1;
var slices = 1;
function mainLoop(){
if(mouse.button){
if(frameCount++ % framesToStep === framesToStep-1){ // every 30 Frames
slices += sliceStep;
if(slices > 150 || slices < 2){ sliceStep = -sliceStep }
counterElement.textContent = slices; // Prevent reflow by using textContent
sCtx.clearRect(0,0,w,h);
sCtx.imageSmoothingEnabled = true;
const len = scrambleMethods.length;
for(var i = 0; i < len; i ++){
scrambleMethods[i](iCtx,sCtx,(128/len) * i, 128/len-2);
scrambleMethods[i](iCtx,sCtx,(128/len) * i + 128, 128/len-2);
}
}
}
dCtx.fillStyle = "white";
dCtx.fillRect(0,0,w,h);
dCtx.drawImage(sCtx.canvas,0,0);
showMouseZoom(dCtx,zCtx);
dCtx.drawImage(zCtx.canvas,0,0);
requestAnimationFrame(mainLoop);
}
//scramble(iCtx,sCtx);
requestAnimationFrame(mainLoop);
canvas {
border: 1px solid black;
}
#count {
position : absolute;
top : 0px;
left : 10px;
font-family: monospace;
font-size: 20px;
}
<canvas id="myCanvas" height = "256" title="Hold mouse button to chance slice count"></canvas>
<p id="count"></p>

Automatically Crop HTML5 canvas to contents

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>

Categories