I have developed a game in NodeJS where you have to guess an image's name meanwhile the image depixelates.
The problem is that the server uses canvas to pixelate the image but the render don't fit entirely in the frame as you can see :
The pixelate function :
function pixelate(image, ctx, canvas, value) {
var size = value / 100,
w = canvas.width * size,
h = canvas.height * size;
ctx.drawImage(image, 0, 0, w, h);
ctx.msImageSmoothingEnabled = false;
ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;
ctx.drawImage(canvas, 0, 0, w, h, 0, 0, canvas.width, canvas.height)
}
And the loop were i pixelate the image :
function image_pixel(bool = 1) {
if (bool) {
if (pixel_state > 24) {
restartGame("", false);
} else {
loadImage('image.jpg').then((image) => {
pixel_state += 0.1;
var canvas = createCanvas(image.width, image.height);
var ctx = canvas.getContext('2d');
pixelate(image, ctx, canvas, pixel_state);
io.emit('image', canvas.toDataURL());
})
}
} else { // Image without pixelisation
loadImage('image.jpg').then((image) => {
var canvas = createCanvas(image.width, image.height);
var ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
io.emit('image', canvas.toDataURL());
})
}
};
I tried to round the "w" and "h", the image will fill entirely in frame but some of the images sent will be the same so it'll feel laggy for the user.
Finally found something, I resized all of my pictures to square aspect ratio and then for the "pixel_state" if it's like 100/(2^x) i won't have any ghost pixels almost anymore.
Related
Problem
I want to draw arc on HTML Canvas, by which an image is masked.
I could draw the arc, which has an animation of expansion.
But, I couldn't make it shrink or smaller...
What's the problem?
Google Chrome dev tool doesn't expose any errors.
Thansk in advance.
Codes
<canvas id="canvas" width="350" height="184"></canvas>
<script>
var w = 350;
var h = 184;
var c = 0;
window.onload = function () {
draw();
}
function draw() {
var canvas = document.getElementById('canvas');
if(!canvas || !canvas.getContext) return false;
var ctx = canvas.getContext('2d');
var img = new Image();
img.src = "MY_IMAGE.jpg";
img.onload = () => {
setInterval(() => {
ctx.restore();
ctx.save();
ctx.beginPath();
ctx.arc(0, 0, w - c, Math.PI * 2, 0, false); // Doens't work!
// ctx.arc(0, 0, c, Math.PI * 2, 0, false); // Works! But I want to make the arc smaller, not bigger...
ctx.clip();
ctx.drawImage(img, 0, 0, 350, 184);
c += 1;
if(c >= w) {
c = 0;
}
}, 20);
}
}
The code is working fine. It just appears to not be working because you are not clearing your canvas between re-draw frames. Once something is drawn on canvas it stays there. So the image is shrinking but you aren’t seeing it because the bigger image is already drawn and stays on the screen.
Add ctx.clearRect(0,0,canvas.width,canvas.height)
Before you draw the image. You also don’t need the save and restore methods.
I am in the process of learning javascript for game design, and wanted to make a separate achievement page that the user can navigate to that will allow them to check their achievements on various games. (at the moment I am not concerned with implementing localstorage/cookies etc, I just want to work on the page for now)
So the requirements of my base idea is as follows:
Able to drag around the viewport/page to view all the achievement categories as they will likely not all be in view on smaller screens
Able to click on a category to open a small box containing all achievements belonging to that game/category
Able to mouse over all achievements in the boxes to get text descriptions of what they are
OPTIONAL: have lines connecting each box on the "overworld" to show users where nearby boxes are if they are off screen
At first, I thought I would need canvas to be able to do this. I learned a bit about it and got decently far until I realized that canvas has a lot of restrictions like not being able to do mouseover events unless manually implementing each one. Here is the current progress I was at in doing a test-run of learning canvas, but it's not very far:
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
function resize()
{
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.translate(panning.offset.x, panning.offset.y);
draw();
}
window.addEventListener("resize", resize);
var global = {
scale : 1,
offset : {
x : 0,
y : 0,
},
};
var panning = {
start : {
x : null,
y : null,
},
offset : {
x : 0,
y : 0,
},
};
var canvasCenterWidth = (canvas.width / 2);
var canvasCenterHeight = (canvas.height / 2);
function draw() {
ctx.beginPath();
ctx.rect(canvasCenterWidth, canvasCenterHeight, 100, 100);
ctx.fillStyle = 'blue';
ctx.fill();
ctx.beginPath();
ctx.arc(350, 250, 50, 0, 2 * Math.PI, false);
ctx.fillStyle = 'red';
ctx.fill();
}
draw();
canvas.addEventListener("mousedown", startPan);
function pan() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.translate(panning.offset.x, panning.offset.y);
draw();
}
function startPan(e) {
window.addEventListener("mousemove", trackMouse);
window.addEventListener("mousemove", pan);
window.addEventListener("mouseup", endPan);
panning.start.x = e.clientX;
panning.start.y = e.clientY;
}
function endPan(e) {
window.removeEventListener("mousemove", trackMouse);
window.removeEventListener("mousemove", pan);
window.removeEventListener("mouseup", endPan);
panning.start.x = null;
panning.start.y = null;
global.offset.x = panning.offset.x;
global.offset.y = panning.offset.y;
}
function trackMouse(e) {
var offsetX = e.clientX - panning.start.x;
var offsetY = e.clientY - panning.start.y;
panning.offset.x = global.offset.x + offsetX;
panning.offset.y = global.offset.y + offsetY;
}
body{
overflow: hidden;
}
canvas {
top: 0;
left: 0;
position: absolute; }
<canvas id="canvas"></canvas>
So I guess my question is now: what is the best way to implement this? Is it feasable to do it with canvas, or should I just scrap that and try to figure out something with div movement? Should I be concerned with performance issues and should that affect how I implement it?
I want to accomplish a pixelated effect using the canvas option imageSmoothingEnabled=false; so the image "unblurs" on scroll.
Everything works fine until using transparent images namely PNGs. The scaled image is projected, which stays in the background.
Also the image does not get loaded until the user has scrolled a few pixels.
I've found out that the canvas.drawImage() function owns parameters to set the offset. However I haven't found a solution to this.
Demo https://jsfiddle.net/aLjfemru/
var ctx = canvas.getContext('2d'),
img = new Image(),
play = false;
/// turn off image smoothing - this will give the pixelated effect
ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;
/// wait until image is actually available
img.onload = function(){
image1.src="nf.png";
context.drawImage(image1, 50, 50, 10, 10);
};
img.src = 'https://upload.wikimedia.org/wikipedia/commons/b/bb/Gorgosaurus_BW_transparent.png';
/// MAIN function
function pixelate(v) {
document.getElementById("v").innerHTML = "(v): " + v;
/// if in play mode use that value, else use slider value
var size = v * 0.01;
var w = canvas.width * size;
var h = canvas.height * size;
/// draw original image to the scaled size
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, w, h);
ctx.drawImage(canvas, 0, 0, w, h, 0, 0, canvas.width, canvas.height);
}
function onScroll() {
$(window).on('scroll', function() {
var y = window.pageYOffset;
if (y > 10) {
y = Math.pow(y, 0.8);
if (y >= 60) {
y = 100;
}
pixelate(y);
}
});
}
onScroll();
Some quick changes to get it happening
Use a second canvas to do the pixelation
Wait for the images to load before doing the rendering.
The onscroll will not fire until you scroll, so when image has loaded call the rendering function to display the image.
canvas.width = innerWidth-20;
ctx = canvas.getContext("2d");
var ctxImage;
const img = new Image;
img.src = 'https://upload.wikimedia.org/wikipedia/commons/b/bb/Gorgosaurus_BW_transparent.png';
/// wait until image is actually available
img.onload = function(){
// I dont knwo what this is for so removed the following two lines
//image1.src="nf.png";
//context.drawImage(image1, 50, 50, 10, 10);
// Create a canvas to match the image
var c = document.createElement("canvas");
canvas.width = Math.min(canvas.width,(c.width = this.naturalWidth));
canvas.height = c.height = this.naturalHeight;
ctxImage = c.getContext("2d");
// changing canvas size resets the state so need to set this again.
ctx.imageSmoothingEnabled = false;
onScroll();
pixelate(100); // call first time
};
ctx.font = "32px arial";
ctx.textAlign = "center";
ctx.fillText("Loading please wait.",ctx.canvas.width /2, ctx.canvas.height / 4);
/// MAIN function
function pixelate(v) {
document.getElementById("v").innerHTML = "(v): " + v;
/// if in play mode use that value, else use slider value
var size = Number(v) * 0.01;
var w = img.width * size;
var h = img.height * size;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctxImage.clearRect(0, 0, ctxImage.canvas.width, ctxImage.canvas.height);
ctxImage.drawImage(img, 0, 0, w, h);
ctx.drawImage(ctxImage.canvas, 0, 0, w, h, 0, 0, canvas.width, canvas.height);
}
function onScroll() {
addEventListener("scroll", function() {
var y = window.pageYOffset;
if (y > 10) {
y = Math.pow(y, 0.65);
if (y >= 100) {
y = 100;
}
pixelate(y);
}
});
}
#fix {
position: fixed;
}
html {
height: 2000px;
}
<div id="fix">
<p id="v" value="Animate">1</p><br />
<canvas id="canvas"></canvas>
</div>
This has since been made into an extremely minimalist library, and my PR for PNG support can be found here.
Once it has been merged I will come back and update this answer.
The full code, generalized and simplified from #Blindman67's answer:
/**
* 8bit
*
* A module that converts an image into a pixelated version (just like
* 8bit artwork).
*
* #author rogeriopvl <https://github.com/rogeriopvl>
* #license MIT
*/
(function (root, factory) {
if (typeof define === "function" && define.amd) {
define([], factory);
} else if (typeof exports === "object") {
module.exports = factory();
} else {
root.eightBit = factory();
}
} (this, function () {
// Necessary to hide the original image with PNG transparency
const invisibleCanvas = document.createElement("canvas");
const invisibleCtx = invisibleCanvas.getContext("2d");
/**
* Draws a pixelated version of an image in a given canvas.
* #param {object} canvas - a canvas object
* #param {object} image - an image HTMLElement object
* #param {number} quality - the new quality: between 0 and 100
*/
const eightBit = function (canvas, image, quality) {
quality /= 100;
canvas.width = invisibleCanvas.width = image.width;
canvas.height = invisibleCanvas.height = image.height;
const scaledW = canvas.width * quality;
const scaledH = canvas.height * quality;
const ctx = canvas.getContext("2d");
ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;
// Draws image scaled to desired quality on the invisible canvas, then
// draws that scaled image on the visible canvas.
ctx.clearRect(0, 0, canvas.width, canvas.height);
invisibleCtx.clearRect(0, 0, invisibleCtx.canvas.width, invisibleCtx.canvas.height);
invisibleCtx.drawImage(image, 0, 0, scaledW, scaledH);
ctx.drawImage(invisibleCtx.canvas, 0, 0, scaledW, scaledH, 0, 0, canvas.width, canvas.height);
};
return eightBit;
}));
I am trying to make a variant of the boomshine game in javascript and everything works when I draw a circular shape with the arc function. However when i try to replace the arc function with the drawImage function to use a coin image instead of a circular shape I start having problems when I clear the canvas to delete the previous drawn circular shapes. If I don't clear the canvas before rendering the images, the images are drawn on the canvas except the old images are still on the canvas. But when i do clear the canvas before rendering the images again, nothing is drawn on the canvas.
I have included screenshots, the links are below.
This is how I clear the canvas:
var ctx = game.context;
ctx.fillStyle = "darkgray";
ctx.fillRect(0, 0, game.canvas.width, game.canvas.height);
This is how i draw the image:
function drawImageBall(x,y,radius,startAngle,color)
{
var img = document.createElement('img');
img.src = 'img/coin-icon.png';
var tmpCtx= game.context;
var ax = x-radius;
var ay = y-radius;
img.onload = function() {
tmpCtx.save();
tmpCtx.beginPath();
tmpCtx.arc(x, y, radius, 0, Math.PI * 2, true);
tmpCtx.closePath();
tmpCtx.clip();
tmpCtx.drawImage(img, ax, ay, img.width, img.height);
tmpCtx.beginPath();
tmpCtx.arc(0, 0, radius, 0, Math.PI * 2, true);
tmpCtx.clip();
tmpCtx.closePath();
tmpCtx.restore();
};
}
Clearing canvas (screenshot)
Without clearing canvas (screenshot)
Keep in mind that downloading the img will take some time.
During that downloading time, Javascript does not stop(!). Instead JS will continue executing any following code. This is causing your unexpected problems.
So download the img just once at the start of your app. That way your drawImage will be done in the order that you expect because there will be no delay while your image is downloading.
Using your code, I maked some changes, I removed the tmpTcx.clip(), look the fidlle. Tip: For performace questions you don't need load the image every time that you want write the canvas.
Poor Example: https://jsfiddle.net/wf4z0d2h/1/
function clearCanvas(){
var ctx = game.context;
ctx.fillStyle = "darkgray";
ctx.fillRect(0, 0, game.canvas.width, game.canvas.height);
}
function drawImageBall(x,y,radius,startAngle,color)
{
if(x == undefined){x = 100;}
if(y == undefined){y = 100;}
if(radius == undefined){radius = 40;}
//var img = document.createElement('img');
//img.src = 'img/coin-icon.png';
//img.src = "http://ps2.lansa.com/images/icons/normal/256/coin_256.png";
var tmpCtx= game.context;
var ax = x-radius;
var ay = y-radius;
//img.onload = function() {
tmpCtx.save();
tmpCtx.beginPath();
tmpCtx.arc(x, y, radius, 0, Math.PI * 2, true);
tmpCtx.stroke(); // Draw it
tmpCtx.closePath();
//tmpCtx.clip();
tmpCtx.drawImage(img, ax, ay, img.width, img.height);
//tmpCtx.beginPath();
//tmpCtx.arc(0, 0, radius, 0, Math.PI * 2, true);
////tmpCtx.clip();
//tmpCtx.stroke(); // Draw it
//tmpCtx.closePath();
//tmpCtx.restore();
//};
}
var img = document.createElement('img');
//img.src = 'img/coin-icon.png';
img.src = "http://ps2.lansa.com/images/icons/normal/256/coin_256.png";
//drawImageBall();
img.onload = function(){
x = 0;
y = 0;
setInterval(function(){
x = x+10;
y = y+10;
clearCanvas();
drawImageBall(x,y);
},300);
}
Say we have a canvas:
<canvas id="one" width="100" height="200"></canvas>
And on a button click the canvas gets rotated 90 degrees clockwise (around the center) and the dimensions of the canvas get also updated, so in a sense it looks like this afterwards:
<canvas id="one" width="200" height="100"></canvas>
Note that the id of the canvas is the same.
Imagine simply rotating an image clockwise without it being cropped or being padded.
Any suggestions before I do it the long way of creating a new canvas and rotating and copying pixel by pixel?
UPDATE sample code with suggestion from comments still not working:
function imageRotatecw90(){
var canvas = document.getElementById("one");
var context = canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var myImageData = context.getImageData(0,0, cw,ch);
context.save();
context.translate(cw / 2, ch / 2);
context.rotate(Math.PI/2);
context.putImageData(myImageData, 0, 0);
context.restore();
canvas.width=ch;
canvas.height=cw;
}
FiddleJS
Look at this DEMO.
To achieve the results seen in demo, I made use of canvas.toDataURL to cache the canvas into an image, then reset the canvas to their new dimensions, translate and rotate the context properly and finally draw the cached image back to modified canvas.
That way you easily rotate the canvas without need to redraw everything again. But because anti-aliasing methods used by browser, each time this operation is done you'll notice some blurriness in result. If you don't like this behavior the only solution I could figure out is to draw everything again, what is much more difficult to track.
Here follows the code:
var canvas = document.getElementById("one");
var context = canvas.getContext("2d");
var cw = canvas.width;
var ch = canvas.height;
// Sample graphic
context.beginPath();
context.rect(10, 10, 20, 50);
context.fillStyle = 'yellow';
context.fill();
context.lineWidth = 7;
context.strokeStyle = 'black';
context.stroke();
// create button
var button = document.getElementById("rotate");
button.onclick = function () {
// rotate the canvas 90 degrees each time the button is pressed
rotate();
}
var myImageData, rotating = false;
var rotate = function () {
if (!rotating) {
rotating = true;
// store current data to an image
myImageData = new Image();
myImageData.src = canvas.toDataURL();
myImageData.onload = function () {
// reset the canvas with new dimensions
canvas.width = ch;
canvas.height = cw;
cw = canvas.width;
ch = canvas.height;
context.save();
// translate and rotate
context.translate(cw, ch / cw);
context.rotate(Math.PI / 2);
// draw the previows image, now rotated
context.drawImage(myImageData, 0, 0);
context.restore();
// clear the temporary image
myImageData = null;
rotating = false;
}
}
}
Rotation
Note it is not possible to rotate a single element.
ctx.save();
ctx.rotate(0.17);
// Clear the current drawings.
ctx.fillRect()
// draw your object
ctx.restore();
Width/height adjustment
The only way I ever found to properly deal with display ratios, screen sizes etc:
canvas.width = 20;// DO NOT USE PIXELS
canvas.height = 40; // AGAIN NO PIXELS
Notice I am intentionally not using canvas.style.width or canvas.style.height. Also for an adjustable canvas don't rely on CSS or media queries to do the transformations, they are a headache because of the pixel ratio differences. JavaScript automatically accounts for those.
Update
You also have to update the width and the height before you draw. Not sure what you are trying to achieve, but I guess this isn't a problem:
Demo here
var canvas = document.getElementById("one");
var context = canvas.getContext("2d");
var cw = canvas.width;
var ch = canvas.height;
canvas.width = 200;
canvas.height = 400;
// Sample graphic
context.beginPath();
context.rect(10,10,20,50);
context.fillStyle = 'yellow';
context.fill();
context.lineWidth = 7;
context.strokeStyle = 'black';
context.stroke();
var myImageData = context.getImageData(0, 0, cw, ch);
context.save();
context.translate(cw / 2, ch / 2);
context.putImageData(myImageData, 0, 0);
context.rotate(0.20);
If you want to rotate an image by 90 degrees this might be helpful:
export const rotateBase64Image = async (base64data: string) => {
const image = new Image();
image.src = base64data;
return new Promise<string>((resolve, reject) => {
image.onload = function () {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("cannnot get context '2d'");
canvas.width = image.height;
canvas.height = image.width;
ctx.setTransform(0, 1, -1, 0, canvas.width, 0); // overwrite existing transform
ctx!.drawImage(image, 0, 0);
canvas.toBlob((blob) => {
if (!blob) {
return reject("Canvas is empty");
}
const fileUrl = window.URL.createObjectURL(blob);
resolve(fileUrl);
}, "image/jpeg");
};
});
};
If you don't have image in base64 format you can do it like this:
const handleRotate = async () => {
const res = await fetch(link);
const blob = await res.blob();
const b64: string = await blobToB64(blob);
const rotatedImage = await rotateBase64Image(b64)
setLink(rotatedImage);
}
Here is my blobTob64 function:
export const blobToB64 = async (blob) => {
return new Promise((resolve, _) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
};