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.
- package/README.md +2 -3
- package/admin/css/admin.css +1 -1200
- package/admin/js/api.js +1 -242
- package/admin/js/app.js +5 -279
- package/admin/js/config/sidebar-config.js +1 -115
- package/admin/js/lib/card.js +1 -63
- package/admin/js/lib/image-editor.js +1 -869
- package/admin/js/lib/markdown-toolbar.js +46 -421
- package/admin/js/templates/layouts.html +44 -7
- package/admin/js/templates/page-editor.html +9 -0
- package/admin/js/templates/settings.html +18 -1
- package/admin/js/templates/users.html +29 -4
- package/admin/js/views/collection-editor.js +3 -487
- package/admin/js/views/collection-entries.js +1 -484
- package/admin/js/views/collections.js +1 -153
- package/admin/js/views/dashboard.js +1 -56
- package/admin/js/views/documentation.js +1 -12
- package/admin/js/views/index.js +1 -39
- package/admin/js/views/layouts.js +9 -42
- package/admin/js/views/login.js +7 -251
- package/admin/js/views/media.js +1 -240
- package/admin/js/views/navigation.js +14 -212
- package/admin/js/views/page-editor.js +53 -661
- package/admin/js/views/pages.js +5 -72
- package/admin/js/views/plugins.js +13 -90
- package/admin/js/views/settings.js +1 -199
- package/admin/js/views/tutorials.js +1 -12
- package/admin/js/views/user-editor.js +1 -88
- package/admin/js/views/users.js +7 -76
- package/bin/cli.js +18 -9
- package/config/auth.json +1 -17
- package/config/navigation.json +15 -0
- package/config/site.json +5 -4
- package/package.json +1 -1
- package/plugins/domma-effects/public/celebrations/core/canvas.js +2 -104
- package/plugins/domma-effects/public/celebrations/core/particles.js +1 -144
- package/plugins/domma-effects/public/celebrations/core/physics.js +1 -166
- package/plugins/domma-effects/public/celebrations/index.js +1 -535
- package/plugins/domma-effects/public/celebrations/themes/christmas.js +1 -1805
- package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1 -1477
- package/plugins/domma-effects/public/celebrations/themes/halloween.js +1 -1837
- package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1 -1175
- package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1 -1258
- package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1 -1754
- package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1 -1290
- package/plugins/domma-effects/public/celebrations/themes/valentines.js +1 -1361
- package/plugins/example-analytics/stats.json +16 -12
- package/plugins/form-builder/admin/templates/form-editor.html +158 -130
- package/plugins/form-builder/admin/views/form-editor.js +3 -1
- package/plugins/form-builder/data/forms/contact-details.json +71 -35
- package/plugins/form-builder/data/forms/feedback.json +130 -0
- package/plugins/form-builder/data/submissions/feedback.json +1 -0
- package/plugins/form-builder/public/form-logic-engine.js +1 -568
- package/public/css/site.css +1 -302
- package/public/js/btt.js +1 -90
- package/public/js/cookie-consent.js +1 -61
- package/public/js/site.js +1 -204
- package/scripts/setup.js +12 -9
- package/server/middleware/auth.js +44 -21
- package/server/routes/api/auth.js +38 -8
- package/server/routes/api/collections.js +18 -5
- package/server/routes/api/layouts.js +18 -4
- package/server/routes/api/media.js +2 -3
- package/server/routes/api/navigation.js +2 -3
- package/server/routes/api/pages.js +3 -3
- package/server/routes/api/settings.js +2 -3
- package/server/routes/api/users.js +4 -6
- package/server/routes/public.js +3 -3
- package/server/server.js +8 -0
- package/server/services/markdown.js +102 -3
- package/server/services/userTypes.js +167 -0
- 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
|
|
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
|
|
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
|
-
//
|
|
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:
|
|
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
|
-
//
|
|
338
|
+
// Seed default content (pages, forms, collections)
|
|
332
339
|
// ---------------------------------------------------------------------------
|
|
333
340
|
|
|
334
|
-
if (
|
|
335
|
-
console.log('
|
|
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(
|
|
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
|
}
|
package/config/navigation.json
CHANGED
|
@@ -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": "
|
|
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":
|
|
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,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)}
|