-
-
Notifications
You must be signed in to change notification settings - Fork 2
Description
Summary
Add a declarative <SVG> React component to load and interpret SVG files using Two.js's load() method. This will enable developers to easily import SVG artwork created in tools like Adobe Illustrator directly into their Two.js scenes using familiar React patterns.
Motivation
Two.js provides powerful SVG interpretation capabilities via the two.load() method, which can:
- Load external SVG files from URLs
- Parse inline SVG markup strings
- Convert SVG elements into native Two.js shapes
- Enable designers to create artwork in vector editors and import directly
However, the current imperative API requires manual XHR handling and callback management. A React component would make this functionality declarative, easy to use, and consistent with the library's existing component patterns.
Proposed API
Component Signature
interface SVGProps {
// Source (one required)
src?: string; // URL to .svg file
content?: string; // Inline SVG markup string
// Positioning & Transform
x?: number;
y?: number;
scale?: number;
rotation?: number;
// Callbacks
onLoad?: (group: RefGroup, svg: SVGElement | SVGElement[]) => void;
onError?: (error: Error) => void;
// Loading behavior
shallow?: boolean; // Flatten groups when interpreting
// Group properties (fill, stroke, opacity, etc.)
fill?: string;
stroke?: string;
opacity?: number;
visible?: boolean;
linewidth?: number;
// ... all Two.Group properties
}
export type RefSVG = RefGroup;
export const SVG: React.ForwardRefExoticComponent<
SVGProps & React.RefAttributes<RefSVG>
>;Basic Usage Example
import { Canvas, SVG } from 'react-two.js';
function App() {
return (
<Canvas fullscreen>
{/* Load from external URL */}
<SVG src="/assets/logo.svg" x={100} y={100} />
{/* Load inline SVG markup */}
<SVG
content={`
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" fill="red" />
</svg>
`}
x={300}
y={100}
/>
</Canvas>
);
}With Loading State
function Logo() {
const [loaded, setLoaded] = useState(false);
const [error, setError] = useState<Error | null>(null);
if (error) {
return <Text value={`Error: ${error.message}`} />;
}
return (
<>
{!loaded && <Text value="Loading..." />}
<SVG
src="/logo.svg"
x={200}
y={200}
onLoad={(group, svg) => {
console.log('Loaded:', group.children.length, 'objects');
setLoaded(true);
}}
onError={setError}
/>
</>
);
}Advanced - Ref Access & Animation
function AnimatedSVG() {
const svgRef = useRef<RefSVG>(null);
useFrame((elapsed) => {
if (svgRef.current) {
svgRef.current.rotation = Math.sin(elapsed) * 0.5;
svgRef.current.scale = 1 + Math.sin(elapsed * 2) * 0.1;
}
});
const handleLoad = (group, svg) => {
// Manipulate loaded SVG objects
group.children.forEach((child, i) => {
child.opacity = 0;
// Fade in with delay
setTimeout(() => {
child.opacity = 1;
}, i * 100);
});
};
return (
<SVG
ref={svgRef}
src="/animated-icon.svg"
x={400}
y={300}
onLoad={handleLoad}
/>
);
}With Fallback Content
function SVGWithFallback() {
return (
<SVG src="/icon.svg" x={100} y={100}>
{/* Children shown while loading (optional pattern) */}
<Circle radius={20} fill="gray" opacity={0.5} />
</SVG>
);
}Implementation Phases
Phase 1: Core Component Structure
Files: lib/SVG.tsx (new file)
- Create
SVG.tsxwith React component boilerplate - Define
SVGPropsandRefSVGTypeScript interfaces - Implement basic component structure with
forwardRef - Add props validation (require either
srcorcontent) - Set up state management for:
ref- The loaded Group instanceloading- Loading state (boolean)error- Error state
- Integrate with
useTwo()context
Deliverable: Basic component shell with TypeScript types
Phase 2: SVG Loading Logic
Files: lib/SVG.tsx
- Implement
useEffectfor loading SVG whensrcorcontentchanges - Call
two.load(pathOrSVGContent, callback)with appropriate parameter - Handle loading from URL (when
srcprop provided) - Handle inline SVG (when
contentprop provided) - Store returned Group in component state
- Implement callback handling:
- Invoke
onLoadprop with(group, svg)when successful - Invoke
onErrorprop when loading fails
- Invoke
- Handle cleanup on unmount
Implementation pattern:
useEffect(() => {
if (!two) return;
const source = src || content;
if (!source) return;
try {
const group = two.load(source, (loadedGroup, svg) => {
set(loadedGroup);
onLoad?.(loadedGroup, svg);
});
// Store group reference immediately (even though empty)
set(group);
} catch (err) {
onError?.(err);
}
return () => {
// Cleanup
};
}, [two, src, content]);Deliverable: Working SVG loading with async handling
Phase 3: Group Integration & Positioning
Files: lib/SVG.tsx
- Add loaded Group to parent using existing patterns (see
Group.tsx) - Implement position props (
x,y) - Implement transform props (
scale,rotation) - Apply Group properties (
fill,stroke,opacity, etc.) - Handle property updates when props change
- Ensure loaded Group is added to scene correctly
- Implement proper removal on unmount
Pattern to follow:
useEffect(() => {
if (parent && ref) {
parent.add(ref);
return () => {
parent.remove(ref);
};
}
}, [parent, ref]);
useEffect(() => {
if (ref) {
if (typeof x === 'number') ref.translation.x = x;
if (typeof y === 'number') ref.translation.y = y;
if (typeof scale === 'number') ref.scale = scale;
// ... other properties
}
}, [ref, x, y, scale, ...]);Deliverable: Positioned and styled SVG groups in the scene
Phase 4: Error Handling & Edge Cases
Files: lib/SVG.tsx
- Add error handling for:
- Network errors (404, timeout, etc.)
- Invalid SVG markup
- Missing
twoinstance - Both
srcandcontentprovided (warn or prefer one) - Neither
srcnorcontentprovided
- Implement error state management
- Add TypeScript error types
- Handle XHR failures gracefully
- Add console warnings for common mistakes
- Test with various edge cases
Deliverable: Robust error handling and validation
Phase 5: Ref Forwarding & Advanced Features
Files: lib/SVG.tsx
- Implement
useImperativeHandlefor ref forwarding - Expose loaded Group via ref
- Add support for
shallowprop (pass to interpret) - Handle loading state transitions properly
- Ensure ref updates when SVG loads
- Support changing
src/contentdynamically (reload) - Add optional children support (fallback content while loading)
Deliverable: Full ref support and advanced features
Phase 6: Integration & Export
Files: lib/main.ts, lib/SVG.tsx
- Export
SVGcomponent fromlib/main.ts - Export
RefSVGtype fromlib/main.ts - Export
SVGPropsinterface (optional, for advanced users) - Verify tree-shaking works correctly
- Update package exports if needed
Deliverable: Component available in public API
Phase 7: Testing
Files: lib/__tests__/SVG.test.tsx (new file)
- Test component rendering with
srcprop - Test component rendering with
contentprop - Test loading state transitions
- Test
onLoadcallback invocation - Test
onErrorcallback invocation - Test positioning (x, y, scale, rotation)
- Test property updates (fill, stroke, etc.)
- Test ref forwarding
- Test dynamic src changes (reload behavior)
- Test cleanup on unmount
- Mock
two.load()for isolated testing - Test error scenarios (404, invalid SVG, etc.)
Deliverable: Comprehensive test coverage
Phase 8: Documentation & Examples
Files: README.md, src/App.tsx, documentation site
- Add
<SVG>section to main README - Document all props and their types
- Create interactive examples in dev app:
- Basic SVG loading from URL
- Inline SVG content
- Loading states and error handling
- Animated SVG with ref
- Dynamic SVG switching
- Add usage patterns and best practices
- Document limitations (e.g., SVG features not supported by Two.js)
- Include troubleshooting section
- Add TypeScript usage examples
- Link to Two.js SVG interpretation docs
Deliverable: Complete documentation and working examples
Technical Considerations
How Two.js load() Works
Two.js provides two.load(pathOrSVGContent, callback) which:
Parameters:
pathOrSVGContent: String - URL path ending in.svgOR inline SVG markupcallback: Optional(group, svg) => void- Called after loading completes
Behavior:
- Returns a
Two.Groupimmediately (empty initially) - If path ends with
.svg, fetches file via XHR - Otherwise, treats string as inline SVG markup
- Parses SVG into DOM elements using temporary container
- Calls
two.interpret()on each SVG child element - Populates returned Group with interpreted Two.js objects
- Invokes callback with
(group, svgElements)when complete
Related Methods:
two.interpret(svgElement, shallow, add)- Convert SVG DOM to Two.js shapestwo.xhr(path, callback)- Internal XHR utility
Design Decisions
- Component-based: Matches existing library patterns (Circle, Rectangle, etc.)
- Declarative: SVG loading feels like any other React component
- Async-friendly: Manages loading state internally, exposes via callbacks
- Dual source: Supports both external URLs (
src) and inline markup (content) - Group wrapper: Returns Group (like Two.js), inherits all Group properties
- Ref forwarding: Provides imperative access for animations
- Error handling: Graceful degradation with
onErrorcallback - TypeScript first: Full type safety for SVG properties
- Reload support: Changing
src/contenttriggers new load
Alternative Approaches Considered
1. Hook-based (useSVG)
const { group, loading, error } = useSVG('/logo.svg');Pros: More flexible, separates loading from rendering
Cons: Less declarative, doesn't match component patterns in library
Decision: Component approach is more consistent with existing API
2. Canvas-level loading
<Canvas svgs={{ logo: '/logo.svg' }}>
<SVGInstance name="logo" x={100} y={100} />
</Canvas>Pros: Centralized loading, could implement resource management
Cons: Complex, breaks component composition, adds global state
Decision: Component-level loading is simpler and more flexible
3. Children as SVG content
<SVG>
<svg>
<circle cx="50" cy="50" r="40" />
</svg>
</SVG>Pros: JSX-native SVG syntax
Cons: Requires parsing React children as SVG, complex, non-standard
Decision: String-based content prop is simpler and clearer
Performance Considerations
- SVG loading is async and non-blocking
- Group returned immediately (can be positioned before content loads)
- XHR requests are not cancelled on unmount (Two.js limitation)
- Component should track mounted state to avoid setState on unmounted component
- Large SVGs may cause parsing delays (Two.js limitation)
- Consider memoization for
contentprop to avoid re-parsing
Browser Compatibility
- Requires XHR support (all modern browsers)
- Relies on Two.js SVG interpretation (SVG 1.1 subset)
- Not all SVG features supported by Two.js (filters, animations, etc.)
Known Limitations
Two.js SVG interpretation has limitations:
- Not all SVG elements supported (see Two.js docs)
- SVG filters and effects not interpreted
- SMIL animations ignored (use Two.js animation instead)
- Text rendering may differ from original SVG
- Embedded images may need special handling
Resources
Success Criteria
-
<SVG>component successfully loads external SVG files - Component handles inline SVG markup strings
- Loading and error states are managed properly
-
onLoadandonErrorcallbacks work correctly - Positioning and transforms apply to loaded SVG groups
- Group properties (fill, stroke, etc.) cascade to loaded content
- Ref forwarding provides access to the loaded Group
- Dynamic src/content changes trigger reload
- Full TypeScript support with proper types
- Comprehensive tests with good coverage
- Documentation includes examples and API reference
- Works with all existing components and hooks
- No breaking changes to existing API
- Performance equivalent to direct
two.load()usage
Labels: enhancement, feature request, svg, component
Milestone: v0.3.0