@viji-dev/sdk 1.0.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/.gitignore +29 -0
- package/LICENSE +13 -0
- package/README.md +103 -0
- package/bin/viji.js +75 -0
- package/eslint.config.js +37 -0
- package/index.html +20 -0
- package/package.json +82 -0
- package/postcss.config.js +6 -0
- package/public/favicon.png +0 -0
- package/scenes/audio-visualizer/main.js +287 -0
- package/scenes/core-demo/main.js +532 -0
- package/scenes/demo-scene/main.js +619 -0
- package/scenes/global.d.ts +15 -0
- package/scenes/particle-system/main.js +349 -0
- package/scenes/tsconfig.json +12 -0
- package/scenes/video-mirror/main.ts +436 -0
- package/src/App.css +42 -0
- package/src/App.tsx +279 -0
- package/src/cli/commands/build.js +147 -0
- package/src/cli/commands/create.js +71 -0
- package/src/cli/commands/dev.js +108 -0
- package/src/cli/commands/init.js +262 -0
- package/src/cli/utils/cli-utils.js +208 -0
- package/src/cli/utils/scene-compiler.js +432 -0
- package/src/components/SDKPage.tsx +337 -0
- package/src/components/core/CoreContainer.tsx +126 -0
- package/src/components/ui/DeviceSelectionList.tsx +137 -0
- package/src/components/ui/FPSCounter.tsx +78 -0
- package/src/components/ui/FileDropzonePanel.tsx +120 -0
- package/src/components/ui/FileListPanel.tsx +285 -0
- package/src/components/ui/InputExpansionPanel.tsx +31 -0
- package/src/components/ui/MediaPlayerControls.tsx +191 -0
- package/src/components/ui/MenuContainer.tsx +71 -0
- package/src/components/ui/ParametersMenu.tsx +797 -0
- package/src/components/ui/ProjectSwitcherMenu.tsx +192 -0
- package/src/components/ui/QuickInputControls.tsx +542 -0
- package/src/components/ui/SDKMenuSystem.tsx +96 -0
- package/src/components/ui/SettingsMenu.tsx +346 -0
- package/src/components/ui/SimpleInputControls.tsx +137 -0
- package/src/index.css +68 -0
- package/src/main.tsx +10 -0
- package/src/scenes-hmr.ts +158 -0
- package/src/services/project-filesystem.ts +436 -0
- package/src/stores/scene-player/index.ts +3 -0
- package/src/stores/scene-player/input-manager.store.ts +1045 -0
- package/src/stores/scene-player/scene-session.store.ts +659 -0
- package/src/styles/globals.css +111 -0
- package/src/templates/minimal-template.js +11 -0
- package/src/utils/debounce.js +34 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +18 -0
- package/tsconfig.app.json +27 -0
- package/tsconfig.json +27 -0
- package/tsconfig.node.json +27 -0
- package/vite.config.ts +54 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scene Compiler - Advanced compilation and bundling for Viji scenes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFile, writeFile, readdir, stat } from 'fs/promises';
|
|
6
|
+
import { join, dirname, extname, relative, resolve } from 'path';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
|
|
9
|
+
export class SceneCompiler {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.options = {
|
|
12
|
+
minify: false, // forced off by CLI contract
|
|
13
|
+
sourceMap: false, // optional, not used now
|
|
14
|
+
inlineAssets: false, // keep simple, no inlining
|
|
15
|
+
validateScene: false, // minimal validation
|
|
16
|
+
...options
|
|
17
|
+
};
|
|
18
|
+
this.dependencies = new Map();
|
|
19
|
+
this.assets = new Map();
|
|
20
|
+
this.warnings = [];
|
|
21
|
+
this.errors = [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Compile a project folder-based scene
|
|
26
|
+
* @param {string} projectPath - Path to the project folder
|
|
27
|
+
* @param {Object} buildOptions - Build configuration options
|
|
28
|
+
*/
|
|
29
|
+
async compileProject(projectPath) {
|
|
30
|
+
console.log('🔄 Starting project compilation (simple bundle)...');
|
|
31
|
+
const startTime = Date.now();
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Find main scene file (main.js or main.ts)
|
|
35
|
+
const mainFile = await this.findMainFile(projectPath);
|
|
36
|
+
if (!mainFile) {
|
|
37
|
+
throw new Error('No main.js or main.ts file found in project');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Create dist directory
|
|
41
|
+
const distPath = join(projectPath, 'dist');
|
|
42
|
+
await this.ensureDirectory(distPath);
|
|
43
|
+
|
|
44
|
+
// Set output file path
|
|
45
|
+
const outputFile = join(distPath, 'scene.js');
|
|
46
|
+
|
|
47
|
+
// Read and analyze main scene file
|
|
48
|
+
const sceneCode = await readFile(mainFile, 'utf8');
|
|
49
|
+
const analysis = await this.analyzeScene(sceneCode, mainFile);
|
|
50
|
+
|
|
51
|
+
if (this.errors.length > 0) {
|
|
52
|
+
throw new Error(`Compilation failed: ${this.errors.join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Bundle local dependencies (very simple: concatenate in order)
|
|
56
|
+
const bundle = await this.createBundle(sceneCode, analysis, projectPath);
|
|
57
|
+
const output = await this.generateOutput(bundle, analysis);
|
|
58
|
+
await writeFile(outputFile, output, 'utf8');
|
|
59
|
+
|
|
60
|
+
// Create metadata
|
|
61
|
+
const metadata = this.generateMetadata(analysis, {
|
|
62
|
+
compilationTime: Date.now() - startTime,
|
|
63
|
+
outputSize: output.length,
|
|
64
|
+
sourceFile: mainFile,
|
|
65
|
+
outputFile,
|
|
66
|
+
projectPath
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Write metadata file
|
|
70
|
+
await writeFile(join(distPath, 'scene.json'), JSON.stringify(metadata, null, 2), 'utf8');
|
|
71
|
+
|
|
72
|
+
console.log(`✅ Project compiled successfully: ${outputFile}`);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
success: true,
|
|
76
|
+
outputFile,
|
|
77
|
+
metadata,
|
|
78
|
+
warnings: this.warnings
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('💥 Compilation failed:', error.message);
|
|
83
|
+
return {
|
|
84
|
+
success: false,
|
|
85
|
+
error: error.message,
|
|
86
|
+
warnings: this.warnings,
|
|
87
|
+
errors: this.errors
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Legacy compile method for backward compatibility
|
|
94
|
+
*/
|
|
95
|
+
async compile(sceneFile, outputFile) {
|
|
96
|
+
console.log('🔄 Starting scene compilation...');
|
|
97
|
+
const startTime = Date.now();
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
// Read and analyze main scene file
|
|
101
|
+
const sceneCode = await readFile(sceneFile, 'utf8');
|
|
102
|
+
const analysis = await this.analyzeScene(sceneCode, sceneFile);
|
|
103
|
+
|
|
104
|
+
if (this.errors.length > 0) {
|
|
105
|
+
throw new Error(`Compilation failed: ${this.errors.join(', ')}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const bundle = await this.createBundle(sceneCode, analysis, dirname(sceneFile));
|
|
109
|
+
const output = await this.generateOutput(bundle, analysis);
|
|
110
|
+
|
|
111
|
+
// Create metadata
|
|
112
|
+
const metadata = this.generateMetadata(analysis, {
|
|
113
|
+
compilationTime: Date.now() - startTime,
|
|
114
|
+
outputSize: output.length,
|
|
115
|
+
sourceFile: sceneFile,
|
|
116
|
+
outputFile
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
code: output,
|
|
121
|
+
metadata,
|
|
122
|
+
warnings: this.warnings,
|
|
123
|
+
sourceMap: this.options.sourceMap ? this.generateSourceMap(bundle) : null
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error('💥 Compilation failed:', error.message);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Find the main scene file (main.js or main.ts) in a project directory
|
|
134
|
+
*/
|
|
135
|
+
async findMainFile(projectPath) {
|
|
136
|
+
const possibleFiles = ['main.ts', 'main.js', 'main.tsx', 'main.jsx'];
|
|
137
|
+
|
|
138
|
+
for (const filename of possibleFiles) {
|
|
139
|
+
const filepath = join(projectPath, filename);
|
|
140
|
+
if (existsSync(filepath)) {
|
|
141
|
+
return filepath;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Ensure a directory exists, create it if it doesn't
|
|
150
|
+
*/
|
|
151
|
+
async ensureDirectory(dirPath) {
|
|
152
|
+
try {
|
|
153
|
+
// In browser environment, this would need to be simulated
|
|
154
|
+
// For now, we'll use a simple check
|
|
155
|
+
if (!existsSync(dirPath)) {
|
|
156
|
+
// In a real implementation: await mkdir(dirPath, { recursive: true });
|
|
157
|
+
console.log(`📁 Directory would be created: ${dirPath}`);
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.warn(`Could not create directory: ${dirPath}`, error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async analyzeScene(code, filePath) {
|
|
165
|
+
const analysis = {
|
|
166
|
+
parameters: [],
|
|
167
|
+
functions: [],
|
|
168
|
+
imports: [],
|
|
169
|
+
exports: [],
|
|
170
|
+
dependencies: [],
|
|
171
|
+
assets: [],
|
|
172
|
+
shaders: [],
|
|
173
|
+
validation: {
|
|
174
|
+
hasRender: false,
|
|
175
|
+
hasInit: false,
|
|
176
|
+
hasParameters: false,
|
|
177
|
+
isValid: true,
|
|
178
|
+
issues: []
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Extract exports
|
|
183
|
+
const exportMatches = [...code.matchAll(/export\s+(?:const|function|class|let|var)\s+(\w+)/g)];
|
|
184
|
+
analysis.exports = exportMatches.map(match => match[1]);
|
|
185
|
+
|
|
186
|
+
// Validate required exports
|
|
187
|
+
analysis.validation.hasRender = analysis.exports.includes('render');
|
|
188
|
+
analysis.validation.hasInit = analysis.exports.includes('init');
|
|
189
|
+
|
|
190
|
+
if (!analysis.validation.hasRender) {
|
|
191
|
+
analysis.validation.issues.push('Missing required export: render');
|
|
192
|
+
analysis.validation.isValid = false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Extract parameters
|
|
196
|
+
const parameterMatch = code.match(/export\s+const\s+parameters\s*=\s*(\[[\s\S]*?\]);/);
|
|
197
|
+
if (parameterMatch) {
|
|
198
|
+
try {
|
|
199
|
+
analysis.parameters = this.parseParameters(parameterMatch[1]);
|
|
200
|
+
analysis.validation.hasParameters = true;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
this.warnings.push(`Could not parse parameters: ${error.message}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Extract imports and resolve dependencies
|
|
207
|
+
const importMatches = [...code.matchAll(/import\s+.*?from\s+['"]([^'"]+)['"]/g)];
|
|
208
|
+
for (const match of importMatches) {
|
|
209
|
+
const importPath = match[1];
|
|
210
|
+
analysis.imports.push(importPath);
|
|
211
|
+
|
|
212
|
+
if (!importPath.startsWith('.')) {
|
|
213
|
+
// External dependency
|
|
214
|
+
analysis.dependencies.push(importPath);
|
|
215
|
+
} else {
|
|
216
|
+
// Local file - resolve and analyze
|
|
217
|
+
const resolvedPath = resolve(dirname(filePath), importPath);
|
|
218
|
+
if (existsSync(resolvedPath) || existsSync(resolvedPath + '.js')) {
|
|
219
|
+
const localDep = existsSync(resolvedPath) ? resolvedPath : resolvedPath + '.js';
|
|
220
|
+
analysis.dependencies.push(localDep);
|
|
221
|
+
} else {
|
|
222
|
+
this.warnings.push(`Could not resolve local import: ${importPath}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Find shader files
|
|
228
|
+
const shaderExtensions = ['.glsl', '.vert', '.frag', '.vs', '.fs'];
|
|
229
|
+
const projectDir = dirname(filePath);
|
|
230
|
+
analysis.shaders = await this.findFiles(projectDir, shaderExtensions);
|
|
231
|
+
|
|
232
|
+
// Find assets
|
|
233
|
+
const assetExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.mp3', '.wav', '.ogg'];
|
|
234
|
+
analysis.assets = await this.findFiles(projectDir, assetExtensions);
|
|
235
|
+
|
|
236
|
+
return analysis;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async findFiles(directory, extensions) {
|
|
240
|
+
const files = [];
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
244
|
+
|
|
245
|
+
for (const entry of entries) {
|
|
246
|
+
const fullPath = join(directory, entry.name);
|
|
247
|
+
|
|
248
|
+
if (entry.isDirectory() && entry.name !== 'node_modules' && !entry.name.startsWith('.')) {
|
|
249
|
+
files.push(...await this.findFiles(fullPath, extensions));
|
|
250
|
+
} else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
|
|
251
|
+
files.push(fullPath);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} catch (error) {
|
|
255
|
+
// Directory doesn't exist or can't be read
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return files;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
parseParameters(parameterString) {
|
|
262
|
+
// Safe parameter parsing with error handling
|
|
263
|
+
try {
|
|
264
|
+
// Replace template literals and clean up for evaluation
|
|
265
|
+
const cleanedString = parameterString
|
|
266
|
+
.replace(/`([^`]*)`/g, '"$1"') // Convert template literals to strings
|
|
267
|
+
.replace(/\$\{[^}]*\}/g, '""'); // Remove template expressions
|
|
268
|
+
|
|
269
|
+
return eval(cleanedString);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
throw new Error(`Invalid parameters syntax: ${error.message}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async createBundle(sceneCode, analysis, projectDir) {
|
|
276
|
+
const bundle = {
|
|
277
|
+
main: sceneCode,
|
|
278
|
+
dependencies: new Map(),
|
|
279
|
+
assets: new Map(),
|
|
280
|
+
shaders: new Map()
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// Bundle local dependencies (shallow: only direct relative imports that exist)
|
|
284
|
+
for (const dep of analysis.dependencies) {
|
|
285
|
+
if (dep.startsWith('/') || dep.includes(':\\')) {
|
|
286
|
+
// Absolute path - local file
|
|
287
|
+
try {
|
|
288
|
+
const content = await readFile(dep, 'utf8');
|
|
289
|
+
const relativePath = relative(projectDir, dep);
|
|
290
|
+
bundle.dependencies.set(relativePath, content);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
this.warnings.push(`Could not read dependency: ${dep}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Skip asset inlining for simple build
|
|
298
|
+
|
|
299
|
+
// Bundle shaders
|
|
300
|
+
for (const shaderPath of analysis.shaders) {
|
|
301
|
+
try {
|
|
302
|
+
const content = await readFile(shaderPath, 'utf8');
|
|
303
|
+
const relativePath = relative(projectDir, shaderPath);
|
|
304
|
+
bundle.shaders.set(relativePath, content);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
this.warnings.push(`Could not read shader: ${shaderPath}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return bundle;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async generateOutput(bundle, analysis) {
|
|
314
|
+
const timestamp = new Date().toISOString();
|
|
315
|
+
|
|
316
|
+
let output = `/**
|
|
317
|
+
* Viji Scene Bundle
|
|
318
|
+
* Generated: ${timestamp}
|
|
319
|
+
* Minified: ${this.options.minify}
|
|
320
|
+
* Platform: Universal
|
|
321
|
+
*/
|
|
322
|
+
|
|
323
|
+
`;
|
|
324
|
+
|
|
325
|
+
// Add bundled dependencies
|
|
326
|
+
if (bundle.dependencies.size > 0) {
|
|
327
|
+
output += `// === Bundled Dependencies ===\n`;
|
|
328
|
+
for (const [path, content] of bundle.dependencies) {
|
|
329
|
+
output += `// File: ${path}\n`;
|
|
330
|
+
output += content + '\n\n';
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// No assets bundling in simple build
|
|
335
|
+
|
|
336
|
+
// No shader bundling in simple build
|
|
337
|
+
|
|
338
|
+
// Scene parameters (if present in code via export const parameters) are not transformed
|
|
339
|
+
|
|
340
|
+
// Add main scene code
|
|
341
|
+
output += `// === Main Scene Code ===\n`;
|
|
342
|
+
output += bundle.main + '\n\n';
|
|
343
|
+
|
|
344
|
+
// No compatibility wrapper; emit plain concatenated code
|
|
345
|
+
|
|
346
|
+
// Add bundle metadata
|
|
347
|
+
output += `\n// === Bundle Metadata ===\n`;
|
|
348
|
+
output += `const BUNDLE_METADATA = {\n`;
|
|
349
|
+
output += ` version: "1.0.0",\n`;
|
|
350
|
+
output += ` generatedAt: "${timestamp}",\n`;
|
|
351
|
+
output += ` parameters: SCENE_PARAMETERS,\n`;
|
|
352
|
+
output += ` exports: ${JSON.stringify(analysis.exports)},\n`;
|
|
353
|
+
output += ` hasAssets: ${bundle.assets.size > 0},\n`;
|
|
354
|
+
output += ` hasShaders: ${bundle.shaders.size > 0},\n`;
|
|
355
|
+
output += ` minified: ${this.options.minify}\n`;
|
|
356
|
+
output += `};\n`;
|
|
357
|
+
|
|
358
|
+
// Never minify per current contract
|
|
359
|
+
|
|
360
|
+
return output;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Removed compatibility wrapper
|
|
364
|
+
|
|
365
|
+
// Removed minifier
|
|
366
|
+
|
|
367
|
+
// Removed source map generation
|
|
368
|
+
|
|
369
|
+
generateMetadata(analysis, buildInfo) {
|
|
370
|
+
return {
|
|
371
|
+
version: '1.0.0',
|
|
372
|
+
buildInfo,
|
|
373
|
+
scene: {
|
|
374
|
+
parameters: analysis.parameters,
|
|
375
|
+
exports: analysis.exports,
|
|
376
|
+
validation: analysis.validation,
|
|
377
|
+
dependencies: analysis.dependencies.length,
|
|
378
|
+
assets: analysis.assets.length,
|
|
379
|
+
shaders: analysis.shaders.length
|
|
380
|
+
},
|
|
381
|
+
compatibility: {
|
|
382
|
+
vijiCore: '>=0.2.0',
|
|
383
|
+
platform: 'universal'
|
|
384
|
+
},
|
|
385
|
+
performance: {
|
|
386
|
+
complexity: this.assessComplexity(analysis),
|
|
387
|
+
estimatedMemory: this.estimateMemoryUsage(analysis),
|
|
388
|
+
features: this.detectFeatures(analysis)
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
assessComplexity(analysis) {
|
|
394
|
+
let score = 0;
|
|
395
|
+
|
|
396
|
+
if (analysis.exports.includes('onMouseMove')) score += 10;
|
|
397
|
+
if (analysis.exports.includes('onKeyPress')) score += 5;
|
|
398
|
+
if (analysis.parameters.length > 10) score += 15;
|
|
399
|
+
if (analysis.assets.length > 5) score += 10;
|
|
400
|
+
if (analysis.shaders.length > 0) score += 20;
|
|
401
|
+
if (analysis.dependencies.length > 3) score += 10;
|
|
402
|
+
|
|
403
|
+
if (score < 20) return 'low';
|
|
404
|
+
if (score < 50) return 'medium';
|
|
405
|
+
return 'high';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
estimateMemoryUsage(analysis) {
|
|
409
|
+
let size = 100; // Base KB
|
|
410
|
+
|
|
411
|
+
size += analysis.parameters.length * 2;
|
|
412
|
+
size += analysis.assets.length * 50; // Assume average 50KB per asset
|
|
413
|
+
size += analysis.shaders.length * 10;
|
|
414
|
+
size += analysis.dependencies.length * 20;
|
|
415
|
+
|
|
416
|
+
return `~${Math.round(size)}KB`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
detectFeatures(analysis) {
|
|
420
|
+
const features = [];
|
|
421
|
+
|
|
422
|
+
if (analysis.exports.includes('onMouseMove')) features.push('mouse-interaction');
|
|
423
|
+
if (analysis.exports.includes('onKeyPress')) features.push('keyboard-input');
|
|
424
|
+
if (analysis.exports.includes('onResize')) features.push('responsive');
|
|
425
|
+
if (analysis.assets.length > 0) features.push('assets');
|
|
426
|
+
if (analysis.shaders.length > 0) features.push('shaders');
|
|
427
|
+
if (analysis.parameters.some(group => group.category === 'audio')) features.push('audio-reactive');
|
|
428
|
+
|
|
429
|
+
return features;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|