I have the following code;
export default async function draw(elRef : RefObject<HTMLCanvasElement>, tileData : TileProps) {
const canvas = elRef.current!;
const ctx = canvas.getContext('2d')!;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
tileData.forEach(async tile => {
const image = await loadImage(tile.tileType);
ctx.drawImage(image, 0, 0, TILE_WIDTH, TILE_HEIGHT, tile.coordinates.x, tile.coordinates.y, TILE_WIDTH, TILE_HEIGHT);
})
}
Every time draw is called with an array of image src's to render onto the canvas, loadImage is called;
const loadImage = (tileType : string) : Promise<HTMLImageElement> => {
return new Promise((resolve, reject) => {
const image = new Image();
image.addEventListener('load', () => resolve(image));
image.addEventListener('error', reject);
image.src = "./media/images/tiles/Dog.png";
})
}
However, since draw will be constantly called, it's going to call loadImage repeatedly.
How can I save an image so I won't need to consistantly load it everytime I want to place it onto a canvas?
You can make your loadImage memoized, so you do not request new image if it is exactly same image, you can do this by making an in memory cache of the parameters you pass to the loadImage and return from it if they are same parameters.
Is your tileData ever changing? If not you do not even need to call the loadImage inside the draw.
Related
So as the title says, i'm trying to load a base64 encoded png image into a p5js image, here's a simplification of how im doing it:
PS: I'm using a base64 image because it's generated from a server
var img;
function setup() {
// Canvas setup code ....
img = loadImage('loading.png'); // show an image which says that it's loading
img.loadPixels();
// More setup code ...
}
// ...
function draw() {
image(img, 0, 0, img.width, img.height);
// ... more code
}
// ...
function receivedCallback(data) { // This function gets called once we receive the data
// Data looks like this: { width: 100, height: 100, data: "...base64 data...." }
img.resize(data.width, data.height);
var imgData = data.data;
var imgBlob = new Blob([imgData], { type: "image/png" }); // Create a blob
var urlCreator = window.URL || window.webkitURL;
var imageUrl = urlCreator.createObjectURL(imgBlob); // Craft the url
img.src = imageUrl;
img.loadPixels();
img.updatePixels();
}
But, it's not working which is why I am asking here.
If there is any way to do it I would appreciate it very much.
Thanks in advance.
EDIT
Steve's didn't quite work, I had to replace img.src = 'data:image/png;base64,' + data.data with img = loadImage('data:image/png;base64,' + data.data);
You can use Data URLs directly. It can look something like this:
function receivedCallback(data) { // This function gets called once we receive the data
// Data looks like this: { width: 100, height: 100, data: "...base64 data...." }
loadImage('data:image/png;base64,' + data.data, function (newImage) {
img = newImage;
});
}
If you need to use blobs for some reason, you can look into base64 to blob conversions, like this answer.
Steve's didn't quite work, I had to load the image instead of directly changing it's source.
So instead of img.src = 'data:image/png;base64,' + data.data.
I needed to do img = loadImage('data:image/png;base64,' + data.data);
Thanks to Steve for his answer though!
There's no difference between loading a base64 image and a regular image from a file in P5.js. Simply replace the URL in your loadImage call with the base64 data.
In the interests of a runnable, complete example (note that the base64 image is very small):
const imgData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII";
let img;
function preload() {
img = loadImage(imgData);
}
function draw() {
image(img, 0, 0);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.min.js"></script>
If your data is supposed to load at some indeterminate point in the future, say, after an interaction, then you can use a callback to determine when the image has loaded. This is somewhat use-case specific, but the pattern might look like:
const imgData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII";
let img;
function setup() {
createCanvas(300, 300);
}
function draw() {
clear();
textSize(14);
text("click to load image", 10, 10);
if (img) {
image(img, 0, 0);
}
}
function mousePressed() {
if (!img) {
loadImage(imgData, imgResult => {
img = imgResult;
});
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.min.js"></script>
If you have some async code in there, no problem. Just call loadImage whenever you have the base64 string ready, for example:
function mousePressed() {
if (!img) {
fetch("your endpoint")
.then(response => response.json())
.then(data => {
// prepend "data:image/png;base64," if
// `data.data` doesn't already have it
loadImage(data.data, imgResult => {
img = imgResult;
});
});
}
}
All this said, loading an image in response to a click is a somewhat odd pattern for most apps. Usually, you'd want to preload all of your asssets in the preload function so that you can render them immediately when requested. If you load via a fetch call in response to an event, there'll likely be an ugly-looking delay (or needing to show a temporary message/placeholder image/spinner) before rendering the image, so make sure this isn't an xy problem.
I'm trying to use the merge-images script (https://github.com/lukechilds/merge-images) to merge some images into a single one using nodejs.
I'm having trouble understanding what ecatly I'm supposed to provide in the then() method of mergeImages.
This is what I have so far:
const mergeImages = require('merge-images');
const { createCanvas, Canvas, Image } = require('canvas');
const width = 2880;
const height = 2880;
var canvas = createCanvas(width, height);
const ctx = canvas.getContext('2d');
var img = new Image();
img.onload = () => ctx.drawImage(img, 0, 0);
img.onerror = err => { throw err }
img.src = './result.png';
mergeImages([
'./lunas_parts/1.png', './lunas_parts/2.png', './lunas_parts/3.png',
], {
Canvas: Canvas,
Image: Image
})
.then(b64 => img = b64);
I do have an empty result.png image in the right location, as well as the 1, 2 and 3 .png files. The console isn't showing any error when I execute the above script, but result.png remains empty after the execution.
Is the canvas image source I'm using in then() not correct? What am I supposed to pass there exactly?
Thanks in advance for any help.
I want to access and manipulate an image's binary data using javascript. As far as I could research, it can be done using Canvas API - creating a canvas and a context in memory, and then using their built-in methods. What I've come up looks like this:
function fileToBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(<string>reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(file);
});
}
function base64ToImageData(base64: string): Promise<ImageData> {
return new Promise((resolve, reject) => {
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
resolve(imageData);
}
img.onerror = () => reject();
img.src = base64;
});
}
And what I don't like about this approach is that it is verbose and probably uses more resources than it should.
So the question is: Is there a way to get a binary representation of an image without using canvas API?
you could construct a ImageData manually but for that you need a Uint8ClampedArray. For that you would also have to decode a binary file to pixel data yourself. So you probably want to use canvas or offscreanCanvas for that anyway
but there are some tools to ease the way to get imageData from blob using createImageBitmap
This imageBitmap contains the pixel data but you are not able to read it, but at least it will be faster to paint it to a canvas element.
(async () => {
// just to simulate a file you would get from file input
const res = await fetch('https://httpbin.org/image/png')
const blob = await res.blob()
const file = new File([blob], 'image.png', blob)
async function getImageData (blob) {
const bitmap = await createImageBitmap(blob)
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = bitmap.width
canvas.height = bitmap.height
ctx.drawImage(bitmap, 0, 0)
return ctx.getImageData(0, 0, canvas.width, canvas.height)
}
getImageData(blob).then(imageData => console.log(imageData.width))
})()
but you might not use createImageBitmap due to compatibility issues, so then i suggest that you instead of creating a image from file input -> base64 -> image with the FileReader that you instead go from file to image using a pointer references with object urls
const img = new Image()
img.src = URL.createObjectURL(file)
this will use less resources cuz you don't need to encode it to base64 and decode it back to binary again, it will use less memory since it will just load the image from your disk instead directly
Desired result: I want that the images (image and image2) should load one-by-one. Following is my code:
'use strict';
window.addEventListener('DOMContentLoaded', () => {
const cvs = document.querySelector('#cvs');
cvs.width = window.innerWidth;
cvs.height = window.innerHeight;
const c = cvs.getContext('2d');
let image = new Image(); // first image
image.src = 'https://images5.alphacoders.com/559/559956.jpg';
let image2 = new Image(); // second image
image2.src = 'https://www.highreshdwallpapers.com/wp-content/uploads/2013/03/Avengers-A.jpg';
let y = 0;
let loader = img => new Promise(resolve => img.onload = resolve(img, 0, y, 100, 60));
let logger = img => new Promise(resolve => img.onload = resolve(img.src + ' loaded!'));
async function loadImages() {
await logger(image).then(console.log); // this works
await logger(image2).then(console.log); // this works
await loader(image).then(c.drawImage);
y += 60;
await loader(image2).then(c.drawImage);
};
loadImages();
});
body {
margin: 0;
}
#cvs {
position: fixed;
}
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
<link rel="stylesheet" type="text/css" href="./style.css">
</head>
<body>
<canvas id='cvs'>Canvas not supported</canvas>
<script type="text/javascript" src='./script.js'></script>
</body>
</html>
Here's my concept:
I create a function that returns a new Promise which resolves when the image loads and then draws it on the canvas context:
let loader = img => new Promise(resolve => img.onload = resolve(img, 0, height, 100, 60));
Then, I create an asynchronous function that calls the loader for every image and draws it on canvas:
async function loadImages() {
// ...
await loader(image).then(c.drawImage);
y += 60;
await loader(image2).then(c.drawImage);
};
But, for some reasons, the code isn't working. And, I get the following error: Uncaught (in promise) TypeError: Illegal invocation.
I've tried replacing c.drawImage with:
c.drawImage.bind(c), as recommended in this post and,
c.drawImage.call, in this case, I've changed my resolve as resolve(c, img, 0, height, 100, 60)
But, neither of them worked out!
What I am doing wrong?
Your loadImages returns undefined (since you don't return anything explicitly), so drawImage won't do anything.
Even though it's not entirely clear what you wish to do, I have the feeling that you wanted to draw both images in this then().
In order to do as little modifications in your code as possible, you could just return both images in an Array, then in the then iterate over this array and call drawImage on it.
You'd also have to pass all your arguments as an array in the loader function.
Also, note that you had a typo in img.onload = resolve(.., it should be img.onload = e => resolve(..., otherwise resolve gets called directly.
And also remember that the onload event will fire only once, so once you awaited for it, it on't happen again (unless you force it to).
window.addEventListener('DOMContentLoaded', () => {
const cvs = document.querySelector('#cvs');
cvs.width = window.innerWidth;
cvs.height = window.innerHeight;
const c = cvs.getContext('2d');
let image = new Image(); // first image
image.src = 'https://images5.alphacoders.com/559/559956.jpg';
let image2 = new Image(); // second image
image2.src = 'https://www.highreshdwallpapers.com/wp-content/uploads/2013/03/Avengers-A.jpg';
let height = 0;
let loader = img => new Promise(resolve => {
// resolve the arguments as an Array
img.onload = e => resolve([img, 0, height, 100, 60]);
// force resetting the src, otherwise onload may already have fired
img.src = img.src;
});
async function loadImages() {
const a = await loader(image);
height += 60;
const b = await loader(image2);
// you must return something if you it to be passed in the then()
return [a, b];
};
loadImages().then(arr => {
arr.forEach(args => c.drawImage.apply(c, args));
}).catch(console.error);
});
<canvas id='cvs'>Canvas not supported</canvas>
But if I may, your code is far from being clear... You'd probably win by being more verbose, and split your logic differently:
First deal with loading the assets, then deal with your rendering objects (here the arguments you pass to drawImage).
Mixing both will just make your code harder to maintain.
Also, your variable named height would probably be better called y.
I am trying to take an image from the clipboard within electron, and put it on a canvas.
In the electron main process, I have this function which is triggered by a menu item. If I write img.toBitmap().toString() to the console it outputs the image data, so I know that there is an image there.
export function pasteImage() {
let img = clipboard.readImage()
console.log('send paste message')
mainWindow.webContents.send('paste-image', img.toBitmap())
}
Next I have this method which takes the buffer and converts it to a blob. It then should load it into the image and draw the image to the canvas.
public pasteImage(image: Buffer, x: number = 0, y: number = 0) {
let blob = new Blob([image], { type: 'image/bmp' })
let url = URL.createObjectURL(blob)
let img = new Image
img.addEventListener('load', () => {
console.log('loaded image')
this.ctx.drawImage(img, x, y)
URL.revokeObjectURL(url)
})
img.src = url
}
ipcRenderer.addListener('paste-image', (e: EventEmitter, img: Buffer) => {
let canvas = document.createElement('canvas')
let layer = new layer(canvas)
layer.pasteImage(img)
})
The issue I am having is that the load event never gets triggered, however the method pasteImage(image, x, y) is executing.