diff --git a/animation/projects/src/dummy/scenes/dummy.tsx b/animation/projects/src/dummy/scenes/dummy.tsx index 3c6cc1c..4634f5f 100644 --- a/animation/projects/src/dummy/scenes/dummy.tsx +++ b/animation/projects/src/dummy/scenes/dummy.tsx @@ -3,6 +3,9 @@ import { makeScene2D, Code, LezerHighlighter } from '@motion-canvas/2d'; import { all, createRef, waitFor } from '@motion-canvas/core'; import { MyStyle } from '../../styles'; import { parser as parser_cpp } from '@lezer/cpp'; +import { DEFAULT } from '@motion-canvas/core/lib/signals'; +import { centerOn } from '../../utils'; +import { lines } from '@motion-canvas/2d'; import dummyCode from '@lectures/dummy.md?snippet=dummy_snippet/main.cpp'; @@ -17,11 +20,13 @@ export default makeScene2D(function* (view) { fontSize={28} fontFamily={'Fira Mono'} highlighter={CppHighlighter} - code={''} + code={dummyCode} /> ); - yield* waitFor(0.5); - yield* codeRef().code(dummyCode, 1); + yield* centerOn(codeRef(), DEFAULT, 1, 36); yield* waitFor(1); + + yield* centerOn(codeRef(), lines(3, 3), 1, 40); + yield* waitFor(3); }); diff --git a/animation/projects/src/utils.ts b/animation/projects/src/utils.ts new file mode 100644 index 0000000..4ad7e7b --- /dev/null +++ b/animation/projects/src/utils.ts @@ -0,0 +1,63 @@ +import { Code } from '@motion-canvas/2d'; +import { BBox, all, DEFAULT, ThreadGenerator } from '@motion-canvas/core'; + +export function* centerOn( + codeRef: Code, + selectionInput: any, + duration: number, + targetFontSize?: number, +): ThreadGenerator { + // We only care about the bounding box of the new selection + let bboxes: BBox[] = []; + if (selectionInput !== DEFAULT) { + // getSelectionBBox computes the bounds for the given selection using the CURRENT layout state. + // We do NOT need to set the selection signal to measure it! + bboxes = codeRef.getSelectionBBox(selectionInput); + } + + if (bboxes.length === 0) { + const fallbackAnimations: ThreadGenerator[] = [ + codeRef.selection(selectionInput, duration), + codeRef.y(0, duration) + ]; + if (targetFontSize !== undefined) { + fallbackAnimations.push(codeRef.fontSize(targetFontSize, duration)); + } + yield* all(...fallbackAnimations); + return; + } + + let minY = Infinity; + let maxY = -Infinity; + + for (const bbox of bboxes) { + if (bbox.top !== undefined && !isNaN(bbox.top)) minY = Math.min(minY, bbox.top); + if (bbox.bottom !== undefined && !isNaN(bbox.bottom)) maxY = Math.max(maxY, bbox.bottom); + } + + let centerY = (minY + maxY) / 2; + if (!isFinite(centerY) || isNaN(centerY)) { + centerY = 0; + } + + // Scale the calculated target offset based on how the font size will change. + // This perfectly centers it without needing to pollute the animation timeline + // with synchronous layout mutations. + if (targetFontSize !== undefined) { + const currentFontSize = codeRef.fontSize(); + if (currentFontSize > 0) { + centerY = centerY * (targetFontSize / currentFontSize); + } + } + + const animations: ThreadGenerator[] = [ + codeRef.selection(selectionInput, duration), + codeRef.y(-centerY, duration) + ]; + + if (targetFontSize !== undefined) { + animations.push(codeRef.fontSize(targetFontSize, duration)); + } + + yield* all(...animations); +}