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>
Related
I have a requirement to move many dots here and there inside a canvas.
Hence I created several arcs with different radius and placed them at random places.
var context = document.getElementById('stage').getContext('2d');
var radian = Math.PI / 180;
var x = 40;
var y = 40;
var r = 20;
var colorPoints = [];
var frames = 50;
var currentFrame = 0;
var toggle = false;
var iconsLoaded = false;
context.beginPath();
context.arc(x,y, r, 0 * radian, 360 * radian, false)
context.fill();
var drawMultipleCurves = function(ctx){
if(!iconsLoaded){
for (let i = 0; i < 600; i++) {
ctx.beginPath();
ctx.filter = 'blur(5px)';
ctx.fillStyle = '#B835FF';
colorPoints.push({x: Math.floor((Math.random() * 700) + 0), xMove: Math.floor((Math.random() * 2) + 0) , yMove: Math.floor((Math.random() * 2) + 0) , y: Math.floor((Math.random() * 700) + 0), radius: Math.floor((Math.random() * 20) + 5)});
ctx.arc(colorPoints[colorPoints.length - 1].x, colorPoints[colorPoints.length - 1].y, colorPoints[colorPoints.length - 1].radius, 0 * radian, 360 * radian, false);
ctx.fill();
ctx.closePath();
iconsLoaded = true;
}
}
else{
for(let i =0;i< colorPoints.length; i++){
if(frames === currentFrame ){
toggle = !toggle;
currentFrame = 0;
}
if(!toggle){
colorPoints[i].xMove === 1 ? colorPoints[i].x = colorPoints[i].x + 5 : colorPoints[i].x = colorPoints[i].x - 5;
colorPoints[i].yMove === 1 ? colorPoints[i].y = colorPoints[i].y + 5 : colorPoints[i].y = colorPoints[i].y - 5;
}
else{
colorPoints[i].xMove === 1 ? colorPoints[i].x = colorPoints[i].x - 5 : colorPoints[i].x = colorPoints[i].x + 5;
colorPoints[i].yMove === 1 ? colorPoints[i].y = colorPoints[i].y - 5 : colorPoints[i].y = colorPoints[i].y + 5;
}
ctx.beginPath();
ctx.arc(colorPoints[i].x, colorPoints[i].y, colorPoints[i].radius, 0 * radian, 360 * radian, false);
context.closePath( );
ctx.fill();
currentFrame = currentFrame + 1;
}
}
}
var animate = function(){
setTimeout(()=>{
context.clearRect(0,0,400,400);
context.beginPath();
drawMultipleCurves(context);
context.fill();
requestAnimationFrame(animate)
}, 1000/30)
}
requestAnimationFrame(animate)
<canvas id="stage" width="400" height="400">
<p>Your browser doesn't support canvas.</p>
</canvas>
Above is the code that I have tried.
I have first created and placed several dots at random places with random radius.
When I created them I saved all these random places in an array 'colorPoints'
Now I'm looping into this array and moving all the dots everytime 'requestAnimation' is called.
I'm able to achieve my animation of moving the dots randomly but as I have used 800 dots and then saving them into an array and then again looping them to move their position, the animation is not looking smooth.
It looks like it is moving and strucking. How can I achieve this animation smoothly?
Thanks in advance :)
Render "fill" once per style
Your code is slowing down due to where you placed fill (same if you use stroke)
When you have many objects with the same style call fill only once per frame for each object.
You had something like
for (const c of circles) {
ctx.beginPath();
ctx.arc(c.x, c.y, c.r, 0, TAU)
ctx.fill();
}
With a filter active the fill command forces the filter to be reset, which for blur is complex.
Rather add all the arcs then fill.
ctx.beginPath();
for (const c of circles) {
ctx.moveTo(c.x + c.r, c.y);
ctx.arc(c.x, c.y, c.r, 0, TAU)
}
ctx.fill();
The move ctx.moveTo(c.x + c.r, c.y); is used to close the previous arc.
You can also close the arc with ctx.closePath but this can be a lot slower when you have many arcs in the path buffer.
// slower than using moveTo
ctx.beginPath();
for (const c of circles) {
ctx.arc(c.x, c.y, c.r, 0, TAU)
ctx.closePath();
}
ctx.fill();
Example
Example draws 600 arcs using the blur filter as it only calls fill once per frame. This should run smooth on all but the most low end devices.
See function drawCircles
requestAnimationFrame(animate);
const ctx = canvas.getContext('2d');
const W = canvas.width;
const BLUR = 5;
const CIRCLE_COUNT = 600;
const MIN_RADIUS = BLUR;
const MAX_RADIUS = 30;
const MAX_DELTA = 1;
const MAX_CIR_R = MAX_RADIUS + BLUR;
const MOVE_SIZE = MAX_CIR_R * 2 + W;
const TAU = 2 * Math.PI;
const setOf = (c, cb, i = 0, a = []) => { while(i < c) { a.push(cb(i++)) } return a };
const rnd = (m, M) => Math.random() * (M - m) + m;
const style = {
filter: "blur(" + BLUR + "px)",
fillStyle: '#B835FF',
};
var currentStyle;
function setStyle(ctx, style) {
if (currentStyle !== style) {
Object.assign(ctx, style);
currentStyle = style;
}
}
const circle = {
get x() { return rnd(-MAX_CIR_R, W + MAX_CIR_R) },
get y() { return rnd(-MAX_CIR_R, W + MAX_CIR_R) },
get dx() { return rnd(-MAX_DELTA, MAX_DELTA) },
get dy() { return rnd(-MAX_DELTA, MAX_DELTA) },
get r() { return rnd(MIN_RADIUS, MAX_RADIUS) },
move() {
var x = this.x + this.dx + MOVE_SIZE + MAX_CIR_R;
var y = this.y + this.dy + MOVE_SIZE + MAX_CIR_R;
this.x = x % MOVE_SIZE - MAX_CIR_R;
this.y = y % MOVE_SIZE - MAX_CIR_R;
}
};
const circles = setOf(CIRCLE_COUNT, () => Object.assign({}, circle));
function drawCircles(circles, ctx, style) {
setStyle(ctx, style);
ctx.beginPath();
for (const c of circles) {
ctx.moveTo(c.x + c.r, c.y);
ctx.arc(c.x, c.y, c.r, 0, TAU);
}
ctx.fill();
}
function updateCircles(circles) {
for (const c of circles) { c.move(); }
}
function animate() {
ctx.clearRect(0,0,W, W);
updateCircles(circles);
drawCircles(circles, ctx, style);
requestAnimationFrame(animate);
}
<canvas id="canvas" width="600" height="600"> </canvas>
If you have several colors, group all the same colors so you can keep the number of fill calls as low as possible.
There are many ways to get the same effect with many colors (each circle a different color) but will need more setup code.
The CanvasRenderingContext2D blur filter is quite heavy - especially if you use it on a canvas consisting of 600 circles. That means on every screen update it has to re-draw 600 circles and apply a blur filter afterwards.
The usual approach is a little different. Initially you create a master texture with a blurred circle. This texture can then be re-used and drawn onto the canvas using the drawImage() method. To vary the size of the circles there is no radius anymore though. We can get the same effect by using a scale instead.
Here's an example:
var context = document.getElementById('stage').getContext('2d');
var radian = Math.PI / 180;
var x = 40;
var y = 40;
var r = 20;
var colorPoints = [];
var frames = 50;
var currentFrame = 0;
var toggle = false;
var iconsLoaded = false;
var texture = document.createElement("canvas");
var textureContext = texture.getContext("2d");
texture.width = 80;
texture.height = 80;
textureContext.filter = 'blur(5px)';
textureContext.fillStyle = '#B835FF';
textureContext.arc(texture.width / 2, texture.height / 2, 25, 0 * radian, 360 * radian, false);
textureContext.fill();
textureContext.closePath();
var drawMultipleCurves = function(ctx) {
if (!iconsLoaded) {
for (let i = 0; i < 600; i++) {
colorPoints.push({
x: Math.floor((Math.random() * 700) + 0),
xMove: Math.floor((Math.random() * 2) + 0),
yMove: Math.floor((Math.random() * 2) + 0),
y: Math.floor((Math.random() * 700) + 0),
scale: 0.2 + Math.random() * 0.8
});
iconsLoaded = true;
}
} else {
for (let i = 0; i < colorPoints.length; i++) {
if (frames === currentFrame) {
toggle = !toggle;
currentFrame = 0;
}
if (!toggle) {
colorPoints[i].xMove === 1 ? colorPoints[i].x = colorPoints[i].x + 5 : colorPoints[i].x = colorPoints[i].x - 5;
colorPoints[i].yMove === 1 ? colorPoints[i].y = colorPoints[i].y + 5 : colorPoints[i].y = colorPoints[i].y - 5;
} else {
colorPoints[i].xMove === 1 ? colorPoints[i].x = colorPoints[i].x - 5 : colorPoints[i].x = colorPoints[i].x + 5;
colorPoints[i].yMove === 1 ? colorPoints[i].y = colorPoints[i].y - 5 : colorPoints[i].y = colorPoints[i].y + 5;
}
ctx.drawImage(texture, colorPoints[i].x, colorPoints[i].y, texture.width * colorPoints[i].scale, texture.height * colorPoints[i].scale);
currentFrame = currentFrame + 1;
}
}
}
var animate = function() {
setTimeout(() => {
context.clearRect(0, 0, 400, 400);
context.beginPath();
drawMultipleCurves(context);
context.fill();
requestAnimationFrame(animate)
})
}
requestAnimationFrame(animate)
<canvas id="stage" width="400" height="400">
<p>Your browser doesn't support canvas.</p>
</canvas>
I am trying to draw some letters to a canvas in a very specific way - able to target individual letters and apply an alpha.
These words need to be centered on the baseline and aligned center in the canvas and filled with strokeText rather than fill style.
The text also needs to be line broken resulting in eg;
Now, I have tried several ways of getting this out - it works fine (without the fade) when writing out the words (as full words) - however when I attempt to write them out as individual letters I cannot center them correctly. My code is below omitting the alpha on the specific letters, which once I can center things correctly shouldn't be an issue!
I realize the issue is I am trying to draw each letter separately centered at 0 on the canvas and adding letter spacing for each letter, but given the different size of the middle line I cannot figure a way to have them centered!
var can = document.querySelector('canvas'),
ctx = can.getContext('2d');
function drawStroked(text, fontSize, color, offsetX, offsetY) {
let line = text.split('\n');
this.ctx.font = fontSize + 'px ' + 'TimesNewRoman';
this.ctx.strokeStyle = color;
this.ctx.lineWidth = 2;
this.ctx.textBaseline = 'middle';
this.ctx.textAlign = 'center';
let positionX = this.ctx.canvas.width/3;
let positionY = this.ctx.canvas.height/4;
if(offsetX !== 0) {
positionX += offsetX;
}
if(offsetY !== 0) {
positionY += offsetY;
}
for (var i = 0; i < line.length; i++) {
for (var j = 0; j < line[i].length; j++) {
let letterSpacing = 0;
let lineHeight = positionY;
if(line[i][j] === line[i].length) {
lineHeight = lineHeight * i;
}
this.ctx.strokeText(line[i][j], positionX + (letterSpacing + (j*130)), positionY + (i*fontSize));
}
}
}
drawStroked('THIS\nIS THE\nTEXT', 100, '#000', 0, 0);
<canvas width="1000" height="1000"></canvas>
Resulting output & Finished code thanks to Blindman67!
const Hero = class {
constructor(pos, canvas) {
this.position = document.getElementById(pos);
this.canvas = document.getElementById(canvas);
this.height = document.getElementsByClassName('home')[0].clientHeight;
this.width = this.position.offsetWidth;
this.ctx = this.canvas.getContext('2d');
this.title = 'THIS\nIS THE\nTEXT';
this.canvas.width = this.width;
this.canvas.height = this.height;
this._init_ui();
}
// Draw text to text canvas
_init_ui() {
// BLUE
this.drawStroked(300, '#1816ff', -3, 2, 0.95, [1, 5, 9, 12]);
// GREEN
this.drawStroked(300, '#1bff32', 0, 0, 0.95, [1, 5, 9, 12]);
// RED
this.drawStroked(300, '#ff162f', 3, -2, 0.95, [1, 5, 9, 12]);
}
drawStroked(fontSize, color, offsetX, offsetY, textVertSpacing, fade) {
// Random Char's to scramble through --- to do
// let chars = '!<>-_\\/[]{}—=+*^?#________';
// The words
let line = this.title.split('\n');
// Set the font + size
this.ctx.font = fontSize + 'px ' + 'Kommissar';
// Set the colour - NEED TO ADD ALPHA LOGIC
this.ctx.strokeStyle = color;
// Set the stroke width
this.ctx.lineWidth = 1;
// Set the baseline
this.ctx.textBaseline = 'middle';
// Set the align
this.ctx.textAlign = 'center';
let positionX = this.width/2;
let positionY = this.height/4;
positionX += offsetX;
positionY += offsetY;
let charIndex = 0;
for (var i = 0; i < line.length; i++) {
// get the width of the whole line
let width = this.ctx.measureText(line[i]).width;
console.log(width);
// use the width to find start
var textPosX = positionX - width / 2;
for (let j = 0; j < line[i].length; j++) {
// get char
let char = line[i][j];
// get its width
let cWidth = this.ctx.measureText(char).width;
// check if char needs to fade
if (fade.indexOf(charIndex) > -1) {
this.ctx.globalAlpha = 0.2;
} else {
this.ctx.globalAlpha = 1;
}
// draw the char offset by half its width (center)
this.ctx.strokeText(char, textPosX + cWidth / 2, positionY);
// move too the next pos
textPosX += cWidth;
// count the char
charIndex += 1;
}
// move down one line
positionY += fontSize * textVertSpacing;
}
}
};
export default Hero;
Use ctx.measureText
You need to use ctx.measureText and get the the width of each character, then you can space them correctly.
Correct char spacing
Because you have alignment center, you have to move the character half its width, then draw it and then move half the width again. The spacing between character's centers is half the width of each added. So if a "I" is 20 pixels wide and a "W" is 60 then the space between them is 10 + 30 = 40;
Fade characters
To do the fade I passed an array with the index of the characters to fade. Each Time I draw a character I count it. To check if a character should fade I check the index array for the character count. If they match then fade that character.
See the example for more information
Simple example...
...of what I think you want. I added two red lines to make sure the alignment was correct.
const ctx = canvas.getContext('2d');
ctx.fillStyle = "#FDD"; // mark the center
ctx.fillRect(canvas.width / 2 | 0, 0, 1, canvas.height);
ctx.fillRect(0, canvas.height / 2 | 0, canvas.width, 1);
ctx.fillStyle = "black";
// textVertSpacing is fraction of FontSize
// fade is the index of characters to fade, including spaces
// centerX and y is center of all text
function drawStroked(text, fontSize, color, centerX, centerY, textVertSpacing, fade) {
let line = text.split('\n');
ctx.font = fontSize + 'px ' + 'TimesNewRoman';
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
// to count each character
var charIndex = 0;
// find the top ypos and then move down half a char space
var yPos = centerY - fontSize * line.length * 0.5 * textVertSpacing + fontSize * textVertSpacing / 2;
for (var i = 0; i < line.length; i++) {
// get the width of the whole line
var width = ctx.measureText(line[i]).width;
// use the width to find start
var textPosX = centerX - width / 2;
for (var j = 0; j < line[i].length; j++) {
// get char
var char = line[i][j];
// get its width
var cWidth = ctx.measureText(char).width;
// check if char needs to fade
if (fade.indexOf(charIndex) > -1) {
ctx.globalAlpha = 0.5;
} else {
ctx.globalAlpha = 1;
}
// draw the char offset by half its width (center)
ctx.fillText(char, textPosX + cWidth / 2, yPos);
// move too the next pos
textPosX += cWidth;
// count the char
charIndex += 1
}
// move down one line
yPos += fontSize * textVertSpacing;
}
}
drawStroked('THIS\nIS THE\nTEXT', 60, '#000', canvas.width / 2, canvas.height / 2, 0.9, [2, 4, 8, 12]);
<canvas id="canvas" width="500" height="200"></canvas>
Update
Added some flicker to text by adding an animation loop and calling the text rendering function every few frames. The flicker is done by randomizing the alpha. See snippet below for more info.
requestAnimationFrame(animLoop);
const flickerRate = 4; // change alpha every 4 frames
var frameCount = 0;
const ctx = canvas.getContext('2d');
ctx.fillStyle = "#FDD"; // mark the center
ctx.fillRect(canvas.width / 2 | 0, 0, 1, canvas.height);
ctx.fillRect(0, canvas.height / 2 | 0, canvas.width, 1);
ctx.fillStyle = "black";
function drawStroked(text, fontSize, color, centerX, centerY, textVertSpacing, fade) {
let line = text.split('\n');
ctx.font = fontSize + 'px ' + 'TimesNewRoman';
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
var charIndex = 0;
var yPos = centerY - fontSize * line.length * 0.5 * textVertSpacing + fontSize * textVertSpacing / 2;
for (var i = 0; i < line.length; i++) {
var width = ctx.measureText(line[i]).width;
var textPosX = centerX - width / 2;
for (var j = 0; j < line[i].length; j++) {
var char = line[i][j];
var cWidth = ctx.measureText(char).width;
ctx.globalAlpha = fade.indexOf(charIndex) > -1 ? Math.random()* 0.5+0.25 : 1;
ctx.fillText(char, textPosX + cWidth / 2, yPos);
textPosX += cWidth;
charIndex += 1
}
yPos += fontSize * textVertSpacing;
}
}
function animLoop(){
if((frameCount % flickerRate) === 0){
ctx.clearRect(0,0,canvas.width,canvas.height);
drawStroked('THIS\nIS THE\nTEXT', 60, '#000', canvas.width / 2, canvas.height / 2, 0.9, [2, 4, 8, 12]);
}
frameCount ++;
requestAnimationFrame(animLoop);
}
<canvas id="canvas" width="500" height="200"></canvas>
Using this in a webpack setup so sorry it doesnt run!
// Code in UTIL
getRandomInt(max) {
return Math.floor(Math.random() * (max - 0 + 1)) + 0;
};
const $window = $(window);
let running = false;
const Hero = {
init() {
this.home = $('#home');
this.position = $('#hero');
this.canvas = $('#title');
this.ctx = this.canvas[0].getContext('2d');
this.width = this.position.width();
this.height = this.home.height();
this.ctx.lineWidth = 1.5;
this.fontSize = null;
this.letterSpacing = null;
if(this.position.lenth === 0) {
return;
}
if(running) {
return;
}
// Set hero opacity to 0 for animation
// $('#hero').css('opacity', 0);
this.size();
$window.on('resize', () => {
clearTimeout(this.debounce);
this.debounce = setTimeout( () => {
this.height = this.home.height();
this.width = this.position.width();
this.size();
}, 50);
});
},
size() {
running = true;
this.canvas[0].width = this.width;
this.canvas[0].height = this.height;
if(this.width < 1000) {
this.fontSize = 150;
this.letterSpacing = 5;
} else {
this.fontSize = 300;
this.letterSpacing = 30;
}
},
animate(frames) {
var frameCount = frames || 0;
const flickerRate = 4;
const fade = [Utils.getRandomInt(13), Utils.getRandomInt(13)];
if((frameCount % flickerRate) === 0){
this.ctx.clearRect(0, 0, this.width, this.height);
// Blue
this.drawStroked(this.fontSize, '#0426ff', -2, 2, true, fade);
// Green
this.drawStroked(this.fontSize, '#04ffae', 1, 2, true, fade);
// Pink
this.drawStroked(this.fontSize, '#ff29ad', 0, 0, true, fade);
// White
this.drawStroked(this.fontSize, '#fff', 0, 0, true, fade);
}
frameCount ++;
console.log(frameCount);
// requestAnimationFrame(this.animate);
setTimeout(() => {
this.animate(frameCount);
}, 0.5);
},
drawStroked(fontSize, color, offsetX, offsetY, flicker, fade) {
let line = 'CODE\nIN THE\nDARK'.split('\n'),
chars = line.join('');
// Set the font + size
this.ctx.font = fontSize + 'px ' + 'Kommissar';
// Set the colour
this.ctx.strokeStyle = color;
// Set the baseline
this.ctx.textBaseline = 'middle';
// Set the align
this.ctx.textAlign = 'center';
let letterSpacing = this.letterSpacing,
positionX = (this.width/2 + letterSpacing) + offsetX,
positionY = (this.height/4) + offsetY,
charIndex = 0;
for (var i = 0; i < line.length; i++) {
// get the width of the whole line
let width = this.ctx.measureText(line[i]).width;
// use the width to find start
var textPosX = positionX - width / 2;
for (let j = 0; j < line[i].length; j++) {
// get char
let char = line[i][j];
// get its width
let cWidth = this.ctx.measureText(char).width;
// check if char needs to fade
if(flicker) {
this.ctx.globalAlpha = fade.indexOf(charIndex) > -1 ? Math.random() * 0.5 + 0.25 : 0;
} else {
this.ctx.globalAlpha = 1;
}
// draw the char offset by half its width (center)
this.ctx.shadowColor = color;
this.ctx.shadowBlur = 15;
this.ctx.strokeText(char, textPosX + cWidth / 2, positionY);
// move too the next pos
textPosX += cWidth;
// count the char
charIndex += 1;
}
// move down one line
positionY += fontSize * 1.05;
}
}
};
export default Hero;
#home {
width: 100%;
#hero {
position: absolute;
z-index: 5;
top: 0;
left: 0;
width: 100%;
padding: 30px 0;
> canvas {
margin: 0 auto;
display: block;
}
}
}
<div id="home">
<div id="hero">
<canvas id="title"></canvas>
</div>
</div>
I want to create a random generated image (random colors), like this one. But, I want to do it in javascript, but for some reason I am getting black screen.
Here is my code:
var g=document . createElement( 'canvas').getContext('2d');
g.canvas.width=g.canvas.height = 800;
g.imgd = g.getImageData(0, 0, 800, 800);
g.data = g.imgd.data;
g.data.forEach((_, index) => (index & 3) < 3 && (g.data[index] = Math.random()));
g.putImageData(g.imgd, 0, 0);
document.body.appendChild(g.canvas);;;
And i am getting black screen, and on some websites it is white screen. So what is what not working in my script? My english is not very good, but can someone explain what is wrong, my code dont'esnt working.
I also tried different dimensions of canvas and I dont see any errors so what is wrong?
You are using Math.random() which generates floats from 0 to 1 without including 1. Since you're applying zeroes to the color components (the data from getImageData().data), you get the color black (rgb(0, 0, 0)).
Here's a more readable solution:
var canvas = document.createElement('canvas');
canvas.width = canvas.height = 800;
var ctx = canvas.getContext('2d');
var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
for (var i = 0; i < imgData.data.length; i += 4) {
imgData.data[i] = randomInt(0, 255); // red
imgData.data[i+1] = randomInt(0, 255); // green
imgData.data[i+2] = randomInt(0, 255); // blue
imgData.data[i+3] = 255; // alpha
}
ctx.putImageData(imgData, 0, 0);
document.body.appendChild(canvas);
Math.random() returns a floating point number, not within the full range of 0-255. You can alternatively use .fillStyle() and set the color to a random hex color.
function pixels(width = 100, height = 100, size = 1, canvas) {
var canvas = canvas || document.createElement("canvas");
var ctx = canvas.getContext("2d");
var total = [];
canvas.width = width;
canvas.height = height;
function random() {
return "XXXXXX".replace(/X/g, function() {
var seed = "a0b1c2d3e4f56789";
return seed.charAt(Math.floor(Math.random() * seed.length))
})
};
for (var x = 0; x <= width; x += size) {
total.push(x)
};
total.forEach(function(value, index) {
for (var i = 0; i <= height; i++) {
ctx.fillStyle = "#" + random();
ctx.fillRect(value, total[i], size, size);
}
});
document.body.appendChild(canvas);
return ctx;
};
var c = pixels(window.innerWidth - 20, window.innerHeight - 20);
I'm trying to create a hyperdrive effect, like from Star Wars, where the stars have a motion trail. I've gotten as far as creating the motion trail on a single circle, it still looks like the trail is going down in the y direction and not forwards or positive in the z direction.
Also, how could I do this with (many) randomly placed circles as if they were stars?
My code is on jsfiddle (https://jsfiddle.net/5m7x5zxu/) and below:
var canvas = document.querySelector("canvas");
var context = canvas.getContext("2d");
var xPos = 180;
var yPos = 100;
var motionTrailLength = 16;
var positions = [];
function storeLastPosition(xPos, yPos) {
// push an item
positions.push({
x: xPos,
y: yPos
});
//get rid of first item
if (positions.length > motionTrailLength) {
positions.pop();
}
}
function update() {
context.clearRect(0, 0, canvas.width, canvas.height);
for (var i = positions.length-1; i > 0; i--) {
var ratio = (i - 1) / positions.length;
drawCircle(positions[i].x, positions[i].y, ratio);
}
drawCircle(xPos, yPos, "source");
var k=2;
storeLastPosition(xPos, yPos);
// update position
if (yPos > 125) {
positions.pop();
}
else{
yPos += k*1.1;
}
requestAnimationFrame(update);
}
update();
function drawCircle(x, y, r) {
if (r == "source") {
r = 1;
} else {
r*=1.1;
}
context.beginPath();
context.arc(x, y, 3, 0, 2 * Math.PI, true);
context.fillStyle = "rgba(255, 255, 255, " + parseFloat(1-r) + ")";
context.fill();
}
Canvas feedback and particles.
This type of FX can be done many ways.
You could just use a particle systems and draw stars (as lines) moving away from a central point, as the speed increase you increase the line length. When at low speed the line becomes a circle if you set ctx.lineWidth > 1 and ctx.lineCap = "round"
To add to the FX you can use render feedback as I think you have done by rendering the canvas over its self. If you render it slightly larger you get a zoom FX. If you use ctx.globalCompositeOperation = "lighter" you can increase the stars intensity as you speed up to make up for the overall loss of brightness as stars move faster.
Example
I got carried away so you will have to sift through the code to find what you need.
The particle system uses the Point object and a special array called bubbleArray to stop GC hits from janking the animation.
You can use just an ordinary array if you want. The particles are independent of the bubble array. When they have moved outside the screen they are move to a pool and used again when a new particle is needed. The update function moves them and the draw Function draws them I guess LOL
The function loop is the main loop and adds and draws particles (I have set the particle count to 400 but should handle many more)
The hyper drive is operated via the mouse button. Press for on, let go for off. (It will distort the text if it's being displayed)
The canvas feedback is set via that hyperSpeed variable, the math is a little complex. The sCurce function just limits the value to 0,1 in this case to stop alpha from going over or under 1,0. The hyperZero is just the sCurve return for 1 which is the hyper drives slowest speed.
I have pushed the feedback very close to the limit. In the first few lines of the loop function you can set the top speed if(mouse.button){ if(hyperSpeed < 1.75){ Over this value 1.75 and you will start to get bad FX, at about 2 the whole screen will just go white (I think that was where)
Just play with it and if you have questions ask in the comments.
const ctx = canvas.getContext("2d");
// very simple mouse
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));
// High performance array pool using buubleArray to separate pool objects and active object.
// This is designed to eliminate GC hits involved with particle systems and
// objects that have short lifetimes but used often.
// Warning this code is not well tested.
const bubbleArray = () => {
const items = [];
var count = 0;
return {
clear(){ // warning this dereferences all locally held references and can incur Big GC hit. Use it wisely.
this.items.length = 0;
count = 0;
},
update() {
var head, tail;
head = tail = 0;
while(head < count){
if(items[head].update() === false) {head += 1 }
else{
if(tail < head){
const temp = items[head];
items[head] = items[tail];
items[tail] = temp;
}
head += 1;
tail += 1;
}
}
return count = tail;
},
createCallFunction(name, earlyExit = false){
name = name.split(" ")[0];
const keys = Object.keys(this);
if(Object.keys(this).indexOf(name) > -1){ throw new Error(`Can not create function name '${name}' as it already exists.`) }
if(!/\W/g.test(name)){
let func;
if(earlyExit){
func = `var items = this.items; var count = this.getCount(); var i = 0;\nwhile(i < count){ if (items[i++].${name}() === true) { break } }`;
}else{
func = `var items = this.items; var count = this.getCount(); var i = 0;\nwhile(i < count){ items[i++].${name}() }`;
}
!this.items && (this.items = items);
this[name] = new Function(func);
}else{ throw new Error(`Function name '${name}' contains illegal characters. Use alpha numeric characters.`) }
},
callEach(name){var i = 0; while(i < count){ if (items[i++][name]() === true) { break } } },
each(cb) { var i = 0; while(i < count){ if (cb(items[i], i++) === true) { break } } },
next() { if (count < items.length) { return items[count ++] } },
add(item) {
if(count === items.length){
items.push(item);
count ++;
}else{
items.push(items[count]);
items[count++] = item;
}
return item;
},
getCount() { return count },
}
}
// Helpers rand float, randI random Int
// doFor iterator
// sCurve curve input -Infinity to Infinity out -1 to 1
// randHSLA creates random colour
// CImage, CImageCtx create image and image with context attached
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 doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); }; // the ; after while loop is important don't remove
const sCurve = (v,p) => (2 / (1 + Math.pow(p,-v))) -1;
const randHSLA = (h, h1, s = 100, s1 = 100, l = 50, l1 = 50, a = 1, a1 = 1) => { return `hsla(${randI(h,h1) % 360},${randI(s,s1)}%,${randI(l,l1)}%,${rand(a,a1)})` }
const CImage = (w = 128, h = w) => (c = document.createElement("canvas"),c.width = w,c.height = h, c);
const CImageCtx = (w = 128, h = w) => (c = CImage(w,h), c.ctx = c.getContext("2d"), c);
// create image to hold text
var textImage = CImageCtx(1024, 1024);
var c = textImage.ctx;
c.fillStyle = "#FF0";
c.font = "64px arial black";
c.textAlign = "center";
c.textBaseline = "middle";
const text = "HYPER,SPEED FX,VII,,Battle of Jank,,Hold the mouse,button to increase,speed.".split(",");
text.forEach((line,i) => { c.fillText(line,512,i * 68 + 68) });
const maxLines = text.length * 68 + 68;
function starWarIntro(image,x1,y1,x2,y2,pos){
var iw = image.width;
var ih = image.height;
var hh = (x2 - x1) / (y2 - y1); // Slope of left edge
var w2 = iw / 2; // half width
var z1 = w2 - x1; // Distance (z) to first line
var z2 = (z1 / (w2 - x2)) * z1 - z1; // distance (z) between first and last line
var sk,t3,t3a,z3a,lines, z3, dd = 0, a = 0, as = 2 / (y2 - y1);
for (var y = y1; y < y2 && dd < maxLines; y++) { // for each line
t3 = ((y - y1) * hh) + x1; // get scan line top left edge
t3a = (((y+1) - y1) * hh) + x1; // get scan line bottom left edge
z3 = (z1 / (w2 - t3)) * z1; // get Z distance to top of this line
z3a = (z1 / (w2 - t3a)) * z1; // get Z distance to bottom of this line
dd = ((z3 - z1) / z2) * ih; // get y bitmap coord
a += as;
ctx.globalAlpha = a < 1 ? a : 1;
dd += pos; // kludge for this answer to make text move
// does not move text correctly
lines = ((z3a - z1) / z2) * ih-dd; // get number of lines to copy
ctx.drawImage(image, 0, dd , iw, lines, t3, y, w - t3 * 2, 1.5);
}
}
// canvas settings
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
// diagonal distance used to set point alpha (see point update)
var diag = Math.sqrt(w * w + h * h);
// If window size is changed this is called to resize the canvas
// It is not called via the resize event as that can fire to often and
// debounce makes it feel sluggish so is called from main loop.
function resizeCanvas(){
points.clear();
canvas.width = innerWidth;
canvas.height = innerHeight;
w = canvas.width;
h = canvas.height;
cw = w / 2; // center
ch = h / 2;
diag = Math.sqrt(w * w + h * h);
}
// create array of points
const points = bubbleArray();
// create optimised draw function itterator
points.createCallFunction("draw",false);
// spawns a new star
function spawnPoint(pos){
var p = points.next();
p = points.add(new Point())
if (p === undefined) { p = points.add(new Point()) }
p.reset(pos);
}
// point object represents a single star
function Point(pos){ // this function is duplicated as reset
if(pos){
this.x = pos.x;
this.y = pos.y;
this.dead = false;
}else{
this.x = 0;
this.y = 0;
this.dead = true;
}
this.alpha = 0;
var x = this.x - cw;
var y = this.y - ch;
this.dir = Math.atan2(y,x);
this.distStart = Math.sqrt(x * x + y * y);
this.speed = rand(0.01,1);
this.col = randHSLA(220,280,100,100,50,100);
this.dx = Math.cos(this.dir) * this.speed;
this.dy = Math.sin(this.dir) * this.speed;
}
Point.prototype = {
reset : Point, // resets the point
update(){ // moves point and returns false when outside
this.speed *= hyperSpeed; // increase speed the more it has moved
this.x += Math.cos(this.dir) * this.speed;
this.y += Math.sin(this.dir) * this.speed;
var x = this.x - cw;
var y = this.y - ch;
this.alpha = (Math.sqrt(x * x + y * y) - this.distStart) / (diag * 0.5 - this.distStart);
if(this.alpha > 1 || this.x < 0 || this.y < 0 || this.x > w || this.h > h){
this.dead = true;
}
return !this.dead;
},
draw(){ // draws the point
ctx.strokeStyle = this.col;
ctx.globalAlpha = 0.25 + this.alpha *0.75;
ctx.beginPath();
ctx.lineTo(this.x - this.dx * this.speed, this.y - this.dy * this.speed);
ctx.lineTo(this.x, this.y);
ctx.stroke();
}
}
const maxStarCount = 400;
const p = {x : 0, y : 0};
var hyperSpeed = 1.001;
const alphaZero = sCurve(1,2);
var startTime;
function loop(time){
if(startTime === undefined){
startTime = time;
}
if(w !== innerWidth || h !== innerHeight){
resizeCanvas();
}
// if mouse down then go to hyper speed
if(mouse.button){
if(hyperSpeed < 1.75){
hyperSpeed += 0.01;
}
}else{
if(hyperSpeed > 1.01){
hyperSpeed -= 0.01;
}else if(hyperSpeed > 1.001){
hyperSpeed -= 0.001;
}
}
var hs = sCurve(hyperSpeed,2);
ctx.globalAlpha = 1;
ctx.setTransform(1,0,0,1,0,0); // reset transform
//==============================================================
// UPDATE the line below could be the problem. Remove it and try
// what is under that
//==============================================================
//ctx.fillStyle = `rgba(0,0,0,${1-(hs-alphaZero)*2})`;
// next two lines are the replacement
ctx.fillStyle = "Black";
ctx.globalAlpha = 1-(hs-alphaZero) * 2;
//==============================================================
ctx.fillRect(0,0,w,h);
// the amount to expand canvas feedback
var sx = (hyperSpeed-1) * cw * 0.1;
var sy = (hyperSpeed-1) * ch * 0.1;
// increase alpha as speed increases
ctx.globalAlpha = (hs-alphaZero)*2;
ctx.globalCompositeOperation = "lighter";
// draws feedback twice
ctx.drawImage(canvas,-sx, -sy, w + sx*2 , h + sy*2)
ctx.drawImage(canvas,-sx/2, -sy/2, w + sx , h + sy)
ctx.globalCompositeOperation = "source-over";
// add stars if count < maxStarCount
if(points.getCount() < maxStarCount){
var cent = (hyperSpeed - 1) *0.5; // pulls stars to center as speed increases
doFor(10,()=>{
p.x = rand(cw * cent ,w - cw * cent); // random screen position
p.y = rand(ch * cent,h - ch * cent);
spawnPoint(p)
})
}
// as speed increases make lines thicker
ctx.lineWidth = 2 + hs*2;
ctx.lineCap = "round";
points.update(); // update points
points.draw(); // draw points
ctx.globalAlpha = 1;
// scroll the perspective star wars text FX
var scrollTime = (time - startTime) / 5 - 2312;
if(scrollTime < 1024){
starWarIntro(textImage,cw - h * 0.5, h * 0.2, cw - h * 3, h , scrollTime );
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
canvas { position : absolute; top : 0px; left : 0px; }
<canvas id="canvas"></canvas>
Here's another simple example, based mainly on the same idea as Blindman67, concetric lines moving away from center at different velocities (the farther from center, the faster it moves..) also no recycling pool here.
"use strict"
var c = document.createElement("canvas");
document.body.append(c);
var ctx = c.getContext("2d");
var w = window.innerWidth;
var h = window.innerHeight;
var ox = w / 2;
var oy = h / 2;
c.width = w; c.height = h;
const stars = 120;
const speed = 0.5;
const trailLength = 90;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, w, h);
ctx.fillStyle = "#fff"
ctx.fillRect(ox, oy, 1, 1);
init();
function init() {
var X = [];
var Y = [];
for(var i = 0; i < stars; i++) {
var x = Math.random() * w;
var y = Math.random() * h;
X.push( translateX(x) );
Y.push( translateY(y) );
}
drawTrails(X, Y)
}
function translateX(x) {
return x - ox;
}
function translateY(y) {
return oy - y;
}
function getDistance(x, y) {
return Math.sqrt(x * x + y * y);
}
function getLineEquation(x, y) {
return function(n) {
return y / x * n;
}
}
function drawTrails(X, Y) {
var count = 1;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, w, h);
function anim() {
for(var i = 0; i < X.length; i++) {
var x = X[i];
var y = Y[i];
drawNextPoint(x, y, count);
}
count+= speed;
if(count < trailLength) {
window.requestAnimationFrame(anim);
}
else {
init();
}
}
anim();
}
function drawNextPoint(x, y, step) {
ctx.fillStyle = "#fff";
var f = getLineEquation(x, y);
var coef = Math.abs(x) / 100;
var dist = getDistance( x, y);
var sp = speed * dist / 100;
for(var i = 0; i < sp; i++) {
var newX = x + Math.sign(x) * (step + i) * coef;
var newY = translateY( f(newX) );
ctx.fillRect(newX + ox, newY, 1, 1);
}
}
body {
overflow: hidden;
}
canvas {
position: absolute;
left: 0;
top: 0;
}
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>