@newkrok/three-particles 0.4.1 → 0.6.2

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/README.md CHANGED
@@ -6,6 +6,10 @@ Particle system for ThreeJS
6
6
 
7
7
  You can create your own particle effects with it's editor https://github.com/NewKrok/three-particles-editor
8
8
 
9
+ # Video
10
+ - Projectiles: https://youtu.be/Q352JuxON04
11
+ - First preview: https://youtu.be/dtN_bndvoGU
12
+
9
13
  # Live demo
10
14
 
11
15
  https://newkrok.com/three-particles-editor/index.html
@@ -18,4 +22,4 @@ Install with npm
18
22
  `npm i @newkrok/three-particles`
19
23
 
20
24
  Add as a package.json dependency
21
- `"dependencies": { ... "@newkrok/three-particles": "0.4.0" ... }, `
25
+ `"dependencies": { ... "@newkrok/three-particles": "0.6.2" ... }, `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newkrok/three-particles",
3
- "version": "0.4.1",
3
+ "version": "0.6.2",
4
4
  "description": "Particle system for ThreeJS",
5
5
  "main": "src/js/three-particles.js",
6
6
  "bin": {
@@ -12,7 +12,10 @@
12
12
  },
13
13
  "keywords": [
14
14
  "threejs",
15
+ "particle",
15
16
  "particles",
17
+ "particle engine",
18
+ "effects",
16
19
  "3d",
17
20
  "lib"
18
21
  ],
@@ -24,7 +27,7 @@
24
27
  "homepage": "https://github.com/NewKrok/three-particles#readme",
25
28
  "dependencies": {
26
29
  "easing-functions": "1.0.1",
27
- "three": "0.136.0",
30
+ "three": "0.137.0",
28
31
  "three-noise": "^1.1.1"
29
32
  },
30
33
  "scripts": {
@@ -0,0 +1,36 @@
1
+ const nCr = (n, k) => {
2
+ let z = 1;
3
+ for (let i = 1; i <= k; i++) z *= (n + 1 - i) / i;
4
+ return z;
5
+ };
6
+
7
+ export const createBezierCurveFunction = (bezierPoints) => (percentage) => {
8
+ if (percentage < 0) return bezierPoints[0];
9
+ if (percentage > 1) return bezierPoints[bezierPoints.length - 1];
10
+
11
+ let start = 0;
12
+ let stop = bezierPoints.length - 1;
13
+
14
+ bezierPoints.find((point, index) => {
15
+ const result = percentage < point.percentage;
16
+ if (result) stop = index;
17
+ else if (point.percentage !== undefined) start = index;
18
+ return result;
19
+ });
20
+
21
+ const n = stop - start;
22
+ const calculatedPercentage =
23
+ (percentage - bezierPoints[start].percentage) /
24
+ (bezierPoints[stop].percentage - bezierPoints[start].percentage);
25
+
26
+ let value = 0;
27
+ for (let i = 0; i <= n; i++) {
28
+ const p = bezierPoints[start + i];
29
+ const c =
30
+ nCr(n, i) *
31
+ Math.pow(1 - calculatedPercentage, n - i) *
32
+ Math.pow(calculatedPercentage, i);
33
+ value += c * p.y;
34
+ }
35
+ return value;
36
+ };
@@ -1,6 +1,7 @@
1
1
  import Easing from "easing-functions";
2
2
 
3
3
  export const CurveFunction = {
4
+ BEZIER: "BEZIER",
4
5
  LINEAR: "LINEAR",
5
6
  QUADRATIC_IN: "QUADRATIC_IN",
6
7
  QUADRATIC_OUT: "QUADRATIC_OUT",
@@ -1,7 +1,6 @@
1
- import * as THREE from "three/build/three.module.js";
1
+ import * as THREE from "three";
2
2
 
3
3
  import { getCurveFunction } from "./three-particles-curves.js";
4
- import { size } from "lodash";
5
4
 
6
5
  const noiseInput = new THREE.Vector3(0, 0, 0);
7
6
 
@@ -1,4 +1,6 @@
1
- import * as THREE from "three/build/three.module.js";
1
+ import * as THREE from "three";
2
+
3
+ import { EmitFrom } from "../three-particles.js";
2
4
 
3
5
  export const patchObject = (
4
6
  objectA,
@@ -8,7 +10,12 @@ export const patchObject = (
8
10
  const result = {};
9
11
  Object.keys(objectA).forEach((key) => {
10
12
  if (!config.skippedProperties || !config.skippedProperties.includes(key)) {
11
- if (typeof objectA[key] === "object" && objectA[key] && objectB[key]) {
13
+ if (
14
+ typeof objectA[key] === "object" &&
15
+ objectA[key] &&
16
+ objectB[key] &&
17
+ !Array.isArray(objectA[key])
18
+ ) {
12
19
  result[key] = patchObject(objectA[key], objectB[key], config);
13
20
  } else {
14
21
  result[key] =
@@ -109,6 +116,54 @@ export const calculateRandomPositionAndVelocityOnCone = (
109
116
  );
110
117
  };
111
118
 
119
+ export const calculateRandomPositionAndVelocityOnBox = (
120
+ position,
121
+ velocity,
122
+ startSpeed,
123
+ { scale, emitFrom }
124
+ ) => {
125
+ switch (emitFrom) {
126
+ case EmitFrom.VOLUME:
127
+ position.x = Math.random() * scale.x - scale.x / 2;
128
+ position.y = Math.random() * scale.y - scale.y / 2;
129
+ position.z = Math.random() * scale.z - scale.z / 2;
130
+ break;
131
+
132
+ case EmitFrom.SHELL:
133
+ const side = Math.floor(Math.random() * 6);
134
+ const perpendicularAxis = side % 3;
135
+ const shellResult = [];
136
+ shellResult[perpendicularAxis] = side > 2 ? 1 : 0;
137
+ shellResult[(perpendicularAxis + 1) % 3] = Math.random();
138
+ shellResult[(perpendicularAxis + 2) % 3] = Math.random();
139
+ position.x = shellResult[0] * scale.x - scale.x / 2;
140
+ position.y = shellResult[1] * scale.y - scale.y / 2;
141
+ position.z = shellResult[2] * scale.z - scale.z / 2;
142
+ break;
143
+
144
+ case EmitFrom.EDGE:
145
+ const side2 = Math.floor(Math.random() * 6);
146
+ const perpendicularAxis2 = side2 % 3;
147
+ const edge = Math.floor(Math.random() * 4);
148
+ const edgeResult = [];
149
+ edgeResult[perpendicularAxis2] = side2 > 2 ? 1 : 0;
150
+ edgeResult[(perpendicularAxis2 + 1) % 3] =
151
+ edge < 2 ? Math.random() : edge - 2;
152
+ edgeResult[(perpendicularAxis2 + 2) % 3] =
153
+ edge < 2 ? edge : Math.random();
154
+ position.x = edgeResult[0] * scale.x - scale.x / 2;
155
+ position.y = edgeResult[1] * scale.y - scale.y / 2;
156
+ position.z = edgeResult[2] * scale.z - scale.z / 2;
157
+ break;
158
+ }
159
+
160
+ const randomizedSpeed = THREE.MathUtils.randFloat(
161
+ startSpeed.min,
162
+ startSpeed.max
163
+ );
164
+ velocity.set(0, 0, randomizedSpeed);
165
+ };
166
+
112
167
  export const calculateRandomPositionAndVelocityOnCircle = (
113
168
  position,
114
169
  velocity,
@@ -1,6 +1,7 @@
1
- import * as THREE from "three/build/three.module.js";
1
+ import * as THREE from "three";
2
2
 
3
3
  import {
4
+ calculateRandomPositionAndVelocityOnBox,
4
5
  calculateRandomPositionAndVelocityOnCircle,
5
6
  calculateRandomPositionAndVelocityOnCone,
6
7
  calculateRandomPositionAndVelocityOnRectangle,
@@ -13,6 +14,7 @@ import { FBM } from "three-noise/build/three-noise.module.js";
13
14
  import ParticleSystemFragmentShader from "./three-particles/shaders/particle-system-fragment-shader.glsl.js";
14
15
  import ParticleSystemVertexShader from "./three-particles/shaders/particle-system-vertex-shader.glsl.js";
15
16
  import { applyModifiers } from "./three-particles/three-particles-modifiers.js";
17
+ import { createBezierCurveFunction } from "./three-particles/three-particles-bezier";
16
18
 
17
19
  let createdParticleSystems = [];
18
20
 
@@ -29,6 +31,12 @@ export const Shape = {
29
31
  RECTANGLE: "RECTANGLE",
30
32
  };
31
33
 
34
+ export const EmitFrom = {
35
+ VOLUME: "VOLUME",
36
+ SHELL: "SHELL",
37
+ EDGE: "EDGE",
38
+ };
39
+
32
40
  export const TimeMode = {
33
41
  LIFETIME: "LIFETIME",
34
42
  FPS: "FPS",
@@ -92,6 +100,10 @@ const DEFAULT_PARTICLE_SYSTEM_CONFIG = {
92
100
  rotation: { x: 0.0, y: 0.0 }, // TODO: add z rotation
93
101
  scale: { x: 1.0, y: 1.0 },
94
102
  },
103
+ box: {
104
+ scale: { x: 1.0, y: 1.0, z: 1.0 },
105
+ emitFrom: EmitFrom.VOLUME,
106
+ },
95
107
  },
96
108
  map: null,
97
109
  renderer: {
@@ -115,7 +127,11 @@ const DEFAULT_PARTICLE_SYSTEM_CONFIG = {
115
127
  },
116
128
  sizeOverLifetime: {
117
129
  isActive: false,
118
- curveFunction: CurveFunction.LINEAR,
130
+ curveFunction: CurveFunction.BEZIER,
131
+ bezierPoints: [
132
+ { x: 0, y: 0, percentage: 0 },
133
+ { x: 1, y: 1, percentage: 1 },
134
+ ],
119
135
  },
120
136
  /* colorOverLifetime: {
121
137
  isActive: false,
@@ -123,7 +139,11 @@ const DEFAULT_PARTICLE_SYSTEM_CONFIG = {
123
139
  }, */
124
140
  opacityOverLifetime: {
125
141
  isActive: false,
126
- curveFunction: CurveFunction.LINEAR,
142
+ curveFunction: CurveFunction.BEZIER,
143
+ bezierPoints: [
144
+ { x: 0, y: 0, percentage: 0 },
145
+ { x: 1, y: 1, percentage: 1 },
146
+ ],
127
147
  },
128
148
  rotationOverLifetime: {
129
149
  isActive: false,
@@ -169,7 +189,7 @@ const createFloat32Attributes = ({
169
189
  };
170
190
 
171
191
  const calculatePositionAndVelocity = (
172
- { shape, sphere, cone, circle, rectangle },
192
+ { shape, sphere, cone, circle, rectangle, box },
173
193
  startSpeed,
174
194
  position,
175
195
  velocity,
@@ -211,6 +231,15 @@ const calculatePositionAndVelocity = (
211
231
  rectangle
212
232
  );
213
233
  break;
234
+
235
+ case Shape.BOX:
236
+ calculateRandomPositionAndVelocityOnBox(
237
+ position,
238
+ velocity,
239
+ startSpeed,
240
+ box
241
+ );
242
+ break;
214
243
  }
215
244
 
216
245
  if (velocityOverLifetime.isActive) {
@@ -249,6 +278,22 @@ export const createParticleSystem = (
249
278
  };
250
279
 
251
280
  const normalizedConfig = patchObject(DEFAULT_PARTICLE_SYSTEM_CONFIG, config);
281
+
282
+ const bezierCompatibleProperties = [
283
+ "sizeOverLifetime",
284
+ "opacityOverLifetime",
285
+ ];
286
+ bezierCompatibleProperties.forEach((key) => {
287
+ if (
288
+ normalizedConfig[key].isActive &&
289
+ normalizedConfig[key].curveFunction === CurveFunction.BEZIER &&
290
+ normalizedConfig[key].bezierPoints
291
+ )
292
+ normalizedConfig[key].curveFunction = createBezierCurveFunction(
293
+ normalizedConfig[key].bezierPoints
294
+ );
295
+ });
296
+
252
297
  const {
253
298
  transform,
254
299
  duration,
@@ -435,7 +480,7 @@ export const createParticleSystem = (
435
480
  geometry.attributes.colorA.needsUpdate = true;
436
481
  };
437
482
 
438
- const activateParticle = ({ particleIndex, activationTime }) => {
483
+ const activateParticle = ({ particleIndex, activationTime, position }) => {
439
484
  geometry.attributes.isActive.array[particleIndex] = true;
440
485
  generalData.creationTimes[particleIndex] = activationTime;
441
486
 
@@ -498,11 +543,12 @@ export const createParticleSystem = (
498
543
  );
499
544
  const positionIndex = Math.floor(particleIndex * 3);
500
545
  geometry.attributes.position.array[positionIndex] =
501
- startPositions[particleIndex].x;
546
+ (position ? position.x : 0) + startPositions[particleIndex].x;
502
547
  geometry.attributes.position.array[positionIndex + 1] =
503
- startPositions[particleIndex].y;
548
+ (position ? position.y : 0) + startPositions[particleIndex].y;
504
549
  geometry.attributes.position.array[positionIndex + 2] =
505
- startPositions[particleIndex].z;
550
+ (position ? position.z : 0) + startPositions[particleIndex].z;
551
+ geometry.attributes.position.needsUpdate = true;
506
552
 
507
553
  geometry.attributes.lifetime.array[particleIndex] = 0;
508
554
  geometry.attributes.lifetime.needsUpdate = true;
@@ -596,19 +642,22 @@ export const updateParticleSystems = ({ now, delta, elapsed }) => {
596
642
  gravityVelocity,
597
643
  } = generalData;
598
644
 
645
+ const lastWorldPositionSnapshot = { ...lastWorldPosition };
646
+
599
647
  const lifetime = now - creationTime;
600
648
  particleSystem.material.uniforms.elapsed.value = elapsed;
601
649
 
602
650
  particleSystem.getWorldPosition(currentWorldPosition);
603
- if (lastWorldPosition.x !== -99999)
651
+ if (lastWorldPosition.x !== -99999) {
604
652
  worldPositionChange.set(
605
653
  currentWorldPosition.x - lastWorldPosition.x,
606
654
  currentWorldPosition.y - lastWorldPosition.y,
607
655
  currentWorldPosition.z - lastWorldPosition.z
608
656
  );
657
+ worldPositionChange.applyQuaternion(worldQuaternion.invert());
658
+ }
609
659
  generalData.distanceFromLastEmitByDistance += worldPositionChange.length();
610
660
  particleSystem.getWorldPosition(lastWorldPosition);
611
-
612
661
  particleSystem.getWorldQuaternion(worldQuaternion);
613
662
  if (
614
663
  lastWorldQuaternion.x === -99999 ||
@@ -618,16 +667,12 @@ export const updateParticleSystems = ({ now, delta, elapsed }) => {
618
667
  ) {
619
668
  worldEuler.setFromQuaternion(worldQuaternion);
620
669
  lastWorldQuaternion.copy(worldQuaternion);
621
-
622
- const tempPosX = particleSystem.position.x;
623
- const tempPosY = particleSystem.position.y;
624
- const tempPosZ = particleSystem.position.z;
625
- gravityVelocity.set(0, gravity, 0);
626
- particleSystem.position.set(0, 0, 0);
627
- particleSystem.updateMatrixWorld();
670
+ gravityVelocity.set(
671
+ lastWorldPosition.x,
672
+ lastWorldPosition.y + gravity,
673
+ lastWorldPosition.z
674
+ );
628
675
  particleSystem.worldToLocal(gravityVelocity);
629
- particleSystem.position.set(tempPosX, tempPosY, tempPosZ);
630
- particleSystem.updateMatrixWorld();
631
676
  }
632
677
 
633
678
  generalData.creationTimes.forEach((entry, index) => {
@@ -648,11 +693,15 @@ export const updateParticleSystems = ({ now, delta, elapsed }) => {
648
693
  gravity !== 0 ||
649
694
  velocity.x !== 0 ||
650
695
  velocity.y !== 0 ||
651
- velocity.z !== 0
696
+ velocity.z !== 0 ||
697
+ worldPositionChange.x !== 0 ||
698
+ worldPositionChange.y !== 0 ||
699
+ worldPositionChange.z !== 0
652
700
  ) {
653
701
  const positionIndex = index * 3;
654
702
  const positionArr =
655
703
  particleSystem.geometry.attributes.position.array;
704
+
656
705
  if (simulationSpace === SimulationSpace.WORLD) {
657
706
  positionArr[positionIndex] -= worldPositionChange.x;
658
707
  positionArr[positionIndex + 1] -= worldPositionChange.y;
@@ -699,12 +748,28 @@ export const updateParticleSystems = ({ now, delta, elapsed }) => {
699
748
  (1 / emission.rateOverDistance)
700
749
  )
701
750
  : 0;
751
+ const distanceStep =
752
+ neededParticlesByDistance > 0
753
+ ? {
754
+ x:
755
+ (currentWorldPosition.x - lastWorldPositionSnapshot.x) /
756
+ neededParticlesByDistance,
757
+ y:
758
+ (currentWorldPosition.y - lastWorldPositionSnapshot.y) /
759
+ neededParticlesByDistance,
760
+ z:
761
+ (currentWorldPosition.z - lastWorldPositionSnapshot.z) /
762
+ neededParticlesByDistance,
763
+ }
764
+ : null;
702
765
  const neededParticles = neededParticlesByTime + neededParticlesByDistance;
703
766
 
704
- if (emission.rateOverDistance > 0 && neededParticlesByDistance >= 1)
767
+ if (emission.rateOverDistance > 0 && neededParticlesByDistance >= 1) {
705
768
  generalData.distanceFromLastEmitByDistance = 0;
769
+ }
706
770
 
707
771
  if (neededParticles > 0) {
772
+ let generatedParticlesByDistanceNeeds = 0;
708
773
  for (let i = 0; i < neededParticles; i++) {
709
774
  let particleIndex = -1;
710
775
  particleSystem.geometry.attributes.isActive.array.find(
@@ -723,7 +788,19 @@ export const updateParticleSystems = ({ now, delta, elapsed }) => {
723
788
  particleIndex <
724
789
  particleSystem.geometry.attributes.isActive.array.length
725
790
  ) {
726
- activateParticle({ particleIndex, activationTime: now });
791
+ let position;
792
+ if (generatedParticlesByDistanceNeeds < neededParticlesByDistance) {
793
+ position =
794
+ generatedParticlesByDistanceNeeds < neededParticlesByDistance
795
+ ? {
796
+ x: distanceStep.x * generatedParticlesByDistanceNeeds,
797
+ y: distanceStep.y * generatedParticlesByDistanceNeeds,
798
+ z: distanceStep.z * generatedParticlesByDistanceNeeds,
799
+ }
800
+ : null;
801
+ generatedParticlesByDistanceNeeds++;
802
+ }
803
+ activateParticle({ particleIndex, activationTime: now, position });
727
804
  props.lastEmissionTime = now;
728
805
  }
729
806
  }