Skip to content

Commit

Permalink
Merge pull request #203 from catdad/#81-paths
Browse files Browse the repository at this point in the history
adding support for custom confetti using path shapes
  • Loading branch information
catdad committed Oct 3, 2023
2 parents faa0fdb + 2766b9d commit 98ef206
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 16 deletions.
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

0 comments on commit 98ef206

Please sign in to comment.