@viji-dev/sdk 1.0.0 → 1.0.2

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 (77) hide show
  1. package/README.md +70 -63
  2. package/bin/viji.js +9 -29
  3. package/dist/assets/artist-dts-BHUsvSI6.js +613 -0
  4. package/dist/assets/artist-dts-p5-Cyw8vmy_.js +736 -0
  5. package/dist/assets/core-CiQx3w0t.js +12 -0
  6. package/dist/assets/dark-plus-C3mMm8J8.js +1 -0
  7. package/dist/assets/docs-api-PBLtY4Ni.js +12381 -0
  8. package/dist/assets/engine-javascript-CXyY7cc8.js +141 -0
  9. package/dist/assets/essentia-wasm.web-0S-sW98u-CYV1l1zv.js +38 -0
  10. package/dist/assets/essentia.js-core.es-DnrJE0uR-DOSrF5_G.js +32 -0
  11. package/dist/assets/glsl-DMyvO4G4.js +1 -0
  12. package/dist/assets/index-BhFxsauQ.js +215 -0
  13. package/dist/assets/index-BqhVeA7U.css +1 -0
  14. package/dist/assets/index-T4TOjvD0.js +1 -0
  15. package/dist/assets/index-Wz9WqGqz.js +52 -0
  16. package/dist/assets/index-t24aGwla.js +1 -0
  17. package/dist/assets/javascript-wDzz0qaB.js +1 -0
  18. package/dist/assets/shader-uniforms-GdaUkQPK.js +1 -0
  19. package/dist/assets/typescript-BPQ3VLAy.js +1 -0
  20. package/dist/assets/viji.worker-CQSJ0SiO-ljtBlcNZ.js +27018 -0
  21. package/{index.html → dist/index.html} +2 -1
  22. package/package.json +31 -35
  23. package/src/cli/commands/build.js +50 -99
  24. package/src/cli/commands/create.js +49 -46
  25. package/src/cli/commands/dev.js +30 -97
  26. package/src/cli/server/dev-server.js +233 -0
  27. package/src/cli/server/scene-scanner.js +93 -0
  28. package/src/cli/server/vite-scene-plugin.d.ts +2 -0
  29. package/src/cli/server/vite-scene-plugin.js +134 -0
  30. package/src/cli/utils/cli-utils.js +29 -139
  31. package/src/cli/utils/scene-compiler.js +10 -17
  32. package/src/templates/scene-templates.js +85 -0
  33. package/.gitignore +0 -29
  34. package/eslint.config.js +0 -37
  35. package/postcss.config.js +0 -6
  36. package/scenes/audio-visualizer/main.js +0 -287
  37. package/scenes/core-demo/main.js +0 -532
  38. package/scenes/demo-scene/main.js +0 -619
  39. package/scenes/global.d.ts +0 -15
  40. package/scenes/particle-system/main.js +0 -349
  41. package/scenes/tsconfig.json +0 -12
  42. package/scenes/video-mirror/main.ts +0 -436
  43. package/src/App.css +0 -42
  44. package/src/App.tsx +0 -279
  45. package/src/cli/commands/init.js +0 -262
  46. package/src/components/SDKPage.tsx +0 -337
  47. package/src/components/core/CoreContainer.tsx +0 -126
  48. package/src/components/ui/DeviceSelectionList.tsx +0 -137
  49. package/src/components/ui/FPSCounter.tsx +0 -78
  50. package/src/components/ui/FileDropzonePanel.tsx +0 -120
  51. package/src/components/ui/FileListPanel.tsx +0 -285
  52. package/src/components/ui/InputExpansionPanel.tsx +0 -31
  53. package/src/components/ui/MediaPlayerControls.tsx +0 -191
  54. package/src/components/ui/MenuContainer.tsx +0 -71
  55. package/src/components/ui/ParametersMenu.tsx +0 -797
  56. package/src/components/ui/ProjectSwitcherMenu.tsx +0 -192
  57. package/src/components/ui/QuickInputControls.tsx +0 -542
  58. package/src/components/ui/SDKMenuSystem.tsx +0 -96
  59. package/src/components/ui/SettingsMenu.tsx +0 -346
  60. package/src/components/ui/SimpleInputControls.tsx +0 -137
  61. package/src/index.css +0 -68
  62. package/src/main.tsx +0 -10
  63. package/src/scenes-hmr.ts +0 -158
  64. package/src/services/project-filesystem.ts +0 -436
  65. package/src/stores/scene-player/index.ts +0 -3
  66. package/src/stores/scene-player/input-manager.store.ts +0 -1045
  67. package/src/stores/scene-player/scene-session.store.ts +0 -659
  68. package/src/styles/globals.css +0 -111
  69. package/src/templates/minimal-template.js +0 -11
  70. package/src/utils/debounce.js +0 -34
  71. package/src/vite-env.d.ts +0 -1
  72. package/tailwind.config.js +0 -18
  73. package/tsconfig.app.json +0 -27
  74. package/tsconfig.json +0 -27
  75. package/tsconfig.node.json +0 -27
  76. package/vite.config.ts +0 -54
  77. /package/{public → dist}/favicon.png +0 -0
@@ -149,16 +149,8 @@ export class SceneCompiler {
149
149
  * Ensure a directory exists, create it if it doesn't
150
150
  */
151
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
- }
152
+ const { mkdir } = await import('fs/promises');
153
+ await mkdir(dirPath, { recursive: true });
162
154
  }
163
155
 
164
156
  async analyzeScene(code, filePath) {
@@ -183,12 +175,15 @@ export class SceneCompiler {
183
175
  const exportMatches = [...code.matchAll(/export\s+(?:const|function|class|let|var)\s+(\w+)/g)];
184
176
  analysis.exports = exportMatches.map(match => match[1]);
185
177
 
186
- // Validate required exports
187
- analysis.validation.hasRender = analysis.exports.includes('render');
188
- analysis.validation.hasInit = analysis.exports.includes('init');
189
-
178
+ analysis.validation.hasRender =
179
+ analysis.exports.includes('render') ||
180
+ /function\s+render\s*\(/.test(code);
181
+ analysis.validation.hasInit =
182
+ analysis.exports.includes('init') ||
183
+ /function\s+(?:setup|init)\s*\(/.test(code);
184
+
190
185
  if (!analysis.validation.hasRender) {
191
- analysis.validation.issues.push('Missing required export: render');
186
+ analysis.validation.issues.push('Missing required function: render');
192
187
  analysis.validation.isValid = false;
193
188
  }
194
189
 
@@ -343,12 +338,10 @@ export class SceneCompiler {
343
338
 
344
339
  // No compatibility wrapper; emit plain concatenated code
345
340
 
346
- // Add bundle metadata
347
341
  output += `\n// === Bundle Metadata ===\n`;
348
342
  output += `const BUNDLE_METADATA = {\n`;
349
343
  output += ` version: "1.0.0",\n`;
350
344
  output += ` generatedAt: "${timestamp}",\n`;
351
- output += ` parameters: SCENE_PARAMETERS,\n`;
352
345
  output += ` exports: ${JSON.stringify(analysis.exports)},\n`;
353
346
  output += ` hasAssets: ${bundle.assets.size > 0},\n`;
354
347
  output += ` hasShaders: ${bundle.shaders.size > 0},\n`;
@@ -0,0 +1,85 @@
1
+ const TEMPLATES = {
2
+ native: `// @renderer native
3
+
4
+ const speed = viji.slider(1.0, { min: 0.1, max: 5.0, label: 'Speed' });
5
+ const baseColor = viji.color('#4488ff', { label: 'Color' });
6
+
7
+ function render(viji) {
8
+ const ctx = viji.useContext('2d');
9
+ const { width, height, time } = viji;
10
+
11
+ ctx.fillStyle = '#000000';
12
+ ctx.fillRect(0, 0, width, height);
13
+
14
+ const x = width / 2 + Math.cos(time * speed.value) * 100;
15
+ const y = height / 2 + Math.sin(time * speed.value) * 100;
16
+
17
+ ctx.beginPath();
18
+ ctx.arc(x, y, 40, 0, Math.PI * 2);
19
+ ctx.fillStyle = baseColor.value;
20
+ ctx.fill();
21
+ }
22
+ `,
23
+
24
+ p5: `// @renderer p5
25
+
26
+ const speed = viji.slider(1.0, { min: 0.1, max: 5.0, label: 'Speed' });
27
+ const baseColor = viji.color('#4488ff', { label: 'Color' });
28
+
29
+ function render(viji, p5) {
30
+ p5.background(0);
31
+
32
+ const x = viji.width / 2 + p5.cos(viji.time * speed.value) * 100;
33
+ const y = viji.height / 2 + p5.sin(viji.time * speed.value) * 100;
34
+
35
+ p5.noStroke();
36
+ p5.fill(baseColor.value);
37
+ p5.ellipse(x, y, 80, 80);
38
+ }
39
+ `,
40
+
41
+ shader: `// @renderer shader
42
+ precision highp float;
43
+
44
+ uniform vec2 u_resolution;
45
+ uniform float u_time;
46
+ uniform vec2 u_mouse;
47
+
48
+ void main() {
49
+ vec2 uv = gl_FragCoord.xy / u_resolution;
50
+ vec2 center = u_mouse / u_resolution;
51
+
52
+ float d = length(uv - center);
53
+ float ring = smoothstep(0.3, 0.29, d) - smoothstep(0.25, 0.24, d);
54
+
55
+ vec3 col = 0.5 + 0.5 * cos(u_time + uv.xyx + vec3(0.0, 2.0, 4.0));
56
+ col = mix(col * 0.2, col, ring);
57
+
58
+ gl_FragColor = vec4(col, 1.0);
59
+ }
60
+ `,
61
+ };
62
+
63
+ const GLOBAL_DTS_FILE = {
64
+ native: 'artist-global.d.ts',
65
+ p5: 'artist-global-p5.d.ts',
66
+ shader: null,
67
+ };
68
+
69
+ export function getSceneTemplate(renderer = 'native') {
70
+ const template = TEMPLATES[renderer];
71
+ if (!template) {
72
+ throw new Error(`Unknown renderer: "${renderer}". Valid options: ${Object.keys(TEMPLATES).join(', ')}`);
73
+ }
74
+ return template;
75
+ }
76
+
77
+ export function getGlobalDtsFileName(renderer = 'native') {
78
+ return GLOBAL_DTS_FILE[renderer] ?? GLOBAL_DTS_FILE.native;
79
+ }
80
+
81
+ export function getSceneFileExtension(renderer = 'native') {
82
+ return renderer === 'shader' ? '.glsl' : '.js';
83
+ }
84
+
85
+ export const VALID_RENDERERS = Object.keys(TEMPLATES);
package/.gitignore DELETED
@@ -1,29 +0,0 @@
1
- # Logs
2
- logs
3
- *.log
4
- npm-debug.log*
5
- yarn-debug.log*
6
- yarn-error.log*
7
- pnpm-debug.log*
8
- lerna-debug.log*
9
-
10
- node_modules
11
- dist
12
- dist-ssr
13
- *.local
14
-
15
- # Editor directories and files
16
- .vscode/*
17
- !.vscode/extensions.json
18
- .idea
19
- .DS_Store
20
- *.suo
21
- *.ntvs*
22
- *.njsproj
23
- *.sln
24
- *.sw?
25
-
26
- # Documentation for other project parts
27
- /core_docs
28
- /backend_docs
29
- /sdk_docs
package/eslint.config.js DELETED
@@ -1,37 +0,0 @@
1
- import js from '@eslint/js'
2
- import globals from 'globals'
3
- import reactHooks from 'eslint-plugin-react-hooks'
4
- import reactRefresh from 'eslint-plugin-react-refresh'
5
- import tseslint from 'typescript-eslint'
6
- import { globalIgnores } from 'eslint/config'
7
-
8
- export default tseslint.config([
9
- globalIgnores(['dist']),
10
- {
11
- files: ['scenes/**/*.{js,ts}'],
12
- languageOptions: {
13
- ecmaVersion: 2020,
14
- globals: {
15
- ...globals.browser,
16
- viji: 'readonly',
17
- render: 'readonly',
18
- },
19
- },
20
- rules: {
21
- 'no-undef': 'off',
22
- },
23
- },
24
- {
25
- files: ['**/*.{ts,tsx}'],
26
- extends: [
27
- js.configs.recommended,
28
- tseslint.configs.recommended,
29
- reactHooks.configs['recommended-latest'],
30
- reactRefresh.configs.vite,
31
- ],
32
- languageOptions: {
33
- ecmaVersion: 2020,
34
- globals: globals.browser,
35
- },
36
- },
37
- ])
package/postcss.config.js DELETED
@@ -1,6 +0,0 @@
1
- export default {
2
- plugins: {
3
- tailwindcss: {},
4
- autoprefixer: {},
5
- },
6
- };
@@ -1,287 +0,0 @@
1
- // Audio Reactive Visualizer Scene - Parameter Object API
2
- console.log('🎵 AUDIO VISUALIZER SCENE LOADED');
3
-
4
- // Define parameters using helper functions (returns parameter objects)
5
- const primaryColor = viji.color('#ff6b6b', {
6
- label: 'Primary Color',
7
- description: 'Main color for frequency bars',
8
- group: 'colors'
9
- });
10
-
11
- const secondaryColor = viji.color('#4ecdc4', {
12
- label: 'Secondary Color',
13
- description: 'Background and accent color',
14
- group: 'colors'
15
- });
16
-
17
- const backgroundColor = viji.color('#1a1a2e', {
18
- label: 'Background Color',
19
- description: 'Scene background color',
20
- group: 'colors'
21
- });
22
-
23
- const audioSensitivity = viji.slider(2.0, {
24
- min: 0.1,
25
- max: 5.0,
26
- step: 0.1,
27
- label: 'Audio Sensitivity',
28
- description: 'Overall sensitivity to audio input',
29
- group: 'audio',
30
- category: 'audio'
31
- });
32
-
33
- const bassBoost = viji.slider(1.5, {
34
- min: 0.5,
35
- max: 3.0,
36
- step: 0.1,
37
- label: 'Bass Boost',
38
- description: 'Amplification for bass frequencies',
39
- group: 'audio',
40
- category: 'audio'
41
- });
42
-
43
- const trebleBoost = viji.slider(1.2, {
44
- min: 0.5,
45
- max: 3.0,
46
- step: 0.1,
47
- label: 'Treble Boost',
48
- description: 'Amplification for treble frequencies',
49
- group: 'audio',
50
- category: 'audio'
51
- });
52
-
53
- const barCount = viji.slider(64, {
54
- min: 8,
55
- max: 128,
56
- step: 8,
57
- label: 'Bar Count',
58
- description: 'Number of frequency analysis bars',
59
- group: 'visual'
60
- });
61
-
62
- const barSpacing = viji.slider(2, {
63
- min: 0,
64
- max: 10,
65
- step: 1,
66
- label: 'Bar Spacing',
67
- description: 'Space between frequency bars',
68
- group: 'visual'
69
- });
70
-
71
- const showWaveform = viji.toggle(true, {
72
- label: 'Show Waveform',
73
- description: 'Display audio waveform overlay',
74
- group: 'visual'
75
- });
76
-
77
- const showCenterCircle = viji.toggle(true, {
78
- label: 'Show Center Circle',
79
- description: 'Show reactive circle in center',
80
- group: 'visual'
81
- });
82
-
83
- const rotationSpeed = viji.slider(0.5, {
84
- min: -2.0,
85
- max: 2.0,
86
- step: 0.1,
87
- label: 'Rotation Speed',
88
- description: 'Speed of circular rotation',
89
- group: 'animation'
90
- });
91
-
92
- const pulseIntensity = viji.slider(0.8, {
93
- min: 0.0,
94
- max: 2.0,
95
- step: 0.1,
96
- label: 'Pulse Intensity',
97
- description: 'Intensity of audio-driven pulsing',
98
- group: 'animation'
99
- });
100
-
101
- const smoothing = viji.slider(0.7, {
102
- min: 0.0,
103
- max: 0.95,
104
- step: 0.05,
105
- label: 'Smoothing',
106
- description: 'Temporal smoothing of audio data',
107
- group: 'animation'
108
- });
109
-
110
- const mouseInfluence = viji.toggle(true, {
111
- label: 'Mouse Influence',
112
- description: 'Allow mouse to affect visualization',
113
- group: 'interaction',
114
- category: 'interaction'
115
- });
116
-
117
- const mouseRadius = viji.slider(100, {
118
- min: 50,
119
- max: 300,
120
- step: 10,
121
- label: 'Mouse Radius',
122
- description: 'Radius of mouse influence',
123
- group: 'interaction',
124
- category: 'interaction'
125
- });
126
-
127
- // Render function using parameter object API with interactions, audio, and video
128
- function render(viji) {
129
- const ctx = viji.useContext('2d');
130
-
131
- // Get interaction state
132
- const mouse = viji.mouse;
133
- const keyboard = viji.keyboard;
134
- const touches = viji.touches;
135
-
136
- // Get audio state
137
- const audio = viji.audio;
138
-
139
- // Get video state
140
- const video = viji.video;
141
-
142
- // Clear background
143
- ctx.fillStyle = backgroundColor.value;
144
- ctx.fillRect(0, 0, viji.width, viji.height);
145
-
146
- // Only proceed if audio is connected
147
- if (!audio || !audio.isConnected) {
148
- // Show instructions when no audio
149
- ctx.fillStyle = primaryColor.value;
150
- ctx.font = '24px Arial';
151
- ctx.textAlign = 'center';
152
- ctx.fillText('🎵 Connect audio to see visualization', viji.width / 2, viji.height / 2);
153
-
154
- ctx.font = '16px Arial';
155
- ctx.fillStyle = secondaryColor.value;
156
- ctx.fillText('Use the audio controls to connect microphone, file, or screen audio', viji.width / 2, viji.height / 2 + 40);
157
- return;
158
- }
159
-
160
- const centerX = viji.width / 2;
161
- const centerY = viji.height / 2;
162
-
163
- // Get audio data with parameters
164
- const currentVolume = (((audio && audio.volume && audio.volume.rms) || 0)) * audioSensitivity.value;
165
- const bassEnergy = ((((audio && audio.bands && audio.bands.bass) || 0) + ((audio && audio.bands && audio.bands.subBass) || 0)) / 2) * bassBoost.value;
166
- const trebleEnergy = ((((audio && audio.bands && audio.bands.treble) || 0) + ((audio && audio.bands && audio.bands.presence) || 0)) / 2) * trebleBoost.value;
167
-
168
- // Center reactive circle
169
- if (showCenterCircle.value) {
170
- const circleRadius = 50 + (currentVolume * pulseIntensity.value * 100);
171
- const circleAlpha = Math.min(1, currentVolume * 2);
172
-
173
- // Outer circle (bass reactive)
174
- ctx.beginPath();
175
- ctx.arc(centerX, centerY, circleRadius + (bassEnergy * 50), 0, Math.PI * 2);
176
- ctx.strokeStyle = primaryColor.value + Math.floor(circleAlpha * 128).toString(16).padStart(2, '0');
177
- ctx.lineWidth = 3;
178
- ctx.stroke();
179
-
180
- // Inner circle (treble reactive)
181
- ctx.beginPath();
182
- ctx.arc(centerX, centerY, circleRadius * 0.6 + (trebleEnergy * 30), 0, Math.PI * 2);
183
- ctx.fillStyle = secondaryColor.value + Math.floor(circleAlpha * 64).toString(16).padStart(2, '0');
184
- ctx.fill();
185
- }
186
-
187
- // Frequency bars in circular arrangement
188
- const radius = Math.min(viji.width, viji.height) * 0.3;
189
- const bands = ['subBass', 'bass', 'lowMid', 'mid', 'highMid', 'presence', 'brilliance', 'treble'];
190
-
191
- for (let i = 0; i < barCount.value; i++) {
192
- const angle = (i / barCount.value) * Math.PI * 2 + (viji.time * rotationSpeed.value);
193
- const bandIndex = Math.floor((i / barCount.value) * bands.length);
194
- const bandValue = (audio && audio.bands ? (audio.bands[bands[bandIndex]] || 0) : 0);
195
-
196
- // Apply audio sensitivity
197
- const barHeight = bandValue * audioSensitivity.value * 200;
198
-
199
- // Mouse influence
200
- let mouseInfluenceValue = 1;
201
- if (mouseInfluence.value && mouse && mouse.isInCanvas) {
202
- const barX = centerX + Math.cos(angle) * radius;
203
- const barY = centerY + Math.sin(angle) * radius;
204
- const distToMouse = Math.sqrt((barX - mouse.x) ** 2 + (barY - mouse.y) ** 2);
205
-
206
- if (distToMouse < mouseRadius.value) {
207
- mouseInfluenceValue = 1 + (1 - distToMouse / mouseRadius.value) * 2;
208
- }
209
- }
210
-
211
- const finalBarHeight = barHeight * mouseInfluenceValue;
212
-
213
- // Calculate bar position
214
- const startX = centerX + Math.cos(angle) * radius;
215
- const startY = centerY + Math.sin(angle) * radius;
216
- const endX = centerX + Math.cos(angle) * (radius + finalBarHeight);
217
- const endY = centerY + Math.sin(angle) * (radius + finalBarHeight);
218
-
219
- // Color based on frequency band
220
- const hue = (bandIndex / bands.length) * 360;
221
- const intensity = Math.min(1, bandValue * audioSensitivity.value * 3);
222
- ctx.strokeStyle = `hsla(${hue}, 80%, ${50 + intensity * 30}%, ${0.6 + intensity * 0.4})`;
223
- ctx.lineWidth = Math.max(1, (viji.width / barCount.value) - barSpacing.value);
224
-
225
- // Draw frequency bar
226
- ctx.beginPath();
227
- ctx.moveTo(startX, startY);
228
- ctx.lineTo(endX, endY);
229
- ctx.stroke();
230
- }
231
-
232
- // Waveform overlay
233
- if (showWaveform.value && audio.isConnected) {
234
- ctx.strokeStyle = primaryColor.value + '60';
235
- ctx.lineWidth = 2;
236
- ctx.beginPath();
237
-
238
- // Simulate waveform
239
- for (let x = 0; x < viji.width; x += 4) {
240
- const waveY = centerY + Math.sin(x * 0.02 + viji.time * 3 + bassEnergy * 10) *
241
- (currentVolume * 50) +
242
- Math.sin(x * 0.05 + viji.time * 5 + trebleEnergy * 15) *
243
- (currentVolume * 25);
244
-
245
- if (x === 0) {
246
- ctx.moveTo(x, waveY);
247
- } else {
248
- ctx.lineTo(x, waveY);
249
- }
250
- }
251
- ctx.stroke();
252
- }
253
-
254
- // Audio info display
255
- ctx.fillStyle = primaryColor.value + 'CC';
256
- ctx.font = '14px monospace';
257
- ctx.textAlign = 'left';
258
-
259
- let infoY = 20;
260
- ctx.fillText('🎵 AUDIO REACTIVE VISUALIZER', 20, infoY);
261
- infoY += 20;
262
- ctx.fillText(`Volume: ${(currentVolume * 100).toFixed(1)}%`, 20, infoY);
263
- infoY += 16;
264
- ctx.fillText(`Bass: ${(bassEnergy * 100).toFixed(1)}%`, 20, infoY);
265
- infoY += 16;
266
- ctx.fillText(`Treble: ${(trebleEnergy * 100).toFixed(1)}%`, 20, infoY);
267
- infoY += 16;
268
- ctx.fillText(`Bars: ${barCount.value}`, 20, infoY);
269
-
270
- // Mouse interaction indicator
271
- if (mouseInfluence.value && mouse && mouse.isInCanvas) {
272
- ctx.strokeStyle = secondaryColor.value + '80';
273
- ctx.lineWidth = 2;
274
- ctx.setLineDash([5, 5]);
275
- ctx.beginPath();
276
- ctx.arc(mouse.x, mouse.y, mouseRadius.value, 0, Math.PI * 2);
277
- ctx.stroke();
278
- ctx.setLineDash([]);
279
- }
280
-
281
- // Performance info
282
- ctx.fillStyle = secondaryColor.value + 'AA';
283
- ctx.font = '12px monospace';
284
- ctx.textAlign = 'right';
285
- ctx.fillText(`Frame: ${viji.frameCount}`, viji.width - 20, viji.height - 25);
286
- ctx.fillText(`Time: ${viji.time.toFixed(1)}s`, viji.width - 20, viji.height - 10);
287
- }