pax_global_header 0000666 0000000 0000000 00000000064 14200565716 0014520 g ustar 00root root 0000000 0000000 52 comment=a85c3139458acaf62d7238aa22918351690975a1
canvas-confetti-1.5.1/ 0000775 0000000 0000000 00000000000 14200565716 0014610 5 ustar 00root root 0000000 0000000 canvas-confetti-1.5.1/.brackets.json 0000664 0000000 0000000 00000000644 14200565716 0017363 0 ustar 00root root 0000000 0000000 {
"spaceUnits": 2,
"useTabChar": false,
"language": {
"javascript": {
"linting.prefer": [
"ESLint"
],
"linting.usePreferredOnly": true
},
"markdown": {
"wordWrap": true
}
},
"language.fileNames": {
"Jenkinsfile": "groovy"
},
"language.fileExtensions": {
"mjs": "javascript"
}
}
canvas-confetti-1.5.1/.editorconfig 0000664 0000000 0000000 00000000273 14200565716 0017267 0 ustar 00root root 0000000 0000000 root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{package.json,*.yml}]
indent_size = 2
canvas-confetti-1.5.1/.eslintrc.yml 0000664 0000000 0000000 00000000160 14200565716 0017231 0 ustar 00root root 0000000 0000000 extends:
- eslint:recommended
env:
es6: false
browser: true
node: true
rules:
semi: [ error, always ]
canvas-confetti-1.5.1/.gitattributes 0000664 0000000 0000000 00000001240 14200565716 0017500 0 ustar 00root root 0000000 0000000 # Auto detect text files and perform LF normalization
* text=auto
*.js text eol=lf
*.jsx text eol=lf
*.json text eol=lf
*.html text eol=lf
*.md text eol=lf
*.yml text eol=lf
*.css text eol=lf
*.less text eol=lf
*.scss text eol=lf
*.sass text eol=lf
*.svg text eol=lf
*.xml text eol=lf
*.sh text eol=lf
# Custom for Visual Studio
*.cs diff=csharp
# Standard to msysgit
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
canvas-confetti-1.5.1/.github/ 0000775 0000000 0000000 00000000000 14200565716 0016150 5 ustar 00root root 0000000 0000000 canvas-confetti-1.5.1/.github/funding.yml 0000664 0000000 0000000 00000000135 14200565716 0020324 0 ustar 00root root 0000000 0000000 github: catdad
custom: ["https://www.paypal.me/kirilvatev", "https://venmo.com/Kiril-Vatev"]
canvas-confetti-1.5.1/.github/workflows/ 0000775 0000000 0000000 00000000000 14200565716 0020205 5 ustar 00root root 0000000 0000000 canvas-confetti-1.5.1/.github/workflows/ci.yml 0000664 0000000 0000000 00000001105 14200565716 0021320 0 ustar 00root root 0000000 0000000 name: CI
on:
push:
pull_request:
branches: [master]
env:
FORCE_COLOR: 1
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
registry-url: https://registry.npmjs.org/
- run: npm install
- run: npm run lint
- run: npm test
- run: npm pack --dry-run
- run: npm publish
if: startsWith(github.ref, 'refs/tags/') && github.event_name != 'pull_request'
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
canvas-confetti-1.5.1/.gitignore 0000664 0000000 0000000 00000001061 14200565716 0016576 0 ustar 00root root 0000000 0000000 # Windows image file caches
Thumbs.db
ehthumbs.db
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows shortcuts
*.lnk
# OSX
.DS_Store
.AppleDouble
.LSOverride
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Node stuff
node_modules/
coverage/
.nyc_output/
# project-specific stuff
shots/
dist/
temp/
canvas-confetti-1.5.1/.npmignore 0000664 0000000 0000000 00000001171 14200565716 0016607 0 ustar 00root root 0000000 0000000 # Windows image file caches
Thumbs.db
ehthumbs.db
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows shortcuts
*.lnk
# OSX
.DS_Store
.AppleDouble
.LSOverride
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Node stuff
node_modules/
coverage/
.nyc_output/
# project-specific stuff
bin/
test/
shots/
temp/
build/
fixtures/
index.html
# dotfiles don't need to be in npm
.*
canvas-confetti-1.5.1/.npmrc 0000664 0000000 0000000 00000000023 14200565716 0015723 0 ustar 00root root 0000000 0000000 package-lock=false
canvas-confetti-1.5.1/LICENSE 0000664 0000000 0000000 00000001347 14200565716 0015622 0 ustar 00root root 0000000 0000000 ISC License
Copyright (c) 2020, Kiril Vatev
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
canvas-confetti-1.5.1/README.md 0000664 0000000 0000000 00000026452 14200565716 0016100 0 ustar 00root root 0000000 0000000 # [](https://github.com/catdad/canvas-confetti/)
[![github actions ci][ci.svg]][ci.link]
[![jsdelivr][jsdelivr.svg]][jsdelivr.link]
[![npm-downloads][npm-downloads.svg]][npm.link]
[![npm-version][npm-version.svg]][npm.link]
[ci.svg]: https://github.com/catdad/canvas-confetti/actions/workflows/ci.yml/badge.svg
[ci.link]: https://github.com/catdad/canvas-confetti/actions/workflows/ci.yml?query=branch%3Amaster
[jsdelivr.svg]: https://data.jsdelivr.com/v1/package/npm/canvas-confetti/badge?style=rounded
[jsdelivr.link]: https://www.jsdelivr.com/package/npm/canvas-confetti
[npm-downloads.svg]: https://img.shields.io/npm/dm/canvas-confetti.svg
[npm.link]: https://www.npmjs.com/package/canvas-confetti
[npm-version.svg]: https://img.shields.io/npm/v/canvas-confetti.svg
## Demo
[catdad.github.io/canvas-confetti](https://catdad.github.io/canvas-confetti/)
## Install
You can install this module as a component from NPM:
```bash
npm install --save canvas-confetti
```
You can then `require('canvas-confetti');` to use it in your project build. _Note: this is a client component, and will not run in Node. You will need to build your project with something like [webpack](https://github.com/webpack/webpack) in order to use this._
You can also include this library in your HTML page directly from a CDN:
```html
```
_Note: you should use the latest version at the time that you include your project. You can see all versions [on the releases page](https://github.com/catdad/canvas-confetti/releases)._
## Reduced Motion
Thank you for joining me in this very important message about motion on your website. See, [not everyone likes it, and some actually prefer no motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion). They have [ways to tell us about it](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion) and we should listen. While I don't want to go as far as tell you not to have confetti on your page just yet, I do want to make it easy for you to respect what your users want. There is a `disableForReducedMotion` option you can use so that users that have trouble with chaotic animations don't need to struggle on your website. This is disabled by default, but I am considering changing that in a future major release. If you have strong feelings about this, [please let me know](https://github.com/catdad/canvas-confetti/issues/new). For now, please confetti responsibly.
## API
When installed from `npm`, this library can be required as a client component in your project build. When using the CDN version, it is exposed as a `confetti` function on `window`.
### `confetti([options {Object}])` → `Promise|null`
`confetti` takes a single optional object. When `window.Promise` is available, it will return a Promise to let you know when it is done. When promises are not available (like in IE), it will return `null`. You can polyfill promises using any of the popular polyfills. You can also provide a promise implementation to `confetti` through:
```javascript
const MyPromise = require('some-promise-lib');
const confetti = require('canvas-confetti');
confetti.Promise = MyPromise;
```
If you call `confetti` multiple times before it is done, it will return the same promise every time. Internally, the same canvas element will be reused, continuing the existing animation with the new confetti added. The promise returned by each call to `confetti` will resolve once all animations are done.
#### `options`
The `confetti` parameter is a single optional `options` object, which has the following properties:
- `particleCount` _Integer (default: 50)_: The number of confetti to launch. More is always fun... but be cool, there's a lot of math involved.
- `angle` _Number (default: 90)_: The angle in which to launch the confetti, in degrees. 90 is straight up.
- `spread` _Number (default: 45)_: How far off center the confetti can go, in degrees. 45 means the confetti will launch at the defined `angle` plus or minus 22.5 degrees.
- `startVelocity` _Number (default: 45)_: How fast the confetti will start going, in pixels.
- `decay` _Number (default: 0.9)_: How quickly the confetti will lose speed. Keep this number between 0 and 1, otherwise the confetti will gain speed. Better yet, just never change it.
- `gravity` _Number (default: 1)_: How quickly the particles are pulled down. 1 is full gravity, 0.5 is half gravity, etc., but there are no limits. You can even make particles go up if you'd like.
- `drift` _Number (default: 0)_: How much to the side the confetti will drift. The default is 0, meaning that they will fall straight down. Use a negative number for left and positive number for right.
- `ticks` _Number (default: 200)_: How many times the confetti will move. This is abstract... but play with it if the confetti disappear too quickly for you.
- `origin` _Object_: Where to start firing confetti from. Feel free to launch off-screen if you'd like.
- `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` and `circle`. The default is to use both shapes in an even mix. You can even change the mix by providing a value such as `['circle', 'circle', 'square']` to use two third circles and one third squares.
- `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.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).
_Canvas can be misunderstood a bit though, so let me explain why you might want to let the module modify the canvas just a bit. By default, a `canvas` is a relatively small image -- somewhere around 300x150, depending on the browser. When you resize it using CSS, this sets the display size of the canvas, but not the image being represented on that canvas. Think of it as loading a 300x150 jpeg image in an `img` tag and then setting the CSS for that tag to `1500x600` -- your image will end up stretched and blurry. In the case of a canvas, you need to also set the width and height of the canvas image itself. If you don't want to do that, you can allow `confetti` to set it for you._
Note also that you should persist the custom instance and avoid initializing an instance of confetti with the same canvas element more than once.
The following global options are available:
* `resize` _Boolean (default: false)_: Whether to allow setting the canvas image size, as well as keep it correctly sized if the window changes size (e.g. resizing the window, rotating a mobile device, etc.). By default, the canvas size will not be modified.
* `useWorker` _Boolean (default: false)_: Whether to use an asynchronous web worker to render the confetti animation, whenever possible. This is turned off by default, meaning that the animation will always execute on the main thread. If turned on and the browser supports it, the animation will execute off of the main thread so that it is not blocking any other work your page needs to do. Using this option will also modify the canvas, but more on that directly below -- do read it. If it is not supported by the browser, this value will be ignored.
* `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). When set to true, use of this confetti instance will always respect a user's request for reduced motion and disable confetti for them.
_**Important: If you use `useWorker: true`, I own your canvas now. It's mine now and I can do whatever I want with it (don't worry... I'll just put confetti inside it, I promise). You must not try to use the canvas in any way (other than I guess removing it from the DOM), as it will throw an error. When using workers for rendering, control of the canvas must be transferred to the web worker, preventing any usage of that canvas on the main thread. If you must manipulate the canvas in any way, do not use this option.**_
```javascript
var myCanvas = document.createElement('canvas');
document.body.appendChild(myCanvas);
var myConfetti = confetti.create(myCanvas, {
resize: true,
useWorker: true
});
myConfetti({
particleCount: 100,
spread: 160
// any other options from the global
// confetti function
});
```
### `confetti.reset()`
Stops the animation and clears all confetti, as well as immediately resolves any outstanding promises. In the case of a separate confetti instance created with [`confetti.create`](#confetticreatecanvas-globaloptions--function), that instance will have its own `reset` method.
```javascript
confetti();
setTimeout(() => {
confetti.reset();
}, 100);
```
```javascript
var myCanvas = document.createElement('canvas');
document.body.appendChild(myCanvas);
var myConfetti = confetti.create(myCanvas, { resize: true });
myConfetti();
setTimeout(() => {
myConfetti.reset();
}, 100);
```
## Examples
Launch some confetti the default way:
```javascript
confetti();
```
Launch a bunch of confetti:
```javascript
confetti({
particleCount: 150
});
```
Launch some confetti really wide:
```javascript
confetti({
spread: 180
});
```
Get creative. Launch a small poof of confetti from a random part of the page:
```javascript
confetti({
particleCount: 100,
startVelocity: 30,
spread: 360,
origin: {
x: Math.random(),
// since they fall down, start a bit higher than random
y: Math.random() - 0.2
}
});
```
I said creative... we can do better. Since it doesn't matter how many times we call `confetti` (just the total number of confetti in the air), we can do some fun things, like continuously launch more and more confetti for 30 seconds, from multiple directions:
```javascript
// do this for 30 seconds
var duration = 30 * 1000;
var end = Date.now() + duration;
(function frame() {
// launch a few confetti from the left edge
confetti({
particleCount: 7,
angle: 60,
spread: 55,
origin: { x: 0 }
});
// and launch a few from the right edge
confetti({
particleCount: 7,
angle: 120,
spread: 55,
origin: { x: 1 }
});
// keep going until we are out of time
if (Date.now() < end) {
requestAnimationFrame(frame);
}
}());
```
canvas-confetti-1.5.1/bin/ 0000775 0000000 0000000 00000000000 14200565716 0015360 5 ustar 00root root 0000000 0000000 canvas-confetti-1.5.1/bin/on_failure.sh 0000775 0000000 0000000 00000000771 14200565716 0020047 0 ustar 00root root 0000000 0000000 green=`tput setaf 2`
reset=`tput sgr0`
line_break () {
echo --------------------------------------
}
print_green () {
echo "${green}$@${reset}"
}
upload_file () {
filename=$1
urlname=${filename// /_}
downloadurl=`curl -sS --upload-file "./$filename" https://transfer.sh/$urlname`
echo image \"$filename\"
print_green " uploaded to: $downloadurl"
}
find_files () {
cd shots
echo list of files present:
ls -l
line_break
for i in *.png;do upload_file "$i";done
}
find_files
canvas-confetti-1.5.1/build/ 0000775 0000000 0000000 00000000000 14200565716 0015707 5 ustar 00root root 0000000 0000000 canvas-confetti-1.5.1/build/.eslintrc.yml 0000664 0000000 0000000 00000000135 14200565716 0020332 0 ustar 00root root 0000000 0000000 env:
es6: true
node: true
browser: false
parser: babel-eslint
rules:
no-console: off
canvas-confetti-1.5.1/build/build.js 0000664 0000000 0000000 00000002461 14200565716 0017347 0 ustar 00root root 0000000 0000000 const fs = require('fs');
const { promisify } = require('util');
const { name, version, main } = require('../package.json');
const buildDate = (new Date()).toISOString();
function mkdir(dir) {
return promisify(fs.mkdir)(dir).then(() => {
return Promise.resolve();
}).catch(err => {
if (err.code === 'EEXIST') {
return Promise.resolve();
}
return Promise.reject(err);
});
}
function readFile(file, encoding) {
return promisify(fs.readFile)(file, encoding);
}
function writeFile(file, content) {
return promisify(fs.writeFile)(file, content);
}
function buildCommonJs(content) {
return `// ${name} v${version} built on ${buildDate}
!(function (window, module) {
// source content
${content}
// end source content
window.confetti = module.exports;
}(window, {}));
`;
}
function buildModule(content) {
return `// ${name} v${version} built on ${buildDate}
var module = {};
// source content
${content}
// end source content
export default module.exports;
export var create = module.exports.create;
`;
}
mkdir('dist')
.then(() => readFile(main))
.then(file => {
return Promise.all([
writeFile('dist/confetti.browser.js', buildCommonJs(file)),
writeFile('dist/confetti.module.mjs', buildModule(file))
]);
})
.catch(err => {
console.error(err);
process.exitCode = 1;
});
canvas-confetti-1.5.1/build/serve.js 0000664 0000000 0000000 00000003377 14200565716 0017403 0 ustar 00root root 0000000 0000000 const path = require('path');
const http = require('http');
const send = require('send');
const root = require('rootrequire');
const PORT = 9001;
http.createServer(function (req, res) {
const url = req.url === '/' ? '/index.html' : req.url;
const file = path.resolve(root, url.slice(1));
console.log(req.method, url, '->', file);
const cspRules = [
`default-src 'self' https://cdnjs.cloudflare.com`,
`img-src * data: blob:`,
`media-src * data: blob:`,
`font-src https://fonts.googleapis.com https://fonts.gstatic.com`,
`style-src 'self' 'unsafe-inline' https://fonts.googleapis.com`,
`script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdnjs.cloudflare.com`,
// `worker-src 'self' blob:`
];
res.setHeader('content-security-policy', cspRules.join('; '));
send(req, file).pipe(res);
}).listen(PORT, () => {
console.log(`listening on port ${PORT}`);
});
// twitch csp... see !112
// default-src 'self' https://6rh8h42nhnjmirlyxc0w9lbpmeehi0.ext-twitch.tv
// block-all-mixed-content
// img-src * data: blob:
// media-src * data: blob:
// frame-ancestors https://supervisor.ext-twitch.tv https://extension-files.twitch.tv https://*.twitch.tv https://*.twitch.tech https://localhost.twitch.tv:* https://localhost.twitch.tech:* http://localhost.rig.twitch.tv:*
// font-src https://6rh8h42nhnjmirlyxc0w9lbpmeehi0.ext-twitch.tv https://fonts.googleapis.com https://fonts.gstatic.com
// style-src 'self' 'unsafe-inline' https://6rh8h42nhnjmirlyxc0w9lbpmeehi0.ext-twitch.tv https://fonts.googleapis.com
// connect-src https: wss: https://www.google-analytics.com https://stats.g.doubleclick.net
// script-src 'self' https://6rh8h42nhnjmirlyxc0w9lbpmeehi0.ext-twitch.tv https://extension-files.twitch.tv https://www.google-analytics.com
canvas-confetti-1.5.1/fixtures/ 0000775 0000000 0000000 00000000000 14200565716 0016461 5 ustar 00root root 0000000 0000000 canvas-confetti-1.5.1/fixtures/page.browserify.html 0000664 0000000 0000000 00000000430 14200565716 0022452 0 ustar 00root root 0000000 0000000
Go crazy with some randomness. Shoot a random amount of confetti in random directions.
(Go ahead... you know you want to click that button more than once.)
If you happened to get curious and changed the particle count to 400 or so, you saw
something disappointing. An even "flattened cone" look to the confetti, making it look
way too perfect and ruining the illusion. We can fix that by mixing a few effects together.
Why click a button repeatedly when you can have code do it for you? Shoot some firework
of confetti from the sides of page so you can still read the content in the center.
But if you are into crazy rapid fire of confetti, what could be a better use than
to show everyone what you are all about? Tell people where you are from with two
confetti cannons from either side of the page.
But if you just hate confetti all over the place, there's something here for you
as well. You can limit where the confetti appear by providing your own canvas element.
canvas-confetti-1.5.1/package.json 0000664 0000000 0000000 00000003211 14200565716 0017073 0 ustar 00root root 0000000 0000000 {
"name": "canvas-confetti",
"version": "1.5.1",
"description": "performant confetti animation in the browser",
"main": "src/confetti.js",
"module": "dist/confetti.module.mjs",
"jsdelivr": "dist/confetti.browser.js",
"scripts": {
"build": "node build/build.js",
"browserify": "browserify --entry src/confetti.js --outfile temp/confetti.bundle.js --standalone confetti",
"minify": "terser --compress --mangle -o temp/confetti.min.js -- dist/confetti.browser.js",
"pretest": "npm run build -s && npm run browserify -s && npm run -s minify",
"test": "cd test && ava --verbose --serial",
"lint": "eslint src/**/*.js test/**/*.js build/**/*.js",
"dev": "node build/serve.js",
"devtest": "cross-env CONFETTI_SHOW=1 npm test",
"citest": "npm run test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/catdad/canvas-confetti.git"
},
"author": "Kiril Vatev ",
"license": "ISC",
"bugs": {
"url": "https://github.com/catdad/canvas-confetti/issues"
},
"homepage": "https://github.com/catdad/canvas-confetti#readme",
"devDependencies": {
"ava": "2.4.0",
"babel-eslint": "^8.2.1",
"browserify": "^15.2.0",
"cross-env": "^5.1.3",
"eslint": "^4.16.0",
"eslint-plugin-ava": "9.0.0",
"jimp": "^0.2.28",
"puppeteer": "^1.0.0",
"rootrequire": "^1.0.0",
"send": "^0.16.1",
"terser": "^3.14.1"
},
"dependencies": {},
"keywords": [
"canvas",
"confetti",
"animation",
"burst",
"fireworks",
"snow",
"particles"
],
"funding": {
"type": "donate",
"url": "https://www.paypal.me/kirilvatev"
}
}
canvas-confetti-1.5.1/src/ 0000775 0000000 0000000 00000000000 14200565716 0015377 5 ustar 00root root 0000000 0000000 canvas-confetti-1.5.1/src/confetti.js 0000664 0000000 0000000 00000040437 14200565716 0017560 0 ustar 00root root 0000000 0000000 (function main(global, module, isWorker, workerSize) {
var canUseWorker = !!(
global.Worker &&
global.Blob &&
global.Promise &&
global.OffscreenCanvas &&
global.OffscreenCanvasRenderingContext2D &&
global.HTMLCanvasElement &&
global.HTMLCanvasElement.prototype.transferControlToOffscreen &&
global.URL &&
global.URL.createObjectURL);
function noop() {}
// create a promise if it exists, otherwise, just
// call the function directly
function promise(func) {
var ModulePromise = module.exports.Promise;
var Prom = ModulePromise !== void 0 ? ModulePromise : global.Promise;
if (typeof Prom === 'function') {
return new Prom(func);
}
func(noop, noop);
return null;
}
var raf = (function () {
var TIME = Math.floor(1000 / 60);
var frame, cancel;
var frames = {};
var lastFrameTime = 0;
if (typeof requestAnimationFrame === 'function' && typeof cancelAnimationFrame === 'function') {
frame = function (cb) {
var id = Math.random();
frames[id] = requestAnimationFrame(function onFrame(time) {
if (lastFrameTime === time || lastFrameTime + TIME - 1 < time) {
lastFrameTime = time;
delete frames[id];
cb();
} else {
frames[id] = requestAnimationFrame(onFrame);
}
});
return id;
};
cancel = function (id) {
if (frames[id]) {
cancelAnimationFrame(frames[id]);
}
};
} else {
frame = function (cb) {
return setTimeout(cb, TIME);
};
cancel = function (timer) {
return clearTimeout(timer);
};
}
return { frame: frame, cancel: cancel };
}());
var getWorker = (function () {
var worker;
var prom;
var resolves = {};
function decorate(worker) {
function execute(options, callback) {
worker.postMessage({ options: options || {}, callback: callback });
}
worker.init = function initWorker(canvas) {
var offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);
};
worker.fire = function fireWorker(options, size, done) {
if (prom) {
execute(options, null);
return prom;
}
var id = Math.random().toString(36).slice(2);
prom = promise(function (resolve) {
function workerDone(msg) {
if (msg.data.callback !== id) {
return;
}
delete resolves[id];
worker.removeEventListener('message', workerDone);
prom = null;
done();
resolve();
}
worker.addEventListener('message', workerDone);
execute(options, id);
resolves[id] = workerDone.bind(null, { data: { callback: id }});
});
return prom;
};
worker.reset = function resetWorker() {
worker.postMessage({ reset: true });
for (var id in resolves) {
resolves[id]();
delete resolves[id];
}
};
}
return function () {
if (worker) {
return worker;
}
if (!isWorker && canUseWorker) {
var code = [
'var CONFETTI, SIZE = {}, module = {};',
'(' + main.toString() + ')(this, module, true, SIZE);',
'onmessage = function(msg) {',
' if (msg.data.options) {',
' CONFETTI(msg.data.options).then(function () {',
' if (msg.data.callback) {',
' postMessage({ callback: msg.data.callback });',
' }',
' });',
' } else if (msg.data.reset) {',
' CONFETTI.reset();',
' } else if (msg.data.resize) {',
' SIZE.width = msg.data.resize.width;',
' SIZE.height = msg.data.resize.height;',
' } else if (msg.data.canvas) {',
' SIZE.width = msg.data.canvas.width;',
' SIZE.height = msg.data.canvas.height;',
' CONFETTI = module.exports.create(msg.data.canvas);',
' }',
'}',
].join('\n');
try {
worker = new Worker(URL.createObjectURL(new Blob([code])));
} catch (e) {
// eslint-disable-next-line no-console
typeof console !== undefined && typeof console.warn === 'function' ? console.warn('🎊 Could not load worker', e) : null;
return null;
}
decorate(worker);
}
return worker;
};
})();
var defaults = {
particleCount: 50,
angle: 90,
spread: 45,
startVelocity: 45,
decay: 0.9,
gravity: 1,
drift: 0,
ticks: 200,
x: 0.5,
y: 0.5,
shapes: ['square', 'circle'],
zIndex: 100,
colors: [
'#26ccff',
'#a25afd',
'#ff5e7e',
'#88ff5a',
'#fcff42',
'#ffa62d',
'#ff36ff'
],
// probably should be true, but back-compat
disableForReducedMotion: false,
scalar: 1
};
function convert(val, transform) {
return transform ? transform(val) : val;
}
function isOk(val) {
return !(val === null || val === undefined);
}
function prop(options, name, transform) {
return convert(
options && isOk(options[name]) ? options[name] : defaults[name],
transform
);
}
function onlyPositiveInt(number){
return number < 0 ? 0 : Math.floor(number);
}
function randomInt(min, max) {
// [min, max)
return Math.floor(Math.random() * (max - min)) + min;
}
function toDecimal(str) {
return parseInt(str, 16);
}
function colorsToRgb(colors) {
return colors.map(hexToRgb);
}
function hexToRgb(str) {
var val = String(str).replace(/[^0-9a-f]/gi, '');
if (val.length < 6) {
val = val[0]+val[0]+val[1]+val[1]+val[2]+val[2];
}
return {
r: toDecimal(val.substring(0,2)),
g: toDecimal(val.substring(2,4)),
b: toDecimal(val.substring(4,6))
};
}
function getOrigin(options) {
var origin = prop(options, 'origin', Object);
origin.x = prop(origin, 'x', Number);
origin.y = prop(origin, 'y', Number);
return origin;
}
function setCanvasWindowSize(canvas) {
canvas.width = document.documentElement.clientWidth;
canvas.height = document.documentElement.clientHeight;
}
function setCanvasRectSize(canvas) {
var rect = canvas.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
}
function getCanvas(zIndex) {
var canvas = document.createElement('canvas');
canvas.style.position = 'fixed';
canvas.style.top = '0px';
canvas.style.left = '0px';
canvas.style.pointerEvents = 'none';
canvas.style.zIndex = zIndex;
return canvas;
}
function ellipse(context, x, y, radiusX, radiusY, rotation, startAngle, endAngle, antiClockwise) {
context.save();
context.translate(x, y);
context.rotate(rotation);
context.scale(radiusX, radiusY);
context.arc(0, 0, 1, startAngle, endAngle, antiClockwise);
context.restore();
}
function randomPhysics(opts) {
var radAngle = opts.angle * (Math.PI / 180);
var radSpread = opts.spread * (Math.PI / 180);
return {
x: opts.x,
y: opts.y,
wobble: Math.random() * 10,
wobbleSpeed: Math.min(0.11, Math.random() * 0.1 + 0.05),
velocity: (opts.startVelocity * 0.5) + (Math.random() * opts.startVelocity),
angle2D: -radAngle + ((0.5 * radSpread) - (Math.random() * radSpread)),
tiltAngle: (Math.random() * (0.75 - 0.25) + 0.25) * Math.PI,
color: opts.color,
shape: opts.shape,
tick: 0,
totalTicks: opts.ticks,
decay: opts.decay,
drift: opts.drift,
random: Math.random() + 2,
tiltSin: 0,
tiltCos: 0,
wobbleX: 0,
wobbleY: 0,
gravity: opts.gravity * 3,
ovalScalar: 0.6,
scalar: opts.scalar
};
}
function updateFetti(context, fetti) {
fetti.x += Math.cos(fetti.angle2D) * fetti.velocity + fetti.drift;
fetti.y += Math.sin(fetti.angle2D) * fetti.velocity + fetti.gravity;
fetti.wobble += fetti.wobbleSpeed;
fetti.velocity *= fetti.decay;
fetti.tiltAngle += 0.1;
fetti.tiltSin = Math.sin(fetti.tiltAngle);
fetti.tiltCos = Math.cos(fetti.tiltAngle);
fetti.random = Math.random() + 2;
fetti.wobbleX = fetti.x + ((10 * fetti.scalar) * Math.cos(fetti.wobble));
fetti.wobbleY = fetti.y + ((10 * fetti.scalar) * Math.sin(fetti.wobble));
var progress = (fetti.tick++) / fetti.totalTicks;
var x1 = fetti.x + (fetti.random * fetti.tiltCos);
var y1 = fetti.y + (fetti.random * fetti.tiltSin);
var x2 = fetti.wobbleX + (fetti.random * fetti.tiltCos);
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') {
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);
} else {
context.moveTo(Math.floor(fetti.x), Math.floor(fetti.y));
context.lineTo(Math.floor(fetti.wobbleX), Math.floor(y1));
context.lineTo(Math.floor(x2), Math.floor(y2));
context.lineTo(Math.floor(x1), Math.floor(fetti.wobbleY));
}
context.closePath();
context.fill();
return fetti.tick < fetti.totalTicks;
}
function animate(canvas, fettis, resizer, size, done) {
var animatingFettis = fettis.slice();
var context = canvas.getContext('2d');
var animationFrame;
var destroy;
var prom = promise(function (resolve) {
function onDone() {
animationFrame = destroy = null;
context.clearRect(0, 0, size.width, size.height);
done();
resolve();
}
function update() {
if (isWorker && !(size.width === workerSize.width && size.height === workerSize.height)) {
size.width = canvas.width = workerSize.width;
size.height = canvas.height = workerSize.height;
}
if (!size.width && !size.height) {
resizer(canvas);
size.width = canvas.width;
size.height = canvas.height;
}
context.clearRect(0, 0, size.width, size.height);
animatingFettis = animatingFettis.filter(function (fetti) {
return updateFetti(context, fetti);
});
if (animatingFettis.length) {
animationFrame = raf.frame(update);
} else {
onDone();
}
}
animationFrame = raf.frame(update);
destroy = onDone;
});
return {
addFettis: function (fettis) {
animatingFettis = animatingFettis.concat(fettis);
return prom;
},
canvas: canvas,
promise: prom,
reset: function () {
if (animationFrame) {
raf.cancel(animationFrame);
}
if (destroy) {
destroy();
}
}
};
}
function confettiCannon(canvas, globalOpts) {
var isLibCanvas = !canvas;
var allowResize = !!prop(globalOpts || {}, 'resize');
var globalDisableForReducedMotion = prop(globalOpts, 'disableForReducedMotion', Boolean);
var shouldUseWorker = canUseWorker && !!prop(globalOpts || {}, 'useWorker');
var worker = shouldUseWorker ? getWorker() : null;
var resizer = isLibCanvas ? setCanvasWindowSize : setCanvasRectSize;
var initialized = (canvas && worker) ? !!canvas.__confetti_initialized : false;
var preferLessMotion = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion)').matches;
var animationObj;
function fireLocal(options, size, done) {
var particleCount = prop(options, 'particleCount', onlyPositiveInt);
var angle = prop(options, 'angle', Number);
var spread = prop(options, 'spread', Number);
var startVelocity = prop(options, 'startVelocity', Number);
var decay = prop(options, 'decay', Number);
var gravity = prop(options, 'gravity', Number);
var drift = prop(options, 'drift', Number);
var colors = prop(options, 'colors', colorsToRgb);
var ticks = prop(options, 'ticks', Number);
var shapes = prop(options, 'shapes');
var scalar = prop(options, 'scalar');
var origin = getOrigin(options);
var temp = particleCount;
var fettis = [];
var startX = canvas.width * origin.x;
var startY = canvas.height * origin.y;
while (temp--) {
fettis.push(
randomPhysics({
x: startX,
y: startY,
angle: angle,
spread: spread,
startVelocity: startVelocity,
color: colors[temp % colors.length],
shape: shapes[randomInt(0, shapes.length)],
ticks: ticks,
decay: decay,
gravity: gravity,
drift: drift,
scalar: scalar
})
);
}
// if we have a previous canvas already animating,
// add to it
if (animationObj) {
return animationObj.addFettis(fettis);
}
animationObj = animate(canvas, fettis, resizer, size , done);
return animationObj.promise;
}
function fire(options) {
var disableForReducedMotion = globalDisableForReducedMotion || prop(options, 'disableForReducedMotion', Boolean);
var zIndex = prop(options, 'zIndex', Number);
if (disableForReducedMotion && preferLessMotion) {
return promise(function (resolve) {
resolve();
});
}
if (isLibCanvas && animationObj) {
// use existing canvas from in-progress animation
canvas = animationObj.canvas;
} else if (isLibCanvas && !canvas) {
// create and initialize a new canvas
canvas = getCanvas(zIndex);
document.body.appendChild(canvas);
}
if (allowResize && !initialized) {
// initialize the size of a user-supplied canvas
resizer(canvas);
}
var size = {
width: canvas.width,
height: canvas.height
};
if (worker && !initialized) {
worker.init(canvas);
}
initialized = true;
if (worker) {
canvas.__confetti_initialized = true;
}
function onResize() {
if (worker) {
// TODO this really shouldn't be immediate, because it is expensive
var obj = {
getBoundingClientRect: function () {
if (!isLibCanvas) {
return canvas.getBoundingClientRect();
}
}
};
resizer(obj);
worker.postMessage({
resize: {
width: obj.width,
height: obj.height
}
});
return;
}
// don't actually query the size here, since this
// can execute frequently and rapidly
size.width = size.height = null;
}
function done() {
animationObj = null;
if (allowResize) {
global.removeEventListener('resize', onResize);
}
if (isLibCanvas && canvas) {
document.body.removeChild(canvas);
canvas = null;
initialized = false;
}
}
if (allowResize) {
global.addEventListener('resize', onResize, false);
}
if (worker) {
return worker.fire(options, size, done);
}
return fireLocal(options, size, done);
}
fire.reset = function () {
if (worker) {
worker.reset();
}
if (animationObj) {
animationObj.reset();
}
};
return fire;
}
// Make default export lazy to defer worker creation until called.
var defaultFire;
function getDefaultFire() {
if (!defaultFire) {
defaultFire = confettiCannon(null, { useWorker: true, resize: true });
}
return defaultFire;
}
module.exports = function() {
return getDefaultFire().apply(this, arguments);
};
module.exports.reset = function() {
getDefaultFire().reset();
};
module.exports.create = confettiCannon;
}((function () {
if (typeof window !== 'undefined') {
return window;
}
if (typeof self !== 'undefined') {
return self;
}
return this || {};
})(), module, false));
canvas-confetti-1.5.1/test/ 0000775 0000000 0000000 00000000000 14200565716 0015567 5 ustar 00root root 0000000 0000000 canvas-confetti-1.5.1/test/.eslintrc.yml 0000664 0000000 0000000 00000000235 14200565716 0020213 0 ustar 00root root 0000000 0000000 extends:
- plugin:ava/recommended
env:
es6: true
node: true
browser: false
parser: babel-eslint
parserOptions:
sourceType: module
plugins:
- ava
canvas-confetti-1.5.1/test/index.test.js 0000664 0000000 0000000 00000065412 14200565716 0020222 0 ustar 00root root 0000000 0000000 import fs from 'fs';
import http from 'http';
import path from 'path';
import { promisify } from 'util';
import test from 'ava';
import puppeteer from 'puppeteer';
import send from 'send';
import root from 'rootrequire';
import jimp from 'jimp';
const PORT = 9999;
const width = 500;
const height = 500;
// Docker-based CIs need this disabled
// https://github.com/Quramy/puppeteer-example/blob/c28a5aa52fe3968c2d6cfca362ec28c36963be26/README.md#with-docker-based-ci-services
const args = process.env.CI ? [
'--no-sandbox', '--disable-setuid-sandbox'
] : [];
const headless = process.env.CI ? true :
process.env['CONFETTI_SHOW'] ? false : true;
const mkdir = async (dir) => {
return promisify(fs.mkdir)(dir)
.then(() => Promise.resolve())
.catch(err => {
if (err.code === 'EEXIST') {
return Promise.resolve();
}
return Promise.reject(err);
});
};
const testServer = (function startServer() {
let server;
return function () {
return new Promise((resolve) => {
if (server) {
return resolve(server);
}
server = http.createServer(function (req, res) {
var file = path.resolve(root, req.url.slice(1));
send(req, file).pipe(res);
}).listen(PORT, () => {
resolve(server);
});
});
};
}());
const testBrowser = (() => {
let browser;
return function () {
if (browser) {
return Promise.resolve(browser);
}
return puppeteer.launch({
headless,
args: [ '--disable-background-timer-throttling' ].concat(args)
}).then(thisBrowser => {
browser = thisBrowser;
return Promise.resolve(browser);
});
};
})();
const testPage = async () => {
const browser = await testBrowser();
const page = await browser.newPage();
await page.setViewport({ width, height });
// eslint-disable-next-line no-console
page.on('pageerror', err => console.error(err));
return page;
};
const fixturePage = async (urlPath = 'fixtures/page.html') => {
const page = await testPage();
await page.goto(`http://localhost:${PORT}/${urlPath}`);
return page;
};
// eslint-disable-next-line no-unused-vars
const sleep = (time) => {
return new Promise(resolve => {
setTimeout(() => resolve(), time);
});
};
const createBuffer = (data, format) => {
try {
return Buffer.from(data, format);
} catch(e) {
return new Buffer(data, format);
}
};
function confetti(opts, wait = false, funcName = 'confetti') {
return `
${wait ? '' : `${funcName}.Promise = null;`}
${funcName}(${opts ? JSON.stringify(opts) : ''});
`;
}
async function confettiImage(page, opts = {}, funcName = 'confetti') {
const base64png = await page.evaluate(`
${funcName}(${JSON.stringify(opts)});
new Promise(function (resolve, reject) {
setTimeout(function () {
var canvas = document.querySelector('canvas');
return resolve(canvas.toDataURL('image/png'));
}, 200);
});
`);
const imageData = base64png.replace(/data:image\/png;base64,/, '');
return createBuffer(imageData, 'base64');
}
function hex(n) {
const pad = (n) => {
while (n.length < 2) {
n = '0'+n;
}
return n;
};
return pad(n.toString(16));
}
const getImageBuffer = async (image) => {
return await promisify(image.getBuffer.bind(image))(jimp.MIME_PNG);
};
const readImage = async (buffer) => {
return Buffer.isBuffer(buffer) ? await jimp.read(buffer) : buffer;
};
const uniqueColors = async (buffer) => {
const image = await readImage(buffer);
const pixels = new Set();
image.scan(0, 0, image.bitmap.width, image.bitmap.height, (x, y, idx) => {
const r = image.bitmap.data[idx + 0];
const g = image.bitmap.data[idx + 1];
const b = image.bitmap.data[idx + 2];
pixels.add(`#${hex(r)}${hex(g)}${hex(b)}`);
});
return Array.from(pixels).sort();
};
const uniqueColorsBySide = async (buffer) => {
const image = await readImage(buffer);
const { width, height } = image.bitmap;
const leftImage = image.clone().crop(0, 0, width / 2, height);
const rightImage = image.clone().crop(width / 2, 0, width/2, height);
return {
left: await uniqueColors(leftImage),
right: await uniqueColors(rightImage)
};
};
const totalPixels = async(buffer) => {
const image = await readImage(buffer);
let pixels = 0;
image.scan(0, 0, image.bitmap.width, image.bitmap.height, (x, y, idx) => {
const r = image.bitmap.data[idx + 0];
const g = image.bitmap.data[idx + 1];
const b = image.bitmap.data[idx + 2];
if (r === 255 && g === 255 && b === 255) { return; }
pixels++;
});
return pixels;
};
const removeOpacity = async (buffer) => {
const image = await readImage(buffer);
image.rgba(false).background(0xFFFFFFFF);
var opaqueBuffer = await promisify(image.getBuffer.bind(image))(jimp.MIME_PNG);
return await jimp.read(opaqueBuffer);
};
const reduceImg = async (buffer, opaque = true) => {
const image = opaque ?
await removeOpacity(buffer) :
await readImage(buffer);
// basically dialate the crap out of everything
image.blur(2);
image.posterize(1);
return image;
};
const emptyImg = function (width, height) {
return new Promise((resolve, reject) => {
new jimp(width, height, (err, img) => {
if (err) {
return reject(err);
}
resolve(img);
});
});
};
test.before(async () => {
await mkdir('./shots');
await testServer();
await testBrowser();
});
test.after(async () => {
const browser = await testBrowser();
await browser.close();
const server = await testServer();
await new Promise(resolve => {
server.close(() => resolve());
});
});
// hack to get the status of a test, until AVA implements this
// https://github.com/avajs/ava/issues/840
test.beforeEach((t) => {
t.context.page = null;
t.context.passing = false;
});
test.afterEach((t) => {
t.context.passing = true;
});
test.afterEach.always(async t => {
if (t.context.page) {
await t.context.page.close();
}
if (t.context.passing && !process.env['CONFETTI_SHOW']) {
return;
}
// this is allowed, but still needs the eslint plugin to be updated
// https://github.com/avajs/eslint-plugin-ava/issues/176
// eslint-disable-next-line ava/use-t-well
const name = t.title.replace(/^afterEach\.always hook for /, '');
// save the raw buffer image, if one is present
if (t.context.buffer) {
await promisify(fs.writeFile)(`shots/${name}.original.png`, t.context.buffer);
}
// save the simplified/tested image, if one is present
if (t.context.image) {
await promisify(t.context.image.write.bind(t.context.image))(`shots/${name}.reduced.png`);
}
});
/*
* Image-based tests
*/
test('shoots default confetti', async t => {
const page = t.context.page = await fixturePage();
t.context.buffer = await confettiImage(page);
t.context.image = await reduceImg(t.context.buffer);
const pixels = await uniqueColors(t.context.image);
t.true(pixels.length >= 7);
t.true(pixels.length <= 8);
});
test('shoots red confetti', async t => {
const page = t.context.page = await fixturePage();
t.context.buffer = await confettiImage(page, {
colors: ['#ff0000']
});
t.context.image = await reduceImg(t.context.buffer);
const pixels = await uniqueColors(t.context.image);
t.deepEqual(pixels, ['#ff0000', '#ffffff']);
});
test('shoots blue confetti', async t => {
const page = t.context.page = await fixturePage();
t.context.buffer = await confettiImage(page, {
colors: ['#0000ff']
});
t.context.image = await reduceImg(t.context.buffer);
const pixels = await uniqueColors(t.context.image);
t.deepEqual(pixels, ['#0000ff', '#ffffff']);
});
test('shoots circle confetti', async t => {
const page = t.context.page = await fixturePage();
t.context.buffer = await confettiImage(page, {
colors: ['#0000ff'],
shapes: ['circle']
});
t.context.image = await reduceImg(t.context.buffer);
const pixels = await uniqueColors(t.context.image);
t.deepEqual(pixels, ['#0000ff', '#ffffff']);
});
test('shoots default scaled confetti', async t => {
const page = t.context.page = await fixturePage();
t.context.buffer = await confettiImage(page, {
colors: ['#0000ff'],
shapes: ['circle'],
particleCount: 10
});
t.context.image = await removeOpacity(t.context.buffer);
const pixels = await totalPixels(t.context.image);
t.is(pixels > 100 && pixels < 500, true);
});
test('shoots larger scaled confetti', async t => {
const page = t.context.page = await fixturePage();
t.context.buffer = await confettiImage(page, {
colors: ['#0000ff'],
shapes: ['circle'],
scalar: 10,
particleCount: 10
});
t.context.image = await removeOpacity(t.context.buffer);
const pixels = await totalPixels(t.context.image);
t.is(pixels > 2000, true);
});
test('shoots confetti to the left', async t => {
const page = t.context.page = await fixturePage();
t.context.buffer = await confettiImage(page, {
colors: ['#0000ff'],
particleCount: 100,
angle: 180,
startVelocity: 20
});
t.context.image = await reduceImg(t.context.buffer);
const pixels = await uniqueColorsBySide(t.context.image);
// left side has stuff on it
t.deepEqual(pixels.left, ['#0000ff', '#ffffff']);
// right side is all white
t.deepEqual(pixels.right, ['#ffffff']);
});
test('shoots confetti to the right', async t => {
const page = t.context.page = await fixturePage();
t.context.buffer = await confettiImage(page, {
colors: ['#0000ff'],
particleCount: 100,
angle: 0,
startVelocity: 20
});
t.context.image = await reduceImg(t.context.buffer);
const pixels = await uniqueColorsBySide(t.context.image);
// right side has stuff on it
t.deepEqual(pixels.right, ['#0000ff', '#ffffff']);
// left side is all white
t.deepEqual(pixels.left, ['#ffffff']);
});
/*
* Operational tests
*/
test('shoots confetti repeatedly using requestAnimationFrame', async t => {
const page = t.context.page = await fixturePage();
const time = 6 * 1000;
let opts = {
colors: ['#0000ff'],
origin: { y: 1 },
count: 1
};
// continuously animate more and more confetti
// for 10 seconds... that should be longer than
// this test... we won't wait for it anyway
page.evaluate(`
var opts = ${JSON.stringify(opts)};
var end = Date.now() + (${time});
(function frame() {
confetti(opts);
if (Date.now() < end) {
requestAnimationFrame(frame);
}
}());
`);
await sleep(time / 4);
const buff1 = await page.screenshot({ type: 'png' });
await sleep(time / 4);
const buff2 = await page.screenshot({ type: 'png' });
await sleep(time / 4);
const buff3 = await page.screenshot({ type: 'png' });
await sleep(time / 4);
const buff4 = await page.screenshot({ type: 'png' });
const img1 = await readImage(buff1);
const img2 = await readImage(buff2);
const img3 = await readImage(buff3);
const img4 = await readImage(buff4);
const { width, height } = img1.bitmap;
const comp = await emptyImg(width * 4, height);
await comp.composite(img1, 0, 0);
await comp.composite(img2, width, 0);
await comp.composite(img3, width * 2, 0);
await comp.composite(img4, width * 3, 0);
t.context.buffer = await getImageBuffer(comp);
t.context.image = await reduceImg(t.context.buffer);
t.deepEqual(await uniqueColors(await reduceImg(img1)), ['#0000ff', '#ffffff']);
t.deepEqual(await uniqueColors(await reduceImg(img2)), ['#0000ff', '#ffffff']);
t.deepEqual(await uniqueColors(await reduceImg(img3)), ['#0000ff', '#ffffff']);
t.deepEqual(await uniqueColors(await reduceImg(img4)), ['#0000ff', '#ffffff']);
});
test('uses promises when available', async t => {
const page = t.context.page = await fixturePage();
await page.evaluate(confetti({}, true));
t.context.buffer = await page.screenshot({ type: 'png' });
t.context.image = await reduceImg(t.context.buffer);
const pixels = await uniqueColors(t.context.image);
// make sure that all confetti have disappeared
t.deepEqual(pixels, ['#ffffff']);
});
test('removes the canvas when done', async t => {
const page = t.context.page = await fixturePage();
function hasCanvas() {
return page.evaluate(`!!document.querySelector('canvas')`);
}
// make sure there is no canvas before executing confetti
t.is(await hasCanvas(), false);
const promise = page.evaluate(confetti({}, true));
// confetti is running, make sure a canvas exists
t.is(await hasCanvas(), true);
await promise;
// confetti is done, canvas should be gone now
t.is(await hasCanvas(), false);
});
test('handles window resizes', async t => {
const time = 50;
const page = t.context.page = await fixturePage();
await page.setViewport({ width: width / 2, height });
let opts = {
colors: ['#0000ff'],
origin: { x: 1, y: 0 },
angle: 0,
startVelocity: 0,
particleCount: 2
};
// continuously animate more and more confetti
// for 10 seconds... that should be longer than
// this test... we won't wait for it anyway
page.evaluate(`
var opts = ${JSON.stringify(opts)};
var end = Date.now() + (10 * 1000);
var promise = confetti(opts);
var interval = setInterval(function() {
if (Date.now() > end) {
return clearInterval(interval);
}
confetti(opts);
}, ${time});
`);
await sleep(time * 4);
await page.setViewport({ width, height });
await sleep(time * 4);
t.context.buffer = await page.screenshot({ type: 'png' });
t.context.image = await reduceImg(t.context.buffer);
// chop this image into thirds
let widthThird = Math.floor(width / 3);
let first = t.context.image.clone().crop(widthThird * 0, 0, widthThird, height);
let second = t.context.image.clone().crop(widthThird * 1, 0, widthThird, height);
let third = t.context.image.clone().crop(widthThird * 2, 0, widthThird, height);
// the first will be white, the second and third will have confetti in them
t.deepEqual(await uniqueColors(first), ['#ffffff']);
t.deepEqual(await uniqueColors(second), ['#0000ff', '#ffffff']);
t.deepEqual(await uniqueColors(third), ['#0000ff', '#ffffff']);
});
test('stops and removes canvas immediately when `reset` is called', async t => {
const page = t.context.page = await fixturePage();
const promise = page.evaluate(`new Promise((resolve, reject) => {
const results = [];
results.push(!!document.querySelector('canvas'));
confetti().then(() => {
results.push('done');
});
results.push(!!document.querySelector('canvas'));
confetti.reset();
results.push(!!document.querySelector('canvas'));
resolve(results);
})`);
const results = await promise;
t.deepEqual(results, [false, true, false, 'done']);
});
/*
* Custom canvas
*/
const injectCanvas = async (page, opts = {}, createName = 'confetti.create') => {
const allowResize = 'allowResize' in opts ? opts.allowResize : true;
const useWorker = 'useWorker' in opts ? opts.useWorker : false;
await page.evaluate(`
var canvas = document.createElement('canvas');
canvas.style.width = '100%';
canvas.style.height = '100%';
document.body.appendChild(canvas);
window.myConfetti = ${createName}(canvas, {
resize: ${!!allowResize},
useWorker: ${!!useWorker}
});
`);
};
const getCanvasSize = async (page) => {
return await page.evaluate(`
var canvas = document.querySelector('canvas');
var size = { width: canvas.width, height: canvas.height };
Promise.resolve(size);
`);
};
test('can create instances of confetti in separate canvas', async t => {
const page = t.context.page = await fixturePage();
await injectCanvas(page);
const beforeSize = await getCanvasSize(page);
t.context.buffer = await confettiImage(page, {
colors: ['#ff0000']
}, 'myConfetti');
t.context.image = await reduceImg(t.context.buffer);
const afterSize = await getCanvasSize(page);
t.deepEqual(await uniqueColors(t.context.image), ['#ff0000', '#ffffff']);
t.notDeepEqual(beforeSize, afterSize);
});
test('can use a custom canvas without resizing', async t => {
const page = t.context.page = await fixturePage();
await injectCanvas(page, { allowResize: false });
const beforeSize = await getCanvasSize(page);
t.context.buffer = await confettiImage(page, {
colors: ['#ff0000'],
startVelocity: 2,
spread: 360,
origin: { y: 0 }
}, 'myConfetti');
t.context.image = await reduceImg(t.context.buffer);
const afterSize = await getCanvasSize(page);
t.deepEqual(await uniqueColors(t.context.image), ['#ff0000', '#ffffff']);
t.deepEqual(beforeSize, afterSize);
});
const resizeTest = async (t, createOpts, createName = 'confetti.create') => {
const time = 50;
const page = t.context.page = await fixturePage();
await page.setViewport({ width: width / 2, height });
let fireOpts = {
colors: ['#0000ff'],
origin: { x: 1, y: 0 },
angle: 0,
startVelocity: 0,
particleCount: 2
};
// continuously animate more and more confetti
// for 10 seconds... that should be longer than
// this test... we won't wait for it anyway
page.evaluate(`
var canvas = document.createElement('canvas');
canvas.style.width = '100%';
canvas.style.height = '100%';
document.body.appendChild(canvas);
var myConfetti = ${createName}(canvas, ${JSON.stringify(createOpts)});
var opts = ${JSON.stringify(fireOpts)};
var end = Date.now() + (10 * 1000);
var promise = myConfetti(opts);
var interval = setInterval(function() {
if (Date.now() > end) {
return clearInterval(interval);
}
myConfetti(opts);
}, ${time});
`);
await sleep(time * 4);
await page.setViewport({ width, height });
await sleep(time * 4);
t.context.buffer = await page.screenshot({ type: 'png' });
t.context.image = await reduceImg(t.context.buffer);
// chop this image into thirds
let widthThird = Math.floor(width / 3);
let first = t.context.image.clone().crop(widthThird * 0, 0, widthThird, height);
let second = t.context.image.clone().crop(widthThird * 1, 0, widthThird, height);
let third = t.context.image.clone().crop(widthThird * 2, 0, widthThird, height);
// the first will be white, the second and third will have confetti in them
t.deepEqual(await uniqueColors(first), ['#ffffff']);
t.deepEqual(await uniqueColors(second), ['#0000ff', '#ffffff']);
t.deepEqual(await uniqueColors(third), ['#0000ff', '#ffffff']);
};
test('resizes the custom canvas when the window resizes', async t => {
await resizeTest(t, {
resize: true
});
});
test('resizes the custom canvas when the window resizes and a worker is used', async t => {
await resizeTest(t, {
resize: true,
useWorker: true
});
});
test('can use a custom canvas with workers and resize it', async t => {
const page = t.context.page = await fixturePage();
await injectCanvas(page, {
allowResize: true,
useWorker: true
});
const beforeSize = await getCanvasSize(page);
t.context.buffer = await confettiImage(page, {
colors: ['#ff0000']
}, 'myConfetti');
t.context.image = await reduceImg(t.context.buffer);
const afterSize = await getCanvasSize(page);
t.deepEqual(await uniqueColors(t.context.image), ['#ff0000', '#ffffff']);
t.notDeepEqual(beforeSize, afterSize);
});
test('shoots confetti repeatedly in defaut and custom canvas using requestAnimationFrame', async t => {
const page = t.context.page = await fixturePage();
await injectCanvas(page);
const time = 6 * 1000;
let regular = {
colors: ['#0000ff'],
origin: { x: 0.2, y: 1 },
count: 1,
spread: 10
};
let custom = {
colors: ['#ff0000'],
origin: { x: 0.8, y: 1 },
count: 1,
spread: 10
};
// continuously animate more and more confetti
// for 10 seconds... that should be longer than
// this test... we won't wait for it anyway
page.evaluate(`
var regular = ${JSON.stringify(regular)};
var custom = ${JSON.stringify(custom)};
var end = Date.now() + (${time});
(function frame() {
confetti(regular);
myConfetti(custom);
if (Date.now() < end) {
requestAnimationFrame(frame);
}
}());
`);
await sleep(time / 4);
const buff1 = await page.screenshot({ type: 'png' });
await sleep(time / 4);
const buff2 = await page.screenshot({ type: 'png' });
await sleep(time / 4);
const buff3 = await page.screenshot({ type: 'png' });
await sleep(time / 4);
const buff4 = await page.screenshot({ type: 'png' });
const img1 = await readImage(buff1);
const img2 = await readImage(buff2);
const img3 = await readImage(buff3);
const img4 = await readImage(buff4);
const { width, height } = img1.bitmap;
const comp = await emptyImg(width * 4, height);
await comp.composite(img1, 0, 0);
await comp.composite(img2, width, 0);
await comp.composite(img3, width * 2, 0);
await comp.composite(img4, width * 3, 0);
t.context.buffer = await getImageBuffer(comp);
t.context.image = await reduceImg(t.context.buffer);
t.deepEqual(await uniqueColors(await reduceImg(img1)), ['#0000ff', '#ff0000', '#ffffff']);
t.deepEqual(await uniqueColors(await reduceImg(img2)), ['#0000ff', '#ff0000', '#ffffff']);
t.deepEqual(await uniqueColors(await reduceImg(img3)), ['#0000ff', '#ff0000', '#ffffff']);
t.deepEqual(await uniqueColors(await reduceImg(img4)), ['#0000ff', '#ff0000', '#ffffff']);
});
test('can initialize the same canvas multiple times when using a worker', async t => {
const page = t.context.page = await fixturePage();
await page.evaluate(`
var canvas = document.createElement('canvas');
canvas.id = 'testcanvas';
canvas.style.width = '100%';
canvas.style.height = '100%';
document.body.appendChild(canvas);
`);
await page.evaluate(`
var canvas = document.querySelector('#testcanvas');
var instance1 = confetti.create(canvas, { resize: true, useWorker: true });
`);
t.context.buffer = await confettiImage(page, {
colors: ['#ff0000'],
startVelocity: 2,
spread: 360,
}, 'instance1');
t.context.image = await reduceImg(t.context.buffer);
t.deepEqual(await uniqueColors(t.context.image), ['#ff0000', '#ffffff']);
await page.evaluate(`
var canvas = document.querySelector('#testcanvas');
var instance2 = confetti.create(canvas, { resize: true, useWorker: true });
`);
t.context.buffer = await confettiImage(page, {
colors: ['#ff00ff'],
startVelocity: 2,
spread: 360,
}, 'instance2');
t.context.image = await reduceImg(t.context.buffer);
// note: canvas is owned by the worker, so an existing animation will continue
t.deepEqual(await uniqueColors(t.context.image), ['#ff0000', '#ff00ff', '#ffffff']);
});
test('can initialize the same canvas multiple times without using a worker', async t => {
const page = t.context.page = await fixturePage();
await page.evaluate(`
var canvas = document.createElement('canvas');
canvas.id = 'testcanvas';
canvas.style.width = '100%';
canvas.style.height = '100%';
document.body.appendChild(canvas);
`);
await page.evaluate(`
var canvas = document.querySelector('#testcanvas');
var instance1 = confetti.create(canvas, { resize: true, useWorker: false });
`);
t.context.buffer = await confettiImage(page, {
colors: ['#ff0000'],
startVelocity: 2,
spread: 360,
}, 'instance1');
t.context.image = await reduceImg(t.context.buffer);
t.deepEqual(await uniqueColors(t.context.image), ['#ff0000', '#ffffff']);
await page.evaluate(`
var canvas = document.querySelector('#testcanvas');
var instance2 = confetti.create(canvas, { resize: true, useWorker: false });
`);
t.context.buffer = await confettiImage(page, {
colors: ['#ff00ff'],
startVelocity: 2,
spread: 360,
}, 'instance2');
t.context.image = await reduceImg(t.context.buffer);
// note: canvas can only be owned by one instance in the main thread,
// so the animation is reset
t.deepEqual(await uniqueColors(t.context.image), ['#ff00ff', '#ffffff']);
});
test('calling `reset` method clears all existing confetti but more can be launched after', async t => {
const page = t.context.page = await fixturePage();
await injectCanvas(page);
const prom1 = page.evaluate(confetti({ colors: ['#ff0000'] }, true, 'myConfetti'));
await sleep(50);
const img1 = await page.screenshot({ type: 'png' });
await Promise.all([
prom1,
page.evaluate(`myConfetti.reset();`)
]);
const img2 = await page.screenshot({ type: 'png' });
const prom2 = page.evaluate(confetti({ colors: ['#ff0000'] }, true, 'myConfetti'));
await sleep(50);
const img3 = await page.screenshot({ type: 'png' });
await prom2;
t.deepEqual(await uniqueColors(await reduceImg(img1)), ['#ff0000', '#ffffff']);
t.deepEqual(await uniqueColors(await reduceImg(img2)), ['#ffffff']);
t.deepEqual(await uniqueColors(await reduceImg(img3)), ['#ff0000', '#ffffff']);
});
/*
* Browserify tests
*/
test('works using the browserify bundle', async t => {
const page = t.context.page = await fixturePage('fixtures/page.browserify.html');
await page.evaluate(`void confetti({
colors: ['#00ff00'],
particleCount: 200,
spread: 270
})`);
await sleep(100);
t.context.buffer = await page.screenshot({ type: 'png' });
t.context.image = await reduceImg(t.context.buffer);
const pixels = await uniqueColors(t.context.image);
t.deepEqual(pixels, ['#00ff00', '#ffffff']);
});
/*
* Minified tests
* using minification close to that of jsDelivr
*/
test('works using the terser minified and compressed code', async t => {
const page = t.context.page = await fixturePage('fixtures/page.minified.html');
await page.evaluate(`void confetti({
colors: ['#ff00ff'],
particleCount: 200,
spread: 270
})`);
await sleep(100);
t.context.buffer = await page.screenshot({ type: 'png' });
t.context.image = await reduceImg(t.context.buffer);
const pixels = await uniqueColors(t.context.image);
t.deepEqual(pixels, ['#ff00ff', '#ffffff']);
});
/*
* ESM tests
*/
test('the esm module exposed confetti as the default', async t => {
const page = t.context.page = await fixturePage('fixtures/page.module.html');
t.context.buffer = await confettiImage(page, {
colors: ['#ff00ff']
}, 'confettiAlias');
t.context.buffer = await page.screenshot({ type: 'png' });
t.context.image = await reduceImg(t.context.buffer);
const pixels = await uniqueColors(t.context.image);
t.deepEqual(pixels, ['#ff00ff', '#ffffff']);
});
test('the esm module exposed confetti.create as create', async t => {
const page = t.context.page = await fixturePage('fixtures/page.module.html');
await injectCanvas(page, { allowResize: true }, 'createAlias');
t.context.buffer = await confettiImage(page, {
colors: ['#ff00ff']
}, 'myConfetti');
t.context.image = await reduceImg(t.context.buffer);
const pixels = await uniqueColors(t.context.image);
t.deepEqual(pixels, ['#ff00ff', '#ffffff']);
});
test('exposed confetti method has a `reset` property', async t => {
const page = t.context.page = await fixturePage('fixtures/page.module.html');
t.is(await page.evaluate(`typeof confettiAlias.reset`), 'function');
});
canvas-confetti-1.5.1/test/ssr.test.js 0000664 0000000 0000000 00000001152 14200565716 0017711 0 ustar 00root root 0000000 0000000 import vm from 'vm';
import path from 'path';
import fs from 'fs';
import { promisify } from 'util';
import test from 'ava';
import root from 'rootrequire';
import pkg from '../package.json';
test('can be evaluated in a node vm', async t => {
const file = await promisify(fs.readFile)(path.resolve(root, pkg.main), 'utf8');
t.is(typeof file, 'string');
const context = vm.createContext({ module: {} });
vm.runInContext(file, context);
t.is(typeof context.module.exports, 'function');
t.is(typeof context.module.exports.create, 'function');
t.is(typeof context.module.exports.reset, 'function');
});