Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding support for custom confetti using path shapes #203

Merged
merged 26 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8c63f15
adding initial implementation of Path2D confetti
catdad Oct 23, 2022
4eb9547
Merge branch 'master' into #81-paths
catdad Oct 24, 2022
12cb321
also making sure DOMMatrix exists
catdad Oct 27, 2022
5d99f31
Merge branch 'master' into #81-paths
catdad Sep 28, 2023
e858231
cleaning up code comment
catdad Sep 28, 2023
ac8c8e1
using DOMMatrix that works in workers, and also using serializable pa…
catdad Sep 28, 2023
a0c3696
adjusting demo scalar for custom shapes to look better
catdad Sep 28, 2023
fa62972
cleaning up custom shapes demo
catdad Sep 28, 2023
d5f0982
more demo cleanup
catdad Sep 28, 2023
cd85326
simplifying implementation to dedupe unnecessary actions
catdad Sep 28, 2023
7ac7af6
making sure that a square path is by default the same size as the def…
catdad Sep 29, 2023
a039713
adding a type to the shape... I know I want other shapes to have a type
catdad Sep 29, 2023
ff41cbe
including a full transform matrix in the shape rather than shape meta…
catdad Sep 29, 2023
ff4154d
updating sample with a cached matrix
catdad Sep 29, 2023
444a1d7
removing ghost... too many shapes is confusing, and the white color d…
catdad Sep 29, 2023
aeb70a2
starting to add some tests
catdad Oct 1, 2023
8245b44
using tags in test names, for better readability
catdad Oct 1, 2023
3397b63
cleanup
catdad Oct 1, 2023
be86bc4
Merge branch 'master' into #81-paths
catdad Oct 2, 2023
0e99a1c
adding test to make sure custom shape is rendered
catdad Oct 2, 2023
62592e4
Merge branch 'master' into #81-paths
catdad Oct 2, 2023
3d9b243
adding readme content for path shapes
catdad Oct 3, 2023
f6eacbe
fixing documentation to use new properties
catdad Oct 3, 2023
d356aa5
adding an example for path shape
catdad Oct 3, 2023
e4f7098
cleanup
catdad Oct 3, 2023
2766b9d
removing fallback information, it's not correct
catdad Oct 3, 2023
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
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,32 @@ 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>_: An array of shapes for the confetti. The possible values are `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.
- `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.
- `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`

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.
- Paths are limited to a single color, so keep that in mind.
- All paths need a valid transform matrix. You can pass one in, or you can leave it out and use this helper to calculate the matrix for you. Do note that calculating the matrix is a bit expensive, so it is best to calculate it once for each path in development and cache that value, so that production confetti remain fast. The matrix is deterministic and will always be the same given the same path value.
- For best forward compatibility, it is best to re-generate and re-cache the matrix if you update the `canvas-confetti` library.
- Support for path-based confetti is limited to browsers which support [`Path2D`](https://developer.mozilla.org/en-US/docs/Web/API/Path2D), which should really be all major browser at this point.

This method will return a `Shape` -- it's really just a plain object with some properties, but shhh... we'll pretend it's a shape. Pass this `Shape` object into the `shapes` array directly.

As an example, here's how you might do a triangle confetti:

```javascript
var triangle = confetti.shapeFromPath({ path: 'M0 10 L5 0 L10 10z' });

confetti({
shapes: [triangle]
});
```

### `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
69 changes: 69 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,30 @@ <h2><a href="#continuous" id="continuous" class="anchor">School Pride</a></h2>
</div>
</div>

<div class="container">
<div class="group" data-name="paths">
<div class="flex-rows">
<div class="left">
<h2><a href="#paths" id="paths" class="anchor">Custom Shapes</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>
Celebrate some holidays with holiday-appropriate shapes! You can use any SVG path
to make a confetti out of it. Go wild!
</p>
<p class="center">🎃🎄💜</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 @@ -786,6 +810,51 @@ <h2><a href="#custom-canvas" id="custom-canvas" class="anchor">Custom Canvas</a>
spread: 70,
origin: { y: 1.2 }
});
},
paths: function() {
// note: you CAN only use a path for confetti.shapeFrompath(), but for
// performance reasons it is best to use it once in development and save
// the result to avoid the performance penalty at runtime

// pumpkin shape from https://thenounproject.com/icon/pumpkin-5253388/
var pumpkin = confetti.shapeFromPath({
path: 'M449.4 142c-5 0-10 .3-15 1a183 183 0 0 0-66.9-19.1V87.5a17.5 17.5 0 1 0-35 0v36.4a183 183 0 0 0-67 19c-4.9-.6-9.9-1-14.8-1C170.3 142 105 219.6 105 315s65.3 173 145.7 173c5 0 10-.3 14.8-1a184.7 184.7 0 0 0 169 0c4.9.7 9.9 1 14.9 1 80.3 0 145.6-77.6 145.6-173s-65.3-173-145.7-173zm-220 138 27.4-40.4a11.6 11.6 0 0 1 16.4-2.7l54.7 40.3a11.3 11.3 0 0 1-7 20.3H239a11.3 11.3 0 0 1-9.6-17.5zM444 383.8l-43.7 17.5a17.7 17.7 0 0 1-13 0l-37.3-15-37.2 15a17.8 17.8 0 0 1-13 0L256 383.8a17.5 17.5 0 0 1 13-32.6l37.3 15 37.2-15c4.2-1.6 8.8-1.6 13 0l37.3 15 37.2-15a17.5 17.5 0 0 1 13 32.6zm17-86.3h-82a11.3 11.3 0 0 1-6.9-20.4l54.7-40.3a11.6 11.6 0 0 1 16.4 2.8l27.4 40.4a11.3 11.3 0 0 1-9.6 17.5z',
matrix: [0.020491803278688523, 0, 0, 0.020491803278688523, -7.172131147540983, -5.9016393442622945]
});
// tree shape from https://thenounproject.com/icon/pine-tree-1471679/
var tree = confetti.shapeFromPath({
path: 'M120 240c-41,14 -91,18 -120,1 29,-10 57,-22 81,-40 -18,2 -37,3 -55,-3 25,-14 48,-30 66,-51 -11,5 -26,8 -45,7 20,-14 40,-30 57,-49 -13,1 -26,2 -38,-1 18,-11 35,-25 51,-43 -13,3 -24,5 -35,6 21,-19 40,-41 53,-67 14,26 32,48 54,67 -11,-1 -23,-3 -35,-6 15,18 32,32 51,43 -13,3 -26,2 -38,1 17,19 36,35 56,49 -19,1 -33,-2 -45,-7 19,21 42,37 67,51 -19,6 -37,5 -56,3 25,18 53,30 82,40 -30,17 -79,13 -120,-1l0 41 -31 0 0 -41z',
matrix: [0.03597122302158273, 0, 0, 0.03597122302158273, -4.856115107913669, -5.071942446043165]
});
// heart shape from https://thenounproject.com/icon/heart-1545381/
var heart = confetti.shapeFromPath({
path: 'M167 72c19,-38 37,-56 75,-56 42,0 76,33 76,75 0,76 -76,151 -151,227 -76,-76 -151,-151 -151,-227 0,-42 33,-75 75,-75 38,0 57,18 76,56z',
matrix: [0.03333333333333333, 0, 0, 0.03333333333333333, -5.566666666666666, -5.533333333333333]
});

var defaults = {
scalar: 2,
spread: 270,
particleCount: 25,
origin: { y: 0.4 },
startVelocity: 35
};

confetti({
...defaults,
shapes: [ pumpkin ],
colors: ['#ff9a00', '#ff7400', '#ff4d00']
});
confetti({
...defaults,
shapes: [ tree ],
colors: ['#8d960f', '#be0f10', '#445404']
});
confetti({
...defaults,
shapes: [ heart ],
colors: ['#f93963', '#a10864', '#ee0b93']
});
}
};

Expand Down
96 changes: 95 additions & 1 deletion src/confetti.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
global.URL &&
global.URL.createObjectURL);

var canUsePaths = typeof Path2D === 'function' && typeof DOMMatrix === 'function';

function noop() {}

// create a promise if it exists, otherwise, just
Expand Down Expand Up @@ -341,9 +343,20 @@
var y2 = fetti.wobbleY + (fetti.random * fetti.tiltSin);

context.fillStyle = 'rgba(' + fetti.color.r + ', ' + fetti.color.g + ', ' + fetti.color.b + ', ' + (1 - progress) + ')';

context.beginPath();

if (fetti.shape === 'circle') {
if (canUsePaths && fetti.shape.type === 'path' && typeof fetti.shape.path === 'string' && Array.isArray(fetti.shape.matrix)) {
context.fill(transformPath2D(
fetti.shape.path,
fetti.shape.matrix,
fetti.x,
fetti.y,
Math.abs(x2 - x1) * 0.1,
Math.abs(y2 - y1) * 0.1,
Math.PI / 10 * fetti.wobble
));
} 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) :
ellipse(context, 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 @@ -624,13 +637,94 @@
return defaultFire;
}

function transformPath2D(pathString, pathMatrix, x, y, scaleX, scaleY, rotation) {
var path2d = new Path2D(pathString);

var t1 = new Path2D();
t1.addPath(path2d, new DOMMatrix(pathMatrix));

var t2 = new Path2D();
// see https://developer.mozilla.org/en-US/docs/Web/API/DOMMatrix/DOMMatrix
t2.addPath(t1, new DOMMatrix([
Math.cos(rotation) * scaleX,
Math.sin(rotation) * scaleX,
-Math.sin(rotation) * scaleY,
Math.cos(rotation) * scaleY,
x,
y
]));

return t2;
}

function createPathFetti(pathData) {
if (!canUsePaths) {
throw new Error('path confetti are not supported in this browser');
}

var path, matrix;

if (typeof pathData === 'string') {
path = pathData;
} else {
path = pathData.path;
matrix = pathData.matrix;
}

var path2d = new Path2D(path);
var tempCanvas = document.createElement('canvas');
var tempCtx = tempCanvas.getContext('2d');

if (!matrix) {
// attempt to figure out the width of the path, up to 1000x1000
var maxSize = 1000;
var minX = maxSize;
var minY = maxSize;
var maxX = 0;
var maxY = 0;
var width, height;

// do some line skipping... this is faster than checking
// every pixel and will be mostly still correct
for (var x = 0; x < maxSize; x += 2) {
for (var y = 0; y < maxSize; y += 2) {
if (tempCtx.isPointInPath(path2d, x, y, 'nonzero')) {
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}
}
}

width = maxX - minX;
height = maxY - minY;

var maxDesiredSize = 10;
var scale = Math.min(maxDesiredSize/width, maxDesiredSize/height);

matrix = [
scale, 0, 0, scale,
-Math.round((width/2) + minX) * scale,
-Math.round((height/2) + minY) * scale
];
}

return {
type: 'path',
path: path,
matrix: matrix
};
}

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