domma-cms 0.2.0 → 0.3.0

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 (72) hide show
  1. package/README.md +2 -3
  2. package/admin/css/admin.css +1 -1200
  3. package/admin/js/api.js +1 -242
  4. package/admin/js/app.js +5 -279
  5. package/admin/js/config/sidebar-config.js +1 -115
  6. package/admin/js/lib/card.js +1 -63
  7. package/admin/js/lib/image-editor.js +1 -869
  8. package/admin/js/lib/markdown-toolbar.js +46 -421
  9. package/admin/js/templates/layouts.html +44 -7
  10. package/admin/js/templates/page-editor.html +9 -0
  11. package/admin/js/templates/settings.html +18 -1
  12. package/admin/js/templates/users.html +29 -4
  13. package/admin/js/views/collection-editor.js +3 -487
  14. package/admin/js/views/collection-entries.js +1 -484
  15. package/admin/js/views/collections.js +1 -153
  16. package/admin/js/views/dashboard.js +1 -56
  17. package/admin/js/views/documentation.js +1 -12
  18. package/admin/js/views/index.js +1 -39
  19. package/admin/js/views/layouts.js +9 -42
  20. package/admin/js/views/login.js +7 -251
  21. package/admin/js/views/media.js +1 -240
  22. package/admin/js/views/navigation.js +14 -212
  23. package/admin/js/views/page-editor.js +53 -661
  24. package/admin/js/views/pages.js +5 -72
  25. package/admin/js/views/plugins.js +13 -90
  26. package/admin/js/views/settings.js +1 -199
  27. package/admin/js/views/tutorials.js +1 -12
  28. package/admin/js/views/user-editor.js +1 -88
  29. package/admin/js/views/users.js +7 -76
  30. package/bin/cli.js +18 -9
  31. package/config/auth.json +1 -17
  32. package/config/navigation.json +15 -0
  33. package/config/site.json +5 -4
  34. package/package.json +1 -1
  35. package/plugins/domma-effects/public/celebrations/core/canvas.js +2 -104
  36. package/plugins/domma-effects/public/celebrations/core/particles.js +1 -144
  37. package/plugins/domma-effects/public/celebrations/core/physics.js +1 -166
  38. package/plugins/domma-effects/public/celebrations/index.js +1 -535
  39. package/plugins/domma-effects/public/celebrations/themes/christmas.js +1 -1805
  40. package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1 -1477
  41. package/plugins/domma-effects/public/celebrations/themes/halloween.js +1 -1837
  42. package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1 -1175
  43. package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1 -1258
  44. package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1 -1754
  45. package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1 -1290
  46. package/plugins/domma-effects/public/celebrations/themes/valentines.js +1 -1361
  47. package/plugins/example-analytics/stats.json +16 -12
  48. package/plugins/form-builder/admin/templates/form-editor.html +158 -130
  49. package/plugins/form-builder/admin/views/form-editor.js +3 -1
  50. package/plugins/form-builder/data/forms/contact-details.json +71 -35
  51. package/plugins/form-builder/data/forms/feedback.json +130 -0
  52. package/plugins/form-builder/data/submissions/feedback.json +1 -0
  53. package/plugins/form-builder/public/form-logic-engine.js +1 -568
  54. package/public/css/site.css +1 -302
  55. package/public/js/btt.js +1 -90
  56. package/public/js/cookie-consent.js +1 -61
  57. package/public/js/site.js +1 -204
  58. package/scripts/setup.js +12 -9
  59. package/server/middleware/auth.js +44 -21
  60. package/server/routes/api/auth.js +38 -8
  61. package/server/routes/api/collections.js +18 -5
  62. package/server/routes/api/layouts.js +18 -4
  63. package/server/routes/api/media.js +2 -3
  64. package/server/routes/api/navigation.js +2 -3
  65. package/server/routes/api/pages.js +3 -3
  66. package/server/routes/api/settings.js +2 -3
  67. package/server/routes/api/users.js +4 -6
  68. package/server/routes/public.js +3 -3
  69. package/server/server.js +8 -0
  70. package/server/services/markdown.js +102 -3
  71. package/server/services/userTypes.js +167 -0
  72. package/plugins/form-builder/email.js +0 -103
package/bin/cli.js CHANGED
@@ -42,7 +42,7 @@ if (flags.has('--help') || args.includes('-h')) {
42
42
  Options:
43
43
  --no-install Skip npm install
44
44
  --no-setup Skip the interactive setup wizard
45
- --seed Run npm run seed after setup
45
+ --no-seed Skip seeding default pages, forms, and collections
46
46
  --help Show this help message
47
47
 
48
48
  Example:
@@ -82,7 +82,7 @@ if (existsSync(target)) {
82
82
 
83
83
  const noInstall = flags.has('--no-install');
84
84
  const noSetup = flags.has('--no-setup') || noInstall;
85
- const withSeed = flags.has('--seed');
85
+ const noSeed = flags.has('--no-seed');
86
86
 
87
87
  // ---------------------------------------------------------------------------
88
88
  // Banner
@@ -207,7 +207,13 @@ const DEFAULT_NAV = {
207
207
  position: 'sticky'
208
208
  };
209
209
 
210
- // Discover all plugins and default each to disabled in a fresh project
210
+ // Plugins enabled by default in a fresh project
211
+ const ENABLED_BY_DEFAULT = new Set([
212
+ 'domma-effects',
213
+ 'form-builder',
214
+ ]);
215
+
216
+ // Discover all plugins and set enabled state accordingly
211
217
  const DEFAULT_PLUGINS = {};
212
218
  try {
213
219
  const pluginsRoot = path.join(target, 'plugins');
@@ -218,7 +224,7 @@ try {
218
224
  const manifestPath = path.join(pluginsRoot, name, 'plugin.json');
219
225
  try {
220
226
  JSON.parse(readFileSync(manifestPath, 'utf8')); // validate manifest exists
221
- DEFAULT_PLUGINS[name] = {enabled: false, settings: {}};
227
+ DEFAULT_PLUGINS[name] = {enabled: ENABLED_BY_DEFAULT.has(name), settings: {}};
222
228
  } catch { /* no valid plugin.json — skip */
223
229
  }
224
230
  }
@@ -237,6 +243,7 @@ done();
237
243
 
238
244
  step('Creating content directories');
239
245
  mkdirSync(path.join(target, 'content/pages'), {recursive: true});
246
+ mkdirSync(path.join(target, 'content/collections'), {recursive: true});
240
247
  mkdirSync(path.join(target, 'content/media'), {recursive: true});
241
248
  mkdirSync(path.join(target, 'content/users'), {recursive: true});
242
249
  done();
@@ -328,13 +335,15 @@ if (noSetup) {
328
335
  }
329
336
 
330
337
  // ---------------------------------------------------------------------------
331
- // Optional seed
338
+ // Seed default content (pages, forms, collections)
332
339
  // ---------------------------------------------------------------------------
333
340
 
334
- if (withSeed) {
335
- console.log(' Running seed (npm run seed)');
341
+ if (noSeed) {
342
+ console.log(' ⚠ Skipping seed (--no-seed).');
343
+ } else {
344
+ console.log(' Seeding default pages, forms and collections…');
336
345
  console.log('');
337
- const result = spawnSync('npm', ['run', 'seed'], {cwd: target, stdio: 'inherit'});
346
+ const result = spawnSync(process.execPath, ['scripts/seed.js'], {cwd: target, stdio: 'inherit'});
338
347
  if (result.status !== 0) {
339
348
  console.error('\n ✗ Seed failed.\n');
340
349
  process.exit(result.status ?? 1);
@@ -354,7 +363,7 @@ console.log('');
354
363
  console.log(` Next steps:`);
355
364
  console.log(` cd ${projectName}`);
356
365
  if (noInstall) console.log(` npm install`);
357
- if (noSetup) console.log(` npm run setup`);
366
+ if (noSetup && !noInstall) console.log(` npm run setup`);
358
367
  console.log(` npm run dev`);
359
368
  console.log('');
360
369
  console.log(` Then open: http://localhost:3050/admin`);
package/config/auth.json CHANGED
@@ -1,21 +1,5 @@
1
1
  {
2
2
  "accessTokenExpiry": "15m",
3
3
  "refreshTokenExpiry": "7d",
4
- "bcryptRounds": 10,
5
- "roles": {
6
- "admin": { "label": "Admin", "level": 0 },
7
- "manager": { "label": "Manager", "level": 1 },
8
- "editor": { "label": "Editor", "level": 2 },
9
- "subscriber": { "label": "Subscriber", "level": 3 }
10
- },
11
- "permissions": {
12
- "pages": ["admin", "manager", "editor"],
13
- "settings": ["admin", "manager"],
14
- "navigation": ["admin", "manager"],
15
- "layouts": ["admin", "manager"],
16
- "media": ["admin", "manager", "editor"],
17
- "users": ["admin", "manager"],
18
- "plugins": ["admin"],
19
- "collections": ["admin", "manager"]
20
- }
4
+ "bcryptRounds": 10
21
5
  }
@@ -25,6 +25,11 @@
25
25
  "url": "/contact",
26
26
  "icon": "mail"
27
27
  },
28
+ {
29
+ "text": "Feedback",
30
+ "url": "/feedback",
31
+ "icon": "message-circle"
32
+ },
28
33
  {
29
34
  "text": "Resources",
30
35
  "url": "/resources",
@@ -64,6 +69,16 @@
64
69
  "text": "Interactive",
65
70
  "url": "/resources/interactive",
66
71
  "icon": "mouse-pointer"
72
+ },
73
+ {
74
+ "text": "Components",
75
+ "url": "/resources/components",
76
+ "icon": "layers"
77
+ },
78
+ {
79
+ "text": "Dependencies",
80
+ "url": "/resources/dependencies",
81
+ "icon": "package"
67
82
  }
68
83
  ]
69
84
  }
package/config/site.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "tagline": "My Dead Good Site",
4
4
  "fontFamily": "Roboto",
5
5
  "fontSize": 16,
6
- "theme": "charcoal-dark",
6
+ "theme": "silver-dark",
7
7
  "adminTheme": "charcoal-dark",
8
8
  "seo": {
9
9
  "defaultTitle": "My Boss Site",
@@ -46,11 +46,12 @@
46
46
  },
47
47
  "backToTop": {
48
48
  "enabled": true,
49
- "scrollThreshold": 150,
49
+ "scrollThreshold": 180,
50
50
  "position": "bottom-right",
51
+ "offset": 16,
52
+ "bottomOffset": 16,
51
53
  "label": "",
52
- "smooth": true,
53
- "offset": 48
54
+ "smooth": true
54
55
  },
55
56
  "cookieConsent": {
56
57
  "enabled": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -1,30 +1,4 @@
1
- /**
2
- * Canvas Management Utilities
3
- * Handles canvas creation, resizing, and context management
4
- */
5
-
6
- export class CanvasManager {
7
- constructor(options = {}) {
8
- this.canvasId = options.canvasId || 'celebrations-canvas';
9
- this.zIndex = options.zIndex || 999;
10
- this.canvas = null;
11
- this.ctx = null;
12
- this.resizeTimeout = null;
13
- this.resizeCallback = options.onResize || null;
14
-
15
- // Bind methods
16
- this._handleResize = this._handleResize.bind(this);
17
- }
18
-
19
- /**
20
- * Create and append canvas to DOM
21
- */
22
- create() {
23
- if (this.canvas) return; // Already created
24
-
25
- this.canvas = document.createElement('canvas');
26
- this.canvas.id = this.canvasId;
27
- this.canvas.style.cssText = `
1
+ export class CanvasManager{constructor(e={}){this.canvasId=e.canvasId||"celebrations-canvas",this.zIndex=e.zIndex||999,this.canvas=null,this.ctx=null,this.resizeTimeout=null,this.resizeCallback=e.onResize||null,this._handleResize=this._handleResize.bind(this)}create(){this.canvas||(this.canvas=document.createElement("canvas"),this.canvas.id=this.canvasId,this.canvas.style.cssText=`
28
2
  position: fixed;
29
3
  top: 0;
30
4
  left: 0;
@@ -32,80 +6,4 @@ export class CanvasManager {
32
6
  height: 100vh;
33
7
  pointer-events: none;
34
8
  z-index: ${this.zIndex};
35
- `;
36
-
37
- this.ctx = this.canvas.getContext('2d', { alpha: true });
38
- this.resize();
39
- document.body.appendChild(this.canvas);
40
-
41
- // Bind resize listener
42
- window.addEventListener('resize', this._handleResize);
43
- }
44
-
45
- /**
46
- * Resize canvas to match window dimensions
47
- */
48
- resize() {
49
- if (!this.canvas) return;
50
-
51
- this.canvas.width = window.innerWidth;
52
- this.canvas.height = window.innerHeight;
53
- }
54
-
55
- /**
56
- * Clear entire canvas
57
- */
58
- clear() {
59
- if (!this.ctx) return;
60
- this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
61
- }
62
-
63
- /**
64
- * Get canvas dimensions
65
- */
66
- getDimensions() {
67
- return {
68
- width: this.canvas?.width || 0,
69
- height: this.canvas?.height || 0
70
- };
71
- }
72
-
73
- /**
74
- * Check if mobile device
75
- */
76
- isMobile() {
77
- return window.innerWidth < 768;
78
- }
79
-
80
- /**
81
- * Remove canvas from DOM and cleanup
82
- */
83
- destroy() {
84
- window.removeEventListener('resize', this._handleResize);
85
-
86
- if (this.canvas && this.canvas.parentNode) {
87
- this.canvas.parentNode.removeChild(this.canvas);
88
- }
89
-
90
- this.canvas = null;
91
- this.ctx = null;
92
- }
93
-
94
- /**
95
- * Handle window resize with debouncing
96
- * @private
97
- */
98
- _handleResize() {
99
- if (this.resizeTimeout) {
100
- clearTimeout(this.resizeTimeout);
101
- }
102
-
103
- this.resizeTimeout = setTimeout(() => {
104
- this.resize();
105
-
106
- if (this.resizeCallback) {
107
- this.resizeCallback();
108
- }
109
- }, 250);
110
- }
111
- }
9
+ `,this.ctx=this.canvas.getContext("2d",{alpha:!0}),this.resize(),document.body.appendChild(this.canvas),window.addEventListener("resize",this._handleResize))}resize(){this.canvas&&(this.canvas.width=window.innerWidth,this.canvas.height=window.innerHeight)}clear(){this.ctx&&this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height)}getDimensions(){return{width:this.canvas?.width||0,height:this.canvas?.height||0}}isMobile(){return window.innerWidth<768}destroy(){window.removeEventListener("resize",this._handleResize),this.canvas&&this.canvas.parentNode&&this.canvas.parentNode.removeChild(this.canvas),this.canvas=null,this.ctx=null}_handleResize(){this.resizeTimeout&&clearTimeout(this.resizeTimeout),this.resizeTimeout=setTimeout(()=>{this.resize(),this.resizeCallback&&this.resizeCallback()},250)}}
@@ -1,144 +1 @@
1
- /**
2
- * Particle System Base
3
- * Handles particle creation, depth layers, and lifecycle management
4
- */
5
-
6
- /**
7
- * Create a base particle with depth layering
8
- * @param {Object} config - Configuration for particle creation
9
- * @param {number} canvasWidth - Canvas width for positioning
10
- * @param {number} canvasHeight - Canvas height for positioning
11
- * @returns {Object} Particle object
12
- */
13
- export function createParticle(config, canvasWidth, canvasHeight) {
14
- // Determine depth layer (front, middle, back)
15
- const depth = Math.random();
16
- let size, speed, opacity, windSpeed;
17
-
18
- if (depth < 0.33) {
19
- // Front layer - larger, faster, more opaque
20
- size = config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]) * 1.5;
21
- speed = config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0]) * 1.3;
22
- opacity = 0.8 + Math.random() * 0.2;
23
- windSpeed = 0.01 + Math.random() * 0.02;
24
- } else if (depth < 0.66) {
25
- // Middle layer
26
- size = config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]);
27
- speed = config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0]);
28
- opacity = 0.5 + Math.random() * 0.2;
29
- windSpeed = 0.02 + Math.random() * 0.03;
30
- } else {
31
- // Back layer - smaller, slower, less opaque
32
- size = config.sizeRange[0] + Math.random() * (config.sizeRange[1] - config.sizeRange[0]) * 0.7;
33
- speed = config.speedRange[0] + Math.random() * (config.speedRange[1] - config.speedRange[0]) * 0.7;
34
- opacity = 0.3 + Math.random() * 0.2;
35
- windSpeed = 0.03 + Math.random() * 0.04;
36
- }
37
-
38
- return {
39
- x: Math.random() * canvasWidth,
40
- y: Math.random() * canvasHeight - canvasHeight, // Start above viewport
41
- size: size,
42
- speed: speed,
43
- opacity: opacity,
44
- windOffset: Math.random() * Math.PI * 2,
45
- windSpeed: windSpeed,
46
- rotation: Math.random() * Math.PI * 2,
47
- rotationSpeed: (Math.random() - 0.5) * 0.02,
48
- depth: depth
49
- };
50
- }
51
-
52
- /**
53
- * Create a static decoration (tree, house, etc.)
54
- * @param {string} type - Type of decoration
55
- * @param {number} canvasWidth - Canvas width for positioning
56
- * @param {number} canvasHeight - Canvas height for positioning
57
- * @param {Object} options - Additional options
58
- * @returns {Object} Static particle object
59
- */
60
- export function createStaticDecoration(type, canvasWidth, canvasHeight, options = {}) {
61
- return {
62
- type: type,
63
- x: options.x !== undefined ? options.x : Math.random() * canvasWidth,
64
- y: options.y !== undefined ? options.y : Math.random() * canvasHeight,
65
- vx: 0,
66
- vy: 0,
67
- size: options.size || (20 + Math.random() * 15),
68
- opacity: options.opacity || (0.6 + Math.random() * 0.3),
69
- rotation: options.rotation || 0,
70
- rotationSpeed: 0,
71
- active: true,
72
- static: true,
73
- ...options
74
- };
75
- }
76
-
77
- /**
78
- * Create a moving special particle (sleigh, bird, etc.)
79
- * @param {string} type - Type of special particle
80
- * @param {number} canvasWidth - Canvas width for positioning
81
- * @param {number} canvasHeight - Canvas height for positioning
82
- * @param {Object} options - Movement and appearance options
83
- * @returns {Object} Moving particle object
84
- */
85
- export function createMovingParticle(type, canvasWidth, canvasHeight, options = {}) {
86
- const startX = options.startX !== undefined ? options.startX : -100;
87
- const startY = options.startY !== undefined ? options.startY : canvasHeight * 0.3;
88
-
89
- return {
90
- type: type,
91
- x: startX,
92
- y: startY,
93
- vx: options.vx || 1.5,
94
- vy: options.vy || 0,
95
- size: options.size || 40,
96
- opacity: options.opacity || 1,
97
- rotation: options.rotation || 0,
98
- rotationSpeed: options.rotationSpeed || 0,
99
- active: true,
100
- static: false,
101
- // Sine wave motion
102
- waveAmplitude: options.waveAmplitude || 0,
103
- waveFrequency: options.waveFrequency || 0,
104
- waveOffset: options.waveOffset || 0,
105
- ...options
106
- };
107
- }
108
-
109
- /**
110
- * Recycle particle to top/side of canvas
111
- * @param {Object} particle - Particle to recycle
112
- * @param {number} canvasWidth - Canvas width
113
- * @param {number} canvasHeight - Canvas height
114
- */
115
- export function recycleParticle(particle, canvasWidth, canvasHeight) {
116
- if (particle.y > canvasHeight + 10) {
117
- particle.y = -10;
118
- particle.x = Math.random() * canvasWidth;
119
- }
120
-
121
- // Wrap horizontally
122
- if (particle.x < -10) {
123
- particle.x = canvasWidth + 10;
124
- } else if (particle.x > canvasWidth + 10) {
125
- particle.x = -10;
126
- }
127
- }
128
-
129
- /**
130
- * Check if particle is off-screen and should be removed
131
- * @param {Object} particle - Particle to check
132
- * @param {number} canvasWidth - Canvas width
133
- * @param {number} canvasHeight - Canvas height
134
- * @param {number} buffer - Extra buffer space (default 100)
135
- * @returns {boolean} True if particle is off-screen
136
- */
137
- export function isOffScreen(particle, canvasWidth, canvasHeight, buffer = 100) {
138
- return (
139
- particle.x < -buffer ||
140
- particle.x > canvasWidth + buffer ||
141
- particle.y < -buffer ||
142
- particle.y > canvasHeight + buffer
143
- );
144
- }
1
+ export function createParticle(e,t,d){const a=Math.random();let r,n,M,h;return a<.33?(r=e.sizeRange[0]+Math.random()*(e.sizeRange[1]-e.sizeRange[0])*1.5,n=e.speedRange[0]+Math.random()*(e.speedRange[1]-e.speedRange[0])*1.3,M=.8+Math.random()*.2,h=.01+Math.random()*.02):a<.66?(r=e.sizeRange[0]+Math.random()*(e.sizeRange[1]-e.sizeRange[0]),n=e.speedRange[0]+Math.random()*(e.speedRange[1]-e.speedRange[0]),M=.5+Math.random()*.2,h=.02+Math.random()*.03):(r=e.sizeRange[0]+Math.random()*(e.sizeRange[1]-e.sizeRange[0])*.7,n=e.speedRange[0]+Math.random()*(e.speedRange[1]-e.speedRange[0])*.7,M=.3+Math.random()*.2,h=.03+Math.random()*.04),{x:Math.random()*t,y:Math.random()*d-d,size:r,speed:n,opacity:M,windOffset:Math.random()*Math.PI*2,windSpeed:h,rotation:Math.random()*Math.PI*2,rotationSpeed:(Math.random()-.5)*.02,depth:a}}export function createStaticDecoration(e,t,d,a={}){return{type:e,x:a.x!==void 0?a.x:Math.random()*t,y:a.y!==void 0?a.y:Math.random()*d,vx:0,vy:0,size:a.size||20+Math.random()*15,opacity:a.opacity||.6+Math.random()*.3,rotation:a.rotation||0,rotationSpeed:0,active:!0,static:!0,...a}}export function createMovingParticle(e,t,d,a={}){const r=a.startX!==void 0?a.startX:-100,n=a.startY!==void 0?a.startY:d*.3;return{type:e,x:r,y:n,vx:a.vx||1.5,vy:a.vy||0,size:a.size||40,opacity:a.opacity||1,rotation:a.rotation||0,rotationSpeed:a.rotationSpeed||0,active:!0,static:!1,waveAmplitude:a.waveAmplitude||0,waveFrequency:a.waveFrequency||0,waveOffset:a.waveOffset||0,...a}}export function recycleParticle(e,t,d){e.y>d+10&&(e.y=-10,e.x=Math.random()*t),e.x<-10?e.x=t+10:e.x>t+10&&(e.x=-10)}export function isOffScreen(e,t,d,a=100){return e.x<-a||e.x>t+a||e.y<-a||e.y>d+a}
@@ -1,166 +1 @@
1
- /**
2
- * Physics Utilities
3
- * Handles wind, gravity, movement, and physics simulation
4
- */
5
-
6
- export class PhysicsEngine {
7
- constructor(options = {}) {
8
- this.windGust = 0; // Current wind strength
9
- this.windGustTarget = 0; // Target wind strength
10
- this.lastGustTime = 0;
11
- this.gustInterval = options.gustInterval || [5000, 15000]; // Min/max ms between gusts
12
- this.gustStrength = options.gustStrength || [-2, 2]; // Min/max wind force
13
- }
14
-
15
- /**
16
- * Update wind physics
17
- * @param {number} currentTime - Current timestamp
18
- */
19
- updateWind(currentTime) {
20
- // Generate new wind gust periodically
21
- const [minInterval, maxInterval] = this.gustInterval;
22
- if (currentTime - this.lastGustTime > minInterval + Math.random() * (maxInterval - minInterval)) {
23
- const [minStrength, maxStrength] = this.gustStrength;
24
- this.windGustTarget = minStrength + Math.random() * (maxStrength - minStrength);
25
- this.lastGustTime = currentTime;
26
- }
27
-
28
- // Smooth wind transition
29
- this.windGust += (this.windGustTarget - this.windGust) * 0.02;
30
- }
31
-
32
- /**
33
- * Get current wind force
34
- * @returns {number} Current wind force
35
- */
36
- getWindForce() {
37
- return this.windGust;
38
- }
39
-
40
- /**
41
- * Reset wind to calm
42
- */
43
- resetWind() {
44
- this.windGust = 0;
45
- this.windGustTarget = 0;
46
- }
47
- }
48
-
49
- /**
50
- * Update particle position with gravity and wind
51
- * @param {Object} particle - Particle to update
52
- * @param {number} deltaTime - Time since last frame
53
- * @param {number} windForce - Current wind force
54
- */
55
- export function updateParticlePhysics(particle, deltaTime, windForce = 0) {
56
- const normalizedDelta = deltaTime / (1000 / 60); // Normalize to 60fps
57
-
58
- // Apply gravity (downward movement) - support both speed and vy
59
- const verticalSpeed = particle.vy || particle.speed || 0;
60
- particle.y += verticalSpeed * normalizedDelta;
61
-
62
- // Apply horizontal velocity if present
63
- if (particle.vx) {
64
- particle.x += particle.vx * normalizedDelta;
65
- }
66
-
67
- // Apply wind drift + wind gust
68
- if (particle.windOffset !== undefined && particle.windSpeed !== undefined) {
69
- particle.windOffset += particle.windSpeed * normalizedDelta;
70
- const baseWind = Math.sin(particle.windOffset) * 0.5;
71
- particle.x += (baseWind + windForce) * normalizedDelta;
72
- }
73
-
74
- // Apply rotation
75
- if (particle.rotationSpeed) {
76
- particle.rotation += particle.rotationSpeed * normalizedDelta;
77
- }
78
- }
79
-
80
- /**
81
- * Update moving particle with sine wave motion
82
- * @param {Object} particle - Moving particle to update
83
- * @param {number} deltaTime - Time since last frame
84
- * @param {number} currentTime - Current timestamp
85
- */
86
- export function updateMovingParticle(particle, deltaTime, currentTime) {
87
- const normalizedDelta = deltaTime / (1000 / 60);
88
-
89
- // Update horizontal position
90
- particle.x += particle.vx * normalizedDelta;
91
-
92
- // Apply sine wave vertical motion if configured
93
- if (particle.waveAmplitude && particle.waveFrequency) {
94
- const waveProgress = (currentTime * particle.waveFrequency) / 1000;
95
- particle.y = particle.baseY + Math.sin(waveProgress + particle.waveOffset) * particle.waveAmplitude;
96
- } else {
97
- particle.y += particle.vy * normalizedDelta;
98
- }
99
-
100
- // Apply rotation
101
- if (particle.rotationSpeed) {
102
- particle.rotation += particle.rotationSpeed * normalizedDelta;
103
- }
104
- }
105
-
106
- /**
107
- * Apply gravity to particle
108
- * @param {Object} particle - Particle to affect
109
- * @param {number} deltaTime - Time since last frame
110
- * @param {number} gravity - Gravity force (default 0.5)
111
- */
112
- export function applyGravity(particle, deltaTime, gravity = 0.5) {
113
- const normalizedDelta = deltaTime / (1000 / 60);
114
- particle.vy += gravity * normalizedDelta;
115
- particle.y += particle.vy * normalizedDelta;
116
- }
117
-
118
- /**
119
- * Apply bounce physics when particle hits ground
120
- * @param {Object} particle - Particle to bounce
121
- * @param {number} groundY - Y position of ground
122
- * @param {number} restitution - Bounce factor (0-1, default 0.6)
123
- */
124
- export function applyBounce(particle, groundY, restitution = 0.6) {
125
- if (particle.y >= groundY && particle.vy > 0) {
126
- particle.y = groundY;
127
- particle.vy *= -restitution;
128
-
129
- // Stop bouncing if velocity is too low
130
- if (Math.abs(particle.vy) < 0.5) {
131
- particle.vy = 0;
132
- }
133
- }
134
- }
135
-
136
- /**
137
- * Calculate normalized delta time for consistent animation
138
- * @param {number} deltaTime - Time since last frame in ms
139
- * @param {number} targetFPS - Target framerate (default 60)
140
- * @returns {number} Normalized delta
141
- */
142
- export function normalizeDelta(deltaTime, targetFPS = 60) {
143
- return deltaTime / (1000 / targetFPS);
144
- }
145
-
146
- /**
147
- * Interpolate between two values
148
- * @param {number} start - Start value
149
- * @param {number} end - End value
150
- * @param {number} t - Interpolation factor (0-1)
151
- * @returns {number} Interpolated value
152
- */
153
- export function lerp(start, end, t) {
154
- return start + (end - start) * t;
155
- }
156
-
157
- /**
158
- * Clamp value between min and max
159
- * @param {number} value - Value to clamp
160
- * @param {number} min - Minimum value
161
- * @param {number} max - Maximum value
162
- * @returns {number} Clamped value
163
- */
164
- export function clamp(value, min, max) {
165
- return Math.min(Math.max(value, min), max);
166
- }
1
+ export class PhysicsEngine{constructor(n={}){this.windGust=0,this.windGustTarget=0,this.lastGustTime=0,this.gustInterval=n.gustInterval||[5e3,15e3],this.gustStrength=n.gustStrength||[-2,2]}updateWind(n){const[s,e]=this.gustInterval;if(n-this.lastGustTime>s+Math.random()*(e-s)){const[o,i]=this.gustStrength;this.windGustTarget=o+Math.random()*(i-o),this.lastGustTime=n}this.windGust+=(this.windGustTarget-this.windGust)*.02}getWindForce(){return this.windGust}resetWind(){this.windGust=0,this.windGustTarget=0}}export function updateParticlePhysics(t,n,s=0){const e=n/16.666666666666668,o=t.vy||t.speed||0;if(t.y+=o*e,t.vx&&(t.x+=t.vx*e),t.windOffset!==void 0&&t.windSpeed!==void 0){t.windOffset+=t.windSpeed*e;const i=Math.sin(t.windOffset)*.5;t.x+=(i+s)*e}t.rotationSpeed&&(t.rotation+=t.rotationSpeed*e)}export function updateMovingParticle(t,n,s){const e=n/16.666666666666668;if(t.x+=t.vx*e,t.waveAmplitude&&t.waveFrequency){const o=s*t.waveFrequency/1e3;t.y=t.baseY+Math.sin(o+t.waveOffset)*t.waveAmplitude}else t.y+=t.vy*e;t.rotationSpeed&&(t.rotation+=t.rotationSpeed*e)}export function applyGravity(t,n,s=.5){const e=n/16.666666666666668;t.vy+=s*e,t.y+=t.vy*e}export function applyBounce(t,n,s=.6){t.y>=n&&t.vy>0&&(t.y=n,t.vy*=-s,Math.abs(t.vy)<.5&&(t.vy=0))}export function normalizeDelta(t,n=60){return t/(1e3/n)}export function lerp(t,n,s){return t+(n-t)*s}export function clamp(t,n,s){return Math.min(Math.max(t,n),s)}