Skip to content

Feature Request: Add Arc component for open arc shapes #10

@jonobr1

Description

@jonobr1

Summary

Add an <Arc> React component to wrap Two.js's Arc class from the extras module. This will enable developers to create open arc shapes (partial circles/ellipses) for use in pie charts, progress indicators, and circular UI elements.

Motivation

Two.js provides an Arc class in extras/jsm/arc.js that creates open arc shapes (as opposed to ArcSegment which creates closed pie-slice shapes). This is useful for:

  • Progress indicators (loading circles)
  • Pie charts and donut charts
  • Circular gauges and meters
  • Partial circle decorations
  • Arc-based animations

Currently, react-two.js includes ArcSegment but not Arc, leaving a gap for developers who need open arc shapes.

Arc vs ArcSegment

Arc (from extras):

  • Open arc shape (no filled center)
  • Creates a curved line from startAngle to endAngle
  • Like drawing part of a circle's outline
  • Useful for: progress indicators, gauges, open curves

ArcSegment (core, already in react-two.js):

  • Closed pie-slice shape
  • Creates a filled wedge with center point
  • Like a slice of pizza
  • Useful for: pie charts, radar charts, filled sections
// Two.js Arc example
import { Arc } from 'two.js/extras/jsm/arc.js';
const arc = new Arc(0, 0, 100, 100, 0, Math.PI);
// Creates an open semicircle arc

Proposed API

Component Signature

interface ArcProps {
  // Position
  x?: number;
  y?: number;
  
  // Dimensions
  width?: number;         // Horizontal diameter
  height?: number;        // Vertical diameter
  
  // Arc angles (in radians)
  startAngle?: number;    // Starting angle (default: 0)
  endAngle?: number;      // Ending angle (default: 2π)
  
  // Path properties
  resolution?: number;    // Number of vertices (default: 4)
  curved?: boolean;       // Smooth curve (default: true)
  
  // Styling (inherited from Path)
  stroke?: string;
  linewidth?: number;
  fill?: string;          // Usually 'transparent' for arcs
  cap?: 'butt' | 'round' | 'square';
  join?: 'miter' | 'round' | 'bevel';
  opacity?: number;
  visible?: boolean;
  
  // Transform
  rotation?: number;
  scale?: number;
}

export type RefArc = Arc;  // From two.js/extras/jsm/arc

export const Arc: React.ForwardRefExoticComponent<
  ArcProps & React.RefAttributes<RefArc>
>;

Usage Examples

Basic Arc

import { Canvas, Arc } from 'react-two.js';

function BasicArc() {
  return (
    <Canvas width={400} height={400}>
      <Arc 
        x={200}
        y={200}
        width={100}
        height={100}
        startAngle={0}
        endAngle={Math.PI}
        stroke="blue"
        linewidth={3}
        fill="transparent"
      />
    </Canvas>
  );
}

Progress Indicator

function ProgressIndicator({ progress }: { progress: number }) {
  // progress: 0 to 1
  const startAngle = -Math.PI / 2; // Start at top
  const endAngle = startAngle + (Math.PI * 2 * progress);
  
  return (
    <>
      {/* Background arc */}
      <Arc 
        x={100}
        y={100}
        width={80}
        height={80}
        startAngle={0}
        endAngle={Math.PI * 2}
        stroke="lightgray"
        linewidth={8}
      />
      
      {/* Progress arc */}
      <Arc 
        x={100}
        y={100}
        width={80}
        height={80}
        startAngle={startAngle}
        endAngle={endAngle}
        stroke="blue"
        linewidth={8}
        cap="round"
      />
    </>
  );
}

Animated Loading Spinner

function LoadingSpinner() {
  const arcRef = useRef<RefArc>(null);
  
  useFrame((elapsed) => {
    if (arcRef.current) {
      arcRef.current.rotation = elapsed * 2;
    }
  });
  
  return (
    <Arc 
      ref={arcRef}
      x={200}
      y={200}
      width={60}
      height={60}
      startAngle={0}
      endAngle={Math.PI * 1.5}
      stroke="purple"
      linewidth={4}
      cap="round"
    />
  );
}

Gauge/Meter

function Gauge({ value, max }: { value: number; max: number }) {
  const startAngle = Math.PI * 0.75;  // Start at bottom-left
  const endAngle = startAngle + (Math.PI * 1.5 * (value / max));
  
  return (
    <>
      {/* Gauge background */}
      <Arc 
        x={150}
        y={150}
        width={120}
        height={120}
        startAngle={Math.PI * 0.75}
        endAngle={Math.PI * 2.25}
        stroke="#eee"
        linewidth={10}
      />
      
      {/* Gauge value */}
      <Arc 
        x={150}
        y={150}
        width={120}
        height={120}
        startAngle={startAngle}
        endAngle={endAngle}
        stroke="green"
        linewidth={10}
        cap="round"
      />
      
      {/* Center text */}
      <Text 
        value={`${value}/${max}`}
        x={150}
        y={150}
        size={20}
        alignment="center"
      />
    </>
  );
}

Elliptical Arc

function EllipticalArc() {
  return (
    <Arc 
      x={200}
      y={150}
      width={150}   // Wide
      height={80}   // Narrow
      startAngle={0}
      endAngle={Math.PI}
      stroke="red"
      linewidth={2}
    />
  );
}

Multiple Arcs (Donut Chart Alternative)

function ArcGallery() {
  return (
    <>
      {[0, 1, 2, 3].map((i) => (
        <Arc 
          key={i}
          x={200}
          y={200}
          width={100}
          height={100}
          startAngle={(Math.PI / 2) * i}
          endAngle={(Math.PI / 2) * (i + 0.8)}
          stroke={`hsl(${i * 90}, 70%, 50%)`}
          linewidth={20}
          cap="round"
        />
      ))}
    </>
  );
}

Implementation Phases

Phase 1: Core Component Structure

Files: lib/Arc.tsx (new file)

  • Create Arc.tsx with React component boilerplate
  • Import Arc class from two.js/extras/jsm/arc.js
  • Define ArcProps TypeScript interface
  • Define RefArc type
  • Implement component with forwardRef
  • Set up state management for Arc instance
  • Integrate with useTwo() context

Deliverable: Basic Arc component skeleton with TypeScript types


Phase 2: Arc Instance Management

Files: lib/Arc.tsx

  • Implement useLayoutEffect for Arc creation:
    • Create Arc instance with initial props
    • Store in component state
    • Handle cleanup on unmount
  • Implement parent integration:
    • Add Arc to parent group/scene
    • Remove on unmount
    • Follow existing component patterns (see Circle.tsx)
  • Handle Arc-specific initialization:
    • Set initial width, height
    • Set startAngle, endAngle
    • Set resolution
    • Set position (x, y)

Deliverable: Arc instances created and added to scene


Phase 3: Props Handling & Updates

Files: lib/Arc.tsx

  • Implement useEffect for property updates:
    • Update position (x, y via translation)
    • Update dimensions (width, height)
    • Update angles (startAngle, endAngle)
    • Update resolution
    • Update path properties (stroke, linewidth, etc.)
  • Handle prop changes reactively
  • Apply defaults for undefined props
  • Ensure Arc updates trigger re-render in Two.js
  • Follow existing patterns from other shape components

Deliverable: Reactive prop updates on Arc component


Phase 4: Ref Forwarding

Files: lib/Arc.tsx

  • Implement useImperativeHandle for ref forwarding
  • Expose Arc instance via ref
  • Test ref access for animations
  • Ensure ref updates when instance changes
  • Document ref API and properties available

Deliverable: Working ref forwarding for imperative access


Phase 5: Integration & Export

Files: lib/main.ts

  • Export Arc component from lib/main.ts
  • Export RefArc type from lib/main.ts
  • Verify tree-shaking works correctly
  • Update package exports if needed
  • Ensure Arc class is properly imported from Two.js extras

Deliverable: Arc component available in public API


Phase 6: Testing

Files: lib/__tests__/Arc.test.tsx (new file)

  • Test component rendering
  • Test Arc creation with props
  • Test position updates (x, y)
  • Test dimension updates (width, height)
  • Test angle updates (startAngle, endAngle)
  • Test resolution changes
  • Test styling props (stroke, linewidth, etc.)
  • Test ref forwarding
  • Test cleanup on unmount
  • Test with parent groups
  • Mock Two.js Arc class for isolated testing

Deliverable: Comprehensive test coverage


Phase 7: Documentation & Examples

Files: README.md, src/App.tsx, documentation site

  • Add Arc component to README shape list
  • Document all props with descriptions
  • Create interactive examples:
    • Basic arc shape
    • Progress indicator
    • Loading spinner animation
    • Gauge/meter
    • Elliptical arcs
  • Add comparison with ArcSegment
  • Document use cases and patterns
  • Include performance considerations
  • Add troubleshooting section
  • TypeScript usage examples

Deliverable: Complete documentation with examples


Technical Considerations

Arc Class Implementation

The Arc class (from Two.js extras):

class Arc extends Path {
  constructor(x, y, width, height, startAngle, endAngle, resolution) {
    // Creates vertices based on angles
    // Curved by default
    // Open path (not closed)
  }
  
  // Properties: width, height, startAngle, endAngle
  // Inherits: stroke, linewidth, cap, join, etc. from Path
}

Key characteristics:

  • Extends Two.Path (not a primitive)
  • Creates vertices based on angle range
  • Automatically curved for smooth arcs
  • Open path (no automatic closure)
  • Resolution controls vertex count (higher = smoother)

Design Decisions

  1. Component approach: Matches existing shape component patterns
  2. Extras integration: First component from Two.js extras module
  3. Angle units: Use radians (Two.js convention)
  4. Default curved: Arcs are typically curved (can be overridden)
  5. Stroke-focused: Arcs typically use stroke, not fill
  6. Ref forwarding: Consistent with other shape components
  7. TypeScript: Full type safety for all props

Arc vs ArcSegment

Feature Arc ArcSegment
Shape Open arc line Closed pie slice
Fill Typically transparent Can be filled
Use Case Progress bars, gauges Pie charts, radar
Closure Open path Closed path
Module extras/jsm/arc Core shapes
Available ❌ Missing ✅ Exists

Performance Considerations

  • Arc vertices calculated based on resolution
  • Higher resolution = smoother but more vertices
  • Update performance similar to Path component
  • Angle changes recalculate vertices
  • Consider memoizing angle calculations for animations

Importing from Extras

This will be the first component importing from Two.js extras:

import { Arc } from 'two.js/extras/jsm/arc.js';

Considerations:

  • Verify extras are included in Two.js package
  • Check bundle size impact
  • Ensure tree-shaking works with extras
  • Test across different module systems

Alternative Approaches Considered

1. Enhance ArcSegment with closed prop:

<ArcSegment closed={false} />

Pros: No new component
Cons: Different underlying class, confusing API, different properties
Decision: Separate component is clearer

2. Generic CurvedPath component:

<CurvedPath vertices={...} closed={false} />

Pros: More flexible, covers many use cases
Cons: Too low-level, loses Arc-specific conveniences
Decision: Dedicated Arc component is more user-friendly

3. Path with arc helper:

<Path vertices={generateArcVertices(0, Math.PI)} />

Pros: Uses existing component
Cons: Manual vertex generation, loses Arc class benefits
Decision: Native Arc component is simpler

Resources

Success Criteria

  • <Arc> component successfully creates arc shapes
  • All props work correctly (position, dimensions, angles, styling)
  • Ref forwarding provides access to Arc instance
  • Component integrates with existing Canvas/Group structure
  • Dynamic prop updates work reactively
  • Animations work smoothly via refs and useFrame
  • Full TypeScript support with proper types
  • Comprehensive tests with good coverage
  • Documentation includes examples and API reference
  • Performance equivalent to manual Arc usage
  • Clear distinction from ArcSegment documented
  • Works with all renderers (SVG, Canvas, WebGL)
  • No breaking changes to existing API

Labels: enhancement, feature request, component, shapes
Milestone: v0.3.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions