Skip to content

Commit 5984eee

Browse files
committed
feat: add fit prop to InnerSequence and ScrollTimelineProvider for canvas object-fit control
1 parent 0cca119 commit 5984eee

3 files changed

Lines changed: 113 additions & 103 deletions

File tree

src/react/ScrollSequence.tsx

Lines changed: 94 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -3,84 +3,87 @@ import type { ScrollSequenceProps } from '../types';
33
import { useScrollSequence } from './useScrollSequence';
44
import { ScrollTimelineProvider } from './ScrollTimelineProvider';
55

6+
67
interface InnerSequenceProps {
7-
source: ScrollSequenceProps['source'];
8-
debug: boolean;
9-
memoryStrategy: ScrollSequenceProps['memoryStrategy'];
10-
lazyBuffer?: number;
11-
accessibilityLabel?: string;
12-
fallback?: React.ReactNode;
13-
onError?: (error: Error) => void;
8+
source: ScrollSequenceProps['source'];
9+
debug: boolean;
10+
memoryStrategy: ScrollSequenceProps['memoryStrategy'];
11+
lazyBuffer?: number;
12+
accessibilityLabel?: string;
13+
fit?: React.CSSProperties['objectFit']; // Add fit prop here
14+
fallback?: React.ReactNode;
15+
onError?: (error: Error) => void;
1416
}
1517

16-
const InnerSequence: React.FC<InnerSequenceProps> = ({
17-
source,
18-
debug,
18+
const InnerSequence: React.FC<InnerSequenceProps> = ({
19+
source,
20+
debug,
21+
memoryStrategy,
22+
lazyBuffer,
23+
accessibilityLabel = "Scroll sequence",
24+
fit = 'cover', // Default to cover
25+
fallback,
26+
onError
27+
}) => {
28+
const debugRef = useRef<HTMLDivElement>(null);
29+
const { canvasRef, isLoaded } = useScrollSequence({
30+
source,
31+
debugRef,
1932
memoryStrategy,
2033
lazyBuffer,
21-
accessibilityLabel = "Scroll sequence",
22-
fallback,
2334
onError
24-
}) => {
25-
const debugRef = useRef<HTMLDivElement>(null);
26-
const { canvasRef, isLoaded } = useScrollSequence({
27-
source,
28-
debugRef,
29-
memoryStrategy,
30-
lazyBuffer,
31-
onError
32-
});
33-
34-
// Fallback logic could be handled here or by parent.
35-
// If we handle it here, we overlay it?
36-
// Actually, canvas opacity handles the fade-in.
37-
// Use fallback if provided and not loaded.
35+
});
3836

39-
const canvasStyle: React.CSSProperties = {
40-
display: 'block',
41-
width: '100%',
42-
height: '100%',
43-
objectFit: 'cover',
44-
opacity: isLoaded ? 1 : 0,
45-
transition: 'opacity 0.2s ease-in',
46-
};
47-
48-
const debugStyle: React.CSSProperties = {
49-
position: 'absolute',
50-
top: '10px',
51-
left: '10px',
52-
background: 'rgba(0, 0, 0, 0.7)',
53-
color: '#00ff00',
54-
padding: '8px',
55-
borderRadius: '4px',
56-
fontFamily: 'monospace',
57-
fontSize: '12px',
58-
pointerEvents: 'none',
59-
whiteSpace: 'pre-wrap',
60-
zIndex: 9999,
61-
};
37+
// Fallback logic could be handled here or by parent.
38+
// If we handle it here, we overlay it?
39+
// Actually, canvas opacity handles the fade-in.
40+
// Use fallback if provided and not loaded.
6241

63-
return (
64-
<>
65-
{/* Render fallback behind canvas, or replace?
42+
const canvasStyle: React.CSSProperties = {
43+
display: 'block',
44+
width: '100%',
45+
height: '100%',
46+
objectFit: fit, // Use the prop
47+
opacity: isLoaded ? 1 : 0,
48+
transition: 'opacity 0.2s ease-in',
49+
};
50+
51+
const debugStyle: React.CSSProperties = {
52+
position: 'absolute',
53+
top: '10px',
54+
left: '10px',
55+
background: 'rgba(0, 0, 0, 0.7)',
56+
color: '#00ff00',
57+
padding: '8px',
58+
borderRadius: '4px',
59+
fontFamily: 'monospace',
60+
fontSize: '12px',
61+
pointerEvents: 'none',
62+
whiteSpace: 'pre-wrap',
63+
zIndex: 9999,
64+
};
65+
66+
return (
67+
<>
68+
{/* Render fallback behind canvas, or replace?
6669
If replace, we might loose the canvas ref init?
6770
Better to render both and cross-fade or just hide fallback when loaded.
6871
*/}
69-
{!isLoaded && fallback && (
70-
<div style={{ position: 'absolute', inset: 0, zIndex: 1 }}>
71-
{fallback}
72-
</div>
73-
)}
74-
75-
<canvas
76-
ref={canvasRef}
77-
style={canvasStyle}
78-
role="img"
79-
aria-label={accessibilityLabel}
80-
/>
81-
{debug && <div ref={debugRef} style={debugStyle}>Waiting for scroll...</div>}
82-
</>
83-
);
72+
{!isLoaded && fallback && (
73+
<div style={{ position: 'absolute', inset: 0, zIndex: 1 }}>
74+
{fallback}
75+
</div>
76+
)}
77+
78+
<canvas
79+
ref={canvasRef}
80+
style={canvasStyle}
81+
role="img"
82+
aria-label={accessibilityLabel}
83+
/>
84+
{debug && <div ref={debugRef} style={debugStyle}>Waiting for scroll...</div>}
85+
</>
86+
);
8487
};
8588

8689
export const ScrollSequence = React.forwardRef<HTMLDivElement, ScrollSequenceProps>(
@@ -93,41 +96,37 @@ export const ScrollSequence = React.forwardRef<HTMLDivElement, ScrollSequencePro
9396
memoryStrategy = 'eager',
9497
lazyBuffer = 10,
9598
fallback,
99+
fit = 'cover', // Default here too
96100
accessibilityLabel,
97101
onError,
102+
children // Extract children
98103
} = props;
99104

100105
// Check for reduced motion
101-
const prefersReducedMotion = React.useMemo(() => {
102-
if (typeof window !== 'undefined') {
103-
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
104-
}
105-
return false;
106-
}, []);
106+
// ... logic for reduced motion could be here or inside hook
107107

108-
// Use ScrollSequence now acts as the convenient "Bundle"
109-
// It provides the Timeline context and renders the Canvas consumer.
110108
return (
111-
<div ref={ref} className={className} style={{ width: '100%' }}>
112-
<ScrollTimelineProvider scrollLength={scrollLength}>
113-
{prefersReducedMotion && fallback ? (
114-
<div style={{ position: 'sticky', top: 0, height: '100vh', width: '100%' }}>
115-
{fallback}
116-
</div>
117-
) : (
118-
<InnerSequence
119-
source={source}
120-
debug={debug}
121-
memoryStrategy={memoryStrategy}
122-
lazyBuffer={lazyBuffer}
123-
fallback={fallback}
124-
accessibilityLabel={accessibilityLabel}
125-
onError={onError}
126-
/>
127-
)}
128-
{props.children}
129-
</ScrollTimelineProvider>
130-
</div>
109+
<ScrollTimelineProvider
110+
scrollLength={scrollLength}
111+
className={className}
112+
style={{ position: 'relative' }} // Ensure container is relative
113+
containerRef={ref as React.RefObject<HTMLDivElement>}
114+
>
115+
<InnerSequence
116+
source={source}
117+
debug={debug}
118+
memoryStrategy={memoryStrategy}
119+
lazyBuffer={lazyBuffer}
120+
fallback={fallback}
121+
accessibilityLabel={accessibilityLabel}
122+
onError={onError}
123+
fit={fit as any} // Pass fit prop
124+
/>
125+
{/* Render children ON TOP of canvas */}
126+
<div style={{ position: 'absolute', inset: 0, zIndex: 10, pointerEvents: 'none' }}>
127+
{children}
128+
</div>
129+
</ScrollTimelineProvider>
131130
);
132131
}
133132
);

src/react/ScrollTimelineProvider.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,26 @@ import { ScrollTimelineContext } from './scrollTimelineContext';
44

55
export interface ScrollTimelineProviderProps {
66
children: React.ReactNode;
7-
7+
88
/** CSS height for the scroll container (e.g., "300vh"). */
99
scrollLength?: string;
10-
10+
11+
1112
className?: string;
1213
style?: React.CSSProperties;
14+
/** Optional ref for the container element. */
15+
containerRef?: React.RefObject<HTMLDivElement>;
1316
}
1417

1518
export function ScrollTimelineProvider({
1619
children,
1720
scrollLength = '300vh',
1821
className = '',
1922
style = {},
23+
containerRef: externalRef,
2024
}: ScrollTimelineProviderProps) {
21-
const containerRef = useRef<HTMLDivElement>(null);
25+
const internalRef = useRef<HTMLDivElement>(null);
26+
const containerRef = externalRef || internalRef;
2227
const [timeline, setTimeline] = useState<ScrollTimeline | null>(null);
2328

2429
// Use layout effect to ensure timeline exists before children effects run
@@ -31,10 +36,10 @@ export function ScrollTimelineProvider({
3136

3237
// Future-proof: factory could be passed via props
3338
const instance = new ScrollTimeline(containerRef.current);
34-
39+
3540
// We do NOT call start() anymore, it starts on subscription
3641
// instance.start();
37-
42+
3843
setTimeline(instance);
3944

4045
return () => {
@@ -64,9 +69,9 @@ export function ScrollTimelineProvider({
6469

6570
return (
6671
<ScrollTimelineContext.Provider value={contextValue}>
67-
<div
68-
ref={containerRef}
69-
className={className}
72+
<div
73+
ref={containerRef}
74+
className={className}
7075
style={containerStyle}
7176
>
7277
<div style={stickyWrapperStyle}>

src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ export interface ScrollSequenceProps {
4242
/** Component to render while the sequence is loading. */
4343
fallback?: React.ReactNode;
4444

45+
/**
46+
* Object-fit property for the canvas.
47+
* Defaults to 'cover'.
48+
*/
49+
fit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';
50+
4551
/** Accessibility label for the canvas (role="img"). Defaults to "Scroll sequence". */
4652
accessibilityLabel?: string;
4753

0 commit comments

Comments
 (0)