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)
Deliverable: Basic component shell with TypeScript types
Phase 2: SVG Loading Logic
Files: lib/SVG.tsx
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
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
Deliverable: Robust error handling and validation
Phase 5: Ref Forwarding & Advanced Features
Files: lib/SVG.tsx
Deliverable: Full ref support and advanced features
Phase 6: Integration & Export
Files: lib/main.ts, lib/SVG.tsx
Deliverable: Component available in public API
Phase 7: Testing
Files: lib/__tests__/SVG.test.tsx (new file)
Deliverable: Comprehensive test coverage
Phase 8: Documentation & Examples
Files: README.md, src/App.tsx, documentation site
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 .svg OR inline SVG markup
callback: Optional (group, svg) => void - Called after loading completes
Behavior:
- Returns a
Two.Group immediately (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 shapes
two.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
onError callback
- TypeScript first: Full type safety for SVG properties
- Reload support: Changing
src/content triggers 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
content prop 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
Labels: enhancement, feature request, svg, component
Milestone: v0.3.0
Summary
Add a declarative
<SVG>React component to load and interpret SVG files using Two.js'sload()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: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
Basic Usage Example
With Loading State
Advanced - Ref Access & Animation
With Fallback Content
Implementation Phases
Phase 1: Core Component Structure
Files:
lib/SVG.tsx(new file)SVG.tsxwith React component boilerplateSVGPropsandRefSVGTypeScript interfacesforwardRefsrcorcontent)ref- The loaded Group instanceloading- Loading state (boolean)error- Error stateuseTwo()contextDeliverable: Basic component shell with TypeScript types
Phase 2: SVG Loading Logic
Files:
lib/SVG.tsxuseEffectfor loading SVG whensrcorcontentchangestwo.load(pathOrSVGContent, callback)with appropriate parametersrcprop provided)contentprop provided)onLoadprop with(group, svg)when successfulonErrorprop when loading failsImplementation pattern:
Deliverable: Working SVG loading with async handling
Phase 3: Group Integration & Positioning
Files:
lib/SVG.tsxGroup.tsx)x,y)scale,rotation)fill,stroke,opacity, etc.)Pattern to follow:
Deliverable: Positioned and styled SVG groups in the scene
Phase 4: Error Handling & Edge Cases
Files:
lib/SVG.tsxtwoinstancesrcandcontentprovided (warn or prefer one)srcnorcontentprovidedDeliverable: Robust error handling and validation
Phase 5: Ref Forwarding & Advanced Features
Files:
lib/SVG.tsxuseImperativeHandlefor ref forwardingshallowprop (pass to interpret)src/contentdynamically (reload)Deliverable: Full ref support and advanced features
Phase 6: Integration & Export
Files:
lib/main.ts,lib/SVG.tsxSVGcomponent fromlib/main.tsRefSVGtype fromlib/main.tsSVGPropsinterface (optional, for advanced users)Deliverable: Component available in public API
Phase 7: Testing
Files:
lib/__tests__/SVG.test.tsx(new file)srcpropcontentproponLoadcallback invocationonErrorcallback invocationtwo.load()for isolated testingDeliverable: Comprehensive test coverage
Phase 8: Documentation & Examples
Files:
README.md,src/App.tsx, documentation site<SVG>section to main READMEDeliverable: 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 completesBehavior:
Two.Groupimmediately (empty initially).svg, fetches file via XHRtwo.interpret()on each SVG child element(group, svgElements)when completeRelated Methods:
two.interpret(svgElement, shallow, add)- Convert SVG DOM to Two.js shapestwo.xhr(path, callback)- Internal XHR utilityDesign Decisions
src) and inline markup (content)onErrorcallbacksrc/contenttriggers new loadAlternative Approaches Considered
1. Hook-based (
useSVG)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
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
Pros: JSX-native SVG syntax
Cons: Requires parsing React children as SVG, complex, non-standard
Decision: String-based
contentprop is simpler and clearerPerformance Considerations
contentprop to avoid re-parsingBrowser Compatibility
Known Limitations
Two.js SVG interpretation has limitations:
Resources
Success Criteria
<SVG>component successfully loads external SVG filesonLoadandonErrorcallbacks work correctlytwo.load()usageLabels: enhancement, feature request, svg, component
Milestone: v0.3.0