/** * Night mode toggle / מתג מצב לילה (student motion library, id "ui-toggle") * Use: copy this file into your Remotion project and register it in your Root: * * Render transparent: npx remotion render src/index.ts LibUiToggle out.webm --codec=vp9 --pixel-format=yuva420p --image-format=png --props=props.json * Change the words / colours / side in props.json (same keys as toggleDefaults). A vertical video: width 1080, height 1920. */ // ---------------------------------------------------------------- helpers (shared by every library scene) // Conventions: 1920x1080, 30 fps, transparent background. Sizes follow the frame's short side, so 1080x1920 works too. // Common props: duration (s), side 'right' | 'left' | 'center', x / y (centre in % of the frame, override side), // scale, font (a CSS font-family; default Heebo). Time is in seconds; the first and last frames are empty. import React from 'react'; import {AbsoluteFill, Easing, useCurrentFrame, useVideoConfig} from 'remotion'; import {loadFont as loadHeebo} from '@remotion/google-fonts/Heebo'; const heebo = loadHeebo('normal', {weights: ['400', '500', '700', '800', '900'], subsets: ['hebrew', 'latin']}); /** Hebrew + Latin font (Google "Heebo"). props.font overrides it per scene. */ export const HEEBO = `${heebo.fontFamily}, "Helvetica Neue", Arial, sans-serif`; export const MONO = 'ui-monospace, "SF Mono", Menlo, Consolas, monospace'; export type Side = 'right' | 'left' | 'center'; export type Base = {duration?: number; side?: Side; x?: number; y?: number; scale?: number; font?: string}; export const clamp01 = (x: number) => Math.min(1, Math.max(0, x)); /** 0..1 progress of t between a and b (seconds) */ export const p01 = (t: number, a: number, b: number) => clamp01((t - a) / (b - a)); export const lerp = (a: number, b: number, k: number) => a + (b - a) * k; export const outE = Easing.bezier(0.16, 1, 0.3, 1); // fast out, long settle export const inOut = Easing.bezier(0.65, 0, 0.35, 1); export const inE = Easing.bezier(0.55, 0, 0.9, 0.45); export const backOut = (x: number, s = 1.7) => (x <= 0 ? 0 : x >= 1 ? 1 : 1 + (s + 1) * Math.pow(x - 1, 3) + s * Math.pow(x - 1, 2)); /** Damped spring 0 -> 1 starting at t0 (closed form, so any frame computes on its own). */ export const spr = (t: number, t0: number, freq = 16, damp = 9) => { const x = t - t0; return x <= 0 ? 0 : 1 - Math.exp(-damp * x) * Math.cos(freq * x); }; /** Deterministic pseudo random 0..1 */ export const rnd = (i: number, seed = 1) => { const v = Math.sin(i * 127.1 + seed * 311.7) * 43758.5453; return v - Math.floor(v); }; export const isHe = (s: string) => /[֐-׿]/.test(s); export const dirOf = (s: string): 'rtl' | 'ltr' => (isHe(s) ? 'rtl' : 'ltr'); /** "#a1b2c3" / "#abc" + alpha -> rgba() */ export const rgba = (hex: string, a = 1) => { let h = hex.replace('#', ''); if (h.length === 3) h = h.split('').map((ch) => ch + ch).join(''); const n = parseInt(h.slice(0, 6), 16); return `rgba(${(n >> 16) & 255},${(n >> 8) & 255},${n & 255},${a})`; }; export const useT = () => { const f = useCurrentFrame(); const {fps} = useVideoConfig(); return f / fps; }; /** 12345 -> "12,345" */ export const fmt = (n: number, sep = ',') => Math.round(n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, sep); /** Arrive (scale up out of a blur) and leave (shrink into a blur) for a whole group. */ export const life = (t: number, total: number, inDur = 0.45, outDur = 0.35) => { const a = p01(t, 0, inDur); const z = inOut(p01(t, total - outDur, total)); return { opacity: clamp01(a * 2.2) * (1 - z), scale: lerp(0.82, 1, backOut(a, 1.4)) * (1 - 0.08 * z), blur: (1 - outE(a)) * 12 + z * 10, style: {} as React.CSSProperties, }; }; export const lifeStyle = (t: number, total: number, inDur = 0.45, outDur = 0.35): React.CSSProperties => { const l = life(t, total, inDur, outDur); return {opacity: l.opacity, transform: `scale(${l.scale})`, filter: l.blur > 0.2 ? `blur(${l.blur}px)` : undefined}; }; /** * Places a w x h box (design pixels of a 1080-high frame) on its side of the frame and scales it with the frame's * short side and props.scale. Children are positioned inside the box. */ export const Stage: React.FC<{p: Base; w: number; h: number; children: React.ReactNode; style?: React.CSSProperties; defSide?: Side}> = ({p, w, h, children, style, defSide = 'center'}) => { const {width: W, height: H} = useVideoConfig(); const u = (Math.min(W, H) / 1080) * (p.scale ?? 1); const side = p.side ?? defSide; const cx = ((p.x ?? (side === 'right' ? 73 : side === 'left' ? 27 : 50)) / 100) * W; const cy = ((p.y ?? 50) / 100) * H; return (
{children}
); }; /** Soft two-layer drop shadow for cards on any footage */ export const SHADOW = '0 22px 48px rgba(12,12,20,0.22), 0 4px 12px rgba(12,12,20,0.14)'; /** The classic arrow cursor; (x, y) = its tip; press 0..1 = mouse down. */ export const Cursor: React.FC<{x: number; y: number; press?: number; size?: number; fill?: string; stroke?: string}> = ({x, y, press = 0, size = 1.6, fill = '#111', stroke = '#fff'}) => { const s = size * (1 - 0.12 * press); return ( ); }; /** A four-point sparkle centred on (0,0) in a -50..50 box. */ export const SPARK_PATH = 'M0 -50 C 6 -14 14 -6 50 0 C 14 6 6 14 0 50 C -6 14 -14 6 -50 0 C -14 -6 -6 -14 0 -50 Z'; export const Spark: React.FC<{x: number; y: number; size: number; color: string; rotate?: number; opacity?: number; glow?: boolean}> = ({x, y, size, color, rotate = 0, opacity = 1, glow = false}) => ( ); // ---------------------------------------------------------------- the scene /** * Settings card: three rows slide in, the cursor flips the "night mode" switch (the knob stretches on its way) and the * whole card turns dark in a circle that grows out of the switch. 4.2 s. * Inspired by toggle-switch micro-interactions (Jitter / Dribbble), redesigned: a full settings card whose theme flips. */ export type ToggleProps = Base & { title: string; rows: {label: string; on: boolean; icon?: 'moon' | 'save' | 'bell' | 'wifi' | 'none'; color?: string}[]; target: number; // which row the cursor switches accent: string; // the "on" colour themeFlip: boolean; // switching the target row turns the card dark showCursor: boolean; }; export const toggleDefaults: ToggleProps = { duration: 4.2, side: 'right', title: 'הגדרות', rows: [ {label: 'שמירה אוטומטית', on: true, icon: 'save', color: '#2FB5A6'}, {label: 'מצב לילה', on: false, icon: 'moon', color: '#3D3A8C'}, {label: 'התראות', on: true, icon: 'bell', color: '#FF7A45'}, ], target: 1, accent: '#7C5CFF', themeFlip: true, showCursor: true, }; const ICONS: Record = { moon: 'M15.5 3.5a8.5 8.5 0 1 0 5 15.2A7 7 0 0 1 15.5 3.5z', save: 'M5 4h11l3 3v13H5zM8 4v5h7V4M8 20v-6h8v6', bell: 'M12 3a5 5 0 0 0-5 5v4l-2 3h14l-2-3V8a5 5 0 0 0-5-5zM10 18a2 2 0 0 0 4 0', wifi: 'M3 9a13 13 0 0 1 18 0M6 12.5a8.5 8.5 0 0 1 12 0M9 16a4 4 0 0 1 6 0M12 19.5h.01', }; const Icon: React.FC<{name?: string; color?: string}> = ({name, color = '#7C5CFF'}) => !name || name === 'none' ? null : (
); const W = 700; const PAD = 34; const HEAD = 104; const ROW = 108; const TW = 96; const TH = 56; const KN = 46; export const UiToggle: React.FC = (props) => { const p = {...toggleDefaults, ...props}; const t = useT(); const total = p.duration!; const font = p.font ?? HEEBO; const rtl = dirOf(p.title + p.rows.map((r) => r.label).join('')) === 'rtl'; const H = HEAD + p.rows.length * ROW + 18; const T_PRESS = 1.55; const sw = p01(t, T_PRESS + 0.05, T_PRESS + 0.4); // knob travel 0..1 const swE = inOut(sw); const flip = p.themeFlip ? outE(p01(t, T_PRESS + 0.18, T_PRESS + 0.95)) : 0; // switch geometry (card coords) const tx = rtl ? PAD : W - PAD - TW; const rowY = (i: number) => HEAD + i * ROW; const ty = (i: number) => rowY(i) + (ROW - TH) / 2; const target = Math.min(p.target, p.rows.length - 1); const tcx = tx + TW / 2; const tcy = ty(target) + TH / 2; // cursor: in from below, onto the switch, press, then drift away const cin = inOut(p01(t, 0.55, 1.4)); const cout = inOut(p01(t, 2.25, 2.85)); const cx = lerp(lerp(rtl ? -60 : W + 60, tcx + 6, cin), tcx + (rtl ? -70 : 70), cout); const cy = lerp(lerp(H + 150, tcy + 4, cin), tcy + 110, cout); const press = p01(t, T_PRESS - 0.08, T_PRESS) * (1 - p01(t, T_PRESS + 0.1, T_PRESS + 0.22)); const cursorOp = p01(t, 0.55, 0.75) * (1 - p01(t, 2.7, 3.0)); const card = (dark: boolean) => { const bg = dark ? '#17161c' : '#ffffff'; const ink = dark ? '#f4f3f8' : '#17161c'; const line = dark ? 'rgba(255,255,255,0.08)' : 'rgba(23,22,28,0.07)'; return (
{p.title}
{p.rows.map((r, i) => { const k = outE(p01(t, 0.18 + i * 0.09, 0.62 + i * 0.09)); const isT = i === target; const on = isT ? swE : r.on ? 1 : 0; const stretch = isT ? Math.sin(sw * Math.PI) * 16 : 0; const kx = lerp(rtl ? TW - KN - 5 : 5, rtl ? 5 : TW - KN - 5, on) - stretch * (rtl ? 1 - on : on); return (
{i > 0 &&
}
{r.label}
{/* the switch */}
); })}
); }; const R = Math.hypot(W, H) * 1.05 * flip; return (
{card(false)} {flip > 0 &&
{card(true)}
} {p.showCursor && cursorOp > 0 && (
{press > 0 && (
)}
)}
); }; export const meta = {id: 'LibUiToggle', component: UiToggle, defaults: toggleDefaults};