@redwilly/anima 0.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.
Files changed (239) hide show
  1. package/dist/cli/SceneLoader.d.ts +22 -0
  2. package/dist/cli/SceneLoader.js +47 -0
  3. package/dist/cli/commands/export-frame.d.ts +13 -0
  4. package/dist/cli/commands/export-frame.js +60 -0
  5. package/dist/cli/commands/list-scenes.d.ts +5 -0
  6. package/dist/cli/commands/list-scenes.js +22 -0
  7. package/dist/cli/commands/preview.d.ts +5 -0
  8. package/dist/cli/commands/preview.js +11 -0
  9. package/dist/cli/commands/render.d.ts +16 -0
  10. package/dist/cli/commands/render.js +76 -0
  11. package/dist/cli/index.d.ts +2 -0
  12. package/dist/cli/index.js +63 -0
  13. package/dist/core/animations/Animation.d.ts +41 -0
  14. package/dist/core/animations/Animation.js +76 -0
  15. package/dist/core/animations/camera/Follow.d.ts +70 -0
  16. package/dist/core/animations/camera/Follow.js +69 -0
  17. package/dist/core/animations/camera/Shake.d.ts +90 -0
  18. package/dist/core/animations/camera/Shake.js +87 -0
  19. package/dist/core/animations/camera/index.d.ts +2 -0
  20. package/dist/core/animations/camera/index.js +2 -0
  21. package/dist/core/animations/categories/ExitAnimation.d.ts +17 -0
  22. package/dist/core/animations/categories/ExitAnimation.js +15 -0
  23. package/dist/core/animations/categories/IntroductoryAnimation.d.ts +16 -0
  24. package/dist/core/animations/categories/IntroductoryAnimation.js +14 -0
  25. package/dist/core/animations/categories/TransformativeAnimation.d.ts +25 -0
  26. package/dist/core/animations/categories/TransformativeAnimation.js +25 -0
  27. package/dist/core/animations/categories/index.d.ts +3 -0
  28. package/dist/core/animations/categories/index.js +3 -0
  29. package/dist/core/animations/composition/Parallel.d.ts +37 -0
  30. package/dist/core/animations/composition/Parallel.js +79 -0
  31. package/dist/core/animations/composition/Sequence.d.ts +41 -0
  32. package/dist/core/animations/composition/Sequence.js +95 -0
  33. package/dist/core/animations/composition/index.d.ts +2 -0
  34. package/dist/core/animations/composition/index.js +3 -0
  35. package/dist/core/animations/draw/Draw.d.ts +30 -0
  36. package/dist/core/animations/draw/Draw.js +122 -0
  37. package/dist/core/animations/draw/Unwrite.d.ts +30 -0
  38. package/dist/core/animations/draw/Unwrite.js +120 -0
  39. package/dist/core/animations/draw/Write.d.ts +35 -0
  40. package/dist/core/animations/draw/Write.js +119 -0
  41. package/dist/core/animations/draw/index.d.ts +3 -0
  42. package/dist/core/animations/draw/index.js +3 -0
  43. package/dist/core/animations/draw/partialPath.d.ts +6 -0
  44. package/dist/core/animations/draw/partialPath.js +138 -0
  45. package/dist/core/animations/easing/bounce.d.ts +13 -0
  46. package/dist/core/animations/easing/bounce.js +37 -0
  47. package/dist/core/animations/easing/index.d.ts +7 -0
  48. package/dist/core/animations/easing/index.js +11 -0
  49. package/dist/core/animations/easing/manim.d.ts +46 -0
  50. package/dist/core/animations/easing/manim.js +102 -0
  51. package/dist/core/animations/easing/registry.d.ts +8 -0
  52. package/dist/core/animations/easing/registry.js +25 -0
  53. package/dist/core/animations/easing/standard.d.ts +113 -0
  54. package/dist/core/animations/easing/standard.js +151 -0
  55. package/dist/core/animations/easing/types.d.ts +6 -0
  56. package/dist/core/animations/easing/types.js +0 -0
  57. package/dist/core/animations/fade/FadeIn.d.ts +17 -0
  58. package/dist/core/animations/fade/FadeIn.js +22 -0
  59. package/dist/core/animations/fade/FadeOut.d.ts +17 -0
  60. package/dist/core/animations/fade/FadeOut.js +23 -0
  61. package/dist/core/animations/fade/index.d.ts +2 -0
  62. package/dist/core/animations/fade/index.js +2 -0
  63. package/dist/core/animations/index.d.ts +11 -0
  64. package/dist/core/animations/index.js +17 -0
  65. package/dist/core/animations/keyframes/KeyframeAnimation.d.ts +33 -0
  66. package/dist/core/animations/keyframes/KeyframeAnimation.js +40 -0
  67. package/dist/core/animations/keyframes/KeyframeTrack.d.ts +31 -0
  68. package/dist/core/animations/keyframes/KeyframeTrack.js +83 -0
  69. package/dist/core/animations/keyframes/index.d.ts +4 -0
  70. package/dist/core/animations/keyframes/index.js +5 -0
  71. package/dist/core/animations/keyframes/types.d.ts +25 -0
  72. package/dist/core/animations/keyframes/types.js +6 -0
  73. package/dist/core/animations/morph/MorphTo.d.ts +22 -0
  74. package/dist/core/animations/morph/MorphTo.js +42 -0
  75. package/dist/core/animations/morph/index.d.ts +1 -0
  76. package/dist/core/animations/morph/index.js +1 -0
  77. package/dist/core/animations/transform/MoveTo.d.ts +24 -0
  78. package/dist/core/animations/transform/MoveTo.js +38 -0
  79. package/dist/core/animations/transform/Rotate.d.ts +23 -0
  80. package/dist/core/animations/transform/Rotate.js +34 -0
  81. package/dist/core/animations/transform/Scale.d.ts +23 -0
  82. package/dist/core/animations/transform/Scale.js +35 -0
  83. package/dist/core/animations/transform/index.d.ts +3 -0
  84. package/dist/core/animations/transform/index.js +3 -0
  85. package/dist/core/animations/types.d.ts +52 -0
  86. package/dist/core/animations/types.js +6 -0
  87. package/dist/core/camera/Camera.d.ts +87 -0
  88. package/dist/core/camera/Camera.js +175 -0
  89. package/dist/core/camera/CameraFrame.d.ts +242 -0
  90. package/dist/core/camera/CameraFrame.js +322 -0
  91. package/dist/core/camera/index.d.ts +4 -0
  92. package/dist/core/camera/index.js +3 -0
  93. package/dist/core/camera/types.d.ts +17 -0
  94. package/dist/core/camera/types.js +1 -0
  95. package/dist/core/errors/AnimationErrors.d.ts +12 -0
  96. package/dist/core/errors/AnimationErrors.js +37 -0
  97. package/dist/core/errors/index.d.ts +1 -0
  98. package/dist/core/errors/index.js +1 -0
  99. package/dist/core/math/Vector2/Vector2.d.ts +23 -0
  100. package/dist/core/math/Vector2/Vector2.js +46 -0
  101. package/dist/core/math/Vector2/index.d.ts +1 -0
  102. package/dist/core/math/Vector2/index.js +1 -0
  103. package/dist/core/math/bezier/BezierPath.d.ts +38 -0
  104. package/dist/core/math/bezier/BezierPath.js +264 -0
  105. package/dist/core/math/bezier/evaluators.d.ts +9 -0
  106. package/dist/core/math/bezier/evaluators.js +36 -0
  107. package/dist/core/math/bezier/index.d.ts +8 -0
  108. package/dist/core/math/bezier/index.js +6 -0
  109. package/dist/core/math/bezier/length.d.ts +5 -0
  110. package/dist/core/math/bezier/length.js +27 -0
  111. package/dist/core/math/bezier/morphing.d.ts +16 -0
  112. package/dist/core/math/bezier/morphing.js +151 -0
  113. package/dist/core/math/bezier/sampling.d.ts +7 -0
  114. package/dist/core/math/bezier/sampling.js +153 -0
  115. package/dist/core/math/bezier/split.d.ts +19 -0
  116. package/dist/core/math/bezier/split.js +44 -0
  117. package/dist/core/math/bezier/types.d.ts +8 -0
  118. package/dist/core/math/bezier/types.js +0 -0
  119. package/dist/core/math/color/Color.d.ts +28 -0
  120. package/dist/core/math/color/Color.js +60 -0
  121. package/dist/core/math/color/conversions.d.ts +17 -0
  122. package/dist/core/math/color/conversions.js +100 -0
  123. package/dist/core/math/color/index.d.ts +2 -0
  124. package/dist/core/math/color/index.js +2 -0
  125. package/dist/core/math/index.d.ts +4 -0
  126. package/dist/core/math/index.js +5 -0
  127. package/dist/core/math/matrix/Matrix3x3.d.ts +23 -0
  128. package/dist/core/math/matrix/Matrix3x3.js +91 -0
  129. package/dist/core/math/matrix/factories.d.ts +12 -0
  130. package/dist/core/math/matrix/factories.js +44 -0
  131. package/dist/core/math/matrix/index.d.ts +2 -0
  132. package/dist/core/math/matrix/index.js +2 -0
  133. package/dist/core/renderer/FrameRenderer.d.ts +37 -0
  134. package/dist/core/renderer/FrameRenderer.js +75 -0
  135. package/dist/core/renderer/ProgressReporter.d.ts +19 -0
  136. package/dist/core/renderer/ProgressReporter.js +58 -0
  137. package/dist/core/renderer/Renderer.d.ts +36 -0
  138. package/dist/core/renderer/Renderer.js +102 -0
  139. package/dist/core/renderer/drawMobject.d.ts +8 -0
  140. package/dist/core/renderer/drawMobject.js +109 -0
  141. package/dist/core/renderer/formats/index.d.ts +3 -0
  142. package/dist/core/renderer/formats/index.js +3 -0
  143. package/dist/core/renderer/formats/png.d.ts +5 -0
  144. package/dist/core/renderer/formats/png.js +7 -0
  145. package/dist/core/renderer/formats/sprite.d.ts +6 -0
  146. package/dist/core/renderer/formats/sprite.js +24 -0
  147. package/dist/core/renderer/formats/video.d.ts +8 -0
  148. package/dist/core/renderer/formats/video.js +51 -0
  149. package/dist/core/renderer/index.d.ts +7 -0
  150. package/dist/core/renderer/index.js +9 -0
  151. package/dist/core/renderer/types.d.ts +87 -0
  152. package/dist/core/renderer/types.js +13 -0
  153. package/dist/core/scene/Scene.d.ts +104 -0
  154. package/dist/core/scene/Scene.js +225 -0
  155. package/dist/core/scene/index.d.ts +2 -0
  156. package/dist/core/scene/index.js +1 -0
  157. package/dist/core/scene/types.d.ts +23 -0
  158. package/dist/core/scene/types.js +0 -0
  159. package/dist/core/serialization/animation.d.ts +23 -0
  160. package/dist/core/serialization/animation.js +176 -0
  161. package/dist/core/serialization/easingLookup.d.ts +13 -0
  162. package/dist/core/serialization/easingLookup.js +65 -0
  163. package/dist/core/serialization/index.d.ts +23 -0
  164. package/dist/core/serialization/index.js +29 -0
  165. package/dist/core/serialization/mobject.d.ts +23 -0
  166. package/dist/core/serialization/mobject.js +248 -0
  167. package/dist/core/serialization/prettyPrint.d.ts +12 -0
  168. package/dist/core/serialization/prettyPrint.js +16 -0
  169. package/dist/core/serialization/primitives.d.ts +24 -0
  170. package/dist/core/serialization/primitives.js +98 -0
  171. package/dist/core/serialization/registry.d.ts +29 -0
  172. package/dist/core/serialization/registry.js +39 -0
  173. package/dist/core/serialization/scene.d.ts +28 -0
  174. package/dist/core/serialization/scene.js +114 -0
  175. package/dist/core/serialization/types.d.ts +152 -0
  176. package/dist/core/serialization/types.js +6 -0
  177. package/dist/core/timeline/Timeline.d.ts +70 -0
  178. package/dist/core/timeline/Timeline.js +144 -0
  179. package/dist/core/timeline/index.d.ts +5 -0
  180. package/dist/core/timeline/index.js +4 -0
  181. package/dist/core/timeline/types.d.ts +29 -0
  182. package/dist/core/timeline/types.js +0 -0
  183. package/dist/index.d.ts +18 -0
  184. package/dist/index.js +22 -0
  185. package/dist/mobjects/Mobject.d.ts +98 -0
  186. package/dist/mobjects/Mobject.js +343 -0
  187. package/dist/mobjects/VGroup/VGroup.d.ts +51 -0
  188. package/dist/mobjects/VGroup/VGroup.js +142 -0
  189. package/dist/mobjects/VGroup/index.d.ts +3 -0
  190. package/dist/mobjects/VGroup/index.js +2 -0
  191. package/dist/mobjects/VGroup/layout.d.ts +20 -0
  192. package/dist/mobjects/VGroup/layout.js +139 -0
  193. package/dist/mobjects/VMobject.d.ts +106 -0
  194. package/dist/mobjects/VMobject.js +216 -0
  195. package/dist/mobjects/geometry/Arc.d.ts +8 -0
  196. package/dist/mobjects/geometry/Arc.js +46 -0
  197. package/dist/mobjects/geometry/Arrow.d.ts +7 -0
  198. package/dist/mobjects/geometry/Arrow.js +34 -0
  199. package/dist/mobjects/geometry/Circle.d.ts +4 -0
  200. package/dist/mobjects/geometry/Circle.js +10 -0
  201. package/dist/mobjects/geometry/Line.d.ts +8 -0
  202. package/dist/mobjects/geometry/Line.js +19 -0
  203. package/dist/mobjects/geometry/Point.d.ts +5 -0
  204. package/dist/mobjects/geometry/Point.js +11 -0
  205. package/dist/mobjects/geometry/Polygon.d.ts +7 -0
  206. package/dist/mobjects/geometry/Polygon.js +21 -0
  207. package/dist/mobjects/geometry/Rectangle.d.ts +6 -0
  208. package/dist/mobjects/geometry/Rectangle.js +18 -0
  209. package/dist/mobjects/geometry/index.d.ts +7 -0
  210. package/dist/mobjects/geometry/index.js +7 -0
  211. package/dist/mobjects/graph/Graph.d.ts +28 -0
  212. package/dist/mobjects/graph/Graph.js +119 -0
  213. package/dist/mobjects/graph/GraphEdge.d.ts +26 -0
  214. package/dist/mobjects/graph/GraphEdge.js +64 -0
  215. package/dist/mobjects/graph/GraphNode.d.ts +19 -0
  216. package/dist/mobjects/graph/GraphNode.js +63 -0
  217. package/dist/mobjects/graph/index.d.ts +5 -0
  218. package/dist/mobjects/graph/index.js +5 -0
  219. package/dist/mobjects/graph/layouts/circular.d.ts +8 -0
  220. package/dist/mobjects/graph/layouts/circular.js +23 -0
  221. package/dist/mobjects/graph/layouts/forceDirected.d.ts +9 -0
  222. package/dist/mobjects/graph/layouts/forceDirected.js +102 -0
  223. package/dist/mobjects/graph/layouts/index.d.ts +3 -0
  224. package/dist/mobjects/graph/layouts/index.js +3 -0
  225. package/dist/mobjects/graph/layouts/tree.d.ts +9 -0
  226. package/dist/mobjects/graph/layouts/tree.js +99 -0
  227. package/dist/mobjects/graph/types.d.ts +35 -0
  228. package/dist/mobjects/graph/types.js +0 -0
  229. package/dist/mobjects/index.d.ts +6 -0
  230. package/dist/mobjects/index.js +6 -0
  231. package/dist/mobjects/text/Glyph.d.ts +11 -0
  232. package/dist/mobjects/text/Glyph.js +72 -0
  233. package/dist/mobjects/text/Text.d.ts +19 -0
  234. package/dist/mobjects/text/Text.js +76 -0
  235. package/dist/mobjects/text/index.d.ts +4 -0
  236. package/dist/mobjects/text/index.js +3 -0
  237. package/dist/mobjects/text/types.d.ts +12 -0
  238. package/dist/mobjects/text/types.js +8 -0
  239. package/package.json +51 -0
@@ -0,0 +1,75 @@
1
+ import { createCanvas } from '@napi-rs/canvas';
2
+ import { Matrix3x3 } from '../math/matrix/Matrix3x3';
3
+ import { drawMobject } from './drawMobject';
4
+ import { MANIM_FRAME_HEIGHT } from '../camera/types';
5
+ /**
6
+ * Renders individual frames from a Scene.
7
+ * Responsible for drawing mobjects to a canvas at a specific point in time.
8
+ */
9
+ export class FrameRenderer {
10
+ scene;
11
+ width;
12
+ height;
13
+ constructor(scene, width, height) {
14
+ this.scene = scene;
15
+ this.width = width;
16
+ this.height = height;
17
+ }
18
+ /**
19
+ * Calculates the matrix that transforms Manim world coordinates to screen pixels.
20
+ *
21
+ * Manim coordinate system:
22
+ * - Origin at center of screen
23
+ * - Y-axis points up
24
+ * - frameHeight = 8.0 units
25
+ *
26
+ * Screen coordinate system:
27
+ * - Origin at top-left
28
+ * - Y-axis points down
29
+ * - Width x Height pixels
30
+ */
31
+ calculateWorldToScreenMatrix() {
32
+ const camera = this.scene.getCamera();
33
+ const viewMatrix = camera.getViewMatrix();
34
+ // Scale from Manim units to pixels
35
+ // pixelHeight / MANIM_FRAME_HEIGHT = pixels per unit
36
+ const scale = this.height / MANIM_FRAME_HEIGHT;
37
+ // Build the world-to-screen transform:
38
+ // 1. Apply camera view transform (pan, zoom, rotation)
39
+ // 2. Scale from units to pixels
40
+ // 3. Flip Y-axis (Manim Y-up to screen Y-down)
41
+ // 4. Translate origin to screen center
42
+ const scaleMatrix = Matrix3x3.scale(scale, -scale); // Flip Y
43
+ const translateToCenter = Matrix3x3.translation(this.width / 2, this.height / 2);
44
+ // Combined: translate * scale * view
45
+ return translateToCenter.multiply(scaleMatrix).multiply(viewMatrix);
46
+ }
47
+ /**
48
+ * Renders a single frame at the specified time.
49
+ */
50
+ renderFrame(time) {
51
+ const canvas = createCanvas(this.width, this.height);
52
+ const ctx = canvas.getContext('2d');
53
+ // Seek timeline to the specified time
54
+ const timeline = this.scene.getTimeline();
55
+ timeline.seek(time);
56
+ // Draw background
57
+ const bgColor = this.scene.getBackgroundColor();
58
+ ctx.fillStyle = `rgb(${bgColor.r}, ${bgColor.g}, ${bgColor.b})`;
59
+ ctx.fillRect(0, 0, this.width, this.height);
60
+ // Calculate world-to-screen matrix each frame to pick up camera animations
61
+ const worldToScreen = this.calculateWorldToScreenMatrix();
62
+ // Draw all mobjects in order
63
+ const mobjects = this.scene.getMobjects();
64
+ for (const mobject of mobjects) {
65
+ drawMobject(ctx, mobject, worldToScreen);
66
+ }
67
+ return canvas;
68
+ }
69
+ /**
70
+ * Gets the canvas dimensions.
71
+ */
72
+ getDimensions() {
73
+ return { width: this.width, height: this.height };
74
+ }
75
+ }
@@ -0,0 +1,19 @@
1
+ import type { ProgressCallback } from './types';
2
+ /**
3
+ * Tracks rendering progress and reports updates via callback.
4
+ */
5
+ export declare class ProgressReporter {
6
+ private readonly totalFrames;
7
+ private readonly onProgress?;
8
+ private readonly startTime;
9
+ private currentFrame;
10
+ constructor(totalFrames: number, onProgress?: ProgressCallback);
11
+ /**
12
+ * Report progress for the current frame.
13
+ */
14
+ reportFrame(frameIndex: number): void;
15
+ /**
16
+ * Report rendering complete.
17
+ */
18
+ complete(): void;
19
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Tracks rendering progress and reports updates via callback.
3
+ */
4
+ export class ProgressReporter {
5
+ totalFrames;
6
+ onProgress;
7
+ startTime;
8
+ currentFrame = 0;
9
+ constructor(totalFrames, onProgress) {
10
+ this.totalFrames = totalFrames;
11
+ this.onProgress = onProgress;
12
+ this.startTime = Date.now();
13
+ }
14
+ /**
15
+ * Report progress for the current frame.
16
+ */
17
+ reportFrame(frameIndex) {
18
+ this.currentFrame = frameIndex;
19
+ if (!this.onProgress)
20
+ return;
21
+ const elapsedMs = Date.now() - this.startTime;
22
+ const framesComplete = frameIndex + 1;
23
+ const percentage = this.totalFrames > 0
24
+ ? (framesComplete / this.totalFrames) * 100
25
+ : 100;
26
+ // Estimate remaining time based on average time per frame
27
+ let estimatedRemainingMs = 0;
28
+ if (framesComplete > 0 && framesComplete < this.totalFrames) {
29
+ const msPerFrame = elapsedMs / framesComplete;
30
+ const framesRemaining = this.totalFrames - framesComplete;
31
+ estimatedRemainingMs = msPerFrame * framesRemaining;
32
+ }
33
+ const progress = {
34
+ currentFrame: frameIndex,
35
+ totalFrames: this.totalFrames,
36
+ percentage: Math.min(100, Math.max(0, percentage)),
37
+ elapsedMs,
38
+ estimatedRemainingMs,
39
+ };
40
+ this.onProgress(progress);
41
+ }
42
+ /**
43
+ * Report rendering complete.
44
+ */
45
+ complete() {
46
+ if (!this.onProgress)
47
+ return;
48
+ const elapsedMs = Date.now() - this.startTime;
49
+ const progress = {
50
+ currentFrame: this.totalFrames - 1,
51
+ totalFrames: this.totalFrames,
52
+ percentage: 100,
53
+ elapsedMs,
54
+ estimatedRemainingMs: 0,
55
+ };
56
+ this.onProgress(progress);
57
+ }
58
+ }
@@ -0,0 +1,36 @@
1
+ import type { Scene } from '../scene';
2
+ import type { RenderConfig } from './types';
3
+ /**
4
+ * Main renderer for producing output from Scenes.
5
+ * Supports multiple output formats and provides progress callbacks.
6
+ */
7
+ export declare class Renderer {
8
+ /**
9
+ * Renders a scene to the specified output.
10
+ *
11
+ * @param scene The scene to render
12
+ * @param outputPath Output file or directory path (depends on format)
13
+ * @param config Optional render configuration
14
+ */
15
+ render(scene: Scene, outputPath: string, config?: RenderConfig): Promise<void>;
16
+ /**
17
+ * Renders only the last frame of a scene as a PNG.
18
+ *
19
+ * @param scene The scene to render
20
+ * @param outputPath Output PNG file path
21
+ * @param config Optional render configuration
22
+ */
23
+ renderLastFrame(scene: Scene, outputPath: string, config?: RenderConfig): Promise<void>;
24
+ /**
25
+ * Resolves partial config with scene defaults.
26
+ */
27
+ private resolveConfig;
28
+ /**
29
+ * Ensures the directory exists.
30
+ */
31
+ private ensureDirectory;
32
+ /**
33
+ * Renders a single frame (the last frame of the animation).
34
+ */
35
+ private renderSingleFrame;
36
+ }
@@ -0,0 +1,102 @@
1
+ import { FrameRenderer } from './FrameRenderer';
2
+ import { ProgressReporter } from './ProgressReporter';
3
+ import { writePng, renderSpriteSequence, renderVideo } from './formats';
4
+ import { mkdir } from 'fs/promises';
5
+ import { dirname } from 'path';
6
+ /**
7
+ * Main renderer for producing output from Scenes.
8
+ * Supports multiple output formats and provides progress callbacks.
9
+ */
10
+ export class Renderer {
11
+ /**
12
+ * Renders a scene to the specified output.
13
+ *
14
+ * @param scene The scene to render
15
+ * @param outputPath Output file or directory path (depends on format)
16
+ * @param config Optional render configuration
17
+ */
18
+ async render(scene, outputPath, config = {}) {
19
+ const resolved = this.resolveConfig(scene, config);
20
+ const totalDuration = scene.getTotalDuration();
21
+ // Handle preview quality (half resolution)
22
+ const width = resolved.quality === 'preview'
23
+ ? Math.floor(resolved.width / 2)
24
+ : resolved.width;
25
+ const height = resolved.quality === 'preview'
26
+ ? Math.floor(resolved.height / 2)
27
+ : resolved.height;
28
+ const frameRenderer = new FrameRenderer(scene, width, height);
29
+ const totalFrames = Math.max(1, Math.floor(totalDuration * resolved.frameRate) + 1);
30
+ const progressReporter = new ProgressReporter(totalFrames, resolved.onProgress);
31
+ switch (resolved.format) {
32
+ case 'sprite':
33
+ await this.ensureDirectory(outputPath);
34
+ await renderSpriteSequence(frameRenderer, outputPath, resolved.frameRate, totalDuration, progressReporter);
35
+ break;
36
+ case 'png':
37
+ await this.ensureDirectory(dirname(outputPath));
38
+ await this.renderSingleFrame(frameRenderer, outputPath, totalDuration, progressReporter);
39
+ break;
40
+ case 'mp4':
41
+ case 'webp':
42
+ case 'gif':
43
+ await this.ensureDirectory(dirname(outputPath));
44
+ await renderVideo(frameRenderer, outputPath, resolved.format, resolved.frameRate, totalDuration, progressReporter);
45
+ break;
46
+ default:
47
+ throw new Error(`Unsupported render format: ${resolved.format}`);
48
+ }
49
+ }
50
+ /**
51
+ * Renders only the last frame of a scene as a PNG.
52
+ *
53
+ * @param scene The scene to render
54
+ * @param outputPath Output PNG file path
55
+ * @param config Optional render configuration
56
+ */
57
+ async renderLastFrame(scene, outputPath, config = {}) {
58
+ const resolved = this.resolveConfig(scene, config);
59
+ const totalDuration = scene.getTotalDuration();
60
+ const width = resolved.quality === 'preview'
61
+ ? Math.floor(resolved.width / 2)
62
+ : resolved.width;
63
+ const height = resolved.quality === 'preview'
64
+ ? Math.floor(resolved.height / 2)
65
+ : resolved.height;
66
+ const frameRenderer = new FrameRenderer(scene, width, height);
67
+ const progressReporter = new ProgressReporter(1, resolved.onProgress);
68
+ await this.ensureDirectory(dirname(outputPath));
69
+ const canvas = frameRenderer.renderFrame(totalDuration);
70
+ await writePng(canvas, outputPath);
71
+ progressReporter.complete();
72
+ }
73
+ /**
74
+ * Resolves partial config with scene defaults.
75
+ */
76
+ resolveConfig(scene, config) {
77
+ return {
78
+ width: config.width ?? scene.getWidth(),
79
+ height: config.height ?? scene.getHeight(),
80
+ frameRate: config.frameRate ?? scene.getFrameRate(),
81
+ format: config.format ?? 'sprite',
82
+ quality: config.quality ?? 'production',
83
+ onProgress: config.onProgress,
84
+ };
85
+ }
86
+ /**
87
+ * Ensures the directory exists.
88
+ */
89
+ async ensureDirectory(dirPath) {
90
+ if (!dirPath)
91
+ return;
92
+ await mkdir(dirPath, { recursive: true });
93
+ }
94
+ /**
95
+ * Renders a single frame (the last frame of the animation).
96
+ */
97
+ async renderSingleFrame(frameRenderer, outputPath, totalDuration, progressReporter) {
98
+ const canvas = frameRenderer.renderFrame(totalDuration);
99
+ await writePng(canvas, outputPath);
100
+ progressReporter.complete();
101
+ }
102
+ }
@@ -0,0 +1,8 @@
1
+ import type { SKRSContext2D } from '@napi-rs/canvas';
2
+ import type { Mobject } from '../../mobjects/Mobject';
3
+ import { Matrix3x3 } from '../math/matrix/Matrix3x3';
4
+ /**
5
+ * Draws a Mobject to a canvas context.
6
+ * Handles VMobject path rendering and VGroup recursion.
7
+ */
8
+ export declare function drawMobject(ctx: SKRSContext2D, mobject: Mobject, worldToScreen: Matrix3x3): void;
@@ -0,0 +1,109 @@
1
+ import { VMobject } from '../../mobjects/VMobject';
2
+ import { VGroup } from '../../mobjects/VGroup';
3
+ /**
4
+ * Draws a Mobject to a canvas context.
5
+ * Handles VMobject path rendering and VGroup recursion.
6
+ */
7
+ export function drawMobject(ctx, mobject, worldToScreen) {
8
+ // Skip invisible mobjects
9
+ if (mobject.opacity <= 0)
10
+ return;
11
+ if (mobject instanceof VGroup) {
12
+ drawVGroup(ctx, mobject, worldToScreen);
13
+ }
14
+ else if (mobject instanceof VMobject) {
15
+ drawVMobject(ctx, mobject, worldToScreen);
16
+ }
17
+ // Base Mobject has no visual representation
18
+ }
19
+ /**
20
+ * Draws a VGroup by recursively drawing its children.
21
+ */
22
+ function drawVGroup(ctx, vgroup, worldToScreen) {
23
+ // VGroup opacity affects all children
24
+ if (vgroup.opacity <= 0)
25
+ return;
26
+ ctx.save();
27
+ ctx.globalAlpha *= vgroup.opacity;
28
+ for (const child of vgroup.getChildren()) {
29
+ drawMobject(ctx, child, worldToScreen);
30
+ }
31
+ ctx.restore();
32
+ }
33
+ /**
34
+ * Draws a VMobject's paths to the canvas.
35
+ */
36
+ function drawVMobject(ctx, vmobject, worldToScreen) {
37
+ const paths = vmobject.paths;
38
+ if (paths.length === 0)
39
+ return;
40
+ // Combine mobject's world matrix with world-to-screen transform
41
+ const mobjectWorld = vmobject.getWorldMatrix();
42
+ const transform = worldToScreen.multiply(mobjectWorld);
43
+ ctx.save();
44
+ ctx.globalAlpha *= vmobject.opacity;
45
+ // Draw each path
46
+ for (const path of paths) {
47
+ const commands = path.getCommands();
48
+ if (commands.length === 0)
49
+ continue;
50
+ ctx.beginPath();
51
+ applyPathCommands(ctx, commands, transform);
52
+ // Fill first (so stroke is on top)
53
+ if (vmobject.getFillOpacity() > 0) {
54
+ const fillColor = vmobject.getFillColor();
55
+ ctx.fillStyle = `rgba(${fillColor.r}, ${fillColor.g}, ${fillColor.b}, ${vmobject.getFillOpacity()})`;
56
+ ctx.fill();
57
+ }
58
+ // Then stroke
59
+ if (vmobject.getStrokeWidth() > 0) {
60
+ const strokeColor = vmobject.getStrokeColor();
61
+ ctx.strokeStyle = `rgba(${strokeColor.r}, ${strokeColor.g}, ${strokeColor.b}, ${strokeColor.a})`;
62
+ ctx.lineWidth = vmobject.getStrokeWidth();
63
+ ctx.lineCap = 'round';
64
+ ctx.lineJoin = 'round';
65
+ ctx.stroke();
66
+ }
67
+ }
68
+ ctx.restore();
69
+ }
70
+ /**
71
+ * Applies BezierPath commands to a canvas context.
72
+ */
73
+ function applyPathCommands(ctx, commands, transform) {
74
+ for (const cmd of commands) {
75
+ switch (cmd.type) {
76
+ case 'Move': {
77
+ const p = transform.transformPoint(cmd.end);
78
+ ctx.moveTo(p.x, p.y);
79
+ break;
80
+ }
81
+ case 'Line': {
82
+ const p = transform.transformPoint(cmd.end);
83
+ ctx.lineTo(p.x, p.y);
84
+ break;
85
+ }
86
+ case 'Quadratic': {
87
+ if (cmd.control1) {
88
+ const cp = transform.transformPoint(cmd.control1);
89
+ const ep = transform.transformPoint(cmd.end);
90
+ ctx.quadraticCurveTo(cp.x, cp.y, ep.x, ep.y);
91
+ }
92
+ break;
93
+ }
94
+ case 'Cubic': {
95
+ if (cmd.control1 && cmd.control2) {
96
+ const cp1 = transform.transformPoint(cmd.control1);
97
+ const cp2 = transform.transformPoint(cmd.control2);
98
+ const ep = transform.transformPoint(cmd.end);
99
+ ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, ep.x, ep.y);
100
+ }
101
+ break;
102
+ }
103
+ case 'Close': {
104
+ ctx.closePath();
105
+ break;
106
+ }
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,3 @@
1
+ export { writePng } from './png';
2
+ export { renderSpriteSequence } from './sprite';
3
+ export { renderVideo } from './video';
@@ -0,0 +1,3 @@
1
+ export { writePng } from './png';
2
+ export { renderSpriteSequence } from './sprite';
3
+ export { renderVideo } from './video';
@@ -0,0 +1,5 @@
1
+ import type { Canvas } from '@napi-rs/canvas';
2
+ /**
3
+ * Encodes a canvas frame to PNG and writes to file.
4
+ */
5
+ export declare function writePng(canvas: Canvas, outputPath: string): Promise<void>;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Encodes a canvas frame to PNG and writes to file.
3
+ */
4
+ export async function writePng(canvas, outputPath) {
5
+ const buffer = canvas.toBuffer('image/png');
6
+ await Bun.write(outputPath, buffer);
7
+ }
@@ -0,0 +1,6 @@
1
+ import type { FrameRenderer } from '../FrameRenderer';
2
+ import type { ProgressReporter } from '../ProgressReporter';
3
+ /**
4
+ * Renders frames to a sprite sequence (numbered PNG files).
5
+ */
6
+ export declare function renderSpriteSequence(frameRenderer: FrameRenderer, outputDir: string, frameRate: number, totalDuration: number, progressReporter: ProgressReporter): Promise<void>;
@@ -0,0 +1,24 @@
1
+ import { writePng } from './png';
2
+ import { join } from 'path';
3
+ /**
4
+ * Pads a number with leading zeros to reach the specified length.
5
+ */
6
+ function padNumber(n, length) {
7
+ return n.toString().padStart(length, '0');
8
+ }
9
+ /**
10
+ * Renders frames to a sprite sequence (numbered PNG files).
11
+ */
12
+ export async function renderSpriteSequence(frameRenderer, outputDir, frameRate, totalDuration, progressReporter) {
13
+ const totalFrames = Math.max(1, Math.floor(totalDuration * frameRate) + 1);
14
+ const digitCount = Math.max(4, totalFrames.toString().length);
15
+ for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
16
+ const time = frameIndex / frameRate;
17
+ const canvas = frameRenderer.renderFrame(time);
18
+ const filename = `frame_${padNumber(frameIndex, digitCount)}.png`;
19
+ const outputPath = join(outputDir, filename);
20
+ await writePng(canvas, outputPath);
21
+ progressReporter.reportFrame(frameIndex);
22
+ }
23
+ progressReporter.complete();
24
+ }
@@ -0,0 +1,8 @@
1
+ import type { FrameRenderer } from '../FrameRenderer';
2
+ import type { ProgressReporter } from '../ProgressReporter';
3
+ import type { RenderFormat } from '../types';
4
+ /**
5
+ * Renders a scene to a video file using FFmpeg.
6
+ * Frames are rendered sequentially and piped to FFmpeg's stdin.
7
+ */
8
+ export declare function renderVideo(frameRenderer: FrameRenderer, outputPath: string, format: RenderFormat, frameRate: number, totalDuration: number, progressReporter: ProgressReporter): Promise<void>;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Renders a scene to a video file using FFmpeg.
3
+ * Frames are rendered sequentially and piped to FFmpeg's stdin.
4
+ */
5
+ export async function renderVideo(frameRenderer, outputPath, format, frameRate, totalDuration, progressReporter) {
6
+ const { width, height } = frameRenderer.getDimensions();
7
+ const totalFrames = Math.max(1, Math.floor(totalDuration * frameRate) + 1);
8
+ // Determine FFmpeg arguments based on format
9
+ const ffmpegArgs = [
10
+ '-y', // Overwrite output
11
+ '-f', 'image2pipe',
12
+ '-vcodec', 'png', // Input format from pipe
13
+ '-r', frameRate.toString(),
14
+ '-i', '-', // Read from stdin
15
+ ];
16
+ if (format === 'mp4') {
17
+ ffmpegArgs.push('-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-crf', '18');
18
+ }
19
+ else if (format === 'webp') {
20
+ ffmpegArgs.push('-c:v', 'libwebp', '-lossless', '0', '-compression_level', '4', '-q:v', '75', '-loop', '0');
21
+ }
22
+ else if (format === 'gif') {
23
+ ffmpegArgs.push('-vf', `fps=${frameRate},scale=${width}:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse`);
24
+ }
25
+ ffmpegArgs.push(outputPath);
26
+ const process = Bun.spawn(['ffmpeg', ...ffmpegArgs], {
27
+ stdin: 'pipe',
28
+ });
29
+ try {
30
+ for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
31
+ const time = frameIndex / frameRate;
32
+ const canvas = frameRenderer.renderFrame(time);
33
+ // Convert canvas to PNG buffer and write to FFmpeg's stdin
34
+ const pngBuffer = await canvas.toBuffer('image/png');
35
+ process.stdin.write(pngBuffer);
36
+ progressReporter.reportFrame(frameIndex);
37
+ }
38
+ // Close stdin and wait for FFmpeg to finish
39
+ process.stdin.end();
40
+ const status = await process.exited;
41
+ if (status !== 0) {
42
+ throw new Error(`FFmpeg process exited with code ${status}`);
43
+ }
44
+ }
45
+ catch (error) {
46
+ // Kill FFmpeg process if something went wrong
47
+ process.kill();
48
+ throw error;
49
+ }
50
+ progressReporter.complete();
51
+ }
@@ -0,0 +1,7 @@
1
+ export { Renderer } from './Renderer';
2
+ export { FrameRenderer } from './FrameRenderer';
3
+ export { ProgressReporter } from './ProgressReporter';
4
+ export { drawMobject } from './drawMobject';
5
+ export type { RenderConfig, ResolvedRenderConfig, RenderFormat, RenderQuality, RenderProgress, ProgressCallback, } from './types';
6
+ export { Resolution } from './types';
7
+ export { writePng, renderSpriteSequence } from './formats';
@@ -0,0 +1,9 @@
1
+ // Renderer Module
2
+ // Provides frame rendering and output generation for Scenes.
3
+ export { Renderer } from './Renderer';
4
+ export { FrameRenderer } from './FrameRenderer';
5
+ export { ProgressReporter } from './ProgressReporter';
6
+ export { drawMobject } from './drawMobject';
7
+ export { Resolution } from './types';
8
+ // Format utilities
9
+ export { writePng, renderSpriteSequence } from './formats';
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Supported render output formats.
3
+ * - sprite: PNG sequence (frame_0000.png, frame_0001.png, ...)
4
+ * - png: Single PNG frame
5
+ * - mp4: H.264 video (requires FFmpeg - future)
6
+ * - webp: Animated WebP (requires FFmpeg - future)
7
+ * - gif: Animated GIF (requires FFmpeg - future)
8
+ */
9
+ export type RenderFormat = 'sprite' | 'png' | 'mp4' | 'webp' | 'gif';
10
+ /**
11
+ * Quality presets for rendering.
12
+ * - production: Full resolution, best quality
13
+ * - preview: Half resolution for faster preview
14
+ */
15
+ export type RenderQuality = 'production' | 'preview';
16
+ /**
17
+ * Standard resolution presets.
18
+ */
19
+ export declare const Resolution: {
20
+ /** 854x480 (16:9) */
21
+ readonly p480: {
22
+ readonly width: 854;
23
+ readonly height: 480;
24
+ };
25
+ /** 1280x720 (16:9) */
26
+ readonly p720: {
27
+ readonly width: 1280;
28
+ readonly height: 720;
29
+ };
30
+ /** 1920x1080 (16:9) */
31
+ readonly p1080: {
32
+ readonly width: 1920;
33
+ readonly height: 1080;
34
+ };
35
+ /** 3840x2160 (16:9) */
36
+ readonly p4K: {
37
+ readonly width: 3840;
38
+ readonly height: 2160;
39
+ };
40
+ };
41
+ /**
42
+ * Configuration for rendering.
43
+ */
44
+ export interface RenderConfig {
45
+ /** Output width in pixels. Defaults to scene width. */
46
+ width?: number;
47
+ /** Output height in pixels. Defaults to scene height. */
48
+ height?: number;
49
+ /** Frames per second. Defaults to scene frame rate. */
50
+ frameRate?: number;
51
+ /** Output format. Defaults to 'sprite'. */
52
+ format?: RenderFormat;
53
+ /** Quality preset. Defaults to 'production'. */
54
+ quality?: RenderQuality;
55
+ /** Progress callback for render updates. */
56
+ onProgress?: ProgressCallback;
57
+ }
58
+ /**
59
+ * Resolved render configuration with all values specified.
60
+ */
61
+ export interface ResolvedRenderConfig {
62
+ width: number;
63
+ height: number;
64
+ frameRate: number;
65
+ format: RenderFormat;
66
+ quality: RenderQuality;
67
+ onProgress?: ProgressCallback;
68
+ }
69
+ /**
70
+ * Progress information during rendering.
71
+ */
72
+ export interface RenderProgress {
73
+ /** Current frame being rendered (0-indexed). */
74
+ currentFrame: number;
75
+ /** Total number of frames to render. */
76
+ totalFrames: number;
77
+ /** Percentage complete (0-100). */
78
+ percentage: number;
79
+ /** Elapsed time in milliseconds. */
80
+ elapsedMs: number;
81
+ /** Estimated remaining time in milliseconds. */
82
+ estimatedRemainingMs: number;
83
+ }
84
+ /**
85
+ * Callback for progress updates during rendering.
86
+ */
87
+ export type ProgressCallback = (progress: RenderProgress) => void;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Standard resolution presets.
3
+ */
4
+ export const Resolution = {
5
+ /** 854x480 (16:9) */
6
+ p480: { width: 854, height: 480 },
7
+ /** 1280x720 (16:9) */
8
+ p720: { width: 1280, height: 720 },
9
+ /** 1920x1080 (16:9) */
10
+ p1080: { width: 1920, height: 1080 },
11
+ /** 3840x2160 (16:9) */
12
+ p4K: { width: 3840, height: 2160 },
13
+ };