@wolsok/thanos 16.0.1 → 17.0.15
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/CHANGELOG.md +59 -0
- package/README.md +52 -0
- package/eslint.config.mjs +64 -0
- package/jest.config.ts +23 -0
- package/ng-package.json +8 -0
- package/package.json +10 -23
- package/project.json +40 -0
- package/src/assets/how-to-be-funny.png.ts +4 -0
- package/{index.d.ts → src/index.ts} +4 -2
- package/src/lib/animation.state.ts +6 -0
- package/src/lib/create-ws-thanos-options.ts +12 -0
- package/src/lib/random-util.ts +24 -0
- package/src/lib/simplex-noise-options.ts +9 -0
- package/src/lib/simplex-noise.ts +240 -0
- package/src/lib/ws-thanos-options.token.spec.ts +19 -0
- package/src/lib/ws-thanos-options.token.ts +20 -0
- package/src/lib/ws-thanos.directive.spec.ts +175 -0
- package/src/lib/ws-thanos.directive.ts +70 -0
- package/src/lib/ws-thanos.options.ts +5 -0
- package/src/lib/ws-thanos.service.spec.ts +9 -0
- package/src/lib/ws-thanos.service.ts +387 -0
- package/src/test-setup.ts +9 -0
- package/tsconfig.json +32 -0
- package/tsconfig.lib.json +15 -0
- package/tsconfig.lib.prod.json +10 -0
- package/tsconfig.spec.json +10 -0
- package/esm2022/index.mjs +0 -10
- package/esm2022/lib/animation.state.mjs +0 -2
- package/esm2022/lib/create-ws-thanos-options.mjs +0 -9
- package/esm2022/lib/simplex-noise-options.mjs +0 -2
- package/esm2022/lib/simplex-noise.mjs +0 -194
- package/esm2022/lib/ws-thanos-options.token.mjs +0 -13
- package/esm2022/lib/ws-thanos.directive.mjs +0 -76
- package/esm2022/lib/ws-thanos.options.mjs +0 -2
- package/esm2022/lib/ws-thanos.service.mjs +0 -253
- package/esm2022/wolsok-thanos.mjs +0 -5
- package/index.d.ts.map +0 -1
- package/lib/animation.state.d.ts +0 -7
- package/lib/animation.state.d.ts.map +0 -1
- package/lib/create-ws-thanos-options.d.ts +0 -3
- package/lib/create-ws-thanos-options.d.ts.map +0 -1
- package/lib/simplex-noise-options.d.ts +0 -10
- package/lib/simplex-noise-options.d.ts.map +0 -1
- package/lib/simplex-noise.d.ts +0 -19
- package/lib/simplex-noise.d.ts.map +0 -1
- package/lib/ws-thanos-options.token.d.ts +0 -5
- package/lib/ws-thanos-options.token.d.ts.map +0 -1
- package/lib/ws-thanos.directive.d.ts +0 -36
- package/lib/ws-thanos.directive.d.ts.map +0 -1
- package/lib/ws-thanos.options.d.ts +0 -6
- package/lib/ws-thanos.options.d.ts.map +0 -1
- package/lib/ws-thanos.service.d.ts +0 -27
- package/lib/ws-thanos.service.d.ts.map +0 -1
- package/wolsok-thanos.d.ts.map +0 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Public API Surface of ws-thanos
|
|
3
|
+
*/
|
|
1
4
|
export * from './lib/ws-thanos.service';
|
|
2
5
|
export * from './lib/ws-thanos.directive';
|
|
3
6
|
export * from './lib/ws-thanos-options.token';
|
|
4
7
|
export * from './lib/ws-thanos.options';
|
|
5
8
|
export * from './lib/create-ws-thanos-options';
|
|
6
9
|
export * from './lib/simplex-noise';
|
|
7
|
-
export { AnimationState } from './lib/animation.state';
|
|
8
|
-
//# sourceMappingURL=index.d.ts.map
|
|
10
|
+
export type { AnimationState } from './lib/animation.state';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { WsThanosOptions } from './ws-thanos.options';
|
|
2
|
+
|
|
3
|
+
export function createWsThanosOptions(
|
|
4
|
+
options?: Partial<WsThanosOptions>
|
|
5
|
+
): WsThanosOptions {
|
|
6
|
+
return {
|
|
7
|
+
animationLength: 5000,
|
|
8
|
+
maxParticleCount: 400000,
|
|
9
|
+
particleAcceleration: 30,
|
|
10
|
+
...options,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const epsilon = 0.0000000001;
|
|
2
|
+
const two_pi = 2.0 * Math.PI;
|
|
3
|
+
let generate = true;
|
|
4
|
+
let z1: number;
|
|
5
|
+
|
|
6
|
+
export function gaussian(mu: number, sigma: number): number {
|
|
7
|
+
if (!generate) {
|
|
8
|
+
generate = true;
|
|
9
|
+
return z1 * sigma + mu;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let u1 = 0;
|
|
13
|
+
let u2 = 0;
|
|
14
|
+
while (u1 <= epsilon) {
|
|
15
|
+
u1 = Math.random();
|
|
16
|
+
u2 = Math.random();
|
|
17
|
+
generate = false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const sqrtLog = Math.sqrt(-2.0 * Math.log(u1));
|
|
21
|
+
const z0 = sqrtLog * Math.cos(two_pi * u2);
|
|
22
|
+
z1 = sqrtLog * Math.sin(two_pi * u2);
|
|
23
|
+
return z0 * sigma + mu;
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface SimplexNoiseOptions {
|
|
2
|
+
amplitude: number; // The base amplitude (default: 1.0)
|
|
3
|
+
frequency: number; // The base frequency (default: 1.0)
|
|
4
|
+
max: number; // The maximum scaled value to return (effective default: 1.0)
|
|
5
|
+
min: number; // The minimum scaled value to return (effective default: -1.0)
|
|
6
|
+
octaves: number; // Integer; the number of octaves to sum for noise generation (default: 1)
|
|
7
|
+
persistence: number; // The persistence of amplitude per octave (default: 0.5)
|
|
8
|
+
random: () => number; // A function that generates random values between 0 and 1 (default: Math.random)
|
|
9
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import type { SimplexNoiseOptions } from './simplex-noise-options';
|
|
2
|
+
|
|
3
|
+
export class SimplexNoise {
|
|
4
|
+
constructor(options?: Partial<SimplexNoiseOptions>) {
|
|
5
|
+
const { min, max, random, amplitude, frequency, octaves, persistence } =
|
|
6
|
+
createOptionsWithDefaults(options);
|
|
7
|
+
|
|
8
|
+
this.random = random;
|
|
9
|
+
this.amplitude = amplitude;
|
|
10
|
+
this.frequency = frequency;
|
|
11
|
+
this.octaves = octaves;
|
|
12
|
+
this.persistence = persistence;
|
|
13
|
+
|
|
14
|
+
this.scale =
|
|
15
|
+
min === -1 && max === 1
|
|
16
|
+
? (value) => value
|
|
17
|
+
: (value) => min + ((value + 1) / 2) * (max - min);
|
|
18
|
+
|
|
19
|
+
const p = new Uint8Array(256);
|
|
20
|
+
for (let i = 0; i < 256; i++) {
|
|
21
|
+
p[i] = i;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let n: number;
|
|
25
|
+
let q: number;
|
|
26
|
+
for (let i = 255; i > 0; i--) {
|
|
27
|
+
n = Math.floor((i + 1) * this.random());
|
|
28
|
+
q = p[i];
|
|
29
|
+
p[i] = p[n];
|
|
30
|
+
p[n] = q;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.perm = new Uint8Array(512);
|
|
34
|
+
this.permMod12 = new Uint8Array(512);
|
|
35
|
+
for (let i = 0; i < 512; i++) {
|
|
36
|
+
this.perm[i] = p[i & 255];
|
|
37
|
+
this.permMod12[i] = this.perm[i] % 12;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static G3 = 1.0 / 6.0;
|
|
42
|
+
|
|
43
|
+
static GRAD3D = [
|
|
44
|
+
[1, 1, 0],
|
|
45
|
+
[-1, 1, 0],
|
|
46
|
+
[1, -1, 0],
|
|
47
|
+
[-1, -1, 0],
|
|
48
|
+
[1, 0, 1],
|
|
49
|
+
[-1, 0, 1],
|
|
50
|
+
[1, 0, -1],
|
|
51
|
+
[-1, 0, -1],
|
|
52
|
+
[0, 1, 1],
|
|
53
|
+
[0, -1, -1],
|
|
54
|
+
[0, 1, -1],
|
|
55
|
+
[0, -1, -1],
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
readonly amplitude: number;
|
|
59
|
+
readonly frequency: number;
|
|
60
|
+
readonly octaves: number;
|
|
61
|
+
readonly perm: Uint8Array;
|
|
62
|
+
readonly permMod12: Uint8Array;
|
|
63
|
+
readonly persistence: number;
|
|
64
|
+
readonly random: () => number;
|
|
65
|
+
readonly scale: (value: number) => number;
|
|
66
|
+
private memScale3D: { [key: string]: number } = {};
|
|
67
|
+
|
|
68
|
+
dot(gs: number[], coords: number[]): number {
|
|
69
|
+
return gs
|
|
70
|
+
.slice(0, Math.min(gs.length, coords.length))
|
|
71
|
+
.reduce((total, g, i) => total + g * coords[i], 0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
raw3D(x: number, y: number, z: number): number {
|
|
75
|
+
// Skew the input space to determine which simplex cell we're in
|
|
76
|
+
const s = (x + y + z) / 3.0; // Very nice and simple skew factor for 3D
|
|
77
|
+
const i = Math.floor(x + s);
|
|
78
|
+
const j = Math.floor(y + s);
|
|
79
|
+
const k = Math.floor(z + s);
|
|
80
|
+
const t = (i + j + k) * SimplexNoise.G3;
|
|
81
|
+
const X0 = i - t; // Unskew the cell origin back to (x,y,z) space
|
|
82
|
+
const Y0 = j - t;
|
|
83
|
+
const Z0 = k - t;
|
|
84
|
+
const x0 = x - X0; // The x,y,z distances from the cell origin
|
|
85
|
+
const y0 = y - Y0;
|
|
86
|
+
const z0 = z - Z0;
|
|
87
|
+
|
|
88
|
+
// Determine which simplex we are in
|
|
89
|
+
let i1: number, j1: number, k1: number; // Offsets for second corner of simplex in (i,j,k) coords
|
|
90
|
+
let i2: number, j2: number, k2: number; // Offsets for third corner of simplex in (i,j,k) coords
|
|
91
|
+
if (x0 >= y0) {
|
|
92
|
+
if (y0 >= z0) {
|
|
93
|
+
i1 = i2 = j2 = 1;
|
|
94
|
+
j1 = k1 = k2 = 0;
|
|
95
|
+
} else if (x0 >= z0) {
|
|
96
|
+
i1 = i2 = k2 = 1;
|
|
97
|
+
j1 = k1 = j2 = 0;
|
|
98
|
+
} else {
|
|
99
|
+
k1 = i2 = k2 = 1;
|
|
100
|
+
i1 = j1 = j2 = 0;
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
if (y0 < z0) {
|
|
104
|
+
k1 = j2 = k2 = 1;
|
|
105
|
+
i1 = j1 = i2 = 0;
|
|
106
|
+
} else if (x0 < z0) {
|
|
107
|
+
j1 = j2 = k2 = 1;
|
|
108
|
+
i1 = k1 = i2 = 0;
|
|
109
|
+
} else {
|
|
110
|
+
j1 = i2 = j2 = 1;
|
|
111
|
+
i1 = k1 = k2 = 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const x1 = x0 - i1 + SimplexNoise.G3; // Offsets for second corner in (x,y,z) coords
|
|
116
|
+
const y1 = y0 - j1 + SimplexNoise.G3;
|
|
117
|
+
const z1 = z0 - k1 + SimplexNoise.G3;
|
|
118
|
+
const x2 = x0 - i2 + 2.0 * SimplexNoise.G3; // Offsets for third corner in (x,y,z) coords
|
|
119
|
+
const y2 = y0 - j2 + 2.0 * SimplexNoise.G3;
|
|
120
|
+
const z2 = z0 - k2 + 2.0 * SimplexNoise.G3;
|
|
121
|
+
const x3 = x0 - 1.0 + 3.0 * SimplexNoise.G3; // Offsets for last corner in (x,y,z) coords
|
|
122
|
+
const y3 = y0 - 1.0 + 3.0 * SimplexNoise.G3;
|
|
123
|
+
const z3 = z0 - 1.0 + 3.0 * SimplexNoise.G3;
|
|
124
|
+
|
|
125
|
+
// Work out the hashed gradient indices of the four simplex corners
|
|
126
|
+
const ii = i & 255;
|
|
127
|
+
const jj = j & 255;
|
|
128
|
+
const kk = k & 255;
|
|
129
|
+
const gi0 = this.permMod12[ii + this.perm[jj + this.perm[kk]]];
|
|
130
|
+
const gi1 =
|
|
131
|
+
this.permMod12[ii + i1 + this.perm[jj + j1 + this.perm[kk + k1]]];
|
|
132
|
+
const gi2 =
|
|
133
|
+
this.permMod12[ii + i2 + this.perm[jj + j2 + this.perm[kk + k2]]];
|
|
134
|
+
const gi3 = this.permMod12[ii + 1 + this.perm[jj + 1 + this.perm[kk + 1]]];
|
|
135
|
+
|
|
136
|
+
// Calculate the contribution from the four corners
|
|
137
|
+
const t0 = 0.5 - x0 * x0 - y0 * y0 - z0 * z0;
|
|
138
|
+
const n0 =
|
|
139
|
+
t0 < 0
|
|
140
|
+
? 0.0
|
|
141
|
+
: Math.pow(t0, 4) * this.dot(SimplexNoise.GRAD3D[gi0], [x0, y0, z0]);
|
|
142
|
+
const t1 = 0.5 - x1 * x1 - y1 * y1 - z1 * z1;
|
|
143
|
+
const n1 =
|
|
144
|
+
t1 < 0
|
|
145
|
+
? 0.0
|
|
146
|
+
: Math.pow(t1, 4) * this.dot(SimplexNoise.GRAD3D[gi1], [x1, y1, z1]);
|
|
147
|
+
const t2 = 0.5 - x2 * x2 - y2 * y2 - z2 * z2;
|
|
148
|
+
const n2 =
|
|
149
|
+
t2 < 0
|
|
150
|
+
? 0.0
|
|
151
|
+
: Math.pow(t2, 4) * this.dot(SimplexNoise.GRAD3D[gi2], [x2, y2, z2]);
|
|
152
|
+
const t3 = 0.5 - x3 * x3 - y3 * y3 - z3 * z3;
|
|
153
|
+
const n3 =
|
|
154
|
+
t3 < 0
|
|
155
|
+
? 0.0
|
|
156
|
+
: Math.pow(t3, 4) * this.dot(SimplexNoise.GRAD3D[gi3], [x3, y3, z3]);
|
|
157
|
+
|
|
158
|
+
// Add contributions from each corner to get the final noise value.
|
|
159
|
+
// The result is scaled to stay just inside [-1,1]
|
|
160
|
+
return 94.68493150681972 * (n0 + n1 + n2 + n3);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
scaled3D(x: number, y: number, z: number, resolution: number): number {
|
|
164
|
+
const memIndex = `${Math.round(x * resolution)}${Math.round(
|
|
165
|
+
y * resolution
|
|
166
|
+
)}${Math.round(z * resolution)}`;
|
|
167
|
+
const memElem = this.memScale3D[memIndex];
|
|
168
|
+
if (memElem != null) {
|
|
169
|
+
return memElem;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let amplitude = this.amplitude;
|
|
173
|
+
let frequency = this.frequency;
|
|
174
|
+
let maxAmplitude = 0;
|
|
175
|
+
let noise = 0;
|
|
176
|
+
|
|
177
|
+
for (let i = 0; i < this.octaves; i++) {
|
|
178
|
+
noise +=
|
|
179
|
+
this.raw3D(x * frequency, y * frequency, z * frequency) * amplitude;
|
|
180
|
+
maxAmplitude += amplitude;
|
|
181
|
+
amplitude *= this.persistence;
|
|
182
|
+
frequency *= 2;
|
|
183
|
+
}
|
|
184
|
+
const result = this.scale(noise / maxAmplitude);
|
|
185
|
+
this.memScale3D[memIndex] = result;
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function createOptionsWithDefaults(options?: Partial<SimplexNoiseOptions>) {
|
|
191
|
+
if (options != null) {
|
|
192
|
+
if (options['amplitude'] && typeof options.amplitude !== 'number') {
|
|
193
|
+
throw new Error('options.amplitude must be a number');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (options['frequency'] && typeof options.frequency !== 'number') {
|
|
197
|
+
throw new Error('options.frequency must be a number');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (
|
|
201
|
+
options['octaves'] &&
|
|
202
|
+
(typeof options.octaves !== 'number' ||
|
|
203
|
+
!isFinite(options.octaves) ||
|
|
204
|
+
Math.floor(options.octaves) !== options.octaves)
|
|
205
|
+
) {
|
|
206
|
+
throw new Error('options.octaves must be an integer');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (options['persistence'] && typeof options.persistence !== 'number') {
|
|
210
|
+
throw new Error('options.persistence must be a number');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (options['random'] && typeof options.random !== 'function') {
|
|
214
|
+
throw new Error('options.random must be a function');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (options['min'] && typeof options.min !== 'number') {
|
|
218
|
+
throw new Error('options.min must be a number');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (options['max'] && typeof options.max !== 'number') {
|
|
222
|
+
throw new Error('options.max must be a number');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const optionsResult = {
|
|
227
|
+
amplitude: 1.0,
|
|
228
|
+
frequency: 1.0,
|
|
229
|
+
max: 1.0,
|
|
230
|
+
min: -1.0,
|
|
231
|
+
octaves: 1,
|
|
232
|
+
persistence: 0.5,
|
|
233
|
+
random: Math.random,
|
|
234
|
+
...options,
|
|
235
|
+
};
|
|
236
|
+
if (optionsResult.min > optionsResult.max) {
|
|
237
|
+
throw new Error('min must be smaller max');
|
|
238
|
+
}
|
|
239
|
+
return optionsResult;
|
|
240
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import {
|
|
3
|
+
provideWsThanosOptions,
|
|
4
|
+
WS_THANOS_OPTIONS_TOKEN,
|
|
5
|
+
} from './ws-thanos-options.token';
|
|
6
|
+
|
|
7
|
+
describe('WsThanosOptions', () => {
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
await TestBed.configureTestingModule({
|
|
10
|
+
providers: [provideWsThanosOptions({ maxParticleCount: 12345 })],
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should override default options', () => {
|
|
15
|
+
expect(TestBed.inject(WS_THANOS_OPTIONS_TOKEN).maxParticleCount).toEqual(
|
|
16
|
+
12345
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { InjectionToken, Provider } from '@angular/core';
|
|
2
|
+
import { createWsThanosOptions } from './create-ws-thanos-options';
|
|
3
|
+
import type { WsThanosOptions } from './ws-thanos.options';
|
|
4
|
+
|
|
5
|
+
export const WS_THANOS_OPTIONS_TOKEN = new InjectionToken<WsThanosOptions>(
|
|
6
|
+
'thanosOptions',
|
|
7
|
+
{
|
|
8
|
+
providedIn: 'root',
|
|
9
|
+
factory: () => createWsThanosOptions(),
|
|
10
|
+
}
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export function provideWsThanosOptions(
|
|
14
|
+
options?: Partial<WsThanosOptions>
|
|
15
|
+
): Provider {
|
|
16
|
+
return {
|
|
17
|
+
provide: WS_THANOS_OPTIONS_TOKEN,
|
|
18
|
+
useValue: createWsThanosOptions(options),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
QueryList,
|
|
5
|
+
ViewChildren,
|
|
6
|
+
} from '@angular/core';
|
|
7
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
8
|
+
import { combineLatest, firstValueFrom, of } from 'rxjs';
|
|
9
|
+
import image from '../assets/how-to-be-funny.png';
|
|
10
|
+
import { AnimationState } from './animation.state';
|
|
11
|
+
import { provideWsThanosOptions } from './ws-thanos-options.token';
|
|
12
|
+
import { WsThanosDirective } from './ws-thanos.directive';
|
|
13
|
+
import { WsThanosService } from './ws-thanos.service';
|
|
14
|
+
|
|
15
|
+
describe('Integration Test: WsThanosDirective', () => {
|
|
16
|
+
@Component({
|
|
17
|
+
template: `
|
|
18
|
+
<div
|
|
19
|
+
class="thanos-test-container"
|
|
20
|
+
wsThanos
|
|
21
|
+
(wsThanosComplete)="completed()"
|
|
22
|
+
>
|
|
23
|
+
<h1>My content for the div</h1>
|
|
24
|
+
<img alt="funny-face" style="height: 400px" src="${image}" />
|
|
25
|
+
</div>
|
|
26
|
+
<div class="grid-test">
|
|
27
|
+
<ws-thanos class="div-without-remove" #myThanos="thanos">
|
|
28
|
+
This div should be vaporized when clicked on button and then become
|
|
29
|
+
visible again!
|
|
30
|
+
<div class="inner">Inner container</div>
|
|
31
|
+
</ws-thanos>
|
|
32
|
+
<button (click)="myThanos.vaporize(false)">Vaporize div above</button>
|
|
33
|
+
</div>
|
|
34
|
+
`,
|
|
35
|
+
styles: [
|
|
36
|
+
`
|
|
37
|
+
* {
|
|
38
|
+
box-sizing: border-box;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.div-without-remove {
|
|
42
|
+
display: grid;
|
|
43
|
+
grid-template-rows: 1fr 1fr 1fr;
|
|
44
|
+
grid-template-columns: 1fr 1fr 1fr;
|
|
45
|
+
place-content: center;
|
|
46
|
+
padding: 50px;
|
|
47
|
+
border: 10px solid darkorange;
|
|
48
|
+
border-radius: 30px;
|
|
49
|
+
height: 300px;
|
|
50
|
+
background-image: linear-gradient(to right, #cbe7e1, #00ae85);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.inner {
|
|
54
|
+
border-radius: 50%;
|
|
55
|
+
grid-column: 2;
|
|
56
|
+
grid-row: 1;
|
|
57
|
+
background-image: linear-gradient(to right, azure, darkorchid);
|
|
58
|
+
text-align: center;
|
|
59
|
+
padding-top: 1rem;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.grid-test {
|
|
63
|
+
display: grid;
|
|
64
|
+
grid-template-columns: 1fr;
|
|
65
|
+
grid-template-rows: 1fr auto;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.thanos-test-container {
|
|
69
|
+
padding: 14px;
|
|
70
|
+
}
|
|
71
|
+
`,
|
|
72
|
+
],
|
|
73
|
+
imports: [WsThanosDirective],
|
|
74
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
75
|
+
})
|
|
76
|
+
class HostComponent {
|
|
77
|
+
wsThanosDirective!: WsThanosDirective;
|
|
78
|
+
|
|
79
|
+
@ViewChildren(WsThanosDirective)
|
|
80
|
+
set thanos(thanos: QueryList<WsThanosDirective>) {
|
|
81
|
+
this.wsThanosDirective = thanos.first;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
startThanos() {
|
|
85
|
+
return this.wsThanosDirective.vaporize().subscribe();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
completed() {
|
|
89
|
+
console.log('Completed');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let directive: WsThanosDirective;
|
|
94
|
+
let hostFixture: ComponentFixture<HostComponent>;
|
|
95
|
+
let hostComp: HostComponent;
|
|
96
|
+
|
|
97
|
+
beforeEach(async () => {
|
|
98
|
+
await TestBed.configureTestingModule({
|
|
99
|
+
imports: [WsThanosDirective, HostComponent],
|
|
100
|
+
providers: [
|
|
101
|
+
provideWsThanosOptions({
|
|
102
|
+
maxParticleCount: 500,
|
|
103
|
+
animationLength: 1000,
|
|
104
|
+
}),
|
|
105
|
+
],
|
|
106
|
+
}).compileComponents();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
hostFixture = TestBed.createComponent(HostComponent);
|
|
111
|
+
hostComp = hostFixture.componentInstance;
|
|
112
|
+
hostFixture.detectChanges();
|
|
113
|
+
directive = hostComp.wsThanosDirective;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should create', () => {
|
|
117
|
+
expect(directive).toBeTruthy();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('vaporize()', () => {
|
|
121
|
+
it('should call thanosService.vaporize', () => {
|
|
122
|
+
givenStubbedThanosService_VaporizeReturns();
|
|
123
|
+
whenVaporizeIsCalled();
|
|
124
|
+
thenThanosServiceVaporizeWasCalled();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should emit animationState', (done) => {
|
|
128
|
+
const animationState: AnimationState = {
|
|
129
|
+
animationT: 0,
|
|
130
|
+
deltaTSec: 0,
|
|
131
|
+
maxHeight: 0,
|
|
132
|
+
maxWidth: 0,
|
|
133
|
+
};
|
|
134
|
+
givenStubbedThanosService_VaporizeReturns(of(animationState));
|
|
135
|
+
directive.vaporize$(false).subscribe({
|
|
136
|
+
next: (result) => {
|
|
137
|
+
expect(result).toEqual(animationState);
|
|
138
|
+
done();
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// SKIPPED: This test requires real browser features (html2canvas, canvas rendering, getComputedStyle)
|
|
144
|
+
// that are not properly simulated in jsdom. See E2E tests in apps/ws-thanos-test-app instead.
|
|
145
|
+
it.skip('should emit complete when vaporize is complete', (done) => {
|
|
146
|
+
combineLatest([
|
|
147
|
+
firstValueFrom(directive.wsThanosComplete),
|
|
148
|
+
directive.vaporize$(),
|
|
149
|
+
]).subscribe({
|
|
150
|
+
complete: done,
|
|
151
|
+
});
|
|
152
|
+
}, 10000);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
function givenStubbedThanosService_VaporizeReturns(
|
|
156
|
+
vaporizeReturns: ReturnType<WsThanosService['vaporize']> = of(
|
|
157
|
+
{} as AnimationState
|
|
158
|
+
)
|
|
159
|
+
): void {
|
|
160
|
+
const thanosService = getWsThanosService();
|
|
161
|
+
jest.spyOn(thanosService, 'vaporize').mockReturnValue(vaporizeReturns);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function getWsThanosService(): WsThanosService {
|
|
165
|
+
return TestBed.inject(WsThanosService);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function thenThanosServiceVaporizeWasCalled() {
|
|
169
|
+
expect(getWsThanosService().vaporize).toHaveBeenCalled();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function whenVaporizeIsCalled(): void {
|
|
173
|
+
directive.vaporize$(false).subscribe();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Directive, ElementRef, Output, inject } from '@angular/core';
|
|
2
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
3
|
+
import { finalize, Observable, Subject, Subscription, take, tap } from 'rxjs';
|
|
4
|
+
import { AnimationState } from './animation.state';
|
|
5
|
+
import { WS_THANOS_OPTIONS_TOKEN } from './ws-thanos-options.token';
|
|
6
|
+
import type { WsThanosOptions } from './ws-thanos.options';
|
|
7
|
+
import { WsThanosService } from './ws-thanos.service';
|
|
8
|
+
|
|
9
|
+
@Directive({
|
|
10
|
+
selector: '[wsThanos], ws-thanos',
|
|
11
|
+
exportAs: 'thanos',
|
|
12
|
+
})
|
|
13
|
+
export class WsThanosDirective {
|
|
14
|
+
private wsThanosCompleteSubject = new Subject<void>();
|
|
15
|
+
private untilDestroyed = takeUntilDestroyed();
|
|
16
|
+
|
|
17
|
+
@Output()
|
|
18
|
+
public wsThanosComplete: Observable<void> =
|
|
19
|
+
this.wsThanosCompleteSubject.asObservable();
|
|
20
|
+
private subscriptions: Subscription = new Subscription();
|
|
21
|
+
|
|
22
|
+
private readonly vaporizeDomElem: ElementRef<HTMLElement> =
|
|
23
|
+
inject(ElementRef);
|
|
24
|
+
private readonly thanosService: WsThanosService = inject(WsThanosService);
|
|
25
|
+
private readonly thanosOptions: WsThanosOptions = inject(
|
|
26
|
+
WS_THANOS_OPTIONS_TOKEN
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
*
|
|
31
|
+
* Vaporize the dom element of this directive
|
|
32
|
+
* @param removeElem remove the vaporized dom element or blend back in after a while
|
|
33
|
+
*
|
|
34
|
+
* Subscribe to the returned observable to start vaporization
|
|
35
|
+
*
|
|
36
|
+
*/
|
|
37
|
+
public vaporize$(removeElem = true): Observable<AnimationState> {
|
|
38
|
+
const elem = this.vaporizeDomElem.nativeElement;
|
|
39
|
+
return this.thanosService.vaporize(elem).pipe(
|
|
40
|
+
tap({
|
|
41
|
+
error: (error) => {
|
|
42
|
+
console.error('Error vaporizing', error, {
|
|
43
|
+
elemTovaporize: elem,
|
|
44
|
+
removeElem,
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
finalize(() => {
|
|
49
|
+
if (removeElem) {
|
|
50
|
+
elem.remove();
|
|
51
|
+
} else {
|
|
52
|
+
// make visible again
|
|
53
|
+
elem.style.transition = 'opacity 700ms';
|
|
54
|
+
elem.style.opacity = '1';
|
|
55
|
+
}
|
|
56
|
+
this.wsThanosCompleteSubject.next();
|
|
57
|
+
})
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Vaporize the dom element of this directive
|
|
63
|
+
* @param removeElem remove the vaporized dom element or blend back in after a while
|
|
64
|
+
*
|
|
65
|
+
*/
|
|
66
|
+
public vaporize(removeElem = true): Observable<void> {
|
|
67
|
+
this.vaporize$(removeElem).pipe(this.untilDestroyed).subscribe();
|
|
68
|
+
return this.wsThanosComplete.pipe(take(1));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { WsThanosService } from './ws-thanos.service';
|
|
3
|
+
|
|
4
|
+
describe('WsThanosService', () => {
|
|
5
|
+
it('should be created', () => {
|
|
6
|
+
const service: WsThanosService = TestBed.inject(WsThanosService);
|
|
7
|
+
expect(service).toBeTruthy();
|
|
8
|
+
});
|
|
9
|
+
});
|