Skip to content

Commit

Permalink
Merge pull request #206 from catdad/#81-emoji-confetti
Browse files Browse the repository at this point in the history
adding support for text confetti (a.k.a. emoji!!)
  • Loading branch information
catdad committed Oct 5, 2023
2 parents 320072e + 005de0b commit e4be9da
Show file tree
Hide file tree
Showing 6 changed files with 352 additions and 17 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3.8.1
with:
node-version: 12
node-version: 14
registry-url: https://registry.npmjs.org/
- run: npm install
- run: npm run lint
Expand All @@ -25,3 +25,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/') && github.event_name != 'pull_request'
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
- uses: actions/upload-artifact@v3
with:
name: test-screenshots
path: shots/
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,12 @@ The `confetti` parameter is a single optional `options` object, which has the fo
- `origin.x` _Number (default: 0.5)_: The `x` position on the page, with `0` being the left edge and `1` being the right edge.
- `origin.y` _Number (default: 0.5)_: The `y` position on the page, with `0` being the top edge and `1` being the bottom edge.
- `colors` _Array<String>_: An array of color strings, in the HEX format... you know, like `#bada55`.
- `shapes` _Array<String|Shape>_: An array of shapes for the confetti. There are 3 built-in values of `square`, `circle`, and `star`. The default is to use both squares and circles in an even mix. To use a single shape, you can provide just one shape in the array, such as `['star']`. You can also change the mix by providing a value such as `['circle', 'circle', 'square']` to use two third circles and one third squares. You can also create your own shapes using the `confetti.shapeFromPath` helper method.
- `shapes` _Array<String|Shape>_: An array of shapes for the confetti. There are 3 built-in values of `square`, `circle`, and `star`. The default is to use both squares and circles in an even mix. To use a single shape, you can provide just one shape in the array, such as `['star']`. You can also change the mix by providing a value such as `['circle', 'circle', 'square']` to use two third circles and one third squares. You can also create your own shapes using the [`confetti.shapeFromPath`](#confettishapefrompath-path-matrix---shape) or [`confetti.shapeFromText`](#confettishapefromtext-text-scalar-color-fontfamily---shape) helper methods.
- `scalar` _Number (default: 1)_: Scale factor for each confetti particle. Use decimals to make the confetti smaller. Go on, try teeny tiny confetti, they are adorable!
- `zIndex` _Integer (default: 100)_: The confetti should be on top, after all. But if you have a crazy high page, you can set it even higher.
- `disableForReducedMotion` _Boolean (default: false)_: Disables confetti entirely for users that [prefer reduced motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion). The `confetti()` promise will resolve immediately in this case.

### `confetti.shapeFromPath({ path, matrix? })` -> `Shape`
### `confetti.shapeFromPath({ path, matrix? })` `Shape`

This helper method lets you create a custom confetti shape using an [SVG Path string](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d). Any valid path should work, though there are a few caveats:
- All paths will be filed. If you were hoping to have a stroke path, that is not implemented.
Expand All @@ -98,6 +98,31 @@ confetti({
});
```

### `confetti.shapeFromText({ text, scalar?, color?, fontFamily? })``Shape`

This is the highly anticipated feature to render emoji confetti! Use any standard unicode emoji. Or other text, but... maybe don't use other text.

While any text should work, there are some caveats:
- For flailing confetti, something that is mostly square works best. That is, a single character, especially an emoji.
- Rather than rendering text every time a confetti is drawn, this helper actually rasterizes the text. Therefore, it does not scale well after it is created. If you plan to use the `scalar` value to scale your confetti, use the same `scalar` value here when creating the shape. This will make sure the confetti are not blurry.

The options for this method are:
- `options` _`Object`_:
- `text` _`String`_: the text to be rendered as a confetti. If you can't make up your mind, I suggest "🐈".
- `scalar` _`Number, optional, default: 1`_: a scale value relative to the default size. It matches the `scalar` value in the confetti options.
- `color` _`String, optional, default: #000000`_: the color used to render the text.
- `fontFamily` _`String, optional, default: native emoji`_: the font family name to use when rendering the text. The default follows [best practices for rendring the native OS emoji of the device](https://nolanlawson.com/2022/04/08/the-struggle-of-using-native-emoji-on-the-web/), falling back to `sans-serif`. If using a web font, make sure this [font is loaded](https://developer.mozilla.org/en-US/docs/Web/API/FontFace/load) before rendering your confetti.

```javascript
var scalar = 2;
var pineapple = confetti.shapeFromText({ text: '🍍', scalar });

confetti({
shapes: [pineapple],
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
62 changes: 62 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,30 @@ <h2><a href="#paths" id="paths" class="anchor">Custom Shapes</a></h2>
</div>
</div>

<div class="container">
<div class="group" data-name="emoji">
<div class="flex-rows">
<div class="left">
<h2><a href="#emoji" id="emoji" class="anchor">Emoji</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 know any custom shapes? That's okay, just use emoji instead! That's what they
are for anyway. I mean... why do they even exist if we weren't supposed to make
confetti out of them? Think about it.
</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 @@ -856,6 +880,44 @@ <h2><a href="#custom-canvas" id="custom-canvas" class="anchor">Custom Canvas</a>
shapes: [ heart ],
colors: ['#f93963', '#a10864', '#ee0b93']
});
},
emoji: function () {
var scalar = 2;
var unicorn = confetti.shapeFromText({ text: '🦄', scalar });

var defaults = {
spread: 360,
ticks: 60,
gravity: 0,
decay: 0.96,
startVelocity: 20,
shapes: [unicorn],
scalar
};

function shoot() {
confetti({
...defaults,
particleCount: 30
});

confetti({
...defaults,
particleCount: 5,
flat: true
});

confetti({
...defaults,
particleCount: 15,
scalar: scalar / 2,
shapes: ['circle']
});
}

setTimeout(shoot, 0);
setTimeout(shoot, 100);
setTimeout(shoot, 200);
}
};

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"eslint": "^4.16.0",
"eslint-plugin-ava": "9.0.0",
"jimp": "^0.2.28",
"puppeteer": "^1.0.0",
"puppeteer": "^19.11.1",
"rootrequire": "^1.0.0",
"send": "^0.16.1",
"terser": "^3.14.1"
Expand Down
82 changes: 80 additions & 2 deletions src/confetti.js
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,37 @@
Math.abs(y2 - y1) * 0.1,
Math.PI / 10 * fetti.wobble
));
} else if (fetti.shape.type === 'bitmap') {
var rotation = Math.PI / 10 * fetti.wobble;
var scaleX = Math.abs(x2 - x1) * 0.1;
var scaleY = Math.abs(y2 - y1) * 0.1;
var width = fetti.shape.bitmap.width * fetti.scalar;
var height = fetti.shape.bitmap.height * fetti.scalar;

var matrix = new DOMMatrix([
Math.cos(rotation) * scaleX,
Math.sin(rotation) * scaleX,
-Math.sin(rotation) * scaleY,
Math.cos(rotation) * scaleY,
fetti.x,
fetti.y
]);

// apply the transform matrix from the confetti shape
matrix.multiplySelf(new DOMMatrix(fetti.shape.matrix));

var pattern = context.createPattern(fetti.shape.bitmap, 'no-repeat');
pattern.setTransform(matrix);

context.globalAlpha = (1 - progress);
context.fillStyle = pattern;
context.fillRect(
fetti.x - (width / 2),
fetti.y - (height / 2),
width,
height
);
context.globalAlpha = 1;
} else if (fetti.shape === 'circle') {
context.ellipse ?
context.ellipse(fetti.x, fetti.y, Math.abs(x2 - x1) * fetti.ovalScalar, Math.abs(y2 - y1) * fetti.ovalScalar, Math.PI / 10 * fetti.wobble, 0, 2 * Math.PI) :
Expand Down Expand Up @@ -657,7 +688,7 @@
return t2;
}

function createPathFetti(pathData) {
function shapeFromPath(pathData) {
if (!canUsePaths) {
throw new Error('path confetti are not supported in this browser');
}
Expand Down Expand Up @@ -717,14 +748,61 @@
};
}

function shapeFromText(textData) {
var text,
scalar = 1,
color = '#000000',
// see https://nolanlawson.com/2022/04/08/the-struggle-of-using-native-emoji-on-the-web/
fontFamily = '"Twemoji Mozilla", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", "EmojiOne Color", "Android Emoji", "system emoji", sans-serif';

if (typeof textData === 'string') {
text = textData;
} else {
text = textData.text;
scalar = 'scalar' in textData ? textData.scalar : scalar;
fontFamily = 'fontFamily' in textData ? textData.fontFamily : fontFamily;
color = 'color' in textData ? textData.color : color;
}

// all other confetti are 10 pixels,
// so this pixel size is the de-facto 100% scale confetti
var fontSize = 10 * scalar;
var font = '' + fontSize + 'px ' + fontFamily;

var canvas = new OffscreenCanvas(fontSize, fontSize);
var ctx = canvas.getContext('2d');

ctx.font = font;
var size = ctx.measureText(text);
var width = Math.floor(size.width);
var height = Math.floor(size.fontBoundingBoxAscent + size.fontBoundingBoxDescent);

canvas = new OffscreenCanvas(width, height);
ctx = canvas.getContext('2d');
ctx.font = font;
ctx.fillStyle = color;

ctx.fillText(text, 0, fontSize);

var scale = 1 / scalar;

return {
type: 'bitmap',
// TODO these probably need to be transfered for workers
bitmap: canvas.transferToImageBitmap(),
matrix: [scale, 0, 0, scale, -width * scale / 2, -height * scale / 2]
};
}

module.exports = function() {
return getDefaultFire().apply(this, arguments);
};
module.exports.reset = function() {
getDefaultFire().reset();
};
module.exports.create = confettiCannon;
module.exports.shapeFromPath = createPathFetti;
module.exports.shapeFromPath = shapeFromPath;
module.exports.shapeFromText = shapeFromText;
}((function () {
if (typeof window !== 'undefined') {
return window;
Expand Down

0 comments on commit e4be9da

Please sign in to comment.