I am trying to generate an image using p5.js and p5.js-svg and then save the SVG xml as a dataURL for storage. Here is an example script that I've simplified for this question:
const canvasWidth = 600;
const canvasHeight = 600;
const bgColor = "#fffcf3";
let unencodedDataURL = "";
const sketch = (p) => {
p.setup = () => {
p.createCanvas(canvasWidth, canvasHeight, p.SVG);
p.background(bgColor);
p.noLoop();
}
p.draw = () => {
p.noFill();
p.ellipse(z
canvasWidth / 2,
canvasHeight / 2,
150,
150
);
unencodedDataURL = p.getDataURL();
console.log(`inside: ${unencodedDataURL}`);
}
}
const test = new p5(sketch, document.body);
console.log(`outside: ${unencodedDataURL}`);
The first log statement prints the right dataURL. But of course the second log statement executes before the one inside draw() and I cannot figure out how to capture the dataURL correctly. I'm sure I am missing something in the p5.js or p5.js-svg libraries and there is an easier way. But I am stuck. Anyone have an idea here? Thanks in advance!
Because p5js run asynchronous,
You can add callback or write a Promise to get data after sketch draw, like this
var handleDataCallback = null;
//........
p.ellipse(
canvasWidth / 2,
canvasHeight / 2,
150,
150
);
unencodedDataURL = p.getDataURL();
if (handleDataCallback) handleDataCallback(unencodedDataURL);
//....
//for callback
handleDataCallback = dataURL => {
//todo something with dataURL
}
if you want to write as a function to generate image
function generateImage(param, callback) {
return new Promise(resolve => {
//......
// do something
p.ellipse(
canvasWidth / 2,
canvasHeight / 2,
150,
150
);
var unencodedDataURL = p.getDataURL();
if (callback) callback(unencodedDataURL);
resolve(unencodedDataURL);
// ....
})
}
//use Promise
generateImage(yourParam, null).then(unencodedDataURL=>{
console.log(`outside: ${unencodedDataURL}`);
})
//or callback
generateImage(yourParam, unencodedDataURL=> {
console.log(`outside: ${unencodedDataURL}`);
});
Related
I'm trying to download PDF with SVG content using jsPDF library, it is able to download the file, but there is no content inside it, it is empty PDF.
This is my code:
const downloadPDF = (goJSDiagram) => {
const svg = goJSDiagram.makeSvg({scale: 1, background: "white"});
const svgStr = new XMLSerializer().serializeToString(svg);
const pdfDoc = new jsPDF();
pdfDoc.addSvgAsImage(svgStr, 0, 0, pdfDoc.internal.pageSize.width, pdfDoc.internal.pageSize.height)
pdfDoc.save(props.model[0].cName?.split(" (")[0] + ".pdf");
}
When I do console.log(svgStr), I can see the SVG XML string. What changes should I make to render the content inside PDF?
I think I know what is going on after getting a good hint from this Github issue:
There's the issue that addSvgAsImage() is asynchronous
You are not awaiting the call to finish before calling save! That means you are trying to save before the SVG has started rendering to the PDF.
See the quite simple code in question:
jsPDFAPI.addSvgAsImage = function(
// ... bla bla
return loadCanvg()
.then(
function(canvg) {
return canvg.fromString(ctx, svg, options);
},
function() {
return Promise.reject(new Error("Could not load canvg."));
}
)
.then(function(instance) {
return instance.render(options);
})
.then(function() {
doc.addImage(
canvas.toDataURL("image/jpeg", 1.0),
x,
y,
w,
h,
compression,
rotation
);
});
As you see, it is just a chain of Thenables. So you simply need to await the Promise, which means your code would look something like this in ES2015+:
const downloadPDF = async (goJSDiagram) => {
const svg = goJSDiagram.makeSvg({scale: 1, background: "white"});
const svgStr = new XMLSerializer().serializeToString(svg);
const pdfDoc = new jsPDF();
await pdfDoc.addSvgAsImage(svgStr, 0, 0, pdfDoc.internal.pageSize.width, pdfDoc.internal.pageSize.height)
pdfDoc.save(props.model[0].cName?.split(" (")[0] + ".pdf");
}
After lot of searching, I found the right way to do this, though the content rendered is little blurred.
const waitForImage = imgElem => new Promise(resolve => imgElem.complete ? resolve() : imgElem.onload = imgElem.onerror = resolve);
const downloadPDF = async (goJSDiagram) => {
const svg = goJSDiagram.makeSvg({scale: 1, background: "white"});
const svgStr = new XMLSerializer().serializeToString(svg);
const img = document.createElement('img');
img.src = 'data:image/svg+xml;base64,' + window.btoa(svgStr);
waitForImage(img)
.then(_ => {
const canvas = document.createElement('canvas');
canvas.width = 500;
canvas.height = 500;
canvas.getContext('2d').drawImage(img, 0, 0, 500, 500);
const pdfDoc = new jsPDF('p', 'pt', 'a4');
pdfDoc.addImage(canvas.toDataURL('image/png', 1.0), 0, 200, 500, 500);
pdfDoc.save(props.model[0].cName?.split(" (")[0] + ".pdf");
});
}
sorry for the amatuer question, I'm really new to Pixi.js
Well, I'm trying to make a scratch card.
There will be two image, one for background("A"), and one for hidden background( "B" , I think this is mask, but I'm not sure about this terminology , so to avoid confusion I'll describe as hidden background)
So, image 'A' will cover up image 'B'.
Then, I'will use bristle image for the brush.
When I execute dragging event on Image "A" , I want to show up image 'B" along path mouse moved.
Sorry , for my terrible description. Think I'm cloning this website ( https://art4globalgoals.com/en )
I followed this example( https://pixijs.io/examples/#/demos-advanced/scratchcard.js ), and succeed to make a brush effect, with only 1 image. But couldn't implement it with two image.
Precisely speaking , when I run my app, there is black screen rendered , and if I scratch screen, the hidden Image appears.
but the problem is that, I want to set other Image instead of black screen.
below image is my current status , which will happen when running below code
import * as PIXI from 'pixi.js';
// const canvas = document.getElementById('webgl');
const screenSize = {
width: window.innerWidth,
height: window.innerHeight
};
let brushWidth = (window.innerHeight / window.innerWidth) * 200;
let brushHeight = (window.innerHeight / window.innerWidth) * 200;
// canvas.width = screenSize.width;
// canvas.height = screenSize.height;
const app = new PIXI.Application({
width: window.innerWidth,
height: window.innerHeight,
resolution: window.devicePixelRatio,
autoDensity: true
});
document.body.appendChild(app.view);
app.loader
.add('background', '/png/effel.png')
.add('mask', '/png/effel-gray.png')
.add('bristle1', '/png/brush6.png')
.add('bristle2', '/png/bristle2.png')
.load(() => {
setup();
});
const setup = () => {
const brushTexture = app.loader.resources.bristle1.texture;
const brushTexture2 = app.loader.resources.bristle2.texture;
const brush = new PIXI.Sprite(brushTexture);
const brush2 = new PIXI.Sprite(brushTexture2);
brush.width = brushWidth;
brush.height = brushHeight;
brush2.width = brushWidth;
brush2.height = brushHeight;
const backgroundTexture = app.loader.resources.background.texture;
const maskTexture = app.loader.resources.mask.texture;
const background = new PIXI.Sprite(backgroundTexture);
background.x = app.renderer.screen.width / 2;
background.y = app.renderer.screen.height / 2;
background.anchor.x = 0.5;
background.anchor.y = 0.5;
background.width = window.innerWidth;
background.height = window.innerHeight;
const Mask = new PIXI.Sprite(maskTexture);
Mask.width = app.renderer.screen.width;
Mask.height = app.renderer.screen.height;
Mask.x = app.renderer.screen.width / 2;
Mask.y = app.renderer.screen.height / 2;
Mask.anchor.x = 0.5;
Mask.anchor.y = 0.5;
Mask.width = window.innerWidth;
Mask.height = window.innerHeight;
// app.stage.addChild(background);
app.stage.addChild(Mask);
const renderTexture = PIXI.RenderTexture.create(app.screen.width, app.screen.height);
const renderTextureSprite = new PIXI.Sprite(renderTexture);
app.stage.addChild(renderTextureSprite);
Mask.mask = renderTextureSprite;
app.stage.interactive = true;
app.stage.on('pointerdown', pointerDown);
app.stage.on('pointerup', pointerUp);
app.stage.on('pointermove', pointerMove);
let dragging = false;
let bristle2Render = false;
function pointerMove(event) {
if (dragging) {
brush.position.copyFrom(event.data.global);
// brushWidth += 0.5;
// brush.width = brushWidth;
if (!bristle2Render) {
setTimeout(() => (bristle2Render = true), 500);
}
app.renderer.render(brush, renderTexture, false, null, false);
if (bristle2Render) {
brush2.position.copyFrom(event.data.global);
app.renderer.render(brush2, renderTexture, false, null, false);
}
// if (brush.width === 100) {
// dragging = false;
// brushWidth = 0;
// }
}
}
function pointerDown(event) {
dragging = true;
console.log(11);
pointerMove(event);
}
function pointerUp(event) {
dragging = false;
bristle2Render = false;
}
window.addEventListener('resize', () => {
screenSize.width = window.innerWidth;
screenSize.height = window.innerHeight;
app.renderer.resize(window.innerWidth, window.innerHeight);
app.renderer.stage.width = window.innerWidth;
app.renderer.stage.height = window.innerHeight;
});
};
// const backgroundTexture = PIXI.Texture.from('/png/effel.png');
// const maskTexture = PIXI.Texture.from('/png/effel-gray.png');
I guess the problem is that you haven't added background to the stage, because you commented it.
// app.stage.addChild(background);
app.stage.addChild(Mask);
So, there is nothing to be shown at the beginning, so it is a black screen.
Here is a CodePen that I created. I removed some comments and replaced the resources with some other images. It should be somehow working after you uncommented the line.
By the way, you may want to ensure that:
your variables have a capital name only for some special reasons (e.g. Mask)
you don't overwrite values that you have set before (e.g. Mask.width = ...).
Although the code still works without fixing the above, this may make the code harder to work with for others, including a future "you". ;)
When your code works, you may want to post it on Code Review to see how to improve the code.
I'm trying to make 'slitscan' effects.
I set 2 canvases. One on the right is for source image, and another is for scan effect.
When i tried it with text function it works perfectly, however with an image, the copy function gives me an error message.
This is the p5 code I wrote
let a1, a2;
let cv1, cv2;
let dy = 0;
let s1 = function(p) {
p.setup = () => {
cv1 = p.createCanvas(300, 300);
p.background(150);
cv1.position(0, 0);
}
p.draw = () => {
p.copy(cv2, 0, dy, 400, 1, 0, dy, 400, 1);
if (dy > cv2.height) {
dy = 0;
} else {
dy += 1;
}
}
}
a1 = new p5(s1);
let s2 = function(p) {
let img;
p.preload = () => {
img = p.loadImage('https://pbs.twimg.com/profile_images/635910989498724356/uY4bc8q2.jpg');
}
p.setup = () => {
cv2 = p.createCanvas(300, 300);
cv2.position(300, 0);
}
p.draw = () => {
p.background(30);
p.imageMode(p.CENTER);
p.image(img, p.mouseX, p.mouseY);
}
}
a2 = new p5(s2);
In addition, if you have a better idea to make multi-canvas or any suggestion about my code, please leave a comment.
Thanks,
In your s1 sketch, you try to do stuff with the canvas cv2. Yet this canvas is only created later in your sketch.
To fix it, just change the order of your two sketches, i.e., call a1 = new p5(s1); after a2 = new p5(s2); (it doesn't matter when you define s1 and s2, only when you instantiate them).
See, for example, this p5 editor sketch.
I'm trying to learn JavaScript, making my first game. How I can make all images onload in one function and later draw it in the canvas making my code shorter?
How can I put a lot of images in an array and later us it in a function.
This is my third day of learning JavaScript.
Thanks in advance.
var cvs = document.getElementById('canvas');
var ctx = cvs.getContext('2d');
//load images
var bird = new Image();
var bg = new Image();
var fg = new Image();
var pipeNorth = new Image();
var pipeSouth = new Image();
//images directions
bg.src = "assets/bg.png";
bird.src = "assets/bird.png";
fg.src = "assets/fg.png";
pipeNorth.src = "assets/pipeNorth.png";
pipeSouth.src = "assets/pipeSouth.png";
var heightnum = 80;
var myHeight = pipeSouth.height+heightnum;
var bX = 10;
var bY = 150;
var gravity = 0.5;
// Key Control :D
document.addEventListener("keydown",moveUP)
function moveUP(){
bY -= 20;
}
//pipe coordinates
var pipe = [];
pipe[0] = {
x : cvs.width,
y : 0
}
//draw images
//Background img
bg.onload = function back(){
ctx.drawImage(bg,0,0);
}
//pipe north
pipeNorth.onload = function tubo(){
for(var i = 0; i < pipe.length; i++){
ctx.drawImage(pipeNorth,pipe[i].x,pipe[i].y);
pipe[i].x--;
}
}
pipeSouth.onload = function tuba(){
ctx.drawImage(pipeSouth,pipe[i].x,pipe[i].y+myHeight);
}
bird.onload = function pajaro(){
ctx.drawImage(bird,bX,bY);
bY += gravity;
requestAnimationFrame(pajaro);
}
fg.onload = function flor(){
ctx.drawImage(fg,0,cvs.height - fg.height);
}
moveUP();
back();
tuba();
pajaro();
flor();
This can be done with Promise.all. We'll make a new promise for each image we want to load, resolving when onload is called. Once Promise.all resolves, we can call our initialize function and continue on with the rest of our logic. This avoids race conditions where the main game loop's requestAnimationFrame is called from bird.onload, but it's possible that pipe entities and so forth haven't loaded yet.
Here's a minimal, complete example:
const initialize = images => {
// images are loaded here and we can go about our business
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
canvas.width = 400;
canvas.height = 200;
const ctx = canvas.getContext("2d");
Object.values(images).forEach((e, i) =>
ctx.drawImage(e, i * 100, 0)
);
};
const imageUrls = [
"http://placekitten.com/90/100",
"http://placekitten.com/90/130",
"http://placekitten.com/90/160",
"http://placekitten.com/90/190",
];
Promise.all(imageUrls.map(e =>
new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = e;
})
)).then(initialize);
Notice that I used an array in the above example to store the images. The problem this solves is that the
var foo = ...
var bar = ...
var baz = ...
var qux = ...
foo.src = ...
bar.src = ...
baz.src = ...
qux.src = ...
foo.onload = ...
bar.onload = ...
baz.onload = ...
qux.onload = ...
pattern is extremely difficult to manage and scale. If you decide to add another thing into the game, then the code needs to be re-written to account for it and game logic becomes very wet. Bugs become difficult to spot and eliminate. Also, if we want a specific image, we'd prefer to access it like images.bird rather than images[1], preserving the semantics of the individual variables, but giving us the power to loop through the object and call each entity's render function, for example.
All of this motivates an object to aggregate game entities. Some information we'd like to have per entity might include, for example, the entity's current position, dead/alive status, functions for moving and rendering it, etc.
It's also a nice idea to have some kind of separate raw data object that contains all of the initial game state (this would typically be an external JSON file).
Clearly, this can turn into a significant refactor, but it's a necessary step when the game grows beyond small (and we can incrementally adopt these design ideas). It's generally a good idea to bite the bullet up front.
Here's a proof-of-concept illustrating some of the the musings above. Hopefully this offers some ideas for how you might manage game state and logic.
const entityData = [
{
name: "foo",
path: "http://placekitten.com/80/80",
x: 0,
y: 0
},
{
name: "baz",
path: "http://placekitten.com/80/150",
x: 0,
y: 90
},
{
name: "quux",
path: "http://placekitten.com/100/130",
x: 90,
y: 110
},
{
name: "corge",
path: "http://placekitten.com/200/240",
x: 200,
y: 0
},
{
name: "bar",
path: "http://placekitten.com/100/100",
x: 90,
y: 0
}
/* you can add more properties and functions
(movement, etc) to each entity
... try adding more entities ...
*/
];
const entities = entityData.reduce((a, e) => {
a[e.name] = {...e, image: new Image(), path: e.path};
return a;
}, {});
const initialize = () => {
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
canvas.width = innerWidth;
canvas.height = innerHeight;
const ctx = canvas.getContext("2d");
for (const key of Object.keys(entities)) {
entities[key].alpha = Math.random();
}
(function render () {
ctx.clearRect(0, 0, canvas.width, canvas.height);
Object.values(entities).forEach(e => {
ctx.globalAlpha = Math.abs(Math.sin(e.alpha += 0.005));
ctx.drawImage(e.image, e.x, e.y);
ctx.globalAlpha = 1;
});
requestAnimationFrame(render);
})();
};
Promise.all(Object.values(entities).map(e =>
new Promise((resolve, reject) => {
e.image.onload = () => resolve(e.image);
e.image.onerror = () =>
reject(`${e.path} failed to load`)
;
e.image.src = e.path;
})
))
.then(initialize)
.catch(err => console.error(err))
;
I want to create a function that return an observable of file
My current working code uses a promise
testPromise(): Promise<File> {
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 600;
const context = canvas.getContext('2d');
context.drawImage(image, 0, 0, 800, 600);
canvas.toBlob((blob: Blob) => resolve(new File([blob], name)), type, quality);
});
}
Instead of that, I want to return Observable<File>
I tried the following but I cannot pass the toBlob() arguments such as type and quality
testObservable(): Observable<File> {
const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 600;
const context = canvas.getContext('2d');
context.drawImage(image, 0, 0, 800, 600);
const toBlob = bindCallback(canvas.toBlob);
return toBlob().pipe(
map((blob: Blob) => {
return new File([blob], name)
})
);
}
Expected behavior
const toBlob = bindCallback(canvas.toBlob);
toBlob(type, quality) // <= Expected 0 arguments, but got 2.
Here I get an error on toBlob Expected 0 arguments, but got 2.
Current behavior
const toBlob = bindCallback(canvas.toBlob);
toBlob() // <= No type or quality arguments
Here is the canvas.toBlob interface according to MDN docs
void canvas.toBlob(callback, mimeType, qualityArgument);
PS: I don't want to convert the Promise to an Observable,but I need to convert the callback directly to an Observable
Any ideas?
I'm afraid bindCallback is not a good choice here because it expects the callback to be the last argument and not the first as what toBlob expects.
I think you'll have to wrap this call with na Observable yourself:
return new Observable(observer => {
canvas.toBlob(result => {
observer.next(result);
observer.complete();
}, type, quality);
});