Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,64 @@ confetti({
});
```

### `confetti.shapeFromImage({ src, scalar?, x?, y?, width?, height? })` → `Promise<Shape>`

You can also create confetti from images! The same caveats apply as for `confetti.shapeFromText`.

The options for this method are:
- `options` _`Object`_:
- `src` _`String`_: the URL of the image to render. This must be "origin-clean", which means you may get problems if the image is hosted at a different origi). If the resource can be fetched cross-origin, you can do something like `URL.createObjectURL(await (await fetch(src)).blob())`, or if all else fails, data URIs will always work.
- `scalar` _`Number, optional, default: 1`_: a scale value relative to the default size of 10px. It should typically match the `scalar` value in the confetti options. If the source image is not a square, this will apply to the width rather than the height (e.g. rendering a 50x20 image at a scalar of `2` will render a 20x8 confetti).

The `x`, `y`, `width`, and `height` options are used to specify cordinates within the source image to render. These can be used to render a single section of a spritesheet.
- `x` _`Number, optional, default: 0`_: the x position within the image to start rendering from.
- `y` _`Number, optional, default: 0`_: the y position within the image to start rendering from.
- `width` _`Number, optional, default: img.naturalWidth`_: the width within the image to render.
- `height` _`Number, optional, default: img.naturalHeight`_: the height within the image to render.

```javascript
const scalar = 2;

const shape = await confetti.shapeFromImage({
src: 'data:image/gif;base64,R0lGODlhBQAFAIABAP8AAAAAACH5BAEKAAEALAAAAAAFAAUAAAIIjA+RwKxuUigAOw',
scalar,
});

confetti({
shapes: [shape],
scalar,
});
```

#### Using a spritesheet

```javascript
const spritesheetSvg = `<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<defs><polygon id="bow" points="0,0 0,25 30,0 30,25"/></defs>
<use href="#bow" x="0" y="0" fill="red"/>
<use href="#bow" x="35" y="0" fill="cyan"/>
<use href="#bow" x="0" y="30" fill="orange"/>
<use href="#bow" x="35" y="30" fill="fuchsia"/>
</svg>`;

const src = `data:image/svg+xml,${encodeURIComponent(spritesheetSvg)}`;

const scalar = 5;

const origins = [
{ x: 0, y: 0 },
{ x: 35, y: 0 },
{ x: 0, y: 30 },
{ x: 35, y: 30 },
];

const shapes = await Promise.all(origins.map(async ({ x, y }) =>
confetti.shapeFromImage({ src, scalar, x, y, width: 30, height: 25 })
));

confetti({ shapes, scalar });
```

### `confetti.create(canvas, [globalOptions])` → `function`

This method creates an instance of the `confetti` function that uses a custom canvas. This is useful if you want to limit the area on your page in which confetti appear. By default, this method will not modify the canvas in any way (other than drawing to it).
Expand Down
95 changes: 95 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,54 @@ <h2><a href="#emoji" id="emoji" class="anchor">Emoji</a></h2>
</div>
</div>

<div class="container">
<div class="group" data-name="image">
<div class="flex-rows">
<div class="left">
<h2><a href="#image" id="image" class="anchor">Image</a></h2>
<button class="run">
Run
<span class="icon">
<svg class="icon">
<use xlink:href="#run"></use>
</svg>
</span>
</button>
</div>
<div class="description">
<p>
Don't fancy using emoji? You can use any image you want!
</p>
</div>
</div>
<div class="editor"></div>
</div>
</div>

<div class="container">
<div class="group" data-name="spritesheet">
<div class="flex-rows">
<div class="left">
<h2><a href="#spritesheet" id="spritesheet" class="anchor">Spritesheet</a></h2>
<button class="run">
Run
<span class="icon">
<svg class="icon">
<use xlink:href="#run"></use>
</svg>
</span>
</button>
</div>
<div class="description">
<p>
You can even cut out multiple images from a single spritesheet!
</p>
</div>
</div>
<div class="editor"></div>
</div>
</div>

<div class="container">
<div class="group" data-name="custom">
<div class="flex-rows">
Expand Down Expand Up @@ -923,6 +971,53 @@ <h2><a href="#custom-canvas" id="custom-canvas" class="anchor">Custom Canvas</a>
setTimeout(shoot, 0);
setTimeout(shoot, 100);
setTimeout(shoot, 200);
},
image: async function image() {
async function run() {
const scalar = 2;

const shape = await confetti.shapeFromImage({
src: 'data:image/gif;base64,R0lGODlhBQAFAIABAP8AAAAAACH5BAEKAAEALAAAAAAFAAUAAAIIjA+RwKxuUigAOw',
scalar,
});

confetti({
shapes: [shape],
scalar,
});
}

run();
},
spritesheet: async function spritesheet() {
const spritesheetSvg = `<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<defs><polygon id="bow" points="0,0 0,25 30,0 30,25"/></defs>
<use href="#bow" x="0" y="0" fill="red"/>
<use href="#bow" x="35" y="0" fill="cyan"/>
<use href="#bow" x="0" y="30" fill="orange"/>
<use href="#bow" x="35" y="30" fill="fuchsia"/>
</svg>`;
Comment on lines +990 to +996

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be a moot point given my other comment about sprites, but I think this is a bad thing to demonstrate. SVG paths perform better than images (and text), so when you can you are far better off using shapeFromPath when at all possible. This demonstration looks like an endorsement to use SVGs as images.


const src = `data:image/svg+xml,${encodeURIComponent(spritesheetSvg)}`;

const scalar = 5;

const origins = [
{ x: 0, y: 0 },
{ x: 35, y: 0 },
{ x: 0, y: 30 },
{ x: 35, y: 30 },
];

async function run() {
const shapes = await Promise.all(origins.map(async ({ x, y }) =>
confetti.shapeFromImage({ src, scalar, x, y, width: 30, height: 25 })
));

confetti({ shapes, scalar });
}

run();
}
};

Expand Down
37 changes: 37 additions & 0 deletions src/confetti.js
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,42 @@
};
}

function shapeFromImage(imageData) {
var src = imageData.src;
var scalar = 'scalar' in imageData ? imageData.scalar : 1;

var scale = 1 / scalar;

var img = new Image();
img.src = src;

return promise(function (resolve) {
Comment on lines +860 to +867

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, so I know why you did this. It is so convenient for this lib to just load the image for you, even from a URL. I've seen this backfire a lot in libraries though, from folks who don't totally understand how image loading works thinking that the library has a bug when their images are not served as expected.

Not to mention, the image here doesn't include error handling (because why not add two problems to one comment, am I right?)

Anyway, I think a much more flexible API would be shapeFromCanvas. Have the user load anything they want into a canvas (could be an image, or you could draw anything in code using the canvas API), and let this lib just create the bitmap, scaling, other shape stuff.

img.addEventListener('load', function() {
var size = 10 * scalar;

var sx = 'x' in imageData ? imageData.x : 0;
var sy = 'y' in imageData ? imageData.y : 0;
var sWidth = 'width' in imageData ? imageData.width : img.naturalWidth;
var sHeight = 'height' in imageData ? imageData.height : img.naturalHeight;

var x = 0;
var y = 0;
var width = size;
var height = size * sHeight / sWidth;

var canvas = new OffscreenCanvas(width, height);
var ctx = canvas.getContext('2d');
ctx.drawImage(img, sx, sy, sWidth, sHeight, x, y, width, height);

resolve({
type: 'bitmap',
bitmap: canvas.transferToImageBitmap(),
matrix: [scale, 0, 0, scale, -width * scale / 2, -height * scale / 2]
});
});
});
}

module.exports = function() {
return getDefaultFire().apply(this, arguments);
};
Expand All @@ -866,6 +902,7 @@
module.exports.create = confettiCannon;
module.exports.shapeFromPath = shapeFromPath;
module.exports.shapeFromText = shapeFromText;
module.exports.shapeFromImage = shapeFromImage;
}((function () {
if (typeof window !== 'undefined') {
return window;
Expand Down
74 changes: 74 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,74 @@ test('[text] shoots confetti of an emoji shape', async t => {
t.is(t.context.image.hash(), 'cPpcSrcCjdC');
});

const shapeFromImageImage = async (page, args) => {
const { base64png, ...shape } = await page.evaluate(`
Promise.resolve().then(async () => {
const { bitmap, ...shape } = await confetti.shapeFromImage(${JSON.stringify(args)});

const canvas = document.createElement('canvas');
canvas.width = bitmap.width;
canvas.height = bitmap.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height);

return {
...shape,
base64png: canvas.toDataURL('image/png')
};
});
`);

return {
...shape,
buffer: base64ToBuffer(base64png)
};
};

test('[image] shapeFromImage renders an image', async t => {
const page = t.context.page = await fixturePage();

const { buffer, ...shape } = await shapeFromImageImage(page, { src: 'data:image/gif;base64,R0lGODlhBQAFAIABAP8AAAAAACH5BAEKAAEALAAAAAAFAAUAAAIIjA+RwKxuUigAOw', scalar: 2 });

t.context.buffer = buffer;
t.context.image = await readImage(buffer);

t.deepEqual({
hash: t.context.image.hash(),
...shape
}, {
type: 'bitmap',
matrix: [0.5, 0, 0, 0.5, -5, -5],
hash: '80anMEa00G0'
});
});

// this test renders a black canvas in a headless browser
// but works fine when it is not headless
// eslint-disable-next-line ava/no-skip-test
Comment on lines +861 to +863

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic of this test is copied from the corresponding emoji one, so I copied this comment over too, even though for me it seemed to render fine in headless.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not entirely surprising. That test was written quite a while ago, and I know a lot of work was done on headless mode since. It's possible both tests just work now.

test('[image] shoots confetti of an image', async t => {
const page = t.context.page = await fixturePage();

await page.evaluate(`Promise.resolve().then(async () => {
window.__image = await confetti.shapeFromImage({ src: 'data:image/gif;base64,R0lGODlhBQAFAIABAP8AAAAAACH5BAEKAAEALAAAAAAFAAUAAAIIjA+RwKxuUigAOw', scalar: 2 });
})`);

// these parameters should create an image
// that is the same every time
t.context.buffer = await confettiImage(page, {
startVelocity: 0,
gravity: 0,
scalar: 10,
flat: 1,
ticks: 1000,
// eslint-disable-next-line no-undef
shapes: [() => __image]
});
t.context.image = await readImage(t.context.buffer);

t.is(t.context.image.hash(), '9D_pL$p_Sr_');
});

/*
* Custom canvas
*/
Expand Down Expand Up @@ -1247,3 +1315,9 @@ test('[esm] exposed confetti method has a `shapeFromText` property', async t =>

t.is(await page.evaluate(`typeof confettiAlias.shapeFromText`), 'function');
});

test('[esm] exposed confetti method has a `shapeFromImage` property', async t => {
const page = t.context.page = await fixturePage('fixtures/page.module.html');

t.is(await page.evaluate(`typeof confettiAlias.shapeFromImage`), 'function');
});