@guinetik/gcanvas 1.0.0 → 1.0.1

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.
Files changed (68) hide show
  1. package/demos/fluid-simple.html +22 -0
  2. package/demos/fluid.html +37 -0
  3. package/demos/index.html +2 -0
  4. package/demos/js/blob.js +18 -5
  5. package/demos/js/fluid-simple.js +253 -0
  6. package/demos/js/fluid.js +527 -0
  7. package/demos/js/tde/accretiondisk.js +64 -11
  8. package/demos/js/tde/blackholescene.js +2 -2
  9. package/demos/js/tde/config.js +2 -2
  10. package/demos/js/tde/index.js +152 -27
  11. package/demos/js/tde/lensedstarfield.js +32 -25
  12. package/demos/js/tde/tdestar.js +78 -98
  13. package/demos/js/tde/tidalstream.js +23 -7
  14. package/docs/README.md +230 -222
  15. package/docs/api/FluidSystem.md +173 -0
  16. package/docs/concepts/architecture-overview.md +204 -204
  17. package/docs/concepts/rendering-pipeline.md +279 -279
  18. package/docs/concepts/two-layer-architecture.md +229 -229
  19. package/docs/fluid-dynamics.md +97 -0
  20. package/docs/getting-started/first-game.md +354 -354
  21. package/docs/getting-started/installation.md +175 -157
  22. package/docs/modules/collision/README.md +2 -2
  23. package/docs/modules/fluent/README.md +6 -6
  24. package/docs/modules/game/README.md +303 -303
  25. package/docs/modules/isometric-camera.md +2 -2
  26. package/docs/modules/isometric.md +1 -1
  27. package/docs/modules/painter/README.md +328 -328
  28. package/docs/modules/particle/README.md +3 -3
  29. package/docs/modules/shapes/README.md +221 -221
  30. package/docs/modules/shapes/base/euclidian.md +123 -123
  31. package/docs/modules/shapes/base/shape.md +262 -262
  32. package/docs/modules/shapes/base/transformable.md +243 -243
  33. package/docs/modules/state/README.md +2 -2
  34. package/docs/modules/util/README.md +1 -1
  35. package/docs/modules/util/camera3d.md +3 -3
  36. package/docs/modules/util/scene3d.md +1 -1
  37. package/package.json +3 -1
  38. package/readme.md +19 -5
  39. package/src/collision/collision.js +75 -0
  40. package/src/game/index.js +2 -1
  41. package/src/game/pipeline.js +3 -3
  42. package/src/game/systems/FluidSystem.js +835 -0
  43. package/src/game/systems/index.js +11 -0
  44. package/src/game/ui/button.js +39 -18
  45. package/src/game/ui/cursor.js +14 -0
  46. package/src/game/ui/fps.js +12 -4
  47. package/src/game/ui/index.js +2 -0
  48. package/src/game/ui/stepper.js +549 -0
  49. package/src/game/ui/theme.js +121 -0
  50. package/src/game/ui/togglebutton.js +9 -3
  51. package/src/game/ui/tooltip.js +11 -4
  52. package/src/math/fluid.js +507 -0
  53. package/src/math/index.js +2 -0
  54. package/src/mixins/anchor.js +17 -7
  55. package/src/motion/tweenetik.js +16 -0
  56. package/src/shapes/index.js +1 -0
  57. package/src/util/camera3d.js +218 -12
  58. package/types/fluent.d.ts +361 -0
  59. package/types/game.d.ts +303 -0
  60. package/types/index.d.ts +144 -5
  61. package/types/math.d.ts +361 -0
  62. package/types/motion.d.ts +271 -0
  63. package/types/particle.d.ts +373 -0
  64. package/types/shapes.d.ts +107 -9
  65. package/types/util.d.ts +353 -0
  66. package/types/webgl.d.ts +109 -0
  67. package/disk_example.png +0 -0
  68. package/tde.png +0 -0
@@ -1,229 +1,229 @@
1
- # Two-Layer Architecture
2
-
3
- > Understanding when to use the Shape Layer vs the Game Layer.
4
-
5
- ## Overview
6
-
7
- GCanvas provides two complementary approaches to building canvas applications:
8
-
9
- 1. **Shape Layer** - For declarative, static graphics
10
- 2. **Game Layer** - For interactive, dynamic applications
11
-
12
- You can use either layer independently or combine them for complex applications.
13
-
14
- ## Shape Layer
15
-
16
- The Shape Layer is for drawing graphics without a game loop. Shapes are self-contained objects that know how to render themselves.
17
-
18
- ### When to Use
19
-
20
- - Static visualizations
21
- - Data charts and graphs
22
- - Decorative graphics
23
- - Simple animations (manual redraw)
24
- - Learning canvas fundamentals
25
-
26
- ### Key Classes
27
-
28
- - `Shape` and subclasses (`Circle`, `Rectangle`, `Star`, etc.)
29
- - `Group` for composing shapes
30
- - `Painter` for direct canvas control
31
-
32
- ### Example
33
-
34
- ```js
35
- import { Circle, Rectangle, Group, Painter } from 'gcanvas';
36
-
37
- // Initialize Painter with canvas context
38
- const canvas = document.getElementById('canvas');
39
- Painter.init(canvas.getContext('2d'));
40
-
41
- // Create shapes
42
- const circle = new Circle(50, { x: 100, y: 100, color: 'red' });
43
- const rect = new Rectangle(80, 40, { x: 200, y: 100, color: 'blue' });
44
-
45
- // Draw immediately
46
- Painter.clear();
47
- circle.draw();
48
- rect.draw();
49
- ```
50
-
51
- ### Characteristics
52
-
53
- | Aspect | Shape Layer |
54
- |--------|-------------|
55
- | Loop | None (manual redraw) |
56
- | State | Shapes hold their own state |
57
- | Input | Manual event handling |
58
- | Animation | Call draw() on each frame manually |
59
- | Complexity | Simple, direct |
60
-
61
- ## Game Layer
62
-
63
- The Game Layer provides a complete game loop with automatic updates, rendering, and input handling.
64
-
65
- ### When to Use
66
-
67
- - Games and simulations
68
- - Interactive applications
69
- - Real-time animations
70
- - Complex state management
71
- - Multi-object coordination
72
-
73
- ### Key Classes
74
-
75
- - `Game` - Main loop and canvas management
76
- - `Pipeline` - Object lifecycle management
77
- - `GameObject` - Interactive entities
78
- - `Scene` - Hierarchical organization
79
-
80
- ### Example
81
-
82
- ```js
83
- import { Game, Scene, GameObject, Circle } from 'gcanvas';
84
-
85
- class Player extends GameObject {
86
- constructor(game) {
87
- super(game);
88
- this.shape = new Circle(40, { color: 'blue' });
89
- this.enableInteractivity(this.shape);
90
- }
91
-
92
- update(dt) {
93
- // Called every frame
94
- if (this.game.input.isKeyDown('ArrowRight')) {
95
- this.shape.x += 200 * dt;
96
- }
97
- }
98
-
99
- render() {
100
- // Called every frame after update
101
- this.shape.draw();
102
- }
103
-
104
- onPointerDown(e) {
105
- // Automatic input handling
106
- console.log('Clicked!');
107
- }
108
- }
109
-
110
- class MyGame extends Game {
111
- init() {
112
- this.enableFluidSize();
113
- const scene = new Scene(this);
114
- scene.add(new Player(this));
115
- this.pipeline.add(scene);
116
- }
117
- }
118
-
119
- const game = new MyGame(document.getElementById('canvas'));
120
- game.start();
121
- ```
122
-
123
- ### Characteristics
124
-
125
- | Aspect | Game Layer |
126
- |--------|-------------|
127
- | Loop | Automatic (requestAnimationFrame) |
128
- | State | Managed by Pipeline |
129
- | Input | Automatic dispatching to objects |
130
- | Animation | Built-in with dt (delta time) |
131
- | Complexity | Full-featured |
132
-
133
- ## Comparison
134
-
135
- ```
136
- ┌─────────────────────────────────────────────────────────────┐
137
- │ Shape Layer │
138
- │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
139
- │ │ Circle │ │Rectangle│ │ Star │ ... more shapes │
140
- │ └────┬────┘ └────┬────┘ └────┬────┘ │
141
- │ │ │ │ │
142
- │ └────────────┼────────────┘ │
143
- │ ▼ │
144
- │ ┌──────────┐ │
145
- │ │ Group │ (optional composition) │
146
- │ └────┬─────┘ │
147
- │ │ │
148
- │ ▼ │
149
- │ ┌──────────┐ │
150
- │ │ draw() │ (manual call) │
151
- │ └──────────┘ │
152
- └─────────────────────────────────────────────────────────────┘
153
-
154
- ┌─────────────────────────────────────────────────────────────┐
155
- │ Game Layer │
156
- │ ┌─────────────────────────────────────────────────────┐ │
157
- │ │ Game │ │
158
- │ │ ┌─────────────────────────────────────────────┐ │ │
159
- │ │ │ Pipeline │ │ │
160
- │ │ │ ┌─────────────────────────────────────┐ │ │ │
161
- │ │ │ │ Scene │ │ │ │
162
- │ │ │ │ ┌───────────┐ ┌───────────┐ │ │ │ │
163
- │ │ │ │ │GameObject │ │GameObject │ ... │ │ │ │
164
- │ │ │ │ │ + Shape │ │ + Shape │ │ │ │ │
165
- │ │ │ │ └───────────┘ └───────────┘ │ │ │ │
166
- │ │ │ └─────────────────────────────────────┘ │ │ │
167
- │ │ └─────────────────────────────────────────────┘ │ │
168
- │ └─────────────────────────────────────────────────────┘ │
169
- │ │ │
170
- │ ▼ │
171
- │ ┌────────────┐ │
172
- │ │ Game Loop │ (automatic) │
173
- │ │ update(dt) │ │
174
- │ │ render() │ │
175
- │ └────────────┘ │
176
- └─────────────────────────────────────────────────────────────┘
177
- ```
178
-
179
- ## Mixing Layers
180
-
181
- You can use shapes without GameObjects, or combine both approaches:
182
-
183
- ```js
184
- class HybridGame extends Game {
185
- init() {
186
- // Game layer for interactive elements
187
- const scene = new Scene(this);
188
- scene.add(new Player(this));
189
- this.pipeline.add(scene);
190
-
191
- // Shape layer for static background
192
- this.background = new Group();
193
- this.background.add(new Rectangle(this.width, this.height, { color: '#222' }));
194
- }
195
-
196
- render() {
197
- // Draw static shapes first
198
- this.background.draw();
199
-
200
- // Then let pipeline draw interactive objects
201
- super.render();
202
- }
203
- }
204
- ```
205
-
206
- ## Decision Guide
207
-
208
- | If you need... | Use |
209
- |---------------|-----|
210
- | Static graphics | Shape Layer |
211
- | Simple animations | Shape Layer + manual loop |
212
- | Keyboard/mouse input | Game Layer |
213
- | Multiple interactive objects | Game Layer |
214
- | Collision detection | Game Layer |
215
- | Scene organization | Game Layer |
216
- | Quick prototype | Shape Layer |
217
- | Full game | Game Layer |
218
-
219
- ## Related
220
-
221
- - [Architecture Overview](./architecture-overview.md) - Full system architecture
222
- - [Rendering Pipeline](./rendering-pipeline.md) - Shape inheritance chain
223
- - [Game Lifecycle](./lifecycle.md) - Update/render cycle
224
-
225
- ## See Also
226
-
227
- - [Shapes Module](../modules/shapes/README.md)
228
- - [Game Module](../modules/game/README.md)
229
- - [Getting Started: Hello World](../getting-started/hello-world.md)
1
+ # Two-Layer Architecture
2
+
3
+ > Understanding when to use the Shape Layer vs the Game Layer.
4
+
5
+ ## Overview
6
+
7
+ GCanvas provides two complementary approaches to building canvas applications:
8
+
9
+ 1. **Shape Layer** - For declarative, static graphics
10
+ 2. **Game Layer** - For interactive, dynamic applications
11
+
12
+ You can use either layer independently or combine them for complex applications.
13
+
14
+ ## Shape Layer
15
+
16
+ The Shape Layer is for drawing graphics without a game loop. Shapes are self-contained objects that know how to render themselves.
17
+
18
+ ### When to Use
19
+
20
+ - Static visualizations
21
+ - Data charts and graphs
22
+ - Decorative graphics
23
+ - Simple animations (manual redraw)
24
+ - Learning canvas fundamentals
25
+
26
+ ### Key Classes
27
+
28
+ - `Shape` and subclasses (`Circle`, `Rectangle`, `Star`, etc.)
29
+ - `Group` for composing shapes
30
+ - `Painter` for direct canvas control
31
+
32
+ ### Example
33
+
34
+ ```js
35
+ import { Circle, Rectangle, Group, Painter } from '@guinetik/gcanvas';
36
+
37
+ // Initialize Painter with canvas context
38
+ const canvas = document.getElementById('canvas');
39
+ Painter.init(canvas.getContext('2d'));
40
+
41
+ // Create shapes
42
+ const circle = new Circle(50, { x: 100, y: 100, color: 'red' });
43
+ const rect = new Rectangle(80, 40, { x: 200, y: 100, color: 'blue' });
44
+
45
+ // Draw immediately
46
+ Painter.clear();
47
+ circle.draw();
48
+ rect.draw();
49
+ ```
50
+
51
+ ### Characteristics
52
+
53
+ | Aspect | Shape Layer |
54
+ |--------|-------------|
55
+ | Loop | None (manual redraw) |
56
+ | State | Shapes hold their own state |
57
+ | Input | Manual event handling |
58
+ | Animation | Call draw() on each frame manually |
59
+ | Complexity | Simple, direct |
60
+
61
+ ## Game Layer
62
+
63
+ The Game Layer provides a complete game loop with automatic updates, rendering, and input handling.
64
+
65
+ ### When to Use
66
+
67
+ - Games and simulations
68
+ - Interactive applications
69
+ - Real-time animations
70
+ - Complex state management
71
+ - Multi-object coordination
72
+
73
+ ### Key Classes
74
+
75
+ - `Game` - Main loop and canvas management
76
+ - `Pipeline` - Object lifecycle management
77
+ - `GameObject` - Interactive entities
78
+ - `Scene` - Hierarchical organization
79
+
80
+ ### Example
81
+
82
+ ```js
83
+ import { Game, Scene, GameObject, Circle } from '@guinetik/gcanvas';
84
+
85
+ class Player extends GameObject {
86
+ constructor(game) {
87
+ super(game);
88
+ this.shape = new Circle(40, { color: 'blue' });
89
+ this.enableInteractivity(this.shape);
90
+ }
91
+
92
+ update(dt) {
93
+ // Called every frame
94
+ if (this.game.input.isKeyDown('ArrowRight')) {
95
+ this.shape.x += 200 * dt;
96
+ }
97
+ }
98
+
99
+ render() {
100
+ // Called every frame after update
101
+ this.shape.draw();
102
+ }
103
+
104
+ onPointerDown(e) {
105
+ // Automatic input handling
106
+ console.log('Clicked!');
107
+ }
108
+ }
109
+
110
+ class MyGame extends Game {
111
+ init() {
112
+ this.enableFluidSize();
113
+ const scene = new Scene(this);
114
+ scene.add(new Player(this));
115
+ this.pipeline.add(scene);
116
+ }
117
+ }
118
+
119
+ const game = new MyGame(document.getElementById('canvas'));
120
+ game.start();
121
+ ```
122
+
123
+ ### Characteristics
124
+
125
+ | Aspect | Game Layer |
126
+ |--------|-------------|
127
+ | Loop | Automatic (requestAnimationFrame) |
128
+ | State | Managed by Pipeline |
129
+ | Input | Automatic dispatching to objects |
130
+ | Animation | Built-in with dt (delta time) |
131
+ | Complexity | Full-featured |
132
+
133
+ ## Comparison
134
+
135
+ ```
136
+ ┌─────────────────────────────────────────────────────────────┐
137
+ │ Shape Layer │
138
+ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
139
+ │ │ Circle │ │Rectangle│ │ Star │ ... more shapes │
140
+ │ └────┬────┘ └────┬────┘ └────┬────┘ │
141
+ │ │ │ │ │
142
+ │ └────────────┼────────────┘ │
143
+ │ ▼ │
144
+ │ ┌──────────┐ │
145
+ │ │ Group │ (optional composition) │
146
+ │ └────┬─────┘ │
147
+ │ │ │
148
+ │ ▼ │
149
+ │ ┌──────────┐ │
150
+ │ │ draw() │ (manual call) │
151
+ │ └──────────┘ │
152
+ └─────────────────────────────────────────────────────────────┘
153
+
154
+ ┌─────────────────────────────────────────────────────────────┐
155
+ │ Game Layer │
156
+ │ ┌─────────────────────────────────────────────────────┐ │
157
+ │ │ Game │ │
158
+ │ │ ┌─────────────────────────────────────────────┐ │ │
159
+ │ │ │ Pipeline │ │ │
160
+ │ │ │ ┌─────────────────────────────────────┐ │ │ │
161
+ │ │ │ │ Scene │ │ │ │
162
+ │ │ │ │ ┌───────────┐ ┌───────────┐ │ │ │ │
163
+ │ │ │ │ │GameObject │ │GameObject │ ... │ │ │ │
164
+ │ │ │ │ │ + Shape │ │ + Shape │ │ │ │ │
165
+ │ │ │ │ └───────────┘ └───────────┘ │ │ │ │
166
+ │ │ │ └─────────────────────────────────────┘ │ │ │
167
+ │ │ └─────────────────────────────────────────────┘ │ │
168
+ │ └─────────────────────────────────────────────────────┘ │
169
+ │ │ │
170
+ │ ▼ │
171
+ │ ┌────────────┐ │
172
+ │ │ Game Loop │ (automatic) │
173
+ │ │ update(dt) │ │
174
+ │ │ render() │ │
175
+ │ └────────────┘ │
176
+ └─────────────────────────────────────────────────────────────┘
177
+ ```
178
+
179
+ ## Mixing Layers
180
+
181
+ You can use shapes without GameObjects, or combine both approaches:
182
+
183
+ ```js
184
+ class HybridGame extends Game {
185
+ init() {
186
+ // Game layer for interactive elements
187
+ const scene = new Scene(this);
188
+ scene.add(new Player(this));
189
+ this.pipeline.add(scene);
190
+
191
+ // Shape layer for static background
192
+ this.background = new Group();
193
+ this.background.add(new Rectangle(this.width, this.height, { color: '#222' }));
194
+ }
195
+
196
+ render() {
197
+ // Draw static shapes first
198
+ this.background.draw();
199
+
200
+ // Then let pipeline draw interactive objects
201
+ super.render();
202
+ }
203
+ }
204
+ ```
205
+
206
+ ## Decision Guide
207
+
208
+ | If you need... | Use |
209
+ |---------------|-----|
210
+ | Static graphics | Shape Layer |
211
+ | Simple animations | Shape Layer + manual loop |
212
+ | Keyboard/mouse input | Game Layer |
213
+ | Multiple interactive objects | Game Layer |
214
+ | Collision detection | Game Layer |
215
+ | Scene organization | Game Layer |
216
+ | Quick prototype | Shape Layer |
217
+ | Full game | Game Layer |
218
+
219
+ ## Related
220
+
221
+ - [Architecture Overview](./architecture-overview.md) - Full system architecture
222
+ - [Rendering Pipeline](./rendering-pipeline.md) - Shape inheritance chain
223
+ - [Game Lifecycle](./lifecycle.md) - Update/render cycle
224
+
225
+ ## See Also
226
+
227
+ - [Shapes Module](../modules/shapes/README.md)
228
+ - [Game Module](../modules/game/README.md)
229
+ - [Getting Started: Hello World](../getting-started/hello-world.md)
@@ -0,0 +1,97 @@
1
+ # Fluid & Gas Dynamics (Math-Only)
2
+
3
+ Pure math helpers for SPH-style liquids and lightweight gas simulation. The math stays in `src/math/fluid.js`; consumers (games/demos) are responsible for applying forces to their particles.
4
+
5
+ ## What’s Included
6
+ - SPH density, pressure, and viscosity kernels (`computeDensities`, `computePressures`, `computeFluidForces`).
7
+ - Simplified gas mixing with diffusion/pressure/turbulence (`computeGasForces`).
8
+ - Thermal buoyancy coupling (`computeThermalBuoyancy`) that pairs with `src/math/heat.js`.
9
+ - Force blending and pure Euler integration (`blendForces`, `integrateEuler`).
10
+ - Config factory with no magic numbers (`getDefaultFluidConfig`).
11
+
12
+ ## Config
13
+ Defined in `src/math/fluid.js` and merged via `mergeConfig` internally.
14
+
15
+ ```js
16
+ const CONFIG = {
17
+ kernel: { smoothingRadius: 28 },
18
+ fluid: {
19
+ restDensity: 1.1,
20
+ particleMass: 1,
21
+ pressureStiffness: 1800,
22
+ viscosity: 0.18,
23
+ surfaceTension: 0,
24
+ maxForce: 6000,
25
+ },
26
+ gas: {
27
+ interactionRadius: 34,
28
+ pressure: 12,
29
+ diffusion: 0.08,
30
+ drag: 0.04,
31
+ buoyancy: 260,
32
+ neutralTemperature: 0.5,
33
+ turbulence: 16,
34
+ },
35
+ external: { gravity: { x: 0, y: 820 } },
36
+ };
37
+ ```
38
+
39
+ Override by passing `{ fluid: { … }, gas: { … }, kernel: { … } }` to any helper.
40
+
41
+ ## Particle Shape
42
+ Any object with `{ x, y, vx, vy, size?, mass?, custom? }`. Mass is resolved from:
43
+ `custom.mass` → `mass` → `size` → `config.fluid.particleMass`.
44
+
45
+ ## Core API (pure)
46
+ - `computeDensities(particles, cfg?)` → `Float32Array densities`.
47
+ - `computePressures(densities, cfg?)` → `Float32Array pressures`.
48
+ - `computeFluidForces(particles, cfg?)` → `{ forces, densities, pressures }`.
49
+ - `computeGasForces(particles, cfg?)` → `{ forces }`.
50
+ - `computeThermalBuoyancy(particles, cfg?)` → `forces[]` using `temperature` or `custom.temperature`.
51
+ - `blendForces(a, b, t)` → lerped forces.
52
+ - `integrateEuler(particles, forces, dt, cfg?)` → new `{ x, y, vx, vy }[]` (no mutation).
53
+
54
+ ## Applying in a Game (pattern)
55
+ 1) Build forces:
56
+
57
+ ```js
58
+ import { computeFluidForces, computeGasForces, blendForces, computeThermalBuoyancy } from "../../src/math/fluid.js";
59
+
60
+ const liquid = computeFluidForces(particles, { kernel: { smoothingRadius: 26 } });
61
+ const gas = computeGasForces(particles, { gas: { interactionRadius: 40 } });
62
+ const buoyancy = computeThermalBuoyancy(particles);
63
+
64
+ // Combine as you see fit (example: mix liquid & gas modes, then add buoyancy)
65
+ const mixed = blendForces(liquid.forces, gas.forces, modeT); // modeT: 0..1
66
+ for (let i = 0; i < particles.length; i++) {
67
+ mixed[i].x += buoyancy[i].x;
68
+ mixed[i].y += buoyancy[i].y;
69
+ }
70
+ ```
71
+
72
+ 2) Apply to your particles (consumer-controlled):
73
+
74
+ ```js
75
+ // Mutate in your game loop; math module stays pure.
76
+ for (let i = 0; i < particles.length; i++) {
77
+ const p = particles[i];
78
+ const f = mixed[i];
79
+ const mass = p.custom.mass ?? 1;
80
+ p.vx += (f.x / mass) * dt;
81
+ p.vy += (f.y / mass) * dt;
82
+ }
83
+ ```
84
+
85
+ 3) Let your normal updaters move/render (e.g., `ParticleSystem` velocity updater).
86
+
87
+ ## Heat Coupling
88
+ Assign `p.temperature` or `p.custom.temperature` each frame (e.g., via `heat.zoneTemperature`). Pass the same particles to `computeThermalBuoyancy` and add the resulting forces.
89
+
90
+ ## Notes & Tips
91
+ - Keep `smoothingRadius` proportional to particle spacing (roughly 2–3× average spacing).
92
+ - Clamp velocities in the consumer if you target 10k–20k particles to keep the frame budget.
93
+ - For gases, favor lower `pressure` and higher `diffusion` to avoid jitter.
94
+ - The math never allocates inside the hot path besides output arrays; reuse them between frames if you need fewer allocations (pass your own particles array).
95
+
96
+
97
+