@dg-scripts/webpack-template 0.0.0-1867
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.babelrc.json +7 -0
- package/.prettierrc.json +9 -0
- package/.stylelintrc.json +3 -0
- package/.test.env +1 -0
- package/CHANGELOG.md +349 -0
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/eslint.config.mjs +1 -0
- package/index.d.ts +60 -0
- package/jest.config.js +39 -0
- package/jest.env.setup.js +6 -0
- package/jest.setup.js +21 -0
- package/package.json +98 -0
- package/postcss.config.js +14 -0
- package/scripts/badge.ts +77 -0
- package/src/index.css +30 -0
- package/src/index.html +18 -0
- package/src/index.ts +5 -0
- package/src/particle/ExplodingParticle.ts +94 -0
- package/src/particle/ParticleFactory.ts +25 -0
- package/src/particle/ParticleSystem.ts +120 -0
- package/src/particle/__tests__/ExplodingParticle.test.ts +114 -0
- package/src/particle/__tests__/ParticleFactory.test.ts +30 -0
- package/src/particle/index.ts +1 -0
- package/tsconfig.json +38 -0
- package/webpack.config.js +235 -0
package/package.json
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dg-scripts/webpack-template",
|
|
3
|
+
"version": "0.0.0-1867",
|
|
4
|
+
"packageManager": "pnpm@8.15.5",
|
|
5
|
+
"description": "Minimal webpack boilerplate",
|
|
6
|
+
"author": "sabertazimi",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/sabertazimi/bod",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/sabertazimi/bod.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/sabertazimi/bod/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"webpack",
|
|
18
|
+
"boilerplate",
|
|
19
|
+
"template"
|
|
20
|
+
],
|
|
21
|
+
"main": "./src/index.js",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"browserslist": {
|
|
26
|
+
"production": [
|
|
27
|
+
">0.2%",
|
|
28
|
+
"not dead",
|
|
29
|
+
"not op_mini all"
|
|
30
|
+
],
|
|
31
|
+
"development": [
|
|
32
|
+
"last 1 chrome version",
|
|
33
|
+
"last 1 firefox version",
|
|
34
|
+
"last 1 safari version"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"badge": "ts-node scripts/badge.ts",
|
|
39
|
+
"build": "cross-env NODE_ENV=production webpack",
|
|
40
|
+
"dev": "cross-env NODE_ENV=development webpack serve",
|
|
41
|
+
"lint": "pnpm lint:style && pnpm lint:type-check",
|
|
42
|
+
"lint:style": "stylelint ./src/**/*.css && eslint ./src",
|
|
43
|
+
"lint:fix": "stylelint ./src/**/*.css --fix && eslint ./src --fix",
|
|
44
|
+
"lint:type-check": "tsc --noEmit",
|
|
45
|
+
"changeset": "commit-and-tag-version --dry-run -s",
|
|
46
|
+
"release": "commit-and-tag-version -s",
|
|
47
|
+
"start": "pnpm dev",
|
|
48
|
+
"test": "jest",
|
|
49
|
+
"test:watch": "jest --watch"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@babel/core": "^7.24.3",
|
|
53
|
+
"@babel/plugin-transform-class-properties": "^7.24.1",
|
|
54
|
+
"@babel/plugin-transform-object-rest-spread": "^7.24.1",
|
|
55
|
+
"@babel/preset-env": "^7.24.3",
|
|
56
|
+
"@dg-scripts/eslint-config": "0.0.0-1867",
|
|
57
|
+
"@dg-scripts/stylelint-config": "0.0.0-1867",
|
|
58
|
+
"@svgr/webpack": "^8.1.0",
|
|
59
|
+
"@types/jest": "^29.5.12",
|
|
60
|
+
"@types/node": "^20.12.2",
|
|
61
|
+
"babel-loader": "^9.1.3",
|
|
62
|
+
"cross-env": "^7.0.3",
|
|
63
|
+
"css-loader": "^6.10.0",
|
|
64
|
+
"css-minimizer-webpack-plugin": "^6.0.0",
|
|
65
|
+
"dotenv": "^16.4.5",
|
|
66
|
+
"eslint": "^8.57.0",
|
|
67
|
+
"eslint-webpack-plugin": "^4.1.0",
|
|
68
|
+
"file-loader": "^6.2.0",
|
|
69
|
+
"html-loader": "^5.0.0",
|
|
70
|
+
"html-webpack-plugin": "^5.6.0",
|
|
71
|
+
"jest": "^29.7.0",
|
|
72
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
73
|
+
"mini-css-extract-plugin": "^2.8.1",
|
|
74
|
+
"postcss": "^8.4.38",
|
|
75
|
+
"postcss-flexbugs-fixes": "^5.0.2",
|
|
76
|
+
"postcss-loader": "^8.1.1",
|
|
77
|
+
"postcss-preset-env": "^9.5.2",
|
|
78
|
+
"prettier": "^3.2.5",
|
|
79
|
+
"sass-loader": "^14.1.1",
|
|
80
|
+
"style-loader": "^3.3.4",
|
|
81
|
+
"stylelint": "^16.3.1",
|
|
82
|
+
"stylelint-webpack-plugin": "^5.0.0",
|
|
83
|
+
"ts-jest": "^29.1.2",
|
|
84
|
+
"ts-loader": "^9.5.1",
|
|
85
|
+
"ts-node": "^10.9.2",
|
|
86
|
+
"tsconfig-paths-webpack-plugin": "^4.1.0",
|
|
87
|
+
"tslib": "^2.6.2",
|
|
88
|
+
"typescript": "^5.4.3",
|
|
89
|
+
"undici": "^6.10.2",
|
|
90
|
+
"url-loader": "^4.1.1",
|
|
91
|
+
"webpack": "^5.91.0",
|
|
92
|
+
"webpack-bundle-analyzer": "^4.10.1",
|
|
93
|
+
"webpack-cli": "^5.1.4",
|
|
94
|
+
"webpack-dev-server": "^5.0.4",
|
|
95
|
+
"webpackbar": "^6.0.1"
|
|
96
|
+
},
|
|
97
|
+
"gitHead": "27b404c9bab635676f0939f84764ddc292fa681d"
|
|
98
|
+
}
|
package/scripts/badge.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import cp from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fetch } from 'undici';
|
|
5
|
+
|
|
6
|
+
const rootPath = path.join(__dirname, '..');
|
|
7
|
+
const SummaryFilePath = path.join(rootPath, 'coverage/coverage-summary.json');
|
|
8
|
+
const OutputBadgePath = path.join(rootPath, 'dist');
|
|
9
|
+
const CoverageType = ['statements', 'branches', 'functions', 'lines'];
|
|
10
|
+
const BadgeStyle = [
|
|
11
|
+
'for-the-badge',
|
|
12
|
+
'flat',
|
|
13
|
+
'flat-square',
|
|
14
|
+
'plastic',
|
|
15
|
+
'social',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const getCoveragePercentage = (
|
|
19
|
+
summaryFilePath: string,
|
|
20
|
+
coverageType: string
|
|
21
|
+
) => {
|
|
22
|
+
try {
|
|
23
|
+
const summary = fs.readFileSync(summaryFilePath, 'utf8');
|
|
24
|
+
return JSON.parse(summary).total[coverageType].pct;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error instanceof Error) console.error(error.message);
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getBadgeColor = (percentage: number) => {
|
|
32
|
+
if (percentage < 80) return 'red';
|
|
33
|
+
if (percentage < 90) return 'yellow';
|
|
34
|
+
return 'brightgreen';
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const getBadgeUrl = (
|
|
38
|
+
summaryFilePath: string,
|
|
39
|
+
coverageType: string,
|
|
40
|
+
badgeStyle: string
|
|
41
|
+
) => {
|
|
42
|
+
const percentage = getCoveragePercentage(summaryFilePath, coverageType);
|
|
43
|
+
const coverage = `${percentage}${encodeURI('%')}`;
|
|
44
|
+
const color = getBadgeColor(percentage);
|
|
45
|
+
const url = `https://img.shields.io/badge/${coverageType}-${coverage}-${color}?logo=jest&style=${badgeStyle}`;
|
|
46
|
+
return url;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const downloadBadgeFile = async (url: string) => {
|
|
50
|
+
const response = await fetch(url);
|
|
51
|
+
const data = await response.text();
|
|
52
|
+
return data;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const generateCoverageFile = async (
|
|
56
|
+
summaryFilePath: string,
|
|
57
|
+
coverageType: string,
|
|
58
|
+
badgeStyle: string,
|
|
59
|
+
outputDir: string
|
|
60
|
+
) => {
|
|
61
|
+
cp.spawnSync('mkdir', ['-p', outputDir]);
|
|
62
|
+
const badgeUrl = getBadgeUrl(summaryFilePath, coverageType, badgeStyle);
|
|
63
|
+
const output = path.join(outputDir, `coverage-${coverageType}.svg`);
|
|
64
|
+
const file = await downloadBadgeFile(badgeUrl);
|
|
65
|
+
fs.writeFileSync(output, file, { encoding: 'utf8' });
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const main = () => {
|
|
69
|
+
generateCoverageFile(
|
|
70
|
+
SummaryFilePath,
|
|
71
|
+
CoverageType[3],
|
|
72
|
+
BadgeStyle[0],
|
|
73
|
+
OutputBadgePath
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
main();
|
package/src/index.css
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
@import url('https://fonts.googleapis.com/css?family=Raleway:400,500');
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--color-primary: #7048e8;
|
|
5
|
+
--color-secondary: #495057;
|
|
6
|
+
--color-info: #1c7ed6;
|
|
7
|
+
--color-success: #37b24d;
|
|
8
|
+
--color-warning: #f59f00;
|
|
9
|
+
--color-danger: #f03e3e;
|
|
10
|
+
--font-stack: 'Raleway', 'HelveticaNeue', 'Helvetica Neue', helvetica,
|
|
11
|
+
'Open Sans', arial, sans-serif, serif;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
justify-content: center;
|
|
19
|
+
width: 100%;
|
|
20
|
+
min-height: 100vh;
|
|
21
|
+
padding: 0;
|
|
22
|
+
margin: 0 auto;
|
|
23
|
+
font-family: var(--font-stack);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
*,
|
|
27
|
+
*::before,
|
|
28
|
+
*::after {
|
|
29
|
+
box-sizing: inherit;
|
|
30
|
+
}
|
package/src/index.html
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta
|
|
6
|
+
name="viewport"
|
|
7
|
+
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
|
8
|
+
/>
|
|
9
|
+
<meta name="theme-color" content="#000000" />
|
|
10
|
+
<title>Boilerplate</title>
|
|
11
|
+
</head>
|
|
12
|
+
|
|
13
|
+
<body>
|
|
14
|
+
<noscript> You need to enable JavaScript to run this app. </noscript>
|
|
15
|
+
<div>Click blank space to spawn particles.</div>
|
|
16
|
+
<canvas id="canvas"></canvas>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
interface Speed {
|
|
2
|
+
x: number
|
|
3
|
+
y: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface ParticleProps {
|
|
7
|
+
x?: number
|
|
8
|
+
y?: number
|
|
9
|
+
color?: number[]
|
|
10
|
+
duration?: number
|
|
11
|
+
speed?: Speed
|
|
12
|
+
radius?: number
|
|
13
|
+
life?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class ExplodingParticle {
|
|
17
|
+
startX: number
|
|
18
|
+
startY: number
|
|
19
|
+
color: number[]
|
|
20
|
+
speed: Speed
|
|
21
|
+
radius: number
|
|
22
|
+
startTime: number
|
|
23
|
+
animationDuration: number
|
|
24
|
+
life: number
|
|
25
|
+
remainingLife: number
|
|
26
|
+
|
|
27
|
+
static getDefaultProps(): ParticleProps {
|
|
28
|
+
const defaultProps: ParticleProps = {
|
|
29
|
+
x: 0,
|
|
30
|
+
y: 0,
|
|
31
|
+
color: [156, 39, 176],
|
|
32
|
+
duration: 1000,
|
|
33
|
+
speed: {
|
|
34
|
+
x: -5 + Math.random() * 10,
|
|
35
|
+
y: -5 + Math.random() * 10,
|
|
36
|
+
},
|
|
37
|
+
radius: 5 + Math.random() * 5,
|
|
38
|
+
life: 30 + Math.random() * 10,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return defaultProps
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
constructor(props?: ParticleProps) {
|
|
45
|
+
const defaultProps = ExplodingParticle.getDefaultProps()
|
|
46
|
+
const { x, y, color, duration, speed, radius, life } = {
|
|
47
|
+
...defaultProps,
|
|
48
|
+
...props,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.startX = x as number
|
|
52
|
+
this.startY = y as number
|
|
53
|
+
this.color = color as number[]
|
|
54
|
+
|
|
55
|
+
// Speed
|
|
56
|
+
this.speed = speed as Speed
|
|
57
|
+
|
|
58
|
+
// Size particle
|
|
59
|
+
this.radius = radius as number
|
|
60
|
+
|
|
61
|
+
// Set how long particle to animate for X ms
|
|
62
|
+
this.startTime = Date.now()
|
|
63
|
+
this.animationDuration = duration as number
|
|
64
|
+
|
|
65
|
+
// Set a max time to live for particle
|
|
66
|
+
this.life = life as number
|
|
67
|
+
this.remainingLife = this.life
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// This function will be called by animation logic
|
|
71
|
+
draw(ctx: CanvasRenderingContext2D): void {
|
|
72
|
+
const shouldDraw = this.radius > 0 && this.remainingLife > 0
|
|
73
|
+
|
|
74
|
+
if (shouldDraw) {
|
|
75
|
+
// Draw a circle at the current location
|
|
76
|
+
ctx.save()
|
|
77
|
+
ctx.beginPath()
|
|
78
|
+
ctx.arc(this.startX, this.startY, this.radius, 0, Math.PI * 2)
|
|
79
|
+
ctx.fillStyle = `rgba(${this.color[0]},${this.color[1]},${this.color[2]},1)`
|
|
80
|
+
ctx.fill()
|
|
81
|
+
ctx.closePath()
|
|
82
|
+
ctx.restore()
|
|
83
|
+
|
|
84
|
+
// Update the particle's location and life
|
|
85
|
+
this.startX += this.speed.x
|
|
86
|
+
this.startY += this.speed.y
|
|
87
|
+
this.radius -= 0.25
|
|
88
|
+
this.remainingLife--
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default ExplodingParticle
|
|
94
|
+
export type { Speed, ParticleProps }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ParticleProps } from './ExplodingParticle'
|
|
2
|
+
import ExplodingParticle from './ExplodingParticle'
|
|
3
|
+
|
|
4
|
+
class ParticleFactory {
|
|
5
|
+
particles: ExplodingParticle[]
|
|
6
|
+
constructor() {
|
|
7
|
+
this.particles = []
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
getParticles(): ExplodingParticle[] {
|
|
11
|
+
return this.particles
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
clearParticles(): void {
|
|
15
|
+
while (this.particles.length)
|
|
16
|
+
this.particles.pop()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
emit(particleProps?: ParticleProps): void {
|
|
20
|
+
const particle = new ExplodingParticle(particleProps)
|
|
21
|
+
this.particles.push(particle)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default ParticleFactory
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { ParticleProps } from './ExplodingParticle'
|
|
2
|
+
import ParticleFactory from './ParticleFactory'
|
|
3
|
+
|
|
4
|
+
class ParticleSystem {
|
|
5
|
+
factory: ParticleFactory
|
|
6
|
+
bufferCanvas: HTMLCanvasElement
|
|
7
|
+
buffer: CanvasRenderingContext2D
|
|
8
|
+
screenCanvas: HTMLCanvasElement
|
|
9
|
+
screen: CanvasRenderingContext2D
|
|
10
|
+
factor: number
|
|
11
|
+
|
|
12
|
+
constructor(factor = 20) {
|
|
13
|
+
this.factory = new ParticleFactory()
|
|
14
|
+
this.bufferCanvas = document.createElement('canvas')
|
|
15
|
+
this.buffer = this.bufferCanvas.getContext('2d') as CanvasRenderingContext2D
|
|
16
|
+
this.screenCanvas = document.getElementById('canvas') as HTMLCanvasElement
|
|
17
|
+
this.screen = this.screenCanvas.getContext('2d') as CanvasRenderingContext2D
|
|
18
|
+
this.factor = factor
|
|
19
|
+
|
|
20
|
+
this.init()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
init(): void {
|
|
24
|
+
this.resize()
|
|
25
|
+
this.initStyle()
|
|
26
|
+
this.handleClick()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
resize(): void {
|
|
30
|
+
// Size canvas
|
|
31
|
+
this.screenCanvas.width = window.innerWidth
|
|
32
|
+
this.screenCanvas.height = window.innerHeight
|
|
33
|
+
this.bufferCanvas.width = window.innerWidth
|
|
34
|
+
this.bufferCanvas.height = window.innerHeight
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
initStyle(): void {
|
|
38
|
+
// Position out canvas
|
|
39
|
+
this.screenCanvas.style.position = 'absolute'
|
|
40
|
+
this.screenCanvas.style.top = '0'
|
|
41
|
+
this.screenCanvas.style.left = '0'
|
|
42
|
+
|
|
43
|
+
// Make sure it's on top of other elements
|
|
44
|
+
this.screenCanvas.style.zIndex = '1001'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
handleClick(): void {
|
|
48
|
+
// bind click event to screen canvas
|
|
49
|
+
this.screenCanvas.addEventListener('click', (event) => {
|
|
50
|
+
const x = event.clientX
|
|
51
|
+
const y = event.clientY
|
|
52
|
+
const getRandomInt = (min: number, max: number) => () =>
|
|
53
|
+
Math.floor(Math.random() * (max - min)) + min
|
|
54
|
+
const getColor = getRandomInt(0, 255)
|
|
55
|
+
|
|
56
|
+
for (let count = this.factor; count > 0; --count) {
|
|
57
|
+
const color = [getColor(), getColor(), getColor()]
|
|
58
|
+
const speed = {
|
|
59
|
+
x: -5 + Math.random() * 10,
|
|
60
|
+
y: -5 + Math.random() * 10,
|
|
61
|
+
}
|
|
62
|
+
this.emit({ x, y, color, speed })
|
|
63
|
+
count--
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
emit({ x = 0, y = 0, color = [156, 39, 176], speed }: ParticleProps): void {
|
|
69
|
+
this.factory.emit({ x, y, color, speed })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
draw(): void {
|
|
73
|
+
// render particles to offscreen canvas
|
|
74
|
+
const particles = this.factory.getParticles()
|
|
75
|
+
|
|
76
|
+
particles.forEach((particle, index, particles) => {
|
|
77
|
+
particle.draw(this.buffer)
|
|
78
|
+
|
|
79
|
+
// Simple way to clean up if the last particle is done animating
|
|
80
|
+
if (index === particles.length - 1) {
|
|
81
|
+
const percent
|
|
82
|
+
= (Date.now() - particle.startTime) / particle.animationDuration
|
|
83
|
+
|
|
84
|
+
if (percent > 1)
|
|
85
|
+
this.factory.clearParticles()
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// render to screen canvas
|
|
90
|
+
this.screen.drawImage(this.bufferCanvas, 0, 0)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
clear(): void {
|
|
94
|
+
this.buffer.clearRect(
|
|
95
|
+
0,
|
|
96
|
+
0,
|
|
97
|
+
this.bufferCanvas.width,
|
|
98
|
+
this.bufferCanvas.height,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
this.screen.clearRect(
|
|
102
|
+
0,
|
|
103
|
+
0,
|
|
104
|
+
this.screenCanvas.width,
|
|
105
|
+
this.screenCanvas.height,
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
update(): void {
|
|
110
|
+
this.clear()
|
|
111
|
+
this.draw()
|
|
112
|
+
window.requestAnimationFrame(this.update.bind(this))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
start(): void {
|
|
116
|
+
window.requestAnimationFrame(this.update.bind(this))
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default ParticleSystem
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import ExplodingParticle from '../ExplodingParticle'
|
|
2
|
+
|
|
3
|
+
describe('explodingParticle', () => {
|
|
4
|
+
let mockContextFunctions: object
|
|
5
|
+
let mockContextProps: object
|
|
6
|
+
let mockContext: CanvasRenderingContext2D
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
mockContextFunctions = {
|
|
10
|
+
save: jest.fn(),
|
|
11
|
+
restore: jest.fn(),
|
|
12
|
+
beginPath: jest.fn(),
|
|
13
|
+
closePath: jest.fn(),
|
|
14
|
+
arc: jest.fn(),
|
|
15
|
+
fill: jest.fn(),
|
|
16
|
+
}
|
|
17
|
+
mockContextProps = {
|
|
18
|
+
fillStyle: '',
|
|
19
|
+
}
|
|
20
|
+
mockContext = {
|
|
21
|
+
...mockContextFunctions,
|
|
22
|
+
...mockContextProps,
|
|
23
|
+
} as unknown as CanvasRenderingContext2D
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
mockContextFunctions = {}
|
|
28
|
+
mockContextProps = {}
|
|
29
|
+
mockContext = {} as unknown as CanvasRenderingContext2D
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should get same x, y, color and duration with empty props', () => {
|
|
33
|
+
const particle = new ExplodingParticle()
|
|
34
|
+
const defaultProps = ExplodingParticle.getDefaultProps()
|
|
35
|
+
|
|
36
|
+
expect(particle.startX).toBe(defaultProps.x)
|
|
37
|
+
expect(particle.startY).toBe(defaultProps.y)
|
|
38
|
+
expect(particle.color).toStrictEqual(defaultProps.color)
|
|
39
|
+
expect(particle.animationDuration).toBe(defaultProps.duration)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should get random radius, speed and life with empty props', () => {
|
|
43
|
+
const particle = new ExplodingParticle()
|
|
44
|
+
const defaultProps = ExplodingParticle.getDefaultProps()
|
|
45
|
+
|
|
46
|
+
expect(particle.radius).not.toBe(defaultProps.radius)
|
|
47
|
+
expect(particle.speed).not.toStrictEqual(defaultProps.speed)
|
|
48
|
+
expect(particle.life).toBe(particle.remainingLife)
|
|
49
|
+
expect(particle.life).not.toBe(defaultProps.life)
|
|
50
|
+
expect(particle.remainingLife).not.toBe(defaultProps.life)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should move according to its speed', () => {
|
|
54
|
+
const particle = new ExplodingParticle({
|
|
55
|
+
x: 1,
|
|
56
|
+
y: 2,
|
|
57
|
+
speed: { x: 1, y: -2 },
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
expect(particle.startX).toBe(1)
|
|
61
|
+
expect(particle.startY).toBe(2)
|
|
62
|
+
particle.draw(mockContext)
|
|
63
|
+
expect(particle.startX).toBe(2)
|
|
64
|
+
expect(particle.startY).toBe(0)
|
|
65
|
+
particle.draw(mockContext)
|
|
66
|
+
expect(particle.startX).toBe(3)
|
|
67
|
+
expect(particle.startY).toBe(-2)
|
|
68
|
+
Object.values(mockContextFunctions).forEach((mockFunction) => {
|
|
69
|
+
expect(mockFunction).toHaveBeenCalledTimes(2)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should shrink radius when time passed', () => {
|
|
74
|
+
const particle = new ExplodingParticle({
|
|
75
|
+
radius: 1.25,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
expect(particle.radius).toBe(1.25)
|
|
79
|
+
particle.draw(mockContext)
|
|
80
|
+
expect(particle.radius).toBe(1)
|
|
81
|
+
particle.draw(mockContext)
|
|
82
|
+
expect(particle.radius).toBe(0.75)
|
|
83
|
+
particle.draw(mockContext)
|
|
84
|
+
expect(particle.radius).toBe(0.5)
|
|
85
|
+
Object.values(mockContextFunctions).forEach((mockFunction) => {
|
|
86
|
+
expect(mockFunction).toHaveBeenCalledTimes(3)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should disappear (freezed) when radius becomes zero', () => {
|
|
91
|
+
const particle = new ExplodingParticle({
|
|
92
|
+
x: 0,
|
|
93
|
+
y: 0,
|
|
94
|
+
speed: { x: 1, y: 1 },
|
|
95
|
+
radius: 0.25,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
expect(particle.startX).toBe(0)
|
|
99
|
+
expect(particle.startY).toBe(0)
|
|
100
|
+
expect(particle.radius).toBe(0.25)
|
|
101
|
+
expect(particle.speed).toStrictEqual({ x: 1, y: 1 })
|
|
102
|
+
particle.draw(mockContext)
|
|
103
|
+
expect(particle.startX).toBe(1)
|
|
104
|
+
expect(particle.startY).toBe(1)
|
|
105
|
+
expect(particle.radius).toBe(0)
|
|
106
|
+
particle.draw(mockContext)
|
|
107
|
+
expect(particle.startX).toBe(1)
|
|
108
|
+
expect(particle.startY).toBe(1)
|
|
109
|
+
expect(particle.radius).toBe(0)
|
|
110
|
+
Object.values(mockContextFunctions).forEach((mockFunction) => {
|
|
111
|
+
expect(mockFunction).toHaveBeenCalledTimes(1)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import ParticleFactory from '../ParticleFactory'
|
|
2
|
+
|
|
3
|
+
describe('particleFactory', () => {
|
|
4
|
+
it('should initial with empty particles array', () => {
|
|
5
|
+
const factory = new ParticleFactory()
|
|
6
|
+
expect(factory.getParticles()).toHaveLength(0)
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('should emit new particle toe particles array', () => {
|
|
10
|
+
const factory = new ParticleFactory()
|
|
11
|
+
|
|
12
|
+
expect(factory.getParticles()).toHaveLength(0)
|
|
13
|
+
factory.emit()
|
|
14
|
+
expect(factory.getParticles()).toHaveLength(1)
|
|
15
|
+
factory.emit()
|
|
16
|
+
expect(factory.getParticles()).toHaveLength(2)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should clear all particles when flush particles array', () => {
|
|
20
|
+
const factory = new ParticleFactory()
|
|
21
|
+
|
|
22
|
+
expect(factory.getParticles()).toHaveLength(0)
|
|
23
|
+
factory.emit()
|
|
24
|
+
factory.emit()
|
|
25
|
+
factory.emit()
|
|
26
|
+
expect(factory.getParticles()).toHaveLength(3)
|
|
27
|
+
factory.clearParticles()
|
|
28
|
+
expect(factory.getParticles()).toHaveLength(0)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ParticleSystem } from './ParticleSystem'
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es6",
|
|
4
|
+
"jsx": "react",
|
|
5
|
+
"lib": ["DOM", "ESNext", "ES2021"],
|
|
6
|
+
"baseUrl": "./",
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"moduleResolution": "Bundler",
|
|
9
|
+
"paths": {},
|
|
10
|
+
"allowJs": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"declaration": false,
|
|
13
|
+
"importHelpers": true,
|
|
14
|
+
"outDir": "./dist",
|
|
15
|
+
"removeComments": true,
|
|
16
|
+
"sourceMap": true,
|
|
17
|
+
"allowSyntheticDefaultImports": true,
|
|
18
|
+
"esModuleInterop": true,
|
|
19
|
+
"forceConsistentCasingInFileNames": true,
|
|
20
|
+
"isolatedModules": true,
|
|
21
|
+
"skipLibCheck": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["./src/**/*", "index.d.ts"],
|
|
24
|
+
"exclude": [
|
|
25
|
+
"node_modules",
|
|
26
|
+
"build",
|
|
27
|
+
"coverage",
|
|
28
|
+
"dist",
|
|
29
|
+
"typings"
|
|
30
|
+
],
|
|
31
|
+
"ts-node": {
|
|
32
|
+
"files": true,
|
|
33
|
+
"compilerOptions": {
|
|
34
|
+
"target": "es6",
|
|
35
|
+
"module": "commonjs"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|