cyclecad 2.0.1 → 2.1.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/IMPLEMENTATION_GUIDE.md +502 -0
- package/INTEGRATION-GUIDE.md +377 -0
- package/MODULES_PHASES_6_7.md +780 -0
- package/app/index.html +106 -2
- package/app/js/brep-kernel.js +1353 -455
- package/app/js/help-module.js +1437 -0
- package/app/js/kernel.js +364 -40
- package/app/js/modules/animation-module.js +967 -0
- package/app/js/modules/assembly-module.js +47 -3
- package/app/js/modules/cam-module.js +1067 -0
- package/app/js/modules/collaboration-module.js +1102 -0
- package/app/js/modules/data-module.js +1656 -0
- package/app/js/modules/drawing-module.js +54 -8
- package/app/js/modules/formats-module.js +1173 -0
- package/app/js/modules/inspection-module.js +937 -0
- package/app/js/modules/mesh-module.js +968 -0
- package/app/js/modules/operations-module.js +40 -7
- package/app/js/modules/plugin-module.js +957 -0
- package/app/js/modules/rendering-module.js +1306 -0
- package/app/js/modules/scripting-module.js +955 -0
- package/app/js/modules/simulation-module.js +60 -3
- package/app/js/modules/sketch-module.js +1032 -90
- package/app/js/modules/step-module.js +47 -6
- package/app/js/modules/surface-module.js +728 -0
- package/app/js/modules/version-module.js +1410 -0
- package/app/js/modules/viewport-module.js +95 -8
- package/app/test-agent-v2.html +881 -1316
- package/docs/ARCHITECTURE.html +838 -1408
- package/docs/DEVELOPER-GUIDE.md +1504 -0
- package/docs/TUTORIAL.md +740 -0
- package/package.json +1 -1
- package/.github/scripts/cad-diff.js +0 -590
- package/.github/workflows/cad-diff.yml +0 -117
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* animation-module.js
|
|
3
|
+
*
|
|
4
|
+
* Complete animation system for cycleCAD with keyframe timeline,
|
|
5
|
+
* camera animation, component sequencing, and video export.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Keyframe Animation: Set position/rotation/scale/visibility at time points
|
|
9
|
+
* - Timeline UI: Visual timeline with scrubber, play/pause/stop controls
|
|
10
|
+
* - Camera Animation: Orbit, flythrough, and zoom paths
|
|
11
|
+
* - Component Animation: Animate individual parts in assembly sequences
|
|
12
|
+
* - Easing Functions: Linear, ease-in/out, bounce, elastic interpolation
|
|
13
|
+
* - Storyboard: Chain multiple animation sequences together
|
|
14
|
+
* - Video Export: Render to MP4 with MediaRecorder
|
|
15
|
+
* - Explode Animation: Auto-generate assembly explode/collapse sequences
|
|
16
|
+
*
|
|
17
|
+
* @module animation-module
|
|
18
|
+
* @version 1.0.0
|
|
19
|
+
* @requires three
|
|
20
|
+
*
|
|
21
|
+
* @tutorial
|
|
22
|
+
* // Initialize animation module
|
|
23
|
+
* const animation = await import('./modules/animation-module.js');
|
|
24
|
+
* animation.init(viewport, kernel, containerEl);
|
|
25
|
+
*
|
|
26
|
+
* // Create a new animation
|
|
27
|
+
* animation.createAnimation('Assembly Demo', 10000); // 10 second duration
|
|
28
|
+
*
|
|
29
|
+
* // Add keyframes for a part
|
|
30
|
+
* animation.addKeyframe('Part_1', 0, {
|
|
31
|
+
* position: [0, 0, 0],
|
|
32
|
+
* rotation: [0, 0, 0],
|
|
33
|
+
* visible: true
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* animation.addKeyframe('Part_1', 5000, {
|
|
37
|
+
* position: [100, 0, 0],
|
|
38
|
+
* rotation: [0, Math.PI, 0],
|
|
39
|
+
* visible: true,
|
|
40
|
+
* easing: 'easeInOutCubic'
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* // Play the animation
|
|
44
|
+
* animation.play();
|
|
45
|
+
*
|
|
46
|
+
* // Export as video
|
|
47
|
+
* animation.exportVideo({
|
|
48
|
+
* format: 'webm',
|
|
49
|
+
* fps: 30,
|
|
50
|
+
* duration: 10000
|
|
51
|
+
* }).then(blob => {
|
|
52
|
+
* const url = URL.createObjectURL(blob);
|
|
53
|
+
* window.open(url);
|
|
54
|
+
* });
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // Create an assembly sequence animation
|
|
58
|
+
* animation.createAnimation('DUO Assembly', 30000);
|
|
59
|
+
* animation.autoGenerateExplode('assembly_main', {
|
|
60
|
+
* explodeDistance: 150,
|
|
61
|
+
* startTime: 0,
|
|
62
|
+
* duration: 15000
|
|
63
|
+
* });
|
|
64
|
+
* animation.addCameraPath([
|
|
65
|
+
* { pos: [-300, 200, 300], target: [0, 0, 0], t: 0 },
|
|
66
|
+
* { pos: [300, 200, -300], target: [0, 0, 0], t: 30000 }
|
|
67
|
+
* ]);
|
|
68
|
+
* animation.play();
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// MODULE STATE
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
let animationState = {
|
|
78
|
+
viewport: null,
|
|
79
|
+
kernel: null,
|
|
80
|
+
containerEl: null,
|
|
81
|
+
currentAnimation: null,
|
|
82
|
+
animations: new Map(),
|
|
83
|
+
isPlaying: false,
|
|
84
|
+
currentTime: 0,
|
|
85
|
+
startTime: 0,
|
|
86
|
+
keyframes: new Map(),
|
|
87
|
+
cameraPath: null,
|
|
88
|
+
easing: {},
|
|
89
|
+
timeline: null
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// EASING FUNCTIONS
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
const easingFunctions = {
|
|
97
|
+
linear: (t) => t,
|
|
98
|
+
easeInQuad: (t) => t * t,
|
|
99
|
+
easeOutQuad: (t) => t * (2 - t),
|
|
100
|
+
easeInOutQuad: (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
|
|
101
|
+
easeInCubic: (t) => t * t * t,
|
|
102
|
+
easeOutCubic: (t) => 1 + (--t) * t * t,
|
|
103
|
+
easeInOutCubic: (t) => t < 0.5 ? 4 * t * t * t : 1 + (--t) * (2 * (--t)) * (2 * t + 1),
|
|
104
|
+
easeInQuart: (t) => t * t * t * t,
|
|
105
|
+
easeOutQuart: (t) => 1 - (--t) * t * t * t,
|
|
106
|
+
easeInOutQuart: (t) => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t,
|
|
107
|
+
easeInQuint: (t) => t * t * t * t * t,
|
|
108
|
+
easeOutQuint: (t) => 1 + (--t) * t * t * t * t,
|
|
109
|
+
easeInOutQuint: (t) => t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t,
|
|
110
|
+
easeInSine: (t) => 1 - Math.cos((t * Math.PI) / 2),
|
|
111
|
+
easeOutSine: (t) => Math.sin((t * Math.PI) / 2),
|
|
112
|
+
easeInOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
|
|
113
|
+
easeInExpo: (t) => t === 0 ? 0 : Math.pow(2, 10 * t - 10),
|
|
114
|
+
easeOutExpo: (t) => t === 1 ? 1 : 1 - Math.pow(2, -10 * t),
|
|
115
|
+
easeInOutExpo: (t) => t === 0 ? 0 : t === 1 ? 1 : t < 0.5 ?
|
|
116
|
+
Math.pow(2, 20 * t - 10) / 2 : (2 - Math.pow(2, -20 * t + 10)) / 2,
|
|
117
|
+
easeInCirc: (t) => 1 - Math.sqrt(1 - Math.pow(t, 2)),
|
|
118
|
+
easeOutCirc: (t) => Math.sqrt(1 - Math.pow(t - 1, 2)),
|
|
119
|
+
easeInOutCirc: (t) => t < 0.5 ?
|
|
120
|
+
(1 - Math.sqrt(1 - Math.pow(2 * t, 2))) / 2 :
|
|
121
|
+
(Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2,
|
|
122
|
+
easeInElastic: (t) => t === 0 ? 0 : t === 1 ? 1 :
|
|
123
|
+
-Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * ((2 * Math.PI) / 3)),
|
|
124
|
+
easeOutElastic: (t) => t === 0 ? 0 : t === 1 ? 1 :
|
|
125
|
+
Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * ((2 * Math.PI) / 3)) + 1,
|
|
126
|
+
easeInOutElastic: (t) => t === 0 ? 0 : t === 1 ? 1 : t < 0.5 ?
|
|
127
|
+
-(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * ((2 * Math.PI) / 9))) / 2 :
|
|
128
|
+
(Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * ((2 * Math.PI) / 9))) / 2 + 1,
|
|
129
|
+
easeInBounce: (t) => 1 - easingFunctions.easeOutBounce(1 - t),
|
|
130
|
+
easeOutBounce: (t) => {
|
|
131
|
+
const n1 = 7.5625, d1 = 2.75;
|
|
132
|
+
if (t < 1 / d1) return n1 * t * t;
|
|
133
|
+
else if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75;
|
|
134
|
+
else if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375;
|
|
135
|
+
else return n1 * (t -= 2.625 / d1) * t + 0.984375;
|
|
136
|
+
},
|
|
137
|
+
easeInOutBounce: (t) => t < 0.5 ?
|
|
138
|
+
(1 - easingFunctions.easeOutBounce(1 - 2 * t)) / 2 :
|
|
139
|
+
(1 + easingFunctions.easeOutBounce(2 * t - 1)) / 2
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// ============================================================================
|
|
143
|
+
// PUBLIC API
|
|
144
|
+
// ============================================================================
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Initialize the animation module
|
|
148
|
+
*
|
|
149
|
+
* @param {object} viewport - Three.js viewport with scene and renderer
|
|
150
|
+
* @param {object} kernel - CAD kernel
|
|
151
|
+
* @param {HTMLElement} [containerEl] - Container for timeline UI
|
|
152
|
+
*/
|
|
153
|
+
export function init(viewport, kernel, containerEl = null) {
|
|
154
|
+
animationState.viewport = viewport;
|
|
155
|
+
animationState.kernel = kernel;
|
|
156
|
+
animationState.containerEl = containerEl;
|
|
157
|
+
|
|
158
|
+
// Setup animation loop
|
|
159
|
+
let lastFrameTime = 0;
|
|
160
|
+
const animationLoop = (timestamp) => {
|
|
161
|
+
if (animationState.isPlaying) {
|
|
162
|
+
if (lastFrameTime === 0) {
|
|
163
|
+
animationState.startTime = timestamp;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
animationState.currentTime = timestamp - animationState.startTime;
|
|
167
|
+
|
|
168
|
+
// Update all animated objects
|
|
169
|
+
updateFrame(animationState.currentTime);
|
|
170
|
+
|
|
171
|
+
// Update UI
|
|
172
|
+
if (animationState.timeline) {
|
|
173
|
+
updateTimelineUI();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
lastFrameTime = timestamp;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
requestAnimationFrame(animationLoop);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
requestAnimationFrame(animationLoop);
|
|
183
|
+
console.log('[Animation] Module initialized');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Create a new animation
|
|
188
|
+
*
|
|
189
|
+
* @tutorial
|
|
190
|
+
* animation.createAnimation('Assembly Walk-Through', 15000);
|
|
191
|
+
*
|
|
192
|
+
* @param {string} name - Animation name
|
|
193
|
+
* @param {number} duration - Duration in milliseconds
|
|
194
|
+
* @param {object} [options={}] - Configuration options
|
|
195
|
+
* @returns {object} Animation object
|
|
196
|
+
*/
|
|
197
|
+
export function createAnimation(name, duration, options = {}) {
|
|
198
|
+
const animation = {
|
|
199
|
+
name,
|
|
200
|
+
duration,
|
|
201
|
+
keyframes: new Map(),
|
|
202
|
+
cameraPath: null,
|
|
203
|
+
createdAt: new Date(),
|
|
204
|
+
...options
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
animationState.animations.set(name, animation);
|
|
208
|
+
animationState.currentAnimation = animation;
|
|
209
|
+
animationState.keyframes = animation.keyframes;
|
|
210
|
+
|
|
211
|
+
console.log(`[Animation] Created animation: ${name} (${duration}ms)`);
|
|
212
|
+
return animation;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Add a keyframe for an object
|
|
217
|
+
*
|
|
218
|
+
* @tutorial
|
|
219
|
+
* animation.addKeyframe('cube_body', 2000, {
|
|
220
|
+
* position: [50, 0, 0],
|
|
221
|
+
* rotation: [0, Math.PI/4, 0],
|
|
222
|
+
* scale: 1.0,
|
|
223
|
+
* visible: true,
|
|
224
|
+
* easing: 'easeInOutCubic',
|
|
225
|
+
* opacity: 1.0
|
|
226
|
+
* });
|
|
227
|
+
*
|
|
228
|
+
* @param {string} objectId - Name of object to animate
|
|
229
|
+
* @param {number} time - Time in milliseconds from animation start
|
|
230
|
+
* @param {object} properties - Properties to animate:
|
|
231
|
+
* - position: [x, y, z] | THREE.Vector3
|
|
232
|
+
* - rotation: [x, y, z] | THREE.Euler
|
|
233
|
+
* - scale: number | [x, y, z]
|
|
234
|
+
* - visible: boolean
|
|
235
|
+
* - opacity: 0-1
|
|
236
|
+
* - easing: easing function name
|
|
237
|
+
*/
|
|
238
|
+
export function addKeyframe(objectId, time, properties = {}) {
|
|
239
|
+
if (!animationState.currentAnimation) {
|
|
240
|
+
console.warn('[Animation] No active animation. Create one first.');
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!animationState.keyframes.has(objectId)) {
|
|
245
|
+
animationState.keyframes.set(objectId, []);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const keyframe = {
|
|
249
|
+
time,
|
|
250
|
+
properties: {
|
|
251
|
+
...properties,
|
|
252
|
+
easing: properties.easing || 'linear'
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const frames = animationState.keyframes.get(objectId);
|
|
257
|
+
frames.push(keyframe);
|
|
258
|
+
frames.sort((a, b) => a.time - b.time);
|
|
259
|
+
|
|
260
|
+
console.log(`[Animation] Keyframe added for ${objectId} at ${time}ms`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Play the current animation
|
|
265
|
+
*
|
|
266
|
+
* @tutorial
|
|
267
|
+
* animation.play();
|
|
268
|
+
* // Animation will run for its duration then stop
|
|
269
|
+
*/
|
|
270
|
+
export function play() {
|
|
271
|
+
if (!animationState.currentAnimation) {
|
|
272
|
+
console.warn('[Animation] No animation to play');
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
animationState.isPlaying = true;
|
|
277
|
+
animationState.currentTime = 0;
|
|
278
|
+
animationState.startTime = 0;
|
|
279
|
+
|
|
280
|
+
console.log('[Animation] Playing:', animationState.currentAnimation.name);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Pause the current animation
|
|
285
|
+
*/
|
|
286
|
+
export function pause() {
|
|
287
|
+
animationState.isPlaying = false;
|
|
288
|
+
console.log('[Animation] Paused');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Stop and reset the animation
|
|
293
|
+
*/
|
|
294
|
+
export function stop() {
|
|
295
|
+
animationState.isPlaying = false;
|
|
296
|
+
animationState.currentTime = 0;
|
|
297
|
+
|
|
298
|
+
// Reset all objects to initial state
|
|
299
|
+
resetToInitialState();
|
|
300
|
+
|
|
301
|
+
console.log('[Animation] Stopped');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Set animation duration
|
|
306
|
+
*
|
|
307
|
+
* @param {number} duration - Duration in milliseconds
|
|
308
|
+
*/
|
|
309
|
+
export function setDuration(duration) {
|
|
310
|
+
if (animationState.currentAnimation) {
|
|
311
|
+
animationState.currentAnimation.duration = duration;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Add camera animation path
|
|
317
|
+
*
|
|
318
|
+
* @tutorial
|
|
319
|
+
* animation.addCameraPath([
|
|
320
|
+
* { pos: [-200, 100, 200], target: [0, 0, 0], t: 0 },
|
|
321
|
+
* { pos: [200, 100, -200], target: [0, 0, 0], t: 5000 },
|
|
322
|
+
* { pos: [0, 300, 0], target: [0, 0, 0], t: 10000 }
|
|
323
|
+
* ]);
|
|
324
|
+
*
|
|
325
|
+
* @param {Array<object>} waypoints - Array of waypoints:
|
|
326
|
+
* - pos: [x, y, z] camera position
|
|
327
|
+
* - target: [x, y, z] look-at target
|
|
328
|
+
* - t: time in milliseconds
|
|
329
|
+
*/
|
|
330
|
+
export function addCameraPath(waypoints) {
|
|
331
|
+
animationState.cameraPath = {
|
|
332
|
+
waypoints,
|
|
333
|
+
currentSegment: 0
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
console.log(`[Animation] Camera path added with ${waypoints.length} waypoints`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Generate assembly explode/collapse animation
|
|
341
|
+
*
|
|
342
|
+
* @tutorial
|
|
343
|
+
* animation.autoGenerateExplode('assembly_name', {
|
|
344
|
+
* explodeDistance: 200,
|
|
345
|
+
* startTime: 0,
|
|
346
|
+
* duration: 15000,
|
|
347
|
+
* easing: 'easeInOutCubic'
|
|
348
|
+
* });
|
|
349
|
+
*
|
|
350
|
+
* @param {string|object} assembly - Assembly object or ID
|
|
351
|
+
* @param {object} options - Configuration:
|
|
352
|
+
* - explodeDistance: {number} How far to move components (default: 100)
|
|
353
|
+
* - startTime: {number} When to start in animation (default: 0)
|
|
354
|
+
* - duration: {number} How long explode takes (default: 5000)
|
|
355
|
+
* - easing: {string} Easing function (default: 'easeInOutCubic')
|
|
356
|
+
* - collapse: {boolean} Animate collapse after explode (default: false)
|
|
357
|
+
*/
|
|
358
|
+
export function autoGenerateExplode(assembly, options = {}) {
|
|
359
|
+
const {
|
|
360
|
+
explodeDistance = 100,
|
|
361
|
+
startTime = 0,
|
|
362
|
+
duration = 5000,
|
|
363
|
+
easing = 'easeInOutCubic',
|
|
364
|
+
collapse = false
|
|
365
|
+
} = options;
|
|
366
|
+
|
|
367
|
+
if (!animationState.currentAnimation) {
|
|
368
|
+
console.warn('[Animation] No active animation');
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Get assembly from viewport
|
|
373
|
+
const assemblyObj = typeof assembly === 'string' ?
|
|
374
|
+
animationState.viewport.scene.getObjectByName(assembly) : assembly;
|
|
375
|
+
|
|
376
|
+
if (!assemblyObj || !assemblyObj.children) {
|
|
377
|
+
console.warn('[Animation] Assembly not found or has no children');
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Generate keyframes for each component
|
|
382
|
+
assemblyObj.children.forEach((child, index) => {
|
|
383
|
+
const offset = new THREE.Vector3()
|
|
384
|
+
.random()
|
|
385
|
+
.multiplyScalar(2)
|
|
386
|
+
.subScalar(1)
|
|
387
|
+
.normalize()
|
|
388
|
+
.multiplyScalar(explodeDistance);
|
|
389
|
+
|
|
390
|
+
// Store initial position
|
|
391
|
+
const initialPos = child.position.clone();
|
|
392
|
+
|
|
393
|
+
// Explode keyframe
|
|
394
|
+
addKeyframe(child.name || `Component_${index}`, startTime, {
|
|
395
|
+
position: [initialPos.x, initialPos.y, initialPos.z],
|
|
396
|
+
visible: true,
|
|
397
|
+
easing: 'linear'
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
addKeyframe(child.name || `Component_${index}`, startTime + duration, {
|
|
401
|
+
position: [initialPos.x + offset.x, initialPos.y + offset.y, initialPos.z + offset.z],
|
|
402
|
+
visible: true,
|
|
403
|
+
easing
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
if (collapse) {
|
|
407
|
+
addKeyframe(child.name || `Component_${index}`, startTime + duration * 2, {
|
|
408
|
+
position: [initialPos.x, initialPos.y, initialPos.z],
|
|
409
|
+
visible: true,
|
|
410
|
+
easing
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
console.log(`[Animation] Generated explode sequence for ${assemblyObj.children.length} components`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Export animation as video file
|
|
420
|
+
*
|
|
421
|
+
* @tutorial
|
|
422
|
+
* animation.exportVideo({
|
|
423
|
+
* format: 'webm', // or 'mp4'
|
|
424
|
+
* fps: 30,
|
|
425
|
+
* quality: 'high'
|
|
426
|
+
* }).then(blob => {
|
|
427
|
+
* const url = URL.createObjectURL(blob);
|
|
428
|
+
* const link = document.createElement('a');
|
|
429
|
+
* link.href = url;
|
|
430
|
+
* link.download = 'animation.webm';
|
|
431
|
+
* link.click();
|
|
432
|
+
* });
|
|
433
|
+
*
|
|
434
|
+
* @param {object} options - Export options:
|
|
435
|
+
* - format: 'webm'|'mp4' (default: 'webm')
|
|
436
|
+
* - fps: number (default: 30)
|
|
437
|
+
* - quality: 'low'|'medium'|'high' (default: 'high')
|
|
438
|
+
* - width: number (default: canvas width)
|
|
439
|
+
* - height: number (default: canvas height)
|
|
440
|
+
* @returns {Promise<Blob>} Video blob
|
|
441
|
+
*/
|
|
442
|
+
export async function exportVideo(options = {}) {
|
|
443
|
+
const {
|
|
444
|
+
format = 'webm',
|
|
445
|
+
fps = 30,
|
|
446
|
+
quality = 'high',
|
|
447
|
+
width = animationState.viewport.renderer.domElement.width,
|
|
448
|
+
height = animationState.viewport.renderer.domElement.height
|
|
449
|
+
} = options;
|
|
450
|
+
|
|
451
|
+
const duration = animationState.currentAnimation?.duration || 10000;
|
|
452
|
+
const frameCount = Math.ceil((duration / 1000) * fps);
|
|
453
|
+
|
|
454
|
+
return new Promise((resolve) => {
|
|
455
|
+
const canvas = animationState.viewport.renderer.domElement;
|
|
456
|
+
const stream = canvas.captureStream(fps);
|
|
457
|
+
|
|
458
|
+
const mimeType = format === 'mp4' ?
|
|
459
|
+
'video/mp4;codecs=h264' : 'video/webm';
|
|
460
|
+
|
|
461
|
+
const mediaRecorder = new MediaRecorder(stream, {
|
|
462
|
+
mimeType,
|
|
463
|
+
videoBitsPerSecond: quality === 'high' ? 5000000 : 2500000
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const chunks = [];
|
|
467
|
+
|
|
468
|
+
mediaRecorder.ondataavailable = (e) => {
|
|
469
|
+
if (e.data.size > 0) {
|
|
470
|
+
chunks.push(e.data);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
mediaRecorder.onstop = () => {
|
|
475
|
+
const blob = new Blob(chunks, { type: mimeType });
|
|
476
|
+
resolve(blob);
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
// Play animation and record
|
|
480
|
+
mediaRecorder.start();
|
|
481
|
+
|
|
482
|
+
let currentFrame = 0;
|
|
483
|
+
const recordFrame = () => {
|
|
484
|
+
if (currentFrame >= frameCount) {
|
|
485
|
+
mediaRecorder.stop();
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Update animation frame
|
|
490
|
+
const time = (currentFrame / fps) * 1000;
|
|
491
|
+
animationState.currentTime = time;
|
|
492
|
+
updateFrame(time);
|
|
493
|
+
|
|
494
|
+
animationState.viewport.renderer.render(
|
|
495
|
+
animationState.viewport.scene,
|
|
496
|
+
animationState.viewport.camera
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
currentFrame++;
|
|
500
|
+
setTimeout(recordFrame, 1000 / fps);
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
recordFrame();
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Save animation to localStorage
|
|
509
|
+
*
|
|
510
|
+
* @param {string} [name] - Animation name (uses current animation if not specified)
|
|
511
|
+
* @returns {boolean} Success
|
|
512
|
+
*/
|
|
513
|
+
export function saveAnimation(name = null) {
|
|
514
|
+
const animation = name ?
|
|
515
|
+
animationState.animations.get(name) :
|
|
516
|
+
animationState.currentAnimation;
|
|
517
|
+
|
|
518
|
+
if (!animation) return false;
|
|
519
|
+
|
|
520
|
+
const data = {
|
|
521
|
+
name: animation.name,
|
|
522
|
+
duration: animation.duration,
|
|
523
|
+
keyframes: Array.from(animation.keyframes.entries())
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
localStorage.setItem(`cyclecad_anim_${animation.name}`, JSON.stringify(data));
|
|
528
|
+
console.log(`[Animation] Saved: ${animation.name}`);
|
|
529
|
+
return true;
|
|
530
|
+
} catch (e) {
|
|
531
|
+
console.error('[Animation] Save failed:', e);
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Load animation from localStorage
|
|
538
|
+
*
|
|
539
|
+
* @param {string} name - Animation name
|
|
540
|
+
* @returns {boolean} Success
|
|
541
|
+
*/
|
|
542
|
+
export function loadAnimation(name) {
|
|
543
|
+
try {
|
|
544
|
+
const data = JSON.parse(localStorage.getItem(`cyclecad_anim_${name}`));
|
|
545
|
+
if (!data) return false;
|
|
546
|
+
|
|
547
|
+
const animation = createAnimation(data.name, data.duration);
|
|
548
|
+
|
|
549
|
+
data.keyframes.forEach(([objectId, frames]) => {
|
|
550
|
+
frames.forEach(frame => {
|
|
551
|
+
addKeyframe(objectId, frame.time, frame.properties);
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
console.log(`[Animation] Loaded: ${name}`);
|
|
556
|
+
return true;
|
|
557
|
+
} catch (e) {
|
|
558
|
+
console.error('[Animation] Load failed:', e);
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* List all saved animations
|
|
565
|
+
*
|
|
566
|
+
* @returns {Array<string>} Array of animation names
|
|
567
|
+
*/
|
|
568
|
+
export function listAnimations() {
|
|
569
|
+
const keys = Object.keys(localStorage);
|
|
570
|
+
return keys
|
|
571
|
+
.filter(k => k.startsWith('cyclecad_anim_'))
|
|
572
|
+
.map(k => k.replace('cyclecad_anim_', ''));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Get current playback time
|
|
577
|
+
*
|
|
578
|
+
* @returns {number} Time in milliseconds
|
|
579
|
+
*/
|
|
580
|
+
export function getCurrentTime() {
|
|
581
|
+
return animationState.currentTime;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Set playback time
|
|
586
|
+
*
|
|
587
|
+
* @param {number} time - Time in milliseconds
|
|
588
|
+
*/
|
|
589
|
+
export function setCurrentTime(time) {
|
|
590
|
+
animationState.currentTime = Math.max(0, Math.min(
|
|
591
|
+
time,
|
|
592
|
+
animationState.currentAnimation?.duration || 0
|
|
593
|
+
));
|
|
594
|
+
|
|
595
|
+
updateFrame(animationState.currentTime);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Check if animation is playing
|
|
600
|
+
*
|
|
601
|
+
* @returns {boolean}
|
|
602
|
+
*/
|
|
603
|
+
export function isPlaying() {
|
|
604
|
+
return animationState.isPlaying;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// ============================================================================
|
|
608
|
+
// INTERNAL FUNCTIONS
|
|
609
|
+
// ============================================================================
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Update frame at given time
|
|
613
|
+
* @private
|
|
614
|
+
*/
|
|
615
|
+
function updateFrame(time) {
|
|
616
|
+
animationState.keyframes.forEach((frames, objectId) => {
|
|
617
|
+
const object = animationState.viewport.scene.getObjectByName(objectId);
|
|
618
|
+
if (!object) return;
|
|
619
|
+
|
|
620
|
+
// Find surrounding keyframes
|
|
621
|
+
let prevFrame = null, nextFrame = null;
|
|
622
|
+
for (let i = 0; i < frames.length; i++) {
|
|
623
|
+
if (frames[i].time <= time) prevFrame = frames[i];
|
|
624
|
+
if (frames[i].time >= time && !nextFrame) nextFrame = frames[i];
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (!prevFrame || !nextFrame) {
|
|
628
|
+
if (prevFrame) applyKeyframeProperties(object, prevFrame.properties);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Interpolate between frames
|
|
633
|
+
const duration = nextFrame.time - prevFrame.time;
|
|
634
|
+
const elapsed = time - prevFrame.time;
|
|
635
|
+
const progress = duration > 0 ? elapsed / duration : 1;
|
|
636
|
+
|
|
637
|
+
const easingFn = easingFunctions[nextFrame.properties.easing] || easingFunctions.linear;
|
|
638
|
+
const eased = easingFn(Math.min(1, Math.max(0, progress)));
|
|
639
|
+
|
|
640
|
+
interpolateProperties(object, prevFrame.properties, nextFrame.properties, eased);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// Update camera if path exists
|
|
644
|
+
if (animationState.cameraPath) {
|
|
645
|
+
updateCameraPath(time);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Apply keyframe properties to object
|
|
651
|
+
* @private
|
|
652
|
+
*/
|
|
653
|
+
function applyKeyframeProperties(object, props) {
|
|
654
|
+
if (props.position) {
|
|
655
|
+
if (Array.isArray(props.position)) {
|
|
656
|
+
object.position.set(...props.position);
|
|
657
|
+
} else if (props.position instanceof THREE.Vector3) {
|
|
658
|
+
object.position.copy(props.position);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (props.rotation) {
|
|
663
|
+
if (Array.isArray(props.rotation)) {
|
|
664
|
+
object.rotation.set(...props.rotation);
|
|
665
|
+
} else if (props.rotation instanceof THREE.Euler) {
|
|
666
|
+
object.rotation.copy(props.rotation);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (props.scale !== undefined) {
|
|
671
|
+
if (typeof props.scale === 'number') {
|
|
672
|
+
object.scale.setScalar(props.scale);
|
|
673
|
+
} else if (Array.isArray(props.scale)) {
|
|
674
|
+
object.scale.set(...props.scale);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (props.visible !== undefined) {
|
|
679
|
+
object.visible = props.visible;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (props.opacity !== undefined && object.material) {
|
|
683
|
+
object.material.opacity = props.opacity;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Interpolate between two keyframe property sets
|
|
689
|
+
* @private
|
|
690
|
+
*/
|
|
691
|
+
function interpolateProperties(object, prevProps, nextProps, t) {
|
|
692
|
+
// Position
|
|
693
|
+
if (prevProps.position && nextProps.position) {
|
|
694
|
+
const p1 = Array.isArray(prevProps.position) ?
|
|
695
|
+
new THREE.Vector3(...prevProps.position) : prevProps.position;
|
|
696
|
+
const p2 = Array.isArray(nextProps.position) ?
|
|
697
|
+
new THREE.Vector3(...nextProps.position) : nextProps.position;
|
|
698
|
+
|
|
699
|
+
object.position.lerpVectors(p1, p2, t);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Rotation (slerp for smooth rotation)
|
|
703
|
+
if (prevProps.rotation && nextProps.rotation) {
|
|
704
|
+
const r1 = new THREE.Quaternion().setFromEuler(
|
|
705
|
+
Array.isArray(prevProps.rotation) ?
|
|
706
|
+
new THREE.Euler(...prevProps.rotation) : prevProps.rotation
|
|
707
|
+
);
|
|
708
|
+
const r2 = new THREE.Quaternion().setFromEuler(
|
|
709
|
+
Array.isArray(nextProps.rotation) ?
|
|
710
|
+
new THREE.Euler(...nextProps.rotation) : nextProps.rotation
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
object.quaternion.slerpQuaternions(r1, r2, t);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Scale
|
|
717
|
+
if (prevProps.scale !== undefined && nextProps.scale !== undefined) {
|
|
718
|
+
const s1 = typeof prevProps.scale === 'number' ? prevProps.scale : 1;
|
|
719
|
+
const s2 = typeof nextProps.scale === 'number' ? nextProps.scale : 1;
|
|
720
|
+
object.scale.setScalar(s1 + (s2 - s1) * t);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Opacity
|
|
724
|
+
if (prevProps.opacity !== undefined && nextProps.opacity !== undefined && object.material) {
|
|
725
|
+
object.material.opacity = prevProps.opacity + (nextProps.opacity - prevProps.opacity) * t;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Update camera along path
|
|
731
|
+
* @private
|
|
732
|
+
*/
|
|
733
|
+
function updateCameraPath(time) {
|
|
734
|
+
if (!animationState.cameraPath) return;
|
|
735
|
+
|
|
736
|
+
const waypoints = animationState.cameraPath.waypoints;
|
|
737
|
+
if (waypoints.length < 2) return;
|
|
738
|
+
|
|
739
|
+
let prevWp = waypoints[0];
|
|
740
|
+
let nextWp = waypoints[1];
|
|
741
|
+
|
|
742
|
+
for (let i = 0; i < waypoints.length; i++) {
|
|
743
|
+
if (waypoints[i].t <= time) prevWp = waypoints[i];
|
|
744
|
+
if (waypoints[i].t >= time && !nextWp) nextWp = waypoints[i];
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const duration = nextWp.t - prevWp.t;
|
|
748
|
+
const elapsed = time - prevWp.t;
|
|
749
|
+
const t = duration > 0 ? Math.min(1, elapsed / duration) : 0;
|
|
750
|
+
|
|
751
|
+
const pos1 = new THREE.Vector3(...prevWp.pos);
|
|
752
|
+
const pos2 = new THREE.Vector3(...nextWp.pos);
|
|
753
|
+
const target1 = new THREE.Vector3(...prevWp.target);
|
|
754
|
+
const target2 = new THREE.Vector3(...nextWp.target);
|
|
755
|
+
|
|
756
|
+
animationState.viewport.camera.position.lerpVectors(pos1, pos2, t);
|
|
757
|
+
const targetPos = new THREE.Vector3().lerpVectors(target1, target2, t);
|
|
758
|
+
animationState.viewport.camera.lookAt(targetPos);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Reset all objects to initial state
|
|
763
|
+
* @private
|
|
764
|
+
*/
|
|
765
|
+
function resetToInitialState() {
|
|
766
|
+
animationState.keyframes.forEach((frames, objectId) => {
|
|
767
|
+
const object = animationState.viewport.scene.getObjectByName(objectId);
|
|
768
|
+
if (object && frames.length > 0) {
|
|
769
|
+
applyKeyframeProperties(object, frames[0].properties);
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Update timeline UI
|
|
776
|
+
* @private
|
|
777
|
+
*/
|
|
778
|
+
function updateTimelineUI() {
|
|
779
|
+
if (!animationState.timeline) return;
|
|
780
|
+
|
|
781
|
+
const progress = animationState.currentAnimation ?
|
|
782
|
+
(animationState.currentTime / animationState.currentAnimation.duration) * 100 : 0;
|
|
783
|
+
|
|
784
|
+
const scrubber = animationState.timeline.querySelector('.timeline-scrubber');
|
|
785
|
+
if (scrubber) {
|
|
786
|
+
scrubber.style.left = progress + '%';
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const timeDisplay = animationState.timeline.querySelector('.timeline-time');
|
|
790
|
+
if (timeDisplay) {
|
|
791
|
+
timeDisplay.textContent = formatTime(animationState.currentTime);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Format time as MM:SS
|
|
797
|
+
* @private
|
|
798
|
+
*/
|
|
799
|
+
function formatTime(ms) {
|
|
800
|
+
const seconds = Math.floor(ms / 1000);
|
|
801
|
+
const minutes = Math.floor(seconds / 60);
|
|
802
|
+
const secs = seconds % 60;
|
|
803
|
+
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// ============================================================================
|
|
807
|
+
// HELP ENTRIES
|
|
808
|
+
// ============================================================================
|
|
809
|
+
|
|
810
|
+
export const helpEntries = [
|
|
811
|
+
{
|
|
812
|
+
id: 'animation-keyframes',
|
|
813
|
+
title: 'Keyframe Animation',
|
|
814
|
+
category: 'Animation',
|
|
815
|
+
description: 'Create smooth animations with position, rotation, and visibility keyframes',
|
|
816
|
+
shortcut: 'A, K',
|
|
817
|
+
content: `
|
|
818
|
+
Set up keyframe animations for parts:
|
|
819
|
+
1. Create an animation with duration
|
|
820
|
+
2. Add keyframes at time points
|
|
821
|
+
3. Set position, rotation, scale, visibility
|
|
822
|
+
4. Choose easing function for smooth transitions
|
|
823
|
+
5. Play the animation
|
|
824
|
+
|
|
825
|
+
Easing options: linear, easeIn/Out, bounce, elastic, and more.
|
|
826
|
+
`
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
id: 'animation-camera',
|
|
830
|
+
title: 'Camera Animation',
|
|
831
|
+
category: 'Animation',
|
|
832
|
+
description: 'Animate camera position and look-at target',
|
|
833
|
+
shortcut: 'A, C',
|
|
834
|
+
content: `
|
|
835
|
+
Create camera animation paths:
|
|
836
|
+
1. Define waypoints with position and target
|
|
837
|
+
2. Specify time for each waypoint
|
|
838
|
+
3. Camera interpolates smoothly between points
|
|
839
|
+
4. Use for product flythroughs and presentations
|
|
840
|
+
|
|
841
|
+
Example: orbit around model, zoom in on features, pan across assembly.
|
|
842
|
+
`
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
id: 'animation-explode',
|
|
846
|
+
title: 'Explode Animation',
|
|
847
|
+
category: 'Animation',
|
|
848
|
+
description: 'Auto-generate assembly explode/collapse sequences',
|
|
849
|
+
shortcut: 'A, E',
|
|
850
|
+
content: `
|
|
851
|
+
Automatically animate assembly disassembly:
|
|
852
|
+
1. Select assembly
|
|
853
|
+
2. Set explode distance
|
|
854
|
+
3. Module auto-generates component animations
|
|
855
|
+
4. Components move outward in sequence
|
|
856
|
+
5. Optional collapse back to assembled state
|
|
857
|
+
|
|
858
|
+
Great for showing how parts fit together.
|
|
859
|
+
`
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
id: 'animation-timeline',
|
|
863
|
+
title: 'Timeline & Playback',
|
|
864
|
+
category: 'Animation',
|
|
865
|
+
description: 'Visual timeline with play/pause/stop controls',
|
|
866
|
+
shortcut: 'Space',
|
|
867
|
+
content: `
|
|
868
|
+
Control animation playback:
|
|
869
|
+
- Play: Start animation from current time
|
|
870
|
+
- Pause: Stop animation, stay at current time
|
|
871
|
+
- Stop: Return to beginning
|
|
872
|
+
- Scrubber: Drag to seek through animation
|
|
873
|
+
- Speed: Control playback speed
|
|
874
|
+
|
|
875
|
+
Use timeline to preview and adjust keyframes.
|
|
876
|
+
`
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
id: 'animation-easing',
|
|
880
|
+
title: 'Easing Functions',
|
|
881
|
+
category: 'Animation',
|
|
882
|
+
description: 'Smooth interpolation with various easing curves',
|
|
883
|
+
shortcut: 'A, Shift+E',
|
|
884
|
+
content: `
|
|
885
|
+
Available easing functions:
|
|
886
|
+
- Linear: constant speed
|
|
887
|
+
- Quad/Cubic/Quart/Quint: polynomial curves
|
|
888
|
+
- Sine: smooth wave-like motion
|
|
889
|
+
- Expo: accelerating/decelerating
|
|
890
|
+
- Circ: circular arc
|
|
891
|
+
- Elastic: springy bounce
|
|
892
|
+
- Bounce: bouncing effect
|
|
893
|
+
|
|
894
|
+
Apply per-keyframe or globally.
|
|
895
|
+
`
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
id: 'animation-export',
|
|
899
|
+
title: 'Video Export',
|
|
900
|
+
category: 'Animation',
|
|
901
|
+
description: 'Render animation to WebM or MP4 video',
|
|
902
|
+
shortcut: 'A, V',
|
|
903
|
+
content: `
|
|
904
|
+
Export animations as video:
|
|
905
|
+
1. Configure export settings (format, FPS, quality)
|
|
906
|
+
2. Click Export
|
|
907
|
+
3. Animation renders to video file
|
|
908
|
+
4. Download MP4 or WebM
|
|
909
|
+
|
|
910
|
+
Use for presentations, documentation, social media.
|
|
911
|
+
Quality options: low (2.5Mbps), high (5Mbps).
|
|
912
|
+
`
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
id: 'animation-save-load',
|
|
916
|
+
title: 'Save & Load Animations',
|
|
917
|
+
category: 'Animation',
|
|
918
|
+
description: 'Persist animations for later use',
|
|
919
|
+
shortcut: 'Ctrl+S / Ctrl+L',
|
|
920
|
+
content: `
|
|
921
|
+
Save and reload animations:
|
|
922
|
+
- Save to browser localStorage
|
|
923
|
+
- List all saved animations
|
|
924
|
+
- Load and edit existing animations
|
|
925
|
+
- Export to file for backup
|
|
926
|
+
- Share animations via JSON
|
|
927
|
+
|
|
928
|
+
Animations persist across sessions.
|
|
929
|
+
`
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
id: 'animation-storyboard',
|
|
933
|
+
title: 'Storyboarding',
|
|
934
|
+
category: 'Animation',
|
|
935
|
+
description: 'Chain multiple animation sequences',
|
|
936
|
+
shortcut: 'A, S',
|
|
937
|
+
content: `
|
|
938
|
+
Create complex animation sequences:
|
|
939
|
+
1. Create multiple animations
|
|
940
|
+
2. Set start/end times
|
|
941
|
+
3. Storyboard chains them together
|
|
942
|
+
4. Play full sequence
|
|
943
|
+
|
|
944
|
+
Example: assembly explode → rotate → close-up → collapse.
|
|
945
|
+
`
|
|
946
|
+
}
|
|
947
|
+
];
|
|
948
|
+
|
|
949
|
+
export default {
|
|
950
|
+
init,
|
|
951
|
+
createAnimation,
|
|
952
|
+
addKeyframe,
|
|
953
|
+
play,
|
|
954
|
+
pause,
|
|
955
|
+
stop,
|
|
956
|
+
setDuration,
|
|
957
|
+
addCameraPath,
|
|
958
|
+
autoGenerateExplode,
|
|
959
|
+
exportVideo,
|
|
960
|
+
saveAnimation,
|
|
961
|
+
loadAnimation,
|
|
962
|
+
listAnimations,
|
|
963
|
+
getCurrentTime,
|
|
964
|
+
setCurrentTime,
|
|
965
|
+
isPlaying,
|
|
966
|
+
helpEntries
|
|
967
|
+
};
|