diff --git a/marketing/product-film/src/components/Crane.tsx b/marketing/product-film/src/components/Crane.tsx new file mode 100644 index 0000000..6596614 --- /dev/null +++ b/marketing/product-film/src/components/Crane.tsx @@ -0,0 +1,52 @@ +// Blueprint tower-crane line art, ported from src/pages/Landing.jsx (dark-theme colors inlined). +const STRONG = "rgba(255,255,255,0.16)"; +const MEDIUM = "rgba(255,255,255,0.10)"; +const SUBTLE = "rgba(255,255,255,0.06)"; +const SURFACE = "rgba(255,255,255,0.03)"; + +export const Crane: React.FC<{ width?: number }> = ({ width = 1100 }) => ( + + {/* tower rails */} + + + {[93,131,169,207,245,283,321,359,397,435].map((y) => ( + + ))} + {/* X-bracing */} + {[[93,131],[169,207],[245,283],[321,359],[359,397],[397,435],[435,460]].map(([a,b],i)=>( + + + + + ))} + {/* slewing platform + cab */} + + + + {/* A-frame apex */} + + + + {/* support cables */} + + + {/* counter-jib */} + + + + + + {/* main jib */} + + + + {[[215,260],[280,325],[345,390],[410,455],[475,520],[540,585],[605,650],[670,715]].map(([a,b],i)=>( + + ))} + {/* trolley + hoist cable + hook */} + + + + + +); diff --git a/marketing/product-film/src/components/CraneCard.tsx b/marketing/product-film/src/components/CraneCard.tsx new file mode 100644 index 0000000..2c253cc --- /dev/null +++ b/marketing/product-film/src/components/CraneCard.tsx @@ -0,0 +1,47 @@ +import { useCurrentFrame, useVideoConfig, spring, interpolate } from "remotion"; +import { theme } from "../theme"; +import { swingRotation } from "./craneSwing"; + +export const CraneCard: React.FC<{ dropStartFrame?: number }> = ({ dropStartFrame = 0 }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const local = frame - dropStartFrame; + + // descend into place over the first ~1.2s + const drop = spring({ frame: local, fps, durationInFrames: 36, config: { damping: 14, mass: 0.8 } }); + const dropY = interpolate(drop, [0, 1], [-220, 0]); + const rotate = swingRotation(Math.max(0, local), fps); + + return ( +
+ {/* attachment point connecting to the cable */} +
+
+
+
✓ Project Ready
+
Ready to Build?
+
+ Whether you protect one home or manage a thousand roofs, the future of roofing starts here. +
+
+
Schedule Demo →
+
Start Free Trial →
+
+
+ ); +}; diff --git a/marketing/product-film/src/components/craneSwing.test.ts b/marketing/product-film/src/components/craneSwing.test.ts new file mode 100644 index 0000000..2404352 --- /dev/null +++ b/marketing/product-film/src/components/craneSwing.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from "vitest"; +import { swingRotation } from "./craneSwing"; + +describe("swingRotation", () => { + it("starts at the +3deg keyframe, amplified by the entry boost", () => { + // t=0: base = +3, boost = 3 -> 9deg + expect(swingRotation(0, 30)).toBeCloseTo(9, 5); + }); + it("settles to within +/-3deg after the settle window", () => { + for (let f = 30 * 3; f < 30 * 12; f++) { + expect(Math.abs(swingRotation(f, 30))).toBeLessThanOrEqual(3.0001); + } + }); + it("is periodic at 4s once settled (same phase 4s apart)", () => { + const a = swingRotation(30 * 4, 30); // t=4s + const b = swingRotation(30 * 8, 30); // t=8s + expect(a).toBeCloseTo(b, 5); + }); +}); diff --git a/marketing/product-film/src/components/craneSwing.ts b/marketing/product-film/src/components/craneSwing.ts new file mode 100644 index 0000000..cb76e3e --- /dev/null +++ b/marketing/product-film/src/components/craneSwing.ts @@ -0,0 +1,10 @@ +// Reproduces the landing page .crane-swing animation: rotate(+3deg) <-> rotate(-3deg), +// 4s ease-in-out loop, transform-origin top center. A cosine approximates the eased pendulum. +// An entry "boost" damps a larger initial swing down to +/-3deg over settleSeconds. +export function swingRotation(frame: number, fps: number, settleSeconds = 2): number { + const t = frame / fps; + const period = 4; // seconds + const base = 3 * Math.cos((2 * Math.PI) / period * t); // +3 at t=0 + const boost = 1 + 2 * Math.max(0, 1 - t / settleSeconds); // 3x -> 1x across settle window + return base * boost; +}